[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\n    \"electron\",\n    \"react\"\n  ],\n  \"sourceMaps\": \"inline\"\n}\n"
  },
  {
    "path": ".coffeelint.json",
    "content": "{\n    \"arrow_spacing\": {\n        \"level\": \"ignore\"\n    },\n    \"camel_case_classes\": {\n        \"level\": \"error\"\n    },\n    \"coffeescript_error\": {\n        \"level\": \"error\"\n    },\n    \"colon_assignment_spacing\": {\n        \"level\": \"ignore\",\n        \"spacing\": {\n            \"left\": 0,\n            \"right\": 0\n        }\n    },\n    \"cyclomatic_complexity\": {\n        \"value\": 10,\n        \"level\": \"ignore\"\n    },\n    \"duplicate_key\": {\n        \"level\": \"error\"\n    },\n    \"empty_constructor_needs_parens\": {\n        \"level\": \"ignore\"\n    },\n    \"indentation\": {\n        \"value\": 2,\n        \"level\": \"error\"\n    },\n    \"max_line_length\": {\n        \"value\": 140,\n        \"level\": \"error\",\n        \"limitComments\": true\n    },\n    \"missing_fat_arrows\": {\n        \"level\": \"ignore\"\n    },\n    \"newlines_after_classes\": {\n        \"value\": 3,\n        \"level\": \"ignore\"\n    },\n    \"no_backticks\": {\n        \"level\": \"error\"\n    },\n    \"no_debugger\": {\n        \"level\": \"warn\"\n    },\n    \"no_empty_functions\": {\n        \"level\": \"ignore\"\n    },\n    \"no_empty_param_list\": {\n        \"level\": \"ignore\"\n    },\n    \"no_implicit_braces\": {\n        \"level\": \"ignore\",\n        \"strict\": true\n    },\n    \"no_implicit_parens\": {\n        \"strict\": true,\n        \"level\": \"ignore\"\n    },\n    \"no_interpolation_in_single_quotes\": {\n        \"level\": \"ignore\"\n    },\n    \"no_plusplus\": {\n        \"level\": \"ignore\"\n    },\n    \"no_stand_alone_at\": {\n        \"level\": \"ignore\"\n    },\n    \"no_tabs\": {\n        \"level\": \"error\"\n    },\n    \"no_throwing_strings\": {\n        \"level\": \"error\"\n    },\n    \"no_trailing_semicolons\": {\n        \"level\": \"error\"\n    },\n    \"no_trailing_whitespace\": {\n        \"level\": \"error\",\n        \"allowed_in_comments\": false,\n        \"allowed_in_empty_lines\": true\n    },\n    \"no_unnecessary_double_quotes\": {\n        \"level\": \"ignore\"\n    },\n    \"no_unnecessary_fat_arrows\": {\n        \"level\": \"warn\"\n    },\n    \"non_empty_constructor_needs_parens\": {\n        \"level\": \"ignore\"\n    },\n    \"prefer_english_operator\": {\n        \"level\": \"ignore\",\n        \"doubleNotLevel\": \"ignore\"\n    },\n    \"space_operators\": {\n        \"level\": \"ignore\"\n    },\n    \"spacing_after_comma\": {\n        \"level\": \"ignore\"\n    }\n}\n"
  },
  {
    "path": ".dockerignore",
    "content": ".arc*\n.git*\narclib\n**/node_modules\npackages/client-*\n!packages/client-app/.babelrc\n\n*.swp\n*~\n.DS_Store\n**/npm-debug.log\n**/lerna-debug.log\n\n# Vim temp files\n*.swp\n*.swo\n\n# Elastic Beanstalk Files\n.elasticbeanstalk/*\n!.elasticbeanstalk/*.cfg.yml\n!.elasticbeanstalk/*.global.yml\n/packages/client-sync/spec-saved-state.json\n\n# Built cloud files\nn1_cloud_dist\n"
  },
  {
    "path": ".ebextensions/enable_docker_cli_on_ssh.config",
    "content": "# This lets you log in via `eb ssh` and access the docker daemon.\n# If we don't add the ec2-user to the docker group, then calls to docker\n# (like `docker ps`) will fail with `Cannot connect to the Docker daemon`\n#\n# See: https://blog.cloudinvaders.com/connect-to-docker-daemon-on-aws-beanstalk-ec2-instance/\ncommands:\n  0_add_docker_group_to_ec2_user:\n    command: gpasswd -a ec2-user docker\n    test: groups ec2-user | grep -qv docker\n"
  },
  {
    "path": ".ebignore",
    "content": ".arc*\n.git*\narclib/\n**/node_modules/\npackages/client-*\n!packages/client-app/.babelrc\n\n*.swp\n*~\n.DS_Store\n**/npm-debug.log\n**/lerna-debug.log\n\n# Vim temp files\n*.swp\n*.swo\n\n# Elastic Beanstalk Files\n.elasticbeanstalk/*\n!.elasticbeanstalk/*.cfg.yml\n!.elasticbeanstalk/*.global.yml\n/packages/client-sync/spec-saved-state.json\n\n# Built cloud files\nn1_cloud_dist/\n"
  },
  {
    "path": ".eslintrc",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"extends\": \"airbnb\",\n  \"globals\": {\n    \"NylasEnv\": false,\n    \"$n\": false,\n    \"waitsForPromise\": false,\n    \"advanceClock\": false,\n    \"TEST_ACCOUNT_ID\": false,\n    \"TEST_ACCOUNT_NAME\": false,\n    \"TEST_ACCOUNT_EMAIL\": false,\n    \"TEST_ACCOUNT_ALIAS_EMAIL\": false\n  },\n  \"env\": {\n    \"browser\": true,\n    \"node\": true,\n    \"jasmine\": true\n  },\n  \"rules\": {\n    \"arrow-body-style\": \"off\",\n    \"arrow-parens\": \"off\",\n    \"class-methods-use-this\": \"off\",\n    \"prefer-arrow-callback\": [\"error\", {\"allowNamedFunctions\": true}],\n    \"eqeqeq\": [\"error\", \"smart\"],\n    \"id-length\": \"off\",\n    \"object-curly-spacing\": \"off\",\n    \"max-len\": \"off\",\n    \"new-cap\": [\"error\", {\"capIsNew\": false}],\n    \"newline-per-chained-call\": \"off\",\n    \"no-bitwise\": \"off\",\n    \"no-lonely-if\": \"off\",\n    \"no-console\": \"off\",\n    \"no-continue\": \"off\",\n    \"no-constant-condition\": \"off\",\n    \"no-loop-func\": \"off\",\n    \"no-plusplus\": \"off\",\n    \"no-shadow\": \"error\",\n    \"no-underscore-dangle\": \"off\",\n    \"object-shorthand\": \"off\",\n    \"quotes\": \"off\",\n    \"quote-props\": [\"error\", \"consistent-as-needed\", { \"keywords\": true }],\n    \"no-param-reassign\": [\"error\", { \"props\": false }],\n    \"semi\": \"off\",\n    \"no-mixed-operators\": \"off\",\n    \"import/extensions\": [\"error\", \"never\", { \"json\": \"always\" }],\n    \"import/no-unresolved\": [\"error\", {\"ignore\": [\"nylas-exports\", \"nylas-component-kit\", \"electron\", \"nylas-store\", \"react-dom/server\", \"nylas-observables\", \"windows-shortcuts\", \"moment-round\", \"better-sqlite3\", \"chrono-node\", \"event-kit\", \"enzyme\", \"isomorphic-core\"]}],\n    \"import/no-extraneous-dependencies\": \"off\",\n    \"import/newline-after-import\": \"off\",\n    \"import/prefer-default-export\": \"off\",\n    \"react/no-multi-comp\": \"off\",\n    \"react/no-find-dom-node\": \"off\",\n    \"react/no-string-refs\": \"off\",\n    \"react/no-unused-prop-types\": \"off\",\n    \"react/forbid-prop-types\": \"off\",\n    \"jsx-a11y/no-static-element-interactions\": \"off\",\n    \"react/prop-types\": [\"error\", {\"ignore\": [\"children\"]}],\n    \"react/sort-comp\": \"error\",\n    \"no-restricted-syntax\": [\n      \"error\", \"ForInStatement\", \"LabeledStatement\", \"WithStatement\"\n    ],\n    \"comma-dangle\": [\"error\", {\n      \"arrays\": \"always-multiline\",\n      \"objects\": \"always-multiline\",\n      \"imports\": \"always-multiline\",\n      \"exports\": \"always-multiline\",\n      \"functions\": \"ignore\"\n    }],\n    \"no-useless-return\": \"off\"\n  },\n  \"settings\": {\n    \"import/core-modules\": [ \"nylas-exports\", \"nylas-component-kit\", \"electron\", \"nylas-store\", \"nylas-observables\" ],\n    \"import/resolver\": {\"node\": {\"extensions\": [\".es6\", \".jsx\", \".coffee\", \".json\", \".cjsx\", \".js\"]}}\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "##### Elastic Beanstalk Files\n.elasticbeanstalk/*\n!.elasticbeanstalk/*.cfg.yml\n!.elasticbeanstalk/*.global.yml\n\n##### Arcanist / Phab\n**/.arcconfig\n**/.arclint\n**/arclib\n\n##### Node modules\nnode_modules\n!packages/client-app/spec/fixtures/packages/package-with-incompatible-native-module/node_modules\n**/npm-debug.log*\n**/lerna-debug.log*\n\n##### Certs for building\n**/build/resources/certs\n\n##### Misc swap files\n**/*.swp\n**/*.swo\n**/*~\n**/*#\n**/.DS_Store\n**/Thumbs.db\n**/#emacs\n\n# Built cloud files\nn1_cloud_dist\n\n# Built Nylas Mail client\npackages/client-app/dist\n\n# Tests\nspec-saved-state.json\n\n# Symlinked Jasmine config files\n**/jasmine/config.json\n!packages/isomorphic-core/spec/jasmine/config.json\n\n# Symlinked isomorphic-core Specs\npackages/client-app/spec/isomorphic-core\n\n# Elastic Beanstalk Files\n.elasticbeanstalk/*\n!.elasticbeanstalk/*.cfg.yml\n!.elasticbeanstalk/*.global.yml\n\n# Sqlite amalgamation for scripts\nscripts/sqlite\n\n# Scripts for calculating statistics\nscripts/toolbox\nscripts/venv\n\n# Python\n*.pyc\n\n# OAuth client secret for talking to Google Sheets\nclient_secret.json\n/packages/client-app/internal_packages/client-sync\n"
  },
  {
    "path": ".travis.yml",
    "content": "# The private Nylas monorepo build script. This will build a full signed\n# release for the Nylas Mail client\nsudo: false\n\naddons:\n  apt:\n    sources:\n    - ubuntu-toolchain-r-test\n    packages:\n    - build-essential\n    - clang\n    - fakeroot\n    - g++-4.8\n    - git\n    - libgnome-keyring-dev\n    - xvfb\n    - rpm\n    - libxext-dev\n    - libxtst-dev\n    - libxkbfile-dev\n\nbranches:\n  only:\n  - master\n  - /ci-.*/\n  - /stable.*/\n\nmatrix:\n  include:\n  - os: linux\n    env: NODE_VERSION=6.9 CC=gcc-4.8 CXX=g++-4.8 DEBUG=\"electron-packager:*\" INSTALL_TARGET=client\n  - os: osx\n    env: NODE_VERSION=6.9 CC=clang CXX=clang++ DEBUG=\"electron-packager:*\" INSTALL_TARGET=client\n\ninstall:\n- git clone https://github.com/creationix/nvm.git /tmp/.nvm\n- source /tmp/.nvm/nvm.sh\n- nvm install $NODE_VERSION\n- nvm use --delete-prefix $NODE_VERSION\n\nscript:\n- npm install && npm run build-client\n\ncache:\n  directories:\n  - node_modules\n  - apm/node_modules\n"
  },
  {
    "path": "Dockerfile",
    "content": "# This Dockerfile builds a production-ready image of K2 to be used across all\n# services. See the Dockerfile documentation here:\n# https://docs.docker.com/engine/reference/builder/\n\n# Use the latest Node 6 base docker image\n# https://github.com/nodejs/docker-node\nFROM node:6\nENV INSTALL_TARGET=cloud\n\n# Copy everything (excluding what's in .dockerignore) into an empty dir\nCOPY . /home\nWORKDIR /home\n\n# This installs global dependencies, then in the postinstall script, runs lerna\n# bootstrap to install and link cloud-api, cloud-core, and cloud-workers.\n# We need the --unsafe-perm param to run the postinstall script since Docker\n# will run everything as sudo\nRUN npm install --unsafe-perm\n\n# This uses babel to compile any es6 to stock js for plain node\nRUN node packages/cloud-core/build/build-n1-cloud\n\n# External services run on port 80. Expose it.\nEXPOSE 5100\n\n# We use a start-aws command that automatically spawns the correct process\n# based on environmpackages/cloud-coreent variables (which changes instance to instance)\nCMD packages/cloud-core/_n1cloud_docker_launcher.sh ${AWS_SERVICE_NAME}\n"
  },
  {
    "path": "ISSUE_TEMPLATE.md",
    "content": "\n##### IMPORTANT: Nylas Mail is no longer maintained\n\nNylas Mail is no longer maintained by Nylas - nobody will read or respond to issues filed here.\nThis issue tracker is being kept online because many existing issues contain valuable tips\nand information.\n\nIf you're looking for an alternative to Nylas Mail, check out https://getmailspring.com/.\nIt is based on the Nylas Mail source code, maintained by one of the original developers, \nand features an entirely re-written sync engine.\n"
  },
  {
    "path": "LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2017 Nylas, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# Nylas Mail - the open-source, extensible mail client\n![N1 Screenshot](https://github.com/nylas/nylas-mail/raw/master/screenshot/hero_graphic_mac%402x.png)\n\n**Nylas Mail was an open-source mail client built on the modern web with [Electron](https://github.com/atom/electron), [React](https://facebook.github.io/react/), and [Flux](https://facebook.github.io/flux/).** It was designed to be easy to extend, and many third-party plugins are available that add functionality to the client. \n\n**⚠️ Nylas Mail was initially released and open-sourced in early 2015 and was maintained by Nylas until Spring 2017.** While Nylas no longer supports Nylas Mail, you can download the latest release or build it from source. There are also **[several forks](#forks)** that are being actively developed and maintained.\n\n# Getting Started\n\n## Setup your Environment (Mac):\n\n1. Install [Homebrew](http://brew.sh/)\n1. Install [NVM](https://github.com/creationix/nvm) & Redis `brew install nvm redis`\n1. Install Node 6 via NVM: `nvm install 6`\n1. `npm install`\n\n## Setup your Environment (Linux - Debian/Ubuntu):\n\n1. Install Node 6+ via NodeSource (trusted):\n  1. `curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -`\n  1. `sudo apt-get install -y nodejs`\n1. Install Redis locally `sudo apt-get install -y redis-server redis-tools`\nbenefit of letting us use subdomains.\n1. `npm install`\n\n## Running Nylas Mail\n\n1. `npm run client`: Starts the app\n1. `npm run test-client`: Run the tests\n1. `npm run lint-client`: Lint the source (ESLint + Coffeelint + LESSLint)\n\n### Exploring the Source\n\nThis repository contains the full source code to the Nylas Mail client and it's backend services. It is divided into the following packages:\n\n1. [**Isomorphic Core**](https://github.com/nylas/nylas-mail/tree/master/packages/isomorphic-core): Shared code across local client and cloud servers\n1. [**Client App**](https://github.com/nylas/nylas-mail/tree/master/packages/client-app): The main Electron app for Nylas Mail\n   mirrored to open source repo.\n1. [**Client Sync**](https://github.com/nylas/nylas-mail/tree/master/packages/client-sync): The local mailsync engine integreated in Nylas Mail\n1. [**Client Private Plugins**](https://github.com/nylas/nylas-mail/tree/master/packages/client-private-plugins): Private Nylas Mail plugins (like SFDC)\n1. [**Cloud API**](https://github.com/nylas/nylas-mail/tree/master/packages/cloud-api): The cloud-based auth and metadata APIs for N1\n1. [**Cloud Core**](https://github.com/nylas/nylas-mail/tree/master/packages/cloud-core): Shared code used in all remote cloud services\n1. [**Cloud Workers**](https://github.com/nylas/nylas-mail/tree/master/packages/cloud-workers): Cloud workers for services like send later\n\nSee `/packages` for the separate pieces. Each folder in `/packages` is\ndesigned to be its own stand-alone repository. They are all bundled here\nfor the ease of source control management.\n\n## Digging Deeper\n\nIn early 2016, the Nylas Mail team wrote [extensive documentation](https://nylas.github.io/nylas-mail/) for the app that was intended for plugin developers. This documentation lives on GitHub Pages and offers a great overview of the app's architecture and important classes. Here are some good places to get started:\n\n- [Application Architecture](https://nylas.github.io/nylas-mail/guides/Architecture.html)\n- [Debugging Nylas Mail](https://nylas.github.io/nylas-mail/guides/Debugging.html)\n\nThe team has also given conference talks and published blog posts about the client:\n\n- [ReactEurope: How React & Flux Turn Apps Into Extensible Platforms](https://www.youtube.com/watch?v=Uu4Yz2HmCgE)\n- [ForwardJS: Electron, React & Pixel Perfect Experiences](https://www.youtube.com/watch?v=jRPUB-D1Wx0&list=PL7i8CwZBnlf7iUTn2JMVLLWofAhaiK7l3)\n\n- [Blog: Splitting from Atom](https://github.com/nylas/nylas-mail/raw/master/blog-posts/splitting-from-atom.pdf)\n- [Blog: Building Plugins for React Apps](https://github.com/nylas/nylas-mail/raw/master/blog-posts/plugins.pdf)\n- [Blog: Nylas Mail Build Process](https://github.com/nylas/nylas-mail/raw/master/blog-posts/build-process.pdf)\n- [Blog: Low level Electron Debugging](https://github.com/nylas/nylas-mail/raw/master/blog-posts/electron-debugging.pdf)\n- [Blog: A New Search Parser](https://github.com/nylas/nylas-mail/raw/master/blog-posts/search-parser.pdf)\n- [Blog: Developers Guide to Emoji](https://github.com/nylas/nylas-mail/raw/master/blog-posts/emoji.pdf)\n- [Blog: Nylas Pro](https://github.com/nylas/nylas-mail/raw/master/blog-posts/nylas-pro.pdf)\n- [Blog: Nylas Mail & PGP](https://github.com/nylas/nylas-mail/raw/master/blog-posts/pgp.pdf)\n- [Blog: Calendar Events and RRULEs](https://github.com/nylas/nylas-mail/raw/master/blog-posts/rrules.pdf)\n\n## Running the Cloud\n\nWhen you download and build Nylas Mail from source it runs without its cloud components. The concept of a \"Nylas ID\" / subscription has been removed, and plugins that require server-side processing are disabled by default. (Plugins like Snooze, Send Later, etc.)\n\nIn order to use these plugins and get the full Nylas Mail experience, you need to deploy the backend infrastructure located in the `cloud-*` packages. Deploying these services is challenging because they are implemented as microservices and designed to be run at enterprise scale with Redis, Postgres, etc. Because these backend services must access your email account, it is also important to use security best-practices (at the very least, SSL, encryption at rest, and a partitioned VPC). For more information about building and deploying this part of the stack, check out the [cloud-core README](https://github.com/nylas/nylas-mail/blob/master/packages/cloud-core/README.md).\n\n## Themes\n\nThe Nylas Mail user interface is styled using CSS, which means it's easy to modify and extend. Nylas Mail comes stock with a few beautiful themes, and there are many more which have been built by community developers\n\n![N1 Themes](https://github.com/nylas/nylas-mail/raw/master/screenshot/687474703a2f2f692e696d6775722e636f6d2f505751374e6c592e6a7067.jpg)\n\n#### Bundled Themes\n- [Dark](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/ui-dark)\n- [Darkside](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/ui-darkside) (designed by [Jamie Wilson](https://github.com/jamiewilson))\n- [Taiga](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/ui-taiga) (designed by [Noah Buscher](https://github.com/noahbuscher))\n- [Ubuntu](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/ui-ubuntu) (designed by [Ahmed Elhanafy](https://github.com/ahmedlhanafy))\n- [Less Is More](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/ui-less-is-more) (designed by [Alexander Adkins](https://github.com/P0WW0W))\n\n#### Community Themes\n|       |       |       |\n| ----- | ----- | ----- |\n| [ToogaBooga](https://github.com/brycedorn/N1-ToogaBooga) | [Material](https://github.com/jackiehluo/n1-material) | [Monokai](https://github.com/dcondrey/n1-monokai)  |\n| [Agapanthus](https://github.com/taniadaniela/n1-agapanthus)—Inbox-inspired theme | [Stripe](https://github.com/oeaeee/n1-stripe)| [Kleinstein](https://github.com/diklein/Kleinstein)—Hides account sidebar|\n| [Arc Dark](https://github.com/varlesh/Nylas-Arc-Dark-Theme)| [Solarized Dark](https://github.com/NSHenry/N1-Solarized-Dark) | [Darkish](https://github.com/dyrnade/N1-Darkish)|\n| [Predawn](https://github.com/adambmedia/N1-Predawn)| [Ido](https://github.com/edipox/n1-ido)—Polymail-inspired theme|[Berend](https://github.com/Frique/N1-Berend) |\n| [ElementaryOS](https://github.com/edipox/elementary-nylas) | [LevelUp](https://github.com/stolinski/level-up-nylas-n1-theme)|[Sunrise](https://github.com/jackiehluo/n1-sunrise) |\n| [BoraBora](https://github.com/arimai/N1-BoraBora) | [Honeyduke](https://github.com/arimai/n1-honeyduke)| [Snow](https://github.com/Wattenberger/N1-snow-theme)|\n|[Hull](https://github.com/unity/n1-hull)|[Express](https://github.com/oeaeee/n1-express)|[DarkSoda](https://github.com/adambullmer/N1-theme-DarkSoda)|\n|[Bemind](https://github.com/bemindinteractive/Bemind-N1-Theme)|[Dracula](https://github.com/dracula/nylas-n1)|[MouseEatsCat](https://github.com/MouseEatsCat/MouseEatsCat-N1)|\n|[Sublime Dark](https://github.com/rishabhkesarwani/Nylas-Sublime-Dark-Theme)|[Firefox](https://github.com/darshandsoni/n1-firefox-theme)|[Gmail](https://github.com/dregitsky/n1-gmail-theme)|\n\n#### To install community themes:\n\n1. Download and unzip the repo\n2. In Nylas Mail, select `Developer > Install a Package Manually... `\n3. Navigate to where you downloaded the theme and select the root folder. The theme is copied into the `~/.nylas-mail` folder for your convinence\n5. Select `Change Theme...` from the top level menu, and you'll see the newly installed theme. That's it!\n\nWant to dive in more? Try [creating your own theme](https://github.com/nylas/nylas-mail-theme-starter)!\n\n## Plugins\n\nSome plugins come pre-installed, and are a great starting points for creating your own:\n\n- [Translate](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/composer-translate)—Works with 10 languages\n- [Quick Replies](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/composer-templates)—Send emails faster with templates\n- [Emoji Keyboard](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/composer-emoji)—Insert emoji by typing a colon (:) followed by the name of an emoji symbol\n- [GitHub Sidebar Info](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/github-contact-card)\n- [View on GitHub](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/message-view-on-github)\n- [Personal Level Indicators](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/personal-level-indicators)\n- [Phishing Detection](https://github.com/nylas/nylas-mail/tree/master/packages/client-app/internal_packages/phishing-detection)\n\n#### Community Plugins\n\nNote these are not tested or officially supported by Nylas, but we still think they are really cool! If you find bugs with them, please open GitHub issues on their individual project pages, not the Nylas Mail (N1) repo page. Thanks!\n\n|       |       |       |\n| ----- | ----- | ----- |\n|[Jiffy](http://noahbuscher.github.io/N1-Jiffy/)—Insert animated GIFs|[Weather](https://github.com/jackiehluo/n1-weather)|[Todoist](https://github.com/alexfruehwirth/N1TodoistIntegration)|\n|[Unsubscribe](https://github.com/colinking/n1-unsubscribe)|[Squirt Speed Reader](https://github.com/HarleyKwyn/squirt-reader-N1-plugin/)|[Website Launcher](https://github.com/adriangrantdotorg/nylas-n1-background-webpage)—Opens a URL in separate window|\n|[Cypher](https://github.com/mbilker/cypher)—PGP Encryption|[Avatars](https://github.com/unity/n1-avatars)|[Events Calendar (WIP)](https://github.com/nerdenough/n1-events-calendar)|\n|[Mail in Chat (WIP)](https://github.com/yjchen/mail_in_chat)|[Evernote](https://github.com/grobgl/n1-evernote)|[Wunderlist](https://github.com/miguelrs/n1-wunderlist)|\n|[Participants Display](https://github.com/kbruccoleri/nylas-participants-display)|[GitHub](https://github.com/ForbesLindesay/N1-GitHub)||\n\nWhen you install packages, they're moved to ~/.nylas-mail/packages, and Nylas Mail runs apm install on the command line to fetch dependencies listed in the package's package.json\n\n# Forks\n\nThere are several forks of Nylas Mail that you should check out. If you're just learning about Nylas Mail, it is highly recommended you use one of these instead.\n\n - [Mailspring](http://www.getmailspring.com/) - Significant rewrite by one of the original authors focused on performance and cloud plugins\n - [Nylas Mail Lives](https://github.com/nylas-mail-lives/nylas-mail) - Community effort to fix bugs and improve the client! (Seeking Maintainers)\n"
  },
  {
    "path": "appveyor.yml",
    "content": "version: '{build}'\n\nbranches:\n  only:\n  - master\n  - /ci.*/\n  - /stable.*/\n\n# We need to only clone the main module because our submodule requires the\n# encrypted ssh key to access submodules\ninstall:\n- ps: Install-Product node $env:NODE_VERSION\n- ps: nuget install secure-file -ExcludeVersion\n- ps: npm config set msvs_version 2013\n\nbuild_script:\n- cmd: npm install\n\ndeploy_script:\n- ps: |\n    npm run build-client\n\nenvironment:\n  matrix:\n  - NODE_VERSION: 6.9\n  global:\n    DEBUG: \"electron-windows-installer:*,electron-packager:*\"\n    SIGN_BUILD: true\n    INSTALL_TARGET: \"client\"\n    CERTIFICATE_FILE: .\\packages\\client-private-plugins\\encrypted_certificates\\appveyor\\win-nylas-n1.p12\n    DECRYPTION_PASSWORD:\n      secure: 48VSzDtdBd52Xlo3TZ1NeR1yRRrZ3AU6Px5XjD5RDp44cFU5GYVspecGqX6DGCV7i0D7nldGMyEbXNrjM1t1Kw==\n\ncache:\n  - node_modules -> package.json\n  - packages\\client-app\\node_modules -> packages\\client-app\\package.json\n  - '%USERPROFILE%\\.npm'\n"
  },
  {
    "path": "lerna.json",
    "content": "{\n  \"lerna\": \"2.0.0-beta.38\",\n  \"version\": \"0.0.1\"\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"nylas-mail-all\",\n  \"version\": \"0.0.1\",\n  \"description\": \"All components required to run Nylas Mail\",\n  \"devDependencies\": {\n    \"babel-cli\": \"6.x.x\",\n    \"babel-core\": \"6.x.x\",\n    \"babel-eslint\": \"7.1.0\",\n    \"babel-preset-electron\": \"1.4.15\",\n    \"babel-preset-react\": \"6.x.x\",\n    \"chalk\": \"1.x.x\",\n    \"coffeelint-cjsx\": \"2.x.x\",\n    \"commander\": \"^2.9.0\",\n    \"electron-installer-dmg\": \"0.2.x\",\n    \"electron-packager\": \"8.4.x\",\n    \"electron-winstaller\": \"2.x.x\",\n    \"eslint\": \"3.10.1\",\n    \"eslint-config-airbnb\": \"13.0.0\",\n    \"eslint-plugin-import\": \"2.2.0\",\n    \"eslint-plugin-jsx-a11y\": \"2.2.3\",\n    \"eslint-plugin-react\": \"6.7.1\",\n    \"eslint_d\": \"4.2.0\",\n    \"fs-extra\": \"2.x.x\",\n    \"fs-plus\": \"2.x.x\",\n    \"glob\": \"7.x.x\",\n    \"grunt\": \"0.4.x\",\n    \"grunt-cli\": \"0.1.x\",\n    \"grunt-coffeelint\": \"git+https://github.com/atom/grunt-coffeelint.git#cfb99aa99811d52687969532bd5a98011ed95bfe\",\n    \"grunt-coffeelint-cjsx\": \"0.1.x\",\n    \"grunt-contrib-coffee\": \"0.12.x\",\n    \"grunt-contrib-csslint\": \"0.5.x\",\n    \"grunt-contrib-less\": \"0.8.x\",\n    \"grunt-lesslint\": \"0.13.x\",\n    \"jasmine\": \"2.x.x\",\n    \"lerna\": \"emorikawa/lerna#v2.0.0-beta.38.forked\",\n    \"load-grunt-parent-tasks\": \"0.1.1\",\n    \"mkdirp\": \"^0.5.1\",\n    \"pm2\": \"2.4.0\",\n    \"request\": \"2.x.x\",\n    \"s3\": \"4.x.x\",\n    \"temp\": \"0.8.x\",\n    \"underscore\": \"1.8.x\"\n  },\n  \"scripts\": {\n    \"start\": \"npm run client\",\n    \"test\": \"npm run test-client && npm run test-cloud\",\n    \"client\": \"packages/client-app/node_modules/.bin/electron packages/client-app --enable-logging --dev\",\n    \"benchmark\": \"packages/client-app/node_modules/.bin/electron packages/client-app --enable-logging --dev --benchmark\",\n    \"test-client\": \"packages/client-app/node_modules/.bin/electron packages/client-app --enable-logging --test\",\n    \"test-client-window\": \"packages/client-app/node_modules/.bin/electron packages/client-app --enable-logging --test=window\",\n    \"test-client-junit\": \"\",\n    \"lint-client\": \"grunt lint --gruntfile=packages/client-app/build/Gruntfile.js --base=./\",\n    \"build-client\": \"grunt build-client --gruntfile=packages/client-app/build/Gruntfile.js --base=./\",\n    \"cloud\": \"pm2 stop all; pm2 delete all; pm2 start packages/cloud-core/pm2-dev.yml --no-daemon\",\n    \"cloud-debug\": \"pm2 stop all; pm2 delete all; pm2 start packages/cloud-core/pm2-debug-cloud-api.yml --no-daemon\",\n    \"test-cloud\": \"cd packages/cloud-api && npm test && cd ../cloud-core && npm test && cd ../cloud-workers && npm test && cd ../isomorphic-core && npm test\",\n    \"stop\": \"npm run stop-cloud\",\n    \"stop-cloud\": \"pm2 stop all; pm2 delete all;\",\n    \"build-cloud\": \"docker build .\",\n    \"postinstall\": \"babel-node scripts/postinstall.es6\",\n    \"daily\": \"babel-node scripts/daily.js\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/nylas/nylas-mail-all.git\"\n  },\n  \"author\": \"Nylas\",\n  \"license\": \"proprietary\",\n  \"engines\": {\n    \"node\": \"6.9.1\",\n    \"npm\": \"3.10.8\"\n  }\n}\n"
  },
  {
    "path": "packages/README.md",
    "content": "# Monorepo Packages\n\nEach folder here is designed to act as its own repository. For development\nconvenience, they are all included here in one monorepo. This allows us to grep\nacross multiple codebases, not use submodules, and keep a unified commit\nhistory.\n\nWe use [Lerna](https://github.com/lerna/lerna) to manage the monorepo and tie\nthem all together with the main `nylas-mail-all/scripts/postinstall.es6` script,\nwhich in turn, calls `lerna bootstrap`\n"
  },
  {
    "path": "packages/client-app/.babelrc",
    "content": "{\n  \"presets\": [\n    \"electron\",\n    \"react\"\n  ],\n  \"sourceMaps\": \"inline\"\n}\n"
  },
  {
    "path": "packages/client-app/.travis.yml",
    "content": "# The open source Nylas Mail Client for Linux and Mac. See AppVeyor for\n# Windows\nsudo: false\n\naddons:\n  apt:\n    sources:\n    - ubuntu-toolchain-r-test\n    packages:\n    - build-essential\n    - clang\n    - fakeroot\n    - g++-4.8\n    - git\n    - libgnome-keyring-dev\n    - xvfb\n    - rpm\n    - libxext-dev\n    - libxtst-dev\n    - libxkbfile-dev\n\nbranches:\n  only:\n  - master\n  - /ci-.*/\n  - /stable.*/\n\nmatrix:\n  include:\n  - os: linux\n    env: NODE_VERSION=6.9 CC=gcc-4.8 CXX=g++-4.8\n  - os: osx\n    env: NODE_VERSION=6.9 CC=clang CXX=clang++\n\ninstall:\n- git clone https://github.com/creationix/nvm.git /tmp/.nvm\n- source /tmp/.nvm/nvm.sh\n- nvm install $NODE_VERSION\n- nvm use --delete-prefix $NODE_VERSION\n\nbefore_script:\n- if [ \"${TRAVIS_OS_NAME}\" == \"linux\" ]; then\n  export DISPLAY=:99.0;\n  sh -e /etc/init.d/xvfb start;\n  fi\n\nscript:\n- npm install && npm test\n\ncache:\n  directories:\n    - node_modules\n    - apm/node_modules\n"
  },
  {
    "path": "packages/client-app/.watchmanconfig",
    "content": "{\n  \"ignore_dirs\": [\n    \"build/node_modules\",\n    \"apm/node_modules\",\n    \"node_modules\",\n    \"src/K2/node_modules\",\n    \"src/K2/packages/local-sync/node_modules\",\n    \"src/K2/packages/isomorphic-core/node_modules\",\n    \"src/K2/packages/cloud-api/node_modules\",\n    \"src/K2/packages/cloud-workers/node_modules\",\n    \"src/K2/packages/cloud-core/node_modules\"\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/CHANGELOG.md",
    "content": "# Nylas Mail Changelog\n\n### 2.0.32 (5/1/2017)\n\n  + Remove unnecessary tracking events\n\n### 2.0.31 (4/28/2017)\n\n- Fixes:\n  + Fix invalid credentials error when sending on Gmail. This error will happen\n    sporadically, and was introduced in version 2.0.28\n\n### 2.0.28 (4/26/2017)\n\n- Fixes:\n  + Fix send later with open/link tracking\n\n### 2.0.27 (4/26/2017)\n\n- Fixes:\n  + Don't throw error when search query has trailing whitespace\n\n### 2.0.26 (4/26/2017)\n\n- Fixes:\n  + Fix self-triggering in open and link tracking\n  + Speed up sending per recipient\n  + Fix open and link tracking when sending per recipient\n\n- Development:\n  + Consolidate TrackingUtils & better documentation\n\n### 2.0.23 (4/25/2017)\n\n- Fixes:\n  + Properly retry retryable errors in syncback tasks\n\n### 2.0.21 (4/24/2017)\n\n- Fixes:\n  + Fix throwing errors inside Interruptible\n  + Fix sending on Gmail with large attachments (caused by conflict with syncing\n    sent folder)\n  + Increment max size for attachments\n\n### 2.0.20 (4/24/2017)\n\n- Fixes:\n  + Correctly pass connSettings to convertSmtpError\n  + Fix attachment previews\n  + Fix link editor jumping away from you in composer\n  + Fix certificate error msg\n  + Detect smtp cert errors and relax condition to detect them\n\n### 2.0.19 (4/21/2017)\n\n- Features:\n  + Allow users to select custom folder mappings for Sent and Trash folders\n  + Move messages out of db into compressed flat files for better space\n    efficiency\n\n- Performance:\n  + 10x speed improvement for sending messages\n  + Improve performance of all syncback tasks by 500ms\n\n- Fixes:\n  + Correctly cleanup orphaned messages during sync\n\n- Development:\n  + Refactor sending code and remove cruft\n  + Fix the specs\n\n### 2.0.18 (4/21/2017)\n\n- Fixes:\n  + Correctly track all auth errors & correlate to email\n  + Add more IMAP provider settings from Mozilla's ISPDB\n  + Allow bypassing of invalid certificates during authentication\n  + Don't double report auth errors\n\n### 2.0.17 (4/19/2017)\n\n- Fixes:\n  + Record auth error location to Mixpanel\n  + Show proper auth error messages to users\n  + Correctly identify more certificate errors\n  + Fix offline notification behind proxies\n  + Fix attachment filename encodings\n\n- Development\n  + Prevent from running daily when untracked files present in working dir\n  + Fixup auth helpers\n\n### 2.0.16 (4/18/2017)\n\n- Fixes:\n  + Better handling of startup errors\n  + Fix occasional EPERM issues on boot on Windows\n  + Reduce CPU limits for historical sync\n  + Fix search parser to handle nested queries properly\n  + Update copy that still referenced N1 to Nylas Mail\n\n- Development:\n  + Fix benchmark mode\n\n### 2.0.15 (4/17/2017)\n\n  + Correctly handle and inform users about database malformed errors that can\n    occur both in main process and/or window processes\n\n### 2.0.14 (4/14/2017)\n\n- Fixes:\n  + Prevent from adding duplicate accounts and sync workers due to account id changes\n  + Correctly remove sync worker reference when destroying it\n  + Correctly initialize SyncProcessManager with Identity\n  + Fix contact ranking runtime error\n\n### 2.0.13 (4/13/2017)\n\n- Fixes:\n  + Upload nupkg with correct name for win32 autoupdater to work\n  + Correctly handle window.unhandledrejection events\n\n### 2.0.12 (4/13/2017)\n\n- Fixes:\n  + Prevent NM from overwriting N1 binary on windows\n  + Fix runtime error in sync process\n  + Prevent old N1 config from getting wiped when installing Nylas Mail\n\n- Development:\n  + Remove useless docs\n\n### 2.0.11 (4/12/2017)\n\n- Fixes:\n  + Dispose of mail listener connection before getting new one. This will\n    prevent sync process from leaking Imap connections and getting stuck.\n  + Fix performance regression when polling for gmail attribute changes\n  + Don't double report unhandled rejections\n  + Fix unhandled rejection handling (fix ipc parse error)\n  + Fix regression when processing messages under a transaction\n  + Rate limit database malformed error reports to sentry\n\n### 2.0.10 (4/11/2017)\n\n- Fixes:\n  + Fix missing UID error when archiving threads after sending\n  + Ensure all mail folder exists before trying to access it\n  + Fix SyncbackMetadataTask dependency\n\n- Development:\n  + Don't report stuck sync processes to Sentry\n  + MessageFactory -> MessageUtils, SendUtils -> ModelUtils\n\n### 2.0.9 (4/11/2017)\n\n- Features:\n  + Re-add imap to the onboarding accounts page\n\n- Fixes:\n  + Correctly detect changes in labels, starred and unread for Gmail accounts\n  + Fix delta streaming connection retries\n  + Handle weird MIME edge case with @ symbol\n\n- Performance:\n  + Wrap message processing in transaction for better performance\n  + Increase sqlite `page_size` and `cache_size`\n\n- Cloud:\n  + Improve performance of reminders worker\n  + Add DataDog StatsD for heartbeats\n  + Restart automatically on unhandeld rejections\n\n- Development:\n  + Add benchmark mode\n\n### 2.0.8 (4/7/2017)\n\n- Fixes:\n  + Revamp SSL options during authentication to be able to properly auth against\n    SMTP and prevent sending failures\n  + Ensure IMAPConnnectionPool uses updated account credentials\n  + Always fetch and update identity regardless of environment\n  + Properly handle serialization errors for JSON columns in database\n\n- Cloud:\n  + Switch MySQL charset to utf8mb4\n  + Add exponential backoff for cloud worker jobs when encountering errors\n  + Use IMAP connection pool in cloud workers to limit number of connections\n  + Properly generate metadata deltas when clearing expiration field\n  + Increment default imap connection socket timeout in cloud workers\n\n- Plugins:\n  + Correctly syncback metadata for send later\n  + Delete drafts after they are sent later\n  + Correctly ensure messages in sent folder for send later in gmail\n  + Fix send reminders version conflict error\n  + Correctly set metadata values for send reminders\n  + Fix imap folder names in send-reminders\n  + Fix send later access token refresh\n\n- Development:\n  + Add view of CloudJobs in n1.nylas.com/admin\n  + Ensure daily script grabs current version after pulling latest changes\n\n### 2.0.1 (4/5/2017)\n\n- Features:\n  + Limit search to focused perspective\n\n- Fixes:\n  + IMAPConnectionPool now correctly disposes connections\n  + Ensure we use refreshed access token for all imap connections during sync\n  + Prevent IMAP connection leaking in sync worker\n  + Fix send later button saving state and sending action\n  + Fix inline images for send later\n  + Correctly enable plugins on 2.0.1\n  + Make sure app can update even after signing out of NylasID\n  + Don't make any requests when NylasID isn't present\n\n- Cloud:\n  + Make cloud workers more robust\n  + Remove old SignalFX reporter & add docs\n  + Log errors according to bunyan specs\n\n- Development:\n  + Add script to run benchmarks once per day at specified time\n  + Add script to upload benchmark data to Google Sheets\n  + Add better logging when restarting stuck sync worker\n\n### 2.0.0 (4/4/2017)\n\nIntroducing Nylas Mail Pro\n\n- Features:\n  + Enable snooze, send later, and send reminders\n  + Add feature limits to reminders and send later\n\n- Fixes\n  + Don't assign duplicate folder roles\n  + Re-setup IdentityStore in new window\n\n- Development:\n  + Fix sqlite build for older versions of clang\n  + Remove rogue scripts-tmp folder\n  + Remove unecessary db setup for mail rules\n\n### 1.0.55 (3/31/2017)\n\n- Fixes\n  + Ensure open/link tracking work when sending multiple consecutive emails\n  + Fix performance of contact rankings database query\n  + Fix performance of thread search index database queries\n  + Fix performance of ANALYZE queries\n\n### 1.0.54 (3/31/2017)\n\n- Features:\n  + Add search support for `has:attachment`\n\n- Fixes:\n  + Reduce database thrashing caused by thread search indexing\n  + Interrupt long-running syncback tasks\n  + Fix performance of contact rankings db query\n  + Don't hit contact rankings endpoint until account is ready\n  + Ensure sync worker is stopped correctly when removing accounts or when\n    restarting it\n\n- Metrics:\n  + Report metrics about SyncbackTask runs\n\n- Perf:\n  + Delay building new hot window to improve win perf\n\n- Development:\n  + Add script to benchmarks new commits\n  + Add DEBUG flag to be able to log all query activity for both databases\n  + Add `DatabaseStore.write` which doesn't use Transactions\n  + Metadata test fixes\n\n### 1.0.52 (3/29/2017)\n\n- Fixes:\n  + Fix open and link tracking:\n    + No longer triggers your own opens & link clicks\n    + Link tracking indicator is now always present in sent messages\n  + Fix regression in DB query execution which would delay all queries in the\n    system.\n  + Reduce max retry backoff for DB queries, which could hold a query open for\n    too long\n  + Fix thread reindexing issues, which should help performance and correctly\n    index threads for search\n  + Fix `in:` search syntax for non-gmail search\n  + Fix references to RetryableError imports\n\n- Development:\n  + Add initial sync benchmarking script\n  + Clean up logging in DatabaseStore: differentiate background queries from\n    regular queries in the logs, only log queries that actually take more than\n    100ms.\n  + Point the billing server URL to staging by default for easier development,\n    and allow it to be overriden\n  + Add index to expiration field on Metadata\n\n### 1.0.51 (3/28/2017)\n\n- Features:\n  + Restore contact rankings feature for better contact predictions in composer\n    recipient fields\n\n- Fixes:\n  + Correctly listen for new mail in between sync loops\n  + Verify SMTP credentials in /auth endpoint\n  + Also prioritize sent label for initial Gmail sync\n  + Properly relaunch windows on autoupdate\n  + Properly set up local /health endpoint by making sure to attach route files\n    ending in .es6 to local-api\n\n- Perf:\n  + Don't throttle while syncing first 500 threads\n\n- Metrics:\n  + Report battery state changes to Mixpanel\n\n- Development:\n  + Make deploy-it say what it's doing instead of hanging silently\n  + Make deploy-it print link to the EB console\n  + Make help message better on deploy-it\n  + Add `SHOW_HOT_WINDOW` env for prod debugging of window launches\n  + Correctly ignore `node_modules` in .ebignore for faster deploys\n  + Only bootstrap specific pkgs in postinstall for faster npm installs\n\n### 1.0.50 (3/28/2017)\n\n- Fixes:\n  + Fix SyncActivity errors introduced in 1.0.49\n\n### 1.0.49 (3/27/2017)\n\n- Fixes:\n  + Ensure sync process does not get stuck\n  + Ensure the worker window is always available\n  + Retry database operations when encountering locking issues\n\n- Metrics:\n  + Detect and report when the worker window is unavailable\n  + Detect and report when a sync process is stuck\n\n- Development:\n  + Windows autoupdater fixes\n  + Add better documentation for windows autoupdater\n  + Remap windows dev shortcuts to match the ones used on darwin and linux\n  + When building app, only re-install for optional dependencies on darwin\n\n- Cloud:\n  + Timeout streaming API connections every 15 minutes\n  + Add missing database indexes from SQL review\n\n### 1.0.48 (3/27/2017)\n\n- Fixes:\n  + Reindex threads when they're updated\n  + Don't try to restart sync on every IdentityStore change\n  + Correctly remove inline images with x button\n\n### 1.0.47 (3/23/2017)\n\n- Fixes:\n  + Report hard crashes using Electron's built-in crash reporter\n\n- Development:\n  + Don't handle IMAP timeouts in the connection pool\n  + Record file download times\n\n### 1.0.46 (3/22/2017)\n\n- Fixes:\n  + Ensure files get transferred in forwarded messages\n  + Correctly sign out of NylasID\n  + Don't report non-reportable errors in delta connection\n  + Fix S3 attachment upload for send later\n\n- Development:\n  + Rename downloadDataForFile(s) -> getDownloadDataForFile(s)\n  + Switch type of Metadata value column\n  + Fix build condition\n  + Fix DraftFactory specs\n  + Refactor sync worker IMAPConnectionPool callbacks\n\n### 1.0.45 (3/21/2017)\n\n- Fixes:\n  + Correctly report unhandled errors caught in window.\n  + Fix passing cursor to delta streams\n\n### 1.0.44 (3/20/2017)\n\n- Fixes:\n  + Add error handling when creating syncback requests\n  + Fix path for tmp dir in daily script\n\n### 1.0.43 (3/17/2017)\n\n- Fixes:\n\n + Revert nodemailer to previous version\n + Creating a folder no longer creates a non-existent duplicate subfolder\n + Don't bump threads to the top of list when a message is sent: only update lastReceivedDate if the message was actually received\n\n### 1.0.42 (3/16/2017)\n\n- Fixes:\n + Fix spellchecker regression (Don't exclude source maps in build)\n\n### 1.0.41 (3/16/2017)\n\n- Development:\n  + Upgrade nodemailer to latest version\n\n### 1.0.40 (3/15/2017)\n\n- Features:\n  + Add support for attachments in send later\n\n- Development:\n  + Improve build time\n  + Windows Autoupdater fixes\n\n### 1.0.39 (3/14/2017)\n\n- Fixes:\n  + Fix missing depedency for imap-provider-settings\n\n- Development:\n  + Only upload 7 characters of the commit hash for Windows build\n\n### 1.0.38 (3/13/2017)\n\n- Fixes:\n + Restart sync when computer awakes from sleep\n + Fix issue that made users log out of NylasID, restart, and then force them to log out and restart again in a loop (#3325)\n + Don't start sync or delta connections without an identity\n\n- Development:\n + Restore windows build\n + Remove specs from production build\n + Fix arc lint\n + Specify Content-Type in developer bar curl commands\n\n### 1.0.37 (3/10/2017)\n\n- Fixes:\n  + Fix regression introduced in 1.0.36 in the message processor\n  + Correctly show auth error when we can't connect to n1cloud\n  + Fix error thrown sometimes when handling send errors\n\n### 1.0.36 (3/10/2017)\n\n- Fixes:\n  + Increase the IMAP connection pool size\n  + Shim sequelize to timeout after 1 minute on every database operation. This\n    is a safeguard to prevent unresolved db promises from halting the sync loop.\n  + Better error handling to prevent the message processor from halting sync\n\n- Development:\n  + Measure and report inline composer open times\n  + Refactor MessageProcessor to be more robust to errors\n\n### 1.0.35 (3/9/2017)\n\n- Fixes:\n  + Make sure delta connection is restarted when an account is re-authed\n  + More defensive error handling to prevent sync from halting\n  + Prevent delta streaming connection from retrying too much\n  + Fix error when attempting to report a fetch id error\n  + Prevent  error restart loop when database is malformed\n  + Correctly cancel search when the search perspective is cleared\n  + When many search results are returned from the server, don't try to sync them all at once, otherwise would slow down the main sync process.\n  + When restarting the app, don't try to continue syncing search results from an old search\n\n- Development:\n  + Consolidate delta connection stores, remove `internal_package/deltas`\n  + Rename NylasSyncStatusStore to FolderSyncProgressStore\n  + Consolidate APIError status code that we should not report\n  + Don't report incorrect username or password to Sentry\n  + Rate limit error reporting for message processing errors\n  + Fix circular reference error when reporting errors\n  + Refactor file download IMAPConnectionPool usage\n  + Don't focus the Console tab in dev tools every time an error is logged\n  + Correctly set process title\n\n### 1.0.34 (3/8/2017)\n\n- Fixes:\n  + Sync should not get stuck anymore due to sequelize\n  + Delta Streaming connections now correctly retry after they are closed or an error occurs\n  + Handle errors when opening imap box correctly\n\n- Development:\n  + Add script/daily\n  + Provide better info to Sentry on sending errors\n  + Refactor and clean up delta streaming code\n  + Refactor message processing throttling\n\n### 1.0.33 (3/8/2017)\n\n- Features:\n\n  + Add intitial support for send later\n\n- Fixes:\n\n  + Fetch unknown message uids returned in search results\n  + Don't throttle message processing when syncing specific UIDs\n\n- Development:\n\n  + Better grouping for APIError by URL also\n  + Don't generate sourceMapCache in prod mode\n  + Upload a next-version to S3 for autoupdate testing\n  + Windows build fixes\n\n### 1.0.32 (3/7/2017)\n\n- Development:\n\n  + Report provider when reporting remove-from-threads-from-list\n  + Report provider when reporting send perf metrics\n\n### 1.0.31 (3/6/2017)\n\n- Fixes:\n\n  + Improve initial sync speed by scaling number of messages synced based on\n    folder SELECT duration\n  + Immediately restore sync process when app comes back online after being\n    disconnected from the internet.\n  + Can now reply from within notifications again\n\n- Development:\n\n  + Add basic rate limiting to Sentry\n  + Report all search performance metrics\n  + Prevent noisy uncaught errors when closing long connection\n  + Improve reporting of refresh access token errors\n  + Don't double report refresh access token API errors\n  + Replace `setImmediate` with `setTimeout` as Promise scheduler\n  + Use new Bluebird preferred `longStackTraces` syntax\n  + NylasAPIRequest refactored and cleaned up\n  + Search refactors and improvements\n  + Protect from operating on IMAP connection while opening a box\n  + Enable logging in prod builds\n  + Make deploy-it support -h/--help\n  + Restore cloud testing environments\n\n### 1.0.30 (2/28/2017)\n\n- Fixes:\n\n  + Can properly add signatures and select them as default for different\n    accounts.\n  + Can now correctly reply to a thread and immediately archive it or move it to\n    another folder without throwing an error (#3290)\n  + Correctly fix IMAP connection timeout issues (#3232)\n  + Nylas Mail no longer opens an increasing number of IMAP connections which\n    caused some users to reach IMAP server connection limits (#3228)\n  + Fix memory leak while syncing which caused sync process to restart\n    sometimes.\n  + Correctly handle IMAP connections ending unexpectedly\n  + Correctly detect retryable IMAP errors during sync + detect more\n    retryable errors\n  + Correctly catch more authentication errors when sending\n  + Improve speed of processing messages during sync\n  + Prevent unnecessary re-renders of the thread list\n\n- Development:\n\n  + Report performance metrics\n  + More Coffeescript to Javascript conversions\n\n### 1.0.29 (2/21/2017)\n\n- Fixes:\n\n  + You can now click inline images in messages to open them\n  + More IMAP errors have been identified as retryable, which means users will\n    see less errors when syncing an account\n  + Improve performance of thread search indexing queries\n  + Correctly catch Invalid Login errors when sending\n\n- Development:\n\n  + Developer bar in Worker window now shows single delta connection\n  + More code converted to Javascript\n\n### 1.0.28 (2/16/2017)\n\n- Fixes:\n\n  + Fix offline notification bug that caused api outage\n  + We now properly handle gmail auth token errors in the middle of the sync loop. This means less red boxes for users!\n  + Less battery usage when initial sync has completed!\n  + No more errors when saving sent messages to sent folders (`auth or accountId` errors)\n  + No more `Lingering tasks in progress marked as failed errors`\n  + Syncback tasks will continue retrying even after closing app\n  + Syncback tasks retry more aggressively\n  + Detect more offline errors when sending, sending is more reliable\n  + Imap connection pooling (yet to land)\n  + More retryable IMAP errors, means less red boxes for users\n  + Offline notification now shows itself when we’re actually offline, shows countdown for next reconnect attempt\n\n- Development:\n\n  + More tests\n  + Don't use breadcrumbs in dev mode\n  + Add a better reason when waking sync for syncback in the logs\n  + BackoffScheduler, BatteryManager added for reusability\n\n### 1.0.27 (2/14/17)\n\n- Fixes:\n\n  + Offline notification fixes\n\n### 1.0.26 (2/10/17)\n\n- Fixes:\n\n  + Downloads retry if they fail\n  + NylasID doesn't intermittently log out or throw errors\n  + Fix initial sync for Inbox Zero Gmail accounts\n\n### 1.0.25 (2/10/17)\n\n- Fixes:\n\n  + When replying to a thread, properly add it to the sent folder\n\n- Development:\n\n  + Can now once again run Nylas Mail test suite\n\n### 1.0.24 (2/9/17)\n\n- Fixes:\n\n  + Fix error reporter when reporting an error without an identity (this would\n    crash the app)\n\n- Development:\n\n  + Fix logging inside local-sync api requests\n  + Stop reporting handled API errors to Sentry\n  + Report thread-list perf metrics\n\n### 1.0.23 (2/8/17)\n\n- Fixes:\n\n  + Fix emails occasionally being sent with an incomplete body (#3269)\n  + Correctly thread messages together when open/link tracking is enabled\n  + Fix `Mailbox does not exist` error for iCloud users (#3253)\n  + When adding account, correctly remove whitespace from emails\n  + Fix link in update notification to point to latest changelog\n\n- Performance:\n\n  + Thread list actions no longer sporadically lag for ~1sec (this is especially\n    noticeable when many accounts have been added)\n  + No longer slow down sync process when more than 100,000 threads have been synced\n\n- Development:\n\n  + Better logging in worker window\n  + You can now run a development build of Nylas Mail alongside a production\n    build\n\n### 1.0.22 (2/7/17)\n\n- Fixes:\n\n  + New mail notification sounds on startup are combined when multiple new messages have arrived\n  + You can now correctly select threads using `cmd` and `shift`\n  + Improve message fetching by making sure we always fetch the most recent\n    messages first.\n  + Improve IMAP connection timeouts by incrementing the socket timeout (#3232)\n  + When adding a Google account, make sure to show the Account Chooser\n\n- Development:\n\n  + Nylas Identity is no longer stored in config.json\n\n### 1.0.21 (2/3/17)\n\n- Fixes:\n\n  + Fixed an issue where Nylas Mail could delete all accounts (addresses #3231)\n  + Correctly delete and archive threads when they contain sent messages (addresses #2706)\n  + Improve performance and prevent crashes when running several sync actions\n  + Improve error handling when sync actions fail\n  + Fix JSON serialization issue which could cause sync process to error.\n\n### 1.0.20 (2/1/17)\n\n- Fixes:\n\n  + Properly clean up broken replies\n\n### 1.0.19 (1/31/17)\n\n- Fixes:\n\n  + Replies on threads won't create duplicate-looking emails. This began\n    to happen on midnight February 1 UTC due to a date parsing bug\n  + Improve error handling in sync\n  + Better retrying of certain syncback actions\n\n- Development:\n\n  + Now using Electron 1.4.15\n\n### 1.0.18 (1/30/17)\n\n- Performance:\n\n  + 60% reduction of CPU usage during initial sync due to optimizing\n    unnecessary rendering\n\n- Fixes:\n\n  + New composer stays in \"to\" field when initially typing\n\n- Development:\n\n  + Better documentation for Nylas Mail SDKs\n  + GitHub repository renamed from nylas/N1 to nylas/nylas-mail\n  + `master` branch now has Nylas Mail (1.0.x)\n  + `n1-pro` branch now has Nylas Pro (1.5.x)\n\n### 1.0.17 (1/27/17)\n\n- Fixes:\n\n  + Fix send and archive: Can now archive after sending without errors\n  + Local search now includes more thread results\n  + Contact autocomplete in composer participant fields now includes more results\n\n### 1.0.16 (1/27/17)\n\n- Performance:\n\n  + Improved typing performance in the composer, especially with\n    misspelled words\n\n- Fixes:\n\n  + Nylas Mail plugins install properly\n  + Fix undo and occasional archive & move tasks failing due to not having uids\n  + Fix logging for auth\n  + Properly clean up after file downloads\n  + Properly recover from IMAP uid invalidity\n\n### 1.0.15 (1/25/17)\n\n- Features:\n\n  + Improve CPU performance of idle windows\n\n- Fixes:\n\n  + Correctly detect initial battery status for throttling.\n  + Correctly allow auth for Custom IMAP accounts only #3185\n\n### 1.0.14 (1/25/17)\n\n- Features:\n\n  + Improved spellchecker\n\n- Fixes:\n\n  + Correctly update attributes like starred and unread when syncing folders.\n    Marking as read or starred will no longer bounce back.\n  + Correctly detect new mail while syncing Gmail inbox.\n\n### 1.0.13 (1/25/17)\n\n- Fixes:\n\n  + Messages immediately appear in sent folder. No bouncing back.\n  + Login more likely to succeed. Waits longer for IMAP\n  + Doesn't allow invalid form submission\n  + Correctly handles token refresh failing\n  + Auto updater says \"Nylas Mail\" properly\n  + Sync drafts correctly on Gmail\n\n- Development:\n\n  + Local sync account API deprecated\n  + Silence noisy queries in the logs\n\n### 1.0.12 (1/24/17)\n\n- Features:\n\n  + New 'Debug' sync button that opens up the console\n  + Faster search\n  + Message processing now throttles when on battery\n  + Analytics for change mail tasks\n\n- Fixes:\n\n  + Archive, Mark as Unread, and Move to trash don't \"bounce back\"\n  + Adding a new account is now smoother\n  + Improved threading\n  + Drafts are no longer in the inbox\n\n### 1.0.11 (1/19/17)\n\n- Features:\n\n  + Nylas Mail's installer on Mac uses a DMG\n\n- Fixes:\n\n  + Fixed app being occasionally unresponsive\n  + Decreased odds of failed logins (by bumping connection timeout value)\n  + Sync erroring notification no longer tripped by timeouts\n\n### 1.0.10 (1/19/17)\n\n- Features:\n\n  + \"Contact Support\" button now auto-fills information\n  + Actions reach providers faster\n\n- Fixes:\n\n  + Show errors on the GMail auth screen\n  + Show draft sending errors\n  + Can now correctly search threads via `from:` and `to:`\n  + Other error management improvements\n  + The database will now be reset if malformed\n  + Improve the offline notification\n\n- Development:\n\n  + Update Thread indexing\n  + Add loadFromColumm option to Attribute\n\n### 1.0.9 (1/17/17)\n\n- Fixes:\n\n  + All Fastmail domains now use the correct credentials\n  + Offline notification more reliable\n  + Fix error logging\n\n### 1.0.8 (1/17/17)\n\n- Introducing Nylas Mail Basic! Read more about it [here](https://blog.nylas.com/nylas-mail-is-now-free-8350d6a1044d)\n"
  },
  {
    "path": "packages/client-app/CONFIGURATION.md",
    "content": "# Configuration\n\nThis document outlines configuration options which aren't exposed via N1's\npreferences interface but may be useful.\n\n## Running Against Open Source Sync Engine\n\nIf you want to point N1 to your self-hosted sync engine, select \"Hosting your own sync engine?\" under the \"Get Started\" button on the welcome screen. There, follow the instructions for creating your own instance of the sync engine and enter the URL and port number where you have it running.\n\n\n## Other Config Options\n\n- `core.workspace.interfaceZoom`: If you'd like the N1 interface to be smaller or larger, this option allows you to scale the UI globally. (Default: 1)\n"
  },
  {
    "path": "packages/client-app/CONTRIBUTING.md",
    "content": "# Filing an Issue\n\nThanks for checking out N1! If you have a feature request, be sure to check out the [open source roadmap](http://trello.com/b/hxsqB6vx/n1-open-source-roadmap). If someone has already requested\nthe feature you have in mind, you can upvote the card on Trello—to keep things organized, we\noften close feature requests on GitHub after creating Trello cards.\n\nIf you've found a bug, try searching for similars issue before filing a new one. Please include\nthe version of N1 you're using, the platform you're using (Mac / Windows / Linux), and the\ntype of email account. (Gmail, Outlook 365, etc.)\n\n# Contributing to N1\n\nThe hosted sync engine allows us to control adoption of N1 and maintain a great\nexperience for our users. However, the sync engine is\n[open source](https://github.com/nylas/sync-engine) and you can set it\nup yourself to begin using N1 immediately. Follow instructions on the [sync\nengine](https://github.com/nylas/sync-engine) repository.\n\n### Getting Started\n\nBefore you get started, make sure you've installed the following dependencies.\nN1's build scripts and tooling use modern JavaScript features and require:\n\n - Node 6.0 or above with npm3\n - python 2.7\n\nLinux users should make sure they've installed all the packages listed at\nhttps://github.com/nylas/nylas-mail/blob/master/.travis.yml#L10. Linux users on\nDebian 8 and Ubuntu 15.04 onward must also install libgcrypt11 and gnome-keyring.\n\nNext, clone and build N1 from source:\n\n    git clone https://github.com/nylas/nylas-mail.git\n    cd nylas-mail\n    script/bootstrap\n\nRead the [getting started guides](https://nylas.github.io/N1/getting-started/).\n\n**Building Nylas on Windows? See the [Windows instructions.](https://github.com/nylas/nylas-mail/blob/master/docs/Windows.md)**\n\n### Running N1\n\n    npm start\n\n### Testing N1\n\n    npm test\n\nThis will run the full suite of automated unit tests. We use [Jasmine 1.3](http://jasmine.github.io/1.3/introduction.html).\n\nIt runs all tests inside of the `/spec` folder and all tests inside of\n`/internal_packages/**/spec`\n\nYou may skip certain tests (temporarily) with `xit` and `xdescribe`, or focus on only certain tests with `fit` and `fdescribe`.\n\n### Linting N1\n\nN1 lints clean against eslint, coffeelint, csslint, lesslint, and our own internal\ntool, nylaslint. To run the linters, just run `npm run lint`.\n\n### Creating binaries\n\nOnce you've checked out N1 and run `script/bootstrap`, you can create a packaged\nversion of the application by running `script/build`. Note that the builds\navailable at [https://nylas.com/N1](https://nylas.com/N1) include licensed\nfonts, sounds, and other improvements. If you're just looking to run N1, you\nshould download it there!\n\n\n# Pull requests\n\nWe require all authors sign our [Contributor License\nAgreement](https://www.nylas.com/cla.html) before pull requests (even\nminor ones) can be accepted. (It's similar to other projects, like NodeJS\nMeteor, or React). I'm really sorry, but Legal made us do it.\n\n### Commit Format\n\nWe decided to not impose super strict commit guidelines on the community.\n\nWe're trusting you to be thoughtful, responsible, committers.\n\nWe do have a few heuristics:\n\n- Keep commits fairly isolated. Don't jam lots of different functionality\n  in 1 squashed commit. `git bisect` and `git cherry-pick` should still be\n  reasonable things to do.\n- Keep commits fairly significant. DO `squash` all those little file\n  changes and \"fixmes\". Don't make it difficult to browse our history.\n  Play the balance between this idea and the last point. If a commit\n  doesn't deserve your time to write a long thoughtful message about, then\n  squash it.\n- Be hyper-descriptive in your commit messages. I care less about what\n  you did (I can read the code), **I want to know WHY you did it**. Put\n  that in the commit body (not the subject). Itemize the major semantic\n  changes that happened.\n- Read \"[How to Write a Git Commit Message](http://chris.beams.io/posts/git-commit/)\" if you haven't already (but don't be too prescriptivist about it!)\n\n# Running Against Open Source Sync Engine\n\nSee [Configuration](https://github.com/nylas/nylas-mail/blob/master/CONFIGURATION.md)\n"
  },
  {
    "path": "packages/client-app/LICENSE.md",
    "content": "MIT License\n\nCopyright (c) 2017 Nylas, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/client-app/README.md",
    "content": "# Nylas Mail - the open-source, extensible mail client\n![N1 Screenshot](https://nylas.com/static/img/home/screenshot-hero-mac@2x.png)\n\n  **Nylas Mail is an open-source mail client built on the modern web with [Electron](https://github.com/atom/electron), [React](https://facebook.github.io/react/), and [Flux](https://facebook.github.io/flux/).** It is designed to be extensible, so it's easy to create new experiences and workflows around email. Want to learn more? Check out the [full documentation](https://nylas.github.io/nylas-mail/).\n\n[![Build Status](https://travis-ci.org/nylas/nylas-mail.svg?branch=master)](https://travis-ci.org/nylas/nylas-mail)\n[![Slack Invite Button](http://slack-invite.nylas.com/badge.svg)](http://slack-invite.nylas.com)\n\n#### Want to help build the future of email? [Nylas is hiring](https://jobs.lever.co/nylas)!\n\n## Download Nylas Mail\n\nYou can download compiled versions of Nylas Mail for Windows, Mac OS X, and Linux (.deb) from [https://nylas.com/download](https://nylas.com/download). You can also build and run Nylas Mail (Previously N1) on Fedora. On Arch Linux, you can install **[n1](https://aur.archlinux.org/packages/n1/)** or **[n1-git](https://aur.archlinux.org/packages/n1-git/)** from the aur.\n\n## Build A Plugin\n\nPlugins lie at the heart of Nylas Mail and give it its powerful features. Building your own plugins allows you to integrate the app with other tools, experiment with new workflows, and more. Follow the [Getting Started guide](https://nylas.github.io/nylas-mail/) to write your first plugin in five minutes. To create your own theme, go to our [Theme Starter guide](https://github.com/nylas/N1-theme-starter).\n\nIf you would like to run the N1 source and contribute, check out our [contributing\nguide](https://github.com/nylas/nylas-mail/blob/master/CONTRIBUTING.md).\n\n## Themes\n\nThe Nylas Mail user interface is styled using CSS, which means it's easy to modify and extend. Nylas Mail comes stock with a few beautiful themes, and there are many more which have been built by community developers\n\n<center><img width=550 src=\"http://i.imgur.com/PWQ7NlY.jpg\"></center>\n\n\n#### Bundled Themes\n- [Dark](https://github.com/nylas/nylas-mail/tree/master/internal_packages/ui-dark)\n- [Darkside](https://github.com/nylas/nylas-mail/tree/master/internal_packages/ui-darkside) (designed by [Jamie Wilson](https://github.com/jamiewilson))\n- [Taiga](https://github.com/nylas/nylas-mail/tree/master/internal_packages/ui-taiga) (designed by [Noah Buscher](https://github.com/noahbuscher))\n- [Ubuntu](https://github.com/nylas/nylas-mail/tree/master/internal_packages/ui-ubuntu) (designed by [Ahmed Elhanafy](https://github.com/ahmedlhanafy))\n- [Less Is More](https://github.com/nylas/nylas-mail/tree/master/internal_packages/ui-less-is-more) (designed by [Alexander Adkins](https://github.com/P0WW0W))\n\n\n\n#### Community Themes\n- [Arc Dark](https://github.com/varlesh/Nylas-Arc-Dark-Theme)\n- [Predawn](https://github.com/adambmedia/N1-Predawn)\n- [ElementaryOS](https://github.com/edipox/elementary-nylas)\n- [Ido](https://github.com/edipox/n1-ido)—Polymail-inspired theme\n- [Solarized Dark](https://github.com/NSHenry/N1-Solarized-Dark)\n- [Berend](https://github.com/Frique/N1-Berend)\n- [LevelUp](https://github.com/stolinski/level-up-nylas-n1-theme)\n- [Sunrise](https://github.com/jackiehluo/n1-sunrise)\n- [ToogaBooga](https://github.com/brycedorn/N1-ToogaBooga)\n- [Material](https://github.com/jackiehluo/n1-material)\n- [Monokai](https://github.com/dcondrey/n1-monokai)\n- [Agapanthus](https://github.com/taniadaniela/n1-agapanthus)—Inbox-inspired theme\n- [Stripe](https://github.com/oeaeee/n1-stripe)\n- [Kleinstein] (https://github.com/diklein/Kleinstein)—Hide the account list sidebar\n- [BoraBora](https://github.com/arimai/N1-BoraBora)\n- [Honeyduke](https://github.com/arimai/n1-honeyduke)\n- [Snow](https://github.com/Wattenberger/N1-snow-theme)\n- [Hull](https://github.com/unity/n1-hull)\n- [Express](https://github.com/oeaeee/n1-express)\n- [DarkSoda](https://github.com/adambullmer/N1-theme-DarkSoda)\n- [Bemind](https://github.com/bemindinteractive/Bemind-N1-Theme)\n- [Dracula](https://github.com/dracula/nylas-n1)\n- [MouseEatsCat](https://github.com/MouseEatsCat/MouseEatsCat-N1)\n- [Sublime Dark](https://github.com/rishabhkesarwani/Nylas-Sublime-Dark-Theme)\n- [Firefox](https://github.com/darshandsoni/n1-firefox-theme)\n- [Gmail](https://github.com/dregitsky/n1-gmail-theme)\n- [Darkish](https://github.com/dyrnade/N1-Darkish)\n\n#### To install community themes:\n\n1. Download and unzip the repo\n2. In Nylas Mail, select `Developer > Install a Package Manually... `\n3. Navigate to where you downloaded the theme and select the root folder. The theme is copied into the `~/.nylas-mail` folder for your convinence\n5. Select `Change Theme...` from the top level menu, and you'll see the newly installed theme. That's it!\n\n\nWant to dive in more? Try [creating your own theme](https://github.com/nylas/nylas-mail-theme-starter)!\n\n\n## Plugin List\nWe're working on building a plugin index that makes it super easy to add them to Nylas Mail. For now, check out the list below! (Feel free to submit a PR if you build a plugin and want it featured here.)\n\n\n#### Bundled Plugins\nGreat starting points for creating your own plugins!\n- [Translate](https://github.com/nylas/nylas-mail/tree/master/internal_packages/composer-translate)—Works with 10 languages\n- [Quick Replies](https://github.com/nylas/nylas-mail/tree/master/internal_packages/composer-templates)—Send emails faster with templates\n- [Emoji Keyboard](https://github.com/nylas/nylas-mail/tree/master/internal_packages/composer-emoji)—Insert emoji by typing a colon (:) followed by the name of an emoji symbol\n- [GitHub Sidebar Info](https://github.com/nylas/nylas-mail/tree/master/internal_packages/github-contact-card)\n- [View on GitHub](https://github.com/nylas/nylas-mail/tree/master/internal_packages/message-view-on-github)\n- [Personal Level Indicators](https://github.com/nylas/nylas-mail/tree/master/internal_packages/personal-level-indicators)\n- [Phishing Detection](https://github.com/nylas/nylas-mail/tree/master/internal_packages/phishing-detection)\n\n#### Community Plugins\n\nNote these are not tested or officially supported by Nylas, but we still think they are really cool! If you find bugs with them, please open GitHub issues on their individual project pages, not the Nylas Mail (N1) repo page. Thanks!\n\n- [Jiffy](http://noahbuscher.github.io/N1-Jiffy/)—Insert animated GIFs\n- [Weather](https://github.com/jackiehluo/n1-weather)\n- [Todoist](https://github.com/alexfruehwirth/N1TodoistIntegration)\n- [Unsubscribe](https://github.com/colinking/n1-unsubscribe)\n- [Squirt Speed Reader](https://github.com/HarleyKwyn/squirt-reader-N1-plugin/)\n- [Website Launcher](https://github.com/adriangrantdotorg/nylas-n1-background-webpage)—Opens a URL in separate window\n- In Development: [Cypher](https://github.com/mbilker/cypher) (PGP Encryption)\n- [Avatars](https://github.com/unity/n1-avatars)\n- [Events Calendar (WIP)](https://github.com/nerdenough/n1-events-calendar)\n- [Mail in Chat (WIP)](https://github.com/yjchen/mail_in_chat)\n- [Evernote](https://github.com/grobgl/n1-evernote)\n- [Wunderlist](https://github.com/miguelrs/n1-wunderlist)\n- [Participants Display](https://github.com/kbruccoleri/nylas-participants-display)\n- [GitHub](https://github.com/ForbesLindesay/N1-GitHub)\n\nWhen you install packages, they're moved to ~/.nylas-mail/packages, and Nylas Mail runs apm install on the command line to fetch dependencies listed in the package's package.json\n\n\n## Building the docs\n\nPlugin SDK docs are available at [https://nylas.github.io/nylas-mail/](https://nylas.github.io/nylas-mail/). Here's how you build them:\n\nUntil my patch gets merged, docs need to be built manually using mg's fork.\n\n    git clone git@github.com:grinich/gitbook.git\n\n    cd nylas-mail\n\n    ./node_modules/.bin/gitbook alias ../gitbook latest\n\nThen to actually build the docs:\n\n    script/grunt docs\n\n    ./node_modules/.bin/gitbook --gitbook=latest build . ./_docs_output --log=debug --debug\n\n    rm -r docs_src/classes\n\nIf you want to preview the docs:\n\n    pushd ./_docs_output; python -m SimpleHTTPServer; popd\n\nJust want to publish everything? There's a helper script that does it for you:\n\n    script/publish-docs\n\n\n## Configuration\nYou can configure Nylas Mail in a few ways—for instance, pointing it to your self-hosted instance of the sync engine or changing the interface zoom level. [Learn more about how.](https://github.com/nylas/nylas-mail/blob/master/CONFIGURATION.md)\n\n## Feature Requests / Plugin Ideas\n\nHave an idea for a package or a feature you'd love to see in Nylas Mail? Search for existing [GitHub issues](https://github.com/nylas/nylas-mail/issues) and join the conversation!\n"
  },
  {
    "path": "packages/client-app/apm/README.md",
    "content": "Nylas Mail ships a copy of [apm](https://github.com/atom/apm) to build packages\nwhen users choose to install them. This won't be true much longer.\n"
  },
  {
    "path": "packages/client-app/apm/package.json",
    "content": "{\n  \"name\": \"n1-bundled-apm\",\n  \"description\": \"N1's bundled apm\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nylas/nylas-mail\"\n  },\n  \"dependencies\": {\n    \"atom-package-manager\": \"1.1.1\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/build/Gruntfile.js",
    "content": "/* eslint global-require: 0 */\n/* eslint import/no-dynamic-require: 0 */\nconst path = require('path');\n\nmodule.exports = (grunt) => {\n  if (!grunt.option('platform')) {\n    grunt.option('platform', process.platform);\n  }\n\n  /**\n   * The main appDir is that of the root nylas-mail-all repo. This Gruntfile\n   * is designed to be run from the npm-build-client task whose repo root is\n   * the main nylas-mail-all package.\n   */\n  const appDir = path.resolve(path.join('packages', 'client-app'));\n  const buildDir = path.join(appDir, 'build');\n  const tasksDir = path.join(buildDir, 'tasks');\n  const taskHelpers = require(path.join(tasksDir, 'task-helpers'))(grunt)\n\n  // This allows all subsequent paths to the relative to the root of the repo\n  grunt.config.init({\n    'taskHelpers': taskHelpers,\n    'rootDir': path.resolve('./'),\n    'buildDir': buildDir,\n    'appDir': appDir,\n    'classDocsOutputDir': './docs_src/classes',\n    'outputDir': path.join(appDir, 'dist'),\n    'appJSON': grunt.file.readJSON(path.join(appDir, 'package.json')),\n    'source:coffeescript': [\n      'internal_packages/**/*.cjsx',\n      'internal_packages/**/*.coffee',\n      'dot-nylas/**/*.coffee',\n      'src/**/*.coffee',\n      'src/**/*.cjsx',\n      '!src/**/node_modules/**/*.coffee',\n      '!internal_packages/**/node_modules/**/*.coffee',\n    ],\n    'source:es6': [\n      'internal_packages/**/*.jsx',\n      'internal_packages/**/*.es6',\n      'internal_packages/**/*.es',\n      'dot-nylas/**/*.es6',\n      'dot-nylas/**/*.es',\n      'src/**/*.es6',\n      'src/**/*.es',\n      'src/**/*.jsx',\n      'src/K2/**/*.js', // K2 doesn't use ES6 extension, lint it anyway!\n      '!src/K2/packages/local-private/src/error-logger-extensions/*.js',\n      '!src/**/node_modules/**/*.es6',\n      '!src/**/node_modules/**/*.es',\n      '!src/**/node_modules/**/*.jsx',\n      '!src/K2/**/node_modules/**/*.js',\n      '!internal_packages/**/node_modules/**/*.es6',\n      '!internal_packages/**/node_modules/**/*.es',\n      '!internal_packages/**/node_modules/**/*.jsx',\n    ],\n  });\n\n  grunt.loadTasks(tasksDir);\n  grunt.file.setBase(appDir);\n\n  grunt.registerTask('docs', ['docs-build', 'docs-render']);\n  grunt.registerTask('lint', [\n    'eslint',\n    'lesslint',\n    'nylaslint',\n    'coffeelint',\n    'csslint',\n  ]);\n\n  if (grunt.option('platform') === 'win32') {\n    grunt.registerTask(\"build-client\", [\n      \"package\",\n      // The Windows electron-winstaller task must be run outside of grunt\n    ]);\n  } else if (grunt.option('platform') === 'darwin') {\n    grunt.registerTask(\"build-client\", [\n      \"package\",\n      \"create-mac-zip\",\n      \"create-mac-dmg\",\n    ]);\n  } else if (grunt.option('platform') === 'linux') {\n    grunt.registerTask(\"build-client\", [\n      \"package\",\n      \"create-deb-installer\",\n      \"create-rpm-installer\",\n    ]);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/build/README.md",
    "content": "# N1 Build Environment\nNode version 0.10.x (Due to the version of electron currently used.)\n\n# N1 Building and Tasks\n\nThis folder contains tasks to create production builds of N1\n\nTasks should not be executed from this folder, but rather from `/scripts`. The\n`/scripts` folder has convenient methods that fix paths and do environment\nchecks.\n\nNote that most of the task definitions are stored in `/build/tasks`\n\n## Some useful tasks\n\nNOTE: Run all of these from the N1 root folder.\n\n**Linting:**\n\n    `script/grunt lint`\n\n**Building:**\n\n    `script/grunt build`\n\nThe build folder has its own package.json and is isolated so we can use `npm`\nto compile against v8's headers instead of `apm`\n"
  },
  {
    "path": "packages/client-app/build/config/coffeelint.json",
    "content": "{\n  \"max_line_length\": {\n    \"level\": \"ignore\"\n  },\n  \"no_empty_param_list\": {\n    \"level\": \"error\"\n  },\n  \"arrow_spacing\": {\n    \"level\": \"error\"\n  },\n  \"no_unnecessary_fat_arrows\": {\n    \"level\": \"ignore\"\n  },\n  \"no_interpolation_in_single_quotes\": {\n    \"level\": \"error\"\n  },\n  \"no_debugger\": {\n    \"level\": \"error\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/build/docs_templates/_function.html",
    "content": "<h4 id={{name}} class=\"function-name\">\n  {{name}}(<span class=\"args\">{{#each arguments}}<span class=\"arg\">{{#if isOptional}}[{{/if}}{{name}}{{#if isOptional}}]{{/if}}</span>{{/each}}</span>) <a href=\"#{{name}}\" class=\"link\"></a>\n</h4>\n\n<div class=\"function-description markdown-from-sourecode\">\n  <p>{{{description}}}</p>\n</div>\n\n{{#if arguments.length}}\n<strong>Parameters</strong>\n<table class=\"arguments\">\n    <tr>\n      <th>Argument</th>\n      <th>Description</th>\n    </tr>\n    {{#each arguments}}\n    <tr>\n      <td style=\"width:15%;\">\n        <em>{{name}}</em>\n      </td>\n      <td class=\"markdown-from-sourecode\">\n        {{#if isOptional}}<span class=\"optional\">Optional</span>{{/if}}\n        {{{description}}}\n      </td>\n    </tr>\n    {{/each}}\n</table>\n{{/if}}\n\n{{#if returnValues.length}}\n<strong>Returns</strong>\n<table class=\"arguments\">\n  <tr>\n    <th>Return Values</th>\n  </tr>\n  {{#each returnValues}}\n  <tr><td class=\"markdown-from-sourecode\">{{{description}}}</td></tr>\n  {{/each}}\n</table>\n{{/if}}\n"
  },
  {
    "path": "packages/client-app/build/docs_templates/_property.html",
    "content": "<h4 id={{name}}>{{name}} <a href=\"#{{name}}\" class=\"link\"></a></h4>\n<p>{{{description}}}</p>\n{{#if arguments.length}}\n<table class=\"arguments\">\n    {{#each arguments}}\n    <tr>\n      <td style=\"width:15%;\">\n        <em>{{name}}</em>\n      </td>\n      <td>\n        {{#if isOptional}}<span class=\"optional\">Optional</span>{{/if}}\n        {{{description}}}\n      </td>\n    </tr>\n    {{/each}}\n</table>\n{{/if}}\n"
  },
  {
    "path": "packages/client-app/build/docs_templates/class.md",
    "content": "# {{ name }}\n\n## Summary\n\n{{{documentation.description}}}\n\n<ul>\n    {{#each documentation.sections}}\n    <li><a href=\"#{{name}}\">{{name}}</a></li>\n    {{/each}}\n</ul>\n\n\n{{#if documentation.classProperties.length}}\n\n### Class Properties\n\n{{#each documentation.classProperties}}\n{{> _property.html}}\n{{/each}}\n\n{{/if}}\n\n\n{{#if documentation.classMethods.length}}\n\n### Class Methods\n\n{{#each documentation.classMethods}}\n{{> _function.html}}\n{{/each}}\n\n{{/if}}\n\n\n{{#if documentation.instanceMethods.length}}\n\n### Instance Methods\n\n{{#each documentation.instanceMethods}}\n{{> _function.html}}\n{{/each}}\n\n{{/if}}\n"
  },
  {
    "path": "packages/client-app/build/docs_templates/sidebar.md",
    "content": "{{!-- This is our preferred ordering, so do it manually! --}}\n## General\n{{#each sidebar.General}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## Component Kit\n{{#each sidebar.[Component Kit]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## Extensions\n{{#each sidebar.[Extensions]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## Models\n{{#each sidebar.[Models]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## Stores\n{{#each sidebar.[Stores]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## Database\n{{#each sidebar.[Database]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## Drafts\n{{#each sidebar.[Drafts]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## NylasEnv\n{{#each sidebar.[NylasEnv]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n\n## Atom\n{{#each sidebar.[Atom]}}\n* [{{this}}](/classes/{{this}}.md)\n{{/each}}\n"
  },
  {
    "path": "packages/client-app/build/resources/asar-ordering-hint.txt",
    "content": "956764: package.json\n1760546: src/browser/main.js\n1766806: node_modules/fs-plus/package.json\n1768751: node_modules/fs-plus/lib/fs-plus.js\n1789356: node_modules/fs-plus/node_modules/underscore-plus/package.json\n1791321: node_modules/fs-plus/node_modules/underscore-plus/lib/underscore-plus.js\n1806109: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/package.json\n1807823: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/underscore.js\n1853312: node_modules/fs-plus/node_modules/async/package.json\n1854649: node_modules/fs-plus/node_modules/async/lib/async.js\n1884050: node_modules/fs-plus/node_modules/mkdirp/package.json\n1885107: node_modules/fs-plus/node_modules/mkdirp/index.js\n1887478: node_modules/fs-plus/node_modules/rimraf/package.json\n1889121: node_modules/fs-plus/node_modules/rimraf/rimraf.js\n1894875: node_modules/mkdirp/package.json\n1896279: node_modules/mkdirp/index.js\n1898909: node_modules/optimist/package.json\n1900199: node_modules/optimist/index.js\n1914385: node_modules/optimist/node_modules/wordwrap/package.json\n1915858: node_modules/optimist/node_modules/wordwrap/index.js\n1918089: src/error-logger.js\n1926560: node_modules/nslog/package.json\n1928316: node_modules/nslog/lib/nslog.js\n1928640: src/error-logger-extensions/nylas-private-error-reporter.js\n1933337: node_modules/raven/package.json\n1935222: node_modules/raven/index.js\n1935631: node_modules/raven/lib/client.js\n1941308: node_modules/raven/lib/parsers.js\n1945141: node_modules/raven/node_modules/cookie/package.json\n1946217: node_modules/raven/node_modules/cookie/index.js\n1948116: node_modules/raven/lib/utils.js\n1953762: node_modules/raven/lib/transports.js\n1956068: node_modules/raven/node_modules/lsmod/package.json\n1957254: node_modules/raven/node_modules/lsmod/index.js\n1958729: node_modules/raven/node_modules/stack-trace/package.json\n1960042: node_modules/raven/node_modules/stack-trace/lib/stack-trace.js\n1962761: node_modules/node-uuid/package.json\n1964629: node_modules/node-uuid/uuid.js\n1933337: node_modules/raven/package.json\n1972642: node_modules/raven/lib/middleware/connect.js\n1973294: src/compile-cache.js\n1978902: src/compile-support/babel.js\n1980527: static/babelrc.json\n1980671: src/compile-support/coffee-script.js\n1981853: src/compile-support/typescript.js\n1983139: node_modules/underscore/package.json\n1985095: node_modules/underscore/underscore.js\n2038014: node_modules/source-map-support/package.json\n2039642: node_modules/source-map-support/source-map-support.js\n2054178: node_modules/source-map-support/node_modules/source-map/package.json\n2057217: node_modules/source-map-support/node_modules/source-map/lib/source-map.js\n2057643: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-generator.js\n2070902: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/package.json\n2072148: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/amdefine.js\n2082064: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64-vlq.js\n2086956: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64.js\n2088093: node_modules/source-map-support/node_modules/source-map/lib/source-map/util.js\n2093422: node_modules/source-map-support/node_modules/source-map/lib/source-map/array-set.js\n2096140: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-consumer.js\n2113947: node_modules/source-map-support/node_modules/source-map/lib/source-map/binary-search.js\n2117157: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-node.js\n2130092: src/browser/application.js\n2161879: src/browser/system-tray-manager.js\n2173675: src/browser/nylas-window.js\n2187662: src/browser/window-manager.js\n91213297: src/browser/window-launcher.js\n91200495: src/browser/file-list-cache.js\n2195924: src/browser/application-menu.js\n2207382: src/flux/models/utils.js\n8174990: node_modules/moment-timezone/package.json\n8177056: node_modules/moment-timezone/index.js\n8177170: node_modules/moment-timezone/moment-timezone.js\n3971136: node_modules/moment/package.json\n3974612: node_modules/moment/moment.js\n8190929: node_modules/moment-timezone/data/packed/latest.json\n3261374: src/task-registry.js\n3264167: src/serializable-registry.js\n3271782: src/database-object-registry.js\n2228693: src/browser/auto-update-manager.js\n2235663: src/browser/nylas-protocol-handler.js\n2396007: node_modules/season/package.json\n2397840: node_modules/season/lib/cson.js\n2238228: src/config.js\n2261295: src/config-utils.js\n2264399: node_modules/emissary/package.json\n2266273: node_modules/emissary/lib/emissary.js\n2266555: node_modules/emissary/lib/helpers.js\n2268129: node_modules/emissary/lib/behavior.js\n2272107: node_modules/emissary/node_modules/underscore-plus/package.json\n2274072: node_modules/emissary/node_modules/underscore-plus/lib/underscore-plus.js\n2288860: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/package.json\n2290574: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/underscore.js\n2336063: node_modules/property-accessors/package.json\n2337929: node_modules/property-accessors/lib/property-accessors.js\n2340201: node_modules/property-accessors/node_modules/mixto/package.json\n2341779: node_modules/property-accessors/node_modules/mixto/lib/mixin.js\n2343179: node_modules/emissary/lib/signal.js\n2352520: node_modules/emissary/lib/emitter.js\n2367779: node_modules/emissary/node_modules/mixto/package.json\n2369463: node_modules/emissary/node_modules/mixto/lib/mixin.js\n2370863: node_modules/emissary/lib/subscriber.js\n2374948: node_modules/emissary/lib/subscription.js\n2376215: node_modules/event-kit/package.json\n2378208: node_modules/event-kit/lib/event-kit.js\n2378397: node_modules/event-kit/lib/emitter.js\n2382101: node_modules/event-kit/lib/disposable.js\n2394595: node_modules/event-kit/lib/composite-disposable.js\n2441735: node_modules/pathwatcher/package.json\n2444020: node_modules/pathwatcher/lib/main.js\n2450494: node_modules/pathwatcher/lib/file.js\n2466571: node_modules/pathwatcher/node_modules/underscore-plus/package.json\n2468584: node_modules/pathwatcher/node_modules/underscore-plus/lib/underscore-plus.js\n2483372: node_modules/pathwatcher/node_modules/underscore-plus/node_modules/underscore/package.json\n2485130: node_modules/pathwatcher/node_modules/underscore-plus/node_modules/underscore/underscore.js\n2530619: node_modules/pathwatcher/lib/directory.js\n2542040: node_modules/pathwatcher/node_modules/async/package.json\n2543482: node_modules/pathwatcher/node_modules/async/lib/async.js\n2572883: src/color.js\n1973294: src/compile-cache.js\n1766806: node_modules/fs-plus/package.json\n1768751: node_modules/fs-plus/lib/fs-plus.js\n1789356: node_modules/fs-plus/node_modules/underscore-plus/package.json\n1791321: node_modules/fs-plus/node_modules/underscore-plus/lib/underscore-plus.js\n1806109: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/package.json\n1807823: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/underscore.js\n1973294: src/compile-cache.js\n1766806: node_modules/fs-plus/package.json\n1768751: node_modules/fs-plus/lib/fs-plus.js\n1789356: node_modules/fs-plus/node_modules/underscore-plus/package.json\n1791321: node_modules/fs-plus/node_modules/underscore-plus/lib/underscore-plus.js\n1806109: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/package.json\n1973294: src/compile-cache.js\n1807823: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/underscore.js\n1766806: node_modules/fs-plus/package.json\n1768751: node_modules/fs-plus/lib/fs-plus.js\n1789356: node_modules/fs-plus/node_modules/underscore-plus/package.json\n1791321: node_modules/fs-plus/node_modules/underscore-plus/lib/underscore-plus.js\n1853312: node_modules/fs-plus/node_modules/async/package.json\n1854649: node_modules/fs-plus/node_modules/async/lib/async.js\n1806109: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/package.json\n1807823: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/underscore.js\n1884050: node_modules/fs-plus/node_modules/mkdirp/package.json\n1885107: node_modules/fs-plus/node_modules/mkdirp/index.js\n1887478: node_modules/fs-plus/node_modules/rimraf/package.json\n1889121: node_modules/fs-plus/node_modules/rimraf/rimraf.js\n1978902: src/compile-support/babel.js\n1853312: node_modules/fs-plus/node_modules/async/package.json\n1854649: node_modules/fs-plus/node_modules/async/lib/async.js\n1884050: node_modules/fs-plus/node_modules/mkdirp/package.json\n1885107: node_modules/fs-plus/node_modules/mkdirp/index.js\n1887478: node_modules/fs-plus/node_modules/rimraf/package.json\n1889121: node_modules/fs-plus/node_modules/rimraf/rimraf.js\n1853312: node_modules/fs-plus/node_modules/async/package.json\n1980527: static/babelrc.json\n1854649: node_modules/fs-plus/node_modules/async/lib/async.js\n1980671: src/compile-support/coffee-script.js\n1981853: src/compile-support/typescript.js\n1978902: src/compile-support/babel.js\n1983139: node_modules/underscore/package.json\n1985095: node_modules/underscore/underscore.js\n1884050: node_modules/fs-plus/node_modules/mkdirp/package.json\n1885107: node_modules/fs-plus/node_modules/mkdirp/index.js\n1887478: node_modules/fs-plus/node_modules/rimraf/package.json\n1889121: node_modules/fs-plus/node_modules/rimraf/rimraf.js\n1978902: src/compile-support/babel.js\n1980527: static/babelrc.json\n1980671: src/compile-support/coffee-script.js\n1981853: src/compile-support/typescript.js\n2038014: node_modules/source-map-support/package.json\n1983139: node_modules/underscore/package.json\n2039642: node_modules/source-map-support/source-map-support.js\n1985095: node_modules/underscore/underscore.js\n2054178: node_modules/source-map-support/node_modules/source-map/package.json\n2057217: node_modules/source-map-support/node_modules/source-map/lib/source-map.js\n1980527: static/babelrc.json\n2057643: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-generator.js\n1980671: src/compile-support/coffee-script.js\n2070902: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/package.json\n1981853: src/compile-support/typescript.js\n2072148: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/amdefine.js\n1983139: node_modules/underscore/package.json\n1985095: node_modules/underscore/underscore.js\n2082064: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64-vlq.js\n2038014: node_modules/source-map-support/package.json\n2039642: node_modules/source-map-support/source-map-support.js\n2086956: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64.js\n2054178: node_modules/source-map-support/node_modules/source-map/package.json\n2057217: node_modules/source-map-support/node_modules/source-map/lib/source-map.js\n2057643: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-generator.js\n2088093: node_modules/source-map-support/node_modules/source-map/lib/source-map/util.js\n2070902: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/package.json\n2072148: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/amdefine.js\n2093422: node_modules/source-map-support/node_modules/source-map/lib/source-map/array-set.js\n2096140: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-consumer.js\n2038014: node_modules/source-map-support/package.json\n2039642: node_modules/source-map-support/source-map-support.js\n2113947: node_modules/source-map-support/node_modules/source-map/lib/source-map/binary-search.js\n2054178: node_modules/source-map-support/node_modules/source-map/package.json\n2117157: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-node.js\n2082064: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64-vlq.js\n2057217: node_modules/source-map-support/node_modules/source-map/lib/source-map.js\n2057643: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-generator.js\n2086956: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64.js\n2070902: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/package.json\n2072148: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/amdefine.js\n2851648: src/module-cache.js\n2088093: node_modules/source-map-support/node_modules/source-map/lib/source-map/util.js\n2093422: node_modules/source-map-support/node_modules/source-map/lib/source-map/array-set.js\n2866883: node_modules/semver/package.json\n2868219: node_modules/semver/semver.js\n2096140: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-consumer.js\n2082064: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64-vlq.js\n2113947: node_modules/source-map-support/node_modules/source-map/lib/source-map/binary-search.js\n2086956: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64.js\n2117157: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-node.js\n2088093: node_modules/source-map-support/node_modules/source-map/lib/source-map/util.js\n956764: package.json\n2851648: src/module-cache.js\n2093422: node_modules/source-map-support/node_modules/source-map/lib/source-map/array-set.js\n2096140: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-consumer.js\n2866883: node_modules/semver/package.json\n2868219: node_modules/semver/semver.js\n2113947: node_modules/source-map-support/node_modules/source-map/lib/source-map/binary-search.js\n2117157: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-node.js\n2851648: src/module-cache.js\n956764: package.json\n2866883: node_modules/semver/package.json\n2868219: node_modules/semver/semver.js\n956764: package.json\n2397840: node_modules/season/lib/cson.js\n94886363: src/secondary-window-bootstrap.js\n2901212: node_modules/bluebird/js/main/bluebird.js\n2901506: node_modules/bluebird/js/main/promise.js\n2926597: node_modules/bluebird/js/main/util.js\n2935230: node_modules/bluebird/js/main/es5.js\n2397840: node_modules/season/lib/cson.js\n2937208: node_modules/bluebird/js/main/async.js\n2925857: src/window-bootstrap.js\n2941161: node_modules/bluebird/js/main/schedule.js\n2901212: node_modules/bluebird/js/main/bluebird.js\n2901506: node_modules/bluebird/js/main/promise.js\n2942440: node_modules/bluebird/js/main/queue.js\n2944794: node_modules/bluebird/js/main/errors.js\n2948412: node_modules/bluebird/js/main/thenables.js\n2950756: node_modules/bluebird/js/main/promise_array.js\n2926597: node_modules/bluebird/js/main/util.js\n2954903: node_modules/bluebird/js/main/captured_trace.js\n2935230: node_modules/bluebird/js/main/es5.js\n2397840: node_modules/season/lib/cson.js\n2937208: node_modules/bluebird/js/main/async.js\n94886363: src/secondary-window-bootstrap.js\n2941161: node_modules/bluebird/js/main/schedule.js\n2901212: node_modules/bluebird/js/main/bluebird.js\n2942440: node_modules/bluebird/js/main/queue.js\n2901506: node_modules/bluebird/js/main/promise.js\n2969949: node_modules/bluebird/js/main/debuggability.js\n2944794: node_modules/bluebird/js/main/errors.js\n2975097: node_modules/bluebird/js/main/context.js\n2948412: node_modules/bluebird/js/main/thenables.js\n2976035: node_modules/bluebird/js/main/catch_filter.js\n2950756: node_modules/bluebird/js/main/promise_array.js\n2978109: node_modules/bluebird/js/main/promise_resolver.js\n2926597: node_modules/bluebird/js/main/util.js\n2954903: node_modules/bluebird/js/main/captured_trace.js\n2981972: node_modules/bluebird/js/main/progress.js\n2935230: node_modules/bluebird/js/main/es5.js\n2984464: node_modules/bluebird/js/main/method.js\n2985800: node_modules/bluebird/js/main/bind.js\n2987812: node_modules/bluebird/js/main/finally.js\n2937208: node_modules/bluebird/js/main/async.js\n2990436: node_modules/bluebird/js/main/direct_resolve.js\n2991902: node_modules/bluebird/js/main/synchronous_inspection.js\n2941161: node_modules/bluebird/js/main/schedule.js\n2942440: node_modules/bluebird/js/main/queue.js\n2994543: node_modules/bluebird/js/main/join.js\n2969949: node_modules/bluebird/js/main/debuggability.js\n2998433: node_modules/bluebird/js/main/map.js\n2944794: node_modules/bluebird/js/main/errors.js\n2975097: node_modules/bluebird/js/main/context.js\n3002811: node_modules/bluebird/js/main/cancel.js\n2948412: node_modules/bluebird/js/main/thenables.js\n2976035: node_modules/bluebird/js/main/catch_filter.js\n3004209: node_modules/bluebird/js/main/using.js\n2950756: node_modules/bluebird/js/main/promise_array.js\n2978109: node_modules/bluebird/js/main/promise_resolver.js\n2954903: node_modules/bluebird/js/main/captured_trace.js\n3011204: node_modules/bluebird/js/main/generators.js\n2981972: node_modules/bluebird/js/main/progress.js\n2984464: node_modules/bluebird/js/main/method.js\n3015905: node_modules/bluebird/js/main/nodeify.js\n2985800: node_modules/bluebird/js/main/bind.js\n3017541: node_modules/bluebird/js/main/call_get.js\n2987812: node_modules/bluebird/js/main/finally.js\n3021885: node_modules/bluebird/js/main/props.js\n2990436: node_modules/bluebird/js/main/direct_resolve.js\n2991902: node_modules/bluebird/js/main/synchronous_inspection.js\n3024057: node_modules/bluebird/js/main/race.js\n2994543: node_modules/bluebird/js/main/join.js\n3025282: node_modules/bluebird/js/main/reduce.js\n2969949: node_modules/bluebird/js/main/debuggability.js\n2998433: node_modules/bluebird/js/main/map.js\n3030305: node_modules/bluebird/js/main/settle.js\n2975097: node_modules/bluebird/js/main/context.js\n3031473: node_modules/bluebird/js/main/some.js\n2976035: node_modules/bluebird/js/main/catch_filter.js\n3002811: node_modules/bluebird/js/main/cancel.js\n3004209: node_modules/bluebird/js/main/using.js\n2978109: node_modules/bluebird/js/main/promise_resolver.js\n3034851: node_modules/bluebird/js/main/promisify.js\n2981972: node_modules/bluebird/js/main/progress.js\n3011204: node_modules/bluebird/js/main/generators.js\n2984464: node_modules/bluebird/js/main/method.js\n3046413: node_modules/bluebird/js/main/any.js\n3015905: node_modules/bluebird/js/main/nodeify.js\n3046834: node_modules/bluebird/js/main/each.js\n2985800: node_modules/bluebird/js/main/bind.js\n3047132: node_modules/bluebird/js/main/timers.js\n3017541: node_modules/bluebird/js/main/call_get.js\n2987812: node_modules/bluebird/js/main/finally.js\n3048873: node_modules/bluebird/js/main/filter.js\n2990436: node_modules/bluebird/js/main/direct_resolve.js\n3021885: node_modules/bluebird/js/main/props.js\n2991902: node_modules/bluebird/js/main/synchronous_inspection.js\n2994543: node_modules/bluebird/js/main/join.js\n3024057: node_modules/bluebird/js/main/race.js\n3025282: node_modules/bluebird/js/main/reduce.js\n2998433: node_modules/bluebird/js/main/map.js\n2937208: node_modules/bluebird/js/main/async.js\n3049187: node_modules/babel-core/package.json\n3030305: node_modules/bluebird/js/main/settle.js\n3002811: node_modules/bluebird/js/main/cancel.js\n3031473: node_modules/bluebird/js/main/some.js\n3004209: node_modules/bluebird/js/main/using.js\n3034851: node_modules/bluebird/js/main/promisify.js\n3011204: node_modules/bluebird/js/main/generators.js\n1973294: src/compile-cache.js\n3015905: node_modules/bluebird/js/main/nodeify.js\n3046413: node_modules/bluebird/js/main/any.js\n3046834: node_modules/bluebird/js/main/each.js\n3017541: node_modules/bluebird/js/main/call_get.js\n3047132: node_modules/bluebird/js/main/timers.js\n3021885: node_modules/bluebird/js/main/props.js\n2901506: node_modules/bluebird/js/main/promise.js\n3048873: node_modules/bluebird/js/main/filter.js\n3024057: node_modules/bluebird/js/main/race.js\n3025282: node_modules/bluebird/js/main/reduce.js\n2937208: node_modules/bluebird/js/main/async.js\n3030305: node_modules/bluebird/js/main/settle.js\n2901212: node_modules/bluebird/js/main/bluebird.js\n3049187: node_modules/babel-core/package.json\n3031473: node_modules/bluebird/js/main/some.js\n2926597: node_modules/bluebird/js/main/util.js\n3034851: node_modules/bluebird/js/main/promisify.js\n3046413: node_modules/bluebird/js/main/any.js\n3052709: src/window.js\n3046834: node_modules/bluebird/js/main/each.js\n3053190: src/nylas-env.js\n1973294: src/compile-cache.js\n3047132: node_modules/bluebird/js/main/timers.js\n3048873: node_modules/bluebird/js/main/filter.js\n2901506: node_modules/bluebird/js/main/promise.js\n2937208: node_modules/bluebird/js/main/async.js\n3049187: node_modules/babel-core/package.json\n2901212: node_modules/bluebird/js/main/bluebird.js\n2926597: node_modules/bluebird/js/main/util.js\n1973294: src/compile-cache.js\n2266273: node_modules/emissary/lib/emissary.js\n2266555: node_modules/emissary/lib/helpers.js\n3052709: src/window.js\n2268129: node_modules/emissary/lib/behavior.js\n2274072: node_modules/emissary/node_modules/underscore-plus/lib/underscore-plus.js\n2901506: node_modules/bluebird/js/main/promise.js\n3053190: src/nylas-env.js\n2290574: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/underscore.js\n2901212: node_modules/bluebird/js/main/bluebird.js\n2926597: node_modules/bluebird/js/main/util.js\n2266273: node_modules/emissary/lib/emissary.js\n3052709: src/window.js\n2266555: node_modules/emissary/lib/helpers.js\n3053190: src/nylas-env.js\n2268129: node_modules/emissary/lib/behavior.js\n2274072: node_modules/emissary/node_modules/underscore-plus/lib/underscore-plus.js\n2337929: node_modules/property-accessors/lib/property-accessors.js\n2290574: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/underscore.js\n2369463: node_modules/emissary/node_modules/mixto/lib/mixin.js\n2343179: node_modules/emissary/lib/signal.js\n2352520: node_modules/emissary/lib/emitter.js\n2370863: node_modules/emissary/lib/subscriber.js\n2374948: node_modules/emissary/lib/subscription.js\n2266273: node_modules/emissary/lib/emissary.js\n2266555: node_modules/emissary/lib/helpers.js\n2268129: node_modules/emissary/lib/behavior.js\n2274072: node_modules/emissary/node_modules/underscore-plus/lib/underscore-plus.js\n2378208: node_modules/event-kit/lib/event-kit.js\n2337929: node_modules/property-accessors/lib/property-accessors.js\n2378397: node_modules/event-kit/lib/emitter.js\n2369463: node_modules/emissary/node_modules/mixto/lib/mixin.js\n2290574: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/underscore.js\n2382101: node_modules/event-kit/lib/disposable.js\n2343179: node_modules/emissary/lib/signal.js\n2394595: node_modules/event-kit/lib/composite-disposable.js\n3088814: node_modules/coffeestack/index.js\n2352520: node_modules/emissary/lib/emitter.js\n3093983: src/window-event-handler.js\n2370863: node_modules/emissary/lib/subscriber.js\n2374948: node_modules/emissary/lib/subscription.js\n2378208: node_modules/event-kit/lib/event-kit.js\n2337929: node_modules/property-accessors/lib/property-accessors.js\n3106501: src/styles-element.js\n2378397: node_modules/event-kit/lib/emitter.js\n2369463: node_modules/emissary/node_modules/mixto/lib/mixin.js\n2382101: node_modules/event-kit/lib/disposable.js\n94901394: src/store-registry.js\n2343179: node_modules/emissary/lib/signal.js\n2394595: node_modules/event-kit/lib/composite-disposable.js\n3264167: src/serializable-registry.js\n3088814: node_modules/coffeestack/index.js\n2352520: node_modules/emissary/lib/emitter.js\n3093983: src/window-event-handler.js\n2207382: src/flux/models/utils.js\n2370863: node_modules/emissary/lib/subscriber.js\n2374948: node_modules/emissary/lib/subscription.js\n8174990: node_modules/moment-timezone/package.json\n8177056: node_modules/moment-timezone/index.js\n8177170: node_modules/moment-timezone/moment-timezone.js\n2378208: node_modules/event-kit/lib/event-kit.js\n2378397: node_modules/event-kit/lib/emitter.js\n3974612: node_modules/moment/moment.js\n3106501: src/styles-element.js\n2382101: node_modules/event-kit/lib/disposable.js\n2394595: node_modules/event-kit/lib/composite-disposable.js\n94901394: src/store-registry.js\n3088814: node_modules/coffeestack/index.js\n3264167: src/serializable-registry.js\n3093983: src/window-event-handler.js\n2207382: src/flux/models/utils.js\n8174990: node_modules/moment-timezone/package.json\n8177056: node_modules/moment-timezone/index.js\n8177170: node_modules/moment-timezone/moment-timezone.js\n3106501: src/styles-element.js\n3974612: node_modules/moment/moment.js\n94901394: src/store-registry.js\n3264167: src/serializable-registry.js\n2207382: src/flux/models/utils.js\n8174990: node_modules/moment-timezone/package.json\n8177056: node_modules/moment-timezone/index.js\n8177170: node_modules/moment-timezone/moment-timezone.js\n3974612: node_modules/moment/moment.js\n8190929: node_modules/moment-timezone/data/packed/latest.json\n8190929: node_modules/moment-timezone/data/packed/latest.json\n8190929: node_modules/moment-timezone/data/packed/latest.json\n3261374: src/task-registry.js\n3271782: src/database-object-registry.js\n3113921: src/flux/errors.js\n3261374: src/task-registry.js\n3271782: src/database-object-registry.js\n3113921: src/flux/errors.js\n3261374: src/task-registry.js\n1918089: src/error-logger.js\n1918089: src/error-logger.js\n3271782: src/database-object-registry.js\n3113921: src/flux/errors.js\n1928640: src/error-logger-extensions/nylas-private-error-reporter.js\n1933337: node_modules/raven/package.json\n1928640: src/error-logger-extensions/nylas-private-error-reporter.js\n1935222: node_modules/raven/index.js\n1935631: node_modules/raven/lib/client.js\n1933337: node_modules/raven/package.json\n1935222: node_modules/raven/index.js\n1941308: node_modules/raven/lib/parsers.js\n1935631: node_modules/raven/lib/client.js\n1946217: node_modules/raven/node_modules/cookie/index.js\n1948116: node_modules/raven/lib/utils.js\n1941308: node_modules/raven/lib/parsers.js\n1953762: node_modules/raven/lib/transports.js\n1946217: node_modules/raven/node_modules/cookie/index.js\n1948116: node_modules/raven/lib/utils.js\n1953762: node_modules/raven/lib/transports.js\n1918089: src/error-logger.js\n1928640: src/error-logger-extensions/nylas-private-error-reporter.js\n1933337: node_modules/raven/package.json\n1935222: node_modules/raven/index.js\n1935631: node_modules/raven/lib/client.js\n1941308: node_modules/raven/lib/parsers.js\n1946217: node_modules/raven/node_modules/cookie/index.js\n1948116: node_modules/raven/lib/utils.js\n1953762: node_modules/raven/lib/transports.js\n1957254: node_modules/raven/node_modules/lsmod/index.js\n1957254: node_modules/raven/node_modules/lsmod/index.js\n1960042: node_modules/raven/node_modules/stack-trace/lib/stack-trace.js\n1960042: node_modules/raven/node_modules/stack-trace/lib/stack-trace.js\n1964629: node_modules/node-uuid/uuid.js\n1964629: node_modules/node-uuid/uuid.js\n1933337: node_modules/raven/package.json\n1933337: node_modules/raven/package.json\n1972642: node_modules/raven/lib/middleware/connect.js\n1972642: node_modules/raven/lib/middleware/connect.js\n1957254: node_modules/raven/node_modules/lsmod/index.js\n1960042: node_modules/raven/node_modules/stack-trace/lib/stack-trace.js\n1964629: node_modules/node-uuid/uuid.js\n1933337: node_modules/raven/package.json\n1972642: node_modules/raven/lib/middleware/connect.js\n2238228: src/config.js\n2238228: src/config.js\n2261295: src/config-utils.js\n2261295: src/config-utils.js\n2444020: node_modules/pathwatcher/lib/main.js\n2444020: node_modules/pathwatcher/lib/main.js\n2450494: node_modules/pathwatcher/lib/file.js\n2450494: node_modules/pathwatcher/lib/file.js\n2530619: node_modules/pathwatcher/lib/directory.js\n2530619: node_modules/pathwatcher/lib/directory.js\n2572883: src/color.js\n2572883: src/color.js\n2238228: src/config.js\n3115571: src/keymap-manager.js\n3115571: src/keymap-manager.js\n2261295: src/config-utils.js\n72365667: node_modules/mousetrap/mousetrap.js\n72365667: node_modules/mousetrap/mousetrap.js\n2444020: node_modules/pathwatcher/lib/main.js\n3122755: src/command-registry.js\n3122755: src/command-registry.js\n3153088: src/package-manager.js\n3153088: src/package-manager.js\n2450494: node_modules/pathwatcher/lib/file.js\n3176053: node_modules/q/q.js\n3176053: node_modules/q/q.js\n2530619: node_modules/pathwatcher/lib/directory.js\n2572883: src/color.js\n3115571: src/keymap-manager.js\n72365667: node_modules/mousetrap/mousetrap.js\n3122755: src/command-registry.js\n3176053: node_modules/q/q.js\n3176053: node_modules/q/q.js\n3153088: src/package-manager.js\n3153088: src/package-manager.js\n3153088: src/package-manager.js\n3053190: src/nylas-env.js\n3053190: src/nylas-env.js\n3176053: node_modules/q/q.js\n94886363: src/secondary-window-bootstrap.js\n2925857: src/window-bootstrap.js\n3238845: src/package.js\n3238845: src/package.js\n2407083: node_modules/async/lib/async.js\n2407083: node_modules/async/lib/async.js\n3176053: node_modules/q/q.js\n3274725: src/theme-package.js\n3274725: src/theme-package.js\n3276790: src/flux/stores/database-store.js\n3276790: src/flux/stores/database-store.js\n3153088: src/package-manager.js\n3053190: src/nylas-env.js\n94886363: src/secondary-window-bootstrap.js\n2405266: node_modules/async/package.json\n2405266: node_modules/async/package.json\n3305440: node_modules/sqlite3/package.json\n3305440: node_modules/sqlite3/package.json\n3317559: node_modules/sqlite3/lib/sqlite3.js\n3317559: node_modules/sqlite3/lib/sqlite3.js\n3323109: node_modules/node-pre-gyp/lib/node-pre-gyp.js\n3323109: node_modules/node-pre-gyp/lib/node-pre-gyp.js\n3238845: src/package.js\n74661469: node_modules/nopt/lib/nopt.js\n74661469: node_modules/nopt/lib/nopt.js\n41220040: node_modules/abbrev/abbrev.js\n41220040: node_modules/abbrev/abbrev.js\n2407083: node_modules/async/lib/async.js\n74736261: node_modules/npmlog/log.js\n74736261: node_modules/npmlog/log.js\n41757580: node_modules/are-we-there-yet/index.js\n41757580: node_modules/are-we-there-yet/index.js\n41760200: node_modules/are-we-there-yet/tracker-group.js\n41760200: node_modules/are-we-there-yet/tracker-group.js\n41759926: node_modules/are-we-there-yet/tracker-base.js\n41759926: node_modules/are-we-there-yet/tracker-base.js\n41764374: node_modules/are-we-there-yet/tracker.js\n41764374: node_modules/are-we-there-yet/tracker.js\n41763431: node_modules/are-we-there-yet/tracker-stream.js\n41763431: node_modules/are-we-there-yet/tracker-stream.js\n3327439: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/readable.js\n3327439: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/readable.js\n3327909: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_readable.js\n3274725: src/theme-package.js\n3327909: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_readable.js\n3276790: src/flux/stores/database-store.js\n56978227: node_modules/falafel/node_modules/isarray/index.js\n56978227: node_modules/falafel/node_modules/isarray/index.js\n56174974: node_modules/core-util-is/lib/util.js\n56174974: node_modules/core-util-is/lib/util.js\n3357009: node_modules/babel-core/node_modules/regenerator/node_modules/commoner/node_modules/glob/node_modules/inherits/inherits.js\n3357009: node_modules/babel-core/node_modules/regenerator/node_modules/commoner/node_modules/glob/node_modules/inherits/inherits.js\n3357051: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_writable.js\n2405266: node_modules/async/package.json\n3357051: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_writable.js\n3305440: node_modules/sqlite3/package.json\n3317559: node_modules/sqlite3/lib/sqlite3.js\n3370120: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_duplex.js\n3370120: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_duplex.js\n3372931: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_transform.js\n3323109: node_modules/node-pre-gyp/lib/node-pre-gyp.js\n3372931: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_transform.js\n3380281: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_passthrough.js\n74661469: node_modules/nopt/lib/nopt.js\n3380281: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_passthrough.js\n56223160: node_modules/delegates/index.js\n56223160: node_modules/delegates/index.js\n41220040: node_modules/abbrev/abbrev.js\n57167856: node_modules/gauge/progress-bar.js\n57167856: node_modules/gauge/progress-bar.js\n74736261: node_modules/npmlog/log.js\n57204450: node_modules/has-unicode/index.js\n57204450: node_modules/has-unicode/index.js\n41738129: node_modules/ansi/lib/ansi.js\n41738129: node_modules/ansi/lib/ansi.js\n41757580: node_modules/are-we-there-yet/index.js\n41760200: node_modules/are-we-there-yet/tracker-group.js\n41759926: node_modules/are-we-there-yet/tracker-base.js\n41746103: node_modules/ansi/lib/newlines.js\n41746103: node_modules/ansi/lib/newlines.js\n41764374: node_modules/are-we-there-yet/tracker.js\n41763431: node_modules/are-we-there-yet/tracker-stream.js\n3327439: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/readable.js\n67599487: node_modules/lodash.pad/index.js\n67599487: node_modules/lodash.pad/index.js\n3327909: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_readable.js\n67593383: node_modules/lodash._baseslice/index.js\n67593383: node_modules/lodash._baseslice/index.js\n67646936: node_modules/lodash.tostring/index.js\n67646936: node_modules/lodash.tostring/index.js\n56978227: node_modules/falafel/node_modules/isarray/index.js\n56174974: node_modules/core-util-is/lib/util.js\n67615561: node_modules/lodash.padend/index.js\n67615561: node_modules/lodash.padend/index.js\n3357009: node_modules/babel-core/node_modules/regenerator/node_modules/commoner/node_modules/glob/node_modules/inherits/inherits.js\n3357051: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_writable.js\n67631576: node_modules/lodash.padstart/index.js\n67631576: node_modules/lodash.padstart/index.js\n3370120: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_duplex.js\n3372931: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_transform.js\n3380281: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/readable-stream/lib/_stream_passthrough.js\n56223160: node_modules/delegates/index.js\n57167856: node_modules/gauge/progress-bar.js\n3382008: node_modules/node-pre-gyp/lib/pre-binding.js\n3382008: node_modules/node-pre-gyp/lib/pre-binding.js\n3382833: node_modules/node-pre-gyp/lib/util/versioning.js\n57204450: node_modules/has-unicode/index.js\n3382833: node_modules/node-pre-gyp/lib/util/versioning.js\n41738129: node_modules/ansi/lib/ansi.js\n3396883: node_modules/node-pre-gyp/node_modules/semver/semver.js\n3396883: node_modules/node-pre-gyp/node_modules/semver/semver.js\n41746103: node_modules/ansi/lib/newlines.js\n67599487: node_modules/lodash.pad/index.js\n67593383: node_modules/lodash._baseslice/index.js\n3429402: node_modules/node-pre-gyp/lib/util/abi_crosswalk.json\n67646936: node_modules/lodash.tostring/index.js\n3429402: node_modules/node-pre-gyp/lib/util/abi_crosswalk.json\n3446169: node_modules/node-pre-gyp/package.json\n67615561: node_modules/lodash.padend/index.js\n3446169: node_modules/node-pre-gyp/package.json\n3305440: node_modules/sqlite3/package.json\n67631576: node_modules/lodash.padstart/index.js\n3305440: node_modules/sqlite3/package.json\n3382008: node_modules/node-pre-gyp/lib/pre-binding.js\n3382833: node_modules/node-pre-gyp/lib/util/versioning.js\n3396883: node_modules/node-pre-gyp/node_modules/semver/semver.js\n3449351: src/flux/models/model.js\n3449351: src/flux/models/model.js\n3454751: src/flux/attributes.js\n3454751: src/flux/attributes.js\n3458403: src/flux/attributes/matcher.js\n3458403: src/flux/attributes/matcher.js\n3429402: node_modules/node-pre-gyp/lib/util/abi_crosswalk.json\n3446169: node_modules/node-pre-gyp/package.json\n3305440: node_modules/sqlite3/package.json\n3470821: src/flux/attributes/sort-order.js\n3470821: src/flux/attributes/sort-order.js\n3471723: src/flux/attributes/attribute.js\n3471723: src/flux/attributes/attribute.js\n3474483: src/flux/attributes/attribute-number.js\n3474483: src/flux/attributes/attribute-number.js\n3477709: src/flux/attributes/attribute-string.js\n3477709: src/flux/attributes/attribute-string.js\n3479579: src/flux/attributes/attribute-object.js\n3479579: src/flux/attributes/attribute-object.js\n3481139: src/flux/attributes/attribute-boolean.js\n3481139: src/flux/attributes/attribute-boolean.js\n3482541: src/flux/attributes/attribute-datetime.js\n3482541: src/flux/attributes/attribute-datetime.js\n3485716: src/flux/attributes/attribute-collection.js\n3485716: src/flux/attributes/attribute-collection.js\n3490067: src/flux/attributes/attribute-joined-data.js\n3490067: src/flux/attributes/attribute-joined-data.js\n3492659: src/flux/attributes/attribute-serverid.js\n3492659: src/flux/attributes/attribute-serverid.js\n3494243: src/flux/actions.js\n3449351: src/flux/models/model.js\n3494243: src/flux/actions.js\n3509768: node_modules/reflux/package.json\n3454751: src/flux/attributes.js\n3509768: node_modules/reflux/package.json\n3511690: node_modules/reflux/src/index.js\n3458403: src/flux/attributes/matcher.js\n3511690: node_modules/reflux/src/index.js\n3513193: node_modules/reflux/src/ListenerMethods.js\n3513193: node_modules/reflux/src/ListenerMethods.js\n3519984: node_modules/reflux/src/utils.js\n3519984: node_modules/reflux/src/utils.js\n3521383: node_modules/reflux/node_modules/eventemitter3/index.js\n3521383: node_modules/reflux/node_modules/eventemitter3/index.js\n3470821: src/flux/attributes/sort-order.js\n3527461: node_modules/reflux/src/joins.js\n3471723: src/flux/attributes/attribute.js\n3527461: node_modules/reflux/src/joins.js\n3530350: node_modules/reflux/src/createStore.js\n3530350: node_modules/reflux/src/createStore.js\n3474483: src/flux/attributes/attribute-number.js\n3531848: node_modules/reflux/src/Keep.js\n3477709: src/flux/attributes/attribute-string.js\n3531848: node_modules/reflux/src/Keep.js\n3479579: src/flux/attributes/attribute-object.js\n3532111: node_modules/reflux/src/PublisherMethods.js\n3532111: node_modules/reflux/src/PublisherMethods.js\n3481139: src/flux/attributes/attribute-boolean.js\n3534287: node_modules/reflux/src/createAction.js\n3534287: node_modules/reflux/src/createAction.js\n3482541: src/flux/attributes/attribute-datetime.js\n3535424: node_modules/reflux/src/connect.js\n3535424: node_modules/reflux/src/connect.js\n3485716: src/flux/attributes/attribute-collection.js\n3536186: node_modules/reflux/src/ListenerMixin.js\n3536186: node_modules/reflux/src/ListenerMixin.js\n3490067: src/flux/attributes/attribute-joined-data.js\n3536620: node_modules/reflux/src/listenTo.js\n3536620: node_modules/reflux/src/listenTo.js\n3538124: node_modules/reflux/src/listenToMany.js\n3492659: src/flux/attributes/attribute-serverid.js\n3538124: node_modules/reflux/src/listenToMany.js\n3494243: src/flux/actions.js\n3509768: node_modules/reflux/package.json\n3511690: node_modules/reflux/src/index.js\n3513193: node_modules/reflux/src/ListenerMethods.js\n3519984: node_modules/reflux/src/utils.js\n3521383: node_modules/reflux/node_modules/eventemitter3/index.js\n3527461: node_modules/reflux/src/joins.js\n3530350: node_modules/reflux/src/createStore.js\n3531848: node_modules/reflux/src/Keep.js\n3532111: node_modules/reflux/src/PublisherMethods.js\n3534287: node_modules/reflux/src/createAction.js\n3535424: node_modules/reflux/src/connect.js\n3539478: src/flux/models/query.js\n3539478: src/flux/models/query.js\n3536186: node_modules/reflux/src/ListenerMixin.js\n3536620: node_modules/reflux/src/listenTo.js\n3538124: node_modules/reflux/src/listenToMany.js\n3553781: src/flux/models/query-range.js\n3553781: src/flux/models/query-range.js\n3556781: src/global/nylas-store.js\n3556781: src/global/nylas-store.js\n3557281: src/flux/modules/reflux-coffee.js\n3557281: src/flux/modules/reflux-coffee.js\n3564303: node_modules/underscore.string/package.json\n3564303: node_modules/underscore.string/package.json\n3126085: node_modules/underscore.string/index.js\n3126085: node_modules/underscore.string/index.js\n3130619: node_modules/underscore.string/isBlank.js\n3130619: node_modules/underscore.string/isBlank.js\n3130755: node_modules/underscore.string/helper/makeString.js\n3130755: node_modules/underscore.string/helper/makeString.js\n3130916: node_modules/underscore.string/stripTags.js\n3130916: node_modules/underscore.string/stripTags.js\n3131065: node_modules/underscore.string/capitalize.js\n3131065: node_modules/underscore.string/capitalize.js\n3131341: node_modules/underscore.string/decapitalize.js\n3131341: node_modules/underscore.string/decapitalize.js\n3131518: node_modules/underscore.string/chop.js\n3131518: node_modules/underscore.string/chop.js\n3131710: node_modules/underscore.string/trim.js\n3131710: node_modules/underscore.string/trim.js\n3132143: node_modules/underscore.string/helper/defaultToWhiteSpace.js\n3132143: node_modules/underscore.string/helper/defaultToWhiteSpace.js\n3132413: node_modules/underscore.string/helper/escapeRegExp.js\n3132413: node_modules/underscore.string/helper/escapeRegExp.js\n3132577: node_modules/underscore.string/clean.js\n3132577: node_modules/underscore.string/clean.js\n3132693: node_modules/underscore.string/cleanDiacritics.js\n3132693: node_modules/underscore.string/cleanDiacritics.js\n3133280: node_modules/underscore.string/count.js\n3133280: node_modules/underscore.string/count.js\n3133530: node_modules/underscore.string/chars.js\n3133530: node_modules/underscore.string/chars.js\n3539478: src/flux/models/query.js\n3133658: node_modules/underscore.string/swapCase.js\n3133883: node_modules/underscore.string/escapeHTML.js\n3133658: node_modules/underscore.string/swapCase.js\n3134273: node_modules/underscore.string/helper/escapeChars.js\n3133883: node_modules/underscore.string/escapeHTML.js\n3134692: node_modules/underscore.string/unescapeHTML.js\n3553781: src/flux/models/query-range.js\n3134273: node_modules/underscore.string/helper/escapeChars.js\n3135351: node_modules/underscore.string/helper/htmlEntities.js\n3134692: node_modules/underscore.string/unescapeHTML.js\n3135655: node_modules/underscore.string/splice.js\n3135351: node_modules/underscore.string/helper/htmlEntities.js\n3556781: src/global/nylas-store.js\n3135836: node_modules/underscore.string/insert.js\n3135655: node_modules/underscore.string/splice.js\n3135961: node_modules/underscore.string/replaceAll.js\n3135836: node_modules/underscore.string/insert.js\n3136217: node_modules/underscore.string/include.js\n3557281: src/flux/modules/reflux-coffee.js\n3135961: node_modules/underscore.string/replaceAll.js\n3136402: node_modules/underscore.string/join.js\n3136217: node_modules/underscore.string/include.js\n3136622: node_modules/underscore.string/lines.js\n3136402: node_modules/underscore.string/join.js\n3136734: node_modules/underscore.string/dedent.js\n3136622: node_modules/underscore.string/lines.js\n3136734: node_modules/underscore.string/dedent.js\n3564303: node_modules/underscore.string/package.json\n3137344: node_modules/underscore.string/reverse.js\n3126085: node_modules/underscore.string/index.js\n3137461: node_modules/underscore.string/startsWith.js\n3137811: node_modules/underscore.string/helper/toPositive.js\n3130619: node_modules/underscore.string/isBlank.js\n3137903: node_modules/underscore.string/endsWith.js\n3130755: node_modules/underscore.string/helper/makeString.js\n3137344: node_modules/underscore.string/reverse.js\n3138345: node_modules/underscore.string/pred.js\n3130916: node_modules/underscore.string/stripTags.js\n3138460: node_modules/underscore.string/helper/adjacent.js\n3137461: node_modules/underscore.string/startsWith.js\n3131065: node_modules/underscore.string/capitalize.js\n3137811: node_modules/underscore.string/helper/toPositive.js\n3131341: node_modules/underscore.string/decapitalize.js\n3138722: node_modules/underscore.string/succ.js\n3137903: node_modules/underscore.string/endsWith.js\n3131518: node_modules/underscore.string/chop.js\n3138836: node_modules/underscore.string/titleize.js\n3131710: node_modules/underscore.string/trim.js\n3138345: node_modules/underscore.string/pred.js\n3139043: node_modules/underscore.string/camelize.js\n3132143: node_modules/underscore.string/helper/defaultToWhiteSpace.js\n3138460: node_modules/underscore.string/helper/adjacent.js\n3139364: node_modules/underscore.string/underscored.js\n3132413: node_modules/underscore.string/helper/escapeRegExp.js\n3139540: node_modules/underscore.string/dasherize.js\n3132577: node_modules/underscore.string/clean.js\n3139703: node_modules/underscore.string/classify.js\n3132693: node_modules/underscore.string/cleanDiacritics.js\n3139981: node_modules/underscore.string/humanize.js\n3138722: node_modules/underscore.string/succ.js\n3140227: node_modules/underscore.string/ltrim.js\n3138836: node_modules/underscore.string/titleize.js\n3140651: node_modules/underscore.string/rtrim.js\n3133280: node_modules/underscore.string/count.js\n3139043: node_modules/underscore.string/camelize.js\n3141074: node_modules/underscore.string/truncate.js\n3133530: node_modules/underscore.string/chars.js\n3141347: node_modules/underscore.string/prune.js\n3139364: node_modules/underscore.string/underscored.js\n3133658: node_modules/underscore.string/swapCase.js\n3139540: node_modules/underscore.string/dasherize.js\n3142255: node_modules/underscore.string/words.js\n3139703: node_modules/underscore.string/classify.js\n3142463: node_modules/underscore.string/pad.js\n3133883: node_modules/underscore.string/escapeHTML.js\n3139981: node_modules/underscore.string/humanize.js\n3143150: node_modules/underscore.string/helper/strRepeat.js\n3134273: node_modules/underscore.string/helper/escapeChars.js\n3143345: node_modules/underscore.string/lpad.js\n3140227: node_modules/underscore.string/ltrim.js\n3143466: node_modules/underscore.string/rpad.js\n3134692: node_modules/underscore.string/unescapeHTML.js\n3140651: node_modules/underscore.string/rtrim.js\n3143596: node_modules/underscore.string/lrpad.js\n3135351: node_modules/underscore.string/helper/htmlEntities.js\n3141074: node_modules/underscore.string/truncate.js\n3135655: node_modules/underscore.string/splice.js\n3141347: node_modules/underscore.string/prune.js\n3143726: node_modules/underscore.string/sprintf.js\n3135836: node_modules/underscore.string/insert.js\n4476299: node_modules/juice/node_modules/util-deprecate/node.js\n3142255: node_modules/underscore.string/words.js\n3135961: node_modules/underscore.string/replaceAll.js\n3142463: node_modules/underscore.string/pad.js\n89722232: node_modules/underscore.string/node_modules/sprintf-js/src/sprintf.js\n3136217: node_modules/underscore.string/include.js\n3143150: node_modules/underscore.string/helper/strRepeat.js\n3136402: node_modules/underscore.string/join.js\n3143345: node_modules/underscore.string/lpad.js\n3136622: node_modules/underscore.string/lines.js\n3143466: node_modules/underscore.string/rpad.js\n3136734: node_modules/underscore.string/dedent.js\n3143923: node_modules/underscore.string/vsprintf.js\n3143596: node_modules/underscore.string/lrpad.js\n3137344: node_modules/underscore.string/reverse.js\n3144122: node_modules/underscore.string/toNumber.js\n3143726: node_modules/underscore.string/sprintf.js\n3137461: node_modules/underscore.string/startsWith.js\n3144317: node_modules/underscore.string/numberFormat.js\n4476299: node_modules/juice/node_modules/util-deprecate/node.js\n3137811: node_modules/underscore.string/helper/toPositive.js\n3144704: node_modules/underscore.string/strRight.js\n3137903: node_modules/underscore.string/endsWith.js\n3144959: node_modules/underscore.string/strRightBack.js\n89722232: node_modules/underscore.string/node_modules/sprintf-js/src/sprintf.js\n3138345: node_modules/underscore.string/pred.js\n3145222: node_modules/underscore.string/strLeft.js\n3138460: node_modules/underscore.string/helper/adjacent.js\n3145454: node_modules/underscore.string/strLeftBack.js\n3145682: node_modules/underscore.string/toSentence.js\n3143923: node_modules/underscore.string/vsprintf.js\n3138722: node_modules/underscore.string/succ.js\n3146093: node_modules/underscore.string/toSentenceSerial.js\n3144122: node_modules/underscore.string/toNumber.js\n3144317: node_modules/underscore.string/numberFormat.js\n3138836: node_modules/underscore.string/titleize.js\n3146253: node_modules/underscore.string/slugify.js\n3144704: node_modules/underscore.string/strRight.js\n3139043: node_modules/underscore.string/camelize.js\n3146513: node_modules/underscore.string/surround.js\n3144959: node_modules/underscore.string/strRightBack.js\n3139364: node_modules/underscore.string/underscored.js\n3146610: node_modules/underscore.string/quote.js\n3145222: node_modules/underscore.string/strLeft.js\n3139540: node_modules/underscore.string/dasherize.js\n3146744: node_modules/underscore.string/unquote.js\n3145454: node_modules/underscore.string/strLeftBack.js\n3139703: node_modules/underscore.string/classify.js\n3146956: node_modules/underscore.string/repeat.js\n3145682: node_modules/underscore.string/toSentence.js\n3147436: node_modules/underscore.string/naturalCmp.js\n3139981: node_modules/underscore.string/humanize.js\n3146093: node_modules/underscore.string/toSentenceSerial.js\n3148137: node_modules/underscore.string/levenshtein.js\n3140227: node_modules/underscore.string/ltrim.js\n3140651: node_modules/underscore.string/rtrim.js\n3149427: node_modules/underscore.string/toBoolean.js\n3146253: node_modules/underscore.string/slugify.js\n3150095: node_modules/underscore.string/exports.js\n3141074: node_modules/underscore.string/truncate.js\n3150336: node_modules/underscore.string/wrap.js\n3146513: node_modules/underscore.string/surround.js\n3141347: node_modules/underscore.string/prune.js\n3146610: node_modules/underscore.string/quote.js\n3152857: node_modules/underscore.string/map.js\n3142255: node_modules/underscore.string/words.js\n3146744: node_modules/underscore.string/unquote.js\n3142463: node_modules/underscore.string/pad.js\n3146956: node_modules/underscore.string/repeat.js\n3143150: node_modules/underscore.string/helper/strRepeat.js\n3147436: node_modules/underscore.string/naturalCmp.js\n3143345: node_modules/underscore.string/lpad.js\n3148137: node_modules/underscore.string/levenshtein.js\n3568295: src/flux/coffee-helpers.js\n3143466: node_modules/underscore.string/rpad.js\n3149427: node_modules/underscore.string/toBoolean.js\n3143596: node_modules/underscore.string/lrpad.js\n3150095: node_modules/underscore.string/exports.js\n3143726: node_modules/underscore.string/sprintf.js\n3150336: node_modules/underscore.string/wrap.js\n4476299: node_modules/juice/node_modules/util-deprecate/node.js\n3152857: node_modules/underscore.string/map.js\n3570836: node_modules/promise-queue/package.json\n89722232: node_modules/underscore.string/node_modules/sprintf-js/src/sprintf.js\n3572457: node_modules/promise-queue/index.js\n3568295: src/flux/coffee-helpers.js\n3572560: node_modules/promise-queue/lib/index.js\n3143923: node_modules/underscore.string/vsprintf.js\n3144122: node_modules/underscore.string/toNumber.js\n3144317: node_modules/underscore.string/numberFormat.js\n3577195: src/priority-ui-coordinator.js\n3144704: node_modules/underscore.string/strRight.js\n3144959: node_modules/underscore.string/strRightBack.js\n3570836: node_modules/promise-queue/package.json\n3145222: node_modules/underscore.string/strLeft.js\n3579245: src/flux/stores/database-setup-query-builder.js\n3572457: node_modules/promise-queue/index.js\n3145454: node_modules/underscore.string/strLeftBack.js\n3145682: node_modules/underscore.string/toSentence.js\n3583641: src/flux/stores/database-change-record.js\n3572560: node_modules/promise-queue/lib/index.js\n3146093: node_modules/underscore.string/toSentenceSerial.js\n3585323: src/flux/stores/database-writer.js\n3146253: node_modules/underscore.string/slugify.js\n3577195: src/priority-ui-coordinator.js\n3146513: node_modules/underscore.string/surround.js\n3146610: node_modules/underscore.string/quote.js\n3146744: node_modules/underscore.string/unquote.js\n3146956: node_modules/underscore.string/repeat.js\n3579245: src/flux/stores/database-setup-query-builder.js\n3147436: node_modules/underscore.string/naturalCmp.js\n3148137: node_modules/underscore.string/levenshtein.js\n3583641: src/flux/stores/database-change-record.js\n3149427: node_modules/underscore.string/toBoolean.js\n3150095: node_modules/underscore.string/exports.js\n3585323: src/flux/stores/database-writer.js\n3150336: node_modules/underscore.string/wrap.js\n3152857: node_modules/underscore.string/map.js\n3568295: src/flux/coffee-helpers.js\n3599638: src/apm-wrapper.js\n3570836: node_modules/promise-queue/package.json\n3572457: node_modules/promise-queue/index.js\n3613961: src/buffered-process.js\n3572560: node_modules/promise-queue/lib/index.js\n3577195: src/priority-ui-coordinator.js\n3599638: src/apm-wrapper.js\n3621867: src/clipboard.js\n3579245: src/flux/stores/database-setup-query-builder.js\n3583641: src/flux/stores/database-change-record.js\n3613961: src/buffered-process.js\n3622937: src/theme-manager.js\n3585323: src/flux/stores/database-writer.js\n3621867: src/clipboard.js\n3642517: src/style-manager.js\n3648228: src/flux/action-bridge.js\n3622937: src/theme-manager.js\n3654791: src/menu-manager.js\n3658911: src/menu-helpers.js\n3642517: src/style-manager.js\n3648228: src/flux/action-bridge.js\n3599638: src/apm-wrapper.js\n3654791: src/menu-manager.js\n3658911: src/menu-helpers.js\n3613961: src/buffered-process.js\n3621867: src/clipboard.js\n3622937: src/theme-manager.js\n3677020: menus/darwin.json\n3642517: src/style-manager.js\n3648228: src/flux/action-bridge.js\n3654791: src/menu-manager.js\n3677020: menus/darwin.json\n3662409: src/nylas-spellchecker.js\n3658911: src/menu-helpers.js\n3662409: src/nylas-spellchecker.js\n3677020: menus/darwin.json\n3662409: src/nylas-spellchecker.js\n3798011: src/global/nylas-exports.js\n3798011: src/global/nylas-exports.js\n5912469: src/deprecate-utils.js\n3668481: src/config-schema.js\n5912469: src/deprecate-utils.js\n3798011: src/global/nylas-exports.js\n5912469: src/deprecate-utils.js\n3668481: src/config-schema.js\n4687050: src/global/nylas-observables.js\n4697858: node_modules/rx-lite/package.json\n4699647: node_modules/rx-lite/rx.lite.js\n4687050: src/global/nylas-observables.js\n4697858: node_modules/rx-lite/package.json\n4699647: node_modules/rx-lite/rx.lite.js\n4699647: node_modules/rx-lite/rx.lite.js\n4687050: src/global/nylas-observables.js\n4699647: node_modules/rx-lite/rx.lite.js\n4687050: src/global/nylas-observables.js\n3925280: src/flux/models/category.js\n5180618: src/flux/models/query-subscription-pool.js\n5195386: src/flux/models/query-subscription.js\n5231737: src/flux/models/mutable-query-result-set.js\n5237299: src/flux/models/query-result-set.js\n5389575: src/flux/stores/task-queue.js\n3925280: src/flux/models/category.js\n5180618: src/flux/models/query-subscription-pool.js\n3813280: src/flux/tasks/task.js\n5195386: src/flux/models/query-subscription.js\n5231737: src/flux/models/mutable-query-result-set.js\n4200026: src/flux/nylas-api.js\n5237299: src/flux/models/query-result-set.js\n5389575: src/flux/stores/task-queue.js\n4222287: node_modules/request/package.json\n4225420: node_modules/request/index.js\n80787899: node_modules/request/node_modules/extend/index.js\n4229450: node_modules/request/lib/cookies.js\n4230419: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/cookie.js\n3813280: src/flux/tasks/task.js\n4267962: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/pubsuffix.js\n4200026: src/flux/nylas-api.js\n4222287: node_modules/request/package.json\n4225420: node_modules/request/index.js\n80787899: node_modules/request/node_modules/extend/index.js\n4229450: node_modules/request/lib/cookies.js\n4230419: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/cookie.js\n4267962: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/pubsuffix.js\n4417589: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/store.js\n4420430: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/memstore.js\n4425944: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/permuteDomain.js\n4428210: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/pathMatch.js\n4430645: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/package.json\n4432858: node_modules/request/lib/helpers.js\n4434482: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/json-stringify-safe/stringify.js\n4435389: node_modules/request/request.js\n4417589: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/store.js\n80567623: node_modules/request/node_modules/bl/bl.js\n4420430: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/memstore.js\n4425944: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/permuteDomain.js\n80636139: node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js\n4428210: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/pathMatch.js\n80636191: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js\n4430645: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/package.json\n75226954: node_modules/process-nextick-args/index.js\n80638643: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js\n4432858: node_modules/request/lib/helpers.js\n4434482: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/json-stringify-safe/stringify.js\n4435389: node_modules/request/request.js\n57417682: node_modules/isarray/index.js\n80670635: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js\n81345592: node_modules/request/node_modules/hawk/lib/index.js\n80567623: node_modules/request/node_modules/bl/bl.js\n81416906: node_modules/request/node_modules/hawk/node_modules/boom/lib/index.js\n80636139: node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js\n80636191: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js\n75226954: node_modules/process-nextick-args/index.js\n80638643: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js\n81490930: node_modules/request/node_modules/hawk/node_modules/hoek/lib/index.js\n57417682: node_modules/isarray/index.js\n80670635: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js\n81488262: node_modules/request/node_modules/hawk/node_modules/hoek/lib/escape.js\n81520773: node_modules/request/node_modules/hawk/node_modules/sntp/index.js\n81345592: node_modules/request/node_modules/hawk/lib/index.js\n81416906: node_modules/request/node_modules/hawk/node_modules/boom/lib/index.js\n81520807: node_modules/request/node_modules/hawk/node_modules/sntp/lib/index.js\n81345973: node_modules/request/node_modules/hawk/lib/server.js\n81490930: node_modules/request/node_modules/hawk/node_modules/hoek/lib/index.js\n81488262: node_modules/request/node_modules/hawk/node_modules/hoek/lib/escape.js\n81520773: node_modules/request/node_modules/hawk/node_modules/sntp/index.js\n81520807: node_modules/request/node_modules/hawk/node_modules/sntp/lib/index.js\n81428267: node_modules/request/node_modules/hawk/node_modules/cryptiles/lib/index.js\n81342006: node_modules/request/node_modules/hawk/lib/crypto.js\n81364519: node_modules/request/node_modules/hawk/lib/utils.js\n81331415: node_modules/request/node_modules/hawk/lib/client.js\n80480692: node_modules/request/node_modules/aws-sign2/index.js\n81551606: node_modules/request/node_modules/http-signature/lib/index.js\n81552232: node_modules/request/node_modules/http-signature/lib/parser.js\n81585919: node_modules/request/node_modules/http-signature/node_modules/assert-plus/assert.js\n81345973: node_modules/request/node_modules/hawk/lib/server.js\n81428267: node_modules/request/node_modules/hawk/node_modules/cryptiles/lib/index.js\n81342006: node_modules/request/node_modules/hawk/lib/crypto.js\n81574938: node_modules/request/node_modules/http-signature/lib/utils.js\n81364519: node_modules/request/node_modules/hawk/lib/utils.js\n81887698: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/index.js\n81888429: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/key.js\n81331415: node_modules/request/node_modules/hawk/lib/client.js\n81950012: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/assert-plus/assert.js\n80480692: node_modules/request/node_modules/aws-sign2/index.js\n81551606: node_modules/request/node_modules/http-signature/lib/index.js\n81552232: node_modules/request/node_modules/http-signature/lib/parser.js\n81585919: node_modules/request/node_modules/http-signature/node_modules/assert-plus/assert.js\n81830178: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/algs.js\n81847974: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/fingerprint.js\n81574938: node_modules/request/node_modules/http-signature/lib/utils.js\n81845672: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/errors.js\n81911179: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/utils.js\n81887698: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/index.js\n81888429: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/key.js\n81895828: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/private-key.js\n81950012: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/assert-plus/assert.js\n81901963: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/signature.js\n81942773: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/index.js\n81928490: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/index.js\n81830178: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/algs.js\n81928251: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/errors.js\n81934548: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/types.js\n81847974: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/fingerprint.js\n81845672: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/errors.js\n81928959: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/reader.js\n81911179: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/utils.js\n81935186: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/writer.js\n81895828: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/private-key.js\n81901963: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/signature.js\n81908010: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ssh-buffer.js\n81942773: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/index.js\n81843337: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ed-compat.js\n81928490: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/index.js\n81928251: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/errors.js\n81851404: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/auto.js\n81934548: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/types.js\n81853302: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pem.js\n81858111: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs1.js\n81928959: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/reader.js\n81865807: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs8.js\n81935186: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/writer.js\n81881346: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh-private.js\n81877740: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/rfc4253.js\n81908010: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ssh-buffer.js\n81843337: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ed-compat.js\n81884606: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh.js\n81851404: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/auto.js\n81835020: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/dhe.js\n81853302: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pem.js\n81858111: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs1.js\n81562004: node_modules/request/node_modules/http-signature/lib/signer.js\n81865807: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs8.js\n81602307: node_modules/request/node_modules/http-signature/node_modules/jsprim/lib/jsprim.js\n81881346: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh-private.js\n81632804: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/extsprintf/lib/extsprintf.js\n81877740: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/rfc4253.js\n81797823: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/verror/lib/verror.js\n81763963: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/json-schema/lib/validate.js\n81884606: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh.js\n81835020: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/dhe.js\n81577777: node_modules/request/node_modules/http-signature/lib/verify.js\n82371286: node_modules/request/node_modules/mime-types/index.js\n81562004: node_modules/request/node_modules/http-signature/lib/signer.js\n82530510: node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js\n82387545: node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json\n81602307: node_modules/request/node_modules/http-signature/node_modules/jsprim/lib/jsprim.js\n81632804: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/extsprintf/lib/extsprintf.js\n81797823: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/verror/lib/verror.js\n81763963: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/json-schema/lib/validate.js\n81577777: node_modules/request/node_modules/http-signature/lib/verify.js\n82371286: node_modules/request/node_modules/mime-types/index.js\n82530510: node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js\n82387545: node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json\n4476422: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/stringstream/stringstream.js\n80754327: node_modules/request/node_modules/caseless/index.js\n80801324: node_modules/request/node_modules/forever-agent/index.js\n80814876: node_modules/request/node_modules/form-data/lib/form_data.js\n3668481: src/config-schema.js\n80764215: node_modules/request/node_modules/combined-stream/lib/combined_stream.js\n80773528: node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js\n80944514: node_modules/request/node_modules/form-data/node_modules/async/lib/async.js\n4476422: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/stringstream/stringstream.js\n80754327: node_modules/request/node_modules/caseless/index.js\n80801324: node_modules/request/node_modules/forever-agent/index.js\n80814876: node_modules/request/node_modules/form-data/lib/form_data.js\n80826033: node_modules/request/node_modules/form-data/lib/populate.js\n82348558: node_modules/request/node_modules/isstream/isstream.js\n82341499: node_modules/request/node_modules/is-typedarray/index.js\n80764215: node_modules/request/node_modules/combined-stream/lib/combined_stream.js\n4479214: node_modules/request/lib/getProxyFromURI.js\n4481482: node_modules/request/lib/querystring.js\n80773528: node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js\n82587962: node_modules/request/node_modules/qs/lib/index.js\n82593363: node_modules/request/node_modules/qs/lib/stringify.js\n82597342: node_modules/request/node_modules/qs/lib/utils.js\n80944514: node_modules/request/node_modules/form-data/node_modules/async/lib/async.js\n82588115: node_modules/request/node_modules/qs/lib/parse.js\n4482815: node_modules/request/lib/har.js\n81001381: node_modules/request/node_modules/har-validator/lib/index.js\n81163939: node_modules/request/node_modules/har-validator/node_modules/pinkie-promise/index.js\n81001935: node_modules/request/node_modules/har-validator/lib/runner.js\n80826033: node_modules/request/node_modules/form-data/lib/populate.js\n81005359: node_modules/request/node_modules/har-validator/lib/schemas/index.js\n81002525: node_modules/request/node_modules/har-validator/lib/schemas/cache.json\n82348558: node_modules/request/node_modules/isstream/isstream.js\n81002712: node_modules/request/node_modules/har-validator/lib/schemas/cacheEntry.json\n81003206: node_modules/request/node_modules/har-validator/lib/schemas/content.json\n82341499: node_modules/request/node_modules/is-typedarray/index.js\n81003583: node_modules/request/node_modules/har-validator/lib/schemas/cookie.json\n81004081: node_modules/request/node_modules/har-validator/lib/schemas/creator.json\n4479214: node_modules/request/lib/getProxyFromURI.js\n81004311: node_modules/request/node_modules/har-validator/lib/schemas/entry.json\n81005242: node_modules/request/node_modules/har-validator/lib/schemas/har.json\n4481482: node_modules/request/lib/querystring.js\n81007111: node_modules/request/node_modules/har-validator/lib/schemas/log.json\n81007604: node_modules/request/node_modules/har-validator/lib/schemas/page.json\n82587962: node_modules/request/node_modules/qs/lib/index.js\n81008181: node_modules/request/node_modules/har-validator/lib/schemas/pageTimings.json\n81008406: node_modules/request/node_modules/har-validator/lib/schemas/postData.json\n82593363: node_modules/request/node_modules/qs/lib/stringify.js\n81009060: node_modules/request/node_modules/har-validator/lib/schemas/record.json\n81009286: node_modules/request/node_modules/har-validator/lib/schemas/request.json\n81010139: node_modules/request/node_modules/har-validator/lib/schemas/response.json\n82597342: node_modules/request/node_modules/qs/lib/utils.js\n81010946: node_modules/request/node_modules/har-validator/lib/schemas/timings.json\n81001195: node_modules/request/node_modules/har-validator/lib/error.js\n81108729: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/index.js\n82588115: node_modules/request/node_modules/qs/lib/parse.js\n81132607: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/index.js\n4482815: node_modules/request/lib/har.js\n81134601: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/node_modules/is-property/is-property.js\n81001381: node_modules/request/node_modules/har-validator/lib/index.js\n81127606: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-function/index.js\n81163939: node_modules/request/node_modules/har-validator/node_modules/pinkie-promise/index.js\n81149530: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/jsonpointer/jsonpointer.js\n81001935: node_modules/request/node_modules/har-validator/lib/runner.js\n4525735: node_modules/babel-core/node_modules/output-file-sync/node_modules/xtend/immutable.js\n81106380: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/formats.js\n81005359: node_modules/request/node_modules/har-validator/lib/schemas/index.js\n81002525: node_modules/request/node_modules/har-validator/lib/schemas/cache.json\n81002712: node_modules/request/node_modules/har-validator/lib/schemas/cacheEntry.json\n4533728: node_modules/request/lib/auth.js\n81003206: node_modules/request/node_modules/har-validator/lib/schemas/content.json\n81003583: node_modules/request/node_modules/har-validator/lib/schemas/cookie.json\n81004081: node_modules/request/node_modules/har-validator/lib/schemas/creator.json\n81004311: node_modules/request/node_modules/har-validator/lib/schemas/entry.json\n81005242: node_modules/request/node_modules/har-validator/lib/schemas/har.json\n81007111: node_modules/request/node_modules/har-validator/lib/schemas/log.json\n4550226: node_modules/request/lib/oauth.js\n81007604: node_modules/request/node_modules/har-validator/lib/schemas/page.json\n81008181: node_modules/request/node_modules/har-validator/lib/schemas/pageTimings.json\n81008406: node_modules/request/node_modules/har-validator/lib/schemas/postData.json\n81009060: node_modules/request/node_modules/har-validator/lib/schemas/record.json\n81009286: node_modules/request/node_modules/har-validator/lib/schemas/request.json\n82544802: node_modules/request/node_modules/oauth-sign/index.js\n81010139: node_modules/request/node_modules/har-validator/lib/schemas/response.json\n81010946: node_modules/request/node_modules/har-validator/lib/schemas/timings.json\n81001195: node_modules/request/node_modules/har-validator/lib/error.js\n4568756: node_modules/request/lib/multipart.js\n81108729: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/index.js\n4585628: node_modules/request/lib/redirect.js\n4594531: node_modules/request/lib/tunnel.js\n81132607: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/index.js\n4601413: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tunnel-agent/index.js\n81134601: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/node_modules/is-property/is-property.js\n81127606: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-function/index.js\n81149530: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/jsonpointer/jsonpointer.js\n91866084: src/flux/nylas-long-connection.js\n4525735: node_modules/babel-core/node_modules/output-file-sync/node_modules/xtend/immutable.js\n81106380: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/formats.js\n2376215: node_modules/event-kit/package.json\n4533728: node_modules/request/lib/auth.js\n3905749: src/flux/models/account.js\n3911874: src/flux/models/model-with-metadata.js\n4550226: node_modules/request/lib/oauth.js\n82544802: node_modules/request/node_modules/oauth-sign/index.js\n4568756: node_modules/request/lib/multipart.js\n4585628: node_modules/request/lib/redirect.js\n3959955: src/flux/models/message.js\n4594531: node_modules/request/lib/tunnel.js\n3971136: node_modules/moment/package.json\n4601413: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tunnel-agent/index.js\n3809622: src/flux/models/file.js\n91866084: src/flux/nylas-long-connection.js\n3870842: src/flux/models/event.js\n3875483: src/flux/models/contact.js\n2376215: node_modules/event-kit/package.json\n3886010: src/regexp-utils.js\n3905749: src/flux/models/account.js\n56664169: node_modules/emoji-data/lib/emoji_data.js\n3911874: src/flux/models/model-with-metadata.js\n56662240: node_modules/emoji-data/lib/emoji_char.js\n56691400: node_modules/emoji-data/node_modules/underscore.string/lib/underscore.string.js\n3959955: src/flux/models/message.js\n3971136: node_modules/moment/package.json\n3809622: src/flux/models/file.js\n3870842: src/flux/models/event.js\n3875483: src/flux/models/contact.js\n3886010: src/regexp-utils.js\n56664169: node_modules/emoji-data/lib/emoji_data.js\n56662240: node_modules/emoji-data/lib/emoji_char.js\n56691400: node_modules/emoji-data/node_modules/underscore.string/lib/underscore.string.js\n3889367: src/flux/stores/account-store.js\n65006241: node_modules/keytar/package.json\n65004180: node_modules/keytar/lib/keytar.js\n3889367: src/flux/stores/account-store.js\n65006241: node_modules/keytar/package.json\n65004180: node_modules/keytar/lib/keytar.js\n4687050: src/global/nylas-observables.js\n4697858: node_modules/rx-lite/package.json\n3925080: src/flux/models/label.js\n4699647: node_modules/rx-lite/rx.lite.js\n3930438: src/flux/models/folder.js\n3930638: src/flux/models/thread.js\n4096909: src/flux/models/calendar.js\n4098495: src/flux/models/json-blob.js\n3925080: src/flux/models/label.js\n3930438: src/flux/models/folder.js\n3930638: src/flux/models/thread.js\n4096909: src/flux/models/calendar.js\n5951047: src/flux/stores/badge-store.js\n5726955: src/flux/stores/focused-perspective-store.js\n5744701: src/flux/stores/workspace-store.js\n5951047: src/flux/stores/badge-store.js\n5726955: src/flux/stores/focused-perspective-store.js\n4648657: src/flux/stores/category-store.js\n5744701: src/flux/stores/workspace-store.js\n9942964: src/flux/stores/nylas-sync-status-store.js\n4648657: src/flux/stores/category-store.js\n9942964: src/flux/stores/nylas-sync-status-store.js\n4699647: node_modules/rx-lite/rx.lite.js\n4098495: src/flux/models/json-blob.js\n4687050: src/global/nylas-observables.js\n4100089: src/mailbox-perspective.js\n3925280: src/flux/models/category.js\n4120446: src/flux/tasks/task-factory.js\n4139229: src/flux/tasks/change-folder-task.js\n5180618: src/flux/models/query-subscription-pool.js\n4156736: src/flux/tasks/change-mail-task.js\n5195386: src/flux/models/query-subscription.js\n4100089: src/mailbox-perspective.js\n5231737: src/flux/models/mutable-query-result-set.js\n4120446: src/flux/tasks/task-factory.js\n4636283: src/flux/tasks/syncback-category-task.js\n5237299: src/flux/models/query-result-set.js\n4139229: src/flux/tasks/change-folder-task.js\n5389575: src/flux/stores/task-queue.js\n5273907: src/flux/tasks/change-labels-task.js\n4156736: src/flux/tasks/change-mail-task.js\n5304677: src/flux/tasks/change-unread-task.js\n3813280: src/flux/tasks/task.js\n4636283: src/flux/tasks/syncback-category-task.js\n5341333: src/flux/tasks/change-starred-task.js\n4200026: src/flux/nylas-api.js\n5350052: src/flux/stores/outbox-store.js\n5356198: src/flux/tasks/send-draft-task.js\n5273907: src/flux/tasks/change-labels-task.js\n5417961: src/sound-registry.js\n91943581: src/flux/tasks/base-draft-task.js\n4222287: node_modules/request/package.json\n5304677: src/flux/tasks/change-unread-task.js\n4225420: node_modules/request/index.js\n5419733: src/flux/tasks/syncback-metadata-task.js\n80787899: node_modules/request/node_modules/extend/index.js\n4229450: node_modules/request/lib/cookies.js\n5431422: src/flux/tasks/syncback-model-task.js\n5341333: src/flux/tasks/change-starred-task.js\n4230419: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/cookie.js\n91991403: src/flux/tasks/notify-plugins-of-send-task.js\n5350052: src/flux/stores/outbox-store.js\n5356198: src/flux/tasks/send-draft-task.js\n4267962: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/pubsuffix.js\n91821594: src/flux/edgehill-api.js\n5417961: src/sound-registry.js\n91943581: src/flux/tasks/base-draft-task.js\n5471010: src/flux/stores/task-queue-status-store.js\n5419733: src/flux/tasks/syncback-metadata-task.js\n5431422: src/flux/tasks/syncback-model-task.js\n91991403: src/flux/tasks/notify-plugins-of-send-task.js\n5512912: src/flux/stores/thread-counts-store.js\n91821594: src/flux/edgehill-api.js\n91919008: src/flux/stores/recently-read-store.js\n5501930: src/flux/models/mutable-query-subscription.js\n5471010: src/flux/stores/task-queue-status-store.js\n91852488: src/flux/models/unread-query-subscription.js\n5512912: src/flux/stores/thread-counts-store.js\n91919008: src/flux/stores/recently-read-store.js\n5501930: src/flux/models/mutable-query-subscription.js\n4417589: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/store.js\n91852488: src/flux/models/unread-query-subscription.js\n4420430: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/memstore.js\n4425944: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/permuteDomain.js\n4428210: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/lib/pathMatch.js\n4430645: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tough-cookie/package.json\n5680865: src/flux/stores/draft-store.js\n4432858: node_modules/request/lib/helpers.js\n4434482: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/json-stringify-safe/stringify.js\n4435389: node_modules/request/request.js\n5704584: src/flux/stores/draft-editing-session.js\n5886976: src/extension-registry.js\n5895610: src/extensions/composer-extension-adapter.js\n80567623: node_modules/request/node_modules/bl/bl.js\n5860903: src/dom-utils.js\n80636139: node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js\n80636191: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js\n5680865: src/flux/stores/draft-store.js\n75226954: node_modules/process-nextick-args/index.js\n80638643: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js\n5913732: src/extensions/extension-utils.js\n5915158: src/extensions/message-view-extension-adapter.js\n5704584: src/flux/stores/draft-editing-session.js\n57417682: node_modules/isarray/index.js\n5886976: src/extension-registry.js\n91896596: src/flux/stores/draft-factory.js\n80670635: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js\n5895610: src/extensions/composer-extension-adapter.js\n5714320: src/flux/stores/contact-store.js\n81345592: node_modules/request/node_modules/hawk/lib/index.js\n5860903: src/dom-utils.js\n81416906: node_modules/request/node_modules/hawk/node_modules/boom/lib/index.js\n5723311: src/flux/stores/contact-ranking-store.js\n5913732: src/extensions/extension-utils.js\n81490930: node_modules/request/node_modules/hawk/node_modules/hoek/lib/index.js\n5915158: src/extensions/message-view-extension-adapter.js\n5523481: src/window-bridge.js\n91896596: src/flux/stores/draft-factory.js\n5714320: src/flux/stores/contact-store.js\n5917902: src/flux/stores/message-store.js\n5723311: src/flux/stores/contact-ranking-store.js\n5733766: src/flux/stores/focused-content-store.js\n5523481: src/window-bridge.js\n5756110: src/services/inline-style-transformer.js\n81488262: node_modules/request/node_modules/hawk/node_modules/hoek/lib/escape.js\n81520773: node_modules/request/node_modules/hawk/node_modules/sntp/index.js\n5917902: src/flux/stores/message-store.js\n81520807: node_modules/request/node_modules/hawk/node_modules/sntp/lib/index.js\n5758660: src/services/sanitize-transformer.js\n5763630: node_modules/sanitize-html/package.json\n5733766: src/flux/stores/focused-content-store.js\n81345973: node_modules/request/node_modules/hawk/lib/server.js\n5765975: node_modules/sanitize-html/index.js\n81428267: node_modules/request/node_modules/hawk/node_modules/cryptiles/lib/index.js\n5775765: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/index.js\n81342006: node_modules/request/node_modules/hawk/lib/crypto.js\n81364519: node_modules/request/node_modules/hawk/lib/utils.js\n5777529: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/Parser.js\n81331415: node_modules/request/node_modules/hawk/lib/client.js\n5756110: src/services/inline-style-transformer.js\n5785458: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/Tokenizer.js\n80480692: node_modules/request/node_modules/aws-sign2/index.js\n81551606: node_modules/request/node_modules/http-signature/lib/index.js\n81552232: node_modules/request/node_modules/http-signature/lib/parser.js\n5810958: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/lib/decode_codepoint.js\n81585919: node_modules/request/node_modules/http-signature/node_modules/assert-plus/assert.js\n5758660: src/services/sanitize-transformer.js\n5811580: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/decode.json\n5811878: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/entities.json\n5763630: node_modules/sanitize-html/package.json\n5765975: node_modules/sanitize-html/index.js\n81574938: node_modules/request/node_modules/http-signature/lib/utils.js\n5775765: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/index.js\n81887698: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/index.js\n81888429: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/key.js\n5777529: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/Parser.js\n81950012: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/assert-plus/assert.js\n5852494: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/legacy.json\n5785458: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/Tokenizer.js\n5854241: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/xml.json\n81830178: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/algs.js\n5854294: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/index.js\n5810958: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/lib/decode_codepoint.js\n5858733: node_modules/juice/node_modules/cheerio/node_modules/css-select/node_modules/domutils/node_modules/domelementtype/index.js\n81847974: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/fingerprint.js\n5811580: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/decode.json\n5859144: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/lib/node.js\n5811878: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/entities.json\n81845672: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/errors.js\n5860059: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/lib/element.js\n81911179: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/utils.js\n5860502: node_modules/sanitize-html/node_modules/regexp-quote/regexp-quote.js\n81895828: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/private-key.js\n5860597: src/flux/models/message-utils.js\n81901963: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/signature.js\n92019120: src/flux/tasks/syncback-draft-files-task.js\n81942773: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/index.js\n92064500: src/multi-request-progress-monitor.js\n81928490: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/index.js\n5852494: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/legacy.json\n81928251: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/errors.js\n5555349: src/flux/tasks/destroy-draft-task.js\n81934548: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/types.js\n81928959: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/reader.js\n5854241: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/xml.json\n81935186: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/writer.js\n5854294: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/index.js\n81908010: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ssh-buffer.js\n5858733: node_modules/juice/node_modules/cheerio/node_modules/css-select/node_modules/domutils/node_modules/domelementtype/index.js\n6145019: src/flux/stores/modal-store.js\n5859144: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/lib/node.js\n81843337: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ed-compat.js\n4488696: node_modules/react/package.json\n81851404: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/auto.js\n5860059: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/lib/element.js\n4490634: node_modules/react/react.js\n81853302: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pem.js\n4490690: node_modules/react/lib/React.js\n5860502: node_modules/sanitize-html/node_modules/regexp-quote/regexp-quote.js\n79997473: node_modules/react/node_modules/object-assign/index.js\n81858111: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs1.js\n5860597: src/flux/models/message-utils.js\n4503558: node_modules/react/lib/ReactChildren.js\n92019120: src/flux/tasks/syncback-draft-files-task.js\n4509426: node_modules/react/lib/PooledClass.js\n81865807: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs8.js\n92064500: src/multi-request-progress-monitor.js\n77945402: node_modules/react/node_modules/fbjs/lib/invariant.js\n4515504: node_modules/react/lib/ReactElement.js\n81881346: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh-private.js\n5555349: src/flux/tasks/destroy-draft-task.js\n81877740: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/rfc4253.js\n4526119: node_modules/react/lib/ReactCurrentOwner.js\n77997237: node_modules/react/node_modules/fbjs/lib/warning.js\n77865983: node_modules/react/node_modules/fbjs/lib/emptyFunction.js\n81884606: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh.js\n77610157: node_modules/react/lib/canDefineProperty.js\n81835020: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/dhe.js\n4526776: node_modules/react/lib/traverseAllChildren.js\n6145019: src/flux/stores/modal-store.js\n4549078: node_modules/react/lib/getIteratorFn.js\n4488696: node_modules/react/package.json\n81562004: node_modules/request/node_modules/http-signature/lib/signer.js\n4554180: node_modules/react/lib/ReactComponent.js\n4490634: node_modules/react/react.js\n77543851: node_modules/react/lib/ReactNoopUpdateQueue.js\n4490690: node_modules/react/lib/React.js\n77541351: node_modules/react/lib/ReactInstrumentation.js\n81602307: node_modules/request/node_modules/http-signature/node_modules/jsprim/lib/jsprim.js\n77520716: node_modules/react/lib/ReactDebugTool.js\n79997473: node_modules/react/node_modules/object-assign/index.js\n4503558: node_modules/react/lib/ReactChildren.js\n77541813: node_modules/react/lib/ReactInvalidSetStateWarningDevTool.js\n81632804: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/extsprintf/lib/extsprintf.js\n4509426: node_modules/react/lib/PooledClass.js\n77868121: node_modules/react/node_modules/fbjs/lib/emptyObject.js\n81797823: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/verror/lib/verror.js\n4658325: node_modules/react/lib/ReactClass.js\n77945402: node_modules/react/node_modules/fbjs/lib/invariant.js\n4515504: node_modules/react/lib/ReactElement.js\n81763963: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/json-schema/lib/validate.js\n4622568: node_modules/react/lib/ReactPropTypeLocations.js\n4526119: node_modules/react/lib/ReactCurrentOwner.js\n77955086: node_modules/react/node_modules/fbjs/lib/keyMirror.js\n77997237: node_modules/react/node_modules/fbjs/lib/warning.js\n81577777: node_modules/request/node_modules/http-signature/lib/verify.js\n77865983: node_modules/react/node_modules/fbjs/lib/emptyFunction.js\n4623120: node_modules/react/lib/ReactPropTypeLocationNames.js\n77610157: node_modules/react/lib/canDefineProperty.js\n77961237: node_modules/react/node_modules/fbjs/lib/keyOf.js\n82371286: node_modules/request/node_modules/mime-types/index.js\n4526776: node_modules/react/lib/traverseAllChildren.js\n77505957: node_modules/react/lib/ReactDOMFactories.js\n82530510: node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js\n4611818: node_modules/react/lib/ReactElementValidator.js\n4549078: node_modules/react/lib/getIteratorFn.js\n82387545: node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json\n4554180: node_modules/react/lib/ReactComponent.js\n77963437: node_modules/react/node_modules/fbjs/lib/mapObject.js\n77543851: node_modules/react/lib/ReactNoopUpdateQueue.js\n77541351: node_modules/react/lib/ReactInstrumentation.js\n77520716: node_modules/react/lib/ReactDebugTool.js\n77541813: node_modules/react/lib/ReactInvalidSetStateWarningDevTool.js\n77868121: node_modules/react/node_modules/fbjs/lib/emptyObject.js\n4658325: node_modules/react/lib/ReactClass.js\n4622568: node_modules/react/lib/ReactPropTypeLocations.js\n77955086: node_modules/react/node_modules/fbjs/lib/keyMirror.js\n5328125: node_modules/react/lib/ReactPropTypes.js\n4623120: node_modules/react/lib/ReactPropTypeLocationNames.js\n77961237: node_modules/react/node_modules/fbjs/lib/keyOf.js\n77505957: node_modules/react/lib/ReactDOMFactories.js\n77568528: node_modules/react/lib/ReactVersion.js\n4611818: node_modules/react/lib/ReactElementValidator.js\n5544770: node_modules/react/lib/onlyChild.js\n75786540: node_modules/react-dom/package.json\n75786477: node_modules/react-dom/index.js\n77963437: node_modules/react/node_modules/fbjs/lib/mapObject.js\n4476422: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/stringstream/stringstream.js\n4693093: node_modules/react/lib/ReactDOM.js\n80754327: node_modules/request/node_modules/caseless/index.js\n77494923: node_modules/react/lib/ReactDOMComponentTree.js\n4954924: node_modules/react/lib/DOMProperty.js\n80801324: node_modules/request/node_modules/forever-agent/index.js\n77494452: node_modules/react/lib/ReactDOMComponentFlags.js\n5154239: node_modules/react/lib/ReactDefaultInjection.js\n5157979: node_modules/react/lib/BeforeInputEventPlugin.js\n80814876: node_modules/request/node_modules/form-data/lib/form_data.js\n4501390: node_modules/react/lib/EventConstants.js\n80764215: node_modules/request/node_modules/combined-stream/lib/combined_stream.js\n5171831: node_modules/react/lib/EventPropagators.js\n80773528: node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js\n5034693: node_modules/react/lib/EventPluginHub.js\n5328125: node_modules/react/lib/ReactPropTypes.js\n5042578: node_modules/react/lib/EventPluginRegistry.js\n80944514: node_modules/request/node_modules/form-data/node_modules/async/lib/async.js\n4493310: node_modules/react/lib/EventPluginUtils.js\n77568528: node_modules/react/lib/ReactVersion.js\n4684789: node_modules/react/lib/ReactErrorUtils.js\n5544770: node_modules/react/lib/onlyChild.js\n5051772: node_modules/react/lib/accumulateInto.js\n75786540: node_modules/react-dom/package.json\n5053515: node_modules/react/lib/forEachAccumulated.js\n75786477: node_modules/react-dom/index.js\n77682133: node_modules/react/node_modules/fbjs/lib/ExecutionEnvironment.js\n4693093: node_modules/react/lib/ReactDOM.js\n5177151: node_modules/react/lib/FallbackCompositionState.js\n77494923: node_modules/react/lib/ReactDOMComponentTree.js\n80826033: node_modules/request/node_modules/form-data/lib/populate.js\n5179621: node_modules/react/lib/getTextContentAccessor.js\n82348558: node_modules/request/node_modules/isstream/isstream.js\n4954924: node_modules/react/lib/DOMProperty.js\n5185473: node_modules/react/lib/SyntheticCompositionEvent.js\n82341499: node_modules/request/node_modules/is-typedarray/index.js\n5186617: node_modules/react/lib/SyntheticEvent.js\n77494452: node_modules/react/lib/ReactDOMComponentFlags.js\n4479214: node_modules/request/lib/getProxyFromURI.js\n5154239: node_modules/react/lib/ReactDefaultInjection.js\n4481482: node_modules/request/lib/querystring.js\n5157979: node_modules/react/lib/BeforeInputEventPlugin.js\n5236171: node_modules/react/lib/SyntheticInputEvent.js\n82587962: node_modules/request/node_modules/qs/lib/index.js\n82593363: node_modules/request/node_modules/qs/lib/stringify.js\n5241892: node_modules/react/lib/ChangeEventPlugin.js\n4501390: node_modules/react/lib/EventConstants.js\n82597342: node_modules/request/node_modules/qs/lib/utils.js\n5171831: node_modules/react/lib/EventPropagators.js\n4571389: node_modules/react/lib/ReactUpdates.js\n5034693: node_modules/react/lib/EventPluginHub.js\n82588115: node_modules/request/node_modules/qs/lib/parse.js\n5042578: node_modules/react/lib/EventPluginRegistry.js\n4580474: node_modules/react/lib/CallbackQueue.js\n4482815: node_modules/request/lib/har.js\n77540690: node_modules/react/lib/ReactFeatureFlags.js\n4493310: node_modules/react/lib/EventPluginUtils.js\n4583141: node_modules/react/lib/ReactPerf.js\n81001381: node_modules/request/node_modules/har-validator/lib/index.js\n4590060: node_modules/react/lib/ReactReconciler.js\n81163939: node_modules/request/node_modules/har-validator/node_modules/pinkie-promise/index.js\n4684789: node_modules/react/lib/ReactErrorUtils.js\n4599068: node_modules/react/lib/ReactRef.js\n81001935: node_modules/request/node_modules/har-validator/lib/runner.js\n5051772: node_modules/react/lib/accumulateInto.js\n4608257: node_modules/react/lib/ReactOwner.js\n5053515: node_modules/react/lib/forEachAccumulated.js\n81005359: node_modules/request/node_modules/har-validator/lib/schemas/index.js\n4626735: node_modules/react/lib/Transaction.js\n77682133: node_modules/react/node_modules/fbjs/lib/ExecutionEnvironment.js\n81002525: node_modules/request/node_modules/har-validator/lib/schemas/cache.json\n5177151: node_modules/react/lib/FallbackCompositionState.js\n81002712: node_modules/request/node_modules/har-validator/lib/schemas/cacheEntry.json\n5230693: node_modules/react/lib/getEventTarget.js\n81003206: node_modules/request/node_modules/har-validator/lib/schemas/content.json\n5179621: node_modules/react/lib/getTextContentAccessor.js\n81003583: node_modules/request/node_modules/har-validator/lib/schemas/cookie.json\n5056054: node_modules/react/lib/isEventSupported.js\n81004081: node_modules/request/node_modules/har-validator/lib/schemas/creator.json\n5185473: node_modules/react/lib/SyntheticCompositionEvent.js\n81004311: node_modules/request/node_modules/har-validator/lib/schemas/entry.json\n5253386: node_modules/react/lib/isTextInputElement.js\n5186617: node_modules/react/lib/SyntheticEvent.js\n81005242: node_modules/request/node_modules/har-validator/lib/schemas/har.json\n81007111: node_modules/request/node_modules/har-validator/lib/schemas/log.json\n81007604: node_modules/request/node_modules/har-validator/lib/schemas/page.json\n81008181: node_modules/request/node_modules/har-validator/lib/schemas/pageTimings.json\n81008406: node_modules/request/node_modules/har-validator/lib/schemas/postData.json\n5254419: node_modules/react/lib/DefaultEventPluginOrder.js\n81009060: node_modules/request/node_modules/har-validator/lib/schemas/record.json\n5236171: node_modules/react/lib/SyntheticInputEvent.js\n81009286: node_modules/request/node_modules/har-validator/lib/schemas/request.json\n5255683: node_modules/react/lib/EnterLeaveEventPlugin.js\n81010139: node_modules/request/node_modules/har-validator/lib/schemas/response.json\n81010946: node_modules/request/node_modules/har-validator/lib/schemas/timings.json\n5241892: node_modules/react/lib/ChangeEventPlugin.js\n5259147: node_modules/react/lib/SyntheticMouseEvent.js\n81001195: node_modules/request/node_modules/har-validator/lib/error.js\n5261327: node_modules/react/lib/SyntheticUIEvent.js\n81108729: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/index.js\n4571389: node_modules/react/lib/ReactUpdates.js\n5055413: node_modules/react/lib/ViewportMetrics.js\n5262952: node_modules/react/lib/getEventModifierState.js\n4580474: node_modules/react/lib/CallbackQueue.js\n5264226: node_modules/react/lib/HTMLDOMPropertyConfig.js\n77540690: node_modules/react/lib/ReactFeatureFlags.js\n81132607: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/index.js\n4583141: node_modules/react/lib/ReactPerf.js\n81134601: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/node_modules/is-property/is-property.js\n4965198: node_modules/react/lib/ReactComponentBrowserEnvironment.js\n4590060: node_modules/react/lib/ReactReconciler.js\n4981199: node_modules/react/lib/DOMChildrenOperations.js\n4599068: node_modules/react/lib/ReactRef.js\n81127606: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-function/index.js\n4608257: node_modules/react/lib/ReactOwner.js\n77469073: node_modules/react/lib/DOMLazyTree.js\n4626735: node_modules/react/lib/Transaction.js\n81149530: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/jsonpointer/jsonpointer.js\n77613716: node_modules/react/lib/createMicrosoftUnsafeLocalFunction.js\n4525735: node_modules/babel-core/node_modules/output-file-sync/node_modules/xtend/immutable.js\n4994328: node_modules/react/lib/setTextContent.js\n81106380: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/formats.js\n4964346: node_modules/react/lib/escapeTextContentForBrowser.js\n4995531: node_modules/react/lib/setInnerHTML.js\n5230693: node_modules/react/lib/getEventTarget.js\n5056054: node_modules/react/lib/isEventSupported.js\n4533728: node_modules/request/lib/auth.js\n4986498: node_modules/react/lib/Danger.js\n5253386: node_modules/react/lib/isTextInputElement.js\n4550226: node_modules/request/lib/oauth.js\n77856589: node_modules/react/node_modules/fbjs/lib/createNodesFromMarkup.js\n5254419: node_modules/react/lib/DefaultEventPluginOrder.js\n77848650: node_modules/react/node_modules/fbjs/lib/createArrayFromMixed.js\n5255683: node_modules/react/lib/EnterLeaveEventPlugin.js\n82544802: node_modules/request/node_modules/oauth-sign/index.js\n77922333: node_modules/react/node_modules/fbjs/lib/getMarkupWrap.js\n5259147: node_modules/react/lib/SyntheticMouseEvent.js\n4568756: node_modules/request/lib/multipart.js\n5261327: node_modules/react/lib/SyntheticUIEvent.js\n4585628: node_modules/request/lib/redirect.js\n4594531: node_modules/request/lib/tunnel.js\n5055413: node_modules/react/lib/ViewportMetrics.js\n5262952: node_modules/react/lib/getEventModifierState.js\n4993464: node_modules/react/lib/ReactMultiChildUpdateTypes.js\n4601413: node_modules/less-cache/node_modules/less/node_modules/request/node_modules/tunnel-agent/index.js\n5264226: node_modules/react/lib/HTMLDOMPropertyConfig.js\n4966626: node_modules/react/lib/ReactDOMIDOperations.js\n91866084: src/flux/nylas-long-connection.js\n5100253: node_modules/react/lib/ReactDOMComponent.js\n4965198: node_modules/react/lib/ReactComponentBrowserEnvironment.js\n4981199: node_modules/react/lib/DOMChildrenOperations.js\n77469073: node_modules/react/lib/DOMLazyTree.js\n77613716: node_modules/react/lib/createMicrosoftUnsafeLocalFunction.js\n2376215: node_modules/event-kit/package.json\n4994328: node_modules/react/lib/setTextContent.js\n4964346: node_modules/react/lib/escapeTextContentForBrowser.js\n77468440: node_modules/react/lib/AutoFocusUtils.js\n4995531: node_modules/react/lib/setInnerHTML.js\n3905749: src/flux/models/account.js\n77909940: node_modules/react/node_modules/fbjs/lib/focusNode.js\n4986498: node_modules/react/lib/Danger.js\n4967813: node_modules/react/lib/CSSPropertyOperations.js\n3911874: src/flux/models/model-with-metadata.js\n77856589: node_modules/react/node_modules/fbjs/lib/createNodesFromMarkup.js\n4974604: node_modules/react/lib/CSSProperty.js\n77848650: node_modules/react/node_modules/fbjs/lib/createArrayFromMixed.js\n77922333: node_modules/react/node_modules/fbjs/lib/getMarkupWrap.js\n77831606: node_modules/react/node_modules/fbjs/lib/camelizeStyleName.js\n77830175: node_modules/react/node_modules/fbjs/lib/camelize.js\n4993464: node_modules/react/lib/ReactMultiChildUpdateTypes.js\n3959955: src/flux/models/message.js\n4978298: node_modules/react/lib/dangerousStyleValue.js\n4966626: node_modules/react/lib/ReactDOMIDOperations.js\n77943412: node_modules/react/node_modules/fbjs/lib/hyphenateStyleName.js\n3971136: node_modules/moment/package.json\n5100253: node_modules/react/lib/ReactDOMComponent.js\n77941796: node_modules/react/node_modules/fbjs/lib/hyphenate.js\n77967845: node_modules/react/node_modules/fbjs/lib/memoizeStringOnly.js\n3809622: src/flux/models/file.js\n77472180: node_modules/react/lib/DOMNamespaces.js\n4947570: node_modules/react/lib/DOMPropertyOperations.js\n3870842: src/flux/models/event.js\n77468440: node_modules/react/lib/AutoFocusUtils.js\n77509750: node_modules/react/lib/ReactDOMInstrumentation.js\n3875483: src/flux/models/contact.js\n77502083: node_modules/react/lib/ReactDOMDebugTool.js\n77514692: node_modules/react/lib/ReactDOMUnknownPropertyDevtool.js\n77909940: node_modules/react/node_modules/fbjs/lib/focusNode.js\n4967813: node_modules/react/lib/CSSPropertyOperations.js\n3886010: src/regexp-utils.js\n4963597: node_modules/react/lib/quoteAttributeValueForBrowser.js\n5022146: node_modules/react/lib/ReactBrowserEventEmitter.js\n4974604: node_modules/react/lib/CSSProperty.js\n56664169: node_modules/emoji-data/lib/emoji_data.js\n56662240: node_modules/emoji-data/lib/emoji_char.js\n5054412: node_modules/react/lib/ReactEventEmitterMixin.js\n77617819: node_modules/react/lib/getVendorPrefixedEventName.js\n56691400: node_modules/emoji-data/node_modules/underscore.string/lib/underscore.string.js\n77831606: node_modules/react/node_modules/fbjs/lib/camelizeStyleName.js\n77830175: node_modules/react/node_modules/fbjs/lib/camelize.js\n5303529: node_modules/react/lib/ReactDOMButton.js\n4978298: node_modules/react/lib/dangerousStyleValue.js\n5313340: node_modules/react/lib/ReactDOMInput.js\n77943412: node_modules/react/node_modules/fbjs/lib/hyphenateStyleName.js\n5322982: node_modules/react/lib/LinkedValueUtils.js\n77941796: node_modules/react/node_modules/fbjs/lib/hyphenate.js\n77967845: node_modules/react/node_modules/fbjs/lib/memoizeStringOnly.js\n5379029: node_modules/react/lib/ReactDOMOption.js\n77472180: node_modules/react/lib/DOMNamespaces.js\n5382256: node_modules/react/lib/ReactDOMSelect.js\n4947570: node_modules/react/lib/DOMPropertyOperations.js\n5405374: node_modules/react/lib/ReactDOMTextarea.js\n77509750: node_modules/react/lib/ReactDOMInstrumentation.js\n77502083: node_modules/react/lib/ReactDOMDebugTool.js\n5134927: node_modules/react/lib/ReactMultiChild.js\n77514692: node_modules/react/lib/ReactDOMUnknownPropertyDevtool.js\n4963597: node_modules/react/lib/quoteAttributeValueForBrowser.js\n5097145: node_modules/react/lib/ReactComponentEnvironment.js\n5147906: node_modules/react/lib/ReactChildReconciler.js\n5022146: node_modules/react/lib/ReactBrowserEventEmitter.js\n5061495: node_modules/react/lib/instantiateReactComponent.js\n5054412: node_modules/react/lib/ReactEventEmitterMixin.js\n5065922: node_modules/react/lib/ReactCompositeComponent.js\n77617819: node_modules/react/lib/getVendorPrefixedEventName.js\n5303529: node_modules/react/lib/ReactDOMButton.js\n4567499: node_modules/react/lib/ReactInstanceMap.js\n5313340: node_modules/react/lib/ReactDOMInput.js\n77542853: node_modules/react/lib/ReactNodeTypes.js\n4559113: node_modules/react/lib/ReactUpdateQueue.js\n5322982: node_modules/react/lib/LinkedValueUtils.js\n5098804: node_modules/react/lib/shouldUpdateReactComponent.js\n5379029: node_modules/react/lib/ReactDOMOption.js\n5058027: node_modules/react/lib/ReactEmptyComponent.js\n5382256: node_modules/react/lib/ReactDOMSelect.js\n4623734: node_modules/react/lib/ReactNativeComponent.js\n5152586: node_modules/react/lib/flattenChildren.js\n5405374: node_modules/react/lib/ReactDOMTextarea.js\n5134927: node_modules/react/lib/ReactMultiChild.js\n77987568: node_modules/react/node_modules/fbjs/lib/shallowEqual.js\n77629907: node_modules/react/lib/validateDOMNesting.js\n5097145: node_modules/react/lib/ReactComponentEnvironment.js\n5147906: node_modules/react/lib/ReactChildReconciler.js\n5061495: node_modules/react/lib/instantiateReactComponent.js\n77503986: node_modules/react/lib/ReactDOMEmptyComponent.js\n5065922: node_modules/react/lib/ReactCompositeComponent.js\n77510993: node_modules/react/lib/ReactDOMTreeTraversal.js\n4941549: node_modules/react/lib/ReactDOMTextComponent.js\n5271987: node_modules/react/lib/ReactDefaultBatchingStrategy.js\n4567499: node_modules/react/lib/ReactInstanceMap.js\n77542853: node_modules/react/lib/ReactNodeTypes.js\n5411190: node_modules/react/lib/ReactEventListener.js\n4559113: node_modules/react/lib/ReactUpdateQueue.js\n77676891: node_modules/react/node_modules/fbjs/lib/EventListener.js\n5098804: node_modules/react/lib/shouldUpdateReactComponent.js\n5058027: node_modules/react/lib/ReactEmptyComponent.js\n77934933: node_modules/react/node_modules/fbjs/lib/getUnboundedScrollPosition.js\n4623734: node_modules/react/lib/ReactNativeComponent.js\n5416577: node_modules/react/lib/ReactInjection.js\n5152586: node_modules/react/lib/flattenChildren.js\n5426571: node_modules/react/lib/ReactReconcileTransaction.js\n5447146: node_modules/react/lib/ReactInputSelection.js\n77987568: node_modules/react/node_modules/fbjs/lib/shallowEqual.js\n5451456: node_modules/react/lib/ReactDOMSelection.js\n77629907: node_modules/react/lib/validateDOMNesting.js\n5458269: node_modules/react/lib/getNodeForCharacterOffset.js\n77503986: node_modules/react/lib/ReactDOMEmptyComponent.js\n77836612: node_modules/react/node_modules/fbjs/lib/containsNode.js\n77951977: node_modules/react/node_modules/fbjs/lib/isTextNode.js\n77510993: node_modules/react/lib/ReactDOMTreeTraversal.js\n77950580: node_modules/react/node_modules/fbjs/lib/isNode.js\n4941549: node_modules/react/lib/ReactDOMTextComponent.js\n77913930: node_modules/react/node_modules/fbjs/lib/getActiveElement.js\n5530086: node_modules/react/lib/SVGDOMPropertyConfig.js\n5271987: node_modules/react/lib/ReactDefaultBatchingStrategy.js\n3889367: src/flux/stores/account-store.js\n5411190: node_modules/react/lib/ReactEventListener.js\n77676891: node_modules/react/node_modules/fbjs/lib/EventListener.js\n77934933: node_modules/react/node_modules/fbjs/lib/getUnboundedScrollPosition.js\n5416577: node_modules/react/lib/ReactInjection.js\n65006241: node_modules/keytar/package.json\n5426571: node_modules/react/lib/ReactReconcileTransaction.js\n65004180: node_modules/keytar/lib/keytar.js\n5447146: node_modules/react/lib/ReactInputSelection.js\n5451456: node_modules/react/lib/ReactDOMSelection.js\n5458269: node_modules/react/lib/getNodeForCharacterOffset.js\n5474425: node_modules/react/lib/SelectEventPlugin.js\n77836612: node_modules/react/node_modules/fbjs/lib/containsNode.js\n77951977: node_modules/react/node_modules/fbjs/lib/isTextNode.js\n77950580: node_modules/react/node_modules/fbjs/lib/isNode.js\n5480910: node_modules/react/lib/SimpleEventPlugin.js\n77913930: node_modules/react/node_modules/fbjs/lib/getActiveElement.js\n5530086: node_modules/react/lib/SVGDOMPropertyConfig.js\n77602647: node_modules/react/lib/SyntheticAnimationEvent.js\n5499613: node_modules/react/lib/SyntheticClipboardEvent.js\n5500825: node_modules/react/lib/SyntheticFocusEvent.js\n5508624: node_modules/react/lib/SyntheticKeyboardEvent.js\n5511371: node_modules/react/lib/getEventCharCode.js\n5519469: node_modules/react/lib/getEventKey.js\n5522372: node_modules/react/lib/SyntheticDragEvent.js\n5526795: node_modules/react/lib/SyntheticTouchEvent.js\n77603899: node_modules/react/lib/SyntheticTransitionEvent.js\n5528111: node_modules/react/lib/SyntheticWheelEvent.js\n5474425: node_modules/react/lib/SelectEventPlugin.js\n5480910: node_modules/react/lib/SimpleEventPlugin.js\n4998763: node_modules/react/lib/ReactMount.js\n77602647: node_modules/react/lib/SyntheticAnimationEvent.js\n5499613: node_modules/react/lib/SyntheticClipboardEvent.js\n5500825: node_modules/react/lib/SyntheticFocusEvent.js\n77501092: node_modules/react/lib/ReactDOMContainerInfo.js\n77509290: node_modules/react/lib/ReactDOMFeatureFlags.js\n5508624: node_modules/react/lib/SyntheticKeyboardEvent.js\n5058770: node_modules/react/lib/ReactMarkupChecksum.js\n5511371: node_modules/react/lib/getEventCharCode.js\n5519469: node_modules/react/lib/getEventKey.js\n5060281: node_modules/react/lib/adler32.js\n5269677: node_modules/react/lib/findDOMNode.js\n5522372: node_modules/react/lib/SyntheticDragEvent.js\n5526795: node_modules/react/lib/SyntheticTouchEvent.js\n77616438: node_modules/react/lib/getNativeComponentFromComposite.js\n77627726: node_modules/react/lib/renderSubtreeIntoContainer.js\n77603899: node_modules/react/lib/SyntheticTransitionEvent.js\n5528111: node_modules/react/lib/SyntheticWheelEvent.js\n4998763: node_modules/react/lib/ReactMount.js\n77501092: node_modules/react/lib/ReactDOMContainerInfo.js\n77509290: node_modules/react/lib/ReactDOMFeatureFlags.js\n5058770: node_modules/react/lib/ReactMarkupChecksum.js\n5060281: node_modules/react/lib/adler32.js\n5269677: node_modules/react/lib/findDOMNode.js\n77616438: node_modules/react/lib/getNativeComponentFromComposite.js\n77627726: node_modules/react/lib/renderSubtreeIntoContainer.js\n5634095: src/global/nylas-component-kit.js\n3925080: src/flux/models/label.js\n5634095: src/global/nylas-component-kit.js\n3930438: src/flux/models/folder.js\n3930638: src/flux/models/thread.js\n6135910: src/flux/stores/popover-store.js\n91364936: src/components/fixed-popover.js\n4096909: src/flux/models/calendar.js\n5951047: src/flux/stores/badge-store.js\n5726955: src/flux/stores/focused-perspective-store.js\n5744701: src/flux/stores/workspace-store.js\n5932960: src/flux/stores/metadata-store.js\n4648657: src/flux/stores/category-store.js\n6135910: src/flux/stores/popover-store.js\n9942964: src/flux/stores/nylas-sync-status-store.js\n91364936: src/components/fixed-popover.js\n5541756: src/flux/stores/undo-redo-store.js\n5614032: src/flux/stores/mail-rules-store.js\n4098495: src/flux/models/json-blob.js\n5592720: src/flux/tasks/reprocess-mail-rules-task.js\n5605792: src/mail-rules-processor.js\n5621142: src/mail-rules-templates.js\n5629111: src/components/scenario-editor-models.js\n5932960: src/flux/stores/metadata-store.js\n5541756: src/flux/stores/undo-redo-store.js\n5941725: src/flux/stores/file-upload-store.js\n5614032: src/flux/stores/mail-rules-store.js\n5592720: src/flux/tasks/reprocess-mail-rules-task.js\n1894875: node_modules/mkdirp/package.json\n1896279: node_modules/mkdirp/index.js\n5605792: src/mail-rules-processor.js\n5621142: src/mail-rules-templates.js\n5629111: src/components/scenario-editor-models.js\n5941725: src/flux/stores/file-upload-store.js\n4100089: src/mailbox-perspective.js\n1894875: node_modules/mkdirp/package.json\n1896279: node_modules/mkdirp/index.js\n4120446: src/flux/tasks/task-factory.js\n4139229: src/flux/tasks/change-folder-task.js\n4156736: src/flux/tasks/change-mail-task.js\n4636283: src/flux/tasks/syncback-category-task.js\n5273907: src/flux/tasks/change-labels-task.js\n5304677: src/flux/tasks/change-unread-task.js\n5341333: src/flux/tasks/change-starred-task.js\n5350052: src/flux/stores/outbox-store.js\n5960988: src/flux/stores/file-download-store.js\n5356198: src/flux/tasks/send-draft-task.js\n5417961: src/sound-registry.js\n5975123: node_modules/request-progress/package.json\n91943581: src/flux/tasks/base-draft-task.js\n5976554: node_modules/request-progress/index.js\n5978608: node_modules/request-progress/node_modules/throttleit/index.js\n5419733: src/flux/tasks/syncback-metadata-task.js\n5431422: src/flux/tasks/syncback-model-task.js\n91991403: src/flux/tasks/notify-plugins-of-send-task.js\n91821594: src/flux/edgehill-api.js\n5960988: src/flux/stores/file-download-store.js\n5975123: node_modules/request-progress/package.json\n5976554: node_modules/request-progress/index.js\n5471010: src/flux/stores/task-queue-status-store.js\n5978608: node_modules/request-progress/node_modules/throttleit/index.js\n5986780: src/flux/stores/preferences-ui-store.js\n5512912: src/flux/stores/thread-counts-store.js\n5990595: node_modules/immutable/package.json\n5992964: node_modules/immutable/dist/immutable.js\n91919008: src/flux/stores/recently-read-store.js\n5501930: src/flux/models/mutable-query-subscription.js\n91852488: src/flux/models/unread-query-subscription.js\n5986780: src/flux/stores/preferences-ui-store.js\n5990595: node_modules/immutable/package.json\n5992964: node_modules/immutable/dist/immutable.js\n5680865: src/flux/stores/draft-store.js\n5704584: src/flux/stores/draft-editing-session.js\n5886976: src/extension-registry.js\n5895610: src/extensions/composer-extension-adapter.js\n5860903: src/dom-utils.js\n5913732: src/extensions/extension-utils.js\n5915158: src/extensions/message-view-extension-adapter.js\n91896596: src/flux/stores/draft-factory.js\n5714320: src/flux/stores/contact-store.js\n5723311: src/flux/stores/contact-ranking-store.js\n6183183: src/flux/stores/message-body-processor.js\n5523481: src/window-bridge.js\n5979358: src/flux/stores/focused-contacts-store.js\n5917902: src/flux/stores/message-store.js\n6153847: src/flux/stores/searchable-component-store.js\n5733766: src/flux/stores/focused-content-store.js\n94875052: src/searchable-components/search-constants.js\n3672501: keymaps/base.json\n6183183: src/flux/stores/message-body-processor.js\n5756110: src/services/inline-style-transformer.js\n5979358: src/flux/stores/focused-contacts-store.js\n5758660: src/services/sanitize-transformer.js\n5763630: node_modules/sanitize-html/package.json\n6153847: src/flux/stores/searchable-component-store.js\n5765975: node_modules/sanitize-html/index.js\n94875052: src/searchable-components/search-constants.js\n5775765: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/index.js\n5777529: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/Parser.js\n3672501: keymaps/base.json\n5785458: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/lib/Tokenizer.js\n3674831: keymaps/base-darwin.json\n5810958: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/lib/decode_codepoint.js\n5811580: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/decode.json\n3675274: keymaps/templates/Gmail.json\n5811878: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/entities.json\n5852494: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/legacy.json\n5854241: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/entities/maps/xml.json\n3675274: keymaps/templates/Gmail.json\n3674831: keymaps/base-darwin.json\n5854294: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/index.js\n3682264: src/less-compile-cache.js\n5858733: node_modules/juice/node_modules/cheerio/node_modules/css-select/node_modules/domutils/node_modules/domelementtype/index.js\n3675274: keymaps/templates/Gmail.json\n3684143: node_modules/less-cache/lib/less-cache.js\n5859144: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/lib/node.js\n5860059: node_modules/juice/node_modules/cheerio/node_modules/htmlparser2/node_modules/domhandler/lib/element.js\n3693157: node_modules/less-cache/node_modules/less/lib/less/fs.js\n5860502: node_modules/sanitize-html/node_modules/regexp-quote/regexp-quote.js\n3693264: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/graceful-fs.js\n5860597: src/flux/models/message-utils.js\n3696362: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/fs.js\n92019120: src/flux/tasks/syncback-draft-files-task.js\n92064500: src/multi-request-progress-monitor.js\n5555349: src/flux/tasks/destroy-draft-task.js\n3675274: keymaps/templates/Gmail.json\n3682264: src/less-compile-cache.js\n3684143: node_modules/less-cache/lib/less-cache.js\n6145019: src/flux/stores/modal-store.js\n3696740: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/polyfills.js\n3693157: node_modules/less-cache/node_modules/less/lib/less/fs.js\n4488696: node_modules/react/package.json\n3693264: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/graceful-fs.js\n4490634: node_modules/react/react.js\n4490690: node_modules/react/lib/React.js\n3696362: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/fs.js\n79997473: node_modules/react/node_modules/object-assign/index.js\n3703233: node_modules/jasmine-tagged/node_modules/jasmine-focused/node_modules/walkdir/walkdir.js\n4503558: node_modules/react/lib/ReactChildren.js\n4509426: node_modules/react/lib/PooledClass.js\n77945402: node_modules/react/node_modules/fbjs/lib/invariant.js\n4515504: node_modules/react/lib/ReactElement.js\n4526119: node_modules/react/lib/ReactCurrentOwner.js\n77997237: node_modules/react/node_modules/fbjs/lib/warning.js\n77865983: node_modules/react/node_modules/fbjs/lib/emptyFunction.js\n3696740: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/polyfills.js\n77610157: node_modules/react/lib/canDefineProperty.js\n4526776: node_modules/react/lib/traverseAllChildren.js\n4549078: node_modules/react/lib/getIteratorFn.js\n3703233: node_modules/jasmine-tagged/node_modules/jasmine-focused/node_modules/walkdir/walkdir.js\n4554180: node_modules/react/lib/ReactComponent.js\n77543851: node_modules/react/lib/ReactNoopUpdateQueue.js\n77541351: node_modules/react/lib/ReactInstrumentation.js\n77520716: node_modules/react/lib/ReactDebugTool.js\n77541813: node_modules/react/lib/ReactInvalidSetStateWarningDevTool.js\n77868121: node_modules/react/node_modules/fbjs/lib/emptyObject.js\n4658325: node_modules/react/lib/ReactClass.js\n4622568: node_modules/react/lib/ReactPropTypeLocations.js\n77955086: node_modules/react/node_modules/fbjs/lib/keyMirror.js\n4623120: node_modules/react/lib/ReactPropTypeLocationNames.js\n77961237: node_modules/react/node_modules/fbjs/lib/keyOf.js\n77505957: node_modules/react/lib/ReactDOMFactories.js\n4611818: node_modules/react/lib/ReactElementValidator.js\n77963437: node_modules/react/node_modules/fbjs/lib/mapObject.js\n5328125: node_modules/react/lib/ReactPropTypes.js\n77568528: node_modules/react/lib/ReactVersion.js\n5544770: node_modules/react/lib/onlyChild.js\n75786540: node_modules/react-dom/package.json\n75786477: node_modules/react-dom/index.js\n4693093: node_modules/react/lib/ReactDOM.js\n77494923: node_modules/react/lib/ReactDOMComponentTree.js\n4954924: node_modules/react/lib/DOMProperty.js\n77494452: node_modules/react/lib/ReactDOMComponentFlags.js\n5154239: node_modules/react/lib/ReactDefaultInjection.js\n5157979: node_modules/react/lib/BeforeInputEventPlugin.js\n4501390: node_modules/react/lib/EventConstants.js\n5171831: node_modules/react/lib/EventPropagators.js\n5034693: node_modules/react/lib/EventPluginHub.js\n5042578: node_modules/react/lib/EventPluginRegistry.js\n4493310: node_modules/react/lib/EventPluginUtils.js\n4684789: node_modules/react/lib/ReactErrorUtils.js\n5051772: node_modules/react/lib/accumulateInto.js\n5053515: node_modules/react/lib/forEachAccumulated.js\n77682133: node_modules/react/node_modules/fbjs/lib/ExecutionEnvironment.js\n5177151: node_modules/react/lib/FallbackCompositionState.js\n5179621: node_modules/react/lib/getTextContentAccessor.js\n5185473: node_modules/react/lib/SyntheticCompositionEvent.js\n5186617: node_modules/react/lib/SyntheticEvent.js\n5236171: node_modules/react/lib/SyntheticInputEvent.js\n5241892: node_modules/react/lib/ChangeEventPlugin.js\n4571389: node_modules/react/lib/ReactUpdates.js\n4580474: node_modules/react/lib/CallbackQueue.js\n77540690: node_modules/react/lib/ReactFeatureFlags.js\n4583141: node_modules/react/lib/ReactPerf.js\n4590060: node_modules/react/lib/ReactReconciler.js\n4599068: node_modules/react/lib/ReactRef.js\n4608257: node_modules/react/lib/ReactOwner.js\n4626735: node_modules/react/lib/Transaction.js\n5230693: node_modules/react/lib/getEventTarget.js\n5056054: node_modules/react/lib/isEventSupported.js\n5253386: node_modules/react/lib/isTextInputElement.js\n5254419: node_modules/react/lib/DefaultEventPluginOrder.js\n5255683: node_modules/react/lib/EnterLeaveEventPlugin.js\n5259147: node_modules/react/lib/SyntheticMouseEvent.js\n5261327: node_modules/react/lib/SyntheticUIEvent.js\n5055413: node_modules/react/lib/ViewportMetrics.js\n5262952: node_modules/react/lib/getEventModifierState.js\n5264226: node_modules/react/lib/HTMLDOMPropertyConfig.js\n4965198: node_modules/react/lib/ReactComponentBrowserEnvironment.js\n4981199: node_modules/react/lib/DOMChildrenOperations.js\n77469073: node_modules/react/lib/DOMLazyTree.js\n77613716: node_modules/react/lib/createMicrosoftUnsafeLocalFunction.js\n4994328: node_modules/react/lib/setTextContent.js\n4964346: node_modules/react/lib/escapeTextContentForBrowser.js\n4995531: node_modules/react/lib/setInnerHTML.js\n4986498: node_modules/react/lib/Danger.js\n77856589: node_modules/react/node_modules/fbjs/lib/createNodesFromMarkup.js\n77848650: node_modules/react/node_modules/fbjs/lib/createArrayFromMixed.js\n77922333: node_modules/react/node_modules/fbjs/lib/getMarkupWrap.js\n4993464: node_modules/react/lib/ReactMultiChildUpdateTypes.js\n4966626: node_modules/react/lib/ReactDOMIDOperations.js\n5100253: node_modules/react/lib/ReactDOMComponent.js\n77468440: node_modules/react/lib/AutoFocusUtils.js\n77909940: node_modules/react/node_modules/fbjs/lib/focusNode.js\n4967813: node_modules/react/lib/CSSPropertyOperations.js\n4974604: node_modules/react/lib/CSSProperty.js\n77831606: node_modules/react/node_modules/fbjs/lib/camelizeStyleName.js\n77830175: node_modules/react/node_modules/fbjs/lib/camelize.js\n4978298: node_modules/react/lib/dangerousStyleValue.js\n77943412: node_modules/react/node_modules/fbjs/lib/hyphenateStyleName.js\n77941796: node_modules/react/node_modules/fbjs/lib/hyphenate.js\n77967845: node_modules/react/node_modules/fbjs/lib/memoizeStringOnly.js\n77472180: node_modules/react/lib/DOMNamespaces.js\n4947570: node_modules/react/lib/DOMPropertyOperations.js\n77509750: node_modules/react/lib/ReactDOMInstrumentation.js\n77502083: node_modules/react/lib/ReactDOMDebugTool.js\n77514692: node_modules/react/lib/ReactDOMUnknownPropertyDevtool.js\n4963597: node_modules/react/lib/quoteAttributeValueForBrowser.js\n5022146: node_modules/react/lib/ReactBrowserEventEmitter.js\n5054412: node_modules/react/lib/ReactEventEmitterMixin.js\n77617819: node_modules/react/lib/getVendorPrefixedEventName.js\n5303529: node_modules/react/lib/ReactDOMButton.js\n5313340: node_modules/react/lib/ReactDOMInput.js\n5322982: node_modules/react/lib/LinkedValueUtils.js\n5379029: node_modules/react/lib/ReactDOMOption.js\n5382256: node_modules/react/lib/ReactDOMSelect.js\n5405374: node_modules/react/lib/ReactDOMTextarea.js\n5134927: node_modules/react/lib/ReactMultiChild.js\n5097145: node_modules/react/lib/ReactComponentEnvironment.js\n5147906: node_modules/react/lib/ReactChildReconciler.js\n5061495: node_modules/react/lib/instantiateReactComponent.js\n5065922: node_modules/react/lib/ReactCompositeComponent.js\n4567499: node_modules/react/lib/ReactInstanceMap.js\n77542853: node_modules/react/lib/ReactNodeTypes.js\n4559113: node_modules/react/lib/ReactUpdateQueue.js\n5098804: node_modules/react/lib/shouldUpdateReactComponent.js\n5058027: node_modules/react/lib/ReactEmptyComponent.js\n4623734: node_modules/react/lib/ReactNativeComponent.js\n5152586: node_modules/react/lib/flattenChildren.js\n77987568: node_modules/react/node_modules/fbjs/lib/shallowEqual.js\n77629907: node_modules/react/lib/validateDOMNesting.js\n77503986: node_modules/react/lib/ReactDOMEmptyComponent.js\n77510993: node_modules/react/lib/ReactDOMTreeTraversal.js\n4941549: node_modules/react/lib/ReactDOMTextComponent.js\n5271987: node_modules/react/lib/ReactDefaultBatchingStrategy.js\n5411190: node_modules/react/lib/ReactEventListener.js\n77676891: node_modules/react/node_modules/fbjs/lib/EventListener.js\n77934933: node_modules/react/node_modules/fbjs/lib/getUnboundedScrollPosition.js\n5416577: node_modules/react/lib/ReactInjection.js\n5426571: node_modules/react/lib/ReactReconcileTransaction.js\n5447146: node_modules/react/lib/ReactInputSelection.js\n5451456: node_modules/react/lib/ReactDOMSelection.js\n5458269: node_modules/react/lib/getNodeForCharacterOffset.js\n77836612: node_modules/react/node_modules/fbjs/lib/containsNode.js\n77951977: node_modules/react/node_modules/fbjs/lib/isTextNode.js\n77950580: node_modules/react/node_modules/fbjs/lib/isNode.js\n77913930: node_modules/react/node_modules/fbjs/lib/getActiveElement.js\n5530086: node_modules/react/lib/SVGDOMPropertyConfig.js\n5474425: node_modules/react/lib/SelectEventPlugin.js\n5480910: node_modules/react/lib/SimpleEventPlugin.js\n77602647: node_modules/react/lib/SyntheticAnimationEvent.js\n5499613: node_modules/react/lib/SyntheticClipboardEvent.js\n5500825: node_modules/react/lib/SyntheticFocusEvent.js\n5508624: node_modules/react/lib/SyntheticKeyboardEvent.js\n5511371: node_modules/react/lib/getEventCharCode.js\n5519469: node_modules/react/lib/getEventKey.js\n5522372: node_modules/react/lib/SyntheticDragEvent.js\n5526795: node_modules/react/lib/SyntheticTouchEvent.js\n77603899: node_modules/react/lib/SyntheticTransitionEvent.js\n5528111: node_modules/react/lib/SyntheticWheelEvent.js\n4998763: node_modules/react/lib/ReactMount.js\n77501092: node_modules/react/lib/ReactDOMContainerInfo.js\n77509290: node_modules/react/lib/ReactDOMFeatureFlags.js\n5058770: node_modules/react/lib/ReactMarkupChecksum.js\n5060281: node_modules/react/lib/adler32.js\n5269677: node_modules/react/lib/findDOMNode.js\n77616438: node_modules/react/lib/getNativeComponentFromComposite.js\n77627726: node_modules/react/lib/renderSubtreeIntoContainer.js\n5634095: src/global/nylas-component-kit.js\n6135910: src/flux/stores/popover-store.js\n91364936: src/components/fixed-popover.js\n5932960: src/flux/stores/metadata-store.js\n5541756: src/flux/stores/undo-redo-store.js\n5614032: src/flux/stores/mail-rules-store.js\n5592720: src/flux/tasks/reprocess-mail-rules-task.js\n5605792: src/mail-rules-processor.js\n5621142: src/mail-rules-templates.js\n5629111: src/components/scenario-editor-models.js\n5941725: src/flux/stores/file-upload-store.js\n1894875: node_modules/mkdirp/package.json\n1896279: node_modules/mkdirp/index.js\n5960988: src/flux/stores/file-download-store.js\n5975123: node_modules/request-progress/package.json\n5976554: node_modules/request-progress/index.js\n3708754: static/index.less\n5978608: node_modules/request-progress/node_modules/throttleit/index.js\n3709902: static/variables/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3719008: static/normalize.less\n3726688: static/type.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3731800: static/inputs.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3733216: static/buttons.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3737856: static/dropdowns.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3738091: static/workspace.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3746538: static/resizable.less\n3747129: static/selection.less\n3747443: static/utilities.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3708754: static/index.less\n3717773: static/variables/ui-mixins.less\n3748613: static/components/popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3750797: static/components/menu.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3752952: static/components/switch.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3753558: static/components/tokenizing-text-field.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3759326: static/components/extra.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3761494: static/components/list-tabular.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3765991: static/components/disclosure-triangle.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3709902: static/variables/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3717773: static/variables/ui-mixins.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3766478: static/components/button-dropdown.less\n3719008: static/normalize.less\n3733216: static/buttons.less\n3726688: static/type.less\n3770706: static/components/scroll-region.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3731800: static/inputs.less\n3773921: static/components/spinner.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3733216: static/buttons.less\n3774954: static/components/generated-form.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3776905: static/components/unsafe.less\n3717773: static/variables/ui-mixins.less\n3737856: static/dropdowns.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3738091: static/workspace.less\n3777342: static/components/key-commands-region.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3777420: static/components/contenteditable.less\n3746538: static/resizable.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3747129: static/selection.less\n3781503: static/components/editable-list.less\n3747443: static/utilities.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3784402: static/components/outline-view.less\n3717773: static/variables/ui-mixins.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3748613: static/components/popover.less\n3717773: static/variables/ui-mixins.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3750797: static/components/menu.less\n3787888: static/components/fixed-popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3790891: static/components/modal.less\n3752952: static/components/switch.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791241: static/components/date-input.less\n3753558: static/components/tokenizing-text-field.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97491765: static/components/nylas-calendar.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97490322: static/components/empty-list-state.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489398: static/components/date-picker.less\n3759326: static/components/extra.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97499252: static/components/time-picker.less\n3717773: static/variables/ui-mixins.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3761494: static/components/list-tabular.less\n97498180: static/components/table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489787: static/components/editable-table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3765991: static/components/disclosure-triangle.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3766478: static/components/button-dropdown.less\n3733216: static/buttons.less\n3770706: static/components/scroll-region.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3773921: static/components/spinner.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3774954: static/components/generated-form.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3776905: static/components/unsafe.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3777342: static/components/key-commands-region.less\n3777420: static/components/contenteditable.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3781503: static/components/editable-list.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3784402: static/components/outline-view.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3787888: static/components/fixed-popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3790891: static/components/modal.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791241: static/components/date-input.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n5986780: src/flux/stores/preferences-ui-store.js\n97491765: static/components/nylas-calendar.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97490322: static/components/empty-list-state.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489398: static/components/date-picker.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97499252: static/components/time-picker.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97498180: static/components/table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489787: static/components/editable-table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n5990595: node_modules/immutable/package.json\n5992964: node_modules/immutable/dist/immutable.js\n3791399: static/email-frame.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791399: static/email-frame.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n6330626: src/sheet-container.js\n75770892: node_modules/react-addons-css-transition-group/index.js\n6285435: node_modules/react/lib/ReactCSSTransitionGroup.js\n6288370: node_modules/react/lib/ReactTransitionGroup.js\n6294511: node_modules/react/lib/ReactTransitionChildMapping.js\n6300645: node_modules/react/lib/ReactCSSTransitionGroupChild.js\n77653837: node_modules/react/node_modules/fbjs/lib/CSSCore.js\n6305123: node_modules/react/lib/ReactTransitionEvents.js\n6336675: src/sheet.js\n3795009: internal_packages/send-later/package.json\n6259077: src/component-registry.js\n956764: package.json\n5658995: src/components/retina-img.js\n6345466: src/components/flexbox.js\n6347603: src/components/injected-component-set.js\n6355674: src/components/unsafe-component.js\n6361626: src/components/injected-component-label.js\n6363606: src/components/resizable-region.js\n6372438: src/sheet-toolbar.js\n6183183: src/flux/stores/message-body-processor.js\n3795593: internal_packages/nylas-private-analytics/lib/main.js\n5979358: src/flux/stores/focused-contacts-store.js\n3795926: internal_packages/nylas-private-fonts/stylesheets/nylas-fonts.less\n3709902: static/variables/ui-variables.less\n3797131: internal_packages/nylas-private-fonts/lib/main.js\n6153847: src/flux/stores/searchable-component-store.js\n3797276: internal_packages/nylas-private-sounds/lib/main.js\n94875052: src/searchable-components/search-constants.js\n6217392: internal_packages/screenshot-mode/lib/main.js\n3672501: keymaps/base.json\n38810714: internal_packages/thread-search-index/lib/main.js\n38818707: internal_packages/thread-search-index/lib/search-index-store.js\n6218172: internal_packages/deltas/lib/main.js\n6218486: internal_packages/deltas/lib/account-delta-connection-pool.js\n6226311: internal_packages/deltas/lib/nylas-long-connection.js\n3674831: keymaps/base-darwin.json\n6234072: internal_packages/deltas/lib/account-delta-connection.js\n3675274: keymaps/templates/Gmail.json\n6249716: internal_packages/deltas/lib/contact-rankings-cache.js\n6251800: internal_packages/deltas/lib/refreshing-json-cache.js\n6254175: internal_packages/worker-ui/stylesheets/worker-ui.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n6258340: internal_packages/worker-ui/lib/main.js\n3675274: keymaps/templates/Gmail.json\n3682264: src/less-compile-cache.js\n6259077: src/component-registry.js\n3684143: node_modules/less-cache/lib/less-cache.js\n6267062: internal_packages/worker-ui/lib/developer-bar.js\n3693157: node_modules/less-cache/node_modules/less/lib/less/fs.js\n3693264: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/graceful-fs.js\n3696362: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/fs.js\n6311954: internal_packages/worker-ui/lib/developer-bar-store.js\n6318392: internal_packages/worker-ui/lib/developer-bar-task.js\n6322329: node_modules/classnames/package.json\n6323631: node_modules/classnames/index.js\n6324643: internal_packages/worker-ui/lib/developer-bar-curl-item.js\n3696740: node_modules/less-cache/node_modules/less/node_modules/graceful-fs/polyfills.js\n6327819: internal_packages/worker-ui/lib/developer-bar-long-poll-item.js\n6330626: src/sheet-container.js\n3703233: node_modules/jasmine-tagged/node_modules/jasmine-focused/node_modules/walkdir/walkdir.js\n75770892: node_modules/react-addons-css-transition-group/index.js\n6285435: node_modules/react/lib/ReactCSSTransitionGroup.js\n6288370: node_modules/react/lib/ReactTransitionGroup.js\n6294511: node_modules/react/lib/ReactTransitionChildMapping.js\n6300645: node_modules/react/lib/ReactCSSTransitionGroupChild.js\n77653837: node_modules/react/node_modules/fbjs/lib/CSSCore.js\n6305123: node_modules/react/lib/ReactTransitionEvents.js\n6336675: src/sheet.js\n5658995: src/components/retina-img.js\n6345466: src/components/flexbox.js\n6347603: src/components/injected-component-set.js\n6355674: src/components/unsafe-component.js\n6361626: src/components/injected-component-label.js\n6363606: src/components/resizable-region.js\n6372438: src/sheet-toolbar.js\n6388649: internal_packages/nylas-private-analytics/lib/analytics-store.js\n6395255: internal_packages/nylas-private-analytics/node_modules/underscore/package.json\n6397211: internal_packages/nylas-private-analytics/node_modules/underscore/underscore.js\n6450130: internal_packages/nylas-private-analytics/node_modules/mixpanel/package.json\n6451584: internal_packages/nylas-private-analytics/node_modules/mixpanel/lib/mixpanel-node.js\n3709902: static/variables/ui-variables.less\n3708754: static/index.less\n3709902: static/variables/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3719008: static/normalize.less\n3726688: static/type.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3731800: static/inputs.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3733216: static/buttons.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3737856: static/dropdowns.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3738091: static/workspace.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3746538: static/resizable.less\n3747129: static/selection.less\n3747443: static/utilities.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3748613: static/components/popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3750797: static/components/menu.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3752952: static/components/switch.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3753558: static/components/tokenizing-text-field.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3759326: static/components/extra.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3761494: static/components/list-tabular.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3765991: static/components/disclosure-triangle.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3766478: static/components/button-dropdown.less\n3733216: static/buttons.less\n3770706: static/components/scroll-region.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3773921: static/components/spinner.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3774954: static/components/generated-form.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3776905: static/components/unsafe.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3777342: static/components/key-commands-region.less\n3777420: static/components/contenteditable.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3781503: static/components/editable-list.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3784402: static/components/outline-view.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3787888: static/components/fixed-popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3790891: static/components/modal.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791241: static/components/date-input.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97491765: static/components/nylas-calendar.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97490322: static/components/empty-list-state.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489398: static/components/date-picker.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97499252: static/components/time-picker.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97498180: static/components/table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489787: static/components/editable-table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791399: static/email-frame.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3795009: internal_packages/send-later/package.json\n956764: package.json\n4487636: internal_packages/account-sidebar/stylesheets/account-sidebar.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n4488033: internal_packages/account-sidebar/lib/main.js\n5625206: internal_packages/account-sidebar/lib/components/account-sidebar.js\n5639191: src/components/outline-view.js\n5658995: src/components/retina-img.js\n6475049: src/components/outline-view-item.js\n6322329: node_modules/classnames/package.json\n6323631: node_modules/classnames/index.js\n6515079: src/components/disclosure-triangle.js\n6516563: src/components/drop-zone.js\n6519250: src/components/scroll-region.js\n6541041: src/components/scrollbar-ticks.js\n6345466: src/components/flexbox.js\n6259077: src/component-registry.js\n6547498: internal_packages/account-sidebar/lib/components/account-switcher.js\n6550507: internal_packages/account-sidebar/lib/account-commands.js\n6555086: internal_packages/account-sidebar/lib/sidebar-actions.js\n6555434: internal_packages/account-sidebar/lib/sidebar-store.js\n6561356: internal_packages/account-sidebar/lib/sidebar-section.js\n5568947: src/flux/tasks/destroy-category-task.js\n6568512: internal_packages/account-sidebar/lib/sidebar-item.js\n11121379: internal_packages/activity-list/stylesheets/activity-list.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n11116731: internal_packages/activity-list/lib/main.js\n11044916: internal_packages/activity-list/lib/activity-list-button.js\n11103679: internal_packages/activity-list/lib/activity-list.js\n11078805: internal_packages/activity-list/lib/activity-list-store.js\n11043296: internal_packages/activity-list/lib/activity-list-actions.js\n11119272: internal_packages/activity-list/lib/plugins.js\n11059386: internal_packages/activity-list/lib/activity-list-item-container.js\n11054031: internal_packages/activity-list/lib/activity-list-empty-state.js\n6576439: internal_packages/attachments/stylesheets/attachments.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n6581032: internal_packages/attachments/lib/main.js\n6583425: internal_packages/attachments/lib/attachment-component.js\n6602716: internal_packages/attachments/lib/image-attachment-component.js\n6621086: internal_packages/category-picker/stylesheets/category-picker.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n6622362: internal_packages/category-picker/lib/main.js\n6622941: internal_packages/category-picker/lib/category-picker.js\n6650228: src/components/key-commands-region.js\n11125195: internal_packages/category-picker/lib/category-picker-popover.js\n6662242: internal_packages/composer/stylesheets/composer.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3733216: static/buttons.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n6672270: internal_packages/composer/lib/main.js\n6688298: internal_packages/composer/lib/compose-button.js\n6693285: internal_packages/composer/lib/composer-view.js\n6801894: internal_packages/composer/lib/file-upload.js\n6810718: internal_packages/composer/lib/image-upload.js\n6817376: internal_packages/composer/lib/composer-editor.js\n15488775: internal_packages/composer/lib/composer-header.js\n6881838: internal_packages/composer/lib/account-contact-field.js\n6918854: internal_packages/composer/lib/collapsed-participants.js\n15477990: internal_packages/composer/lib/composer-header-actions.js\n6916905: internal_packages/composer/lib/fields.js\n91309256: src/components/decorators/listens-to-flux-store.js\n6854571: internal_packages/composer/lib/send-action-button.js\n15467409: internal_packages/composer/lib/action-bar-plugins.js\n15526379: internal_packages/composer/lib/decorators/inflate-draft-client-id.js\n12062174: internal_packages/composer-emoji/stylesheets/composer-emoji.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n11895302: internal_packages/composer-emoji/lib/main.js\n11884154: internal_packages/composer-emoji/lib/emoji-store.js\n11220121: internal_packages/composer-emoji/lib/emoji-actions.js\n11311506: internal_packages/composer-emoji/lib/emoji-data.js\n3708754: static/index.less\n3709902: static/variables/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3719008: static/normalize.less\n3726688: static/type.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3731800: static/inputs.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3733216: static/buttons.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3737856: static/dropdowns.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3738091: static/workspace.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3746538: static/resizable.less\n3747129: static/selection.less\n3747443: static/utilities.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3748613: static/components/popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3750797: static/components/menu.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3752952: static/components/switch.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3753558: static/components/tokenizing-text-field.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3759326: static/components/extra.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3761494: static/components/list-tabular.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3765991: static/components/disclosure-triangle.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3766478: static/components/button-dropdown.less\n3733216: static/buttons.less\n3770706: static/components/scroll-region.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3773921: static/components/spinner.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3774954: static/components/generated-form.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3776905: static/components/unsafe.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3777342: static/components/key-commands-region.less\n3777420: static/components/contenteditable.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3781503: static/components/editable-list.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3784402: static/components/outline-view.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3787888: static/components/fixed-popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3790891: static/components/modal.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791241: static/components/date-input.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97491765: static/components/nylas-calendar.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97490322: static/components/empty-list-state.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489398: static/components/date-picker.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97499252: static/components/time-picker.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97498180: static/components/table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489787: static/components/editable-table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791399: static/email-frame.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n11269567: internal_packages/composer-emoji/lib/emoji-composer-extension.js\n11874201: internal_packages/composer-emoji/lib/emoji-picker.js\n12036836: internal_packages/composer-emoji/node_modules/node-emoji/package.json\n11900180: internal_packages/composer-emoji/node_modules/node-emoji/index.js\n11900220: internal_packages/composer-emoji/node_modules/node-emoji/lib/emoji.js\n11901752: internal_packages/composer-emoji/node_modules/node-emoji/lib/emoji.json\n6209520: src/extensions/composer-extension.js\n6199449: src/extensions/contenteditable-extension.js\n11863789: internal_packages/composer-emoji/lib/emoji-message-extension.js\n6216016: src/extensions/message-view-extension.js\n11263608: internal_packages/composer-emoji/lib/emoji-button.js\n11220330: internal_packages/composer-emoji/lib/emoji-button-popover.js\n11197213: internal_packages/composer-emoji/lib/categorized-emoji.js\n12365614: internal_packages/composer-mail-merge/stylesheets/mail-merge.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n12246852: internal_packages/composer-mail-merge/lib/main.js\n12078922: internal_packages/composer-mail-merge/lib/mail-merge-button.js\n12115700: internal_packages/composer-mail-merge/lib/mail-merge-draft-editing-session.js\n12255616: internal_packages/composer-mail-merge/lib/table-data-reducer.js\n12097808: internal_packages/composer-mail-merge/lib/mail-merge-constants.js\n12365120: internal_packages/composer-mail-merge/package.json\n12275198: internal_packages/composer-mail-merge/lib/workspace-data-reducer.js\n12195717: internal_packages/composer-mail-merge/lib/mail-merge-utils.js\n12294536: internal_packages/composer-mail-merge/node_modules/papaparse/package.json\n12296719: internal_packages/composer-mail-merge/node_modules/papaparse/papaparse.js\n75788185: node_modules/react-dom/server.js\n77510224: node_modules/react/lib/ReactDOMServer.js\n5537399: node_modules/react/lib/ReactServerRendering.js\n77547198: node_modules/react/lib/ReactServerBatchingStrategy.js\n5539994: node_modules/react/lib/ReactServerRenderingTransaction.js\n12182972: internal_packages/composer-mail-merge/lib/mail-merge-token.js\n12164340: internal_packages/composer-mail-merge/lib/mail-merge-send-button.js\n12143212: internal_packages/composer-mail-merge/lib/mail-merge-participants-text-field.js\n12101370: internal_packages/composer-mail-merge/lib/mail-merge-container.js\n12224650: internal_packages/composer-mail-merge/lib/mail-merge-workspace.js\n12175665: internal_packages/composer-mail-merge/lib/mail-merge-table.js\n12133498: internal_packages/composer-mail-merge/lib/mail-merge-header-input.js\n91336318: src/components/editable-table.js\n91714196: src/components/selectable-table.js\n91306664: src/components/decorators/compose.js\n91296149: src/components/decorators/auto-focuses.js\n91318034: src/components/decorators/listens-to-movement-keys.js\n91751055: src/components/table.js\n91429095: src/components/lazy-rendered-list.js\n12089622: internal_packages/composer-mail-merge/lib/mail-merge-composer-extension.js\n12774978: internal_packages/composer-scheduler/stylesheets/scheduler.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n12597570: internal_packages/composer-scheduler/lib/main.js\n12401184: internal_packages/composer-scheduler/lib/calendar/proposed-time-event.js\n12634143: internal_packages/composer-scheduler/lib/scheduler-actions.js\n12637241: internal_packages/composer-scheduler/lib/scheduler-constants.js\n12639598: internal_packages/composer-scheduler/package.json\n12409433: internal_packages/composer-scheduler/lib/calendar/proposed-time-picker.js\n12611238: internal_packages/composer-scheduler/lib/proposed-time-calendar-store.js\n12604799: internal_packages/composer-scheduler/lib/proposal.js\n67807413: node_modules/moment-round/package.json\n67804644: node_modules/moment-round/dist/moment-round.js\n12394346: internal_packages/composer-scheduler/lib/calendar/proposed-time-calendar-data-source.js\n91490885: src/components/nylas-calendar/calendar-data-source.js\n12458476: internal_packages/composer-scheduler/lib/composer/new-event-card-container.js\n12472599: internal_packages/composer-scheduler/lib/composer/new-event-card.js\n12509765: internal_packages/composer-scheduler/lib/composer/new-event-helper.js\n12529558: internal_packages/composer-scheduler/lib/composer/proposed-time-list.js\n12435429: internal_packages/composer-scheduler/lib/composer/email-b64-images.js\n12557233: internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.js\n12577398: internal_packages/composer-scheduler/lib/composer/scheduler-composer-extension.js\n12517105: internal_packages/composer-scheduler/lib/composer/new-event-preview.js\n6940725: internal_packages/composer-signature/styles/composer-signature.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n6942112: internal_packages/composer-signature/lib/main.js\n6945805: internal_packages/composer-signature/lib/signature-composer-extension.js\n6952671: internal_packages/composer-signature/lib/signature-utils.js\n6956430: internal_packages/composer-signature/lib/signature-store.js\n12790035: internal_packages/composer-signature/lib/signature-actions.js\n6964420: internal_packages/composer-signature/lib/preferences-signatures.js\n7087046: internal_packages/composer-spellcheck/lib/main.js\n7088733: internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.js\n15649511: internal_packages/draft-list/stylesheets/draft-list.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n15640684: internal_packages/draft-list/lib/main.js\n15634286: internal_packages/draft-list/lib/draft-list.js\n10328875: src/components/flux-container.js\n91409314: src/components/focus-container.js\n91356893: src/components/empty-list-state.js\n9573988: src/components/evented-iframe.js\n9590240: src/searchable-components/searchable-component-maker.js\n9604588: src/searchable-components/virtual-dom-parser.js\n9625948: src/searchable-components/search-match.js\n9630803: src/searchable-components/unified-dom-parser.js\n9656602: src/searchable-components/iframe-searcher.js\n9660189: src/searchable-components/real-dom-parser.js\n10348470: src/components/multiselect-list.js\n10242985: src/components/list-tabular.js\n6613819: src/components/spinner.js\n10255150: src/components/list-data-source.js\n10259550: src/components/list-selection.js\n10266495: src/components/list-tabular-item.js\n10270866: src/components/swipe-container.js\n10361741: src/components/multiselect-list-interaction-handler.js\n10365624: src/components/multiselect-split-interaction-handler.js\n15622621: internal_packages/draft-list/lib/draft-list-store.js\n10317803: src/flux/stores/observable-list-data-source.js\n15613521: internal_packages/draft-list/lib/draft-list-columns.js\n6347603: src/components/injected-component-set.js\n6355674: src/components/unsafe-component.js\n6361626: src/components/injected-component-label.js\n15626659: internal_packages/draft-list/lib/draft-list-toolbar.js\n91329623: src/components/decorators/listens-to-observable.js\n15616539: internal_packages/draft-list/lib/draft-list-send-status.js\n15647651: internal_packages/draft-list/lib/sending-progress-bar.js\n15637453: internal_packages/draft-list/lib/draft-toolbar-buttons.js\n8166806: internal_packages/events/stylesheets/events.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n8168831: internal_packages/events/lib/main.js\n8169447: internal_packages/events/lib/event-header.js\n5545981: src/flux/tasks/event-rsvp-task.js\n9406822: internal_packages/link-tracking/stylesheets/main.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9407650: internal_packages/link-tracking/lib/main.js\n9411086: internal_packages/link-tracking/lib/link-tracking-button.js\n9419481: internal_packages/link-tracking/lib/link-tracking-constants.js\n9421038: internal_packages/link-tracking/package.json\n9421801: internal_packages/link-tracking/lib/link-tracking-composer-extension.js\n9433912: internal_packages/link-tracking/lib/link-tracking-message-extension.js\n9443214: internal_packages/message-autoload-images/stylesheets/message-autoload-images.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9443764: internal_packages/message-autoload-images/lib/main.js\n9447860: internal_packages/message-autoload-images/lib/autoload-images-extension.js\n9452536: internal_packages/message-autoload-images/lib/autoload-images-store.js\n9464787: internal_packages/message-autoload-images/lib/autoload-images-actions.js\n9466378: internal_packages/message-autoload-images/lib/autoload-images-header.js\n9475435: internal_packages/message-list/stylesheets/find-in-thread.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9476423: internal_packages/message-list/stylesheets/message-list.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9492060: internal_packages/message-list/lib/main.js\n9493451: internal_packages/message-list/lib/message-list.js\n9514819: internal_packages/message-list/lib/find-in-thread.js\n9533550: internal_packages/message-list/lib/message-item-container.js\n9538634: internal_packages/message-list/lib/message-item.js\n9554145: internal_packages/message-list/lib/email-frame.js\n21971387: internal_packages/message-list/lib/autolinker.js\n21985271: internal_packages/message-list/lib/autoscale-images.js\n9674883: internal_packages/message-list/lib/email-frame-styles-store.js\n9677072: internal_packages/message-list/lib/message-participants.js\n9684453: internal_packages/message-list/lib/message-item-body.js\n10181263: src/canvas-utils.js\n6774422: src/services/quoted-html-transformer.js\n6783209: src/services/quote-string-detector.js\n6789262: src/dom-walkers.js\n9691982: internal_packages/message-list/lib/message-timestamp.js\n9694947: internal_packages/message-list/lib/message-controls.js\n6876890: src/components/button-dropdown.js\n6626794: src/components/menu.js\n6791782: src/components/injected-component.js\n9704877: src/components/mail-label-set.js\n6659234: src/components/mail-label.js\n9717628: src/components/mail-important-icon.js\n9723341: internal_packages/message-list/lib/message-list-hidden-messages-toggle.js\n22004384: internal_packages/message-list/lib/sidebar-plugin-container.js\n21992491: internal_packages/message-list/lib/sidebar-participant-picker.js\n9914437: internal_packages/mode-switch/stylesheets/mode-switch.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9914642: internal_packages/mode-switch/lib/main.js\n9915888: internal_packages/mode-switch/lib/mode-toggle.js\n9918736: internal_packages/notification-mailto/lib/main.js\n9920876: src/default-client-helper.js\n9923701: internal_packages/notification-update-available/stylesheets/release-bar.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9924225: internal_packages/notification-update-available/lib/main.js\n9927264: internal_packages/notifications/stylesheets/notifications.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9931964: internal_packages/notifications/lib/main.js\n25522410: internal_packages/notifications/lib/sidebar/activity-sidebar.js\n75770954: node_modules/react-addons-css-transition-group/package.json\n75770892: node_modules/react-addons-css-transition-group/index.js\n6285435: node_modules/react/lib/ReactCSSTransitionGroup.js\n6288370: node_modules/react/lib/ReactTransitionGroup.js\n6294511: node_modules/react/lib/ReactTransitionChildMapping.js\n6300645: node_modules/react/lib/ReactCSSTransitionGroupChild.js\n77653837: node_modules/react/node_modules/fbjs/lib/CSSCore.js\n6305123: node_modules/react/lib/ReactTransitionEvents.js\n9936813: internal_packages/notifications/lib/notifications-store.js\n25534637: internal_packages/notifications/lib/sidebar/streaming-sync-activity.js\n25528307: internal_packages/notifications/lib/sidebar/initial-sync-activity.js\n25500590: internal_packages/notifications/lib/headers/connection-status-header.js\n25486624: internal_packages/notifications/lib/headers/account-error-header.js\n25515642: internal_packages/notifications/lib/headers/notifications-header.js\n25518220: internal_packages/notifications/lib/headers/notifications-item.js\n3795593: internal_packages/nylas-private-analytics/lib/main.js\n3795926: internal_packages/nylas-private-fonts/stylesheets/nylas-fonts.less\n3709902: static/variables/ui-variables.less\n3797131: internal_packages/nylas-private-fonts/lib/main.js\n3797276: internal_packages/nylas-private-sounds/lib/main.js\n9948811: internal_packages/open-tracking/stylesheets/main.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n9949921: internal_packages/open-tracking/lib/main.js\n9954068: internal_packages/open-tracking/lib/open-tracking-button.js\n9962734: internal_packages/open-tracking/lib/open-tracking-constants.js\n9964291: internal_packages/open-tracking/package.json\n9965050: internal_packages/open-tracking/lib/open-tracking-icon.js\n9974239: internal_packages/open-tracking/lib/open-tracking-message-status.js\n9983231: internal_packages/open-tracking/lib/open-tracking-composer-extension.js\n29621096: internal_packages/participant-profile/stylesheets/participant-profile.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n27770485: internal_packages/participant-profile/lib/main.js\n27771226: internal_packages/participant-profile/lib/participant-profile-store.js\n27766292: internal_packages/participant-profile/lib/clearbit-data-source.js\n27772813: internal_packages/participant-profile/lib/sidebar-participant-profile.js\n27794981: internal_packages/participant-profile/lib/sidebar-related-threads.js\n10002861: internal_packages/plugins/stylesheets/plugins.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10005819: internal_packages/plugins/lib/main.js\n10584362: internal_packages/plugins/lib/preferences-plugins.js\n10591138: internal_packages/plugins/lib/tabs-store.js\n10593603: internal_packages/plugins/lib/plugins-actions.js\n10595942: internal_packages/plugins/lib/tabs.js\n10597146: internal_packages/plugins/lib/tab-installed.js\n10612734: internal_packages/plugins/lib/package-set.js\n10619274: internal_packages/plugins/lib/package.js\n10636183: internal_packages/plugins/lib/packages-store.js\n10007988: internal_packages/preferences/stylesheets/preferences-accounts.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10010736: internal_packages/preferences/stylesheets/preferences-mail-rules.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10013552: internal_packages/preferences/stylesheets/preferences.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10018740: internal_packages/preferences/lib/main.js\n10942956: internal_packages/preferences/lib/preferences-root.js\n36888774: internal_packages/preferences/lib/preferences-tabs-bar.js\n10678179: internal_packages/preferences/lib/tabs/preferences-general.js\n10687249: internal_packages/preferences/lib/tabs/config-schema-item.js\n10702281: internal_packages/preferences/lib/tabs/workspace-section.js\n10744972: internal_packages/preferences/lib/tabs/sending-section.js\n10752957: internal_packages/preferences/lib/tabs/preferences-accounts.js\n10763596: internal_packages/preferences/lib/tabs/preferences-account-list.js\n10775702: internal_packages/preferences/lib/tabs/preferences-account-details.js\n36901668: internal_packages/preferences/lib/tabs/preferences-appearance.js\n10803666: internal_packages/preferences/lib/tabs/preferences-keymaps.js\n10835061: internal_packages/preferences/lib/tabs/preferences-mail-rules.js\n10027324: internal_packages/print/lib/main.js\n10028732: internal_packages/print/lib/printer.js\n10034622: internal_packages/print/lib/print-window.js\n36971547: internal_packages/remove-tracking-pixels/lib/main.js\n6217392: internal_packages/screenshot-mode/lib/main.js\n10043968: internal_packages/send-and-archive/styles/send-and-archive.less\n3709902: static/variables/ui-variables.less\n10043990: internal_packages/send-and-archive/lib/main.js\n10044567: internal_packages/send-and-archive/lib/send-and-archive-extension.js\n3795009: internal_packages/send-later/package.json\n10046563: internal_packages/send-later/stylesheets/send-later.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10047505: internal_packages/send-later/lib/main.js\n37038743: internal_packages/send-later/lib/send-later-button.js\n10048157: internal_packages/send-later/lib/send-later-popover.js\n10062336: src/date-utils.js\n10080490: node_modules/chrono-node/src/chrono.js\n10082187: node_modules/chrono-node/src/options.js\n10084390: node_modules/chrono-node/src/parsers/parser.js\n10087416: node_modules/chrono-node/src/parsers/EN/ENISOFormatParser.js\n10090699: node_modules/chrono-node/src/result.js\n10094393: node_modules/chrono-node/src/parsers/EN/ENDeadlineFormatParser.js\n10096163: node_modules/chrono-node/src/parsers/EN/ENMonthNameLittleEndianParser.js\n10099767: node_modules/chrono-node/src/utils/EN.js\n10100643: node_modules/chrono-node/src/parsers/EN/ENMonthNameMiddleEndianParser.js\n54657192: node_modules/chrono-node/src/parsers/EN/ENMonthNameParser.js\n10104644: node_modules/chrono-node/src/parsers/EN/ENSlashDateFormatParser.js\n10107976: node_modules/chrono-node/src/parsers/EN/ENSlashDateFormatStartWithYearParser.js\n54659809: node_modules/chrono-node/src/parsers/EN/ENSlashMonthFormatParser.js\n10109509: node_modules/chrono-node/src/parsers/EN/ENTimeAgoFormatParser.js\n10112162: node_modules/chrono-node/src/parsers/EN/ENTimeExpressionParser.js\n10119452: node_modules/chrono-node/src/parsers/EN/ENWeekdayParser.js\n10122038: node_modules/chrono-node/src/parsers/EN/ENCasualDateParser.js\n10124502: node_modules/chrono-node/src/parsers/JP/JPStandardParser.js\n10127111: node_modules/chrono-node/src/utils/JP.js\n10128540: node_modules/chrono-node/src/parsers/JP/JPCasualDateParser.js\n54661209: node_modules/chrono-node/src/parsers/ES/ESCasualDateParser.js\n54664898: node_modules/chrono-node/src/parsers/ES/ESDeadlineFormatParser.js\n54673468: node_modules/chrono-node/src/parsers/ES/ESTimeAgoFormatParser.js\n54675930: node_modules/chrono-node/src/parsers/ES/ESTimeExpressionParser.js\n54682534: node_modules/chrono-node/src/parsers/ES/ESWeekdayParser.js\n54666650: node_modules/chrono-node/src/parsers/ES/ESMonthNameLittleEndianParser.js\n54686880: node_modules/chrono-node/src/utils/ES.js\n54670068: node_modules/chrono-node/src/parsers/ES/ESSlashDateFormatParser.js\n10130127: node_modules/chrono-node/src/refiners/refiner.js\n10131338: node_modules/chrono-node/src/refiners/OverlapRemovalRefiner.js\n10132351: node_modules/chrono-node/src/refiners/ExtractTimezoneOffsetRefiner.js\n10133766: node_modules/chrono-node/src/refiners/ExtractTimezoneAbbrRefiner.js\n10137202: node_modules/chrono-node/src/refiners/UnlikelyFormatFilter.js\n10137518: node_modules/chrono-node/src/refiners/EN/ENMergeDateTimeRefiner.js\n10141811: node_modules/chrono-node/src/refiners/EN/ENMergeDateRangeRefiner.js\n10144348: node_modules/chrono-node/src/refiners/JP/JPMergeDateRangeRefiner.js\n10061936: internal_packages/send-later/lib/send-later-actions.js\n10062177: internal_packages/send-later/lib/send-later-constants.js\n3795009: internal_packages/send-later/package.json\n10144604: internal_packages/send-later/lib/send-later-store.js\n10146708: internal_packages/send-later/lib/send-later-status.js\n10154325: internal_packages/sidebar-fullcontact/stylesheets/sidebar-fullcontact.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10155637: internal_packages/sidebar-fullcontact/lib/main.js\n10156198: internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact.js\n10158666: internal_packages/sidebar-fullcontact/lib/fullcontact-store.js\n10160817: internal_packages/sidebar-fullcontact/lib/sidebar-fullcontact-details.js\n10169244: internal_packages/system-tray/lib/main.js\n10170796: internal_packages/system-tray/lib/system-tray-icon-store.js\n37317747: internal_packages/theme-picker/styles/theme-picker.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10189521: internal_packages/theme-picker/lib/main.js\n10189999: internal_packages/theme-picker/lib/theme-picker.js\n10205690: internal_packages/theme-picker/lib/theme-option.js\n38809494: internal_packages/thread-list/stylesheets/selected-items-stack.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10222854: internal_packages/thread-list/stylesheets/thread-list.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10235013: internal_packages/thread-list/lib/main.js\n10331274: internal_packages/thread-list/lib/thread-list.js\n10370299: internal_packages/thread-list/lib/thread-list-columns.js\n10379446: internal_packages/thread-list/lib/thread-list-quick-actions.js\n10383457: internal_packages/thread-list/lib/thread-list-participants.js\n10238079: internal_packages/thread-list/lib/thread-list-store.js\n10313585: internal_packages/thread-list/lib/thread-list-data-source.js\n10389852: internal_packages/thread-list/lib/thread-list-icon.js\n10393161: internal_packages/thread-list/lib/thread-list-scroll-tooltip.js\n10395728: internal_packages/thread-list/lib/thread-list-context-menu.js\n10414166: internal_packages/thread-list/lib/category-removal-target-rulesets.js\n38750037: internal_packages/thread-list/lib/thread-list-toolbar.js\n38717639: internal_packages/thread-list/lib/injects-toolbar-buttons.js\n38726612: internal_packages/thread-list/lib/message-list-toolbar.js\n38735117: internal_packages/thread-list/lib/selected-items-stack.js\n38756427: internal_packages/thread-list/lib/thread-toolbar-buttons.js\n38900474: internal_packages/thread-search/stylesheets/search-bar.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n38846525: internal_packages/thread-search/lib/main.js\n38848780: internal_packages/thread-search/lib/search-bar.js\n38890613: internal_packages/thread-search/lib/search-store.js\n38848436: internal_packages/thread-search/lib/search-actions.js\n38857942: internal_packages/thread-search/lib/search-mailbox-perspective.js\n38869907: internal_packages/thread-search/lib/search-query-subscription.js\n10422956: internal_packages/thread-snooze/stylesheets/snooze-mail-label.less\n3709902: static/variables/ui-variables.less\n10423117: internal_packages/thread-snooze/stylesheets/snooze-popover.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10424653: internal_packages/thread-snooze/lib/main.js\n39002329: internal_packages/thread-snooze/lib/snooze-buttons.js\n10425464: internal_packages/thread-snooze/lib/snooze-popover.js\n10439168: internal_packages/thread-snooze/lib/snooze-actions.js\n10439380: internal_packages/thread-snooze/lib/snooze-mail-label.js\n10448736: internal_packages/thread-snooze/lib/snooze-constants.js\n10448947: internal_packages/thread-snooze/package.json\n10449468: internal_packages/thread-snooze/lib/snooze-utils.js\n10453553: internal_packages/thread-snooze/lib/snooze-store.js\n10456694: internal_packages/undo-redo/stylesheets/undo-redo.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n10458145: internal_packages/undo-redo/lib/main.js\n10458810: internal_packages/undo-redo/lib/undo-redo-component.js\n10463691: internal_packages/unread-notifications/lib/main.js\n10471380: src/native-notifications.js\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n6330626: src/sheet-container.js\n6336675: src/sheet.js\n6363606: src/components/resizable-region.js\n6372438: src/sheet-toolbar.js\n94914523: src/task.js\n6388649: internal_packages/nylas-private-analytics/lib/analytics-store.js\n6395255: internal_packages/nylas-private-analytics/node_modules/underscore/package.json\n6397211: internal_packages/nylas-private-analytics/node_modules/underscore/underscore.js\n6450130: internal_packages/nylas-private-analytics/node_modules/mixpanel/package.json\n6451584: internal_packages/nylas-private-analytics/node_modules/mixpanel/lib/mixpanel-node.js\n3709902: static/variables/ui-variables.less\n1973294: src/compile-cache.js\n1766806: node_modules/fs-plus/package.json\n1768751: node_modules/fs-plus/lib/fs-plus.js\n3708754: static/index.less\n3709902: static/variables/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3717937: static/mixins/common-ui-elements.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3718450: static/mixins/text-emphasis.less\n3718566: static/mixins/background-variant.less\n3718705: static/mixins/windows.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3719008: static/normalize.less\n3726688: static/type.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3731800: static/inputs.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n1789356: node_modules/fs-plus/node_modules/underscore-plus/package.json\n3733216: static/buttons.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3737856: static/dropdowns.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3738091: static/workspace.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n1791321: node_modules/fs-plus/node_modules/underscore-plus/lib/underscore-plus.js\n3746538: static/resizable.less\n3747129: static/selection.less\n3747443: static/utilities.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3748613: static/components/popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3750797: static/components/menu.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3752952: static/components/switch.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3753558: static/components/tokenizing-text-field.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3759326: static/components/extra.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3761494: static/components/list-tabular.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3765991: static/components/disclosure-triangle.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3766478: static/components/button-dropdown.less\n3733216: static/buttons.less\n3770706: static/components/scroll-region.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3773921: static/components/spinner.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3774954: static/components/generated-form.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3776905: static/components/unsafe.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3777342: static/components/key-commands-region.less\n3777420: static/components/contenteditable.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3781503: static/components/editable-list.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3784402: static/components/outline-view.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3717773: static/variables/ui-mixins.less\n3787888: static/components/fixed-popover.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3790891: static/components/modal.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n3791241: static/components/date-input.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97491765: static/components/nylas-calendar.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97490322: static/components/empty-list-state.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489398: static/components/date-picker.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97499252: static/components/time-picker.less\n1806109: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/package.json\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97498180: static/components/table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n97489787: static/components/editable-table.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n1807823: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/underscore.js\n3791399: static/email-frame.less\n3709902: static/variables/ui-variables.less\n3718416: internal_packages/ui-light/styles/ui-variables.less\n1853312: node_modules/fs-plus/node_modules/async/package.json\n1854649: node_modules/fs-plus/node_modules/async/lib/async.js\n1884050: node_modules/fs-plus/node_modules/mkdirp/package.json\n1885107: node_modules/fs-plus/node_modules/mkdirp/index.js\n1887478: node_modules/fs-plus/node_modules/rimraf/package.json\n1889121: node_modules/fs-plus/node_modules/rimraf/rimraf.js\n1978902: src/compile-support/babel.js\n1980527: static/babelrc.json\n1980671: src/compile-support/coffee-script.js\n1981853: src/compile-support/typescript.js\n1983139: node_modules/underscore/package.json\n1985095: node_modules/underscore/underscore.js\n2038014: node_modules/source-map-support/package.json\n2039642: node_modules/source-map-support/source-map-support.js\n2054178: node_modules/source-map-support/node_modules/source-map/package.json\n2057217: node_modules/source-map-support/node_modules/source-map/lib/source-map.js\n2057643: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-generator.js\n2070902: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/package.json\n2072148: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/amdefine.js\n2082064: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64-vlq.js\n2086956: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64.js\n2088093: node_modules/source-map-support/node_modules/source-map/lib/source-map/util.js\n2093422: node_modules/source-map-support/node_modules/source-map/lib/source-map/array-set.js\n2096140: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-consumer.js\n2113947: node_modules/source-map-support/node_modules/source-map/lib/source-map/binary-search.js\n2117157: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-node.js\n94911340: src/task-bootstrap.js\n2264399: node_modules/emissary/package.json\n2266273: node_modules/emissary/lib/emissary.js\n2266555: node_modules/emissary/lib/helpers.js\n2268129: node_modules/emissary/lib/behavior.js\n2272107: node_modules/emissary/node_modules/underscore-plus/package.json\n2274072: node_modules/emissary/node_modules/underscore-plus/lib/underscore-plus.js\n2288860: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/package.json\n2290574: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/underscore.js\n2336063: node_modules/property-accessors/package.json\n2337929: node_modules/property-accessors/lib/property-accessors.js\n2340201: node_modules/property-accessors/node_modules/mixto/package.json\n2341779: node_modules/property-accessors/node_modules/mixto/lib/mixin.js\n2343179: node_modules/emissary/lib/signal.js\n2352520: node_modules/emissary/lib/emitter.js\n2367779: node_modules/emissary/node_modules/mixto/package.json\n2369463: node_modules/emissary/node_modules/mixto/lib/mixin.js\n2370863: node_modules/emissary/lib/subscriber.js\n2374948: node_modules/emissary/lib/subscription.js\n4222287: node_modules/request/package.json\n4225420: node_modules/request/index.js\n80790165: node_modules/request/node_modules/extend/package.json\n80787899: node_modules/request/node_modules/extend/index.js\n4229450: node_modules/request/lib/cookies.js\n82838107: node_modules/request/node_modules/tough-cookie/package.json\n82637881: node_modules/request/node_modules/tough-cookie/lib/cookie.js\n82685639: node_modules/request/node_modules/tough-cookie/lib/pubsuffix.js\n82835266: node_modules/request/node_modules/tough-cookie/lib/store.js\n82675424: node_modules/request/node_modules/tough-cookie/lib/memstore.js\n82683373: node_modules/request/node_modules/tough-cookie/lib/permuteDomain.js\n82680938: node_modules/request/node_modules/tough-cookie/lib/pathMatch.js\n82838107: node_modules/request/node_modules/tough-cookie/package.json\n4432858: node_modules/request/lib/helpers.js\n82361121: node_modules/request/node_modules/json-stringify-safe/package.json\n82362804: node_modules/request/node_modules/json-stringify-safe/stringify.js\n4435389: node_modules/request/request.js\n80742369: node_modules/request/node_modules/bl/package.json\n80567623: node_modules/request/node_modules/bl/bl.js\n80636139: node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js\n80636191: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js\n80720291: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/package.json\n80718759: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/index.js\n80705839: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/package.json\n80702818: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/lib/util.js\n80712444: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/package.json\n80711730: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits.js\n80638643: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js\n80716737: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/package.json\n80716605: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/index.js\n80670635: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js\n80738031: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/package.json\n80737908: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/node.js\n81531974: node_modules/request/node_modules/hawk/package.json\n81345592: node_modules/request/node_modules/hawk/lib/index.js\n81424556: node_modules/request/node_modules/hawk/node_modules/boom/package.json\n81416906: node_modules/request/node_modules/hawk/node_modules/boom/lib/index.js\n81515577: node_modules/request/node_modules/hawk/node_modules/hoek/package.json\n81490930: node_modules/request/node_modules/hawk/node_modules/hoek/lib/index.js\n81488262: node_modules/request/node_modules/hawk/node_modules/hoek/lib/escape.js\n81530565: node_modules/request/node_modules/hawk/node_modules/sntp/package.json\n81520773: node_modules/request/node_modules/hawk/node_modules/sntp/index.js\n81520807: node_modules/request/node_modules/hawk/node_modules/sntp/lib/index.js\n81345973: node_modules/request/node_modules/hawk/lib/server.js\n81429631: node_modules/request/node_modules/hawk/node_modules/cryptiles/package.json\n81428267: node_modules/request/node_modules/hawk/node_modules/cryptiles/lib/index.js\n81342006: node_modules/request/node_modules/hawk/lib/crypto.js\n81364519: node_modules/request/node_modules/hawk/lib/utils.js\n81331415: node_modules/request/node_modules/hawk/lib/client.js\n80485113: node_modules/request/node_modules/aws-sign2/package.json\n80480692: node_modules/request/node_modules/aws-sign2/index.js\n82337952: node_modules/request/node_modules/http-signature/package.json\n81551606: node_modules/request/node_modules/http-signature/lib/index.js\n81552232: node_modules/request/node_modules/http-signature/lib/parser.js\n81591249: node_modules/request/node_modules/http-signature/node_modules/assert-plus/package.json\n81585919: node_modules/request/node_modules/http-signature/node_modules/assert-plus/assert.js\n81574938: node_modules/request/node_modules/http-signature/lib/utils.js\n82335507: node_modules/request/node_modules/http-signature/node_modules/sshpk/package.json\n81887698: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/index.js\n81888429: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/key.js\n81955466: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/assert-plus/package.json\n81950012: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/assert-plus/assert.js\n81830178: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/algs.js\n81847974: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/fingerprint.js\n81845672: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/errors.js\n81911179: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/utils.js\n81895828: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/private-key.js\n81901963: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/signature.js\n81943093: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/package.json\n81942773: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/index.js\n81928490: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/index.js\n81928251: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/errors.js\n81934548: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/types.js\n81928959: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/reader.js\n81935186: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/writer.js\n81908010: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ssh-buffer.js\n81843337: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ed-compat.js\n81851404: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/auto.js\n81853302: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pem.js\n81858111: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs1.js\n81865807: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs8.js\n81881346: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh-private.js\n81877740: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/rfc4253.js\n81884606: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh.js\n81835020: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/dhe.js\n81562004: node_modules/request/node_modules/http-signature/lib/signer.js\n81802301: node_modules/request/node_modules/http-signature/node_modules/jsprim/package.json\n81602307: node_modules/request/node_modules/http-signature/node_modules/jsprim/lib/jsprim.js\n81636568: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/extsprintf/package.json\n81632804: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/extsprintf/lib/extsprintf.js\n81801412: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/verror/package.json\n81797823: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/verror/lib/verror.js\n81774451: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/json-schema/package.json\n81763963: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/json-schema/lib/validate.js\n81577777: node_modules/request/node_modules/http-signature/lib/verify.js\n82533240: node_modules/request/node_modules/mime-types/package.json\n82371286: node_modules/request/node_modules/mime-types/index.js\n82530646: node_modules/request/node_modules/mime-types/node_modules/mime-db/package.json\n82530510: node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js\n82387545: node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json\n82606571: node_modules/request/node_modules/stringstream/package.json\n82607891: node_modules/request/node_modules/stringstream/stringstream.js\n80756086: node_modules/request/node_modules/caseless/package.json\n80754327: node_modules/request/node_modules/caseless/index.js\n80805500: node_modules/request/node_modules/forever-agent/package.json\n80801324: node_modules/request/node_modules/forever-agent/index.js\n80986121: node_modules/request/node_modules/form-data/package.json\n80814876: node_modules/request/node_modules/form-data/lib/form_data.js\n80777465: node_modules/request/node_modules/combined-stream/package.json\n80764215: node_modules/request/node_modules/combined-stream/lib/combined_stream.js\n80775847: node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/package.json\n80773528: node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js\n80983078: node_modules/request/node_modules/form-data/node_modules/async/package.json\n80944514: node_modules/request/node_modules/form-data/node_modules/async/lib/async.js\n80826033: node_modules/request/node_modules/form-data/lib/populate.js\n82349146: node_modules/request/node_modules/isstream/package.json\n82348558: node_modules/request/node_modules/isstream/isstream.js\n82342515: node_modules/request/node_modules/is-typedarray/package.json\n82341499: node_modules/request/node_modules/is-typedarray/index.js\n4479214: node_modules/request/lib/getProxyFromURI.js\n4481482: node_modules/request/lib/querystring.js\n82601463: node_modules/request/node_modules/qs/package.json\n82587962: node_modules/request/node_modules/qs/lib/index.js\n82593363: node_modules/request/node_modules/qs/lib/stringify.js\n82597342: node_modules/request/node_modules/qs/lib/utils.js\n82588115: node_modules/request/node_modules/qs/lib/parse.js\n4482815: node_modules/request/lib/har.js\n81178912: node_modules/request/node_modules/har-validator/package.json\n81001381: node_modules/request/node_modules/har-validator/lib/index.js\n81176502: node_modules/request/node_modules/har-validator/node_modules/pinkie-promise/package.json\n81163939: node_modules/request/node_modules/har-validator/node_modules/pinkie-promise/index.js\n81001935: node_modules/request/node_modules/har-validator/lib/runner.js\n81005359: node_modules/request/node_modules/har-validator/lib/schemas/index.js\n81002525: node_modules/request/node_modules/har-validator/lib/schemas/cache.json\n81002712: node_modules/request/node_modules/har-validator/lib/schemas/cacheEntry.json\n81003206: node_modules/request/node_modules/har-validator/lib/schemas/content.json\n81003583: node_modules/request/node_modules/har-validator/lib/schemas/cookie.json\n81004081: node_modules/request/node_modules/har-validator/lib/schemas/creator.json\n81004311: node_modules/request/node_modules/har-validator/lib/schemas/entry.json\n81005242: node_modules/request/node_modules/har-validator/lib/schemas/har.json\n81007111: node_modules/request/node_modules/har-validator/lib/schemas/log.json\n81007604: node_modules/request/node_modules/har-validator/lib/schemas/page.json\n81008181: node_modules/request/node_modules/har-validator/lib/schemas/pageTimings.json\n81008406: node_modules/request/node_modules/har-validator/lib/schemas/postData.json\n81009060: node_modules/request/node_modules/har-validator/lib/schemas/record.json\n81009286: node_modules/request/node_modules/har-validator/lib/schemas/request.json\n81010139: node_modules/request/node_modules/har-validator/lib/schemas/response.json\n81010946: node_modules/request/node_modules/har-validator/lib/schemas/timings.json\n81001195: node_modules/request/node_modules/har-validator/lib/error.js\n81161590: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/package.json\n81108729: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/index.js\n81147021: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/package.json\n81132607: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/index.js\n81145615: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/node_modules/is-property/package.json\n81134601: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/node_modules/is-property/is-property.js\n81128902: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-function/package.json\n81127606: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-function/index.js\n81151111: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/jsonpointer/package.json\n81149530: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/jsonpointer/jsonpointer.js\n81158029: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/xtend/package.json\n81157276: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/xtend/immutable.js\n81106380: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/formats.js\n4533728: node_modules/request/lib/auth.js\n1962761: node_modules/node-uuid/package.json\n1964629: node_modules/node-uuid/uuid.js\n4550226: node_modules/request/lib/oauth.js\n82548387: node_modules/request/node_modules/oauth-sign/package.json\n82544802: node_modules/request/node_modules/oauth-sign/index.js\n4568756: node_modules/request/lib/multipart.js\n4585628: node_modules/request/lib/redirect.js\n4594531: node_modules/request/lib/tunnel.js\n82856379: node_modules/request/node_modules/tunnel-agent/package.json\n82849535: node_modules/request/node_modules/tunnel-agent/index.js\n3599638: src/apm-wrapper.js\n3613961: src/buffered-process.js\n94914523: src/task.js\n1973294: src/compile-cache.js\n1766806: node_modules/fs-plus/package.json\n1768751: node_modules/fs-plus/lib/fs-plus.js\n1789356: node_modules/fs-plus/node_modules/underscore-plus/package.json\n1791321: node_modules/fs-plus/node_modules/underscore-plus/lib/underscore-plus.js\n1806109: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/package.json\n1807823: node_modules/fs-plus/node_modules/underscore-plus/node_modules/underscore/underscore.js\n1853312: node_modules/fs-plus/node_modules/async/package.json\n1854649: node_modules/fs-plus/node_modules/async/lib/async.js\n1884050: node_modules/fs-plus/node_modules/mkdirp/package.json\n1885107: node_modules/fs-plus/node_modules/mkdirp/index.js\n1887478: node_modules/fs-plus/node_modules/rimraf/package.json\n1889121: node_modules/fs-plus/node_modules/rimraf/rimraf.js\n1978902: src/compile-support/babel.js\n1980527: static/babelrc.json\n1980671: src/compile-support/coffee-script.js\n1981853: src/compile-support/typescript.js\n1983139: node_modules/underscore/package.json\n1985095: node_modules/underscore/underscore.js\n2038014: node_modules/source-map-support/package.json\n2039642: node_modules/source-map-support/source-map-support.js\n2054178: node_modules/source-map-support/node_modules/source-map/package.json\n2057217: node_modules/source-map-support/node_modules/source-map/lib/source-map.js\n2057643: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-generator.js\n2070902: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/package.json\n2072148: node_modules/source-map-support/node_modules/source-map/node_modules/amdefine/amdefine.js\n2082064: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64-vlq.js\n2086956: node_modules/source-map-support/node_modules/source-map/lib/source-map/base64.js\n2088093: node_modules/source-map-support/node_modules/source-map/lib/source-map/util.js\n2093422: node_modules/source-map-support/node_modules/source-map/lib/source-map/array-set.js\n2096140: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-map-consumer.js\n2113947: node_modules/source-map-support/node_modules/source-map/lib/source-map/binary-search.js\n2117157: node_modules/source-map-support/node_modules/source-map/lib/source-map/source-node.js\n94911340: src/task-bootstrap.js\n2264399: node_modules/emissary/package.json\n2266273: node_modules/emissary/lib/emissary.js\n2266555: node_modules/emissary/lib/helpers.js\n2268129: node_modules/emissary/lib/behavior.js\n2272107: node_modules/emissary/node_modules/underscore-plus/package.json\n2274072: node_modules/emissary/node_modules/underscore-plus/lib/underscore-plus.js\n2288860: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/package.json\n2290574: node_modules/emissary/node_modules/underscore-plus/node_modules/underscore/underscore.js\n2336063: node_modules/property-accessors/package.json\n2337929: node_modules/property-accessors/lib/property-accessors.js\n2340201: node_modules/property-accessors/node_modules/mixto/package.json\n2341779: node_modules/property-accessors/node_modules/mixto/lib/mixin.js\n2343179: node_modules/emissary/lib/signal.js\n2352520: node_modules/emissary/lib/emitter.js\n2367779: node_modules/emissary/node_modules/mixto/package.json\n2369463: node_modules/emissary/node_modules/mixto/lib/mixin.js\n2370863: node_modules/emissary/lib/subscriber.js\n2374948: node_modules/emissary/lib/subscription.js\n4222287: node_modules/request/package.json\n4225420: node_modules/request/index.js\n80790165: node_modules/request/node_modules/extend/package.json\n80787899: node_modules/request/node_modules/extend/index.js\n4229450: node_modules/request/lib/cookies.js\n82838107: node_modules/request/node_modules/tough-cookie/package.json\n82637881: node_modules/request/node_modules/tough-cookie/lib/cookie.js\n82685639: node_modules/request/node_modules/tough-cookie/lib/pubsuffix.js\n82835266: node_modules/request/node_modules/tough-cookie/lib/store.js\n82675424: node_modules/request/node_modules/tough-cookie/lib/memstore.js\n82683373: node_modules/request/node_modules/tough-cookie/lib/permuteDomain.js\n82680938: node_modules/request/node_modules/tough-cookie/lib/pathMatch.js\n82838107: node_modules/request/node_modules/tough-cookie/package.json\n4432858: node_modules/request/lib/helpers.js\n82361121: node_modules/request/node_modules/json-stringify-safe/package.json\n82362804: node_modules/request/node_modules/json-stringify-safe/stringify.js\n4435389: node_modules/request/request.js\n80742369: node_modules/request/node_modules/bl/package.json\n80567623: node_modules/request/node_modules/bl/bl.js\n80636139: node_modules/request/node_modules/bl/node_modules/readable-stream/duplex.js\n80636191: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_duplex.js\n80720291: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/package.json\n80718759: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/process-nextick-args/index.js\n80705839: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/package.json\n80702818: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/core-util-is/lib/util.js\n80712444: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/package.json\n80711730: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/inherits/inherits.js\n80638643: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_readable.js\n80716737: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/package.json\n80716605: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/isarray/index.js\n80670635: node_modules/request/node_modules/bl/node_modules/readable-stream/lib/_stream_writable.js\n80738031: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/package.json\n80737908: node_modules/request/node_modules/bl/node_modules/readable-stream/node_modules/util-deprecate/node.js\n81531974: node_modules/request/node_modules/hawk/package.json\n81345592: node_modules/request/node_modules/hawk/lib/index.js\n81424556: node_modules/request/node_modules/hawk/node_modules/boom/package.json\n81416906: node_modules/request/node_modules/hawk/node_modules/boom/lib/index.js\n81515577: node_modules/request/node_modules/hawk/node_modules/hoek/package.json\n81490930: node_modules/request/node_modules/hawk/node_modules/hoek/lib/index.js\n81488262: node_modules/request/node_modules/hawk/node_modules/hoek/lib/escape.js\n81530565: node_modules/request/node_modules/hawk/node_modules/sntp/package.json\n81520773: node_modules/request/node_modules/hawk/node_modules/sntp/index.js\n81520807: node_modules/request/node_modules/hawk/node_modules/sntp/lib/index.js\n81345973: node_modules/request/node_modules/hawk/lib/server.js\n81429631: node_modules/request/node_modules/hawk/node_modules/cryptiles/package.json\n81428267: node_modules/request/node_modules/hawk/node_modules/cryptiles/lib/index.js\n81342006: node_modules/request/node_modules/hawk/lib/crypto.js\n81364519: node_modules/request/node_modules/hawk/lib/utils.js\n81331415: node_modules/request/node_modules/hawk/lib/client.js\n80485113: node_modules/request/node_modules/aws-sign2/package.json\n80480692: node_modules/request/node_modules/aws-sign2/index.js\n82337952: node_modules/request/node_modules/http-signature/package.json\n81551606: node_modules/request/node_modules/http-signature/lib/index.js\n81552232: node_modules/request/node_modules/http-signature/lib/parser.js\n81591249: node_modules/request/node_modules/http-signature/node_modules/assert-plus/package.json\n81585919: node_modules/request/node_modules/http-signature/node_modules/assert-plus/assert.js\n81574938: node_modules/request/node_modules/http-signature/lib/utils.js\n82335507: node_modules/request/node_modules/http-signature/node_modules/sshpk/package.json\n81887698: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/index.js\n81888429: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/key.js\n81955466: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/assert-plus/package.json\n81950012: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/assert-plus/assert.js\n81830178: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/algs.js\n81847974: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/fingerprint.js\n81845672: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/errors.js\n81911179: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/utils.js\n81895828: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/private-key.js\n81901963: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/signature.js\n81943093: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/package.json\n81942773: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/index.js\n81928490: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/index.js\n81928251: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/errors.js\n81934548: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/types.js\n81928959: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/reader.js\n81935186: node_modules/request/node_modules/http-signature/node_modules/sshpk/node_modules/asn1/lib/ber/writer.js\n81908010: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ssh-buffer.js\n81843337: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/ed-compat.js\n81851404: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/auto.js\n81853302: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pem.js\n81858111: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs1.js\n81865807: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/pkcs8.js\n81881346: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh-private.js\n81877740: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/rfc4253.js\n81884606: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/formats/ssh.js\n81835020: node_modules/request/node_modules/http-signature/node_modules/sshpk/lib/dhe.js\n81562004: node_modules/request/node_modules/http-signature/lib/signer.js\n81802301: node_modules/request/node_modules/http-signature/node_modules/jsprim/package.json\n81602307: node_modules/request/node_modules/http-signature/node_modules/jsprim/lib/jsprim.js\n81636568: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/extsprintf/package.json\n81632804: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/extsprintf/lib/extsprintf.js\n81801412: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/verror/package.json\n81797823: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/verror/lib/verror.js\n81774451: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/json-schema/package.json\n81763963: node_modules/request/node_modules/http-signature/node_modules/jsprim/node_modules/json-schema/lib/validate.js\n81577777: node_modules/request/node_modules/http-signature/lib/verify.js\n82533240: node_modules/request/node_modules/mime-types/package.json\n82371286: node_modules/request/node_modules/mime-types/index.js\n82530646: node_modules/request/node_modules/mime-types/node_modules/mime-db/package.json\n82530510: node_modules/request/node_modules/mime-types/node_modules/mime-db/index.js\n82387545: node_modules/request/node_modules/mime-types/node_modules/mime-db/db.json\n82606571: node_modules/request/node_modules/stringstream/package.json\n82607891: node_modules/request/node_modules/stringstream/stringstream.js\n80756086: node_modules/request/node_modules/caseless/package.json\n80754327: node_modules/request/node_modules/caseless/index.js\n80805500: node_modules/request/node_modules/forever-agent/package.json\n80801324: node_modules/request/node_modules/forever-agent/index.js\n80986121: node_modules/request/node_modules/form-data/package.json\n80814876: node_modules/request/node_modules/form-data/lib/form_data.js\n80777465: node_modules/request/node_modules/combined-stream/package.json\n80764215: node_modules/request/node_modules/combined-stream/lib/combined_stream.js\n80775847: node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/package.json\n80773528: node_modules/request/node_modules/combined-stream/node_modules/delayed-stream/lib/delayed_stream.js\n80983078: node_modules/request/node_modules/form-data/node_modules/async/package.json\n80944514: node_modules/request/node_modules/form-data/node_modules/async/lib/async.js\n80826033: node_modules/request/node_modules/form-data/lib/populate.js\n82349146: node_modules/request/node_modules/isstream/package.json\n82348558: node_modules/request/node_modules/isstream/isstream.js\n82342515: node_modules/request/node_modules/is-typedarray/package.json\n82341499: node_modules/request/node_modules/is-typedarray/index.js\n4479214: node_modules/request/lib/getProxyFromURI.js\n4481482: node_modules/request/lib/querystring.js\n82601463: node_modules/request/node_modules/qs/package.json\n82587962: node_modules/request/node_modules/qs/lib/index.js\n82593363: node_modules/request/node_modules/qs/lib/stringify.js\n82597342: node_modules/request/node_modules/qs/lib/utils.js\n82588115: node_modules/request/node_modules/qs/lib/parse.js\n4482815: node_modules/request/lib/har.js\n81178912: node_modules/request/node_modules/har-validator/package.json\n81001381: node_modules/request/node_modules/har-validator/lib/index.js\n81176502: node_modules/request/node_modules/har-validator/node_modules/pinkie-promise/package.json\n81163939: node_modules/request/node_modules/har-validator/node_modules/pinkie-promise/index.js\n81001935: node_modules/request/node_modules/har-validator/lib/runner.js\n81005359: node_modules/request/node_modules/har-validator/lib/schemas/index.js\n81002525: node_modules/request/node_modules/har-validator/lib/schemas/cache.json\n81002712: node_modules/request/node_modules/har-validator/lib/schemas/cacheEntry.json\n81003206: node_modules/request/node_modules/har-validator/lib/schemas/content.json\n81003583: node_modules/request/node_modules/har-validator/lib/schemas/cookie.json\n81004081: node_modules/request/node_modules/har-validator/lib/schemas/creator.json\n81004311: node_modules/request/node_modules/har-validator/lib/schemas/entry.json\n81005242: node_modules/request/node_modules/har-validator/lib/schemas/har.json\n81007111: node_modules/request/node_modules/har-validator/lib/schemas/log.json\n81007604: node_modules/request/node_modules/har-validator/lib/schemas/page.json\n81008181: node_modules/request/node_modules/har-validator/lib/schemas/pageTimings.json\n81008406: node_modules/request/node_modules/har-validator/lib/schemas/postData.json\n81009060: node_modules/request/node_modules/har-validator/lib/schemas/record.json\n81009286: node_modules/request/node_modules/har-validator/lib/schemas/request.json\n81010139: node_modules/request/node_modules/har-validator/lib/schemas/response.json\n81010946: node_modules/request/node_modules/har-validator/lib/schemas/timings.json\n81001195: node_modules/request/node_modules/har-validator/lib/error.js\n81161590: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/package.json\n81108729: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/index.js\n81147021: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/package.json\n81132607: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/index.js\n81145615: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/node_modules/is-property/package.json\n81134601: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-object-property/node_modules/is-property/is-property.js\n81128902: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-function/package.json\n81127606: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/generate-function/index.js\n81151111: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/jsonpointer/package.json\n81149530: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/jsonpointer/jsonpointer.js\n81158029: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/xtend/package.json\n81157276: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/node_modules/xtend/immutable.js\n81106380: node_modules/request/node_modules/har-validator/node_modules/is-my-json-valid/formats.js\n4533728: node_modules/request/lib/auth.js\n1962761: node_modules/node-uuid/package.json\n1964629: node_modules/node-uuid/uuid.js\n4550226: node_modules/request/lib/oauth.js\n82548387: node_modules/request/node_modules/oauth-sign/package.json\n82544802: node_modules/request/node_modules/oauth-sign/index.js\n4568756: node_modules/request/lib/multipart.js\n4585628: node_modules/request/lib/redirect.js\n4594531: node_modules/request/lib/tunnel.js\n82856379: node_modules/request/node_modules/tunnel-agent/package.json\n82849535: node_modules/request/node_modules/tunnel-agent/index.js\n"
  },
  {
    "path": "packages/client-app/build/resources/linux/debian/control.in",
    "content": "Package: <%= name %>\nVersion: <%= version %>\nDepends: libgnome-keyring0, gir1.2-gnomekeyring-1.0, git, gconf2, gconf-service, libgtk2.0-0, libudev0 | libudev1, libgcrypt11 | libgcrypt20, libnotify4, libxtst6, libnss3, python, gvfs-bin, xdg-utils\nSection: <%= section %>\nPriority: optional\nArchitecture: <%= arch %>\nInstalled-Size: <%= installedSize %>\nMaintainer: <%= maintainer %>\nDescription: <%= description %>\n <%= description %>\n"
  },
  {
    "path": "packages/client-app/build/resources/linux/debian/lintian-overrides",
    "content": "nylas: arch-dependent-file-in-usr-share\nnylas: changelog-file-missing-in-native-package\nnylas: copyright-file-contains-full-apache-2-license\nnylas: copyright-should-refer-to-common-license-file-for-apache-2\nnylas: copyright-should-refer-to-common-license-file-for-lgpl\nnylas: embedded-library\nnylas: package-installs-python-bytecode\nnylas: unstripped-binary-or-object\nnylas: extra-license-file\n"
  },
  {
    "path": "packages/client-app/build/resources/linux/debian/postinst",
    "content": "#!/bin/sh\n# postinst script for Nylas\n#\n# see: dh_installdeb(1)\n\n# summary of how this script can be called:\n#        * <postinst> `configure' <most-recently-configured-version>\n#        * <old-postinst> abort-upgrade' <new version>\n#        * <conflictor's-postinst> `abort-remove' `in-favour' <package>\n#          <new-version>\n#        * <postinst> `abort-remove'\n#        * <deconfigured's-postinst> `abort-deconfigure' `in-favour'\n#          <failed-install-package> <version> `removing'\n#          <conflicting-package> <version>\n# for details, see http://www.debian.org/doc/debian-policy/ or\n# the debian-policy package\n\nUBUNTU_CODENAMES=\"precise trusty utopic vivid\" # \"xenial is not yet available in nylas repos.\"\nDEBIAN_CODENAMES=\"squeeze wheezy jessie sid\"\n\ncase \"$1\" in\n    configure)\n    \tgtk-update-icon-cache /usr/share/icons/hicolor > /dev/null 2>&1\n\n        DISTRO=`lsb_release -s -i`\n\n        if [ \"$DISTRO\" = \"Ubuntu\" ] || [ \"$DISTRO\" = \"elementary OS\" ] || [ \"$DISTRO\" = \"LinuxMint\" ] ; then\n          DISTS=$UBUNTU_CODENAMES\n          DISTRO=\"ubuntu\"\n        elif [ \"$DISTRO\" = \"Debian\" ]; then\n          DISTS=$DEBIAN_CODENAMES\n          DISTRO=\"debian\"\n        else\n          echo \"You are not running Debian, Ubuntu, ElementaryOS or LinuxMint.  Not adding Nylas repository.\"\n          DISTRO=\"\"\n        fi\n\n        if [ -n \"$DISTRO\" ]; then\n          # Add the Nylas repository.\n          # Copyright (c) 2009 The Chromium Authors. All rights reserved.\n          # Use of this source code is governed by a BSD-style license.\n\n          # Install the repository signing key\n          install_key() {\n            APT_KEY=\"`which apt-key 2> /dev/null`\"\n            if [ -x \"$APT_KEY\" ]; then\n              \"$APT_KEY\" add - >/dev/null 2>&1 <<KEYDATA\n-----BEGIN PGP PUBLIC KEY BLOCK-----\nVersion: GnuPG v1\n\nmQSuBFX0SVwRDACLnMu8V6T6fyp5n5x/ZpOUuI1AEWkPfRUzF/lVdo2P7llJw1xG\niovmCwI8KdvRmjtrDG7BxnwHgZfEdYOWx80SunTLQL/MBx3pfqN74WzCFjuI6GRU\nxbyM7PbwsHkefMtcHNfX74SLMdxq3gGgWlds2UOBuWlB2arz8x7WhnvwcWwtYeuo\n+P/suo69vWRsP4vm/8XPi07NU9s0Kny/bpB2CF8lMlwg/C2NiFyzMW236WH6obJ9\nACkcrPpxfgjPuhppTQuMUp0wB++nIZdzzAGQpiqGgPq7vCSnXrSR/pFZYWWPdq5C\n64cmHLO1LaO18+41ogL1zAkPadf+T6i6APq3M6jWsOmEME1bnjoQjhvR2xt7st1R\nIyRWK7Yj/9EeMtTbsTOHwR2qcQknRlNgiCny3hRiNP8Dy3AlaZqiwfYAgX5/cfUr\nwtbJzv1tFfIBFXOzFBl2zsHLkCm/FGoeWxZDe1S3YzgSsJlNxSJRmrUPRHTItDIb\n/03MvSm6gp2J9t8BALaFZEOHCYNqoxXHGIsfwjnrnQqRG59jlx+dgk+JALZnC/92\nzkt2i5BRxF3HkyAqA1L+M8daomESQvEjHMtGSQrGK6B/VzYQYVigyn91c41E2ijh\n0cvzDj2m0jgmI1CwsC3B9FR3IPAZwip9PC65yU8Z95VnmcPEbOLJ4CLPPXV/8x+I\nvEyqp9ePRVyfz1x2dlBWCq+sIjVqiMPMNT49+mfAdOBwikt+F9z+F+3ao1XfX2z3\nisXpAlgr2eNty9EuPx3uYocs6vTlAvrzgqbdVgkPlr4hATdVteMU3Y+I9zlJTIFt\nSqGLROF4mu+j9Pr3HG0j0eCPbj/5b+Wsmw9mNjE7o8y3Og66O8kn4o/FnJ7gT21d\nfljg+Js4p9i3qsPxaLfHSJc7g5URBBYDQ/P/QOcIc5P/hnGC/dOpFC3+o47FTc2e\n8/OUw5UNuPJ3oDZW8rPNlWT4EehP+1QuXhd4I59Xbm3OCAFDMPrX/uWNB8s2kFkn\n6CuzqOWOmNimukKqy6gnrRvMzHV1afQDDL77qOA79hEDW715gkZ4QPO5yjNdKUAL\n/3RkuNaCxmYa7Ny+nU6xQvbQM/Kp/RpNTNgy31LyvpRQY4mskEmIJcuxc6CXmpPI\npsdbHTMzK0qByfRFtkAVD4p+AuWuEc+SMWx5lHy62TObPpGtgkjPCy4u0K2ntx7w\nvaM9x33q+aLC/GKRADQSbtKVbBO5ei/OnQv0Bo4QkBHNVz92P5jlhLiwh3VDWkc+\nvvFeXI2UxvDpQ4AVrM0QWmyIWRjLjWrP5aoIo9Q3druTgsWIE+hsWWKUda2ajhh+\n6Jg2HYSzvMxB3W1sMl+ezrksKSr9ziFUGOObxnPBVM7AI2qa0zqkLF6WyUGY4SEZ\nGFL7jsahos27wLcz5bVMM12Y7k2YIlmzxK9TY2sfn4Z43uvwB9/rUbOGdRm5G24I\nXGicrvcnB0oFtvlmgRkPt3prEWEwRIIGWrU1K8OL52dSovUoD5rkmBCeQ7WITQbT\ntmyBiNmoD16Eo1p77NDSgNjvJS1BjLuo7co4mIqZmsK0xJuWNh4WaJ9AFaxAqeep\nDLQeTnlsYXMgVGVhbSA8c3VwcG9ydEBueWxhcy5jb20+iHcEExEIAB8FAlX0SVwC\nGwMFCwkIBwMFFQoJCAsDFgIDAh4BAheAAAoJEDj8bpZ9Cs9KTNkA/jsks9Q89PaS\ndZVWAsu8QAqbMQQkrk4QFr0Aha5P0vRfAP43CgplwMIQkdDxf02Etuj8JYdzRGdm\nMKFYFOTibEgbfw==\n=3EXM\n-----END PGP PUBLIC KEY BLOCK-----\nKEYDATA\n            fi\n          }\n\n          DISTRIB_CODENAME=`lsb_release -s -c`\n\n          for DIST in $DISTS; do\n              REPO=$DIST\n              if [ \"$DIST\" = \"$DISTRIB_CODENAME\" ]; then\n                break\n              fi\n          done\n\n          REPOCONFIG=\"deb [arch=i386,amd64] http://apt.nylas.com/$DISTRO $REPO main\"\n\n          APT_GET=\"`which apt-get 2> /dev/null`\"\n          APT_CONFIG=\"`which apt-config 2> /dev/null`\"\n\n          # Parse apt configuration and return requested variable value.\n          apt_config_val() {\n            APTVAR=\"$1\"\n            if [ -x \"$APT_CONFIG\" ]; then\n              \"$APT_CONFIG\" dump | sed -e \"/^$APTVAR /\"'!d' -e \"s/^$APTVAR \\\"\\(.*\\)\\\".*/\\1/\"\n            fi\n          }\n\n          # Set variables for the locations of the apt sources lists.\n          find_apt_sources() {\n            APTDIR=$(apt_config_val Dir)\n            APTETC=$(apt_config_val 'Dir::Etc')\n            APT_SOURCES=\"$APTDIR$APTETC$(apt_config_val 'Dir::Etc::sourcelist')\"\n            APT_SOURCESDIR=\"$APTDIR$APTETC$(apt_config_val 'Dir::Etc::sourceparts')\"\n          }\n\n          # Add the Nylas repository to the apt sources.\n          # Returns:\n          # 0 - no update necessary\n          # 1 - sources were updated\n          # 2 - error\n          update_sources_lists() {\n            if [ ! \"$REPOCONFIG\" ]; then\n              return 0\n            fi\n\n            find_apt_sources\n\n            if [ -d \"$APT_SOURCESDIR\" ]; then\n              # Nothing to do if it's already there.\n              SOURCELIST=$(grep -H \"$REPOCONFIG\" \"$APT_SOURCESDIR/nylas.list\" \\\n                2>/dev/null | cut -d ':' -f 1)\n              if [ -n \"$SOURCELIST\" ]; then\n                return 0\n              fi\n\n              printf \"$REPOCONFIG\\n\" > \"$APT_SOURCESDIR/nylas.list\"\n              if [ $? -eq 0 ]; then\n                return 1\n              fi\n            fi\n            return 2\n          }\n\n          install_key\n          update_sources_lists\n\n        fi\n\t;;\n\n    abort-upgrade|abort-remove|abort-deconfigure)\n    ;;\n\n    *)\n        echo \"postinst called with unknown argument '$1'\" >&2\n        exit 1\n    ;;\nesac\n\nset -e\n\nexit 0\n"
  },
  {
    "path": "packages/client-app/build/resources/linux/debian/postrm",
    "content": "#!/bin/sh\n# Remove the Nylas repository.\n# Copyright (c) 2009 The Chromium Authors. All rights reserved.\n# Use of this source code is governed by a BSD-style license.\n\nset -e\naction=\"$1\"\n\n# Only do complete clean-up on purge.\nif [ \"$action\" != \"purge\" ] ; then\n  exit 0\nfi\n\nAPT_GET=\"`which apt-get 2> /dev/null`\"\nAPT_CONFIG=\"`which apt-config 2> /dev/null`\"\n\n# Parse apt configuration and return requested variable value.\napt_config_val() {\n  APTVAR=\"$1\"\n  if [ -x \"$APT_CONFIG\" ]; then\n    \"$APT_CONFIG\" dump | sed -e \"/^$APTVAR /\"'!d' -e \"s/^$APTVAR \\\"\\(.*\\)\\\".*/\\1/\"\n  fi\n}\n\nuninstall_key() {\n  APT_KEY=\"`which apt-key 2> /dev/null`\"\n  if [ -x \"$APT_KEY\" ]; then\n    # don't fail if the key wasn't found\n    \"$APT_KEY\" rm 7D0ACF4A >/dev/null 2>&1 || true\n  fi\n}\n\n# Set variables for the locations of the apt sources lists.\nfind_apt_sources() {\n  APTDIR=$(apt_config_val Dir)\n  APTETC=$(apt_config_val 'Dir::Etc')\n  APT_SOURCES=\"$APTDIR$APTETC$(apt_config_val 'Dir::Etc::sourcelist')\"\n  APT_SOURCESDIR=\"$APTDIR$APTETC$(apt_config_val 'Dir::Etc::sourceparts')\"\n}\n\n# Remove a repository from the apt sources.\n# Returns:\n# 0 - successfully removed, or not configured\n# 1 - failed to remove\nclean_sources_lists() {\n  find_apt_sources\n\n  if [ -d \"$APT_SOURCESDIR\" ]; then\n    rm -f \"$APT_SOURCESDIR/nylas.list\"\n  fi\n\n  return 0\n}\n\nuninstall_key\nclean_sources_lists\n\nexit 0\n"
  },
  {
    "path": "packages/client-app/build/resources/linux/nylas-mail.desktop.in",
    "content": "[Desktop Entry]\nName=<%= productName %>\nComment=<%= description %>\nGenericName=<%= productName %>\nExec=/usr/bin/nylas-mail %U\nIcon=nylas-mail\nType=Application\nStartupNotify=true\nStartupWMClass=<%= productName %>\nCategories=GNOME;GTK;Network;Email;Utility;Development;\nMimeType=text/plain;x-scheme-handler/mailto;x-scheme-handler/nylas;\n"
  },
  {
    "path": "packages/client-app/build/resources/linux/redhat/nylas.spec.in",
    "content": "Name:           <%= name %>\nVersion:        <%= version %>\nRelease:        0.1%{?dist}\nSummary:        <%= description %>\nLicense:        GPLv3\nURL:            https://github.com/nylas/nylas-mail\nAutoReqProv:    no # Avoid libchromiumcontent.so missing dependency\n\nrequires:       libgnome-keyring\n\n%description\n<%= description %>\n\n%install\nmkdir -p %{buildroot}/usr/share/nylas-mail\ncp -r <%= contentsDir %>/* %{buildroot}/usr/share/nylas-mail\n\nmkdir -p %{buildroot}/usr/bin/\n\nln -s ../share/nylas-mail/nylas %{buildroot}/usr/bin/nylas-mail\nchmod 755 %{buildroot}/usr/bin/nylas-mail\n\nmkdir -p %{buildroot}/usr/share/applications/\nmv nylas-mail.desktop %{buildroot}/usr/share/applications/\n\nfor s in 16 32 64 128 256 512; do\n    mkdir -p %{buildroot}/usr/share/icons/hicolor/${s}x${s}/apps\n    cp -p <%= linuxAssetsDir %>/icons/${s}.png %{buildroot}/usr/share/icons/hicolor/${s}x${s}/apps/nylas-mail.png\ndone\n\n%files\n/usr/bin/nylas-mail\n/usr/share/nylas-mail\n/usr/share/applications/nylas-mail.desktop\n/usr/share/icons/hicolor/16x16/apps/nylas-mail.png\n/usr/share/icons/hicolor/32x32/apps/nylas-mail.png\n/usr/share/icons/hicolor/64x64/apps/nylas-mail.png\n/usr/share/icons/hicolor/128x128/apps/nylas-mail.png\n/usr/share/icons/hicolor/256x256/apps/nylas-mail.png\n/usr/share/icons/hicolor/512x512/apps/nylas-mail.png\n"
  },
  {
    "path": "packages/client-app/build/resources/mac/Nylas Calendar.app/Contents/Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>BuildMachineOSBuild</key>\n\t<string>15E65</string>\n\t<key>CFBundleDevelopmentRegion</key>\n\t<string>en</string>\n\t<key>CFBundleExecutable</key>\n\t<string>Nylas Calendar</string>\n\t<key>CFBundleIconFile</key>\n\t<string>AppIcon</string>\n\t<key>CFBundleIdentifier</key>\n\t<string>com.nylas.Nylas-Calendar</string>\n\t<key>CFBundleInfoDictionaryVersion</key>\n\t<string>6.0</string>\n\t<key>CFBundleName</key>\n\t<string>Nylas Calendar</string>\n\t<key>CFBundlePackageType</key>\n\t<string>APPL</string>\n\t<key>CFBundleShortVersionString</key>\n\t<string>1.0</string>\n\t<key>CFBundleSignature</key>\n\t<string>????</string>\n\t<key>CFBundleSupportedPlatforms</key>\n\t<array>\n\t\t<string>MacOSX</string>\n\t</array>\n\t<key>CFBundleURLTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>CFBundleURLName</key>\n\t\t\t<string>nylas-calendar-shim</string>\n\t\t</dict>\n\t</array>\n\t<key>CFBundleVersion</key>\n\t<string>1</string>\n\t<key>DTCompiler</key>\n\t<string>com.apple.compilers.llvm.clang.1_0</string>\n\t<key>DTPlatformBuild</key>\n\t<string>7D1014</string>\n\t<key>DTPlatformVersion</key>\n\t<string>GM</string>\n\t<key>DTSDKBuild</key>\n\t<string>15E60</string>\n\t<key>DTSDKName</key>\n\t<string>macosx10.11</string>\n\t<key>DTXcode</key>\n\t<string>0731</string>\n\t<key>DTXcodeBuild</key>\n\t<string>7D1014</string>\n\t<key>LSMinimumSystemVersion</key>\n\t<string>10.11</string>\n\t<key>NSHumanReadableCopyright</key>\n\t<string>Copyright © 2016 Ben Gotow. All rights reserved.</string>\n\t<key>NSMainNibFile</key>\n\t<string>MainMenu</string>\n\t<key>NSPrincipalClass</key>\n\t<string>NSApplication</string>\n</dict>\n</plist>\n"
  },
  {
    "path": "packages/client-app/build/resources/mac/Nylas Calendar.app/Contents/PkgInfo",
    "content": "APPL????"
  },
  {
    "path": "packages/client-app/build/resources/mac/Nylas Calendar.app/Contents/_CodeSignature/CodeResources",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>files</key>\n\t<dict>\n\t\t<key>Resources/AppIcon.icns</key>\n\t\t<data>\n\t\t5v+SAI+weVs8VSrknYJvITKSEws=\n\t\t</data>\n\t\t<key>Resources/Base.lproj/MainMenu.nib</key>\n\t\t<dict>\n\t\t\t<key>hash</key>\n\t\t\t<data>\n\t\t\t3K0ICUQtN1fqxnQ0VFIIBLmDg00=\n\t\t\t</data>\n\t\t\t<key>optional</key>\n\t\t\t<true/>\n\t\t</dict>\n\t</dict>\n\t<key>files2</key>\n\t<dict>\n\t\t<key>Resources/AppIcon.icns</key>\n\t\t<dict>\n\t\t\t<key>hash</key>\n\t\t\t<data>\n\t\t\t5v+SAI+weVs8VSrknYJvITKSEws=\n\t\t\t</data>\n\t\t\t<key>hash2</key>\n\t\t\t<data>\n\t\t\tcDmR/kt31C8E2IvUvGZRLOTDSOnZAtRnMPhGIJs6k00=\n\t\t\t</data>\n\t\t</dict>\n\t\t<key>Resources/Base.lproj/MainMenu.nib</key>\n\t\t<dict>\n\t\t\t<key>hash</key>\n\t\t\t<data>\n\t\t\t3K0ICUQtN1fqxnQ0VFIIBLmDg00=\n\t\t\t</data>\n\t\t\t<key>hash2</key>\n\t\t\t<data>\n\t\t\tv9hB8Bi1UaRdestGh/P5TKwS9ntchvYPnE+TZp/y76w=\n\t\t\t</data>\n\t\t\t<key>optional</key>\n\t\t\t<true/>\n\t\t</dict>\n\t</dict>\n\t<key>rules</key>\n\t<dict>\n\t\t<key>^Resources/</key>\n\t\t<true/>\n\t\t<key>^Resources/.*\\.lproj/</key>\n\t\t<dict>\n\t\t\t<key>optional</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>1000</real>\n\t\t</dict>\n\t\t<key>^Resources/.*\\.lproj/locversion.plist$</key>\n\t\t<dict>\n\t\t\t<key>omit</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>1100</real>\n\t\t</dict>\n\t\t<key>^version.plist$</key>\n\t\t<true/>\n\t</dict>\n\t<key>rules2</key>\n\t<dict>\n\t\t<key>.*\\.dSYM($|/)</key>\n\t\t<dict>\n\t\t\t<key>weight</key>\n\t\t\t<real>11</real>\n\t\t</dict>\n\t\t<key>^(.*/)?\\.DS_Store$</key>\n\t\t<dict>\n\t\t\t<key>omit</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>2000</real>\n\t\t</dict>\n\t\t<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>\n\t\t<dict>\n\t\t\t<key>nested</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>10</real>\n\t\t</dict>\n\t\t<key>^.*</key>\n\t\t<true/>\n\t\t<key>^Info\\.plist$</key>\n\t\t<dict>\n\t\t\t<key>omit</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>20</real>\n\t\t</dict>\n\t\t<key>^PkgInfo$</key>\n\t\t<dict>\n\t\t\t<key>omit</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>20</real>\n\t\t</dict>\n\t\t<key>^Resources/</key>\n\t\t<dict>\n\t\t\t<key>weight</key>\n\t\t\t<real>20</real>\n\t\t</dict>\n\t\t<key>^Resources/.*\\.lproj/</key>\n\t\t<dict>\n\t\t\t<key>optional</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>1000</real>\n\t\t</dict>\n\t\t<key>^Resources/.*\\.lproj/locversion.plist$</key>\n\t\t<dict>\n\t\t\t<key>omit</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>1100</real>\n\t\t</dict>\n\t\t<key>^[^/]+$</key>\n\t\t<dict>\n\t\t\t<key>nested</key>\n\t\t\t<true/>\n\t\t\t<key>weight</key>\n\t\t\t<real>10</real>\n\t\t</dict>\n\t\t<key>^embedded\\.provisionprofile$</key>\n\t\t<dict>\n\t\t\t<key>weight</key>\n\t\t\t<real>20</real>\n\t\t</dict>\n\t\t<key>^version\\.plist$</key>\n\t\t<dict>\n\t\t\t<key>weight</key>\n\t\t\t<real>20</real>\n\t\t</dict>\n\t</dict>\n</dict>\n</plist>\n"
  },
  {
    "path": "packages/client-app/build/resources/mac/nylas-Info.plist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n<plist version=\"1.0\">\n<dict>\n\t<key>NSPrincipalClass</key>\n\t<string>AtomApplication</string>\n\t<key>CFBundleDocumentTypes</key>\n\t<array>\n\t\t<dict>\n\t\t\t<key>LSHandlerRank</key>\n\t\t\t<string>Alternate</string>\n\t\t\t<key>CFBundleTypeRole</key>\n\t\t\t<string>Viewer</string>\n\t\t\t<key>CFBundleTypeName</key>\n\t\t\t<string>File</string>\n\t\t\t<key>CFBundleTypeExtensions</key>\n\t\t\t<array>\n\t\t\t\t<string>*</string>\n\t\t\t</array>\n\t\t</dict>\n\t</array>\n</dict>\n</plist>\n"
  },
  {
    "path": "packages/client-app/build/resources/win/elevate.cmd",
    "content": "@setlocal\n@echo off\nset CMD=%*\nset APP=%1\nstart wscript //nologo \"%~dpn0.vbs\" %*"
  },
  {
    "path": "packages/client-app/build/resources/win/elevate.vbs",
    "content": "Set Shell = CreateObject(\"Shell.Application\")\nSet WShell = WScript.CreateObject(\"WScript.Shell\")\nSet ProcEnv = WShell.Environment(\"PROCESS\")\n\ncmd = ProcEnv(\"CMD\")\napp = ProcEnv(\"APP\")\nargs= Right(cmd,(Len(cmd)-Len(app)))\n\nIf (WScript.Arguments.Count >= 1) Then\n  Shell.ShellExecute app, args, \"\", \"runas\", 0\nElse\n  WScript.Quit\nEnd If"
  },
  {
    "path": "packages/client-app/build/resources/win/nylas-mailto-registration.reg",
    "content": "Windows Registry Editor Version 5.00\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Classes\\Nylas.Url.mailto]\n\"FriendlyTypeName\"=\"Nylas Url\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Classes\\Nylas.Url.mailto\\shell]\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Classes\\Nylas.Url.mailto\\shell\\open]\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Classes\\Nylas.Url.mailto\\shell\\open\\command]\n@=\"\\\"{{PATH_TO_ROOT_FOLDER}}\\\\Update.exe\\\" --processStart nylas.exe --process-start-args %1\"\n\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas]\n@=\"Nylas\"\n\"LocalizedString\"=\"@{{PATH_TO_ROOT_FOLDER}}\\\\Update.exe,-123\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\DefaultIcon]\n@=\"{{PATH_TO_APP_FOLDER}}\\\\nylas.exe,1\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Capabilities]\n\"ApplicationName\"=\"Nylas Mail\"\n\"ApplicationDescription\"=\"A fast, modern mail client designed to help you boost your productivity.\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Capabilities\\StartMenu]\n\"Mail\"=\"Nylas\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Capabilities\\URLAssociations]\n\"mailto\"=\"Nylas.Url.mailto\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Protocols]\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Protocols\\mailto]\n@=\"URL:MailTo Protocol\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Protocols\\mailto\\DefaultIcon]\n@=\"{{PATH_TO_APP_FOLDER}}\\\\nylas.exe,1\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Protocols\\mailto\\shell]\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Protocols\\mailto\\shell\\open]\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\Protocols\\mailto\\shell\\open\\command]\n@=\"\\\"{{PATH_TO_ROOT_FOLDER}}\\\\Update.exe\\\" --processStart nylas.exe --process-start-args %1\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\shell]\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\shell\\open]\n\n[{{HKEY_ROOT}}\\SOFTWARE\\Clients\\Mail\\Nylas\\shell\\open\\command]\n@=\"\\\"{{PATH_TO_ROOT_FOLDER}}\\\\Update.exe\\\" --processStart nylas.exe\"\n\n[{{HKEY_ROOT}}\\SOFTWARE\\RegisteredApplications]\n\"Nylas\"=\"Software\\\\Clients\\\\Mail\\\\Nylas\\\\Capabilities\"\n\n\n[HKEY_CURRENT_USER\\SOFTWARE\\Classes\\Local Settings\\Software\\Microsoft\\Windows\\Shell\\MuiCache]\n\"{{PATH_TO_ROOT_FOLDER}}\\\\Update.exe\"=\"Nylas Mail\"\n\"{{PATH_TO_ROOT_FOLDER}}\\\\Update.exe.FriendlyAppName\"=\"Nylas Mail\"\n\"{{PATH_TO_ROOT_FOLDER}}\\\\Update.exe.ApplicationCompany\"=\"Nylas\"\n"
  },
  {
    "path": "packages/client-app/build/tasks/coffeelint-task.js",
    "content": "module.exports = (grunt) => {\n  grunt.config.merge({\n    coffeelint: {\n      'options': {\n        configFile: 'build/config/coffeelint.json',\n      },\n      'src': grunt.config('source:coffeescript'),\n      'build': [\n        'build/tasks/**/*.coffee',\n      ],\n      'test': [\n        'spec/**/*.cjsx',\n        'spec/**/*.coffee',\n      ],\n      'static': [\n        'static/**/*.coffee',\n        'static/**/*.cjsx',\n      ],\n      'target': (grunt.option(\"target\") ? grunt.option(\"target\").split(\" \") : []),\n    },\n  });\n\n  grunt.loadNpmTasks('grunt-contrib-coffee');\n  grunt.loadNpmTasks('grunt-coffeelint-cjsx');\n}\n"
  },
  {
    "path": "packages/client-app/build/tasks/create-mac-dmg.js",
    "content": "const path = require('path');\nconst createDMG = require('electron-installer-dmg')\n\nmodule.exports = (grunt) => {\n  grunt.registerTask('create-mac-dmg', 'Create DMG for Nylas Mail', function pack() {\n    const done = this.async();\n    const dmgPath = path.join(grunt.config('outputDir'), \"NylasMail.dmg\");\n    createDMG({\n      appPath: path.join(grunt.config('outputDir'), \"Nylas Mail-darwin-x64\", \"Nylas Mail.app\"),\n      name: \"NylasMail\",\n      background: path.resolve(grunt.config('appDir'), 'build', 'resources', 'mac', 'Nylas-Mail-DMG-background.png'),\n      icon: path.resolve(grunt.config('appDir'), 'build', 'resources', 'mac', 'nylas.icns'),\n      overwrite: true,\n      out: grunt.config('outputDir'),\n    }, (err) => {\n      if (err) {\n        done(err);\n        return\n      }\n\n      grunt.log.writeln(`>> Created ${dmgPath}`);\n      done(null);\n    })\n  });\n};\n"
  },
  {
    "path": "packages/client-app/build/tasks/create-mac-zip.js",
    "content": "/* eslint prefer-template: 0 */\n/* eslint global-require: 0 */\n/* eslint quote-props: 0 */\nconst path = require('path');\n\nmodule.exports = (grunt) => {\n  const {spawn} = grunt.config('taskHelpers')\n\n  grunt.registerTask('create-mac-zip', 'Zip up Nylas Mail', function pack() {\n    const done = this.async();\n    const zipPath = path.join(grunt.config('outputDir'), 'NylasMail.zip');\n\n    if (grunt.file.exists(zipPath)) {\n      grunt.file.delete(zipPath, {force: true});\n    }\n\n    const orig = process.cwd();\n    process.chdir(path.join(grunt.config('outputDir'), 'Nylas Mail-darwin-x64'));\n\n    spawn({\n      cmd: \"zip\",\n      args: [\"-9\", \"-y\", \"-r\", \"-9\", \"-X\", zipPath, 'Nylas Mail.app'],\n    }, (error) => {\n      process.chdir(orig);\n\n      if (error) {\n        done(error);\n        return;\n      }\n\n      grunt.log.writeln(`>> Created ${zipPath}`);\n      done(null);\n    });\n  });\n};\n"
  },
  {
    "path": "packages/client-app/build/tasks/csslint-task.js",
    "content": "module.exports = (grunt) => {\n  grunt.config.merge({\n    csslint: {\n      options: {\n        'adjoining-classes': false,\n        'duplicate-background-images': false,\n        'box-model': false,\n        'box-sizing': false,\n        'bulletproof-font-face': false,\n        'compatible-vendor-prefixes': false,\n        'display-property-grouping': false,\n        'fallback-colors': false,\n        'font-sizes': false,\n        'gradients': false,\n        'ids': false,\n        'important': false,\n        'known-properties': false,\n        'outline-none': false,\n        'overqualified-elements': false,\n        'qualified-headings': false,\n        'unique-headings': false,\n        'universal-selector': false,\n        'vendor-prefix': false,\n        'duplicate-properties': false, // doesn't place nice with mixins\n      },\n      src: [\n        'static/**/*.css',\n      ],\n    },\n  });\n\n  grunt.loadNpmTasks('grunt-contrib-csslint');\n}\n"
  },
  {
    "path": "packages/client-app/build/tasks/docs-build-task.js",
    "content": "const path = require('path');\nconst cjsxtransform = require('coffee-react-transform');\nconst rimraf = require('rimraf');\n\nconst fs = require('fs-plus');\nvar fs_extra = require('fs-extra');\n\nconst _ = require('underscore');\n\nconst donna = require('donna');\nconst joanna = require('joanna');\nconst tello = require('tello');\n\nmodule.exports = function(grunt) {\n\n  let {cp, mkdir, rm} = grunt.config('taskHelpers');\n\n  let getClassesToInclude = function() {\n    let modulesPath = path.resolve(__dirname, '..', '..', 'internal_packages');\n    let classes = {};\n    fs.traverseTreeSync(modulesPath, function(modulePath) {\n      // Don't traverse inside dependencies\n      if (modulePath.match(/node_modules/g)) { return false; }\n\n      // Don't traverse blacklisted packages (that have docs, but we don't want to include)\n      if (path.basename(modulePath) !== 'package.json') { return true; }\n      if (!fs.isFileSync(modulePath)) { return true; }\n\n      let apiPath = path.join(path.dirname(modulePath), 'api.json');\n      if (fs.isFileSync(apiPath)) {\n        _.extend(classes, grunt.file.readJSON(apiPath).classes);\n      }\n      return true;\n    });\n    return classes;\n  };\n\n  let sortClasses = function(classes) {\n    let sortedClasses = {};\n    for (let className of Array.from(Object.keys(classes).sort())) {\n      sortedClasses[className] = classes[className];\n    }\n    return sortedClasses;\n  };\n\n  return grunt.registerTask('docs-build', 'Builds the API docs in src', function() {\n\n     grunt.log.writeln(\"Time to build the docs!\")\n\n    let done = this.async();\n\n\n    let classDocsOutputDir = grunt.config.get('classDocsOutputDir');\n\n    let cjsxOutputDir = path.join(classDocsOutputDir, 'temp-cjsx');\n\n    return rimraf(cjsxOutputDir, function() {\n      let api;\n      fs.mkdir(cjsxOutputDir);\n\n      let srcPath = path.resolve(__dirname, '..', '..', 'src');\n\n      const blacklist = ['/K2/',\n                         'legacy-edgehill-api',\n                         'edgehill-api'];\n\n      let in_blacklist = function(file) {\n        for (var i = 0; i < blacklist.length; i++) {\n          if (file.indexOf(blacklist[i]) >= 0) {\n            return true;\n          }\n        }\n        return false;\n      };\n\n      fs.traverseTreeSync(srcPath, function(file) {\n\n        if (in_blacklist(file)) {\n          console.log(\"Skipping \" + file);\n          // Skip K2\n        }\n\n        // Convert CJSX into coffeescript that can be read by Donna\n        else if (path.extname(file) === '.cjsx') {\n          let transformed = cjsxtransform(grunt.file.read(file));\n\n          // Only attempt to parse this file as documentation if it contains\n          // real Coffeescript classes.\n          if (transformed.indexOf('\\nclass ') > 0) {\n\n            grunt.log.writeln(\"Found class in file: \" + file)\n\n            grunt.file.write(path.join(cjsxOutputDir, path.basename(file).slice(0, -5 + 1 || undefined)+'coffee'), transformed);\n          }\n        }\n        else if (path.extname(file) === '.jsx') {\n          console.log('Transforming ' + file)\n\n          let fileStr = grunt.file.read(file);\n\n          let transformed = require(\"babel-core\").transform(fileStr, {\n            plugins: [\"transform-react-jsx\",\n                      \"transform-class-properties\"],\n            presets: ['react', 'stage-2']\n          });\n\n          grunt.file.write(path.join(cjsxOutputDir, path.basename(file).slice(0, -3 || undefined)+'js'), transformed.code);\n        }\n        else if (path.extname(file) == '.es6') {\n            console.log(file);\n\n          let fileStr = grunt.file.read(file);\n\n          let transformed = require(\"babel-core\").transform(fileStr, {\n            plugins: [\"transform-class-properties\",\n                      \"transform-function-bind\"],\n            presets: ['react', 'stage-2']\n\n          });\n\n          if (transformed.code.indexOf('class ') > 0) {\n            grunt.log.writeln(\"Found class in file: \" + file)\n\n            grunt.file.write(path.join(cjsxOutputDir, path.basename(file).slice(0, -3 || undefined)+'js'), transformed.code);\n          }\n        }\n        else if (path.extname(file) == '.coffee' ||\n                 path.extname(file) == '.js') {\n          let dest_path = path.join(cjsxOutputDir, path.basename(file));\n          console.log(\"Copying \" + file + \" to \" + dest_path);\n          fs_extra.copySync(file, dest_path);\n        }\n        return true;\n      });\n\n      grunt.log.ok('Done transforming, starting donna extraction')\n      grunt.log.writeln('cjsxOutputDir: ' + cjsxOutputDir)\n\n      // Process coffeescript source\n      let metadata = donna.generateMetadata([cjsxOutputDir]);\n      grunt.log.ok('---- Done with Donna (cjsx metadata)----');\n\n\n      // DEBUG\n      // Use to check individual files\n      var js_files = []\n      fs.traverseTreeSync(cjsxOutputDir, function(file) {\n        if (path.extname(file) === '.js') {\n          console.log('testing joanna on ' + file)\n          let meta = joanna([file])\n          console.log('testing tello on ' + file)\n          tello.digest(meta)\n          console.log('passed')\n        }\n      });\n\n      var js_files = []\n      fs.traverseTreeSync(cjsxOutputDir, function(file) {\n        if (path.extname(file) === '.js') {\n          js_files.push(file.toString())\n        }\n      });\n\n      console.log(js_files);\n      grunt.log.ok('---- Starting Jonna (jsx metadata)----');\n      let jsx_metadata = joanna(js_files);\n      grunt.log.ok('---- Done with Joanna (jsx metadata)----');\n\n\n      Object.assign(metadata[0].files, jsx_metadata.files);\n      console.log(metadata[0]);\n\n      grunt.file.write('/tmp/metadata.json', JSON.stringify(metadata, null, 2));\n\n\n\n      try {\n        api = tello.digest(metadata);\n      } catch (e) {\n        console.log(e)\n        console.log(e.stack);\n\n        console.log(metadata)\n        return;\n      }\n\n      console.log('---- Done with Tello ----');\n      _.extend(api.classes, getClassesToInclude());\n\n      console.log(api.classes)\n\n\n      api.classes = sortClasses(api.classes);\n      console.log(api.classes)\n\n      let apiJson = JSON.stringify(api, null, 2);\n      let apiJsonPath = path.join(classDocsOutputDir, 'api.json');\n      grunt.file.write(apiJsonPath, apiJson);\n      return done();\n    });\n  });\n\n\n};\n"
  },
  {
    "path": "packages/client-app/build/tasks/docs-render-task.js",
    "content": "const path = require('path');\nconst Handlebars = require('handlebars');\nconst marked = require('meta-marked');\nconst fs = require('fs-plus');\nconst _ = require('underscore');\n\nmarked.setOptions({\n  highlight(code) {\n    return require('highlight.js').highlightAuto(code).value;\n  }\n});\n\nlet standardClassURLRoot = 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/';\n\nlet standardClasses = [\n  'string',\n  'object',\n  'array',\n  'function',\n  'number',\n  'date',\n  'error',\n  'boolean',\n  'null',\n  'undefined',\n  'json',\n  'set',\n  'map',\n  'typeerror',\n  'syntaxerror',\n  'referenceerror',\n  'rangeerror'\n];\n\nlet thirdPartyClasses = {\n  'react.component': 'https://facebook.github.io/react/docs/component-api.html',\n  'promise': 'https://github.com/petkaantonov/bluebird/blob/master/API.md',\n  'range': 'https://developer.mozilla.org/en-US/docs/Web/API/Range',\n  'selection': 'https://developer.mozilla.org/en-US/docs/Web/API/Selection',\n  'node': 'https://developer.mozilla.org/en-US/docs/Web/API/Node',\n};\n\nmodule.exports = function(grunt) {\n\n  let {cp, mkdir, rm} = grunt.config('taskHelpers');\n\n  let relativePathForClass = classname => classname+'.html';\n\n  let outputPathFor = function(relativePath) {\n    let classDocsOutputDir = grunt.config.get('classDocsOutputDir');\n    return path.join(classDocsOutputDir, relativePath);\n  };\n\n  var processFields = function(json, fields, tasks) {\n    let val;\n    if (fields == null) { fields = []; }\n    if (tasks == null) { tasks = []; }\n    if (json instanceof Array) {\n      return (() => {\n        let result = [];\n        for (val of Array.from(json)) {\n          result.push(processFields(val, fields, tasks));\n        }\n        return result;\n      })();\n    } else {\n      return (() => {\n        let result1 = [];\n        for (let key in json) {\n          val = json[key];\n          let item;\n          if (Array.from(fields).includes(key)) {\n            for (let task of Array.from(tasks)) {\n              val = task(val);\n            }\n            json[key] = val;\n          }\n          if (_.isObject(val)) {\n            item = processFields(val, fields, tasks);\n          }\n          result1.push(item);\n        }\n        return result1;\n      })();\n    }\n  };\n\n  return grunt.registerTask('docs-render', 'Builds html from the API docs', function() {\n\n    let documentation, filename, html, match, meta, name, result, section, val;\n    let classDocsOutputDir = grunt.config.get('classDocsOutputDir');\n\n    // Parse API reference Markdown\n\n    let classes = [];\n    let apiJsonPath = path.join(classDocsOutputDir, 'api.json');\n    let apiJSON = JSON.parse(grunt.file.read(apiJsonPath));\n\n\n    for (var classname in apiJSON.classes) {\n      // Parse a \"@Section\" out of the description if one is present\n      let contents = apiJSON.classes[classname];\n      let sectionRegex = /Section: ?([\\w ]*)(?:$|\\n)/;\n      section = 'General';\n\n      match = sectionRegex.exec(contents.description);\n      if (match) {\n        contents.description = contents.description.replace(match[0], '');\n        section = match[1].trim();\n      }\n\n      // Replace superClass \"React\" with \"React.Component\". The Coffeescript Lexer\n      // is so bad.\n      if (contents.superClass === \"React\") {\n        contents.superClass = \"React.Component\";\n      }\n\n      classes.push({\n        name: classname,\n        documentation: contents,\n        section\n      });\n    }\n\n\n    // Build Sidebar metadata we can hand off to each of the templates to\n    // generate the sidebar\n    let sidebar = {};\n    for (var i = 0; i < classes.length; i++) {\n        var current_class = classes[i];\n        console.log(current_class.name + ' ' + current_class.section)\n\n        if (!(current_class.section in sidebar)) {\n          sidebar[current_class.section] = []\n        }\n        sidebar[current_class.section].push(current_class.name)\n    }\n\n\n    // Prepare to render by loading handlebars partials\n    let templatesPath = path.resolve(grunt.config('buildDir'), 'docs_templates');\n    grunt.file.recurse(templatesPath, function(abspath, root, subdir, filename) {\n      if ((filename[0] === '_') && (path.extname(filename) === '.html')) {\n        return Handlebars.registerPartial(filename, grunt.file.read(abspath));\n      }\n    });\n\n    // Render Helpers\n\n    let knownClassnames = {};\n    for (classname in apiJSON.classes) {\n      val = apiJSON.classes[classname];\n      knownClassnames[classname.toLowerCase()] = val;\n    }\n\n\n    let expandTypeReferences = function(val) {\n      let refRegex = /{([\\w.]*)}/g;\n      while ((match = refRegex.exec(val)) !== null) {\n        let term = match[1].toLowerCase();\n        let label = match[1];\n        let url = false;\n        if (Array.from(standardClasses).includes(term)) {\n          url = standardClassURLRoot+term;\n        } else if (thirdPartyClasses[term]) {\n          url = thirdPartyClasses[term];\n        } else if (knownClassnames[term]) {\n          url = relativePathForClass(knownClassnames[term].name);\n          grunt.log.ok(\"Found: \" + term)\n        } else {\n          console.warn(`Cannot find class named ${term}`);\n        }\n\n        if (url) {\n          val = val.replace(match[0], `<a href='${url}'>${label}</a>`);\n        }\n      }\n      return val;\n    };\n\n    let expandFuncReferences = function(val) {\n      let refRegex = /{([\\w]*)?::([\\w]*)}/g;\n      while ((match = refRegex.exec(val)) !== null) {\n        var label;\n        let [text, a, b] = Array.from(match);\n        let url = false;\n        if (a && b) {\n          url = `${relativePathForClass(a)}#${b}`;\n          label = `${a}::${b}`;\n        } else {\n          url = `#${b}`;\n          label = `${b}`;\n        }\n        if (url) {\n          val = val.replace(text, `<a href='${url}'>${label}</a>`);\n        }\n      }\n      return val;\n    };\n\n    // DEBUG Render sidebar json\n    // grunt.file.write(outputPathFor('sidebar.json'), JSON.stringify(sidebar, null, 2));\n\n    // Render Class Pages\n    let classTemplatePath = path.join(templatesPath, 'class.md');\n    let classTemplate = Handlebars.compile(grunt.file.read(classTemplatePath));\n\n    for ({name, documentation, section} of Array.from(classes)) {\n      // Recursively process `description` and `type` fields to process markdown,\n      // expand references to types, functions and other files.\n      processFields(documentation, ['description'], [expandFuncReferences]);\n      processFields(documentation, ['type'], [expandTypeReferences]);\n\n      result = classTemplate({name, documentation, section});\n      grunt.file.write(outputPathFor(name + '.md'), result);\n    }\n\n    let sidebarTemplatePath = path.join(templatesPath, 'sidebar.md');\n    let sidebarTemplate = Handlebars.compile(grunt.file.read(sidebarTemplatePath));\n\n    grunt.file.write(outputPathFor('Sidebar.md'),\n                     sidebarTemplate({sidebar}));\n\n\n    // Remove temp cjsx output\n    return fs.removeSync(outputPathFor(\"temp-cjsx\"));\n  });\n};\n\nfunction __guard__(value, transform) {\n  return (typeof value !== 'undefined' && value !== null) ? transform(value) : undefined;\n}\n"
  },
  {
    "path": "packages/client-app/build/tasks/docs-task.js",
    "content": "const path = require('path');\nconst cjsxtransform = require('coffee-react-transform');\nconst rimraf = require('rimraf');\n\nconst fs = require('fs-plus');\nconst _ = require('underscore');\n\nconst donna = require('donna');\nconst joanna = require('joanna');\nconst tello = require('tello');\n\nmodule.exports = function(grunt) {\n\n  let {cp, mkdir, rm} = grunt.config('taskHelpers');\n\n  let getClassesToInclude = function() {\n    let modulesPath = path.join(grunt.config('appDir'), 'internal_packages');\n    let classes = {};\n    fs.traverseTreeSync(modulesPath, function(modulePath) {\n      // Don't traverse inside dependencies\n      if (modulePath.match(/node_modules/g)) { return false; }\n\n      // Don't traverse blacklisted packages (that have docs, but we don't want to include)\n      if (path.basename(modulePath) !== 'package.json') { return true; }\n      if (!fs.isFileSync(modulePath)) { return true; }\n\n      let apiPath = path.join(path.dirname(modulePath), 'api.json');\n      if (fs.isFileSync(apiPath)) {\n        _.extend(classes, grunt.file.readJSON(apiPath).classes);\n      }\n      return true;\n    });\n    return classes;\n  };\n\n  let sortClasses = function(classes) {\n    let sortedClasses = {};\n    for (let className of Array.from(Object.keys(classes).sort())) {\n      sortedClasses[className] = classes[className];\n    }\n    return sortedClasses;\n  };\n\n  var processFields = function(json, fields, tasks) {\n    let val;\n    if (fields == null) { fields = []; }\n    if (tasks == null) { tasks = []; }\n    if (json instanceof Array) {\n      return (() => {\n        let result = [];\n        for (val of Array.from(json)) {\n          result.push(processFields(val, fields, tasks));\n        }\n        return result;\n      })();\n    } else {\n      return (() => {\n        let result1 = [];\n        for (let key in json) {\n          val = json[key];\n          let item;\n          if (Array.from(fields).includes(key)) {\n            for (let task of Array.from(tasks)) {\n              val = task(val);\n            }\n            json[key] = val;\n          }\n          if (_.isObject(val)) {\n            item = processFields(val, fields, tasks);\n          }\n          result1.push(item);\n        }\n        return result1;\n      })();\n    }\n  };\n\n  return grunt.registerTask('docs', 'Builds the API docs in src', function() {\n\n     grunt.log.writeln(\"Time to build the docs!\")\n\n    let done = this.async();\n\n    // Convert CJSX into coffeescript that can be read by Donna\n\n    // let classDocsOutputDir = grunt.config.get('classDocsOutputDir');\n\n    let classDocsOutputDir = '~/Desktop/Nylas Mail Docs/'\n    let cjsxOutputDir = path.join(classDocsOutputDir, 'temp-cjsx');\n\n    return rimraf(cjsxOutputDir, function() {\n      let api;\n      fs.mkdir(cjsxOutputDir);\n\n      let srcPath = path.resolve(grunt.config('appDir'), 'src');\n\n      fs.traverseTreeSync(srcPath, function(file) {\n\n        if (file.indexOf('/K2/') > 0) {\n          // Skip K2\n        }\n        else if (path.extname(file) === '.cjsx') {  // Should also look for jsx and es6\n          let transformed = cjsxtransform(grunt.file.read(file));\n\n          // Only attempt to parse this file as documentation if it contains\n          // real Coffeescript classes.\n          if (transformed.indexOf('\\nclass ') > 0) {\n\n            grunt.log.writeln(\"Found class in file: \" + file)\n\n            grunt.file.write(path.join(cjsxOutputDir, path.basename(file).slice(0, -5 + 1 || undefined)+'coffee'), transformed);\n          }\n        }\n        else if (path.extname(file) === '.jsx') {\n          console.log('Transforming ' + file)\n\n          let fileStr = grunt.file.read(file);\n\n          let transformed = require(\"babel-core\").transform(fileStr, {\n            plugins: [\"transform-react-jsx\",\n                      \"transform-class-properties\"],\n            presets: ['react', 'stage-2']\n          });\n\n\n          if (transformed.code.indexOf('class ') > 0) {\n            grunt.log.writeln(\"Found class in file: \" + file)\n\n            grunt.file.write(path.join(cjsxOutputDir, path.basename(file).slice(0, -3 || undefined)+'js'), transformed.code);\n          }\n        }\n        return true;\n      });\n\n      grunt.log.ok('Done transforming, starting donna extraction')\n      grunt.log.writeln('cjsxOutputDir: ' + cjsxOutputDir)\n\n      // Process coffeescript source\n      let metadata = donna.generateMetadata([cjsxOutputDir]);\n      grunt.log.ok('---- Done with Donna (cjsx metadata)----');\n\n      console.log(js_files);\n\n      var js_files = []\n      fs.traverseTreeSync(cjsxOutputDir, function(file) {\n        if (path.extname(file) === '.js') {\n          js_files.push(file.toString())\n        }\n      });\n\n      console.log(js_files);\n      let jsx_metadata = joanna(js_files);\n      grunt.log.ok('---- Done with Joanna (jsx metadata)----');\n\n      Object.assign(metadata, jsx_metadata);\n\n      console.log(metadata);\n\n      try {\n        api = tello.digest(metadata);\n      } catch (e) {\n        console.log(e.stack);\n      }\n\n      console.log('---- Done with Tello ----');\n      _.extend(api.classes, getClassesToInclude());\n      api.classes = sortClasses(api.classes);\n\n      let apiJson = JSON.stringify(api, null, 2);\n      let apiJsonPath = path.join(classDocsOutputDir, 'api.json');\n      grunt.file.write(apiJsonPath, apiJson);\n      return done();\n    });\n  });\n\n\n};\n"
  },
  {
    "path": "packages/client-app/build/tasks/eslint-task.js",
    "content": "const chalk = require('chalk');\nconst eslint = require('eslint');\n\nmodule.exports = (grunt) => {\n  grunt.config.merge({\n    eslint: {\n      options: {\n        ignore: false,\n        configFile: '../../.eslintrc',\n      },\n      target: grunt.config('source:es6'),\n    },\n\n    eslintFixer: {\n      src: grunt.config('source:es6'),\n    },\n  });\n\n  grunt.registerMultiTask('eslint', 'Validate files with ESLint', function task() {\n    const opts = this.options({\n      outputFile: false,\n      quiet: false,\n      maxWarnings: -1,\n    });\n\n    if (this.filesSrc.length === 0) {\n      grunt.log.writeln(chalk.magenta('Could not find any files to validate.'));\n      return true;\n    }\n\n    const formatter = eslint.CLIEngine.getFormatter(opts.format);\n\n    if (!formatter) {\n      grunt.warn(`Could not find formatter ${opts.format}.`);\n      return false;\n    }\n\n    const engine = new eslint.CLIEngine(opts);\n\n    let report = null;\n    try {\n      report = engine.executeOnFiles(this.filesSrc);\n    } catch (err) {\n      grunt.warn(err);\n      return false;\n    }\n\n    if (opts.fix) {\n      eslint.CLIEngine.outputFixes(report);\n    }\n\n    let results = report.results;\n    if (opts.quiet) {\n      results = eslint.CLIEngine.getErrorResults(results);\n    }\n\n    const output = formatter(results);\n\n    if (opts.outputFile) {\n      grunt.file.write(opts.outputFile, output);\n    } else if (output) {\n      console.log(output);\n    }\n\n    const tooManyWarnings = opts.maxWarnings >= 0 && report.warningCount > opts.maxWarnings;\n    if (report.errorCount === 0 && tooManyWarnings) {\n      grunt.warn(`ESLint found too many warnings (maximum:${opts.maxWarnings})`);\n    }\n\n    return report.errorCount === 0;\n  });\n};\n"
  },
  {
    "path": "packages/client-app/build/tasks/installer-linux-task.js",
    "content": "/* eslint global-require:0 */\nconst fs = require('fs');\nconst path = require('path');\nconst _ = require('underscore');\n\nmodule.exports = (grunt) => {\n  const {spawn} = grunt.config('taskHelpers');\n\n  const outputDir = grunt.config.get('outputDir');\n  const contentsDir = path.join(grunt.config('outputDir'), `nylas-linux-${process.arch}`);\n  const linuxAssetsDir = path.resolve(path.join(grunt.config('buildDir'), 'resources', 'linux'));\n  const arch = {\n    ia32: 'i386',\n    x64: 'amd64',\n  }[process.arch];\n\n  // a few helpers\n\n  const writeFromTemplate = (filePath, data) => {\n    const template = _.template(String(fs.readFileSync(filePath)))\n    const finishedPath = path.join(outputDir, path.basename(filePath).replace('.in', ''));\n    grunt.file.write(finishedPath, template(data));\n    return finishedPath;\n  }\n\n  const getInstalledSize = (dir, callback) => {\n    const cmd = 'du';\n    const args = ['-sk', dir];\n    spawn({cmd, args}, (error, {stdout}) => {\n      const installedSize = stdout.split(/\\s+/).shift() || '200000'; // default to 200MB\n      callback(null, installedSize);\n    });\n  }\n\n  grunt.registerTask('create-rpm-installer', 'Create rpm package', function mkrpmf() {\n    const done = this.async()\n    if (!arch) {\n      done(new Error(`Unsupported arch ${process.arch}`));\n      return;\n    }\n\n    const rpmDir = path.join(grunt.config('outputDir'), 'rpm');\n    if (grunt.file.exists(rpmDir)) {\n      grunt.file.delete(rpmDir, {force: true});\n    }\n\n    const templateData = {\n      name: grunt.config('appJSON').name,\n      version: grunt.config('appJSON').version,\n      description: grunt.config('appJSON').description,\n      productName: grunt.config('appJSON').productName,\n      linuxShareDir: '/usr/local/share/nylas',\n      linuxAssetsDir: linuxAssetsDir,\n      contentsDir: contentsDir,\n    }\n\n    // This populates nylas.spec\n    const specInFilePath = path.join(linuxAssetsDir, 'redhat', 'nylas.spec.in')\n    writeFromTemplate(specInFilePath, templateData)\n\n    // This populates nylas.desktop\n    const desktopInFilePath = path.join(linuxAssetsDir, 'nylas-mail.desktop.in')\n    writeFromTemplate(desktopInFilePath, templateData)\n\n    const cmd = path.join(grunt.config('appDir'), 'script', 'mkrpm')\n    const args = [outputDir, contentsDir, linuxAssetsDir]\n    spawn({cmd, args}, (error) => {\n      if (error) {\n        return done(error);\n      }\n      grunt.log.ok(`Created rpm package in ${rpmDir}`);\n      return done();\n    });\n  });\n\n  grunt.registerTask('create-deb-installer', 'Create debian package', function mkdebf() {\n    const done = this.async()\n    if (!arch) {\n      done(`Unsupported arch ${process.arch}`);\n      return;\n    }\n\n    getInstalledSize(contentsDir, (error, installedSize) => {\n      if (error) {\n        done(error);\n        return;\n      }\n\n      const version = grunt.config('appJSON').version;\n      const data = {\n        version: version,\n        name: grunt.config('appJSON').name,\n        description: grunt.config('appJSON').description,\n        productName: grunt.config('appJSON').productName,\n        linuxShareDir: '/usr/share/nylas-mail',\n        arch: arch,\n        section: 'devel',\n        maintainer: 'Nylas Team',\n        installedSize: installedSize,\n      }\n      writeFromTemplate(path.join(linuxAssetsDir, 'debian', 'control.in'), data)\n      writeFromTemplate(path.join(linuxAssetsDir, 'nylas-mail.desktop.in'), data)\n\n      const icon = path.join(grunt.config('appDir'), 'build', 'resources', 'nylas.png')\n      const cmd = path.join(grunt.config('appDir'), 'script', 'mkdeb');\n      const args = [version, arch, icon, linuxAssetsDir, contentsDir, outputDir];\n      spawn({cmd, args}, (spawnError) => {\n        if (spawnError) {\n          return done(spawnError);\n        }\n        grunt.log.ok(`Created ${outputDir}/nylas-${version}-${arch}.deb`);\n        return done()\n      });\n    });\n  });\n}\n"
  },
  {
    "path": "packages/client-app/build/tasks/lesslint-task.js",
    "content": "module.exports = (grunt) => {\n  grunt.config.merge({\n    lesslint: {\n      src: [\n        'internal_packages/**/*.less',\n        'dot-nylas/**/*.less',\n        'static/**/*.less',\n      ],\n      options: {\n        less: {\n          paths: ['static', 'static/variables/'],\n        },\n        imports: ['static/variables/*.less'],\n      },\n    },\n  });\n\n  grunt.loadNpmTasks('grunt-contrib-less');\n  grunt.loadNpmTasks('grunt-lesslint');\n}\n"
  },
  {
    "path": "packages/client-app/build/tasks/nylaslint-task.js",
    "content": "/* eslint no-cond-assign: 0 */\nconst path = require('path');\nconst fs = require('fs-plus');\n\nfunction normalizeRequirePath(requirePath, fPath) {\n  if (requirePath[0] === \".\") {\n    return path.normalize(path.join(path.dirname(fPath), requirePath));\n  }\n  return requirePath;\n}\n\n\nmodule.exports = (grunt) => {\n  grunt.config.merge({\n    nylaslint: {\n      src: grunt.config('source:coffeescript').concat(grunt.config('source:es6')),\n    },\n  });\n\n  grunt.registerMultiTask('nylaslint', 'Check requires for file extensions compiled away', function nylaslint() {\n    const done = this.async();\n\n    // Enable once path errors are fixed.\n    if (process.platform === 'win32') {\n      done();\n      return;\n    }\n\n    const extensionRegex = /require ['\"].*\\.(coffee|cjsx|jsx|es6|es)['\"]/i;\n\n    for (const fileset of this.files) {\n      grunt.log.writeln(`Nylinting ${fileset.src.length} files.`);\n\n      const esExtensions = {\n        \".es6\": true,\n        \".es\": true,\n        \".jsx\": true,\n      };\n\n      const errors = [];\n\n      const esExport = {};\n      const esNoExport = {};\n      const esExportDefault = {};\n\n      // Temp TODO. Fix spec files\n      for (const f of fileset.src) {\n        if (!esExtensions[path.extname(f)]) { continue; }\n        if (!/-spec/.test(f)) { continue; }\n\n        const content = fs.readFileSync(f, {encoding: 'utf8'});\n\n        // https://regex101.com/r/rQ3eD0/1\n        // Matches only the first describe block\n        const describeRe = /[\\n]describe\\(['\"](.*?)['\"], ?\\(\\) ?=> ?/m;\n        if (describeRe.test(content)) {\n          errors.push(`${f}: Spec has to start with function`);\n        }\n      }\n\n      // NOTE: Comment me in if you want to fix these files.\n      // _str = require('underscore.string')\n      // replacer = (match, describeName) ->\n      //   fnName = _str.camelize(describeName, true)\n      //   return \"\\ndescribe('#{describeName}', function #{fnName}() \"\n      // newContent = content.replace(describeRe, replacer)\n      // fs.writeFileSync(f, newContent, encoding:'utf8')\n\n      // Build the list of ES6 files that export things and categorize\n      for (const f of fileset.src) {\n        if (!esExtensions[path.extname(f)]) { continue; }\n        const lookupPath = `${path.dirname(f)}/${path.basename(f, path.extname(f))}`;\n        const content = fs.readFileSync(f, {encoding: 'utf8'});\n\n        if (/module.exports\\s?=\\s?.+/gmi.test(content)) {\n          if (!f.endsWith('nylas-exports.es6')) {\n            errors.push(`${f}: Don't use module.exports in ES6`);\n          }\n        }\n\n        if (/^export/gmi.test(content)) {\n          if (/^export default/gmi.test(content)) {\n            esExportDefault[lookupPath] = true;\n          } else {\n            esExport[lookupPath] = true;\n          }\n        } else {\n          esNoExport[lookupPath] = true;\n        }\n      }\n\n      // Now look again through all ES6 files, this time to check imports\n      // instead of exports.\n      for (const f of fileset.src) {\n        let result = null;\n        if (!esExtensions[path.extname(f)]) {\n          continue;\n        }\n        const content = fs.readFileSync(f, {encoding: 'utf8'});\n        const importRe = /import \\{.*\\} from ['\"](.*?)['\"]/gmi;\n\n        while (result = importRe.exec(content)) {\n          for (const requirePath of result.slice(1)) {\n            const lookupPath = normalizeRequirePath(requirePath, f);\n            if (esExportDefault[lookupPath] || esNoExport[lookupPath]) {\n              errors.push(`${f}: Don't destructure default export ${requirePath}`);\n            }\n          }\n        }\n      }\n\n      // Now look through all coffeescript files\n      // If they require things from ES6 files, ensure they're using the\n      // proper syntax.\n      for (const f of fileset.src) {\n        let result = null;\n        if (esExtensions[path.extname(f)]) {\n          continue;\n        }\n        const content = fs.readFileSync(f, {encoding: 'utf8'});\n        if (extensionRegex.test(content)) {\n          errors.push(`${f}: Remove extensions when requiring files`);\n        }\n\n        const requireRe = /require[ (]['\"]([\\w_./-]*?)['\"]/gmi;\n\n        while (result = requireRe.exec(content)) {\n          for (const requirePath of result.slice(1)) {\n            const lookupPath = normalizeRequirePath(requirePath, f);\n\n            const baseRequirePath = path.basename(requirePath);\n\n            const plainRequireRe = new RegExp(`require[ (]['\"].*${baseRequirePath}['\"]\\\\)?$`, \"gm\");\n            const defaultRequireRe = new RegExp(`require\\\\(['\"].*${baseRequirePath}['\"]\\\\)\\\\.default`, \"gm\");\n\n            if (esExport[lookupPath]) {\n              if (!plainRequireRe.test(content)) {\n                errors.push(`${f}: No \\`default\\` exported ${requirePath}`);\n              }\n            } else if (esNoExport[lookupPath]) {\n              errors.push(`${f}: Nothing exported from ${requirePath}`);\n            } else if (esExportDefault[lookupPath]) {\n              if (!defaultRequireRe.test(content)) {\n                errors.push(`${f}: Add \\`default\\` to require ${requirePath}`);\n              }\n            } else {\n              // must be a coffeescript or core file\n              if (defaultRequireRe.test(content)) {\n                errors.push(`${f}: Don't ask for \\`default\\` from ${requirePath}`);\n              }\n            }\n          }\n        }\n      }\n\n      if (errors.length > 0) {\n        for (const err of errors) { grunt.log.error(err); }\n        const error = `\n        Please fix the #{errors.length} linter errors above. These are the issues we're looking for:\n\n        ISSUES WITH COFFEESCRIPT FILES:\n\n        1. Remove extensions when requiring files:\n        Since we compile files in production to plain \".js\" files it's very important you do NOT include the file extension when \"require\"ing a file.\n\n        2. Add \"default\" to require:\n        As of Babel 6, \"require\" no longer returns whatever the \"default\" value is. If you are \"require\"ing an es6 file from a coffeescript file, you must explicitly request the \"default\" property. For example: do \"require('./my-es6-file').default\"\n\n        3. Don't ask for \"default\":\n        If you're requiring a coffeescript file from a coffeescript file, you will almost never need to load a \"default\" object. This is likely an indication you incorrectly thought you were importing an ES6 file.\n\n        ISSUES WITH ES6 FILES:\n\n        4. Don't use module.exports in ES6:\n        You sholudn't manually assign module.exports anymore. Use proper ES6 module syntax like \"export default\" or \"export const FOO\".\n\n        5. Don't destructure default export:\n        If you're using \"import {FOO} from './bar'\" in ES6 files, it's important that \"./bar\" does NOT export a \"default\". Instead, in './bar', do \"export const FOO = 'foo'\"\n\n        6. Spec has to start with function\n        Top-level \"describe\" blocks can no longer use the \"() => {}\" function syntax. This will incorrectly bind \"this\" to the \"window\" object instead of the jasmine object. The top-level \"describe\" block must use the \"function describeName() {}\" syntax\n        `;\n        done(new Error(error));\n      }\n    }\n\n    done(null);\n  });\n}\n"
  },
  {
    "path": "packages/client-app/build/tasks/package-task.js",
    "content": "/* eslint global-require: 0 *//* eslint prefer-template: 0 */\n/* eslint quote-props: 0 */\nconst packager = require('electron-packager');\nconst path = require('path');\nconst util = require('util');\nconst tmpdir = path.resolve(require('os').tmpdir(), 'nylas-build');\nconst fs = require('fs-plus');\nconst coffeereact = require('coffee-react');\nconst glob = require('glob');\nconst babel = require('babel-core');\nconst symlinkedPackages = []\n\nmodule.exports = (grunt) => {\n  const packageJSON = grunt.config('appJSON');\n  const babelPath = path.join(grunt.config('rootDir'), '.babelrc')\n  const babelOptions = JSON.parse(fs.readFileSync(babelPath))\n\n  function runCopyAPM(buildPath, electronVersion, platform, arch, callback) {\n    // Move APM up out of the /app folder which will be inside the ASAR\n    const apmTargetDir = path.resolve(buildPath, '..', 'apm');\n    fs.moveSync(path.join(buildPath, 'apm'), apmTargetDir)\n\n    // Move /apm/node_modules/atom-package-manager up a level. We're\n    // essentially pulling the atom-package-manager module up outside of\n    // the node_modules folder, which is necessary because npmV3 installs\n    // nested dependencies in the same dir.\n    const apmPackageDir = path.join(apmTargetDir, 'node_modules', 'atom-package-manager')\n    for (const name of fs.readdirSync(apmPackageDir)) {\n      fs.renameSync(path.join(apmPackageDir, name), path.join(apmTargetDir, name));\n    }\n\n    const apmSymlink = path.join(apmTargetDir, 'node_modules', '.bin', 'apm');\n    if (fs.existsSync(apmSymlink)) {\n      fs.unlinkSync(apmSymlink);\n    }\n    fs.rmdirSync(apmPackageDir);\n    callback();\n  }\n\n  function runCopyPlatformSpecificResources(buildPath, electronVersion, platform, arch, callback) {\n    // these files (like nylas-mailto-default.reg) go alongside the ASAR,\n    // not inside it, so we need to move out of the `app` directory.\n    const resourcesDir = path.resolve(buildPath, '..');\n    if (platform === 'win32') {\n      fs.copySync(path.resolve(grunt.config('appDir'), 'build', 'resources', 'win'), resourcesDir);\n    }\n    callback();\n  }\n\n  /**\n   * We have to resolve the symlink paths (and cache the results) before\n   * copying over the files since some symlinks may be relative paths (like\n   * those created by lerna). We'll keep absolute references of those paths\n   * for the symlink copy function to use after the packaging is complete.\n   */\n  function resolveRealSymlinkPaths(appDir) {\n    console.log(\"---> Resolving symlinks\");\n    const dirs = [\n      'internal_packages',\n      'src',\n      'spec',\n      'node_modules',\n    ];\n\n    dirs.forEach((dir) => {\n      const absoluteDir = path.join(appDir, dir);\n      fs.readdirSync(absoluteDir).forEach((packageName) => {\n        const relativePackageDir = path.join(dir, packageName)\n        const absolutePackageDir = path.join(absoluteDir, packageName)\n        const realPackagePath = fs.realpathSync(absolutePackageDir).replace('/private/', '/')\n        if (realPackagePath !== absolutePackageDir) {\n          console.log(`  ---> Resolving '${relativePackageDir}' to '${realPackagePath}'`)\n          symlinkedPackages.push({realPackagePath, relativePackageDir})\n        }\n      });\n    });\n  }\n\n  function runCopySymlinkedPackages(buildPath, electronVersion, platform, arch, callback) {\n    console.log(\"---> Moving symlinked node modules / internal packages into build folder.\")\n\n    symlinkedPackages.forEach(({realPackagePath, relativePackageDir}) => {\n      const packagePath = path.join(buildPath, relativePackageDir)\n      console.log(`  ---> Copying ${realPackagePath} to ${packagePath}`);\n      fs.removeSync(packagePath);\n      fs.copySync(realPackagePath, packagePath);\n    });\n\n    callback();\n  }\n\n  /**\n   * We don't need the K2 folder anymore since the previous step hard\n   * copied the client-sync package (and its isomorphic-core dependency)\n   * into /internal_packages. The remains of the folder are N1-Cloud\n   * pieces that aren't necessary\n   */\n  function removeUnnecessaryFiles(buildPath, electronVersion, platform, arch, callback) {\n    fs.removeSync(path.join(buildPath, 'src', 'K2'))\n    callback();\n  }\n\n  function runTranspilers(buildPath, electronVersion, platform, arch, callback) {\n    console.log(\"---> Running babel and coffeescript transpilers\")\n\n    grunt.config('source:coffeescript').forEach(pattern => {\n      glob.sync(pattern, {cwd: buildPath}).forEach((relPath) => {\n        const coffeepath = path.join(buildPath, relPath)\n        if (/(node_modules|\\.js$)/.test(coffeepath)) return\n        console.log(`  ---> Compiling ${coffeepath.slice(coffeepath.indexOf(\"/app\") + 4)}`)\n        const outPath = coffeepath.replace(path.extname(coffeepath), '.js');\n        const res = coffeereact.compile(grunt.file.read(coffeepath), {\n          bare: false,\n          join: false,\n          separator: grunt.util.normalizelf(grunt.util.linefeed),\n\n          sourceMap: true,\n          sourceRoot: '/',\n          generatedFile: path.basename(outPath),\n          sourceFiles: [path.relative(buildPath, coffeepath)],\n        });\n        grunt.file.write(outPath, `${res.js}\\n//# sourceMappingURL=${path.basename(outPath)}.map\\n`);\n        grunt.file.write(`${outPath}.map`, res.v3SourceMap);\n        fs.unlinkSync(coffeepath);\n      });\n    });\n\n    grunt.config('source:es6').forEach(pattern => {\n      glob.sync(pattern, {cwd: buildPath}).forEach((relPath) => {\n        const es6Path = path.join(buildPath, relPath)\n        if (/(node_modules|\\.js$)/.test(es6Path)) return\n        const outPath = es6Path.replace(path.extname(es6Path), '.js');\n        console.log(`  ---> Compiling ${es6Path.slice(es6Path.indexOf(\"/app\") + 4)}`)\n        const res = babel.transformFileSync(es6Path, Object.assign(babelOptions, {\n          sourceMaps: true,\n          sourceRoot: '/',\n          sourceMapTarget: path.relative(buildPath, outPath),\n          sourceFileName: path.relative(buildPath, es6Path),\n        }));\n        grunt.file.write(outPath, `${res.code}\\n//# sourceMappingURL=${path.basename(outPath)}.map\\n`);\n        grunt.file.write(`${outPath}.map`, JSON.stringify(res.map));\n        fs.unlinkSync(es6Path);\n      });\n    });\n\n    callback();\n  }\n\n  const platform = grunt.option('platform');\n\n  // See: https://github.com/electron-userland/electron-packager/blob/master/usage.txt\n  grunt.config.merge({\n    'packager': {\n      'app-version': packageJSON.version,\n      'platform': platform,\n      'protocols': [{\n        name: \"Nylas Protocol\",\n        schemes: [\"nylas\"],\n      }, {\n        name: \"Mailto Protocol\",\n        schemes: [\"mailto\"],\n      }],\n      'dir': grunt.config('appDir'),\n      'app-category-type': \"public.app-category.business\",\n      'tmpdir': tmpdir,\n      'arch': {\n        'win32': 'ia32',\n      }[platform],\n      'icon': {\n        darwin: path.resolve(grunt.config('appDir'), 'build', 'resources', 'mac', 'nylas.icns'),\n        win32: path.resolve(grunt.config('appDir'), 'build', 'resources', 'win', 'nylas.ico'),\n        linux: undefined,\n      }[platform],\n      'name': {\n        darwin: 'Nylas Mail',\n        win32: 'nylas',\n        linux: 'nylas',\n      }[platform],\n      'app-copyright': `Copyright (C) 2014-${new Date().getFullYear()} Nylas, Inc. All rights reserved.`,\n      'derefSymlinks': false,\n      'asar': {\n        'unpack': \"{\" + [\n          '*.node',\n          '**/vendor/**',\n          'examples/**',\n          '**/src/tasks/**',\n          '**/node_modules/spellchecker/**',\n          '**/node_modules/windows-shortcuts/**',\n        ].join(',') + \"}\",\n      },\n      \"ignore\": [ // These are all relative to client-app\n        /.*\\.watchmanconfig.*/,\n\n        // top level dirs we never want\n        /^\\/build.*/,\n        /^\\/dist.*/,\n        /^\\/docs.*/,\n        /^\\/docs_src.*/,\n        /^\\/script.*/,\n        /^\\/spec.*/,\n\n        // general dirs we never want\n        /[/]+gh-pages$/,\n        /[/]+docs$/,\n        /[/]+obj[/]+gen/,\n        /[/]+\\.deps$/,\n\n        // File types we know we never want in the prod build\n        /\\.md$/i,\n        /\\.log$/i,\n        /\\.yml$/i,\n        /\\.gz/i,\n        /\\.zip/i,\n        /\\.pdb$/,\n        /\\.h$/,\n        /\\.cc$/,\n        /\\.ts$/,\n        /\\.flow$/,\n        /\\.gyp/,\n        /\\.mk/,\n        /\\.dYSM$/,\n\n        // specific (large) module bits we know we don't need\n        /node_modules[/]+less[/]+dist$/,\n        /node_modules[/]+react[/]+dist$/,\n        /node_modules[/].*[/]tests?$/,\n        /node_modules[/].*[/]coverage$/,\n        /node_modules[/].*[/]benchmark$/,\n        /@paulbetts[/]+cld[/]+deps[/]+cld/,\n      ],\n      'out': grunt.config('outputDir'),\n      'overwrite': true,\n      'prune': true,\n      'win32metadata': {\n        CompanyName: 'Nylas, Inc.',\n        FileDescription: 'Nylas Mail',\n        LegalCopyright: `Copyright (C) 2014-${new Date().getFullYear()} Nylas, Inc. All rights reserved.`,\n        ProductName: 'Nylas Mail',\n      },\n      // NOTE: The following plist keys can NOT be set in the\n      // nylas-Info.plist since they are manually overridden by\n      // electron-packager based on this config file:\n      //\n      // CFBundleDisplayName: 'name',\n      // CFBundleExecutable: 'name',\n      // CFBundleIdentifier: 'app-bundle-id',\n      // CFBundleName: 'name'\n      //\n      // See https://github.com/electron-userland/electron-packager/blob/master/mac.js#L50\n      //\n      // Our own nylas-Info.plist gets extended on top of the\n      // Electron.app/Contents/Info.plist. A majority of the defaults are\n      // left in the Electron Info.plist file\n      'extend-info': path.resolve(grunt.config('appDir'), 'build', 'resources', 'mac', 'nylas-Info.plist'),\n      'app-bundle-id': \"com.nylas.nylas-mail\",\n      'afterCopy': [\n        runCopyPlatformSpecificResources,\n        runCopyAPM,\n        runCopySymlinkedPackages,\n        removeUnnecessaryFiles,\n        runTranspilers,\n      ],\n    },\n  })\n\n  grunt.registerTask('package', 'Package Nylas Mail', function pack() {\n    const done = this.async();\n    const start = Date.now();\n\n    console.log('---> Running packager with options:');\n    console.log(util.inspect(grunt.config.get('packager'), true, 7, true));\n\n    const ongoing = setInterval(() => {\n      const elapsed = Math.round((Date.now() - start) / 1000.0)\n      console.log(`---> Packaging for ${elapsed}s`);\n    }, 1000)\n\n    resolveRealSymlinkPaths(grunt.config('appDir'))\n\n    packager(grunt.config.get('packager'), (err, appPaths) => {\n      clearInterval(ongoing)\n      if (err) {\n        grunt.fail.fatal(err);\n        return done(err);\n      }\n      console.log(`---> Done Successfully. Built into: ${appPaths}`);\n      return done();\n    });\n  });\n};\n"
  },
  {
    "path": "packages/client-app/build/tasks/task-helpers.js",
    "content": "const childProcess = require('child_process');\n\nmodule.exports = (grunt) => {\n  function spawn(options, callback) {\n    const stdout = [];\n    const stderr = [];\n    let error = null;\n    const proc = childProcess.spawn(options.cmd, options.args, options.opts);\n    proc.stdout.on('data', data => stdout.push(data.toString()));\n    proc.stderr.on('data', data => stderr.push(data.toString()));\n    proc.on('error', (processError) => {\n      return error != null ? error : (error = processError)\n    });\n    proc.on('close', (exitCode, signal) => {\n      if (exitCode !== 0) { if (typeof error === 'undefined' || error === null) { error = new Error(signal); } }\n      const results = {stderr: stderr.join(''), stdout: stdout.join(''), code: exitCode};\n      if (exitCode !== 0) { grunt.log.error(results.stderr); }\n      return callback(error, results, exitCode);\n    });\n  }\n\n  function spawnP(options) {\n    return new Promise((resolve, reject) => {\n      spawn(options, (error) => {\n        if (error) return reject(error);\n        return resolve()\n      })\n    })\n  }\n\n  return {spawn, spawnP};\n}\n"
  },
  {
    "path": "packages/client-app/docs/ContinuousIntegration.md",
    "content": "# Building N1 with Continuous Integration\n\n    script/grunt ci\n\nN1 is designed to be built into a production app for Mac, Windows, and Linux.\nOnly Nylas core team members currently have access to produce a production\nbuild.\n\nProduction builds are code-signed with a Nylas, Inc. certificate and include a\nhandful of other proprietary assets such as custom fonts and sounds.\n\nWe currently use [Travis](https://travis-ci.org/nylas/nylas-mail) to build\non Mac & Windows and AppVeyor to build on Windows.\n\nA build can be run from a local machines by Jenkins or manually; however,\nseveral environment variables must be setup.:\n\n**ALL ENVIRONMENT VARIABLES ARE ENCRYPTED**\n\nThey exist in an encrypted file that only Travis can read in\n`build/resources/certs/set_env.sh`\n\n**IMPORTANT** Do NOT remove the `2>/dev/null 1>/dev/null` in the\n`before_install` scripts. If any of commands fail we don't want to leak\nsensitive data in the output.\n\nThat file must be decrypted and `source`d before the environment variables can\nuse.\n\nIf not building on Travis, the environment variables must be manually decrypted\nvia gpg and sourced\n\nWe use [Travis encryption](https://docs.travis-ci.com/user/encrypting-files/)\nand AppVeyor encryption to store the certificates, keys, and passwords\n\nTo login to GitHub and clone the Nylas submodule with private assets you need\nto clone recursively (or `git submodule init; git submodule update`) with a\nvalid SSH key or login username and password.\n\nWe have a CI GitHub account: https://github.com/nylas-deploy-scripts\nThe password for that account is stored in the environment variable:\n- `GITHUB_CI_ACCOUNT_PASSWORD`\n\nFor signing builds on Mac only when the certificates are already in the\nKeychain (not Travis):\n- `XCODE_KEYCHAIN` - The name of the Mac keychain that contains the\n  certificates and private key.\n- `XCODE_KEYCHAIN_PASSWORD` - Th password to that keychain.\n- `KEYCHAIN_ACCESS` - Alternatively, the `XCODE_KEYCHAIN` and\n  `XCODE_KEYCHAIN_PASSWORD` in a single colon-separated string.\n\nAlternatively, on Travis we decrypt the actual certificate files and create a\ntemporary keychain. To do this we need the password to the private key. That's\nstored in:\n- `APPLE_CODESIGN_KEY_PASSWORD`\n\nFor signing builds on Windows only:\n- `CERTIFICATE_FILE` - The Windows certificate\n- `CERTIFICATE_PASSWORD` - The password for the private key on the cert\n\nTo download Electron:\n- `NYLAS_GITHUB_OAUTH_TOKEN` - The OAuth token to use for GitHub API requests. See\n  https://github.com/atom/grunt-download-electron\n\nTo upload built artifacts to S3:\n- `AWS_ACCESS_KEY_ID`\n- `AWS_SECRET_ACCESS_KEY`\n\nTo notify when builds are done:\n- `NYLAS_INTERNAL_HOOK_URL` - Nylas internal Slack token and url\n"
  },
  {
    "path": "packages/client-app/docs/README.md",
    "content": "These are now available at [https://nylas.github.io/nylas-mail](https://nylas.github.io/nylas-mail)"
  },
  {
    "path": "packages/client-app/dot-nylas/README.md",
    "content": "# Default Config\n\nThese are the default Nylas configs. This folder on setup is copied to\n`~/.nylas-mail` on unix machines.\n"
  },
  {
    "path": "packages/client-app/dot-nylas/config.json",
    "content": "{\n  \"*\": {\n    \"env\": \"production\",\n    \"core\": {\n      \"themes\": [\n        \"ui-light\"\n      ],\n      \"disabledPackages\": [\n        \"message-view-on-github\",\n        \"personal-level-indicators\",\n        \"phishing-detection\",\n        \"nylas-private-salesforce\",\n        \"github-contact-card\",\n        \"keybase\",\n        \"thread-sharing\",\n        \"composer-markdown\",\n        \"composer-scheduler\",\n        \"composer-mail-merge\",\n        \"send-and-archive\",\n        \"main-calendar\",\n        \"open-tracking\",\n        \"link-tracking\",\n        \"send-later\",\n        \"thread-snooze\",\n        \"activity-list\"\n      ]\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/dot-nylas/keymap.json",
    "content": ""
  },
  {
    "path": "packages/client-app/dot-nylas/packages/README.md",
    "content": "All packages in this directory will be automatically loaded\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/README.md",
    "content": "# React version of thread list\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/account-commands.coffee",
    "content": "_ = require 'underscore'\n{Actions, MenuHelpers} = require 'nylas-exports'\n\n\nclass AccountCommands\n\n  @_focusAccounts: (accounts) ->\n    Actions.focusDefaultMailboxPerspectiveForAccounts(accounts)\n    NylasEnv.show() unless NylasEnv.isVisible()\n\n  @_isSelected: (account, sidebarAccountIds) =>\n    if sidebarAccountIds.length > 1\n      return account instanceof Array\n    else if sidebarAccountIds.length is 1\n      return account?.id is sidebarAccountIds[0]\n    else\n      return false\n\n  @registerCommands: (accounts) ->\n    @_commandsDisposable?.dispose()\n    commands = {}\n\n    allKey = \"window:select-account-0\"\n    commands[allKey] = @_focusAccounts.bind(@, accounts)\n\n    [1..8].forEach (index) =>\n      account = accounts[index - 1]\n      return unless account\n      key = \"window:select-account-#{index}\"\n      commands[key] = @_focusAccounts.bind(@, [account])\n\n    @_commandsDisposable = NylasEnv.commands.add(document.body, commands)\n\n  @registerMenuItems: (accounts, sidebarAccountIds) ->\n    windowMenu = _.find NylasEnv.menu.template, ({label}) ->\n      MenuHelpers.normalizeLabel(label) is 'Window'\n    return unless windowMenu\n\n    submenu = _.reject windowMenu.submenu, (item) -> item.account\n    return unless submenu\n\n    idx = _.findIndex submenu, ({type}) -> type is 'separator'\n    return unless idx > 0\n\n    template = @menuTemplate(accounts, sidebarAccountIds)\n    submenu.splice(idx + 1, 0, template...)\n    windowMenu.submenu = submenu\n    NylasEnv.menu.update()\n\n  @menuItem: (account, idx, {isSelected, clickHandlers} = {}) =>\n    item = {\n      label: account.label ? \"All Accounts\",\n      command: \"window:select-account-#{idx}\",\n      account: true\n    }\n    if isSelected\n      item.type = 'checkbox'\n      item.checked = true\n    if clickHandlers\n      accounts = if account instanceof Array then account else [account]\n      item.click = @_focusAccounts.bind(@, accounts)\n      item.accelerator = \"CmdOrCtrl+#{idx + 1}\"\n    return item\n\n  @menuTemplate: (accounts, sidebarAccountIds, {clickHandlers} = {}) =>\n    template = []\n    multiAccount = accounts.length > 1\n\n    if multiAccount\n      isSelected = @_isSelected(accounts, sidebarAccountIds)\n      template = [\n        @menuItem(accounts, 0, {isSelected, clickHandlers})\n      ]\n\n    template = template.concat accounts.map((account, idx) =>\n      # If there's only one account, it should be mapped to command+1, not command+2\n      accIdx = if multiAccount then idx + 1 else idx\n      isSelected = @_isSelected(account, sidebarAccountIds)\n      return @menuItem(account, accIdx, {isSelected, clickHandlers})\n    )\n    return template\n\n  @register: (accounts, sidebarAccountIds) ->\n    @registerCommands(accounts)\n    @registerMenuItems(accounts, sidebarAccountIds)\n\n\nmodule.exports = AccountCommands\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/components/account-sidebar.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\n{Utils, AccountStore} = require 'nylas-exports'\n{OutlineView, ScrollRegion, Flexbox} = require 'nylas-component-kit'\nAccountSwitcher = require './account-switcher'\nSidebarStore = require '../sidebar-store'\n\n\nclass AccountSidebar extends React.Component\n  @displayName: 'AccountSidebar'\n\n  @containerRequired: false\n  @containerStyles:\n    minWidth: 165\n    maxWidth: 250\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @unsubscribers = []\n    @unsubscribers.push SidebarStore.listen @_onStoreChange\n    @unsubscribers.push AccountStore.listen @_onStoreChange\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  componentWillUnmount: =>\n    unsubscribe() for unsubscribe in @unsubscribers\n\n  _onStoreChange: =>\n    @setState @_getStateFromStores()\n\n  _getStateFromStores: =>\n    accounts: AccountStore.accounts()\n    sidebarAccountIds: SidebarStore.sidebarAccountIds()\n    userSections: SidebarStore.userSections()\n    standardSection: SidebarStore.standardSection()\n\n  _renderUserSections: (sections) =>\n    sections.map (section) =>\n      <OutlineView key={section.title} {...section} />\n\n  render: =>\n    {accounts, sidebarAccountIds, userSections, standardSection} = @state\n\n    <Flexbox direction=\"column\" style={order: 0, flexShrink: 1, flex: 1}>\n      <ScrollRegion className=\"account-sidebar\" style={order: 2}>\n        <AccountSwitcher accounts={accounts} sidebarAccountIds={sidebarAccountIds} />\n        <div className=\"account-sidebar-sections\">\n          <OutlineView {...standardSection} />\n          {@_renderUserSections(userSections)}\n        </div>\n      </ScrollRegion>\n    </Flexbox>\n\n\nmodule.exports = AccountSidebar\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/components/account-switcher.cjsx",
    "content": "React = require 'react'\n{Actions} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\nAccountCommands = require '../account-commands'\n\n\nclass AccountSwitcher extends React.Component\n  @displayName: 'AccountSwitcher'\n\n  @propTypes:\n    accounts: React.PropTypes.array.isRequired\n    sidebarAccountIds: React.PropTypes.array.isRequired\n\n\n  _makeMenuTemplate: =>\n    template = AccountCommands.menuTemplate(\n      @props.accounts,\n      @props.sidebarAccountIds,\n      clickHandlers: true\n    )\n    template = template.concat [\n      {type: 'separator'}\n      {label: 'Add Account...', click: @_onAddAccount}\n      {label: 'Manage Accounts...', click: @_onManageAccounts}\n    ]\n    return template\n\n  # Handlers\n\n  _onAddAccount: =>\n    ipc = require('electron').ipcRenderer\n    ipc.send('command', 'application:add-account', {source: 'Sidebar'})\n\n  _onManageAccounts: =>\n    Actions.switchPreferencesTab('Accounts')\n    Actions.openPreferences()\n\n  _onShowMenu: =>\n    remote = require('electron').remote\n    Menu = remote.Menu\n    menu = Menu.buildFromTemplate(@_makeMenuTemplate())\n    menu.popup()\n\n  render: =>\n    <div className=\"account-switcher\" onMouseDown={@_onShowMenu}>\n      <RetinaImg\n        style={width: 13, height: 14}\n        name=\"account-switcher-dropdown.png\"\n        mode={RetinaImg.Mode.ContentDark} />\n    </div>\n\n\nmodule.exports = AccountSwitcher\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/main.coffee",
    "content": "React = require \"react\"\nAccountSidebar = require \"./components/account-sidebar\"\n{ComponentRegistry, WorkspaceStore} = require \"nylas-exports\"\n\nmodule.exports =\n  item: null # The DOM item the main React component renders into\n\n  activate: (@state) ->\n    ComponentRegistry.register AccountSidebar,\n      location: WorkspaceStore.Location.RootSidebar\n\n  deactivate: (@state) ->\n    ComponentRegistry.unregister(AccountSidebar)\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/sidebar-actions.coffee",
    "content": "Reflux = require 'reflux'\n\nActions = [\n  'focusAccounts',\n  'setKeyCollapsed',\n]\n\nfor idx in Actions\n  Actions[idx] = Reflux.createAction(Actions[idx])\n  Actions[idx].sync = true\n\nmodule.exports = Actions\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/sidebar-item.coffee",
    "content": "_ = require 'underscore'\n_str = require 'underscore.string'\n{WorkspaceStore,\n MailboxPerspective,\n FocusedPerspectiveStore,\n SyncbackCategoryTask,\n DestroyCategoryTask,\n CategoryStore,\n Actions,\n Utils,\n RegExpUtils} = require 'nylas-exports'\n{OutlineViewItem} = require 'nylas-component-kit'\n\nSidebarActions = require './sidebar-actions'\n\nidForCategories = (categories) ->\n  _.pluck(categories, 'id').join('-')\n\ncountForItem = (perspective) ->\n  unreadCountEnabled = NylasEnv.config.get('core.workspace.showUnreadForAllCategories')\n  if perspective.isInbox() or unreadCountEnabled\n    return perspective.unreadCount()\n  return 0\n\nisItemSelected = (perspective) ->\n  (WorkspaceStore.rootSheet() in [WorkspaceStore.Sheet.Threads, WorkspaceStore.Sheet.Drafts] and\n    FocusedPerspectiveStore.current().isEqual(perspective))\n\nisItemCollapsed = (id) ->\n  if NylasEnv.savedState.sidebarKeysCollapsed[id] isnt undefined\n    NylasEnv.savedState.sidebarKeysCollapsed[id]\n  else\n    true\n\ntoggleItemCollapsed = (item) ->\n  return unless item.children.length > 0\n  SidebarActions.setKeyCollapsed(item.id, not isItemCollapsed(item.id))\n\nonDeleteItem = (item) ->\n  # TODO Delete multiple categories at once\n  return if item.deleted is true\n  category = item.perspective.category()\n  return unless category\n\n  Actions.queueTask(new DestroyCategoryTask({category}))\n\nonEditItem = (item, value) ->\n  return unless value\n  return if item.deleted is true\n  category = item.perspective.category()\n  return unless category\n  re = RegExpUtils.subcategorySplitRegex()\n  match = re.exec(category.displayName)\n  lastMatch = match\n  while match\n    lastMatch = match\n    match = re.exec(category.displayName)\n  if lastMatch\n    newDisplayName = category.displayName.slice(0, lastMatch.index + 1) + value\n  else\n    newDisplayName = value\n  if newDisplayName is category.displayName\n    return\n  Actions.queueTask(new SyncbackCategoryTask({category, displayName: newDisplayName}))\n\n\nclass SidebarItem\n\n  @forPerspective: (id, perspective, opts = {}) ->\n    counterStyle = OutlineViewItem.CounterStyles.Alt if perspective.isInbox()\n\n    return _.extend({\n      id: id\n      name: perspective.name\n      contextMenuLabel: perspective.name\n      count: countForItem(perspective)\n      iconName: perspective.iconName\n      children: []\n      perspective: perspective\n      selected: isItemSelected(perspective)\n      collapsed: isItemCollapsed(id) ? true\n      counterStyle: counterStyle\n      onDelete: if opts.deletable then onDeleteItem else undefined\n      onEdited: if opts.editable then onEditItem else undefined\n      onCollapseToggled: toggleItemCollapsed\n\n      onDrop: (item, event) ->\n        jsonString = event.dataTransfer.getData('nylas-threads-data')\n        jsonData = null\n        try\n          jsonData = JSON.parse(jsonString)\n        catch err\n          console.error(\"JSON parse error: #{err}\")\n        return unless jsonData\n        Actions.moveThreadsToPerspective({\n          targetPerspective: item.perspective,\n          threadIds: jsonData.threadIds,\n          accountIds: jsonData.accountIds,\n        })\n\n      shouldAcceptDrop: (item, event) ->\n        target = item.perspective\n        current = FocusedPerspectiveStore.current()\n        return false unless event.dataTransfer.types.includes('nylas-threads-data')\n        return false if target.isEqual(current)\n\n        # We can't inspect the drag payload until drop, so we use a dataTransfer\n        # type to encode the account IDs of threads currently being dragged.\n        accountsType = event.dataTransfer.types.find((t) => t.startsWith('nylas-accounts='))\n        accountIds = (accountsType || \"\").replace('nylas-accounts=', '').split(',')\n        return target.canReceiveThreadsFromAccountIds(accountIds)\n\n      onSelect: (item) ->\n        Actions.focusMailboxPerspective(item.perspective)\n    }, opts)\n\n\n  @forCategories: (categories = [], opts = {}) ->\n    id = idForCategories(categories)\n    contextMenuLabel = _str.capitalize(categories[0]?.displayType())\n    perspective = MailboxPerspective.forCategories(categories)\n\n    opts.deletable ?= true\n    opts.editable ?= true\n    opts.contextMenuLabel = contextMenuLabel\n    @forPerspective(id, perspective, opts)\n\n  @forSnoozed: (accountIds, opts = {}) ->\n    # TODO This constant should be available elsewhere\n    constants = require('../../thread-snooze/lib/snooze-constants')\n    displayName = constants.SNOOZE_CATEGORY_NAME\n    id = displayName\n    id += \"-#{opts.name}\" if opts.name\n    opts.name = \"Snoozed\" unless opts.name\n    opts.iconName= 'snooze.png'\n\n    categories = accountIds.map (accId) =>\n      _.findWhere CategoryStore.categories(accId), {displayName}\n    categories = _.compact(categories)\n\n    perspective = MailboxPerspective.forCategories(categories)\n    perspective.name = id unless perspective.name\n    @forPerspective(id, perspective, opts)\n\n  @forStarred: (accountIds, opts = {}) ->\n    perspective = MailboxPerspective.forStarred(accountIds)\n    id = 'Starred'\n    id += \"-#{opts.name}\" if opts.name\n    @forPerspective(id, perspective, opts)\n\n  @forUnread: (accountIds, opts = {}) ->\n    categories = accountIds.map (accId) =>\n      CategoryStore.getStandardCategory(accId, 'inbox')\n\n    # NOTE: It's possible for an account to not yet have an `inbox`\n    # category. Since the `SidebarStore` triggers on `AccountStore`\n    # changes, it'll trigger the exact moment an account is added to the\n    # config. However, the API has not yet come back with the list of\n    # `categories` for that account.\n    categories = _.compact(categories)\n\n    perspective = MailboxPerspective.forUnread(categories)\n    id = 'Unread'\n    id += \"-#{opts.name}\" if opts.name\n    @forPerspective(id, perspective, opts)\n\n  @forDrafts: (accountIds, opts = {}) ->\n    perspective = MailboxPerspective.forDrafts(accountIds)\n    id = \"Drafts-#{opts.name}\"\n    @forPerspective(id, perspective, opts)\n\nmodule.exports = SidebarItem\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/sidebar-section.coffee",
    "content": "_ = require 'underscore'\n{Actions,\n SyncbackCategoryTask,\n DestroyCategoryTask,\n CategoryStore,\n Category,\n ExtensionRegistry,\n RegExpUtils} = require 'nylas-exports'\nSidebarItem = require './sidebar-item'\nSidebarActions = require './sidebar-actions'\n\nisSectionCollapsed = (title) ->\n  if NylasEnv.savedState.sidebarKeysCollapsed[title] isnt undefined\n    NylasEnv.savedState.sidebarKeysCollapsed[title]\n  else\n    false\n\ntoggleSectionCollapsed = (section) ->\n  return unless section\n  SidebarActions.setKeyCollapsed(section.title, not isSectionCollapsed(section.title))\n\nclass SidebarSection\n\n  @empty: (title) ->\n    return {\n      title,\n      items: []\n    }\n\n  @standardSectionForAccount: (account) ->\n    if not account\n      throw new Error(\"standardSectionForAccount: You must pass an account.\")\n\n    cats = CategoryStore.standardCategories(account)\n    return @empty(account.label) if cats.length is 0\n\n    items = _\n      .reject(cats, (cat) -> cat.name is 'drafts')\n      .map (cat) => SidebarItem.forCategories([cat], editable: false, deletable: false)\n\n    unreadItem = SidebarItem.forUnread([account.id])\n    starredItem = SidebarItem.forStarred([account.id])\n    draftsItem = SidebarItem.forDrafts([account.id])\n    snoozedItem = SidebarItem.forSnoozed([account.id])\n\n    extensionItems = ExtensionRegistry.AccountSidebar.extensions()\n    .filter((ext) => ext.sidebarItem?)\n    .map((ext) => ext.sidebarItem([account.id]))\n    .map(({id, name, iconName, perspective}) =>\n      SidebarItem.forPerspective(id, perspective, {name, iconName})\n    )\n\n    # Order correctly: Inbox, Unread, Starred, rest... , Drafts\n    items.splice(1, 0, unreadItem, starredItem, snoozedItem, extensionItems...)\n    items.push(draftsItem)\n\n    return {\n      title: account.label\n      items: items\n    }\n\n  @standardSectionForAccounts: (accounts) ->\n    return @empty('All Accounts') if not accounts or accounts.length is 0\n    return @empty('All Accounts') if CategoryStore.categories().length is 0\n    return @standardSectionForAccount(accounts[0]) if accounts.length is 1\n\n    standardNames = [\n      'inbox',\n      'important'\n      'sent',\n      ['archive', 'all'],\n      'spam'\n      'trash'\n    ]\n    items = []\n\n    for names in standardNames\n      names = if Array.isArray(names) then names else [names]\n      categories = CategoryStore.getStandardCategories(accounts, names...)\n      continue if categories.length is 0\n\n      children = []\n      accounts.forEach (acc) ->\n        cat = _.first(_.compact(\n          names.map((name) -> CategoryStore.getStandardCategory(acc, name))\n        ))\n        return unless cat\n        children.push(SidebarItem.forCategories([cat], name: acc.label, editable: false, deletable: false))\n\n      items.push SidebarItem.forCategories(categories, {children, editable: false, deletable: false})\n\n    accountIds = _.pluck(accounts, 'id')\n\n    starredItem = SidebarItem.forStarred(accountIds,\n      children: accounts.map (acc) -> SidebarItem.forStarred([acc.id], name: acc.label)\n    )\n    unreadItem = SidebarItem.forUnread(accountIds,\n      children: accounts.map (acc) -> SidebarItem.forUnread([acc.id], name: acc.label)\n    )\n    draftsItem = SidebarItem.forDrafts(accountIds,\n      children: accounts.map (acc) -> SidebarItem.forDrafts([acc.id], name: acc.label)\n    )\n    snoozedItem = SidebarItem.forSnoozed(accountIds,\n      children: accounts.map (acc) -> SidebarItem.forSnoozed([acc.id], name: acc.label)\n    )\n\n    extensionItems =  ExtensionRegistry.AccountSidebar.extensions()\n    .filter((ext) => ext.sidebarItem?)\n    .map((ext) =>\n      {id, name, iconName, perspective} = ext.sidebarItem(accountIds)\n      return SidebarItem.forPerspective(id, perspective, {\n        name,\n        iconName,\n        children: accounts.map((acc) =>\n          subItem = ext.sidebarItem([acc.id])\n          return SidebarItem.forPerspective(\n            subItem.id + \"-#{acc.id}\",\n            subItem.perspective,\n            {name: acc.label, iconName: subItem.iconName}\n          )\n        )\n      })\n    )\n\n    # Order correctly: Inbox, Unread, Starred, rest... , Drafts\n    items.splice(1, 0, unreadItem, starredItem, snoozedItem, extensionItems...)\n    items.push(draftsItem)\n\n    return {\n      title: 'All Accounts'\n      items: items\n    }\n\n\n  @forUserCategories: (account, {title, collapsible} = {}) ->\n    return unless account\n    # Compute hierarchy for user categories using known \"path\" separators\n    # NOTE: This code uses the fact that userCategoryItems is a sorted set, eg:\n    #\n    # Inbox\n    # Inbox.FolderA\n    # Inbox.FolderA.FolderB\n    # Inbox.FolderB\n    #\n    items = []\n    seenItems = {}\n    for category in CategoryStore.userCategories(account)\n      # https://regex101.com/r/jK8cC2/1\n      re = RegExpUtils.subcategorySplitRegex()\n      itemKey = category.displayName.replace(re, '/')\n\n      parent = null\n      parentComponents = itemKey.split('/')\n      for i in [parentComponents.length..1] by -1\n        parentKey = parentComponents[0...i].join('/')\n        parent = seenItems[parentKey]\n        break if parent\n\n      if parent\n        itemDisplayName = category.displayName.substr(parentKey.length+1)\n        item = SidebarItem.forCategories([category], name: itemDisplayName)\n        parent.children.push(item)\n      else\n        item = SidebarItem.forCategories([category])\n        items.push(item)\n      seenItems[itemKey] = item\n\n\n    title ?= account.categoryLabel()\n    collapsed = isSectionCollapsed(title)\n    if collapsible\n      onCollapseToggled = toggleSectionCollapsed\n\n    return {\n      title: title\n      iconName: account.categoryIcon()\n      items: items\n      collapsed: collapsed\n      onCollapseToggled: onCollapseToggled\n      onItemCreated: (displayName) ->\n        return unless displayName\n        category = new Category\n          displayName: displayName\n          accountId: account.id\n        Actions.queueTask(new SyncbackCategoryTask({category}))\n    }\n\n\nmodule.exports = SidebarSection\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/lib/sidebar-store.coffee",
    "content": "_ = require 'underscore'\nNylasStore = require 'nylas-store'\n{Actions,\n AccountStore,\n ThreadCountsStore,\n WorkspaceStore,\n OutboxStore,\n FocusedPerspectiveStore,\n CategoryStore} = require 'nylas-exports'\n\nSidebarSection = require './sidebar-section'\nSidebarActions = require './sidebar-actions'\nAccountCommands = require './account-commands'\n\nSections = {\n  \"Standard\",\n  \"User\"\n}\n\nclass SidebarStore extends NylasStore\n\n  constructor: ->\n    NylasEnv.savedState.sidebarKeysCollapsed ?= {}\n\n    @_sections = {}\n    @_sections[Sections.Standard] = {}\n    @_sections[Sections.User] = []\n    @_registerCommands()\n    @_registerMenuItems()\n    @_registerListeners()\n    @_updateSections()\n\n  accounts: ->\n    AccountStore.accounts()\n\n  sidebarAccountIds: ->\n    FocusedPerspectiveStore.sidebarAccountIds()\n\n  standardSection: ->\n    @_sections[Sections.Standard]\n\n  userSections: ->\n    @_sections[Sections.User]\n\n  _registerListeners: ->\n    @listenTo Actions.setCollapsedSidebarItem, @_onSetCollapsedByName\n    @listenTo SidebarActions.setKeyCollapsed, @_onSetCollapsedByKey\n    @listenTo AccountStore, @_onAccountsChanged\n    @listenTo FocusedPerspectiveStore, @_onFocusedPerspectiveChanged\n    @listenTo WorkspaceStore, @_updateSections\n    @listenTo OutboxStore, @_updateSections\n    @listenTo ThreadCountsStore, @_updateSections\n    @listenTo CategoryStore, @_updateSections\n\n    @configSubscription = NylasEnv.config.onDidChange(\n      'core.workspace.showUnreadForAllCategories',\n      @_updateSections\n    )\n\n    return\n\n  _onSetCollapsedByKey: (itemKey, collapsed) =>\n    currentValue = NylasEnv.savedState.sidebarKeysCollapsed[itemKey]\n    if currentValue isnt collapsed\n      NylasEnv.savedState.sidebarKeysCollapsed[itemKey] = collapsed\n      @_updateSections()\n\n  _onSetCollapsedByName: (itemName, collapsed) =>\n    item = _.findWhere(@standardSection().items, {name: itemName})\n    if not item\n      for section in @userSections()\n        item = _.findWhere(section.items, {name: itemName})\n        break if item\n    return unless item\n    @_onSetCollapsedByKey(item.id, collapsed)\n\n  _registerCommands: (accounts = AccountStore.accounts()) =>\n    AccountCommands.registerCommands(accounts)\n\n  _registerMenuItems: (accounts = AccountStore.accounts()) =>\n    AccountCommands.registerMenuItems(accounts, FocusedPerspectiveStore.sidebarAccountIds())\n\n  # TODO Refactor this\n  # Listen to changes on the account store only for when the account label\n  # or order changes. When accounts or added or removed, those changes will\n  # come in through the FocusedPerspectiveStore\n  _onAccountsChanged: =>\n    @_updateSections()\n\n  # TODO Refactor this\n  # The FocusedPerspectiveStore tells this store the accounts that should be\n  # displayed in the sidebar (i.e. unified inbox vs single account) and will\n  # trigger whenever an account is added or removed, as well as when a\n  # perspective is focused.\n  # However, when udpating the SidebarSections, we also depend on the actual\n  # accounts in the AccountStore. The problem is that the FocusedPerspectiveStore\n  # triggers before the AccountStore is actually updated, so we need to wait for\n  # the AccountStore to get updated (via `defer`) before updateing our sidebar\n  # sections\n  _onFocusedPerspectiveChanged: =>\n    _.defer =>\n      @_registerCommands()\n      @_registerMenuItems()\n      @_updateSections()\n\n  _updateSections: =>\n    accounts = FocusedPerspectiveStore.sidebarAccountIds()\n      .map((id) => AccountStore.accountForId(id))\n      .filter((a) => !!a)\n\n    return if accounts.length is 0\n    multiAccount = accounts.length > 1\n\n    @_sections[Sections.Standard] = SidebarSection.standardSectionForAccounts(accounts)\n    @_sections[Sections.User] = accounts.map (acc) ->\n      opts = {}\n      if multiAccount\n        opts.title = acc.label\n        opts.collapsible = true\n      SidebarSection.forUserCategories(acc, opts)\n    @trigger()\n\n\nmodule.exports = new SidebarStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/package.json",
    "content": "{\n  \"name\": \"account-sidebar\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Sidebar view that shows your account and important tags\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/spec/sidebar-item-spec.es6",
    "content": "import {Category, Actions} from \"nylas-exports\"\nimport SidebarItem from \"../lib/sidebar-item\"\n\ndescribe(\"sidebar-item\", function sidebarItemSpec() {\n  it(\"preserves nested labels on rename\", () => {\n    spyOn(Actions, \"queueTask\")\n    const categories = [new Category({displayName: 'a.b/c', accountId: window.TEST_ACCOUNT_ID})]\n    NylasEnv.savedState.sidebarKeysCollapsed = {}\n    const item = SidebarItem.forCategories(categories)\n    item.onEdited(item, 'd')\n    const task = Actions.queueTask.calls[0].args[0]\n    expect(task.displayName).toBe(\"a.b/d\")\n  })\n  it(\"preserves labels on rename\", () => {\n    spyOn(Actions, \"queueTask\")\n    const categories = [new Category({displayName: 'a', accountId: window.TEST_ACCOUNT_ID})]\n    NylasEnv.savedState.sidebarKeysCollapsed = {}\n    const item = SidebarItem.forCategories(categories)\n    item.onEdited(item, 'b')\n    const task = Actions.queueTask.calls[0].args[0]\n    expect(task.displayName).toBe(\"b\")\n  })\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/account-sidebar/stylesheets/account-sidebar.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.account-sidebar {\n  flex: 1;\n  height: 100%;\n  background-color: @source-list-bg;\n\n  .item.deleted {\n    opacity: 0.5;\n  }\n\n  .nylas-outline-view:first-child {\n    .heading span {\n      margin-right: 30px;\n    }\n  }\n}\n\n.account-switcher {\n  position: absolute;\n  top: 7px;\n  right: 17px;\n  z-index: 3;\n  img {\n    transform: rotateX(180deg);\n  }\n}\n\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/activity-data-source.es6",
    "content": "import {Rx, Message, DatabaseStore} from 'nylas-exports';\n\nexport default class ActivityDataSource {\n  buildObservable({openTrackingId, linkTrackingId, messageLimit}) {\n    const query = DatabaseStore\n      .findAll(Message)\n      .order(Message.attributes.date.descending())\n      .where(Message.attributes.pluginMetadata.contains(openTrackingId, linkTrackingId))\n      .limit(messageLimit);\n    this.observable = Rx.Observable.fromQuery(query);\n    return this.observable;\n  }\n\n  subscribe(callback) {\n    return this.observable.subscribe(callback);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/activity-list-actions.es6",
    "content": "import Reflux from 'reflux';\n\nconst ActivityListActions = Reflux.createActions([\n  \"resetSeen\",\n]);\n\nfor (const key of Object.keys(ActivityListActions)) {\n  ActivityListActions[key].sync = true;\n}\n\nexport default ActivityListActions;\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/activity-list-button.jsx",
    "content": "import React from 'react';\nimport {Actions, ReactDOM} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\n\nimport ActivityList from './activity-list';\nimport ActivityListStore from './activity-list-store';\n\n\nclass ActivityListButton extends React.Component {\n  static displayName = 'ActivityListButton';\n\n  constructor() {\n    super();\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsub = ActivityListStore.listen(this._onDataChanged);\n  }\n\n  componentWillUnmount() {\n    this._unsub();\n  }\n\n  onClick = () => {\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect();\n    Actions.openPopover(\n      <ActivityList />,\n      {originRect: buttonRect, direction: 'down'}\n    );\n  }\n\n  _onDataChanged = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  _getStateFromStores() {\n    return {\n      unreadCount: ActivityListStore.unreadCount(),\n    }\n  }\n\n  render() {\n    let unreadCountClass = \"unread-count\";\n    let iconClass = \"activity-toolbar-icon\";\n    if (this.state.unreadCount) {\n      unreadCountClass += \" active\";\n      iconClass += \" unread\";\n    }\n    return (\n      <div\n        tabIndex={-1}\n        className=\"toolbar-activity\"\n        title=\"View activity\"\n        onClick={this.onClick}\n      >\n        <div className={unreadCountClass}>\n          {this.state.unreadCount}\n        </div>\n        <RetinaImg\n          name=\"icon-toolbar-activity.png\"\n          className={iconClass}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </div>\n    );\n  }\n}\n\nexport default ActivityListButton;\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/activity-list-empty-state.jsx",
    "content": "import React from 'react';\nimport {RetinaImg} from 'nylas-component-kit';\n\nconst ActivityListEmptyState = function ActivityListEmptyState() {\n  return (\n    <div className=\"empty\">\n      <RetinaImg\n        className=\"logo\"\n        name=\"activity-list-empty.png\"\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n      <div className=\"text\">\n        Enable read receipts <RetinaImg name=\"icon-activity-mailopen.png\" mode={RetinaImg.Mode.ContentDark} /> or\n        link tracking <RetinaImg name=\"icon-activity-linkopen.png\" mode={RetinaImg.Mode.ContentDark} /> to\n        see notifications here.\n      </div>\n    </div>\n  );\n}\n\nexport default ActivityListEmptyState;\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/activity-list-item-container.jsx",
    "content": "import React from 'react';\n\nimport {DisclosureTriangle,\n  Flexbox,\n  RetinaImg} from 'nylas-component-kit';\nimport {DateUtils} from 'nylas-exports';\nimport ActivityListStore from './activity-list-store';\nimport {pluginFor} from './plugin-helpers';\n\n\nclass ActivityListItemContainer extends React.Component {\n\n  static displayName = 'ActivityListItemContainer';\n\n  static propTypes = {\n    group: React.PropTypes.array,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      collapsed: true,\n    };\n  }\n\n  _onClick(threadId) {\n    ActivityListStore.focusThread(threadId);\n  }\n\n  _onCollapseToggled = (event) => {\n    event.stopPropagation();\n    this.setState({collapsed: !this.state.collapsed});\n  }\n\n  _getText() {\n    const text = {\n      recipient: \"Someone\",\n      title: \"(No Subject)\",\n      date: new Date(0),\n    };\n    const lastAction = this.props.group[0];\n    if (this.props.group.length === 1 && lastAction.recipient) {\n      text.recipient = lastAction.recipient.displayName();\n    } else if (this.props.group.length > 1 && lastAction.recipient) {\n      const people = [];\n      for (const action of this.props.group) {\n        if (!people.includes(action.recipient)) {\n          people.push(action.recipient);\n        }\n      }\n      if (people.length === 1) text.recipient = people[0].displayName();\n      else if (people.length === 2) text.recipient = `${people[0].displayName()} and 1 other`;\n      else text.recipient = `${people[0].displayName()} and ${people.length - 1} others`;\n    }\n    if (lastAction.title) text.title = lastAction.title;\n    text.date.setUTCSeconds(lastAction.timestamp);\n    return text;\n  }\n\n  renderActivityContainer() {\n    if (this.props.group.length === 1) return null;\n    const actions = [];\n    for (const action of this.props.group) {\n      const date = new Date(0);\n      date.setUTCSeconds(action.timestamp);\n      actions.push(\n        <div\n          key={`${action.messageId}-${action.timestamp}`}\n          className=\"activity-list-toggle-item\"\n        >\n          <Flexbox direction=\"row\">\n            <div className=\"action-message\">\n              {action.recipient ? action.recipient.displayName() : \"Someone\"}\n            </div>\n            <div className=\"spacer\" />\n            <div className=\"timestamp\">\n              {DateUtils.shortTimeString(date)}\n            </div>\n          </Flexbox>\n        </div>\n      );\n    }\n    return (\n      <div\n        key={`activity-toggle-container`}\n        className={`activity-toggle-container ${this.state.collapsed ? \"hidden\" : \"\"}`}\n      >\n        {actions}\n      </div>\n    );\n  }\n\n  render() {\n    const lastAction = this.props.group[0];\n    let className = \"activity-list-item\";\n    if (!ActivityListStore.hasBeenViewed(lastAction)) className += \" unread\";\n    const text = this._getText();\n    let disclosureTriangle = (<div style={{width: \"7px\"}} />);\n    if (this.props.group.length > 1) {\n      disclosureTriangle = (\n        <DisclosureTriangle\n          visible\n          collapsed={this.state.collapsed}\n          onCollapseToggled={this._onCollapseToggled}\n        />\n      );\n    }\n    return (\n      <div onClick={() => { this._onClick(lastAction.threadId) }}>\n        <Flexbox direction=\"column\" className={className}>\n          <Flexbox\n            direction=\"row\"\n          >\n            <div className=\"activity-icon-container\">\n              <RetinaImg\n                className=\"activity-icon\"\n                name={pluginFor(lastAction.pluginId).iconName}\n                mode={RetinaImg.Mode.ContentPreserve}\n              />\n            </div>\n            {disclosureTriangle}\n            <div className=\"action-message\">\n              {text.recipient} {pluginFor(lastAction.pluginId).predicate}:\n            </div>\n            <div className=\"spacer\" />\n            <div className=\"timestamp\">\n              {DateUtils.shortTimeString(text.date)}\n            </div>\n          </Flexbox>\n          <div className=\"title\">\n            {text.title}\n          </div>\n        </Flexbox>\n        {this.renderActivityContainer()}\n      </div>\n    );\n  }\n\n}\n\nexport default ActivityListItemContainer;\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/activity-list-store.jsx",
    "content": "import NylasStore from 'nylas-store';\nimport {\n  Actions,\n  Thread,\n  DatabaseStore,\n  NativeNotifications,\n  FocusedPerspectiveStore,\n} from 'nylas-exports';\nimport ActivityListActions from './activity-list-actions';\nimport ActivityDataSource from './activity-data-source';\nimport {pluginFor} from './plugin-helpers';\n\n\nclass ActivityListStore extends NylasStore {\n  activate() {\n    this._getActivity();\n    this.listenTo(ActivityListActions.resetSeen, this._onResetSeen);\n    this.listenTo(FocusedPerspectiveStore, this._updateActivity);\n  }\n\n  actions() {\n    return this._actions;\n  }\n\n  unreadCount() {\n    if (this._unreadCount < 1000) {\n      return this._unreadCount;\n    } else if (!this._unreadCount) {\n      return null;\n    }\n    return \"999+\";\n  }\n\n  hasBeenViewed(action) {\n    if (!NylasEnv.savedState.activityListViewed) return false;\n    return action.timestamp < NylasEnv.savedState.activityListViewed;\n  }\n\n  focusThread(threadId) {\n    NylasEnv.displayWindow()\n    Actions.closePopover()\n    DatabaseStore.find(Thread, threadId).then((thread) => {\n      if (!thread) {\n        NylasEnv.reportError(new Error(`ActivityListStore::focusThread: Can't find thread`, {threadId}))\n        NylasEnv.showErrorDialog(`Can't find the selected thread in your mailbox`)\n        return;\n      }\n      Actions.ensureCategoryIsFocused('sent', thread.accountId);\n      Actions.setFocus({collection: 'thread', item: thread});\n    });\n  }\n\n  getRecipient(recipientEmail, recipients) {\n    if (recipientEmail) {\n      for (const recipient of recipients) {\n        if (recipientEmail === recipient.email) {\n          return recipient;\n        }\n      }\n    } else if (recipients.length === 1) {\n      return recipients[0];\n    }\n    return null;\n  }\n\n  _dataSource() {\n    return new ActivityDataSource();\n  }\n\n  _onResetSeen() {\n    NylasEnv.savedState.activityListViewed = Date.now() / 1000;\n    this._unreadCount = 0;\n    this.trigger();\n  }\n\n  _getActivity() {\n    const dataSource = this._dataSource();\n    this._subscription = dataSource.buildObservable({\n      openTrackingId: NylasEnv.packages.pluginIdFor('open-tracking'),\n      linkTrackingId: NylasEnv.packages.pluginIdFor('link-tracking'),\n      messageLimit: 500,\n    }).subscribe((messages) => {\n      this._messages = messages;\n      this._updateActivity();\n    });\n  }\n\n  _updateActivity() {\n    this._actions = this._messages ? this._getActions(this._messages) : [];\n    this.trigger();\n  }\n\n  _getActions(messages) {\n    let actions = [];\n    this._notifications = [];\n    this._unreadCount = 0;\n    const sidebarAccountIds = FocusedPerspectiveStore.sidebarAccountIds();\n    for (const message of messages) {\n      if (sidebarAccountIds.length > 1 || message.accountId === sidebarAccountIds[0]) {\n        const openTrackingId = NylasEnv.packages.pluginIdFor('open-tracking')\n        const linkTrackingId = NylasEnv.packages.pluginIdFor('link-tracking')\n        if (message.metadataForPluginId(openTrackingId) ||\n          message.metadataForPluginId(linkTrackingId)) {\n          actions = actions.concat(this._openActionsForMessage(message));\n          actions = actions.concat(this._linkActionsForMessage(message));\n        }\n      }\n    }\n    if (!this._lastNotified) this._lastNotified = {};\n    for (const notification of this._notifications) {\n      const lastNotified = this._lastNotified[notification.threadId];\n      const {notificationInterval} = pluginFor(notification.pluginId);\n      if (!lastNotified || lastNotified < Date.now() - notificationInterval) {\n        NativeNotifications.displayNotification(notification.data);\n        this._lastNotified[notification.threadId] = Date.now();\n      }\n    }\n    const d = new Date();\n    this._lastChecked = d.getTime() / 1000;\n\n    actions = actions.sort((a, b) => b.timestamp - a.timestamp);\n    // For performance reasons, only display the last 100 actions\n    if (actions.length > 100) {\n      actions.length = 100;\n    }\n    return actions;\n  }\n\n  _openActionsForMessage(message) {\n    const openTrackingId = NylasEnv.packages.pluginIdFor('open-tracking')\n    const openMetadata = message.metadataForPluginId(openTrackingId);\n    const recipients = message.to.concat(message.cc, message.bcc);\n    const actions = [];\n    if (openMetadata) {\n      if (openMetadata.open_count > 0) {\n        for (const open of openMetadata.open_data) {\n          const recipient = this.getRecipient(open.recipient, recipients);\n          if (open.timestamp > this._lastChecked) {\n            this._notifications.push({\n              pluginId: openTrackingId,\n              threadId: message.threadId,\n              data: {\n                title: \"New open\",\n                subtitle: `${recipient ? recipient.displayName() : \"Someone\"} just opened ${message.subject}`,\n                canReply: false,\n                tag: \"message-open\",\n                onActivate: () => {\n                  this.focusThread(message.threadId);\n                },\n              },\n            });\n          }\n          if (!this.hasBeenViewed(open)) this._unreadCount += 1;\n          actions.push({\n            messageId: message.id,\n            threadId: message.threadId,\n            title: message.subject,\n            recipient: recipient,\n            pluginId: openTrackingId,\n            timestamp: open.timestamp,\n          });\n        }\n      }\n    }\n    return actions;\n  }\n\n  _linkActionsForMessage(message) {\n    const linkTrackingId = NylasEnv.packages.pluginIdFor('link-tracking')\n    const linkMetadata = message.metadataForPluginId(linkTrackingId)\n    const recipients = message.to.concat(message.cc, message.bcc);\n    const actions = [];\n    if (linkMetadata && linkMetadata.links) {\n      for (const link of linkMetadata.links) {\n        for (const click of link.click_data) {\n          const recipient = this.getRecipient(click.recipient, recipients);\n          if (click.timestamp > this._lastChecked) {\n            this._notifications.push({\n              pluginId: linkTrackingId,\n              threadId: message.threadId,\n              data: {\n                title: \"New click\",\n                subtitle: `${recipient ? recipient.displayName() : \"Someone\"} just clicked ${link.url}.`,\n                canReply: false,\n                tag: \"link-open\",\n                onActivate: () => {\n                  this.focusThread(message.threadId);\n                },\n              },\n            });\n          }\n          if (!this.hasBeenViewed(click)) this._unreadCount += 1;\n          actions.push({\n            messageId: message.id,\n            threadId: message.threadId,\n            title: link.url,\n            recipient: recipient,\n            pluginId: linkTrackingId,\n            timestamp: click.timestamp,\n          });\n        }\n      }\n    }\n    return actions;\n  }\n}\n\nexport default new ActivityListStore();\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/activity-list.jsx",
    "content": "import React from 'react';\nimport classnames from 'classnames';\n\nimport {Flexbox,\n  ScrollRegion} from 'nylas-component-kit';\nimport ActivityListStore from './activity-list-store';\nimport ActivityListActions from './activity-list-actions';\nimport ActivityListItemContainer from './activity-list-item-container';\nimport ActivityListEmptyState from './activity-list-empty-state';\n\nclass ActivityList extends React.Component {\n\n  static displayName = 'ActivityList';\n\n  constructor() {\n    super();\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsub = ActivityListStore.listen(this._onDataChanged);\n  }\n\n  componentWillUnmount() {\n    ActivityListActions.resetSeen();\n    this._unsub();\n  }\n\n  _onDataChanged = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  _getStateFromStores() {\n    const actions = ActivityListStore.actions();\n    return {\n      actions: actions,\n      empty: actions instanceof Array && actions.length === 0,\n      collapsedToggles: this.state ? this.state.collapsedToggles : {},\n    }\n  }\n\n  _groupActions(actions) {\n    const groupedActions = [];\n    for (const action of actions) {\n      if (groupedActions.length > 0) {\n        const currentGroup = groupedActions[groupedActions.length - 1];\n        if (action.messageId === currentGroup[0].messageId &&\n          action.pluginId === currentGroup[0].pluginId) {\n          groupedActions[groupedActions.length - 1].push(action);\n        } else {\n          groupedActions.push([action]);\n        }\n      } else {\n        groupedActions.push([action]);\n      }\n    }\n    return groupedActions;\n  }\n\n  renderActions() {\n    if (this.state.empty) {\n      return (\n        <ActivityListEmptyState />\n      )\n    }\n\n    const groupedActions = this._groupActions(this.state.actions);\n    return groupedActions.map((group) => {\n      return (\n        <ActivityListItemContainer\n          key={`${group[0].messageId}-${group[0].timestamp}`}\n          group={group}\n        />\n      );\n    });\n  }\n\n  render() {\n    if (!this.state.actions) return null;\n\n    const classes = classnames({\n      \"activity-list-container\": true,\n      \"empty\": this.state.empty,\n    })\n    return (\n      <Flexbox\n        direction=\"column\"\n        height=\"none\"\n        className={classes}\n        tabIndex=\"-1\"\n      >\n        <ScrollRegion style={{height: \"100%\"}}>\n          {this.renderActions()}\n        </ScrollRegion>\n      </Flexbox>\n    );\n  }\n}\n\nexport default ActivityList;\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/main.es6",
    "content": "import {ComponentRegistry, WorkspaceStore} from 'nylas-exports';\nimport {HasTutorialTip} from 'nylas-component-kit';\nimport ActivityListButton from './activity-list-button';\nimport ActivityListStore from './activity-list-store';\n\nconst ActivityListButtonWithTutorialTip = HasTutorialTip(ActivityListButton, {\n  title: \"Open and link tracking\",\n  instructions: \"If you've enabled link tracking or read receipts, those events will appear here!\",\n});\n\nexport function activate() {\n  ComponentRegistry.register(ActivityListButtonWithTutorialTip, {\n    location: WorkspaceStore.Location.RootSidebar.Toolbar,\n  });\n  ActivityListStore.activate();\n}\n\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ActivityListButtonWithTutorialTip);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/plugin-helpers.es6",
    "content": "\nexport function pluginFor(id) {\n  const openTrackingId = NylasEnv.packages.pluginIdFor('open-tracking')\n  const linkTrackingId = NylasEnv.packages.pluginIdFor('link-tracking')\n  if (id === openTrackingId) {\n    return {\n      name: \"open\",\n      predicate: \"opened\",\n      iconName: \"icon-activity-mailopen.png\",\n      notificationInterval: 600000, // 10 minutes in ms\n    }\n  }\n  if (id === linkTrackingId) {\n    return {\n      name: \"link\",\n      predicate: \"clicked\",\n      iconName: \"icon-activity-linkopen.png\",\n      notificationInterval: 10000, // 10 seconds in ms\n    }\n  }\n  return undefined\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/lib/test-data-source.es6",
    "content": "export default class TestDataSource {\n  buildObservable() {\n    return this;\n  }\n\n  manuallyTrigger = (messages = []) => {\n    this.onNext(messages);\n  }\n\n  subscribe(onNext) {\n    this.onNext = onNext;\n    this.manuallyTrigger();\n    const dispose = () => {\n      this._unsub();\n    }\n    return {dispose};\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/package.json",
    "content": "{\n  \"name\": \"activity-list\",\n  \"main\": \"./lib/main\",\n  \"version\": \"0.1.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"\"\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n\n  \"isOptional\": true,\n\n  \"title\":\"Activity List\",\n  \"icon\":\"./assets/icon.png\",\n  \"description\": \"Get notifications for open and link tracking activity.\",\n  \"supportedEnvs\": [\"development\", \"staging\", \"production\"],\n\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/spec/activity-list-spec.jsx",
    "content": "import React from 'react';\nimport ReactTestUtils from 'react-addons-test-utils';\nimport {\n  Thread,\n  Actions,\n  Contact,\n  Message,\n  DatabaseStore,\n  FocusedPerspectiveStore,\n} from 'nylas-exports';\nimport ActivityList from '../lib/activity-list';\nimport ActivityListStore from '../lib/activity-list-store';\nimport TestDataSource from '../lib/test-data-source';\n\nconst OPEN_TRACKING_ID = 'open-tracking-id'\nconst LINK_TRACKING_ID = 'link-tracking-id'\n\nconst messages = [\n  new Message({\n    accountId: \"0000000000000000000000000\",\n    bcc: [],\n    cc: [],\n    snippet: \"Testing.\",\n    subject: \"Open me!\",\n    threadId: \"0000000000000000000000000\",\n    to: [new Contact({\n      name: \"Jackie Luo\",\n      email: \"jackie@nylas.com\",\n    })],\n  }),\n  new Message({\n    accountId: \"0000000000000000000000000\",\n    bcc: [new Contact({\n      name: \"Ben Gotow\",\n      email: \"ben@nylas.com\",\n    })],\n    cc: [],\n    snippet: \"Hey! I am in town for the week...\",\n    subject: \"Coffee?\",\n    threadId: \"0000000000000000000000000\",\n    to: [new Contact({\n      name: \"Jackie Luo\",\n      email: \"jackie@nylas.com\",\n    })],\n  }),\n  new Message({\n    accountId: \"0000000000000000000000000\",\n    bcc: [],\n    cc: [new Contact({\n      name: \"Evan Morikawa\",\n      email: \"evan@nylas.com\",\n    })],\n    snippet: \"Here's the latest deals!\",\n    subject: \"Newsletter\",\n    threadId: \"0000000000000000000000000\",\n    to: [new Contact({\n      name: \"Juan Tejada\",\n      email: \"juan@nylas.com\",\n    })],\n  }),\n];\n\nlet pluginValue = {\n  open_count: 1,\n  open_data: [{\n    timestamp: 1461361759.351055,\n  }],\n};\nmessages[0].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue);\npluginValue = {\n  links: [{\n    click_count: 1,\n    click_data: [{\n      timestamp: 1461349232.495837,\n    }],\n  }],\n  tracked: true,\n};\nmessages[0].applyPluginMetadata(LINK_TRACKING_ID, pluginValue);\npluginValue = {\n  open_count: 1,\n  open_data: [{\n    timestamp: 1461361763.283720,\n  }],\n};\nmessages[1].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue);\npluginValue = {\n  links: [],\n  tracked: false,\n};\nmessages[1].applyPluginMetadata(LINK_TRACKING_ID, pluginValue);\npluginValue = {\n  open_count: 0,\n  open_data: [],\n};\nmessages[2].applyPluginMetadata(OPEN_TRACKING_ID, pluginValue);\npluginValue = {\n  links: [{\n    click_count: 0,\n    click_data: [],\n  }],\n  tracked: true,\n};\nmessages[2].applyPluginMetadata(LINK_TRACKING_ID, pluginValue);\n\n\ndescribe('ActivityList', function activityList() {\n  beforeEach(() => {\n    this.testSource = new TestDataSource();\n    spyOn(NylasEnv.packages, 'pluginIdFor').andCallFake((pluginName) => {\n      if (pluginName === 'open-tracking') {\n        return OPEN_TRACKING_ID\n      }\n      if (pluginName === 'link-tracking') {\n        return LINK_TRACKING_ID\n      }\n      return null\n    })\n    spyOn(ActivityListStore, \"_dataSource\").andReturn(this.testSource);\n    spyOn(FocusedPerspectiveStore, \"sidebarAccountIds\").andReturn([\"0000000000000000000000000\"]);\n    spyOn(DatabaseStore, \"run\").andCallFake((query) => {\n      if (query._klass === Thread) {\n        const thread = new Thread({\n          id: \"0000000000000000000000000\",\n          accountId: TEST_ACCOUNT_ID,\n        });\n        return Promise.resolve(thread);\n      }\n      return null;\n    });\n    spyOn(ActivityListStore, \"focusThread\").andCallThrough();\n    spyOn(NylasEnv, \"displayWindow\");\n    spyOn(Actions, \"closePopover\");\n    spyOn(Actions, \"setFocus\");\n    spyOn(Actions, \"ensureCategoryIsFocused\");\n    ActivityListStore.activate();\n    this.component = ReactTestUtils.renderIntoDocument(<ActivityList />);\n  });\n\n  describe('when no actions are found', () => {\n    it('should show empty state', () => {\n      const items = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\");\n      expect(items.length).toBe(0);\n    });\n  });\n\n  describe('when actions are found', () => {\n    it('should show activity list items', () => {\n      this.testSource.manuallyTrigger(messages);\n      waitsFor(() => {\n        const items = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\");\n        return items.length > 0;\n      });\n      runs(() => {\n        expect(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\").length).toBe(3);\n      });\n    });\n\n    it('should show the correct items', () => {\n      this.testSource.manuallyTrigger(messages);\n      waitsFor(() => {\n        const items = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\");\n        return items.length > 0;\n      });\n      runs(() => {\n        expect(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\")[0].textContent).toBe(\"Someone opened:Apr 22 2016Coffee?\");\n        expect(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\")[1].textContent).toBe(\"Jackie Luo opened:Apr 22 2016Open me!\");\n        expect(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\")[2].textContent).toBe(\"Jackie Luo clicked:Apr 22 2016(No Subject)\");\n      });\n    });\n\n    xit('should focus the thread', () => {\n      runs(() => {\n        return this.testSource.manuallyTrigger(messages);\n      })\n      waitsFor(() => {\n        const items = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\");\n        return items.length > 0;\n      });\n      runs(() => {\n        const item = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, \"activity-list-item\")[0];\n        ReactTestUtils.Simulate.click(item);\n      });\n      waitsFor(() => {\n        return ActivityListStore.focusThread.calls.length > 0;\n      });\n      runs(() => {\n        expect(NylasEnv.displayWindow.calls.length).toBe(1);\n        expect(Actions.closePopover.calls.length).toBe(1);\n        expect(Actions.setFocus.calls.length).toBe(1);\n        expect(Actions.ensureCategoryIsFocused.calls.length).toBe(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/activity-list/stylesheets/activity-list.less",
    "content": "@import \"ui-variables\";\n\n.toolbar-activity {\n  order: 100;\n  position: relative;\n\n  .unread-count {\n    display: none;\n    &.active {\n      display: inline-block;\n      background: @component-active-color;\n      text-align: center;\n      color: @white;\n      border-radius: @border-radius-base;\n      font-size: 8px;\n      padding: 0 4px;\n      position: absolute;\n      right: -7px;\n      top: 5px;\n      line-height: 11px;\n    }\n  }\n  .activity-toolbar-icon {\n    margin-top: 20px;\n    background: @gray;\n    &.unread {\n      background: @component-active-color;\n    }\n  }\n}\n\n.activity-list-container {\n  width: 260px;\n  overflow: hidden;\n  font-size: @font-size-small;\n  color: @text-color-subtle;\n  .spacer {\n    flex: 1 1 0;\n  }\n\n  height: 282px;\n  &.empty {\n    height: 182px;\n  }\n\n  .empty {\n      text-align: center;\n      padding: @padding-base-horizontal * 2;\n      padding-top: @padding-base-vertical * 8;\n      img.logo {\n        background-color: @text-color-very-subtle;\n      }\n      .text {\n        margin-top: @padding-base-vertical * 6;\n        color: @text-color-very-subtle;\n      }\n  }\n\n  .activity-list-item {\n    padding: @padding-small-vertical @padding-small-horizontal;\n    white-space: nowrap;\n    border-bottom: 1px solid @border-color-primary;\n    cursor: default;\n    &.unread {\n      color: @text-color;\n      background: @background-primary;\n      &:hover {\n        background: darken(@background-primary, 2%);\n      }\n      .action-message {\n        font-weight: 600;\n      }\n    }\n\n    &:hover {\n      background: darken(@background-secondary, 2%);\n    }\n\n    .disclosure-triangle {\n      padding-top: 5px;\n      padding-bottom: 0;\n    }\n    .activity-icon-container {\n      flex-shrink: 0;\n    }\n    .activity-icon {\n      vertical-align: text-bottom;\n    }\n    .action-message, .title {\n      text-overflow: ellipsis;\n      overflow: hidden;\n    }\n    .timestamp {\n      color: @text-color-very-subtle;\n      text-overflow: ellipsis;\n      overflow: hidden;\n      flex-shrink: 0;\n      padding-left: 5px;\n    }\n  }\n  .activity-list-toggle-item {\n    height: 30px;\n    white-space: nowrap;\n    background: @background-secondary;\n    cursor: default;\n    overflow-y: hidden;\n    transition-property: all;\n    transition-duration: .5s;\n    transition-timing-function: cubic-bezier(0, 1, 0.5, 1);\n    &:last-child {\n      border-bottom: 1px solid @border-color-primary;\n    }\n    .action-message {\n      padding: @padding-small-vertical @padding-small-horizontal;\n      text-overflow: ellipsis;\n      overflow: hidden;\n    }\n    .timestamp {\n      padding: @padding-small-vertical @padding-small-horizontal;\n      color: @text-color-very-subtle;\n      text-overflow: ellipsis;\n      overflow: hidden;\n    }\n  }\n  .activity-toggle-container {\n    &.hidden {\n      .activity-list-toggle-item {\n        height: 0;\n        &:last-child {\n          border-bottom: none;\n        }\n      }\n    }\n  }\n}\n\nbody.platform-win32,\nbody.platform-linux {\n  .toolbar-activity {\n    margin-right: @padding-base-horizontal;\n  }\n}"
  },
  {
    "path": "packages/client-app/internal_packages/attachments/lib/main.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports';\nimport MessageAttachments from './message-attachments'\n\nexport function activate() {\n  ComponentRegistry.register(MessageAttachments, {role: 'MessageAttachments'})\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(MessageAttachments);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/attachments/lib/message-attachments.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {Actions, Utils, FileDownloadStore} from 'nylas-exports'\nimport {AttachmentItem, ImageAttachmentItem} from 'nylas-component-kit'\n\n\nclass MessageAttachments extends Component {\n  static displayName = 'MessageAttachments'\n\n  static containerRequired = false\n\n  static propTypes = {\n    files: PropTypes.array,\n    downloads: PropTypes.object,\n    messageClientId: PropTypes.string,\n    filePreviewPaths: PropTypes.object,\n    canRemoveAttachments: PropTypes.bool,\n  }\n\n  static defaultProps = {\n    downloads: {},\n    filePreviewPaths: {},\n  }\n\n  onOpenAttachment = (file) => {\n    Actions.fetchAndOpenFile(file)\n  }\n\n  onRemoveAttachment = (file) => {\n    const {messageClientId} = this.props\n    Actions.removeFile({\n      file: file,\n      messageClientId: messageClientId,\n    })\n  }\n\n  onDownloadAttachment = (file) => {\n    Actions.fetchAndSaveFile(file)\n  }\n\n  onAbortDownload = (file) => {\n    Actions.abortFetchFile(file)\n  }\n\n  renderAttachment(AttachmentRenderer, file) {\n    const {canRemoveAttachments, downloads, filePreviewPaths} = this.props\n    const download = downloads[file.id]\n    const filePath = FileDownloadStore.pathForFile(file)\n    const fileIconName = `file-${file.displayExtension()}.png`\n    const displayName = file.displayName()\n    const displaySize = file.displayFileSize()\n    const contentType = file.contentType\n    const displayFilePreview = NylasEnv.config.get('core.attachments.displayFilePreview')\n    const filePreviewPath = displayFilePreview ? filePreviewPaths[file.id] : null;\n\n    return (\n      <AttachmentRenderer\n        key={file.id}\n        focusable\n        previewable\n        filePath={filePath}\n        download={download}\n        contentType={contentType}\n        displayName={displayName}\n        displaySize={displaySize}\n        fileIconName={fileIconName}\n        filePreviewPath={filePreviewPath}\n        onOpenAttachment={() => this.onOpenAttachment(file)}\n        onDownloadAttachment={() => this.onDownloadAttachment(file)}\n        onAbortDownload={() => this.onAbortDownload(file)}\n        onRemoveAttachment={canRemoveAttachments ? () => this.onRemoveAttachment(file) : null}\n      />\n    )\n  }\n\n  render() {\n    const {files} = this.props;\n    const nonImageFiles = files.filter((f) => !Utils.shouldDisplayAsImage(f));\n    const imageFiles = files.filter((f) => Utils.shouldDisplayAsImage(f));\n    return (\n      <div>\n        {nonImageFiles.map((file) =>\n          this.renderAttachment(AttachmentItem, file)\n        )}\n        {imageFiles.map((file) =>\n          this.renderAttachment(ImageAttachmentItem, file)\n        )}\n      </div>\n    )\n  }\n}\n\nexport default MessageAttachments\n"
  },
  {
    "path": "packages/client-app/internal_packages/attachments/package.json",
    "content": "{\n  \"name\": \"attachments\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"View attachments on messages\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"composer-preload\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-mapper/lib/category-selection.jsx",
    "content": "import {\n  Menu,\n  RetinaImg,\n  DropdownMenu,\n  LabelColorizer,\n  BoldedSearchResult,\n} from 'nylas-component-kit'\nimport {\n  Utils,\n  React,\n} from 'nylas-exports'\nimport {Categories} from 'nylas-observables'\n\nexport default class CategorySelection extends React.Component {\n  static propTypes = {\n    account: React.PropTypes.object,\n    currentCategory: React.PropTypes.string,\n  }\n  constructor(props) {\n    super(props)\n    this._categories = []\n    this.state = {\n      categoryData: this._recalculateCategories({searchValue: ''}),\n      searchValue: \"\",\n    }\n  }\n\n  componentDidMount() {\n    this._disposable = Categories.forAccount(this.props.account).sort().subscribe(this._onCategoriesChanged)\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !(Utils.isEqualReact(nextState, this.state) && Utils.isEqualReact(nextProps, this.props))\n  }\n\n  componentWillUnmount() {\n    this._disposable.dispose()\n  }\n\n  _isInSearch = (searchValue, category) => {\n    return Utils.wordSearchRegExp(searchValue).test(category.displayName)\n  };\n\n  _isUserFacing = (category) => {\n    const hiddenCategories = ['N1-Snoozed', 'EI']\n    return !hiddenCategories.includes(category.displayName)\n  };\n\n  _itemForCategory = (category) => {\n    if (!category.divider) {\n      category.backgroundColor = LabelColorizer.backgroundColorDark(category)\n    }\n    return category\n  };\n\n  _recalculateCategories = ({searchValue = this.state.searchValue} = {}) => {\n    let categories = this._categories\n\n    if (!this.props.account.usesLabels()) {\n      const standardCategories = categories.filter((cat) => cat.isStandardCategory())\n      const userCategories = categories.filter((cat) => cat.isUserCategory())\n      categories = standardCategories\n        .concat([{divider: true, id: \"category-divider\"}])\n        .concat(userCategories)\n    }\n\n    const categoryData = categories\n      .filter(this._isUserFacing)\n      .filter(c => this._isInSearch(searchValue, c))\n      .map(this._itemForCategory)\n\n    return categoryData\n  };\n\n  _onCategoriesChanged = (categories) => {\n    this._categories = categories\n    this.setState({categoryData: this._recalculateCategories()})\n  };\n\n  _onSearchValueChange = (event) => {\n    const searchValue = event.target.value;\n    this.setState({\n      searchValue,\n      categoryData: this._recalculateCategories({searchValue}),\n    })\n  };\n\n  _renderFolderIcon = (item) => {\n    return (\n      <RetinaImg\n        name={`${item.name}.png`}\n        fallback={'folder.png'}\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n    )\n  };\n\n  _renderLabelIcon = (item) => {\n    return (\n      <RetinaImg\n        name={`${item.name}.png`}\n        fallback={'tag.png'}\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n    )\n  }\n\n  _renderItem = (item = {empty: true}) => {\n    if (item.divider) {\n      return <Menu.Item key={item.id} divider={item.divider} />\n    }\n\n    let icon;\n    if (item.empty) {\n      icon = (<div className=\"empty-icon\" />)\n      item.displayName = \"(None)\"\n    } else {\n      icon = this.props.account.usesLabels() ? this._renderLabelIcon(item) : this._renderFolderIcon(item);\n    }\n\n    return (\n      <div className=\"category-item\">\n        {icon}\n        <div className=\"category-display-name\">\n          <BoldedSearchResult value={item.displayName} query={this.state.searchValue || \"\"} />\n        </div>\n      </div>\n    )\n  };\n\n  render() {\n    const placeholder = this.props.account.usesLabels() ? 'Choose folder or label' : 'Choose folder'\n\n    const headerComponents = [\n      <input\n        type=\"text\"\n        tabIndex=\"-1\"\n        key=\"textfield\"\n        className=\"search\"\n        placeholder={placeholder}\n        value={this.state.searchValue}\n        onChange={this._onSearchValueChange}\n      />,\n    ]\n\n    return (\n      <div className=\"category-selection\">\n        <DropdownMenu\n          intitialSelectionItem={this.props.currentCategory}\n          headerComponents={headerComponents}\n          footerComponents={[]}\n          items={this.state.categoryData}\n          itemKey={item => item.id}\n          itemContent={this._renderItem}\n          defaultSelectedIndex={this.state.searchValue === \"\" ? -1 : 0}\n          {...this.props}\n        />\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-mapper/lib/main.es6",
    "content": "import {PreferencesUIStore} from 'nylas-exports';\nimport PreferencesCategoryMapper from './preferences-category-mapper'\n\nexport function activate() {\n  this.preferencesTab = new PreferencesUIStore.TabItem({\n    tabId: \"Folders\",\n    displayName: \"Folders\",\n    component: PreferencesCategoryMapper,\n  });\n\n  PreferencesUIStore.registerPreferencesTab(this.preferencesTab);\n}\n\nexport function deactivate() {\n  PreferencesUIStore.unregisterPreferencesTab(this.preferencesTab.sectionId)\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-mapper/lib/preferences-category-mapper.jsx",
    "content": "import {\n  AccountStore,\n  Category,\n  DatabaseStore,\n  NylasAPI,\n  NylasAPIRequest,\n  React,\n} from 'nylas-exports'\nimport CategorySelection from './category-selection'\n\n\nconst ROLES = ['inbox', 'sent', 'drafts', 'spam', 'trash']\n\nexport default class PreferencesCategoryMapper extends React.Component {\n  constructor() {\n    super()\n    this.state = {ready: false}\n    this._mounted = false\n    this._populateRoleAssignments()\n  }\n\n  componentDidMount() {\n    this._mounted = true\n    this._populateRoleAssignments()\n  }\n\n  componentWillUnmount() {\n    this._mounted = false\n  }\n\n  async _populateRoleAssignments() {\n    const roleAssignment = {}\n    await Promise.all(ROLES.map(async (role) => {\n      const existingAssignments = await DatabaseStore.findAll(Category).where([\n        Category.attributes.name.equal(role),\n      ])\n      for (const category of existingAssignments) {\n        const {accountId} = category;\n        if (!roleAssignment[accountId]) {\n          roleAssignment[accountId] = {}\n        }\n        roleAssignment[accountId][role] = category\n      }\n    }))\n\n    if (this._mounted) {\n      this.setState({ready: true, roleAssignment})\n    }\n  }\n\n  _onCategorySelection = async (account, role, category) => {\n    const {roleAssignment} = this.state;\n\n    const originalRole = category.name\n    category.name = role\n    await DatabaseStore.inTransaction(t => t.persistModel(category))\n\n    const originalCategory = roleAssignment[account.id][role]\n    roleAssignment[account.id][role] = category\n    this.setState({roleAssignment})\n\n    try {\n      const request = new NylasAPIRequest({\n        api: NylasAPI,\n        options: {\n          path: `/${category.displayType()}s/${category.id}`,\n          accountId: category.accountId,\n          method: \"PUT\",\n          body: {role},\n        },\n      })\n      await request.run()\n    } catch (err) {\n      err.message = `Could not set ${category.displayName} as ${role} ${category.displayType()}: ${err.message}`\n      NylasEnv.reportError(err)\n      NylasEnv.showErrorDialog(err.message, {detail: err.stack})\n\n      // Revert optimistic changes\n      category.name = originalRole\n      await DatabaseStore.inTransaction(t => t.persistModel(category))\n      roleAssignment[account.id][role] = originalCategory\n      this.setState({roleAssignment})\n    }\n  }\n\n  _renderAccountSection = (account) => {\n    const roleSections = ROLES.map(role => this._renderRoleSection(account, role))\n    return (\n      <div>\n        <div className=\"account-section-title\">{account.label}</div>\n        {roleSections}\n      </div>\n    )\n  }\n\n  _renderRoleSection = (account, role) => {\n    return (\n      <div className=\"role-section\">\n        <div className=\"col-left\">{role}:</div>\n        <div className=\"col-right\">\n          <CategorySelection\n            account={account}\n            currentCategory={this.state.roleAssignment[account.id][role]}\n            onSelect={category => this._onCategorySelection(account, role, category)}\n          />\n        </div>\n      </div>\n    )\n  }\n\n  render() {\n    if (!this.state.ready) {\n      return <span />\n    }\n\n    const accountSections = AccountStore.accounts().map(this._renderAccountSection)\n\n    return (\n      <div className=\"category-mapper-container\">\n        {accountSections}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-mapper/package.json",
    "content": "{\n  \"name\": \"category-mapper\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Allow users to manually map roles to categories\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-mapper/stylesheets/category-mapper.less",
    "content": "@import \"ui-variables\";\n\n.category-mapper-container {\n  width: 40%;\n  min-width: 460px;\n  margin: 0 auto;\n}\n\n.account-section-title {\n  border-bottom: 1px solid @border-color-divider;\n  margin: @padding-large-vertical * 1.5 0;\n}\n\ninput.search {\n  border: 1px solid darken(@background-secondary, 10%);\n  border-radius: 3px;\n  background-color: @background-primary;\n  box-shadow: inset 0 1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(0,0,0,0.05);\n  color: @text-color;\n  padding-left: 0;\n  background-repeat: no-repeat;\n  background-image: url(\"../static/images/search/searchloupe@2x.png\");\n  background-size: 15px 15px;\n  background-position: 7px 4px;\n  text-indent: 31px;\n}\n\n.role-section {\n  display: flex;\n\n  .col-left {\n    color: @text-color-very-subtle;\n    text-align: right;\n    flex: 1;\n    margin-right: 20px;\n  }\n  .col-right {\n    text-align: left;\n    flex: 1;\n  }\n}\n\n.category-selection {\n  .menu {\n    background: @background-secondary;\n    width: 250px;\n    max-height: 400px;\n    box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 4px 7px rgba(0,0,0,0.15);\n\n    .header-container {\n      border-bottom: 0;\n    }\n\n    .item.divider {\n      background-color: #e0e0e0;\n      margin: 4px 0;\n      height: 2px;\n      padding: 0;\n    }\n\n  }\n\n  .category-item {\n    font-size: 14px;\n    display: flex;\n\n    img.content-mask {\n      position: relative;\n      top:3px;\n      background-color: @text-color-subtle;\n    }\n  }\n\n  .category-display-name {\n    display: inline-block;\n    margin-left: 10px;\n    margin-right: 5px;\n    word-break: break-word;\n    flex: 1;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-picker/lib/category-picker-popover.jsx",
    "content": "/* eslint jsx-a11y/tabindex-no-positive: 0 */\nimport _ from 'underscore'\nimport React, {Component, PropTypes} from 'react'\nimport {\n  Menu,\n  RetinaImg,\n  LabelColorizer,\n  BoldedSearchResult,\n} from 'nylas-component-kit'\nimport {\n  Utils,\n  Actions,\n  TaskQueueStatusStore,\n  DatabaseStore,\n  TaskFactory,\n  Category,\n  SyncbackCategoryTask,\n  CategoryStore,\n  FocusedPerspectiveStore,\n} from 'nylas-exports'\nimport {Categories} from 'nylas-observables'\n\n\nexport default class CategoryPickerPopover extends Component {\n\n  static propTypes = {\n    threads: PropTypes.array.isRequired,\n    account: PropTypes.object.isRequired,\n  };\n\n  constructor(props) {\n    super(props)\n    this._categories = []\n    this._standardCategories = []\n    this._userCategories = []\n    this.state = this._recalculateState(this.props, {searchValue: ''})\n  }\n\n  componentDidMount() {\n    this._registerObservables()\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this._registerObservables(nextProps)\n    this.setState(this._recalculateState(nextProps))\n  }\n\n  componentWillUnmount() {\n    this._unregisterObservables()\n  }\n\n  _registerObservables = (props = this.props) => {\n    this._unregisterObservables()\n    this.disposables = [\n      Categories.forAccount(props.account).sort().subscribe(this._onCategoriesChanged),\n    ]\n  };\n\n  _unregisterObservables = () => {\n    if (this.disposables) {\n      this.disposables.forEach(disp => disp.dispose())\n    }\n  };\n\n  _isInSearch = (searchValue, category) => {\n    return Utils.wordSearchRegExp(searchValue).test(category.displayName)\n  };\n\n  _isUserFacing = (allInInbox, category) => {\n    const currentCategories = FocusedPerspectiveStore.current().categories() || []\n    const currentCategoryIds = _.pluck(currentCategories, 'id')\n    const {account} = this.props\n    let hiddenCategories = []\n\n    if (account) {\n      if (account.usesLabels()) {\n        hiddenCategories = Category.StandardCategoryNames.concat([\"starred\", \"N1-Snoozed\"])\n        if (allInInbox) {\n          hiddenCategories.push(\"inbox\")\n        }\n        if (category.divider) {\n          return false\n        }\n      } else if (account.usesFolders()) {\n        hiddenCategories = [\"drafts\", \"sent\", \"N1-Snoozed\"]\n      }\n    }\n    return (\n      (!hiddenCategories.includes(category.name)) &&\n      (!hiddenCategories.includes(category.displayName)) &&\n      (!currentCategoryIds.includes(category.id))\n    )\n  };\n\n  _itemForCategory = ({usageCount, numThreads}, category) => {\n    if (category.divider) {\n      return category\n    }\n    const item = category.toJSON()\n    item.category = category\n    item.backgroundColor = LabelColorizer.backgroundColorDark(category)\n    item.usage = usageCount[category.id] || 0\n    item.numThreads = numThreads\n    return item\n  };\n\n  _allInInbox = (usageCount, numThreads) => {\n    const {account} = this.props\n    const inbox = CategoryStore.getStandardCategory(account, \"inbox\")\n    if (!inbox) return false\n    return usageCount[inbox.id] === numThreads\n  };\n\n  _categoryUsageCount = (props) => {\n    const {threads} = props\n    const categoryUsageCount = {}\n    _.flatten(_.pluck(threads, 'categories')).forEach((category) => {\n      categoryUsageCount[category.id] = categoryUsageCount[category.id] || 0\n      categoryUsageCount[category.id] += 1\n    })\n    return categoryUsageCount;\n  };\n\n  _recalculateState = (props = this.props, {searchValue = (this.state.searchValue || \"\")} = {}) => {\n    const {account, threads} = props\n\n    const numThreads = threads.length\n    let categories;\n\n    if (numThreads === 0) {\n      return {categoryData: [], searchValue}\n    }\n\n    if (account.usesLabels()) {\n      categories = this._categories\n    } else {\n      categories = this._standardCategories\n        .concat([{divider: true, id: \"category-divider\"}])\n        .concat(this._userCategories)\n    }\n\n    const usageCount = this._categoryUsageCount(props, categories)\n    const allInInbox = this._allInInbox(usageCount, numThreads)\n    const displayData = {usageCount, numThreads}\n\n    const categoryData = _.chain(categories)\n      .filter(_.partial(this._isUserFacing, allInInbox))\n      .filter(_.partial(this._isInSearch, searchValue))\n      .map(_.partial(this._itemForCategory, displayData))\n      .value()\n\n    if (searchValue.length > 0) {\n      const newItemData = {\n        searchValue: searchValue,\n        newCategoryItem: true,\n        id: \"category-create-new\",\n      }\n      categoryData.push(newItemData)\n    }\n    return {categoryData, searchValue}\n  };\n\n  _onCategoriesChanged = (categories) => {\n    this._categories = categories\n    this._standardCategories = categories.filter((cat) => cat.isStandardCategory())\n    this._userCategories = categories.filter((cat) => cat.isUserCategory())\n    this.setState(this._recalculateState())\n  };\n\n  _onEscape = () => {\n    Actions.closePopover()\n  };\n\n  _onSelectCategory = (item) => {\n    const {account, threads} = this.props\n\n    if (threads.length === 0) return;\n\n    if (item.newCategoryItem) {\n      const category = new Category({\n        displayName: this.state.searchValue,\n        accountId: account.id,\n      })\n      const syncbackTask = new SyncbackCategoryTask({category})\n\n      TaskQueueStatusStore.waitForPerformRemote(syncbackTask).then(() => {\n        DatabaseStore.findBy(category.constructor, {clientId: category.clientId})\n        .then((cat) => {\n          if (!cat) {\n            const categoryType = account.usesLabels() ? \"label\" : \"folder\";\n            NylasEnv.showErrorDialog({title: \"Error\", message: `Could not create ${categoryType}.`})\n            return;\n          }\n          Actions.applyCategoryToThreads({\n            source: \"Category Picker: New Category\",\n            threads: threads,\n            categoryToApply: cat,\n          })\n        })\n      })\n      Actions.queueTask(syncbackTask)\n    } else if (item.usage === threads.length) {\n      Actions.removeCategoryFromThreads({\n        source: \"Category Picker: Existing Category\",\n        threads: threads,\n        categoryToRemove: item.category,\n      })\n    } else {\n      Actions.applyCategoryToThreads({\n        source: \"Category Picker: Existing Category\",\n        threads: threads,\n        categoryToApply: item.category,\n      })\n    }\n    if (account.usesFolders()) {\n      // In case we are drilled down into a message\n      Actions.popSheet()\n    }\n    Actions.closePopover()\n  };\n\n  _onSearchValueChange = (event) => {\n    this.setState(\n      this._recalculateState(this.props, {searchValue: event.target.value})\n    )\n  };\n\n  _renderFolderIcon = (item) => {\n    return (\n      <RetinaImg\n        name={`${item.name}.png`}\n        fallback={'folder.png'}\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n    )\n  };\n\n  _renderCheckbox = (item) => {\n    const styles = {}\n    let checkStatus;\n    styles.backgroundColor = item.backgroundColor\n\n    if (item.usage === 0) {\n      checkStatus = <span />\n    } else if (item.usage < item.numThreads) {\n      checkStatus = (\n        <RetinaImg\n          className=\"check-img dash\"\n          name=\"tagging-conflicted.png\"\n          mode={RetinaImg.Mode.ContentPreserve}\n          onClick={() => this._onSelectCategory(item)}\n        />\n      )\n    } else {\n      checkStatus = (\n        <RetinaImg\n          className=\"check-img check\"\n          name=\"tagging-checkmark.png\"\n          mode={RetinaImg.Mode.ContentPreserve}\n          onClick={() => this._onSelectCategory(item)}\n        />\n      )\n    }\n\n    return (\n      <div className=\"check-wrap\" style={styles}>\n        <RetinaImg\n          className=\"check-img check\"\n          name=\"tagging-checkbox.png\"\n          mode={RetinaImg.Mode.ContentPreserve}\n          onClick={() => this._onSelectCategory(item)}\n        />\n        {checkStatus}\n      </div>\n    )\n  };\n\n  _renderCreateNewItem = ({searchValue}) => {\n    const {account} = this.props\n    let picName = ''\n    if (account) {\n      picName = account.usesLabels() ? 'tag' : 'folder'\n    }\n\n    return (\n      <div className=\"category-item category-create-new\">\n        <RetinaImg\n          name={`${picName}.png`}\n          className={`category-create-new-${picName}`}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        <div className=\"category-display-name\">\n          <strong>&ldquo;{searchValue}&rdquo;</strong> (create new)\n        </div>\n      </div>\n    )\n  };\n\n  _renderItem = (item) => {\n    if (item.divider) {\n      return <Menu.Item key={item.id} divider={item.divider} />\n    } else if (item.newCategoryItem) {\n      return this._renderCreateNewItem(item)\n    }\n\n    const {account} = this.props\n    let icon;\n\n    if (account) {\n      icon = account.usesLabels() ? this._renderCheckbox(item) : this._renderFolderIcon(item);\n    } else {\n      return <span />\n    }\n\n    return (\n      <div className=\"category-item\">\n        {icon}\n        <div className=\"category-display-name\">\n          <BoldedSearchResult value={item.display_name} query={this.state.searchValue || \"\"} />\n        </div>\n      </div>\n    )\n  };\n\n  render() {\n    const {account} = this.props\n    let placeholder = ''\n    if (account) {\n      placeholder = account.usesLabels() ? 'Label as' : 'Move to folder'\n    }\n\n    const headerComponents = [\n      <input\n        type=\"text\"\n        tabIndex=\"1\"\n        key=\"textfield\"\n        className=\"search\"\n        placeholder={placeholder}\n        value={this.state.searchValue}\n        onChange={this._onSearchValueChange}\n      />,\n    ]\n\n    return (\n      <div className=\"category-picker-popover\">\n        <Menu\n          ref=\"menu\"\n          headerComponents={headerComponents}\n          footerComponents={[]}\n          items={this.state.categoryData}\n          itemKey={item => item.id}\n          itemContent={this._renderItem}\n          onSelect={this._onSelectCategory}\n          onEscape={this._onEscape}\n          defaultSelectedIndex={this.state.searchValue === \"\" ? -1 : 0}\n        />\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-picker/lib/category-picker.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\n\n{Actions,\n AccountStore,\n WorkspaceStore} = require 'nylas-exports'\n\n{RetinaImg,\n KeyCommandsRegion} = require 'nylas-component-kit'\n\nCategoryPickerPopover = require('./category-picker-popover').default\n\n\n# This changes the category on one or more threads.\nclass CategoryPicker extends React.Component\n  @displayName: \"CategoryPicker\"\n\n  @containerRequired: false\n\n  @propTypes:\n    items: React.PropTypes.array\n\n  @contextTypes:\n    sheetDepth: React.PropTypes.number\n\n  constructor: (@props) ->\n    @_account = AccountStore.accountForItems(@props.items)\n\n  # If the threads we're picking categories for change, (like when they\n  # get their categories updated), we expect our parents to pass us new\n  # props. We don't listen to the DatabaseStore ourselves.\n  componentWillReceiveProps: (nextProps) ->\n    @_account = AccountStore.accountForItems(nextProps.items)\n\n  _keymapHandlers: ->\n    \"core:change-category\": @_onOpenCategoryPopover\n\n  _onOpenCategoryPopover: =>\n    return unless @props.items.length > 0\n    return unless @context.sheetDepth is WorkspaceStore.sheetStack().length - 1\n    buttonRect = ReactDOM.findDOMNode(@refs.button).getBoundingClientRect()\n    Actions.openPopover(\n      <CategoryPickerPopover\n        threads={@props.items}\n        account={@_account} />,\n      {originRect: buttonRect, direction: 'down'}\n    )\n    return\n\n  render: =>\n    return <span /> unless @_account\n    btnClasses = \"btn btn-toolbar btn-category-picker\"\n    img = \"\"\n    tooltip = \"\"\n    if @_account.usesLabels()\n      img = \"toolbar-tag.png\"\n      tooltip = \"Apply Labels\"\n    else\n      img = \"toolbar-movetofolder.png\"\n      tooltip = \"Move to Folder\"\n\n    return (\n      <KeyCommandsRegion\n        style={order: -103}\n        globalHandlers={@_keymapHandlers()}\n        globalMenuItems={[\n          {\n            \"label\": \"Thread\",\n            \"submenu\": [{ \"label\": \"#{tooltip}...\", \"command\": \"core:change-category\", \"position\": \"endof=thread-actions\" }]\n          }\n        ]}\n        >\n        <button\n          tabIndex={-1}\n          ref=\"button\"\n          title={tooltip}\n          onClick={@_onOpenCategoryPopover}\n          className={btnClasses} >\n          <RetinaImg name={img} mode={RetinaImg.Mode.ContentIsMask}/>\n        </button>\n      </KeyCommandsRegion>\n    )\n\n\nmodule.exports = CategoryPicker\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-picker/lib/main.cjsx",
    "content": "CategoryPicker = require \"./category-picker\"\n\n{ComponentRegistry,\n WorkspaceStore} = require 'nylas-exports'\n\nmodule.exports =\n  activate: (@state={}) ->\n    ComponentRegistry.register CategoryPicker,\n      role: 'ThreadActionsToolbarButton'\n\n  deactivate: ->\n    ComponentRegistry.unregister(CategoryPicker)\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-picker/package.json",
    "content": "{\n  \"name\": \"category-picker\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Label & Folder Picker\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-picker/spec/category-picker-spec.cjsx",
    "content": "_ = require 'underscore'\nReact = require \"react\"\nReactDOM = require 'react-dom'\nReactTestUtils = require 'react-addons-test-utils'\nCategoryPickerPopover = require('../lib/category-picker-popover').default\n\n{Utils,\n Category,\n Thread,\n Actions,\n AccountStore,\n CategoryStore,\n DatabaseStore,\n TaskFactory,\n SyncbackCategoryTask,\n FocusedPerspectiveStore,\n MailboxPerspective,\n NylasTestUtils,\n TaskQueueStatusStore} = require 'nylas-exports'\n\n{Categories} = require 'nylas-observables'\n\ndescribe 'CategoryPickerPopover', ->\n  beforeEach ->\n    CategoryStore._categoryCache = {}\n\n  afterEach ->\n    NylasEnv.testOrganizationUnit = null\n\n  setupFor = (organizationUnit) ->\n    NylasEnv.testOrganizationUnit = organizationUnit\n    @account = {\n      id: TEST_ACCOUNT_ID\n      usesLabels: -> organizationUnit is \"label\"\n      usesFolders: -> organizationUnit isnt \"label\"\n    }\n\n    @inboxCategory = new Category(id: 'id-123', name: 'inbox', displayName: \"INBOX\", accountId: TEST_ACCOUNT_ID)\n    @archiveCategory = new Category(id: 'id-456', name: 'archive', displayName: \"ArCHIVe\", accountId: TEST_ACCOUNT_ID)\n    @userCategory = new Category(id: 'id-789', name: null, displayName: \"MyCategory\", accountId: TEST_ACCOUNT_ID)\n\n    observable = NylasTestUtils.mockObservable([@inboxCategory, @archiveCategory, @userCategory])\n    observable.sort = => observable\n\n    spyOn(Categories, \"forAccount\").andReturn observable\n    spyOn(CategoryStore, \"getStandardCategory\").andReturn @inboxCategory\n    spyOn(AccountStore, \"accountForItems\").andReturn @account\n    spyOn(Actions, \"closePopover\")\n\n    # By default we're going to set to \"inbox\". This has implications for\n    # what categories get filtered out of the list.\n    spyOn(FocusedPerspectiveStore, 'current').andCallFake =>\n      MailboxPerspective.forCategory(@inboxCategory)\n\n  setupForCreateNew = (orgUnit = \"folder\") ->\n    setupFor.call(@, orgUnit)\n\n    @testThread = new Thread(id: 't1', subject: \"fake\", accountId: TEST_ACCOUNT_ID, categories: [])\n    @picker = ReactTestUtils.renderIntoDocument(\n      <CategoryPickerPopover threads={[@testThread]} account={@account} />\n    )\n\n  describe 'when using labels', ->\n    beforeEach ->\n      setupFor.call(@, \"label\")\n\n  describe 'when using folders', ->\n    beforeEach ->\n      setupFor.call(@, \"folder\")\n\n      @testThread = new Thread(id: 't1', subject: \"fake\", accountId: TEST_ACCOUNT_ID, categories: [])\n      @picker = ReactTestUtils.renderIntoDocument(\n        <CategoryPickerPopover threads={[@testThread]} account={@account} />\n      )\n\n    it 'lists the desired categories', ->\n      data = @picker.state.categoryData\n      # NOTE: The inbox category is not included here because it's the\n      # currently focused category, which gets filtered out of the list.\n      expect(data.length).toBe 3\n\n      expect(data[0].id).toBe \"id-456\"\n      expect(data[0].name).toBe \"archive\"\n      expect(data[0].category).toBe @archiveCategory\n\n      expect(data[1].divider).toBe true\n      expect(data[1].id).toBe \"category-divider\"\n\n      expect(data[2].id).toBe \"id-789\"\n      expect(data[2].name).toBeUndefined()\n      expect(data[2].category).toBe @userCategory\n\n  describe \"'create new' item\", ->\n    beforeEach ->\n      setupForCreateNew.call @\n\n    afterEach -> NylasEnv.testOrganizationUnit = null\n\n    it \"is not visible when the search box is empty\", ->\n      count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new').length\n      expect(count).toBe 0\n\n    it \"is visible when the search box has text\", ->\n      inputNode = ReactDOM.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithTag(@picker, \"input\")[0])\n      ReactTestUtils.Simulate.change inputNode, target: { value: \"calendar\" }\n      count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new').length\n      expect(count).toBe 1\n\n    it \"shows folder icon if we're using exchange\", ->\n      inputNode = ReactDOM.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithTag(@picker, \"input\")[0])\n      ReactTestUtils.Simulate.change inputNode, target: { value: \"calendar\" }\n      count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new-folder').length\n      expect(count).toBe 1\n\n  describe \"'create new' item with labels\", ->\n    beforeEach ->\n      setupForCreateNew.call @, \"label\"\n\n    it \"shows label icon if we're using gmail\", ->\n      inputNode = ReactDOM.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithTag(@picker, \"input\")[0])\n      ReactTestUtils.Simulate.change inputNode, target: { value: \"calendar\" }\n      count = ReactTestUtils.scryRenderedDOMComponentsWithClass(@picker, 'category-create-new-tag').length\n      expect(count).toBe 1\n\n  describe \"_onSelectCategory\", ->\n    beforeEach ->\n      setupForCreateNew.call @, \"folder\"\n      spyOn(Actions, \"applyCategoryToThreads\")\n      spyOn(Actions, \"removeCategoryFromThreads\")\n      spyOn(Actions, \"queueTask\")\n      spyOn(Actions, \"queueTasks\")\n\n    it \"closes the popover\", ->\n      @picker._onSelectCategory { usage: 0, category: \"asdf\" }\n      expect(Actions.closePopover).toHaveBeenCalled()\n\n    describe \"when selecting a category currently on all the selected items\", ->\n      it \"fires a task to remove the category\", ->\n        input =\n          category: \"asdf\"\n          usage: 1\n\n        @picker._onSelectCategory(input)\n        expect(Actions.removeCategoryFromThreads).toHaveBeenCalledWith\n          threads: [@testThread]\n          source: 'Category Picker: Existing Category'\n          categoryToRemove: \"asdf\"\n\n    describe \"when selecting a category not on all the selected items\", ->\n      it \"fires a task to add the category\", ->\n        input =\n          category: \"asdf\"\n          usage: 0\n\n        @picker._onSelectCategory(input)\n        expect(Actions.applyCategoryToThreads).toHaveBeenCalledWith\n          source: 'Category Picker: Existing Category'\n          threads: [@testThread]\n          categoryToApply: \"asdf\"\n\n    describe \"when selecting a new category\", ->\n      beforeEach ->\n        @input =\n          newCategoryItem: true\n        @picker.setState(searchValue: \"teSTing!\")\n\n      it \"queues a new syncback task for creating a category\", ->\n        @picker._onSelectCategory(@input)\n        expect(Actions.queueTask).toHaveBeenCalled()\n        syncbackTask = Actions.queueTask.calls[0].args[0]\n        newCategory  = syncbackTask.category\n        expect(newCategory instanceof Category).toBe(true)\n        expect(newCategory.displayName).toBe \"teSTing!\"\n        expect(newCategory.accountId).toBe TEST_ACCOUNT_ID\n\n      it \"queues a task for applying the category after it has saved\", ->\n        category = false\n        resolveSave = false\n        spyOn(TaskQueueStatusStore, \"waitForPerformRemote\").andCallFake (task) ->\n          expect(task instanceof SyncbackCategoryTask).toBe true\n          new Promise (resolve, reject) ->\n            resolveSave = resolve\n\n        spyOn(DatabaseStore, \"findBy\").andCallFake (klass, {clientId}) ->\n          expect(klass).toBe(Category)\n          expect(typeof clientId).toBe(\"string\")\n          Promise.resolve(category)\n\n        @picker._onSelectCategory(@input)\n\n        waitsFor ->\n          Actions.queueTask.callCount > 0\n\n        runs ->\n          category = Actions.queueTask.calls[0].args[0].category\n          resolveSave()\n\n        waitsFor ->\n          Actions.applyCategoryToThreads.calls.length is 1\n\n        runs ->\n          expect(Actions.applyCategoryToThreads).toHaveBeenCalledWith\n            source: 'Category Picker: New Category'\n            threads: [@testThread]\n            categoryToApply: category\n"
  },
  {
    "path": "packages/client-app/internal_packages/category-picker/stylesheets/category-picker.less",
    "content": "@import \"ui-variables\";\n\n@popover-width: 250px;\n\nbody.platform-win32 {\n  .category-picker-popover {\n    margin-left: 0;\n  }\n}\n\n.sheet-toolbar .btn-category-picker:only-of-type {\n  margin-right: 0;\n}\n\n.category-picker-popover {\n  .menu {\n    background: @background-secondary;\n    width: @popover-width;\n    max-height: 400px;\n\n    .header-container {\n      border-bottom: 0;\n    }\n\n    .item.divider {\n      background-color: #e0e0e0;\n      margin: 4px 0;\n      height: 2px;\n      padding: 0;\n    }\n\n  }\n\n  .btn.btn-toolbar {\n    margin-left: 0;\n    margin-right: 0;\n  }\n\n  .check-wrap {\n    width: 14px;\n    height: 14px;\n    border-radius: 2px;\n    display: inline-block;\n    position: relative;\n    flex-shrink: 0;\n    top: 2px;\n  }\n\n  .item {\n    img.content-mask {\n      position: relative;\n      top:3px;\n      background-color: @text-color-subtle;\n    }\n  }\n  .item.selected, .item.active {\n    img.content-mask {\n      background-color: @text-color-inverse;\n    }\n  }\n\n  img.check-img {\n    position: absolute;\n  }\n\n  .category-item {\n    font-size: 14px;\n    display: flex;\n  }\n\n  .category-create-new-tag {\n    flex-shrink: 0;\n  }\n\n  .category-display-name {\n    display: inline-block;\n    margin-left: 10px;\n    margin-right: 5px;\n    word-break: break-word;\n    flex: 1;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/README.md",
    "content": "# composer package\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/keymaps/composer.json",
    "content": "{\n  \"composer:focus-to\": \"mod+shift+t\",\n  \"composer:show-and-focus-cc\": \"mod+shift+c\",\n  \"composer:show-and-focus-bcc\": \"mod+shift+b\",\n  \"composer:show-and-focus-from\": \"mod+shift+f\",\n  \"composer:send-message\": \"mod+enter\",\n  \"composer:no-op\": \"del\",\n  \"composer:delete-empty-draft\": \"escape\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/account-contact-field.jsx",
    "content": "import React from 'react';\nimport classnames from 'classnames';\nimport {\n  AccountStore,\n} from 'nylas-exports';\nimport {Menu, ButtonDropdown, InjectedComponentSet} from 'nylas-component-kit';\n\nexport default class AccountContactField extends React.Component {\n  static displayName = 'AccountContactField';\n\n  static propTypes = {\n    value: React.PropTypes.object,\n    accounts: React.PropTypes.array,\n    session: React.PropTypes.object.isRequired,\n    draft: React.PropTypes.object.isRequired,\n    onChange: React.PropTypes.func.isRequired,\n  };\n\n  _onChooseContact = (contact) => {\n    this.props.onChange({from: [contact]});\n    this.props.session.ensureCorrectAccount()\n    this.refs.dropdown.toggleDropdown();\n  }\n\n  _renderAccountSelector() {\n    if (!this.props.value) {\n      return (\n        <span />\n      );\n    }\n\n    const label = this.props.value.toString();\n    const multipleAccounts = this.props.accounts.length > 1;\n    const hasAliases = this.props.accounts[0] && this.props.accounts[0].aliases.length > 0;\n\n    if (multipleAccounts || hasAliases) {\n      return (\n        <ButtonDropdown\n          ref=\"dropdown\"\n          bordered={false}\n          primaryItem={<span>{label}</span>}\n          menu={this._renderAccounts(this.props.accounts)}\n        />\n      );\n    }\n    return this._renderAccountSpan(label);\n  }\n\n  _renderAccountSpan = (label) => {\n    return (\n      <span className=\"from-single-name\" style={{position: \"relative\", top: 13, left: \"0.5em\"}}>\n        {label}\n      </span>\n    );\n  }\n\n  _renderMenuItem = (contact) => {\n    const className = classnames({\n      'contact': true,\n      'is-alias': contact.isAlias,\n    });\n    return (\n      <span className={className}>{contact.toString()}</span>\n    );\n  }\n\n  _renderAccounts(accounts) {\n    const items = AccountStore.aliasesFor(accounts);\n    return (\n      <Menu\n        items={items}\n        itemKey={contact => contact.id}\n        itemContent={this._renderMenuItem}\n        onSelect={this._onChooseContact}\n      />\n    );\n  }\n\n\n  _renderFromFieldComponents = () => {\n    const {draft, session, accounts} = this.props\n    return (\n      <InjectedComponentSet\n        className=\"dropdown-component\"\n        matching={{role: \"Composer:FromFieldComponents\"}}\n        exposedProps={{\n          draft,\n          session,\n          accounts,\n          currentAccount: draft.from[0],\n        }}\n      />\n    )\n  }\n\n  render() {\n    return (\n      <div className=\"composer-participant-field from-field\">\n        <div className=\"composer-field-label\">From:</div>\n        {this._renderAccountSelector()}\n        {this._renderFromFieldComponents()}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/action-bar-plugins.jsx",
    "content": "import React from 'react'\nimport classnames from 'classnames'\nimport {ComponentRegistry} from 'nylas-exports'\nimport {InjectedComponentSet} from 'nylas-component-kit'\n\nconst ROLE = \"Composer:ActionButton\";\n\nexport default class ActionBarPlugins extends React.Component {\n  static displayName = \"ActionBarPlugins\";\n\n  static propTypes = {\n    draft: React.PropTypes.object,\n    session: React.PropTypes.object,\n    isValidDraft: React.PropTypes.func,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = this._getStateFromStores()\n  }\n\n  componentDidMount() {\n    this._usub = ComponentRegistry.listen(this._onComponentsChange)\n  }\n\n  componentWillUnmount() {\n    this._usub();\n  }\n\n  _onComponentsChange = () => {\n    if (this._getPluginsLength() > 0) {\n      // The `InjectedComponentSet` also listens to the ComponentRegistry.\n      // Since we can't guarantee the order the listeners are fired in and\n      // we want to make sure we add the class after the injected component\n      // set has rendered, put the call in this requestAnimationFrame\n      //\n      // It also takes 2 frames to reliably get all of the icons painted.\n      window.requestAnimationFrame(() => {\n        window.requestAnimationFrame(() => {\n          this.setState(this._getStateFromStores())\n        })\n      })\n    }\n  }\n\n  _getPluginsLength() {\n    return ComponentRegistry.findComponentsMatching({role: ROLE}).length;\n  }\n\n  _getStateFromStores() {\n    return {\n      pluginsLoaded: this._getPluginsLength() > 0,\n    }\n  }\n\n  render() {\n    const className = classnames({\n      \"action-bar-animation-wrap\": true,\n      \"plugins-loaded\": this.state.pluginsLoaded,\n    });\n\n    return (\n      <span className={className}>\n        <div className=\"action-bar-cover\" />\n        <InjectedComponentSet\n          className=\"composer-action-bar-plugins\"\n          matching={{role: ROLE}}\n          exposedProps={{\n            draft: this.props.draft,\n            threadId: this.props.draft.threadId,\n            draftClientId: this.props.draft.clientId,\n            session: this.props.session,\n            isValidDraft: this.props.isValidDraft,\n          }}\n        />\n      </span>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/collapsed-participants.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport {Utils} from 'nylas-exports';\nimport {DropZone, InjectedComponentSet} from 'nylas-component-kit';\n\nconst NUM_TO_DISPLAY_MAX = 999;\n\nexport default class CollapsedParticipants extends React.Component {\n  static displayName = \"CollapsedParticipants\";\n\n  static propTypes = {\n    // Arrays of Contact objects.\n    to: React.PropTypes.array,\n    cc: React.PropTypes.array,\n    bcc: React.PropTypes.array,\n    onDrop: React.PropTypes.func,\n    onDragChange: React.PropTypes.func,\n  }\n\n  static defaultProps = {\n    to: [],\n    cc: [],\n    bcc: [],\n    onDrop: () => {},\n    onDragChange: () => {},\n  }\n\n  constructor(props = {}) {\n    super(props);\n    this.state = {\n      numToDisplay: NUM_TO_DISPLAY_MAX,\n      numRemaining: 0,\n      numBccRemaining: 0,\n    }\n  }\n\n  componentDidMount() {\n    this._setNumHiddenParticipants();\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (!Utils.isEqualReact(nextProps, this.props)) {\n      // Always re-evaluate the hidden participant count when the participant set changes\n      this.setState({\n        numToDisplay: NUM_TO_DISPLAY_MAX,\n        numRemaining: 0,\n        numBccRemaining: 0,\n      });\n    }\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);\n  }\n\n  componentDidUpdate() {\n    if (this.state.numToDisplay === NUM_TO_DISPLAY_MAX) {\n      this._setNumHiddenParticipants();\n    }\n  }\n\n  _setNumHiddenParticipants() {\n    const $wrap = ReactDOM.findDOMNode(this.refs.participantsWrap);\n    const $regulars = Array.from($wrap.getElementsByClassName(\"regular-contact\"));\n    const $bccs = Array.from($wrap.getElementsByClassName(\"bcc-contact\"));\n\n    const availableSpace = $wrap.getBoundingClientRect().width;\n    let numRemaining = this.props.to.length + this.props.cc.length;\n    let numBccRemaining = this.props.bcc.length;\n    let numToDisplay = 0;\n    let widthAccumulator = 0;\n\n    for (const $p of $regulars) {\n      widthAccumulator += $p.getBoundingClientRect().width;\n      if (widthAccumulator >= availableSpace) {\n        break;\n      }\n      numRemaining -= 1;\n      numToDisplay += 1;\n    }\n\n    for (const $p of $bccs) {\n      widthAccumulator += $p.getBoundingClientRect().width;\n      if (widthAccumulator >= availableSpace) {\n        break;\n      }\n      numBccRemaining -= 1;\n      numToDisplay += 1;\n    }\n\n    this.setState({numToDisplay, numRemaining, numBccRemaining});\n  }\n\n  _renderNumRemaining() {\n    let str = null;\n    if (this.state.numRemaining === 0 && this.state.numBccRemaining === 0) {\n      return null;\n    } else if (this.state.numRemaining > 0 && this.state.numBccRemaining === 0) {\n      str = `${this.state.numRemaining} more`;\n    } else if (this.state.numRemaining === 0 && this.state.numBccRemaining > 0) {\n      str = `${this.state.numBccRemaining} Bcc`;\n    } else if (this.state.numRemaining > 0 && this.state.numBccRemaining > 0) {\n      str = `${this.state.numRemaining + this.state.numBccRemaining} more (${this.state.numBccRemaining} Bcc)`;\n    }\n\n    return (\n      <div className=\"num-remaining-wrap tokenizing-field\">\n        <div className=\"show-more-fade\" />\n        <div className=\"num-remaining token\">{str}</div>\n      </div>\n    );\n  }\n\n  _collapsedContact = (contact) => {\n    const name = contact.displayName();\n    const key = contact.email + contact.name;\n\n    return (\n      <span\n        key={key}\n        className=\"collapsed-contact regular-contact\"\n      >\n        <InjectedComponentSet\n          matching={{role: \"Composer:RecipientChip\"}}\n          exposedProps={{contact: contact, collapsed: true}}\n          direction=\"row\"\n          inline\n        />\n        {name}\n      </span>\n    );\n  }\n\n  _collapsedBccContact = (contact, i) => {\n    let name = contact.displayName();\n    const key = contact.email + contact.name;\n    if (i === 0) {\n      name = `Bcc: ${name}`;\n    }\n    return (\n      <span key={key} className=\"collapsed-contact bcc-contact\">{name}</span>\n    );\n  }\n\n  render() {\n    const contacts = this.props.to.concat(this.props.cc).map(this._collapsedContact)\n    const bcc = this.props.bcc.map(this._collapsedBccContact);\n\n    let toDisplay = contacts.concat(bcc);\n    toDisplay = toDisplay.splice(0, this.state.numToDisplay);\n    if (toDisplay.length === 0) {\n      toDisplay = \"Recipients\";\n    }\n\n    return (\n      <DropZone\n        shouldAcceptDrop={() => true}\n        onDragStateChange={this.props.onDragChange}\n        onDrop={this.props.onDrop}\n      >\n        <div\n          tabIndex={0}\n          ref=\"participantsWrap\"\n          className=\"collapsed-composer-participants\"\n        >\n          {this._renderNumRemaining()}\n          {toDisplay}\n        </div>\n      </DropZone>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/compose-button.jsx",
    "content": "import React from 'react';\nimport {Actions} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\n\nexport default class ComposeButton extends React.Component {\n  static displayName = 'ComposeButton';\n\n  _onNewCompose = () => {\n    Actions.composeNewBlankDraft()\n  }\n\n  render() {\n    return (\n      <button\n        className=\"btn btn-toolbar item-compose\"\n        title=\"Compose new message\"\n        onClick={this._onNewCompose}\n      >\n        <RetinaImg name=\"toolbar-compose.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/composer-editor.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport {ExtensionRegistry, DOMUtils} from 'nylas-exports';\nimport {DropZone, ScrollRegion, Contenteditable} from 'nylas-component-kit';\n\n/**\n * Renders the text editor for the composer\n * Any component registering in the ComponentRegistry with the role\n * 'Composer:Editor' will receive these set of props.\n *\n * In order for the Composer to work correctly and have a complete set of\n * functionality (like file pasting), any registered editor *must* call the\n * provided callbacks at the appropriate time.\n *\n * @param {object} props - props for ComposerEditor\n * @param {string} props.body - Html string with the draft content to be\n * rendered by the editor\n * @param {string} props.draftClientId - Id of the draft being currently edited\n * @param {object} props.parentActions - Object containg helper actions\n * associated with the parent container\n * @param {props.parentActions.getComposerBoundingRect} props.parentActions.getComposerBoundingRect\n * @param {props.parentActions.scrollTo} props.parentActions.scrollTo\n * @param {props.onFilePaste} props.onFilePaste\n * @param {props.onBodyChanged} props.onBodyChanged\n * @class ComposerEditor\n */\n\nconst NODE_END = false;\nconst NODE_BEGINNING = true;\n\nclass ComposerEditor extends Component {\n  static displayName = 'ComposerEditor';\n\n  /**\n   * This function will return the {DOMRect} for the parent component\n   * @function\n   * @name props.parentActions.getComposerBoundingRect\n   */\n  /**\n   * This function will make the screen scrollTo the desired position in the\n   * message list\n   * @function\n   * @name props.parentActions.scrollTo\n   * @param {object} options\n   * @param {string} options.clientId - Id of the message we want to scroll to\n   * @param {string} [options.positon] - If clientId is provided, this optional\n   * parameter will indicate what position of the message to scrollTo. See\n   * {ScrollRegion}\n   * @param {DOMRect} options.rect - Bounding rect we want to scroll to\n   */\n  /**\n   * This function should be called when the user pastes a file into the editing\n   * region\n   * @callback props.onFilePaste\n   */\n  /**\n   * This function should be called when the body of the draft changes, i.e.\n   * when the editor is being typed into. It should pass in an object that looks\n   * like a DOM Event with the current value of the content.\n   * @callback props.onBodyChanged\n   * @param {object} event - DOMEvent-like object that contains information\n   * about the current value of the body\n   * @param {string} event.target.value - HTML string that represents the\n   * current content of the editor body\n   */\n  static propTypes = {\n    body: PropTypes.string.isRequired,\n    draftClientId: PropTypes.string,\n    onFilePaste: PropTypes.func,\n    onBodyChanged: PropTypes.func,\n    parentActions: PropTypes.shape({\n      scrollTo: PropTypes.func,\n      getComposerBoundingRect: PropTypes.func,\n    }),\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      extensions: ExtensionRegistry.Composer.extensions(),\n    };\n  }\n\n  componentDidMount() {\n    this.unsub = ExtensionRegistry.Composer.listen(this._onExtensionsChanged);\n  }\n\n  componentWillUnmount() {\n    this.unsub();\n  }\n\n\n  // Public methods\n\n  // TODO Get rid of these selection methods\n  getCurrentSelection() {\n    return this.refs.contenteditable.getCurrentSelection();\n  }\n\n  getPreviousSelection() {\n    return this.refs.contenteditable.getPreviousSelection();\n  }\n\n  setSelection(selection) {\n    this.refs.contenteditable.setSelection(selection);\n  }\n\n  focus() {\n    // focus the composer and place the insertion point at the last text node of\n    // the body. Be sure to choose the last node /above/ the signature and any\n    // quoted text that is visible. (as in forwarded messages.)\n    //\n    this.refs.contenteditable.atomicEdit(({editor}) => {\n      editor.rootNode.focus();\n      const lastNode = this._findLastNodeBeforeQuoteOrSignature(editor)\n      if (lastNode) {\n        this._selectNode(lastNode, {collapseTo: NODE_END});\n      } else {\n        this._selectNode(editor.rootNode, {collapseTo: NODE_BEGINNING});\n      }\n    });\n  }\n\n  focusAbsoluteEnd() {\n    this.refs.contenteditable.atomicEdit(({editor}) => {\n      editor.rootNode.focus();\n      this._selectNode(editor.rootNode, {collapseTo: NODE_END});\n    });\n  }\n\n  // Note: This method returns null for new drafts, because the leading\n  // <br> tags contain no text nodes.\n  _findLastNodeBeforeQuoteOrSignature(editor) {\n    const walker = document.createTreeWalker(editor.rootNode, NodeFilter.SHOW_TEXT);\n    const nodesBelowUserBody = editor.rootNode.querySelectorAll('signature, .gmail_quote, blockquote');\n\n    let lastNode = null;\n    let node = walker.nextNode();\n    while (node != null) {\n      let belowUserBody = false;\n      for (let i = 0; i < nodesBelowUserBody.length; ++i) {\n        if (nodesBelowUserBody[i].contains(node)) {\n          belowUserBody = true;\n          break;\n        }\n      }\n      if (belowUserBody) {\n        break;\n      }\n      lastNode = node;\n      node = walker.nextNode();\n    }\n    return lastNode\n  }\n\n  _selectNode(node, {collapseTo} = {}) {\n    const range = document.createRange();\n    range.selectNodeContents(node);\n    range.collapse(collapseTo);\n    const selection = window.getSelection();\n    selection.removeAllRanges();\n    selection.addRange(range);\n  }\n\n  /**\n   * @private\n   * This method was included so that the tests don't break\n   * TODO refactor the tests!\n   */\n  _onDOMMutated(mutations) {\n    this.refs.contenteditable._onDOMMutated(mutations);\n  }\n\n  _onDrop = (event) => {\n    this.refs.contenteditable._onDrop(event)\n  }\n\n  _onDragOver = (event) => {\n    this.refs.contenteditable._onDragOver(event)\n  }\n\n  _shouldAcceptDrop = (event) => {\n    return this.refs.contenteditable._shouldAcceptDrop(event)\n  }\n  // Helpers\n\n  _scrollToBottom = () => {\n    this.props.parentActions.scrollTo({\n      clientId: this.props.draftClientId,\n      position: ScrollRegion.ScrollPosition.Bottom,\n    });\n  };\n\n  /**\n   * @private\n   * If the bottom of the container we're scrolling to is really far away\n   * from the contenteditable and your scroll position, we don't want to\n   * jump away. This can commonly happen if the composer has a very tall\n   * image attachment. The \"send\" button may be 1000px away from the bottom\n   * of the contenteditable. props.parentActions.scrollToBottom moves to the bottom of\n   * the \"send\" button.\n   */\n  _bottomIsNearby = (editableNode) => {\n    const parentRect = this.props.parentActions.getComposerBoundingRect();\n    const selfRect = editableNode.getBoundingClientRect();\n    return Math.abs(parentRect.bottom - selfRect.bottom) <= 250;\n  };\n\n  /**\n   * @private\n   * As you're typing a lot of content and the cursor begins to scroll off\n   * to the bottom, we want to make it look like we're tracking your\n   * typing.\n   */\n  _shouldScrollToBottom(selection, editableNode) {\n    return (\n      this.props.parentActions.scrollTo != null &&\n      DOMUtils.atEndOfContent(selection, editableNode) &&\n      this._bottomIsNearby(editableNode)\n    );\n  }\n\n  /**\n   * @private\n   * When the selectionState gets set (e.g. undo-ing and\n   * redo-ing) we need to make sure it's visible to the user.\n   *\n   * Unfortunately, we can't use the native `scrollIntoView` because it\n   * naively scrolls the whole window and doesn't know not to scroll if\n   * it's already in view. There's a new native method called\n   * `scrollIntoViewIfNeeded`, but this only works when the scroll\n   * container is a direct parent of the requested element. In this case\n   * the scroll container may be many levels up.\n  */\n  _ensureSelectionVisible = (selection, editableNode) => {\n    // If our parent supports scroll, check for that\n    if (this._shouldScrollToBottom(selection, editableNode)) {\n      this._scrollToBottom();\n    } else if (this.props.parentActions.scrollTo != null) {\n      // Don't bother computing client rects if no scroll method has been provided\n      const rangeInScope = DOMUtils.getRangeInScope(editableNode);\n      if (!rangeInScope) return;\n\n      let rect = rangeInScope.getBoundingClientRect();\n      if (DOMUtils.isEmptyBoundingRect(rect)) {\n        rect = DOMUtils.getSelectionRectFromDOM(selection);\n      }\n      if (rect) {\n        this.props.parentActions.scrollTo({rect});\n      }\n    }\n  };\n\n\n  // Handlers\n\n  _onExtensionsChanged = () => {\n    this.setState({extensions: ExtensionRegistry.Composer.extensions()});\n  };\n\n\n  // Renderers\n\n  render() {\n    return (\n      <DropZone\n        className=\"composer-inner-wrap\"\n        onDrop={this._onDrop}\n        onDragOver={this._onDragOver}\n        shouldAcceptDrop={this._shouldAcceptDrop}\n      >\n        <Contenteditable\n          ref=\"contenteditable\"\n          value={this.props.body}\n          onChange={this.props.onBodyChanged}\n          onFilePaste={this.props.onFilePaste}\n          onSelectionRestored={this._ensureSelectionVisible}\n          extensions={this.state.extensions}\n        />\n      </DropZone>\n    );\n  }\n}\nComposerEditor.containerRequired = false\n\nexport default ComposerEditor;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/composer-header-actions.jsx",
    "content": "import React from 'react';\nimport {Actions} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\nimport Fields from './fields';\n\nexport default class ComposerHeaderActions extends React.Component {\n  static displayName = 'ComposerHeaderActions';\n\n  static propTypes = {\n    draftClientId: React.PropTypes.string.isRequired,\n    enabledFields: React.PropTypes.array.isRequired,\n    participantsFocused: React.PropTypes.bool,\n    onShowAndFocusField: React.PropTypes.func.isRequired,\n  }\n\n  _onPopoutComposer = () => {\n    Actions.composePopoutDraft(this.props.draftClientId);\n  }\n\n  render() {\n    const items = [];\n\n    if (this.props.participantsFocused) {\n      if (!this.props.enabledFields.includes(Fields.Cc)) {\n        items.push(\n          <span\n            className=\"action show-cc\" key=\"cc\"\n            onClick={() => this.props.onShowAndFocusField(Fields.Cc)}\n          >Cc</span>\n        );\n      }\n\n      if (!this.props.enabledFields.includes(Fields.Bcc)) {\n        items.push(\n          <span\n            className=\"action show-bcc\" key=\"bcc\"\n            onClick={() => this.props.onShowAndFocusField(Fields.Bcc)}\n          >Bcc</span>\n        );\n      }\n    }\n\n    if (!this.props.enabledFields.includes(Fields.Subject)) {\n      items.push(\n        <span\n          className=\"action show-subject\" key=\"subject\"\n          onClick={() => this.props.onShowAndFocusField(Fields.Subject)}\n        >Subject</span>\n      );\n    }\n\n    if (!NylasEnv.isComposerWindow()) {\n      items.push(\n        <span\n          className=\"action show-popout\"\n          key=\"popout\"\n          title=\"Popout composer…\"\n          onClick={this._onPopoutComposer}\n        >\n          <RetinaImg\n            name=\"composer-popout.png\"\n            mode={RetinaImg.Mode.ContentIsMask}\n            style={{position: \"relative\", top: \"-2px\"}}\n          />\n        </span>\n      );\n    }\n\n    return (\n      <div className=\"composer-header-actions\">\n        {items}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/composer-header.jsx",
    "content": "import _ from 'underscore';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport {Utils, DraftHelpers, Actions, AccountStore} from 'nylas-exports';\nimport {\n  InjectedComponent,\n  KeyCommandsRegion,\n  ParticipantsTextField,\n  ListensToFluxStore,\n} from 'nylas-component-kit';\nimport AccountContactField from './account-contact-field';\nimport CollapsedParticipants from './collapsed-participants';\nimport ComposerHeaderActions from './composer-header-actions';\nimport SubjectTextField from './subject-text-field';\nimport Fields from './fields';\n\n\nconst ScopedFromField = ListensToFluxStore(AccountContactField, {\n  stores: [AccountStore],\n  getStateFromStores: (props) => {\n    const savedOrReplyToThread = !!props.draft.threadId;\n    if (savedOrReplyToThread) {\n      return {accounts: [AccountStore.accountForId(props.draft.accountId)]};\n    }\n    return {accounts: AccountStore.accounts()}\n  },\n});\n\nexport default class ComposerHeader extends React.Component {\n  static displayName = \"ComposerHeader\";\n\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n    initiallyFocused: React.PropTypes.bool,\n    // Subject text field injected component needs to call this function\n    // when it is rendered with a new header component\n    onNewHeaderComponents: React.PropTypes.func,\n  }\n\n  static contextTypes = {\n    parentTabGroup: React.PropTypes.object,\n  }\n\n  constructor(props = {}) {\n    super(props)\n    this.state = this._initialStateForDraft(this.props.draft, props);\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (this.props.session !== nextProps.session) {\n      this.setState(this._initialStateForDraft(nextProps.draft, nextProps));\n    } else {\n      this._ensureFilledFieldsEnabled(nextProps.draft);\n    }\n  }\n\n  focus() {\n    if (this.state.subjectFocused) {\n      this.refs.subject.focus();\n    } else if (this.state.participantsFocused) {\n      this.showAndFocusField(Fields.To);\n    }\n    console.warn(\"Nothing is marked as focused. This shouldn't happen!\");\n    this.showAndFocusField(Fields.To);\n  }\n\n  showAndFocusField = (fieldName) => {\n    const enabledFields = _.uniq([].concat(this.state.enabledFields, [fieldName]));\n    const participantsFocused = this.state.participantsFocused || Fields.ParticipantFields.includes(fieldName);\n\n    Utils.waitFor(() => this.refs[fieldName]).then(() =>\n      this.refs[fieldName].focus()\n    ).catch(() => {\n    })\n\n    this.setState({enabledFields, participantsFocused});\n  }\n\n  hideField = (fieldName) => {\n    if (ReactDOM.findDOMNode(this.refs[fieldName]).contains(document.activeElement)) {\n      this.context.parentTabGroup.shiftFocus(-1)\n    }\n\n    const enabledFields = _.without(this.state.enabledFields, fieldName)\n    this.setState({enabledFields})\n  }\n\n  _ensureFilledFieldsEnabled(draft) {\n    let enabledFields = this.state.enabledFields;\n    if (!_.isEmpty(draft.cc)) {\n      enabledFields = enabledFields.concat([Fields.Cc]);\n    }\n    if (!_.isEmpty(draft.bcc)) {\n      enabledFields = enabledFields.concat([Fields.Bcc]);\n    }\n    if (enabledFields !== this.state.enabledFields) {\n      this.setState({enabledFields});\n    }\n  }\n\n  _initialStateForDraft(draft, props) {\n    const enabledFields = [Fields.To];\n    if (!_.isEmpty(draft.cc)) {\n      enabledFields.push(Fields.Cc);\n    }\n    if (!_.isEmpty(draft.bcc)) {\n      enabledFields.push(Fields.Bcc);\n    }\n    enabledFields.push(Fields.From);\n    if (this._shouldEnableSubject()) {\n      enabledFields.push(Fields.Subject);\n    }\n\n    return {\n      enabledFields,\n      participantsFocused: props.initiallyFocused,\n      subjectFocused: false,\n    };\n  }\n\n  _shouldEnableSubject = () => {\n    if (_.isEmpty(this.props.draft.subject)) {\n      return true;\n    }\n    if (DraftHelpers.isForwardedMessage(this.props.draft)) {\n      return true;\n    }\n    if (this.props.draft.replyToMessageId) {\n      return false;\n    }\n    return true;\n  }\n\n  _onChangeParticipants = (changes) => {\n    this.props.session.changes.add(changes);\n    Actions.draftParticipantsChanged(this.props.draft.clientId, changes);\n  }\n\n  _onSubjectChange = (value) => {\n    this.props.session.changes.add({subject: value});\n  }\n\n  _onFocusInParticipants = () => {\n    const fieldName = this.state.participantsLastActiveField || Fields.To;\n    Utils.waitFor(() =>\n      this.refs[fieldName]\n    ).then(() =>\n      this.refs[fieldName].focus()\n    ).catch(() => {\n    });\n\n    this.setState({\n      participantsFocused: true,\n      participantsLastActiveField: null,\n    });\n  }\n\n  _onFocusOutParticipants = (lastFocusedEl) => {\n    const active = Fields.ParticipantFields.find((fieldName) => {\n      return this.refs[fieldName] ? ReactDOM.findDOMNode(this.refs[fieldName]).contains(lastFocusedEl) : false\n    }\n    );\n    this.setState({\n      participantsFocused: false,\n      participantsLastActiveField: active,\n    });\n  }\n\n  _onFocusInSubject = () => {\n    this.setState({\n      subjectFocused: true,\n    });\n  }\n\n  _onFocusOutSubject = () => {\n    this.setState({\n      subjectFocused: false,\n    });\n  }\n\n  isFocused() {\n    return this.state.participantsFocused || this.state.subjectFocused;\n  }\n\n  _onDragCollapsedParticipants = ({isDropping}) => {\n    if (isDropping) {\n      this.setState({\n        participantsFocused: true,\n        enabledFields: [...Fields.ParticipantFields, Fields.From, Fields.Subject],\n      })\n    }\n  }\n\n  _renderParticipants = () => {\n    let content = null;\n    if (this.state.participantsFocused) {\n      content = this._renderFields();\n    } else {\n      content = (\n        <CollapsedParticipants\n          to={this.props.draft.to}\n          cc={this.props.draft.cc}\n          bcc={this.props.draft.bcc}\n          onDragChange={this._onDragCollapsedParticipants}\n        />\n      )\n    }\n\n    // When the participants field collapses, we store the field that was last\n    // focused onto our state, so that we can restore focus to it when the fields\n    // are expanded again.\n    return (\n      <KeyCommandsRegion\n        tabIndex={-1}\n        ref=\"participantsContainer\"\n        className=\"expanded-participants\"\n        onFocusIn={this._onFocusInParticipants}\n        onFocusOut={this._onFocusOutParticipants}\n      >\n        {content}\n      </KeyCommandsRegion>\n    );\n  }\n\n  _renderSubject = () => {\n    if (!this.state.enabledFields.includes(Fields.Subject)) {\n      return false;\n    }\n    const {draft, session} = this.props\n    return (\n      <KeyCommandsRegion\n        tabIndex={-1}\n        ref=\"subjectContainer\"\n        onFocusIn={this._onFocusInSubject}\n        onFocusOut={this._onFocusOutSubject}\n      >\n        <InjectedComponent\n          ref={Fields.Subject}\n          key=\"subject-wrap\"\n          matching={{role: 'Composer:SubjectTextField'}}\n          exposedProps={{\n            draft,\n            session,\n            value: draft.subject,\n            draftClientId: draft.clientId,\n            onSubjectChange: this._onSubjectChange,\n          }}\n          requiredMethods={['focus']}\n          fallback={SubjectTextField}\n          onComponentDidChange={this.props.onNewHeaderComponents}\n        />\n      </KeyCommandsRegion>\n    )\n  }\n\n  _renderFields = () => {\n    const {to, cc, bcc, from} = this.props.draft;\n\n    // Note: We need to physically add and remove these elements, not just hide them.\n    // If they're hidden, shift-tab between fields breaks.\n    const fields = [];\n\n    fields.push(\n      <ParticipantsTextField\n        ref={Fields.To}\n        key=\"to\"\n        field=\"to\"\n        change={this._onChangeParticipants}\n        className=\"composer-participant-field to-field\"\n        participants={{to, cc, bcc}}\n        draft={this.props.draft}\n        session={this.props.session}\n      />\n    )\n\n    if (this.state.enabledFields.includes(Fields.Cc)) {\n      fields.push(\n        <ParticipantsTextField\n          ref={Fields.Cc}\n          key=\"cc\"\n          field=\"cc\"\n          change={this._onChangeParticipants}\n          onEmptied={() => this.hideField(Fields.Cc)}\n          className=\"composer-participant-field cc-field\"\n          participants={{to, cc, bcc}}\n          draft={this.props.draft}\n          session={this.props.session}\n        />\n      )\n    }\n\n    if (this.state.enabledFields.includes(Fields.Bcc)) {\n      fields.push(\n        <ParticipantsTextField\n          ref={Fields.Bcc}\n          key=\"bcc\"\n          field=\"bcc\"\n          change={this._onChangeParticipants}\n          onEmptied={() => this.hideField(Fields.Bcc)}\n          className=\"composer-participant-field bcc-field\"\n          participants={{to, cc, bcc}}\n          draft={this.props.draft}\n          session={this.props.session}\n        />\n      )\n    }\n\n    if (this.state.enabledFields.includes(Fields.From)) {\n      fields.push(\n        <ScopedFromField\n          key=\"from\"\n          ref={Fields.From}\n          value={from[0]}\n          draft={this.props.draft}\n          session={this.props.session}\n          onChange={this._onChangeParticipants}\n        />\n      )\n    }\n\n    return fields;\n  }\n\n  render() {\n    return (\n      <div className=\"composer-header\">\n        <ComposerHeaderActions\n          draftClientId={this.props.draft.clientId}\n          enabledFields={this.state.enabledFields}\n          participantsFocused={this.state.participantsFocused}\n          onShowAndFocusField={this.showAndFocusField}\n        />\n        {this._renderParticipants()}\n        {this._renderSubject()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/composer-view.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport {remote} from 'electron'\nimport {\n  Utils,\n  Actions,\n  DraftStore,\n  DraftHelpers,\n} from 'nylas-exports'\nimport {\n  DropZone,\n  RetinaImg,\n  ScrollRegion,\n  TabGroupRegion,\n  AttachmentItem,\n  InjectedComponent,\n  KeyCommandsRegion,\n  OverlaidComponents,\n  ImageAttachmentItem,\n  InjectedComponentSet,\n} from 'nylas-component-kit'\nimport ComposerEditor from './composer-editor'\nimport ComposerHeader from './composer-header'\nimport SendActionButton from './send-action-button'\nimport ActionBarPlugins from './action-bar-plugins'\nimport Fields from './fields'\n\n// The ComposerView is a unique React component because it (currently) is a\n// singleton. Normally, the React way to do things would be to re-render the\n// Composer with new props.\nexport default class ComposerView extends React.Component {\n  static displayName = 'ComposerView';\n\n  static propTypes = {\n    session: React.PropTypes.object.isRequired,\n    draft: React.PropTypes.object.isRequired,\n\n    // Sometimes when changes in the composer happens it's desirable to\n    // have the parent scroll to a certain location. A parent component can\n    // pass a callback that gets called when this composer wants to be\n    // scrolled to.\n    scrollTo: React.PropTypes.func,\n    className: React.PropTypes.string,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      showQuotedText: DraftHelpers.isForwardedMessage(props.draft),\n      showQuotedTextControl: DraftHelpers.shouldAppendQuotedText(props.draft),\n    }\n  }\n\n  componentDidMount() {\n    this._recordComposerOpenTime()\n    if (this.props.session) {\n      this._setupForProps(this.props);\n    }\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (nextProps.session !== this.props.session) {\n      this._teardownForProps();\n      this._setupForProps(nextProps);\n    }\n    if (DraftHelpers.isForwardedMessage(this.props.draft) !== DraftHelpers.isForwardedMessage(nextProps.draft) ||\n      DraftHelpers.shouldAppendQuotedText(this.props.draft) !== DraftHelpers.shouldAppendQuotedText(nextProps.draft)) {\n      this.setState({\n        showQuotedText: DraftHelpers.isForwardedMessage(nextProps.draft),\n        showQuotedTextControl: DraftHelpers.shouldAppendQuotedText(nextProps.draft),\n      });\n    }\n  }\n\n  componentWillUnmount() {\n    this._teardownForProps();\n  }\n\n  focus() {\n    if (this.refs.header.isFocused()) {\n      this.refs.header.focus();\n    } else {\n      this.refs[Fields.Body].focus();\n    }\n  }\n\n  _recordComposerOpenTime() {\n    const {draft: {threadId, replyToMessageId}} = this.props\n    if (NylasEnv.isComposerWindow()) { return }\n\n    // This method only records inline composer opening times. Composer window\n    // opening times are recorded in ComposerWithWindowPros\n    const replyTimerKey = `compose-reply-${replyToMessageId}`\n    const forwardTimerKey = `compose-forward-${threadId}`\n    let actionTimeMs;\n    if (NylasEnv.timer.isPending(replyTimerKey)) {\n      actionTimeMs = NylasEnv.timer.stop(replyTimerKey)\n    }\n    if (NylasEnv.timer.isPending(forwardTimerKey)) {\n      actionTimeMs = NylasEnv.timer.stop(forwardTimerKey)\n    }\n    if (actionTimeMs != null) {\n      Actions.recordPerfMetric({\n        action: 'open-inline-composer',\n        actionTimeMs,\n        maxValue: 4000,\n        sample: 0.9,\n      })\n    }\n  }\n\n  _keymapHandlers() {\n    return {\n      'composer:send-message': () => this._onPrimarySend(),\n      'composer:delete-empty-draft': () => {\n        if (this.props.draft.pristine) {\n          this._onDestroyDraft();\n        }\n      },\n      'composer:show-and-focus-bcc': () => this.refs.header.showAndFocusField(Fields.Bcc),\n      'composer:show-and-focus-cc': () => this.refs.header.showAndFocusField(Fields.Cc),\n      'composer:focus-to': () => this.refs.header.showAndFocusField(Fields.To),\n      \"composer:show-and-focus-from\": () => {},\n      \"core:undo\": (event) => {\n        event.preventDefault();\n        event.stopPropagation();\n        this.props.session.undo();\n      },\n      \"core:redo\": (event) => {\n        event.preventDefault();\n        event.stopPropagation();\n        this.props.session.redo();\n      },\n    };\n  }\n\n  _setupForProps({draft, session}) {\n    this.setState({\n      showQuotedText: DraftHelpers.isForwardedMessage(draft),\n      showQuotedTextControl: DraftHelpers.shouldAppendQuotedText(draft),\n    });\n\n    // TODO: This is a dirty hack to save selection state into the undo/redo\n    // history. Remove it if / when selection is written into the body with\n    // marker tags, or when selection is moved from `contenteditable.innerState`\n    // into a first-order part of the session state.\n\n    session._composerViewSelectionRetrieve = () => {\n      // Selection updates /before/ the contenteditable emits it's change event,\n      // so the selection that goes with the snapshot state is the previous one.\n      if (this.refs[Fields.Body].getPreviousSelection) {\n        return this.refs[Fields.Body].getPreviousSelection();\n      }\n      return null;\n    }\n\n    session._composerViewSelectionRestore = (selection) => {\n      this.refs[Fields.Body].setSelection(selection);\n    }\n\n    draft.files.forEach((file) => {\n      if (Utils.shouldDisplayAsImage(file)) {\n        Actions.fetchFile(file);\n      }\n    });\n  }\n\n  _teardownForProps() {\n    if (this.props.session) {\n      this.props.session._composerViewSelectionRestore = null;\n      this.props.session._composerViewSelectionRetrieve = null;\n    }\n  }\n\n  _renderContentScrollRegion() {\n    if (NylasEnv.isComposerWindow()) {\n      return (\n        <ScrollRegion className=\"compose-body-scroll\" ref=\"scrollregion\">\n          {this._renderContent()}\n        </ScrollRegion>\n      );\n    }\n    return this._renderContent();\n  }\n\n  _onNewHeaderComponents = () => {\n    if (this.refs.header) {\n      this.focus()\n    }\n  }\n\n  _renderContent() {\n    return (\n      <div className=\"composer-centered\">\n        <ComposerHeader\n          ref=\"header\"\n          draft={this.props.draft}\n          session={this.props.session}\n          initiallyFocused={this.props.draft.to.length === 0}\n          onNewHeaderComponents={this._onNewHeaderComponents}\n        />\n        <div\n          className=\"compose-body\"\n          ref=\"composeBody\"\n          onMouseUp={this._onMouseUpComposerBody}\n          onMouseDown={this._onMouseDownComposerBody}\n        >\n          {this._renderBodyRegions()}\n          {this._renderFooterRegions()}\n        </div>\n      </div>\n    );\n  }\n\n  _renderBodyRegions() {\n    const exposedProps = {\n      draft: this.props.draft,\n      session: this.props.session,\n    }\n    return (\n      <div ref=\"composerBodyWrap\" className=\"composer-body-wrap\">\n        <OverlaidComponents exposedProps={exposedProps}>\n          {this._renderEditor()}\n        </OverlaidComponents>\n        {this._renderQuotedTextControl()}\n        {this._renderAttachments()}\n      </div>\n    );\n  }\n\n  _renderEditor() {\n    const exposedProps = {\n      body: this.props.draft.body,\n      draftClientId: this.props.draft.clientId,\n      parentActions: {\n        getComposerBoundingRect: this._getComposerBoundingRect,\n        scrollTo: this.props.scrollTo,\n      },\n      onFilePaste: this._onFileReceived,\n      onBodyChanged: this._onBodyChanged,\n    };\n\n    return (\n      <InjectedComponent\n        ref={Fields.Body}\n        className=\"body-field\"\n        matching={{role: \"Composer:Editor\"}}\n        fallback={ComposerEditor}\n        requiredMethods={[\n          'focus',\n          'focusAbsoluteEnd',\n          'getPreviousSelection',\n          'setSelection',\n          '_onDOMMutated',\n        ]}\n        exposedProps={exposedProps}\n      />\n    );\n  }\n\n  // The contenteditable decides when to request a scroll based on the\n  // position of the cursor and its relative distance to this composer\n  // component. We provide it our boundingClientRect so it can calculate\n  // this value.\n  _getComposerBoundingRect = () => {\n    return ReactDOM.findDOMNode(this.refs.composerWrap).getBoundingClientRect()\n  }\n\n  _renderQuotedTextControl() {\n    if (this.state.showQuotedTextControl) {\n      return (\n        <a className=\"quoted-text-control\" onClick={this._onExpandQuotedText}>\n          <span className=\"dots\">&bull;&bull;&bull;</span>\n          <span className=\"remove-quoted-text\" onClick={this._onRemoveQuotedText}>\n            <RetinaImg\n              title=\"Remove quoted text\"\n              name=\"image-cancel-button.png\"\n              mode={RetinaImg.Mode.ContentPreserve}\n            />\n          </span>\n        </a>\n      );\n    }\n    return false;\n  }\n\n  _onExpandQuotedText = () => {\n    this.setState({\n      showQuotedText: true,\n      showQuotedTextControl: false,\n    }, () => {\n      DraftHelpers.appendQuotedTextToDraft(this.props.draft)\n      .then((draftWithQuotedText) => {\n        this.props.session.changes.add({\n          body: `${draftWithQuotedText.body}<div id=\"n1-quoted-text-marker\" />`,\n        })\n      })\n    })\n  }\n\n  _onRemoveQuotedText = (event) => {\n    event.stopPropagation()\n    const {session, draft} = this.props\n    session.changes.add({\n      body: `${draft.body}<div id=\"n1-quoted-text-marker\" />`,\n    })\n    this.setState({\n      showQuotedText: false,\n      showQuotedTextControl: false,\n    })\n  }\n\n  _renderFooterRegions() {\n    return (\n      <div className=\"composer-footer-region\">\n        <InjectedComponentSet\n          matching={{role: \"Composer:Footer\"}}\n          exposedProps={{\n            draft: this.props.draft,\n            threadId: this.props.draft.threadId,\n            draftClientId: this.props.draft.clientId,\n            session: this.props.session,\n          }}\n          direction=\"column\"\n        />\n      </div>\n    );\n  }\n\n  _renderAttachments() {\n    return (\n      <div className=\"attachments-area\">\n        {this._renderFileAttachments()}\n        {this._renderUploadAttachments()}\n      </div>\n    );\n  }\n\n  _renderFileAttachments() {\n    const {files, clientId: messageClientId} = this.props.draft\n    return (\n      <InjectedComponent\n        matching={{role: 'MessageAttachments'}}\n        exposedProps={{files, messageClientId, canRemoveAttachments: true}}\n      />\n    )\n  }\n\n  _imageFiles(files) {\n    return files.filter(f => Utils.shouldDisplayAsImage(f));\n  }\n\n  _nonImageFiles(files) {\n    return files.filter(f => !Utils.shouldDisplayAsImage(f));\n  }\n\n  _renderUploadAttachments() {\n    const {uploads} = this.props.draft;\n\n    const nonImageUploads = this._nonImageFiles(uploads)\n      .map((upload) =>\n        <AttachmentItem\n          key={upload.id}\n          className=\"file-upload\"\n          draggable={false}\n          filePath={upload.targetPath}\n          displayName={upload.filename}\n          fileIconName={`file-${upload.extension}.png`}\n          onRemoveAttachment={() => Actions.removeAttachment(upload)}\n        />\n      );\n    const imageUploads = this._imageFiles(uploads)\n      .filter(u => !u.inline)\n      .map((upload) =>\n        <ImageAttachmentItem\n          key={upload.id}\n          className=\"file-upload\"\n          draggable={false}\n          filePath={upload.targetPath}\n          displayName={upload.filename}\n          onRemoveAttachment={() => Actions.removeAttachment(upload)}\n        />\n      );\n    return nonImageUploads.concat(imageUploads);\n  }\n\n  _renderActionsWorkspaceRegion() {\n    return (\n      <InjectedComponentSet\n        matching={{role: \"Composer:ActionBarWorkspace\"}}\n        exposedProps={{\n          draft: this.props.draft,\n          threadId: this.props.draft.threadId,\n          draftClientId: this.props.draft.clientId,\n          session: this.props.session,\n        }}\n      />\n    )\n  }\n\n  _renderActionsRegion() {\n    return (\n      <div className=\"composer-action-bar-content\">\n        <ActionBarPlugins\n          draft={this.props.draft}\n          session={this.props.session}\n          isValidDraft={this._isValidDraft}\n        />\n\n        <button\n          tabIndex={-1}\n          className=\"btn btn-toolbar btn-trash\"\n          style={{order: 100}}\n          title=\"Delete draft\"\n          onClick={this._onDestroyDraft}\n        >\n          <RetinaImg name=\"icon-composer-trash.png\" mode={RetinaImg.Mode.ContentIsMask} />\n        </button>\n\n        <button\n          tabIndex={-1}\n          className=\"btn btn-toolbar btn-attach\"\n          style={{order: 50}}\n          title=\"Attach file\"\n          onClick={this._onSelectAttachment}\n        >\n          <RetinaImg name=\"icon-composer-attachment.png\" mode={RetinaImg.Mode.ContentIsMask} />\n        </button>\n\n        <div style={{order: 0, flex: 1}} />\n\n\n        <InjectedComponent\n          ref=\"sendActionButton\"\n          tabIndex={-1}\n          style={{order: -100}}\n          matching={{role: \"Composer:SendActionButton\"}}\n          fallback={SendActionButton}\n          requiredMethods={[\n            'primarySend',\n          ]}\n          exposedProps={{\n            draft: this.props.draft,\n            draftClientId: this.props.draft.clientId,\n            session: this.props.session,\n            isValidDraft: this._isValidDraft,\n          }}\n        />\n      </div>\n    );\n  }\n\n  // This lets us click outside of the `contenteditable`'s `contentBody`\n  // and simulate what happens when you click beneath the text *in* the\n  // contentEditable.\n\n  // Unfortunately, we need to manually keep track of the \"click\" in\n  // separate mouseDown, mouseUp events because we need to ensure that the\n  // start and end target are both not in the contenteditable. This ensures\n  // that this behavior doesn't interfear with a click and drag selection.\n  _onMouseDownComposerBody = (event) => {\n    if (ReactDOM.findDOMNode(this.refs[Fields.Body]).contains(event.target)) {\n      this._mouseDownTarget = null;\n    } else {\n      this._mouseDownTarget = event.target;\n    }\n  }\n\n  _inFooterRegion(el) {\n    return el.closest && el.closest(\".composer-footer-region, .overlaid-components\")\n  }\n\n  _onMouseUpComposerBody = (event) => {\n    if (event.target === this._mouseDownTarget && !this._inFooterRegion(event.target)) {\n      // We don't set state directly here because we want the native\n      // contenteditable focus behavior. When the contenteditable gets focused\n      const bodyRect = ReactDOM.findDOMNode(this.refs[Fields.Body]).getBoundingClientRect()\n      if (event.pageY < bodyRect.top) {\n        this.refs[Fields.Body].focus()\n      } else {\n        this.refs[Fields.Body].focusAbsoluteEnd();\n      }\n    }\n    this._mouseDownTarget = null;\n  }\n\n  _onMouseMoveComposeBody = () => {\n    if (this._mouseComposeBody === \"down\") {\n      this._mouseComposeBody = \"move\";\n    }\n  }\n\n  _shouldAcceptDrop = (event) => {\n    // Ensure that you can't pick up a file and drop it on the same draft\n    const nonNativeFilePath = this._nonNativeFilePathForDrop(event);\n\n    const hasNativeFile = event.dataTransfer.files.length > 0;\n    const hasNonNativeFilePath = nonNativeFilePath !== null;\n\n    return hasNativeFile || hasNonNativeFilePath;\n  }\n\n  _nonNativeFilePathForDrop = (event) => {\n    if (event.dataTransfer.types.includes(\"text/nylas-file-url\")) {\n      const downloadURL = event.dataTransfer.getData(\"text/nylas-file-url\");\n      const downloadFilePath = downloadURL.split('file://')[1];\n      if (downloadFilePath) {\n        return downloadFilePath;\n      }\n    }\n\n    // Accept drops of images from within the app\n    if (event.dataTransfer.types.includes(\"text/uri-list\")) {\n      const uri = event.dataTransfer.getData('text/uri-list')\n      if (uri.indexOf('file://') === 0) {\n        return decodeURI(uri.split('file://')[1]);\n      }\n    }\n    return null;\n  }\n\n  _onDrop = (event) => {\n    // Accept drops of real files from other applications\n    for (const file of Array.from(event.dataTransfer.files)) {\n      this._onFileReceived(file.path);\n    }\n\n    // Accept drops from attachment components / images within the app\n    const uri = this._nonNativeFilePathForDrop(event);\n    if (uri) {\n      this._onFileReceived(uri);\n    }\n  }\n\n  _onFileReceived = (filePath) => {\n    // called from onDrop and onFilePaste - assume images should be inline\n    Actions.addAttachment({\n      filePath: filePath,\n      messageClientId: this.props.draft.clientId,\n      onUploadCreated: (upload) => {\n        if (Utils.shouldDisplayAsImage(upload)) {\n          const {draft, session} = this.props;\n\n          const uploads = [].concat(draft.uploads);\n          const matchingUpload = uploads.find(u => u.id === upload.id);\n          if (matchingUpload) {\n            matchingUpload.inline = true;\n            session.changes.add({uploads})\n\n            Actions.insertAttachmentIntoDraft({\n              draftClientId: draft.clientId,\n              uploadId: matchingUpload.id,\n            });\n          }\n        }\n      },\n    });\n  }\n\n  _onBodyChanged = (event) => {\n    this.props.session.changes.add({body: event.target.value});\n    return;\n  }\n\n  _isValidDraft = (options = {}) => {\n    // We need to check the `DraftStore` because the `DraftStore` is\n    // immediately and synchronously updated as soon as this function\n    // fires. Since `setState` is asynchronous, if we used that as our only\n    // check, then we might get a false reading.\n    if (DraftStore.isSendingDraft(this.props.draft.clientId)) {\n      return false;\n    }\n\n    const dialog = remote.dialog;\n    const {session} = this.props\n    const {errors, warnings} = session.validateDraftForSending()\n\n    if (errors.length > 0) {\n      dialog.showMessageBox(remote.getCurrentWindow(), {\n        type: 'warning',\n        buttons: ['Edit Message', 'Cancel'],\n        message: 'Cannot Send',\n        detail: errors[0],\n      });\n      return false;\n    }\n\n    if ((warnings.length > 0) && (!options.force)) {\n      const response = dialog.showMessageBox(remote.getCurrentWindow(), {\n        type: 'warning',\n        buttons: ['Send Anyway', 'Cancel'],\n        message: 'Are you sure?',\n        detail: `Send ${warnings.join(' and ')}?`,\n      });\n      if (response === 0) { // response is button array index\n        return this._isValidDraft({force: true});\n      }\n      return false;\n    }\n    return true;\n  }\n\n  _onPrimarySend = () => {\n    this.refs.sendActionButton.primarySend();\n  }\n\n  _onDestroyDraft = () => {\n    Actions.destroyDraft(this.props.draft.clientId);\n  }\n\n  _onSelectAttachment = () => {\n    Actions.selectAttachment({messageClientId: this.props.draft.clientId});\n  }\n\n  render() {\n    const dropCoverDisplay = this.state.isDropping ? 'block' : 'none';\n\n    return (\n      <div className={this.props.className}>\n        <KeyCommandsRegion\n          localHandlers={this._keymapHandlers()}\n          className={\"message-item-white-wrap composer-outer-wrap\"}\n          tabIndex=\"-1\"\n          ref=\"composerWrap\"\n        >\n          <TabGroupRegion className=\"composer-inner-wrap\">\n            <DropZone\n              className=\"composer-inner-wrap\"\n              shouldAcceptDrop={this._shouldAcceptDrop}\n              onDragStateChange={({isDropping}) => this.setState({isDropping})}\n              onDrop={this._onDrop}\n            >\n              <div className=\"composer-drop-cover\" style={{display: dropCoverDisplay}}>\n                <div className=\"centered\">\n                  <RetinaImg\n                    name=\"composer-drop-to-attach.png\"\n                    mode={RetinaImg.Mode.ContentIsMask}\n                  />\n                  Drop to attach\n                </div>\n              </div>\n\n              <div className=\"composer-content-wrap\">\n                {this._renderContentScrollRegion()}\n              </div>\n\n              <div className=\"composer-action-bar-workspace-wrap\">\n                {this._renderActionsWorkspaceRegion()}\n              </div>\n\n              <div className=\"composer-action-bar-wrap\" data-tooltips-anchor>\n                <div className=\"tooltips-container\" />\n                {this._renderActionsRegion()}\n              </div>\n            </DropZone>\n          </TabGroupRegion>\n        </KeyCommandsRegion>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/fields.es6",
    "content": "const Fields = {\n  To: \"textFieldTo\",\n  Cc: \"textFieldCc\",\n  Bcc: \"textFieldBcc\",\n  From: \"fromField\",\n  Subject: \"textFieldSubject\",\n  Body: \"contentBody\",\n};\n\nFields.ParticipantFields = [Fields.To, Fields.Cc, Fields.Bcc];\n\nFields.Order = {\n  textFieldTo: 1,\n  textFieldCc: 2,\n  textFieldBcc: 3,\n  fromField: -1, // Not selectable\n  textFieldSubject: 5,\n  contentBody: 6,\n};\n\nexport default Fields\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/image-upload-composer-extension.es6",
    "content": "import {\n  Actions,\n  ComposerExtension,\n} from 'nylas-exports'\n\nexport default class ImageUploadComposerExtension extends ComposerExtension {\n\n  static editingActions() {\n    return [{\n      action: Actions.insertAttachmentIntoDraft,\n      callback: ImageUploadComposerExtension._onInsertAttachmentIntoDraft,\n    }, {\n      action: Actions.removeAttachment,\n      callback: ImageUploadComposerExtension._onRemovedAttachment,\n    }]\n  }\n\n  static _onRemovedAttachment({editor, actionArg}) {\n    const upload = actionArg;\n    const el = editor.rootNode.querySelector(`.inline-container-${upload.id}`)\n    if (el) {\n      el.parentNode.removeChild(el);\n    }\n  }\n\n  static _onInsertAttachmentIntoDraft({editor, actionArg}) {\n    if (editor.draftClientId === actionArg.draftClientId) { return }\n\n    editor.insertCustomComponent(\"InlineImageUploadContainer\", {\n      className: `inline-container-${actionArg.uploadId}`,\n      uploadId: actionArg.uploadId,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/inline-image-upload-container.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport ReactDOM from 'react-dom';\nimport fs from 'fs';\nimport path from 'path';\nimport {Actions} from 'nylas-exports'\nimport {ImageAttachmentItem} from 'nylas-component-kit'\n\nexport default class InlineImageUploadContainer extends Component {\n  static displayName = 'InlineImageUploadContainer';\n\n  static supportsPreviewWithinEditor = false;\n\n  static propTypes = {\n    draft: PropTypes.object.isRequired,\n    uploadId: PropTypes.string.isRequired,\n    session: PropTypes.object,\n    isPreview: PropTypes.bool,\n  }\n\n  _onGoEdit = () => {\n    if (!this.props.session) {\n      console.warn(\"InlineImage editor cannot be activated, `session` prop not present. (isPreview?)\")\n      return;\n    }\n    // This is just a fun temporary hack because I was jealous of Apple Mail.\n    //\n    const el = ReactDOM.findDOMNode(this);\n    const rect = el.getBoundingClientRect();\n\n    const editorEl = document.createElement('div');\n    editorEl.style.position = 'absolute';\n    editorEl.style.left = `${rect.left}px`;\n    editorEl.style.top = `${rect.top}px`;\n    editorEl.style.width = `${rect.width}px`;\n    editorEl.style.height = `${rect.height}px`;\n    editorEl.style.zIndex = 2000;\n\n    const editorCanvas = document.createElement('canvas');\n    editorCanvas.width = rect.width * window.devicePixelRatio;\n    editorCanvas.height = rect.height * window.devicePixelRatio;\n    editorCanvas.style.width = `${rect.width}px`;\n    editorCanvas.style.height = `${rect.height}px`;\n    editorEl.appendChild(editorCanvas);\n\n    const editorCtx = editorCanvas.getContext(\"2d\");\n    editorCtx.drawImage(el.querySelector('.file-preview img'), 0, 0, editorCanvas.width, editorCanvas.height);\n    editorCtx.strokeStyle = \"#df4b26\";\n    editorCtx.lineJoin = \"round\";\n    editorCtx.lineWidth = 3 * window.devicePixelRatio;\n\n    let penDown = false;\n    let penXY = null;\n    editorCanvas.addEventListener('mousedown', (event) => {\n      penDown = true;\n      penXY = {\n        x: event.offsetX,\n        y: event.offsetY,\n      }\n    });\n    editorCanvas.addEventListener('mousemove', (event) => {\n      if (penDown) {\n        const nextPenXY = {\n          x: event.offsetX,\n          y: event.offsetY,\n        }\n        editorCtx.beginPath();\n        editorCtx.moveTo(penXY.x * window.devicePixelRatio, penXY.y * window.devicePixelRatio);\n        editorCtx.lineTo(nextPenXY.x * window.devicePixelRatio, nextPenXY.y * window.devicePixelRatio);\n        editorCtx.closePath();\n        editorCtx.stroke();\n        penXY = nextPenXY;\n      }\n    });\n\n    editorCanvas.addEventListener('mouseup', () => {\n      penDown = false;\n      penXY = null;\n    });\n\n    const backgroundEl = document.createElement('div');\n    backgroundEl.style.background = 'rgba(0,0,0,0.4)';\n    backgroundEl.style.position = 'absolute';\n    backgroundEl.style.top = '0px';\n    backgroundEl.style.left = '0px';\n    backgroundEl.style.right = '0px';\n    backgroundEl.style.bottom = '0px';\n    backgroundEl.style.zIndex = 1999;\n    backgroundEl.addEventListener('click', () => {\n      editorCanvas.toBlob((blob) => {\n        const reader = new FileReader();\n        reader.addEventListener('loadend', () => {\n          const {draft, session, uploadId} = this.props;\n          const buffer = new Buffer(new Uint8Array(reader.result));\n          const upload = draft.uploads.find(u =>\n            u.id === uploadId\n          );\n\n          const nextTargetPath = path.join(path.dirname(upload.targetPath), `edited-${Date.now()}.png`);\n          fs.writeFile(nextTargetPath, buffer, (err) => {\n            if (err) {\n              NylasEnv.showErrorDialog(err.toString())\n              return;\n            }\n            const img = el.querySelector('.file-preview img');\n            img.style.width = `${rect.width}px`;\n            img.style.height = `${rect.height}px`;\n            img.src = `${img.src}?${Date.now()}`;\n\n            fs.unlink(upload.targetPath);\n\n            const nextUploads = [].concat(draft.uploads);\n            nextUploads.forEach((u) => {\n              if (u.targetPath === upload.targetPath) {\n                u.targetPath = nextTargetPath;\n              }\n            });\n            session.changes.add({uploads: nextUploads});\n          });\n        });\n        reader.readAsArrayBuffer(blob);\n      });\n      document.body.removeChild(editorEl);\n      document.body.removeChild(backgroundEl);\n    });\n    document.body.appendChild(backgroundEl);\n    document.body.appendChild(editorEl);\n  }\n\n  render() {\n    const {draft, uploadId, isPreview} = this.props;\n    const upload = draft.uploads.find(u => uploadId === u.id);\n\n    if (!upload) {\n      return (\n        <span />\n      );\n    }\n    if (isPreview) {\n      return (\n        <img src={`cid:${upload.id}`} alt={upload.name} />\n      );\n    }\n\n    return (\n      <div\n        data-src={`cid:${upload.id}`}\n        className=\"inline-image-upload-container\"\n        onDoubleClick={this._onGoEdit}\n      >\n        <ImageAttachmentItem\n          className=\"file-upload\"\n          draggable={false}\n          filePath={upload.targetPath}\n          displayName={upload.filename}\n          onRemoveAttachment={() => Actions.removeAttachment(upload)}\n        />\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/main.jsx",
    "content": "/* eslint react/sort-comp: 0 */\nimport _ from 'underscore';\nimport React from 'react';\nimport {\n  Message,\n  Actions,\n  DraftStore,\n  WorkspaceStore,\n  ComponentRegistry,\n  ExtensionRegistry,\n  InflatesDraftClientId,\n  CustomContenteditableComponents,\n} from 'nylas-exports';\nimport {OverlaidComposerExtension} from 'nylas-component-kit'\nimport ComposeButton from './compose-button';\nimport ComposerView from './composer-view';\nimport ImageUploadComposerExtension from './image-upload-composer-extension';\nimport InlineImageUploadContainer from \"./inline-image-upload-container\";\n\n\nconst ComposerViewForDraftClientId = InflatesDraftClientId(ComposerView);\n\nclass ComposerWithWindowProps extends React.Component {\n  static displayName = 'ComposerWithWindowProps';\n  static containerRequired = false;\n\n  constructor(props) {\n    super(props);\n\n    // We'll now always have windowProps by the time we construct this.\n    const windowProps = NylasEnv.getWindowProps();\n    const {draftJSON, draftClientId} = windowProps;\n    if (!draftJSON) {\n      throw new Error(\"Initialize popout composer windows with valid draftJSON\")\n    }\n    const draft = new Message().fromJSON(draftJSON);\n    DraftStore._createSession(draftClientId, draft);\n    this.state = windowProps\n  }\n\n  componentWillUnmount() {\n    if (this._usub) { this._usub() }\n  }\n\n  componentDidUpdate() {\n    this.refs.composer.focus()\n  }\n\n  _onDraftReady = () => {\n    this.refs.composer.focus().then(() => {\n      NylasEnv.displayWindow();\n      this._recordComposerOpenTime()\n\n      if (this.state.errorMessage) {\n        this._showInitialErrorDialog(this.state.errorMessage, this.state.errorDetail);\n      }\n\n      // This will start loading the rest of the composer's plugins. This\n      // may take a while (hundreds of ms) depending on how many plugins\n      // you have installed. For some reason it takes two frames to\n      // reliably get the basic composer (Send button, etc) painted\n      // properly.\n      window.requestAnimationFrame(() => {\n        window.requestAnimationFrame(() => {\n          NylasEnv.getCurrentWindow().updateLoadSettings({\n            windowType: \"composer\",\n          })\n        })\n      })\n    });\n  }\n\n  _recordComposerOpenTime() {\n    const {timerId} = NylasEnv.getWindowProps()\n    const timerKey = `open-composer-window-${timerId}`\n    if (NylasEnv.timer.isPending(timerKey)) {\n      const actionTimeMs = NylasEnv.timer.stop(timerKey);\n      if (actionTimeMs && actionTimeMs <= 4000) {\n        // TODO do we still need to record this legacy event?\n        Actions.recordUserEvent(\"Composer Popout Timed\", {timeInMs: actionTimeMs})\n      }\n      Actions.recordPerfMetric({\n        action: 'open-composer-window',\n        actionTimeMs,\n        maxValue: 4000,\n        sample: 0.9,\n      })\n    }\n  }\n\n  render() {\n    return (\n      <ComposerViewForDraftClientId\n        ref=\"composer\"\n        onDraftReady={this._onDraftReady}\n        draftClientId={this.state.draftClientId}\n        className=\"composer-full-window\"\n      />\n    );\n  }\n\n  _showInitialErrorDialog(msg, detail) {\n    // We delay so the view has time to update the restored draft. If we\n    // don't delay the modal may come up in a state where the draft looks\n    // like it hasn't been restored or has been lost.\n    _.delay(() => {\n      NylasEnv.showErrorDialog({title: 'Error', message: msg}, {detail: detail})\n    }, 100);\n  }\n}\n\nexport function activate() {\n  if (NylasEnv.isMainWindow()) {\n    ComponentRegistry.register(ComposerViewForDraftClientId, {\n      role: 'Composer',\n    });\n    ComponentRegistry.register(ComposeButton, {\n      location: WorkspaceStore.Location.RootSidebar.Toolbar,\n    });\n  } else if (NylasEnv.isThreadWindow()) {\n    ComponentRegistry.register(ComposerViewForDraftClientId, {\n      role: 'Composer',\n    });\n  } else {\n    NylasEnv.getCurrentWindow().setMinimumSize(480, 250);\n    ComponentRegistry.register(ComposerWithWindowProps, {\n      location: WorkspaceStore.Location.Center,\n    });\n  }\n\n  ExtensionRegistry.Composer.register(OverlaidComposerExtension, {priority: 1})\n  ExtensionRegistry.Composer.register(ImageUploadComposerExtension);\n  CustomContenteditableComponents.register(\"InlineImageUploadContainer\", InlineImageUploadContainer);\n}\n\nexport function deactivate() {\n  if (NylasEnv.isMainWindow()) {\n    ComponentRegistry.unregister(ComposerViewForDraftClientId);\n    ComponentRegistry.unregister(ComposeButton);\n  } else {\n    ComponentRegistry.unregister(ComposerWithWindowProps);\n  }\n\n  ExtensionRegistry.Composer.unregister(OverlaidComposerExtension)\n  ExtensionRegistry.Composer.unregister(ImageUploadComposerExtension);\n  CustomContenteditableComponents.unregister(\"InlineImageUploadContainer\");\n}\n\nexport function serialize() {\n  return this.state;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/send-action-button.jsx",
    "content": "import React from 'react'\nimport {Actions, SendActionsStore} from 'nylas-exports'\nimport {Menu, RetinaImg, ButtonDropdown, ListensToFluxStore} from 'nylas-component-kit'\n\n\nclass SendActionButton extends React.Component {\n  static displayName = \"SendActionButton\";\n\n  static containerRequired = false\n\n  static propTypes = {\n    draft: React.PropTypes.object,\n    isValidDraft: React.PropTypes.func,\n    sendActions: React.PropTypes.array,\n    orderedSendActions: React.PropTypes.object,\n  };\n\n  primarySend() {\n    this._onPrimaryClick();\n  }\n\n  _onPrimaryClick = () => {\n    const {orderedSendActions} = this.props\n    const {preferred} = orderedSendActions\n    this._onSendWithAction(preferred);\n  }\n\n  _onSendWithAction = (sendAction) => {\n    const {isValidDraft, draft} = this.props\n    if (isValidDraft()) {\n      Actions.sendDraft(draft.clientId, sendAction.configKey)\n    }\n  }\n\n  _renderSendActionItem = ({iconUrl}) => {\n    let plusHTML = \"\";\n    let additionalImg = false;\n\n    if (iconUrl) {\n      plusHTML = <span>&nbsp;+&nbsp;</span>;\n      additionalImg = <RetinaImg url={iconUrl} mode={RetinaImg.Mode.ContentIsMask} />;\n    }\n\n    return (\n      <span>\n        <RetinaImg name=\"icon-composer-send.png\" mode={RetinaImg.Mode.ContentIsMask} />\n        <span className=\"text\">Send{plusHTML}</span>{additionalImg}\n      </span>\n    );\n  }\n\n  _renderSingleButton() {\n    const {sendActions} = this.props\n    return (\n      <button\n        tabIndex={-1}\n        className={\"btn btn-toolbar btn-normal btn-emphasis btn-text btn-send\"}\n        style={{order: -100}}\n        onClick={this._onPrimaryClick}\n      >\n        {this._renderSendActionItem(sendActions[0])}\n      </button>\n    );\n  }\n\n  _renderButtonDropdown() {\n    const {orderedSendActions} = this.props\n    const {preferred, rest} = orderedSendActions\n\n    const menu = (\n      <Menu\n        items={rest}\n        itemKey={(actionConfig) => actionConfig.configKey}\n        itemContent={this._renderSendActionItem}\n        onSelect={this._onSendWithAction}\n      />\n  );\n\n    return (\n      <ButtonDropdown\n        className={\"btn-send btn-emphasis btn-text\"}\n        style={{order: -100}}\n        primaryItem={this._renderSendActionItem(preferred)}\n        primaryTitle={preferred.title}\n        primaryClick={this._onPrimaryClick}\n        closeOnMenuClick\n        menu={menu}\n      />\n    );\n  }\n\n  render() {\n    const {sendActions} = this.props\n    if (sendActions.length === 1) {\n      return this._renderSingleButton();\n    }\n    return this._renderButtonDropdown();\n  }\n}\n\nconst EnhancedSendActionButton = ListensToFluxStore(SendActionButton, {\n  stores: [SendActionsStore],\n  getStateFromStores(props) {\n    const {draft} = props\n    return {\n      sendActions: SendActionsStore.availableSendActionsForDraft(draft),\n      orderedSendActions: SendActionsStore.orderedSendActionsForDraft(draft),\n    }\n  },\n})\n// TODO this is a hack so that the send button can still expose\n// the `primarySend` method required by the ComposerView. Ideally, this\n// decorator mechanism should expose whatever instance methods are exposed\n// by the component its wrapping.\n// However, I think the better fix will happen when mail merge lives in its\n// own window and doesn't need to override the Composer's send button, which\n// is already a bit of a hack.\nObject.assign(EnhancedSendActionButton.prototype, {\n  primarySend() {\n    if (this.refs.composed) {\n      this.refs.composed.primarySend()\n    }\n  },\n})\n\nEnhancedSendActionButton.UndecoratedSendActionButton = SendActionButton\n\nexport default EnhancedSendActionButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/lib/subject-text-field.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {findDOMNode} from 'react-dom'\n\n\nexport default class SubjectTextField extends Component {\n  static displayName = 'SubjectTextField'\n\n  static containerRequired = false\n\n  static propTypes = {\n    value: PropTypes.string,\n    onSubjectChange: PropTypes.func,\n  }\n\n  onInputChange = ({target: {value}}) => {\n    this.props.onSubjectChange(value)\n  }\n\n  focus() {\n    findDOMNode(this.refs.input).focus()\n  }\n\n  render() {\n    const {value} = this.props\n\n    return (\n      <div className=\"composer-subject subject-field\">\n        <input\n          ref=\"input\"\n          type=\"text\"\n          name=\"subject\"\n          placeholder=\"Subject\"\n          value={value}\n          onChange={this.onInputChange}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/package.json",
    "content": "{\n  \"name\": \"composer\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Nylas Composer Component\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"scripts\": {\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"composer-preload\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/spec/collapsed-participants-spec.cjsx",
    "content": "_ = require \"underscore\"\nReact = require \"react\"\nReactDOM = require 'react-dom'\nReactTestUtils = require 'react-addons-test-utils'\n\nFields = require('../lib/fields').default\nCollapsedParticipants = require('../lib/collapsed-participants').default\n\n{Contact} = require 'nylas-exports'\n\ndescribe \"CollapsedParticipants\", ->\n  makeField = (props={}) ->\n    @fields = ReactTestUtils.renderIntoDocument(\n      <CollapsedParticipants {...props} />\n    )\n\n  numStr = ->\n    ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(@fields, \"num-remaining\")).innerHTML\n\n  it \"doesn't render num remaining when nothing remains\", ->\n    makeField.call(@)\n    els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@fields, \"num-remaining\")\n    expect(els.length).toBe 0\n\n  it \"renders num remaining when remaining with no bcc\", ->\n    makeField.call(@)\n    spyOn(@fields, \"_setNumHiddenParticipants\")\n    @fields.setState numRemaining: 10, numBccRemaining: 0\n    str = numStr.call(@)\n    expect(str).toBe \"10 more\"\n\n  it \"renders num remaining when only bcc\", ->\n    makeField.call(@)\n    spyOn(@fields, \"_setNumHiddenParticipants\")\n    @fields.setState numRemaining: 0, numBccRemaining: 5\n    str = numStr.call(@)\n    expect(str).toBe \"5 Bcc\"\n\n  it \"renders num remaining when both remaining andj bcc\", ->\n    makeField.call(@)\n    spyOn(@fields, \"_setNumHiddenParticipants\")\n    @fields.setState numRemaining: 10, numBccRemaining: 5\n    str = numStr.call(@)\n    expect(str).toBe \"15 more (5 Bcc)\"\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/spec/composer-header-actions-spec.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\nComposerHeaderActions = require('../lib/composer-header-actions').default\nFields = require('../lib/fields').default\nReactTestUtils = require('react-addons-test-utils')\n{Actions} = require 'nylas-exports'\n\ndescribe \"ComposerHeaderActions\", ->\n  makeField = (props = {}) ->\n    @onShowAndFocusField = jasmine.createSpy(\"onShowAndFocusField\")\n    props.onShowAndFocusField = @onShowAndFocusField\n    props.enabledFields ?= []\n    props.draftClientId = 'a'\n    @component = ReactTestUtils.renderIntoDocument(\n      <ComposerHeaderActions {...props} />\n    )\n\n  it \"renders the 'show' buttons for 'cc', 'bcc' when participantsFocused\", ->\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: true})\n    showCc = ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"show-cc\")\n    showBcc = ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"show-bcc\")\n    showSubject = ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"show-subject\")\n    expect(showCc).toBeDefined()\n    expect(showBcc).toBeDefined()\n\n  it \"does not render the 'show' buttons for 'cc', 'bcc' when participantsFocused is false\", ->\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: false})\n    showCc = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-cc\")\n    showBcc = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-bcc\")\n    showSubject = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-subject\")\n    expect(showCc.length).toBe 0\n    expect(showBcc.length).toBe 0\n\n  it \"hides show cc if it's enabled\", ->\n    makeField.call(@, {enabledFields: [Fields.To, Fields.Cc], participantsFocused: true})\n    els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-cc\")\n    expect(els.length).toBe 0\n\n  it \"hides show bcc if it's enabled\", ->\n    makeField.call(@, {enabledFields: [Fields.To, Fields.Bcc], participantsFocused: true})\n    els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-bcc\")\n    expect(els.length).toBe 0\n\n  it \"hides show subject if it's enabled\", ->\n    makeField.call(@, {enabledFields: [Fields.To, Fields.Subject], participantsFocused: true})\n    els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-subject\")\n    expect(els.length).toBe 0\n\n  it \"renders 'popout composer' in the inline mode\", ->\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: true})\n    els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-popout\")\n    expect(els.length).toBe 1\n\n  it \"doesn't render 'popout composer' if in a composer window\", ->\n    spyOn(NylasEnv, 'isComposerWindow').andReturn(true)\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: true})\n    els = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"show-popout\")\n    expect(els.length).toBe 0\n\n  it \"pops out the composer when clicked\", ->\n    spyOn(Actions, \"composePopoutDraft\")\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: true})\n    el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"show-popout\")\n    ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(el))\n    expect(Actions.composePopoutDraft).toHaveBeenCalled()\n\n  it \"shows and focuses cc when clicked\", ->\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: true})\n    el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"show-cc\")\n    ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(el))\n    expect(@onShowAndFocusField).toHaveBeenCalledWith Fields.Cc\n\n  it \"shows and focuses bcc when clicked\", ->\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: true})\n    el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"show-bcc\")\n    ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(el))\n    expect(@onShowAndFocusField).toHaveBeenCalledWith Fields.Bcc\n\n  it \"shows subject when clicked\", ->\n    makeField.call(@, {enabledFields: [Fields.To], participantsFocused: false})\n    el = ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"show-subject\")\n    ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(el))\n    expect(@onShowAndFocusField).toHaveBeenCalledWith Fields.Subject\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/spec/composer-header-spec.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport ReactTestUtils from 'react-addons-test-utils';\n\nimport {Contact, Message} from 'nylas-exports';\nimport ComposerHeader from '../lib/composer-header';\nimport Fields from '../lib/fields';\n\ndescribe('ComposerHeader', function composerHeader() {\n  beforeEach(() => {\n    this.createWithDraft = (draft) => {\n      const session = {\n        changes: {\n          add: jasmine.createSpy('changes.add'),\n        },\n      };\n      this.component = ReactTestUtils.renderIntoDocument(\n        <ComposerHeader\n          draft={draft}\n          initiallyFocused={false}\n          session={session}\n        />\n      )\n    };\n    advanceClock()\n  });\n\n  describe(\"showAndFocusField\", () => {\n    beforeEach(() => {\n      const draft = new Message({\n        draft: true,\n        accountId: TEST_ACCOUNT_ID,\n      });\n      this.createWithDraft(draft);\n    });\n\n    it(\"should ensure the field is in enabledFields\", () => {\n      expect(this.component.state.enabledFields).toEqual(['textFieldTo', 'fromField', 'textFieldSubject'])\n      this.component.showAndFocusField(Fields.Bcc);\n      expect(this.component.state.enabledFields).toEqual(['textFieldTo', 'fromField', 'textFieldSubject', 'textFieldBcc'])\n    });\n\n    it(\"should ensure participantsFocused is true if necessary\", () => {\n      expect(this.component.state.participantsFocused).toEqual(false);\n      this.component.showAndFocusField(Fields.Subject);\n      expect(this.component.state.participantsFocused).toEqual(false);\n      this.component.showAndFocusField(Fields.Bcc);\n      expect(this.component.state.participantsFocused).toEqual(true);\n    });\n\n    it(\"should wait for the field to become available and then focus it\", () => {\n      const $el = ReactDOM.findDOMNode(this.component);\n      expect($el.querySelector('.bcc-field')).toBe(null);\n      this.component.showAndFocusField(Fields.Bcc);\n      advanceClock();\n      expect($el.querySelector('.bcc-field')).not.toBe(null);\n    });\n  });\n\n  describe(\"hideField\", () => {\n    beforeEach(() => {\n      const draft = new Message({draft: true, accountId: TEST_ACCOUNT_ID});\n      this.createWithDraft(draft);\n    });\n\n    it(\"should remove the field from enabledFields\", () => {\n      const $el = ReactDOM.findDOMNode(this.component);\n\n      this.component.showAndFocusField(Fields.Bcc);\n      advanceClock();\n      expect($el.querySelector('.bcc-field')).not.toBe(null);\n      this.component.hideField(Fields.Bcc);\n      advanceClock();\n      expect($el.querySelector('.bcc-field')).toBe(null);\n    });\n  });\n\n  describe(\"initial state\", () => {\n    it(\"should enable any fields that are populated\", () => {\n      let draft = null;\n\n      draft = new Message({draft: true, accountId: TEST_ACCOUNT_ID});\n      this.createWithDraft(draft);\n      expect(this.component.state.enabledFields).toEqual(['textFieldTo', 'fromField', 'textFieldSubject'])\n\n      draft = new Message({\n        draft: true,\n        cc: [new Contact({id: 'a', email: 'a'})],\n        bcc: [new Contact({id: 'b', email: 'b'})],\n        accountId: TEST_ACCOUNT_ID,\n      });\n      this.createWithDraft(draft);\n      expect(this.component.state.enabledFields).toEqual(['textFieldTo', 'textFieldCc', 'textFieldBcc', 'fromField', 'textFieldSubject'])\n    });\n\n    describe(\"subject\", () => {\n      it(\"should be enabled if it is empty\", () => {\n        const draft = new Message({draft: true, subject: '', accountId: TEST_ACCOUNT_ID});\n        this.createWithDraft(draft);\n        expect(this.component.state.enabledFields).toEqual(['textFieldTo', 'fromField', 'textFieldSubject'])\n      });\n\n      it(\"should be enabled if the message is a forward\", () => {\n        const draft = new Message({draft: true, subject: 'Fwd: 1234', accountId: TEST_ACCOUNT_ID});\n        this.createWithDraft(draft);\n        expect(this.component.state.enabledFields).toEqual(['textFieldTo', 'fromField', 'textFieldSubject'])\n      });\n\n      it(\"should be hidden if the message is a reply\", () => {\n        const draft = new Message({draft: true, subject: 'Re: 1234', replyToMessageId: '123', accountId: TEST_ACCOUNT_ID});\n        this.createWithDraft(draft);\n        expect(this.component.state.enabledFields).toEqual(['textFieldTo', 'fromField'])\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/spec/composer-view-spec.cjsx",
    "content": "_ = require \"underscore\"\nReact = require \"react\"\nReactDOM = require 'react-dom'\nReactTestUtils = require('react-addons-test-utils')\n\n{Actions,\n Utils,\n File,\n Contact,\n Message,\n Account,\n DraftStore,\n DatabaseStore,\n NylasTestUtils,\n AccountStore,\n FileUploadStore,\n ContactStore,\n FocusedContentStore,\n ComponentRegistry} = require \"nylas-exports\"\n\n{InjectedComponent, ParticipantsTextField} = require 'nylas-component-kit'\n\nDraftEditingSession = require '../../../src/flux/stores/draft-editing-session'\nComposerEditor = require('../lib/composer-editor').default\nFields = require('../lib/fields').default\n\nu1 = new Contact(name: \"Christine Spang\", email: \"spang@nylas.com\")\nu2 = new Contact(name: \"Michael Grinich\", email: \"mg@nylas.com\")\nu3 = new Contact(name: \"Evan Morikawa\",   email: \"evan@nylas.com\")\nu4 = new Contact(name: \"Zoë Leiper\",      email: \"zip@nylas.com\")\nu5 = new Contact(name: \"Ben Gotow\",       email: \"ben@nylas.com\")\n\nf1 = new File(id: 'file_1_id', filename: 'a.png', contentType: 'image/png', size: 10, object: \"file\")\nf2 = new File(id: 'file_2_id', filename: 'b.pdf', contentType: '', size: 999999, object: \"file\")\n\nusers = [u1, u2, u3, u4, u5]\n\nComposerView = require(\"../lib/composer-view\").default\n\n# This will setup the mocks necessary to make the composer element (once\n# mounted) think it's attached to the given draft. This mocks out the\n# proxy system used by the composer.\nDRAFT_CLIENT_ID = \"local-123\"\n\nuseDraft = (draftAttributes={}) ->\n  @draft = new Message _.extend({draft: true, body: \"\"}, draftAttributes)\n  @draft.clientId = DRAFT_CLIENT_ID\n  @session = new DraftEditingSession(DRAFT_CLIENT_ID, @draft)\n  # spyOn().andCallFake wasn't working properly on ensureCorrectAccount for some reason\n  @session.ensureCorrectAccount = => Promise.resolve(@session)\n  DraftStore._draftSessions[DRAFT_CLIENT_ID] = @session\n  @session._draftPromise\n\nuseFullDraft = ->\n  useDraft.call @,\n    from: [AccountStore.accounts()[0].me()]\n    to: [u2]\n    cc: [u3, u4]\n    bcc: [u5]\n    files: [f1, f2]\n    subject: \"Test Message 1\"\n    body: \"Hello <b>World</b><br/> This is a test\"\n    replyToMessageId: null\n\nmakeComposer = (props={}) ->\n  @composer = NylasTestUtils.renderIntoDocument(\n    <ComposerView draft={@draft} session={@session} {...props} />\n  )\n  advanceClock()\n\ndescribe \"ComposerView\", ->\n  beforeEach ->\n    ComposerEditor.containerRequired = false\n    ComponentRegistry.register(ComposerEditor, role: \"Composer:Editor\")\n\n    spyOn(Actions, 'queueTask')\n    spyOn(Actions, 'queueTasks')\n    spyOn(DraftStore, \"isSendingDraft\").andCallThrough()\n    spyOn(DraftEditingSession.prototype, 'changeSetCommit').andCallFake (draft) =>\n      @draft = draft\n    spyOn(ContactStore, \"searchContacts\").andCallFake (email) =>\n      return _.filter(users, (u) u.email.toLowerCase() is email.toLowerCase())\n    spyOn(Contact.prototype, \"isValid\").andCallFake (contact) ->\n      return @email.indexOf('@') > 0\n\n  afterEach ->\n    ComposerEditor.containerRequired = undefined\n    ComponentRegistry.unregister(ComposerEditor)\n    DraftStore._cleanupAllSessions()\n    NylasTestUtils.removeFromDocument(@composer)\n\n  describe \"when sending a new message\", ->\n    it 'makes a request with the message contents', ->\n      sessionSetupComplete = false\n      useDraft.call(@).then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        editableNode = ReactDOM.findDOMNode(@composer).querySelector('[contenteditable]')\n        spyOn(@session.changes, \"add\")\n        editableNode.innerHTML = \"Hello <strong>world</strong>\"\n        @composer.refs[Fields.Body]._onDOMMutated([\"mutated\"])\n        expect(@session.changes.add).toHaveBeenCalled()\n        expect(@session.changes.add.calls.length).toBe 1\n        body = @session.changes.add.calls[0].args[0].body\n        expect(body).toBe \"Hello <strong>world</strong>\"\n      )\n\n  describe \"when sending a reply-to message\", ->\n    beforeEach ->\n      sessionSetupComplete = false\n      useDraft.call(@,\n        from: [u1]\n        to: [u2]\n        subject: \"Test Reply Message 1\"\n        body: \"\"\n        replyToMessageId: \"1\")\n      .then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n\n      runs( =>\n        makeComposer.call(@)\n        @editableNode = ReactDOM.findDOMNode(@composer).querySelector('[contenteditable]')\n        spyOn(@session.changes, \"add\")\n      )\n\n    it 'begins with empty body', ->\n      expect(@editableNode.innerHTML).toBe \"\"\n\n  describe \"when sending a forwarded message\", ->\n    beforeEach ->\n      @fwdBody = \"\"\"<br><br><blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n      ---------- Forwarded message ---------\n      <br><br>\n      From: Evan Morikawa &lt;evan@evanmorikawa.com&gt;<br>Subject: Test Forward Message 1<br>Date: Sep 3 2015, at 12:14 pm<br>To: Evan Morikawa &lt;evan@nylas.com&gt;\n      <br><br>\n\n      <meta content=\"text/html; charset=us-ascii\">This is a test!\n      </blockquote>\"\"\"\n\n      sessionSetupComplete = false\n      useDraft.call(@,\n        from: [u1]\n        to: [u2]\n        subject: \"Fwd: Test Forward Message 1\"\n        body: @fwdBody)\n      .then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n\n      runs( =>\n        makeComposer.call(@)\n        @editableNode = ReactDOM.findDOMNode(@composer).querySelector('[contenteditable]')\n        spyOn(@session.changes, \"add\")\n      )\n\n    it 'begins with the forwarded message expanded', ->\n      expect(@editableNode.innerHTML).toBe @fwdBody\n\n    it 'saves the full new body, plus forwarded text', ->\n      @editableNode.innerHTML = \"Hello <strong>world</strong>#{@fwdBody}\"\n      @composer.refs[Fields.Body]._onDOMMutated([\"mutated\"])\n      expect(@session.changes.add).toHaveBeenCalled()\n      expect(@session.changes.add.calls.length).toBe 1\n      body = @session.changes.add.calls[0].args[0].body\n      expect(body).toBe \"\"\"Hello <strong>world</strong>#{@fwdBody}\"\"\"\n\n  describe \"When sending a message\", ->\n    beforeEach ->\n      spyOn(NylasEnv, \"isMainWindow\").andReturn true\n      {remote} = require('electron')\n      @dialog = remote.dialog\n      spyOn(remote, \"getCurrentWindow\")\n      spyOn(@dialog, \"showMessageBox\")\n      spyOn(Actions, \"sendDraft\").andCallThrough()\n\n    it \"shows an error if there are no recipients\", ->\n      sessionSetupComplete = false\n      useDraft.call(@, subject: \"no recipients\").then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        status = @composer._isValidDraft()\n        expect(status).toBe false\n        expect(@dialog.showMessageBox).toHaveBeenCalled()\n        dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]\n        expect(dialogArgs.detail).toEqual(\"You need to provide one or more recipients before sending the message.\")\n        expect(dialogArgs.buttons).toEqual ['Edit Message', 'Cancel']\n      )\n\n    it \"shows an error if a recipient is invalid\", ->\n      sessionSetupComplete = false\n      useDraft.call(@,\n        subject: 'hello world!'\n        to: [new Contact(email: 'lol', name: 'lol')])\n      .then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        status = @composer._isValidDraft()\n        expect(status).toBe false\n        expect(@dialog.showMessageBox).toHaveBeenCalled()\n        dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]\n        expect(dialogArgs.detail).toEqual(\"lol is not a valid email address - please remove or edit it before sending.\")\n        expect(dialogArgs.buttons).toEqual ['Edit Message', 'Cancel']\n      )\n\n    describe \"empty body warning\", ->\n      it \"warns if the body of the email is still the pristine body\", ->\n        pristineBody = \"<br><br>\"\n        sessionSetupComplete = false\n        useDraft.call(@,\n          to: [u1]\n          subject: \"Hello World\"\n          body: pristineBody)\n        .then( =>\n          sessionSetupComplete = true\n        )\n        waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n\n        runs( =>\n          makeComposer.call(@)\n          spyOn(@session, 'draftPristineBody').andCallFake -> pristineBody\n\n          status = @composer._isValidDraft()\n          expect(status).toBe false\n          expect(@dialog.showMessageBox).toHaveBeenCalled()\n          dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]\n          expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']\n        )\n\n      it \"does not warn if the body of the email is all quoted text, but the email is a forward\", ->\n        sessionSetupComplete = false\n        useDraft.call(@,\n          to: [u1]\n          subject: \"Fwd: Hello World\"\n          body: \"<br><br><blockquote class='gmail_quote'>This is my quoted text!</blockquote>\")\n        .then( =>\n          sessionSetupComplete = true\n        )\n        waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n        runs( =>\n          makeComposer.call(@)\n          status = @composer._isValidDraft()\n          expect(status).toBe true\n        )\n\n      it \"does not warn if the user has attached a file\", ->\n        sessionSetupComplete = false\n        useDraft.call(@,\n          to: [u1]\n          subject: \"Hello World\"\n          body: \"\"\n          files: [f1])\n        .then( =>\n          sessionSetupComplete = true\n        )\n        waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n\n        runs( =>\n          makeComposer.call(@)\n          status = @composer._isValidDraft()\n          expect(status).toBe true\n          expect(@dialog.showMessageBox).not.toHaveBeenCalled()\n        )\n\n    it \"shows a warning if there's no subject\", ->\n      sessionSetupComplete = false\n      useDraft.call(@, to: [u1], subject: \"\").then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        status = @composer._isValidDraft()\n        expect(status).toBe false\n        expect(@dialog.showMessageBox).toHaveBeenCalled()\n        dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]\n        expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']\n      )\n\n    it \"doesn't show a warning if requirements are satisfied\", ->\n      sessionSetupComplete = false\n      useFullDraft.apply(@).then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        status = @composer._isValidDraft()\n        expect(status).toBe true\n        expect(@dialog.showMessageBox).not.toHaveBeenCalled()\n      )\n\n    describe \"Checking for attachments\", ->\n      warn = (body) ->\n        sessionSetupComplete = false\n        useDraft.call(@, subject: \"Subject\", to: [u1], body: body).then( =>\n          sessionSetupComplete = true\n        )\n        waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n        runs( =>\n          makeComposer.call(@)\n          status = @composer._isValidDraft()\n          expect(status).toBe false\n          expect(@dialog.showMessageBox).toHaveBeenCalled()\n          dialogArgs = @dialog.showMessageBox.mostRecentCall.args[1]\n          expect(dialogArgs.buttons).toEqual ['Send Anyway', 'Cancel']\n        )\n\n      noWarn = (body) ->\n        sessionSetupComplete = false\n        useDraft.call(@, subject: \"Subject\", to: [u1], body: body).then( =>\n          sessionSetupComplete = true\n        )\n        waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n        runs( =>\n          makeComposer.call(@)\n          status = @composer._isValidDraft()\n          expect(status).toBe true\n          expect(@dialog.showMessageBox).not.toHaveBeenCalled()\n        )\n\n      it \"warns\", -> warn.call(@, \"Check out the attached file\")\n      it \"warns\", -> warn.call(@, \"I've added an attachment\")\n      it \"warns\", -> warn.call(@, \"I'm going to attach the file\")\n      it \"warns\", -> warn.call(@, \"Hey attach me <blockquote class='gmail_quote'>sup</blockquote>\")\n\n      it \"doesn't warn\", -> noWarn.call(@, \"sup yo\")\n      it \"doesn't warn\", -> noWarn.call(@, \"Look at the file\")\n      it \"doesn't warn\", -> noWarn.call(@, \"Hey there <blockquote class='gmail_quote'>attach</blockquote>\")\n\n    it \"doesn't show a warning if you've attached a file\", ->\n      sessionSetupComplete = false\n      useDraft.call(@,\n        subject: \"Subject\"\n        to: [u1]\n        body: \"Check out attached file\"\n        files: [f1])\n      .then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        status = @composer._isValidDraft()\n        expect(status).toBe true\n        expect(@dialog.showMessageBox).not.toHaveBeenCalled()\n      )\n\n    it \"bypasses the warning if force bit is set\", ->\n      sessionSetupComplete = false\n      useDraft.call(@, to: [u1], subject: \"\").then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        status = @composer._isValidDraft(force: true)\n        expect(status).toBe true\n        expect(@dialog.showMessageBox).not.toHaveBeenCalled()\n      )\n\n    it \"sends when you click the send button\", ->\n      sessionSetupComplete = false\n      useFullDraft.apply(@).then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        sendBtn = @composer.refs.sendActionButton\n        sendBtn.primarySend()\n        expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID, 'send')\n        expect(Actions.sendDraft.calls.length).toBe 1\n        # Delete the draft from _draftsSending so we can send it in other tests\n        delete DraftStore._draftsSending[DRAFT_CLIENT_ID]\n      )\n\n    it \"doesn't send twice if you double click\", =>\n      sessionSetupComplete = false\n      useFullDraft.apply(@).then( =>\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n      runs( =>\n        makeComposer.call(@)\n        sendBtn = @composer.refs.sendActionButton\n        sendBtn.primarySend()\n        sendBtn.primarySend()\n        expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID, 'send')\n        expect(Actions.sendDraft.calls.length).toBe 1\n        # Delete the draft from _draftsSending so we can send it in other tests\n        delete DraftStore._draftsSending[DRAFT_CLIENT_ID]\n      )\n\n    describe \"when sending a message with keyboard inputs\", ->\n      beforeEach ->\n        sessionSetupComplete = false\n        useFullDraft.apply(@).then =>\n          makeComposer.call(@)\n          @$composer = @composer.refs.composerWrap\n          sessionSetupComplete = true\n          waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n\n      afterEach ->\n        # Delete the draft from _draftsSending so we can send it in other tests\n        delete DraftStore._draftsSending[DRAFT_CLIENT_ID]\n\n      it \"sends the draft on cmd-enter\", ->\n        ReactDOM.findDOMNode(@$composer).dispatchEvent(new CustomEvent('composer:send-message'))\n        expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID, 'send')\n        expect(Actions.sendDraft.calls.length).toBe 1\n\n      it \"doesn't let you send twice\", ->\n        ReactDOM.findDOMNode(@$composer).dispatchEvent(new CustomEvent('composer:send-message'))\n        expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID, 'send')\n        expect(Actions.sendDraft.calls.length).toBe 1\n        ReactDOM.findDOMNode(@$composer).dispatchEvent(new CustomEvent('composer:send-message'))\n        expect(Actions.sendDraft).toHaveBeenCalledWith(DRAFT_CLIENT_ID, 'send')\n        expect(Actions.sendDraft.calls.length).toBe 1\n\n  describe \"drag and drop\", ->\n    beforeEach ->\n      sessionSetupComplete = false\n      useDraft.call(@,\n        to: [u1]\n        subject: \"Hello World\"\n        body: \"\"\n        files: [f1])\n      .then( =>\n        makeComposer.call(@)\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n\n    describe \"_shouldAcceptDrop\", ->\n      it \"should return true if the event is carrying native files\", ->\n        event =\n          dataTransfer:\n            files:[{'pretend':'imafile'}]\n            types:[]\n        expect(@composer._shouldAcceptDrop(event)).toBe(true)\n\n      it \"should return true if the event is carrying a non-native file URL\", ->\n        event =\n          dataTransfer:\n            files:[]\n            types:['text/uri-list']\n        spyOn(@composer, '_nonNativeFilePathForDrop').andReturn(\"file://one-file\")\n\n        expect(@composer._shouldAcceptDrop(event)).toBe(true)\n        expect(@draft.files.length).toBe(1)\n\n      it \"should return false otherwise\", ->\n        event =\n          dataTransfer:\n            files:[]\n            types:['text/plain']\n        expect(@composer._shouldAcceptDrop(event)).toBe(false)\n\n    describe \"_nonNativeFilePathForDrop\", ->\n      it \"should return a path in the text/nylas-file-url data\", ->\n        event =\n          dataTransfer:\n            types: ['text/nylas-file-url']\n            getData: -> \"image/png:test.png:file:///Users/bengotow/Desktop/test.png\"\n        expect(@composer._nonNativeFilePathForDrop(event)).toBe(\"/Users/bengotow/Desktop/test.png\")\n\n      it \"should return a path in the text/uri-list data\", ->\n        event =\n          dataTransfer:\n            types: ['text/uri-list']\n            getData: -> \"file:///Users/bengotow/Desktop/test.png\"\n        expect(@composer._nonNativeFilePathForDrop(event)).toBe(\"/Users/bengotow/Desktop/test.png\")\n\n      it \"should return null otherwise\", ->\n        event =\n          dataTransfer:\n            types: ['text/plain']\n            getData: -> \"Hello world\"\n        expect(@composer._nonNativeFilePathForDrop(event)).toBe(null)\n\n      it \"should urldecode the contents of the text/uri-list field\", ->\n        event =\n          dataTransfer:\n            types: ['text/uri-list']\n            getData: -> \"file:///Users/bengotow/Desktop/Screen%20shot.png\"\n        expect(@composer._nonNativeFilePathForDrop(event)).toBe(\"/Users/bengotow/Desktop/Screen shot.png\")\n\n      it \"should return null if text/uri-list contains a non-file path\", ->\n        event =\n          dataTransfer:\n            types: ['text/uri-list']\n            getData: -> \"http://apple.com\"\n        expect(@composer._nonNativeFilePathForDrop(event)).toBe(null)\n\n      it \"should return null if text/nylas-file-url contains a non-file path\", ->\n        event =\n          dataTransfer:\n            types: ['text/nylas-file-url']\n            getData: -> \"application/json:filename.json:undefined\"\n        expect(@composer._nonNativeFilePathForDrop(event)).toBe(null)\n\n  describe \"A draft with files (attachments) and uploads\", ->\n    beforeEach ->\n      @file1 = new File\n        id: \"f_1\"\n        filename: \"f1.pdf\"\n        size: 1230\n\n      @file2 = new File\n        id: \"f_2\"\n        filename: \"f2.jpg\"\n        size: 4560\n\n      @file3 = new File\n        id: \"f_3\"\n        filename: \"f3.png\"\n        size: 7890\n\n      spyOn(Actions, \"fetchFile\")\n\n      sessionSetupComplete = false\n      useDraft.call(@, files: [@file1, @file2]).then( =>\n        makeComposer.call(@)\n        sessionSetupComplete = true\n      )\n      waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n\n    it 'starts fetching attached files', ->\n      waitsFor ->\n        Actions.fetchFile.callCount == 1\n      runs ->\n        expect(Actions.fetchFile).toHaveBeenCalled()\n        expect(Actions.fetchFile.calls.length).toBe(1)\n        expect(Actions.fetchFile.calls[0].args[0]).toBe @file2\n\n    it 'injects a MessageAttachments component for any present attachments', ->\n      els = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@composer, InjectedComponent, matching: {role: \"MessageAttachments\"})\n      expect(els.length).toBe 1\n      el = els[0]\n      expect(el.props.exposedProps.files).toEqual(@draft.files)\n\ndescribe \"when a file is received (via drag and drop or paste)\", ->\n  beforeEach ->\n    sessionSetupComplete = false\n    useDraft.call(@).then( =>\n      sessionSetupComplete = true\n    )\n    waitsFor(( => sessionSetupComplete), \"The session's draft needs to be set\", 500)\n    runs( =>\n      makeComposer.call(@)\n      @upload = {targetPath: 'a/f.txt', size: 1000, name: 'f.txt', id: 'f'}\n      spyOn(Actions, 'addAttachment').andCallFake ({filePath, messageClientId, onUploadCreated}) =>\n        @draft.uploads.push(@upload)\n        onUploadCreated(@upload)\n      spyOn(Actions, 'insertAttachmentIntoDraft')\n    )\n\n  it \"should call addAttachment with the path and clientId\", ->\n    @composer._onFileReceived('../../f.txt')\n    expect(Actions.addAttachment.callCount).toBe(1)\n    expect(Object.keys(Actions.addAttachment.calls[0].args[0])).toEqual([\n      'filePath', 'messageClientId', 'onUploadCreated',\n    ])\n\n  it \"should call insertAttachmentIntoDraft if the upload looks like an image\", ->\n    @upload = {targetPath: 'a/f.txt', size: 1000, name: 'f.txt', id: 'f'}\n    @composer._onFileReceived('../../f.txt')\n    advanceClock()\n    expect(Actions.insertAttachmentIntoDraft).not.toHaveBeenCalled()\n    expect(@upload.inline).not.toEqual(true)\n\n    @upload = {targetPath: 'a/f.png', size: 1000, name: 'f.png', id: 'g'}\n    expect(Utils.shouldDisplayAsImage(@upload)).toBe(true) # sanity check\n\n    @composer._onFileReceived('../../f.png')\n    advanceClock()\n    expect(Actions.insertAttachmentIntoDraft).toHaveBeenCalled()\n    expect(@upload.inline).toEqual(true)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/spec/quoted-text-spec.cjsx",
    "content": "# This tests just quoted text within a contenteditable.\n#\n# For a test of the basic component itself see\n# contenteditable-component-spec.cjsx\n#\n_ = require \"underscore\"\nReact = require \"react\"\nReactDOM = require 'react-dom'\nReactTestUtils = require('react-addons-test-utils')\n\nFields = require('../lib/fields').default\nComposer = require(\"../lib/composer-view\").default\nComposerEditor = require('../lib/composer-editor').default\n\n{Message, DraftStore, ComponentRegistry} = require 'nylas-exports'\n\ndescribe \"Composer Quoted Text\", ->\n  beforeEach ->\n    ComponentRegistry.register(ComposerEditor, role: \"Composer:Editor\")\n\n    @onChange = jasmine.createSpy('onChange')\n    @htmlNoQuote = 'Test <strong>HTML</strong><br>'\n    @htmlWithQuote = 'Test <strong>HTML</strong><div id=\"n1-quoted-text-marker\"></div><br><blockquote class=\"gmail_quote\">QUOTE</blockquote>'\n\n    @draft = new Message(draft: true, clientId: \"client-123\")\n    @session =\n      trigger: ->\n      changes:\n        add: jasmine.createSpy('changes.add')\n      draft: => @draft\n\n  afterEach ->\n    DraftStore._cleanupAllSessions()\n    ComposerEditor.containerRequired = undefined\n    ComponentRegistry.unregister(ComposerEditor)\n\n  # Must be called with the test's scope\n  setHTML = (newHTML) ->\n    @$contentEditable.innerHTML = newHTML\n    @contentEditable._onDOMMutated([\"mutated\"])\n\n  describe \"when the message is a reply\", ->\n    beforeEach ->\n      @draft.body = @htmlNoQuote\n      @composer = ReactTestUtils.renderIntoDocument(\n        <Composer draft={@draft} session={@session}/>\n      )\n      @composer.setState\n        showQuotedText: false\n        showQuotedTextControl: true\n      @contentEditable = @composer.refs[Fields.Body]\n      @$contentEditable = ReactDOM.findDOMNode(@contentEditable).querySelector('[contenteditable]')\n      @$composerBodyWrap = ReactDOM.findDOMNode(@composer.refs.composerBodyWrap)\n\n    it 'should render the quoted-text-control toggle', ->\n      toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@composer, 'quoted-text-control')\n      expect(toggles.length).toBe 1\n\n  describe 'when the quoted text has been expanded', ->\n    beforeEach ->\n      @draft.body = @htmlWithQuote\n      @composer = ReactTestUtils.renderIntoDocument(\n        <Composer draft={@draft} session={@session}/>\n      )\n      @composer.setState\n        showQuotedText: true\n        showQuotedTextControl: false\n      @contentEditable = @composer.refs[Fields.Body]\n      @$contentEditable = ReactDOM.findDOMNode(@contentEditable).querySelector('[contenteditable]')\n      @$composerBodyWrap = ReactDOM.findDOMNode(@composer.refs.composerBodyWrap)\n\n    it \"should call add changes with the entire HTML string\", ->\n      textToAdd = \"MORE <strong>TEXT</strong>!\"\n      expect(@$contentEditable.innerHTML).toBe @htmlWithQuote\n      setHTML.call(@, textToAdd + @htmlWithQuote)\n      ev = @session.changes.add.mostRecentCall.args[0].body\n      expect(ev).toEqual(textToAdd + @htmlWithQuote)\n\n    it \"should allow the quoted text to be changed\", ->\n      newText = 'Test <strong>NEW 1 HTML</strong><blockquote class=\"gmail_quote\">QUOTE CHANGED!!!</blockquote>'\n      expect(@$contentEditable.innerHTML).toBe @htmlWithQuote\n      setHTML.call(@, newText)\n      ev = @session.changes.add.mostRecentCall.args[0].body\n      expect(ev).toEqual(newText)\n\n    describe 'quoted text control toggle button', ->\n      it 'should not be rendered', ->\n        toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@composer, 'quoted-text-control')\n        expect(toggles.length).toBe(0)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/spec/send-action-button-spec.jsx",
    "content": "import React from 'react';\nimport {mount} from 'enzyme';\nimport {ButtonDropdown, RetinaImg} from 'nylas-component-kit';\nimport {Actions, Message, SendActionsStore} from 'nylas-exports';\nimport SendActionButton from '../lib/send-action-button';\n\nconst {UndecoratedSendActionButton} = SendActionButton;\n\nconst {DefaultSendAction} = SendActionsStore\n\nconst GoodSendAction = {\n  title: \"Good Send Action\",\n  configKey: 'good-send-action',\n  isAvailableForDraft: () => true,\n  performSendAction: () => {},\n}\n\nconst SecondSendAction = {\n  title: \"Second Send Action\",\n  configKey: 'second-send-action',\n  isAvailableForDraft: () => true,\n  performSendAction: () => {},\n}\n\nconst NoIconUrl = {\n  title: \"No Icon\",\n  configKey: 'no-icon',\n  iconUrl: null,\n  isAvailableForDraft: () => true,\n  performSendAction() {},\n}\n\ndescribe('SendActionButton', function describeBlock() {\n  beforeEach(() => {\n    spyOn(NylasEnv, 'reportError')\n    spyOn(Actions, 'sendDraft')\n    this.isValidDraft = jasmine.createSpy('isValidDraft')\n    this.clientId = \"client-23\"\n    this.draft = new Message({clientId: this.clientId, draft: true})\n  })\n\n  const render = (draft, {isValid = true, sendActions = [], ordered = {}} = {}) => {\n    this.isValidDraft.andReturn(isValid)\n    return mount(\n      <UndecoratedSendActionButton\n        draft={draft}\n        isValidDraft={this.isValidDraft}\n        sendActions={[DefaultSendAction].concat(sendActions)}\n        orderedSendActions={{\n          preferred: ordered.preferred || DefaultSendAction,\n          rest: ordered.rest || [],\n        }}\n      />\n    )\n  }\n\n  it(\"renders without error\", () => {\n    const sendActionButton = render(this.draft);\n    expect(sendActionButton.is(UndecoratedSendActionButton)).toBe(true);\n  });\n\n  it(\"initializes with the default and shows the standard Send option\", () => {\n    const sendActionButton = render(this.draft);\n    const button = sendActionButton.find('button').first();\n    expect(button.text()).toEqual('Send');\n  });\n\n  it(\"is a single button when there are no send actions\", () => {\n    const sendActionButton = render(this.draft, {sendActions: []});\n    const dropdowns = sendActionButton.find(ButtonDropdown);\n    const buttons = sendActionButton.find('button');\n    expect(buttons.length).toBe(1);\n    expect(dropdowns.length).toBe(0);\n    expect(buttons.first().text()).toBe('Send');\n  });\n\n  it(\"is a dropdown when there's more than one send action\", () => {\n    const sendActionButton = render(this.draft, {\n      sendActions: [GoodSendAction],\n    });\n    const dropdowns = sendActionButton.find(ButtonDropdown);\n    const buttons = sendActionButton.find('button');\n    expect(buttons.length).toBe(0);\n    expect(dropdowns.length).toBe(1);\n    expect(dropdowns.first().prop('primaryTitle')).toBe('Send');\n  });\n\n  it(\"has the correct primary item\", () => {\n    const sendActionButton = render(this.draft, {\n      sendActions: [GoodSendAction, SecondSendAction],\n      ordered: {preferred: SecondSendAction, rest: [DefaultSendAction, GoodSendAction]},\n    });\n    const dropdown = sendActionButton.find(ButtonDropdown).first();\n    expect(dropdown.prop('primaryTitle')).toBe(\"Second Send Action\");\n  });\n\n  it(\"still renders with a null iconUrl and doesn't show the image\", () => {\n    const sendActionButton = render(this.draft, {\n      sendActions: [NoIconUrl],\n      ordered: {preferred: NoIconUrl, rest: [DefaultSendAction]},\n    });\n    const dropdowns = sendActionButton.find(ButtonDropdown);\n    const buttons = sendActionButton.find('button');\n    const icons = sendActionButton.find(RetinaImg)\n    expect(buttons.length).toBe(0);\n    expect(dropdowns.length).toBe(1);\n    expect(icons.length).toBe(3);\n  });\n\n  it(\"sends a draft by default if no extra actions present\", () => {\n    const sendActionButton = render(this.draft);\n    const button = sendActionButton.find('button').first();\n    button.simulate('click')\n    expect(this.isValidDraft).toHaveBeenCalled();\n    expect(Actions.sendDraft).toHaveBeenCalledWith(this.draft.clientId, 'send');\n  });\n\n  it(\"doesn't send a draft if the isValidDraft fails\", () => {\n    const sendActionButton = render(this.draft, {isValid: false});\n    const button = sendActionButton.find('button').first();\n    button.simulate('click')\n    expect(this.isValidDraft).toHaveBeenCalled();\n    expect(Actions.sendDraft).not.toHaveBeenCalled();\n  });\n\n  it(\"does the preferred action when more than one action present\", () => {\n    const sendActionButton = render(this.draft, {\n      sendActions: [GoodSendAction],\n      ordered: {preferred: GoodSendAction, rest: [DefaultSendAction]},\n    });\n    const button = sendActionButton.find('.primary-item').first();\n    button.simulate('click')\n    expect(this.isValidDraft).toHaveBeenCalled();\n    expect(Actions.sendDraft).toHaveBeenCalledWith(this.draft.clientId, 'good-send-action');\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer/stylesheets/composer.less",
    "content": "// The ui-variables file is provided by base themes provided by N1.\n@import \"ui-variables\";\n@import \"ui-mixins\";\n@import \"buttons\";\n\n@compose-width: 800px;\n@compose-min-height: 70px;\n\n@blurred-primary-color: mix(@background-primary, #ffbb00, 96%);\n@blurred-off-primary-color: mix(@background-off-primary, #ffbb00, 96%);\n\nbody.platform-win32 {\n  .composer-inner-wrap {\n    .composer-drop-cover {\n      border-radius: 0;\n    }\n    .composer-action-bar-wrap {\n      border-radius: 0;\n    }\n    input, input:focus {\n      box-shadow: none;\n    }\n  }\n}\n\n.action-bar-cover-gen() {\n  .action-bar-cover {\n    background-image: -webkit-linear-gradient(left, fade(@action-bar-bg, 0) 0%,\n    @action-bar-bg 10%);\n  }\n}\n\n// Used to allow the click targets to extend into the margins\n.composer-field-bottom-border() {\n  width: calc(~\"100% - 44px\");\n  height: 1px;\n  position: absolute;\n  bottom: 0;\n  left: 23px;\n  content: \" \";\n  background: @border-color-divider;\n}\n\n.composer-inner-wrap {\n  position: relative;\n  height: 100%;\n  display: flex;\n  flex-direction: column;\n\n  .composer-drop-cover {\n    position: absolute;\n    top: 0; right: 0; bottom: 0; left: 0;\n    z-index: 1000;\n    background: rgba(255,255,255,0.7);\n    border-radius: @border-radius-base;\n    border: 4px dashed lighten(@gray, 30%);\n    text-align: center;\n    line-height:2.3em;\n    pointer-events: none;\n\n    .centered {\n      position: absolute;\n      left: 50%;\n      top: 50%;\n      transform: translate(-50%, -50%);\n      color: lighten(@gray, 20%);\n      font-weight: 500;\n      font-size:1.1em;\n      img { margin: auto; display:block; margin-bottom:20px; background-color: lighten(@gray, 20%); }\n    }\n  }\n\n  .inline-image-upload-container {\n    display: inline-block;\n\n    .nylas-attachment-item.image-attachment-item {\n      margin: 0;\n      max-width: 100%;\n      min-width: 0;\n    }\n  }\n\n  .composer-action-bar-wrap {\n    @action-bar-bg: @background-off-primary;\n    position: relative;\n    width: 100%;\n    background: @action-bar-bg;\n    border-top: 1px solid darken(@background-off-primary, 7%);\n    box-shadow: inset 0 2px 1px rgba(0,0,0,0.03);\n    border-bottom: 0;\n    border-radius: @border-radius-base;\n\n    .action-bar-cover-gen;\n\n    // Buttons in the composer footer\n    .btn.btn-toolbar:not(.btn-emphasis) {\n      background: transparent;\n      box-shadow: 0 0 0;\n      margin: 0;\n      padding: 0 9px;\n      white-space: nowrap;\n\n      img.content-mask {\n        background-color: fadeout(mix(@btn-default-text-color, @component-active-color, 88%), 30%);\n      }\n      &:hover {\n        img.content-mask {\n          background-color: fadeout(mix(@btn-default-text-color, @component-active-color, 88%), 5%);\n        }\n      }\n      &.btn-enabled {\n        color: @component-active-color;\n        img.content-mask {\n          background-color: @component-active-color;\n        }\n      }\n    }\n\n    .btn-send {\n      margin-right: 10px;\n      white-space: nowrap;\n    }\n\n    .composer-action-bar-content {\n      display:flex;\n      margin: 0 auto;\n      flex-direction:row;\n      max-width: @compose-width;\n      padding: 9px 22.5px;\n\n      .composer-action-bar-plugins {\n        flex-wrap: wrap;\n      }\n    }\n\n    .action-bar-animation-wrap {\n      position: relative;\n      overflow: hidden;\n\n      .composer-action-bar-plugins {\n        opacity: 0;\n        transition: opacity 30ms;\n      }\n\n      .action-bar-cover {\n        transition: left 200ms ease-out;\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        z-index: 2;\n      }\n\n      &.plugins-loaded {\n        .composer-action-bar-plugins {\n          opacity: 1;\n        }\n        .action-bar-cover {\n          left: 100%;\n        }\n      }\n    }\n  }\n\n  .composer-content-wrap {\n    padding: 0;\n    flex: 1;\n    display: flex;\n    position: relative;\n    flex-flow: column;\n  }\n\n  .composer-centered {\n    display:flex;\n    position: relative;\n    flex-direction: column;\n    flex: 1;\n    width: 100%;\n    max-width: @compose-width;\n    margin: 0 auto;\n    padding-top: @spacing-standard * 0.7;\n  }\n  .text-actions {\n    text-align: right;\n    line-height: 1.4;\n    position: relative;\n    top: -3px;\n  }\n\n  .composer-body-wrap {\n    padding: 0 0 5px 0;\n  }\n\n  .composer-header-actions {\n    position: relative;\n    float: right;\n    z-index: 2;\n    cursor: default;\n    padding-right: @spacing-standard + @spacing-half;\n    padding-top: 12px;\n\n    .action {\n      color: @text-color-very-subtle;\n      img.content-mask { background-color: @text-color-very-subtle; }\n      font-size: @font-size-small;\n      padding: 10px 6px;\n      &:first-child {\n        padding-left: @spacing-standard;\n      }\n      &:last-child {\n        padding-right: 0;\n      }\n      &:hover {\n        color: @text-color-link;\n        img.content-mask { background-color: @text-color-link; }\n        cursor: default;\n      }\n    }\n  }\n\n  input, textarea {\n    color: @text-color;\n    position: relative;\n    display: block;\n    background: inherit;\n    width: 100%;\n    resize: none;\n    border: none;\n  }\n\n  .composer-field-label {\n    color: @text-color-very-subtle;\n    float: left;\n    padding-top: 13px;\n    display: block;\n    &:hover {\n      cursor: default;\n    }\n  }\n\n  .collapsed-composer-participants {\n    position: relative;\n    margin: 0 23px;\n    border-bottom: 1px solid @border-color-divider;\n    flex-shrink:0;\n    color: @text-color-very-subtle;\n    padding: 10px 0 10px 0;\n\n    .collapsed-contact {\n      padding-right: 0.25em;\n      color: @text-color;\n      &:after {\n        content: \",\"\n      }\n      &:last-child:after {\n        content: \"\"\n      }\n    }\n\n    .num-remaining.token {\n      color: @text-color;\n      padding-right: 12px;\n      margin-left: 0;\n      margin-top: 2px;\n    }\n\n    .num-remaining-wrap {\n      position: absolute;\n      right: -3px;\n      z-index: 2;\n      top: 6px;\n      .show-more-fade {\n        position: absolute;\n        width: 190px;\n        height: 32px;\n        right: 0;\n        top: 0;\n        background: linear-gradient(to right, fade(@background-primary, 0%) 0%, fade(@background-primary, 100%) 40%);\n      }\n    }\n  }\n\n  .composer-subject {\n    position: relative;\n    margin: 0;\n    flex-shrink:0;\n\n    &:after {\n      .composer-field-bottom-border;\n    }\n\n    // for Mail Merge\n    div[contenteditable] {\n      padding-left: 22px;\n      padding-right: 22px;\n    }\n\n    input {\n      display: inline-block;\n      padding: 13px 22px 9px 22px;\n      min-width: 5em;\n      background-color: transparent;\n      border: none;\n      margin: 0;\n      &::-webkit-input-placeholder {\n        color: @text-color-very-subtle;\n      }\n      &:focus {\n        box-shadow: none;\n      }\n    }\n  }\n\n  .compose-body-scroll {\n    position:initial;\n    .scroll-region-content .scroll-region-content-inner {\n      min-height: 100%;\n      display: flex;\n    }\n  }\n\n  .compose-body {\n    flex: 1;\n    cursor: text;\n    overflow-x: auto;\n    position: relative;\n    margin: 0;\n\n    .quoted-text-control {\n      position: relative;\n      // The quoted-text-control has no top margin since the\n      // div[contentedtiable] has 10px of bottom padding. It's better to\n      // put the padding on the contenteditable so the bottom looks nice\n      // in popout windows when there's no quoted text control.\n      margin: 0 @spacing-standard @spacing-standard 22px;\n\n      .remove-quoted-text {\n        display: none;\n        cursor: pointer;\n        position: absolute;\n        z-index: 2;\n        right: -6px;\n        top: -6px;\n        border-radius: 0 0 0 3px;\n        &:active {\n          background: none;\n          -webkit-filter: brightness(95%);\n        }\n        img {\n          height: 24px;\n        }\n      }\n\n      &:hover .remove-quoted-text {\n        display: block;\n      }\n    }\n\n    // All of the padding is placed on the contenteditable itself so\n    // clicks on the margins register within the region.\n    div[contenteditable] {\n      padding: 20px 22px 0 22px;\n      min-height: @compose-min-height;\n    }\n  }\n\n  .composer-footer-region {\n    cursor: default;\n    &:hover {\n      cursor: default;\n    }\n  }\n\n  // TODO FIXME DRY From stylesheets/message-list.less\n  .attachments-area {\n    padding: 0;\n    margin: 0 8px;\n  }\n}\n\n// Overrides for the full-window popout composer\n.composer-full-window {\n  width: 100%;\n  height: 100%;\n\n  .composer-outer-wrap {\n    width: 100%;\n    height: 100%;\n  }\n\n  .composer-inner-wrap {\n\n    .composer-action-bar-wrap {\n      @action-bar-bg: darken(@background-primary, 1%);\n      background: @action-bar-bg;\n      border-top: 1px solid darken(@background-primary, 8%);\n      box-shadow: inset 0 1px 2px rgba(0,0,0,0.03);\n      .action-bar-cover-gen;\n    }\n\n    .composer-action-bar-content {\n      padding: 9px 13.5px 9px 22.5px;\n    }\n\n    .compose-body {\n      margin-bottom: 0;\n      position: relative;\n    }\n\n  }\n\n  .compose-body {\n    div[contenteditable] {\n      min-height: @line-height-computed;\n    }\n  }\n}\n\n// Overrides for the composer in a message-list\n#message-list {\n  .message-item-wrap {\n    .message-item-white-wrap.composer-outer-wrap {\n      background: @blurred-primary-color;\n\n      .btn.btn-toolbar.btn-trash {\n        padding-right: 0;\n      }\n      .show-more-fade {\n        background: linear-gradient(to right, fade(@blurred-primary-color, 0%) 0%, fade(@blurred-primary-color, 100%) 40%);\n      }\n      .composer-action-bar-wrap {\n        @action-bar-bg: @blurred-off-primary-color;\n        background: @action-bar-bg;\n        .action-bar-cover-gen;\n      }\n    }\n    .message-item-white-wrap.composer-outer-wrap.focused {\n      box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.28), 0 1px 1.5px rgba(0, 0, 0, 0.08), 0 0 3px @accent-primary;\n      background-color: @background-primary;\n      .show-more-fade {\n        background: linear-gradient(to right, fade(@background-primary, 0%) 0%, fade(@background-primary, 100%) 40%);\n      }\n      .composer-action-bar-wrap {\n        @action-bar-bg: @background-off-primary;\n        background: @action-bar-bg;\n        .action-bar-cover-gen;\n      }\n    }\n  }\n}\n\n\n//////////////////////////////////\n// participants-text-field.cjsx //\n//////////////////////////////////\n.composer-participant-field {\n  position: relative;\n  margin: 0;\n  flex-shrink: 0;\n  min-height: 46px;\n  color: @text-color;\n\n  .tokenizing-field-wrap {\n    padding: 0 22px;\n  }\n  .content-container {\n    margin-left: 22px;\n    width: calc(~\"100% - 44px\");\n  }\n  .button-dropdown {\n    .content-container {\n      margin-left: 0;\n      width: 100%;\n    }\n  }\n  &:after {\n    .composer-field-bottom-border;\n  }\n\n  &.from-field {\n    padding: 0 22px;\n  }\n\n  .from-single-name {\n    &:hover {\n      cursor: default;\n    }\n  }\n\n  .button-dropdown {\n    margin-left: 10px;\n    padding-top: 11px;\n    vertical-align: -webkit-baseline-middle;\n\n    .primary-item, .only-item {\n      line-height: 2em;\n    }\n    &:hover {\n      .primary-item,\n      .only-item {\n        border-radius: @border-radius-base;\n      }\n    }\n    .secondary-items {\n      border-radius: @border-radius-base;\n    }\n    .item {\n      .contact.is-alias {\n        font-style: italic;\n        padding-left: @padding-small-horizontal;\n        border-left: 2px solid @border-color-divider;\n        margin-left: 2px;\n      }\n    }\n  }\n\n  .participant {\n    display: flex;\n    align-items: center;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    overflow: hidden;\n\n    .participant-primary {\n      font-weight: @font-weight-semi-bold;\n    }\n    .participant-secondary {\n      color: @text-color-very-subtle;\n    }\n  }\n\n  .tokenizing-field-input {\n    margin-left: 2.8em;\n  }\n}\n\n\n//////////////////////////////////\n//      Overlaid Components     //\n//////////////////////////////////\n.overlaid-components {\n  position: absolute;\n  top: 0;\n  left: 0;\n  width: 100%;\n  height: 0;\n  line-height: 1.4; // must match contenteditable container\n  overflow: visible;\n  z-index: 1;\n}\nimg.n1-overlaid-component-anchor-container {\n  border: 0;\n  vertical-align: baseline;\n}\n.toggle-preview {\n  position: absolute;\n  top: 5px;\n  right: 20px;\n  font-size: @font-size-smaller;\n  opacity: 0.3;\n  cursor: pointer;\n  &:hover {\n    cursor: pointer;\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/categorized-emoji.es6",
    "content": "const categorizedEmojiList = {\n  'People': [\n    'grinning',\n    'grimacing',\n    'grin',\n    'joy',\n    'smiley',\n    'smile',\n    'sweat_smile',\n    'laughing',\n    'innocent',\n    'wink',\n    'blush',\n    'slightly_smiling_face',\n    'upside_down_face',\n    'relaxed',\n    'yum',\n    'relieved',\n    'heart_eyes',\n    'kissing_heart',\n    'kissing',\n    'kissing_smiling_eyes',\n    'kissing_closed_eyes',\n    'stuck_out_tongue_winking_eye',\n    'stuck_out_tongue_closed_eyes',\n    'stuck_out_tongue',\n    'money_mouth_face',\n    'nerd_face',\n    'sunglasses',\n    'hugging_face',\n    'smirk',\n    'no_mouth',\n    'neutral_face',\n    'expressionless',\n    'unamused',\n    'face_with_rolling_eyes',\n    'thinking_face',\n    'flushed',\n    'disappointed',\n    'worried',\n    'angry',\n    'rage',\n    'pensive',\n    'confused',\n    'slightly_frowning_face',\n    'white_frowning_face',\n    'persevere',\n    'confounded',\n    'tired_face',\n    'weary',\n    'triumph',\n    'open_mouth',\n    'scream',\n    'fearful',\n    'cold_sweat',\n    'hushed',\n    'frowning',\n    'anguished',\n    'cry',\n    'disappointed_relieved',\n    'sleepy',\n    'sweat',\n    'sob',\n    'dizzy_face',\n    'astonished',\n    'zipper_mouth_face',\n    'mask',\n    'face_with_thermometer',\n    'face_with_head_bandage',\n    'sleeping',\n    'zzz',\n    'poop',\n    'smiling_imp',\n    'imp',\n    'japanese_ogre',\n    'japanese_goblin',\n    'skull',\n    'ghost',\n    'alien',\n    'robot_face',\n    'smiley_cat',\n    'smile_cat',\n    'joy_cat',\n    'heart_eyes_cat',\n    'smirk_cat',\n    'kissing_cat',\n    'scream_cat',\n    'crying_cat_face',\n    'pouting_cat',\n    'raised_hands',\n    'clap',\n    'wave',\n    'thumbsup',\n    'thumbsdown',\n    'punch',\n    'fist',\n    'v',\n    'ok_hand',\n    'hand',\n    'open_hands',\n    'muscle',\n    'pray',\n    'point_up',\n    'point_up_2',\n    'point_down',\n    'point_left',\n    'point_right',\n    'middle_finger',\n    'raised_hand_with_fingers_splayed',\n    'the_horns',\n    'spock-hand',\n    'writing_hand',\n    'nail_care',\n    'lips',\n    'tongue',\n    'ear',\n    'nose',\n    'eye',\n    'eyes',\n    'bust_in_silhouette',\n    'busts_in_silhouette',\n    'speaking_head_in_silhouette',\n    'baby',\n    'boy',\n    'girl',\n    'man',\n    'woman',\n    'person_with_blond_hair',\n    'older_man',\n    'older_woman',\n    'man_with_gua_pi_mao',\n    'man_with_turban',\n    'cop',\n    'construction_worker',\n    'guardsman',\n    'sleuth_or_spy',\n    'santa',\n    'angel',\n    'princess',\n    'bride_with_veil',\n    'walking',\n    'running',\n    'dancer',\n    'dancers',\n    'couple',\n    'two_men_holding_hands',\n    'two_women_holding_hands',\n    'bow',\n    'information_desk_person',\n    'no_good',\n    'ok_woman',\n    'raising_hand',\n    'person_with_pouting_face',\n    'person_frowning',\n    'haircut',\n    'massage',\n    'couple_with_heart',\n    'woman-heart-woman',\n    'man-heart-man',\n    'couplekiss',\n    'woman-kiss-woman',\n    'man-kiss-man',\n    'family',\n    'man-woman-girl',\n    'man-woman-girl-boy',\n    'man-woman-boy-boy',\n    'man-woman-girl-girl',\n    'woman-woman-boy',\n    'woman-woman-girl',\n    'woman-woman-girl-boy',\n    'woman-woman-boy-boy',\n    'woman-woman-girl-girl',\n    'man-man-boy',\n    'man-man-girl',\n    'man-man-girl-boy',\n    'man-man-boy-boy',\n    'man-man-girl-girl',\n    'womans_clothes',\n    'shirt',\n    'jeans',\n    'necktie',\n    'dress',\n    'bikini',\n    'kimono',\n    'lipstick',\n    'kiss',\n    'footprints',\n    'high_heel',\n    'sandal',\n    'boot',\n    'mans_shoe',\n    'athletic_shoe',\n    'womans_hat',\n    'tophat',\n    'helmet_with_white_cross',\n    'mortar_board',\n    'crown',\n    'school_satchel',\n    'pouch',\n    'purse',\n    'handbag',\n    'briefcase',\n    'eyeglasses',\n    'dark_sunglasses',\n    'ring',\n    'closed_umbrella',\n  ],\n  'Nature': [\n    'dog',\n    'cat',\n    'mouse',\n    'hamster',\n    'rabbit',\n    'bear',\n    'panda_face',\n    'koala',\n    'tiger',\n    'lion_face',\n    'cow',\n    'pig',\n    'pig_nose',\n    'frog',\n    'octopus',\n    'monkey_face',\n    'see_no_evil',\n    'hear_no_evil',\n    'speak_no_evil',\n    'monkey',\n    'chicken',\n    'penguin',\n    'bird',\n    'baby_chick',\n    'hatching_chick',\n    'hatched_chick',\n    'wolf',\n    'boar',\n    'horse',\n    'unicorn_face',\n    'bee',\n    'bug',\n    'snail',\n    'beetle',\n    'ant',\n    'spider',\n    'scorpion',\n    'crab',\n    'snake',\n    'turtle',\n    'tropical_fish',\n    'fish',\n    'blowfish',\n    'dolphin',\n    'whale',\n    'whale2',\n    'crocodile',\n    'leopard',\n    'tiger2',\n    'water_buffalo',\n    'ox',\n    'cow2',\n    'dromedary_camel',\n    'camel',\n    'elephant',\n    'goat',\n    'ram',\n    'sheep',\n    'racehorse',\n    'pig2',\n    'rat',\n    'mouse2',\n    'rooster',\n    'turkey',\n    'dove_of_peace',\n    'dog2',\n    'poodle',\n    'cat2',\n    'rabbit2',\n    'chipmunk',\n    'paw_prints',\n    'dragon',\n    'dragon_face',\n    'cactus',\n    'christmas_tree',\n    'evergreen_tree',\n    'deciduous_tree',\n    'palm_tree',\n    'seedling',\n    'herb',\n    'shamrock',\n    'four_leaf_clover',\n    'bamboo',\n    'tanabata_tree',\n    'leaves',\n    'fallen_leaf',\n    'maple_leaf',\n    'ear_of_rice',\n    'hibiscus',\n    'sunflower',\n    'rose',\n    'tulip',\n    'blossom',\n    'cherry_blossom',\n    'bouquet',\n    'mushroom',\n    'chestnut',\n    'jack_o_lantern',\n    'shell',\n    'spider_web',\n    'earth_americas',\n    'earth_africa',\n    'earth_asia',\n    'full_moon',\n    'waning_gibbous_moon',\n    'last_quarter_moon',\n    'waning_crescent_moon',\n    'new_moon',\n    'waxing_crescent_moon',\n    'first_quarter_moon',\n    'moon',\n    'new_moon_with_face',\n    'full_moon_with_face',\n    'first_quarter_moon_with_face',\n    'last_quarter_moon_with_face',\n    'sun_with_face',\n    'crescent_moon',\n    'star',\n    'star2',\n    'dizzy',\n    'sparkles',\n    'comet',\n    'sunny',\n    'mostly_sunny',\n    'partly_sunny',\n    'barely_sunny',\n    'partly_sunny_rain',\n    'cloud',\n    'rain_cloud',\n    'thunder_cloud_and_rain',\n    'lightning',\n    'zap',\n    'fire',\n    'boom',\n    'snowflake',\n    'snow_cloud',\n    'showman',\n    'snowman',\n    'wind_blowing_face',\n    'dash',\n    'tornado',\n    'fog',\n    'umbrella',\n    'droplet',\n    'sweat_drops',\n    'ocean',\n  ],\n  'Food and Drink': [\n    'green_apple',\n    'apple',\n    'pear',\n    'tangerine',\n    'lemon',\n    'banana',\n    'watermelon',\n    'grapes',\n    'strawberry',\n    'melon',\n    'cherries',\n    'peach',\n    'pineapple',\n    'tomato',\n    'eggplant',\n    'hot_pepper',\n    'corn',\n    'sweet_potato',\n    'honey_pot',\n    'bread',\n    'cheese_wedge',\n    'poultry_leg',\n    'meat_on_bone',\n    'fried_shrimp',\n    'egg',\n    'hamburger',\n    'fries',\n    'hotdog',\n    'pizza',\n    'spaghetti',\n    'taco',\n    'burrito',\n    'ramen',\n    'stew',\n    'fish_cake',\n    'sushi',\n    'bento',\n    'curry',\n    'rice_ball',\n    'rice',\n    'rice_cracker',\n    'oden',\n    'dango',\n    'shaved_ice',\n    'ice_cream',\n    'icecream',\n    'cake',\n    'birthday',\n    'custard',\n    'candy',\n    'lollipop',\n    'chocolate_bar',\n    'popcorn',\n    'doughnut',\n    'cookie',\n    'beer',\n    'beers',\n    'wine_glass',\n    'cocktail',\n    'tropical_drink',\n    'champagne',\n    'sake',\n    'tea',\n    'coffee',\n    'baby_bottle',\n    'fork_and_knife',\n    'knife_fork_plate',\n  ],\n  'Activity': [\n    'soccer',\n    'basketball',\n    'football',\n    'baseball',\n    'tennis',\n    'volleyball',\n    'rugby_football',\n    '8ball',\n    'golf',\n    'golfer',\n    'table_tennis_paddle_and_ball',\n    'badminton_racquet_and_shuttlecock',\n    'ice_hockey_stick_and_puck',\n    'field_hockey_stick_and_ball',\n    'cricket_bat_and_ball',\n    'ski',\n    'skier',\n    'snowboarder',\n    'ice_skate',\n    'bow_and_arrow',\n    'fishing_pole_and_fish',\n    'rowboat',\n    'swimmer',\n    'surfer',\n    'bath',\n    'person_with_ball',\n    'weight_lifter',\n    'bicyclist',\n    'mountain_bicyclist',\n    'horse_racing',\n    'man_in_business_suit_levitating',\n    'trophy',\n    'running_shirt_with_sash',\n    'sports_medal',\n    'medal',\n    'reminder_ribbon',\n    'rosette',\n    'ticket',\n    'admission_tickets',\n    'performing_arts',\n    'art',\n    'circus_tent',\n    'microphone',\n    'headphones',\n    'musical_score',\n    'musical_keyboard',\n    'saxophone',\n    'trumpet',\n    'guitar',\n    'violin',\n    'clapper',\n    'video_game',\n    'space_invader',\n    'dart',\n    'game_die',\n    'slot_machine',\n    'bowling',\n  ],\n  'Travel and Places': [\n    'car',\n    'taxi',\n    'blue_car',\n    'bus',\n    'trolleybus',\n    'racing_car',\n    'police_car',\n    'ambulance',\n    'fire_engine',\n    'minibus',\n    'truck',\n    'articulated_lorry',\n    'tractor',\n    'racing_motorcycle',\n    'bike',\n    'rotating_light',\n    'oncoming_police_car',\n    'oncoming_bus',\n    'oncoming_automobile',\n    'oncoming_taxi',\n    'aerial_tramway',\n    'mountain_cableway',\n    'suspension_railway',\n    'railway_car',\n    'train',\n    'monorail',\n    'bullettrain_side',\n    'bullettrain_front',\n    'light_rail',\n    'mountain_railway',\n    'steam_locomotive',\n    'train2',\n    'metro',\n    'tram',\n    'station',\n    'helicopter',\n    'small_airplane',\n    'airplane',\n    'airplane_departure',\n    'airplane_arriving',\n    'boat',\n    'motor_boat',\n    'speedboat',\n    'ferry',\n    'passenger_ship',\n    'rocket',\n    'satellite',\n    'seat',\n    'anchor',\n    'construction',\n    'fuelpump',\n    'busstop',\n    'vertical_traffic_light',\n    'traffic_light',\n    'checkered_flag',\n    'ship',\n    'ferris_wheel',\n    'roller_coaster',\n    'carousel_horse',\n    'building_construction',\n    'foggy',\n    'tokyo_tower',\n    'factory',\n    'fountain',\n    'rice_scene',\n    'mountain',\n    'snow_capped_mountain',\n    'mount_fuji',\n    'volcano',\n    'japan',\n    'camping',\n    'tent',\n    'national_park',\n    'motorway',\n    'railway_track',\n    'sunrise',\n    'sunrise_over_mountains',\n    'desert',\n    'beach_with_umbrella',\n    'desert_island',\n    'city_sunrise',\n    'city_sunset',\n    'cityscape',\n    'night_with_stars',\n    'bridge_at_night',\n    'milky_way',\n    'stars',\n    'sparkler',\n    'fireworks',\n    'rainbow',\n    'house_buildings',\n    'european_castle',\n    'japanese_castle',\n    'stadium',\n    'statue_of_liberty',\n    'house',\n    'house_with_garden',\n    'derelict_house_building',\n    'office',\n    'department_store',\n    'post_office',\n    'european_post_office',\n    'hospital',\n    'bank',\n    'hotel',\n    'convenience_store',\n    'school',\n    'love_hotel',\n    'wedding',\n    'classical_building',\n    'church',\n    'mosque',\n    'synagogue',\n    'kaaba',\n    'shinto_shrine',\n  ],\n  'Objects': [\n    'watch',\n    'iphone',\n    'calling',\n    'computer',\n    'keyboard',\n    'desktop_computer',\n    'printer',\n    'three_button_mouse',\n    'trackball',\n    'joystick',\n    'compression',\n    'minidisc',\n    'floppy_disk',\n    'cd',\n    'dvd',\n    'vhs',\n    'camera',\n    'camera_with_flash',\n    'video_camera',\n    'movie_camera',\n    'film_projector',\n    'film_frames',\n    'telephone_receiver',\n    'phone',\n    'pager',\n    'fax',\n    'tv',\n    'radio',\n    'studio_microphone',\n    'level_slider',\n    'control_knobs',\n    'stopwatch',\n    'timer_clock',\n    'alarm_clock',\n    'mantelpiece_clock',\n    'hourglass_flowing_sand',\n    'hourglass',\n    'battery',\n    'electric_plug',\n    'bulb',\n    'flashlight',\n    'candle',\n    'wastebasket',\n    'oil_drum',\n    'money_with_wings',\n    'dollar',\n    'yen',\n    'euro',\n    'pound',\n    'moneybag',\n    'credit_card',\n    'gem',\n    'scales',\n    'wrench',\n    'hammer',\n    'hammer_and_pick',\n    'hammer_and_wrench',\n    'pick',\n    'nut_and_bolt',\n    'gear',\n    'chains',\n    'gun',\n    'bomb',\n    'knife',\n    'dagger_knife',\n    'crossed_swords',\n    'shield',\n    'smoking',\n    'skull_and_crossbones',\n    'coffin',\n    'funeral_urn',\n    'amphora',\n    'crystal_ball',\n    'prayer_beads',\n    'barber',\n    'alembic',\n    'telescope',\n    'microscope',\n    'hole',\n    'pill',\n    'syringe',\n    'thermometer',\n    'label',\n    'bookmark',\n    'toilet',\n    'shower',\n    'bathtub',\n    'key',\n    'old_key',\n    'couch_and_lamp',\n    'sleeping_accommodation',\n    'bed',\n    'door',\n    'bellhop_bell',\n    'frame_with_picture',\n    'world_map',\n    'umbrella_on_ground',\n    'moyai',\n    'shopping_bags',\n    'balloon',\n    'flags',\n    'ribbon',\n    'gift',\n    'confetti_ball',\n    'tada',\n    'dolls',\n    'wind_chime',\n    'crossed_flags',\n    'izakaya_lantern',\n    'envelope',\n    'envelope_with_arrow',\n    'incoming_envelope',\n    'e-mail',\n    'love_letter',\n    'postbox',\n    'mailbox_closed',\n    'mailbox',\n    'mailbox_with_mail',\n    'mailbox_with_no_mail',\n    'package',\n    'postal_horn',\n    'inbox_tray',\n    'outbox_tray',\n    'scroll',\n    'page_with_curl',\n    'bookmark_tabs',\n    'bar_chart',\n    'chart_with_upwards_trend',\n    'chart_with_downwards_trend',\n    'page_facing_up',\n    'date',\n    'calendar',\n    'spiral_calendar_pad',\n    'card_index',\n    'card_file_box',\n    'ballot_box_with_ballot',\n    'file_cabinet',\n    'clipboard',\n    'spiral_note_pad',\n    'file_folder',\n    'open_file_folder',\n    'card_index_dividers',\n    'rolled_up_newspaper',\n    'newspaper',\n    'notebook',\n    'closed_book',\n    'green_book',\n    'blue_book',\n    'orange_book',\n    'notebook_with_decorative_cover',\n    'ledger',\n    'books',\n    'book',\n    'link',\n    'paperclip',\n    'linked_paperclips',\n    'scissors',\n    'triangular_ruler',\n    'straight_ruler',\n    'pushpin',\n    'round_pushpin',\n    'triangular_flag_on_post',\n    'waving_white_flag',\n    'waving_black_flag',\n    'closed_lock_with_key',\n    'lock',\n    'unlock',\n    'lock_with_ink_pen',\n    'lower_left_ballpoint_pen',\n    'lower_left_fountain_pen',\n    'black_nib',\n    'memo',\n    'pencil2',\n    'lower_left_crayon',\n    'lower_left_paintbrush',\n    'mag',\n    'mag_right',\n  ],\n  'Symbols': [\n    'heart',\n    'yellow_heart',\n    'green_heart',\n    'blue_heart',\n    'purple_heart',\n    'broken_heart',\n    'heavy_heart_exclamation_mark_ornament',\n    'two_hearts',\n    'revolving_hearts',\n    'heartbeat',\n    'heartpulse',\n    'sparkling_heart',\n    'cupid',\n    'gift_heart',\n    'heart_decoration',\n    'peace_symbol',\n    'latin_cross',\n    'star_and_crescent',\n    'om_symbol',\n    'wheel_of_dharma',\n    'star_of_david',\n    'six_pointed_star',\n    'menorah_with_nine_branches',\n    'yin_yang',\n    'orthodox_cross',\n    'place_of_worship',\n    'ophiuchus',\n    'aries',\n    'taurus',\n    'gemini',\n    'cancer',\n    'leo',\n    'virgo',\n    'libra',\n    'scorpius',\n    'sagittarius',\n    'capricorn',\n    'aquarius',\n    'pisces',\n    'id',\n    'atom_symbol',\n    'u7a7a',\n    'u5272',\n    'radioactive_sign',\n    'biohazard_sign',\n    'mobile_phone_off',\n    'vibration_mode',\n    'u6709',\n    'u7121',\n    'u7533',\n    'u55b6',\n    'u6708',\n    'eight_pointed_black_star',\n    'vs',\n    'accept',\n    'white_flower',\n    'ideograph_advantage',\n    'secret',\n    'congratulations',\n    'u5408',\n    'u6e80',\n    'u7981',\n    'a',\n    'b',\n    'ab',\n    'cl',\n    'o2',\n    'sos',\n    'no_entry',\n    'name_badge',\n    'no_entry_sign',\n    'x',\n    'o',\n    'anger',\n    'hotsprings',\n    'no_pedestrians',\n    'do_not_litter',\n    'no_bicycles',\n    'non-potable_water',\n    'underage',\n    'no_mobile_phones',\n    'exclamation',\n    'grey_exclamation',\n    'question',\n    'grey_question',\n    'bangbang',\n    'interrobang',\n    '100',\n    'low_brightness',\n    'high_brightness',\n    'trident',\n    'fleur_de_lis',\n    'part_alternation_mark',\n    'warning',\n    'children_crossing',\n    'beginner',\n    'recycle',\n    'u6307',\n    'chart',\n    'sparkle',\n    'eight_spoked_asterisk',\n    'negative_squared_cross_mark',\n    'white_check_mark',\n    'diamond_shape_with_a_dot_inside',\n    'cyclone',\n    'loop',\n    'globe_with_meridians',\n    'm',\n    'atm',\n    'sa',\n    'passport_control',\n    'customs',\n    'baggage_claim',\n    'left_luggage',\n    'wheelchair',\n    'no_smoking',\n    'wc',\n    'parking',\n    'potable_water',\n    'mens',\n    'womens',\n    'baby_symbol',\n    'restroom',\n    'put_litter_in_its_place',\n    'cinema',\n    'signal_strength',\n    'koko',\n    'ng',\n    'ok',\n    'up',\n    'cool',\n    'new',\n    'free',\n    'zero',\n    'one',\n    'two',\n    'three',\n    'four',\n    'five',\n    'six',\n    'seven',\n    'eight',\n    'nine',\n    'keycap_ten',\n    'keycap_star',\n    '1234',\n    'arrow_forward',\n    'double_vertical_bar',\n    'black_right_pointing_triangle_with_double_vertical_bar',\n    'black_square_for_stop',\n    'black_circle_for_record',\n    'black_right_pointing_double_triangle_with_vertical_bar',\n    'black_left_pointing_double_triangle_with_vertical_bar',\n    'fast_forward',\n    'rewind',\n    'twisted_rightwards_arrows',\n    'repeat',\n    'repeat_one',\n    'arrow_backward',\n    'arrow_up_small',\n    'arrow_down_small',\n    'arrow_double_up',\n    'arrow_double_down',\n    'arrow_right',\n    'arrow_left',\n    'arrow_up',\n    'arrow_down',\n    'arrow_upper_right',\n    'arrow_lower_right',\n    'arrow_lower_left',\n    'arrow_upper_left',\n    'arrow_up_down',\n    'left_right_arrow',\n    'arrows_counterclockwise',\n    'arrow_right_hook',\n    'leftwards_arrow_with_hook',\n    'arrow_heading_up',\n    'arrow_heading_down',\n    'hash',\n    'information_source',\n    'abc',\n    'abcd',\n    'capital_abcd',\n    'symbols',\n    'musical_note',\n    'notes',\n    'wavy_dash',\n    'curly_loop',\n    'heavy_check_mark',\n    'arrows_clockwise',\n    'heavy_plus_sign',\n    'heavy_minus_sign',\n    'heavy_division_sign',\n    'heavy_multiplication_x',\n    'heavy_dollar_sign',\n    'currency_exchange',\n    'copyright',\n    'registered',\n    'tm',\n    'end',\n    'back',\n    'on',\n    'top',\n    'soon',\n    'ballot_box_with_check',\n    'radio_button',\n    'white_circle',\n    'black_circle',\n    'red_circle',\n    'large_blue_circle',\n    'small_orange_diamond',\n    'small_blue_diamond',\n    'large_orange_diamond',\n    'large_blue_diamond',\n    'small_red_triangle',\n    'black_small_square',\n    'white_small_square',\n    'black_large_square',\n    'white_large_square',\n    'small_red_triangle_down',\n    'black_medium_square',\n    'white_medium_square',\n    'black_medium_small_square',\n    'white_medium_small_square',\n    'black_square_button',\n    'white_square_button',\n    'speaker',\n    'sound',\n    'loud_sound',\n    'mute',\n    'mega',\n    'loudspeaker',\n    'bell',\n    'no_bell',\n    'black_joker',\n    'mahjong',\n    'spades',\n    'clubs',\n    'hearts',\n    'diamonds',\n    'flower_playing_cards',\n    'thought_balloon',\n    'right_anger_bubble',\n    'speech_balloon',\n    'left_speech_bubble',\n    'clock1',\n    'clock2',\n    'clock3',\n    'clock4',\n    'clock5',\n    'clock6',\n    'clock7',\n    'clock8',\n    'clock9',\n    'clock10',\n    'clock11',\n    'clock12',\n    'clock130',\n    'clock230',\n    'clock330',\n    'clock430',\n    'clock530',\n    'clock630',\n    'clock730',\n    'clock830',\n    'clock930',\n    'clock1030',\n    'clock1130',\n    'clock1230',\n  ],\n  'Flags': [\n    'flag-ac',\n    'flag-ad',\n    'flag-ae',\n    'flag-af',\n    'flag-ag',\n    'flag-ai',\n    'flag-al',\n    'flag-am',\n    'flag-ao',\n    'flag-aq',\n    'flag-ar',\n    'flag-as',\n    'flag-at',\n    'flag-au',\n    'flag-aw',\n    'flag-ax',\n    'flag-az',\n    'flag-ba',\n    'flag-bb',\n    'flag-bd',\n    'flag-be',\n    'flag-bf',\n    'flag-bg',\n    'flag-bh',\n    'flag-bi',\n    'flag-bj',\n    'flag-bl',\n    'flag-bm',\n    'flag-bn',\n    'flag-bo',\n    'flag-bq',\n    'flag-br',\n    'flag-bs',\n    'flag-bt',\n    'flag-bv',\n    'flag-bw',\n    'flag-by',\n    'flag-bz',\n    'flag-ca',\n    'flag-cc',\n    'flag-cd',\n    'flag-cf',\n    'flag-cg',\n    'flag-ch',\n    'flag-ci',\n    'flag-ck',\n    'flag-cl',\n    'flag-cm',\n    'flag-cn',\n    'flag-co',\n    'flag-cp',\n    'flag-cr',\n    'flag-cu',\n    'flag-cv',\n    'flag-cw',\n    'flag-cx',\n    'flag-cy',\n    'flag-cz',\n    'flag-de',\n    'flag-dg',\n    'flag-dj',\n    'flag-dk',\n    'flag-dm',\n    'flag-do',\n    'flag-dz',\n    'flag-ea',\n    'flag-ec',\n    'flag-ee',\n    'flag-eg',\n    'flag-eh',\n    'flag-er',\n    'flag-es',\n    'flag-et',\n    'flag-eu',\n    'flag-fi',\n    'flag-fj',\n    'flag-fk',\n    'flag-fm',\n    'flag-fo',\n    'flag-fr',\n    'flag-ga',\n    'flag-gb',\n    'flag-gd',\n    'flag-ge',\n    'flag-gf',\n    'flag-gg',\n    'flag-gh',\n    'flag-gi',\n    'flag-gl',\n    'flag-gm',\n    'flag-gn',\n    'flag-gp',\n    'flag-gq',\n    'flag-gr',\n    'flag-gs',\n    'flag-gt',\n    'flag-gu',\n    'flag-gw',\n    'flag-gy',\n    'flag-hk',\n    'flag-hm',\n    'flag-hn',\n    'flag-hr',\n    'flag-ht',\n    'flag-hu',\n    'flag-ic',\n    'flag-id',\n    'flag-ie',\n    'flag-il',\n    'flag-im',\n    'flag-in',\n    'flag-io',\n    'flag-iq',\n    'flag-ir',\n    'flag-is',\n    'flag-it',\n    'flag-je',\n    'flag-jm',\n    'flag-jo',\n    'flag-jp',\n    'flag-ke',\n    'flag-kg',\n    'flag-kh',\n    'flag-ki',\n    'flag-km',\n    'flag-kn',\n    'flag-kp',\n    'flag-kr',\n    'flag-kw',\n    'flag-ky',\n    'flag-kz',\n    'flag-la',\n    'flag-lb',\n    'flag-lc',\n    'flag-li',\n    'flag-lk',\n    'flag-lr',\n    'flag-ls',\n    'flag-lt',\n    'flag-lu',\n    'flag-lv',\n    'flag-ly',\n    'flag-ma',\n    'flag-mc',\n    'flag-md',\n    'flag-me',\n    'flag-mf',\n    'flag-mg',\n    'flag-mh',\n    'flag-mk',\n    'flag-ml',\n    'flag-mm',\n    'flag-mn',\n    'flag-mo',\n    'flag-mp',\n    'flag-mq',\n    'flag-mr',\n    'flag-ms',\n    'flag-mt',\n    'flag-mu',\n    'flag-mv',\n    'flag-mw',\n    'flag-mx',\n    'flag-my',\n    'flag-mz',\n    'flag-na',\n    'flag-nc',\n    'flag-ne',\n    'flag-nf',\n    'flag-ng',\n    'flag-ni',\n    'flag-nl',\n    'flag-no',\n    'flag-np',\n    'flag-nr',\n    'flag-nu',\n    'flag-nz',\n    'flag-om',\n    'flag-pa',\n    'flag-pe',\n    'flag-pf',\n    'flag-pg',\n    'flag-ph',\n    'flag-pk',\n    'flag-pl',\n    'flag-pm',\n    'flag-pn',\n    'flag-pr',\n    'flag-ps',\n    'flag-pt',\n    'flag-pw',\n    'flag-py',\n    'flag-qa',\n    'flag-re',\n    'flag-ro',\n    'flag-rs',\n    'flag-ru',\n    'flag-rw',\n    'flag-sa',\n    'flag-sb',\n    'flag-sc',\n    'flag-sd',\n    'flag-se',\n    'flag-sg',\n    'flag-sh',\n    'flag-si',\n    'flag-sj',\n    'flag-sk',\n    'flag-sl',\n    'flag-sm',\n    'flag-sn',\n    'flag-so',\n    'flag-sr',\n    'flag-ss',\n    'flag-st',\n    'flag-sv',\n    'flag-sx',\n    'flag-sy',\n    'flag-sz',\n    'flag-ta',\n    'flag-tc',\n    'flag-td',\n    'flag-tf',\n    'flag-tg',\n    'flag-th',\n    'flag-tj',\n    'flag-tk',\n    'flag-tl',\n    'flag-tm',\n    'flag-tn',\n    'flag-to',\n    'flag-tr',\n    'flag-tt',\n    'flag-tv',\n    'flag-tw',\n    'flag-tz',\n    'flag-ua',\n    'flag-ug',\n    'flag-um',\n    'flag-us',\n    'flag-uy',\n    'flag-uz',\n    'flag-va',\n    'flag-vc',\n    'flag-ve',\n    'flag-vg',\n    'flag-vi',\n    'flag-vn',\n    'flag-vu',\n    'flag-wf',\n    'flag-ws',\n    'flag-xk',\n    'flag-ye',\n    'flag-yt',\n    'flag-za',\n    'flag-zm',\n    'flag-zw',\n  ],\n}\nexport default categorizedEmojiList\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-actions.es6",
    "content": "import Reflux from 'reflux';\n\nconst EmojiActions = Reflux.createActions([\n  \"selectEmoji\",\n  \"useEmoji\",\n]);\n\nfor (const key of Object.keys(EmojiActions)) {\n  EmojiActions[key].sync = true;\n}\n\nexport default EmojiActions;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-button-popover.jsx",
    "content": "import React from 'react';\nimport {findDOMNode} from 'react-dom';\nimport {Actions} from 'nylas-exports';\nimport {RetinaImg, ScrollRegion} from 'nylas-component-kit';\n\nimport EmojiStore from './emoji-store';\nimport EmojiActions from './emoji-actions';\nimport categorizedEmojiList from './categorized-emoji';\n\nclass EmojiButtonPopover extends React.Component {\n  static displayName = 'EmojiButtonPopover';\n\n  constructor() {\n    super();\n    const {categoryNames,\n      categorizedEmoji,\n      categoryPositions} = this.getStateFromStore();\n    this.state = {\n      emojiName: \"Emoji Picker\",\n      categoryNames: categoryNames,\n      categorizedEmoji: categorizedEmoji,\n      categoryPositions: categoryPositions,\n      searchValue: \"\",\n      activeTab: Object.keys(categorizedEmoji)[0],\n    };\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    this._emojiPreloadImage = new Image();\n    this.renderCanvas();\n  }\n\n  componentWillUnmount() {\n    this._emojiPreloadImage.onload = null;\n    this._emojiPreloadImage = null;\n    this._mounted = false;\n  }\n\n  onMouseDown = (event) => {\n    const emojiName = this.calcEmojiByPosition(this.calcPosition(event));\n    if (!emojiName) return null;\n    EmojiActions.selectEmoji({emojiName: emojiName, replaceSelection: false});\n    Actions.closePopover();\n    return null\n  }\n\n  onScroll = () => {\n    const emojiContainer = document.querySelector(\".emoji-finder-container .scroll-region-content\");\n    const tabContainer = document.querySelector(\".emoji-tabs\");\n    tabContainer.className = emojiContainer.scrollTop ? \"emoji-tabs shadow\" : \"emoji-tabs\";\n    if (emojiContainer.scrollTop === 0) {\n      this.setState({activeTab: Object.keys(this.state.categorizedEmoji)[0]});\n    } else {\n      for (const category of Object.keys(this.state.categoryPositions)) {\n        if (emojiContainer.scrollTop >= this.state.categoryPositions[category].top &&\n          emojiContainer.scrollTop <= this.state.categoryPositions[category].bottom) {\n          this.setState({activeTab: category});\n        }\n      }\n    }\n  }\n\n  onHover = (event) => {\n    const emojiName = this.calcEmojiByPosition(this.calcPosition(event));\n    if (emojiName) {\n      this.setState({emojiName: emojiName});\n    } else {\n      this.setState({emojiName: \"Emoji Picker\"});\n    }\n  }\n\n  onMouseOut = () => {\n    this.setState({emojiName: \"Emoji Picker\"});\n  }\n\n  onChange = (event) => {\n    const searchValue = event.target.value;\n    if (searchValue.length > 0) {\n      const searchMatches = this.findSearchMatches(searchValue);\n      this.setState({\n        categorizedEmoji: {\n          'Search Results': searchMatches,\n        },\n        categoryPositions: {\n          'Search Results': {\n            top: 25,\n            bottom: 25 + Math.ceil(searchMatches.length / 8) * 24,\n          },\n        },\n        searchValue: searchValue,\n        activeTab: null,\n      }, this.renderCanvas);\n    } else {\n      this.setState(this.getStateFromStore, () => {\n        this.setState({\n          searchValue: searchValue,\n          activeTab: Object.keys(this.state.categorizedEmoji)[0],\n        }, this.renderCanvas);\n      });\n    }\n  }\n\n  getStateFromStore = () => {\n    let categorizedEmoji = categorizedEmojiList;\n    const categoryPositions = {};\n    let categoryNames = [\n      'People',\n      'Nature',\n      'Food and Drink',\n      'Activity',\n      'Travel and Places',\n      'Objects',\n      'Symbols',\n      'Flags',\n    ];\n    const frequentlyUsedEmoji = EmojiStore.frequentlyUsedEmoji();\n    if (frequentlyUsedEmoji.length > 0) {\n      categorizedEmoji = {'Frequently Used': frequentlyUsedEmoji};\n      for (const category of Object.keys(categorizedEmojiList)) {\n        categorizedEmoji[category] = categorizedEmojiList[category];\n      }\n      categoryNames = [\"Frequently Used\"].concat(categoryNames);\n    }\n    // Calculates where each category should be (variable because Frequently\n    // Used may or may not be present)\n    for (const name of categoryNames) {\n      categoryPositions[name] = {top: 0, bottom: 0};\n    }\n    let verticalPos = 25;\n    for (const category of Object.keys(categoryPositions)) {\n      const height = Math.ceil(categorizedEmoji[category].length / 8) * 24;\n      categoryPositions[category].top = verticalPos;\n      verticalPos += height;\n      categoryPositions[category].bottom = verticalPos;\n      verticalPos += 24;\n    }\n    return {\n      categoryNames: categoryNames,\n      categorizedEmoji: categorizedEmoji,\n      categoryPositions: categoryPositions,\n    };\n  }\n\n  scrollToCategory(category) {\n    const container = document.querySelector(\".emoji-finder-container .scroll-region-content\");\n    if (this.state.searchValue.length > 0) {\n      this.setState({searchValue: \"\"});\n      this.setState(this.getStateFromStore, () => {\n        this.renderCanvas();\n        container.scrollTop = this.state.categoryPositions[category].top + 16;\n      });\n    } else {\n      container.scrollTop = this.state.categoryPositions[category].top + 16;\n    }\n    this.setState({activeTab: category})\n  }\n\n  findSearchMatches(searchValue) {\n    // TODO: Find matches for aliases, too.\n    const searchMatches = [];\n    for (const category of Object.keys(categorizedEmojiList)) {\n      categorizedEmojiList[category].forEach((emojiName) => {\n        if (emojiName.indexOf(searchValue) !== -1) {\n          searchMatches.push(emojiName);\n        }\n      });\n    }\n    return searchMatches;\n  }\n\n  calcPosition(event) {\n    const rect = event.target.getBoundingClientRect();\n    const position = {\n      x: event.pageX - rect.left / 2,\n      y: event.pageY - rect.top / 2,\n    };\n    return position;\n  }\n\n  calcEmojiByPosition = (position) => {\n    for (const category of Object.keys(this.state.categoryPositions)) {\n      const LEFT_BOUNDARY = 8;\n      const RIGHT_BOUNDARY = 204;\n      const EMOJI_WIDTH = 24.5;\n      const EMOJI_HEIGHT = 24;\n      const EMOJI_PER_ROW = 8;\n      if (position.x >= LEFT_BOUNDARY &&\n          position.x <= RIGHT_BOUNDARY &&\n          position.y >= this.state.categoryPositions[category].top &&\n          position.y <= this.state.categoryPositions[category].bottom) {\n        const x = Math.round((position.x + 5) / EMOJI_WIDTH);\n        const y = Math.round((position.y - this.state.categoryPositions[category].top + 10) / EMOJI_HEIGHT);\n        const index = x + (y - 1) * EMOJI_PER_ROW - 1;\n        return this.state.categorizedEmoji[category][index];\n      }\n    }\n    return null;\n  }\n\n  renderTabs() {\n    const tabs = [];\n    this.state.categoryNames.forEach((category) => {\n      let className = `emoji-tab ${(category.replace(/ /g, '-')).toLowerCase()}`\n      if (category === this.state.activeTab) {\n        className += \" active\";\n      }\n      tabs.push(\n        <div key={`${category} container`} style={{flex: 1}}>\n          <RetinaImg\n            key={`${category} tab`}\n            className={className}\n            name={`icon-emojipicker-${(category.replace(/ /g, '-')).toLowerCase()}.png`}\n            mode={RetinaImg.Mode.ContentIsMask}\n            onMouseDown={() => this.scrollToCategory(category)}\n          />\n        </div>\n      );\n    });\n    return tabs;\n  }\n\n  renderCanvas() {\n    const canvas = findDOMNode(this.refs.emojiCanvas);\n    const keys = Object.keys(this.state.categoryPositions);\n    canvas.height = this.state.categoryPositions[keys[keys.length - 1]].bottom * 2;\n    const ctx = canvas.getContext(\"2d\");\n    ctx.font = \"24px Nylas-Pro\";\n    ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    const position = {\n      x: 15,\n      y: 45,\n    }\n\n    let idx = 0;\n    const categoryNames = Object.keys(this.state.categorizedEmoji);\n    const renderNextCategory = () => {\n      if (!categoryNames[idx]) return;\n      if (!this._mounted) return;\n      this.renderCategory(categoryNames[idx], idx, ctx, position, renderNextCategory);\n      idx += 1;\n    }\n    renderNextCategory();\n  }\n\n  renderCategory(category, i, ctx, pos, callback) {\n    const position = pos\n    if (i > 0) {\n      position.x = 18;\n      position.y += 48;\n    }\n    ctx.fillText(category, position.x, position.y);\n    position.x = 18;\n    position.y += 48;\n\n    const emojiNames = this.state.categorizedEmoji[category];\n    if (!emojiNames || emojiNames.length === 0) return;\n\n    const emojiToDraw = emojiNames.map((emojiName, j) => {\n      const x = position.x;\n      const y = position.y;\n      const src = EmojiStore.getImagePath(emojiName);\n\n      if (position.x > 325 && j < this.state.categorizedEmoji[category].length - 1) {\n        position.x = 18;\n        position.y += 48;\n      } else {\n        position.x += 50;\n      }\n\n      return {src, x, y};\n    });\n\n    const drawEmojiAt = ({src, x, y} = {}) => {\n      if (!src) {\n        return;\n      }\n      this._emojiPreloadImage.onload = () => {\n        this._emojiPreloadImage.onload = null;\n        ctx.drawImage(this._emojiPreloadImage, x, y - 30, 32, 32);\n        if (emojiToDraw.length === 0) {\n          callback();\n        } else {\n          drawEmojiAt(emojiToDraw.shift());\n        }\n      }\n      this._emojiPreloadImage.src = src;\n    }\n\n    drawEmojiAt(emojiToDraw.shift());\n  }\n\n  render() {\n    return (\n      <div className=\"emoji-button-popover\" tabIndex=\"-1\">\n        <div className=\"emoji-tabs\">\n          {this.renderTabs()}\n        </div>\n        <ScrollRegion\n          className=\"emoji-finder-container\"\n          onScroll={this.onScroll}\n        >\n          <div className=\"emoji-search-container\">\n            <input\n              type=\"text\"\n              className=\"search\"\n              value={this.state.searchValue}\n              onChange={this.onChange}\n            />\n          </div>\n          <canvas\n            ref=\"emojiCanvas\"\n            width=\"400\"\n            height=\"2000\"\n            onMouseDown={this.onMouseDown}\n            onMouseOut={this.onMouseOut}\n            onMouseMove={this.onHover}\n            style={{zoom: \"0.5\"}}\n          />\n        </ScrollRegion>\n        <div className=\"emoji-name\">\n          {this.state.emojiName}\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default EmojiButtonPopover;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-button.jsx",
    "content": "import {Actions, React, ReactDOM} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\n\nimport EmojiButtonPopover from './emoji-button-popover';\n\n\nclass EmojiButton extends React.Component {\n  static displayName = 'EmojiButton';\n\n  onClick = () => {\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect();\n    Actions.openPopover(\n      <EmojiButtonPopover />,\n      {originRect: buttonRect, direction: 'up'}\n    )\n  }\n\n  render() {\n    return (\n      <button tabIndex={-1} className=\"btn btn-toolbar btn-emoji\" title=\"Insert emoji…\" onClick={this.onClick}>\n        <RetinaImg name=\"icon-composer-emoji.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n}\n\nEmojiButton.containerStyles = {\n  order: 2,\n};\n\nexport default EmojiButton;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-composer-extension.jsx",
    "content": "import {DOMUtils, ComposerExtension, RegExpUtils} from 'nylas-exports';\nimport emoji from 'node-emoji';\n\nimport EmojiStore from './emoji-store';\nimport EmojiActions from './emoji-actions';\nimport EmojiPicker from './emoji-picker';\n\n\nclass EmojiComposerExtension extends ComposerExtension {\n\n  static selState = null;\n\n  static onContentChanged = ({editor}) => {\n    const sel = editor.currentSelection()\n    const {emojiOptions, triggerWord} = EmojiComposerExtension._findEmojiOptions(sel);\n    if (sel.anchorNode && sel.isCollapsed) {\n      if (emojiOptions.length > 0) {\n        const offset = sel.anchorOffset;\n        if (!DOMUtils.closest(sel.anchorNode, \"n1-emoji-autocomplete\")) {\n          const anchorOffset = Math.max(sel.anchorOffset - triggerWord.length - 1, 0);\n          editor.select(sel.anchorNode,\n                        anchorOffset,\n                        sel.focusNode,\n                        sel.focusOffset)\n          editor.wrapSelection(\"n1-emoji-autocomplete\");\n          editor.select(sel.anchorNode,\n                        offset,\n                        sel.anchorNode,\n                        offset);\n        }\n      } else {\n        if (DOMUtils.closest(sel.anchorNode, \"n1-emoji-autocomplete\")) {\n          editor.unwrapNodeAndSelectAll(DOMUtils.closest(sel.anchorNode, \"n1-emoji-autocomplete\"));\n          editor.select(sel.anchorNode,\n                        sel.anchorOffset + triggerWord.length + 1,\n                        sel.focusNode,\n                        sel.focusOffset + triggerWord.length + 1);\n        }\n      }\n    } else {\n      if (DOMUtils.closest(sel.anchorNode, \"n1-emoji-autocomplete\")) {\n        editor.unwrapNodeAndSelectAll(DOMUtils.closest(sel.anchorNode, \"n1-emoji-autocomplete\"));\n        editor.select(sel.anchorNode,\n                      sel.anchorOffset + triggerWord.length,\n                      sel.focusNode,\n                      sel.focusOffset + triggerWord.length);\n      }\n    }\n  };\n\n  static onBlur = ({editor}) => {\n    EmojiComposerExtension.selState = editor.currentSelection().exportSelection();\n  };\n\n  static onFocus = ({editor}) => {\n    if (EmojiComposerExtension.selState) {\n      editor.select(EmojiComposerExtension.selState);\n      EmojiComposerExtension.selState = null;\n    }\n  };\n\n  static toolbarComponentConfig = ({toolbarState}) => {\n    const sel = toolbarState.selectionSnapshot;\n    if (sel) {\n      const {emojiOptions} = EmojiComposerExtension._findEmojiOptions(sel);\n      if (emojiOptions.length > 0 && !toolbarState.dragging && !toolbarState.doubleDown) {\n        const locationRefNode = DOMUtils.closest(sel.anchorNode,\n                                                 \"n1-emoji-autocomplete\");\n        if (!locationRefNode) return null;\n        const selectedEmoji = locationRefNode.getAttribute(\"selectedEmoji\");\n        return {\n          component: EmojiPicker,\n          props: {emojiOptions,\n            selectedEmoji},\n          locationRefNode: locationRefNode,\n          width: EmojiComposerExtension._emojiPickerWidth(emojiOptions),\n          height: EmojiComposerExtension._emojiPickerHeight(emojiOptions),\n          hidePointer: true,\n        }\n      }\n    }\n    return null;\n  };\n\n  static editingActions = () => {\n    return [{\n      action: EmojiActions.selectEmoji,\n      callback: EmojiComposerExtension._onSelectEmoji,\n    }]\n  };\n\n  static onKeyDown = ({editor, event}) => {\n    const sel = editor.currentSelection()\n    const {emojiOptions} = EmojiComposerExtension._findEmojiOptions(sel);\n    if (emojiOptions.length > 0) {\n      if (event.key === \"ArrowDown\" || event.key === \"ArrowRight\" ||\n          event.key === \"ArrowUp\" || event.key === \"ArrowLeft\") {\n        event.preventDefault();\n        const moveToNext = (event.key === \"ArrowDown\" || event.key === \"ArrowRight\");\n        const emojiNameNode = DOMUtils.closest(sel.anchorNode, \"n1-emoji-autocomplete\");\n        if (!emojiNameNode) return null;\n        const selectedEmoji = emojiNameNode.getAttribute(\"selectedEmoji\");\n        if (selectedEmoji) {\n          const emojiIndex = emojiOptions.indexOf(selectedEmoji);\n          if (emojiIndex < emojiOptions.length - 1 && moveToNext) {\n            emojiNameNode.setAttribute(\"selectedEmoji\", emojiOptions[emojiIndex + 1]);\n          } else if (emojiIndex > 0 && !moveToNext) {\n            emojiNameNode.setAttribute(\"selectedEmoji\", emojiOptions[emojiIndex - 1]);\n          } else {\n            const index = moveToNext ? 0 : emojiOptions.length - 1;\n            emojiNameNode.setAttribute(\"selectedEmoji\", emojiOptions[index]);\n          }\n        } else {\n          const index = moveToNext ? 1 : emojiOptions.length - 1;\n          emojiNameNode.setAttribute(\"selectedEmoji\", emojiOptions[index]);\n        }\n      } else if (event.key === \"Enter\" || event.key === \"Tab\") {\n        event.preventDefault();\n        const emojiNameNode = DOMUtils.closest(sel.anchorNode, \"n1-emoji-autocomplete\");\n        if (!emojiNameNode) return null;\n        let selectedEmoji = emojiNameNode.getAttribute(\"selectedEmoji\");\n        if (!selectedEmoji) selectedEmoji = emojiOptions[0];\n        const args = {\n          editor: editor,\n          actionArg: {\n            emojiName: selectedEmoji,\n            replaceSelection: true,\n          },\n        };\n        EmojiComposerExtension._onSelectEmoji(args);\n      }\n    }\n    return null;\n  };\n\n  static applyTransformsForSending = ({draftBodyRootNode}) => {\n    const imgs = draftBodyRootNode.querySelectorAll('img')\n    for (const imgEl of Array.from(imgs)) {\n      const names = imgEl.className.split(' ');\n      if (names[0] === 'emoji') {\n        const emojiChar = emoji.get(names[1]);\n        if (emojiChar) {\n          imgEl.parentNode.replaceChild(document.createTextNode(emojiChar), imgEl);\n        }\n      }\n    }\n  }\n\n  static unapplyTransformsForSending = ({draftBodyRootNode}) => {\n    const treeWalker = document.createTreeWalker(draftBodyRootNode, NodeFilter.SHOW_TEXT);\n    while (treeWalker.nextNode()) {\n      const textNode = treeWalker.currentNode;\n      const match = RegExpUtils.emojiRegex().exec(textNode.textContent);\n      if (match) {\n        const emojiPlusTrailingEl = textNode.splitText(match.index);\n        emojiPlusTrailingEl.splitText(match.length);\n        const emojiEl = emojiPlusTrailingEl;\n        const imgEl = document.createElement('img');\n        const emojiName = emoji.which(match[0])\n        imgEl.className = `emoji ${emojiName}`;\n        imgEl.src = EmojiStore.getImagePath(emojiName);\n        imgEl.width = '14';\n        imgEl.height = '14';\n        imgEl.style.marginTop = '-5px';\n        emojiEl.parentNode.replaceChild(imgEl, emojiEl);\n      }\n    }\n  }\n\n  static _findEmojiOptions(sel) {\n    if (sel.anchorNode &&\n        sel.anchorNode.nodeValue &&\n        sel.anchorNode.nodeValue.length > 0 &&\n        sel.isCollapsed) {\n      const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset);\n      let index = words.lastIndexOf(\":\");\n      let lastWord = \"\";\n      if (index !== -1 && words.lastIndexOf(\" \") < index) {\n        lastWord = words.substring(index + 1, sel.anchorOffset);\n      } else {\n        const {text} = EmojiComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);\n        index = text.lastIndexOf(\":\");\n        if (index !== -1 && text.lastIndexOf(\" \") < index) {\n          lastWord = text.substring(index + 1);\n        } else {\n          return {triggerWord: \"\", emojiOptions: []};\n        }\n      }\n      if (lastWord.length > 0) {\n        return {triggerWord: lastWord, emojiOptions: EmojiComposerExtension._findMatches(lastWord)};\n      }\n      return {triggerWord: lastWord, emojiOptions: []};\n    }\n    return {triggerWord: \"\", emojiOptions: []};\n  }\n\n  static _onSelectEmoji = ({editor, actionArg}) => {\n    const {emojiName, replaceSelection} = actionArg;\n    if (!emojiName) return null;\n    if (replaceSelection) {\n      const sel = editor.currentSelection();\n      if (sel.anchorNode &&\n          sel.anchorNode.nodeValue &&\n          sel.anchorNode.nodeValue.length > 0 &&\n            sel.isCollapsed) {\n        const words = sel.anchorNode.nodeValue.substring(0, sel.anchorOffset);\n        let index = words.lastIndexOf(\":\");\n        let lastWord = words.substring(index + 1, sel.anchorOffset);\n        if (index !== -1 && words.lastIndexOf(\" \") < index) {\n          editor.select(sel.anchorNode,\n                        sel.anchorOffset - lastWord.length - 1,\n                        sel.focusNode,\n                        sel.focusOffset);\n        } else {\n          const {text, textNode} = EmojiComposerExtension._getTextUntilSpace(sel.anchorNode, sel.anchorOffset);\n          index = text.lastIndexOf(\":\");\n          lastWord = text.substring(index + 1);\n          const offset = textNode.nodeValue.lastIndexOf(\":\");\n          editor.select(textNode,\n                        offset,\n                        sel.focusNode,\n                        sel.focusOffset);\n          editor.delete();\n        }\n      }\n    }\n    const emojiChar = emoji.get(emojiName);\n    const html = `<img\n                    class=\"emoji ${emojiName}\"\n                    src=\"${EmojiStore.getImagePath(emojiName)}\"\n                    width=\"14\"\n                    height=\"14\"\n                    style=\"margin-top: -5px;\">`;\n    editor.insertHTML(html, {selectInsertion: false});\n    EmojiActions.useEmoji({emojiName: emojiName, emojiChar: emojiChar});\n    return null;\n  };\n\n  static _emojiPickerWidth(emojiOptions) {\n    let maxLength = 0;\n    for (const emojiOption of emojiOptions) {\n      if (emojiOption.length > maxLength) {\n        maxLength = emojiOption.length;\n      }\n    }\n    // TODO: Calculate width of words more accurately for a closer fit.\n    const WIDTH_PER_CHAR = 8;\n    return (maxLength + 10) * WIDTH_PER_CHAR;\n  }\n\n  static _emojiPickerHeight(emojiOptions) {\n    const HEIGHT_PER_EMOJI = 25;\n    if (emojiOptions.length < 5) {\n      return emojiOptions.length * HEIGHT_PER_EMOJI + 20;\n    }\n    return 5 * HEIGHT_PER_EMOJI + 23;\n  }\n\n  static _getTextUntilSpace(node, offset) {\n    let text = node.nodeValue.substring(0, offset);\n    let prevTextNode = DOMUtils.previousTextNode(node);\n    if (!prevTextNode) return {text: text, textNode: node};\n    while (prevTextNode) {\n      if (prevTextNode.nodeValue.indexOf(\" \") === -1 &&\n        prevTextNode.nodeValue.indexOf(\":\") === -1) {\n        text = prevTextNode.nodeValue + text;\n        prevTextNode = DOMUtils.previousTextNode(prevTextNode);\n      } else if (prevTextNode.nextSibling &&\n        prevTextNode.nextSibling.nodeName !== \"DIV\") {\n        text = prevTextNode.nodeValue.trim() + text;\n        break;\n      } else {\n        break;\n      }\n    }\n    return {text: text, textNode: prevTextNode};\n  }\n\n  static _findMatches(word) {\n    const emojiOptions = []\n    const emojiNames = Object.keys(emoji.emoji).sort();\n    for (const emojiName of emojiNames) {\n      if (word === emojiName.substring(0, word.length)) {\n        emojiOptions.push(emojiName);\n      }\n    }\n    return emojiOptions;\n  }\n\n}\n\nexport default EmojiComposerExtension;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-data.json",
    "content": "{\"emojiData\":[{\"name\":\"COPYRIGHT SIGN\",\"unified\":\"00A9\",\"variations\":[\"00A9-FE0F\"],\"docomo\":\"E731\",\"au\":\"E558\",\"softbank\":\"E24E\",\"google\":\"FEB29\",\"image\":\"00a9.png\",\"sheet_x\":0,\"sheet_y\":0,\"short_name\":\"copyright\",\"short_names\":[\"copyright\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":197,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":false,\"has_img_emojione\":true},{\"name\":\"REGISTERED SIGN\",\"unified\":\"00AE\",\"variations\":[\"00AE-FE0F\"],\"docomo\":\"E736\",\"au\":\"E559\",\"softbank\":\"E24F\",\"google\":\"FEB2D\",\"image\":\"00ae.png\",\"sheet_x\":0,\"sheet_y\":1,\"short_name\":\"registered\",\"short_names\":[\"registered\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":198,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":false,\"has_img_emojione\":true},{\"name\":\"DOUBLE EXCLAMATION MARK\",\"unified\":\"203C\",\"variations\":[\"203C-FE0F\"],\"docomo\":\"E704\",\"au\":\"EB30\",\"softbank\":null,\"google\":\"FEB06\",\"image\":\"203c.png\",\"sheet_x\":0,\"sheet_y\":2,\"short_name\":\"bangbang\",\"short_names\":[\"bangbang\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":86,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EXCLAMATION QUESTION MARK\",\"unified\":\"2049\",\"variations\":[\"2049-FE0F\"],\"docomo\":\"E703\",\"au\":\"EB2F\",\"softbank\":null,\"google\":\"FEB05\",\"image\":\"2049.png\",\"sheet_x\":0,\"sheet_y\":3,\"short_name\":\"interrobang\",\"short_names\":[\"interrobang\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":87,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRADE MARK SIGN\",\"unified\":\"2122\",\"variations\":[\"2122-FE0F\"],\"docomo\":\"E732\",\"au\":\"E54E\",\"softbank\":\"E537\",\"google\":\"FEB2A\",\"image\":\"2122.png\",\"sheet_x\":0,\"sheet_y\":4,\"short_name\":\"tm\",\"short_names\":[\"tm\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":199,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":false,\"has_img_emojione\":true},{\"name\":\"INFORMATION SOURCE\",\"unified\":\"2139\",\"variations\":[\"2139-FE0F\"],\"docomo\":null,\"au\":\"E533\",\"softbank\":null,\"google\":\"FEB47\",\"image\":\"2139.png\",\"sheet_x\":0,\"sheet_y\":5,\"short_name\":\"information_source\",\"short_names\":[\"information_source\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":180,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEFT RIGHT ARROW\",\"unified\":\"2194\",\"variations\":[\"2194-FE0F\"],\"docomo\":\"E73C\",\"au\":\"EB7A\",\"softbank\":null,\"google\":\"FEAF6\",\"image\":\"2194.png\",\"sheet_x\":0,\"sheet_y\":6,\"short_name\":\"left_right_arrow\",\"short_names\":[\"left_right_arrow\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":172,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UP DOWN ARROW\",\"unified\":\"2195\",\"variations\":[\"2195-FE0F\"],\"docomo\":\"E73D\",\"au\":\"EB7B\",\"softbank\":null,\"google\":\"FEAF7\",\"image\":\"2195.png\",\"sheet_x\":0,\"sheet_y\":7,\"short_name\":\"arrow_up_down\",\"short_names\":[\"arrow_up_down\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":171,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NORTH WEST ARROW\",\"unified\":\"2196\",\"variations\":[\"2196-FE0F\"],\"docomo\":\"E697\",\"au\":\"E54C\",\"softbank\":\"E237\",\"google\":\"FEAF2\",\"image\":\"2196.png\",\"sheet_x\":0,\"sheet_y\":8,\"short_name\":\"arrow_upper_left\",\"short_names\":[\"arrow_upper_left\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":170,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NORTH EAST ARROW\",\"unified\":\"2197\",\"variations\":[\"2197-FE0F\"],\"docomo\":\"E678\",\"au\":\"E555\",\"softbank\":\"E236\",\"google\":\"FEAF0\",\"image\":\"2197.png\",\"sheet_x\":0,\"sheet_y\":9,\"short_name\":\"arrow_upper_right\",\"short_names\":[\"arrow_upper_right\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":167,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SOUTH EAST ARROW\",\"unified\":\"2198\",\"variations\":[\"2198-FE0F\"],\"docomo\":\"E696\",\"au\":\"E54D\",\"softbank\":\"E238\",\"google\":\"FEAF1\",\"image\":\"2198.png\",\"sheet_x\":0,\"sheet_y\":10,\"short_name\":\"arrow_lower_right\",\"short_names\":[\"arrow_lower_right\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":168,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SOUTH WEST ARROW\",\"unified\":\"2199\",\"variations\":[\"2199-FE0F\"],\"docomo\":\"E6A5\",\"au\":\"E556\",\"softbank\":\"E239\",\"google\":\"FEAF3\",\"image\":\"2199.png\",\"sheet_x\":0,\"sheet_y\":11,\"short_name\":\"arrow_lower_left\",\"short_names\":[\"arrow_lower_left\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":169,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEFTWARDS ARROW WITH HOOK\",\"unified\":\"21A9\",\"variations\":[\"21A9-FE0F\"],\"docomo\":\"E6DA\",\"au\":\"E55D\",\"softbank\":null,\"google\":\"FEB83\",\"image\":\"21a9.png\",\"sheet_x\":0,\"sheet_y\":12,\"short_name\":\"leftwards_arrow_with_hook\",\"short_names\":[\"leftwards_arrow_with_hook\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":175,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RIGHTWARDS ARROW WITH HOOK\",\"unified\":\"21AA\",\"variations\":[\"21AA-FE0F\"],\"docomo\":null,\"au\":\"E55C\",\"softbank\":null,\"google\":\"FEB88\",\"image\":\"21aa.png\",\"sheet_x\":0,\"sheet_y\":13,\"short_name\":\"arrow_right_hook\",\"short_names\":[\"arrow_right_hook\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":174,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WATCH\",\"unified\":\"231A\",\"variations\":[\"231A-FE0F\"],\"docomo\":\"E71F\",\"au\":\"E57A\",\"softbank\":null,\"google\":\"FE01D\",\"image\":\"231a.png\",\"sheet_x\":0,\"sheet_y\":14,\"short_name\":\"watch\",\"short_names\":[\"watch\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOURGLASS\",\"unified\":\"231B\",\"variations\":[\"231B-FE0F\"],\"docomo\":\"E71C\",\"au\":\"E57B\",\"softbank\":null,\"google\":\"FE01C\",\"image\":\"231b.png\",\"sheet_x\":0,\"sheet_y\":15,\"short_name\":\"hourglass\",\"short_names\":[\"hourglass\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYBOARD\",\"unified\":\"2328\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2328.png\",\"sheet_x\":0,\"sheet_y\":16,\"short_name\":\"keyboard\",\"short_names\":[\"keyboard\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK RIGHT-POINTING DOUBLE TRIANGLE\",\"unified\":\"23E9\",\"variations\":[],\"docomo\":null,\"au\":\"E530\",\"softbank\":\"E23C\",\"google\":\"FEAFE\",\"image\":\"23e9.png\",\"sheet_x\":0,\"sheet_y\":17,\"short_name\":\"fast_forward\",\"short_names\":[\"fast_forward\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":153,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK LEFT-POINTING DOUBLE TRIANGLE\",\"unified\":\"23EA\",\"variations\":[],\"docomo\":null,\"au\":\"E52F\",\"softbank\":\"E23D\",\"google\":\"FEAFF\",\"image\":\"23ea.png\",\"sheet_x\":0,\"sheet_y\":18,\"short_name\":\"rewind\",\"short_names\":[\"rewind\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":154,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK UP-POINTING DOUBLE TRIANGLE\",\"unified\":\"23EB\",\"variations\":[],\"docomo\":null,\"au\":\"E545\",\"softbank\":null,\"google\":\"FEB03\",\"image\":\"23eb.png\",\"sheet_x\":0,\"sheet_y\":19,\"short_name\":\"arrow_double_up\",\"short_names\":[\"arrow_double_up\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":161,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK DOWN-POINTING DOUBLE TRIANGLE\",\"unified\":\"23EC\",\"variations\":[],\"docomo\":null,\"au\":\"E544\",\"softbank\":null,\"google\":\"FEB02\",\"image\":\"23ec.png\",\"sheet_x\":0,\"sheet_y\":20,\"short_name\":\"arrow_double_down\",\"short_names\":[\"arrow_double_down\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":162,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK RIGHT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR\",\"unified\":\"23ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23ed.png\",\"sheet_x\":0,\"sheet_y\":21,\"short_name\":\"black_right_pointing_double_triangle_with_vertical_bar\",\"short_names\":[\"black_right_pointing_double_triangle_with_vertical_bar\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":151,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK LEFT-POINTING DOUBLE TRIANGLE WITH VERTICAL BAR\",\"unified\":\"23EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23ee.png\",\"sheet_x\":0,\"sheet_y\":22,\"short_name\":\"black_left_pointing_double_triangle_with_vertical_bar\",\"short_names\":[\"black_left_pointing_double_triangle_with_vertical_bar\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":152,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK RIGHT-POINTING TRIANGLE WITH DOUBLE VERTICAL BAR\",\"unified\":\"23EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23ef.png\",\"sheet_x\":0,\"sheet_y\":23,\"short_name\":\"black_right_pointing_triangle_with_double_vertical_bar\",\"short_names\":[\"black_right_pointing_triangle_with_double_vertical_bar\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":148,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ALARM CLOCK\",\"unified\":\"23F0\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E02D\",\"google\":\"FE02A\",\"image\":\"23f0.png\",\"sheet_x\":0,\"sheet_y\":24,\"short_name\":\"alarm_clock\",\"short_names\":[\"alarm_clock\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STOPWATCH\",\"unified\":\"23F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23f1.png\",\"sheet_x\":0,\"sheet_y\":25,\"short_name\":\"stopwatch\",\"short_names\":[\"stopwatch\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TIMER CLOCK\",\"unified\":\"23F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23f2.png\",\"sheet_x\":0,\"sheet_y\":26,\"short_name\":\"timer_clock\",\"short_names\":[\"timer_clock\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOURGLASS WITH FLOWING SAND\",\"unified\":\"23F3\",\"variations\":[],\"docomo\":\"E71C\",\"au\":\"E47C\",\"softbank\":null,\"google\":\"FE01B\",\"image\":\"23f3.png\",\"sheet_x\":0,\"sheet_y\":27,\"short_name\":\"hourglass_flowing_sand\",\"short_names\":[\"hourglass_flowing_sand\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOUBLE VERTICAL BAR\",\"unified\":\"23F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23f8.png\",\"sheet_x\":0,\"sheet_y\":28,\"short_name\":\"double_vertical_bar\",\"short_names\":[\"double_vertical_bar\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":147,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK SQUARE FOR STOP\",\"unified\":\"23F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23f9.png\",\"sheet_x\":0,\"sheet_y\":29,\"short_name\":\"black_square_for_stop\",\"short_names\":[\"black_square_for_stop\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":149,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK CIRCLE FOR RECORD\",\"unified\":\"23FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"23fa.png\",\"sheet_x\":0,\"sheet_y\":30,\"short_name\":\"black_circle_for_record\",\"short_names\":[\"black_circle_for_record\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":150,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CIRCLED LATIN CAPITAL LETTER M\",\"unified\":\"24C2\",\"variations\":[\"24C2-FE0F\"],\"docomo\":\"E65C\",\"au\":\"E5BC\",\"softbank\":\"E434\",\"google\":\"FE7E1\",\"image\":\"24c2.png\",\"sheet_x\":0,\"sheet_y\":31,\"short_name\":\"m\",\"short_names\":[\"m\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":108,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK SMALL SQUARE\",\"unified\":\"25AA\",\"variations\":[\"25AA-FE0F\"],\"docomo\":null,\"au\":\"E532\",\"softbank\":\"E21A\",\"google\":\"FEB6E\",\"image\":\"25aa.png\",\"sheet_x\":0,\"sheet_y\":32,\"short_name\":\"black_small_square\",\"short_names\":[\"black_small_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":216,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE SMALL SQUARE\",\"unified\":\"25AB\",\"variations\":[\"25AB-FE0F\"],\"docomo\":null,\"au\":\"E531\",\"softbank\":\"E21B\",\"google\":\"FEB6D\",\"image\":\"25ab.png\",\"sheet_x\":0,\"sheet_y\":33,\"short_name\":\"white_small_square\",\"short_names\":[\"white_small_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":217,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK RIGHT-POINTING TRIANGLE\",\"unified\":\"25B6\",\"variations\":[\"25B6-FE0F\"],\"docomo\":null,\"au\":\"E52E\",\"softbank\":\"E23A\",\"google\":\"FEAFC\",\"image\":\"25b6.png\",\"sheet_x\":0,\"sheet_y\":34,\"short_name\":\"arrow_forward\",\"short_names\":[\"arrow_forward\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":146,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK LEFT-POINTING TRIANGLE\",\"unified\":\"25C0\",\"variations\":[\"25C0-FE0F\"],\"docomo\":null,\"au\":\"E52D\",\"softbank\":\"E23B\",\"google\":\"FEAFD\",\"image\":\"25c0.png\",\"sheet_x\":0,\"sheet_y\":35,\"short_name\":\"arrow_backward\",\"short_names\":[\"arrow_backward\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":158,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE MEDIUM SQUARE\",\"unified\":\"25FB\",\"variations\":[\"25FB-FE0F\"],\"docomo\":null,\"au\":\"E538\",\"softbank\":\"E21B\",\"google\":\"FEB71\",\"image\":\"25fb.png\",\"sheet_x\":0,\"sheet_y\":36,\"short_name\":\"white_medium_square\",\"short_names\":[\"white_medium_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":222,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK MEDIUM SQUARE\",\"unified\":\"25FC\",\"variations\":[\"25FC-FE0F\"],\"docomo\":null,\"au\":\"E539\",\"softbank\":\"E21A\",\"google\":\"FEB72\",\"image\":\"25fc.png\",\"sheet_x\":0,\"sheet_y\":37,\"short_name\":\"black_medium_square\",\"short_names\":[\"black_medium_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":221,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE MEDIUM SMALL SQUARE\",\"unified\":\"25FD\",\"variations\":[\"25FD-FE0F\"],\"docomo\":null,\"au\":\"E534\",\"softbank\":\"E21B\",\"google\":\"FEB6F\",\"image\":\"25fd.png\",\"sheet_x\":0,\"sheet_y\":38,\"short_name\":\"white_medium_small_square\",\"short_names\":[\"white_medium_small_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":224,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK MEDIUM SMALL SQUARE\",\"unified\":\"25FE\",\"variations\":[\"25FE-FE0F\"],\"docomo\":null,\"au\":\"E535\",\"softbank\":\"E21A\",\"google\":\"FEB70\",\"image\":\"25fe.png\",\"sheet_x\":0,\"sheet_y\":39,\"short_name\":\"black_medium_small_square\",\"short_names\":[\"black_medium_small_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":223,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK SUN WITH RAYS\",\"unified\":\"2600\",\"variations\":[\"2600-FE0F\"],\"docomo\":\"E63E\",\"au\":\"E488\",\"softbank\":\"E04A\",\"google\":\"FE000\",\"image\":\"2600.png\",\"sheet_x\":0,\"sheet_y\":40,\"short_name\":\"sunny\",\"short_names\":[\"sunny\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":123,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOUD\",\"unified\":\"2601\",\"variations\":[\"2601-FE0F\"],\"docomo\":\"E63F\",\"au\":\"E48D\",\"softbank\":\"E049\",\"google\":\"FE001\",\"image\":\"2601.png\",\"sheet_x\":1,\"sheet_y\":0,\"short_name\":\"cloud\",\"short_names\":[\"cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":128,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UMBRELLA\",\"unified\":\"2602\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2602.png\",\"sheet_x\":1,\"sheet_y\":1,\"short_name\":\"umbrella\",\"short_names\":[\"umbrella\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":143,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SNOWMAN\",\"unified\":\"2603\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2603.png\",\"sheet_x\":1,\"sheet_y\":2,\"short_name\":\"showman\",\"short_names\":[\"showman\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":137,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COMET\",\"unified\":\"2604\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2604.png\",\"sheet_x\":1,\"sheet_y\":3,\"short_name\":\"comet\",\"short_names\":[\"comet\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":122,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK TELEPHONE\",\"unified\":\"260E\",\"variations\":[\"260E-FE0F\"],\"docomo\":\"E687\",\"au\":\"E596\",\"softbank\":\"E009\",\"google\":\"FE523\",\"image\":\"260e.png\",\"sheet_x\":1,\"sheet_y\":4,\"short_name\":\"phone\",\"short_names\":[\"phone\",\"telephone\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BALLOT BOX WITH CHECK\",\"unified\":\"2611\",\"variations\":[\"2611-FE0F\"],\"docomo\":null,\"au\":\"EB02\",\"softbank\":null,\"google\":\"FEB8B\",\"image\":\"2611.png\",\"sheet_x\":1,\"sheet_y\":5,\"short_name\":\"ballot_box_with_check\",\"short_names\":[\"ballot_box_with_check\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":205,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UMBRELLA WITH RAIN DROPS\",\"unified\":\"2614\",\"variations\":[\"2614-FE0F\"],\"docomo\":\"E640\",\"au\":\"E48C\",\"softbank\":\"E04B\",\"google\":\"FE002\",\"image\":\"2614.png\",\"sheet_x\":1,\"sheet_y\":6,\"short_name\":\"umbrella\",\"short_names\":[\"umbrella\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":144,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOT BEVERAGE\",\"unified\":\"2615\",\"variations\":[\"2615-FE0F\"],\"docomo\":\"E670\",\"au\":\"E597\",\"softbank\":\"E045\",\"google\":\"FE981\",\"image\":\"2615.png\",\"sheet_x\":1,\"sheet_y\":7,\"short_name\":\"coffee\",\"short_names\":[\"coffee\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":64,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHAMROCK\",\"unified\":\"2618\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2618.png\",\"sheet_x\":1,\"sheet_y\":8,\"short_name\":\"shamrock\",\"short_names\":[\"shamrock\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":81,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE UP POINTING INDEX\",\"unified\":\"261D\",\"variations\":[\"261D-FE0F\"],\"docomo\":null,\"au\":\"E4F6\",\"softbank\":\"E00F\",\"google\":\"FEB98\",\"image\":\"261d.png\",\"sheet_x\":1,\"sheet_y\":9,\"short_name\":\"point_up\",\"short_names\":[\"point_up\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":101,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"261D-1F3FB\":{\"unified\":\"261D-1F3FB\",\"image\":\"261d-1f3fb.png\",\"sheet_x\":1,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"261D-1F3FC\":{\"unified\":\"261D-1F3FC\",\"image\":\"261d-1f3fc.png\",\"sheet_x\":1,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"261D-1F3FD\":{\"unified\":\"261D-1F3FD\",\"image\":\"261d-1f3fd.png\",\"sheet_x\":1,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"261D-1F3FE\":{\"unified\":\"261D-1F3FE\",\"image\":\"261d-1f3fe.png\",\"sheet_x\":1,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"261D-1F3FF\":{\"unified\":\"261D-1F3FF\",\"image\":\"261d-1f3ff.png\",\"sheet_x\":1,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"SKULL AND CROSSBONES\",\"unified\":\"2620\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2620.png\",\"sheet_x\":1,\"sheet_y\":15,\"short_name\":\"skull_and_crossbones\",\"short_names\":[\"skull_and_crossbones\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":70,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RADIOACTIVE SIGN\",\"unified\":\"2622\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2622.png\",\"sheet_x\":1,\"sheet_y\":16,\"short_name\":\"radioactive_sign\",\"short_names\":[\"radioactive_sign\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BIOHAZARD SIGN\",\"unified\":\"2623\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2623.png\",\"sheet_x\":1,\"sheet_y\":17,\"short_name\":\"biohazard_sign\",\"short_names\":[\"biohazard_sign\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ORTHODOX CROSS\",\"unified\":\"2626\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2626.png\",\"sheet_x\":1,\"sheet_y\":18,\"short_name\":\"orthodox_cross\",\"short_names\":[\"orthodox_cross\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STAR AND CRESCENT\",\"unified\":\"262A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"262a.png\",\"sheet_x\":1,\"sheet_y\":19,\"short_name\":\"star_and_crescent\",\"short_names\":[\"star_and_crescent\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PEACE SYMBOL\",\"unified\":\"262E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"262e.png\",\"sheet_x\":1,\"sheet_y\":20,\"short_name\":\"peace_symbol\",\"short_names\":[\"peace_symbol\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"YIN YANG\",\"unified\":\"262F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"262f.png\",\"sheet_x\":1,\"sheet_y\":21,\"short_name\":\"yin_yang\",\"short_names\":[\"yin_yang\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHEEL OF DHARMA\",\"unified\":\"2638\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2638.png\",\"sheet_x\":1,\"sheet_y\":22,\"short_name\":\"wheel_of_dharma\",\"short_names\":[\"wheel_of_dharma\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE FROWNING FACE\",\"unified\":\"2639\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2639.png\",\"sheet_x\":1,\"sheet_y\":23,\"short_name\":\"white_frowning_face\",\"short_names\":[\"white_frowning_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE SMILING FACE\",\"unified\":\"263A\",\"variations\":[\"263A-FE0F\"],\"docomo\":\"E6F0\",\"au\":\"E4FB\",\"softbank\":\"E414\",\"google\":\"FE336\",\"image\":\"263a.png\",\"sheet_x\":1,\"sheet_y\":24,\"short_name\":\"relaxed\",\"short_names\":[\"relaxed\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ARIES\",\"unified\":\"2648\",\"variations\":[\"2648-FE0F\"],\"docomo\":\"E646\",\"au\":\"E48F\",\"softbank\":\"E23F\",\"google\":\"FE02B\",\"image\":\"2648.png\",\"sheet_x\":1,\"sheet_y\":25,\"short_name\":\"aries\",\"short_names\":[\"aries\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TAURUS\",\"unified\":\"2649\",\"variations\":[\"2649-FE0F\"],\"docomo\":\"E647\",\"au\":\"E490\",\"softbank\":\"E240\",\"google\":\"FE02C\",\"image\":\"2649.png\",\"sheet_x\":1,\"sheet_y\":26,\"short_name\":\"taurus\",\"short_names\":[\"taurus\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GEMINI\",\"unified\":\"264A\",\"variations\":[\"264A-FE0F\"],\"docomo\":\"E648\",\"au\":\"E491\",\"softbank\":\"E241\",\"google\":\"FE02D\",\"image\":\"264a.png\",\"sheet_x\":1,\"sheet_y\":27,\"short_name\":\"gemini\",\"short_names\":[\"gemini\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CANCER\",\"unified\":\"264B\",\"variations\":[\"264B-FE0F\"],\"docomo\":\"E649\",\"au\":\"E492\",\"softbank\":\"E242\",\"google\":\"FE02E\",\"image\":\"264b.png\",\"sheet_x\":1,\"sheet_y\":28,\"short_name\":\"cancer\",\"short_names\":[\"cancer\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEO\",\"unified\":\"264C\",\"variations\":[\"264C-FE0F\"],\"docomo\":\"E64A\",\"au\":\"E493\",\"softbank\":\"E243\",\"google\":\"FE02F\",\"image\":\"264c.png\",\"sheet_x\":1,\"sheet_y\":29,\"short_name\":\"leo\",\"short_names\":[\"leo\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VIRGO\",\"unified\":\"264D\",\"variations\":[\"264D-FE0F\"],\"docomo\":\"E64B\",\"au\":\"E494\",\"softbank\":\"E244\",\"google\":\"FE030\",\"image\":\"264d.png\",\"sheet_x\":1,\"sheet_y\":30,\"short_name\":\"virgo\",\"short_names\":[\"virgo\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LIBRA\",\"unified\":\"264E\",\"variations\":[\"264E-FE0F\"],\"docomo\":\"E64C\",\"au\":\"E495\",\"softbank\":\"E245\",\"google\":\"FE031\",\"image\":\"264e.png\",\"sheet_x\":1,\"sheet_y\":31,\"short_name\":\"libra\",\"short_names\":[\"libra\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SCORPIUS\",\"unified\":\"264F\",\"variations\":[\"264F-FE0F\"],\"docomo\":\"E64D\",\"au\":\"E496\",\"softbank\":\"E246\",\"google\":\"FE032\",\"image\":\"264f.png\",\"sheet_x\":1,\"sheet_y\":32,\"short_name\":\"scorpius\",\"short_names\":[\"scorpius\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SAGITTARIUS\",\"unified\":\"2650\",\"variations\":[\"2650-FE0F\"],\"docomo\":\"E64E\",\"au\":\"E497\",\"softbank\":\"E247\",\"google\":\"FE033\",\"image\":\"2650.png\",\"sheet_x\":1,\"sheet_y\":33,\"short_name\":\"sagittarius\",\"short_names\":[\"sagittarius\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAPRICORN\",\"unified\":\"2651\",\"variations\":[\"2651-FE0F\"],\"docomo\":\"E64F\",\"au\":\"E498\",\"softbank\":\"E248\",\"google\":\"FE034\",\"image\":\"2651.png\",\"sheet_x\":1,\"sheet_y\":34,\"short_name\":\"capricorn\",\"short_names\":[\"capricorn\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AQUARIUS\",\"unified\":\"2652\",\"variations\":[\"2652-FE0F\"],\"docomo\":\"E650\",\"au\":\"E499\",\"softbank\":\"E249\",\"google\":\"FE035\",\"image\":\"2652.png\",\"sheet_x\":1,\"sheet_y\":35,\"short_name\":\"aquarius\",\"short_names\":[\"aquarius\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PISCES\",\"unified\":\"2653\",\"variations\":[\"2653-FE0F\"],\"docomo\":\"E651\",\"au\":\"E49A\",\"softbank\":\"E24A\",\"google\":\"FE036\",\"image\":\"2653.png\",\"sheet_x\":1,\"sheet_y\":36,\"short_name\":\"pisces\",\"short_names\":[\"pisces\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK SPADE SUIT\",\"unified\":\"2660\",\"variations\":[\"2660-FE0F\"],\"docomo\":\"E68E\",\"au\":\"E5A1\",\"softbank\":\"E20E\",\"google\":\"FEB1B\",\"image\":\"2660.png\",\"sheet_x\":1,\"sheet_y\":37,\"short_name\":\"spades\",\"short_names\":[\"spades\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":237,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK CLUB SUIT\",\"unified\":\"2663\",\"variations\":[\"2663-FE0F\"],\"docomo\":\"E690\",\"au\":\"E5A3\",\"softbank\":\"E20F\",\"google\":\"FEB1D\",\"image\":\"2663.png\",\"sheet_x\":1,\"sheet_y\":38,\"short_name\":\"clubs\",\"short_names\":[\"clubs\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":238,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK HEART SUIT\",\"unified\":\"2665\",\"variations\":[\"2665-FE0F\"],\"docomo\":\"E68D\",\"au\":\"EAA5\",\"softbank\":\"E20C\",\"google\":\"FEB1A\",\"image\":\"2665.png\",\"sheet_x\":1,\"sheet_y\":39,\"short_name\":\"hearts\",\"short_names\":[\"hearts\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":239,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK DIAMOND SUIT\",\"unified\":\"2666\",\"variations\":[\"2666-FE0F\"],\"docomo\":\"E68F\",\"au\":\"E5A2\",\"softbank\":\"E20D\",\"google\":\"FEB1C\",\"image\":\"2666.png\",\"sheet_x\":1,\"sheet_y\":40,\"short_name\":\"diamonds\",\"short_names\":[\"diamonds\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":240,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOT SPRINGS\",\"unified\":\"2668\",\"variations\":[\"2668-FE0F\"],\"docomo\":\"E6F7\",\"au\":\"E4BC\",\"softbank\":\"E123\",\"google\":\"FE7FA\",\"image\":\"2668.png\",\"sheet_x\":2,\"sheet_y\":0,\"short_name\":\"hotsprings\",\"short_names\":[\"hotsprings\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":75,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK UNIVERSAL RECYCLING SYMBOL\",\"unified\":\"267B\",\"variations\":[\"267B-FE0F\"],\"docomo\":\"E735\",\"au\":\"EB79\",\"softbank\":null,\"google\":\"FEB2C\",\"image\":\"267b.png\",\"sheet_x\":2,\"sheet_y\":1,\"short_name\":\"recycle\",\"short_names\":[\"recycle\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":97,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHEELCHAIR SYMBOL\",\"unified\":\"267F\",\"variations\":[\"267F-FE0F\"],\"docomo\":\"E69B\",\"au\":\"E47F\",\"softbank\":\"E20A\",\"google\":\"FEB20\",\"image\":\"267f.png\",\"sheet_x\":2,\"sheet_y\":2,\"short_name\":\"wheelchair\",\"short_names\":[\"wheelchair\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":115,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HAMMER AND PICK\",\"unified\":\"2692\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2692.png\",\"sheet_x\":2,\"sheet_y\":3,\"short_name\":\"hammer_and_pick\",\"short_names\":[\"hammer_and_pick\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ANCHOR\",\"unified\":\"2693\",\"variations\":[\"2693-FE0F\"],\"docomo\":\"E661\",\"au\":\"E4A9\",\"softbank\":\"E202\",\"google\":\"FE4C1\",\"image\":\"2693.png\",\"sheet_x\":2,\"sheet_y\":4,\"short_name\":\"anchor\",\"short_names\":[\"anchor\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CROSSED SWORDS\",\"unified\":\"2694\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2694.png\",\"sheet_x\":2,\"sheet_y\":5,\"short_name\":\"crossed_swords\",\"short_names\":[\"crossed_swords\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":67,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SCALES\",\"unified\":\"2696\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2696.png\",\"sheet_x\":2,\"sheet_y\":6,\"short_name\":\"scales\",\"short_names\":[\"scales\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ALEMBIC\",\"unified\":\"2697\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2697.png\",\"sheet_x\":2,\"sheet_y\":7,\"short_name\":\"alembic\",\"short_names\":[\"alembic\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":77,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GEAR\",\"unified\":\"2699\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2699.png\",\"sheet_x\":2,\"sheet_y\":8,\"short_name\":\"gear\",\"short_names\":[\"gear\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":61,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ATOM SYMBOL\",\"unified\":\"269B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"269b.png\",\"sheet_x\":2,\"sheet_y\":9,\"short_name\":\"atom_symbol\",\"short_names\":[\"atom_symbol\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FLEUR-DE-LIS\",\"unified\":\"269C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"269c.png\",\"sheet_x\":2,\"sheet_y\":10,\"short_name\":\"fleur_de_lis\",\"short_names\":[\"fleur_de_lis\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":92,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WARNING SIGN\",\"unified\":\"26A0\",\"variations\":[\"26A0-FE0F\"],\"docomo\":\"E737\",\"au\":\"E481\",\"softbank\":\"E252\",\"google\":\"FEB23\",\"image\":\"26a0.png\",\"sheet_x\":2,\"sheet_y\":11,\"short_name\":\"warning\",\"short_names\":[\"warning\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":94,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HIGH VOLTAGE SIGN\",\"unified\":\"26A1\",\"variations\":[\"26A1-FE0F\"],\"docomo\":\"E642\",\"au\":\"E487\",\"softbank\":\"E13D\",\"google\":\"FE004\",\"image\":\"26a1.png\",\"sheet_x\":2,\"sheet_y\":12,\"short_name\":\"zap\",\"short_names\":[\"zap\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":132,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MEDIUM WHITE CIRCLE\",\"unified\":\"26AA\",\"variations\":[\"26AA-FE0F\"],\"docomo\":\"E69C\",\"au\":\"E53A\",\"softbank\":\"E219\",\"google\":\"FEB65\",\"image\":\"26aa.png\",\"sheet_x\":2,\"sheet_y\":13,\"short_name\":\"white_circle\",\"short_names\":[\"white_circle\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":207,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MEDIUM BLACK CIRCLE\",\"unified\":\"26AB\",\"variations\":[\"26AB-FE0F\"],\"docomo\":\"E69C\",\"au\":\"E53B\",\"softbank\":\"E219\",\"google\":\"FEB66\",\"image\":\"26ab.png\",\"sheet_x\":2,\"sheet_y\":14,\"short_name\":\"black_circle\",\"short_names\":[\"black_circle\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":208,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COFFIN\",\"unified\":\"26B0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26b0.png\",\"sheet_x\":2,\"sheet_y\":15,\"short_name\":\"coffin\",\"short_names\":[\"coffin\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":71,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FUNERAL URN\",\"unified\":\"26B1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26b1.png\",\"sheet_x\":2,\"sheet_y\":16,\"short_name\":\"funeral_urn\",\"short_names\":[\"funeral_urn\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":72,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SOCCER BALL\",\"unified\":\"26BD\",\"variations\":[\"26BD-FE0F\"],\"docomo\":\"E656\",\"au\":\"E4B6\",\"softbank\":\"E018\",\"google\":\"FE7D4\",\"image\":\"26bd.png\",\"sheet_x\":2,\"sheet_y\":17,\"short_name\":\"soccer\",\"short_names\":[\"soccer\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BASEBALL\",\"unified\":\"26BE\",\"variations\":[\"26BE-FE0F\"],\"docomo\":\"E653\",\"au\":\"E4BA\",\"softbank\":\"E016\",\"google\":\"FE7D1\",\"image\":\"26be.png\",\"sheet_x\":2,\"sheet_y\":18,\"short_name\":\"baseball\",\"short_names\":[\"baseball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SNOWMAN WITHOUT SNOW\",\"unified\":\"26C4\",\"variations\":[\"26C4-FE0F\"],\"docomo\":\"E641\",\"au\":\"E485\",\"softbank\":\"E048\",\"google\":\"FE003\",\"image\":\"26c4.png\",\"sheet_x\":2,\"sheet_y\":19,\"short_name\":\"snowman\",\"short_names\":[\"snowman\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":138,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUN BEHIND CLOUD\",\"unified\":\"26C5\",\"variations\":[\"26C5-FE0F\"],\"docomo\":\"E63E-E63F\",\"au\":\"E48E\",\"softbank\":\"E04A-E049\",\"google\":\"FE00F\",\"image\":\"26c5.png\",\"sheet_x\":2,\"sheet_y\":20,\"short_name\":\"partly_sunny\",\"short_names\":[\"partly_sunny\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":125,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"THUNDER CLOUD AND RAIN\",\"unified\":\"26C8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26c8.png\",\"sheet_x\":2,\"sheet_y\":21,\"short_name\":\"thunder_cloud_and_rain\",\"short_names\":[\"thunder_cloud_and_rain\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":130,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OPHIUCHUS\",\"unified\":\"26CE\",\"variations\":[],\"docomo\":null,\"au\":\"E49B\",\"softbank\":\"E24B\",\"google\":\"FE037\",\"image\":\"26ce.png\",\"sheet_x\":2,\"sheet_y\":22,\"short_name\":\"ophiuchus\",\"short_names\":[\"ophiuchus\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PICK\",\"unified\":\"26CF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26cf.png\",\"sheet_x\":2,\"sheet_y\":23,\"short_name\":\"pick\",\"short_names\":[\"pick\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":59,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HELMET WITH WHITE CROSS\",\"unified\":\"26D1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26d1.png\",\"sheet_x\":2,\"sheet_y\":24,\"short_name\":\"helmet_with_white_cross\",\"short_names\":[\"helmet_with_white_cross\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":193,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHAINS\",\"unified\":\"26D3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26d3.png\",\"sheet_x\":2,\"sheet_y\":25,\"short_name\":\"chains\",\"short_names\":[\"chains\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":62,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NO ENTRY\",\"unified\":\"26D4\",\"variations\":[\"26D4-FE0F\"],\"docomo\":\"E72F\",\"au\":\"E484\",\"softbank\":\"E137\",\"google\":\"FEB26\",\"image\":\"26d4.png\",\"sheet_x\":2,\"sheet_y\":26,\"short_name\":\"no_entry\",\"short_names\":[\"no_entry\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":69,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHINTO SHRINE\",\"unified\":\"26E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26e9.png\",\"sheet_x\":2,\"sheet_y\":27,\"short_name\":\"shinto_shrine\",\"short_names\":[\"shinto_shrine\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":115,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHURCH\",\"unified\":\"26EA\",\"variations\":[\"26EA-FE0F\"],\"docomo\":null,\"au\":\"E5BB\",\"softbank\":\"E037\",\"google\":\"FE4BB\",\"image\":\"26ea.png\",\"sheet_x\":2,\"sheet_y\":28,\"short_name\":\"church\",\"short_names\":[\"church\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":111,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOUNTAIN\",\"unified\":\"26F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26f0.png\",\"sheet_x\":2,\"sheet_y\":29,\"short_name\":\"mountain\",\"short_names\":[\"mountain\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":66,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UMBRELLA ON GROUND\",\"unified\":\"26F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26f1.png\",\"sheet_x\":2,\"sheet_y\":30,\"short_name\":\"umbrella_on_ground\",\"short_names\":[\"umbrella_on_ground\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":98,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FOUNTAIN\",\"unified\":\"26F2\",\"variations\":[\"26F2-FE0F\"],\"docomo\":null,\"au\":\"E5CF\",\"softbank\":\"E121\",\"google\":\"FE4BC\",\"image\":\"26f2.png\",\"sheet_x\":2,\"sheet_y\":31,\"short_name\":\"fountain\",\"short_names\":[\"fountain\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":64,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FLAG IN HOLE\",\"unified\":\"26F3\",\"variations\":[\"26F3-FE0F\"],\"docomo\":\"E654\",\"au\":\"E599\",\"softbank\":\"E014\",\"google\":\"FE7D2\",\"image\":\"26f3.png\",\"sheet_x\":2,\"sheet_y\":32,\"short_name\":\"golf\",\"short_names\":[\"golf\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FERRY\",\"unified\":\"26F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26f4.png\",\"sheet_x\":2,\"sheet_y\":33,\"short_name\":\"ferry\",\"short_names\":[\"ferry\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SAILBOAT\",\"unified\":\"26F5\",\"variations\":[\"26F5-FE0F\"],\"docomo\":\"E6A3\",\"au\":\"E4B4\",\"softbank\":\"E01C\",\"google\":\"FE7EA\",\"image\":\"26f5.png\",\"sheet_x\":2,\"sheet_y\":34,\"short_name\":\"boat\",\"short_names\":[\"boat\",\"sailboat\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SKIER\",\"unified\":\"26F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26f7.png\",\"sheet_x\":2,\"sheet_y\":35,\"short_name\":\"skier\",\"short_names\":[\"skier\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ICE SKATE\",\"unified\":\"26F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26f8.png\",\"sheet_x\":2,\"sheet_y\":36,\"short_name\":\"ice_skate\",\"short_names\":[\"ice_skate\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PERSON WITH BALL\",\"unified\":\"26F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"26f9.png\",\"sheet_x\":2,\"sheet_y\":37,\"short_name\":\"person_with_ball\",\"short_names\":[\"person_with_ball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"26F9-1F3FB\":{\"unified\":\"26F9-1F3FB\",\"image\":\"26f9-1f3fb.png\",\"sheet_x\":2,\"sheet_y\":38,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"26F9-1F3FC\":{\"unified\":\"26F9-1F3FC\",\"image\":\"26f9-1f3fc.png\",\"sheet_x\":2,\"sheet_y\":39,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"26F9-1F3FD\":{\"unified\":\"26F9-1F3FD\",\"image\":\"26f9-1f3fd.png\",\"sheet_x\":2,\"sheet_y\":40,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"26F9-1F3FE\":{\"unified\":\"26F9-1F3FE\",\"image\":\"26f9-1f3fe.png\",\"sheet_x\":3,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"26F9-1F3FF\":{\"unified\":\"26F9-1F3FF\",\"image\":\"26f9-1f3ff.png\",\"sheet_x\":3,\"sheet_y\":1,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"TENT\",\"unified\":\"26FA\",\"variations\":[\"26FA-FE0F\"],\"docomo\":null,\"au\":\"E5D0\",\"softbank\":\"E122\",\"google\":\"FE7FB\",\"image\":\"26fa.png\",\"sheet_x\":3,\"sheet_y\":2,\"short_name\":\"tent\",\"short_names\":[\"tent\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":72,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FUEL PUMP\",\"unified\":\"26FD\",\"variations\":[\"26FD-FE0F\"],\"docomo\":\"E66B\",\"au\":\"E571\",\"softbank\":\"E03A\",\"google\":\"FE7F5\",\"image\":\"26fd.png\",\"sheet_x\":3,\"sheet_y\":3,\"short_name\":\"fuelpump\",\"short_names\":[\"fuelpump\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK SCISSORS\",\"unified\":\"2702\",\"variations\":[\"2702-FE0F\"],\"docomo\":\"E675\",\"au\":\"E516\",\"softbank\":\"E313\",\"google\":\"FE53E\",\"image\":\"2702.png\",\"sheet_x\":3,\"sheet_y\":4,\"short_name\":\"scissors\",\"short_names\":[\"scissors\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":158,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE HEAVY CHECK MARK\",\"unified\":\"2705\",\"variations\":[],\"docomo\":null,\"au\":\"E55E\",\"softbank\":null,\"google\":\"FEB4A\",\"image\":\"2705.png\",\"sheet_x\":3,\"sheet_y\":5,\"short_name\":\"white_check_mark\",\"short_names\":[\"white_check_mark\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":103,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AIRPLANE\",\"unified\":\"2708\",\"variations\":[\"2708-FE0F\"],\"docomo\":\"E662\",\"au\":\"E4B3\",\"softbank\":\"E01D\",\"google\":\"FE7E9\",\"image\":\"2708.png\",\"sheet_x\":3,\"sheet_y\":6,\"short_name\":\"airplane\",\"short_names\":[\"airplane\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ENVELOPE\",\"unified\":\"2709\",\"variations\":[\"2709-FE0F\"],\"docomo\":\"E6D3\",\"au\":\"E521\",\"softbank\":\"E103\",\"google\":\"FE529\",\"image\":\"2709.png\",\"sheet_x\":3,\"sheet_y\":7,\"short_name\":\"email\",\"short_names\":[\"email\",\"envelope\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":111,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RAISED FIST\",\"unified\":\"270A\",\"variations\":[],\"docomo\":\"E693\",\"au\":\"EB83\",\"softbank\":\"E010\",\"google\":\"FEB93\",\"image\":\"270a.png\",\"sheet_x\":3,\"sheet_y\":8,\"short_name\":\"fist\",\"short_names\":[\"fist\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":94,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"270A-1F3FB\":{\"unified\":\"270A-1F3FB\",\"image\":\"270a-1f3fb.png\",\"sheet_x\":3,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270A-1F3FC\":{\"unified\":\"270A-1F3FC\",\"image\":\"270a-1f3fc.png\",\"sheet_x\":3,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270A-1F3FD\":{\"unified\":\"270A-1F3FD\",\"image\":\"270a-1f3fd.png\",\"sheet_x\":3,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270A-1F3FE\":{\"unified\":\"270A-1F3FE\",\"image\":\"270a-1f3fe.png\",\"sheet_x\":3,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270A-1F3FF\":{\"unified\":\"270A-1F3FF\",\"image\":\"270a-1f3ff.png\",\"sheet_x\":3,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"RAISED HAND\",\"unified\":\"270B\",\"variations\":[],\"docomo\":\"E695\",\"au\":\"E5A7\",\"softbank\":\"E012\",\"google\":\"FEB95\",\"image\":\"270b.png\",\"sheet_x\":3,\"sheet_y\":14,\"short_name\":\"hand\",\"short_names\":[\"hand\",\"raised_hand\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":97,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"270B-1F3FB\":{\"unified\":\"270B-1F3FB\",\"image\":\"270b-1f3fb.png\",\"sheet_x\":3,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270B-1F3FC\":{\"unified\":\"270B-1F3FC\",\"image\":\"270b-1f3fc.png\",\"sheet_x\":3,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270B-1F3FD\":{\"unified\":\"270B-1F3FD\",\"image\":\"270b-1f3fd.png\",\"sheet_x\":3,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270B-1F3FE\":{\"unified\":\"270B-1F3FE\",\"image\":\"270b-1f3fe.png\",\"sheet_x\":3,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270B-1F3FF\":{\"unified\":\"270B-1F3FF\",\"image\":\"270b-1f3ff.png\",\"sheet_x\":3,\"sheet_y\":19,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"VICTORY HAND\",\"unified\":\"270C\",\"variations\":[\"270C-FE0F\"],\"docomo\":\"E694\",\"au\":\"E5A6\",\"softbank\":\"E011\",\"google\":\"FEB94\",\"image\":\"270c.png\",\"sheet_x\":3,\"sheet_y\":20,\"short_name\":\"v\",\"short_names\":[\"v\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":95,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"270C-1F3FB\":{\"unified\":\"270C-1F3FB\",\"image\":\"270c-1f3fb.png\",\"sheet_x\":3,\"sheet_y\":21,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270C-1F3FC\":{\"unified\":\"270C-1F3FC\",\"image\":\"270c-1f3fc.png\",\"sheet_x\":3,\"sheet_y\":22,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270C-1F3FD\":{\"unified\":\"270C-1F3FD\",\"image\":\"270c-1f3fd.png\",\"sheet_x\":3,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270C-1F3FE\":{\"unified\":\"270C-1F3FE\",\"image\":\"270c-1f3fe.png\",\"sheet_x\":3,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270C-1F3FF\":{\"unified\":\"270C-1F3FF\",\"image\":\"270c-1f3ff.png\",\"sheet_x\":3,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WRITING HAND\",\"unified\":\"270D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"270d.png\",\"sheet_x\":3,\"sheet_y\":26,\"short_name\":\"writing_hand\",\"short_names\":[\"writing_hand\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":110,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"270D-1F3FB\":{\"unified\":\"270D-1F3FB\",\"image\":\"270d-1f3fb.png\",\"sheet_x\":3,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270D-1F3FC\":{\"unified\":\"270D-1F3FC\",\"image\":\"270d-1f3fc.png\",\"sheet_x\":3,\"sheet_y\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270D-1F3FD\":{\"unified\":\"270D-1F3FD\",\"image\":\"270d-1f3fd.png\",\"sheet_x\":3,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270D-1F3FE\":{\"unified\":\"270D-1F3FE\",\"image\":\"270d-1f3fe.png\",\"sheet_x\":3,\"sheet_y\":30,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"270D-1F3FF\":{\"unified\":\"270D-1F3FF\",\"image\":\"270d-1f3ff.png\",\"sheet_x\":3,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PENCIL\",\"unified\":\"270F\",\"variations\":[\"270F-FE0F\"],\"docomo\":\"E719\",\"au\":\"E4A1\",\"softbank\":\"E301\",\"google\":\"FE539\",\"image\":\"270f.png\",\"sheet_x\":3,\"sheet_y\":32,\"short_name\":\"pencil2\",\"short_names\":[\"pencil2\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":174,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK NIB\",\"unified\":\"2712\",\"variations\":[\"2712-FE0F\"],\"docomo\":\"E6AE\",\"au\":\"EB03\",\"softbank\":null,\"google\":\"FE536\",\"image\":\"2712.png\",\"sheet_x\":3,\"sheet_y\":33,\"short_name\":\"black_nib\",\"short_names\":[\"black_nib\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":172,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY CHECK MARK\",\"unified\":\"2714\",\"variations\":[\"2714-FE0F\"],\"docomo\":null,\"au\":\"E557\",\"softbank\":null,\"google\":\"FEB49\",\"image\":\"2714.png\",\"sheet_x\":3,\"sheet_y\":34,\"short_name\":\"heavy_check_mark\",\"short_names\":[\"heavy_check_mark\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":189,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY MULTIPLICATION X\",\"unified\":\"2716\",\"variations\":[\"2716-FE0F\"],\"docomo\":null,\"au\":\"E54F\",\"softbank\":\"E333\",\"google\":\"FEB53\",\"image\":\"2716.png\",\"sheet_x\":3,\"sheet_y\":35,\"short_name\":\"heavy_multiplication_x\",\"short_names\":[\"heavy_multiplication_x\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":194,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LATIN CROSS\",\"unified\":\"271D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"271d.png\",\"sheet_x\":3,\"sheet_y\":36,\"short_name\":\"latin_cross\",\"short_names\":[\"latin_cross\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STAR OF DAVID\",\"unified\":\"2721\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2721.png\",\"sheet_x\":3,\"sheet_y\":37,\"short_name\":\"star_of_david\",\"short_names\":[\"star_of_david\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPARKLES\",\"unified\":\"2728\",\"variations\":[],\"docomo\":\"E6FA\",\"au\":\"EAAB\",\"softbank\":\"E32E\",\"google\":\"FEB60\",\"image\":\"2728.png\",\"sheet_x\":3,\"sheet_y\":38,\"short_name\":\"sparkles\",\"short_names\":[\"sparkles\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":121,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EIGHT SPOKED ASTERISK\",\"unified\":\"2733\",\"variations\":[\"2733-FE0F\"],\"docomo\":\"E6F8\",\"au\":\"E53E\",\"softbank\":\"E206\",\"google\":\"FEB62\",\"image\":\"2733.png\",\"sheet_x\":3,\"sheet_y\":39,\"short_name\":\"eight_spoked_asterisk\",\"short_names\":[\"eight_spoked_asterisk\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":101,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EIGHT POINTED BLACK STAR\",\"unified\":\"2734\",\"variations\":[\"2734-FE0F\"],\"docomo\":\"E6F8\",\"au\":\"E479\",\"softbank\":\"E205\",\"google\":\"FEB61\",\"image\":\"2734.png\",\"sheet_x\":3,\"sheet_y\":40,\"short_name\":\"eight_pointed_black_star\",\"short_names\":[\"eight_pointed_black_star\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SNOWFLAKE\",\"unified\":\"2744\",\"variations\":[\"2744-FE0F\"],\"docomo\":null,\"au\":\"E48A\",\"softbank\":null,\"google\":\"FE00E\",\"image\":\"2744.png\",\"sheet_x\":4,\"sheet_y\":0,\"short_name\":\"snowflake\",\"short_names\":[\"snowflake\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":135,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPARKLE\",\"unified\":\"2747\",\"variations\":[\"2747-FE0F\"],\"docomo\":\"E6FA\",\"au\":\"E46C\",\"softbank\":\"E32E\",\"google\":\"FEB77\",\"image\":\"2747.png\",\"sheet_x\":4,\"sheet_y\":1,\"short_name\":\"sparkle\",\"short_names\":[\"sparkle\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":100,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CROSS MARK\",\"unified\":\"274C\",\"variations\":[],\"docomo\":null,\"au\":\"E550\",\"softbank\":\"E333\",\"google\":\"FEB45\",\"image\":\"274c.png\",\"sheet_x\":4,\"sheet_y\":2,\"short_name\":\"x\",\"short_names\":[\"x\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":72,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEGATIVE SQUARED CROSS MARK\",\"unified\":\"274E\",\"variations\":[],\"docomo\":null,\"au\":\"E551\",\"softbank\":\"E333\",\"google\":\"FEB46\",\"image\":\"274e.png\",\"sheet_x\":4,\"sheet_y\":3,\"short_name\":\"negative_squared_cross_mark\",\"short_names\":[\"negative_squared_cross_mark\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":102,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK QUESTION MARK ORNAMENT\",\"unified\":\"2753\",\"variations\":[],\"docomo\":null,\"au\":\"E483\",\"softbank\":\"E020\",\"google\":\"FEB09\",\"image\":\"2753.png\",\"sheet_x\":4,\"sheet_y\":4,\"short_name\":\"question\",\"short_names\":[\"question\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":84,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE QUESTION MARK ORNAMENT\",\"unified\":\"2754\",\"variations\":[],\"docomo\":null,\"au\":\"E483\",\"softbank\":\"E336\",\"google\":\"FEB0A\",\"image\":\"2754.png\",\"sheet_x\":4,\"sheet_y\":5,\"short_name\":\"grey_question\",\"short_names\":[\"grey_question\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":85,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE EXCLAMATION MARK ORNAMENT\",\"unified\":\"2755\",\"variations\":[],\"docomo\":\"E702\",\"au\":\"E482\",\"softbank\":\"E337\",\"google\":\"FEB0B\",\"image\":\"2755.png\",\"sheet_x\":4,\"sheet_y\":6,\"short_name\":\"grey_exclamation\",\"short_names\":[\"grey_exclamation\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":83,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY EXCLAMATION MARK SYMBOL\",\"unified\":\"2757\",\"variations\":[\"2757-FE0F\"],\"docomo\":\"E702\",\"au\":\"E482\",\"softbank\":\"E021\",\"google\":\"FEB04\",\"image\":\"2757.png\",\"sheet_x\":4,\"sheet_y\":7,\"short_name\":\"exclamation\",\"short_names\":[\"exclamation\",\"heavy_exclamation_mark\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":82,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY HEART EXCLAMATION MARK ORNAMENT\",\"unified\":\"2763\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"2763.png\",\"sheet_x\":4,\"sheet_y\":8,\"short_name\":\"heavy_heart_exclamation_mark_ornament\",\"short_names\":[\"heavy_heart_exclamation_mark_ornament\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY BLACK HEART\",\"unified\":\"2764\",\"variations\":[\"2764-FE0F\"],\"docomo\":\"E6EC\",\"au\":\"E595\",\"softbank\":\"E022\",\"google\":\"FEB0C\",\"image\":\"2764.png\",\"sheet_x\":4,\"sheet_y\":9,\"short_name\":\"heart\",\"short_names\":[\"heart\"],\"text\":\"<3\",\"texts\":[\"<3\"],\"category\":\"Symbols\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY PLUS SIGN\",\"unified\":\"2795\",\"variations\":[],\"docomo\":null,\"au\":\"E53C\",\"softbank\":null,\"google\":\"FEB51\",\"image\":\"2795.png\",\"sheet_x\":4,\"sheet_y\":10,\"short_name\":\"heavy_plus_sign\",\"short_names\":[\"heavy_plus_sign\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":191,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY MINUS SIGN\",\"unified\":\"2796\",\"variations\":[],\"docomo\":null,\"au\":\"E53D\",\"softbank\":null,\"google\":\"FEB52\",\"image\":\"2796.png\",\"sheet_x\":4,\"sheet_y\":11,\"short_name\":\"heavy_minus_sign\",\"short_names\":[\"heavy_minus_sign\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":192,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY DIVISION SIGN\",\"unified\":\"2797\",\"variations\":[],\"docomo\":null,\"au\":\"E554\",\"softbank\":null,\"google\":\"FEB54\",\"image\":\"2797.png\",\"sheet_x\":4,\"sheet_y\":12,\"short_name\":\"heavy_division_sign\",\"short_names\":[\"heavy_division_sign\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":193,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK RIGHTWARDS ARROW\",\"unified\":\"27A1\",\"variations\":[\"27A1-FE0F\"],\"docomo\":null,\"au\":\"E552\",\"softbank\":\"E234\",\"google\":\"FEAFA\",\"image\":\"27a1.png\",\"sheet_x\":4,\"sheet_y\":13,\"short_name\":\"arrow_right\",\"short_names\":[\"arrow_right\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":163,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CURLY LOOP\",\"unified\":\"27B0\",\"variations\":[],\"docomo\":\"E70A\",\"au\":\"EB31\",\"softbank\":null,\"google\":\"FEB08\",\"image\":\"27b0.png\",\"sheet_x\":4,\"sheet_y\":14,\"short_name\":\"curly_loop\",\"short_names\":[\"curly_loop\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":188,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOUBLE CURLY LOOP\",\"unified\":\"27BF\",\"variations\":[],\"docomo\":\"E6DF\",\"au\":null,\"softbank\":\"E211\",\"google\":\"FE82B\",\"image\":\"27bf.png\",\"sheet_x\":4,\"sheet_y\":15,\"short_name\":\"loop\",\"short_names\":[\"loop\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":106,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ARROW POINTING RIGHTWARDS THEN CURVING UPWARDS\",\"unified\":\"2934\",\"variations\":[\"2934-FE0F\"],\"docomo\":\"E6F5\",\"au\":\"EB2D\",\"softbank\":\"E236\",\"google\":\"FEAF4\",\"image\":\"2934.png\",\"sheet_x\":4,\"sheet_y\":16,\"short_name\":\"arrow_heading_up\",\"short_names\":[\"arrow_heading_up\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":176,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ARROW POINTING RIGHTWARDS THEN CURVING DOWNWARDS\",\"unified\":\"2935\",\"variations\":[\"2935-FE0F\"],\"docomo\":\"E700\",\"au\":\"EB2E\",\"softbank\":\"E238\",\"google\":\"FEAF5\",\"image\":\"2935.png\",\"sheet_x\":4,\"sheet_y\":17,\"short_name\":\"arrow_heading_down\",\"short_names\":[\"arrow_heading_down\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":177,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEFTWARDS BLACK ARROW\",\"unified\":\"2B05\",\"variations\":[\"2B05-FE0F\"],\"docomo\":null,\"au\":\"E553\",\"softbank\":\"E235\",\"google\":\"FEAFB\",\"image\":\"2b05.png\",\"sheet_x\":4,\"sheet_y\":18,\"short_name\":\"arrow_left\",\"short_names\":[\"arrow_left\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":164,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UPWARDS BLACK ARROW\",\"unified\":\"2B06\",\"variations\":[\"2B06-FE0F\"],\"docomo\":null,\"au\":\"E53F\",\"softbank\":\"E232\",\"google\":\"FEAF8\",\"image\":\"2b06.png\",\"sheet_x\":4,\"sheet_y\":19,\"short_name\":\"arrow_up\",\"short_names\":[\"arrow_up\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":165,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOWNWARDS BLACK ARROW\",\"unified\":\"2B07\",\"variations\":[\"2B07-FE0F\"],\"docomo\":null,\"au\":\"E540\",\"softbank\":\"E233\",\"google\":\"FEAF9\",\"image\":\"2b07.png\",\"sheet_x\":4,\"sheet_y\":20,\"short_name\":\"arrow_down\",\"short_names\":[\"arrow_down\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":166,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK LARGE SQUARE\",\"unified\":\"2B1B\",\"variations\":[\"2B1B-FE0F\"],\"docomo\":null,\"au\":\"E549\",\"softbank\":\"E21A\",\"google\":\"FEB6C\",\"image\":\"2b1b.png\",\"sheet_x\":4,\"sheet_y\":21,\"short_name\":\"black_large_square\",\"short_names\":[\"black_large_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":218,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE LARGE SQUARE\",\"unified\":\"2B1C\",\"variations\":[\"2B1C-FE0F\"],\"docomo\":null,\"au\":\"E548\",\"softbank\":\"E21B\",\"google\":\"FEB6B\",\"image\":\"2b1c.png\",\"sheet_x\":4,\"sheet_y\":22,\"short_name\":\"white_large_square\",\"short_names\":[\"white_large_square\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":219,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE MEDIUM STAR\",\"unified\":\"2B50\",\"variations\":[\"2B50-FE0F\"],\"docomo\":null,\"au\":\"E48B\",\"softbank\":\"E32F\",\"google\":\"FEB68\",\"image\":\"2b50.png\",\"sheet_x\":4,\"sheet_y\":23,\"short_name\":\"star\",\"short_names\":[\"star\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":118,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY LARGE CIRCLE\",\"unified\":\"2B55\",\"variations\":[\"2B55-FE0F\"],\"docomo\":\"E6A0\",\"au\":\"EAAD\",\"softbank\":\"E332\",\"google\":\"FEB44\",\"image\":\"2b55.png\",\"sheet_x\":4,\"sheet_y\":24,\"short_name\":\"o\",\"short_names\":[\"o\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":73,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WAVY DASH\",\"unified\":\"3030\",\"variations\":[\"3030-FE0F\"],\"docomo\":\"E709\",\"au\":null,\"softbank\":null,\"google\":\"FEB07\",\"image\":\"3030.png\",\"sheet_x\":4,\"sheet_y\":25,\"short_name\":\"wavy_dash\",\"short_names\":[\"wavy_dash\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":187,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PART ALTERNATION MARK\",\"unified\":\"303D\",\"variations\":[\"303D-FE0F\"],\"docomo\":null,\"au\":null,\"softbank\":\"E12C\",\"google\":\"FE81B\",\"image\":\"303d.png\",\"sheet_x\":4,\"sheet_y\":26,\"short_name\":\"part_alternation_mark\",\"short_names\":[\"part_alternation_mark\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":93,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CIRCLED IDEOGRAPH CONGRATULATION\",\"unified\":\"3297\",\"variations\":[\"3297-FE0F\"],\"docomo\":null,\"au\":\"EA99\",\"softbank\":\"E30D\",\"google\":\"FEB43\",\"image\":\"3297.png\",\"sheet_x\":4,\"sheet_y\":27,\"short_name\":\"congratulations\",\"short_names\":[\"congratulations\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":59,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CIRCLED IDEOGRAPH SECRET\",\"unified\":\"3299\",\"variations\":[\"3299-FE0F\"],\"docomo\":\"E734\",\"au\":\"E4F1\",\"softbank\":\"E315\",\"google\":\"FEB2B\",\"image\":\"3299.png\",\"sheet_x\":4,\"sheet_y\":28,\"short_name\":\"secret\",\"short_names\":[\"secret\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":58,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MAHJONG TILE RED DRAGON\",\"unified\":\"1F004\",\"variations\":[\"1F004-FE0F\"],\"docomo\":null,\"au\":\"E5D1\",\"softbank\":\"E12D\",\"google\":\"FE80B\",\"image\":\"1f004.png\",\"sheet_x\":4,\"sheet_y\":29,\"short_name\":\"mahjong\",\"short_names\":[\"mahjong\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":236,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PLAYING CARD BLACK JOKER\",\"unified\":\"1F0CF\",\"variations\":[],\"docomo\":null,\"au\":\"EB6F\",\"softbank\":null,\"google\":\"FE812\",\"image\":\"1f0cf.png\",\"sheet_x\":4,\"sheet_y\":30,\"short_name\":\"black_joker\",\"short_names\":[\"black_joker\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":235,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEGATIVE SQUARED LATIN CAPITAL LETTER A\",\"unified\":\"1F170\",\"variations\":[\"1F170-FE0F\"],\"docomo\":null,\"au\":\"EB26\",\"softbank\":\"E532\",\"google\":\"FE50B\",\"image\":\"1f170.png\",\"sheet_x\":4,\"sheet_y\":31,\"short_name\":\"a\",\"short_names\":[\"a\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":63,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEGATIVE SQUARED LATIN CAPITAL LETTER B\",\"unified\":\"1F171\",\"variations\":[\"1F171-FE0F\"],\"docomo\":null,\"au\":\"EB27\",\"softbank\":\"E533\",\"google\":\"FE50C\",\"image\":\"1f171.png\",\"sheet_x\":4,\"sheet_y\":32,\"short_name\":\"b\",\"short_names\":[\"b\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":64,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEGATIVE SQUARED LATIN CAPITAL LETTER O\",\"unified\":\"1F17E\",\"variations\":[\"1F17E-FE0F\"],\"docomo\":null,\"au\":\"EB28\",\"softbank\":\"E535\",\"google\":\"FE50E\",\"image\":\"1f17e.png\",\"sheet_x\":4,\"sheet_y\":33,\"short_name\":\"o2\",\"short_names\":[\"o2\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":67,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEGATIVE SQUARED LATIN CAPITAL LETTER P\",\"unified\":\"1F17F\",\"variations\":[\"1F17F-FE0F\"],\"docomo\":\"E66C\",\"au\":\"E4A6\",\"softbank\":\"E14F\",\"google\":\"FE7F6\",\"image\":\"1f17f.png\",\"sheet_x\":4,\"sheet_y\":34,\"short_name\":\"parking\",\"short_names\":[\"parking\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":118,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEGATIVE SQUARED AB\",\"unified\":\"1F18E\",\"variations\":[],\"docomo\":null,\"au\":\"EB29\",\"softbank\":\"E534\",\"google\":\"FE50D\",\"image\":\"1f18e.png\",\"sheet_x\":4,\"sheet_y\":35,\"short_name\":\"ab\",\"short_names\":[\"ab\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":65,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CL\",\"unified\":\"1F191\",\"variations\":[],\"docomo\":\"E6DB\",\"au\":\"E5AB\",\"softbank\":null,\"google\":\"FEB84\",\"image\":\"1f191.png\",\"sheet_x\":4,\"sheet_y\":36,\"short_name\":\"cl\",\"short_names\":[\"cl\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":66,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED COOL\",\"unified\":\"1F192\",\"variations\":[],\"docomo\":null,\"au\":\"EA85\",\"softbank\":\"E214\",\"google\":\"FEB38\",\"image\":\"1f192.png\",\"sheet_x\":4,\"sheet_y\":37,\"short_name\":\"cool\",\"short_names\":[\"cool\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":131,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED FREE\",\"unified\":\"1F193\",\"variations\":[],\"docomo\":\"E6D7\",\"au\":\"E578\",\"softbank\":null,\"google\":\"FEB21\",\"image\":\"1f193.png\",\"sheet_x\":4,\"sheet_y\":38,\"short_name\":\"free\",\"short_names\":[\"free\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":133,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED ID\",\"unified\":\"1F194\",\"variations\":[],\"docomo\":\"E6D8\",\"au\":\"EA88\",\"softbank\":\"E229\",\"google\":\"FEB81\",\"image\":\"1f194.png\",\"sheet_x\":4,\"sheet_y\":39,\"short_name\":\"id\",\"short_names\":[\"id\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED NEW\",\"unified\":\"1F195\",\"variations\":[],\"docomo\":\"E6DD\",\"au\":\"E5B5\",\"softbank\":\"E212\",\"google\":\"FEB36\",\"image\":\"1f195.png\",\"sheet_x\":4,\"sheet_y\":40,\"short_name\":\"new\",\"short_names\":[\"new\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":132,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED NG\",\"unified\":\"1F196\",\"variations\":[],\"docomo\":\"E72F\",\"au\":null,\"softbank\":null,\"google\":\"FEB28\",\"image\":\"1f196.png\",\"sheet_x\":5,\"sheet_y\":0,\"short_name\":\"ng\",\"short_names\":[\"ng\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":128,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED OK\",\"unified\":\"1F197\",\"variations\":[],\"docomo\":\"E70B\",\"au\":\"E5AD\",\"softbank\":\"E24D\",\"google\":\"FEB27\",\"image\":\"1f197.png\",\"sheet_x\":5,\"sheet_y\":1,\"short_name\":\"ok\",\"short_names\":[\"ok\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":129,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED SOS\",\"unified\":\"1F198\",\"variations\":[],\"docomo\":null,\"au\":\"E4E8\",\"softbank\":null,\"google\":\"FEB4F\",\"image\":\"1f198.png\",\"sheet_x\":5,\"sheet_y\":2,\"short_name\":\"sos\",\"short_names\":[\"sos\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":68,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED UP WITH EXCLAMATION MARK\",\"unified\":\"1F199\",\"variations\":[],\"docomo\":null,\"au\":\"E50F\",\"softbank\":\"E213\",\"google\":\"FEB37\",\"image\":\"1f199.png\",\"sheet_x\":5,\"sheet_y\":3,\"short_name\":\"up\",\"short_names\":[\"up\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":130,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED VS\",\"unified\":\"1F19A\",\"variations\":[],\"docomo\":null,\"au\":\"E5D2\",\"softbank\":\"E12E\",\"google\":\"FEB32\",\"image\":\"1f19a.png\",\"sheet_x\":5,\"sheet_y\":4,\"short_name\":\"vs\",\"short_names\":[\"vs\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED KATAKANA KOKO\",\"unified\":\"1F201\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E203\",\"google\":\"FEB24\",\"image\":\"1f201.png\",\"sheet_x\":5,\"sheet_y\":5,\"short_name\":\"koko\",\"short_names\":[\"koko\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":127,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED KATAKANA SA\",\"unified\":\"1F202\",\"variations\":[\"1F202-FE0F\"],\"docomo\":null,\"au\":\"EA87\",\"softbank\":\"E228\",\"google\":\"FEB3F\",\"image\":\"1f202.png\",\"sheet_x\":5,\"sheet_y\":6,\"short_name\":\"sa\",\"short_names\":[\"sa\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":110,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-7121\",\"unified\":\"1F21A\",\"variations\":[\"1F21A-FE0F\"],\"docomo\":null,\"au\":null,\"softbank\":\"E216\",\"google\":\"FEB3A\",\"image\":\"1f21a.png\",\"sheet_x\":5,\"sheet_y\":7,\"short_name\":\"u7121\",\"short_names\":[\"u7121\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-6307\",\"unified\":\"1F22F\",\"variations\":[\"1F22F-FE0F\"],\"docomo\":null,\"au\":\"EA8B\",\"softbank\":\"E22C\",\"google\":\"FEB40\",\"image\":\"1f22f.png\",\"sheet_x\":5,\"sheet_y\":8,\"short_name\":\"u6307\",\"short_names\":[\"u6307\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":98,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-7981\",\"unified\":\"1F232\",\"variations\":[],\"docomo\":\"E738\",\"au\":null,\"softbank\":null,\"google\":\"FEB2E\",\"image\":\"1f232.png\",\"sheet_x\":5,\"sheet_y\":9,\"short_name\":\"u7981\",\"short_names\":[\"u7981\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":62,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-7A7A\",\"unified\":\"1F233\",\"variations\":[],\"docomo\":\"E739\",\"au\":\"EA8A\",\"softbank\":\"E22B\",\"google\":\"FEB2F\",\"image\":\"1f233.png\",\"sheet_x\":5,\"sheet_y\":10,\"short_name\":\"u7a7a\",\"short_names\":[\"u7a7a\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-5408\",\"unified\":\"1F234\",\"variations\":[],\"docomo\":\"E73A\",\"au\":null,\"softbank\":null,\"google\":\"FEB30\",\"image\":\"1f234.png\",\"sheet_x\":5,\"sheet_y\":11,\"short_name\":\"u5408\",\"short_names\":[\"u5408\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":60,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-6E80\",\"unified\":\"1F235\",\"variations\":[],\"docomo\":\"E73B\",\"au\":\"EA89\",\"softbank\":\"E22A\",\"google\":\"FEB31\",\"image\":\"1f235.png\",\"sheet_x\":5,\"sheet_y\":12,\"short_name\":\"u6e80\",\"short_names\":[\"u6e80\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":61,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-6709\",\"unified\":\"1F236\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E215\",\"google\":\"FEB39\",\"image\":\"1f236.png\",\"sheet_x\":5,\"sheet_y\":13,\"short_name\":\"u6709\",\"short_names\":[\"u6709\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-6708\",\"unified\":\"1F237\",\"variations\":[\"1F237-FE0F\"],\"docomo\":null,\"au\":null,\"softbank\":\"E217\",\"google\":\"FEB3B\",\"image\":\"1f237.png\",\"sheet_x\":5,\"sheet_y\":14,\"short_name\":\"u6708\",\"short_names\":[\"u6708\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-7533\",\"unified\":\"1F238\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E218\",\"google\":\"FEB3C\",\"image\":\"1f238.png\",\"sheet_x\":5,\"sheet_y\":15,\"short_name\":\"u7533\",\"short_names\":[\"u7533\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-5272\",\"unified\":\"1F239\",\"variations\":[],\"docomo\":null,\"au\":\"EA86\",\"softbank\":\"E227\",\"google\":\"FEB3E\",\"image\":\"1f239.png\",\"sheet_x\":5,\"sheet_y\":16,\"short_name\":\"u5272\",\"short_names\":[\"u5272\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SQUARED CJK UNIFIED IDEOGRAPH-55B6\",\"unified\":\"1F23A\",\"variations\":[],\"docomo\":null,\"au\":\"EA8C\",\"softbank\":\"E22D\",\"google\":\"FEB41\",\"image\":\"1f23a.png\",\"sheet_x\":5,\"sheet_y\":17,\"short_name\":\"u55b6\",\"short_names\":[\"u55b6\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CIRCLED IDEOGRAPH ADVANTAGE\",\"unified\":\"1F250\",\"variations\":[],\"docomo\":null,\"au\":\"E4F7\",\"softbank\":\"E226\",\"google\":\"FEB3D\",\"image\":\"1f250.png\",\"sheet_x\":5,\"sheet_y\":18,\"short_name\":\"ideograph_advantage\",\"short_names\":[\"ideograph_advantage\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CIRCLED IDEOGRAPH ACCEPT\",\"unified\":\"1F251\",\"variations\":[],\"docomo\":null,\"au\":\"EB01\",\"softbank\":null,\"google\":\"FEB50\",\"image\":\"1f251.png\",\"sheet_x\":5,\"sheet_y\":19,\"short_name\":\"accept\",\"short_names\":[\"accept\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CYCLONE\",\"unified\":\"1F300\",\"variations\":[],\"docomo\":\"E643\",\"au\":\"E469\",\"softbank\":\"E443\",\"google\":\"FE005\",\"image\":\"1f300.png\",\"sheet_x\":5,\"sheet_y\":20,\"short_name\":\"cyclone\",\"short_names\":[\"cyclone\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":105,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FOGGY\",\"unified\":\"1F301\",\"variations\":[],\"docomo\":\"E644\",\"au\":\"E598\",\"softbank\":null,\"google\":\"FE006\",\"image\":\"1f301.png\",\"sheet_x\":5,\"sheet_y\":21,\"short_name\":\"foggy\",\"short_names\":[\"foggy\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":61,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOSED UMBRELLA\",\"unified\":\"1F302\",\"variations\":[],\"docomo\":\"E645\",\"au\":\"EAE8\",\"softbank\":\"E43C\",\"google\":\"FE007\",\"image\":\"1f302.png\",\"sheet_x\":5,\"sheet_y\":22,\"short_name\":\"closed_umbrella\",\"short_names\":[\"closed_umbrella\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":204,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NIGHT WITH STARS\",\"unified\":\"1F303\",\"variations\":[],\"docomo\":\"E6B3\",\"au\":\"EAF1\",\"softbank\":\"E44B\",\"google\":\"FE008\",\"image\":\"1f303.png\",\"sheet_x\":5,\"sheet_y\":23,\"short_name\":\"night_with_stars\",\"short_names\":[\"night_with_stars\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":84,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUNRISE OVER MOUNTAINS\",\"unified\":\"1F304\",\"variations\":[],\"docomo\":\"E63E\",\"au\":\"EAF4\",\"softbank\":\"E04D\",\"google\":\"FE009\",\"image\":\"1f304.png\",\"sheet_x\":5,\"sheet_y\":24,\"short_name\":\"sunrise_over_mountains\",\"short_names\":[\"sunrise_over_mountains\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":77,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUNRISE\",\"unified\":\"1F305\",\"variations\":[],\"docomo\":\"E63E\",\"au\":\"EAF4\",\"softbank\":\"E449\",\"google\":\"FE00A\",\"image\":\"1f305.png\",\"sheet_x\":5,\"sheet_y\":25,\"short_name\":\"sunrise\",\"short_names\":[\"sunrise\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":76,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CITYSCAPE AT DUSK\",\"unified\":\"1F306\",\"variations\":[],\"docomo\":null,\"au\":\"E5DA\",\"softbank\":\"E146\",\"google\":\"FE00B\",\"image\":\"1f306.png\",\"sheet_x\":5,\"sheet_y\":26,\"short_name\":\"city_sunset\",\"short_names\":[\"city_sunset\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":82,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUNSET OVER BUILDINGS\",\"unified\":\"1F307\",\"variations\":[],\"docomo\":\"E63E\",\"au\":\"E5DA\",\"softbank\":\"E44A\",\"google\":\"FE00C\",\"image\":\"1f307.png\",\"sheet_x\":5,\"sheet_y\":27,\"short_name\":\"city_sunrise\",\"short_names\":[\"city_sunrise\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":81,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RAINBOW\",\"unified\":\"1F308\",\"variations\":[],\"docomo\":null,\"au\":\"EAF2\",\"softbank\":\"E44C\",\"google\":\"FE00D\",\"image\":\"1f308.png\",\"sheet_x\":5,\"sheet_y\":28,\"short_name\":\"rainbow\",\"short_names\":[\"rainbow\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":90,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BRIDGE AT NIGHT\",\"unified\":\"1F309\",\"variations\":[],\"docomo\":\"E6B3\",\"au\":\"E4BF\",\"softbank\":\"E44B\",\"google\":\"FE010\",\"image\":\"1f309.png\",\"sheet_x\":5,\"sheet_y\":29,\"short_name\":\"bridge_at_night\",\"short_names\":[\"bridge_at_night\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":85,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WATER WAVE\",\"unified\":\"1F30A\",\"variations\":[],\"docomo\":\"E73F\",\"au\":\"EB7C\",\"softbank\":\"E43E\",\"google\":\"FE038\",\"image\":\"1f30a.png\",\"sheet_x\":5,\"sheet_y\":30,\"short_name\":\"ocean\",\"short_names\":[\"ocean\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":147,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VOLCANO\",\"unified\":\"1F30B\",\"variations\":[],\"docomo\":null,\"au\":\"EB53\",\"softbank\":null,\"google\":\"FE03A\",\"image\":\"1f30b.png\",\"sheet_x\":5,\"sheet_y\":31,\"short_name\":\"volcano\",\"short_names\":[\"volcano\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":69,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MILKY WAY\",\"unified\":\"1F30C\",\"variations\":[],\"docomo\":\"E6B3\",\"au\":\"EB5F\",\"softbank\":\"E44B\",\"google\":\"FE03B\",\"image\":\"1f30c.png\",\"sheet_x\":5,\"sheet_y\":32,\"short_name\":\"milky_way\",\"short_names\":[\"milky_way\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":86,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EARTH GLOBE EUROPE-AFRICA\",\"unified\":\"1F30D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f30d.png\",\"sheet_x\":5,\"sheet_y\":33,\"short_name\":\"earth_africa\",\"short_names\":[\"earth_africa\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":102,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EARTH GLOBE AMERICAS\",\"unified\":\"1F30E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f30e.png\",\"sheet_x\":5,\"sheet_y\":34,\"short_name\":\"earth_americas\",\"short_names\":[\"earth_americas\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":101,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EARTH GLOBE ASIA-AUSTRALIA\",\"unified\":\"1F30F\",\"variations\":[],\"docomo\":null,\"au\":\"E5B3\",\"softbank\":null,\"google\":\"FE039\",\"image\":\"1f30f.png\",\"sheet_x\":5,\"sheet_y\":35,\"short_name\":\"earth_asia\",\"short_names\":[\"earth_asia\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":103,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GLOBE WITH MERIDIANS\",\"unified\":\"1F310\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f310.png\",\"sheet_x\":5,\"sheet_y\":36,\"short_name\":\"globe_with_meridians\",\"short_names\":[\"globe_with_meridians\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":107,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEW MOON SYMBOL\",\"unified\":\"1F311\",\"variations\":[],\"docomo\":\"E69C\",\"au\":\"E5A8\",\"softbank\":null,\"google\":\"FE011\",\"image\":\"1f311.png\",\"sheet_x\":5,\"sheet_y\":37,\"short_name\":\"new_moon\",\"short_names\":[\"new_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":108,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WAXING CRESCENT MOON SYMBOL\",\"unified\":\"1F312\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f312.png\",\"sheet_x\":5,\"sheet_y\":38,\"short_name\":\"waxing_crescent_moon\",\"short_names\":[\"waxing_crescent_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":109,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FIRST QUARTER MOON SYMBOL\",\"unified\":\"1F313\",\"variations\":[],\"docomo\":\"E69E\",\"au\":\"E5AA\",\"softbank\":\"E04C\",\"google\":\"FE013\",\"image\":\"1f313.png\",\"sheet_x\":5,\"sheet_y\":39,\"short_name\":\"first_quarter_moon\",\"short_names\":[\"first_quarter_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":110,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WAXING GIBBOUS MOON SYMBOL\",\"unified\":\"1F314\",\"variations\":[],\"docomo\":\"E69D\",\"au\":\"E5A9\",\"softbank\":\"E04C\",\"google\":\"FE012\",\"image\":\"1f314.png\",\"sheet_x\":5,\"sheet_y\":40,\"short_name\":\"moon\",\"short_names\":[\"moon\",\"waxing_gibbous_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":111,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FULL MOON SYMBOL\",\"unified\":\"1F315\",\"variations\":[],\"docomo\":\"E6A0\",\"au\":null,\"softbank\":null,\"google\":\"FE015\",\"image\":\"1f315.png\",\"sheet_x\":6,\"sheet_y\":0,\"short_name\":\"full_moon\",\"short_names\":[\"full_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":104,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WANING GIBBOUS MOON SYMBOL\",\"unified\":\"1F316\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f316.png\",\"sheet_x\":6,\"sheet_y\":1,\"short_name\":\"waning_gibbous_moon\",\"short_names\":[\"waning_gibbous_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":105,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LAST QUARTER MOON SYMBOL\",\"unified\":\"1F317\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f317.png\",\"sheet_x\":6,\"sheet_y\":2,\"short_name\":\"last_quarter_moon\",\"short_names\":[\"last_quarter_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":106,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WANING CRESCENT MOON SYMBOL\",\"unified\":\"1F318\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f318.png\",\"sheet_x\":6,\"sheet_y\":3,\"short_name\":\"waning_crescent_moon\",\"short_names\":[\"waning_crescent_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":107,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CRESCENT MOON\",\"unified\":\"1F319\",\"variations\":[],\"docomo\":\"E69F\",\"au\":\"E486\",\"softbank\":\"E04C\",\"google\":\"FE014\",\"image\":\"1f319.png\",\"sheet_x\":6,\"sheet_y\":4,\"short_name\":\"crescent_moon\",\"short_names\":[\"crescent_moon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":117,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEW MOON WITH FACE\",\"unified\":\"1F31A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f31a.png\",\"sheet_x\":6,\"sheet_y\":5,\"short_name\":\"new_moon_with_face\",\"short_names\":[\"new_moon_with_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":112,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FIRST QUARTER MOON WITH FACE\",\"unified\":\"1F31B\",\"variations\":[],\"docomo\":\"E69E\",\"au\":\"E489\",\"softbank\":\"E04C\",\"google\":\"FE016\",\"image\":\"1f31b.png\",\"sheet_x\":6,\"sheet_y\":6,\"short_name\":\"first_quarter_moon_with_face\",\"short_names\":[\"first_quarter_moon_with_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":114,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LAST QUARTER MOON WITH FACE\",\"unified\":\"1F31C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f31c.png\",\"sheet_x\":6,\"sheet_y\":7,\"short_name\":\"last_quarter_moon_with_face\",\"short_names\":[\"last_quarter_moon_with_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":115,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FULL MOON WITH FACE\",\"unified\":\"1F31D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f31d.png\",\"sheet_x\":6,\"sheet_y\":8,\"short_name\":\"full_moon_with_face\",\"short_names\":[\"full_moon_with_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":113,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUN WITH FACE\",\"unified\":\"1F31E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f31e.png\",\"sheet_x\":6,\"sheet_y\":9,\"short_name\":\"sun_with_face\",\"short_names\":[\"sun_with_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":116,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GLOWING STAR\",\"unified\":\"1F31F\",\"variations\":[],\"docomo\":null,\"au\":\"E48B\",\"softbank\":\"E335\",\"google\":\"FEB69\",\"image\":\"1f31f.png\",\"sheet_x\":6,\"sheet_y\":10,\"short_name\":\"star2\",\"short_names\":[\"star2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":119,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHOOTING STAR\",\"unified\":\"1F320\",\"variations\":[],\"docomo\":null,\"au\":\"E468\",\"softbank\":null,\"google\":\"FEB6A\",\"image\":\"1f320.png\",\"sheet_x\":6,\"sheet_y\":11,\"short_name\":\"stars\",\"short_names\":[\"stars\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":87,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"THERMOMETER\",\"unified\":\"1F321\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f321.png\",\"sheet_x\":6,\"sheet_y\":12,\"short_name\":\"thermometer\",\"short_names\":[\"thermometer\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":83,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE SUN WITH SMALL CLOUD\",\"unified\":\"1F324\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f324.png\",\"sheet_x\":6,\"sheet_y\":13,\"short_name\":\"mostly_sunny\",\"short_names\":[\"mostly_sunny\",\"sun_small_cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":124,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE SUN BEHIND CLOUD\",\"unified\":\"1F325\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f325.png\",\"sheet_x\":6,\"sheet_y\":14,\"short_name\":\"barely_sunny\",\"short_names\":[\"barely_sunny\",\"sun_behind_cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":126,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE SUN BEHIND CLOUD WITH RAIN\",\"unified\":\"1F326\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f326.png\",\"sheet_x\":6,\"sheet_y\":15,\"short_name\":\"partly_sunny_rain\",\"short_names\":[\"partly_sunny_rain\",\"sun_behind_rain_cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":127,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOUD WITH RAIN\",\"unified\":\"1F327\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f327.png\",\"sheet_x\":6,\"sheet_y\":16,\"short_name\":\"rain_cloud\",\"short_names\":[\"rain_cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":129,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOUD WITH SNOW\",\"unified\":\"1F328\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f328.png\",\"sheet_x\":6,\"sheet_y\":17,\"short_name\":\"snow_cloud\",\"short_names\":[\"snow_cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":136,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOUD WITH LIGHTNING\",\"unified\":\"1F329\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f329.png\",\"sheet_x\":6,\"sheet_y\":18,\"short_name\":\"lightning\",\"short_names\":[\"lightning\",\"lightning_cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":131,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOUD WITH TORNADO\",\"unified\":\"1F32A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f32a.png\",\"sheet_x\":6,\"sheet_y\":19,\"short_name\":\"tornado\",\"short_names\":[\"tornado\",\"tornado_cloud\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":141,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FOG\",\"unified\":\"1F32B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f32b.png\",\"sheet_x\":6,\"sheet_y\":20,\"short_name\":\"fog\",\"short_names\":[\"fog\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":142,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WIND BLOWING FACE\",\"unified\":\"1F32C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f32c.png\",\"sheet_x\":6,\"sheet_y\":21,\"short_name\":\"wind_blowing_face\",\"short_names\":[\"wind_blowing_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":139,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOT DOG\",\"unified\":\"1F32D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f32d.png\",\"sheet_x\":6,\"sheet_y\":22,\"short_name\":\"hotdog\",\"short_names\":[\"hotdog\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TACO\",\"unified\":\"1F32E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f32e.png\",\"sheet_x\":6,\"sheet_y\":23,\"short_name\":\"taco\",\"short_names\":[\"taco\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BURRITO\",\"unified\":\"1F32F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f32f.png\",\"sheet_x\":6,\"sheet_y\":24,\"short_name\":\"burrito\",\"short_names\":[\"burrito\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHESTNUT\",\"unified\":\"1F330\",\"variations\":[],\"docomo\":null,\"au\":\"EB38\",\"softbank\":null,\"google\":\"FE04C\",\"image\":\"1f330.png\",\"sheet_x\":6,\"sheet_y\":25,\"short_name\":\"chestnut\",\"short_names\":[\"chestnut\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":97,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SEEDLING\",\"unified\":\"1F331\",\"variations\":[],\"docomo\":\"E746\",\"au\":\"EB7D\",\"softbank\":\"E110\",\"google\":\"FE03E\",\"image\":\"1f331.png\",\"sheet_x\":6,\"sheet_y\":26,\"short_name\":\"seedling\",\"short_names\":[\"seedling\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":79,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EVERGREEN TREE\",\"unified\":\"1F332\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f332.png\",\"sheet_x\":6,\"sheet_y\":27,\"short_name\":\"evergreen_tree\",\"short_names\":[\"evergreen_tree\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":76,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DECIDUOUS TREE\",\"unified\":\"1F333\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f333.png\",\"sheet_x\":6,\"sheet_y\":28,\"short_name\":\"deciduous_tree\",\"short_names\":[\"deciduous_tree\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":77,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PALM TREE\",\"unified\":\"1F334\",\"variations\":[],\"docomo\":null,\"au\":\"E4E2\",\"softbank\":\"E307\",\"google\":\"FE047\",\"image\":\"1f334.png\",\"sheet_x\":6,\"sheet_y\":29,\"short_name\":\"palm_tree\",\"short_names\":[\"palm_tree\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":78,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CACTUS\",\"unified\":\"1F335\",\"variations\":[],\"docomo\":null,\"au\":\"EA96\",\"softbank\":\"E308\",\"google\":\"FE048\",\"image\":\"1f335.png\",\"sheet_x\":6,\"sheet_y\":30,\"short_name\":\"cactus\",\"short_names\":[\"cactus\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":74,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOT PEPPER\",\"unified\":\"1F336\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f336.png\",\"sheet_x\":6,\"sheet_y\":31,\"short_name\":\"hot_pepper\",\"short_names\":[\"hot_pepper\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TULIP\",\"unified\":\"1F337\",\"variations\":[],\"docomo\":\"E743\",\"au\":\"E4E4\",\"softbank\":\"E304\",\"google\":\"FE03D\",\"image\":\"1f337.png\",\"sheet_x\":6,\"sheet_y\":32,\"short_name\":\"tulip\",\"short_names\":[\"tulip\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":92,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHERRY BLOSSOM\",\"unified\":\"1F338\",\"variations\":[],\"docomo\":\"E748\",\"au\":\"E4CA\",\"softbank\":\"E030\",\"google\":\"FE040\",\"image\":\"1f338.png\",\"sheet_x\":6,\"sheet_y\":33,\"short_name\":\"cherry_blossom\",\"short_names\":[\"cherry_blossom\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":94,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROSE\",\"unified\":\"1F339\",\"variations\":[],\"docomo\":null,\"au\":\"E5BA\",\"softbank\":\"E032\",\"google\":\"FE041\",\"image\":\"1f339.png\",\"sheet_x\":6,\"sheet_y\":34,\"short_name\":\"rose\",\"short_names\":[\"rose\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":91,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HIBISCUS\",\"unified\":\"1F33A\",\"variations\":[],\"docomo\":null,\"au\":\"EA94\",\"softbank\":\"E303\",\"google\":\"FE045\",\"image\":\"1f33a.png\",\"sheet_x\":6,\"sheet_y\":35,\"short_name\":\"hibiscus\",\"short_names\":[\"hibiscus\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":89,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUNFLOWER\",\"unified\":\"1F33B\",\"variations\":[],\"docomo\":null,\"au\":\"E4E3\",\"softbank\":\"E305\",\"google\":\"FE046\",\"image\":\"1f33b.png\",\"sheet_x\":6,\"sheet_y\":36,\"short_name\":\"sunflower\",\"short_names\":[\"sunflower\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":90,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLOSSOM\",\"unified\":\"1F33C\",\"variations\":[],\"docomo\":null,\"au\":\"EB49\",\"softbank\":\"E305\",\"google\":\"FE04D\",\"image\":\"1f33c.png\",\"sheet_x\":6,\"sheet_y\":37,\"short_name\":\"blossom\",\"short_names\":[\"blossom\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":93,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EAR OF MAIZE\",\"unified\":\"1F33D\",\"variations\":[],\"docomo\":null,\"au\":\"EB36\",\"softbank\":null,\"google\":\"FE04A\",\"image\":\"1f33d.png\",\"sheet_x\":6,\"sheet_y\":38,\"short_name\":\"corn\",\"short_names\":[\"corn\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EAR OF RICE\",\"unified\":\"1F33E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E444\",\"google\":\"FE049\",\"image\":\"1f33e.png\",\"sheet_x\":6,\"sheet_y\":39,\"short_name\":\"ear_of_rice\",\"short_names\":[\"ear_of_rice\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":88,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HERB\",\"unified\":\"1F33F\",\"variations\":[],\"docomo\":\"E741\",\"au\":\"EB82\",\"softbank\":\"E110\",\"google\":\"FE04E\",\"image\":\"1f33f.png\",\"sheet_x\":6,\"sheet_y\":40,\"short_name\":\"herb\",\"short_names\":[\"herb\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":80,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FOUR LEAF CLOVER\",\"unified\":\"1F340\",\"variations\":[],\"docomo\":\"E741\",\"au\":\"E513\",\"softbank\":\"E110\",\"google\":\"FE03C\",\"image\":\"1f340.png\",\"sheet_x\":7,\"sheet_y\":0,\"short_name\":\"four_leaf_clover\",\"short_names\":[\"four_leaf_clover\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":82,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MAPLE LEAF\",\"unified\":\"1F341\",\"variations\":[],\"docomo\":\"E747\",\"au\":\"E4CE\",\"softbank\":\"E118\",\"google\":\"FE03F\",\"image\":\"1f341.png\",\"sheet_x\":7,\"sheet_y\":1,\"short_name\":\"maple_leaf\",\"short_names\":[\"maple_leaf\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":87,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FALLEN LEAF\",\"unified\":\"1F342\",\"variations\":[],\"docomo\":\"E747\",\"au\":\"E5CD\",\"softbank\":\"E119\",\"google\":\"FE042\",\"image\":\"1f342.png\",\"sheet_x\":7,\"sheet_y\":2,\"short_name\":\"fallen_leaf\",\"short_names\":[\"fallen_leaf\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":86,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEAF FLUTTERING IN WIND\",\"unified\":\"1F343\",\"variations\":[],\"docomo\":null,\"au\":\"E5CD\",\"softbank\":\"E447\",\"google\":\"FE043\",\"image\":\"1f343.png\",\"sheet_x\":7,\"sheet_y\":3,\"short_name\":\"leaves\",\"short_names\":[\"leaves\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":85,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MUSHROOM\",\"unified\":\"1F344\",\"variations\":[],\"docomo\":null,\"au\":\"EB37\",\"softbank\":null,\"google\":\"FE04B\",\"image\":\"1f344.png\",\"sheet_x\":7,\"sheet_y\":4,\"short_name\":\"mushroom\",\"short_names\":[\"mushroom\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":96,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TOMATO\",\"unified\":\"1F345\",\"variations\":[],\"docomo\":null,\"au\":\"EABB\",\"softbank\":\"E349\",\"google\":\"FE055\",\"image\":\"1f345.png\",\"sheet_x\":7,\"sheet_y\":5,\"short_name\":\"tomato\",\"short_names\":[\"tomato\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AUBERGINE\",\"unified\":\"1F346\",\"variations\":[],\"docomo\":null,\"au\":\"EABC\",\"softbank\":\"E34A\",\"google\":\"FE056\",\"image\":\"1f346.png\",\"sheet_x\":7,\"sheet_y\":6,\"short_name\":\"eggplant\",\"short_names\":[\"eggplant\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GRAPES\",\"unified\":\"1F347\",\"variations\":[],\"docomo\":null,\"au\":\"EB34\",\"softbank\":null,\"google\":\"FE059\",\"image\":\"1f347.png\",\"sheet_x\":7,\"sheet_y\":7,\"short_name\":\"grapes\",\"short_names\":[\"grapes\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MELON\",\"unified\":\"1F348\",\"variations\":[],\"docomo\":null,\"au\":\"EB32\",\"softbank\":null,\"google\":\"FE057\",\"image\":\"1f348.png\",\"sheet_x\":7,\"sheet_y\":8,\"short_name\":\"melon\",\"short_names\":[\"melon\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WATERMELON\",\"unified\":\"1F349\",\"variations\":[],\"docomo\":null,\"au\":\"E4CD\",\"softbank\":\"E348\",\"google\":\"FE054\",\"image\":\"1f349.png\",\"sheet_x\":7,\"sheet_y\":9,\"short_name\":\"watermelon\",\"short_names\":[\"watermelon\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TANGERINE\",\"unified\":\"1F34A\",\"variations\":[],\"docomo\":null,\"au\":\"EABA\",\"softbank\":\"E346\",\"google\":\"FE052\",\"image\":\"1f34a.png\",\"sheet_x\":7,\"sheet_y\":10,\"short_name\":\"tangerine\",\"short_names\":[\"tangerine\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEMON\",\"unified\":\"1F34B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f34b.png\",\"sheet_x\":7,\"sheet_y\":11,\"short_name\":\"lemon\",\"short_names\":[\"lemon\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BANANA\",\"unified\":\"1F34C\",\"variations\":[],\"docomo\":\"E744\",\"au\":\"EB35\",\"softbank\":null,\"google\":\"FE050\",\"image\":\"1f34c.png\",\"sheet_x\":7,\"sheet_y\":12,\"short_name\":\"banana\",\"short_names\":[\"banana\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PINEAPPLE\",\"unified\":\"1F34D\",\"variations\":[],\"docomo\":null,\"au\":\"EB33\",\"softbank\":null,\"google\":\"FE058\",\"image\":\"1f34d.png\",\"sheet_x\":7,\"sheet_y\":13,\"short_name\":\"pineapple\",\"short_names\":[\"pineapple\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RED APPLE\",\"unified\":\"1F34E\",\"variations\":[],\"docomo\":\"E745\",\"au\":\"EAB9\",\"softbank\":\"E345\",\"google\":\"FE051\",\"image\":\"1f34e.png\",\"sheet_x\":7,\"sheet_y\":14,\"short_name\":\"apple\",\"short_names\":[\"apple\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GREEN APPLE\",\"unified\":\"1F34F\",\"variations\":[],\"docomo\":\"E745\",\"au\":\"EB5A\",\"softbank\":\"E345\",\"google\":\"FE05B\",\"image\":\"1f34f.png\",\"sheet_x\":7,\"sheet_y\":15,\"short_name\":\"green_apple\",\"short_names\":[\"green_apple\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PEAR\",\"unified\":\"1F350\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f350.png\",\"sheet_x\":7,\"sheet_y\":16,\"short_name\":\"pear\",\"short_names\":[\"pear\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PEACH\",\"unified\":\"1F351\",\"variations\":[],\"docomo\":null,\"au\":\"EB39\",\"softbank\":null,\"google\":\"FE05A\",\"image\":\"1f351.png\",\"sheet_x\":7,\"sheet_y\":17,\"short_name\":\"peach\",\"short_names\":[\"peach\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHERRIES\",\"unified\":\"1F352\",\"variations\":[],\"docomo\":\"E742\",\"au\":\"E4D2\",\"softbank\":null,\"google\":\"FE04F\",\"image\":\"1f352.png\",\"sheet_x\":7,\"sheet_y\":18,\"short_name\":\"cherries\",\"short_names\":[\"cherries\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STRAWBERRY\",\"unified\":\"1F353\",\"variations\":[],\"docomo\":null,\"au\":\"E4D4\",\"softbank\":\"E347\",\"google\":\"FE053\",\"image\":\"1f353.png\",\"sheet_x\":7,\"sheet_y\":19,\"short_name\":\"strawberry\",\"short_names\":[\"strawberry\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HAMBURGER\",\"unified\":\"1F354\",\"variations\":[],\"docomo\":\"E673\",\"au\":\"E4D6\",\"softbank\":\"E120\",\"google\":\"FE960\",\"image\":\"1f354.png\",\"sheet_x\":7,\"sheet_y\":20,\"short_name\":\"hamburger\",\"short_names\":[\"hamburger\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLICE OF PIZZA\",\"unified\":\"1F355\",\"variations\":[],\"docomo\":null,\"au\":\"EB3B\",\"softbank\":null,\"google\":\"FE975\",\"image\":\"1f355.png\",\"sheet_x\":7,\"sheet_y\":21,\"short_name\":\"pizza\",\"short_names\":[\"pizza\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MEAT ON BONE\",\"unified\":\"1F356\",\"variations\":[],\"docomo\":null,\"au\":\"E4C4\",\"softbank\":null,\"google\":\"FE972\",\"image\":\"1f356.png\",\"sheet_x\":7,\"sheet_y\":22,\"short_name\":\"meat_on_bone\",\"short_names\":[\"meat_on_bone\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POULTRY LEG\",\"unified\":\"1F357\",\"variations\":[],\"docomo\":null,\"au\":\"EB3C\",\"softbank\":null,\"google\":\"FE976\",\"image\":\"1f357.png\",\"sheet_x\":7,\"sheet_y\":23,\"short_name\":\"poultry_leg\",\"short_names\":[\"poultry_leg\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RICE CRACKER\",\"unified\":\"1F358\",\"variations\":[],\"docomo\":null,\"au\":\"EAB3\",\"softbank\":\"E33D\",\"google\":\"FE969\",\"image\":\"1f358.png\",\"sheet_x\":7,\"sheet_y\":24,\"short_name\":\"rice_cracker\",\"short_names\":[\"rice_cracker\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RICE BALL\",\"unified\":\"1F359\",\"variations\":[],\"docomo\":\"E749\",\"au\":\"E4D5\",\"softbank\":\"E342\",\"google\":\"FE961\",\"image\":\"1f359.png\",\"sheet_x\":7,\"sheet_y\":25,\"short_name\":\"rice_ball\",\"short_names\":[\"rice_ball\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COOKED RICE\",\"unified\":\"1F35A\",\"variations\":[],\"docomo\":\"E74C\",\"au\":\"EAB4\",\"softbank\":\"E33E\",\"google\":\"FE96A\",\"image\":\"1f35a.png\",\"sheet_x\":7,\"sheet_y\":26,\"short_name\":\"rice\",\"short_names\":[\"rice\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CURRY AND RICE\",\"unified\":\"1F35B\",\"variations\":[],\"docomo\":null,\"au\":\"EAB6\",\"softbank\":\"E341\",\"google\":\"FE96C\",\"image\":\"1f35b.png\",\"sheet_x\":7,\"sheet_y\":27,\"short_name\":\"curry\",\"short_names\":[\"curry\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STEAMING BOWL\",\"unified\":\"1F35C\",\"variations\":[],\"docomo\":\"E74C\",\"au\":\"E5B4\",\"softbank\":\"E340\",\"google\":\"FE963\",\"image\":\"1f35c.png\",\"sheet_x\":7,\"sheet_y\":28,\"short_name\":\"ramen\",\"short_names\":[\"ramen\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPAGHETTI\",\"unified\":\"1F35D\",\"variations\":[],\"docomo\":null,\"au\":\"EAB5\",\"softbank\":\"E33F\",\"google\":\"FE96B\",\"image\":\"1f35d.png\",\"sheet_x\":7,\"sheet_y\":29,\"short_name\":\"spaghetti\",\"short_names\":[\"spaghetti\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BREAD\",\"unified\":\"1F35E\",\"variations\":[],\"docomo\":\"E74D\",\"au\":\"EAAF\",\"softbank\":\"E339\",\"google\":\"FE964\",\"image\":\"1f35e.png\",\"sheet_x\":7,\"sheet_y\":30,\"short_name\":\"bread\",\"short_names\":[\"bread\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FRENCH FRIES\",\"unified\":\"1F35F\",\"variations\":[],\"docomo\":null,\"au\":\"EAB1\",\"softbank\":\"E33B\",\"google\":\"FE967\",\"image\":\"1f35f.png\",\"sheet_x\":7,\"sheet_y\":31,\"short_name\":\"fries\",\"short_names\":[\"fries\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROASTED SWEET POTATO\",\"unified\":\"1F360\",\"variations\":[],\"docomo\":null,\"au\":\"EB3A\",\"softbank\":null,\"google\":\"FE974\",\"image\":\"1f360.png\",\"sheet_x\":7,\"sheet_y\":32,\"short_name\":\"sweet_potato\",\"short_names\":[\"sweet_potato\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DANGO\",\"unified\":\"1F361\",\"variations\":[],\"docomo\":null,\"au\":\"EAB2\",\"softbank\":\"E33C\",\"google\":\"FE968\",\"image\":\"1f361.png\",\"sheet_x\":7,\"sheet_y\":33,\"short_name\":\"dango\",\"short_names\":[\"dango\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ODEN\",\"unified\":\"1F362\",\"variations\":[],\"docomo\":null,\"au\":\"EAB7\",\"softbank\":\"E343\",\"google\":\"FE96D\",\"image\":\"1f362.png\",\"sheet_x\":7,\"sheet_y\":34,\"short_name\":\"oden\",\"short_names\":[\"oden\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUSHI\",\"unified\":\"1F363\",\"variations\":[],\"docomo\":null,\"au\":\"EAB8\",\"softbank\":\"E344\",\"google\":\"FE96E\",\"image\":\"1f363.png\",\"sheet_x\":7,\"sheet_y\":35,\"short_name\":\"sushi\",\"short_names\":[\"sushi\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FRIED SHRIMP\",\"unified\":\"1F364\",\"variations\":[],\"docomo\":null,\"au\":\"EB70\",\"softbank\":null,\"google\":\"FE97F\",\"image\":\"1f364.png\",\"sheet_x\":7,\"sheet_y\":36,\"short_name\":\"fried_shrimp\",\"short_names\":[\"fried_shrimp\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FISH CAKE WITH SWIRL DESIGN\",\"unified\":\"1F365\",\"variations\":[],\"docomo\":\"E643\",\"au\":\"E4ED\",\"softbank\":null,\"google\":\"FE973\",\"image\":\"1f365.png\",\"sheet_x\":7,\"sheet_y\":37,\"short_name\":\"fish_cake\",\"short_names\":[\"fish_cake\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SOFT ICE CREAM\",\"unified\":\"1F366\",\"variations\":[],\"docomo\":null,\"au\":\"EAB0\",\"softbank\":\"E33A\",\"google\":\"FE966\",\"image\":\"1f366.png\",\"sheet_x\":7,\"sheet_y\":38,\"short_name\":\"icecream\",\"short_names\":[\"icecream\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHAVED ICE\",\"unified\":\"1F367\",\"variations\":[],\"docomo\":null,\"au\":\"EAEA\",\"softbank\":\"E43F\",\"google\":\"FE971\",\"image\":\"1f367.png\",\"sheet_x\":7,\"sheet_y\":39,\"short_name\":\"shaved_ice\",\"short_names\":[\"shaved_ice\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ICE CREAM\",\"unified\":\"1F368\",\"variations\":[],\"docomo\":null,\"au\":\"EB4A\",\"softbank\":null,\"google\":\"FE977\",\"image\":\"1f368.png\",\"sheet_x\":7,\"sheet_y\":40,\"short_name\":\"ice_cream\",\"short_names\":[\"ice_cream\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOUGHNUT\",\"unified\":\"1F369\",\"variations\":[],\"docomo\":null,\"au\":\"EB4B\",\"softbank\":null,\"google\":\"FE978\",\"image\":\"1f369.png\",\"sheet_x\":8,\"sheet_y\":0,\"short_name\":\"doughnut\",\"short_names\":[\"doughnut\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COOKIE\",\"unified\":\"1F36A\",\"variations\":[],\"docomo\":null,\"au\":\"EB4C\",\"softbank\":null,\"google\":\"FE979\",\"image\":\"1f36a.png\",\"sheet_x\":8,\"sheet_y\":1,\"short_name\":\"cookie\",\"short_names\":[\"cookie\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHOCOLATE BAR\",\"unified\":\"1F36B\",\"variations\":[],\"docomo\":null,\"au\":\"EB4D\",\"softbank\":null,\"google\":\"FE97A\",\"image\":\"1f36b.png\",\"sheet_x\":8,\"sheet_y\":2,\"short_name\":\"chocolate_bar\",\"short_names\":[\"chocolate_bar\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CANDY\",\"unified\":\"1F36C\",\"variations\":[],\"docomo\":null,\"au\":\"EB4E\",\"softbank\":null,\"google\":\"FE97B\",\"image\":\"1f36c.png\",\"sheet_x\":8,\"sheet_y\":3,\"short_name\":\"candy\",\"short_names\":[\"candy\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOLLIPOP\",\"unified\":\"1F36D\",\"variations\":[],\"docomo\":null,\"au\":\"EB4F\",\"softbank\":null,\"google\":\"FE97C\",\"image\":\"1f36d.png\",\"sheet_x\":8,\"sheet_y\":4,\"short_name\":\"lollipop\",\"short_names\":[\"lollipop\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CUSTARD\",\"unified\":\"1F36E\",\"variations\":[],\"docomo\":null,\"au\":\"EB56\",\"softbank\":null,\"google\":\"FE97D\",\"image\":\"1f36e.png\",\"sheet_x\":8,\"sheet_y\":5,\"short_name\":\"custard\",\"short_names\":[\"custard\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HONEY POT\",\"unified\":\"1F36F\",\"variations\":[],\"docomo\":null,\"au\":\"EB59\",\"softbank\":null,\"google\":\"FE97E\",\"image\":\"1f36f.png\",\"sheet_x\":8,\"sheet_y\":6,\"short_name\":\"honey_pot\",\"short_names\":[\"honey_pot\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHORTCAKE\",\"unified\":\"1F370\",\"variations\":[],\"docomo\":\"E74A\",\"au\":\"E4D0\",\"softbank\":\"E046\",\"google\":\"FE962\",\"image\":\"1f370.png\",\"sheet_x\":8,\"sheet_y\":7,\"short_name\":\"cake\",\"short_names\":[\"cake\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BENTO BOX\",\"unified\":\"1F371\",\"variations\":[],\"docomo\":null,\"au\":\"EABD\",\"softbank\":\"E34C\",\"google\":\"FE96F\",\"image\":\"1f371.png\",\"sheet_x\":8,\"sheet_y\":8,\"short_name\":\"bento\",\"short_names\":[\"bento\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POT OF FOOD\",\"unified\":\"1F372\",\"variations\":[],\"docomo\":null,\"au\":\"EABE\",\"softbank\":\"E34D\",\"google\":\"FE970\",\"image\":\"1f372.png\",\"sheet_x\":8,\"sheet_y\":9,\"short_name\":\"stew\",\"short_names\":[\"stew\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COOKING\",\"unified\":\"1F373\",\"variations\":[],\"docomo\":null,\"au\":\"E4D1\",\"softbank\":\"E147\",\"google\":\"FE965\",\"image\":\"1f373.png\",\"sheet_x\":8,\"sheet_y\":10,\"short_name\":\"egg\",\"short_names\":[\"egg\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FORK AND KNIFE\",\"unified\":\"1F374\",\"variations\":[],\"docomo\":\"E66F\",\"au\":\"E4AC\",\"softbank\":\"E043\",\"google\":\"FE980\",\"image\":\"1f374.png\",\"sheet_x\":8,\"sheet_y\":11,\"short_name\":\"fork_and_knife\",\"short_names\":[\"fork_and_knife\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":66,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TEACUP WITHOUT HANDLE\",\"unified\":\"1F375\",\"variations\":[],\"docomo\":\"E71E\",\"au\":\"EAAE\",\"softbank\":\"E338\",\"google\":\"FE984\",\"image\":\"1f375.png\",\"sheet_x\":8,\"sheet_y\":12,\"short_name\":\"tea\",\"short_names\":[\"tea\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":63,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SAKE BOTTLE AND CUP\",\"unified\":\"1F376\",\"variations\":[],\"docomo\":\"E74B\",\"au\":\"EA97\",\"softbank\":\"E30B\",\"google\":\"FE985\",\"image\":\"1f376.png\",\"sheet_x\":8,\"sheet_y\":13,\"short_name\":\"sake\",\"short_names\":[\"sake\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":62,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WINE GLASS\",\"unified\":\"1F377\",\"variations\":[],\"docomo\":\"E756\",\"au\":\"E4C1\",\"softbank\":\"E044\",\"google\":\"FE986\",\"image\":\"1f377.png\",\"sheet_x\":8,\"sheet_y\":14,\"short_name\":\"wine_glass\",\"short_names\":[\"wine_glass\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":58,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COCKTAIL GLASS\",\"unified\":\"1F378\",\"variations\":[],\"docomo\":\"E671\",\"au\":\"E4C2\",\"softbank\":\"E044\",\"google\":\"FE982\",\"image\":\"1f378.png\",\"sheet_x\":8,\"sheet_y\":15,\"short_name\":\"cocktail\",\"short_names\":[\"cocktail\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":59,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TROPICAL DRINK\",\"unified\":\"1F379\",\"variations\":[],\"docomo\":\"E671\",\"au\":\"EB3E\",\"softbank\":\"E044\",\"google\":\"FE988\",\"image\":\"1f379.png\",\"sheet_x\":8,\"sheet_y\":16,\"short_name\":\"tropical_drink\",\"short_names\":[\"tropical_drink\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":60,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BEER MUG\",\"unified\":\"1F37A\",\"variations\":[],\"docomo\":\"E672\",\"au\":\"E4C3\",\"softbank\":\"E047\",\"google\":\"FE983\",\"image\":\"1f37a.png\",\"sheet_x\":8,\"sheet_y\":17,\"short_name\":\"beer\",\"short_names\":[\"beer\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLINKING BEER MUGS\",\"unified\":\"1F37B\",\"variations\":[],\"docomo\":\"E672\",\"au\":\"EA98\",\"softbank\":\"E30C\",\"google\":\"FE987\",\"image\":\"1f37b.png\",\"sheet_x\":8,\"sheet_y\":18,\"short_name\":\"beers\",\"short_names\":[\"beers\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BABY BOTTLE\",\"unified\":\"1F37C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f37c.png\",\"sheet_x\":8,\"sheet_y\":19,\"short_name\":\"baby_bottle\",\"short_names\":[\"baby_bottle\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":65,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FORK AND KNIFE WITH PLATE\",\"unified\":\"1F37D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f37d.png\",\"sheet_x\":8,\"sheet_y\":20,\"short_name\":\"knife_fork_plate\",\"short_names\":[\"knife_fork_plate\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":67,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOTTLE WITH POPPING CORK\",\"unified\":\"1F37E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f37e.png\",\"sheet_x\":8,\"sheet_y\":21,\"short_name\":\"champagne\",\"short_names\":[\"champagne\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":61,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POPCORN\",\"unified\":\"1F37F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f37f.png\",\"sheet_x\":8,\"sheet_y\":22,\"short_name\":\"popcorn\",\"short_names\":[\"popcorn\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RIBBON\",\"unified\":\"1F380\",\"variations\":[],\"docomo\":\"E684\",\"au\":\"E59F\",\"softbank\":\"E314\",\"google\":\"FE50F\",\"image\":\"1f380.png\",\"sheet_x\":8,\"sheet_y\":23,\"short_name\":\"ribbon\",\"short_names\":[\"ribbon\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":103,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WRAPPED PRESENT\",\"unified\":\"1F381\",\"variations\":[],\"docomo\":\"E685\",\"au\":\"E4CF\",\"softbank\":\"E112\",\"google\":\"FE510\",\"image\":\"1f381.png\",\"sheet_x\":8,\"sheet_y\":24,\"short_name\":\"gift\",\"short_names\":[\"gift\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":104,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BIRTHDAY CAKE\",\"unified\":\"1F382\",\"variations\":[],\"docomo\":\"E686\",\"au\":\"E5A0\",\"softbank\":\"E34B\",\"google\":\"FE511\",\"image\":\"1f382.png\",\"sheet_x\":8,\"sheet_y\":25,\"short_name\":\"birthday\",\"short_names\":[\"birthday\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JACK-O-LANTERN\",\"unified\":\"1F383\",\"variations\":[],\"docomo\":null,\"au\":\"EAEE\",\"softbank\":\"E445\",\"google\":\"FE51F\",\"image\":\"1f383.png\",\"sheet_x\":8,\"sheet_y\":26,\"short_name\":\"jack_o_lantern\",\"short_names\":[\"jack_o_lantern\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":98,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHRISTMAS TREE\",\"unified\":\"1F384\",\"variations\":[],\"docomo\":\"E6A4\",\"au\":\"E4C9\",\"softbank\":\"E033\",\"google\":\"FE512\",\"image\":\"1f384.png\",\"sheet_x\":8,\"sheet_y\":27,\"short_name\":\"christmas_tree\",\"short_names\":[\"christmas_tree\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":75,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FATHER CHRISTMAS\",\"unified\":\"1F385\",\"variations\":[],\"docomo\":null,\"au\":\"EAF0\",\"softbank\":\"E448\",\"google\":\"FE513\",\"image\":\"1f385.png\",\"sheet_x\":8,\"sheet_y\":28,\"short_name\":\"santa\",\"short_names\":[\"santa\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":135,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F385-1F3FB\":{\"unified\":\"1F385-1F3FB\",\"image\":\"1f385-1f3fb.png\",\"sheet_x\":8,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F385-1F3FC\":{\"unified\":\"1F385-1F3FC\",\"image\":\"1f385-1f3fc.png\",\"sheet_x\":8,\"sheet_y\":30,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F385-1F3FD\":{\"unified\":\"1F385-1F3FD\",\"image\":\"1f385-1f3fd.png\",\"sheet_x\":8,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F385-1F3FE\":{\"unified\":\"1F385-1F3FE\",\"image\":\"1f385-1f3fe.png\",\"sheet_x\":8,\"sheet_y\":32,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F385-1F3FF\":{\"unified\":\"1F385-1F3FF\",\"image\":\"1f385-1f3ff.png\",\"sheet_x\":8,\"sheet_y\":33,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"FIREWORKS\",\"unified\":\"1F386\",\"variations\":[],\"docomo\":null,\"au\":\"E5CC\",\"softbank\":\"E117\",\"google\":\"FE515\",\"image\":\"1f386.png\",\"sheet_x\":8,\"sheet_y\":34,\"short_name\":\"fireworks\",\"short_names\":[\"fireworks\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":89,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FIREWORK SPARKLER\",\"unified\":\"1F387\",\"variations\":[],\"docomo\":null,\"au\":\"EAEB\",\"softbank\":\"E440\",\"google\":\"FE51D\",\"image\":\"1f387.png\",\"sheet_x\":8,\"sheet_y\":35,\"short_name\":\"sparkler\",\"short_names\":[\"sparkler\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":88,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BALLOON\",\"unified\":\"1F388\",\"variations\":[],\"docomo\":null,\"au\":\"EA9B\",\"softbank\":\"E310\",\"google\":\"FE516\",\"image\":\"1f388.png\",\"sheet_x\":8,\"sheet_y\":36,\"short_name\":\"balloon\",\"short_names\":[\"balloon\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":101,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PARTY POPPER\",\"unified\":\"1F389\",\"variations\":[],\"docomo\":null,\"au\":\"EA9C\",\"softbank\":\"E312\",\"google\":\"FE517\",\"image\":\"1f389.png\",\"sheet_x\":8,\"sheet_y\":37,\"short_name\":\"tada\",\"short_names\":[\"tada\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":106,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CONFETTI BALL\",\"unified\":\"1F38A\",\"variations\":[],\"docomo\":null,\"au\":\"E46F\",\"softbank\":null,\"google\":\"FE520\",\"image\":\"1f38a.png\",\"sheet_x\":8,\"sheet_y\":38,\"short_name\":\"confetti_ball\",\"short_names\":[\"confetti_ball\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":105,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TANABATA TREE\",\"unified\":\"1F38B\",\"variations\":[],\"docomo\":null,\"au\":\"EB3D\",\"softbank\":null,\"google\":\"FE521\",\"image\":\"1f38b.png\",\"sheet_x\":8,\"sheet_y\":39,\"short_name\":\"tanabata_tree\",\"short_names\":[\"tanabata_tree\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":84,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CROSSED FLAGS\",\"unified\":\"1F38C\",\"variations\":[],\"docomo\":null,\"au\":\"E5D9\",\"softbank\":\"E143\",\"google\":\"FE514\",\"image\":\"1f38c.png\",\"sheet_x\":8,\"sheet_y\":40,\"short_name\":\"crossed_flags\",\"short_names\":[\"crossed_flags\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":109,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PINE DECORATION\",\"unified\":\"1F38D\",\"variations\":[],\"docomo\":null,\"au\":\"EAE3\",\"softbank\":\"E436\",\"google\":\"FE518\",\"image\":\"1f38d.png\",\"sheet_x\":9,\"sheet_y\":0,\"short_name\":\"bamboo\",\"short_names\":[\"bamboo\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":83,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JAPANESE DOLLS\",\"unified\":\"1F38E\",\"variations\":[],\"docomo\":null,\"au\":\"EAE4\",\"softbank\":\"E438\",\"google\":\"FE519\",\"image\":\"1f38e.png\",\"sheet_x\":9,\"sheet_y\":1,\"short_name\":\"dolls\",\"short_names\":[\"dolls\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":107,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CARP STREAMER\",\"unified\":\"1F38F\",\"variations\":[],\"docomo\":null,\"au\":\"EAE7\",\"softbank\":\"E43B\",\"google\":\"FE51C\",\"image\":\"1f38f.png\",\"sheet_x\":9,\"sheet_y\":2,\"short_name\":\"flags\",\"short_names\":[\"flags\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":102,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WIND CHIME\",\"unified\":\"1F390\",\"variations\":[],\"docomo\":null,\"au\":\"EAED\",\"softbank\":\"E442\",\"google\":\"FE51E\",\"image\":\"1f390.png\",\"sheet_x\":9,\"sheet_y\":3,\"short_name\":\"wind_chime\",\"short_names\":[\"wind_chime\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":108,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOON VIEWING CEREMONY\",\"unified\":\"1F391\",\"variations\":[],\"docomo\":null,\"au\":\"EAEF\",\"softbank\":\"E446\",\"google\":\"FE017\",\"image\":\"1f391.png\",\"sheet_x\":9,\"sheet_y\":4,\"short_name\":\"rice_scene\",\"short_names\":[\"rice_scene\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":65,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SCHOOL SATCHEL\",\"unified\":\"1F392\",\"variations\":[],\"docomo\":null,\"au\":\"EAE6\",\"softbank\":\"E43A\",\"google\":\"FE51B\",\"image\":\"1f392.png\",\"sheet_x\":9,\"sheet_y\":5,\"short_name\":\"school_satchel\",\"short_names\":[\"school_satchel\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":196,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GRADUATION CAP\",\"unified\":\"1F393\",\"variations\":[],\"docomo\":null,\"au\":\"EAE5\",\"softbank\":\"E439\",\"google\":\"FE51A\",\"image\":\"1f393.png\",\"sheet_x\":9,\"sheet_y\":6,\"short_name\":\"mortar_board\",\"short_names\":[\"mortar_board\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":194,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MILITARY MEDAL\",\"unified\":\"1F396\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f396.png\",\"sheet_x\":9,\"sheet_y\":7,\"short_name\":\"medal\",\"short_names\":[\"medal\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REMINDER RIBBON\",\"unified\":\"1F397\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f397.png\",\"sheet_x\":9,\"sheet_y\":8,\"short_name\":\"reminder_ribbon\",\"short_names\":[\"reminder_ribbon\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STUDIO MICROPHONE\",\"unified\":\"1F399\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f399.png\",\"sheet_x\":9,\"sheet_y\":9,\"short_name\":\"studio_microphone\",\"short_names\":[\"studio_microphone\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEVEL SLIDER\",\"unified\":\"1F39A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f39a.png\",\"sheet_x\":9,\"sheet_y\":10,\"short_name\":\"level_slider\",\"short_names\":[\"level_slider\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CONTROL KNOBS\",\"unified\":\"1F39B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f39b.png\",\"sheet_x\":9,\"sheet_y\":11,\"short_name\":\"control_knobs\",\"short_names\":[\"control_knobs\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FILM FRAMES\",\"unified\":\"1F39E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f39e.png\",\"sheet_x\":9,\"sheet_y\":12,\"short_name\":\"film_frames\",\"short_names\":[\"film_frames\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ADMISSION TICKETS\",\"unified\":\"1F39F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f39f.png\",\"sheet_x\":9,\"sheet_y\":13,\"short_name\":\"admission_tickets\",\"short_names\":[\"admission_tickets\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAROUSEL HORSE\",\"unified\":\"1F3A0\",\"variations\":[],\"docomo\":\"E679\",\"au\":null,\"softbank\":null,\"google\":\"FE7FC\",\"image\":\"1f3a0.png\",\"sheet_x\":9,\"sheet_y\":14,\"short_name\":\"carousel_horse\",\"short_names\":[\"carousel_horse\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":59,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FERRIS WHEEL\",\"unified\":\"1F3A1\",\"variations\":[],\"docomo\":null,\"au\":\"E46D\",\"softbank\":\"E124\",\"google\":\"FE7FD\",\"image\":\"1f3a1.png\",\"sheet_x\":9,\"sheet_y\":15,\"short_name\":\"ferris_wheel\",\"short_names\":[\"ferris_wheel\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROLLER COASTER\",\"unified\":\"1F3A2\",\"variations\":[],\"docomo\":null,\"au\":\"EAE2\",\"softbank\":\"E433\",\"google\":\"FE7FE\",\"image\":\"1f3a2.png\",\"sheet_x\":9,\"sheet_y\":16,\"short_name\":\"roller_coaster\",\"short_names\":[\"roller_coaster\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":58,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FISHING POLE AND FISH\",\"unified\":\"1F3A3\",\"variations\":[],\"docomo\":\"E751\",\"au\":\"EB42\",\"softbank\":\"E019\",\"google\":\"FE7FF\",\"image\":\"1f3a3.png\",\"sheet_x\":9,\"sheet_y\":17,\"short_name\":\"fishing_pole_and_fish\",\"short_names\":[\"fishing_pole_and_fish\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MICROPHONE\",\"unified\":\"1F3A4\",\"variations\":[],\"docomo\":\"E676\",\"au\":\"E503\",\"softbank\":\"E03C\",\"google\":\"FE800\",\"image\":\"1f3a4.png\",\"sheet_x\":9,\"sheet_y\":18,\"short_name\":\"microphone\",\"short_names\":[\"microphone\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOVIE CAMERA\",\"unified\":\"1F3A5\",\"variations\":[],\"docomo\":\"E677\",\"au\":\"E517\",\"softbank\":\"E03D\",\"google\":\"FE801\",\"image\":\"1f3a5.png\",\"sheet_x\":9,\"sheet_y\":19,\"short_name\":\"movie_camera\",\"short_names\":[\"movie_camera\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CINEMA\",\"unified\":\"1F3A6\",\"variations\":[],\"docomo\":\"E677\",\"au\":\"E517\",\"softbank\":\"E507\",\"google\":\"FE802\",\"image\":\"1f3a6.png\",\"sheet_x\":9,\"sheet_y\":20,\"short_name\":\"cinema\",\"short_names\":[\"cinema\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":125,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEADPHONE\",\"unified\":\"1F3A7\",\"variations\":[],\"docomo\":\"E67A\",\"au\":\"E508\",\"softbank\":\"E30A\",\"google\":\"FE803\",\"image\":\"1f3a7.png\",\"sheet_x\":9,\"sheet_y\":21,\"short_name\":\"headphones\",\"short_names\":[\"headphones\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ARTIST PALETTE\",\"unified\":\"1F3A8\",\"variations\":[],\"docomo\":\"E67B\",\"au\":\"E59C\",\"softbank\":\"E502\",\"google\":\"FE804\",\"image\":\"1f3a8.png\",\"sheet_x\":9,\"sheet_y\":22,\"short_name\":\"art\",\"short_names\":[\"art\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TOP HAT\",\"unified\":\"1F3A9\",\"variations\":[],\"docomo\":\"E67C\",\"au\":\"EAF5\",\"softbank\":\"E503\",\"google\":\"FE805\",\"image\":\"1f3a9.png\",\"sheet_x\":9,\"sheet_y\":23,\"short_name\":\"tophat\",\"short_names\":[\"tophat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":192,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CIRCUS TENT\",\"unified\":\"1F3AA\",\"variations\":[],\"docomo\":\"E67D\",\"au\":\"E59E\",\"softbank\":null,\"google\":\"FE806\",\"image\":\"1f3aa.png\",\"sheet_x\":9,\"sheet_y\":24,\"short_name\":\"circus_tent\",\"short_names\":[\"circus_tent\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TICKET\",\"unified\":\"1F3AB\",\"variations\":[],\"docomo\":\"E67E\",\"au\":\"E49E\",\"softbank\":\"E125\",\"google\":\"FE807\",\"image\":\"1f3ab.png\",\"sheet_x\":9,\"sheet_y\":25,\"short_name\":\"ticket\",\"short_names\":[\"ticket\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLAPPER BOARD\",\"unified\":\"1F3AC\",\"variations\":[],\"docomo\":\"E6AC\",\"au\":\"E4BE\",\"softbank\":\"E324\",\"google\":\"FE808\",\"image\":\"1f3ac.png\",\"sheet_x\":9,\"sheet_y\":26,\"short_name\":\"clapper\",\"short_names\":[\"clapper\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PERFORMING ARTS\",\"unified\":\"1F3AD\",\"variations\":[],\"docomo\":null,\"au\":\"E59D\",\"softbank\":\"E503\",\"google\":\"FE809\",\"image\":\"1f3ad.png\",\"sheet_x\":9,\"sheet_y\":27,\"short_name\":\"performing_arts\",\"short_names\":[\"performing_arts\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VIDEO GAME\",\"unified\":\"1F3AE\",\"variations\":[],\"docomo\":\"E68B\",\"au\":\"E4C6\",\"softbank\":null,\"google\":\"FE80A\",\"image\":\"1f3ae.png\",\"sheet_x\":9,\"sheet_y\":28,\"short_name\":\"video_game\",\"short_names\":[\"video_game\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DIRECT HIT\",\"unified\":\"1F3AF\",\"variations\":[],\"docomo\":null,\"au\":\"E4C5\",\"softbank\":\"E130\",\"google\":\"FE80C\",\"image\":\"1f3af.png\",\"sheet_x\":9,\"sheet_y\":29,\"short_name\":\"dart\",\"short_names\":[\"dart\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLOT MACHINE\",\"unified\":\"1F3B0\",\"variations\":[],\"docomo\":null,\"au\":\"E46E\",\"softbank\":\"E133\",\"google\":\"FE80D\",\"image\":\"1f3b0.png\",\"sheet_x\":9,\"sheet_y\":30,\"short_name\":\"slot_machine\",\"short_names\":[\"slot_machine\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BILLIARDS\",\"unified\":\"1F3B1\",\"variations\":[],\"docomo\":null,\"au\":\"EADD\",\"softbank\":\"E42C\",\"google\":\"FE80E\",\"image\":\"1f3b1.png\",\"sheet_x\":9,\"sheet_y\":31,\"short_name\":\"8ball\",\"short_names\":[\"8ball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GAME DIE\",\"unified\":\"1F3B2\",\"variations\":[],\"docomo\":null,\"au\":\"E4C8\",\"softbank\":null,\"google\":\"FE80F\",\"image\":\"1f3b2.png\",\"sheet_x\":9,\"sheet_y\":32,\"short_name\":\"game_die\",\"short_names\":[\"game_die\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOWLING\",\"unified\":\"1F3B3\",\"variations\":[],\"docomo\":null,\"au\":\"EB43\",\"softbank\":null,\"google\":\"FE810\",\"image\":\"1f3b3.png\",\"sheet_x\":9,\"sheet_y\":33,\"short_name\":\"bowling\",\"short_names\":[\"bowling\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FLOWER PLAYING CARDS\",\"unified\":\"1F3B4\",\"variations\":[],\"docomo\":null,\"au\":\"EB6E\",\"softbank\":null,\"google\":\"FE811\",\"image\":\"1f3b4.png\",\"sheet_x\":9,\"sheet_y\":34,\"short_name\":\"flower_playing_cards\",\"short_names\":[\"flower_playing_cards\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":241,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MUSICAL NOTE\",\"unified\":\"1F3B5\",\"variations\":[],\"docomo\":\"E6F6\",\"au\":\"E5BE\",\"softbank\":\"E03E\",\"google\":\"FE813\",\"image\":\"1f3b5.png\",\"sheet_x\":9,\"sheet_y\":35,\"short_name\":\"musical_note\",\"short_names\":[\"musical_note\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":185,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MULTIPLE MUSICAL NOTES\",\"unified\":\"1F3B6\",\"variations\":[],\"docomo\":\"E6FF\",\"au\":\"E505\",\"softbank\":\"E326\",\"google\":\"FE814\",\"image\":\"1f3b6.png\",\"sheet_x\":9,\"sheet_y\":36,\"short_name\":\"notes\",\"short_names\":[\"notes\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":186,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SAXOPHONE\",\"unified\":\"1F3B7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E040\",\"google\":\"FE815\",\"image\":\"1f3b7.png\",\"sheet_x\":9,\"sheet_y\":37,\"short_name\":\"saxophone\",\"short_names\":[\"saxophone\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GUITAR\",\"unified\":\"1F3B8\",\"variations\":[],\"docomo\":null,\"au\":\"E506\",\"softbank\":\"E041\",\"google\":\"FE816\",\"image\":\"1f3b8.png\",\"sheet_x\":9,\"sheet_y\":38,\"short_name\":\"guitar\",\"short_names\":[\"guitar\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MUSICAL KEYBOARD\",\"unified\":\"1F3B9\",\"variations\":[],\"docomo\":null,\"au\":\"EB40\",\"softbank\":null,\"google\":\"FE817\",\"image\":\"1f3b9.png\",\"sheet_x\":9,\"sheet_y\":39,\"short_name\":\"musical_keyboard\",\"short_names\":[\"musical_keyboard\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRUMPET\",\"unified\":\"1F3BA\",\"variations\":[],\"docomo\":null,\"au\":\"EADC\",\"softbank\":\"E042\",\"google\":\"FE818\",\"image\":\"1f3ba.png\",\"sheet_x\":9,\"sheet_y\":40,\"short_name\":\"trumpet\",\"short_names\":[\"trumpet\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VIOLIN\",\"unified\":\"1F3BB\",\"variations\":[],\"docomo\":null,\"au\":\"E507\",\"softbank\":null,\"google\":\"FE819\",\"image\":\"1f3bb.png\",\"sheet_x\":10,\"sheet_y\":0,\"short_name\":\"violin\",\"short_names\":[\"violin\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MUSICAL SCORE\",\"unified\":\"1F3BC\",\"variations\":[],\"docomo\":\"E6FF\",\"au\":\"EACC\",\"softbank\":\"E326\",\"google\":\"FE81A\",\"image\":\"1f3bc.png\",\"sheet_x\":10,\"sheet_y\":1,\"short_name\":\"musical_score\",\"short_names\":[\"musical_score\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RUNNING SHIRT WITH SASH\",\"unified\":\"1F3BD\",\"variations\":[],\"docomo\":\"E652\",\"au\":null,\"softbank\":null,\"google\":\"FE7D0\",\"image\":\"1f3bd.png\",\"sheet_x\":10,\"sheet_y\":2,\"short_name\":\"running_shirt_with_sash\",\"short_names\":[\"running_shirt_with_sash\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TENNIS RACQUET AND BALL\",\"unified\":\"1F3BE\",\"variations\":[],\"docomo\":\"E655\",\"au\":\"E4B7\",\"softbank\":\"E015\",\"google\":\"FE7D3\",\"image\":\"1f3be.png\",\"sheet_x\":10,\"sheet_y\":3,\"short_name\":\"tennis\",\"short_names\":[\"tennis\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SKI AND SKI BOOT\",\"unified\":\"1F3BF\",\"variations\":[],\"docomo\":\"E657\",\"au\":\"EAAC\",\"softbank\":\"E013\",\"google\":\"FE7D5\",\"image\":\"1f3bf.png\",\"sheet_x\":10,\"sheet_y\":4,\"short_name\":\"ski\",\"short_names\":[\"ski\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BASKETBALL AND HOOP\",\"unified\":\"1F3C0\",\"variations\":[],\"docomo\":\"E658\",\"au\":\"E59A\",\"softbank\":\"E42A\",\"google\":\"FE7D6\",\"image\":\"1f3c0.png\",\"sheet_x\":10,\"sheet_y\":5,\"short_name\":\"basketball\",\"short_names\":[\"basketball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHEQUERED FLAG\",\"unified\":\"1F3C1\",\"variations\":[],\"docomo\":\"E659\",\"au\":\"E4B9\",\"softbank\":\"E132\",\"google\":\"FE7D7\",\"image\":\"1f3c1.png\",\"sheet_x\":10,\"sheet_y\":6,\"short_name\":\"checkered_flag\",\"short_names\":[\"checkered_flag\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SNOWBOARDER\",\"unified\":\"1F3C2\",\"variations\":[],\"docomo\":\"E712\",\"au\":\"E4B8\",\"softbank\":null,\"google\":\"FE7D8\",\"image\":\"1f3c2.png\",\"sheet_x\":10,\"sheet_y\":7,\"short_name\":\"snowboarder\",\"short_names\":[\"snowboarder\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RUNNER\",\"unified\":\"1F3C3\",\"variations\":[],\"docomo\":\"E733\",\"au\":\"E46B\",\"softbank\":\"E115\",\"google\":\"FE7D9\",\"image\":\"1f3c3.png\",\"sheet_x\":10,\"sheet_y\":8,\"short_name\":\"runner\",\"short_names\":[\"runner\",\"running\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":140,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F3C3-1F3FB\":{\"unified\":\"1F3C3-1F3FB\",\"image\":\"1f3c3-1f3fb.png\",\"sheet_x\":10,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C3-1F3FC\":{\"unified\":\"1F3C3-1F3FC\",\"image\":\"1f3c3-1f3fc.png\",\"sheet_x\":10,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C3-1F3FD\":{\"unified\":\"1F3C3-1F3FD\",\"image\":\"1f3c3-1f3fd.png\",\"sheet_x\":10,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C3-1F3FE\":{\"unified\":\"1F3C3-1F3FE\",\"image\":\"1f3c3-1f3fe.png\",\"sheet_x\":10,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C3-1F3FF\":{\"unified\":\"1F3C3-1F3FF\",\"image\":\"1f3c3-1f3ff.png\",\"sheet_x\":10,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"SURFER\",\"unified\":\"1F3C4\",\"variations\":[],\"docomo\":\"E712\",\"au\":\"EB41\",\"softbank\":\"E017\",\"google\":\"FE7DA\",\"image\":\"1f3c4.png\",\"sheet_x\":10,\"sheet_y\":14,\"short_name\":\"surfer\",\"short_names\":[\"surfer\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F3C4-1F3FB\":{\"unified\":\"1F3C4-1F3FB\",\"image\":\"1f3c4-1f3fb.png\",\"sheet_x\":10,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C4-1F3FC\":{\"unified\":\"1F3C4-1F3FC\",\"image\":\"1f3c4-1f3fc.png\",\"sheet_x\":10,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C4-1F3FD\":{\"unified\":\"1F3C4-1F3FD\",\"image\":\"1f3c4-1f3fd.png\",\"sheet_x\":10,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C4-1F3FE\":{\"unified\":\"1F3C4-1F3FE\",\"image\":\"1f3c4-1f3fe.png\",\"sheet_x\":10,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C4-1F3FF\":{\"unified\":\"1F3C4-1F3FF\",\"image\":\"1f3c4-1f3ff.png\",\"sheet_x\":10,\"sheet_y\":19,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"SPORTS MEDAL\",\"unified\":\"1F3C5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3c5.png\",\"sheet_x\":10,\"sheet_y\":20,\"short_name\":\"sports_medal\",\"short_names\":[\"sports_medal\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TROPHY\",\"unified\":\"1F3C6\",\"variations\":[],\"docomo\":null,\"au\":\"E5D3\",\"softbank\":\"E131\",\"google\":\"FE7DB\",\"image\":\"1f3c6.png\",\"sheet_x\":10,\"sheet_y\":21,\"short_name\":\"trophy\",\"short_names\":[\"trophy\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HORSE RACING\",\"unified\":\"1F3C7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3c7.png\",\"sheet_x\":10,\"sheet_y\":22,\"short_name\":\"horse_racing\",\"short_names\":[\"horse_racing\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F3C7-1F3FB\":{\"unified\":\"1F3C7-1F3FB\",\"image\":\"1f3c7-1f3fb.png\",\"sheet_x\":10,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C7-1F3FC\":{\"unified\":\"1F3C7-1F3FC\",\"image\":\"1f3c7-1f3fc.png\",\"sheet_x\":10,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C7-1F3FD\":{\"unified\":\"1F3C7-1F3FD\",\"image\":\"1f3c7-1f3fd.png\",\"sheet_x\":10,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C7-1F3FE\":{\"unified\":\"1F3C7-1F3FE\",\"image\":\"1f3c7-1f3fe.png\",\"sheet_x\":10,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3C7-1F3FF\":{\"unified\":\"1F3C7-1F3FF\",\"image\":\"1f3c7-1f3ff.png\",\"sheet_x\":10,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"AMERICAN FOOTBALL\",\"unified\":\"1F3C8\",\"variations\":[],\"docomo\":null,\"au\":\"E4BB\",\"softbank\":\"E42B\",\"google\":\"FE7DD\",\"image\":\"1f3c8.png\",\"sheet_x\":10,\"sheet_y\":28,\"short_name\":\"football\",\"short_names\":[\"football\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RUGBY FOOTBALL\",\"unified\":\"1F3C9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3c9.png\",\"sheet_x\":10,\"sheet_y\":29,\"short_name\":\"rugby_football\",\"short_names\":[\"rugby_football\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SWIMMER\",\"unified\":\"1F3CA\",\"variations\":[],\"docomo\":null,\"au\":\"EADE\",\"softbank\":\"E42D\",\"google\":\"FE7DE\",\"image\":\"1f3ca.png\",\"sheet_x\":10,\"sheet_y\":30,\"short_name\":\"swimmer\",\"short_names\":[\"swimmer\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F3CA-1F3FB\":{\"unified\":\"1F3CA-1F3FB\",\"image\":\"1f3ca-1f3fb.png\",\"sheet_x\":10,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CA-1F3FC\":{\"unified\":\"1F3CA-1F3FC\",\"image\":\"1f3ca-1f3fc.png\",\"sheet_x\":10,\"sheet_y\":32,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CA-1F3FD\":{\"unified\":\"1F3CA-1F3FD\",\"image\":\"1f3ca-1f3fd.png\",\"sheet_x\":10,\"sheet_y\":33,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CA-1F3FE\":{\"unified\":\"1F3CA-1F3FE\",\"image\":\"1f3ca-1f3fe.png\",\"sheet_x\":10,\"sheet_y\":34,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CA-1F3FF\":{\"unified\":\"1F3CA-1F3FF\",\"image\":\"1f3ca-1f3ff.png\",\"sheet_x\":10,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WEIGHT LIFTER\",\"unified\":\"1F3CB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3cb.png\",\"sheet_x\":10,\"sheet_y\":36,\"short_name\":\"weight_lifter\",\"short_names\":[\"weight_lifter\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F3CB-1F3FB\":{\"unified\":\"1F3CB-1F3FB\",\"image\":\"1f3cb-1f3fb.png\",\"sheet_x\":10,\"sheet_y\":37,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CB-1F3FC\":{\"unified\":\"1F3CB-1F3FC\",\"image\":\"1f3cb-1f3fc.png\",\"sheet_x\":10,\"sheet_y\":38,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CB-1F3FD\":{\"unified\":\"1F3CB-1F3FD\",\"image\":\"1f3cb-1f3fd.png\",\"sheet_x\":10,\"sheet_y\":39,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CB-1F3FE\":{\"unified\":\"1F3CB-1F3FE\",\"image\":\"1f3cb-1f3fe.png\",\"sheet_x\":10,\"sheet_y\":40,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F3CB-1F3FF\":{\"unified\":\"1F3CB-1F3FF\",\"image\":\"1f3cb-1f3ff.png\",\"sheet_x\":11,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"GOLFER\",\"unified\":\"1F3CC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3cc.png\",\"sheet_x\":11,\"sheet_y\":1,\"short_name\":\"golfer\",\"short_names\":[\"golfer\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RACING MOTORCYCLE\",\"unified\":\"1F3CD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3cd.png\",\"sheet_x\":11,\"sheet_y\":2,\"short_name\":\"racing_motorcycle\",\"short_names\":[\"racing_motorcycle\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RACING CAR\",\"unified\":\"1F3CE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3ce.png\",\"sheet_x\":11,\"sheet_y\":3,\"short_name\":\"racing_car\",\"short_names\":[\"racing_car\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CRICKET BAT AND BALL\",\"unified\":\"1F3CF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3cf.png\",\"sheet_x\":11,\"sheet_y\":4,\"short_name\":\"cricket_bat_and_ball\",\"short_names\":[\"cricket_bat_and_ball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VOLLEYBALL\",\"unified\":\"1F3D0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d0.png\",\"sheet_x\":11,\"sheet_y\":5,\"short_name\":\"volleyball\",\"short_names\":[\"volleyball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FIELD HOCKEY STICK AND BALL\",\"unified\":\"1F3D1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d1.png\",\"sheet_x\":11,\"sheet_y\":6,\"short_name\":\"field_hockey_stick_and_ball\",\"short_names\":[\"field_hockey_stick_and_ball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ICE HOCKEY STICK AND PUCK\",\"unified\":\"1F3D2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d2.png\",\"sheet_x\":11,\"sheet_y\":7,\"short_name\":\"ice_hockey_stick_and_puck\",\"short_names\":[\"ice_hockey_stick_and_puck\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TABLE TENNIS PADDLE AND BALL\",\"unified\":\"1F3D3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d3.png\",\"sheet_x\":11,\"sheet_y\":8,\"short_name\":\"table_tennis_paddle_and_ball\",\"short_names\":[\"table_tennis_paddle_and_ball\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SNOW CAPPED MOUNTAIN\",\"unified\":\"1F3D4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d4.png\",\"sheet_x\":11,\"sheet_y\":9,\"short_name\":\"snow_capped_mountain\",\"short_names\":[\"snow_capped_mountain\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":67,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAMPING\",\"unified\":\"1F3D5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d5.png\",\"sheet_x\":11,\"sheet_y\":10,\"short_name\":\"camping\",\"short_names\":[\"camping\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":71,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BEACH WITH UMBRELLA\",\"unified\":\"1F3D6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d6.png\",\"sheet_x\":11,\"sheet_y\":11,\"short_name\":\"beach_with_umbrella\",\"short_names\":[\"beach_with_umbrella\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":79,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BUILDING CONSTRUCTION\",\"unified\":\"1F3D7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d7.png\",\"sheet_x\":11,\"sheet_y\":12,\"short_name\":\"building_construction\",\"short_names\":[\"building_construction\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":60,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOUSE BUILDINGS\",\"unified\":\"1F3D8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d8.png\",\"sheet_x\":11,\"sheet_y\":13,\"short_name\":\"house_buildings\",\"short_names\":[\"house_buildings\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":91,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CITYSCAPE\",\"unified\":\"1F3D9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3d9.png\",\"sheet_x\":11,\"sheet_y\":14,\"short_name\":\"cityscape\",\"short_names\":[\"cityscape\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":83,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DERELICT HOUSE BUILDING\",\"unified\":\"1F3DA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3da.png\",\"sheet_x\":11,\"sheet_y\":15,\"short_name\":\"derelict_house_building\",\"short_names\":[\"derelict_house_building\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":98,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLASSICAL BUILDING\",\"unified\":\"1F3DB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3db.png\",\"sheet_x\":11,\"sheet_y\":16,\"short_name\":\"classical_building\",\"short_names\":[\"classical_building\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":110,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DESERT\",\"unified\":\"1F3DC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3dc.png\",\"sheet_x\":11,\"sheet_y\":17,\"short_name\":\"desert\",\"short_names\":[\"desert\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":78,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DESERT ISLAND\",\"unified\":\"1F3DD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3dd.png\",\"sheet_x\":11,\"sheet_y\":18,\"short_name\":\"desert_island\",\"short_names\":[\"desert_island\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":80,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NATIONAL PARK\",\"unified\":\"1F3DE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3de.png\",\"sheet_x\":11,\"sheet_y\":19,\"short_name\":\"national_park\",\"short_names\":[\"national_park\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":73,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STADIUM\",\"unified\":\"1F3DF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3df.png\",\"sheet_x\":11,\"sheet_y\":20,\"short_name\":\"stadium\",\"short_names\":[\"stadium\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":94,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOUSE BUILDING\",\"unified\":\"1F3E0\",\"variations\":[],\"docomo\":\"E663\",\"au\":\"E4AB\",\"softbank\":\"E036\",\"google\":\"FE4B0\",\"image\":\"1f3e0.png\",\"sheet_x\":11,\"sheet_y\":21,\"short_name\":\"house\",\"short_names\":[\"house\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":96,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOUSE WITH GARDEN\",\"unified\":\"1F3E1\",\"variations\":[],\"docomo\":\"E663\",\"au\":\"EB09\",\"softbank\":\"E036\",\"google\":\"FE4B1\",\"image\":\"1f3e1.png\",\"sheet_x\":11,\"sheet_y\":22,\"short_name\":\"house_with_garden\",\"short_names\":[\"house_with_garden\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":97,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OFFICE BUILDING\",\"unified\":\"1F3E2\",\"variations\":[],\"docomo\":\"E664\",\"au\":\"E4AD\",\"softbank\":\"E038\",\"google\":\"FE4B2\",\"image\":\"1f3e2.png\",\"sheet_x\":11,\"sheet_y\":23,\"short_name\":\"office\",\"short_names\":[\"office\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":99,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JAPANESE POST OFFICE\",\"unified\":\"1F3E3\",\"variations\":[],\"docomo\":\"E665\",\"au\":\"E5DE\",\"softbank\":\"E153\",\"google\":\"FE4B3\",\"image\":\"1f3e3.png\",\"sheet_x\":11,\"sheet_y\":24,\"short_name\":\"post_office\",\"short_names\":[\"post_office\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":101,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EUROPEAN POST OFFICE\",\"unified\":\"1F3E4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3e4.png\",\"sheet_x\":11,\"sheet_y\":25,\"short_name\":\"european_post_office\",\"short_names\":[\"european_post_office\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":102,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOSPITAL\",\"unified\":\"1F3E5\",\"variations\":[],\"docomo\":\"E666\",\"au\":\"E5DF\",\"softbank\":\"E155\",\"google\":\"FE4B4\",\"image\":\"1f3e5.png\",\"sheet_x\":11,\"sheet_y\":26,\"short_name\":\"hospital\",\"short_names\":[\"hospital\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":103,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BANK\",\"unified\":\"1F3E6\",\"variations\":[],\"docomo\":\"E667\",\"au\":\"E4AA\",\"softbank\":\"E14D\",\"google\":\"FE4B5\",\"image\":\"1f3e6.png\",\"sheet_x\":11,\"sheet_y\":27,\"short_name\":\"bank\",\"short_names\":[\"bank\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":104,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AUTOMATED TELLER MACHINE\",\"unified\":\"1F3E7\",\"variations\":[],\"docomo\":\"E668\",\"au\":\"E4A3\",\"softbank\":\"E154\",\"google\":\"FE4B6\",\"image\":\"1f3e7.png\",\"sheet_x\":11,\"sheet_y\":28,\"short_name\":\"atm\",\"short_names\":[\"atm\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":109,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOTEL\",\"unified\":\"1F3E8\",\"variations\":[],\"docomo\":\"E669\",\"au\":\"EA81\",\"softbank\":\"E158\",\"google\":\"FE4B7\",\"image\":\"1f3e8.png\",\"sheet_x\":11,\"sheet_y\":29,\"short_name\":\"hotel\",\"short_names\":[\"hotel\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":105,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOVE HOTEL\",\"unified\":\"1F3E9\",\"variations\":[],\"docomo\":\"E669-E6EF\",\"au\":\"EAF3\",\"softbank\":\"E501\",\"google\":\"FE4B8\",\"image\":\"1f3e9.png\",\"sheet_x\":11,\"sheet_y\":30,\"short_name\":\"love_hotel\",\"short_names\":[\"love_hotel\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":108,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CONVENIENCE STORE\",\"unified\":\"1F3EA\",\"variations\":[],\"docomo\":\"E66A\",\"au\":\"E4A4\",\"softbank\":\"E156\",\"google\":\"FE4B9\",\"image\":\"1f3ea.png\",\"sheet_x\":11,\"sheet_y\":31,\"short_name\":\"convenience_store\",\"short_names\":[\"convenience_store\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":106,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SCHOOL\",\"unified\":\"1F3EB\",\"variations\":[],\"docomo\":\"E73E\",\"au\":\"EA80\",\"softbank\":\"E157\",\"google\":\"FE4BA\",\"image\":\"1f3eb.png\",\"sheet_x\":11,\"sheet_y\":32,\"short_name\":\"school\",\"short_names\":[\"school\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":107,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DEPARTMENT STORE\",\"unified\":\"1F3EC\",\"variations\":[],\"docomo\":null,\"au\":\"EAF6\",\"softbank\":\"E504\",\"google\":\"FE4BD\",\"image\":\"1f3ec.png\",\"sheet_x\":11,\"sheet_y\":33,\"short_name\":\"department_store\",\"short_names\":[\"department_store\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":100,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACTORY\",\"unified\":\"1F3ED\",\"variations\":[],\"docomo\":null,\"au\":\"EAF9\",\"softbank\":\"E508\",\"google\":\"FE4C0\",\"image\":\"1f3ed.png\",\"sheet_x\":11,\"sheet_y\":34,\"short_name\":\"factory\",\"short_names\":[\"factory\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":63,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"IZAKAYA LANTERN\",\"unified\":\"1F3EE\",\"variations\":[],\"docomo\":\"E74B\",\"au\":\"E4BD\",\"softbank\":\"E30B\",\"google\":\"FE4C2\",\"image\":\"1f3ee.png\",\"sheet_x\":11,\"sheet_y\":35,\"short_name\":\"izakaya_lantern\",\"short_names\":[\"izakaya_lantern\",\"lantern\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":110,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JAPANESE CASTLE\",\"unified\":\"1F3EF\",\"variations\":[],\"docomo\":null,\"au\":\"EAF7\",\"softbank\":\"E505\",\"google\":\"FE4BE\",\"image\":\"1f3ef.png\",\"sheet_x\":11,\"sheet_y\":36,\"short_name\":\"japanese_castle\",\"short_names\":[\"japanese_castle\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":93,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EUROPEAN CASTLE\",\"unified\":\"1F3F0\",\"variations\":[],\"docomo\":null,\"au\":\"EAF8\",\"softbank\":\"E506\",\"google\":\"FE4BF\",\"image\":\"1f3f0.png\",\"sheet_x\":11,\"sheet_y\":37,\"short_name\":\"european_castle\",\"short_names\":[\"european_castle\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":92,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WAVING WHITE FLAG\",\"unified\":\"1F3F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3f3.png\",\"sheet_x\":11,\"sheet_y\":38,\"short_name\":\"waving_white_flag\",\"short_names\":[\"waving_white_flag\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":164,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WAVING BLACK FLAG\",\"unified\":\"1F3F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3f4.png\",\"sheet_x\":11,\"sheet_y\":39,\"short_name\":\"waving_black_flag\",\"short_names\":[\"waving_black_flag\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":165,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROSETTE\",\"unified\":\"1F3F5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3f5.png\",\"sheet_x\":11,\"sheet_y\":40,\"short_name\":\"rosette\",\"short_names\":[\"rosette\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LABEL\",\"unified\":\"1F3F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3f7.png\",\"sheet_x\":12,\"sheet_y\":0,\"short_name\":\"label\",\"short_names\":[\"label\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":84,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BADMINTON RACQUET AND SHUTTLECOCK\",\"unified\":\"1F3F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3f8.png\",\"sheet_x\":12,\"sheet_y\":1,\"short_name\":\"badminton_racquet_and_shuttlecock\",\"short_names\":[\"badminton_racquet_and_shuttlecock\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOW AND ARROW\",\"unified\":\"1F3F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3f9.png\",\"sheet_x\":12,\"sheet_y\":2,\"short_name\":\"bow_and_arrow\",\"short_names\":[\"bow_and_arrow\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AMPHORA\",\"unified\":\"1F3FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3fa.png\",\"sheet_x\":12,\"sheet_y\":3,\"short_name\":\"amphora\",\"short_names\":[\"amphora\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":73,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EMOJI MODIFIER FITZPATRICK TYPE-1-2\",\"unified\":\"1F3FB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3fb.png\",\"sheet_x\":12,\"sheet_y\":4,\"short_name\":\"skin-tone-2\",\"short_names\":[\"skin-tone-2\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EMOJI MODIFIER FITZPATRICK TYPE-3\",\"unified\":\"1F3FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3fc.png\",\"sheet_x\":12,\"sheet_y\":5,\"short_name\":\"skin-tone-3\",\"short_names\":[\"skin-tone-3\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EMOJI MODIFIER FITZPATRICK TYPE-4\",\"unified\":\"1F3FD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3fd.png\",\"sheet_x\":12,\"sheet_y\":6,\"short_name\":\"skin-tone-4\",\"short_names\":[\"skin-tone-4\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EMOJI MODIFIER FITZPATRICK TYPE-5\",\"unified\":\"1F3FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3fe.png\",\"sheet_x\":12,\"sheet_y\":7,\"short_name\":\"skin-tone-5\",\"short_names\":[\"skin-tone-5\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EMOJI MODIFIER FITZPATRICK TYPE-6\",\"unified\":\"1F3FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f3ff.png\",\"sheet_x\":12,\"sheet_y\":8,\"short_name\":\"skin-tone-6\",\"short_names\":[\"skin-tone-6\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RAT\",\"unified\":\"1F400\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f400.png\",\"sheet_x\":12,\"sheet_y\":9,\"short_name\":\"rat\",\"short_names\":[\"rat\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":61,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOUSE\",\"unified\":\"1F401\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f401.png\",\"sheet_x\":12,\"sheet_y\":10,\"short_name\":\"mouse2\",\"short_names\":[\"mouse2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":62,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OX\",\"unified\":\"1F402\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f402.png\",\"sheet_x\":12,\"sheet_y\":11,\"short_name\":\"ox\",\"short_names\":[\"ox\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WATER BUFFALO\",\"unified\":\"1F403\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f403.png\",\"sheet_x\":12,\"sheet_y\":12,\"short_name\":\"water_buffalo\",\"short_names\":[\"water_buffalo\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COW\",\"unified\":\"1F404\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f404.png\",\"sheet_x\":12,\"sheet_y\":13,\"short_name\":\"cow2\",\"short_names\":[\"cow2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TIGER\",\"unified\":\"1F405\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f405.png\",\"sheet_x\":12,\"sheet_y\":14,\"short_name\":\"tiger2\",\"short_names\":[\"tiger2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEOPARD\",\"unified\":\"1F406\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f406.png\",\"sheet_x\":12,\"sheet_y\":15,\"short_name\":\"leopard\",\"short_names\":[\"leopard\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RABBIT\",\"unified\":\"1F407\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f407.png\",\"sheet_x\":12,\"sheet_y\":16,\"short_name\":\"rabbit2\",\"short_names\":[\"rabbit2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":69,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAT\",\"unified\":\"1F408\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f408.png\",\"sheet_x\":12,\"sheet_y\":17,\"short_name\":\"cat2\",\"short_names\":[\"cat2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":68,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DRAGON\",\"unified\":\"1F409\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f409.png\",\"sheet_x\":12,\"sheet_y\":18,\"short_name\":\"dragon\",\"short_names\":[\"dragon\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":72,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CROCODILE\",\"unified\":\"1F40A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f40a.png\",\"sheet_x\":12,\"sheet_y\":19,\"short_name\":\"crocodile\",\"short_names\":[\"crocodile\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHALE\",\"unified\":\"1F40B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f40b.png\",\"sheet_x\":12,\"sheet_y\":20,\"short_name\":\"whale2\",\"short_names\":[\"whale2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SNAIL\",\"unified\":\"1F40C\",\"variations\":[],\"docomo\":\"E74E\",\"au\":\"EB7E\",\"softbank\":null,\"google\":\"FE1B9\",\"image\":\"1f40c.png\",\"sheet_x\":12,\"sheet_y\":21,\"short_name\":\"snail\",\"short_names\":[\"snail\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SNAKE\",\"unified\":\"1F40D\",\"variations\":[],\"docomo\":null,\"au\":\"EB22\",\"softbank\":\"E52D\",\"google\":\"FE1D3\",\"image\":\"1f40d.png\",\"sheet_x\":12,\"sheet_y\":22,\"short_name\":\"snake\",\"short_names\":[\"snake\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HORSE\",\"unified\":\"1F40E\",\"variations\":[],\"docomo\":\"E754\",\"au\":\"E4D8\",\"softbank\":\"E134\",\"google\":\"FE7DC\",\"image\":\"1f40e.png\",\"sheet_x\":12,\"sheet_y\":23,\"short_name\":\"racehorse\",\"short_names\":[\"racehorse\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":59,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RAM\",\"unified\":\"1F40F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f40f.png\",\"sheet_x\":12,\"sheet_y\":24,\"short_name\":\"ram\",\"short_names\":[\"ram\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GOAT\",\"unified\":\"1F410\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f410.png\",\"sheet_x\":12,\"sheet_y\":25,\"short_name\":\"goat\",\"short_names\":[\"goat\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHEEP\",\"unified\":\"1F411\",\"variations\":[],\"docomo\":null,\"au\":\"E48F\",\"softbank\":\"E529\",\"google\":\"FE1CF\",\"image\":\"1f411.png\",\"sheet_x\":12,\"sheet_y\":26,\"short_name\":\"sheep\",\"short_names\":[\"sheep\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":58,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MONKEY\",\"unified\":\"1F412\",\"variations\":[],\"docomo\":null,\"au\":\"E4D9\",\"softbank\":\"E528\",\"google\":\"FE1CE\",\"image\":\"1f412.png\",\"sheet_x\":12,\"sheet_y\":27,\"short_name\":\"monkey\",\"short_names\":[\"monkey\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROOSTER\",\"unified\":\"1F413\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f413.png\",\"sheet_x\":12,\"sheet_y\":28,\"short_name\":\"rooster\",\"short_names\":[\"rooster\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":63,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHICKEN\",\"unified\":\"1F414\",\"variations\":[],\"docomo\":null,\"au\":\"EB23\",\"softbank\":\"E52E\",\"google\":\"FE1D4\",\"image\":\"1f414.png\",\"sheet_x\":12,\"sheet_y\":29,\"short_name\":\"chicken\",\"short_names\":[\"chicken\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOG\",\"unified\":\"1F415\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f415.png\",\"sheet_x\":12,\"sheet_y\":30,\"short_name\":\"dog2\",\"short_names\":[\"dog2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":66,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PIG\",\"unified\":\"1F416\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f416.png\",\"sheet_x\":12,\"sheet_y\":31,\"short_name\":\"pig2\",\"short_names\":[\"pig2\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":60,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOAR\",\"unified\":\"1F417\",\"variations\":[],\"docomo\":null,\"au\":\"EB24\",\"softbank\":\"E52F\",\"google\":\"FE1D5\",\"image\":\"1f417.png\",\"sheet_x\":12,\"sheet_y\":32,\"short_name\":\"boar\",\"short_names\":[\"boar\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ELEPHANT\",\"unified\":\"1F418\",\"variations\":[],\"docomo\":null,\"au\":\"EB1F\",\"softbank\":\"E526\",\"google\":\"FE1CC\",\"image\":\"1f418.png\",\"sheet_x\":12,\"sheet_y\":33,\"short_name\":\"elephant\",\"short_names\":[\"elephant\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OCTOPUS\",\"unified\":\"1F419\",\"variations\":[],\"docomo\":null,\"au\":\"E5C7\",\"softbank\":\"E10A\",\"google\":\"FE1C5\",\"image\":\"1f419.png\",\"sheet_x\":12,\"sheet_y\":34,\"short_name\":\"octopus\",\"short_names\":[\"octopus\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPIRAL SHELL\",\"unified\":\"1F41A\",\"variations\":[],\"docomo\":null,\"au\":\"EAEC\",\"softbank\":\"E441\",\"google\":\"FE1C6\",\"image\":\"1f41a.png\",\"sheet_x\":12,\"sheet_y\":35,\"short_name\":\"shell\",\"short_names\":[\"shell\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":99,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BUG\",\"unified\":\"1F41B\",\"variations\":[],\"docomo\":null,\"au\":\"EB1E\",\"softbank\":\"E525\",\"google\":\"FE1CB\",\"image\":\"1f41b.png\",\"sheet_x\":12,\"sheet_y\":36,\"short_name\":\"bug\",\"short_names\":[\"bug\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ANT\",\"unified\":\"1F41C\",\"variations\":[],\"docomo\":null,\"au\":\"E4DD\",\"softbank\":null,\"google\":\"FE1DA\",\"image\":\"1f41c.png\",\"sheet_x\":12,\"sheet_y\":37,\"short_name\":\"ant\",\"short_names\":[\"ant\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HONEYBEE\",\"unified\":\"1F41D\",\"variations\":[],\"docomo\":null,\"au\":\"EB57\",\"softbank\":null,\"google\":\"FE1E1\",\"image\":\"1f41d.png\",\"sheet_x\":12,\"sheet_y\":38,\"short_name\":\"bee\",\"short_names\":[\"bee\",\"honeybee\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LADY BEETLE\",\"unified\":\"1F41E\",\"variations\":[],\"docomo\":null,\"au\":\"EB58\",\"softbank\":null,\"google\":\"FE1E2\",\"image\":\"1f41e.png\",\"sheet_x\":12,\"sheet_y\":39,\"short_name\":\"beetle\",\"short_names\":[\"beetle\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FISH\",\"unified\":\"1F41F\",\"variations\":[],\"docomo\":\"E751\",\"au\":\"E49A\",\"softbank\":\"E019\",\"google\":\"FE1BD\",\"image\":\"1f41f.png\",\"sheet_x\":12,\"sheet_y\":40,\"short_name\":\"fish\",\"short_names\":[\"fish\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TROPICAL FISH\",\"unified\":\"1F420\",\"variations\":[],\"docomo\":\"E751\",\"au\":\"EB1D\",\"softbank\":\"E522\",\"google\":\"FE1C9\",\"image\":\"1f420.png\",\"sheet_x\":13,\"sheet_y\":0,\"short_name\":\"tropical_fish\",\"short_names\":[\"tropical_fish\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLOWFISH\",\"unified\":\"1F421\",\"variations\":[],\"docomo\":\"E751\",\"au\":\"E4D3\",\"softbank\":\"E019\",\"google\":\"FE1D9\",\"image\":\"1f421.png\",\"sheet_x\":13,\"sheet_y\":1,\"short_name\":\"blowfish\",\"short_names\":[\"blowfish\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TURTLE\",\"unified\":\"1F422\",\"variations\":[],\"docomo\":null,\"au\":\"E5D4\",\"softbank\":null,\"google\":\"FE1DC\",\"image\":\"1f422.png\",\"sheet_x\":13,\"sheet_y\":2,\"short_name\":\"turtle\",\"short_names\":[\"turtle\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HATCHING CHICK\",\"unified\":\"1F423\",\"variations\":[],\"docomo\":\"E74F\",\"au\":\"E5DB\",\"softbank\":\"E523\",\"google\":\"FE1DD\",\"image\":\"1f423.png\",\"sheet_x\":13,\"sheet_y\":3,\"short_name\":\"hatching_chick\",\"short_names\":[\"hatching_chick\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BABY CHICK\",\"unified\":\"1F424\",\"variations\":[],\"docomo\":\"E74F\",\"au\":\"E4E0\",\"softbank\":\"E523\",\"google\":\"FE1BA\",\"image\":\"1f424.png\",\"sheet_x\":13,\"sheet_y\":4,\"short_name\":\"baby_chick\",\"short_names\":[\"baby_chick\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FRONT-FACING BABY CHICK\",\"unified\":\"1F425\",\"variations\":[],\"docomo\":\"E74F\",\"au\":\"EB76\",\"softbank\":\"E523\",\"google\":\"FE1BB\",\"image\":\"1f425.png\",\"sheet_x\":13,\"sheet_y\":5,\"short_name\":\"hatched_chick\",\"short_names\":[\"hatched_chick\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BIRD\",\"unified\":\"1F426\",\"variations\":[],\"docomo\":\"E74F\",\"au\":\"E4E0\",\"softbank\":\"E521\",\"google\":\"FE1C8\",\"image\":\"1f426.png\",\"sheet_x\":13,\"sheet_y\":6,\"short_name\":\"bird\",\"short_names\":[\"bird\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PENGUIN\",\"unified\":\"1F427\",\"variations\":[],\"docomo\":\"E750\",\"au\":\"E4DC\",\"softbank\":\"E055\",\"google\":\"FE1BC\",\"image\":\"1f427.png\",\"sheet_x\":13,\"sheet_y\":7,\"short_name\":\"penguin\",\"short_names\":[\"penguin\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KOALA\",\"unified\":\"1F428\",\"variations\":[],\"docomo\":null,\"au\":\"EB20\",\"softbank\":\"E527\",\"google\":\"FE1CD\",\"image\":\"1f428.png\",\"sheet_x\":13,\"sheet_y\":8,\"short_name\":\"koala\",\"short_names\":[\"koala\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POODLE\",\"unified\":\"1F429\",\"variations\":[],\"docomo\":\"E6A1\",\"au\":\"E4DF\",\"softbank\":\"E052\",\"google\":\"FE1D8\",\"image\":\"1f429.png\",\"sheet_x\":13,\"sheet_y\":9,\"short_name\":\"poodle\",\"short_names\":[\"poodle\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":67,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DROMEDARY CAMEL\",\"unified\":\"1F42A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f42a.png\",\"sheet_x\":13,\"sheet_y\":10,\"short_name\":\"dromedary_camel\",\"short_names\":[\"dromedary_camel\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BACTRIAN CAMEL\",\"unified\":\"1F42B\",\"variations\":[],\"docomo\":null,\"au\":\"EB25\",\"softbank\":\"E530\",\"google\":\"FE1D6\",\"image\":\"1f42b.png\",\"sheet_x\":13,\"sheet_y\":11,\"short_name\":\"camel\",\"short_names\":[\"camel\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOLPHIN\",\"unified\":\"1F42C\",\"variations\":[],\"docomo\":null,\"au\":\"EB1B\",\"softbank\":\"E520\",\"google\":\"FE1C7\",\"image\":\"1f42c.png\",\"sheet_x\":13,\"sheet_y\":12,\"short_name\":\"dolphin\",\"short_names\":[\"dolphin\",\"flipper\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOUSE FACE\",\"unified\":\"1F42D\",\"variations\":[],\"docomo\":null,\"au\":\"E5C2\",\"softbank\":\"E053\",\"google\":\"FE1C2\",\"image\":\"1f42d.png\",\"sheet_x\":13,\"sheet_y\":13,\"short_name\":\"mouse\",\"short_names\":[\"mouse\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COW FACE\",\"unified\":\"1F42E\",\"variations\":[],\"docomo\":null,\"au\":\"EB21\",\"softbank\":\"E52B\",\"google\":\"FE1D1\",\"image\":\"1f42e.png\",\"sheet_x\":13,\"sheet_y\":14,\"short_name\":\"cow\",\"short_names\":[\"cow\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TIGER FACE\",\"unified\":\"1F42F\",\"variations\":[],\"docomo\":null,\"au\":\"E5C0\",\"softbank\":\"E050\",\"google\":\"FE1C0\",\"image\":\"1f42f.png\",\"sheet_x\":13,\"sheet_y\":15,\"short_name\":\"tiger\",\"short_names\":[\"tiger\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RABBIT FACE\",\"unified\":\"1F430\",\"variations\":[],\"docomo\":null,\"au\":\"E4D7\",\"softbank\":\"E52C\",\"google\":\"FE1D2\",\"image\":\"1f430.png\",\"sheet_x\":13,\"sheet_y\":16,\"short_name\":\"rabbit\",\"short_names\":[\"rabbit\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAT FACE\",\"unified\":\"1F431\",\"variations\":[],\"docomo\":\"E6A2\",\"au\":\"E4DB\",\"softbank\":\"E04F\",\"google\":\"FE1B8\",\"image\":\"1f431.png\",\"sheet_x\":13,\"sheet_y\":17,\"short_name\":\"cat\",\"short_names\":[\"cat\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DRAGON FACE\",\"unified\":\"1F432\",\"variations\":[],\"docomo\":null,\"au\":\"EB3F\",\"softbank\":null,\"google\":\"FE1DE\",\"image\":\"1f432.png\",\"sheet_x\":13,\"sheet_y\":18,\"short_name\":\"dragon_face\",\"short_names\":[\"dragon_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":73,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPOUTING WHALE\",\"unified\":\"1F433\",\"variations\":[],\"docomo\":null,\"au\":\"E470\",\"softbank\":\"E054\",\"google\":\"FE1C3\",\"image\":\"1f433.png\",\"sheet_x\":13,\"sheet_y\":19,\"short_name\":\"whale\",\"short_names\":[\"whale\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HORSE FACE\",\"unified\":\"1F434\",\"variations\":[],\"docomo\":\"E754\",\"au\":\"E4D8\",\"softbank\":\"E01A\",\"google\":\"FE1BE\",\"image\":\"1f434.png\",\"sheet_x\":13,\"sheet_y\":20,\"short_name\":\"horse\",\"short_names\":[\"horse\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MONKEY FACE\",\"unified\":\"1F435\",\"variations\":[],\"docomo\":null,\"au\":\"E4D9\",\"softbank\":\"E109\",\"google\":\"FE1C4\",\"image\":\"1f435.png\",\"sheet_x\":13,\"sheet_y\":21,\"short_name\":\"monkey_face\",\"short_names\":[\"monkey_face\"],\"text\":null,\"texts\":[\":o)\"],\"category\":\"Nature\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOG FACE\",\"unified\":\"1F436\",\"variations\":[],\"docomo\":\"E6A1\",\"au\":\"E4E1\",\"softbank\":\"E052\",\"google\":\"FE1B7\",\"image\":\"1f436.png\",\"sheet_x\":13,\"sheet_y\":22,\"short_name\":\"dog\",\"short_names\":[\"dog\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PIG FACE\",\"unified\":\"1F437\",\"variations\":[],\"docomo\":\"E755\",\"au\":\"E4DE\",\"softbank\":\"E10B\",\"google\":\"FE1BF\",\"image\":\"1f437.png\",\"sheet_x\":13,\"sheet_y\":23,\"short_name\":\"pig\",\"short_names\":[\"pig\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FROG FACE\",\"unified\":\"1F438\",\"variations\":[],\"docomo\":null,\"au\":\"E4DA\",\"softbank\":\"E531\",\"google\":\"FE1D7\",\"image\":\"1f438.png\",\"sheet_x\":13,\"sheet_y\":24,\"short_name\":\"frog\",\"short_names\":[\"frog\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HAMSTER FACE\",\"unified\":\"1F439\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E524\",\"google\":\"FE1CA\",\"image\":\"1f439.png\",\"sheet_x\":13,\"sheet_y\":25,\"short_name\":\"hamster\",\"short_names\":[\"hamster\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WOLF FACE\",\"unified\":\"1F43A\",\"variations\":[],\"docomo\":\"E6A1\",\"au\":\"E4E1\",\"softbank\":\"E52A\",\"google\":\"FE1D0\",\"image\":\"1f43a.png\",\"sheet_x\":13,\"sheet_y\":26,\"short_name\":\"wolf\",\"short_names\":[\"wolf\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BEAR FACE\",\"unified\":\"1F43B\",\"variations\":[],\"docomo\":null,\"au\":\"E5C1\",\"softbank\":\"E051\",\"google\":\"FE1C1\",\"image\":\"1f43b.png\",\"sheet_x\":13,\"sheet_y\":27,\"short_name\":\"bear\",\"short_names\":[\"bear\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PANDA FACE\",\"unified\":\"1F43C\",\"variations\":[],\"docomo\":null,\"au\":\"EB46\",\"softbank\":null,\"google\":\"FE1DF\",\"image\":\"1f43c.png\",\"sheet_x\":13,\"sheet_y\":28,\"short_name\":\"panda_face\",\"short_names\":[\"panda_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PIG NOSE\",\"unified\":\"1F43D\",\"variations\":[],\"docomo\":\"E755\",\"au\":\"EB48\",\"softbank\":\"E10B\",\"google\":\"FE1E0\",\"image\":\"1f43d.png\",\"sheet_x\":13,\"sheet_y\":29,\"short_name\":\"pig_nose\",\"short_names\":[\"pig_nose\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PAW PRINTS\",\"unified\":\"1F43E\",\"variations\":[],\"docomo\":\"E698\",\"au\":\"E4EE\",\"softbank\":\"E536\",\"google\":\"FE1DB\",\"image\":\"1f43e.png\",\"sheet_x\":13,\"sheet_y\":30,\"short_name\":\"feet\",\"short_names\":[\"feet\",\"paw_prints\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":71,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHIPMUNK\",\"unified\":\"1F43F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f43f.png\",\"sheet_x\":13,\"sheet_y\":31,\"short_name\":\"chipmunk\",\"short_names\":[\"chipmunk\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":70,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EYES\",\"unified\":\"1F440\",\"variations\":[],\"docomo\":\"E691\",\"au\":\"E5A4\",\"softbank\":\"E419\",\"google\":\"FE190\",\"image\":\"1f440.png\",\"sheet_x\":13,\"sheet_y\":32,\"short_name\":\"eyes\",\"short_names\":[\"eyes\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":117,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EYE\",\"unified\":\"1F441\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f441.png\",\"sheet_x\":13,\"sheet_y\":33,\"short_name\":\"eye\",\"short_names\":[\"eye\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":116,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EAR\",\"unified\":\"1F442\",\"variations\":[],\"docomo\":\"E692\",\"au\":\"E5A5\",\"softbank\":\"E41B\",\"google\":\"FE191\",\"image\":\"1f442.png\",\"sheet_x\":13,\"sheet_y\":34,\"short_name\":\"ear\",\"short_names\":[\"ear\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":114,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F442-1F3FB\":{\"unified\":\"1F442-1F3FB\",\"image\":\"1f442-1f3fb.png\",\"sheet_x\":13,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F442-1F3FC\":{\"unified\":\"1F442-1F3FC\",\"image\":\"1f442-1f3fc.png\",\"sheet_x\":13,\"sheet_y\":36,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F442-1F3FD\":{\"unified\":\"1F442-1F3FD\",\"image\":\"1f442-1f3fd.png\",\"sheet_x\":13,\"sheet_y\":37,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F442-1F3FE\":{\"unified\":\"1F442-1F3FE\",\"image\":\"1f442-1f3fe.png\",\"sheet_x\":13,\"sheet_y\":38,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F442-1F3FF\":{\"unified\":\"1F442-1F3FF\",\"image\":\"1f442-1f3ff.png\",\"sheet_x\":13,\"sheet_y\":39,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"NOSE\",\"unified\":\"1F443\",\"variations\":[],\"docomo\":null,\"au\":\"EAD0\",\"softbank\":\"E41A\",\"google\":\"FE192\",\"image\":\"1f443.png\",\"sheet_x\":13,\"sheet_y\":40,\"short_name\":\"nose\",\"short_names\":[\"nose\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":115,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F443-1F3FB\":{\"unified\":\"1F443-1F3FB\",\"image\":\"1f443-1f3fb.png\",\"sheet_x\":14,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F443-1F3FC\":{\"unified\":\"1F443-1F3FC\",\"image\":\"1f443-1f3fc.png\",\"sheet_x\":14,\"sheet_y\":1,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F443-1F3FD\":{\"unified\":\"1F443-1F3FD\",\"image\":\"1f443-1f3fd.png\",\"sheet_x\":14,\"sheet_y\":2,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F443-1F3FE\":{\"unified\":\"1F443-1F3FE\",\"image\":\"1f443-1f3fe.png\",\"sheet_x\":14,\"sheet_y\":3,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F443-1F3FF\":{\"unified\":\"1F443-1F3FF\",\"image\":\"1f443-1f3ff.png\",\"sheet_x\":14,\"sheet_y\":4,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"MOUTH\",\"unified\":\"1F444\",\"variations\":[],\"docomo\":\"E6F9\",\"au\":\"EAD1\",\"softbank\":\"E41C\",\"google\":\"FE193\",\"image\":\"1f444.png\",\"sheet_x\":14,\"sheet_y\":5,\"short_name\":\"lips\",\"short_names\":[\"lips\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":112,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TONGUE\",\"unified\":\"1F445\",\"variations\":[],\"docomo\":\"E728\",\"au\":\"EB47\",\"softbank\":\"E409\",\"google\":\"FE194\",\"image\":\"1f445.png\",\"sheet_x\":14,\"sheet_y\":6,\"short_name\":\"tongue\",\"short_names\":[\"tongue\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":113,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE UP POINTING BACKHAND INDEX\",\"unified\":\"1F446\",\"variations\":[],\"docomo\":null,\"au\":\"EA8D\",\"softbank\":\"E22E\",\"google\":\"FEB99\",\"image\":\"1f446.png\",\"sheet_x\":14,\"sheet_y\":7,\"short_name\":\"point_up_2\",\"short_names\":[\"point_up_2\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":102,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F446-1F3FB\":{\"unified\":\"1F446-1F3FB\",\"image\":\"1f446-1f3fb.png\",\"sheet_x\":14,\"sheet_y\":8,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F446-1F3FC\":{\"unified\":\"1F446-1F3FC\",\"image\":\"1f446-1f3fc.png\",\"sheet_x\":14,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F446-1F3FD\":{\"unified\":\"1F446-1F3FD\",\"image\":\"1f446-1f3fd.png\",\"sheet_x\":14,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F446-1F3FE\":{\"unified\":\"1F446-1F3FE\",\"image\":\"1f446-1f3fe.png\",\"sheet_x\":14,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F446-1F3FF\":{\"unified\":\"1F446-1F3FF\",\"image\":\"1f446-1f3ff.png\",\"sheet_x\":14,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WHITE DOWN POINTING BACKHAND INDEX\",\"unified\":\"1F447\",\"variations\":[],\"docomo\":null,\"au\":\"EA8E\",\"softbank\":\"E22F\",\"google\":\"FEB9A\",\"image\":\"1f447.png\",\"sheet_x\":14,\"sheet_y\":13,\"short_name\":\"point_down\",\"short_names\":[\"point_down\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":103,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F447-1F3FB\":{\"unified\":\"1F447-1F3FB\",\"image\":\"1f447-1f3fb.png\",\"sheet_x\":14,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F447-1F3FC\":{\"unified\":\"1F447-1F3FC\",\"image\":\"1f447-1f3fc.png\",\"sheet_x\":14,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F447-1F3FD\":{\"unified\":\"1F447-1F3FD\",\"image\":\"1f447-1f3fd.png\",\"sheet_x\":14,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F447-1F3FE\":{\"unified\":\"1F447-1F3FE\",\"image\":\"1f447-1f3fe.png\",\"sheet_x\":14,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F447-1F3FF\":{\"unified\":\"1F447-1F3FF\",\"image\":\"1f447-1f3ff.png\",\"sheet_x\":14,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WHITE LEFT POINTING BACKHAND INDEX\",\"unified\":\"1F448\",\"variations\":[],\"docomo\":null,\"au\":\"E4FF\",\"softbank\":\"E230\",\"google\":\"FEB9B\",\"image\":\"1f448.png\",\"sheet_x\":14,\"sheet_y\":19,\"short_name\":\"point_left\",\"short_names\":[\"point_left\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":104,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F448-1F3FB\":{\"unified\":\"1F448-1F3FB\",\"image\":\"1f448-1f3fb.png\",\"sheet_x\":14,\"sheet_y\":20,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F448-1F3FC\":{\"unified\":\"1F448-1F3FC\",\"image\":\"1f448-1f3fc.png\",\"sheet_x\":14,\"sheet_y\":21,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F448-1F3FD\":{\"unified\":\"1F448-1F3FD\",\"image\":\"1f448-1f3fd.png\",\"sheet_x\":14,\"sheet_y\":22,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F448-1F3FE\":{\"unified\":\"1F448-1F3FE\",\"image\":\"1f448-1f3fe.png\",\"sheet_x\":14,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F448-1F3FF\":{\"unified\":\"1F448-1F3FF\",\"image\":\"1f448-1f3ff.png\",\"sheet_x\":14,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WHITE RIGHT POINTING BACKHAND INDEX\",\"unified\":\"1F449\",\"variations\":[],\"docomo\":null,\"au\":\"E500\",\"softbank\":\"E231\",\"google\":\"FEB9C\",\"image\":\"1f449.png\",\"sheet_x\":14,\"sheet_y\":25,\"short_name\":\"point_right\",\"short_names\":[\"point_right\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":105,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F449-1F3FB\":{\"unified\":\"1F449-1F3FB\",\"image\":\"1f449-1f3fb.png\",\"sheet_x\":14,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F449-1F3FC\":{\"unified\":\"1F449-1F3FC\",\"image\":\"1f449-1f3fc.png\",\"sheet_x\":14,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F449-1F3FD\":{\"unified\":\"1F449-1F3FD\",\"image\":\"1f449-1f3fd.png\",\"sheet_x\":14,\"sheet_y\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F449-1F3FE\":{\"unified\":\"1F449-1F3FE\",\"image\":\"1f449-1f3fe.png\",\"sheet_x\":14,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F449-1F3FF\":{\"unified\":\"1F449-1F3FF\",\"image\":\"1f449-1f3ff.png\",\"sheet_x\":14,\"sheet_y\":30,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"FISTED HAND SIGN\",\"unified\":\"1F44A\",\"variations\":[],\"docomo\":\"E6FD\",\"au\":\"E4F3\",\"softbank\":\"E00D\",\"google\":\"FEB96\",\"image\":\"1f44a.png\",\"sheet_x\":14,\"sheet_y\":31,\"short_name\":\"facepunch\",\"short_names\":[\"facepunch\",\"punch\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":93,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F44A-1F3FB\":{\"unified\":\"1F44A-1F3FB\",\"image\":\"1f44a-1f3fb.png\",\"sheet_x\":14,\"sheet_y\":32,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44A-1F3FC\":{\"unified\":\"1F44A-1F3FC\",\"image\":\"1f44a-1f3fc.png\",\"sheet_x\":14,\"sheet_y\":33,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44A-1F3FD\":{\"unified\":\"1F44A-1F3FD\",\"image\":\"1f44a-1f3fd.png\",\"sheet_x\":14,\"sheet_y\":34,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44A-1F3FE\":{\"unified\":\"1F44A-1F3FE\",\"image\":\"1f44a-1f3fe.png\",\"sheet_x\":14,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44A-1F3FF\":{\"unified\":\"1F44A-1F3FF\",\"image\":\"1f44a-1f3ff.png\",\"sheet_x\":14,\"sheet_y\":36,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WAVING HAND SIGN\",\"unified\":\"1F44B\",\"variations\":[],\"docomo\":\"E695\",\"au\":\"EAD6\",\"softbank\":\"E41E\",\"google\":\"FEB9D\",\"image\":\"1f44b.png\",\"sheet_x\":14,\"sheet_y\":37,\"short_name\":\"wave\",\"short_names\":[\"wave\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":90,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F44B-1F3FB\":{\"unified\":\"1F44B-1F3FB\",\"image\":\"1f44b-1f3fb.png\",\"sheet_x\":14,\"sheet_y\":38,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44B-1F3FC\":{\"unified\":\"1F44B-1F3FC\",\"image\":\"1f44b-1f3fc.png\",\"sheet_x\":14,\"sheet_y\":39,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44B-1F3FD\":{\"unified\":\"1F44B-1F3FD\",\"image\":\"1f44b-1f3fd.png\",\"sheet_x\":14,\"sheet_y\":40,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44B-1F3FE\":{\"unified\":\"1F44B-1F3FE\",\"image\":\"1f44b-1f3fe.png\",\"sheet_x\":15,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44B-1F3FF\":{\"unified\":\"1F44B-1F3FF\",\"image\":\"1f44b-1f3ff.png\",\"sheet_x\":15,\"sheet_y\":1,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"OK HAND SIGN\",\"unified\":\"1F44C\",\"variations\":[],\"docomo\":\"E70B\",\"au\":\"EAD4\",\"softbank\":\"E420\",\"google\":\"FEB9F\",\"image\":\"1f44c.png\",\"sheet_x\":15,\"sheet_y\":2,\"short_name\":\"ok_hand\",\"short_names\":[\"ok_hand\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":96,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F44C-1F3FB\":{\"unified\":\"1F44C-1F3FB\",\"image\":\"1f44c-1f3fb.png\",\"sheet_x\":15,\"sheet_y\":3,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44C-1F3FC\":{\"unified\":\"1F44C-1F3FC\",\"image\":\"1f44c-1f3fc.png\",\"sheet_x\":15,\"sheet_y\":4,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44C-1F3FD\":{\"unified\":\"1F44C-1F3FD\",\"image\":\"1f44c-1f3fd.png\",\"sheet_x\":15,\"sheet_y\":5,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44C-1F3FE\":{\"unified\":\"1F44C-1F3FE\",\"image\":\"1f44c-1f3fe.png\",\"sheet_x\":15,\"sheet_y\":6,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44C-1F3FF\":{\"unified\":\"1F44C-1F3FF\",\"image\":\"1f44c-1f3ff.png\",\"sheet_x\":15,\"sheet_y\":7,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"THUMBS UP SIGN\",\"unified\":\"1F44D\",\"variations\":[],\"docomo\":\"E727\",\"au\":\"E4F9\",\"softbank\":\"E00E\",\"google\":\"FEB97\",\"image\":\"1f44d.png\",\"sheet_x\":15,\"sheet_y\":8,\"short_name\":\"+1\",\"short_names\":[\"+1\",\"thumbsup\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":91,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F44D-1F3FB\":{\"unified\":\"1F44D-1F3FB\",\"image\":\"1f44d-1f3fb.png\",\"sheet_x\":15,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44D-1F3FC\":{\"unified\":\"1F44D-1F3FC\",\"image\":\"1f44d-1f3fc.png\",\"sheet_x\":15,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44D-1F3FD\":{\"unified\":\"1F44D-1F3FD\",\"image\":\"1f44d-1f3fd.png\",\"sheet_x\":15,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44D-1F3FE\":{\"unified\":\"1F44D-1F3FE\",\"image\":\"1f44d-1f3fe.png\",\"sheet_x\":15,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44D-1F3FF\":{\"unified\":\"1F44D-1F3FF\",\"image\":\"1f44d-1f3ff.png\",\"sheet_x\":15,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"THUMBS DOWN SIGN\",\"unified\":\"1F44E\",\"variations\":[],\"docomo\":\"E700\",\"au\":\"EAD5\",\"softbank\":\"E421\",\"google\":\"FEBA0\",\"image\":\"1f44e.png\",\"sheet_x\":15,\"sheet_y\":14,\"short_name\":\"-1\",\"short_names\":[\"-1\",\"thumbsdown\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":92,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F44E-1F3FB\":{\"unified\":\"1F44E-1F3FB\",\"image\":\"1f44e-1f3fb.png\",\"sheet_x\":15,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44E-1F3FC\":{\"unified\":\"1F44E-1F3FC\",\"image\":\"1f44e-1f3fc.png\",\"sheet_x\":15,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44E-1F3FD\":{\"unified\":\"1F44E-1F3FD\",\"image\":\"1f44e-1f3fd.png\",\"sheet_x\":15,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44E-1F3FE\":{\"unified\":\"1F44E-1F3FE\",\"image\":\"1f44e-1f3fe.png\",\"sheet_x\":15,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44E-1F3FF\":{\"unified\":\"1F44E-1F3FF\",\"image\":\"1f44e-1f3ff.png\",\"sheet_x\":15,\"sheet_y\":19,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"CLAPPING HANDS SIGN\",\"unified\":\"1F44F\",\"variations\":[],\"docomo\":null,\"au\":\"EAD3\",\"softbank\":\"E41F\",\"google\":\"FEB9E\",\"image\":\"1f44f.png\",\"sheet_x\":15,\"sheet_y\":20,\"short_name\":\"clap\",\"short_names\":[\"clap\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":89,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F44F-1F3FB\":{\"unified\":\"1F44F-1F3FB\",\"image\":\"1f44f-1f3fb.png\",\"sheet_x\":15,\"sheet_y\":21,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44F-1F3FC\":{\"unified\":\"1F44F-1F3FC\",\"image\":\"1f44f-1f3fc.png\",\"sheet_x\":15,\"sheet_y\":22,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44F-1F3FD\":{\"unified\":\"1F44F-1F3FD\",\"image\":\"1f44f-1f3fd.png\",\"sheet_x\":15,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44F-1F3FE\":{\"unified\":\"1F44F-1F3FE\",\"image\":\"1f44f-1f3fe.png\",\"sheet_x\":15,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F44F-1F3FF\":{\"unified\":\"1F44F-1F3FF\",\"image\":\"1f44f-1f3ff.png\",\"sheet_x\":15,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"OPEN HANDS SIGN\",\"unified\":\"1F450\",\"variations\":[],\"docomo\":\"E695\",\"au\":\"EAD6\",\"softbank\":\"E422\",\"google\":\"FEBA1\",\"image\":\"1f450.png\",\"sheet_x\":15,\"sheet_y\":26,\"short_name\":\"open_hands\",\"short_names\":[\"open_hands\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":98,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F450-1F3FB\":{\"unified\":\"1F450-1F3FB\",\"image\":\"1f450-1f3fb.png\",\"sheet_x\":15,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F450-1F3FC\":{\"unified\":\"1F450-1F3FC\",\"image\":\"1f450-1f3fc.png\",\"sheet_x\":15,\"sheet_y\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F450-1F3FD\":{\"unified\":\"1F450-1F3FD\",\"image\":\"1f450-1f3fd.png\",\"sheet_x\":15,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F450-1F3FE\":{\"unified\":\"1F450-1F3FE\",\"image\":\"1f450-1f3fe.png\",\"sheet_x\":15,\"sheet_y\":30,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F450-1F3FF\":{\"unified\":\"1F450-1F3FF\",\"image\":\"1f450-1f3ff.png\",\"sheet_x\":15,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"CROWN\",\"unified\":\"1F451\",\"variations\":[],\"docomo\":\"E71A\",\"au\":\"E5C9\",\"softbank\":\"E10E\",\"google\":\"FE4D1\",\"image\":\"1f451.png\",\"sheet_x\":15,\"sheet_y\":32,\"short_name\":\"crown\",\"short_names\":[\"crown\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":195,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WOMANS HAT\",\"unified\":\"1F452\",\"variations\":[],\"docomo\":null,\"au\":\"EA9E\",\"softbank\":\"E318\",\"google\":\"FE4D4\",\"image\":\"1f452.png\",\"sheet_x\":15,\"sheet_y\":33,\"short_name\":\"womans_hat\",\"short_names\":[\"womans_hat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":191,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EYEGLASSES\",\"unified\":\"1F453\",\"variations\":[],\"docomo\":\"E69A\",\"au\":\"E4FE\",\"softbank\":null,\"google\":\"FE4CE\",\"image\":\"1f453.png\",\"sheet_x\":15,\"sheet_y\":34,\"short_name\":\"eyeglasses\",\"short_names\":[\"eyeglasses\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":201,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NECKTIE\",\"unified\":\"1F454\",\"variations\":[],\"docomo\":null,\"au\":\"EA93\",\"softbank\":\"E302\",\"google\":\"FE4D3\",\"image\":\"1f454.png\",\"sheet_x\":15,\"sheet_y\":35,\"short_name\":\"necktie\",\"short_names\":[\"necktie\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":179,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"T-SHIRT\",\"unified\":\"1F455\",\"variations\":[],\"docomo\":\"E70E\",\"au\":\"E5B6\",\"softbank\":\"E006\",\"google\":\"FE4CF\",\"image\":\"1f455.png\",\"sheet_x\":15,\"sheet_y\":36,\"short_name\":\"shirt\",\"short_names\":[\"shirt\",\"tshirt\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":177,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JEANS\",\"unified\":\"1F456\",\"variations\":[],\"docomo\":\"E711\",\"au\":\"EB77\",\"softbank\":null,\"google\":\"FE4D0\",\"image\":\"1f456.png\",\"sheet_x\":15,\"sheet_y\":37,\"short_name\":\"jeans\",\"short_names\":[\"jeans\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":178,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DRESS\",\"unified\":\"1F457\",\"variations\":[],\"docomo\":null,\"au\":\"EB6B\",\"softbank\":\"E319\",\"google\":\"FE4D5\",\"image\":\"1f457.png\",\"sheet_x\":15,\"sheet_y\":38,\"short_name\":\"dress\",\"short_names\":[\"dress\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":180,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KIMONO\",\"unified\":\"1F458\",\"variations\":[],\"docomo\":null,\"au\":\"EAA3\",\"softbank\":\"E321\",\"google\":\"FE4D9\",\"image\":\"1f458.png\",\"sheet_x\":15,\"sheet_y\":39,\"short_name\":\"kimono\",\"short_names\":[\"kimono\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":182,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BIKINI\",\"unified\":\"1F459\",\"variations\":[],\"docomo\":null,\"au\":\"EAA4\",\"softbank\":\"E322\",\"google\":\"FE4DA\",\"image\":\"1f459.png\",\"sheet_x\":15,\"sheet_y\":40,\"short_name\":\"bikini\",\"short_names\":[\"bikini\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":181,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WOMANS CLOTHES\",\"unified\":\"1F45A\",\"variations\":[],\"docomo\":\"E70E\",\"au\":\"E50D\",\"softbank\":\"E006\",\"google\":\"FE4DB\",\"image\":\"1f45a.png\",\"sheet_x\":16,\"sheet_y\":0,\"short_name\":\"womans_clothes\",\"short_names\":[\"womans_clothes\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":176,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PURSE\",\"unified\":\"1F45B\",\"variations\":[],\"docomo\":\"E70F\",\"au\":\"E504\",\"softbank\":null,\"google\":\"FE4DC\",\"image\":\"1f45b.png\",\"sheet_x\":16,\"sheet_y\":1,\"short_name\":\"purse\",\"short_names\":[\"purse\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":198,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HANDBAG\",\"unified\":\"1F45C\",\"variations\":[],\"docomo\":\"E682\",\"au\":\"E49C\",\"softbank\":\"E323\",\"google\":\"FE4F0\",\"image\":\"1f45c.png\",\"sheet_x\":16,\"sheet_y\":2,\"short_name\":\"handbag\",\"short_names\":[\"handbag\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":199,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POUCH\",\"unified\":\"1F45D\",\"variations\":[],\"docomo\":\"E6AD\",\"au\":null,\"softbank\":null,\"google\":\"FE4F1\",\"image\":\"1f45d.png\",\"sheet_x\":16,\"sheet_y\":3,\"short_name\":\"pouch\",\"short_names\":[\"pouch\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":197,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MANS SHOE\",\"unified\":\"1F45E\",\"variations\":[],\"docomo\":\"E699\",\"au\":\"E5B7\",\"softbank\":\"E007\",\"google\":\"FE4CC\",\"image\":\"1f45e.png\",\"sheet_x\":16,\"sheet_y\":4,\"short_name\":\"mans_shoe\",\"short_names\":[\"mans_shoe\",\"shoe\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":189,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ATHLETIC SHOE\",\"unified\":\"1F45F\",\"variations\":[],\"docomo\":\"E699\",\"au\":\"EB2B\",\"softbank\":\"E007\",\"google\":\"FE4CD\",\"image\":\"1f45f.png\",\"sheet_x\":16,\"sheet_y\":5,\"short_name\":\"athletic_shoe\",\"short_names\":[\"athletic_shoe\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":190,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HIGH-HEELED SHOE\",\"unified\":\"1F460\",\"variations\":[],\"docomo\":\"E674\",\"au\":\"E51A\",\"softbank\":\"E13E\",\"google\":\"FE4D6\",\"image\":\"1f460.png\",\"sheet_x\":16,\"sheet_y\":6,\"short_name\":\"high_heel\",\"short_names\":[\"high_heel\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":186,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WOMANS SANDAL\",\"unified\":\"1F461\",\"variations\":[],\"docomo\":\"E674\",\"au\":\"E51A\",\"softbank\":\"E31A\",\"google\":\"FE4D7\",\"image\":\"1f461.png\",\"sheet_x\":16,\"sheet_y\":7,\"short_name\":\"sandal\",\"short_names\":[\"sandal\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":187,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WOMANS BOOTS\",\"unified\":\"1F462\",\"variations\":[],\"docomo\":null,\"au\":\"EA9F\",\"softbank\":\"E31B\",\"google\":\"FE4D8\",\"image\":\"1f462.png\",\"sheet_x\":16,\"sheet_y\":8,\"short_name\":\"boot\",\"short_names\":[\"boot\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":188,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FOOTPRINTS\",\"unified\":\"1F463\",\"variations\":[],\"docomo\":\"E698\",\"au\":\"EB2A\",\"softbank\":\"E536\",\"google\":\"FE553\",\"image\":\"1f463.png\",\"sheet_x\":16,\"sheet_y\":9,\"short_name\":\"footprints\",\"short_names\":[\"footprints\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":185,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BUST IN SILHOUETTE\",\"unified\":\"1F464\",\"variations\":[],\"docomo\":\"E6B1\",\"au\":null,\"softbank\":null,\"google\":\"FE19A\",\"image\":\"1f464.png\",\"sheet_x\":16,\"sheet_y\":10,\"short_name\":\"bust_in_silhouette\",\"short_names\":[\"bust_in_silhouette\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":118,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BUSTS IN SILHOUETTE\",\"unified\":\"1F465\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f465.png\",\"sheet_x\":16,\"sheet_y\":11,\"short_name\":\"busts_in_silhouette\",\"short_names\":[\"busts_in_silhouette\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":119,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOY\",\"unified\":\"1F466\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"E4FC\",\"softbank\":\"E001\",\"google\":\"FE19B\",\"image\":\"1f466.png\",\"sheet_x\":16,\"sheet_y\":12,\"short_name\":\"boy\",\"short_names\":[\"boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":122,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F466-1F3FB\":{\"unified\":\"1F466-1F3FB\",\"image\":\"1f466-1f3fb.png\",\"sheet_x\":16,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F466-1F3FC\":{\"unified\":\"1F466-1F3FC\",\"image\":\"1f466-1f3fc.png\",\"sheet_x\":16,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F466-1F3FD\":{\"unified\":\"1F466-1F3FD\",\"image\":\"1f466-1f3fd.png\",\"sheet_x\":16,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F466-1F3FE\":{\"unified\":\"1F466-1F3FE\",\"image\":\"1f466-1f3fe.png\",\"sheet_x\":16,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F466-1F3FF\":{\"unified\":\"1F466-1F3FF\",\"image\":\"1f466-1f3ff.png\",\"sheet_x\":16,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"GIRL\",\"unified\":\"1F467\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"E4FA\",\"softbank\":\"E002\",\"google\":\"FE19C\",\"image\":\"1f467.png\",\"sheet_x\":16,\"sheet_y\":18,\"short_name\":\"girl\",\"short_names\":[\"girl\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":123,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F467-1F3FB\":{\"unified\":\"1F467-1F3FB\",\"image\":\"1f467-1f3fb.png\",\"sheet_x\":16,\"sheet_y\":19,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F467-1F3FC\":{\"unified\":\"1F467-1F3FC\",\"image\":\"1f467-1f3fc.png\",\"sheet_x\":16,\"sheet_y\":20,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F467-1F3FD\":{\"unified\":\"1F467-1F3FD\",\"image\":\"1f467-1f3fd.png\",\"sheet_x\":16,\"sheet_y\":21,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F467-1F3FE\":{\"unified\":\"1F467-1F3FE\",\"image\":\"1f467-1f3fe.png\",\"sheet_x\":16,\"sheet_y\":22,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F467-1F3FF\":{\"unified\":\"1F467-1F3FF\",\"image\":\"1f467-1f3ff.png\",\"sheet_x\":16,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"MAN\",\"unified\":\"1F468\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"E4FC\",\"softbank\":\"E004\",\"google\":\"FE19D\",\"image\":\"1f468.png\",\"sheet_x\":16,\"sheet_y\":24,\"short_name\":\"man\",\"short_names\":[\"man\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":124,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F468-1F3FB\":{\"unified\":\"1F468-1F3FB\",\"image\":\"1f468-1f3fb.png\",\"sheet_x\":16,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F468-1F3FC\":{\"unified\":\"1F468-1F3FC\",\"image\":\"1f468-1f3fc.png\",\"sheet_x\":16,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F468-1F3FD\":{\"unified\":\"1F468-1F3FD\",\"image\":\"1f468-1f3fd.png\",\"sheet_x\":16,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F468-1F3FE\":{\"unified\":\"1F468-1F3FE\",\"image\":\"1f468-1f3fe.png\",\"sheet_x\":16,\"sheet_y\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F468-1F3FF\":{\"unified\":\"1F468-1F3FF\",\"image\":\"1f468-1f3ff.png\",\"sheet_x\":16,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WOMAN\",\"unified\":\"1F469\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"E4FA\",\"softbank\":\"E005\",\"google\":\"FE19E\",\"image\":\"1f469.png\",\"sheet_x\":16,\"sheet_y\":30,\"short_name\":\"woman\",\"short_names\":[\"woman\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":125,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F469-1F3FB\":{\"unified\":\"1F469-1F3FB\",\"image\":\"1f469-1f3fb.png\",\"sheet_x\":16,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F469-1F3FC\":{\"unified\":\"1F469-1F3FC\",\"image\":\"1f469-1f3fc.png\",\"sheet_x\":16,\"sheet_y\":32,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F469-1F3FD\":{\"unified\":\"1F469-1F3FD\",\"image\":\"1f469-1f3fd.png\",\"sheet_x\":16,\"sheet_y\":33,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F469-1F3FE\":{\"unified\":\"1F469-1F3FE\",\"image\":\"1f469-1f3fe.png\",\"sheet_x\":16,\"sheet_y\":34,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F469-1F3FF\":{\"unified\":\"1F469-1F3FF\",\"image\":\"1f469-1f3ff.png\",\"sheet_x\":16,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"FAMILY\",\"unified\":\"1F46A\",\"variations\":[\"1F468-200D-1F469-200D-1F466\"],\"docomo\":null,\"au\":\"E501\",\"softbank\":null,\"google\":\"FE19F\",\"image\":\"1f46a.png\",\"sheet_x\":16,\"sheet_y\":36,\"short_name\":\"family\",\"short_names\":[\"family\",\"man-woman-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":161,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MAN AND WOMAN HOLDING HANDS\",\"unified\":\"1F46B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E428\",\"google\":\"FE1A0\",\"image\":\"1f46b.png\",\"sheet_x\":16,\"sheet_y\":37,\"short_name\":\"couple\",\"short_names\":[\"couple\",\"man_and_woman_holding_hands\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":143,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TWO MEN HOLDING HANDS\",\"unified\":\"1F46C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f46c.png\",\"sheet_x\":16,\"sheet_y\":38,\"short_name\":\"two_men_holding_hands\",\"short_names\":[\"two_men_holding_hands\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":144,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TWO WOMEN HOLDING HANDS\",\"unified\":\"1F46D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f46d.png\",\"sheet_x\":16,\"sheet_y\":39,\"short_name\":\"two_women_holding_hands\",\"short_names\":[\"two_women_holding_hands\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":145,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POLICE OFFICER\",\"unified\":\"1F46E\",\"variations\":[],\"docomo\":null,\"au\":\"E5DD\",\"softbank\":\"E152\",\"google\":\"FE1A1\",\"image\":\"1f46e.png\",\"sheet_x\":16,\"sheet_y\":40,\"short_name\":\"cop\",\"short_names\":[\"cop\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":131,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F46E-1F3FB\":{\"unified\":\"1F46E-1F3FB\",\"image\":\"1f46e-1f3fb.png\",\"sheet_x\":17,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F46E-1F3FC\":{\"unified\":\"1F46E-1F3FC\",\"image\":\"1f46e-1f3fc.png\",\"sheet_x\":17,\"sheet_y\":1,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F46E-1F3FD\":{\"unified\":\"1F46E-1F3FD\",\"image\":\"1f46e-1f3fd.png\",\"sheet_x\":17,\"sheet_y\":2,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F46E-1F3FE\":{\"unified\":\"1F46E-1F3FE\",\"image\":\"1f46e-1f3fe.png\",\"sheet_x\":17,\"sheet_y\":3,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F46E-1F3FF\":{\"unified\":\"1F46E-1F3FF\",\"image\":\"1f46e-1f3ff.png\",\"sheet_x\":17,\"sheet_y\":4,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"WOMAN WITH BUNNY EARS\",\"unified\":\"1F46F\",\"variations\":[],\"docomo\":null,\"au\":\"EADB\",\"softbank\":\"E429\",\"google\":\"FE1A2\",\"image\":\"1f46f.png\",\"sheet_x\":17,\"sheet_y\":5,\"short_name\":\"dancers\",\"short_names\":[\"dancers\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":142,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BRIDE WITH VEIL\",\"unified\":\"1F470\",\"variations\":[],\"docomo\":null,\"au\":\"EAE9\",\"softbank\":null,\"google\":\"FE1A3\",\"image\":\"1f470.png\",\"sheet_x\":17,\"sheet_y\":6,\"short_name\":\"bride_with_veil\",\"short_names\":[\"bride_with_veil\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":138,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F470-1F3FB\":{\"unified\":\"1F470-1F3FB\",\"image\":\"1f470-1f3fb.png\",\"sheet_x\":17,\"sheet_y\":7,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F470-1F3FC\":{\"unified\":\"1F470-1F3FC\",\"image\":\"1f470-1f3fc.png\",\"sheet_x\":17,\"sheet_y\":8,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F470-1F3FD\":{\"unified\":\"1F470-1F3FD\",\"image\":\"1f470-1f3fd.png\",\"sheet_x\":17,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F470-1F3FE\":{\"unified\":\"1F470-1F3FE\",\"image\":\"1f470-1f3fe.png\",\"sheet_x\":17,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F470-1F3FF\":{\"unified\":\"1F470-1F3FF\",\"image\":\"1f470-1f3ff.png\",\"sheet_x\":17,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PERSON WITH BLOND HAIR\",\"unified\":\"1F471\",\"variations\":[],\"docomo\":null,\"au\":\"EB13\",\"softbank\":\"E515\",\"google\":\"FE1A4\",\"image\":\"1f471.png\",\"sheet_x\":17,\"sheet_y\":12,\"short_name\":\"person_with_blond_hair\",\"short_names\":[\"person_with_blond_hair\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":126,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F471-1F3FB\":{\"unified\":\"1F471-1F3FB\",\"image\":\"1f471-1f3fb.png\",\"sheet_x\":17,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F471-1F3FC\":{\"unified\":\"1F471-1F3FC\",\"image\":\"1f471-1f3fc.png\",\"sheet_x\":17,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F471-1F3FD\":{\"unified\":\"1F471-1F3FD\",\"image\":\"1f471-1f3fd.png\",\"sheet_x\":17,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F471-1F3FE\":{\"unified\":\"1F471-1F3FE\",\"image\":\"1f471-1f3fe.png\",\"sheet_x\":17,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F471-1F3FF\":{\"unified\":\"1F471-1F3FF\",\"image\":\"1f471-1f3ff.png\",\"sheet_x\":17,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"MAN WITH GUA PI MAO\",\"unified\":\"1F472\",\"variations\":[],\"docomo\":null,\"au\":\"EB14\",\"softbank\":\"E516\",\"google\":\"FE1A5\",\"image\":\"1f472.png\",\"sheet_x\":17,\"sheet_y\":18,\"short_name\":\"man_with_gua_pi_mao\",\"short_names\":[\"man_with_gua_pi_mao\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":129,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F472-1F3FB\":{\"unified\":\"1F472-1F3FB\",\"image\":\"1f472-1f3fb.png\",\"sheet_x\":17,\"sheet_y\":19,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F472-1F3FC\":{\"unified\":\"1F472-1F3FC\",\"image\":\"1f472-1f3fc.png\",\"sheet_x\":17,\"sheet_y\":20,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F472-1F3FD\":{\"unified\":\"1F472-1F3FD\",\"image\":\"1f472-1f3fd.png\",\"sheet_x\":17,\"sheet_y\":21,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F472-1F3FE\":{\"unified\":\"1F472-1F3FE\",\"image\":\"1f472-1f3fe.png\",\"sheet_x\":17,\"sheet_y\":22,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F472-1F3FF\":{\"unified\":\"1F472-1F3FF\",\"image\":\"1f472-1f3ff.png\",\"sheet_x\":17,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"MAN WITH TURBAN\",\"unified\":\"1F473\",\"variations\":[],\"docomo\":null,\"au\":\"EB15\",\"softbank\":\"E517\",\"google\":\"FE1A6\",\"image\":\"1f473.png\",\"sheet_x\":17,\"sheet_y\":24,\"short_name\":\"man_with_turban\",\"short_names\":[\"man_with_turban\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":130,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F473-1F3FB\":{\"unified\":\"1F473-1F3FB\",\"image\":\"1f473-1f3fb.png\",\"sheet_x\":17,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F473-1F3FC\":{\"unified\":\"1F473-1F3FC\",\"image\":\"1f473-1f3fc.png\",\"sheet_x\":17,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F473-1F3FD\":{\"unified\":\"1F473-1F3FD\",\"image\":\"1f473-1f3fd.png\",\"sheet_x\":17,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F473-1F3FE\":{\"unified\":\"1F473-1F3FE\",\"image\":\"1f473-1f3fe.png\",\"sheet_x\":17,\"sheet_y\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F473-1F3FF\":{\"unified\":\"1F473-1F3FF\",\"image\":\"1f473-1f3ff.png\",\"sheet_x\":17,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"OLDER MAN\",\"unified\":\"1F474\",\"variations\":[],\"docomo\":null,\"au\":\"EB16\",\"softbank\":\"E518\",\"google\":\"FE1A7\",\"image\":\"1f474.png\",\"sheet_x\":17,\"sheet_y\":30,\"short_name\":\"older_man\",\"short_names\":[\"older_man\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":127,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F474-1F3FB\":{\"unified\":\"1F474-1F3FB\",\"image\":\"1f474-1f3fb.png\",\"sheet_x\":17,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F474-1F3FC\":{\"unified\":\"1F474-1F3FC\",\"image\":\"1f474-1f3fc.png\",\"sheet_x\":17,\"sheet_y\":32,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F474-1F3FD\":{\"unified\":\"1F474-1F3FD\",\"image\":\"1f474-1f3fd.png\",\"sheet_x\":17,\"sheet_y\":33,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F474-1F3FE\":{\"unified\":\"1F474-1F3FE\",\"image\":\"1f474-1f3fe.png\",\"sheet_x\":17,\"sheet_y\":34,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F474-1F3FF\":{\"unified\":\"1F474-1F3FF\",\"image\":\"1f474-1f3ff.png\",\"sheet_x\":17,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"OLDER WOMAN\",\"unified\":\"1F475\",\"variations\":[],\"docomo\":null,\"au\":\"EB17\",\"softbank\":\"E519\",\"google\":\"FE1A8\",\"image\":\"1f475.png\",\"sheet_x\":17,\"sheet_y\":36,\"short_name\":\"older_woman\",\"short_names\":[\"older_woman\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":128,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F475-1F3FB\":{\"unified\":\"1F475-1F3FB\",\"image\":\"1f475-1f3fb.png\",\"sheet_x\":17,\"sheet_y\":37,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F475-1F3FC\":{\"unified\":\"1F475-1F3FC\",\"image\":\"1f475-1f3fc.png\",\"sheet_x\":17,\"sheet_y\":38,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F475-1F3FD\":{\"unified\":\"1F475-1F3FD\",\"image\":\"1f475-1f3fd.png\",\"sheet_x\":17,\"sheet_y\":39,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F475-1F3FE\":{\"unified\":\"1F475-1F3FE\",\"image\":\"1f475-1f3fe.png\",\"sheet_x\":17,\"sheet_y\":40,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F475-1F3FF\":{\"unified\":\"1F475-1F3FF\",\"image\":\"1f475-1f3ff.png\",\"sheet_x\":18,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"BABY\",\"unified\":\"1F476\",\"variations\":[],\"docomo\":null,\"au\":\"EB18\",\"softbank\":\"E51A\",\"google\":\"FE1A9\",\"image\":\"1f476.png\",\"sheet_x\":18,\"sheet_y\":1,\"short_name\":\"baby\",\"short_names\":[\"baby\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":121,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F476-1F3FB\":{\"unified\":\"1F476-1F3FB\",\"image\":\"1f476-1f3fb.png\",\"sheet_x\":18,\"sheet_y\":2,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F476-1F3FC\":{\"unified\":\"1F476-1F3FC\",\"image\":\"1f476-1f3fc.png\",\"sheet_x\":18,\"sheet_y\":3,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F476-1F3FD\":{\"unified\":\"1F476-1F3FD\",\"image\":\"1f476-1f3fd.png\",\"sheet_x\":18,\"sheet_y\":4,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F476-1F3FE\":{\"unified\":\"1F476-1F3FE\",\"image\":\"1f476-1f3fe.png\",\"sheet_x\":18,\"sheet_y\":5,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F476-1F3FF\":{\"unified\":\"1F476-1F3FF\",\"image\":\"1f476-1f3ff.png\",\"sheet_x\":18,\"sheet_y\":6,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"CONSTRUCTION WORKER\",\"unified\":\"1F477\",\"variations\":[],\"docomo\":null,\"au\":\"EB19\",\"softbank\":\"E51B\",\"google\":\"FE1AA\",\"image\":\"1f477.png\",\"sheet_x\":18,\"sheet_y\":7,\"short_name\":\"construction_worker\",\"short_names\":[\"construction_worker\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":132,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F477-1F3FB\":{\"unified\":\"1F477-1F3FB\",\"image\":\"1f477-1f3fb.png\",\"sheet_x\":18,\"sheet_y\":8,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F477-1F3FC\":{\"unified\":\"1F477-1F3FC\",\"image\":\"1f477-1f3fc.png\",\"sheet_x\":18,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F477-1F3FD\":{\"unified\":\"1F477-1F3FD\",\"image\":\"1f477-1f3fd.png\",\"sheet_x\":18,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F477-1F3FE\":{\"unified\":\"1F477-1F3FE\",\"image\":\"1f477-1f3fe.png\",\"sheet_x\":18,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F477-1F3FF\":{\"unified\":\"1F477-1F3FF\",\"image\":\"1f477-1f3ff.png\",\"sheet_x\":18,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PRINCESS\",\"unified\":\"1F478\",\"variations\":[],\"docomo\":null,\"au\":\"EB1A\",\"softbank\":\"E51C\",\"google\":\"FE1AB\",\"image\":\"1f478.png\",\"sheet_x\":18,\"sheet_y\":13,\"short_name\":\"princess\",\"short_names\":[\"princess\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":137,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F478-1F3FB\":{\"unified\":\"1F478-1F3FB\",\"image\":\"1f478-1f3fb.png\",\"sheet_x\":18,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F478-1F3FC\":{\"unified\":\"1F478-1F3FC\",\"image\":\"1f478-1f3fc.png\",\"sheet_x\":18,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F478-1F3FD\":{\"unified\":\"1F478-1F3FD\",\"image\":\"1f478-1f3fd.png\",\"sheet_x\":18,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F478-1F3FE\":{\"unified\":\"1F478-1F3FE\",\"image\":\"1f478-1f3fe.png\",\"sheet_x\":18,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F478-1F3FF\":{\"unified\":\"1F478-1F3FF\",\"image\":\"1f478-1f3ff.png\",\"sheet_x\":18,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"JAPANESE OGRE\",\"unified\":\"1F479\",\"variations\":[],\"docomo\":null,\"au\":\"EB44\",\"softbank\":null,\"google\":\"FE1AC\",\"image\":\"1f479.png\",\"sheet_x\":18,\"sheet_y\":19,\"short_name\":\"japanese_ogre\",\"short_names\":[\"japanese_ogre\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":73,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JAPANESE GOBLIN\",\"unified\":\"1F47A\",\"variations\":[],\"docomo\":null,\"au\":\"EB45\",\"softbank\":null,\"google\":\"FE1AD\",\"image\":\"1f47a.png\",\"sheet_x\":18,\"sheet_y\":20,\"short_name\":\"japanese_goblin\",\"short_names\":[\"japanese_goblin\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":74,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GHOST\",\"unified\":\"1F47B\",\"variations\":[],\"docomo\":null,\"au\":\"E4CB\",\"softbank\":\"E11B\",\"google\":\"FE1AE\",\"image\":\"1f47b.png\",\"sheet_x\":18,\"sheet_y\":21,\"short_name\":\"ghost\",\"short_names\":[\"ghost\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":76,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BABY ANGEL\",\"unified\":\"1F47C\",\"variations\":[],\"docomo\":null,\"au\":\"E5BF\",\"softbank\":\"E04E\",\"google\":\"FE1AF\",\"image\":\"1f47c.png\",\"sheet_x\":18,\"sheet_y\":22,\"short_name\":\"angel\",\"short_names\":[\"angel\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":136,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F47C-1F3FB\":{\"unified\":\"1F47C-1F3FB\",\"image\":\"1f47c-1f3fb.png\",\"sheet_x\":18,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F47C-1F3FC\":{\"unified\":\"1F47C-1F3FC\",\"image\":\"1f47c-1f3fc.png\",\"sheet_x\":18,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F47C-1F3FD\":{\"unified\":\"1F47C-1F3FD\",\"image\":\"1f47c-1f3fd.png\",\"sheet_x\":18,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F47C-1F3FE\":{\"unified\":\"1F47C-1F3FE\",\"image\":\"1f47c-1f3fe.png\",\"sheet_x\":18,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F47C-1F3FF\":{\"unified\":\"1F47C-1F3FF\",\"image\":\"1f47c-1f3ff.png\",\"sheet_x\":18,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"EXTRATERRESTRIAL ALIEN\",\"unified\":\"1F47D\",\"variations\":[],\"docomo\":null,\"au\":\"E50E\",\"softbank\":\"E10C\",\"google\":\"FE1B0\",\"image\":\"1f47d.png\",\"sheet_x\":18,\"sheet_y\":28,\"short_name\":\"alien\",\"short_names\":[\"alien\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":77,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ALIEN MONSTER\",\"unified\":\"1F47E\",\"variations\":[],\"docomo\":null,\"au\":\"E4EC\",\"softbank\":\"E12B\",\"google\":\"FE1B1\",\"image\":\"1f47e.png\",\"sheet_x\":18,\"sheet_y\":29,\"short_name\":\"space_invader\",\"short_names\":[\"space_invader\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"IMP\",\"unified\":\"1F47F\",\"variations\":[],\"docomo\":null,\"au\":\"E4EF\",\"softbank\":\"E11A\",\"google\":\"FE1B2\",\"image\":\"1f47f.png\",\"sheet_x\":18,\"sheet_y\":30,\"short_name\":\"imp\",\"short_names\":[\"imp\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":72,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SKULL\",\"unified\":\"1F480\",\"variations\":[],\"docomo\":null,\"au\":\"E4F8\",\"softbank\":\"E11C\",\"google\":\"FE1B3\",\"image\":\"1f480.png\",\"sheet_x\":18,\"sheet_y\":31,\"short_name\":\"skull\",\"short_names\":[\"skull\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":75,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INFORMATION DESK PERSON\",\"unified\":\"1F481\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E253\",\"google\":\"FE1B4\",\"image\":\"1f481.png\",\"sheet_x\":18,\"sheet_y\":32,\"short_name\":\"information_desk_person\",\"short_names\":[\"information_desk_person\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":147,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F481-1F3FB\":{\"unified\":\"1F481-1F3FB\",\"image\":\"1f481-1f3fb.png\",\"sheet_x\":18,\"sheet_y\":33,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F481-1F3FC\":{\"unified\":\"1F481-1F3FC\",\"image\":\"1f481-1f3fc.png\",\"sheet_x\":18,\"sheet_y\":34,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F481-1F3FD\":{\"unified\":\"1F481-1F3FD\",\"image\":\"1f481-1f3fd.png\",\"sheet_x\":18,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F481-1F3FE\":{\"unified\":\"1F481-1F3FE\",\"image\":\"1f481-1f3fe.png\",\"sheet_x\":18,\"sheet_y\":36,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F481-1F3FF\":{\"unified\":\"1F481-1F3FF\",\"image\":\"1f481-1f3ff.png\",\"sheet_x\":18,\"sheet_y\":37,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"GUARDSMAN\",\"unified\":\"1F482\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E51E\",\"google\":\"FE1B5\",\"image\":\"1f482.png\",\"sheet_x\":18,\"sheet_y\":38,\"short_name\":\"guardsman\",\"short_names\":[\"guardsman\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":133,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F482-1F3FB\":{\"unified\":\"1F482-1F3FB\",\"image\":\"1f482-1f3fb.png\",\"sheet_x\":18,\"sheet_y\":39,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F482-1F3FC\":{\"unified\":\"1F482-1F3FC\",\"image\":\"1f482-1f3fc.png\",\"sheet_x\":18,\"sheet_y\":40,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F482-1F3FD\":{\"unified\":\"1F482-1F3FD\",\"image\":\"1f482-1f3fd.png\",\"sheet_x\":19,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F482-1F3FE\":{\"unified\":\"1F482-1F3FE\",\"image\":\"1f482-1f3fe.png\",\"sheet_x\":19,\"sheet_y\":1,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F482-1F3FF\":{\"unified\":\"1F482-1F3FF\",\"image\":\"1f482-1f3ff.png\",\"sheet_x\":19,\"sheet_y\":2,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"DANCER\",\"unified\":\"1F483\",\"variations\":[],\"docomo\":null,\"au\":\"EB1C\",\"softbank\":\"E51F\",\"google\":\"FE1B6\",\"image\":\"1f483.png\",\"sheet_x\":19,\"sheet_y\":3,\"short_name\":\"dancer\",\"short_names\":[\"dancer\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":141,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F483-1F3FB\":{\"unified\":\"1F483-1F3FB\",\"image\":\"1f483-1f3fb.png\",\"sheet_x\":19,\"sheet_y\":4,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F483-1F3FC\":{\"unified\":\"1F483-1F3FC\",\"image\":\"1f483-1f3fc.png\",\"sheet_x\":19,\"sheet_y\":5,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F483-1F3FD\":{\"unified\":\"1F483-1F3FD\",\"image\":\"1f483-1f3fd.png\",\"sheet_x\":19,\"sheet_y\":6,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F483-1F3FE\":{\"unified\":\"1F483-1F3FE\",\"image\":\"1f483-1f3fe.png\",\"sheet_x\":19,\"sheet_y\":7,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F483-1F3FF\":{\"unified\":\"1F483-1F3FF\",\"image\":\"1f483-1f3ff.png\",\"sheet_x\":19,\"sheet_y\":8,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"LIPSTICK\",\"unified\":\"1F484\",\"variations\":[],\"docomo\":\"E710\",\"au\":\"E509\",\"softbank\":\"E31C\",\"google\":\"FE195\",\"image\":\"1f484.png\",\"sheet_x\":19,\"sheet_y\":9,\"short_name\":\"lipstick\",\"short_names\":[\"lipstick\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":183,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NAIL POLISH\",\"unified\":\"1F485\",\"variations\":[],\"docomo\":null,\"au\":\"EAA0\",\"softbank\":\"E31D\",\"google\":\"FE196\",\"image\":\"1f485.png\",\"sheet_x\":19,\"sheet_y\":10,\"short_name\":\"nail_care\",\"short_names\":[\"nail_care\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":111,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F485-1F3FB\":{\"unified\":\"1F485-1F3FB\",\"image\":\"1f485-1f3fb.png\",\"sheet_x\":19,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F485-1F3FC\":{\"unified\":\"1F485-1F3FC\",\"image\":\"1f485-1f3fc.png\",\"sheet_x\":19,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F485-1F3FD\":{\"unified\":\"1F485-1F3FD\",\"image\":\"1f485-1f3fd.png\",\"sheet_x\":19,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F485-1F3FE\":{\"unified\":\"1F485-1F3FE\",\"image\":\"1f485-1f3fe.png\",\"sheet_x\":19,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F485-1F3FF\":{\"unified\":\"1F485-1F3FF\",\"image\":\"1f485-1f3ff.png\",\"sheet_x\":19,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"FACE MASSAGE\",\"unified\":\"1F486\",\"variations\":[],\"docomo\":null,\"au\":\"E50B\",\"softbank\":\"E31E\",\"google\":\"FE197\",\"image\":\"1f486.png\",\"sheet_x\":19,\"sheet_y\":16,\"short_name\":\"massage\",\"short_names\":[\"massage\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":154,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F486-1F3FB\":{\"unified\":\"1F486-1F3FB\",\"image\":\"1f486-1f3fb.png\",\"sheet_x\":19,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F486-1F3FC\":{\"unified\":\"1F486-1F3FC\",\"image\":\"1f486-1f3fc.png\",\"sheet_x\":19,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F486-1F3FD\":{\"unified\":\"1F486-1F3FD\",\"image\":\"1f486-1f3fd.png\",\"sheet_x\":19,\"sheet_y\":19,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F486-1F3FE\":{\"unified\":\"1F486-1F3FE\",\"image\":\"1f486-1f3fe.png\",\"sheet_x\":19,\"sheet_y\":20,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F486-1F3FF\":{\"unified\":\"1F486-1F3FF\",\"image\":\"1f486-1f3ff.png\",\"sheet_x\":19,\"sheet_y\":21,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"HAIRCUT\",\"unified\":\"1F487\",\"variations\":[],\"docomo\":\"E675\",\"au\":\"EAA1\",\"softbank\":\"E31F\",\"google\":\"FE198\",\"image\":\"1f487.png\",\"sheet_x\":19,\"sheet_y\":22,\"short_name\":\"haircut\",\"short_names\":[\"haircut\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":153,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F487-1F3FB\":{\"unified\":\"1F487-1F3FB\",\"image\":\"1f487-1f3fb.png\",\"sheet_x\":19,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F487-1F3FC\":{\"unified\":\"1F487-1F3FC\",\"image\":\"1f487-1f3fc.png\",\"sheet_x\":19,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F487-1F3FD\":{\"unified\":\"1F487-1F3FD\",\"image\":\"1f487-1f3fd.png\",\"sheet_x\":19,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F487-1F3FE\":{\"unified\":\"1F487-1F3FE\",\"image\":\"1f487-1f3fe.png\",\"sheet_x\":19,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F487-1F3FF\":{\"unified\":\"1F487-1F3FF\",\"image\":\"1f487-1f3ff.png\",\"sheet_x\":19,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"BARBER POLE\",\"unified\":\"1F488\",\"variations\":[],\"docomo\":null,\"au\":\"EAA2\",\"softbank\":\"E320\",\"google\":\"FE199\",\"image\":\"1f488.png\",\"sheet_x\":19,\"sheet_y\":28,\"short_name\":\"barber\",\"short_names\":[\"barber\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":76,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SYRINGE\",\"unified\":\"1F489\",\"variations\":[],\"docomo\":null,\"au\":\"E510\",\"softbank\":\"E13B\",\"google\":\"FE509\",\"image\":\"1f489.png\",\"sheet_x\":19,\"sheet_y\":29,\"short_name\":\"syringe\",\"short_names\":[\"syringe\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":82,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PILL\",\"unified\":\"1F48A\",\"variations\":[],\"docomo\":null,\"au\":\"EA9A\",\"softbank\":\"E30F\",\"google\":\"FE50A\",\"image\":\"1f48a.png\",\"sheet_x\":19,\"sheet_y\":30,\"short_name\":\"pill\",\"short_names\":[\"pill\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":81,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KISS MARK\",\"unified\":\"1F48B\",\"variations\":[],\"docomo\":\"E6F9\",\"au\":\"E4EB\",\"softbank\":\"E003\",\"google\":\"FE823\",\"image\":\"1f48b.png\",\"sheet_x\":19,\"sheet_y\":31,\"short_name\":\"kiss\",\"short_names\":[\"kiss\"],\"text\":null,\"texts\":[\":*\",\":-*\"],\"category\":\"People\",\"sort_order\":184,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOVE LETTER\",\"unified\":\"1F48C\",\"variations\":[],\"docomo\":\"E717\",\"au\":\"EB78\",\"softbank\":\"E103-E328\",\"google\":\"FE824\",\"image\":\"1f48c.png\",\"sheet_x\":19,\"sheet_y\":32,\"short_name\":\"love_letter\",\"short_names\":[\"love_letter\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":115,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RING\",\"unified\":\"1F48D\",\"variations\":[],\"docomo\":\"E71B\",\"au\":\"E514\",\"softbank\":\"E034\",\"google\":\"FE825\",\"image\":\"1f48d.png\",\"sheet_x\":19,\"sheet_y\":33,\"short_name\":\"ring\",\"short_names\":[\"ring\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":203,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GEM STONE\",\"unified\":\"1F48E\",\"variations\":[],\"docomo\":\"E71B\",\"au\":\"E514\",\"softbank\":\"E035\",\"google\":\"FE826\",\"image\":\"1f48e.png\",\"sheet_x\":19,\"sheet_y\":34,\"short_name\":\"gem\",\"short_names\":[\"gem\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KISS\",\"unified\":\"1F48F\",\"variations\":[],\"docomo\":\"E6F9\",\"au\":\"E5CA\",\"softbank\":\"E111\",\"google\":\"FE827\",\"image\":\"1f48f.png\",\"sheet_x\":19,\"sheet_y\":35,\"short_name\":\"couplekiss\",\"short_names\":[\"couplekiss\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":158,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOUQUET\",\"unified\":\"1F490\",\"variations\":[],\"docomo\":null,\"au\":\"EA95\",\"softbank\":\"E306\",\"google\":\"FE828\",\"image\":\"1f490.png\",\"sheet_x\":19,\"sheet_y\":36,\"short_name\":\"bouquet\",\"short_names\":[\"bouquet\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":95,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COUPLE WITH HEART\",\"unified\":\"1F491\",\"variations\":[],\"docomo\":\"E6ED\",\"au\":\"EADA\",\"softbank\":\"E425\",\"google\":\"FE829\",\"image\":\"1f491.png\",\"sheet_x\":19,\"sheet_y\":37,\"short_name\":\"couple_with_heart\",\"short_names\":[\"couple_with_heart\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":155,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WEDDING\",\"unified\":\"1F492\",\"variations\":[],\"docomo\":null,\"au\":\"E5BB\",\"softbank\":\"E43D\",\"google\":\"FE82A\",\"image\":\"1f492.png\",\"sheet_x\":19,\"sheet_y\":38,\"short_name\":\"wedding\",\"short_names\":[\"wedding\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":109,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BEATING HEART\",\"unified\":\"1F493\",\"variations\":[],\"docomo\":\"E6ED\",\"au\":\"EB75\",\"softbank\":\"E327\",\"google\":\"FEB0D\",\"image\":\"1f493.png\",\"sheet_x\":19,\"sheet_y\":39,\"short_name\":\"heartbeat\",\"short_names\":[\"heartbeat\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BROKEN HEART\",\"unified\":\"1F494\",\"variations\":[],\"docomo\":\"E6EE\",\"au\":\"E477\",\"softbank\":\"E023\",\"google\":\"FEB0E\",\"image\":\"1f494.png\",\"sheet_x\":19,\"sheet_y\":40,\"short_name\":\"broken_heart\",\"short_names\":[\"broken_heart\"],\"text\":\"<\\/3\",\"texts\":[\"<\\/3\"],\"category\":\"Symbols\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TWO HEARTS\",\"unified\":\"1F495\",\"variations\":[],\"docomo\":\"E6EF\",\"au\":\"E478\",\"softbank\":\"E327\",\"google\":\"FEB0F\",\"image\":\"1f495.png\",\"sheet_x\":20,\"sheet_y\":0,\"short_name\":\"two_hearts\",\"short_names\":[\"two_hearts\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPARKLING HEART\",\"unified\":\"1F496\",\"variations\":[],\"docomo\":\"E6EC\",\"au\":\"EAA6\",\"softbank\":\"E327\",\"google\":\"FEB10\",\"image\":\"1f496.png\",\"sheet_x\":20,\"sheet_y\":1,\"short_name\":\"sparkling_heart\",\"short_names\":[\"sparkling_heart\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GROWING HEART\",\"unified\":\"1F497\",\"variations\":[],\"docomo\":\"E6ED\",\"au\":\"EB75\",\"softbank\":\"E328\",\"google\":\"FEB11\",\"image\":\"1f497.png\",\"sheet_x\":20,\"sheet_y\":2,\"short_name\":\"heartpulse\",\"short_names\":[\"heartpulse\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEART WITH ARROW\",\"unified\":\"1F498\",\"variations\":[],\"docomo\":\"E6EC\",\"au\":\"E4EA\",\"softbank\":\"E329\",\"google\":\"FEB12\",\"image\":\"1f498.png\",\"sheet_x\":20,\"sheet_y\":3,\"short_name\":\"cupid\",\"short_names\":[\"cupid\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLUE HEART\",\"unified\":\"1F499\",\"variations\":[],\"docomo\":\"E6EC\",\"au\":\"EAA7\",\"softbank\":\"E32A\",\"google\":\"FEB13\",\"image\":\"1f499.png\",\"sheet_x\":20,\"sheet_y\":4,\"short_name\":\"blue_heart\",\"short_names\":[\"blue_heart\"],\"text\":\"<3\",\"texts\":null,\"category\":\"Symbols\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GREEN HEART\",\"unified\":\"1F49A\",\"variations\":[],\"docomo\":\"E6EC\",\"au\":\"EAA8\",\"softbank\":\"E32B\",\"google\":\"FEB14\",\"image\":\"1f49a.png\",\"sheet_x\":20,\"sheet_y\":5,\"short_name\":\"green_heart\",\"short_names\":[\"green_heart\"],\"text\":\"<3\",\"texts\":null,\"category\":\"Symbols\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"YELLOW HEART\",\"unified\":\"1F49B\",\"variations\":[],\"docomo\":\"E6EC\",\"au\":\"EAA9\",\"softbank\":\"E32C\",\"google\":\"FEB15\",\"image\":\"1f49b.png\",\"sheet_x\":20,\"sheet_y\":6,\"short_name\":\"yellow_heart\",\"short_names\":[\"yellow_heart\"],\"text\":\"<3\",\"texts\":null,\"category\":\"Symbols\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PURPLE HEART\",\"unified\":\"1F49C\",\"variations\":[],\"docomo\":\"E6EC\",\"au\":\"EAAA\",\"softbank\":\"E32D\",\"google\":\"FEB16\",\"image\":\"1f49c.png\",\"sheet_x\":20,\"sheet_y\":7,\"short_name\":\"purple_heart\",\"short_names\":[\"purple_heart\"],\"text\":\"<3\",\"texts\":null,\"category\":\"Symbols\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEART WITH RIBBON\",\"unified\":\"1F49D\",\"variations\":[],\"docomo\":\"E6EC\",\"au\":\"EB54\",\"softbank\":\"E437\",\"google\":\"FEB17\",\"image\":\"1f49d.png\",\"sheet_x\":20,\"sheet_y\":8,\"short_name\":\"gift_heart\",\"short_names\":[\"gift_heart\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REVOLVING HEARTS\",\"unified\":\"1F49E\",\"variations\":[],\"docomo\":\"E6ED\",\"au\":\"E5AF\",\"softbank\":\"E327\",\"google\":\"FEB18\",\"image\":\"1f49e.png\",\"sheet_x\":20,\"sheet_y\":9,\"short_name\":\"revolving_hearts\",\"short_names\":[\"revolving_hearts\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEART DECORATION\",\"unified\":\"1F49F\",\"variations\":[],\"docomo\":\"E6F8\",\"au\":\"E595\",\"softbank\":\"E204\",\"google\":\"FEB19\",\"image\":\"1f49f.png\",\"sheet_x\":20,\"sheet_y\":10,\"short_name\":\"heart_decoration\",\"short_names\":[\"heart_decoration\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DIAMOND SHAPE WITH A DOT INSIDE\",\"unified\":\"1F4A0\",\"variations\":[],\"docomo\":\"E6F8\",\"au\":null,\"softbank\":null,\"google\":\"FEB55\",\"image\":\"1f4a0.png\",\"sheet_x\":20,\"sheet_y\":11,\"short_name\":\"diamond_shape_with_a_dot_inside\",\"short_names\":[\"diamond_shape_with_a_dot_inside\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":104,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ELECTRIC LIGHT BULB\",\"unified\":\"1F4A1\",\"variations\":[],\"docomo\":\"E6FB\",\"au\":\"E476\",\"softbank\":\"E10F\",\"google\":\"FEB56\",\"image\":\"1f4a1.png\",\"sheet_x\":20,\"sheet_y\":12,\"short_name\":\"bulb\",\"short_names\":[\"bulb\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ANGER SYMBOL\",\"unified\":\"1F4A2\",\"variations\":[],\"docomo\":\"E6FC\",\"au\":\"E4E5\",\"softbank\":\"E334\",\"google\":\"FEB57\",\"image\":\"1f4a2.png\",\"sheet_x\":20,\"sheet_y\":13,\"short_name\":\"anger\",\"short_names\":[\"anger\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":74,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOMB\",\"unified\":\"1F4A3\",\"variations\":[],\"docomo\":\"E6FE\",\"au\":\"E47A\",\"softbank\":\"E311\",\"google\":\"FEB58\",\"image\":\"1f4a3.png\",\"sheet_x\":20,\"sheet_y\":14,\"short_name\":\"bomb\",\"short_names\":[\"bomb\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":64,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLEEPING SYMBOL\",\"unified\":\"1F4A4\",\"variations\":[],\"docomo\":\"E701\",\"au\":\"E475\",\"softbank\":\"E13C\",\"google\":\"FEB59\",\"image\":\"1f4a4.png\",\"sheet_x\":20,\"sheet_y\":15,\"short_name\":\"zzz\",\"short_names\":[\"zzz\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":69,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COLLISION SYMBOL\",\"unified\":\"1F4A5\",\"variations\":[],\"docomo\":\"E705\",\"au\":\"E5B0\",\"softbank\":null,\"google\":\"FEB5A\",\"image\":\"1f4a5.png\",\"sheet_x\":20,\"sheet_y\":16,\"short_name\":\"boom\",\"short_names\":[\"boom\",\"collision\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":134,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPLASHING SWEAT SYMBOL\",\"unified\":\"1F4A6\",\"variations\":[],\"docomo\":\"E706\",\"au\":\"E5B1\",\"softbank\":\"E331\",\"google\":\"FEB5B\",\"image\":\"1f4a6.png\",\"sheet_x\":20,\"sheet_y\":17,\"short_name\":\"sweat_drops\",\"short_names\":[\"sweat_drops\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":146,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DROPLET\",\"unified\":\"1F4A7\",\"variations\":[],\"docomo\":\"E707\",\"au\":\"E4E6\",\"softbank\":\"E331\",\"google\":\"FEB5C\",\"image\":\"1f4a7.png\",\"sheet_x\":20,\"sheet_y\":18,\"short_name\":\"droplet\",\"short_names\":[\"droplet\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":145,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DASH SYMBOL\",\"unified\":\"1F4A8\",\"variations\":[],\"docomo\":\"E708\",\"au\":\"E4F4\",\"softbank\":\"E330\",\"google\":\"FEB5D\",\"image\":\"1f4a8.png\",\"sheet_x\":20,\"sheet_y\":19,\"short_name\":\"dash\",\"short_names\":[\"dash\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":140,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PILE OF POO\",\"unified\":\"1F4A9\",\"variations\":[],\"docomo\":null,\"au\":\"E4F5\",\"softbank\":\"E05A\",\"google\":\"FE4F4\",\"image\":\"1f4a9.png\",\"sheet_x\":20,\"sheet_y\":20,\"short_name\":\"hankey\",\"short_names\":[\"hankey\",\"poop\",\"shit\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":70,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FLEXED BICEPS\",\"unified\":\"1F4AA\",\"variations\":[],\"docomo\":null,\"au\":\"E4E9\",\"softbank\":\"E14C\",\"google\":\"FEB5E\",\"image\":\"1f4aa.png\",\"sheet_x\":20,\"sheet_y\":21,\"short_name\":\"muscle\",\"short_names\":[\"muscle\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":99,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F4AA-1F3FB\":{\"unified\":\"1F4AA-1F3FB\",\"image\":\"1f4aa-1f3fb.png\",\"sheet_x\":20,\"sheet_y\":22,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F4AA-1F3FC\":{\"unified\":\"1F4AA-1F3FC\",\"image\":\"1f4aa-1f3fc.png\",\"sheet_x\":20,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F4AA-1F3FD\":{\"unified\":\"1F4AA-1F3FD\",\"image\":\"1f4aa-1f3fd.png\",\"sheet_x\":20,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F4AA-1F3FE\":{\"unified\":\"1F4AA-1F3FE\",\"image\":\"1f4aa-1f3fe.png\",\"sheet_x\":20,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F4AA-1F3FF\":{\"unified\":\"1F4AA-1F3FF\",\"image\":\"1f4aa-1f3ff.png\",\"sheet_x\":20,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"DIZZY SYMBOL\",\"unified\":\"1F4AB\",\"variations\":[],\"docomo\":null,\"au\":\"EB5C\",\"softbank\":\"E407\",\"google\":\"FEB5F\",\"image\":\"1f4ab.png\",\"sheet_x\":20,\"sheet_y\":27,\"short_name\":\"dizzy\",\"short_names\":[\"dizzy\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":120,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPEECH BALLOON\",\"unified\":\"1F4AC\",\"variations\":[],\"docomo\":null,\"au\":\"E4FD\",\"softbank\":null,\"google\":\"FE532\",\"image\":\"1f4ac.png\",\"sheet_x\":20,\"sheet_y\":28,\"short_name\":\"speech_balloon\",\"short_names\":[\"speech_balloon\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":245,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"THOUGHT BALLOON\",\"unified\":\"1F4AD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4ad.png\",\"sheet_x\":20,\"sheet_y\":29,\"short_name\":\"thought_balloon\",\"short_names\":[\"thought_balloon\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":243,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE FLOWER\",\"unified\":\"1F4AE\",\"variations\":[],\"docomo\":null,\"au\":\"E4F0\",\"softbank\":null,\"google\":\"FEB7A\",\"image\":\"1f4ae.png\",\"sheet_x\":20,\"sheet_y\":30,\"short_name\":\"white_flower\",\"short_names\":[\"white_flower\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HUNDRED POINTS SYMBOL\",\"unified\":\"1F4AF\",\"variations\":[],\"docomo\":null,\"au\":\"E4F2\",\"softbank\":null,\"google\":\"FEB7B\",\"image\":\"1f4af.png\",\"sheet_x\":20,\"sheet_y\":31,\"short_name\":\"100\",\"short_names\":[\"100\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":88,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MONEY BAG\",\"unified\":\"1F4B0\",\"variations\":[],\"docomo\":\"E715\",\"au\":\"E4C7\",\"softbank\":\"E12F\",\"google\":\"FE4DD\",\"image\":\"1f4b0.png\",\"sheet_x\":20,\"sheet_y\":32,\"short_name\":\"moneybag\",\"short_names\":[\"moneybag\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CURRENCY EXCHANGE\",\"unified\":\"1F4B1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E149\",\"google\":\"FE4DE\",\"image\":\"1f4b1.png\",\"sheet_x\":20,\"sheet_y\":33,\"short_name\":\"currency_exchange\",\"short_names\":[\"currency_exchange\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":196,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAVY DOLLAR SIGN\",\"unified\":\"1F4B2\",\"variations\":[],\"docomo\":\"E715\",\"au\":\"E579\",\"softbank\":\"E12F\",\"google\":\"FE4E0\",\"image\":\"1f4b2.png\",\"sheet_x\":20,\"sheet_y\":34,\"short_name\":\"heavy_dollar_sign\",\"short_names\":[\"heavy_dollar_sign\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":195,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CREDIT CARD\",\"unified\":\"1F4B3\",\"variations\":[],\"docomo\":null,\"au\":\"E57C\",\"softbank\":null,\"google\":\"FE4E1\",\"image\":\"1f4b3.png\",\"sheet_x\":20,\"sheet_y\":35,\"short_name\":\"credit_card\",\"short_names\":[\"credit_card\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BANKNOTE WITH YEN SIGN\",\"unified\":\"1F4B4\",\"variations\":[],\"docomo\":\"E6D6\",\"au\":\"E57D\",\"softbank\":null,\"google\":\"FE4E2\",\"image\":\"1f4b4.png\",\"sheet_x\":20,\"sheet_y\":36,\"short_name\":\"yen\",\"short_names\":[\"yen\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BANKNOTE WITH DOLLAR SIGN\",\"unified\":\"1F4B5\",\"variations\":[],\"docomo\":\"E715\",\"au\":\"E585\",\"softbank\":\"E12F\",\"google\":\"FE4E3\",\"image\":\"1f4b5.png\",\"sheet_x\":20,\"sheet_y\":37,\"short_name\":\"dollar\",\"short_names\":[\"dollar\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BANKNOTE WITH EURO SIGN\",\"unified\":\"1F4B6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4b6.png\",\"sheet_x\":20,\"sheet_y\":38,\"short_name\":\"euro\",\"short_names\":[\"euro\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BANKNOTE WITH POUND SIGN\",\"unified\":\"1F4B7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4b7.png\",\"sheet_x\":20,\"sheet_y\":39,\"short_name\":\"pound\",\"short_names\":[\"pound\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MONEY WITH WINGS\",\"unified\":\"1F4B8\",\"variations\":[],\"docomo\":null,\"au\":\"EB5B\",\"softbank\":null,\"google\":\"FE4E4\",\"image\":\"1f4b8.png\",\"sheet_x\":20,\"sheet_y\":40,\"short_name\":\"money_with_wings\",\"short_names\":[\"money_with_wings\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHART WITH UPWARDS TREND AND YEN SIGN\",\"unified\":\"1F4B9\",\"variations\":[],\"docomo\":null,\"au\":\"E5DC\",\"softbank\":\"E14A\",\"google\":\"FE4DF\",\"image\":\"1f4b9.png\",\"sheet_x\":21,\"sheet_y\":0,\"short_name\":\"chart\",\"short_names\":[\"chart\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":99,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SEAT\",\"unified\":\"1F4BA\",\"variations\":[],\"docomo\":\"E6B2\",\"au\":null,\"softbank\":\"E11F\",\"google\":\"FE537\",\"image\":\"1f4ba.png\",\"sheet_x\":21,\"sheet_y\":1,\"short_name\":\"seat\",\"short_names\":[\"seat\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PERSONAL COMPUTER\",\"unified\":\"1F4BB\",\"variations\":[],\"docomo\":\"E716\",\"au\":\"E5B8\",\"softbank\":\"E00C\",\"google\":\"FE538\",\"image\":\"1f4bb.png\",\"sheet_x\":21,\"sheet_y\":2,\"short_name\":\"computer\",\"short_names\":[\"computer\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BRIEFCASE\",\"unified\":\"1F4BC\",\"variations\":[],\"docomo\":\"E682\",\"au\":\"E5CE\",\"softbank\":\"E11E\",\"google\":\"FE53B\",\"image\":\"1f4bc.png\",\"sheet_x\":21,\"sheet_y\":3,\"short_name\":\"briefcase\",\"short_names\":[\"briefcase\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":200,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MINIDISC\",\"unified\":\"1F4BD\",\"variations\":[],\"docomo\":null,\"au\":\"E582\",\"softbank\":\"E316\",\"google\":\"FE53C\",\"image\":\"1f4bd.png\",\"sheet_x\":21,\"sheet_y\":4,\"short_name\":\"minidisc\",\"short_names\":[\"minidisc\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FLOPPY DISK\",\"unified\":\"1F4BE\",\"variations\":[],\"docomo\":null,\"au\":\"E562\",\"softbank\":\"E316\",\"google\":\"FE53D\",\"image\":\"1f4be.png\",\"sheet_x\":21,\"sheet_y\":5,\"short_name\":\"floppy_disk\",\"short_names\":[\"floppy_disk\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OPTICAL DISC\",\"unified\":\"1F4BF\",\"variations\":[],\"docomo\":\"E68C\",\"au\":\"E50C\",\"softbank\":\"E126\",\"google\":\"FE81D\",\"image\":\"1f4bf.png\",\"sheet_x\":21,\"sheet_y\":6,\"short_name\":\"cd\",\"short_names\":[\"cd\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DVD\",\"unified\":\"1F4C0\",\"variations\":[],\"docomo\":\"E68C\",\"au\":\"E50C\",\"softbank\":\"E127\",\"google\":\"FE81E\",\"image\":\"1f4c0.png\",\"sheet_x\":21,\"sheet_y\":7,\"short_name\":\"dvd\",\"short_names\":[\"dvd\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FILE FOLDER\",\"unified\":\"1F4C1\",\"variations\":[],\"docomo\":null,\"au\":\"E58F\",\"softbank\":null,\"google\":\"FE543\",\"image\":\"1f4c1.png\",\"sheet_x\":21,\"sheet_y\":8,\"short_name\":\"file_folder\",\"short_names\":[\"file_folder\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":141,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OPEN FILE FOLDER\",\"unified\":\"1F4C2\",\"variations\":[],\"docomo\":null,\"au\":\"E590\",\"softbank\":null,\"google\":\"FE544\",\"image\":\"1f4c2.png\",\"sheet_x\":21,\"sheet_y\":9,\"short_name\":\"open_file_folder\",\"short_names\":[\"open_file_folder\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":142,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PAGE WITH CURL\",\"unified\":\"1F4C3\",\"variations\":[],\"docomo\":\"E689\",\"au\":\"E561\",\"softbank\":\"E301\",\"google\":\"FE540\",\"image\":\"1f4c3.png\",\"sheet_x\":21,\"sheet_y\":10,\"short_name\":\"page_with_curl\",\"short_names\":[\"page_with_curl\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":126,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PAGE FACING UP\",\"unified\":\"1F4C4\",\"variations\":[],\"docomo\":\"E689\",\"au\":\"E569\",\"softbank\":\"E301\",\"google\":\"FE541\",\"image\":\"1f4c4.png\",\"sheet_x\":21,\"sheet_y\":11,\"short_name\":\"page_facing_up\",\"short_names\":[\"page_facing_up\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":131,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CALENDAR\",\"unified\":\"1F4C5\",\"variations\":[],\"docomo\":null,\"au\":\"E563\",\"softbank\":null,\"google\":\"FE542\",\"image\":\"1f4c5.png\",\"sheet_x\":21,\"sheet_y\":12,\"short_name\":\"date\",\"short_names\":[\"date\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":132,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TEAR-OFF CALENDAR\",\"unified\":\"1F4C6\",\"variations\":[],\"docomo\":null,\"au\":\"E56A\",\"softbank\":null,\"google\":\"FE549\",\"image\":\"1f4c6.png\",\"sheet_x\":21,\"sheet_y\":13,\"short_name\":\"calendar\",\"short_names\":[\"calendar\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":133,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CARD INDEX\",\"unified\":\"1F4C7\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E56C\",\"softbank\":\"E148\",\"google\":\"FE54D\",\"image\":\"1f4c7.png\",\"sheet_x\":21,\"sheet_y\":14,\"short_name\":\"card_index\",\"short_names\":[\"card_index\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":135,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHART WITH UPWARDS TREND\",\"unified\":\"1F4C8\",\"variations\":[],\"docomo\":null,\"au\":\"E575\",\"softbank\":\"E14A\",\"google\":\"FE54B\",\"image\":\"1f4c8.png\",\"sheet_x\":21,\"sheet_y\":15,\"short_name\":\"chart_with_upwards_trend\",\"short_names\":[\"chart_with_upwards_trend\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":129,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHART WITH DOWNWARDS TREND\",\"unified\":\"1F4C9\",\"variations\":[],\"docomo\":null,\"au\":\"E576\",\"softbank\":null,\"google\":\"FE54C\",\"image\":\"1f4c9.png\",\"sheet_x\":21,\"sheet_y\":16,\"short_name\":\"chart_with_downwards_trend\",\"short_names\":[\"chart_with_downwards_trend\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":130,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BAR CHART\",\"unified\":\"1F4CA\",\"variations\":[],\"docomo\":null,\"au\":\"E574\",\"softbank\":\"E14A\",\"google\":\"FE54A\",\"image\":\"1f4ca.png\",\"sheet_x\":21,\"sheet_y\":17,\"short_name\":\"bar_chart\",\"short_names\":[\"bar_chart\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":128,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLIPBOARD\",\"unified\":\"1F4CB\",\"variations\":[],\"docomo\":\"E689\",\"au\":\"E564\",\"softbank\":\"E301\",\"google\":\"FE548\",\"image\":\"1f4cb.png\",\"sheet_x\":21,\"sheet_y\":18,\"short_name\":\"clipboard\",\"short_names\":[\"clipboard\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":139,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PUSHPIN\",\"unified\":\"1F4CC\",\"variations\":[],\"docomo\":null,\"au\":\"E56D\",\"softbank\":null,\"google\":\"FE54E\",\"image\":\"1f4cc.png\",\"sheet_x\":21,\"sheet_y\":19,\"short_name\":\"pushpin\",\"short_names\":[\"pushpin\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":161,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROUND PUSHPIN\",\"unified\":\"1F4CD\",\"variations\":[],\"docomo\":null,\"au\":\"E560\",\"softbank\":null,\"google\":\"FE53F\",\"image\":\"1f4cd.png\",\"sheet_x\":21,\"sheet_y\":20,\"short_name\":\"round_pushpin\",\"short_names\":[\"round_pushpin\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":162,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PAPERCLIP\",\"unified\":\"1F4CE\",\"variations\":[],\"docomo\":\"E730\",\"au\":\"E4A0\",\"softbank\":null,\"google\":\"FE53A\",\"image\":\"1f4ce.png\",\"sheet_x\":21,\"sheet_y\":21,\"short_name\":\"paperclip\",\"short_names\":[\"paperclip\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":156,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STRAIGHT RULER\",\"unified\":\"1F4CF\",\"variations\":[],\"docomo\":null,\"au\":\"E570\",\"softbank\":null,\"google\":\"FE550\",\"image\":\"1f4cf.png\",\"sheet_x\":21,\"sheet_y\":22,\"short_name\":\"straight_ruler\",\"short_names\":[\"straight_ruler\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":160,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRIANGULAR RULER\",\"unified\":\"1F4D0\",\"variations\":[],\"docomo\":null,\"au\":\"E4A2\",\"softbank\":null,\"google\":\"FE551\",\"image\":\"1f4d0.png\",\"sheet_x\":21,\"sheet_y\":23,\"short_name\":\"triangular_ruler\",\"short_names\":[\"triangular_ruler\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":159,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOOKMARK TABS\",\"unified\":\"1F4D1\",\"variations\":[],\"docomo\":\"E689\",\"au\":\"EB0B\",\"softbank\":\"E301\",\"google\":\"FE552\",\"image\":\"1f4d1.png\",\"sheet_x\":21,\"sheet_y\":24,\"short_name\":\"bookmark_tabs\",\"short_names\":[\"bookmark_tabs\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":127,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEDGER\",\"unified\":\"1F4D2\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E56E\",\"softbank\":\"E148\",\"google\":\"FE54F\",\"image\":\"1f4d2.png\",\"sheet_x\":21,\"sheet_y\":25,\"short_name\":\"ledger\",\"short_names\":[\"ledger\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":152,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NOTEBOOK\",\"unified\":\"1F4D3\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E56B\",\"softbank\":\"E148\",\"google\":\"FE545\",\"image\":\"1f4d3.png\",\"sheet_x\":21,\"sheet_y\":26,\"short_name\":\"notebook\",\"short_names\":[\"notebook\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":146,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NOTEBOOK WITH DECORATIVE COVER\",\"unified\":\"1F4D4\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E49D\",\"softbank\":\"E148\",\"google\":\"FE547\",\"image\":\"1f4d4.png\",\"sheet_x\":21,\"sheet_y\":27,\"short_name\":\"notebook_with_decorative_cover\",\"short_names\":[\"notebook_with_decorative_cover\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":151,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOSED BOOK\",\"unified\":\"1F4D5\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E568\",\"softbank\":\"E148\",\"google\":\"FE502\",\"image\":\"1f4d5.png\",\"sheet_x\":21,\"sheet_y\":28,\"short_name\":\"closed_book\",\"short_names\":[\"closed_book\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":147,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OPEN BOOK\",\"unified\":\"1F4D6\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E49F\",\"softbank\":\"E148\",\"google\":\"FE546\",\"image\":\"1f4d6.png\",\"sheet_x\":21,\"sheet_y\":29,\"short_name\":\"book\",\"short_names\":[\"book\",\"open_book\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":154,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GREEN BOOK\",\"unified\":\"1F4D7\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E565\",\"softbank\":\"E148\",\"google\":\"FE4FF\",\"image\":\"1f4d7.png\",\"sheet_x\":21,\"sheet_y\":30,\"short_name\":\"green_book\",\"short_names\":[\"green_book\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":148,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLUE BOOK\",\"unified\":\"1F4D8\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E566\",\"softbank\":\"E148\",\"google\":\"FE500\",\"image\":\"1f4d8.png\",\"sheet_x\":21,\"sheet_y\":31,\"short_name\":\"blue_book\",\"short_names\":[\"blue_book\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":149,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ORANGE BOOK\",\"unified\":\"1F4D9\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E567\",\"softbank\":\"E148\",\"google\":\"FE501\",\"image\":\"1f4d9.png\",\"sheet_x\":21,\"sheet_y\":32,\"short_name\":\"orange_book\",\"short_names\":[\"orange_book\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":150,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOOKS\",\"unified\":\"1F4DA\",\"variations\":[],\"docomo\":\"E683\",\"au\":\"E56F\",\"softbank\":\"E148\",\"google\":\"FE503\",\"image\":\"1f4da.png\",\"sheet_x\":21,\"sheet_y\":33,\"short_name\":\"books\",\"short_names\":[\"books\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":153,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NAME BADGE\",\"unified\":\"1F4DB\",\"variations\":[],\"docomo\":null,\"au\":\"E51D\",\"softbank\":null,\"google\":\"FE504\",\"image\":\"1f4db.png\",\"sheet_x\":21,\"sheet_y\":34,\"short_name\":\"name_badge\",\"short_names\":[\"name_badge\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":70,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SCROLL\",\"unified\":\"1F4DC\",\"variations\":[],\"docomo\":\"E70A\",\"au\":\"E55F\",\"softbank\":null,\"google\":\"FE4FD\",\"image\":\"1f4dc.png\",\"sheet_x\":21,\"sheet_y\":35,\"short_name\":\"scroll\",\"short_names\":[\"scroll\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":125,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MEMO\",\"unified\":\"1F4DD\",\"variations\":[],\"docomo\":\"E689\",\"au\":\"EA92\",\"softbank\":\"E301\",\"google\":\"FE527\",\"image\":\"1f4dd.png\",\"sheet_x\":21,\"sheet_y\":36,\"short_name\":\"memo\",\"short_names\":[\"memo\",\"pencil\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":173,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TELEPHONE RECEIVER\",\"unified\":\"1F4DE\",\"variations\":[],\"docomo\":\"E687\",\"au\":\"E51E\",\"softbank\":\"E009\",\"google\":\"FE524\",\"image\":\"1f4de.png\",\"sheet_x\":21,\"sheet_y\":37,\"short_name\":\"telephone_receiver\",\"short_names\":[\"telephone_receiver\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PAGER\",\"unified\":\"1F4DF\",\"variations\":[],\"docomo\":\"E65A\",\"au\":\"E59B\",\"softbank\":null,\"google\":\"FE522\",\"image\":\"1f4df.png\",\"sheet_x\":21,\"sheet_y\":38,\"short_name\":\"pager\",\"short_names\":[\"pager\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FAX MACHINE\",\"unified\":\"1F4E0\",\"variations\":[],\"docomo\":\"E6D0\",\"au\":\"E520\",\"softbank\":\"E00B\",\"google\":\"FE528\",\"image\":\"1f4e0.png\",\"sheet_x\":21,\"sheet_y\":39,\"short_name\":\"fax\",\"short_names\":[\"fax\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SATELLITE ANTENNA\",\"unified\":\"1F4E1\",\"variations\":[],\"docomo\":null,\"au\":\"E4A8\",\"softbank\":\"E14B\",\"google\":\"FE531\",\"image\":\"1f4e1.png\",\"sheet_x\":21,\"sheet_y\":40,\"short_name\":\"satellite\",\"short_names\":[\"satellite\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PUBLIC ADDRESS LOUDSPEAKER\",\"unified\":\"1F4E2\",\"variations\":[],\"docomo\":null,\"au\":\"E511\",\"softbank\":\"E142\",\"google\":\"FE52F\",\"image\":\"1f4e2.png\",\"sheet_x\":22,\"sheet_y\":0,\"short_name\":\"loudspeaker\",\"short_names\":[\"loudspeaker\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":232,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHEERING MEGAPHONE\",\"unified\":\"1F4E3\",\"variations\":[],\"docomo\":null,\"au\":\"E511\",\"softbank\":\"E317\",\"google\":\"FE530\",\"image\":\"1f4e3.png\",\"sheet_x\":22,\"sheet_y\":1,\"short_name\":\"mega\",\"short_names\":[\"mega\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":231,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OUTBOX TRAY\",\"unified\":\"1F4E4\",\"variations\":[],\"docomo\":null,\"au\":\"E592\",\"softbank\":null,\"google\":\"FE533\",\"image\":\"1f4e4.png\",\"sheet_x\":22,\"sheet_y\":2,\"short_name\":\"outbox_tray\",\"short_names\":[\"outbox_tray\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":124,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INBOX TRAY\",\"unified\":\"1F4E5\",\"variations\":[],\"docomo\":null,\"au\":\"E593\",\"softbank\":null,\"google\":\"FE534\",\"image\":\"1f4e5.png\",\"sheet_x\":22,\"sheet_y\":3,\"short_name\":\"inbox_tray\",\"short_names\":[\"inbox_tray\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":123,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PACKAGE\",\"unified\":\"1F4E6\",\"variations\":[],\"docomo\":\"E685\",\"au\":\"E51F\",\"softbank\":\"E112\",\"google\":\"FE535\",\"image\":\"1f4e6.png\",\"sheet_x\":22,\"sheet_y\":4,\"short_name\":\"package\",\"short_names\":[\"package\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":121,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"E-MAIL SYMBOL\",\"unified\":\"1F4E7\",\"variations\":[],\"docomo\":\"E6D3\",\"au\":\"EB71\",\"softbank\":\"E103\",\"google\":\"FEB92\",\"image\":\"1f4e7.png\",\"sheet_x\":22,\"sheet_y\":5,\"short_name\":\"e-mail\",\"short_names\":[\"e-mail\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":114,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INCOMING ENVELOPE\",\"unified\":\"1F4E8\",\"variations\":[],\"docomo\":\"E6CF\",\"au\":\"E591\",\"softbank\":\"E103\",\"google\":\"FE52A\",\"image\":\"1f4e8.png\",\"sheet_x\":22,\"sheet_y\":6,\"short_name\":\"incoming_envelope\",\"short_names\":[\"incoming_envelope\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":113,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ENVELOPE WITH DOWNWARDS ARROW ABOVE\",\"unified\":\"1F4E9\",\"variations\":[],\"docomo\":\"E6CF\",\"au\":\"EB62\",\"softbank\":\"E103\",\"google\":\"FE52B\",\"image\":\"1f4e9.png\",\"sheet_x\":22,\"sheet_y\":7,\"short_name\":\"envelope_with_arrow\",\"short_names\":[\"envelope_with_arrow\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":112,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOSED MAILBOX WITH LOWERED FLAG\",\"unified\":\"1F4EA\",\"variations\":[],\"docomo\":\"E665\",\"au\":\"E51B\",\"softbank\":\"E101\",\"google\":\"FE52C\",\"image\":\"1f4ea.png\",\"sheet_x\":22,\"sheet_y\":8,\"short_name\":\"mailbox_closed\",\"short_names\":[\"mailbox_closed\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":117,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOSED MAILBOX WITH RAISED FLAG\",\"unified\":\"1F4EB\",\"variations\":[],\"docomo\":\"E665\",\"au\":\"EB0A\",\"softbank\":\"E101\",\"google\":\"FE52D\",\"image\":\"1f4eb.png\",\"sheet_x\":22,\"sheet_y\":9,\"short_name\":\"mailbox\",\"short_names\":[\"mailbox\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":118,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OPEN MAILBOX WITH RAISED FLAG\",\"unified\":\"1F4EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4ec.png\",\"sheet_x\":22,\"sheet_y\":10,\"short_name\":\"mailbox_with_mail\",\"short_names\":[\"mailbox_with_mail\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":119,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OPEN MAILBOX WITH LOWERED FLAG\",\"unified\":\"1F4ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4ed.png\",\"sheet_x\":22,\"sheet_y\":11,\"short_name\":\"mailbox_with_no_mail\",\"short_names\":[\"mailbox_with_no_mail\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":120,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POSTBOX\",\"unified\":\"1F4EE\",\"variations\":[],\"docomo\":\"E665\",\"au\":\"E51B\",\"softbank\":\"E102\",\"google\":\"FE52E\",\"image\":\"1f4ee.png\",\"sheet_x\":22,\"sheet_y\":12,\"short_name\":\"postbox\",\"short_names\":[\"postbox\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":116,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POSTAL HORN\",\"unified\":\"1F4EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4ef.png\",\"sheet_x\":22,\"sheet_y\":13,\"short_name\":\"postal_horn\",\"short_names\":[\"postal_horn\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":122,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEWSPAPER\",\"unified\":\"1F4F0\",\"variations\":[],\"docomo\":null,\"au\":\"E58B\",\"softbank\":null,\"google\":\"FE822\",\"image\":\"1f4f0.png\",\"sheet_x\":22,\"sheet_y\":14,\"short_name\":\"newspaper\",\"short_names\":[\"newspaper\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":145,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOBILE PHONE\",\"unified\":\"1F4F1\",\"variations\":[],\"docomo\":\"E688\",\"au\":\"E588\",\"softbank\":\"E00A\",\"google\":\"FE525\",\"image\":\"1f4f1.png\",\"sheet_x\":22,\"sheet_y\":15,\"short_name\":\"iphone\",\"short_names\":[\"iphone\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOBILE PHONE WITH RIGHTWARDS ARROW AT LEFT\",\"unified\":\"1F4F2\",\"variations\":[],\"docomo\":\"E6CE\",\"au\":\"EB08\",\"softbank\":\"E104\",\"google\":\"FE526\",\"image\":\"1f4f2.png\",\"sheet_x\":22,\"sheet_y\":16,\"short_name\":\"calling\",\"short_names\":[\"calling\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VIBRATION MODE\",\"unified\":\"1F4F3\",\"variations\":[],\"docomo\":null,\"au\":\"EA90\",\"softbank\":\"E250\",\"google\":\"FE839\",\"image\":\"1f4f3.png\",\"sheet_x\":22,\"sheet_y\":17,\"short_name\":\"vibration_mode\",\"short_names\":[\"vibration_mode\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOBILE PHONE OFF\",\"unified\":\"1F4F4\",\"variations\":[],\"docomo\":null,\"au\":\"EA91\",\"softbank\":\"E251\",\"google\":\"FE83A\",\"image\":\"1f4f4.png\",\"sheet_x\":22,\"sheet_y\":18,\"short_name\":\"mobile_phone_off\",\"short_names\":[\"mobile_phone_off\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NO MOBILE PHONES\",\"unified\":\"1F4F5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4f5.png\",\"sheet_x\":22,\"sheet_y\":19,\"short_name\":\"no_mobile_phones\",\"short_names\":[\"no_mobile_phones\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":81,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ANTENNA WITH BARS\",\"unified\":\"1F4F6\",\"variations\":[],\"docomo\":null,\"au\":\"EA84\",\"softbank\":\"E20B\",\"google\":\"FE838\",\"image\":\"1f4f6.png\",\"sheet_x\":22,\"sheet_y\":20,\"short_name\":\"signal_strength\",\"short_names\":[\"signal_strength\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":126,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAMERA\",\"unified\":\"1F4F7\",\"variations\":[],\"docomo\":\"E681\",\"au\":\"E515\",\"softbank\":\"E008\",\"google\":\"FE4EF\",\"image\":\"1f4f7.png\",\"sheet_x\":22,\"sheet_y\":21,\"short_name\":\"camera\",\"short_names\":[\"camera\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAMERA WITH FLASH\",\"unified\":\"1F4F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4f8.png\",\"sheet_x\":22,\"sheet_y\":22,\"short_name\":\"camera_with_flash\",\"short_names\":[\"camera_with_flash\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VIDEO CAMERA\",\"unified\":\"1F4F9\",\"variations\":[],\"docomo\":\"E677\",\"au\":\"E57E\",\"softbank\":\"E03D\",\"google\":\"FE4F9\",\"image\":\"1f4f9.png\",\"sheet_x\":22,\"sheet_y\":23,\"short_name\":\"video_camera\",\"short_names\":[\"video_camera\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TELEVISION\",\"unified\":\"1F4FA\",\"variations\":[],\"docomo\":\"E68A\",\"au\":\"E502\",\"softbank\":\"E12A\",\"google\":\"FE81C\",\"image\":\"1f4fa.png\",\"sheet_x\":22,\"sheet_y\":24,\"short_name\":\"tv\",\"short_names\":[\"tv\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RADIO\",\"unified\":\"1F4FB\",\"variations\":[],\"docomo\":null,\"au\":\"E5B9\",\"softbank\":\"E128\",\"google\":\"FE81F\",\"image\":\"1f4fb.png\",\"sheet_x\":22,\"sheet_y\":25,\"short_name\":\"radio\",\"short_names\":[\"radio\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VIDEOCASSETTE\",\"unified\":\"1F4FC\",\"variations\":[],\"docomo\":null,\"au\":\"E580\",\"softbank\":\"E129\",\"google\":\"FE820\",\"image\":\"1f4fc.png\",\"sheet_x\":22,\"sheet_y\":26,\"short_name\":\"vhs\",\"short_names\":[\"vhs\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FILM PROJECTOR\",\"unified\":\"1F4FD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4fd.png\",\"sheet_x\":22,\"sheet_y\":27,\"short_name\":\"film_projector\",\"short_names\":[\"film_projector\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PRAYER BEADS\",\"unified\":\"1F4FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f4ff.png\",\"sheet_x\":22,\"sheet_y\":28,\"short_name\":\"prayer_beads\",\"short_names\":[\"prayer_beads\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":75,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TWISTED RIGHTWARDS ARROWS\",\"unified\":\"1F500\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f500.png\",\"sheet_x\":22,\"sheet_y\":29,\"short_name\":\"twisted_rightwards_arrows\",\"short_names\":[\"twisted_rightwards_arrows\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":155,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS\",\"unified\":\"1F501\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f501.png\",\"sheet_x\":22,\"sheet_y\":30,\"short_name\":\"repeat\",\"short_names\":[\"repeat\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":156,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCKWISE RIGHTWARDS AND LEFTWARDS OPEN CIRCLE ARROWS WITH CIRCLED ONE OVERLAY\",\"unified\":\"1F502\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f502.png\",\"sheet_x\":22,\"sheet_y\":31,\"short_name\":\"repeat_one\",\"short_names\":[\"repeat_one\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":157,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS\",\"unified\":\"1F503\",\"variations\":[],\"docomo\":\"E735\",\"au\":\"EB0D\",\"softbank\":null,\"google\":\"FEB91\",\"image\":\"1f503.png\",\"sheet_x\":22,\"sheet_y\":32,\"short_name\":\"arrows_clockwise\",\"short_names\":[\"arrows_clockwise\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":190,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ANTICLOCKWISE DOWNWARDS AND UPWARDS OPEN CIRCLE ARROWS\",\"unified\":\"1F504\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f504.png\",\"sheet_x\":22,\"sheet_y\":33,\"short_name\":\"arrows_counterclockwise\",\"short_names\":[\"arrows_counterclockwise\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":173,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOW BRIGHTNESS SYMBOL\",\"unified\":\"1F505\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f505.png\",\"sheet_x\":22,\"sheet_y\":34,\"short_name\":\"low_brightness\",\"short_names\":[\"low_brightness\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":89,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HIGH BRIGHTNESS SYMBOL\",\"unified\":\"1F506\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f506.png\",\"sheet_x\":22,\"sheet_y\":35,\"short_name\":\"high_brightness\",\"short_names\":[\"high_brightness\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":90,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPEAKER WITH CANCELLATION STROKE\",\"unified\":\"1F507\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f507.png\",\"sheet_x\":22,\"sheet_y\":36,\"short_name\":\"mute\",\"short_names\":[\"mute\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":230,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPEAKER\",\"unified\":\"1F508\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f508.png\",\"sheet_x\":22,\"sheet_y\":37,\"short_name\":\"speaker\",\"short_names\":[\"speaker\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":227,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPEAKER WITH ONE SOUND WAVE\",\"unified\":\"1F509\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f509.png\",\"sheet_x\":22,\"sheet_y\":38,\"short_name\":\"sound\",\"short_names\":[\"sound\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":228,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPEAKER WITH THREE SOUND WAVES\",\"unified\":\"1F50A\",\"variations\":[],\"docomo\":null,\"au\":\"E511\",\"softbank\":\"E141\",\"google\":\"FE821\",\"image\":\"1f50a.png\",\"sheet_x\":22,\"sheet_y\":39,\"short_name\":\"loud_sound\",\"short_names\":[\"loud_sound\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":229,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BATTERY\",\"unified\":\"1F50B\",\"variations\":[],\"docomo\":null,\"au\":\"E584\",\"softbank\":null,\"google\":\"FE4FC\",\"image\":\"1f50b.png\",\"sheet_x\":22,\"sheet_y\":40,\"short_name\":\"battery\",\"short_names\":[\"battery\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ELECTRIC PLUG\",\"unified\":\"1F50C\",\"variations\":[],\"docomo\":null,\"au\":\"E589\",\"softbank\":null,\"google\":\"FE4FE\",\"image\":\"1f50c.png\",\"sheet_x\":23,\"sheet_y\":0,\"short_name\":\"electric_plug\",\"short_names\":[\"electric_plug\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEFT-POINTING MAGNIFYING GLASS\",\"unified\":\"1F50D\",\"variations\":[],\"docomo\":\"E6DC\",\"au\":\"E518\",\"softbank\":\"E114\",\"google\":\"FEB85\",\"image\":\"1f50d.png\",\"sheet_x\":23,\"sheet_y\":1,\"short_name\":\"mag\",\"short_names\":[\"mag\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":177,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RIGHT-POINTING MAGNIFYING GLASS\",\"unified\":\"1F50E\",\"variations\":[],\"docomo\":\"E6DC\",\"au\":\"EB05\",\"softbank\":\"E114\",\"google\":\"FEB8D\",\"image\":\"1f50e.png\",\"sheet_x\":23,\"sheet_y\":2,\"short_name\":\"mag_right\",\"short_names\":[\"mag_right\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":178,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOCK WITH INK PEN\",\"unified\":\"1F50F\",\"variations\":[],\"docomo\":\"E6D9\",\"au\":\"EB0C\",\"softbank\":\"E144\",\"google\":\"FEB90\",\"image\":\"1f50f.png\",\"sheet_x\":23,\"sheet_y\":3,\"short_name\":\"lock_with_ink_pen\",\"short_names\":[\"lock_with_ink_pen\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":169,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOSED LOCK WITH KEY\",\"unified\":\"1F510\",\"variations\":[],\"docomo\":\"E6D9\",\"au\":\"EAFC\",\"softbank\":\"E144\",\"google\":\"FEB8A\",\"image\":\"1f510.png\",\"sheet_x\":23,\"sheet_y\":4,\"short_name\":\"closed_lock_with_key\",\"short_names\":[\"closed_lock_with_key\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":166,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEY\",\"unified\":\"1F511\",\"variations\":[],\"docomo\":\"E6D9\",\"au\":\"E519\",\"softbank\":\"E03F\",\"google\":\"FEB82\",\"image\":\"1f511.png\",\"sheet_x\":23,\"sheet_y\":5,\"short_name\":\"key\",\"short_names\":[\"key\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":89,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOCK\",\"unified\":\"1F512\",\"variations\":[],\"docomo\":\"E6D9\",\"au\":\"E51C\",\"softbank\":\"E144\",\"google\":\"FEB86\",\"image\":\"1f512.png\",\"sheet_x\":23,\"sheet_y\":6,\"short_name\":\"lock\",\"short_names\":[\"lock\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":167,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OPEN LOCK\",\"unified\":\"1F513\",\"variations\":[],\"docomo\":\"E6D9\",\"au\":\"E51C\",\"softbank\":\"E145\",\"google\":\"FEB87\",\"image\":\"1f513.png\",\"sheet_x\":23,\"sheet_y\":7,\"short_name\":\"unlock\",\"short_names\":[\"unlock\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":168,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BELL\",\"unified\":\"1F514\",\"variations\":[],\"docomo\":\"E713\",\"au\":\"E512\",\"softbank\":\"E325\",\"google\":\"FE4F2\",\"image\":\"1f514.png\",\"sheet_x\":23,\"sheet_y\":8,\"short_name\":\"bell\",\"short_names\":[\"bell\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":233,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BELL WITH CANCELLATION STROKE\",\"unified\":\"1F515\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f515.png\",\"sheet_x\":23,\"sheet_y\":9,\"short_name\":\"no_bell\",\"short_names\":[\"no_bell\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":234,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BOOKMARK\",\"unified\":\"1F516\",\"variations\":[],\"docomo\":null,\"au\":\"EB07\",\"softbank\":null,\"google\":\"FEB8F\",\"image\":\"1f516.png\",\"sheet_x\":23,\"sheet_y\":10,\"short_name\":\"bookmark\",\"short_names\":[\"bookmark\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":85,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LINK SYMBOL\",\"unified\":\"1F517\",\"variations\":[],\"docomo\":null,\"au\":\"E58A\",\"softbank\":null,\"google\":\"FEB4B\",\"image\":\"1f517.png\",\"sheet_x\":23,\"sheet_y\":11,\"short_name\":\"link\",\"short_names\":[\"link\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":155,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RADIO BUTTON\",\"unified\":\"1F518\",\"variations\":[],\"docomo\":null,\"au\":\"EB04\",\"softbank\":null,\"google\":\"FEB8C\",\"image\":\"1f518.png\",\"sheet_x\":23,\"sheet_y\":12,\"short_name\":\"radio_button\",\"short_names\":[\"radio_button\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":206,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BACK WITH LEFTWARDS ARROW ABOVE\",\"unified\":\"1F519\",\"variations\":[],\"docomo\":null,\"au\":\"EB06\",\"softbank\":\"E235\",\"google\":\"FEB8E\",\"image\":\"1f519.png\",\"sheet_x\":23,\"sheet_y\":13,\"short_name\":\"back\",\"short_names\":[\"back\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":201,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"END WITH LEFTWARDS ARROW ABOVE\",\"unified\":\"1F51A\",\"variations\":[],\"docomo\":\"E6B9\",\"au\":null,\"softbank\":null,\"google\":\"FE01A\",\"image\":\"1f51a.png\",\"sheet_x\":23,\"sheet_y\":14,\"short_name\":\"end\",\"short_names\":[\"end\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":200,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ON WITH EXCLAMATION MARK WITH LEFT RIGHT ARROW ABOVE\",\"unified\":\"1F51B\",\"variations\":[],\"docomo\":\"E6B8\",\"au\":null,\"softbank\":null,\"google\":\"FE019\",\"image\":\"1f51b.png\",\"sheet_x\":23,\"sheet_y\":15,\"short_name\":\"on\",\"short_names\":[\"on\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":202,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SOON WITH RIGHTWARDS ARROW ABOVE\",\"unified\":\"1F51C\",\"variations\":[],\"docomo\":\"E6B7\",\"au\":null,\"softbank\":null,\"google\":\"FE018\",\"image\":\"1f51c.png\",\"sheet_x\":23,\"sheet_y\":16,\"short_name\":\"soon\",\"short_names\":[\"soon\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":204,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TOP WITH UPWARDS ARROW ABOVE\",\"unified\":\"1F51D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E24C\",\"google\":\"FEB42\",\"image\":\"1f51d.png\",\"sheet_x\":23,\"sheet_y\":17,\"short_name\":\"top\",\"short_names\":[\"top\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":203,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NO ONE UNDER EIGHTEEN SYMBOL\",\"unified\":\"1F51E\",\"variations\":[],\"docomo\":null,\"au\":\"EA83\",\"softbank\":\"E207\",\"google\":\"FEB25\",\"image\":\"1f51e.png\",\"sheet_x\":23,\"sheet_y\":18,\"short_name\":\"underage\",\"short_names\":[\"underage\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":80,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP TEN\",\"unified\":\"1F51F\",\"variations\":[],\"docomo\":null,\"au\":\"E52B\",\"softbank\":null,\"google\":\"FE83B\",\"image\":\"1f51f.png\",\"sheet_x\":23,\"sheet_y\":19,\"short_name\":\"keycap_ten\",\"short_names\":[\"keycap_ten\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":144,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INPUT SYMBOL FOR LATIN CAPITAL LETTERS\",\"unified\":\"1F520\",\"variations\":[],\"docomo\":null,\"au\":\"EAFD\",\"softbank\":null,\"google\":\"FEB7C\",\"image\":\"1f520.png\",\"sheet_x\":23,\"sheet_y\":20,\"short_name\":\"capital_abcd\",\"short_names\":[\"capital_abcd\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":183,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INPUT SYMBOL FOR LATIN SMALL LETTERS\",\"unified\":\"1F521\",\"variations\":[],\"docomo\":null,\"au\":\"EAFE\",\"softbank\":null,\"google\":\"FEB7D\",\"image\":\"1f521.png\",\"sheet_x\":23,\"sheet_y\":21,\"short_name\":\"abcd\",\"short_names\":[\"abcd\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":182,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INPUT SYMBOL FOR NUMBERS\",\"unified\":\"1F522\",\"variations\":[],\"docomo\":null,\"au\":\"EAFF\",\"softbank\":null,\"google\":\"FEB7E\",\"image\":\"1f522.png\",\"sheet_x\":23,\"sheet_y\":22,\"short_name\":\"1234\",\"short_names\":[\"1234\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":145,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INPUT SYMBOL FOR SYMBOLS\",\"unified\":\"1F523\",\"variations\":[],\"docomo\":null,\"au\":\"EB00\",\"softbank\":null,\"google\":\"FEB7F\",\"image\":\"1f523.png\",\"sheet_x\":23,\"sheet_y\":23,\"short_name\":\"symbols\",\"short_names\":[\"symbols\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":184,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"INPUT SYMBOL FOR LATIN LETTERS\",\"unified\":\"1F524\",\"variations\":[],\"docomo\":null,\"au\":\"EB55\",\"softbank\":null,\"google\":\"FEB80\",\"image\":\"1f524.png\",\"sheet_x\":23,\"sheet_y\":24,\"short_name\":\"abc\",\"short_names\":[\"abc\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":181,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FIRE\",\"unified\":\"1F525\",\"variations\":[],\"docomo\":null,\"au\":\"E47B\",\"softbank\":\"E11D\",\"google\":\"FE4F6\",\"image\":\"1f525.png\",\"sheet_x\":23,\"sheet_y\":25,\"short_name\":\"fire\",\"short_names\":[\"fire\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":133,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ELECTRIC TORCH\",\"unified\":\"1F526\",\"variations\":[],\"docomo\":\"E6FB\",\"au\":\"E583\",\"softbank\":null,\"google\":\"FE4FB\",\"image\":\"1f526.png\",\"sheet_x\":23,\"sheet_y\":26,\"short_name\":\"flashlight\",\"short_names\":[\"flashlight\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WRENCH\",\"unified\":\"1F527\",\"variations\":[],\"docomo\":\"E718\",\"au\":\"E587\",\"softbank\":null,\"google\":\"FE4C9\",\"image\":\"1f527.png\",\"sheet_x\":23,\"sheet_y\":27,\"short_name\":\"wrench\",\"short_names\":[\"wrench\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HAMMER\",\"unified\":\"1F528\",\"variations\":[],\"docomo\":null,\"au\":\"E5CB\",\"softbank\":\"E116\",\"google\":\"FE4CA\",\"image\":\"1f528.png\",\"sheet_x\":23,\"sheet_y\":28,\"short_name\":\"hammer\",\"short_names\":[\"hammer\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NUT AND BOLT\",\"unified\":\"1F529\",\"variations\":[],\"docomo\":null,\"au\":\"E581\",\"softbank\":null,\"google\":\"FE4CB\",\"image\":\"1f529.png\",\"sheet_x\":23,\"sheet_y\":29,\"short_name\":\"nut_and_bolt\",\"short_names\":[\"nut_and_bolt\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":60,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOCHO\",\"unified\":\"1F52A\",\"variations\":[],\"docomo\":null,\"au\":\"E57F\",\"softbank\":null,\"google\":\"FE4FA\",\"image\":\"1f52a.png\",\"sheet_x\":23,\"sheet_y\":30,\"short_name\":\"hocho\",\"short_names\":[\"hocho\",\"knife\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":65,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PISTOL\",\"unified\":\"1F52B\",\"variations\":[],\"docomo\":null,\"au\":\"E50A\",\"softbank\":\"E113\",\"google\":\"FE4F5\",\"image\":\"1f52b.png\",\"sheet_x\":23,\"sheet_y\":31,\"short_name\":\"gun\",\"short_names\":[\"gun\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":63,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MICROSCOPE\",\"unified\":\"1F52C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f52c.png\",\"sheet_x\":23,\"sheet_y\":32,\"short_name\":\"microscope\",\"short_names\":[\"microscope\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":79,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TELESCOPE\",\"unified\":\"1F52D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f52d.png\",\"sheet_x\":23,\"sheet_y\":33,\"short_name\":\"telescope\",\"short_names\":[\"telescope\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":78,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CRYSTAL BALL\",\"unified\":\"1F52E\",\"variations\":[],\"docomo\":null,\"au\":\"EA8F\",\"softbank\":\"E23E\",\"google\":\"FE4F7\",\"image\":\"1f52e.png\",\"sheet_x\":23,\"sheet_y\":34,\"short_name\":\"crystal_ball\",\"short_names\":[\"crystal_ball\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":74,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SIX POINTED STAR WITH MIDDLE DOT\",\"unified\":\"1F52F\",\"variations\":[],\"docomo\":null,\"au\":\"EA8F\",\"softbank\":\"E23E\",\"google\":\"FE4F8\",\"image\":\"1f52f.png\",\"sheet_x\":23,\"sheet_y\":35,\"short_name\":\"six_pointed_star\",\"short_names\":[\"six_pointed_star\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JAPANESE SYMBOL FOR BEGINNER\",\"unified\":\"1F530\",\"variations\":[],\"docomo\":null,\"au\":\"E480\",\"softbank\":\"E209\",\"google\":\"FE044\",\"image\":\"1f530.png\",\"sheet_x\":23,\"sheet_y\":36,\"short_name\":\"beginner\",\"short_names\":[\"beginner\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":96,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRIDENT EMBLEM\",\"unified\":\"1F531\",\"variations\":[],\"docomo\":\"E71A\",\"au\":\"E5C9\",\"softbank\":\"E031\",\"google\":\"FE4D2\",\"image\":\"1f531.png\",\"sheet_x\":23,\"sheet_y\":37,\"short_name\":\"trident\",\"short_names\":[\"trident\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":91,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BLACK SQUARE BUTTON\",\"unified\":\"1F532\",\"variations\":[],\"docomo\":\"E69C\",\"au\":\"E54B\",\"softbank\":\"E21A\",\"google\":\"FEB64\",\"image\":\"1f532.png\",\"sheet_x\":23,\"sheet_y\":38,\"short_name\":\"black_square_button\",\"short_names\":[\"black_square_button\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":225,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WHITE SQUARE BUTTON\",\"unified\":\"1F533\",\"variations\":[],\"docomo\":\"E69C\",\"au\":\"E54B\",\"softbank\":\"E21B\",\"google\":\"FEB67\",\"image\":\"1f533.png\",\"sheet_x\":23,\"sheet_y\":39,\"short_name\":\"white_square_button\",\"short_names\":[\"white_square_button\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":226,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LARGE RED CIRCLE\",\"unified\":\"1F534\",\"variations\":[],\"docomo\":\"E69C\",\"au\":\"E54A\",\"softbank\":\"E219\",\"google\":\"FEB63\",\"image\":\"1f534.png\",\"sheet_x\":23,\"sheet_y\":40,\"short_name\":\"red_circle\",\"short_names\":[\"red_circle\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":209,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LARGE BLUE CIRCLE\",\"unified\":\"1F535\",\"variations\":[],\"docomo\":\"E69C\",\"au\":\"E54B\",\"softbank\":\"E21A\",\"google\":\"FEB64\",\"image\":\"1f535.png\",\"sheet_x\":24,\"sheet_y\":0,\"short_name\":\"large_blue_circle\",\"short_names\":[\"large_blue_circle\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":210,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LARGE ORANGE DIAMOND\",\"unified\":\"1F536\",\"variations\":[],\"docomo\":null,\"au\":\"E546\",\"softbank\":\"E21B\",\"google\":\"FEB73\",\"image\":\"1f536.png\",\"sheet_x\":24,\"sheet_y\":1,\"short_name\":\"large_orange_diamond\",\"short_names\":[\"large_orange_diamond\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":213,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LARGE BLUE DIAMOND\",\"unified\":\"1F537\",\"variations\":[],\"docomo\":null,\"au\":\"E547\",\"softbank\":\"E21B\",\"google\":\"FEB74\",\"image\":\"1f537.png\",\"sheet_x\":24,\"sheet_y\":2,\"short_name\":\"large_blue_diamond\",\"short_names\":[\"large_blue_diamond\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":214,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMALL ORANGE DIAMOND\",\"unified\":\"1F538\",\"variations\":[],\"docomo\":null,\"au\":\"E536\",\"softbank\":\"E21B\",\"google\":\"FEB75\",\"image\":\"1f538.png\",\"sheet_x\":24,\"sheet_y\":3,\"short_name\":\"small_orange_diamond\",\"short_names\":[\"small_orange_diamond\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":211,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMALL BLUE DIAMOND\",\"unified\":\"1F539\",\"variations\":[],\"docomo\":null,\"au\":\"E537\",\"softbank\":\"E21B\",\"google\":\"FEB76\",\"image\":\"1f539.png\",\"sheet_x\":24,\"sheet_y\":4,\"short_name\":\"small_blue_diamond\",\"short_names\":[\"small_blue_diamond\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":212,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UP-POINTING RED TRIANGLE\",\"unified\":\"1F53A\",\"variations\":[],\"docomo\":null,\"au\":\"E55A\",\"softbank\":null,\"google\":\"FEB78\",\"image\":\"1f53a.png\",\"sheet_x\":24,\"sheet_y\":5,\"short_name\":\"small_red_triangle\",\"short_names\":[\"small_red_triangle\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":215,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOWN-POINTING RED TRIANGLE\",\"unified\":\"1F53B\",\"variations\":[],\"docomo\":null,\"au\":\"E55B\",\"softbank\":null,\"google\":\"FEB79\",\"image\":\"1f53b.png\",\"sheet_x\":24,\"sheet_y\":6,\"short_name\":\"small_red_triangle_down\",\"short_names\":[\"small_red_triangle_down\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":220,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UP-POINTING SMALL RED TRIANGLE\",\"unified\":\"1F53C\",\"variations\":[],\"docomo\":null,\"au\":\"E543\",\"softbank\":null,\"google\":\"FEB01\",\"image\":\"1f53c.png\",\"sheet_x\":24,\"sheet_y\":7,\"short_name\":\"arrow_up_small\",\"short_names\":[\"arrow_up_small\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":159,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOWN-POINTING SMALL RED TRIANGLE\",\"unified\":\"1F53D\",\"variations\":[],\"docomo\":null,\"au\":\"E542\",\"softbank\":null,\"google\":\"FEB00\",\"image\":\"1f53d.png\",\"sheet_x\":24,\"sheet_y\":8,\"short_name\":\"arrow_down_small\",\"short_names\":[\"arrow_down_small\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":160,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OM SYMBOL\",\"unified\":\"1F549\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f549.png\",\"sheet_x\":24,\"sheet_y\":9,\"short_name\":\"om_symbol\",\"short_names\":[\"om_symbol\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOVE OF PEACE\",\"unified\":\"1F54A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f54a.png\",\"sheet_x\":24,\"sheet_y\":10,\"short_name\":\"dove_of_peace\",\"short_names\":[\"dove_of_peace\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":65,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KAABA\",\"unified\":\"1F54B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f54b.png\",\"sheet_x\":24,\"sheet_y\":11,\"short_name\":\"kaaba\",\"short_names\":[\"kaaba\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":114,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOSQUE\",\"unified\":\"1F54C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f54c.png\",\"sheet_x\":24,\"sheet_y\":12,\"short_name\":\"mosque\",\"short_names\":[\"mosque\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":112,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SYNAGOGUE\",\"unified\":\"1F54D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f54d.png\",\"sheet_x\":24,\"sheet_y\":13,\"short_name\":\"synagogue\",\"short_names\":[\"synagogue\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":113,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MENORAH WITH NINE BRANCHES\",\"unified\":\"1F54E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f54e.png\",\"sheet_x\":24,\"sheet_y\":14,\"short_name\":\"menorah_with_nine_branches\",\"short_names\":[\"menorah_with_nine_branches\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE ONE OCLOCK\",\"unified\":\"1F550\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E024\",\"google\":\"FE01E\",\"image\":\"1f550.png\",\"sheet_x\":24,\"sheet_y\":15,\"short_name\":\"clock1\",\"short_names\":[\"clock1\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":246,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE TWO OCLOCK\",\"unified\":\"1F551\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E025\",\"google\":\"FE01F\",\"image\":\"1f551.png\",\"sheet_x\":24,\"sheet_y\":16,\"short_name\":\"clock2\",\"short_names\":[\"clock2\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":247,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE THREE OCLOCK\",\"unified\":\"1F552\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E026\",\"google\":\"FE020\",\"image\":\"1f552.png\",\"sheet_x\":24,\"sheet_y\":17,\"short_name\":\"clock3\",\"short_names\":[\"clock3\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":248,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE FOUR OCLOCK\",\"unified\":\"1F553\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E027\",\"google\":\"FE021\",\"image\":\"1f553.png\",\"sheet_x\":24,\"sheet_y\":18,\"short_name\":\"clock4\",\"short_names\":[\"clock4\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":249,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE FIVE OCLOCK\",\"unified\":\"1F554\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E028\",\"google\":\"FE022\",\"image\":\"1f554.png\",\"sheet_x\":24,\"sheet_y\":19,\"short_name\":\"clock5\",\"short_names\":[\"clock5\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":250,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE SIX OCLOCK\",\"unified\":\"1F555\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E029\",\"google\":\"FE023\",\"image\":\"1f555.png\",\"sheet_x\":24,\"sheet_y\":20,\"short_name\":\"clock6\",\"short_names\":[\"clock6\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":251,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE SEVEN OCLOCK\",\"unified\":\"1F556\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E02A\",\"google\":\"FE024\",\"image\":\"1f556.png\",\"sheet_x\":24,\"sheet_y\":21,\"short_name\":\"clock7\",\"short_names\":[\"clock7\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":252,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE EIGHT OCLOCK\",\"unified\":\"1F557\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E02B\",\"google\":\"FE025\",\"image\":\"1f557.png\",\"sheet_x\":24,\"sheet_y\":22,\"short_name\":\"clock8\",\"short_names\":[\"clock8\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":253,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE NINE OCLOCK\",\"unified\":\"1F558\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E02C\",\"google\":\"FE026\",\"image\":\"1f558.png\",\"sheet_x\":24,\"sheet_y\":23,\"short_name\":\"clock9\",\"short_names\":[\"clock9\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":254,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE TEN OCLOCK\",\"unified\":\"1F559\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E02D\",\"google\":\"FE027\",\"image\":\"1f559.png\",\"sheet_x\":24,\"sheet_y\":24,\"short_name\":\"clock10\",\"short_names\":[\"clock10\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":255,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE ELEVEN OCLOCK\",\"unified\":\"1F55A\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E02E\",\"google\":\"FE028\",\"image\":\"1f55a.png\",\"sheet_x\":24,\"sheet_y\":25,\"short_name\":\"clock11\",\"short_names\":[\"clock11\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":256,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE TWELVE OCLOCK\",\"unified\":\"1F55B\",\"variations\":[],\"docomo\":\"E6BA\",\"au\":\"E594\",\"softbank\":\"E02F\",\"google\":\"FE029\",\"image\":\"1f55b.png\",\"sheet_x\":24,\"sheet_y\":26,\"short_name\":\"clock12\",\"short_names\":[\"clock12\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":257,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE ONE-THIRTY\",\"unified\":\"1F55C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f55c.png\",\"sheet_x\":24,\"sheet_y\":27,\"short_name\":\"clock130\",\"short_names\":[\"clock130\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":258,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE TWO-THIRTY\",\"unified\":\"1F55D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f55d.png\",\"sheet_x\":24,\"sheet_y\":28,\"short_name\":\"clock230\",\"short_names\":[\"clock230\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":259,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE THREE-THIRTY\",\"unified\":\"1F55E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f55e.png\",\"sheet_x\":24,\"sheet_y\":29,\"short_name\":\"clock330\",\"short_names\":[\"clock330\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":260,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE FOUR-THIRTY\",\"unified\":\"1F55F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f55f.png\",\"sheet_x\":24,\"sheet_y\":30,\"short_name\":\"clock430\",\"short_names\":[\"clock430\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":261,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE FIVE-THIRTY\",\"unified\":\"1F560\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f560.png\",\"sheet_x\":24,\"sheet_y\":31,\"short_name\":\"clock530\",\"short_names\":[\"clock530\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":262,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE SIX-THIRTY\",\"unified\":\"1F561\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f561.png\",\"sheet_x\":24,\"sheet_y\":32,\"short_name\":\"clock630\",\"short_names\":[\"clock630\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":263,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE SEVEN-THIRTY\",\"unified\":\"1F562\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f562.png\",\"sheet_x\":24,\"sheet_y\":33,\"short_name\":\"clock730\",\"short_names\":[\"clock730\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":264,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE EIGHT-THIRTY\",\"unified\":\"1F563\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f563.png\",\"sheet_x\":24,\"sheet_y\":34,\"short_name\":\"clock830\",\"short_names\":[\"clock830\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":265,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE NINE-THIRTY\",\"unified\":\"1F564\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f564.png\",\"sheet_x\":24,\"sheet_y\":35,\"short_name\":\"clock930\",\"short_names\":[\"clock930\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":266,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE TEN-THIRTY\",\"unified\":\"1F565\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f565.png\",\"sheet_x\":24,\"sheet_y\":36,\"short_name\":\"clock1030\",\"short_names\":[\"clock1030\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":267,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE ELEVEN-THIRTY\",\"unified\":\"1F566\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f566.png\",\"sheet_x\":24,\"sheet_y\":37,\"short_name\":\"clock1130\",\"short_names\":[\"clock1130\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":268,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CLOCK FACE TWELVE-THIRTY\",\"unified\":\"1F567\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f567.png\",\"sheet_x\":24,\"sheet_y\":38,\"short_name\":\"clock1230\",\"short_names\":[\"clock1230\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":269,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CANDLE\",\"unified\":\"1F56F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f56f.png\",\"sheet_x\":24,\"sheet_y\":39,\"short_name\":\"candle\",\"short_names\":[\"candle\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MANTELPIECE CLOCK\",\"unified\":\"1F570\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f570.png\",\"sheet_x\":24,\"sheet_y\":40,\"short_name\":\"mantelpiece_clock\",\"short_names\":[\"mantelpiece_clock\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HOLE\",\"unified\":\"1F573\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f573.png\",\"sheet_x\":25,\"sheet_y\":0,\"short_name\":\"hole\",\"short_names\":[\"hole\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":80,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MAN IN BUSINESS SUIT LEVITATING\",\"unified\":\"1F574\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f574.png\",\"sheet_x\":25,\"sheet_y\":1,\"short_name\":\"man_in_business_suit_levitating\",\"short_names\":[\"man_in_business_suit_levitating\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLEUTH OR SPY\",\"unified\":\"1F575\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f575.png\",\"sheet_x\":25,\"sheet_y\":2,\"short_name\":\"sleuth_or_spy\",\"short_names\":[\"sleuth_or_spy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":134,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DARK SUNGLASSES\",\"unified\":\"1F576\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f576.png\",\"sheet_x\":25,\"sheet_y\":3,\"short_name\":\"dark_sunglasses\",\"short_names\":[\"dark_sunglasses\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":202,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPIDER\",\"unified\":\"1F577\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f577.png\",\"sheet_x\":25,\"sheet_y\":4,\"short_name\":\"spider\",\"short_names\":[\"spider\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPIDER WEB\",\"unified\":\"1F578\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f578.png\",\"sheet_x\":25,\"sheet_y\":5,\"short_name\":\"spider_web\",\"short_names\":[\"spider_web\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":100,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"JOYSTICK\",\"unified\":\"1F579\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f579.png\",\"sheet_x\":25,\"sheet_y\":6,\"short_name\":\"joystick\",\"short_names\":[\"joystick\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LINKED PAPERCLIPS\",\"unified\":\"1F587\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f587.png\",\"sheet_x\":25,\"sheet_y\":7,\"short_name\":\"linked_paperclips\",\"short_names\":[\"linked_paperclips\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":157,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOWER LEFT BALLPOINT PEN\",\"unified\":\"1F58A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f58a.png\",\"sheet_x\":25,\"sheet_y\":8,\"short_name\":\"lower_left_ballpoint_pen\",\"short_names\":[\"lower_left_ballpoint_pen\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":170,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOWER LEFT FOUNTAIN PEN\",\"unified\":\"1F58B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f58b.png\",\"sheet_x\":25,\"sheet_y\":9,\"short_name\":\"lower_left_fountain_pen\",\"short_names\":[\"lower_left_fountain_pen\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":171,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOWER LEFT PAINTBRUSH\",\"unified\":\"1F58C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f58c.png\",\"sheet_x\":25,\"sheet_y\":10,\"short_name\":\"lower_left_paintbrush\",\"short_names\":[\"lower_left_paintbrush\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":176,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOWER LEFT CRAYON\",\"unified\":\"1F58D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f58d.png\",\"sheet_x\":25,\"sheet_y\":11,\"short_name\":\"lower_left_crayon\",\"short_names\":[\"lower_left_crayon\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":175,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RAISED HAND WITH FINGERS SPLAYED\",\"unified\":\"1F590\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f590.png\",\"sheet_x\":25,\"sheet_y\":12,\"short_name\":\"raised_hand_with_fingers_splayed\",\"short_names\":[\"raised_hand_with_fingers_splayed\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":107,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F590-1F3FB\":{\"unified\":\"1F590-1F3FB\",\"image\":\"1f590-1f3fb.png\",\"sheet_x\":25,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F590-1F3FC\":{\"unified\":\"1F590-1F3FC\",\"image\":\"1f590-1f3fc.png\",\"sheet_x\":25,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F590-1F3FD\":{\"unified\":\"1F590-1F3FD\",\"image\":\"1f590-1f3fd.png\",\"sheet_x\":25,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F590-1F3FE\":{\"unified\":\"1F590-1F3FE\",\"image\":\"1f590-1f3fe.png\",\"sheet_x\":25,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F590-1F3FF\":{\"unified\":\"1F590-1F3FF\",\"image\":\"1f590-1f3ff.png\",\"sheet_x\":25,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"REVERSED HAND WITH MIDDLE FINGER EXTENDED\",\"unified\":\"1F595\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f595.png\",\"sheet_x\":25,\"sheet_y\":18,\"short_name\":\"middle_finger\",\"short_names\":[\"middle_finger\",\"reversed_hand_with_middle_finger_extended\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":106,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F595-1F3FB\":{\"unified\":\"1F595-1F3FB\",\"image\":\"1f595-1f3fb.png\",\"sheet_x\":25,\"sheet_y\":19,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F595-1F3FC\":{\"unified\":\"1F595-1F3FC\",\"image\":\"1f595-1f3fc.png\",\"sheet_x\":25,\"sheet_y\":20,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F595-1F3FD\":{\"unified\":\"1F595-1F3FD\",\"image\":\"1f595-1f3fd.png\",\"sheet_x\":25,\"sheet_y\":21,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F595-1F3FE\":{\"unified\":\"1F595-1F3FE\",\"image\":\"1f595-1f3fe.png\",\"sheet_x\":25,\"sheet_y\":22,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F595-1F3FF\":{\"unified\":\"1F595-1F3FF\",\"image\":\"1f595-1f3ff.png\",\"sheet_x\":25,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"RAISED HAND WITH PART BETWEEN MIDDLE AND RING FINGERS\",\"unified\":\"1F596\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f596.png\",\"sheet_x\":25,\"sheet_y\":24,\"short_name\":\"spock-hand\",\"short_names\":[\"spock-hand\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":109,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F596-1F3FB\":{\"unified\":\"1F596-1F3FB\",\"image\":\"1f596-1f3fb.png\",\"sheet_x\":25,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F596-1F3FC\":{\"unified\":\"1F596-1F3FC\",\"image\":\"1f596-1f3fc.png\",\"sheet_x\":25,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F596-1F3FD\":{\"unified\":\"1F596-1F3FD\",\"image\":\"1f596-1f3fd.png\",\"sheet_x\":25,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F596-1F3FE\":{\"unified\":\"1F596-1F3FE\",\"image\":\"1f596-1f3fe.png\",\"sheet_x\":25,\"sheet_y\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F596-1F3FF\":{\"unified\":\"1F596-1F3FF\",\"image\":\"1f596-1f3ff.png\",\"sheet_x\":25,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"DESKTOP COMPUTER\",\"unified\":\"1F5A5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5a5.png\",\"sheet_x\":25,\"sheet_y\":30,\"short_name\":\"desktop_computer\",\"short_names\":[\"desktop_computer\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PRINTER\",\"unified\":\"1F5A8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5a8.png\",\"sheet_x\":25,\"sheet_y\":31,\"short_name\":\"printer\",\"short_names\":[\"printer\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"THREE BUTTON MOUSE\",\"unified\":\"1F5B1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5b1.png\",\"sheet_x\":25,\"sheet_y\":32,\"short_name\":\"three_button_mouse\",\"short_names\":[\"three_button_mouse\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRACKBALL\",\"unified\":\"1F5B2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5b2.png\",\"sheet_x\":25,\"sheet_y\":33,\"short_name\":\"trackball\",\"short_names\":[\"trackball\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FRAME WITH PICTURE\",\"unified\":\"1F5BC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5bc.png\",\"sheet_x\":25,\"sheet_y\":34,\"short_name\":\"frame_with_picture\",\"short_names\":[\"frame_with_picture\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":96,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CARD INDEX DIVIDERS\",\"unified\":\"1F5C2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5c2.png\",\"sheet_x\":25,\"sheet_y\":35,\"short_name\":\"card_index_dividers\",\"short_names\":[\"card_index_dividers\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":143,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CARD FILE BOX\",\"unified\":\"1F5C3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5c3.png\",\"sheet_x\":25,\"sheet_y\":36,\"short_name\":\"card_file_box\",\"short_names\":[\"card_file_box\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":136,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FILE CABINET\",\"unified\":\"1F5C4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5c4.png\",\"sheet_x\":25,\"sheet_y\":37,\"short_name\":\"file_cabinet\",\"short_names\":[\"file_cabinet\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":138,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WASTEBASKET\",\"unified\":\"1F5D1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5d1.png\",\"sheet_x\":25,\"sheet_y\":38,\"short_name\":\"wastebasket\",\"short_names\":[\"wastebasket\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPIRAL NOTE PAD\",\"unified\":\"1F5D2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5d2.png\",\"sheet_x\":25,\"sheet_y\":39,\"short_name\":\"spiral_note_pad\",\"short_names\":[\"spiral_note_pad\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":140,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPIRAL CALENDAR PAD\",\"unified\":\"1F5D3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5d3.png\",\"sheet_x\":25,\"sheet_y\":40,\"short_name\":\"spiral_calendar_pad\",\"short_names\":[\"spiral_calendar_pad\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":134,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COMPRESSION\",\"unified\":\"1F5DC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5dc.png\",\"sheet_x\":26,\"sheet_y\":0,\"short_name\":\"compression\",\"short_names\":[\"compression\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OLD KEY\",\"unified\":\"1F5DD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5dd.png\",\"sheet_x\":26,\"sheet_y\":1,\"short_name\":\"old_key\",\"short_names\":[\"old_key\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":90,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROLLED-UP NEWSPAPER\",\"unified\":\"1F5DE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5de.png\",\"sheet_x\":26,\"sheet_y\":2,\"short_name\":\"rolled_up_newspaper\",\"short_names\":[\"rolled_up_newspaper\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":144,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DAGGER KNIFE\",\"unified\":\"1F5E1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5e1.png\",\"sheet_x\":26,\"sheet_y\":3,\"short_name\":\"dagger_knife\",\"short_names\":[\"dagger_knife\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":66,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPEAKING HEAD IN SILHOUETTE\",\"unified\":\"1F5E3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5e3.png\",\"sheet_x\":26,\"sheet_y\":4,\"short_name\":\"speaking_head_in_silhouette\",\"short_names\":[\"speaking_head_in_silhouette\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":120,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEFT SPEECH BUBBLE\",\"unified\":\"1F5E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5e8.png\",\"sheet_x\":26,\"sheet_y\":5,\"short_name\":\"left_speech_bubble\",\"short_names\":[\"left_speech_bubble\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":false},{\"name\":\"RIGHT ANGER BUBBLE\",\"unified\":\"1F5EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5ef.png\",\"sheet_x\":26,\"sheet_y\":6,\"short_name\":\"right_anger_bubble\",\"short_names\":[\"right_anger_bubble\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":244,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BALLOT BOX WITH BALLOT\",\"unified\":\"1F5F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5f3.png\",\"sheet_x\":26,\"sheet_y\":7,\"short_name\":\"ballot_box_with_ballot\",\"short_names\":[\"ballot_box_with_ballot\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":137,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WORLD MAP\",\"unified\":\"1F5FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f5fa.png\",\"sheet_x\":26,\"sheet_y\":8,\"short_name\":\"world_map\",\"short_names\":[\"world_map\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":97,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOUNT FUJI\",\"unified\":\"1F5FB\",\"variations\":[],\"docomo\":\"E740\",\"au\":\"E5BD\",\"softbank\":\"E03B\",\"google\":\"FE4C3\",\"image\":\"1f5fb.png\",\"sheet_x\":26,\"sheet_y\":9,\"short_name\":\"mount_fuji\",\"short_names\":[\"mount_fuji\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":68,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TOKYO TOWER\",\"unified\":\"1F5FC\",\"variations\":[],\"docomo\":null,\"au\":\"E4C0\",\"softbank\":\"E509\",\"google\":\"FE4C4\",\"image\":\"1f5fc.png\",\"sheet_x\":26,\"sheet_y\":10,\"short_name\":\"tokyo_tower\",\"short_names\":[\"tokyo_tower\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":62,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STATUE OF LIBERTY\",\"unified\":\"1F5FD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E51D\",\"google\":\"FE4C6\",\"image\":\"1f5fd.png\",\"sheet_x\":26,\"sheet_y\":11,\"short_name\":\"statue_of_liberty\",\"short_names\":[\"statue_of_liberty\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":95,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SILHOUETTE OF JAPAN\",\"unified\":\"1F5FE\",\"variations\":[],\"docomo\":null,\"au\":\"E572\",\"softbank\":null,\"google\":\"FE4C7\",\"image\":\"1f5fe.png\",\"sheet_x\":26,\"sheet_y\":12,\"short_name\":\"japan\",\"short_names\":[\"japan\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":70,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOYAI\",\"unified\":\"1F5FF\",\"variations\":[],\"docomo\":null,\"au\":\"EB6C\",\"softbank\":null,\"google\":\"FE4C8\",\"image\":\"1f5ff.png\",\"sheet_x\":26,\"sheet_y\":13,\"short_name\":\"moyai\",\"short_names\":[\"moyai\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":99,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GRINNING FACE\",\"unified\":\"1F600\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f600.png\",\"sheet_x\":26,\"sheet_y\":14,\"short_name\":\"grinning\",\"short_names\":[\"grinning\"],\"text\":\":D\",\"texts\":null,\"category\":\"People\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GRINNING FACE WITH SMILING EYES\",\"unified\":\"1F601\",\"variations\":[],\"docomo\":\"E753\",\"au\":\"EB80\",\"softbank\":\"E404\",\"google\":\"FE333\",\"image\":\"1f601.png\",\"sheet_x\":26,\"sheet_y\":15,\"short_name\":\"grin\",\"short_names\":[\"grin\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH TEARS OF JOY\",\"unified\":\"1F602\",\"variations\":[],\"docomo\":\"E72A\",\"au\":\"EB64\",\"softbank\":\"E412\",\"google\":\"FE334\",\"image\":\"1f602.png\",\"sheet_x\":26,\"sheet_y\":16,\"short_name\":\"joy\",\"short_names\":[\"joy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH OPEN MOUTH\",\"unified\":\"1F603\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"E471\",\"softbank\":\"E057\",\"google\":\"FE330\",\"image\":\"1f603.png\",\"sheet_x\":26,\"sheet_y\":17,\"short_name\":\"smiley\",\"short_names\":[\"smiley\"],\"text\":\":)\",\"texts\":[\"=)\",\"=-)\"],\"category\":\"People\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH OPEN MOUTH AND SMILING EYES\",\"unified\":\"1F604\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"E471\",\"softbank\":\"E415\",\"google\":\"FE338\",\"image\":\"1f604.png\",\"sheet_x\":26,\"sheet_y\":18,\"short_name\":\"smile\",\"short_names\":[\"smile\"],\"text\":\":)\",\"texts\":[\"C:\",\"c:\",\":D\",\":-D\"],\"category\":\"People\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH OPEN MOUTH AND COLD SWEAT\",\"unified\":\"1F605\",\"variations\":[],\"docomo\":\"E722\",\"au\":\"E471-E5B1\",\"softbank\":\"E415-E331\",\"google\":\"FE331\",\"image\":\"1f605.png\",\"sheet_x\":26,\"sheet_y\":19,\"short_name\":\"sweat_smile\",\"short_names\":[\"sweat_smile\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH OPEN MOUTH AND TIGHTLY-CLOSED EYES\",\"unified\":\"1F606\",\"variations\":[],\"docomo\":\"E72A\",\"au\":\"EAC5\",\"softbank\":\"E40A\",\"google\":\"FE332\",\"image\":\"1f606.png\",\"sheet_x\":26,\"sheet_y\":20,\"short_name\":\"laughing\",\"short_names\":[\"laughing\",\"satisfied\"],\"text\":null,\"texts\":[\":>\",\":->\"],\"category\":\"People\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH HALO\",\"unified\":\"1F607\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f607.png\",\"sheet_x\":26,\"sheet_y\":21,\"short_name\":\"innocent\",\"short_names\":[\"innocent\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH HORNS\",\"unified\":\"1F608\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f608.png\",\"sheet_x\":26,\"sheet_y\":22,\"short_name\":\"smiling_imp\",\"short_names\":[\"smiling_imp\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":71,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WINKING FACE\",\"unified\":\"1F609\",\"variations\":[],\"docomo\":\"E729\",\"au\":\"E5C3\",\"softbank\":\"E405\",\"google\":\"FE347\",\"image\":\"1f609.png\",\"sheet_x\":26,\"sheet_y\":23,\"short_name\":\"wink\",\"short_names\":[\"wink\"],\"text\":\";)\",\"texts\":[\";)\",\";-)\"],\"category\":\"People\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH SMILING EYES\",\"unified\":\"1F60A\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"EACD\",\"softbank\":\"E056\",\"google\":\"FE335\",\"image\":\"1f60a.png\",\"sheet_x\":26,\"sheet_y\":24,\"short_name\":\"blush\",\"short_names\":[\"blush\"],\"text\":\":)\",\"texts\":[\":)\",\"(:\",\":-)\"],\"category\":\"People\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE SAVOURING DELICIOUS FOOD\",\"unified\":\"1F60B\",\"variations\":[],\"docomo\":\"E752\",\"au\":\"EACD\",\"softbank\":\"E056\",\"google\":\"FE32B\",\"image\":\"1f60b.png\",\"sheet_x\":26,\"sheet_y\":25,\"short_name\":\"yum\",\"short_names\":[\"yum\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RELIEVED FACE\",\"unified\":\"1F60C\",\"variations\":[],\"docomo\":\"E721\",\"au\":\"EAC5\",\"softbank\":\"E40A\",\"google\":\"FE33E\",\"image\":\"1f60c.png\",\"sheet_x\":26,\"sheet_y\":26,\"short_name\":\"relieved\",\"short_names\":[\"relieved\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH HEART-SHAPED EYES\",\"unified\":\"1F60D\",\"variations\":[],\"docomo\":\"E726\",\"au\":\"E5C4\",\"softbank\":\"E106\",\"google\":\"FE327\",\"image\":\"1f60d.png\",\"sheet_x\":26,\"sheet_y\":27,\"short_name\":\"heart_eyes\",\"short_names\":[\"heart_eyes\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING FACE WITH SUNGLASSES\",\"unified\":\"1F60E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f60e.png\",\"sheet_x\":26,\"sheet_y\":28,\"short_name\":\"sunglasses\",\"short_names\":[\"sunglasses\"],\"text\":null,\"texts\":[\"8)\"],\"category\":\"People\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMIRKING FACE\",\"unified\":\"1F60F\",\"variations\":[],\"docomo\":\"E72C\",\"au\":\"EABF\",\"softbank\":\"E402\",\"google\":\"FE343\",\"image\":\"1f60f.png\",\"sheet_x\":26,\"sheet_y\":29,\"short_name\":\"smirk\",\"short_names\":[\"smirk\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NEUTRAL FACE\",\"unified\":\"1F610\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f610.png\",\"sheet_x\":26,\"sheet_y\":30,\"short_name\":\"neutral_face\",\"short_names\":[\"neutral_face\"],\"text\":null,\"texts\":[\":|\",\":-|\"],\"category\":\"People\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"EXPRESSIONLESS FACE\",\"unified\":\"1F611\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f611.png\",\"sheet_x\":26,\"sheet_y\":31,\"short_name\":\"expressionless\",\"short_names\":[\"expressionless\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UNAMUSED FACE\",\"unified\":\"1F612\",\"variations\":[],\"docomo\":\"E725\",\"au\":\"EAC9\",\"softbank\":\"E40E\",\"google\":\"FE326\",\"image\":\"1f612.png\",\"sheet_x\":26,\"sheet_y\":32,\"short_name\":\"unamused\",\"short_names\":[\"unamused\"],\"text\":\":(\",\"texts\":null,\"category\":\"People\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH COLD SWEAT\",\"unified\":\"1F613\",\"variations\":[],\"docomo\":\"E723\",\"au\":\"E5C6\",\"softbank\":\"E108\",\"google\":\"FE344\",\"image\":\"1f613.png\",\"sheet_x\":26,\"sheet_y\":33,\"short_name\":\"sweat\",\"short_names\":[\"sweat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":60,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PENSIVE FACE\",\"unified\":\"1F614\",\"variations\":[],\"docomo\":\"E720\",\"au\":\"EAC0\",\"softbank\":\"E403\",\"google\":\"FE340\",\"image\":\"1f614.png\",\"sheet_x\":26,\"sheet_y\":34,\"short_name\":\"pensive\",\"short_names\":[\"pensive\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CONFUSED FACE\",\"unified\":\"1F615\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f615.png\",\"sheet_x\":26,\"sheet_y\":35,\"short_name\":\"confused\",\"short_names\":[\"confused\"],\"text\":null,\"texts\":[\":\\\\\",\":-\\\\\",\":\\/\",\":-\\/\"],\"category\":\"People\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CONFOUNDED FACE\",\"unified\":\"1F616\",\"variations\":[],\"docomo\":\"E6F3\",\"au\":\"EAC3\",\"softbank\":\"E407\",\"google\":\"FE33F\",\"image\":\"1f616.png\",\"sheet_x\":26,\"sheet_y\":36,\"short_name\":\"confounded\",\"short_names\":[\"confounded\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KISSING FACE\",\"unified\":\"1F617\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f617.png\",\"sheet_x\":26,\"sheet_y\":37,\"short_name\":\"kissing\",\"short_names\":[\"kissing\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE THROWING A KISS\",\"unified\":\"1F618\",\"variations\":[],\"docomo\":\"E726\",\"au\":\"EACF\",\"softbank\":\"E418\",\"google\":\"FE32C\",\"image\":\"1f618.png\",\"sheet_x\":26,\"sheet_y\":38,\"short_name\":\"kissing_heart\",\"short_names\":[\"kissing_heart\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KISSING FACE WITH SMILING EYES\",\"unified\":\"1F619\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f619.png\",\"sheet_x\":26,\"sheet_y\":39,\"short_name\":\"kissing_smiling_eyes\",\"short_names\":[\"kissing_smiling_eyes\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KISSING FACE WITH CLOSED EYES\",\"unified\":\"1F61A\",\"variations\":[],\"docomo\":\"E726\",\"au\":\"EACE\",\"softbank\":\"E417\",\"google\":\"FE32D\",\"image\":\"1f61a.png\",\"sheet_x\":26,\"sheet_y\":40,\"short_name\":\"kissing_closed_eyes\",\"short_names\":[\"kissing_closed_eyes\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH STUCK-OUT TONGUE\",\"unified\":\"1F61B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f61b.png\",\"sheet_x\":27,\"sheet_y\":0,\"short_name\":\"stuck_out_tongue\",\"short_names\":[\"stuck_out_tongue\"],\"text\":\":p\",\"texts\":[\":p\",\":-p\",\":P\",\":-P\",\":b\",\":-b\"],\"category\":\"People\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH STUCK-OUT TONGUE AND WINKING EYE\",\"unified\":\"1F61C\",\"variations\":[],\"docomo\":\"E728\",\"au\":\"E4E7\",\"softbank\":\"E105\",\"google\":\"FE329\",\"image\":\"1f61c.png\",\"sheet_x\":27,\"sheet_y\":1,\"short_name\":\"stuck_out_tongue_winking_eye\",\"short_names\":[\"stuck_out_tongue_winking_eye\"],\"text\":\";p\",\"texts\":[\";p\",\";-p\",\";b\",\";-b\",\";P\",\";-P\"],\"category\":\"People\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH STUCK-OUT TONGUE AND TIGHTLY-CLOSED EYES\",\"unified\":\"1F61D\",\"variations\":[],\"docomo\":\"E728\",\"au\":\"E4E7\",\"softbank\":\"E409\",\"google\":\"FE32A\",\"image\":\"1f61d.png\",\"sheet_x\":27,\"sheet_y\":2,\"short_name\":\"stuck_out_tongue_closed_eyes\",\"short_names\":[\"stuck_out_tongue_closed_eyes\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DISAPPOINTED FACE\",\"unified\":\"1F61E\",\"variations\":[],\"docomo\":\"E6F2\",\"au\":\"EAC0\",\"softbank\":\"E058\",\"google\":\"FE323\",\"image\":\"1f61e.png\",\"sheet_x\":27,\"sheet_y\":3,\"short_name\":\"disappointed\",\"short_names\":[\"disappointed\"],\"text\":\":(\",\"texts\":[\"):\",\":(\",\":-(\"],\"category\":\"People\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WORRIED FACE\",\"unified\":\"1F61F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f61f.png\",\"sheet_x\":27,\"sheet_y\":4,\"short_name\":\"worried\",\"short_names\":[\"worried\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ANGRY FACE\",\"unified\":\"1F620\",\"variations\":[],\"docomo\":\"E6F1\",\"au\":\"E472\",\"softbank\":\"E059\",\"google\":\"FE320\",\"image\":\"1f620.png\",\"sheet_x\":27,\"sheet_y\":5,\"short_name\":\"angry\",\"short_names\":[\"angry\"],\"text\":null,\"texts\":[\">:(\",\">:-(\"],\"category\":\"People\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POUTING FACE\",\"unified\":\"1F621\",\"variations\":[],\"docomo\":\"E724\",\"au\":\"EB5D\",\"softbank\":\"E416\",\"google\":\"FE33D\",\"image\":\"1f621.png\",\"sheet_x\":27,\"sheet_y\":6,\"short_name\":\"rage\",\"short_names\":[\"rage\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CRYING FACE\",\"unified\":\"1F622\",\"variations\":[],\"docomo\":\"E72E\",\"au\":\"EB69\",\"softbank\":\"E413\",\"google\":\"FE339\",\"image\":\"1f622.png\",\"sheet_x\":27,\"sheet_y\":7,\"short_name\":\"cry\",\"short_names\":[\"cry\"],\"text\":\":'(\",\"texts\":[\":'(\"],\"category\":\"People\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PERSEVERING FACE\",\"unified\":\"1F623\",\"variations\":[],\"docomo\":\"E72B\",\"au\":\"EAC2\",\"softbank\":\"E406\",\"google\":\"FE33C\",\"image\":\"1f623.png\",\"sheet_x\":27,\"sheet_y\":8,\"short_name\":\"persevere\",\"short_names\":[\"persevere\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH LOOK OF TRIUMPH\",\"unified\":\"1F624\",\"variations\":[],\"docomo\":\"E753\",\"au\":\"EAC1\",\"softbank\":\"E404\",\"google\":\"FE328\",\"image\":\"1f624.png\",\"sheet_x\":27,\"sheet_y\":9,\"short_name\":\"triumph\",\"short_names\":[\"triumph\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DISAPPOINTED BUT RELIEVED FACE\",\"unified\":\"1F625\",\"variations\":[],\"docomo\":\"E723\",\"au\":\"E5C6\",\"softbank\":\"E401\",\"google\":\"FE345\",\"image\":\"1f625.png\",\"sheet_x\":27,\"sheet_y\":10,\"short_name\":\"disappointed_relieved\",\"short_names\":[\"disappointed_relieved\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":58,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FROWNING FACE WITH OPEN MOUTH\",\"unified\":\"1F626\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f626.png\",\"sheet_x\":27,\"sheet_y\":11,\"short_name\":\"frowning\",\"short_names\":[\"frowning\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ANGUISHED FACE\",\"unified\":\"1F627\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f627.png\",\"sheet_x\":27,\"sheet_y\":12,\"short_name\":\"anguished\",\"short_names\":[\"anguished\"],\"text\":null,\"texts\":[\"D:\"],\"category\":\"People\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FEARFUL FACE\",\"unified\":\"1F628\",\"variations\":[],\"docomo\":\"E757\",\"au\":\"EAC6\",\"softbank\":\"E40B\",\"google\":\"FE33B\",\"image\":\"1f628.png\",\"sheet_x\":27,\"sheet_y\":13,\"short_name\":\"fearful\",\"short_names\":[\"fearful\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WEARY FACE\",\"unified\":\"1F629\",\"variations\":[],\"docomo\":\"E6F3\",\"au\":\"EB67\",\"softbank\":\"E403\",\"google\":\"FE321\",\"image\":\"1f629.png\",\"sheet_x\":27,\"sheet_y\":14,\"short_name\":\"weary\",\"short_names\":[\"weary\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLEEPY FACE\",\"unified\":\"1F62A\",\"variations\":[],\"docomo\":\"E701\",\"au\":\"EAC4\",\"softbank\":\"E408\",\"google\":\"FE342\",\"image\":\"1f62a.png\",\"sheet_x\":27,\"sheet_y\":15,\"short_name\":\"sleepy\",\"short_names\":[\"sleepy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":59,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TIRED FACE\",\"unified\":\"1F62B\",\"variations\":[],\"docomo\":\"E72B\",\"au\":\"E474\",\"softbank\":\"E406\",\"google\":\"FE346\",\"image\":\"1f62b.png\",\"sheet_x\":27,\"sheet_y\":16,\"short_name\":\"tired_face\",\"short_names\":[\"tired_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GRIMACING FACE\",\"unified\":\"1F62C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f62c.png\",\"sheet_x\":27,\"sheet_y\":17,\"short_name\":\"grimacing\",\"short_names\":[\"grimacing\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LOUDLY CRYING FACE\",\"unified\":\"1F62D\",\"variations\":[],\"docomo\":\"E72D\",\"au\":\"E473\",\"softbank\":\"E411\",\"google\":\"FE33A\",\"image\":\"1f62d.png\",\"sheet_x\":27,\"sheet_y\":18,\"short_name\":\"sob\",\"short_names\":[\"sob\"],\"text\":\":'(\",\"texts\":null,\"category\":\"People\",\"sort_order\":61,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH OPEN MOUTH\",\"unified\":\"1F62E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f62e.png\",\"sheet_x\":27,\"sheet_y\":19,\"short_name\":\"open_mouth\",\"short_names\":[\"open_mouth\"],\"text\":null,\"texts\":[\":o\",\":-o\"],\"category\":\"People\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HUSHED FACE\",\"unified\":\"1F62F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f62f.png\",\"sheet_x\":27,\"sheet_y\":20,\"short_name\":\"hushed\",\"short_names\":[\"hushed\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH OPEN MOUTH AND COLD SWEAT\",\"unified\":\"1F630\",\"variations\":[],\"docomo\":\"E723\",\"au\":\"EACB\",\"softbank\":\"E40F\",\"google\":\"FE325\",\"image\":\"1f630.png\",\"sheet_x\":27,\"sheet_y\":21,\"short_name\":\"cold_sweat\",\"short_names\":[\"cold_sweat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE SCREAMING IN FEAR\",\"unified\":\"1F631\",\"variations\":[],\"docomo\":\"E757\",\"au\":\"E5C5\",\"softbank\":\"E107\",\"google\":\"FE341\",\"image\":\"1f631.png\",\"sheet_x\":27,\"sheet_y\":22,\"short_name\":\"scream\",\"short_names\":[\"scream\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ASTONISHED FACE\",\"unified\":\"1F632\",\"variations\":[],\"docomo\":\"E6F4\",\"au\":\"EACA\",\"softbank\":\"E410\",\"google\":\"FE322\",\"image\":\"1f632.png\",\"sheet_x\":27,\"sheet_y\":23,\"short_name\":\"astonished\",\"short_names\":[\"astonished\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":63,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FLUSHED FACE\",\"unified\":\"1F633\",\"variations\":[],\"docomo\":\"E72A\",\"au\":\"EAC8\",\"softbank\":\"E40D\",\"google\":\"FE32F\",\"image\":\"1f633.png\",\"sheet_x\":27,\"sheet_y\":24,\"short_name\":\"flushed\",\"short_names\":[\"flushed\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLEEPING FACE\",\"unified\":\"1F634\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f634.png\",\"sheet_x\":27,\"sheet_y\":25,\"short_name\":\"sleeping\",\"short_names\":[\"sleeping\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":68,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DIZZY FACE\",\"unified\":\"1F635\",\"variations\":[],\"docomo\":\"E6F4\",\"au\":\"E5AE\",\"softbank\":\"E406\",\"google\":\"FE324\",\"image\":\"1f635.png\",\"sheet_x\":27,\"sheet_y\":26,\"short_name\":\"dizzy_face\",\"short_names\":[\"dizzy_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":62,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITHOUT MOUTH\",\"unified\":\"1F636\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f636.png\",\"sheet_x\":27,\"sheet_y\":27,\"short_name\":\"no_mouth\",\"short_names\":[\"no_mouth\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH MEDICAL MASK\",\"unified\":\"1F637\",\"variations\":[],\"docomo\":null,\"au\":\"EAC7\",\"softbank\":\"E40C\",\"google\":\"FE32E\",\"image\":\"1f637.png\",\"sheet_x\":27,\"sheet_y\":28,\"short_name\":\"mask\",\"short_names\":[\"mask\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":65,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"GRINNING CAT FACE WITH SMILING EYES\",\"unified\":\"1F638\",\"variations\":[],\"docomo\":\"E753\",\"au\":\"EB7F\",\"softbank\":\"E404\",\"google\":\"FE349\",\"image\":\"1f638.png\",\"sheet_x\":27,\"sheet_y\":29,\"short_name\":\"smile_cat\",\"short_names\":[\"smile_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":80,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAT FACE WITH TEARS OF JOY\",\"unified\":\"1F639\",\"variations\":[],\"docomo\":\"E72A\",\"au\":\"EB63\",\"softbank\":\"E412\",\"google\":\"FE34A\",\"image\":\"1f639.png\",\"sheet_x\":27,\"sheet_y\":30,\"short_name\":\"joy_cat\",\"short_names\":[\"joy_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":81,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING CAT FACE WITH OPEN MOUTH\",\"unified\":\"1F63A\",\"variations\":[],\"docomo\":\"E6F0\",\"au\":\"EB61\",\"softbank\":\"E057\",\"google\":\"FE348\",\"image\":\"1f63a.png\",\"sheet_x\":27,\"sheet_y\":31,\"short_name\":\"smiley_cat\",\"short_names\":[\"smiley_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":79,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMILING CAT FACE WITH HEART-SHAPED EYES\",\"unified\":\"1F63B\",\"variations\":[],\"docomo\":\"E726\",\"au\":\"EB65\",\"softbank\":\"E106\",\"google\":\"FE34C\",\"image\":\"1f63b.png\",\"sheet_x\":27,\"sheet_y\":32,\"short_name\":\"heart_eyes_cat\",\"short_names\":[\"heart_eyes_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":82,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CAT FACE WITH WRY SMILE\",\"unified\":\"1F63C\",\"variations\":[],\"docomo\":\"E753\",\"au\":\"EB6A\",\"softbank\":\"E404\",\"google\":\"FE34F\",\"image\":\"1f63c.png\",\"sheet_x\":27,\"sheet_y\":33,\"short_name\":\"smirk_cat\",\"short_names\":[\"smirk_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":83,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KISSING CAT FACE WITH CLOSED EYES\",\"unified\":\"1F63D\",\"variations\":[],\"docomo\":\"E726\",\"au\":\"EB60\",\"softbank\":\"E418\",\"google\":\"FE34B\",\"image\":\"1f63d.png\",\"sheet_x\":27,\"sheet_y\":34,\"short_name\":\"kissing_cat\",\"short_names\":[\"kissing_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":84,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POUTING CAT FACE\",\"unified\":\"1F63E\",\"variations\":[],\"docomo\":\"E724\",\"au\":\"EB5E\",\"softbank\":\"E416\",\"google\":\"FE34E\",\"image\":\"1f63e.png\",\"sheet_x\":27,\"sheet_y\":35,\"short_name\":\"pouting_cat\",\"short_names\":[\"pouting_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":87,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CRYING CAT FACE\",\"unified\":\"1F63F\",\"variations\":[],\"docomo\":\"E72E\",\"au\":\"EB68\",\"softbank\":\"E413\",\"google\":\"FE34D\",\"image\":\"1f63f.png\",\"sheet_x\":27,\"sheet_y\":36,\"short_name\":\"crying_cat_face\",\"short_names\":[\"crying_cat_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":86,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WEARY CAT FACE\",\"unified\":\"1F640\",\"variations\":[],\"docomo\":\"E6F3\",\"au\":\"EB66\",\"softbank\":\"E403\",\"google\":\"FE350\",\"image\":\"1f640.png\",\"sheet_x\":27,\"sheet_y\":37,\"short_name\":\"scream_cat\",\"short_names\":[\"scream_cat\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":85,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLIGHTLY FROWNING FACE\",\"unified\":\"1F641\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f641.png\",\"sheet_x\":27,\"sheet_y\":38,\"short_name\":\"slightly_frowning_face\",\"short_names\":[\"slightly_frowning_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLIGHTLY SMILING FACE\",\"unified\":\"1F642\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f642.png\",\"sheet_x\":27,\"sheet_y\":39,\"short_name\":\"slightly_smiling_face\",\"short_names\":[\"slightly_smiling_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UPSIDE-DOWN FACE\",\"unified\":\"1F643\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f643.png\",\"sheet_x\":27,\"sheet_y\":40,\"short_name\":\"upside_down_face\",\"short_names\":[\"upside_down_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH ROLLING EYES\",\"unified\":\"1F644\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f644.png\",\"sheet_x\":28,\"sheet_y\":0,\"short_name\":\"face_with_rolling_eyes\",\"short_names\":[\"face_with_rolling_eyes\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH NO GOOD GESTURE\",\"unified\":\"1F645\",\"variations\":[],\"docomo\":\"E72F\",\"au\":\"EAD7\",\"softbank\":\"E423\",\"google\":\"FE351\",\"image\":\"1f645.png\",\"sheet_x\":28,\"sheet_y\":1,\"short_name\":\"no_good\",\"short_names\":[\"no_good\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":148,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F645-1F3FB\":{\"unified\":\"1F645-1F3FB\",\"image\":\"1f645-1f3fb.png\",\"sheet_x\":28,\"sheet_y\":2,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F645-1F3FC\":{\"unified\":\"1F645-1F3FC\",\"image\":\"1f645-1f3fc.png\",\"sheet_x\":28,\"sheet_y\":3,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F645-1F3FD\":{\"unified\":\"1F645-1F3FD\",\"image\":\"1f645-1f3fd.png\",\"sheet_x\":28,\"sheet_y\":4,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F645-1F3FE\":{\"unified\":\"1F645-1F3FE\",\"image\":\"1f645-1f3fe.png\",\"sheet_x\":28,\"sheet_y\":5,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F645-1F3FF\":{\"unified\":\"1F645-1F3FF\",\"image\":\"1f645-1f3ff.png\",\"sheet_x\":28,\"sheet_y\":6,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"FACE WITH OK GESTURE\",\"unified\":\"1F646\",\"variations\":[],\"docomo\":\"E70B\",\"au\":\"EAD8\",\"softbank\":\"E424\",\"google\":\"FE352\",\"image\":\"1f646.png\",\"sheet_x\":28,\"sheet_y\":7,\"short_name\":\"ok_woman\",\"short_names\":[\"ok_woman\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":149,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F646-1F3FB\":{\"unified\":\"1F646-1F3FB\",\"image\":\"1f646-1f3fb.png\",\"sheet_x\":28,\"sheet_y\":8,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F646-1F3FC\":{\"unified\":\"1F646-1F3FC\",\"image\":\"1f646-1f3fc.png\",\"sheet_x\":28,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F646-1F3FD\":{\"unified\":\"1F646-1F3FD\",\"image\":\"1f646-1f3fd.png\",\"sheet_x\":28,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F646-1F3FE\":{\"unified\":\"1F646-1F3FE\",\"image\":\"1f646-1f3fe.png\",\"sheet_x\":28,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F646-1F3FF\":{\"unified\":\"1F646-1F3FF\",\"image\":\"1f646-1f3ff.png\",\"sheet_x\":28,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PERSON BOWING DEEPLY\",\"unified\":\"1F647\",\"variations\":[],\"docomo\":null,\"au\":\"EAD9\",\"softbank\":\"E426\",\"google\":\"FE353\",\"image\":\"1f647.png\",\"sheet_x\":28,\"sheet_y\":13,\"short_name\":\"bow\",\"short_names\":[\"bow\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":146,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F647-1F3FB\":{\"unified\":\"1F647-1F3FB\",\"image\":\"1f647-1f3fb.png\",\"sheet_x\":28,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F647-1F3FC\":{\"unified\":\"1F647-1F3FC\",\"image\":\"1f647-1f3fc.png\",\"sheet_x\":28,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F647-1F3FD\":{\"unified\":\"1F647-1F3FD\",\"image\":\"1f647-1f3fd.png\",\"sheet_x\":28,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F647-1F3FE\":{\"unified\":\"1F647-1F3FE\",\"image\":\"1f647-1f3fe.png\",\"sheet_x\":28,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F647-1F3FF\":{\"unified\":\"1F647-1F3FF\",\"image\":\"1f647-1f3ff.png\",\"sheet_x\":28,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"SEE-NO-EVIL MONKEY\",\"unified\":\"1F648\",\"variations\":[],\"docomo\":null,\"au\":\"EB50\",\"softbank\":null,\"google\":\"FE354\",\"image\":\"1f648.png\",\"sheet_x\":28,\"sheet_y\":19,\"short_name\":\"see_no_evil\",\"short_names\":[\"see_no_evil\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HEAR-NO-EVIL MONKEY\",\"unified\":\"1F649\",\"variations\":[],\"docomo\":null,\"au\":\"EB52\",\"softbank\":null,\"google\":\"FE356\",\"image\":\"1f649.png\",\"sheet_x\":28,\"sheet_y\":20,\"short_name\":\"hear_no_evil\",\"short_names\":[\"hear_no_evil\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SPEAK-NO-EVIL MONKEY\",\"unified\":\"1F64A\",\"variations\":[],\"docomo\":null,\"au\":\"EB51\",\"softbank\":null,\"google\":\"FE355\",\"image\":\"1f64a.png\",\"sheet_x\":28,\"sheet_y\":21,\"short_name\":\"speak_no_evil\",\"short_names\":[\"speak_no_evil\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HAPPY PERSON RAISING ONE HAND\",\"unified\":\"1F64B\",\"variations\":[],\"docomo\":null,\"au\":\"EB85\",\"softbank\":\"E012\",\"google\":\"FE357\",\"image\":\"1f64b.png\",\"sheet_x\":28,\"sheet_y\":22,\"short_name\":\"raising_hand\",\"short_names\":[\"raising_hand\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":150,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F64B-1F3FB\":{\"unified\":\"1F64B-1F3FB\",\"image\":\"1f64b-1f3fb.png\",\"sheet_x\":28,\"sheet_y\":23,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64B-1F3FC\":{\"unified\":\"1F64B-1F3FC\",\"image\":\"1f64b-1f3fc.png\",\"sheet_x\":28,\"sheet_y\":24,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64B-1F3FD\":{\"unified\":\"1F64B-1F3FD\",\"image\":\"1f64b-1f3fd.png\",\"sheet_x\":28,\"sheet_y\":25,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64B-1F3FE\":{\"unified\":\"1F64B-1F3FE\",\"image\":\"1f64b-1f3fe.png\",\"sheet_x\":28,\"sheet_y\":26,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64B-1F3FF\":{\"unified\":\"1F64B-1F3FF\",\"image\":\"1f64b-1f3ff.png\",\"sheet_x\":28,\"sheet_y\":27,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PERSON RAISING BOTH HANDS IN CELEBRATION\",\"unified\":\"1F64C\",\"variations\":[],\"docomo\":null,\"au\":\"EB86\",\"softbank\":\"E427\",\"google\":\"FE358\",\"image\":\"1f64c.png\",\"sheet_x\":28,\"sheet_y\":28,\"short_name\":\"raised_hands\",\"short_names\":[\"raised_hands\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":88,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F64C-1F3FB\":{\"unified\":\"1F64C-1F3FB\",\"image\":\"1f64c-1f3fb.png\",\"sheet_x\":28,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64C-1F3FC\":{\"unified\":\"1F64C-1F3FC\",\"image\":\"1f64c-1f3fc.png\",\"sheet_x\":28,\"sheet_y\":30,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64C-1F3FD\":{\"unified\":\"1F64C-1F3FD\",\"image\":\"1f64c-1f3fd.png\",\"sheet_x\":28,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64C-1F3FE\":{\"unified\":\"1F64C-1F3FE\",\"image\":\"1f64c-1f3fe.png\",\"sheet_x\":28,\"sheet_y\":32,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64C-1F3FF\":{\"unified\":\"1F64C-1F3FF\",\"image\":\"1f64c-1f3ff.png\",\"sheet_x\":28,\"sheet_y\":33,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PERSON FROWNING\",\"unified\":\"1F64D\",\"variations\":[],\"docomo\":\"E6F3\",\"au\":\"EB87\",\"softbank\":\"E403\",\"google\":\"FE359\",\"image\":\"1f64d.png\",\"sheet_x\":28,\"sheet_y\":34,\"short_name\":\"person_frowning\",\"short_names\":[\"person_frowning\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":152,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F64D-1F3FB\":{\"unified\":\"1F64D-1F3FB\",\"image\":\"1f64d-1f3fb.png\",\"sheet_x\":28,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64D-1F3FC\":{\"unified\":\"1F64D-1F3FC\",\"image\":\"1f64d-1f3fc.png\",\"sheet_x\":28,\"sheet_y\":36,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64D-1F3FD\":{\"unified\":\"1F64D-1F3FD\",\"image\":\"1f64d-1f3fd.png\",\"sheet_x\":28,\"sheet_y\":37,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64D-1F3FE\":{\"unified\":\"1F64D-1F3FE\",\"image\":\"1f64d-1f3fe.png\",\"sheet_x\":28,\"sheet_y\":38,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64D-1F3FF\":{\"unified\":\"1F64D-1F3FF\",\"image\":\"1f64d-1f3ff.png\",\"sheet_x\":28,\"sheet_y\":39,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PERSON WITH POUTING FACE\",\"unified\":\"1F64E\",\"variations\":[],\"docomo\":\"E6F1\",\"au\":\"EB88\",\"softbank\":\"E416\",\"google\":\"FE35A\",\"image\":\"1f64e.png\",\"sheet_x\":28,\"sheet_y\":40,\"short_name\":\"person_with_pouting_face\",\"short_names\":[\"person_with_pouting_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":151,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F64E-1F3FB\":{\"unified\":\"1F64E-1F3FB\",\"image\":\"1f64e-1f3fb.png\",\"sheet_x\":29,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64E-1F3FC\":{\"unified\":\"1F64E-1F3FC\",\"image\":\"1f64e-1f3fc.png\",\"sheet_x\":29,\"sheet_y\":1,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64E-1F3FD\":{\"unified\":\"1F64E-1F3FD\",\"image\":\"1f64e-1f3fd.png\",\"sheet_x\":29,\"sheet_y\":2,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64E-1F3FE\":{\"unified\":\"1F64E-1F3FE\",\"image\":\"1f64e-1f3fe.png\",\"sheet_x\":29,\"sheet_y\":3,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64E-1F3FF\":{\"unified\":\"1F64E-1F3FF\",\"image\":\"1f64e-1f3ff.png\",\"sheet_x\":29,\"sheet_y\":4,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PERSON WITH FOLDED HANDS\",\"unified\":\"1F64F\",\"variations\":[],\"docomo\":null,\"au\":\"EAD2\",\"softbank\":\"E41D\",\"google\":\"FE35B\",\"image\":\"1f64f.png\",\"sheet_x\":29,\"sheet_y\":5,\"short_name\":\"pray\",\"short_names\":[\"pray\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":100,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F64F-1F3FB\":{\"unified\":\"1F64F-1F3FB\",\"image\":\"1f64f-1f3fb.png\",\"sheet_x\":29,\"sheet_y\":6,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64F-1F3FC\":{\"unified\":\"1F64F-1F3FC\",\"image\":\"1f64f-1f3fc.png\",\"sheet_x\":29,\"sheet_y\":7,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64F-1F3FD\":{\"unified\":\"1F64F-1F3FD\",\"image\":\"1f64f-1f3fd.png\",\"sheet_x\":29,\"sheet_y\":8,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64F-1F3FE\":{\"unified\":\"1F64F-1F3FE\",\"image\":\"1f64f-1f3fe.png\",\"sheet_x\":29,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F64F-1F3FF\":{\"unified\":\"1F64F-1F3FF\",\"image\":\"1f64f-1f3ff.png\",\"sheet_x\":29,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"ROCKET\",\"unified\":\"1F680\",\"variations\":[],\"docomo\":null,\"au\":\"E5C8\",\"softbank\":\"E10D\",\"google\":\"FE7ED\",\"image\":\"1f680.png\",\"sheet_x\":29,\"sheet_y\":11,\"short_name\":\"rocket\",\"short_names\":[\"rocket\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HELICOPTER\",\"unified\":\"1F681\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f681.png\",\"sheet_x\":29,\"sheet_y\":12,\"short_name\":\"helicopter\",\"short_names\":[\"helicopter\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STEAM LOCOMOTIVE\",\"unified\":\"1F682\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f682.png\",\"sheet_x\":29,\"sheet_y\":13,\"short_name\":\"steam_locomotive\",\"short_names\":[\"steam_locomotive\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RAILWAY CAR\",\"unified\":\"1F683\",\"variations\":[],\"docomo\":\"E65B\",\"au\":\"E4B5\",\"softbank\":\"E01E\",\"google\":\"FE7DF\",\"image\":\"1f683.png\",\"sheet_x\":29,\"sheet_y\":14,\"short_name\":\"railway_car\",\"short_names\":[\"railway_car\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HIGH-SPEED TRAIN\",\"unified\":\"1F684\",\"variations\":[],\"docomo\":\"E65D\",\"au\":\"E4B0\",\"softbank\":\"E435\",\"google\":\"FE7E2\",\"image\":\"1f684.png\",\"sheet_x\":29,\"sheet_y\":15,\"short_name\":\"bullettrain_side\",\"short_names\":[\"bullettrain_side\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HIGH-SPEED TRAIN WITH BULLET NOSE\",\"unified\":\"1F685\",\"variations\":[],\"docomo\":\"E65D\",\"au\":\"E4B0\",\"softbank\":\"E01F\",\"google\":\"FE7E3\",\"image\":\"1f685.png\",\"sheet_x\":29,\"sheet_y\":16,\"short_name\":\"bullettrain_front\",\"short_names\":[\"bullettrain_front\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRAIN\",\"unified\":\"1F686\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f686.png\",\"sheet_x\":29,\"sheet_y\":17,\"short_name\":\"train2\",\"short_names\":[\"train2\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"METRO\",\"unified\":\"1F687\",\"variations\":[],\"docomo\":\"E65C\",\"au\":\"E5BC\",\"softbank\":\"E434\",\"google\":\"FE7E0\",\"image\":\"1f687.png\",\"sheet_x\":29,\"sheet_y\":18,\"short_name\":\"metro\",\"short_names\":[\"metro\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LIGHT RAIL\",\"unified\":\"1F688\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f688.png\",\"sheet_x\":29,\"sheet_y\":19,\"short_name\":\"light_rail\",\"short_names\":[\"light_rail\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"STATION\",\"unified\":\"1F689\",\"variations\":[],\"docomo\":null,\"au\":\"EB6D\",\"softbank\":\"E039\",\"google\":\"FE7EC\",\"image\":\"1f689.png\",\"sheet_x\":29,\"sheet_y\":20,\"short_name\":\"station\",\"short_names\":[\"station\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRAM\",\"unified\":\"1F68A\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f68a.png\",\"sheet_x\":29,\"sheet_y\":21,\"short_name\":\"tram\",\"short_names\":[\"tram\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRAM CAR\",\"unified\":\"1F68B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f68b.png\",\"sheet_x\":29,\"sheet_y\":22,\"short_name\":\"train\",\"short_names\":[\"train\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BUS\",\"unified\":\"1F68C\",\"variations\":[],\"docomo\":\"E660\",\"au\":\"E4AF\",\"softbank\":\"E159\",\"google\":\"FE7E6\",\"image\":\"1f68c.png\",\"sheet_x\":29,\"sheet_y\":23,\"short_name\":\"bus\",\"short_names\":[\"bus\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ONCOMING BUS\",\"unified\":\"1F68D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f68d.png\",\"sheet_x\":29,\"sheet_y\":24,\"short_name\":\"oncoming_bus\",\"short_names\":[\"oncoming_bus\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TROLLEYBUS\",\"unified\":\"1F68E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f68e.png\",\"sheet_x\":29,\"sheet_y\":25,\"short_name\":\"trolleybus\",\"short_names\":[\"trolleybus\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BUS STOP\",\"unified\":\"1F68F\",\"variations\":[],\"docomo\":null,\"au\":\"E4A7\",\"softbank\":\"E150\",\"google\":\"FE7E7\",\"image\":\"1f68f.png\",\"sheet_x\":29,\"sheet_y\":26,\"short_name\":\"busstop\",\"short_names\":[\"busstop\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MINIBUS\",\"unified\":\"1F690\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f690.png\",\"sheet_x\":29,\"sheet_y\":27,\"short_name\":\"minibus\",\"short_names\":[\"minibus\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AMBULANCE\",\"unified\":\"1F691\",\"variations\":[],\"docomo\":null,\"au\":\"EAE0\",\"softbank\":\"E431\",\"google\":\"FE7F3\",\"image\":\"1f691.png\",\"sheet_x\":29,\"sheet_y\":28,\"short_name\":\"ambulance\",\"short_names\":[\"ambulance\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FIRE ENGINE\",\"unified\":\"1F692\",\"variations\":[],\"docomo\":null,\"au\":\"EADF\",\"softbank\":\"E430\",\"google\":\"FE7F2\",\"image\":\"1f692.png\",\"sheet_x\":29,\"sheet_y\":29,\"short_name\":\"fire_engine\",\"short_names\":[\"fire_engine\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POLICE CAR\",\"unified\":\"1F693\",\"variations\":[],\"docomo\":null,\"au\":\"EAE1\",\"softbank\":\"E432\",\"google\":\"FE7F4\",\"image\":\"1f693.png\",\"sheet_x\":29,\"sheet_y\":30,\"short_name\":\"police_car\",\"short_names\":[\"police_car\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ONCOMING POLICE CAR\",\"unified\":\"1F694\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f694.png\",\"sheet_x\":29,\"sheet_y\":31,\"short_name\":\"oncoming_police_car\",\"short_names\":[\"oncoming_police_car\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TAXI\",\"unified\":\"1F695\",\"variations\":[],\"docomo\":\"E65E\",\"au\":\"E4B1\",\"softbank\":\"E15A\",\"google\":\"FE7EF\",\"image\":\"1f695.png\",\"sheet_x\":29,\"sheet_y\":32,\"short_name\":\"taxi\",\"short_names\":[\"taxi\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ONCOMING TAXI\",\"unified\":\"1F696\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f696.png\",\"sheet_x\":29,\"sheet_y\":33,\"short_name\":\"oncoming_taxi\",\"short_names\":[\"oncoming_taxi\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AUTOMOBILE\",\"unified\":\"1F697\",\"variations\":[],\"docomo\":\"E65E\",\"au\":\"E4B1\",\"softbank\":\"E01B\",\"google\":\"FE7E4\",\"image\":\"1f697.png\",\"sheet_x\":29,\"sheet_y\":34,\"short_name\":\"car\",\"short_names\":[\"car\",\"red_car\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ONCOMING AUTOMOBILE\",\"unified\":\"1F698\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f698.png\",\"sheet_x\":29,\"sheet_y\":35,\"short_name\":\"oncoming_automobile\",\"short_names\":[\"oncoming_automobile\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RECREATIONAL VEHICLE\",\"unified\":\"1F699\",\"variations\":[],\"docomo\":\"E65F\",\"au\":\"E4B1\",\"softbank\":\"E42E\",\"google\":\"FE7E5\",\"image\":\"1f699.png\",\"sheet_x\":29,\"sheet_y\":36,\"short_name\":\"blue_car\",\"short_names\":[\"blue_car\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DELIVERY TRUCK\",\"unified\":\"1F69A\",\"variations\":[],\"docomo\":null,\"au\":\"E4B2\",\"softbank\":\"E42F\",\"google\":\"FE7F1\",\"image\":\"1f69a.png\",\"sheet_x\":29,\"sheet_y\":37,\"short_name\":\"truck\",\"short_names\":[\"truck\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ARTICULATED LORRY\",\"unified\":\"1F69B\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f69b.png\",\"sheet_x\":29,\"sheet_y\":38,\"short_name\":\"articulated_lorry\",\"short_names\":[\"articulated_lorry\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRACTOR\",\"unified\":\"1F69C\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f69c.png\",\"sheet_x\":29,\"sheet_y\":39,\"short_name\":\"tractor\",\"short_names\":[\"tractor\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MONORAIL\",\"unified\":\"1F69D\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f69d.png\",\"sheet_x\":29,\"sheet_y\":40,\"short_name\":\"monorail\",\"short_names\":[\"monorail\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOUNTAIN RAILWAY\",\"unified\":\"1F69E\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f69e.png\",\"sheet_x\":30,\"sheet_y\":0,\"short_name\":\"mountain_railway\",\"short_names\":[\"mountain_railway\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SUSPENSION RAILWAY\",\"unified\":\"1F69F\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f69f.png\",\"sheet_x\":30,\"sheet_y\":1,\"short_name\":\"suspension_railway\",\"short_names\":[\"suspension_railway\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOUNTAIN CABLEWAY\",\"unified\":\"1F6A0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6a0.png\",\"sheet_x\":30,\"sheet_y\":2,\"short_name\":\"mountain_cableway\",\"short_names\":[\"mountain_cableway\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AERIAL TRAMWAY\",\"unified\":\"1F6A1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6a1.png\",\"sheet_x\":30,\"sheet_y\":3,\"short_name\":\"aerial_tramway\",\"short_names\":[\"aerial_tramway\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHIP\",\"unified\":\"1F6A2\",\"variations\":[],\"docomo\":\"E661\",\"au\":\"EA82\",\"softbank\":\"E202\",\"google\":\"FE7E8\",\"image\":\"1f6a2.png\",\"sheet_x\":30,\"sheet_y\":4,\"short_name\":\"ship\",\"short_names\":[\"ship\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROWBOAT\",\"unified\":\"1F6A3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6a3.png\",\"sheet_x\":30,\"sheet_y\":5,\"short_name\":\"rowboat\",\"short_names\":[\"rowboat\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F6A3-1F3FB\":{\"unified\":\"1F6A3-1F3FB\",\"image\":\"1f6a3-1f3fb.png\",\"sheet_x\":30,\"sheet_y\":6,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6A3-1F3FC\":{\"unified\":\"1F6A3-1F3FC\",\"image\":\"1f6a3-1f3fc.png\",\"sheet_x\":30,\"sheet_y\":7,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6A3-1F3FD\":{\"unified\":\"1F6A3-1F3FD\",\"image\":\"1f6a3-1f3fd.png\",\"sheet_x\":30,\"sheet_y\":8,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6A3-1F3FE\":{\"unified\":\"1F6A3-1F3FE\",\"image\":\"1f6a3-1f3fe.png\",\"sheet_x\":30,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6A3-1F3FF\":{\"unified\":\"1F6A3-1F3FF\",\"image\":\"1f6a3-1f3ff.png\",\"sheet_x\":30,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"SPEEDBOAT\",\"unified\":\"1F6A4\",\"variations\":[],\"docomo\":\"E6A3\",\"au\":\"E4B4\",\"softbank\":\"E135\",\"google\":\"FE7EE\",\"image\":\"1f6a4.png\",\"sheet_x\":30,\"sheet_y\":11,\"short_name\":\"speedboat\",\"short_names\":[\"speedboat\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HORIZONTAL TRAFFIC LIGHT\",\"unified\":\"1F6A5\",\"variations\":[],\"docomo\":\"E66D\",\"au\":\"E46A\",\"softbank\":\"E14E\",\"google\":\"FE7F7\",\"image\":\"1f6a5.png\",\"sheet_x\":30,\"sheet_y\":12,\"short_name\":\"traffic_light\",\"short_names\":[\"traffic_light\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"VERTICAL TRAFFIC LIGHT\",\"unified\":\"1F6A6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6a6.png\",\"sheet_x\":30,\"sheet_y\":13,\"short_name\":\"vertical_traffic_light\",\"short_names\":[\"vertical_traffic_light\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CONSTRUCTION SIGN\",\"unified\":\"1F6A7\",\"variations\":[],\"docomo\":null,\"au\":\"E5D7\",\"softbank\":\"E137\",\"google\":\"FE7F8\",\"image\":\"1f6a7.png\",\"sheet_x\":30,\"sheet_y\":14,\"short_name\":\"construction\",\"short_names\":[\"construction\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POLICE CARS REVOLVING LIGHT\",\"unified\":\"1F6A8\",\"variations\":[],\"docomo\":null,\"au\":\"EB73\",\"softbank\":\"E432\",\"google\":\"FE7F9\",\"image\":\"1f6a8.png\",\"sheet_x\":30,\"sheet_y\":15,\"short_name\":\"rotating_light\",\"short_names\":[\"rotating_light\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TRIANGULAR FLAG ON POST\",\"unified\":\"1F6A9\",\"variations\":[],\"docomo\":\"E6DE\",\"au\":\"EB2C\",\"softbank\":null,\"google\":\"FEB22\",\"image\":\"1f6a9.png\",\"sheet_x\":30,\"sheet_y\":16,\"short_name\":\"triangular_flag_on_post\",\"short_names\":[\"triangular_flag_on_post\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":163,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DOOR\",\"unified\":\"1F6AA\",\"variations\":[],\"docomo\":\"E714\",\"au\":null,\"softbank\":null,\"google\":\"FE4F3\",\"image\":\"1f6aa.png\",\"sheet_x\":30,\"sheet_y\":17,\"short_name\":\"door\",\"short_names\":[\"door\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":94,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NO ENTRY SIGN\",\"unified\":\"1F6AB\",\"variations\":[],\"docomo\":\"E738\",\"au\":\"E541\",\"softbank\":null,\"google\":\"FEB48\",\"image\":\"1f6ab.png\",\"sheet_x\":30,\"sheet_y\":18,\"short_name\":\"no_entry_sign\",\"short_names\":[\"no_entry_sign\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":71,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMOKING SYMBOL\",\"unified\":\"1F6AC\",\"variations\":[],\"docomo\":\"E67F\",\"au\":\"E47D\",\"softbank\":\"E30E\",\"google\":\"FEB1E\",\"image\":\"1f6ac.png\",\"sheet_x\":30,\"sheet_y\":19,\"short_name\":\"smoking\",\"short_names\":[\"smoking\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":69,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NO SMOKING SYMBOL\",\"unified\":\"1F6AD\",\"variations\":[],\"docomo\":\"E680\",\"au\":\"E47E\",\"softbank\":\"E208\",\"google\":\"FEB1F\",\"image\":\"1f6ad.png\",\"sheet_x\":30,\"sheet_y\":20,\"short_name\":\"no_smoking\",\"short_names\":[\"no_smoking\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":116,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PUT LITTER IN ITS PLACE SYMBOL\",\"unified\":\"1F6AE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6ae.png\",\"sheet_x\":30,\"sheet_y\":21,\"short_name\":\"put_litter_in_its_place\",\"short_names\":[\"put_litter_in_its_place\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":124,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"DO NOT LITTER SYMBOL\",\"unified\":\"1F6AF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6af.png\",\"sheet_x\":30,\"sheet_y\":22,\"short_name\":\"do_not_litter\",\"short_names\":[\"do_not_litter\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":77,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"POTABLE WATER SYMBOL\",\"unified\":\"1F6B0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6b0.png\",\"sheet_x\":30,\"sheet_y\":23,\"short_name\":\"potable_water\",\"short_names\":[\"potable_water\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":119,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NON-POTABLE WATER SYMBOL\",\"unified\":\"1F6B1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6b1.png\",\"sheet_x\":30,\"sheet_y\":24,\"short_name\":\"non-potable_water\",\"short_names\":[\"non-potable_water\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":79,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BICYCLE\",\"unified\":\"1F6B2\",\"variations\":[],\"docomo\":\"E71D\",\"au\":\"E4AE\",\"softbank\":\"E136\",\"google\":\"FE7EB\",\"image\":\"1f6b2.png\",\"sheet_x\":30,\"sheet_y\":25,\"short_name\":\"bike\",\"short_names\":[\"bike\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NO BICYCLES\",\"unified\":\"1F6B3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6b3.png\",\"sheet_x\":30,\"sheet_y\":26,\"short_name\":\"no_bicycles\",\"short_names\":[\"no_bicycles\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":78,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BICYCLIST\",\"unified\":\"1F6B4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6b4.png\",\"sheet_x\":30,\"sheet_y\":27,\"short_name\":\"bicyclist\",\"short_names\":[\"bicyclist\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F6B4-1F3FB\":{\"unified\":\"1F6B4-1F3FB\",\"image\":\"1f6b4-1f3fb.png\",\"sheet_x\":30,\"sheet_y\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B4-1F3FC\":{\"unified\":\"1F6B4-1F3FC\",\"image\":\"1f6b4-1f3fc.png\",\"sheet_x\":30,\"sheet_y\":29,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B4-1F3FD\":{\"unified\":\"1F6B4-1F3FD\",\"image\":\"1f6b4-1f3fd.png\",\"sheet_x\":30,\"sheet_y\":30,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B4-1F3FE\":{\"unified\":\"1F6B4-1F3FE\",\"image\":\"1f6b4-1f3fe.png\",\"sheet_x\":30,\"sheet_y\":31,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B4-1F3FF\":{\"unified\":\"1F6B4-1F3FF\",\"image\":\"1f6b4-1f3ff.png\",\"sheet_x\":30,\"sheet_y\":32,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"MOUNTAIN BICYCLIST\",\"unified\":\"1F6B5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6b5.png\",\"sheet_x\":30,\"sheet_y\":33,\"short_name\":\"mountain_bicyclist\",\"short_names\":[\"mountain_bicyclist\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F6B5-1F3FB\":{\"unified\":\"1F6B5-1F3FB\",\"image\":\"1f6b5-1f3fb.png\",\"sheet_x\":30,\"sheet_y\":34,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B5-1F3FC\":{\"unified\":\"1F6B5-1F3FC\",\"image\":\"1f6b5-1f3fc.png\",\"sheet_x\":30,\"sheet_y\":35,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B5-1F3FD\":{\"unified\":\"1F6B5-1F3FD\",\"image\":\"1f6b5-1f3fd.png\",\"sheet_x\":30,\"sheet_y\":36,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B5-1F3FE\":{\"unified\":\"1F6B5-1F3FE\",\"image\":\"1f6b5-1f3fe.png\",\"sheet_x\":30,\"sheet_y\":37,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B5-1F3FF\":{\"unified\":\"1F6B5-1F3FF\",\"image\":\"1f6b5-1f3ff.png\",\"sheet_x\":30,\"sheet_y\":38,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"PEDESTRIAN\",\"unified\":\"1F6B6\",\"variations\":[],\"docomo\":\"E733\",\"au\":\"EB72\",\"softbank\":\"E201\",\"google\":\"FE7F0\",\"image\":\"1f6b6.png\",\"sheet_x\":30,\"sheet_y\":39,\"short_name\":\"walking\",\"short_names\":[\"walking\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":139,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F6B6-1F3FB\":{\"unified\":\"1F6B6-1F3FB\",\"image\":\"1f6b6-1f3fb.png\",\"sheet_x\":30,\"sheet_y\":40,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B6-1F3FC\":{\"unified\":\"1F6B6-1F3FC\",\"image\":\"1f6b6-1f3fc.png\",\"sheet_x\":31,\"sheet_y\":0,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B6-1F3FD\":{\"unified\":\"1F6B6-1F3FD\",\"image\":\"1f6b6-1f3fd.png\",\"sheet_x\":31,\"sheet_y\":1,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B6-1F3FE\":{\"unified\":\"1F6B6-1F3FE\",\"image\":\"1f6b6-1f3fe.png\",\"sheet_x\":31,\"sheet_y\":2,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6B6-1F3FF\":{\"unified\":\"1F6B6-1F3FF\",\"image\":\"1f6b6-1f3ff.png\",\"sheet_x\":31,\"sheet_y\":3,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"NO PEDESTRIANS\",\"unified\":\"1F6B7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6b7.png\",\"sheet_x\":31,\"sheet_y\":4,\"short_name\":\"no_pedestrians\",\"short_names\":[\"no_pedestrians\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":76,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHILDREN CROSSING\",\"unified\":\"1F6B8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6b8.png\",\"sheet_x\":31,\"sheet_y\":5,\"short_name\":\"children_crossing\",\"short_names\":[\"children_crossing\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":95,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MENS SYMBOL\",\"unified\":\"1F6B9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E138\",\"google\":\"FEB33\",\"image\":\"1f6b9.png\",\"sheet_x\":31,\"sheet_y\":6,\"short_name\":\"mens\",\"short_names\":[\"mens\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":120,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WOMENS SYMBOL\",\"unified\":\"1F6BA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":\"E139\",\"google\":\"FEB34\",\"image\":\"1f6ba.png\",\"sheet_x\":31,\"sheet_y\":7,\"short_name\":\"womens\",\"short_names\":[\"womens\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":121,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RESTROOM\",\"unified\":\"1F6BB\",\"variations\":[],\"docomo\":\"E66E\",\"au\":\"E4A5\",\"softbank\":\"E151\",\"google\":\"FE506\",\"image\":\"1f6bb.png\",\"sheet_x\":31,\"sheet_y\":8,\"short_name\":\"restroom\",\"short_names\":[\"restroom\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":123,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BABY SYMBOL\",\"unified\":\"1F6BC\",\"variations\":[],\"docomo\":null,\"au\":\"EB18\",\"softbank\":\"E13A\",\"google\":\"FEB35\",\"image\":\"1f6bc.png\",\"sheet_x\":31,\"sheet_y\":9,\"short_name\":\"baby_symbol\",\"short_names\":[\"baby_symbol\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":122,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TOILET\",\"unified\":\"1F6BD\",\"variations\":[],\"docomo\":\"E66E\",\"au\":\"E4A5\",\"softbank\":\"E140\",\"google\":\"FE507\",\"image\":\"1f6bd.png\",\"sheet_x\":31,\"sheet_y\":10,\"short_name\":\"toilet\",\"short_names\":[\"toilet\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":86,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"WATER CLOSET\",\"unified\":\"1F6BE\",\"variations\":[],\"docomo\":\"E66E\",\"au\":\"E4A5\",\"softbank\":\"E309\",\"google\":\"FE508\",\"image\":\"1f6be.png\",\"sheet_x\":31,\"sheet_y\":11,\"short_name\":\"wc\",\"short_names\":[\"wc\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":117,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHOWER\",\"unified\":\"1F6BF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6bf.png\",\"sheet_x\":31,\"sheet_y\":12,\"short_name\":\"shower\",\"short_names\":[\"shower\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":87,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BATH\",\"unified\":\"1F6C0\",\"variations\":[],\"docomo\":\"E6F7\",\"au\":\"E5D8\",\"softbank\":\"E13F\",\"google\":\"FE505\",\"image\":\"1f6c0.png\",\"sheet_x\":31,\"sheet_y\":13,\"short_name\":\"bath\",\"short_names\":[\"bath\"],\"text\":null,\"texts\":null,\"category\":\"Activity\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F6C0-1F3FB\":{\"unified\":\"1F6C0-1F3FB\",\"image\":\"1f6c0-1f3fb.png\",\"sheet_x\":31,\"sheet_y\":14,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6C0-1F3FC\":{\"unified\":\"1F6C0-1F3FC\",\"image\":\"1f6c0-1f3fc.png\",\"sheet_x\":31,\"sheet_y\":15,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6C0-1F3FD\":{\"unified\":\"1F6C0-1F3FD\",\"image\":\"1f6c0-1f3fd.png\",\"sheet_x\":31,\"sheet_y\":16,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6C0-1F3FE\":{\"unified\":\"1F6C0-1F3FE\",\"image\":\"1f6c0-1f3fe.png\",\"sheet_x\":31,\"sheet_y\":17,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F6C0-1F3FF\":{\"unified\":\"1F6C0-1F3FF\",\"image\":\"1f6c0-1f3ff.png\",\"sheet_x\":31,\"sheet_y\":18,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"BATHTUB\",\"unified\":\"1F6C1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6c1.png\",\"sheet_x\":31,\"sheet_y\":19,\"short_name\":\"bathtub\",\"short_names\":[\"bathtub\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":88,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PASSPORT CONTROL\",\"unified\":\"1F6C2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6c2.png\",\"sheet_x\":31,\"sheet_y\":20,\"short_name\":\"passport_control\",\"short_names\":[\"passport_control\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":111,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CUSTOMS\",\"unified\":\"1F6C3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6c3.png\",\"sheet_x\":31,\"sheet_y\":21,\"short_name\":\"customs\",\"short_names\":[\"customs\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":112,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BAGGAGE CLAIM\",\"unified\":\"1F6C4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6c4.png\",\"sheet_x\":31,\"sheet_y\":22,\"short_name\":\"baggage_claim\",\"short_names\":[\"baggage_claim\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":113,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LEFT LUGGAGE\",\"unified\":\"1F6C5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6c5.png\",\"sheet_x\":31,\"sheet_y\":23,\"short_name\":\"left_luggage\",\"short_names\":[\"left_luggage\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":114,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"COUCH AND LAMP\",\"unified\":\"1F6CB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6cb.png\",\"sheet_x\":31,\"sheet_y\":24,\"short_name\":\"couch_and_lamp\",\"short_names\":[\"couch_and_lamp\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":91,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SLEEPING ACCOMMODATION\",\"unified\":\"1F6CC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6cc.png\",\"sheet_x\":31,\"sheet_y\":25,\"short_name\":\"sleeping_accommodation\",\"short_names\":[\"sleeping_accommodation\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":92,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHOPPING BAGS\",\"unified\":\"1F6CD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6cd.png\",\"sheet_x\":31,\"sheet_y\":26,\"short_name\":\"shopping_bags\",\"short_names\":[\"shopping_bags\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":100,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BELLHOP BELL\",\"unified\":\"1F6CE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6ce.png\",\"sheet_x\":31,\"sheet_y\":27,\"short_name\":\"bellhop_bell\",\"short_names\":[\"bellhop_bell\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":95,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"BED\",\"unified\":\"1F6CF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6cf.png\",\"sheet_x\":31,\"sheet_y\":28,\"short_name\":\"bed\",\"short_names\":[\"bed\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":93,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PLACE OF WORSHIP\",\"unified\":\"1F6D0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6d0.png\",\"sheet_x\":31,\"sheet_y\":29,\"short_name\":\"place_of_worship\",\"short_names\":[\"place_of_worship\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HAMMER AND WRENCH\",\"unified\":\"1F6E0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6e0.png\",\"sheet_x\":31,\"sheet_y\":30,\"short_name\":\"hammer_and_wrench\",\"short_names\":[\"hammer_and_wrench\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":58,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SHIELD\",\"unified\":\"1F6E1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6e1.png\",\"sheet_x\":31,\"sheet_y\":31,\"short_name\":\"shield\",\"short_names\":[\"shield\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":68,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"OIL DRUM\",\"unified\":\"1F6E2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6e2.png\",\"sheet_x\":31,\"sheet_y\":32,\"short_name\":\"oil_drum\",\"short_names\":[\"oil_drum\"],\"text\":null,\"texts\":null,\"category\":\"Objects\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOTORWAY\",\"unified\":\"1F6E3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6e3.png\",\"sheet_x\":31,\"sheet_y\":33,\"short_name\":\"motorway\",\"short_names\":[\"motorway\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":74,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"RAILWAY TRACK\",\"unified\":\"1F6E4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6e4.png\",\"sheet_x\":31,\"sheet_y\":34,\"short_name\":\"railway_track\",\"short_names\":[\"railway_track\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":75,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MOTOR BOAT\",\"unified\":\"1F6E5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6e5.png\",\"sheet_x\":31,\"sheet_y\":35,\"short_name\":\"motor_boat\",\"short_names\":[\"motor_boat\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SMALL AIRPLANE\",\"unified\":\"1F6E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6e9.png\",\"sheet_x\":31,\"sheet_y\":36,\"short_name\":\"small_airplane\",\"short_names\":[\"small_airplane\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AIRPLANE DEPARTURE\",\"unified\":\"1F6EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6eb.png\",\"sheet_x\":31,\"sheet_y\":37,\"short_name\":\"airplane_departure\",\"short_names\":[\"airplane_departure\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"AIRPLANE ARRIVING\",\"unified\":\"1F6EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6ec.png\",\"sheet_x\":31,\"sheet_y\":38,\"short_name\":\"airplane_arriving\",\"short_names\":[\"airplane_arriving\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SATELLITE\",\"unified\":\"1F6F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6f0.png\",\"sheet_x\":31,\"sheet_y\":39,\"short_name\":\"satellite\",\"short_names\":[\"satellite\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"PASSENGER SHIP\",\"unified\":\"1F6F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f6f3.png\",\"sheet_x\":31,\"sheet_y\":40,\"short_name\":\"passenger_ship\",\"short_names\":[\"passenger_ship\"],\"text\":null,\"texts\":null,\"category\":\"Places\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ZIPPER-MOUTH FACE\",\"unified\":\"1F910\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f910.png\",\"sheet_x\":32,\"sheet_y\":0,\"short_name\":\"zipper_mouth_face\",\"short_names\":[\"zipper_mouth_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":64,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"MONEY-MOUTH FACE\",\"unified\":\"1F911\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f911.png\",\"sheet_x\":32,\"sheet_y\":1,\"short_name\":\"money_mouth_face\",\"short_names\":[\"money_mouth_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH THERMOMETER\",\"unified\":\"1F912\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f912.png\",\"sheet_x\":32,\"sheet_y\":2,\"short_name\":\"face_with_thermometer\",\"short_names\":[\"face_with_thermometer\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":66,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"NERD FACE\",\"unified\":\"1F913\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f913.png\",\"sheet_x\":32,\"sheet_y\":3,\"short_name\":\"nerd_face\",\"short_names\":[\"nerd_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"THINKING FACE\",\"unified\":\"1F914\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f914.png\",\"sheet_x\":32,\"sheet_y\":4,\"short_name\":\"thinking_face\",\"short_names\":[\"thinking_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"FACE WITH HEAD-BANDAGE\",\"unified\":\"1F915\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f915.png\",\"sheet_x\":32,\"sheet_y\":5,\"short_name\":\"face_with_head_bandage\",\"short_names\":[\"face_with_head_bandage\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":67,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"ROBOT FACE\",\"unified\":\"1F916\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f916.png\",\"sheet_x\":32,\"sheet_y\":6,\"short_name\":\"robot_face\",\"short_names\":[\"robot_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":78,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HUGGING FACE\",\"unified\":\"1F917\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f917.png\",\"sheet_x\":32,\"sheet_y\":7,\"short_name\":\"hugging_face\",\"short_names\":[\"hugging_face\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SIGN OF THE HORNS\",\"unified\":\"1F918\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f918.png\",\"sheet_x\":32,\"sheet_y\":8,\"short_name\":\"the_horns\",\"short_names\":[\"the_horns\",\"sign_of_the_horns\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":108,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true,\"skin_variations\":{\"1F918-1F3FB\":{\"unified\":\"1F918-1F3FB\",\"image\":\"1f918-1f3fb.png\",\"sheet_x\":32,\"sheet_y\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F918-1F3FC\":{\"unified\":\"1F918-1F3FC\",\"image\":\"1f918-1f3fc.png\",\"sheet_x\":32,\"sheet_y\":10,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F918-1F3FD\":{\"unified\":\"1F918-1F3FD\",\"image\":\"1f918-1f3fd.png\",\"sheet_x\":32,\"sheet_y\":11,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F918-1F3FE\":{\"unified\":\"1F918-1F3FE\",\"image\":\"1f918-1f3fe.png\",\"sheet_x\":32,\"sheet_y\":12,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},\"1F918-1F3FF\":{\"unified\":\"1F918-1F3FF\",\"image\":\"1f918-1f3ff.png\",\"sheet_x\":32,\"sheet_y\":13,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true}}},{\"name\":\"CRAB\",\"unified\":\"1F980\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f980.png\",\"sheet_x\":32,\"sheet_y\":14,\"short_name\":\"crab\",\"short_names\":[\"crab\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"LION FACE\",\"unified\":\"1F981\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f981.png\",\"sheet_x\":32,\"sheet_y\":15,\"short_name\":\"lion_face\",\"short_names\":[\"lion_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"SCORPION\",\"unified\":\"1F982\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f982.png\",\"sheet_x\":32,\"sheet_y\":16,\"short_name\":\"scorpion\",\"short_names\":[\"scorpion\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"TURKEY\",\"unified\":\"1F983\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f983.png\",\"sheet_x\":32,\"sheet_y\":17,\"short_name\":\"turkey\",\"short_names\":[\"turkey\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":64,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"UNICORN FACE\",\"unified\":\"1F984\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f984.png\",\"sheet_x\":32,\"sheet_y\":18,\"short_name\":\"unicorn_face\",\"short_names\":[\"unicorn_face\"],\"text\":null,\"texts\":null,\"category\":\"Nature\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"CHEESE WEDGE\",\"unified\":\"1F9C0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f9c0.png\",\"sheet_x\":32,\"sheet_y\":19,\"short_name\":\"cheese_wedge\",\"short_names\":[\"cheese_wedge\"],\"text\":null,\"texts\":null,\"category\":\"Foods\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"HASH KEY\",\"unified\":\"0023-20E3\",\"variations\":[\"0023-FE0F-20E3\"],\"docomo\":\"E6E0\",\"au\":\"EB84\",\"softbank\":\"E210\",\"google\":\"FE82C\",\"image\":\"0023-20e3.png\",\"sheet_x\":32,\"sheet_y\":20,\"short_name\":\"hash\",\"short_names\":[\"hash\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":178,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"002A-20E3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"002a-20e3.png\",\"sheet_x\":32,\"sheet_y\":21,\"short_name\":\"keycap_star\",\"short_names\":[\"keycap_star\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 0\",\"unified\":\"0030-20E3\",\"variations\":[\"0030-FE0F-20E3\"],\"docomo\":\"E6EB\",\"au\":\"E5AC\",\"softbank\":\"E225\",\"google\":\"FE837\",\"image\":\"0030-20e3.png\",\"sheet_x\":32,\"sheet_y\":22,\"short_name\":\"zero\",\"short_names\":[\"zero\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":134,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 1\",\"unified\":\"0031-20E3\",\"variations\":[\"0031-FE0F-20E3\"],\"docomo\":\"E6E2\",\"au\":\"E522\",\"softbank\":\"E21C\",\"google\":\"FE82E\",\"image\":\"0031-20e3.png\",\"sheet_x\":32,\"sheet_y\":23,\"short_name\":\"one\",\"short_names\":[\"one\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":135,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 2\",\"unified\":\"0032-20E3\",\"variations\":[\"0032-FE0F-20E3\"],\"docomo\":\"E6E3\",\"au\":\"E523\",\"softbank\":\"E21D\",\"google\":\"FE82F\",\"image\":\"0032-20e3.png\",\"sheet_x\":32,\"sheet_y\":24,\"short_name\":\"two\",\"short_names\":[\"two\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":136,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 3\",\"unified\":\"0033-20E3\",\"variations\":[\"0033-FE0F-20E3\"],\"docomo\":\"E6E4\",\"au\":\"E524\",\"softbank\":\"E21E\",\"google\":\"FE830\",\"image\":\"0033-20e3.png\",\"sheet_x\":32,\"sheet_y\":25,\"short_name\":\"three\",\"short_names\":[\"three\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":137,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 4\",\"unified\":\"0034-20E3\",\"variations\":[\"0034-FE0F-20E3\"],\"docomo\":\"E6E5\",\"au\":\"E525\",\"softbank\":\"E21F\",\"google\":\"FE831\",\"image\":\"0034-20e3.png\",\"sheet_x\":32,\"sheet_y\":26,\"short_name\":\"four\",\"short_names\":[\"four\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":138,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 5\",\"unified\":\"0035-20E3\",\"variations\":[\"0035-FE0F-20E3\"],\"docomo\":\"E6E6\",\"au\":\"E526\",\"softbank\":\"E220\",\"google\":\"FE832\",\"image\":\"0035-20e3.png\",\"sheet_x\":32,\"sheet_y\":27,\"short_name\":\"five\",\"short_names\":[\"five\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":139,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 6\",\"unified\":\"0036-20E3\",\"variations\":[\"0036-FE0F-20E3\"],\"docomo\":\"E6E7\",\"au\":\"E527\",\"softbank\":\"E221\",\"google\":\"FE833\",\"image\":\"0036-20e3.png\",\"sheet_x\":32,\"sheet_y\":28,\"short_name\":\"six\",\"short_names\":[\"six\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":140,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 7\",\"unified\":\"0037-20E3\",\"variations\":[\"0037-FE0F-20E3\"],\"docomo\":\"E6E8\",\"au\":\"E528\",\"softbank\":\"E222\",\"google\":\"FE834\",\"image\":\"0037-20e3.png\",\"sheet_x\":32,\"sheet_y\":29,\"short_name\":\"seven\",\"short_names\":[\"seven\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":141,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 8\",\"unified\":\"0038-20E3\",\"variations\":[\"0038-FE0F-20E3\"],\"docomo\":\"E6E9\",\"au\":\"E529\",\"softbank\":\"E223\",\"google\":\"FE835\",\"image\":\"0038-20e3.png\",\"sheet_x\":32,\"sheet_y\":30,\"short_name\":\"eight\",\"short_names\":[\"eight\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":142,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"KEYCAP 9\",\"unified\":\"0039-20E3\",\"variations\":[\"0039-FE0F-20E3\"],\"docomo\":\"E6EA\",\"au\":\"E52A\",\"softbank\":\"E224\",\"google\":\"FE836\",\"image\":\"0039-20e3.png\",\"sheet_x\":32,\"sheet_y\":31,\"short_name\":\"nine\",\"short_names\":[\"nine\"],\"text\":null,\"texts\":null,\"category\":\"Symbols\",\"sort_order\":143,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AC\",\"unified\":\"1F1E6-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1e8.png\",\"sheet_x\":32,\"sheet_y\":32,\"short_name\":\"flag-ac\",\"short_names\":[\"flag-ac\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AD\",\"unified\":\"1F1E6-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1e9.png\",\"sheet_x\":32,\"sheet_y\":33,\"short_name\":\"flag-ad\",\"short_names\":[\"flag-ad\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":6,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AE\",\"unified\":\"1F1E6-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1ea.png\",\"sheet_x\":32,\"sheet_y\":34,\"short_name\":\"flag-ae\",\"short_names\":[\"flag-ae\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":233,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AF\",\"unified\":\"1F1E6-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1eb.png\",\"sheet_x\":32,\"sheet_y\":35,\"short_name\":\"flag-af\",\"short_names\":[\"flag-af\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":1,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AG\",\"unified\":\"1F1E6-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1ec.png\",\"sheet_x\":32,\"sheet_y\":36,\"short_name\":\"flag-ag\",\"short_names\":[\"flag-ag\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":10,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AI\",\"unified\":\"1F1E6-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1ee.png\",\"sheet_x\":32,\"sheet_y\":37,\"short_name\":\"flag-ai\",\"short_names\":[\"flag-ai\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":8,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AL\",\"unified\":\"1F1E6-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1f1.png\",\"sheet_x\":32,\"sheet_y\":38,\"short_name\":\"flag-al\",\"short_names\":[\"flag-al\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":3,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AM\",\"unified\":\"1F1E6-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1f2.png\",\"sheet_x\":32,\"sheet_y\":39,\"short_name\":\"flag-am\",\"short_names\":[\"flag-am\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":12,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AO\",\"unified\":\"1F1E6-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1f4.png\",\"sheet_x\":32,\"sheet_y\":40,\"short_name\":\"flag-ao\",\"short_names\":[\"flag-ao\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":7,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AQ\",\"unified\":\"1F1E6-1F1F6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1f6.png\",\"sheet_x\":33,\"sheet_y\":0,\"short_name\":\"flag-aq\",\"short_names\":[\"flag-aq\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":9,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AR\",\"unified\":\"1F1E6-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1f7.png\",\"sheet_x\":33,\"sheet_y\":1,\"short_name\":\"flag-ar\",\"short_names\":[\"flag-ar\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":11,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AS\",\"unified\":\"1F1E6-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1f8.png\",\"sheet_x\":33,\"sheet_y\":2,\"short_name\":\"flag-as\",\"short_names\":[\"flag-as\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":5,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AT\",\"unified\":\"1F1E6-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1f9.png\",\"sheet_x\":33,\"sheet_y\":3,\"short_name\":\"flag-at\",\"short_names\":[\"flag-at\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":15,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AU\",\"unified\":\"1F1E6-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1fa.png\",\"sheet_x\":33,\"sheet_y\":4,\"short_name\":\"flag-au\",\"short_names\":[\"flag-au\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":14,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AW\",\"unified\":\"1F1E6-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1fc.png\",\"sheet_x\":33,\"sheet_y\":5,\"short_name\":\"flag-aw\",\"short_names\":[\"flag-aw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":13,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AX\",\"unified\":\"1F1E6-1F1FD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1fd.png\",\"sheet_x\":33,\"sheet_y\":6,\"short_name\":\"flag-ax\",\"short_names\":[\"flag-ax\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":2,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS AZ\",\"unified\":\"1F1E6-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e6-1f1ff.png\",\"sheet_x\":33,\"sheet_y\":7,\"short_name\":\"flag-az\",\"short_names\":[\"flag-az\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":16,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BA\",\"unified\":\"1F1E7-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1e6.png\",\"sheet_x\":33,\"sheet_y\":8,\"short_name\":\"flag-ba\",\"short_names\":[\"flag-ba\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":29,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BB\",\"unified\":\"1F1E7-1F1E7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1e7.png\",\"sheet_x\":33,\"sheet_y\":9,\"short_name\":\"flag-bb\",\"short_names\":[\"flag-bb\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":20,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BD\",\"unified\":\"1F1E7-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1e9.png\",\"sheet_x\":33,\"sheet_y\":10,\"short_name\":\"flag-bd\",\"short_names\":[\"flag-bd\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":19,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BE\",\"unified\":\"1F1E7-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1ea.png\",\"sheet_x\":33,\"sheet_y\":11,\"short_name\":\"flag-be\",\"short_names\":[\"flag-be\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":22,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BF\",\"unified\":\"1F1E7-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1eb.png\",\"sheet_x\":33,\"sheet_y\":12,\"short_name\":\"flag-bf\",\"short_names\":[\"flag-bf\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":36,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BG\",\"unified\":\"1F1E7-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1ec.png\",\"sheet_x\":33,\"sheet_y\":13,\"short_name\":\"flag-bg\",\"short_names\":[\"flag-bg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":35,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BH\",\"unified\":\"1F1E7-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1ed.png\",\"sheet_x\":33,\"sheet_y\":14,\"short_name\":\"flag-bh\",\"short_names\":[\"flag-bh\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":18,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BI\",\"unified\":\"1F1E7-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1ee.png\",\"sheet_x\":33,\"sheet_y\":15,\"short_name\":\"flag-bi\",\"short_names\":[\"flag-bi\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":37,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BJ\",\"unified\":\"1F1E7-1F1EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1ef.png\",\"sheet_x\":33,\"sheet_y\":16,\"short_name\":\"flag-bj\",\"short_names\":[\"flag-bj\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":24,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BL\",\"unified\":\"1F1E7-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f1.png\",\"sheet_x\":33,\"sheet_y\":17,\"short_name\":\"flag-bl\",\"short_names\":[\"flag-bl\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":185,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BM\",\"unified\":\"1F1E7-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f2.png\",\"sheet_x\":33,\"sheet_y\":18,\"short_name\":\"flag-bm\",\"short_names\":[\"flag-bm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":25,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BN\",\"unified\":\"1F1E7-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f3.png\",\"sheet_x\":33,\"sheet_y\":19,\"short_name\":\"flag-bn\",\"short_names\":[\"flag-bn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":34,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BO\",\"unified\":\"1F1E7-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f4.png\",\"sheet_x\":33,\"sheet_y\":20,\"short_name\":\"flag-bo\",\"short_names\":[\"flag-bo\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":27,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BQ\",\"unified\":\"1F1E7-1F1F6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f6.png\",\"sheet_x\":33,\"sheet_y\":21,\"short_name\":\"flag-bq\",\"short_names\":[\"flag-bq\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":28,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BR\",\"unified\":\"1F1E7-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f7.png\",\"sheet_x\":33,\"sheet_y\":22,\"short_name\":\"flag-br\",\"short_names\":[\"flag-br\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":31,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BS\",\"unified\":\"1F1E7-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f8.png\",\"sheet_x\":33,\"sheet_y\":23,\"short_name\":\"flag-bs\",\"short_names\":[\"flag-bs\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":17,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BT\",\"unified\":\"1F1E7-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1f9.png\",\"sheet_x\":33,\"sheet_y\":24,\"short_name\":\"flag-bt\",\"short_names\":[\"flag-bt\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":26,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BV\",\"unified\":\"1F1E7-1F1FB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1fb.png\",\"sheet_x\":33,\"sheet_y\":25,\"short_name\":\"flag-bv\",\"short_names\":[\"flag-bv\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BW\",\"unified\":\"1F1E7-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1fc.png\",\"sheet_x\":33,\"sheet_y\":26,\"short_name\":\"flag-bw\",\"short_names\":[\"flag-bw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":30,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BY\",\"unified\":\"1F1E7-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1fe.png\",\"sheet_x\":33,\"sheet_y\":27,\"short_name\":\"flag-by\",\"short_names\":[\"flag-by\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":21,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS BZ\",\"unified\":\"1F1E7-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e7-1f1ff.png\",\"sheet_x\":33,\"sheet_y\":28,\"short_name\":\"flag-bz\",\"short_names\":[\"flag-bz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":23,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CA\",\"unified\":\"1F1E8-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1e6.png\",\"sheet_x\":33,\"sheet_y\":29,\"short_name\":\"flag-ca\",\"short_names\":[\"flag-ca\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":41,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CC\",\"unified\":\"1F1E8-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1e8.png\",\"sheet_x\":33,\"sheet_y\":30,\"short_name\":\"flag-cc\",\"short_names\":[\"flag-cc\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":49,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CD\",\"unified\":\"1F1E8-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1e9.png\",\"sheet_x\":33,\"sheet_y\":31,\"short_name\":\"flag-cd\",\"short_names\":[\"flag-cd\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":53,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CF\",\"unified\":\"1F1E8-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1eb.png\",\"sheet_x\":33,\"sheet_y\":32,\"short_name\":\"flag-cf\",\"short_names\":[\"flag-cf\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":44,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CG\",\"unified\":\"1F1E8-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1ec.png\",\"sheet_x\":33,\"sheet_y\":33,\"short_name\":\"flag-cg\",\"short_names\":[\"flag-cg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":52,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CH\",\"unified\":\"1F1E8-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1ed.png\",\"sheet_x\":33,\"sheet_y\":34,\"short_name\":\"flag-ch\",\"short_names\":[\"flag-ch\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":215,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CI\",\"unified\":\"1F1E8-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1ee.png\",\"sheet_x\":33,\"sheet_y\":35,\"short_name\":\"flag-ci\",\"short_names\":[\"flag-ci\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":110,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CK\",\"unified\":\"1F1E8-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1f0.png\",\"sheet_x\":33,\"sheet_y\":36,\"short_name\":\"flag-ck\",\"short_names\":[\"flag-ck\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":54,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CL\",\"unified\":\"1F1E8-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1f1.png\",\"sheet_x\":33,\"sheet_y\":37,\"short_name\":\"flag-cl\",\"short_names\":[\"flag-cl\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":46,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CM\",\"unified\":\"1F1E8-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1f2.png\",\"sheet_x\":33,\"sheet_y\":38,\"short_name\":\"flag-cm\",\"short_names\":[\"flag-cm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":40,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CN\",\"unified\":\"1F1E8-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":\"EB11\",\"softbank\":\"E513\",\"google\":\"FE4ED\",\"image\":\"1f1e8-1f1f3.png\",\"sheet_x\":33,\"sheet_y\":39,\"short_name\":\"flag-cn\",\"short_names\":[\"flag-cn\",\"cn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":47,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CO\",\"unified\":\"1F1E8-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1f4.png\",\"sheet_x\":33,\"sheet_y\":40,\"short_name\":\"flag-co\",\"short_names\":[\"flag-co\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":50,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CP\",\"unified\":\"1F1E8-1F1F5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1f5.png\",\"sheet_x\":34,\"sheet_y\":0,\"short_name\":\"flag-cp\",\"short_names\":[\"flag-cp\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CR\",\"unified\":\"1F1E8-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1f7.png\",\"sheet_x\":34,\"sheet_y\":1,\"short_name\":\"flag-cr\",\"short_names\":[\"flag-cr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":55,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CU\",\"unified\":\"1F1E8-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1fa.png\",\"sheet_x\":34,\"sheet_y\":2,\"short_name\":\"flag-cu\",\"short_names\":[\"flag-cu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":57,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CV\",\"unified\":\"1F1E8-1F1FB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1fb.png\",\"sheet_x\":34,\"sheet_y\":3,\"short_name\":\"flag-cv\",\"short_names\":[\"flag-cv\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":38,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CW\",\"unified\":\"1F1E8-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1fc.png\",\"sheet_x\":34,\"sheet_y\":4,\"short_name\":\"flag-cw\",\"short_names\":[\"flag-cw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":58,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CX\",\"unified\":\"1F1E8-1F1FD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1fd.png\",\"sheet_x\":34,\"sheet_y\":5,\"short_name\":\"flag-cx\",\"short_names\":[\"flag-cx\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":48,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CY\",\"unified\":\"1F1E8-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1fe.png\",\"sheet_x\":34,\"sheet_y\":6,\"short_name\":\"flag-cy\",\"short_names\":[\"flag-cy\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":59,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS CZ\",\"unified\":\"1F1E8-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e8-1f1ff.png\",\"sheet_x\":34,\"sheet_y\":7,\"short_name\":\"flag-cz\",\"short_names\":[\"flag-cz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":60,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS DE\",\"unified\":\"1F1E9-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":\"EB0E\",\"softbank\":\"E50E\",\"google\":\"FE4E8\",\"image\":\"1f1e9-1f1ea.png\",\"sheet_x\":34,\"sheet_y\":8,\"short_name\":\"flag-de\",\"short_names\":[\"flag-de\",\"de\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":84,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS DG\",\"unified\":\"1F1E9-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e9-1f1ec.png\",\"sheet_x\":34,\"sheet_y\":9,\"short_name\":\"flag-dg\",\"short_names\":[\"flag-dg\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS DJ\",\"unified\":\"1F1E9-1F1EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e9-1f1ef.png\",\"sheet_x\":34,\"sheet_y\":10,\"short_name\":\"flag-dj\",\"short_names\":[\"flag-dj\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":62,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS DK\",\"unified\":\"1F1E9-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e9-1f1f0.png\",\"sheet_x\":34,\"sheet_y\":11,\"short_name\":\"flag-dk\",\"short_names\":[\"flag-dk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":61,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS DM\",\"unified\":\"1F1E9-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e9-1f1f2.png\",\"sheet_x\":34,\"sheet_y\":12,\"short_name\":\"flag-dm\",\"short_names\":[\"flag-dm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":63,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS DO\",\"unified\":\"1F1E9-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e9-1f1f4.png\",\"sheet_x\":34,\"sheet_y\":13,\"short_name\":\"flag-do\",\"short_names\":[\"flag-do\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":64,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS DZ\",\"unified\":\"1F1E9-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1e9-1f1ff.png\",\"sheet_x\":34,\"sheet_y\":14,\"short_name\":\"flag-dz\",\"short_names\":[\"flag-dz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":4,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS EA\",\"unified\":\"1F1EA-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1e6.png\",\"sheet_x\":34,\"sheet_y\":15,\"short_name\":\"flag-ea\",\"short_names\":[\"flag-ea\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS EC\",\"unified\":\"1F1EA-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1e8.png\",\"sheet_x\":34,\"sheet_y\":16,\"short_name\":\"flag-ec\",\"short_names\":[\"flag-ec\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":65,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS EE\",\"unified\":\"1F1EA-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1ea.png\",\"sheet_x\":34,\"sheet_y\":17,\"short_name\":\"flag-ee\",\"short_names\":[\"flag-ee\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":70,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS EG\",\"unified\":\"1F1EA-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1ec.png\",\"sheet_x\":34,\"sheet_y\":18,\"short_name\":\"flag-eg\",\"short_names\":[\"flag-eg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":66,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS EH\",\"unified\":\"1F1EA-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1ed.png\",\"sheet_x\":34,\"sheet_y\":19,\"short_name\":\"flag-eh\",\"short_names\":[\"flag-eh\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":244,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ER\",\"unified\":\"1F1EA-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1f7.png\",\"sheet_x\":34,\"sheet_y\":20,\"short_name\":\"flag-er\",\"short_names\":[\"flag-er\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":69,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ES\",\"unified\":\"1F1EA-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":\"E5D5\",\"softbank\":\"E511\",\"google\":\"FE4EB\",\"image\":\"1f1ea-1f1f8.png\",\"sheet_x\":34,\"sheet_y\":21,\"short_name\":\"flag-es\",\"short_names\":[\"flag-es\",\"es\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":209,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ET\",\"unified\":\"1F1EA-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1f9.png\",\"sheet_x\":34,\"sheet_y\":22,\"short_name\":\"flag-et\",\"short_names\":[\"flag-et\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":71,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS EU\",\"unified\":\"1F1EA-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ea-1f1fa.png\",\"sheet_x\":34,\"sheet_y\":23,\"short_name\":\"flag-eu\",\"short_names\":[\"flag-eu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":72,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS FI\",\"unified\":\"1F1EB-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1eb-1f1ee.png\",\"sheet_x\":34,\"sheet_y\":24,\"short_name\":\"flag-fi\",\"short_names\":[\"flag-fi\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":76,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS FJ\",\"unified\":\"1F1EB-1F1EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1eb-1f1ef.png\",\"sheet_x\":34,\"sheet_y\":25,\"short_name\":\"flag-fj\",\"short_names\":[\"flag-fj\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":75,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS FK\",\"unified\":\"1F1EB-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1eb-1f1f0.png\",\"sheet_x\":34,\"sheet_y\":26,\"short_name\":\"flag-fk\",\"short_names\":[\"flag-fk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":73,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS FM\",\"unified\":\"1F1EB-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1eb-1f1f2.png\",\"sheet_x\":34,\"sheet_y\":27,\"short_name\":\"flag-fm\",\"short_names\":[\"flag-fm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":144,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS FO\",\"unified\":\"1F1EB-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1eb-1f1f4.png\",\"sheet_x\":34,\"sheet_y\":28,\"short_name\":\"flag-fo\",\"short_names\":[\"flag-fo\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":74,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS FR\",\"unified\":\"1F1EB-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":\"EAFA\",\"softbank\":\"E50D\",\"google\":\"FE4E7\",\"image\":\"1f1eb-1f1f7.png\",\"sheet_x\":34,\"sheet_y\":29,\"short_name\":\"flag-fr\",\"short_names\":[\"flag-fr\",\"fr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":77,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GA\",\"unified\":\"1F1EC-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1e6.png\",\"sheet_x\":34,\"sheet_y\":30,\"short_name\":\"flag-ga\",\"short_names\":[\"flag-ga\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":81,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GB\",\"unified\":\"1F1EC-1F1E7\",\"variations\":[],\"docomo\":null,\"au\":\"EB10\",\"softbank\":\"E510\",\"google\":\"FE4EA\",\"image\":\"1f1ec-1f1e7.png\",\"sheet_x\":34,\"sheet_y\":31,\"short_name\":\"flag-gb\",\"short_names\":[\"flag-gb\",\"gb\",\"uk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":234,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GD\",\"unified\":\"1F1EC-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1e9.png\",\"sheet_x\":34,\"sheet_y\":32,\"short_name\":\"flag-gd\",\"short_names\":[\"flag-gd\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":89,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GE\",\"unified\":\"1F1EC-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1ea.png\",\"sheet_x\":34,\"sheet_y\":33,\"short_name\":\"flag-ge\",\"short_names\":[\"flag-ge\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":83,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GF\",\"unified\":\"1F1EC-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1eb.png\",\"sheet_x\":34,\"sheet_y\":34,\"short_name\":\"flag-gf\",\"short_names\":[\"flag-gf\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":78,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GG\",\"unified\":\"1F1EC-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1ec.png\",\"sheet_x\":34,\"sheet_y\":35,\"short_name\":\"flag-gg\",\"short_names\":[\"flag-gg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":93,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GH\",\"unified\":\"1F1EC-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1ed.png\",\"sheet_x\":34,\"sheet_y\":36,\"short_name\":\"flag-gh\",\"short_names\":[\"flag-gh\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":85,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GI\",\"unified\":\"1F1EC-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1ee.png\",\"sheet_x\":34,\"sheet_y\":37,\"short_name\":\"flag-gi\",\"short_names\":[\"flag-gi\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":86,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GL\",\"unified\":\"1F1EC-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f1.png\",\"sheet_x\":34,\"sheet_y\":38,\"short_name\":\"flag-gl\",\"short_names\":[\"flag-gl\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":88,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GM\",\"unified\":\"1F1EC-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f2.png\",\"sheet_x\":34,\"sheet_y\":39,\"short_name\":\"flag-gm\",\"short_names\":[\"flag-gm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":82,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GN\",\"unified\":\"1F1EC-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f3.png\",\"sheet_x\":34,\"sheet_y\":40,\"short_name\":\"flag-gn\",\"short_names\":[\"flag-gn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":94,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GP\",\"unified\":\"1F1EC-1F1F5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f5.png\",\"sheet_x\":35,\"sheet_y\":0,\"short_name\":\"flag-gp\",\"short_names\":[\"flag-gp\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":90,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GQ\",\"unified\":\"1F1EC-1F1F6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f6.png\",\"sheet_x\":35,\"sheet_y\":1,\"short_name\":\"flag-gq\",\"short_names\":[\"flag-gq\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":68,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GR\",\"unified\":\"1F1EC-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f7.png\",\"sheet_x\":35,\"sheet_y\":2,\"short_name\":\"flag-gr\",\"short_names\":[\"flag-gr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":87,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GS\",\"unified\":\"1F1EC-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f8.png\",\"sheet_x\":35,\"sheet_y\":3,\"short_name\":\"flag-gs\",\"short_names\":[\"flag-gs\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":206,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GT\",\"unified\":\"1F1EC-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1f9.png\",\"sheet_x\":35,\"sheet_y\":4,\"short_name\":\"flag-gt\",\"short_names\":[\"flag-gt\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":92,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GU\",\"unified\":\"1F1EC-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1fa.png\",\"sheet_x\":35,\"sheet_y\":5,\"short_name\":\"flag-gu\",\"short_names\":[\"flag-gu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":91,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GW\",\"unified\":\"1F1EC-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1fc.png\",\"sheet_x\":35,\"sheet_y\":6,\"short_name\":\"flag-gw\",\"short_names\":[\"flag-gw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":95,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS GY\",\"unified\":\"1F1EC-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ec-1f1fe.png\",\"sheet_x\":35,\"sheet_y\":7,\"short_name\":\"flag-gy\",\"short_names\":[\"flag-gy\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":96,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS HK\",\"unified\":\"1F1ED-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ed-1f1f0.png\",\"sheet_x\":35,\"sheet_y\":8,\"short_name\":\"flag-hk\",\"short_names\":[\"flag-hk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":99,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS HM\",\"unified\":\"1F1ED-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ed-1f1f2.png\",\"sheet_x\":35,\"sheet_y\":9,\"short_name\":\"flag-hm\",\"short_names\":[\"flag-hm\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS HN\",\"unified\":\"1F1ED-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ed-1f1f3.png\",\"sheet_x\":35,\"sheet_y\":10,\"short_name\":\"flag-hn\",\"short_names\":[\"flag-hn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":98,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS HR\",\"unified\":\"1F1ED-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ed-1f1f7.png\",\"sheet_x\":35,\"sheet_y\":11,\"short_name\":\"flag-hr\",\"short_names\":[\"flag-hr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":56,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS HT\",\"unified\":\"1F1ED-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ed-1f1f9.png\",\"sheet_x\":35,\"sheet_y\":12,\"short_name\":\"flag-ht\",\"short_names\":[\"flag-ht\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":97,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS HU\",\"unified\":\"1F1ED-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ed-1f1fa.png\",\"sheet_x\":35,\"sheet_y\":13,\"short_name\":\"flag-hu\",\"short_names\":[\"flag-hu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":100,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IC\",\"unified\":\"1F1EE-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1e8.png\",\"sheet_x\":35,\"sheet_y\":14,\"short_name\":\"flag-ic\",\"short_names\":[\"flag-ic\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":42,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ID\",\"unified\":\"1F1EE-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1e9.png\",\"sheet_x\":35,\"sheet_y\":15,\"short_name\":\"flag-id\",\"short_names\":[\"flag-id\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":103,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IE\",\"unified\":\"1F1EE-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1ea.png\",\"sheet_x\":35,\"sheet_y\":16,\"short_name\":\"flag-ie\",\"short_names\":[\"flag-ie\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":106,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IL\",\"unified\":\"1F1EE-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1f1.png\",\"sheet_x\":35,\"sheet_y\":17,\"short_name\":\"flag-il\",\"short_names\":[\"flag-il\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":108,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IM\",\"unified\":\"1F1EE-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1f2.png\",\"sheet_x\":35,\"sheet_y\":18,\"short_name\":\"flag-im\",\"short_names\":[\"flag-im\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":107,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IN\",\"unified\":\"1F1EE-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1f3.png\",\"sheet_x\":35,\"sheet_y\":19,\"short_name\":\"flag-in\",\"short_names\":[\"flag-in\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":102,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IO\",\"unified\":\"1F1EE-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1f4.png\",\"sheet_x\":35,\"sheet_y\":20,\"short_name\":\"flag-io\",\"short_names\":[\"flag-io\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":32,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IQ\",\"unified\":\"1F1EE-1F1F6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1f6.png\",\"sheet_x\":35,\"sheet_y\":21,\"short_name\":\"flag-iq\",\"short_names\":[\"flag-iq\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":105,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IR\",\"unified\":\"1F1EE-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1f7.png\",\"sheet_x\":35,\"sheet_y\":22,\"short_name\":\"flag-ir\",\"short_names\":[\"flag-ir\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":104,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IS\",\"unified\":\"1F1EE-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ee-1f1f8.png\",\"sheet_x\":35,\"sheet_y\":23,\"short_name\":\"flag-is\",\"short_names\":[\"flag-is\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":101,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS IT\",\"unified\":\"1F1EE-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":\"EB0F\",\"softbank\":\"E50F\",\"google\":\"FE4E9\",\"image\":\"1f1ee-1f1f9.png\",\"sheet_x\":35,\"sheet_y\":24,\"short_name\":\"flag-it\",\"short_names\":[\"flag-it\",\"it\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":109,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS JE\",\"unified\":\"1F1EF-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ef-1f1ea.png\",\"sheet_x\":35,\"sheet_y\":25,\"short_name\":\"flag-je\",\"short_names\":[\"flag-je\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":113,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS JM\",\"unified\":\"1F1EF-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ef-1f1f2.png\",\"sheet_x\":35,\"sheet_y\":26,\"short_name\":\"flag-jm\",\"short_names\":[\"flag-jm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":111,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS JO\",\"unified\":\"1F1EF-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ef-1f1f4.png\",\"sheet_x\":35,\"sheet_y\":27,\"short_name\":\"flag-jo\",\"short_names\":[\"flag-jo\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":114,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS JP\",\"unified\":\"1F1EF-1F1F5\",\"variations\":[],\"docomo\":null,\"au\":\"E4CC\",\"softbank\":\"E50B\",\"google\":\"FE4E5\",\"image\":\"1f1ef-1f1f5.png\",\"sheet_x\":35,\"sheet_y\":28,\"short_name\":\"flag-jp\",\"short_names\":[\"flag-jp\",\"jp\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":112,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KE\",\"unified\":\"1F1F0-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1ea.png\",\"sheet_x\":35,\"sheet_y\":29,\"short_name\":\"flag-ke\",\"short_names\":[\"flag-ke\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":116,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KG\",\"unified\":\"1F1F0-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1ec.png\",\"sheet_x\":35,\"sheet_y\":30,\"short_name\":\"flag-kg\",\"short_names\":[\"flag-kg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":120,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KH\",\"unified\":\"1F1F0-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1ed.png\",\"sheet_x\":35,\"sheet_y\":31,\"short_name\":\"flag-kh\",\"short_names\":[\"flag-kh\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":39,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KI\",\"unified\":\"1F1F0-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1ee.png\",\"sheet_x\":35,\"sheet_y\":32,\"short_name\":\"flag-ki\",\"short_names\":[\"flag-ki\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":117,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KM\",\"unified\":\"1F1F0-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1f2.png\",\"sheet_x\":35,\"sheet_y\":33,\"short_name\":\"flag-km\",\"short_names\":[\"flag-km\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":51,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KN\",\"unified\":\"1F1F0-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1f3.png\",\"sheet_x\":35,\"sheet_y\":34,\"short_name\":\"flag-kn\",\"short_names\":[\"flag-kn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":187,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KP\",\"unified\":\"1F1F0-1F1F5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1f5.png\",\"sheet_x\":35,\"sheet_y\":35,\"short_name\":\"flag-kp\",\"short_names\":[\"flag-kp\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":165,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KR\",\"unified\":\"1F1F0-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":\"EB12\",\"softbank\":\"E514\",\"google\":\"FE4EE\",\"image\":\"1f1f0-1f1f7.png\",\"sheet_x\":35,\"sheet_y\":36,\"short_name\":\"flag-kr\",\"short_names\":[\"flag-kr\",\"kr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":207,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KW\",\"unified\":\"1F1F0-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1fc.png\",\"sheet_x\":35,\"sheet_y\":37,\"short_name\":\"flag-kw\",\"short_names\":[\"flag-kw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":119,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KY\",\"unified\":\"1F1F0-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1fe.png\",\"sheet_x\":35,\"sheet_y\":38,\"short_name\":\"flag-ky\",\"short_names\":[\"flag-ky\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":43,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS KZ\",\"unified\":\"1F1F0-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f0-1f1ff.png\",\"sheet_x\":35,\"sheet_y\":39,\"short_name\":\"flag-kz\",\"short_names\":[\"flag-kz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":115,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LA\",\"unified\":\"1F1F1-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1e6.png\",\"sheet_x\":35,\"sheet_y\":40,\"short_name\":\"flag-la\",\"short_names\":[\"flag-la\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":121,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LB\",\"unified\":\"1F1F1-1F1E7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1e7.png\",\"sheet_x\":36,\"sheet_y\":0,\"short_name\":\"flag-lb\",\"short_names\":[\"flag-lb\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":123,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LC\",\"unified\":\"1F1F1-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1e8.png\",\"sheet_x\":36,\"sheet_y\":1,\"short_name\":\"flag-lc\",\"short_names\":[\"flag-lc\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":188,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LI\",\"unified\":\"1F1F1-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1ee.png\",\"sheet_x\":36,\"sheet_y\":2,\"short_name\":\"flag-li\",\"short_names\":[\"flag-li\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":127,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LK\",\"unified\":\"1F1F1-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1f0.png\",\"sheet_x\":36,\"sheet_y\":3,\"short_name\":\"flag-lk\",\"short_names\":[\"flag-lk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":210,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LR\",\"unified\":\"1F1F1-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1f7.png\",\"sheet_x\":36,\"sheet_y\":4,\"short_name\":\"flag-lr\",\"short_names\":[\"flag-lr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":125,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LS\",\"unified\":\"1F1F1-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1f8.png\",\"sheet_x\":36,\"sheet_y\":5,\"short_name\":\"flag-ls\",\"short_names\":[\"flag-ls\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":124,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LT\",\"unified\":\"1F1F1-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1f9.png\",\"sheet_x\":36,\"sheet_y\":6,\"short_name\":\"flag-lt\",\"short_names\":[\"flag-lt\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":128,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LU\",\"unified\":\"1F1F1-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1fa.png\",\"sheet_x\":36,\"sheet_y\":7,\"short_name\":\"flag-lu\",\"short_names\":[\"flag-lu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":129,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LV\",\"unified\":\"1F1F1-1F1FB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1fb.png\",\"sheet_x\":36,\"sheet_y\":8,\"short_name\":\"flag-lv\",\"short_names\":[\"flag-lv\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":122,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS LY\",\"unified\":\"1F1F1-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f1-1f1fe.png\",\"sheet_x\":36,\"sheet_y\":9,\"short_name\":\"flag-ly\",\"short_names\":[\"flag-ly\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":126,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MA\",\"unified\":\"1F1F2-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1e6.png\",\"sheet_x\":36,\"sheet_y\":10,\"short_name\":\"flag-ma\",\"short_names\":[\"flag-ma\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":150,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MC\",\"unified\":\"1F1F2-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1e8.png\",\"sheet_x\":36,\"sheet_y\":11,\"short_name\":\"flag-mc\",\"short_names\":[\"flag-mc\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":146,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MD\",\"unified\":\"1F1F2-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1e9.png\",\"sheet_x\":36,\"sheet_y\":12,\"short_name\":\"flag-md\",\"short_names\":[\"flag-md\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":145,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ME\",\"unified\":\"1F1F2-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1ea.png\",\"sheet_x\":36,\"sheet_y\":13,\"short_name\":\"flag-me\",\"short_names\":[\"flag-me\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":148,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MF\",\"unified\":\"1F1F2-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1eb.png\",\"sheet_x\":36,\"sheet_y\":14,\"short_name\":\"flag-mf\",\"short_names\":[\"flag-mf\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MG\",\"unified\":\"1F1F2-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1ec.png\",\"sheet_x\":36,\"sheet_y\":15,\"short_name\":\"flag-mg\",\"short_names\":[\"flag-mg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":132,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MH\",\"unified\":\"1F1F2-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1ed.png\",\"sheet_x\":36,\"sheet_y\":16,\"short_name\":\"flag-mh\",\"short_names\":[\"flag-mh\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":138,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MK\",\"unified\":\"1F1F2-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f0.png\",\"sheet_x\":36,\"sheet_y\":17,\"short_name\":\"flag-mk\",\"short_names\":[\"flag-mk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":131,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ML\",\"unified\":\"1F1F2-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f1.png\",\"sheet_x\":36,\"sheet_y\":18,\"short_name\":\"flag-ml\",\"short_names\":[\"flag-ml\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":136,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MM\",\"unified\":\"1F1F2-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f2.png\",\"sheet_x\":36,\"sheet_y\":19,\"short_name\":\"flag-mm\",\"short_names\":[\"flag-mm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":152,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MN\",\"unified\":\"1F1F2-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f3.png\",\"sheet_x\":36,\"sheet_y\":20,\"short_name\":\"flag-mn\",\"short_names\":[\"flag-mn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":147,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MO\",\"unified\":\"1F1F2-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f4.png\",\"sheet_x\":36,\"sheet_y\":21,\"short_name\":\"flag-mo\",\"short_names\":[\"flag-mo\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":130,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MP\",\"unified\":\"1F1F2-1F1F5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f5.png\",\"sheet_x\":36,\"sheet_y\":22,\"short_name\":\"flag-mp\",\"short_names\":[\"flag-mp\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":164,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MQ\",\"unified\":\"1F1F2-1F1F6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f6.png\",\"sheet_x\":36,\"sheet_y\":23,\"short_name\":\"flag-mq\",\"short_names\":[\"flag-mq\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":139,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MR\",\"unified\":\"1F1F2-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f7.png\",\"sheet_x\":36,\"sheet_y\":24,\"short_name\":\"flag-mr\",\"short_names\":[\"flag-mr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":140,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MS\",\"unified\":\"1F1F2-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f8.png\",\"sheet_x\":36,\"sheet_y\":25,\"short_name\":\"flag-ms\",\"short_names\":[\"flag-ms\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":149,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MT\",\"unified\":\"1F1F2-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1f9.png\",\"sheet_x\":36,\"sheet_y\":26,\"short_name\":\"flag-mt\",\"short_names\":[\"flag-mt\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":137,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MU\",\"unified\":\"1F1F2-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1fa.png\",\"sheet_x\":36,\"sheet_y\":27,\"short_name\":\"flag-mu\",\"short_names\":[\"flag-mu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":141,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MV\",\"unified\":\"1F1F2-1F1FB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1fb.png\",\"sheet_x\":36,\"sheet_y\":28,\"short_name\":\"flag-mv\",\"short_names\":[\"flag-mv\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":135,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MW\",\"unified\":\"1F1F2-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1fc.png\",\"sheet_x\":36,\"sheet_y\":29,\"short_name\":\"flag-mw\",\"short_names\":[\"flag-mw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":133,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MX\",\"unified\":\"1F1F2-1F1FD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1fd.png\",\"sheet_x\":36,\"sheet_y\":30,\"short_name\":\"flag-mx\",\"short_names\":[\"flag-mx\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":143,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MY\",\"unified\":\"1F1F2-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1fe.png\",\"sheet_x\":36,\"sheet_y\":31,\"short_name\":\"flag-my\",\"short_names\":[\"flag-my\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":134,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS MZ\",\"unified\":\"1F1F2-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f2-1f1ff.png\",\"sheet_x\":36,\"sheet_y\":32,\"short_name\":\"flag-mz\",\"short_names\":[\"flag-mz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":151,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NA\",\"unified\":\"1F1F3-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1e6.png\",\"sheet_x\":36,\"sheet_y\":33,\"short_name\":\"flag-na\",\"short_names\":[\"flag-na\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":153,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NC\",\"unified\":\"1F1F3-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1e8.png\",\"sheet_x\":36,\"sheet_y\":34,\"short_name\":\"flag-nc\",\"short_names\":[\"flag-nc\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":157,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NE\",\"unified\":\"1F1F3-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1ea.png\",\"sheet_x\":36,\"sheet_y\":35,\"short_name\":\"flag-ne\",\"short_names\":[\"flag-ne\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":160,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NF\",\"unified\":\"1F1F3-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1eb.png\",\"sheet_x\":36,\"sheet_y\":36,\"short_name\":\"flag-nf\",\"short_names\":[\"flag-nf\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":163,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NG\",\"unified\":\"1F1F3-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1ec.png\",\"sheet_x\":36,\"sheet_y\":37,\"short_name\":\"flag-ng\",\"short_names\":[\"flag-ng\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":161,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NI\",\"unified\":\"1F1F3-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1ee.png\",\"sheet_x\":36,\"sheet_y\":38,\"short_name\":\"flag-ni\",\"short_names\":[\"flag-ni\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":159,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NL\",\"unified\":\"1F1F3-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1f1.png\",\"sheet_x\":36,\"sheet_y\":39,\"short_name\":\"flag-nl\",\"short_names\":[\"flag-nl\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":156,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NO\",\"unified\":\"1F1F3-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1f4.png\",\"sheet_x\":36,\"sheet_y\":40,\"short_name\":\"flag-no\",\"short_names\":[\"flag-no\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":166,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NP\",\"unified\":\"1F1F3-1F1F5\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1f5.png\",\"sheet_x\":37,\"sheet_y\":0,\"short_name\":\"flag-np\",\"short_names\":[\"flag-np\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":155,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NR\",\"unified\":\"1F1F3-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1f7.png\",\"sheet_x\":37,\"sheet_y\":1,\"short_name\":\"flag-nr\",\"short_names\":[\"flag-nr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":154,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NU\",\"unified\":\"1F1F3-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1fa.png\",\"sheet_x\":37,\"sheet_y\":2,\"short_name\":\"flag-nu\",\"short_names\":[\"flag-nu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":162,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS NZ\",\"unified\":\"1F1F3-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f3-1f1ff.png\",\"sheet_x\":37,\"sheet_y\":3,\"short_name\":\"flag-nz\",\"short_names\":[\"flag-nz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":158,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS OM\",\"unified\":\"1F1F4-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f4-1f1f2.png\",\"sheet_x\":37,\"sheet_y\":4,\"short_name\":\"flag-om\",\"short_names\":[\"flag-om\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":167,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PA\",\"unified\":\"1F1F5-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1e6.png\",\"sheet_x\":37,\"sheet_y\":5,\"short_name\":\"flag-pa\",\"short_names\":[\"flag-pa\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":171,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PE\",\"unified\":\"1F1F5-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1ea.png\",\"sheet_x\":37,\"sheet_y\":6,\"short_name\":\"flag-pe\",\"short_names\":[\"flag-pe\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":174,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PF\",\"unified\":\"1F1F5-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1eb.png\",\"sheet_x\":37,\"sheet_y\":7,\"short_name\":\"flag-pf\",\"short_names\":[\"flag-pf\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":79,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PG\",\"unified\":\"1F1F5-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1ec.png\",\"sheet_x\":37,\"sheet_y\":8,\"short_name\":\"flag-pg\",\"short_names\":[\"flag-pg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":172,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PH\",\"unified\":\"1F1F5-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1ed.png\",\"sheet_x\":37,\"sheet_y\":9,\"short_name\":\"flag-ph\",\"short_names\":[\"flag-ph\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":175,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PK\",\"unified\":\"1F1F5-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1f0.png\",\"sheet_x\":37,\"sheet_y\":10,\"short_name\":\"flag-pk\",\"short_names\":[\"flag-pk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":168,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PL\",\"unified\":\"1F1F5-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1f1.png\",\"sheet_x\":37,\"sheet_y\":11,\"short_name\":\"flag-pl\",\"short_names\":[\"flag-pl\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":177,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PM\",\"unified\":\"1F1F5-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1f2.png\",\"sheet_x\":37,\"sheet_y\":12,\"short_name\":\"flag-pm\",\"short_names\":[\"flag-pm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":189,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PN\",\"unified\":\"1F1F5-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1f3.png\",\"sheet_x\":37,\"sheet_y\":13,\"short_name\":\"flag-pn\",\"short_names\":[\"flag-pn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":176,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PR\",\"unified\":\"1F1F5-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1f7.png\",\"sheet_x\":37,\"sheet_y\":14,\"short_name\":\"flag-pr\",\"short_names\":[\"flag-pr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":179,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PS\",\"unified\":\"1F1F5-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1f8.png\",\"sheet_x\":37,\"sheet_y\":15,\"short_name\":\"flag-ps\",\"short_names\":[\"flag-ps\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":170,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PT\",\"unified\":\"1F1F5-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1f9.png\",\"sheet_x\":37,\"sheet_y\":16,\"short_name\":\"flag-pt\",\"short_names\":[\"flag-pt\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":178,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PW\",\"unified\":\"1F1F5-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1fc.png\",\"sheet_x\":37,\"sheet_y\":17,\"short_name\":\"flag-pw\",\"short_names\":[\"flag-pw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":169,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS PY\",\"unified\":\"1F1F5-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f5-1f1fe.png\",\"sheet_x\":37,\"sheet_y\":18,\"short_name\":\"flag-py\",\"short_names\":[\"flag-py\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":173,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS QA\",\"unified\":\"1F1F6-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f6-1f1e6.png\",\"sheet_x\":37,\"sheet_y\":19,\"short_name\":\"flag-qa\",\"short_names\":[\"flag-qa\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":180,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS RE\",\"unified\":\"1F1F7-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f7-1f1ea.png\",\"sheet_x\":37,\"sheet_y\":20,\"short_name\":\"flag-re\",\"short_names\":[\"flag-re\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":181,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS RO\",\"unified\":\"1F1F7-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f7-1f1f4.png\",\"sheet_x\":37,\"sheet_y\":21,\"short_name\":\"flag-ro\",\"short_names\":[\"flag-ro\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":182,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS RS\",\"unified\":\"1F1F7-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f7-1f1f8.png\",\"sheet_x\":37,\"sheet_y\":22,\"short_name\":\"flag-rs\",\"short_names\":[\"flag-rs\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":196,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS RU\",\"unified\":\"1F1F7-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":\"E5D6\",\"softbank\":\"E512\",\"google\":\"FE4EC\",\"image\":\"1f1f7-1f1fa.png\",\"sheet_x\":37,\"sheet_y\":23,\"short_name\":\"flag-ru\",\"short_names\":[\"flag-ru\",\"ru\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":183,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS RW\",\"unified\":\"1F1F7-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f7-1f1fc.png\",\"sheet_x\":37,\"sheet_y\":24,\"short_name\":\"flag-rw\",\"short_names\":[\"flag-rw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":184,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SA\",\"unified\":\"1F1F8-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1e6.png\",\"sheet_x\":37,\"sheet_y\":25,\"short_name\":\"flag-sa\",\"short_names\":[\"flag-sa\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":194,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SB\",\"unified\":\"1F1F8-1F1E7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1e7.png\",\"sheet_x\":37,\"sheet_y\":26,\"short_name\":\"flag-sb\",\"short_names\":[\"flag-sb\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":203,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SC\",\"unified\":\"1F1F8-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1e8.png\",\"sheet_x\":37,\"sheet_y\":27,\"short_name\":\"flag-sc\",\"short_names\":[\"flag-sc\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":197,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SD\",\"unified\":\"1F1F8-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1e9.png\",\"sheet_x\":37,\"sheet_y\":28,\"short_name\":\"flag-sd\",\"short_names\":[\"flag-sd\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":211,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SE\",\"unified\":\"1F1F8-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1ea.png\",\"sheet_x\":37,\"sheet_y\":29,\"short_name\":\"flag-se\",\"short_names\":[\"flag-se\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":214,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SG\",\"unified\":\"1F1F8-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1ec.png\",\"sheet_x\":37,\"sheet_y\":30,\"short_name\":\"flag-sg\",\"short_names\":[\"flag-sg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":199,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SH\",\"unified\":\"1F1F8-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1ed.png\",\"sheet_x\":37,\"sheet_y\":31,\"short_name\":\"flag-sh\",\"short_names\":[\"flag-sh\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":186,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SI\",\"unified\":\"1F1F8-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1ee.png\",\"sheet_x\":37,\"sheet_y\":32,\"short_name\":\"flag-si\",\"short_names\":[\"flag-si\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":202,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SJ\",\"unified\":\"1F1F8-1F1EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1ef.png\",\"sheet_x\":37,\"sheet_y\":33,\"short_name\":\"flag-sj\",\"short_names\":[\"flag-sj\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SK\",\"unified\":\"1F1F8-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f0.png\",\"sheet_x\":37,\"sheet_y\":34,\"short_name\":\"flag-sk\",\"short_names\":[\"flag-sk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":201,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SL\",\"unified\":\"1F1F8-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f1.png\",\"sheet_x\":37,\"sheet_y\":35,\"short_name\":\"flag-sl\",\"short_names\":[\"flag-sl\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":198,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SM\",\"unified\":\"1F1F8-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f2.png\",\"sheet_x\":37,\"sheet_y\":36,\"short_name\":\"flag-sm\",\"short_names\":[\"flag-sm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":192,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SN\",\"unified\":\"1F1F8-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f3.png\",\"sheet_x\":37,\"sheet_y\":37,\"short_name\":\"flag-sn\",\"short_names\":[\"flag-sn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":195,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SO\",\"unified\":\"1F1F8-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f4.png\",\"sheet_x\":37,\"sheet_y\":38,\"short_name\":\"flag-so\",\"short_names\":[\"flag-so\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":204,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SR\",\"unified\":\"1F1F8-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f7.png\",\"sheet_x\":37,\"sheet_y\":39,\"short_name\":\"flag-sr\",\"short_names\":[\"flag-sr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":212,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SS\",\"unified\":\"1F1F8-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f8.png\",\"sheet_x\":37,\"sheet_y\":40,\"short_name\":\"flag-ss\",\"short_names\":[\"flag-ss\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":208,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ST\",\"unified\":\"1F1F8-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1f9.png\",\"sheet_x\":38,\"sheet_y\":0,\"short_name\":\"flag-st\",\"short_names\":[\"flag-st\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":193,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SV\",\"unified\":\"1F1F8-1F1FB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1fb.png\",\"sheet_x\":38,\"sheet_y\":1,\"short_name\":\"flag-sv\",\"short_names\":[\"flag-sv\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":67,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SX\",\"unified\":\"1F1F8-1F1FD\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1fd.png\",\"sheet_x\":38,\"sheet_y\":2,\"short_name\":\"flag-sx\",\"short_names\":[\"flag-sx\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":200,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SY\",\"unified\":\"1F1F8-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1fe.png\",\"sheet_x\":38,\"sheet_y\":3,\"short_name\":\"flag-sy\",\"short_names\":[\"flag-sy\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":216,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS SZ\",\"unified\":\"1F1F8-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f8-1f1ff.png\",\"sheet_x\":38,\"sheet_y\":4,\"short_name\":\"flag-sz\",\"short_names\":[\"flag-sz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":213,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TA\",\"unified\":\"1F1F9-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1e6.png\",\"sheet_x\":38,\"sheet_y\":5,\"short_name\":\"flag-ta\",\"short_names\":[\"flag-ta\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TC\",\"unified\":\"1F1F9-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1e8.png\",\"sheet_x\":38,\"sheet_y\":6,\"short_name\":\"flag-tc\",\"short_names\":[\"flag-tc\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":229,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TD\",\"unified\":\"1F1F9-1F1E9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1e9.png\",\"sheet_x\":38,\"sheet_y\":7,\"short_name\":\"flag-td\",\"short_names\":[\"flag-td\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":45,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TF\",\"unified\":\"1F1F9-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1eb.png\",\"sheet_x\":38,\"sheet_y\":8,\"short_name\":\"flag-tf\",\"short_names\":[\"flag-tf\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":80,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TG\",\"unified\":\"1F1F9-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1ec.png\",\"sheet_x\":38,\"sheet_y\":9,\"short_name\":\"flag-tg\",\"short_names\":[\"flag-tg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":222,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TH\",\"unified\":\"1F1F9-1F1ED\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1ed.png\",\"sheet_x\":38,\"sheet_y\":10,\"short_name\":\"flag-th\",\"short_names\":[\"flag-th\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":220,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TJ\",\"unified\":\"1F1F9-1F1EF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1ef.png\",\"sheet_x\":38,\"sheet_y\":11,\"short_name\":\"flag-tj\",\"short_names\":[\"flag-tj\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":218,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TK\",\"unified\":\"1F1F9-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1f0.png\",\"sheet_x\":38,\"sheet_y\":12,\"short_name\":\"flag-tk\",\"short_names\":[\"flag-tk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":223,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TL\",\"unified\":\"1F1F9-1F1F1\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1f1.png\",\"sheet_x\":38,\"sheet_y\":13,\"short_name\":\"flag-tl\",\"short_names\":[\"flag-tl\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":221,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TM\",\"unified\":\"1F1F9-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1f2.png\",\"sheet_x\":38,\"sheet_y\":14,\"short_name\":\"flag-tm\",\"short_names\":[\"flag-tm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":228,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TN\",\"unified\":\"1F1F9-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1f3.png\",\"sheet_x\":38,\"sheet_y\":15,\"short_name\":\"flag-tn\",\"short_names\":[\"flag-tn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":226,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TO\",\"unified\":\"1F1F9-1F1F4\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1f4.png\",\"sheet_x\":38,\"sheet_y\":16,\"short_name\":\"flag-to\",\"short_names\":[\"flag-to\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":224,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TR\",\"unified\":\"1F1F9-1F1F7\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1f7.png\",\"sheet_x\":38,\"sheet_y\":17,\"short_name\":\"flag-tr\",\"short_names\":[\"flag-tr\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":227,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TT\",\"unified\":\"1F1F9-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1f9.png\",\"sheet_x\":38,\"sheet_y\":18,\"short_name\":\"flag-tt\",\"short_names\":[\"flag-tt\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":225,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TV\",\"unified\":\"1F1F9-1F1FB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1fb.png\",\"sheet_x\":38,\"sheet_y\":19,\"short_name\":\"flag-tv\",\"short_names\":[\"flag-tv\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":230,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TW\",\"unified\":\"1F1F9-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1fc.png\",\"sheet_x\":38,\"sheet_y\":20,\"short_name\":\"flag-tw\",\"short_names\":[\"flag-tw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":217,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS TZ\",\"unified\":\"1F1F9-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1f9-1f1ff.png\",\"sheet_x\":38,\"sheet_y\":21,\"short_name\":\"flag-tz\",\"short_names\":[\"flag-tz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":219,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS UA\",\"unified\":\"1F1FA-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fa-1f1e6.png\",\"sheet_x\":38,\"sheet_y\":22,\"short_name\":\"flag-ua\",\"short_names\":[\"flag-ua\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":232,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS UG\",\"unified\":\"1F1FA-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fa-1f1ec.png\",\"sheet_x\":38,\"sheet_y\":23,\"short_name\":\"flag-ug\",\"short_names\":[\"flag-ug\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":231,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS UM\",\"unified\":\"1F1FA-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fa-1f1f2.png\",\"sheet_x\":38,\"sheet_y\":24,\"short_name\":\"flag-um\",\"short_names\":[\"flag-um\"],\"text\":null,\"texts\":null,\"category\":null,\"sort_order\":null,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS US\",\"unified\":\"1F1FA-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":\"E573\",\"softbank\":\"E50C\",\"google\":\"FE4E6\",\"image\":\"1f1fa-1f1f8.png\",\"sheet_x\":38,\"sheet_y\":25,\"short_name\":\"flag-us\",\"short_names\":[\"flag-us\",\"us\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":235,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS UY\",\"unified\":\"1F1FA-1F1FE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fa-1f1fe.png\",\"sheet_x\":38,\"sheet_y\":26,\"short_name\":\"flag-uy\",\"short_names\":[\"flag-uy\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":237,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS UZ\",\"unified\":\"1F1FA-1F1FF\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fa-1f1ff.png\",\"sheet_x\":38,\"sheet_y\":27,\"short_name\":\"flag-uz\",\"short_names\":[\"flag-uz\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":238,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS VA\",\"unified\":\"1F1FB-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fb-1f1e6.png\",\"sheet_x\":38,\"sheet_y\":28,\"short_name\":\"flag-va\",\"short_names\":[\"flag-va\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":240,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS VC\",\"unified\":\"1F1FB-1F1E8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fb-1f1e8.png\",\"sheet_x\":38,\"sheet_y\":29,\"short_name\":\"flag-vc\",\"short_names\":[\"flag-vc\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":190,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS VE\",\"unified\":\"1F1FB-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fb-1f1ea.png\",\"sheet_x\":38,\"sheet_y\":30,\"short_name\":\"flag-ve\",\"short_names\":[\"flag-ve\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":241,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS VG\",\"unified\":\"1F1FB-1F1EC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fb-1f1ec.png\",\"sheet_x\":38,\"sheet_y\":31,\"short_name\":\"flag-vg\",\"short_names\":[\"flag-vg\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":33,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS VI\",\"unified\":\"1F1FB-1F1EE\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fb-1f1ee.png\",\"sheet_x\":38,\"sheet_y\":32,\"short_name\":\"flag-vi\",\"short_names\":[\"flag-vi\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":236,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS VN\",\"unified\":\"1F1FB-1F1F3\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fb-1f1f3.png\",\"sheet_x\":38,\"sheet_y\":33,\"short_name\":\"flag-vn\",\"short_names\":[\"flag-vn\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":242,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS VU\",\"unified\":\"1F1FB-1F1FA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fb-1f1fa.png\",\"sheet_x\":38,\"sheet_y\":34,\"short_name\":\"flag-vu\",\"short_names\":[\"flag-vu\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":239,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS WF\",\"unified\":\"1F1FC-1F1EB\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fc-1f1eb.png\",\"sheet_x\":38,\"sheet_y\":35,\"short_name\":\"flag-wf\",\"short_names\":[\"flag-wf\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":243,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS WS\",\"unified\":\"1F1FC-1F1F8\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fc-1f1f8.png\",\"sheet_x\":38,\"sheet_y\":36,\"short_name\":\"flag-ws\",\"short_names\":[\"flag-ws\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":191,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS XK\",\"unified\":\"1F1FD-1F1F0\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fd-1f1f0.png\",\"sheet_x\":38,\"sheet_y\":37,\"short_name\":\"flag-xk\",\"short_names\":[\"flag-xk\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":118,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS YE\",\"unified\":\"1F1FE-1F1EA\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fe-1f1ea.png\",\"sheet_x\":38,\"sheet_y\":38,\"short_name\":\"flag-ye\",\"short_names\":[\"flag-ye\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":245,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS YT\",\"unified\":\"1F1FE-1F1F9\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1fe-1f1f9.png\",\"sheet_x\":38,\"sheet_y\":39,\"short_name\":\"flag-yt\",\"short_names\":[\"flag-yt\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":142,\"has_img_apple\":true,\"has_img_google\":false,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ZA\",\"unified\":\"1F1FF-1F1E6\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ff-1f1e6.png\",\"sheet_x\":38,\"sheet_y\":40,\"short_name\":\"flag-za\",\"short_names\":[\"flag-za\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":205,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ZM\",\"unified\":\"1F1FF-1F1F2\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ff-1f1f2.png\",\"sheet_x\":39,\"sheet_y\":0,\"short_name\":\"flag-zm\",\"short_names\":[\"flag-zm\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":246,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":\"REGIONAL INDICATOR SYMBOL LETTERS ZW\",\"unified\":\"1F1FF-1F1FC\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f1ff-1f1fc.png\",\"sheet_x\":39,\"sheet_y\":1,\"short_name\":\"flag-zw\",\"short_names\":[\"flag-zw\"],\"text\":null,\"texts\":null,\"category\":\"Flags\",\"sort_order\":247,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F468-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f468-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":2,\"short_name\":\"man-man-boy\",\"short_names\":[\"man-man-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":171,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F468-200D-1F466-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f468-200d-1f466-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":3,\"short_name\":\"man-man-boy-boy\",\"short_names\":[\"man-man-boy-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":174,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F468-200D-1F467\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f468-200d-1f467.png\",\"sheet_x\":39,\"sheet_y\":4,\"short_name\":\"man-man-girl\",\"short_names\":[\"man-man-girl\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":172,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F468-200D-1F467-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f468-200d-1f467-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":5,\"short_name\":\"man-man-girl-boy\",\"short_names\":[\"man-man-girl-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":173,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F468-200D-1F467-200D-1F467\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f468-200d-1f467-200d-1f467.png\",\"sheet_x\":39,\"sheet_y\":6,\"short_name\":\"man-man-girl-girl\",\"short_names\":[\"man-man-girl-girl\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":175,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F469-200D-1F466-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f469-200d-1f466-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":7,\"short_name\":\"man-woman-boy-boy\",\"short_names\":[\"man-woman-boy-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":164,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F469-200D-1F467\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f469-200d-1f467.png\",\"sheet_x\":39,\"sheet_y\":8,\"short_name\":\"man-woman-girl\",\"short_names\":[\"man-woman-girl\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":162,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F469-200D-1F467-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f469-200d-1f467-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":9,\"short_name\":\"man-woman-girl-boy\",\"short_names\":[\"man-woman-girl-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":163,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-1F469-200D-1F467-200D-1F467\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-1f469-200d-1f467-200d-1f467.png\",\"sheet_x\":39,\"sheet_y\":10,\"short_name\":\"man-woman-girl-girl\",\"short_names\":[\"man-woman-girl-girl\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":165,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F468-200D-2764-FE0F-200D-1F468\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-2764-fe0f-200d-1f468.png\",\"sheet_x\":39,\"sheet_y\":11,\"short_name\":\"man-heart-man\",\"short_names\":[\"man-heart-man\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":157,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":false},{\"name\":null,\"unified\":\"1F468-200D-2764-FE0F-200D-1F48B-200D-1F468\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f468-200d-2764-fe0f-200d-1f48b-200d-1f468.png\",\"sheet_x\":39,\"sheet_y\":12,\"short_name\":\"man-kiss-man\",\"short_names\":[\"man-kiss-man\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":160,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":false},{\"name\":null,\"unified\":\"1F469-200D-1F469-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f469-200d-1f469-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":13,\"short_name\":\"woman-woman-boy\",\"short_names\":[\"woman-woman-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":166,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F469-200D-1F469-200D-1F466-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f469-200d-1f469-200d-1f466-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":14,\"short_name\":\"woman-woman-boy-boy\",\"short_names\":[\"woman-woman-boy-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":169,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F469-200D-1F469-200D-1F467\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f469-200d-1f469-200d-1f467.png\",\"sheet_x\":39,\"sheet_y\":15,\"short_name\":\"woman-woman-girl\",\"short_names\":[\"woman-woman-girl\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":167,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F469-200D-1F469-200D-1F467-200D-1F466\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f469-200d-1f469-200d-1f467-200d-1f466.png\",\"sheet_x\":39,\"sheet_y\":16,\"short_name\":\"woman-woman-girl-boy\",\"short_names\":[\"woman-woman-girl-boy\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":168,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F469-200D-1F469-200D-1F467-200D-1F467\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f469-200d-1f469-200d-1f467-200d-1f467.png\",\"sheet_x\":39,\"sheet_y\":17,\"short_name\":\"woman-woman-girl-girl\",\"short_names\":[\"woman-woman-girl-girl\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":170,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":true},{\"name\":null,\"unified\":\"1F469-200D-2764-FE0F-200D-1F469\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f469-200d-2764-fe0f-200d-1f469.png\",\"sheet_x\":39,\"sheet_y\":18,\"short_name\":\"woman-heart-woman\",\"short_names\":[\"woman-heart-woman\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":156,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":false},{\"name\":null,\"unified\":\"1F469-200D-2764-FE0F-200D-1F48B-200D-1F469\",\"variations\":[],\"docomo\":null,\"au\":null,\"softbank\":null,\"google\":null,\"image\":\"1f469-200d-2764-fe0f-200d-1f48b-200d-1f469.png\",\"sheet_x\":39,\"sheet_y\":19,\"short_name\":\"woman-kiss-woman\",\"short_names\":[\"woman-kiss-woman\"],\"text\":null,\"texts\":null,\"category\":\"People\",\"sort_order\":159,\"has_img_apple\":true,\"has_img_google\":true,\"has_img_twitter\":true,\"has_img_emojione\":false}]}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-message-extension.jsx",
    "content": "/* eslint no-cond-assign:0 */\nimport {MessageViewExtension, RegExpUtils} from 'nylas-exports';\nimport emoji from 'node-emoji';\n\nimport EmojiStore from './emoji-store';\n\nfunction makeIntoEmojiTag(nodeArg, emojiName) {\n  const node = nodeArg;\n  node.src = EmojiStore.getImagePath(emojiName);\n  node.className = `emoji ${emojiName}`;\n  node.width = 14;\n  node.height = 14;\n  node.style = '';\n  node.style.marginTop = '-5px';\n}\n\nclass EmojiMessageExtension extends MessageViewExtension {\n  static renderedMessageBodyIntoDocument({document}) {\n    const emojiRegex = RegExpUtils.emojiRegex();\n\n    // Look for emoji in the content of text nodes\n    const treeWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT);\n\n    while (treeWalker.nextNode()) {\n      emojiRegex.lastIndex = 0;\n\n      const node = treeWalker.currentNode;\n      let match = null;\n\n      while (match = emojiRegex.exec(node.textContent)) {\n        const matchEmojiName = emoji.which(match[0]);\n        if (matchEmojiName) {\n          const matchNode = (match.index === 0) ? node : node.splitText(match.index);\n          matchNode.splitText(match[0].length);\n          const imageNode = document.createElement('img');\n          makeIntoEmojiTag(imageNode, matchEmojiName);\n          matchNode.parentNode.replaceChild(imageNode, matchNode);\n        }\n      }\n    }\n  }\n}\n\nexport default EmojiMessageExtension;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-picker.jsx",
    "content": "import {React, ReactDOM} from 'nylas-exports';\nimport emoji from 'node-emoji';\n\nimport EmojiStore from './emoji-store';\nimport EmojiActions from './emoji-actions';\n\n\nclass EmojiPicker extends React.Component {\n  static displayName = \"EmojiPicker\";\n  static propTypes = {\n    emojiOptions: React.PropTypes.array,\n    selectedEmoji: React.PropTypes.string,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = {};\n  }\n\n  componentDidUpdate() {\n    const selectedButton = ReactDOM.findDOMNode(this).querySelector(\".emoji-option\");\n    if (selectedButton) {\n      selectedButton.scrollIntoViewIfNeeded();\n    }\n  }\n\n  onMouseDown(emojiName) {\n    EmojiActions.selectEmoji({emojiName, replaceSelection: true});\n  }\n\n  render() {\n    const emojiButtons = [];\n    let emojiIndex = this.props.emojiOptions.indexOf(this.props.selectedEmoji);\n    if (emojiIndex === -1) emojiIndex = 0;\n    if (this.props.emojiOptions) {\n      this.props.emojiOptions.forEach((emojiOption, i) => {\n        const emojiClass = emojiIndex === i ? \"btn btn-icon emoji-option\" : \"btn btn-icon\";\n        let emojiChar = emoji.get(emojiOption);\n        emojiChar = (\n          <img\n            alt={emojiOption}\n            src={EmojiStore.getImagePath(emojiOption)}\n            width=\"16\"\n            height=\"16\"\n            style={{marginTop: \"-4px\", marginRight: \"3px\"}}\n          />\n        );\n        emojiButtons.push(\n          <button\n            key={emojiOption}\n            onMouseDown={() => this.onMouseDown(emojiOption)}\n            className={emojiClass}\n          >\n            {emojiChar} :{emojiOption}:\n          </button>\n        );\n        emojiButtons.push(<br key={`${emojiOption} br`} />);\n      });\n    }\n    return (\n      <div className=\"emoji-picker\">\n        {emojiButtons}\n      </div>\n    );\n  }\n}\n\nexport default EmojiPicker;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/emoji-store.jsx",
    "content": "/* eslint global-require: \"off\" */\n\nimport NylasStore from 'nylas-store';\nimport _ from 'underscore';\n\nimport {Rx, DatabaseStore} from 'nylas-exports';\nimport EmojiActions from './emoji-actions';\n\nconst EmojiJSONBlobKey = 'emoji';\n\nlet emojiData;\n\nclass EmojiStore extends NylasStore {\n  constructor(props) {\n    super(props);\n    this._emoji = [];\n  }\n\n  activate = () => {\n    const query = DatabaseStore.findJSONBlob(EmojiJSONBlobKey);\n    this._subscription = Rx.Observable.fromQuery(query).subscribe((emoji) => {\n      this._emoji = emoji || [];\n      this.trigger();\n    });\n    this.listenTo(EmojiActions.useEmoji, this._onUseEmoji);\n  }\n\n  frequentlyUsedEmoji = () => {\n    const sortedEmoji = this._emoji;\n    sortedEmoji.sort((a, b) => {\n      if (a.frequency < b.frequency) return 1;\n      return (b.frequency < a.frequency) ? -1 : 0;\n    });\n    const sortedEmojiNames = [];\n    for (const emoji of sortedEmoji) {\n      sortedEmojiNames.push(emoji.emojiName);\n    }\n    if (sortedEmojiNames.length > 32) {\n      return sortedEmojiNames.slice(0, 32);\n    }\n    return sortedEmojiNames;\n  }\n\n  getImagePath(emojiName) {\n    emojiData = emojiData || require('./emoji-data').emojiData\n    for (const emoji of emojiData) {\n      if (emoji.short_names.indexOf(emojiName) !== -1) {\n        if (process.platform === \"darwin\") {\n          return `images/composer-emoji/apple/${emoji.image}`;\n        }\n        return `images/composer-emoji/twitter/${emoji.image}`;\n      }\n    }\n    return ''\n  }\n\n  _onUseEmoji = (emoji) => {\n    const savedEmoji = _.find(this._emoji, (curEmoji) => {\n      return curEmoji.emojiChar === emoji.emojiChar;\n    });\n    if (savedEmoji) {\n      for (const key of Object.keys(emoji)) {\n        savedEmoji[key] = emoji[key];\n      }\n      savedEmoji.frequency++;\n    } else {\n      _.extend(emoji, {frequency: 1});\n      this._emoji.push(emoji);\n    }\n    this._saveEmoji();\n    this.trigger();\n  }\n\n  _saveEmoji = () => {\n    DatabaseStore.inTransaction((t) => {\n      return t.persistJSONBlob(EmojiJSONBlobKey, this._emoji);\n    });\n  }\n\n}\n\nexport default new EmojiStore();\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/lib/main.es6",
    "content": "import {ExtensionRegistry, ComponentRegistry} from 'nylas-exports';\nimport EmojiStore from './emoji-store';\nimport EmojiComposerExtension from './emoji-composer-extension';\nimport EmojiMessageExtension from './emoji-message-extension';\nimport EmojiButton from './emoji-button';\n\nexport function activate() {\n  ExtensionRegistry.Composer.register(EmojiComposerExtension);\n  ExtensionRegistry.MessageView.register(EmojiMessageExtension);\n  ComponentRegistry.register(EmojiButton, {role: 'Composer:ActionButton'});\n  EmojiStore.activate();\n}\n\nexport function deactivate() {\n  ExtensionRegistry.Composer.unregister(EmojiComposerExtension);\n  ExtensionRegistry.MessageView.unregister(EmojiMessageExtension);\n  ComponentRegistry.unregister(EmojiButton);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/package.json",
    "content": "{\n  \"name\": \"composer-emoji\",\n  \"main\": \"./lib/main\",\n  \"version\": \"0.1.0\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"\"\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n\n  \"isOptional\": true,\n\n  \"title\": \"Emoji Picker\",\n  \"icon\": \"./assets/icon.png\",\n  \"description\": \"Insert emoji into messages by typing a colon (:) and the emoji name or choosing one from the composer toolbar!\",\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/spec/emoji-button-popover-spec.jsx",
    "content": "import React from 'react';\nimport ReactTestUtils from 'react-addons-test-utils';\n\nimport {findDOMNode} from 'react-dom';\nimport {renderIntoDocument} from '../../../spec/nylas-test-utils';\nimport Contenteditable from '../../../src/components/contenteditable/contenteditable';\nimport EmojiButtonPopover from '../lib/emoji-button-popover';\nimport EmojiComposerExtension from '../lib/emoji-composer-extension';\n\ndescribe('EmojiButtonPopover', function emojiButtonPopover() {\n  beforeEach(() => {\n    this.position = {\n      x: 20,\n      y: 40,\n    }\n    spyOn(EmojiButtonPopover.prototype, 'calcPosition').andReturn(this.position);\n    spyOn(EmojiComposerExtension, '_onSelectEmoji').andCallThrough();\n\n    this.component = renderIntoDocument(<EmojiButtonPopover />);\n    this.canvas = findDOMNode(ReactTestUtils.findRenderedDOMComponentWithTag(this.component, 'canvas'));\n\n    this.composer = renderIntoDocument(\n      <Contenteditable\n        value={''}\n        onChange={jasmine.createSpy('onChange')}\n        extensions={[EmojiComposerExtension]}\n      />\n    );\n  });\n\n  describe('when inserting emoji', () => {\n    it('should insert emoji on click', () => {\n      ReactTestUtils.Simulate.mouseDown(this.canvas);\n      expect(EmojiComposerExtension._onSelectEmoji).toHaveBeenCalled();\n    });\n  });\n\n  describe('when searching for emoji', () => {\n    it('should filter for matches', () => {\n      this.searchNode = findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'search'))\n      const event = {\n        target: {\n          value: \"heart\",\n        },\n      }\n      ReactTestUtils.Simulate.change(this.searchNode, event);\n      ReactTestUtils.Simulate.mouseDown(this.canvas);\n      expect(EmojiComposerExtension._onSelectEmoji).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/spec/emoji-composer-extension-spec.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport ReactTestUtils from 'react-addons-test-utils';\n\nimport {renderIntoDocument} from '../../../spec/nylas-test-utils';\nimport Contenteditable from '../../../src/components/contenteditable/contenteditable';\nimport EmojiComposerExtension from '../lib/emoji-composer-extension';\n\ndescribe('EmojiComposerExtension', function emojiComposerExtension() {\n  beforeEach(() => {\n    spyOn(EmojiComposerExtension, 'onContentChanged').andCallThrough()\n    spyOn(EmojiComposerExtension, '_onSelectEmoji').andCallThrough()\n    this.component = renderIntoDocument(\n      <Contenteditable\n        html={''}\n        onChange={jasmine.createSpy('onChange')}\n        extensions={[EmojiComposerExtension]}\n      />\n    )\n    this.editableNode = ReactDOM.findDOMNode(this.component).querySelector('[contenteditable]');\n  })\n\n  describe('when emoji trigger is typed', () => {\n    beforeEach(() => {\n      this._performEdit = (newHTML) => {\n        this.editableNode.innerHTML = newHTML;\n        const sel = document.getSelection()\n        const textNode = this.editableNode.childNodes[0];\n        sel.setBaseAndExtent(textNode, textNode.nodeValue.length, textNode, textNode.nodeValue.length);\n      }\n    })\n\n    it('should show the emoji picker', () => {\n      this._performEdit('Testing! :h');\n      waitsFor(() => {\n        return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0\n      });\n    })\n\n    it('should be focused on the first emoji in the list', () => {\n      this._performEdit('Testing! :h');\n      waitsFor(() => {\n        return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-option').length > 0\n      });\n      runs(() => {\n        expect(ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent.indexOf(\":haircut:\") !== -1).toBe(true);\n      });\n    })\n\n    it('should insert an emoji on enter', () => {\n      this._performEdit('Testing! :h');\n      waitsFor(() => {\n        return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0\n      });\n      runs(() => {\n        ReactTestUtils.Simulate.keyDown(this.editableNode, {key: \"Enter\", keyCode: 13, which: 13});\n      });\n      waitsFor(() => {\n        return EmojiComposerExtension._onSelectEmoji.calls.length > 0\n      })\n      runs(() => {\n        expect(this.editableNode.innerHTML).toContain(\"emoji haircut\")\n      });\n    })\n\n    it('should insert an emoji on click', () => {\n      this._performEdit('Testing! :h');\n      waitsFor(() => {\n        return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-picker').length > 0\n      });\n      runs(() => {\n        const button = ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option'))\n        ReactTestUtils.Simulate.mouseDown(button);\n        expect(EmojiComposerExtension._onSelectEmoji).toHaveBeenCalled()\n      });\n      waitsFor(() => {\n        return EmojiComposerExtension._onSelectEmoji.calls.length > 0\n      })\n      runs(() => {\n        expect(this.editableNode.innerHTML).toContain(\"emoji haircut\")\n      });\n    })\n\n    it('should move to the next emoji on arrow down', () => {\n      this._performEdit('Testing! :h');\n      waitsFor(() => {\n        return ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'emoji-option').length > 0\n      });\n      runs(() => {\n        ReactTestUtils.Simulate.keyDown(this.editableNode, {key: \"ArrowDown\", keyCode: 40, which: 40});\n      });\n      waitsFor(() => {\n        return EmojiComposerExtension.onContentChanged.calls.length > 1\n      });\n      runs(() => {\n        expect(ReactDOM.findDOMNode(ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'emoji-option')).textContent.indexOf(\":hamburger:\") !== -1).toBe(true);\n      });\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-emoji/stylesheets/composer-emoji.less",
    "content": "@import \"ui-variables\";\n\n.emoji-picker {\n  max-height: 130px !important;\n  margin: 10px;\n  overflow: auto;\n  .btn.btn-icon {\n    font-size: 14px !important;\n    padding: 0 0.5em;\n    &:first-child {\n      padding-left: 0.5em !important;\n    }\n    &.emoji-option, &:hover {\n      background-color: @btn-emphasis-bg-color;\n      color: #FFFFFF;\n      border-radius: 5px;\n    }\n  }\n}\n\n.emoji-button-popover {\n  width: 210px;\n  height: 290px;\n  overflow: hidden;\n  .emoji-tabs {\n    display: flex;\n    flex-direction: row;\n    padding: 5px 5px 5px 10px;\n    border-bottom: 1px solid @border-color-primary;\n    transition: box-shadow 0.5s;\n    &.shadow {\n      box-shadow: @standard-shadow;\n    }\n    .emoji-tab {\n      background-color: @gray-light;\n      &.active {\n        background-color: @component-active-color;\n      }\n    }\n  }\n  .emoji-finder-container {\n    height: 232px;\n    .scrollbar-track {\n      background: transparent;\n      border-left: none;\n      width: 10px;\n    }\n    .emoji-search-container {\n      padding: @padding-base-vertical * 1.5 @padding-base-horizontal 0;\n    }\n  }\n  .emoji-name {\n    height: 25px;\n    width: 192px;\n    margin-top: 2px;\n    margin-left: 10px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    color: @text-color-very-subtle;\n  }\n}"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/listens-to-mail-merge-session.jsx",
    "content": "/* eslint no-prototype-builtins: 0 */\nimport React, {Component, PropTypes} from 'react';\nimport {mailMergeSessionForDraft} from './mail-merge-draft-editing-session'\n\n\nexport default function ListensToMailMergeSession(ComposedComponent) {\n  return class extends Component {\n    static displayName = ComposedComponent.displayName\n\n    static containerRequired = false\n\n    static propTypes = {\n      session: PropTypes.object,\n      draftClientId: PropTypes.string,\n      ...ComposedComponent.propTypes,\n    }\n\n    constructor(props) {\n      super(props)\n      this.unlisten = () => {}\n      this.state = {\n        mailMergeSession: mailMergeSessionForDraft(props.draftClientId, props.session),\n      };\n    }\n\n    componentDidMount() {\n      const {mailMergeSession} = this.state;\n      if (mailMergeSession) {\n        this.unlisten = mailMergeSession.listen(() => {\n          this.setState({mailMergeSession})\n        });\n      }\n    }\n\n    componentWillUnmount() {\n      this.unlisten();\n    }\n\n    focus() {\n      if (this.refs.composed) {\n        this.refs.composed.focus()\n      }\n    }\n\n    render() {\n      const {mailMergeSession} = this.state;\n\n      if (!mailMergeSession) {\n        return <ComposedComponent {...this.props} sessionState={{}} />\n      }\n      const componentProps = {\n        ...this.props,\n        mailMergeSession: mailMergeSession,\n        sessionState: mailMergeSession.state,\n      }\n      if (Component.isPrototypeOf(ComposedComponent)) {\n        componentProps.ref = 'composed'\n      }\n      return (\n        <ComposedComponent {...componentProps} />\n      )\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-body-token.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport MailMergeToken from './mail-merge-token'\nimport {DragBehaviors} from './mail-merge-constants'\nimport {tokenQuerySelector} from './mail-merge-utils'\nimport ListensToMailMergeSession from './listens-to-mail-merge-session'\n\n/**\n * MailMergeBodyTokens are rendered by the OverlaidComponents component in the\n * subject and body of the composer.\n * The OverlaidComponents' state is effectively the state of the contenteditable\n * inside those fields, * and it decides what to render based on the\n * anchor (img) tags that are present in the contenteditable.\n *\n * Given this setup, we use the lifecycle methods of MailMergeBodyToken to keep\n * the state of the contenteditable (the tokens actually rendered in the UI),\n * in sync with our token state for mail merge (tokenDataSource)\n */\nclass MailMergeBodyToken extends Component {\n  static displayName = 'MailMergeBodyToken'\n\n  static propTypes = {\n    className: PropTypes.string,\n    tokenId: PropTypes.string,\n    field: PropTypes.string,\n    colName: PropTypes.string,\n    sessionState: PropTypes.object,\n    mailMergeSession: PropTypes.object,\n    draftClientId: PropTypes.string,\n    colIdx: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n    isPreview: PropTypes.bool,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = this.getState(props)\n  }\n\n  componentDidMount() {\n    // When the token gets mounted, it means a mail merge token anchor node was\n    // added to the contenteditable, via drop, paste, or any other means, so we\n    // add it to our mail merge state\n    const {colIdx, field, colName, tokenId, mailMergeSession} = this.props\n    const {tokenDataSource} = mailMergeSession.state\n    const token = tokenDataSource.getToken(field, tokenId)\n    if (!token) {\n      mailMergeSession.linkToDraft({colIdx, field, colName, tokenId})\n    }\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState(this.getState(nextProps, this.state.colIdx))\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (\n      this.props.isPreview !== nextProps.isPreview ||\n      this.state.colIdx !== nextState.colIdx ||\n      this.props.sessionState.selection !== nextProps.sessionState.selection ||\n      this.props.sessionState.tableDataSource !== nextProps.sessionState.tableDataSource ||\n      this.props.sessionState.tokenDataSource !== nextProps.sessionState.tokenDataSource\n    )\n  }\n\n  componentDidUpdate() {\n    // A token might be removed by mutations to the contenteditable, in which\n    // case the tokenDataSource's state is updated by componentWillUnmount.\n    //\n    // However, when a token is removed from state via other means, e.g. when a\n    // table column is removed, we also want to make sure that we remove it from the\n    // UI. Since the contenteditable is effectively the source of state for\n    // OverlaidComponents, we imperatively remove the token from contenteditable\n    // if it has been removed from our state.\n    const {field, tokenId, sessionState: {tokenDataSource}} = this.props\n    const token = tokenDataSource.getToken(field, tokenId)\n    if (!token) {\n      const node = document.querySelector(tokenQuerySelector(tokenId))\n      if (node) {\n        node.parentNode.removeChild(node)\n      }\n    }\n  }\n\n  componentWillUnmount() {\n    // A token might be removed by any sort of mutations to the contenteditable.\n    // When an the actual anchor node in the contenteditable is removed from\n    // the dom tree, OverlaidComponents will unmount our corresponding token,\n    // so this is where we get to update our tokenDataSource's state\n    const {field, tokenId, mailMergeSession} = this.props\n    mailMergeSession.unlinkFromDraft({field, tokenId})\n  }\n\n  getState(props) {\n    // Keep colIdx as state in case the column changes index when importing a\n    // new csv file, thus changing styling\n    const {sessionState: {tokenDataSource}, field, tokenId} = props\n    const nextToken = tokenDataSource.getToken(field, tokenId)\n    if (nextToken) {\n      const {colIdx, colName} = nextToken\n      return {colIdx, colName}\n    }\n    const {colIdx, colName} = props\n    return {colIdx, colName}\n  }\n\n  render() {\n    const {colIdx, colName} = this.state\n    const {className, draftClientId, sessionState, isPreview} = this.props\n    const {tableDataSource, selection} = sessionState\n    const selectionValue = tableDataSource.cellAt({rowIdx: selection.rowIdx, colIdx}) || \"No value selected\"\n\n    if (isPreview) {\n      return <span>{selectionValue}</span>\n    }\n\n    return (\n      <span className={className}>\n        <MailMergeToken\n          draggable\n          colIdx={colIdx}\n          colName={colName}\n          dragBehavior={DragBehaviors.Move}\n          draftClientId={draftClientId}\n        >\n          <span className=\"selection-value\">\n            {selectionValue}\n          </span>\n        </MailMergeToken>\n      </span>\n    )\n  }\n}\nexport default ListensToMailMergeSession(MailMergeBodyToken)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-button.jsx",
    "content": "import classnames from 'classnames'\nimport React, {PropTypes} from 'react'\nimport {RetinaImg} from 'nylas-component-kit'\nimport ListensToMailMergeSession from './listens-to-mail-merge-session'\n\n\nfunction MailMergeButton(props) {\n  if (props.draft.replyToMessageId) {\n    return <span />;\n  }\n\n  const {mailMergeSession, sessionState} = props\n  const {isWorkspaceOpen} = sessionState\n  const classes = classnames({\n    \"btn\": true,\n    \"btn-toolbar\": true,\n    \"btn-enabled\": isWorkspaceOpen,\n    \"btn-mail-merge\": true,\n  })\n\n  return (\n    <button\n      className={classes}\n      title=\"Mass Email\"\n      onClick={mailMergeSession.toggleWorkspace}\n      tabIndex={-1}\n      style={{order: -99}}\n    >\n      <RetinaImg\n        name=\"icon-composer-mailmerge.png\"\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n    </button>\n  )\n}\nMailMergeButton.displayName = 'MailMergeButton'\nMailMergeButton.containerRequired = false\nMailMergeButton.propTypes = {\n  draft: PropTypes.object,\n  session: PropTypes.object,\n  sessionState: PropTypes.object,\n  draftClientId: PropTypes.string,\n  mailMergeSession: PropTypes.object,\n}\n\nexport default ListensToMailMergeSession(MailMergeButton)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-composer-extension.es6",
    "content": "import * as Handlers from './mail-merge-token-dnd-handlers'\n\nexport const name = 'MailMergeComposerExtension'\n\nexport {\n  onDragOver,\n  shouldAcceptDrop,\n} from './mail-merge-token-dnd-handlers'\n\nexport const onDrop = Handlers.onDrop.bind(null, 'body')\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-constants.es6",
    "content": "import plugin from '../package.json'\n\nexport const PLUGIN_ID = plugin.name;\nexport const PLUGIN_NAME = \"Mail Merge\"\nexport const DEBUG = false\nexport const MAX_ROWS = 150\n\nexport const ParticipantFields = ['to', 'cc', 'bcc']\nexport const ContenteditableFields = ['subject', 'body']\nexport const LinkableFields = [...ParticipantFields, ...ContenteditableFields]\n\nexport const DataTransferTypes = {\n  ColIdx: 'mail-merge:col-idx',\n  ColName: 'mail-merge:col-name',\n  DraftId: 'mail-merge:draft-client-id',\n  DragBehavior: 'mail-merge:drag-behavior',\n}\n\nexport const DragBehaviors = {\n  Copy: 'copy',\n  Move: 'move',\n}\n\nexport const ActionNames = [\n  'addColumn',\n  'removeLastColumn',\n  'addRow',\n  'removeRow',\n  'updateCell',\n  'shiftSelection',\n  'setSelection',\n  'clearTableData',\n  'loadTableData',\n  'toggleWorkspace',\n  'linkToDraft',\n  'unlinkFromDraft',\n]\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-container.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport MailMergeWorkspace from './mail-merge-workspace'\nimport ListensToMailMergeSession from './listens-to-mail-merge-session'\n\n\nclass MailMergeContainer extends Component {\n  static displayName = 'MailMergeContainer'\n\n  static containerRequired = false\n\n  static propTypes = {\n    session: PropTypes.object,\n    sessionState: PropTypes.object,\n    draftClientId: PropTypes.string,\n    mailMergeSession: PropTypes.object,\n  }\n\n  shouldComponentUpdate(nextProps) {\n    // Make sure we only update if new state has been set\n    // We do not care about our other props\n    return (\n      this.props.draftClientId !== nextProps.draftClientId ||\n      this.props.sessionState !== nextProps.sessionState\n    )\n  }\n\n  render() {\n    const {draftClientId, sessionState, mailMergeSession} = this.props\n    return (\n      <MailMergeWorkspace\n        {...sessionState}\n        session={mailMergeSession}\n        draftClientId={draftClientId}\n      />\n    )\n  }\n}\n\nexport default ListensToMailMergeSession(MailMergeContainer)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-draft-editing-session.es6",
    "content": "import NylasStore from 'nylas-store'\nimport * as TableStateReducers from './table-state-reducers'\nimport * as TokenStateReducers from './token-state-reducers'\nimport * as SelectionStateReducers from './selection-state-reducers'\nimport * as WorkspaceStateReducers from './workspace-state-reducers'\nimport {ActionNames, PLUGIN_ID, DEBUG} from './mail-merge-constants'\n\n\nconst sessions = new Map()\n\nfunction computeNextState({name, args = []}, previousState = {}, reducers = []) {\n  if (reducers.length === 0) {\n    return previousState\n  }\n  return reducers.reduce((state, reducer) => {\n    if (reducer[name]) {\n      const reduced = reducer[name](previousState, ...args)\n      return {...state, ...reduced}\n    }\n    return state\n  }, previousState)\n}\n\n/**\n * MailMergeDraftEditingSession instances hold the entire state for the Mail Merge\n * plugin for a given draft, as a single state tree. Sessions trigger when any changes\n * on the state tree occur.\n *\n * Mail Merge state for a draft can be modified by dispatching actions on a session instance.\n * Available actions are defined by `MailMergeConstants.ActionNames`.\n * Actions are dispatched by calling the action on a session as a method:\n * ```\n *  session.addColumn()\n * ```\n *\n * Internally, the session acts as a Proxy which forwards action calls into any\n * registered reducers, and merges the resulting state from calling the action\n * on each reducer to compute the new state tree. Registered reducers are\n * currently hardcoded in this class.\n *\n * A session instance also acts as a proxy for the corresponding `DraftEditingSession`,\n * instance, and forwards to it any changes that need to be persisted on the draft object\n *\n * @class MailMergeDraftEditingSession\n */\nexport class MailMergeDraftEditingSession extends NylasStore {\n\n  constructor(session, reducers) {\n    super()\n    this._session = session\n    this._reducers = reducers || [\n      TableStateReducers,\n      TokenStateReducers,\n      SelectionStateReducers,\n      WorkspaceStateReducers,\n    ]\n    this._state = {}\n    this.initializeState()\n    this.initializeActionHandlers()\n  }\n\n  get state() {\n    return this._state\n  }\n\n  draft() {\n    return this._session.draft()\n  }\n\n  draftSession() {\n    return this._session\n  }\n\n  initializeState(draft = this._session.draft()) {\n    const savedMetadata = draft.metadataForPluginId(PLUGIN_ID)\n    const shouldLoadSavedData = (\n      savedMetadata &&\n      savedMetadata.tableDataSource &&\n      savedMetadata.tokenDataSource\n    )\n    const action = {name: 'initialState'}\n    if (shouldLoadSavedData) {\n      const loadedState = this.dispatch({name: 'fromJSON'}, savedMetadata)\n      this._state = this.dispatch(action, loadedState)\n    } else {\n      this._state = this.dispatch(action)\n    }\n  }\n\n  initializeActionHandlers() {\n    ActionNames.forEach((actionName) => {\n      // TODO ES6 Proxies would be nice here\n      this[actionName] = this.actionHandler(actionName).bind(this)\n    })\n  }\n\n  dispatch(action, prevState = this._state) {\n    const nextState = computeNextState(action, prevState, this._reducers)\n    if (DEBUG && action.debug !== false) {\n      console.log('--> action', action.name)\n      console.dir(action)\n      console.log('--> prev state')\n      console.dir(prevState)\n      console.log('--> new state')\n      console.dir(nextState)\n    }\n    return nextState\n  }\n\n  actionHandler(actionName) {\n    return (...args) => {\n      this._state = this.dispatch({name: actionName, args})\n\n      // Defer calling `saveToSession` to make sure our state changes are triggered\n      // before the draft changes\n      this.trigger()\n      setImmediate(this.saveToDraftSession)\n    }\n  }\n\n  saveToDraftSession = () => {\n    // TODO\n    // - What should we save in metadata?\n    //   - The entire table data?\n    //   - A reference to a statically hosted file?\n    //   - Attach csv as a file to the \"base\" or \"template\" draft?\n    const {tokenDataSource, tableDataSource} = this._state\n    const draftChanges = this.dispatch({name: 'toDraftChanges', args: [this._state], debug: false}, this.draft())\n    const serializedState = this.dispatch({name: 'toJSON', debug: false}, {tokenDataSource, tableDataSource})\n\n    this._session.changes.add(draftChanges)\n    this._session.changes.addPluginMetadata(PLUGIN_ID, serializedState)\n  }\n}\n\nexport function mailMergeSessionForDraft(draftId, draftSession) {\n  if (sessions.has(draftId)) {\n    return sessions.get(draftId)\n  }\n  if (!draftSession) {\n    return null\n  }\n  const sess = new MailMergeDraftEditingSession(draftSession)\n  sessions.set(draftId, sess)\n  return sess\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-header-input.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {pickHTMLProps} from 'pick-react-known-prop'\nimport MailMergeToken from './mail-merge-token'\n\n\nfunction getInputSize(value) {\n  return ((value || '').length || 1) + 1\n}\n\nclass MailMergeHeaderInput extends Component {\n\n  static propTypes = {\n    draftClientId: PropTypes.string,\n    colIdx: PropTypes.any,\n    tableDataSource: PropTypes.object,\n    defaultValue: PropTypes.string,\n    onBlur: PropTypes.func,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {inputSize: getInputSize(props.defaultValue)}\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState({inputSize: getInputSize(nextProps.defaultValue)})\n  }\n\n  onInputBlur = (event) => {\n    const {target: {value}} = event\n    this.setState({inputSize: getInputSize(value)})\n    // Can't override the original onBlur handler\n    this.props.onBlur(event)\n  }\n\n  onInputChange = (event) => {\n    const {target: {value}} = event\n    this.setState({inputSize: getInputSize(value)})\n  }\n\n  render() {\n    const {inputSize} = this.state\n    const {draftClientId, tableDataSource, colIdx, ...props} = this.props\n    const colName = tableDataSource.colAt(colIdx)\n\n    return (\n      <div className=\"header-cell\">\n        <MailMergeToken\n          draggable\n          colIdx={colIdx}\n          colName={colName}\n          draftClientId={draftClientId}\n        >\n          <input\n            {...pickHTMLProps(props)}\n            size={inputSize}\n            onBlur={this.onInputBlur}\n            onChange={this.onInputChange}\n            defaultValue={props.defaultValue}\n          />\n        </MailMergeToken>\n      </div>\n    )\n  }\n}\n\nexport default MailMergeHeaderInput\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-participants-text-field.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport classnames from 'classnames'\nimport {DropZone, TokenizingTextField} from 'nylas-component-kit'\nimport MailMergeToken from './mail-merge-token'\nimport {DataTransferTypes} from './mail-merge-constants'\nimport ListensToMailMergeSession from './listens-to-mail-merge-session'\n\n\nfunction MailMergeParticipantToken(props) {\n  const {token: {tableDataSource, rowIdx, colIdx, colName}} = props\n  const selectionValue = tableDataSource.cellAt({rowIdx, colIdx}) || 'No value selected'\n\n  return (\n    <MailMergeToken draggable colIdx={colIdx} colName={colName}>\n      <span>{selectionValue}</span>\n    </MailMergeToken>\n  )\n}\nMailMergeParticipantToken.propTypes = {\n  token: PropTypes.shape({\n    colIdx: PropTypes.any,\n    rowIdx: PropTypes.any,\n    tableDataSource: PropTypes.object,\n  }),\n}\n\n\nclass MailMergeParticipantsTextField extends Component {\n  static displayName = 'MailMergeParticipantsTextField'\n\n  static containerRequired = false\n\n  static propTypes = {\n    onAdd: PropTypes.func,\n    onRemove: PropTypes.func,\n    field: PropTypes.string,\n    session: PropTypes.object,\n    className: PropTypes.string,\n    sessionState: PropTypes.object,\n    draftClientId: PropTypes.string,\n    mailMergeSession: PropTypes.object,\n  }\n\n  static defaultProps = {\n    className: '',\n  }\n\n  constructor(props) {\n    super(props)\n    this._tokenWasMovedBetweenFields = false\n  }\n\n  // This is called by the TokenizingTextField when a token is dragged and dropped\n  // between fields\n  onAddToken = (...args) => {\n    const tokenToAdd = args[0][0]\n    if (args.length > 1 || !tokenToAdd) { return }\n\n    const {mailMergeSession} = this.props\n    const {colIdx, colName, tokenId, field} = tokenToAdd\n    // Remove from previous field\n    mailMergeSession.unlinkFromDraft({field, tokenId})\n    // Add to our current field\n    mailMergeSession.linkToDraft({colIdx, colName, field: this.props.field})\n    this._tokenWasMovedBetweenFields = true\n  }\n\n  onRemoveToken = ([tokenToDelete]) => {\n    const {field, mailMergeSession} = this.props\n    const {tokenId} = tokenToDelete\n    mailMergeSession.unlinkFromDraft({field, tokenId})\n  }\n\n  onDrop = (event) => {\n    if (this._tokenWasMovedBetweenFields) {\n      // Ignore drop if we already added the token\n      this._tokenWasMovedBetweenFields = false\n      return\n    }\n    const {dataTransfer} = event\n    const {field, mailMergeSession} = this.props\n    const colIdx = dataTransfer.getData(DataTransferTypes.ColIdx)\n    const colName = dataTransfer.getData(DataTransferTypes.ColName)\n    mailMergeSession.linkToDraft({colIdx, colName, field})\n  }\n\n  focus() {\n    this.refs.textField.focus()\n  }\n\n  shouldAcceptDrop = (event) => {\n    const {dataTransfer} = event\n    return !!dataTransfer.getData(DataTransferTypes.ColIdx)\n  }\n\n  render() {\n    const {field, className, sessionState} = this.props\n    const {isWorkspaceOpen, tableDataSource, selection, tokenDataSource} = sessionState\n\n    if (!isWorkspaceOpen) {\n      return <TokenizingTextField ref=\"textField\" {...this.props} />\n    }\n\n    const classes = classnames({\n      'mail-merge-participants-text-field': true,\n      [className]: true,\n    })\n    const tokens = (\n      tokenDataSource.tokensForField(field)\n      .map((token) => ({...token, tableDataSource, rowIdx: selection.rowIdx}))\n    )\n\n    return (\n      <DropZone\n        onDrop={this.onDrop}\n        shouldAcceptDrop={this.shouldAcceptDrop}\n      >\n        <TokenizingTextField\n          {...this.props}\n          ref=\"textField\"\n          className={classes}\n          tokens={tokens}\n          tokenKey={(token) => token.tokenId}\n          tokenRenderer={MailMergeParticipantToken}\n          tokenIsValid={() => true}\n          tokenClassNames={(token) => `token-color-${token.colIdx % 5}`}\n          onRequestCompletions={() => []}\n          completionNode={() => <span />}\n          onAdd={this.onAddToken}\n          onRemove={this.onRemoveToken}\n          onTokenAction={false}\n        />\n      </DropZone>\n    )\n  }\n}\n\nexport default ListensToMailMergeSession(MailMergeParticipantsTextField)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-send-button.jsx",
    "content": "import {remote} from 'electron'\nimport React, {Component, PropTypes} from 'react'\nimport {RetinaImg} from 'nylas-component-kit'\nimport {sendMailMerge} from './mail-merge-utils'\nimport ListensToMailMergeSession from './listens-to-mail-merge-session'\n\n\nclass MailMergeSendButton extends Component {\n  static displayName = 'MailMergeSendButton'\n\n  static containerRequired = false\n\n  static propTypes = {\n    draft: PropTypes.object,\n    session: PropTypes.object,\n    sessionState: PropTypes.object,\n    isValidDraft: PropTypes.func,\n    fallback: PropTypes.func,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      sending: false,\n    }\n  }\n\n  onClick = () => {\n    const {sending} = this.state\n    if (sending) { return }\n\n    const {draft, isValidDraft} = this.props\n    if (draft.to.length === 0) {\n      const dialog = remote.dialog;\n      dialog.showMessageBox(remote.getCurrentWindow(), {\n        type: 'warning',\n        buttons: ['Edit Message', 'Cancel'],\n        message: 'Cannot Send',\n        detail: \"Before sending, you need to drag the header cell of the column of emails to the To field in Recipients\",\n      });\n    } else {\n      if (isValidDraft()) {\n        this.setState({sending: true})\n        try {\n          sendMailMerge(draft.clientId)\n        } catch (e) {\n          this.setState({sending: false})\n          NylasEnv.showErrorDialog(e.message)\n        }\n      }\n    }\n  }\n\n  primarySend() {\n    // Primary click is called when mod+enter is pressed.\n    // If mail merge is not open, we should revert to default behavior\n    const {isWorkspaceOpen} = this.props.sessionState\n    if (!isWorkspaceOpen && this.refs.fallbackButton) {\n      this.refs.fallbackButton.primarySend()\n    } else {\n      this.onClick()\n    }\n  }\n\n  render() {\n    const {sending} = this.state\n    const {isWorkspaceOpen, tableDataSource} = this.props.sessionState\n    if (!isWorkspaceOpen) {\n      const Fallback = this.props.fallback\n      return <Fallback ref=\"fallbackButton\" {...this.props} />\n    }\n\n    const count = tableDataSource.rows().length\n    const action = sending ? 'Sending' : 'Send'\n    const sendLabel = count > 1 ? `${action} ${count} messages` : `${action} ${count} message`;\n    let classes = \"btn btn-toolbar btn-normal btn-emphasis btn-text btn-send\"\n    if (sending) {\n      classes += \" btn-disabled\"\n    }\n    return (\n      <button\n        tabIndex={-1}\n        className={classes}\n        style={{order: -100}}\n        onClick={this.onClick}\n      >\n        <span>\n          <RetinaImg\n            name=\"icon-composer-send.png\"\n            mode={RetinaImg.Mode.ContentIsMask}\n          />\n          <span className=\"text\">{sendLabel}</span>\n        </span>\n      </button>\n    );\n  }\n}\n\n// TODO this is a hack so that the mail merge send button can still expose\n// the `primarySend` method required by the ComposerView. Ideally, this\n// decorator mechanism should expose whatever instance methods are exposed\n// by the component its wrapping.\n// However, I think the better fix will happen when mail merge lives in its\n// own window and doesn't need to override the Composer's send button, which\n// is already a bit of a hack.\nconst EnhancedMailMergeSendButton = ListensToMailMergeSession(MailMergeSendButton)\nObject.assign(EnhancedMailMergeSendButton.prototype, {\n  primarySend() {\n    if (this.refs.composed) {\n      this.refs.composed.primarySend()\n    }\n  },\n})\n\nexport default EnhancedMailMergeSendButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-subject-text-field.jsx",
    "content": "/* eslint react/no-danger: 0 */\nimport React, {Component, PropTypes} from 'react'\nimport {findDOMNode} from 'react-dom'\nimport {EditorAPI} from 'nylas-exports'\nimport {OverlaidComponents, DropZone} from 'nylas-component-kit'\nimport ListensToMailMergeSession from './listens-to-mail-merge-session'\nimport * as Handlers from './mail-merge-token-dnd-handlers'\n\n\nclass MailMergeSubjectTextField extends Component {\n  static displayName = 'MailMergeSubjectTextField'\n\n  static containerRequired = false\n\n  static propTypes = {\n    value: PropTypes.string,\n    fallback: PropTypes.func,\n    draft: PropTypes.object,\n    session: PropTypes.object,\n    sessionState: PropTypes.object,\n    draftClientId: PropTypes.string,\n    onSubjectChange: PropTypes.func.isRequired,\n  }\n\n  componentDidMount() {\n    const {isWorkspaceOpen} = this.props.sessionState\n\n    this.savedSelection = null\n    if (isWorkspaceOpen) {\n      this.editor = new EditorAPI(findDOMNode(this.refs.contenteditable))\n    }\n  }\n\n  shouldComponentUpdate(nextProps) {\n    return (\n      this.props.draftClientId !== nextProps.draftClientId ||\n      this.props.value !== nextProps.value ||\n      this.props.sessionState.isWorkspaceOpen !== nextProps.sessionState.isWorkspaceOpen\n    )\n  }\n\n  componentDidUpdate() {\n    const {isWorkspaceOpen} = this.props.sessionState\n\n    if (isWorkspaceOpen) {\n      this.editor = new EditorAPI(findDOMNode(this.refs.contenteditable))\n      if (this.savedSelection && this.savedSelection.rawSelection.anchorNode) {\n        this.editor.select(this.savedSelection)\n        this.savedSelection = null\n      }\n    }\n  }\n\n  onInputChange = (event) => {\n    const value = event.target.innerHTML\n    this.savedSelection = this.editor.currentSelection().exportSelection()\n    this.props.onSubjectChange(value)\n  }\n\n  onInputKeyDown = (event) => {\n    if (['Enter', 'Return'].includes(event.key)) {\n      event.stopPropagation()\n      event.preventDefault()\n    }\n  }\n\n  onDrop = (event) => {\n    Handlers.onDrop('subject', {editor: this.editor, event})\n  }\n\n  onDragOver = (event) => {\n    Handlers.onDragOver({editor: this.editor, event})\n  }\n\n  shouldAcceptDrop = (event) => {\n    return Handlers.shouldAcceptDrop({event})\n  }\n\n  focus() {\n    const {isWorkspaceOpen} = this.props.sessionState\n\n    if (isWorkspaceOpen) {\n      findDOMNode(this.refs.contenteditable).focus()\n    } else {\n      this.refs.fallback.focus()\n    }\n  }\n\n  renderContenteditable() {\n    const {value} = this.props\n    return (\n      <DropZone\n        className=\"mail-merge-subject-text-field composer-subject subject-field\"\n        onDrop={this.onDrop}\n        onDragOver={this.onDragOver}\n        shouldAcceptDrop={this.shouldAcceptDrop}\n      >\n        <div\n          ref=\"contenteditable\"\n          contentEditable\n          name=\"subject\"\n          placeholder=\"Subject\"\n          onBlur={this.onInputChange}\n          onInput={this.onInputChange}\n          onKeyDown={this.onInputKeyDown}\n          dangerouslySetInnerHTML={{__html: value}}\n        />\n      </DropZone>\n    )\n  }\n\n  render() {\n    const {isWorkspaceOpen} = this.props.sessionState\n    if (!isWorkspaceOpen) {\n      const Fallback = this.props.fallback\n      return <Fallback ref=\"fallback\" {...this.props} />\n    }\n\n    const {draft, session} = this.props\n    const exposedProps = {draft, session}\n    return (\n      <OverlaidComponents\n        className=\"mail-merge-subject-overlaid\"\n        exposedProps={exposedProps}\n      >\n        {this.renderContenteditable()}\n      </OverlaidComponents>\n    )\n  }\n}\n\nexport default ListensToMailMergeSession(MailMergeSubjectTextField)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-table.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport {EditableTable} from 'nylas-component-kit'\nimport {pickHTMLProps} from 'pick-react-known-prop'\nimport MailMergeHeaderInput from './mail-merge-header-input'\n\n\nfunction InputRenderer(props) {\n  const {isHeader, draftClientId} = props;\n  if (!isHeader) {\n    return <input {...pickHTMLProps(props)} defaultValue={props.defaultValue} />\n  }\n  return <MailMergeHeaderInput draftClientId={draftClientId} {...props} />\n}\nInputRenderer.propTypes = {\n  isHeader: PropTypes.bool,\n  defaultValue: PropTypes.string,\n  draftClientId: PropTypes.string,\n}\n\nfunction MailMergeTable(props) {\n  const {draftClientId} = props\n  return (\n    <div className=\"mail-merge-table\">\n      <EditableTable\n        {...props}\n        displayHeader\n        displayNumbers\n        rowHeight={30}\n        bodyHeight={150}\n        inputProps={{draftClientId}}\n        InputRenderer={InputRenderer}\n      />\n    </div>\n  )\n}\nMailMergeTable.propTypes = {\n  tableDataSource: EditableTable.propTypes.tableDataSource,\n  selection: PropTypes.object,\n  draftClientId: PropTypes.string,\n  onShiftSelection: PropTypes.func,\n}\n\nexport default MailMergeTable\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-token-dnd-handlers.es6",
    "content": "import {Utils} from 'nylas-exports'\nimport {mailMergeSessionForDraft} from './mail-merge-draft-editing-session'\nimport {DataTransferTypes, DragBehaviors} from './mail-merge-constants'\n\n\nfunction updateCursorPosition({editor, event}) {\n  const {clientX, clientY} = event\n  const range = document.caretRangeFromPoint(clientX, clientY);\n  range.collapse()\n  editor.select(range)\n  return range\n}\n\nexport function shouldAcceptDrop({event}) {\n  const {dataTransfer} = event;\n  return !!dataTransfer.getData(DataTransferTypes.ColIdx);\n}\n\nexport function onDragOver({editor, event}) {\n  updateCursorPosition({editor, event})\n}\n\nexport function onDrop(field, {editor, event}) {\n  const {dataTransfer} = event\n  const colIdx = dataTransfer.getData(DataTransferTypes.ColIdx)\n  const colName = dataTransfer.getData(DataTransferTypes.ColName)\n  const dragBehavior = dataTransfer.getData(DataTransferTypes.DragBehavior)\n  const draftClientId = dataTransfer.getData(DataTransferTypes.DraftId)\n  const mailMergeSession = mailMergeSessionForDraft(draftClientId)\n  if (!mailMergeSession) {\n    return\n  }\n\n  if (dragBehavior === DragBehaviors.Move) {\n    const {tokenDataSource} = mailMergeSession.state\n    const {tokenId} = tokenDataSource.findTokens(field, {colName, colIdx}).pop() || {}\n    editor.removeCustomComponentByAnchorId(tokenId)\n  }\n\n  updateCursorPosition({editor, event})\n  const tokenId = Utils.generateTempId()\n  editor.insertCustomComponent('MailMergeBodyToken', {\n    field,\n    colIdx,\n    colName,\n    tokenId,\n    draftClientId,\n    anchorId: tokenId,\n    className: 'mail-merge-token-wrap',\n  })\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-token.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport classnames from 'classnames'\nimport {RetinaImg} from 'nylas-component-kit'\nimport {DataTransferTypes, DragBehaviors} from './mail-merge-constants'\n\n\nfunction onDragStart(event, {draftClientId, colIdx, colName, dragBehavior}) {\n  const {dataTransfer} = event\n  dataTransfer.effectAllowed = 'move'\n  dataTransfer.setData(DataTransferTypes.DraftId, draftClientId)\n  dataTransfer.setData(DataTransferTypes.ColIdx, colIdx)\n  dataTransfer.setData(DataTransferTypes.ColName, colName)\n  dataTransfer.setData(DataTransferTypes.DragBehavior, dragBehavior)\n}\n\nfunction MailMergeToken(props) {\n  const {draftClientId, colIdx, colName, children, draggable, dragBehavior} = props\n  const classes = classnames({\n    'mail-merge-token': true,\n    [`token-color-${colIdx % 5}`]: true,\n  })\n  const _onDragStart = event => onDragStart(event, {draftClientId, colIdx, colName, dragBehavior})\n  const dragHandle = draggable ? <RetinaImg name=\"mailmerge-grabber.png\" mode={RetinaImg.Mode.ContentIsMask} /> : null;\n\n  return (\n    <span draggable={draggable} className={classes} onDragStart={_onDragStart}>\n      {dragHandle}\n      {children}\n    </span>\n  )\n}\nMailMergeToken.propTypes = {\n  draftClientId: PropTypes.string,\n  colIdx: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),\n  colName: PropTypes.string,\n  children: PropTypes.node,\n  draggable: PropTypes.bool,\n  dragBehavior: PropTypes.string,\n}\n\nMailMergeToken.defaultProps = {\n  draggable: false,\n  dragBehavior: DragBehaviors.Copy,\n}\n\nexport default MailMergeToken\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-utils.es6",
    "content": "import Papa from 'papaparse'\nimport {\n  Utils,\n  Actions,\n  Contact,\n  RegExpUtils,\n  DraftHelpers,\n  DatabaseStore,\n  SoundRegistry,\n} from 'nylas-exports'\n\nimport {PLUGIN_ID, MAX_ROWS, DataTransferTypes, ParticipantFields} from './mail-merge-constants'\nimport {mailMergeSessionForDraft} from './mail-merge-draft-editing-session'\nimport SendManyDraftsTask from './send-many-drafts-task'\n\n\nexport function contactFromColIdx(colIdx, email) {\n  return new Contact({\n    name: email || '',\n    email: email || 'No value selected',\n    clientId: `${DataTransferTypes.ColIdx}:${colIdx}`,\n  })\n}\n\nexport function colIdxFromContact(contact) {\n  const {clientId} = contact\n  if (!clientId.startsWith(DataTransferTypes.ColIdx)) {\n    return null\n  }\n  return contact.clientId.split(':')[2]\n}\n\nexport function tokenQuerySelector(tokenId) {\n  if (!tokenId) {\n    return `img.mail-merge-token-wrap`\n  }\n  return `img.mail-merge-token-wrap[data-overlay-id=\"${tokenId}\"]`\n}\n\nexport function tokenRegex(tokenId) {\n  if (!tokenId) {\n    // https://regex101.com/r/sU7sO6/1\n    return /<img[^>]*?class=\"[^>]*?mail-merge-token-wrap[^>]*?\"[^>]*?>/gim\n  }\n  // https://regex101.com/r/fJ5eN6/5\n  const reStr = `<img[^>]*?class=\"[^>]*?mail-merge-token-wrap[^>]*?\" [^>]*?data-overlay-id=\"${tokenId}\"[^>]*?>`\n  return new RegExp(reStr, 'gim')\n}\n\nfunction replaceContenteditableTokens(html, {field, tableDataSource, tokenDataSource, rowIdx}) {\n  const replaced = tokenDataSource.tokensForField(field)\n  .reduce((currentHtml, {colIdx, tokenId}) => {\n    const fieldValue = tableDataSource.cellAt({rowIdx, colIdx}) || \"\"\n    const markup = `<span>${fieldValue}</span>`\n    return currentHtml.replace(tokenRegex(tokenId), markup)\n  }, html)\n  if (tokenRegex().test(replaced)) {\n    throw new Error(`Field ${field} still contains tokens after attempting to replace for table values`)\n  }\n  return replaced\n}\n\nexport function buildDraft(baseDraft, {tableDataSource, tokenDataSource, rowIdx}) {\n  if (tableDataSource.isEmpty({rowIdx})) {\n    return null\n  }\n  const draftToSend = baseDraft.clone()\n  draftToSend.clientId = Utils.generateTempId()\n\n  // Clear any previous mail merge metadata on the draft we are going to send\n  // and add rowIdx\n  draftToSend.applyPluginMetadata(PLUGIN_ID, {rowIdx})\n\n  // Replace tokens inside subject with values from table data\n  const draftSubject = replaceContenteditableTokens(draftToSend.subject, {\n    field: 'subject',\n    rowIdx,\n    tokenDataSource,\n    tableDataSource,\n  })\n  draftToSend.subject = Utils.extractTextFromHtml(draftSubject)\n\n  // Replace tokens inside body with values from table data\n  draftToSend.body = replaceContenteditableTokens(draftToSend.body, {\n    field: 'body',\n    rowIdx,\n    tokenDataSource,\n    tableDataSource,\n  })\n\n  // Update participant values\n  ParticipantFields.forEach((field) => {\n    draftToSend[field] = tokenDataSource.tokensForField(field).map(({colIdx}) => {\n      const column = tableDataSource.colAt(colIdx)\n      const value = (tableDataSource.cellAt({rowIdx, colIdx}) || \"\").trim()\n      const contact = new Contact({accountId: baseDraft.accountId, name: value, email: value})\n      if (!contact.isValid()) {\n        throw new Error(`Can't send messages:\\nThe column ${column} contains an invalid email address at row ${rowIdx + 1}: \"${value}\"`)\n      }\n      return contact\n    })\n  })\n  return draftToSend\n}\n\nexport function sendManyDrafts(mailMergeSession, recipientDrafts) {\n  const transformedDrafts = [];\n\n  return mailMergeSession.draftSession().ensureCorrectAccount()\n  .then(() => {\n    const baseDraft = mailMergeSession.draft();\n    return Promise.each(recipientDrafts, (recipientDraft) => {\n      recipientDraft.accountId = baseDraft.accountId;\n      recipientDraft.serverId = null;\n      return DraftHelpers.applyExtensionTransforms(recipientDraft).then((transformed) =>\n        transformedDrafts.push(transformed)\n      );\n    });\n  })\n  .then(() =>\n    DatabaseStore.inTransaction(t => t.persistModels(transformedDrafts))\n  )\n  .then(async () => {\n    const baseDraft = mailMergeSession.draft();\n\n    if (baseDraft.uploads.length > 0) {\n      recipientDrafts.forEach(async (d) => {\n        await DraftHelpers.removeStaleUploads(d);\n      })\n    }\n\n    const recipientClientIds = recipientDrafts.map(d => d.clientId)\n\n    Actions.queueTask(new SendManyDraftsTask(baseDraft.clientId, recipientClientIds))\n\n    if (NylasEnv.config.get(\"core.sending.sounds\")) {\n      SoundRegistry.playSound('hit-send');\n    }\n    NylasEnv.close();\n  })\n}\n\nexport function sendMailMerge(draftClientId) {\n  const mailMergeSession = mailMergeSessionForDraft(draftClientId)\n  if (!mailMergeSession) { return }\n\n  const baseDraft = mailMergeSession.draft()\n  const {tableDataSource, tokenDataSource} = mailMergeSession.state\n\n  const recipientDrafts = tableDataSource.rows()\n    .map((row, rowIdx) => (\n      buildDraft(baseDraft, {tableDataSource, tokenDataSource, rowIdx})\n    ))\n    .filter((draft) => draft != null)\n\n  if (recipientDrafts.length === 0) {\n    NylasEnv.showErrorDialog(`There are no drafts to send! Add add some data to the table below`)\n    return\n  }\n  sendManyDrafts(mailMergeSession, recipientDrafts)\n}\n\nexport function parseCSV(file, maxRows = MAX_ROWS) {\n  return new Promise((resolve, reject) => {\n    Papa.parse(file, {\n      skipEmptyLines: true,\n      complete: ({data}) => {\n        if (data.length === 0) {\n          NylasEnv.showErrorDialog(\n            `The csv file you are trying to import contains no rows. Please select another file.`\n          );\n          resolve(null)\n          return;\n        }\n\n        // If a cell in the first row contains a valid email address, assume that\n        // the table has no headers. We need row[0] to be field names, so make some up!\n        const emailRegexp = RegExpUtils.emailRegex();\n        const emailInFirstRow = data[0].find((val) => emailRegexp.test(val));\n        if (emailInFirstRow) {\n          const headers = data[0].map((val, idx) => {\n            return emailInFirstRow === val ? 'Email Address' : `Column ${idx}`\n          })\n          data.unshift(headers);\n        }\n\n        const columns = data[0].slice()\n        const rows = data.slice(1)\n        if (rows.length > maxRows) {\n          NylasEnv.showErrorDialog(\n            `The csv file you are trying to import contains more than the max allowed number of rows (${maxRows}).\\nWe have only imported the first ${maxRows} rows`\n          );\n          resolve({columns, rows: rows.slice(0, maxRows)})\n          return\n        }\n        resolve({columns, rows})\n      },\n      error: (error) => {\n        NylasEnv.showErrorDialog(`Sorry, we were unable to parse the file: ${file.name}\\n${error.message}`);\n        reject(error)\n      },\n    })\n  })\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/mail-merge-workspace.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {RetinaImg, DropZone} from 'nylas-component-kit'\nimport fs from 'fs';\n\nimport {parseCSV} from './mail-merge-utils'\nimport MailMergeTable from './mail-merge-table'\n\n\nclass MailMergeWorkspace extends Component {\n  static displayName = 'MailMergeWorkspace'\n\n  static propTypes = {\n    isWorkspaceOpen: PropTypes.bool,\n    tableDataSource: MailMergeTable.propTypes.tableDataSource,\n    selection: PropTypes.object,\n    draftClientId: PropTypes.string,\n    session: PropTypes.object,\n  }\n\n  constructor() {\n    super()\n    this.state = {isDropping: false}\n  }\n\n  onDragStateChange = ({isDropping}) => {\n    this.setState({isDropping})\n  }\n\n  onChooseCSV = () => {\n    NylasEnv.showOpenDialog({\n      properties: ['openFile'],\n      filters: [\n        { name: 'CSV Files', extensions: ['csv', 'txt'] },\n      ],\n    }, (pathsToOpen) => {\n      if (!pathsToOpen || pathsToOpen.length === 0) {\n        return;\n      }\n\n      fs.readFile(pathsToOpen[0], (err, contents) => {\n        parseCSV(contents.toString()).then((tableData) => {\n          this.loadCSV(tableData)\n        });\n      });\n    });\n  }\n\n  onDropCSV = (event) => {\n    event.stopPropagation()\n    const {dataTransfer} = event\n    const file = dataTransfer.files[0]\n    parseCSV(file)\n    .then(tableData => this.loadCSV(tableData))\n  }\n\n  loadCSV(newTableData) {\n    const {tableDataSource, session} = this.props\n    // TODO We need to reset the table values first because `EditableTable` does\n    // not support controlled inputs, i.e. the inputs just use the\n    // defaultValue props which will only apply when the input is empty\n    session.clearTableData()\n    session.loadTableData({newTableData, prevColumns: tableDataSource.columns()})\n  }\n\n  shouldAcceptDrop = (event) => {\n    event.stopPropagation()\n    const {dataTransfer} = event\n    if (dataTransfer.files.length === 1) {\n      const file = dataTransfer.files[0]\n      if (file.type === 'text/csv') {\n        return true\n      }\n    }\n    return false\n  }\n\n  renderSelectionControls() {\n    const {selection, tableDataSource, session} = this.props\n    const rows = tableDataSource.rows()\n    return (\n      <div className=\"selection-controls\">\n        <div className=\"btn btn-group\">\n          <div\n            className=\"btn-prev\"\n            onClick={() => session.shiftSelection({row: -1})}\n          >\n            <RetinaImg\n              name=\"toolbar-dropdown-chevron.png\"\n              mode={RetinaImg.Mode.ContentIsMask}\n            />\n          </div>\n          <div\n            className=\"btn-next\"\n            onClick={() => session.shiftSelection({row: 1})}\n          >\n            <RetinaImg\n              name=\"toolbar-dropdown-chevron.png\"\n              mode={RetinaImg.Mode.ContentIsMask}\n            />\n          </div>\n        </div>\n        <span>Recipient {selection.rowIdx + 1} of {rows.length}</span>\n        <span style={{flex: 1}} />\n        <div className=\"btn\" onClick={this.onChooseCSV}>\n          Import CSV\n        </div>\n      </div>\n    )\n  }\n\n  renderDropCover() {\n    const {isDropping} = this.state\n    const display = isDropping ? 'block' : 'none';\n    return (\n      <div className=\"composer-drop-cover\" style={{display}}>\n        <div className=\"centered\">\n          Drop to Import CSV\n        </div>\n      </div>\n    )\n  }\n\n  render() {\n    const {session, draftClientId, isWorkspaceOpen, tableDataSource, selection, ...otherProps} = this.props\n    if (!isWorkspaceOpen) {\n      return false\n    }\n\n    return (\n      <DropZone\n        className=\"mail-merge-workspace\"\n        onDrop={this.onDropCSV}\n        shouldAcceptDrop={this.shouldAcceptDrop}\n        onDragStateChange={this.onDragStateChange}\n      >\n        <style>\n          {\".btn-send-later { display:none; }\"}\n        </style>\n        {this.renderDropCover()}\n        {this.renderSelectionControls()}\n        <MailMergeTable\n          {...otherProps}\n          selection={selection}\n          tableDataSource={tableDataSource}\n          draftClientId={draftClientId}\n          onCellEdited={session.updateCell}\n          onSetSelection={session.setSelection}\n          onShiftSelection={session.shiftSelection}\n          onAddColumn={session.addColumn}\n          onRemoveColumn={session.removeLastColumn}\n          onAddRow={session.addRow}\n          onRemoveRow={session.removeRow}\n        />\n      </DropZone>\n    )\n  }\n}\n\nexport default MailMergeWorkspace\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/main.es6",
    "content": "import {\n  TaskRegistry,\n  ExtensionRegistry,\n  ComponentRegistry,\n  CustomContenteditableComponents,\n} from 'nylas-exports'\n\nimport MailMergeButton from './mail-merge-button'\nimport MailMergeContainer from './mail-merge-container'\nimport SendManyDraftsTask from './send-many-drafts-task'\nimport MailMergeSendButton from './mail-merge-send-button'\nimport * as ComposerExtension from './mail-merge-composer-extension'\nimport MailMergeSubjectTextField from './mail-merge-subject-text-field'\nimport MailMergeBodyToken from './mail-merge-body-token'\nimport MailMergeParticipantsTextField from './mail-merge-participants-text-field'\n\nexport function activate() {\n  TaskRegistry.register('SendManyDraftsTask', () => SendManyDraftsTask)\n\n  ComponentRegistry.register(MailMergeContainer,\n    {role: 'Composer:ActionBarWorkspace'});\n\n  ComponentRegistry.register(MailMergeButton,\n    {role: 'Composer:ActionButton'});\n\n  ComponentRegistry.register(MailMergeSendButton,\n    {role: 'Composer:SendActionButton'});\n\n  ComponentRegistry.register(MailMergeParticipantsTextField,\n    {role: 'Composer:ParticipantsTextField'});\n\n  ComponentRegistry.register(MailMergeSubjectTextField,\n    {role: 'Composer:SubjectTextField'});\n\n  CustomContenteditableComponents.register('MailMergeBodyToken', MailMergeBodyToken)\n\n  ExtensionRegistry.Composer.register(ComposerExtension)\n}\n\nexport function deactivate() {\n  TaskRegistry.unregister('SendManyDraftsTask')\n  ComponentRegistry.unregister(MailMergeContainer)\n  ComponentRegistry.unregister(MailMergeButton)\n  ComponentRegistry.unregister(MailMergeSendButton)\n  ComponentRegistry.unregister(MailMergeParticipantsTextField)\n  ComponentRegistry.unregister(MailMergeSubjectTextField)\n  CustomContenteditableComponents.unregister('MailMergeBodyToken');\n  ExtensionRegistry.Composer.unregister(ComposerExtension)\n}\n\nexport function serialize() {\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/selection-state-reducers.es6",
    "content": "import _ from 'underscore'\nimport {MAX_ROWS} from './mail-merge-constants'\n\n\nexport function initialState(savedState) {\n  if (savedState && savedState.tableDataSource) {\n    return {\n      selection: {\n        rowIdx: 0,\n        colIdx: 0,\n        key: null,\n      },\n    }\n  }\n  return {\n    selection: {\n      rowIdx: 0,\n      colIdx: 0,\n      key: 'Enter',\n    },\n  }\n}\n\nexport function clearTableData() {\n  return {\n    selection: {\n      rowIdx: 0,\n      colIdx: 0,\n      key: null,\n    },\n  }\n}\n\nexport function loadTableData() {\n  return {\n    selection: {\n      rowIdx: 0,\n      colIdx: 0,\n      key: null,\n    },\n  }\n}\n\nexport function addColumn({selection, tableDataSource}) {\n  const columns = tableDataSource.columns()\n  return {\n    selection: {\n      ...selection,\n      rowIdx: null,\n      colIdx: columns.length,\n      key: 'Enter',\n    },\n  }\n}\n\nexport function removeLastColumn({selection, tableDataSource}) {\n  const columns = tableDataSource.columns()\n  const nextSelection = {...selection, key: null}\n  if (nextSelection.colIdx === columns.length - 1) {\n    nextSelection.colIdx--\n  }\n\n  return {selection: nextSelection}\n}\n\nexport function addRow({selection, tableDataSource}, {maxRows = MAX_ROWS} = {}) {\n  const rows = tableDataSource.rows()\n  if (rows.length === maxRows) {\n    return {selection}\n  }\n\n  return {\n    selection: {\n      ...selection,\n      rowIdx: rows.length,\n      key: 'Enter',\n    },\n  }\n}\n\nexport function removeRow({selection, tableDataSource}) {\n  const rows = tableDataSource.rows()\n  const nextSelection = {...selection, key: null}\n  if (nextSelection.rowIdx === rows.length - 1) {\n    nextSelection.rowIdx--\n  }\n\n  return {selection: nextSelection}\n}\n\nexport function updateCell({selection}) {\n  return {\n    selection: {...selection, key: null},\n  }\n}\n\nexport function setSelection({selection}, nextSelection) {\n  if (_.isEqual(selection, nextSelection)) {\n    return {selection}\n  }\n  return {\n    selection: {...nextSelection},\n  }\n}\n\nfunction shift(len, idx, delta = 0) {\n  const idxVal = idx != null ? idx : -1\n  return Math.min(len - 1, Math.max(0, idxVal + delta))\n}\n\nexport function shiftSelection({tableDataSource, selection}, deltas) {\n  const rowLen = tableDataSource.rows().length\n  const colLen = tableDataSource.columns().length\n\n  const nextSelection = {\n    rowIdx: shift(rowLen, selection.rowIdx, deltas.row),\n    colIdx: shift(colLen, selection.colIdx, deltas.col),\n    key: deltas.key,\n  }\n\n  return setSelection({selection}, nextSelection)\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/send-many-drafts-task.es6",
    "content": "import {\n  Task,\n  Actions,\n  Message,\n  TaskQueue,\n  DraftStore,\n  BaseDraftTask,\n  SendDraftTask,\n  SoundRegistry,\n  DatabaseStore,\n  TaskQueueStatusStore,\n} from 'nylas-exports'\nimport {PLUGIN_ID} from './mail-merge-constants'\n\n\nconst SEND_DRAFT_THROTTLE = 500\n\nexport default class SendManyDraftsTask extends Task {\n\n  constructor(baseDraftClientId, draftIdsToSend = []) {\n    super()\n    this.baseDraftClientId = baseDraftClientId\n    this.draftIdsToSend = draftIdsToSend\n\n    this.queuedDraftIds = new Set()\n    this.failedDraftIds = []\n  }\n\n  label() {\n    return `Sending ${this.draftIdsToSend.length} messages`\n  }\n\n  shouldDequeueOtherTask(other) {\n    return other instanceof SendManyDraftsTask && other.draftClientId === this.baseDraftClientId;\n  }\n\n  isDependentOnTask(other) {\n    const isSameDraft = other.draftClientId === this.baseDraftClientId;\n    const isSaveOrSend = other instanceof BaseDraftTask;\n    return isSameDraft && isSaveOrSend\n  }\n\n  performLocal() {\n    if (!this.baseDraftClientId) {\n      const errMsg = `Attempt to call SendManyDraftsTask.performLocal without a baseDraftClientId`;\n      return Promise.reject(new Error(errMsg));\n    }\n    if (this.draftIdsToSend.length === 0) {\n      const errMsg = `Attempt to call SendManyDraftsTask.performLocal without draftIdsToSend`;\n      return Promise.reject(new Error(errMsg));\n    }\n\n    return Promise.resolve();\n  }\n\n  performRemote() {\n    const unqueuedDraftIds = this.draftIdsToSend.filter(id => !this.queuedDraftIds.has(id))\n\n    if (unqueuedDraftIds.length > 0) {\n      return (\n        DatabaseStore.modelify(Message, unqueuedDraftIds)\n        .then((draftsToSend) => this.queueSendTasks(draftsToSend))\n        .then(() => this.waitForSendTasks())\n        .then(() => this.onTasksProcessed())\n        .catch((error) => this.handleError(error))\n      )\n    }\n    return (\n      this.waitForSendTasks()\n      .then(() => this.onTasksProcessed())\n      .catch((error) => this.handleError(error))\n    )\n  }\n\n  queueSendTasks(draftsToSend, throttle = SEND_DRAFT_THROTTLE) {\n    return Promise.each(draftsToSend, (draft) => {\n      return new Promise((resolve) => {\n        const task = new SendDraftTask(draft.clientId, {\n          playSound: false,\n          emitError: false,\n          allowMultiSend: false,\n        })\n        Actions.queueTask(task)\n        this.queuedDraftIds.add(draft.clientId)\n        setTimeout(resolve, throttle)\n      })\n    })\n  }\n\n  waitForSendTasks() {\n    const waitForTaskPromises = Array.from(this.queuedDraftIds).map((draftClientId) => {\n      const tasks = TaskQueue.allTasks()\n      const task = tasks.find((t) => t instanceof SendDraftTask && t.draftClientId === draftClientId)\n      if (!task) {\n        console.warn(`SendManyDraftsTask: Can't find queued SendDraftTask for draft id: ${draftClientId}`)\n        this.queuedDraftIds.delete(draftClientId)\n        return Promise.resolve()\n      }\n\n      return TaskQueueStatusStore.waitForPerformRemote(task)\n      .then((completedTask) => {\n        if (!this.queuedDraftIds.has(completedTask.draftClientId)) { return }\n\n        const {status} = completedTask.queueState\n        if (status === Task.Status.Failed) {\n          this.failedDraftIds.push(completedTask.draftClientId)\n        }\n\n        this.queuedDraftIds.delete(completedTask.draftClientId)\n      })\n    })\n    return Promise.all(waitForTaskPromises)\n  }\n\n  onTasksProcessed() {\n    if (this.failedDraftIds.length > 0) {\n      const error = new Error(\n        `Sorry, some of your messages failed to send.\nThis could be due to sending limits imposed by your mail provider.\nPlease try again after a while. Also make sure your messages are addressed correctly and are not too large.`,\n      )\n      return this.handleError(error)\n    }\n\n    Actions.recordUserEvent(\"Mail Merge Sent\", {\n      numItems: this.draftIdsToSend.length,\n      numFailedItems: this.failedDraftIds.length,\n    })\n\n    if (NylasEnv.config.get(\"core.sending.sounds\")) {\n      SoundRegistry.playSound('send');\n    }\n    return Promise.resolve(Task.Status.Success)\n  }\n\n  handleError(error) {\n    return (\n      DraftStore.sessionForClientId(this.baseDraftClientId)\n      .then((session) => {\n        return DatabaseStore.modelify(Message, this.failedDraftIds)\n        .then((failedDrafts) => {\n          const failedDraftRowIdxs = failedDrafts.map((draft) => draft.metadataForPluginId(PLUGIN_ID).rowIdx)\n          const currentMetadata = session.draft().metadataForPluginId(PLUGIN_ID)\n          const nextMetadata = {\n            ...currentMetadata,\n            failedDraftRowIdxs,\n          }\n          session.changes.addPluginMetadata(PLUGIN_ID, nextMetadata)\n          return session.changes.commit()\n        })\n      })\n      .then(() => {\n        this.failedDraftIds.forEach((id) => Actions.destroyDraft(id))\n        Actions.composePopoutDraft(this.baseDraftClientId, {errorMessage: error.message})\n        return Promise.resolve([Task.Status.Failed, error])\n      })\n    )\n  }\n\n  toJSON() {\n    const json = {...super.toJSON()}\n    json.queuedDraftIds = Array.from(json.queuedDraftIds)\n    return json\n  }\n\n  fromJSON(json) {\n    const result = super.fromJSON(json)\n    result.queuedDraftIds = new Set(result.queuedDraftIds)\n    return result\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/table-state-reducers.es6",
    "content": "import {Table} from 'nylas-component-kit'\nimport {MAX_ROWS} from './mail-merge-constants'\n\nconst {TableDataSource} = Table\n\n\nexport function toJSON({tableDataSource}) {\n  return {\n    tableDataSource: tableDataSource.toJSON(),\n  }\n}\n\nexport function fromJSON({tableDataSource}) {\n  return {\n    tableDataSource: new TableDataSource(tableDataSource),\n  }\n}\n\nexport function initialState(savedState) {\n  if (savedState && savedState.tableDataSource instanceof TableDataSource) {\n    if (savedState.failedDraftRowIdxs) {\n      const failedRowIdxs = new Set(savedState.failedDraftRowIdxs)\n      const dataSource = (\n        savedState.tableDataSource\n        .filterRows((row, idx) => failedRowIdxs.has(idx))\n      )\n      return {\n        tableDataSource: dataSource,\n      }\n    }\n    return {\n      tableDataSource: savedState.tableDataSource,\n    }\n  }\n  return {\n    tableDataSource: new TableDataSource({\n      columns: ['email'],\n      rows: [\n        [null],\n      ],\n    }),\n  }\n}\n\nexport function clearTableData({tableDataSource}) {\n  return {\n    tableDataSource: tableDataSource.clear(),\n  }\n}\n\nexport function loadTableData({tableDataSource}, {newTableData}) {\n  const newRows = newTableData.rows\n  const newCols = newTableData.columns\n  if (newRows.length === 0 || newCols.length === 0) {\n    return initialState()\n  }\n  return {\n    tableDataSource: new TableDataSource(newTableData),\n  }\n}\n\nexport function addColumn({tableDataSource}) {\n  return {\n    tableDataSource: tableDataSource.addColumn(),\n  }\n}\n\nexport function removeLastColumn({tableDataSource}) {\n  return {\n    tableDataSource: tableDataSource.removeLastColumn(),\n  }\n}\n\nexport function addRow({tableDataSource}, {maxRows = MAX_ROWS} = {}) {\n  const rows = tableDataSource.rows()\n  if (rows.length === maxRows) {\n    return {tableDataSource}\n  }\n\n  return {\n    tableDataSource: tableDataSource.addRow(),\n  }\n}\n\nexport function removeRow({tableDataSource}) {\n  return {\n    tableDataSource: tableDataSource.removeRow(),\n  }\n}\n\nexport function updateCell({tableDataSource}, {rowIdx, colIdx, isHeader, value}) {\n  return {\n    tableDataSource: tableDataSource.updateCell({rowIdx, colIdx, isHeader, value}),\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/token-data-source.es6",
    "content": "import _ from 'underscore'\nimport {Utils} from 'nylas-exports'\n\n\nclass FieldTokens {\n\n  constructor(field, tokens = {}) {\n    this._field = field\n    this._tokens = tokens\n  }\n\n  linkToken(colProps) {\n    const tokenId = colProps.tokenId ? colProps.tokenId : Utils.generateTempId()\n    return new FieldTokens(this._field, {\n      ...this._tokens,\n      [tokenId]: {...colProps, field: this._field, tokenId},\n    })\n  }\n\n  unlinkToken(tokenId) {\n    const nextTokens = {...this._tokens}\n    delete nextTokens[tokenId]\n    return new FieldTokens(this._field, nextTokens)\n  }\n\n  updateToken(tokenId, props) {\n    const token = this._tokens[tokenId]\n    return new FieldTokens(this._field, {\n      ...this._tokens,\n      [tokenId]: {...token, ...props},\n    })\n  }\n\n  tokens() {\n    return _.values(this._tokens)\n  }\n\n  findTokens(matcher) {\n    return _.where(this.tokens(), matcher)\n  }\n\n  getToken(tokenId) {\n    return this._tokens[tokenId]\n  }\n}\n\nclass TokenDataSource {\n\n  static fromJSON(json) {\n    return json.reduce((dataSource, token) => {\n      const {field, ...props} = token\n      return dataSource.linkToken(field, props)\n    }, new TokenDataSource())\n  }\n\n  constructor(linkedTokensByField = {}) {\n    this._linkedTokensByField = linkedTokensByField\n  }\n\n  findTokens(field, matcher) {\n    if (!this._linkedTokensByField[field]) { return [] }\n    return this._linkedTokensByField[field].findTokens(matcher)\n  }\n\n  tokensForField(field) {\n    if (!this._linkedTokensByField[field]) { return [] }\n    return this._linkedTokensByField[field].tokens()\n  }\n\n  getToken(field, tokenId) {\n    if (!this._linkedTokensByField[field]) { return null }\n    return this._linkedTokensByField[field].getToken(tokenId)\n  }\n\n  linkToken(field, props) {\n    if (!this._linkedTokensByField[field]) {\n      this._linkedTokensByField[field] = new FieldTokens(field)\n    }\n\n    const current = this._linkedTokensByField[field]\n    return new TokenDataSource({\n      ...this._linkedTokensByField,\n      [field]: current.linkToken(props),\n    })\n  }\n\n  unlinkToken(field, tokenId) {\n    if (!this._linkedTokensByField[field]) { return this }\n\n    const current = this._linkedTokensByField[field]\n    return new TokenDataSource({\n      ...this._linkedTokensByField,\n      [field]: current.unlinkToken(tokenId),\n    })\n  }\n\n  updateToken(field, tokenId, props) {\n    if (!this._linkedTokensByField[field]) { return this }\n\n    const current = this._linkedTokensByField[field]\n    return new TokenDataSource({\n      ...this._linkedTokensByField,\n      [field]: current.updateToken(tokenId, props),\n    })\n  }\n\n  toJSON() {\n    return Object.keys(this._linkedTokensByField)\n    .map((field) => this._linkedTokensByField[field])\n    .reduce((prevTokens, dataSource) => prevTokens.concat(dataSource.tokens()), [])\n  }\n}\n\nexport default TokenDataSource\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/token-state-reducers.es6",
    "content": "import {contactFromColIdx} from './mail-merge-utils'\nimport TokenDataSource from './token-data-source'\nimport {LinkableFields, ContenteditableFields, ParticipantFields} from './mail-merge-constants'\n\n\nexport function toDraftChanges(draft, {tableDataSource, selection, tokenDataSource}) {\n  // Save the participant fields to fake Contacts\n  const participantChanges = {}\n  ParticipantFields.forEach((field) => (\n    participantChanges[field] = tokenDataSource.tokensForField(field).map(({colIdx}) => {\n      const selectionValue = tableDataSource.cellAt({rowIdx: selection.rowIdx, colIdx}) || \"\"\n      return contactFromColIdx(colIdx, selectionValue.trim())\n    })\n  ))\n\n  // Save the body and subject if they haven't been saved yet\n  // This is necessary because new tokens wont be saved to the contenteditable\n  // unless the user directly mutates the body or subject\n  const contenteditableChanges = {}\n  ContenteditableFields.forEach((field) => {\n    const node = document.querySelector(`.${field}-field [contenteditable]`)\n    if (node) {\n      const latestValue = node.innerHTML\n      if (draft[field] !== latestValue) {\n        contenteditableChanges[field] = latestValue\n      }\n    }\n  })\n\n  return {...participantChanges, ...contenteditableChanges}\n}\n\nexport function toJSON({tokenDataSource}) {\n  return {tokenDataSource: tokenDataSource.toJSON()}\n}\n\nexport function fromJSON({tokenDataSource}) {\n  return {tokenDataSource: TokenDataSource.fromJSON(tokenDataSource)}\n}\n\nexport function initialState(savedData) {\n  if (savedData && savedData.tokenDataSource) {\n    return {\n      tokenDataSource: savedData.tokenDataSource,\n    }\n  }\n  const tokenDataSource = new TokenDataSource()\n  return { tokenDataSource }\n}\n\nexport function loadTableData({tokenDataSource}, {newTableData}) {\n  const nextColumns = newTableData.columns\n  let nextTokenDataSource = new TokenDataSource()\n\n  // When loading table data, if the new table data contains columns with the same\n  // name, make sure to keep those tokens in our state with the updated position\n  // of the column\n  LinkableFields.forEach((field) => {\n    const currentTokens = tokenDataSource.tokensForField(field)\n    currentTokens.forEach((link) => {\n      const {colName, ...props} = link\n      const newColIdx = nextColumns.indexOf(colName)\n      if (newColIdx !== -1) {\n        nextTokenDataSource = nextTokenDataSource.linkToken(field, {\n          ...props,\n          colName,\n          colIdx: newColIdx,\n        })\n      }\n    })\n  })\n  return {tokenDataSource: nextTokenDataSource}\n}\n\nexport function linkToDraft({tokenDataSource}, args) {\n  const {colIdx, colName, field, ...props} = args\n  if (!field) { throw new Error('MailMerge: Must provide `field` to `linkToDraft`') }\n  if (!colIdx) { throw new Error('MailMerge: Must provide `colIdx` to `linkToDraft`') }\n  if (colName == null) { throw new Error('MailMerge: Must provide `colName` to `linkToDraft`') }\n  return {\n    tokenDataSource: tokenDataSource.linkToken(field, {colIdx, colName, ...props}),\n  }\n}\n\nexport function unlinkFromDraft({tokenDataSource}, {field, tokenId}) {\n  if (!field) { throw new Error('MailMerge: Must provide `field` to `linkToDraft`') }\n  if (!tokenId) { throw new Error('MailMerge: Must provide `tokenId` to `linkToDraft`') }\n  return {\n    tokenDataSource: tokenDataSource.unlinkToken(field, tokenId),\n  }\n}\n\nexport function removeLastColumn({tokenDataSource, tableDataSource}) {\n  const colIdx = tableDataSource.columns().length - 1\n  const colName = tableDataSource.colAt(colIdx)\n  let nextTokenDataSource = tokenDataSource\n\n  // Unlink any fields that where linked to the column that is being removed\n  LinkableFields.forEach((field) => {\n    const tokensToRemove = tokenDataSource.findTokens(field, {colName})\n    nextTokenDataSource = tokensToRemove.reduce((prevTokenDataSource, {tokenId}) => {\n      return prevTokenDataSource.unlinkToken(field, tokenId)\n    }, nextTokenDataSource)\n  })\n  return {tokenDataSource: nextTokenDataSource}\n}\n\nexport function updateCell({tokenDataSource, tableDataSource}, {colIdx, isHeader, value}) {\n  if (!isHeader) { return {tokenDataSource} }\n  const currentColName = tableDataSource.colAt(colIdx)\n  let nextTokenDataSource = tokenDataSource\n\n  // Update any tokens that referenced the column name that is being updated\n  LinkableFields.forEach((field) => {\n    const tokens = tokenDataSource.findTokens(field, {colName: currentColName})\n    tokens.forEach(({tokenId}) => {\n      nextTokenDataSource = nextTokenDataSource.updateToken(field, tokenId, {colName: value})\n    })\n  })\n  return {tokenDataSource: nextTokenDataSource}\n}\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/lib/workspace-state-reducers.es6",
    "content": "export function initialState(savedData) {\n  if (savedData && savedData.tokenDataSource && savedData.tableDataSource) {\n    return {\n      isWorkspaceOpen: true,\n    }\n  }\n  return {\n    isWorkspaceOpen: false,\n  }\n}\n\nexport function toggleWorkspace({isWorkspaceOpen}) {\n  return {isWorkspaceOpen: !isWorkspaceOpen}\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/package.json",
    "content": "{\n  \"name\": \"composer-mail-merge\",\n  \"title\":\"Mail Merge\",\n  \"description\": \"Send personalized emails at scale using CSV-formatted data.\",\n  \"main\": \"./lib/main\",\n  \"isHiddenOnPluginsPage\": true,\n  \"version\": \"0.1.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"icon\": \"./icon.png\",\n  \"isOptional\": true,\n  \"supportedEnvs\": [\"production\", \"staging\"],\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"work\": true,\n    \"thread-popout\": true\n  },\n  \"license\": \"GPL-3.0\",\n  \"dependencies\": {\n    \"papaparse\": \"^4.1.2\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/fixtures.es6",
    "content": "import {Table} from 'nylas-component-kit'\nimport TokenDataSource from '../lib/token-data-source'\n\nconst {TableDataSource} = Table\n\nexport const testData = {\n  columns: ['name', 'email'],\n  rows: [\n    ['donald', 'donald@nylas.com'],\n    ['hilary', 'hilary@nylas.com'],\n  ],\n}\n\nexport const testDataSource = new TableDataSource(testData)\n\nexport const testSelection = {rowIdx: 1, colIdx: 0, key: 'Enter'}\n\nexport const testTokenDataSource =\n  new TokenDataSource()\n  .linkToken('to', {colName: 'name', colIdx: 0, tokenId: 'name-0'})\n  .linkToken('bcc', {colName: 'email', colIdx: 1, tokenId: 'email-1'})\n\nexport const testState = {\n  isWorkspaceOpen: true,\n  selection: testSelection,\n  tableDataSource: testDataSource,\n  tokenDataSource: testTokenDataSource,\n}\n\nexport const testAnchorMarkup = (tokenId) => {\n  return `<img class=\"n1-overlaid-component-anchor-container mail-merge-token-wrap\" src=\"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\" data-overlay-id=\"${tokenId}\" data-component-props=\"{&quot;field&quot;:&quot;subject&quot;,&quot;colIdx&quot;:&quot;0&quot;,&quot;colName&quot;:&quot;email&quot;,&quot;draftClientId&quot;:&quot;local-0cab45d1-c763&quot;,&quot;className&quot;:&quot;mail-merge-token-wrap&quot;}\" data-component-key=\"MailMergeBodyToken\" style=\"width: 132.156px; height: 21px;\">`\n}\n\nexport const testContenteditableContent = () => {\n  const nameSpan = testAnchorMarkup('name-anchor')\n  const emailSpan = testAnchorMarkup('email-anchor')\n  return `<div>${nameSpan}<br>stuff${emailSpan}</div>`\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/mail-merge-draft-editing-session-spec.es6",
    "content": "import {Message} from 'nylas-exports'\nimport {MailMergeDraftEditingSession} from '../lib/mail-merge-draft-editing-session'\n\n\nconst testReducers = [\n  {testAction: state => ({...state, val1: 'reducer1'})},\n  {testAction: state => ({...state, val2: 'reducer2'})},\n]\nconst draftModel = new Message()\nconst draftSess = {\n  draft() { return draftModel },\n}\n\ndescribe('MailMergeDraftEditingSession', function describeBlock() {\n  let mailMergeSess;\n  beforeEach(() => {\n    mailMergeSess = new MailMergeDraftEditingSession(draftSess, testReducers)\n  });\n\n  describe('dispatch', () => {\n    it('computes next state correctly based on registered reducers', () => {\n      const nextState = mailMergeSess.dispatch({name: 'testAction'}, {})\n      expect(nextState).toEqual({\n        val1: 'reducer1',\n        val2: 'reducer2',\n      })\n    });\n\n    it('computes state value for key correctly when 2 reducers ', () => {\n      const reducers = testReducers.concat([\n        {testAction: state => ({...state, val2: 'reducer3'})},\n      ])\n      mailMergeSess = new MailMergeDraftEditingSession(draftSess, reducers)\n\n      const nextState = mailMergeSess.dispatch({name: 'testAction'}, {})\n      expect(nextState).toEqual({\n        val1: 'reducer1',\n        val2: 'reducer3',\n      })\n    });\n\n    it('passes arguments correctly to reducers', () => {\n      const args = ['arg1']\n      const reducers = testReducers.concat([\n        {testAction: (state, arg) => ({...state, val3: arg})},\n      ])\n      mailMergeSess = new MailMergeDraftEditingSession(draftSess, reducers)\n\n      const nextState = mailMergeSess.dispatch({name: 'testAction', args}, {})\n      expect(nextState).toEqual({\n        val1: 'reducer1',\n        val2: 'reducer2',\n        val3: 'arg1',\n      })\n    });\n  });\n\n  describe('initializeState', () => {\n    it('loads any saved metadata on the draft', () => {\n      const savedMetadata = {\n        tableDataSource: {},\n        tokenDataSource: {},\n      }\n      const nextState = {next: 'state'}\n      spyOn(draftModel, 'metadataForPluginId').andReturn(savedMetadata)\n      spyOn(mailMergeSess, 'dispatch').andReturn(nextState)\n\n      mailMergeSess.initializeState(draftModel)\n      expect(mailMergeSess.dispatch.calls.length).toBe(2)\n      const args1 = mailMergeSess.dispatch.calls[0].args\n      const args2 = mailMergeSess.dispatch.calls[1].args\n\n      expect(args1).toEqual([{name: 'fromJSON'}, savedMetadata])\n      expect(args2).toEqual([{name: 'initialState'}, nextState])\n      expect(mailMergeSess._state).toEqual(nextState)\n    });\n\n    it('does not laod saved metadata if saved metadata is incorrect', () => {\n      const savedMetadata = {\n        tableDataSource: {},\n      }\n      const nextState = {next: 'state'}\n      spyOn(draftModel, 'metadataForPluginId').andReturn(savedMetadata)\n      spyOn(mailMergeSess, 'dispatch').andReturn(nextState)\n\n      mailMergeSess.initializeState(draftModel)\n      expect(mailMergeSess.dispatch.calls.length).toBe(1)\n      const {args} = mailMergeSess.dispatch.calls[0]\n\n      expect(args).toEqual([{name: 'initialState'}])\n      expect(mailMergeSess._state).toEqual(nextState)\n    });\n\n    it('just loads initial state if no metadata is saved on the draft', () => {\n      const savedMetadata = {}\n      const nextState = {next: 'state'}\n      spyOn(draftModel, 'metadataForPluginId').andReturn(savedMetadata)\n      spyOn(mailMergeSess, 'dispatch').andReturn(nextState)\n\n      mailMergeSess.initializeState(draftModel)\n      expect(mailMergeSess.dispatch.calls.length).toBe(1)\n      const {args} = mailMergeSess.dispatch.calls[0]\n\n      expect(args).toEqual([{name: 'initialState'}])\n      expect(mailMergeSess._state).toEqual(nextState)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/mail-merge-utils-spec.es6",
    "content": "import Papa from 'papaparse'\nimport {\n  Message,\n  Contact,\n  DraftHelpers,\n  Actions,\n  DatabaseWriter,\n} from 'nylas-exports';\n\nimport {DataTransferTypes} from '../lib/mail-merge-constants'\nimport SendManyDraftsTask from '../lib/send-many-drafts-task'\nimport {\n  parseCSV,\n  buildDraft,\n  sendManyDrafts,\n  contactFromColIdx,\n} from '../lib/mail-merge-utils'\nimport {\n  testData,\n  testDataSource,\n  testAnchorMarkup,\n  testContenteditableContent,\n} from './fixtures'\nimport TokenDataSource from '../lib/token-data-source'\n\n\nxdescribe('MailMergeUtils', function describeBlock() {\n  describe('contactFromColIdx', () => {\n    it('creates a contact with the correct values', () => {\n      const email = 'email@email.com'\n      const contact = contactFromColIdx(0, email)\n      expect(contact instanceof Contact).toBe(true)\n      expect(contact.email).toBe(email)\n      expect(contact.name).toBe(email)\n      expect(contact.clientId).toBe(`${DataTransferTypes.ColIdx}:0`)\n    });\n  });\n\n  describe('buildDraft', () => {\n    beforeEach(() => {\n      this.baseDraft = new Message({\n        draft: true,\n        clientId: 'd1',\n        subject: `<div>Your email is: ${testAnchorMarkup('subject-email-anchor')}`,\n        body: testContenteditableContent(),\n      })\n\n      this.tokenDataSource = new TokenDataSource()\n      .linkToken('to', {colName: 'email', colIdx: 1, tokenId: 'email-0'})\n      .linkToken('bcc', {colName: 'email', colIdx: 1, tokenId: 'email-1'})\n      .linkToken('body', {colName: 'name', colIdx: 0, tokenId: 'name-anchor'})\n      .linkToken('body', {colName: 'email', colIdx: 1, tokenId: 'email-anchor'})\n      .linkToken('subject', {colName: 'email', colIdx: 1, tokenId: 'subject-email-anchor'})\n    });\n\n    it('creates a draft with the correct subject based on linked columns and rowIdx', () => {\n      const draft = buildDraft(this.baseDraft, {\n        rowIdx: 1,\n        tableDataSource: testDataSource,\n        tokenDataSource: this.tokenDataSource,\n      })\n      expect(draft.subject).toEqual('Your email is: hilary@nylas.com')\n    });\n\n    it('creates a draft with the correct body based on linked columns and rowIdx', () => {\n      const draft = buildDraft(this.baseDraft, {\n        rowIdx: 1,\n        tableDataSource: testDataSource,\n        tokenDataSource: this.tokenDataSource,\n      })\n      expect(draft.body).toEqual('<div><span>hilary</span><br>stuff<span>hilary@nylas.com</span></div>')\n    });\n\n    it('creates a draft with the correct participants based on linked columns and rowIdx', () => {\n      const draft = buildDraft(this.baseDraft, {\n        rowIdx: 1,\n        tableDataSource: testDataSource,\n        tokenDataSource: this.tokenDataSource,\n      })\n      expect(draft.to[0].email).toEqual('hilary@nylas.com')\n      expect(draft.bcc[0].email).toEqual('hilary@nylas.com')\n    });\n\n    it('throws error if value for participant field in invalid email address', () => {\n      this.tokenDataSource = this.tokenDataSource.updateToken('to', 'email-0', {colName: 'name', colIdx: 0})\n      expect(() => {\n        buildDraft(this.baseDraft, {\n          rowIdx: 1,\n          tableDataSource: testDataSource,\n          tokenDataSource: this.tokenDataSource,\n        })\n      }).toThrow()\n    });\n  });\n\n  describe('sendManyDrafts', () => {\n    beforeEach(() => {\n      this.baseDraft = new Message({\n        draft: true,\n        accountId: '123',\n        serverId: '111',\n        clientId: 'local-111',\n      })\n      this.drafts = [\n        new Message({draft: true, clientId: 'local-d1'}),\n        new Message({draft: true, clientId: 'local-d2'}),\n        new Message({draft: true, clientId: 'local-d3'}),\n      ]\n      this.draftSession = {\n        ensureCorrectAccount: jasmine.createSpy('ensureCorrectAccount').andCallFake(() => {\n          return Promise.resolve()\n        }),\n      }\n      this.session = {\n        draftSession: () => this.draftSession,\n        draft: () => this.baseDraft,\n      }\n\n      spyOn(DraftHelpers, 'applyExtensionTransforms').andCallFake((d) => {\n        const transformed = d.clone()\n        transformed.body = 'transformed'\n        return Promise.resolve(transformed)\n      })\n      spyOn(DatabaseWriter.prototype, 'persistModels').andReturn(Promise.resolve())\n      spyOn(Actions, 'queueTask')\n      spyOn(Actions, 'queueTasks')\n      spyOn(NylasEnv.config, 'get').andReturn(false)\n      spyOn(NylasEnv, 'close')\n    })\n\n    it('ensures account is correct', () => {\n      waitsForPromise(() => {\n        return sendManyDrafts(this.session, this.drafts)\n        .then(() => {\n          expect(this.draftSession.ensureCorrectAccount).toHaveBeenCalled()\n        })\n      })\n    });\n\n    it('applies extension transforms to each draft and saves them', () => {\n      waitsForPromise(() => {\n        return sendManyDrafts(this.session, this.drafts)\n        .then(() => {\n          const transformedDrafts = DatabaseWriter.prototype.persistModels.calls[0].args[0]\n          expect(transformedDrafts.length).toBe(3)\n          transformedDrafts.forEach((d) => {\n            expect(d.body).toBe('transformed')\n            expect(d.accountId).toBe('123')\n            expect(d.serverId).toBe(null)\n          })\n        })\n      })\n    });\n\n    it('queues the correct task', () => {\n      waitsForPromise(() => {\n        return sendManyDrafts(this.session, this.drafts)\n        .then(() => {\n          const task = Actions.queueTask.calls[0].args[0]\n          expect(task instanceof SendManyDraftsTask).toBe(true)\n          expect(task.baseDraftClientId).toBe('local-111')\n          expect(task.draftIdsToSend).toEqual(['local-d1', 'local-d2', 'local-d3'])\n        })\n      })\n    });\n  });\n\n  describe('parseCSV', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv, 'showErrorDialog')\n    });\n\n    it('shows error when csv file is empty', () => {\n      spyOn(Papa, 'parse').andCallFake((file, {complete}) => {\n        complete({data: []})\n      })\n      waitsForPromise(() => {\n        return parseCSV()\n        .then((data) => {\n          expect(NylasEnv.showErrorDialog).toHaveBeenCalled()\n          expect(data).toBe(null)\n        })\n      })\n    });\n\n    it('returns the correct table data', () => {\n      spyOn(Papa, 'parse').andCallFake((file, {complete}) => {\n        complete({data: [testData.columns].concat(testData.rows)})\n      })\n      waitsForPromise(() => {\n        return parseCSV()\n        .then((data) => {\n          expect(data).toEqual(testData)\n        })\n      })\n    });\n\n    it('adds a header row if the first row contains a value that resembles an email', () => {\n      spyOn(Papa, 'parse').andCallFake((file, {complete}) => {\n        complete({data: [...testData.rows]})\n      })\n      waitsForPromise(() => {\n        return parseCSV()\n        .then((data) => {\n          expect(data).toEqual({\n            columns: ['Column 0', 'Email Address'],\n            rows: testData.rows,\n          })\n        })\n      })\n    });\n\n    it('only imports MAX_ROWS number of rows', () => {\n      spyOn(Papa, 'parse').andCallFake((file, {complete}) => {\n        complete({\n          data: [testData.columns].concat([...testData.rows, ['extra', 'col@email.com']]),\n        })\n      })\n      waitsForPromise(() => {\n        return parseCSV(null, 2)\n        .then((data) => {\n          expect(data.rows.length).toBe(2)\n          expect(data).toEqual(testData)\n          expect(NylasEnv.showErrorDialog).toHaveBeenCalled()\n        })\n      })\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/selection-state-reducers-spec.es6",
    "content": "import {\n  clearTableData,\n  loadTableData,\n  addColumn,\n  removeLastColumn,\n  addRow,\n  removeRow,\n  updateCell,\n  setSelection,\n  shiftSelection,\n} from '../lib/selection-state-reducers'\nimport {testState, testSelection} from './fixtures'\n\n\ndescribe('SelectionStateReducers', function describeBlock() {\n  describe('clearTableData', () => {\n    it('sets selection correctly', () => {\n      const {selection} = clearTableData()\n      expect(selection).toEqual({\n        rowIdx: 0,\n        colIdx: 0,\n        key: null,\n      })\n    });\n  });\n\n  describe('loadTableData', () => {\n    it('sets selection correctly', () => {\n      const {selection} = loadTableData()\n      expect(selection).toEqual({\n        rowIdx: 0,\n        colIdx: 0,\n        key: null,\n      })\n    });\n  });\n\n  describe('addColumn', () => {\n    it('sets selection to the header and last column', () => {\n      const {selection} = addColumn(testState)\n      expect(selection).toEqual({rowIdx: null, colIdx: 2, key: 'Enter'})\n    });\n  });\n\n  describe('removeLastColumn', () => {\n    it('only sets key to null if selection is not in last column', () => {\n      const {selection} = removeLastColumn(testState)\n      expect(selection).toEqual({...testSelection, key: null})\n    });\n\n    it('decreases col selection by 1 if selection is currently in last column', () => {\n      const {selection} = removeLastColumn({...testState, selection: {rowIdx: 1, colIdx: 1, key: 'Enter'}})\n      expect(selection).toEqual({rowIdx: 1, colIdx: 0, key: null})\n    });\n  });\n\n  describe('addRow', () => {\n    it('does nothing if MAX_ROWS reached', () => {\n      const {selection} = addRow(testState, {maxRows: 2})\n      expect(selection).toBe(testSelection)\n    });\n\n    it('sets selection to last row', () => {\n      const {selection} = addRow(testState, {maxRows: 3})\n      expect(selection).toEqual({rowIdx: 2, colIdx: 0, key: 'Enter'})\n    });\n  });\n\n  describe('removeRow', () => {\n    it('only sets key to null if selection is not in last row', () => {\n      const {selection} = removeRow(testState)\n      expect(selection).toEqual({...testSelection, rowIdx: 0, key: null})\n    });\n\n    it('decreases row selection by 1 if selection is currently in last row', () => {\n      const {selection} = removeRow({...testState, selection: {rowIdx: 1, colIdx: 1, key: 'Enter'}})\n      expect(selection).toEqual({rowIdx: 0, colIdx: 1, key: null})\n    });\n  });\n\n  describe('updateCell', () => {\n    it('sets selection key to null (wont make input focus)', () => {\n      const {selection} = updateCell(testState)\n      expect(selection.key).toBe(null)\n    });\n  });\n\n  describe('setSelection', () => {\n    it('sets the selection to the given selection if selection has changed', () => {\n      const {selection} = setSelection(testState, {rowIdx: 1, colIdx: 1, key: null})\n      expect(selection).toEqual({rowIdx: 1, colIdx: 1, key: null})\n    });\n\n    it('returns same selection otherwise', () => {\n      const {selection} = setSelection(testState, {...testSelection})\n      expect(selection).toBe(testSelection)\n    });\n  });\n\n  describe('shiftSelection', () => {\n    it('sets the given key', () => {\n      const {selection} = shiftSelection(testState, {row: 0, col: 0, key: null})\n      expect(selection.key).toBe(null)\n    });\n\n    it('shifts row selection correctly when rowIdx is null (header)', () => {\n      let nextSelection = shiftSelection({\n        ...testState,\n        selection: {rowIdx: null, col: 0},\n      }, {row: 1}).selection\n      expect(nextSelection.rowIdx).toBe(0)\n\n      nextSelection = shiftSelection({\n        ...testState,\n        selection: {rowIdx: null, col: 0},\n      }, {row: 2}).selection\n      expect(nextSelection.rowIdx).toBe(1)\n\n      nextSelection = shiftSelection({\n        ...testState,\n        selection: {rowIdx: null, col: 0},\n      }, {row: -1}).selection\n      expect(nextSelection.rowIdx).toBe(0)\n    });\n\n    it('shifts row selection by correct value', () => {\n      let nextState = shiftSelection(\n        testState,\n        {row: -1}\n      )\n      expect(nextState.selection.rowIdx).toBe(0)\n\n      nextState = shiftSelection(\n        {...testState, selection: {rowIdx: 0, colIdx: 0, key: 'Enter'}},\n        {row: 1}\n      )\n      expect(nextState.selection.rowIdx).toBe(1)\n    });\n\n    it('does not shift row selection when at the edges', () => {\n      let nextState = shiftSelection(\n        testState,\n        {row: 2}\n      )\n      expect(nextState.selection.rowIdx).toBe(1)\n\n      nextState = shiftSelection(\n        {...testState, selection: {rowIdx: 0, colIdx: 0, key: 'Enter'}},\n        {row: -2}\n      )\n      expect(nextState.selection.rowIdx).toBe(0)\n    });\n\n    it('shifts col selection by correct value', () => {\n      let nextState = shiftSelection(\n        testState,\n        {col: 1}\n      )\n      expect(nextState.selection.colIdx).toBe(1)\n\n      nextState = shiftSelection(\n        {...testState, selection: {rowIdx: 0, colIdx: 1, key: 'Enter'}},\n        {col: -1}\n      )\n      expect(nextState.selection.colIdx).toBe(0)\n    });\n\n    it('does not shift col selection when at the edges', () => {\n      let nextState = shiftSelection(\n        testState,\n        {col: -2}\n      )\n      expect(nextState.selection.colIdx).toBe(0)\n\n      nextState = shiftSelection(\n        {...testState, selection: {rowIdx: 0, colIdx: 1, key: 'Enter'}},\n        {col: 2}\n      )\n      expect(nextState.selection.colIdx).toBe(1)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/send-many-drafts-task-spec.es6",
    "content": "import {\n  Task,\n  Actions,\n  Message,\n  TaskQueue,\n  DraftStore,\n  DatabaseStore,\n  SendDraftTask,\n  TaskQueueStatusStore,\n} from 'nylas-exports'\nimport SendManyDraftsTask from '../lib/send-many-drafts-task'\nimport {PLUGIN_ID} from '../lib/mail-merge-constants'\n\n\nxdescribe('SendManyDraftsTask', function describeBlock() {\n  beforeEach(() => {\n    this.baseDraft = new Message({\n      clientId: 'baseId',\n      files: ['f1', 'f2'],\n      uploads: [],\n    })\n    this.d1 = new Message({\n      clientId: 'd1',\n      uploads: ['u1'],\n    })\n    this.d2 = new Message({\n      clientId: 'd2',\n    })\n\n    this.task = new SendManyDraftsTask('baseId', ['d1', 'd2'])\n\n    spyOn(DatabaseStore, 'modelify').andReturn(Promise.resolve([this.baseDraft, this.d1, this.d2]))\n    spyOn(DatabaseStore, 'inTransaction').andCallFake((cb) => {\n      return cb({persistModels() { return Promise.resolve() }})\n    })\n  });\n\n  describe('performRemote', () => {\n    beforeEach(() => {\n      spyOn(this.task, 'prepareDraftsToSend').andCallFake((baseId, draftIds) => {\n        return Promise.resolve(draftIds.map(id => this[id]))\n      })\n      spyOn(this.task, 'queueSendTasks').andReturn(Promise.resolve())\n      spyOn(this.task, 'waitForSendTasks').andReturn(Promise.resolve())\n      spyOn(this.task, 'onTasksProcessed')\n      spyOn(this.task, 'handleError').andCallFake((error) =>\n        Promise.resolve([Task.Status.Failed, error])\n      )\n    });\n\n    it('queues all drafts for sending when no tasks have been queued yet', () => {\n      waitsForPromise(() => {\n        return this.task.performRemote()\n        .then(() => {\n          expect(this.task.prepareDraftsToSend).toHaveBeenCalledWith('baseId', ['d1', 'd2'])\n          expect(this.task.queueSendTasks).toHaveBeenCalledWith([this.d1, this.d2])\n          expect(this.task.waitForSendTasks).toHaveBeenCalled()\n        })\n      })\n    });\n\n    it('only queues drafts that have not been queued for sending', () => {\n      this.task.queuedDraftIds = new Set(['d1'])\n      waitsForPromise(() => {\n        return this.task.performRemote()\n        .then(() => {\n          expect(this.task.prepareDraftsToSend).toHaveBeenCalledWith('baseId', ['d2'])\n          expect(this.task.queueSendTasks).toHaveBeenCalledWith([this.d2])\n          expect(this.task.waitForSendTasks).toHaveBeenCalled()\n        })\n      })\n    });\n\n    it('only waits for tasks to complete when all drafts have been queued for sending', () => {\n      this.task.queuedDraftIds = new Set(['d1', 'd2'])\n      waitsForPromise(() => {\n        return this.task.performRemote()\n        .then(() => {\n          expect(this.task.prepareDraftsToSend).not.toHaveBeenCalled()\n          expect(this.task.queueSendTasks).not.toHaveBeenCalled()\n          expect(this.task.waitForSendTasks).toHaveBeenCalled()\n        })\n      })\n    });\n\n    it('handles errors', () => {\n      jasmine.unspy(this.task, 'onTasksProcessed')\n      spyOn(this.task, 'onTasksProcessed').andReturn(Promise.reject(new Error('Oh no!')))\n      this.task.queuedDraftIds = new Set(['d1', 'd2'])\n      waitsForPromise(() => {\n        return this.task.performRemote()\n        .then(() => {\n          expect(this.task.handleError).toHaveBeenCalled()\n        })\n      })\n    });\n  });\n\n  describe('prepareDraftsToSend', () => {\n    it('updates the files and uploads on each draft to send', () => {\n      waitsForPromise(() => {\n        return this.task.prepareDraftsToSend('baseId', ['d1', 'd2'])\n        .then((draftsToSend) => {\n          expect(DatabaseStore.modelify).toHaveBeenCalledWith(Message, ['baseId', 'd1', 'd2'])\n          expect(draftsToSend.length).toBe(2)\n          expect(draftsToSend[0].files).toEqual(this.baseDraft.files)\n          expect(draftsToSend[0].uploads).toEqual([])\n          expect(draftsToSend[1].files).toEqual(this.baseDraft.files)\n          expect(draftsToSend[1].uploads).toEqual([])\n        })\n      })\n    });\n  });\n\n  describe('queueSendTasks', () => {\n    beforeEach(() => {\n      spyOn(Actions, 'queueTask')\n    });\n\n    it('queues SendDraftTask for all passed in drafts', () => {\n      waitsForPromise(() => {\n        const promise = this.task.queueSendTasks([this.d1, this.d2], 0)\n        advanceClock(1)\n        advanceClock(1)\n        return promise.then(() => {\n          expect(Actions.queueTask.calls.length).toBe(2)\n          expect(Array.from(this.task.queuedDraftIds)).toEqual(['d1', 'd2'])\n          Actions.queueTask.calls.forEach(({args}, idx) => {\n            const task = args[0]\n            expect(task instanceof SendDraftTask).toBe(true)\n            expect(task.draftClientId).toEqual(`d${idx + 1}`)\n          })\n        })\n      })\n    });\n  });\n\n  describe('waitForSendTasks', () => {\n    it('it updates queuedDraftIds and warns if there are no tasks matching the draft client id', () => {\n      this.task.queuedDraftIds = new Set(['d2'])\n      spyOn(TaskQueue, 'allTasks').andReturn([])\n      spyOn(console, 'warn')\n      waitsForPromise(() => {\n        return this.task.waitForSendTasks()\n        .then(() => {\n          expect(this.task.queuedDraftIds.size).toBe(0)\n          expect(console.warn).toHaveBeenCalled()\n        })\n      })\n    });\n\n    it('resolves when all queued tasks complete', () => {\n      this.task.queuedDraftIds = new Set(['d2'])\n      spyOn(TaskQueue, 'allTasks').andReturn([new SendDraftTask('d2')])\n      spyOn(TaskQueueStatusStore, 'waitForPerformRemote').andCallFake((task) => {\n        task.queueState.status = Task.Status.Success\n        return Promise.resolve(task)\n      })\n\n      waitsForPromise(() => {\n        return this.task.waitForSendTasks()\n        .then(() => {\n          expect(Array.from(this.task.queuedDraftIds)).toEqual([])\n          expect(this.task.failedDraftIds).toEqual([])\n        })\n      })\n    });\n\n    it('saves any draft ids of drafts that failed to send', () => {\n      this.task.queuedDraftIds = new Set(['d1', 'd2'])\n      spyOn(TaskQueue, 'allTasks').andReturn([new SendDraftTask('d1'), new SendDraftTask('d2')])\n      spyOn(TaskQueueStatusStore, 'waitForPerformRemote').andCallFake((task) => {\n        if (task.draftClientId === 'd1') {\n          task.queueState.status = Task.Status.Failed\n        } else {\n          task.queueState.status = Task.Status.Success\n        }\n        return Promise.resolve(task)\n      })\n\n      waitsForPromise(() => {\n        return this.task.waitForSendTasks()\n        .then(() => {\n          expect(Array.from(this.task.queuedDraftIds)).toEqual([])\n          expect(this.task.failedDraftIds).toEqual(['d1'])\n        })\n      })\n    });\n  });\n\n  describe('handleError', () => {\n    beforeEach(() => {\n      this.baseDraft.applyPluginMetadata(PLUGIN_ID, {tableDataSource: {}})\n      this.d1.applyPluginMetadata(PLUGIN_ID, {rowIdx: 0})\n      this.d2.applyPluginMetadata(PLUGIN_ID, {rowIdx: 1})\n      this.baseSession = {\n        draft: () => { return this.baseDraft },\n        changes: {\n          addPluginMetadata: jasmine.createSpy('addPluginMetadata'),\n          commit() { return Promise.resolve() },\n        },\n      }\n\n      this.task.failedDraftIds = ['d1', 'd2']\n      spyOn(Actions, 'destroyDraft')\n      spyOn(Actions, 'composePopoutDraft')\n      spyOn(DraftStore, 'sessionForClientId').andReturn(Promise.resolve(this.baseSession))\n\n      jasmine.unspy(DatabaseStore, 'modelify')\n      spyOn(DatabaseStore, 'modelify').andReturn(Promise.resolve([this.d1, this.d2]))\n    });\n\n    it('correctly saves the failed rowIdxs to the base draft metadata', () => {\n      waitsForPromise(() => {\n        return this.task.handleError({message: 'Error!'})\n        .then((status) => {\n          expect(status[0]).toBe(Task.Status.Failed)\n          expect(DatabaseStore.modelify).toHaveBeenCalledWith(Message, this.task.failedDraftIds)\n          expect(this.baseSession.changes.addPluginMetadata).toHaveBeenCalledWith(PLUGIN_ID, {\n            tableDataSource: {},\n            failedDraftRowIdxs: [0, 1],\n          })\n        })\n      })\n    });\n\n    it('correctly destroys failed drafts', () => {\n      waitsForPromise(() => {\n        return this.task.handleError({message: 'Error!'})\n        .then((status) => {\n          expect(status[0]).toBe(Task.Status.Failed)\n          expect(Actions.destroyDraft.calls.length).toBe(2)\n          expect(Actions.destroyDraft.calls[0].args).toEqual(['d1'])\n          expect(Actions.destroyDraft.calls[1].args).toEqual(['d2'])\n        })\n      })\n    });\n\n    it('correctly pops out base composer with error msg', () => {\n      waitsForPromise(() => {\n        return this.task.handleError({message: 'Error!'})\n        .then((status) => {\n          expect(status[0]).toBe(Task.Status.Failed)\n          expect(Actions.composePopoutDraft).toHaveBeenCalledWith('baseId', {\n            errorMessage: 'Error!',\n          })\n        })\n      })\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/table-state-reducers-spec.es6",
    "content": "import {\n  initialState,\n  fromJSON,\n  toJSON,\n  clearTableData,\n  loadTableData,\n  addColumn,\n  removeLastColumn,\n  addRow,\n  removeRow,\n  updateCell,\n} from '../lib/table-state-reducers'\nimport {testData, testDataSource} from './fixtures'\n\n\ndescribe('TableStateReducers', function describeBlock() {\n  describe('initialState', () => {\n    it('returns correct initial state when there is saved state', () => {\n      const savedState = {tableDataSource: testDataSource}\n      expect(initialState(savedState)).toEqual(savedState)\n    });\n\n    it('keeps only rowIdxs that failed if failedRowIdxs present in saved state', () => {\n      const savedState = {tableDataSource: testDataSource, failedDraftRowIdxs: [1]}\n      const {tableDataSource} = initialState(savedState)\n      expect(tableDataSource.rows()).toEqual([testDataSource.rowAt(1)])\n    });\n  });\n\n  describe('fromJSON', () => {\n    it('returns correct data source from json table data', () => {\n      const {tableDataSource} = fromJSON({tableDataSource: testData})\n      expect(tableDataSource.toJSON()).toEqual(testData)\n    });\n  });\n\n  describe('toJSON', () => {\n    it('returns correct json object from data source', () => {\n      const {tableDataSource} = toJSON({tableDataSource: testDataSource})\n      expect(tableDataSource).toEqual(testData)\n    });\n  });\n\n  describe('clearTableData', () => {\n    it('clears all data correcltly', () => {\n      const {tableDataSource} = clearTableData({tableDataSource: testDataSource})\n      expect(tableDataSource.toJSON()).toEqual({\n        columns: [],\n        rows: [[]],\n      })\n    });\n  });\n\n  describe('loadTableData', () => {\n    it('loads table data correctly', () => {\n      const newTableData = {\n        columns: ['my-col'],\n        rows: [['my-val']],\n      }\n      const {tableDataSource} = loadTableData({tableDataSource: testDataSource}, {newTableData})\n      expect(tableDataSource.toJSON()).toEqual(newTableData)\n    });\n\n    it('returns initial state if new table data is empty', () => {\n      const newTableData = {\n        columns: [],\n        rows: [[]],\n      }\n      const {tableDataSource} = loadTableData({tableDataSource: testDataSource}, {newTableData})\n      expect(tableDataSource.toJSON()).toEqual(initialState().tableDataSource.toJSON())\n    });\n  });\n\n  describe('addColumn', () => {\n    it('pushes a new column to the data source\\'s columns', () => {\n      const {tableDataSource} = addColumn({tableDataSource: testDataSource})\n      expect(tableDataSource.columns()).toEqual(['name', 'email', null])\n    });\n\n    it('pushes a new column to every row', () => {\n      const {tableDataSource} = addColumn({tableDataSource: testDataSource})\n      expect(tableDataSource.rows()).toEqual([\n        ['donald', 'donald@nylas.com', null],\n        ['hilary', 'hilary@nylas.com', null],\n      ])\n    });\n  });\n\n  describe('removeLastColumn', () => {\n    it('removes last column from the data source\\'s columns', () => {\n      const {tableDataSource} = removeLastColumn({tableDataSource: testDataSource})\n      expect(tableDataSource.columns()).toEqual(['name'])\n    });\n\n    it('removes last column from every row', () => {\n      const {tableDataSource} = removeLastColumn({tableDataSource: testDataSource})\n      expect(tableDataSource.rows()).toEqual([['donald'], ['hilary']])\n    });\n  });\n\n  describe('addRow', () => {\n    it('does nothing if MAX_ROWS reached', () => {\n      const {tableDataSource} = addRow({tableDataSource: testDataSource}, {maxRows: 2})\n      expect(tableDataSource).toBe(testDataSource)\n    });\n\n    it('pushes an empty row with correct number of columns', () => {\n      const {tableDataSource} = addRow({tableDataSource: testDataSource}, {maxRows: 3})\n      expect(tableDataSource.rows()).toEqual([\n        ['donald', 'donald@nylas.com'],\n        ['hilary', 'hilary@nylas.com'],\n        [null, null],\n      ])\n    });\n  });\n\n  describe('removeRow', () => {\n    it('removes last row', () => {\n      const {tableDataSource} = removeRow({tableDataSource: testDataSource})\n      expect(tableDataSource.rows()).toEqual([['donald', 'donald@nylas.com']])\n    });\n  });\n\n  describe('updateCell', () => {\n    it('updates cell value correctly when updating a cell that is /not/ a header', () => {\n      const {tableDataSource} = updateCell({tableDataSource: testDataSource}, {\n        rowIdx: 0, colIdx: 0, isHeader: false, value: 'new-val',\n      })\n      expect(tableDataSource.rows()).toEqual([\n        ['new-val', 'donald@nylas.com'],\n        ['hilary', 'hilary@nylas.com'],\n      ])\n    });\n\n    it('updates cell value correctly when updating a cell that /is/ a header', () => {\n      const {tableDataSource} = updateCell({tableDataSource: testDataSource}, {\n        rowIdx: null, colIdx: 0, isHeader: true, value: 'new-val',\n      })\n      expect(tableDataSource.columns()).toEqual(['new-val', 'email'])\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/token-state-reducers-spec.es6",
    "content": "import {Contact} from 'nylas-exports'\nimport {\n  toDraftChanges,\n  toJSON,\n  initialState,\n  loadTableData,\n  linkToDraft,\n  unlinkFromDraft,\n  removeLastColumn,\n  updateCell,\n} from '../lib/token-state-reducers'\nimport {testState, testTokenDataSource, testData} from './fixtures'\n\n\ndescribe('WorkspaceStateReducers', function describeBlock() {\n  describe('toDraftChanges', () => {\n    it('returns an object with participant fields populated with the correct Contact objects', () => {\n      const {to, bcc} = toDraftChanges({}, testState)\n      expect(to.length).toBe(1)\n      expect(bcc.length).toBe(1)\n\n      const toContact = to[0]\n      const bccContact = bcc[0]\n      expect(toContact instanceof Contact).toBe(true)\n      expect(toContact.email).toEqual('hilary')\n      expect(bccContact instanceof Contact).toEqual(true)\n      expect(bccContact.email).toEqual('hilary@nylas.com')\n    });\n  });\n\n  describe('toJSON', () => {\n    it('only saves linked fields to json', () => {\n      expect(toJSON(testState)).toEqual({\n        tokenDataSource: [\n          {field: 'to', colName: 'name', colIdx: 0, tokenId: 'name-0'},\n          {field: 'bcc', colName: 'email', colIdx: 1, tokenId: 'email-1'},\n        ],\n      })\n    });\n  });\n\n  describe('initialState', () => {\n    it('loads saved linked fields correctly when provided', () => {\n      expect(initialState({tokenDataSource: testTokenDataSource})).toEqual({\n        tokenDataSource: testTokenDataSource,\n      })\n    });\n  });\n\n  describe('loadTableData', () => {\n    describe('when newTableData contains columns that have already been linked in the prev tableData', () => {\n      it(`preserves the linked fields for the old columns that are still present\n         and update the index to the new value in newTableData`, () => {\n        const newTableData = {\n          columns: ['email', 'other'],\n          rows: [\n            ['donald@nylas.com', 'd'],\n            ['john@gmail.com', 'j'],\n          ],\n        }\n\n        const nextState = loadTableData(testState, {newTableData, prevColumns: testData.columns})\n        expect(nextState.tokenDataSource.toJSON()).toEqual([\n          {field: 'bcc', colName: 'email', colIdx: 0, tokenId: 'email-1'},\n        ])\n      });\n    });\n\n    describe('when newTableData only contains new columns', () => {\n      it('unlinks all fields that are no longer present ', () => {\n        const newTableData = {\n          columns: ['other1'],\n          rows: [\n            ['donald@nylas.com'],\n            ['john@gmail.com'],\n          ],\n        }\n\n        const nextState = loadTableData(testState, {newTableData, prevColumns: testData.columns})\n        expect(nextState.tokenDataSource.toJSON()).toEqual([])\n      });\n    });\n  });\n\n  describe('linkToDraft', () => {\n    it('adds the new field correctly to tokenDataSource state', () => {\n      const nextState = linkToDraft(testState, {\n        colIdx: 1,\n        colName: 'email',\n        field: 'body',\n        name: 'some',\n        tokenId: 'email-2',\n      })\n      expect(nextState.tokenDataSource.toJSON()).toEqual([\n        {field: 'to', colName: 'name', colIdx: 0, tokenId: 'name-0'},\n        {field: 'bcc', colName: 'email', colIdx: 1, tokenId: 'email-1'},\n        {field: 'body', colName: 'email', colIdx: 1, tokenId: 'email-2', name: 'some'},\n      ])\n\n      // Check that object ref is updated\n      expect(testTokenDataSource).not.toBe(nextState.tokenDataSource)\n    });\n\n    it('adds a new link if column has already been linked to that field', () => {\n      const nextState = linkToDraft(testState, {\n        colIdx: 1,\n        colName: 'email',\n        field: 'bcc',\n        name: 'some',\n        tokenId: 'email-2',\n      })\n      expect(nextState.tokenDataSource.toJSON()).toEqual([\n        {field: 'to', colName: 'name', colIdx: 0, tokenId: 'name-0'},\n        {field: 'bcc', colName: 'email', colIdx: 1, tokenId: 'email-1'},\n        {field: 'bcc', colName: 'email', colIdx: 1, tokenId: 'email-2', name: 'some'},\n      ])\n    });\n  });\n\n  describe('unlinkFromDraft', () => {\n    it('removes field correctly from tokenDataSource state', () => {\n      const nextState = unlinkFromDraft(testState, {field: 'bcc', tokenId: 'email-1'})\n      expect(nextState.tokenDataSource.toJSON()).toEqual([\n        {field: 'to', colName: 'name', colIdx: 0, tokenId: 'name-0'},\n      ])\n      // Check that object ref is updated\n      expect(testTokenDataSource).not.toBe(nextState.tokenDataSource)\n    });\n  });\n\n  describe('removeLastColumn', () => {\n    it('removes any tokenDataSource that were associated with the removed column', () => {\n      const nextState = removeLastColumn(testState)\n      expect(nextState.tokenDataSource.toJSON()).toEqual([\n        {field: 'to', colName: 'name', colIdx: 0, tokenId: 'name-0'},\n      ])\n    });\n  });\n\n  describe('updateCell', () => {\n    it('updates tokenDataSource when a column name (header cell) is updated', () => {\n      const nextState = updateCell(testState, {colIdx: 0, isHeader: true, value: 'nombre'})\n      expect(nextState.tokenDataSource.toJSON()).toEqual([\n        {field: 'to', colName: 'nombre', colIdx: 0, tokenId: 'name-0'},\n        {field: 'bcc', colName: 'email', colIdx: 1, tokenId: 'email-1'},\n      ])\n    });\n\n    it('does not update tokens state otherwise', () => {\n      const nextState = updateCell(testState, {colIdx: 0, isHeader: false, value: 'nombre'})\n      expect(nextState.tokenDataSource).toBe(testTokenDataSource)\n    });\n  });\n});\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/spec/workspace-state-reducers-spec.es6",
    "content": "import {\n  initialState,\n  toggleWorkspace,\n} from '../lib/workspace-state-reducers'\nimport {testState} from './fixtures'\n\n\ndescribe('WorkspaceStateReducers', function describeBlock() {\n  describe('initialState', () => {\n    it('always opens the workspace if there is saved data', () => {\n      expect(initialState(testState)).toEqual({\n        isWorkspaceOpen: true,\n      })\n    });\n\n    it('defaults to closed', () => {\n      expect(initialState()).toEqual({\n        isWorkspaceOpen: false,\n      })\n    });\n  });\n\n  describe('toggleWorkspace', () => {\n    it('toggles workspace worrectly', () => {\n      expect(toggleWorkspace({isWorkspaceOpen: false})).toEqual({isWorkspaceOpen: true})\n      expect(toggleWorkspace({isWorkspaceOpen: true})).toEqual({isWorkspaceOpen: false})\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-mail-merge/stylesheets/mail-merge.less",
    "content": "@import 'ui-variables';\n\n.mail-merge-workspace {\n  width: 100%;\n  height: 250px;\n  z-index: 1;\n  border-top: 1px solid lightgrey;\n  padding: @padding-large-vertical * 1.2 @padding-large-horizontal * 1.2;\n\n  .selection-controls {\n    display: flex;\n    align-items: center;\n    color: @text-color-very-subtle;\n    margin-bottom: @padding-small-horizontal;\n\n    .btn.btn-group {\n      padding: 0;\n      &:active {\n        background: initial;\n      }\n    }\n\n    .btn {\n      display: flex;\n      align-items: center;\n      height: 1.5em;\n      margin-right: 5px;\n      color: @text-color-very-subtle;\n\n      &:hover {\n        img {\n          background-color: @text-color-subtle;\n        }\n      }\n\n      .btn-prev,.btn-next {\n        height: 100%;\n        width: 15px;\n        img {\n          background-color: @text-color-very-subtle;\n        }\n        &:active {\n          background: darken(@btn-default-bg-color, 9%);\n        }\n      }\n      .btn-prev img {\n        transform: rotate(90deg) translate(-9px, -8px);\n      }\n      .btn-next img {\n        transform: rotate(-90deg) translate(9px, 8px);\n      }\n    }\n  }\n\n  .mail-merge-table {\n    height: 90%;\n\n    .editable-table-container {\n      width: 100%;\n      &>.key-commands-region {\n        width: initial;\n      }\n    }\n\n    .nylas-table.editable-table {\n      max-width: 700px;\n      font-size: 0.9em;\n\n      .table-row-header {\n        &.selected, th, th.selected {\n          background: initial;\n          border: 1px solid lighten(@border-color-secondary, 5%);\n        }\n      }\n\n      .table-row.table-row-header {\n        height: 35px;\n      }\n\n      .table-row {\n        height: 30px;\n      }\n\n      .numbered-cell {\n        width: 30px;\n      }\n\n      th.table-cell {\n        .mail-merge-token {\n          input {\n            cursor: -webkit-grab;\n          }\n        }\n      }\n      td.table-cell input {\n        cursor: default;\n      }\n      .table-cell:not(.numbered-cell) {\n        width: 120px;\n      }\n\n      .header-cell {\n        display: flex;\n        align-items: center;\n        height: 28px;\n        padding-left: 6px;\n      }\n    }\n  }\n}\n\n.generate-token-colors(@n, @i: 0) when (@i =< @n) {\n  @base-color: hsla(197 + (@i * 20), 58%, 95%, 1);\n  @text-color: darken(desaturate(@base-color, 28%), 49%);\n  @border-color: darken(@base-color, 8%);\n\n  .token-color-@{i}.mail-merge-token {\n    background-color: @base-color;\n    color: @text-color;\n    border-color: darken(@base-color, 8%);\n\n    img {\n      background-color: @text-color;\n    }\n  }\n  .mail-merge-participants-text-field {\n    .token.token-color-@{i} {\n      &.selected,&.dragging {\n        border-color: @text-color;\n      }\n    }\n  }\n  .generate-token-colors(@n, (@i + 1));\n}\n\n.generate-token-colors(4);\n\n.mail-merge-token {\n  border: 1px solid;\n  border-radius: 5px;\n  padding-left: 7px;\n  cursor: -webkit-grab;\n}\n\n.mail-merge-token-wrap,\n.n1-overlaid-component-anchor-container.mail-merge-token-wrap {\n  margin: 0 1px;\n  vertical-align: bottom;\n\n  .mail-merge-token {\n    padding: @padding-small-vertical * 0.5 @padding-small-horizontal * 0.5;\n    padding-right: 2px;\n    img {\n      margin-right: 10px;\n    }\n  }\n}\n\n.header-cell .mail-merge-token {\n  display: flex;\n  align-items: center;\n  height: 22px;\n\n  input {\n    max-width: 90%;\n    text-align: left;\n    height: 100%;\n    line-height: 100%;\n    color: inherit;\n    padding-bottom: 2px;\n  }\n}\n\n.mail-merge-participants-text-field {\n  .token {\n    box-shadow: none;\n    padding: 0;\n    border-radius: 5px;\n\n    .mail-merge-token {\n      display: flex;\n      align-items: center;\n      padding: 0 @padding-base-vertical;\n      font-size: 0.9em;\n      img {\n        margin-right: 10px;\n      }\n    }\n\n    &.selected, &.dragging {\n      background: none;\n      border-radius: 5px;\n    }\n  }\n}\n\n.mail-merge-subject-overlaid {\n  .toggle-preview {\n    top: 2px;\n    right: 22px;\n  }\n}\n\n.mail-merge-subject-text-field {\n  div[contenteditable] {\n    line-height: 21px;\n    font-weight: 400;\n    padding: 13px 0 9px 0;\n    min-width: 5em;\n    background-color: transparent;\n    border: none;\n    margin: 0;\n    &:empty:before {\n      content: attr(placeholder);\n      color: @text-color-very-subtle;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-markdown/README.md",
    "content": "\n# N1 Markdown Composer\n\nA plugin for N1 that allows you to compose emails using markdown.\n\n![Markdown Screenshot Editor](/assets/markdown_screenshot_edit.png?raw=true \"Markdown Composer Editor\")\n![Markdown Screenshot Preview](/assets/markdown_screenshot_preview.png?raw=true \"Markdown Composer Preview\")\n\n## Install this plugin:\n\n1. Download and run N1\n\n2. Clone this repository (Make sure you have `git` installed and available in\n   your system path)\n\n3. From the menu, select `Developer > Install a Package Manually...`\n   From the dialog, choose the directory of this plugin to install it!\n\n   > When you install packages, they're moved to `~/.nylas-mail/packages`,\n   > and N1 runs `apm install` on the command line to fetch dependencies\n   > listed in the package's `package.json`\n\n\n## Usage\n\nJust write emails using markdown.\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-markdown/lib/main.cjsx",
    "content": "# Markdown Editor\n# Last Revised: April 23, 2015 by Ben Gotow\n#\n# Markdown editor is a simple React component that allows you to type your\n# emails in markdown and see the live preview of your email in html\n#\n{ExtensionRegistry, ComponentRegistry} = require 'nylas-exports'\nMarkdownEditor = require './markdown-editor'\nMarkdownComposerExtension = require './markdown-composer-extension'\n\nmodule.exports =\n  activate: ->\n    ComponentRegistry.register MarkdownEditor,\n      role: 'Composer:Editor'\n    ExtensionRegistry.Composer.register(MarkdownComposerExtension)\n\n  serialize: ->\n\n  deactivate: ->\n    ComponentRegistry.unregister(MarkdownEditor)\n    ExtensionRegistry.Composer.unregister(MarkdownComposerExtension)\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-markdown/lib/markdown-composer-extension.coffee",
    "content": "marked = require 'marked'\nUtils = require './utils'\n{ComposerExtension} = require 'nylas-exports'\n\nrawBodies = {}\n\nclass MarkdownComposerExtension extends ComposerExtension\n\n  @applyTransformsForSending: ({draftBodyRootNode, draft}) ->\n    rawBodies[draft.clientId] = draftBodyRootNode.innerHTML\n    draftBodyRootNode.innerHTML = marked(draftBodyRootNode.innerText)\n\n  @unapplyTransformsForSending: ({draftBodyRootNode, draft}) ->\n    if rawBodies[draft.clientId]\n      draftBodyRootNode.innerHTML = rawBodies[draft.clientId]\n\nmodule.exports = MarkdownComposerExtension\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-markdown/lib/markdown-editor.cjsx",
    "content": "Utils = require './utils'\nSimpleMDE = require 'simplemde'\n{React, ReactDOM, QuotedHTMLTransformer} = require 'nylas-exports'\n\n# Keep a file-scope variable containing the contents of the markdown stylesheet.\n# This will be embedded in the markdown preview iFrame, as well as the email body.\n# The stylesheet is loaded when a preview component is first mounted.\nmarkdownStylesheet = null\n\nsplitContents = (contents) ->\n  quoteStart = contents.search(/(<div class=\"gmail_quote|<signature)/i)\n  if quoteStart > 0\n    return [contents.substr(0, quoteStart), contents.substr(quoteStart)]\n  return [contents, \"\"]\n\nclass MarkdownEditor extends React.Component\n  @displayName: 'MarkdownEditor'\n\n  @containerRequired: false\n\n  @contextTypes:\n    parentTabGroup: React.PropTypes.object,\n\n  @propTypes:\n    body: React.PropTypes.string.isRequired,\n    onBodyChanged: React.PropTypes.func.isRequired,\n\n  componentDidMount: =>\n    @mde = new SimpleMDE(\n      inputStyle: 'contenteditable'\n      element: ReactDOM.findDOMNode(@refs.container),\n      hideIcons: ['fullscreen', 'side-by-side']\n      showIcons: ['code', 'table']\n      spellChecker: false,\n    )\n    @mde.codemirror.on(\"change\", @_onBodyChanged)\n    @mde.codemirror.on(\"keydown\", @_onKeyDown)\n    @setCurrentBodyInDOM()\n\n  componentDidUpdate: (prevProps) =>\n    wasEmpty = prevProps.body.length is 0\n\n    if @props.body isnt prevProps.body and @props.body isnt @currentBodyInDOM()\n      @setCurrentBodyInDOM()\n\n    if wasEmpty\n      @mde.codemirror.execCommand('goDocEnd')\n\n  focus: =>\n    @mde.codemirror.focus()\n\n  focusAbsoluteEnd: =>\n    @focus()\n    @mde.codemirror.execCommand('goDocEnd')\n\n  setCurrentBodyInDOM: =>\n    [editable, uneditable] = splitContents(@props.body)\n\n    uneditableEl = ReactDOM.findDOMNode(@refs.uneditable)\n    uneditableEl.innerHTML = uneditable\n    uneditableNoticeEl = ReactDOM.findDOMNode(@refs.uneditableNotice)\n    if Utils.getTextFromHtml(uneditable).length > 0\n      uneditableNoticeEl.style.display = 'block'\n    else\n      uneditableNoticeEl.style.display = 'none'\n\n    @mde.value(Utils.getTextFromHtml(editable))\n\n  currentBodyInDOM: =>\n    uneditableEl = ReactDOM.findDOMNode(@refs.uneditable)\n    return @mde.value() + uneditableEl.innerHTML\n\n  getCurrentSelection: ->\n\n  getPreviousSelection: ->\n\n  setSelection: ->\n    container = ReactDOM.findDOMNode(@refs.container)\n    sel = document.getSelection()\n    sel.setBaseAndExtent(container, 0, container, 0)\n\n  _onDOMMutated: ->\n\n  _onBodyChanged: =>\n    setImmediate =>\n      value = @currentBodyInDOM()\n      @props.onBodyChanged({target: {value}})\n\n  _onKeyDown: (codemirror, e)=>\n    if e.key is 'Tab' and e.shiftKey is true\n      position = codemirror.cursorCoords(true, 'local')\n      isAtBeginning = position.top <= 5 and position.left <= 5\n      if isAtBeginning\n        # TODO i'm /really/ sorry\n        # Subject is at position 2 within the tab group, the focused text area\n        # in this component is at position 17, so that's why we shift back 15\n        # positions.\n        # This will break if the dom elements between here and the subject ever\n        # change\n        @context.parentTabGroup.shiftFocus(-15)\n        e.preventDefault()\n        e.codemirrorIgnore = true\n\n  render: ->\n    # TODO sorry\n    # Add style tag to disable incompatible plugins\n    <div tabIndex=\"1\" className=\"markdown-editor\" onFocus={@focus}>\n      <style>\n        {\".btn-mail-merge { display:none; }\"}\n        {\".btn-emoji { display:none; }\"}\n        {\".btn-templates { display:none; }\"}\n        {\".btn-scheduler { display:none; }\"}\n        {\".btn-translate { display:none; }\"}\n        {\".btn-send-reminder { display:none; }\"}\n      </style>\n      <div\n        ref=\"container\"\n        className=\"editing-region\"\n      />\n      <div ref=\"uneditableNotice\" style={{display: 'none'}} className=\"uneditable-notice\">\n        The markdown editor does not support editing signatures or quoted text. Content below will be included in your message.\n      </div>\n      <div ref=\"uneditable\"></div>\n    </div>\n\nmodule.exports = MarkdownEditor\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-markdown/lib/utils.coffee",
    "content": "\nclass Utils\n\n  @getTextFromHtml: (html) ->\n    div = document.createElement('div')\n    div.innerHTML = html\n    div.textContent ? div.innerText\n\nmodule.exports = Utils\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-markdown/package.json",
    "content": "{\n  \"name\": \"composer-markdown\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"title\": \"Markdown (Experimental)\",\n  \"description\": \"Write emails using Markdown!\",\n  \"isHiddenOnPluginsPage\": true,\n  \"license\": \"GPL-3.0\",\n  \"icon\": \"./icon.png\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"isOptional\": true,\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-markdown/stylesheets/index.less",
    "content": "@import \"../internal_packages/composer-markdown/node_modules/simplemde/dist/simplemde.min.css\";\n@import \"ui-variables\";\n@import \"ui-mixins\";\n\n@blurred-primary-color: mix(@background-primary, #ffbb00, 96%);\n\n.compose-body .markdown-editor {\n  margin: auto;\n  padding: 10px 23px 10px;\n  width: 100%;\n\n  .editing-region div[contenteditable] {\n    min-height: 0;\n    padding: 0;\n  }\n\n  .uneditable-notice {\n    text-align: center;\n    font-size: 0.9em;\n    color: @text-color-very-subtle;\n    margin-bottom: 10px;\n    border-top: 1px dashed;\n    border-bottom: 1px dashed;\n    padding: 4px;\n  }\n}\n\n.CodeMirror {\n  height: 225px;\n  min-height: 225px;\n  background: transparent;\n}\n.CodeMirror-sided {\n  display: inline-block;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/README.md",
    "content": "# QuickSchedule\n\nSay goodbye to the hassle of scheduling! This new plugin lets you avoid\nthe typical back-and-forth of picking a time to meet. Just select a few\noptions, and your recipient confirms with one click. It's the best way to\ninstantly schedule meetings.\n\nThis plugin works by adding a small \"Schedule\" button next to the Send\nbutton in the composer. Clicking the button will prompt the creation of a\nquick event creator.\n\nYou can even select a set of proposed times. When you do this a calendar\npops up with your availability. You can then select some proposed times\nfor the receipient to choose from.\n\n#### Enable this plugin\n\n1. Download and run N1\n\n2. Navigate to Preferences > Plugins and click \"Enable\" beside the plugin.\n\n#### Who is this for?\n\nAnyone who makes a lot of appointments! If you are a developer, this is\nalso a great example of a more complicated plugin that requires a backend\nservice, and demonstrates how arbitrary JavaScript can be inserted to\ncreate custom functionality.\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/calendar/proposed-time-calendar-data-source.es6",
    "content": "import {Rx, CalendarDataSource} from 'nylas-exports'\nimport ProposedTimeCalendarStore from '../proposed-time-calendar-store'\n\nexport default class ProposedTimeCalendarDataSource extends CalendarDataSource {\n  buildObservable({startTime, endTime, disabledCalendars}) {\n    this.observable = Rx.Observable.combineLatest([\n      super.buildObservable({startTime, endTime, disabledCalendars}),\n      Rx.Observable.fromStore(ProposedTimeCalendarStore).map((store) => store.proposalsAsEvents()),\n    ])\n    .map(([superResult, proposedTimes]) => {\n      return {events: superResult.events.concat(proposedTimes)}\n    })\n    return this.observable;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/calendar/proposed-time-event.jsx",
    "content": "import React from 'react'\nimport classnames from 'classnames'\nimport SchedulerActions from '../scheduler-actions'\nimport {CALENDAR_ID} from '../scheduler-constants'\n\n/**\n * Gets rendered in a CalendarEvent\n */\nexport default class ProposedTimeEvent extends React.Component {\n  static displayName = \"ProposedTimeEvent\";\n\n  static propTypes = {\n    event: React.PropTypes.object,\n  }\n\n  // Since ProposedTimeEvent is part of an Injected Component set, by\n  // default it's placed in its own container that's rendered separately.\n  //\n  // This makes two separate React trees which cause the react event\n  // propagations to be separate. See:\n  // https://github.com/facebook/react/issues/1691\n  //\n  // Unfortunately, this means that `stopPropagation` doesn't work from\n  // within injected component sets unless the `containerRequired` is set\n  // to `false`\n  static containerRequired = false;\n\n  _onMouseDown(event) {\n    event.stopPropagation();\n    SchedulerActions.removeProposedTime(event.target.dataset);\n  }\n\n  render() {\n    const className = classnames({\n      \"rm-time\": true,\n      \"proposal\": this.props.event.proposalType === \"proposal\",\n      \"availability\": this.props.event.proposalType === \"availability\",\n    });\n    if (this.props.event.calendarId === CALENDAR_ID) {\n      return (\n        <div\n          className={className}\n          data-end={this.props.event.end}\n          data-start={this.props.event.start}\n          onMouseDown={this._onMouseDown}\n        >&times;</div>\n      )\n    }\n    return false\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/calendar/proposed-time-picker.jsx",
    "content": "import React from 'react'\nimport {Utils} from 'nylas-exports'\nimport {NylasCalendar} from 'nylas-component-kit'\nimport SchedulerActions from '../scheduler-actions'\nimport ProposedTimeCalendarStore from '../proposed-time-calendar-store'\nimport ProposedTimeCalendarDataSource from './proposed-time-calendar-data-source'\n\n/**\n * A an extended NylasCalendar that lets you pick proposed times.\n */\nexport default class ProposedTimePicker extends React.Component {\n  static displayName = \"ProposedTimePicker\";\n\n  static containerStyles = {\n    height: \"100%\",\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      proposals: ProposedTimeCalendarStore.proposals(),\n      duration: ProposedTimeCalendarStore.currentDuration(),\n      pendingSave: ProposedTimeCalendarStore.pendingSave(),\n    }\n  }\n\n  componentDidMount() {\n    this._usub = ProposedTimeCalendarStore.listen(() => {\n      this.setState({\n        duration: ProposedTimeCalendarStore.currentDuration(),\n        proposals: ProposedTimeCalendarStore.proposals(),\n        pendingSave: ProposedTimeCalendarStore.pendingSave(),\n      });\n    })\n    NylasEnv.displayWindow()\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) ||\n            !Utils.isEqualReact(nextState, this.state));\n  }\n\n  componentWillUnmount() {\n    this._usub()\n  }\n\n  _dataSource() {\n    return new ProposedTimeCalendarDataSource()\n  }\n\n  _bannerComponents = () => {\n    return {\n      week: \"Click and drag to propose times.\",\n    }\n  }\n\n  _footerComponents = () => {\n    return {\n      week: [this._leftFooterComponents(), this._rightFooterComponents()],\n    }\n  }\n\n  _renderClearButton() {\n    if (this.state.proposals.length === 0) {\n      return false\n    }\n    return (\n      <button\n        key=\"clear\"\n        style={{order: -99, marginLeft: 20}}\n        onClick={this._onClearProposals}\n        className=\"btn clear-proposed-times\"\n      >\n        Clear Times\n      </button>\n    )\n  }\n\n  _onClearProposals = () => {\n    SchedulerActions.clearProposals()\n  }\n\n  _leftFooterComponents() {\n    const optComponents = ProposedTimeCalendarStore.DURATIONS.map((opt, i) =>\n      <option value={opt.join(\"|\")} key={i}>{opt[2]}</option>\n    )\n\n    const durationPicker = (\n      <div key=\"dp\" className=\"duration-picker\" style={{order: -100}}>\n        <label\n          htmlFor=\"duration-picker-select\"\n          style={{paddingRight: 10}}\n        >\n          Event Duration:\n        </label>\n        <select\n          id=\"duration-picker-select\"\n          className=\"duration-picker-select\"\n          value={this.state.duration.join(\"|\")}\n          onChange={this._onChangeDuration}\n        >\n          {optComponents}\n        </select>\n      </div>\n    )\n\n    return ([durationPicker, this._renderClearButton()]);\n  }\n\n  _rightFooterComponents() {\n    return (\n      <button\n        key=\"done\"\n        style={{order: 100}}\n        onClick={this._onDone}\n        className=\"btn btn-emphasis\"\n        disabled={this.state.pendingSave}\n      >\n        Done\n      </button>\n    );\n  }\n\n  _onChangeDuration = (event) => {\n    SchedulerActions.changeDuration(event.target.value.split(\"|\"))\n  }\n\n  _onDone = () => {\n    const proposals = ProposedTimeCalendarStore.proposals();\n    // NOTE: This gets dispatched to the main window\n    const {draftClientId} = NylasEnv.getWindowProps()\n    SchedulerActions.confirmChoices({proposals, draftClientId});\n    // Make sure the action gets to the main window then close this one.\n    setTimeout(() => { NylasEnv.close() }, 10)\n  }\n\n  _onCalendarMouseUp({time, currentView}) {\n    if (currentView !== NylasCalendar.WEEK_VIEW) { return }\n    if (time) {\n      SchedulerActions.addToProposedTimeBlock(time);\n    }\n    SchedulerActions.endProposedTimeBlock();\n    return\n  }\n\n  _onCalendarMouseMove({time, mouseIsDown, currentView}) {\n    if (!time || !mouseIsDown || currentView !== NylasCalendar.WEEK_VIEW) { return }\n    SchedulerActions.addToProposedTimeBlock(time);\n    return\n  }\n\n  _onCalendarMouseDown({time, currentView}) {\n    if (!time || currentView !== NylasCalendar.WEEK_VIEW) { return }\n    SchedulerActions.startProposedTimeBlock(time);\n    return\n  }\n\n  render() {\n    return (\n      <NylasCalendar\n        dataSource={this._dataSource()}\n        bannerComponents={this._bannerComponents()}\n        footerComponents={this._footerComponents()}\n        onCalendarMouseUp={this._onCalendarMouseUp}\n        onCalendarMouseDown={this._onCalendarMouseDown}\n        onCalendarMouseMove={this._onCalendarMouseMove}\n      />\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/email-b64-images.es6",
    "content": "export const b64Images = {\n  location: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OTVFNzg0RkNFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OTVFNzg0RkRFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3MkU3QjA3RkVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3MkU3QjA4MEVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkDmWKcAAAFpSURBVHjahNNPKERRFMfxNzNK+RMrFhYSpWzobbHQrFBiIwspkdEsRBaUHSsbUhbIgkgaWVkqlCSpiVmJmqJsiTJIGt+r36vrzZuc+tR999175tx73oSy2axjIplMOopqjKEZHwjDLNrFOr7MItd1fxcXOH+jBfu4wzLOUYJOTKMDA3j2NtgJarGHHUz4El9jBYfYRB8yjsrzYgppa3M5hjGEMjyhBw2IBlVgDrWocUS/2K7zt6n0B5yhFwf+Cl5xo3EcrahBvRLE9O4eFd4mO8EnSq1xSCJa9653xXgLOoIZN+IEa2rnpZKY9m1oXaXuKidBWr1f0vMMErqDlOYK1a1E0BFM/5tQ5WtfynqO65hXQQku8IgFJ38MYlvdyEnwgll0oTtgs7mHImzZk2HfomPMYxV11nxMH9SIqsybwMQcTnGk56g+qklrzgnqghff6FfJGfV/3OrOvwkc/Y1H1dpbXVxg/AgwALwFT41l/QPcAAAAAElFTkSuQmCC`,\n\n  description: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzJFN0IwN0RFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzJFN0IwN0VFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3MkU3QjA3QkVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3MkU3QjA3Q0VENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpVUSsoAAABkSURBVHjaYvz//z8DJYBx1AAGliNHjsQAaW4g/kWiXk4gfsECJAKBWASIv5JogCAQXwYZEAXyChDj8wsjDrF/IAN+UhQGQIwvDBihcjuB+BYuA/CFARNU7hYuA0ZTIhUMAAgwABS7Llh6+cz5AAAAAElFTkSuQmCC`,\n\n  time: `data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzJFN0IwNzlFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzJFN0IwN0FFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3MkU3QjA3N0VENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3MkU3QjA3OEVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PmHXWPIAAAIoSURBVHjaXJM7iFNBFIbn3oRkQ3ZJIEUIKrgiaLEoFoLFilupIMq2iqBgI1jYKCJooQg+EGwtBbFRRNBKrES2UAgICoKgEbFIookkhJt34vcP58plB75M7syZ/zzmTFCtVt2mcRguBkGwDIvz+XwGTdY24Ba0ksZh4n8Ab+B5GIYRh29Op9M1Dq+nUqkXrB+CH3A6KZBOCH2BDAf3MNeazabr9XpLiH0vl8sf8/n87clkcpa9x7AF7iYjeAmLsJzJZGqdTse1Wi2H9wvD4fBovV7Xf0ckj7BZgzs2e4FVOAYrGMira7fbDiGH9wVmHE+cIvJ5BsFbpnvwJBa4Dk/hbyyAV3nT/pI3CkPX7XYdNZGAlq5YxKsS2Km8ZDQajVwURS6dTvuQGQXz6lEkJqDxHk5JIAeftTKbzdx4PI6N9LMNhvqW90aj4YVt/ydsj4s4jz2Z51igBzdgr6Lq9/s+FUtvHtdgALt1UEbFYtGHqgZi/Ty8gmuIrysK1UfpMrbCr9BCOSkBpcCdu1Kp5OuB0G8OPZAQ37VsNusKhYJPh7EfngW0slr3tdViIHWlol5QuBIdDAZOhyuVisvlchK/is0l7EuBvYV3di374looHXkSuhk60efO9womn+CE0ouLeMTa84PuXunoNjQrItXFbuK4Hb5vtfnfypH1gwr3DR7CQQR2CMTOMG9Yy6uJLm9+TBpdOGCv7Zy16oLt/THPu+Br8jX+E2AAonoDF+eSc+MAAAAASUVORK5CYII=`,\n};\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/email-images.json",
    "content": "{\n  \"location\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OTVFNzg0RkNFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OTVFNzg0RkRFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3MkU3QjA3RkVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3MkU3QjA4MEVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkDmWKcAAAFpSURBVHjahNNPKERRFMfxNzNK+RMrFhYSpWzobbHQrFBiIwspkdEsRBaUHSsbUhbIgkgaWVkqlCSpiVmJmqJsiTJIGt+r36vrzZuc+tR999175tx73oSy2axjIplMOopqjKEZHwjDLNrFOr7MItd1fxcXOH+jBfu4wzLOUYJOTKMDA3j2NtgJarGHHUz4El9jBYfYRB8yjsrzYgppa3M5hjGEMjyhBw2IBlVgDrWocUS/2K7zt6n0B5yhFwf+Cl5xo3EcrahBvRLE9O4eFd4mO8EnSq1xSCJa9653xXgLOoIZN+IEa2rnpZKY9m1oXaXuKidBWr1f0vMMErqDlOYK1a1E0BFM/5tQ5WtfynqO65hXQQku8IgFJ38MYlvdyEnwgll0oTtgs7mHImzZk2HfomPMYxV11nxMH9SIqsybwMQcTnGk56g+qklrzgnqghff6FfJGfV/3OrOvwkc/Y1H1dpbXVxg/AgwALwFT41l/QPcAAAAAElFTkSuQmCC\",\n  \"description\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzJFN0IwN0RFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzJFN0IwN0VFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3MkU3QjA3QkVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3MkU3QjA3Q0VENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpVUSsoAAABkSURBVHjaYvz//z8DJYBx1AAGliNHjsQAaW4g/kWiXk4gfsECJAKBWASIv5JogCAQXwYZEAXyChDj8wsjDrF/IAN+UhQGQIwvDBihcjuB+BYuA/CFARNU7hYuA0ZTIhUMAAgwABS7Llh6+cz5AAAAAElFTkSuQmCC\",\n  \"time\": \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NzJFN0IwNzlFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NzJFN0IwN0FFRDYzMTFFNTlEOUREQkE1NjlFNkRFRjMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo3MkU3QjA3N0VENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo3MkU3QjA3OEVENjMxMUU1OUQ5RERCQTU2OUU2REVGMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PmHXWPIAAAIoSURBVHjaXJM7iFNBFIbn3oRkQ3ZJIEUIKrgiaLEoFoLFilupIMq2iqBgI1jYKCJooQg+EGwtBbFRRNBKrES2UAgICoKgEbFIookkhJt34vcP58plB75M7syZ/zzmTFCtVt2mcRguBkGwDIvz+XwGTdY24Ba0ksZh4n8Ab+B5GIYRh29Op9M1Dq+nUqkXrB+CH3A6KZBOCH2BDAf3MNeazabr9XpLiH0vl8sf8/n87clkcpa9x7AF7iYjeAmLsJzJZGqdTse1Wi2H9wvD4fBovV7Xf0ckj7BZgzs2e4FVOAYrGMira7fbDiGH9wVmHE+cIvJ5BsFbpnvwJBa4Dk/hbyyAV3nT/pI3CkPX7XYdNZGAlq5YxKsS2Km8ZDQajVwURS6dTvuQGQXz6lEkJqDxHk5JIAeftTKbzdx4PI6N9LMNhvqW90aj4YVt/ydsj4s4jz2Z51igBzdgr6Lq9/s+FUtvHtdgALt1UEbFYtGHqgZi/Ty8gmuIrysK1UfpMrbCr9BCOSkBpcCdu1Kp5OuB0G8OPZAQ37VsNusKhYJPh7EfngW0slr3tdViIHWlol5QuBIdDAZOhyuVisvlchK/is0l7EuBvYV3di374looHXkSuhk60efO9womn+CE0ouLeMTa84PuXunoNjQrItXFbuK4Hb5vtfnfypH1gwr3DR7CQQR2CMTOMG9Yy6uJLm9+TBpdOGCv7Zy16oLt/THPu+Br8jX+E2AAonoDF+eSc+MAAAAASUVORK5CYII=\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/event-datetime-input.jsx",
    "content": "import React from 'react';\nimport moment from 'moment-timezone'\nimport {DateUtils} from 'nylas-exports'\n\nfunction getDateFormat(type) {\n  if (type === \"date\") {\n    return \"YYYY-MM-DD\";\n  } else if (type === \"time\") {\n    return \"HH:mm:ss\"\n  }\n  return null\n}\n\nexport default class EventDatetimeInput extends React.Component {\n  static displayName = \"EventDatetimeInput\";\n\n  static propTypes = {\n    name: React.PropTypes.string,\n    value: React.PropTypes.number.isRequired,\n    onChange: React.PropTypes.func.isRequired,\n    reversed: React.PropTypes.bool,\n  };\n\n  constructor(props) {\n    super(props);\n    this._datePartStrings = {time: \"\", date: \"\"};\n  }\n\n  _onDateChange() {\n    const {date, time} = this._datePartStrings;\n    const format = `${getDateFormat(\"date\")} ${getDateFormat(\"time\")}`;\n    const newDate = moment.tz(`${date} ${time}`, format, DateUtils.timeZone).unix();\n    this.props.onChange(newDate)\n  }\n\n  _renderInput(type) {\n    const unixDate = this.props.value;\n    const str = moment.unix(unixDate).tz(DateUtils.timeZone).format(getDateFormat(type))\n    this._datePartStrings[type] = unixDate != null ? str : null;\n    return (\n      <input\n        type={type}\n        ref={type}\n        name={`${this.props.name}-${type}`}\n        value={this._datePartStrings[type]}\n        onChange={e => {\n          this._datePartStrings[type] = e.target.value;\n          this._onDateChange()\n        }}\n      />\n    )\n  }\n\n  render() {\n    if (this.props.reversed) {\n      return (\n        <span className=\"datetime-input-container\">\n          {this._renderInput(\"time\")} on {this._renderInput(\"date\")}\n        </span>\n      )\n    }\n    return (\n      <span className=\"datetime-input-container\">\n        {this._renderInput(\"date\")} at {this._renderInput(\"time\")}\n      </span>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/event-prep-helper.es6",
    "content": "export const prepareEvent = (inEvent, draft, proposals = []) => {\n  const event = inEvent\n  if (!event.title) {\n    event.title = \"\";\n  }\n\n  event.participants = draft.participants().map((contact) => {\n    return {\n      name: contact.name,\n      email: contact.email,\n      status: \"noreply\",\n    }\n  })\n\n  if (proposals.length > 0) {\n    event.end = null\n    event.start = null\n  }\n  return event;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/new-event-card-container.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport {Utils, Event} from 'nylas-exports';\n\nimport SchedulerActions from '../scheduler-actions'\nimport NewEventCard from './new-event-card'\nimport NewEventPreview from './new-event-preview'\nimport {PLUGIN_ID} from '../scheduler-constants'\nimport NewEventHelper from './new-event-helper'\nimport RemoveEventHelper from './remove-event-helper'\n\n/**\n * When you're creating an event you can either be creating:\n *\n * 1. A Meeting Request with a specific start and end time\n * 2. OR a `pendingEvent` template that has a set of proposed times.\n *\n * Both are represented by a `pendingEvent` object on the `metadata` that\n * holds the JSONified representation of the `Event`\n *\n * #2 adds a set of `proposals` on the metadata object.\n *\n * This component is an OverlayedComponent.\n *\n * The SchedulerComposerExtension::_insertNewEventCard will call\n * `EditorAPI::insert`. We pass `insert` a React element. Under the hood,\n * the `<OverlaidComponent>` wrapper will actually place an \"anchor\" tag\n * and absolutely position our element over that anchor tag.\n *\n * This component is also decorated with the `InflatesDraftClientId`\n * decorator. The former is necessary for OverlaidComponents to work. The\n * latter provides us with up-to-date `draft` and `session` props by\n * inflating a `draftClientId`.\n *\n * If the Anchor is deleted, or cut, then the `<OverlaidComponents />`\n * element will unmount the `NewEventCardContainer`.\n *\n * If the anchor re-appears (via paste or some other mechanism), then this\n * component will be re-mounted.\n *\n * We use the mounting and unmounting of this component as signals to add or\n * remove the metadata on the draft.\n */\nexport default class NewEventCardContainer extends Component {\n  static displayName = 'NewEventCardContainer';\n\n  static propTypes = {\n    draft: PropTypes.object.isRequired,\n    session: PropTypes.object.isRequired,\n    style: PropTypes.object,\n    isPreview: PropTypes.bool,\n  }\n\n  componentDidMount() {\n    this._unlisten = SchedulerActions.confirmChoices.listen(::this._onConfirmChoices);\n    NewEventHelper.restoreOrCreateEvent(this.props.session)\n  }\n\n  componentWillUnmount() {\n    if (this._unlisten) {\n      this._unlisten();\n    }\n    RemoveEventHelper.hideEventData(this.props.session)\n  }\n\n  _onConfirmChoices({proposals = [], draftClientId}) {\n    const {draft} = this.props;\n\n    if (draft.clientId !== draftClientId) {\n      return;\n    }\n\n    const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};\n    if (proposals.length === 0) {\n      delete metadata.proposals;\n    } else {\n      metadata.proposals = proposals;\n    }\n    this.props.session.changes.addPluginMetadata(PLUGIN_ID, metadata);\n  }\n\n  _getEvent() {\n    const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);\n    if (metadata && metadata.pendingEvent) {\n      return new Event().fromJSON(metadata.pendingEvent || {});\n    }\n    return null\n  }\n\n  _updateEvent = (newData) => {\n    const {draft, session} = this.props;\n\n    const newEvent = Object.assign(this._getEvent().clone(), newData);\n    const newEventJSON = newEvent.toJSON();\n\n    const metadata = draft.metadataForPluginId(PLUGIN_ID);\n    if (!Utils.isEqual(metadata.pendingEvent, newEventJSON)) {\n      metadata.pendingEvent = newEventJSON;\n      session.changes.addPluginMetadata(PLUGIN_ID, metadata);\n    }\n  }\n\n  _removeEvent = () => {\n    // This will delete the metadata, but it won't remove the anchor from\n    // the contenteditable. We also need to remove the event card.\n    RemoveEventHelper.deleteEventData(this.props.session);\n    SchedulerActions.removeEventCard();\n  }\n\n  render() {\n    const {style, isPreview} = this.props;\n    const event = this._getEvent();\n    let card = false;\n\n    if (isPreview) {\n      return <NewEventPreview draft={this.props.draft} />\n    }\n\n    if (event) {\n      card = (\n        <NewEventCard\n          event={event}\n          ref=\"newEventCard\"\n          draft={this.props.draft}\n          onRemove={this._removeEvent}\n          onChange={this._updateEvent}\n          onParticipantsClick={() => {}}\n        />\n      )\n    }\n    return (\n      <div className=\"new-event-card-container\" style={style}>\n        {card}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/new-event-card.jsx",
    "content": "import React from 'react';\nimport moment from 'moment-timezone'\nimport {\n  RetinaImg,\n  DatePicker,\n  TimePicker,\n  TabGroupRegion,\n} from 'nylas-component-kit'\n\nimport {\n  DateUtils,\n  Calendar,\n  AccountStore,\n  DatabaseStore} from 'nylas-exports';\n\nimport {PLUGIN_ID} from '../scheduler-constants'\nimport NewEventHelper from './new-event-helper'\nimport ProposedTimeList from './proposed-time-list'\n\nexport default class NewEventCard extends React.Component {\n  static displayName = 'NewEventCard';\n\n  static propTypes = {\n    event: React.PropTypes.object.isRequired,\n    draft: React.PropTypes.object.isRequired,\n    onChange: React.PropTypes.func.isRequired,\n    onRemove: React.PropTypes.func.isRequired,\n    onParticipantsClick: React.PropTypes.func.isRequired,\n  };\n\n  constructor(props) {\n    super(props);\n    this._mounted = false;\n    this.state = {\n      calendars: [],\n    };\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    const email = this.props.draft.from[0].email\n    this._loadCalendarsForEmail(email);\n  }\n\n  componentWillReceiveProps(newProps) {\n    const email = newProps.draft.from[0].email\n    this._loadCalendarsForEmail(email);\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  _loadCalendarsForEmail(email) {\n    if (this._lastEmail === email) {\n      return\n    }\n    this._lastEmail = email\n\n    const account = AccountStore.accountForEmail(email);\n    DatabaseStore.findAll(Calendar, {accountId: account.id})\n    .then((calendars) => {\n      if (!this._mounted || !calendars) { return }\n      this.setState({calendars: calendars.filter(c => !c.readOnly)})\n    });\n  }\n\n  _renderIcon(name) {\n    return (<span className=\"field-icon\">\n      <RetinaImg name={name} mode={RetinaImg.Mode.ContentPreserve} />\n    </span>)\n  }\n\n  _renderParticipants() {\n    return this.props.draft.participants().map(r => r.displayName()).join(\", \")\n  }\n\n  _renderCalendarPicker() {\n    if (this.state.calendars.length <= 1) {\n      return false;\n    }\n    const calOpts = this.state.calendars.map(cal =>\n      <option key={cal.serverId} value={cal.serverId}>{cal.name}</option>\n    );\n    const onChange = (e) => { this.props.onChange({calendarId: e.target.value}) }\n    return (\n      <div className=\"row calendar\">\n        {this._renderIcon(\"ic-eventcard-calendar@2x.png\")}\n        <select onChange={onChange}>{calOpts}</select>\n      </div>\n    )\n  }\n\n  _onProposeTimes = () => {\n    NewEventHelper.launchCalendarWindow(this.props.draft.clientId);\n  }\n\n  _eventStart() {\n    return moment.unix(this.props.event.start || moment().unix())\n  }\n\n  _eventEnd() {\n    return moment.unix(this.props.event.end || moment().unix())\n  }\n\n  _onChangeDay = (newTimestamp) => {\n    const newDay = moment(newTimestamp)\n    const start = this._eventStart()\n    const end = this._eventEnd()\n    start.year(newDay.year())\n    end.year(newDay.year())\n    start.dayOfYear(newDay.dayOfYear())\n    end.dayOfYear(newDay.dayOfYear())\n    this.props.onChange({start: start.unix(), end: end.unix()})\n  }\n\n  _onChangeStartTime = (newTimestamp) => {\n    const newTime = moment(newTimestamp)\n    const start = this._eventStart()\n    const end = this._eventEnd()\n    start.hour(newTime.hour())\n    start.minute(newTime.minute())\n    let newEnd = moment(end)\n    if (end.isSameOrBefore(start)) {\n      const leftInDay = moment(start).endOf('day').diff(start)\n      const move = Math.min(leftInDay, moment.duration(1, 'hour').asMilliseconds());\n      newEnd = moment(start).add(move, 'ms')\n    }\n    this.props.onChange({start: start.unix(), end: newEnd.unix()})\n  }\n\n  _onChangeEndTime = (newTimestamp) => {\n    const newTime = moment(newTimestamp)\n    const start = this._eventStart()\n    const end = this._eventEnd()\n    end.hour(newTime.hour())\n    end.minute(newTime.minute())\n    let newStart = moment(start)\n    if (start.isSameOrAfter(end)) {\n      const sinceDay = end.diff(moment(end).startOf('day'))\n      const move = Math.min(sinceDay, moment.duration(1, 'hour').asMilliseconds());\n      newStart = moment(end).subtract(move, 'ms');\n    }\n    this.props.onChange({end: end.unix(), start: newStart.unix()})\n  }\n\n  _renderTimePicker() {\n    const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);\n    if (metadata && metadata.proposals) {\n      return (\n        <ProposedTimeList\n          event={this.props.event}\n          draft={this.props.draft}\n          proposals={metadata.proposals}\n        />\n      )\n    }\n\n    const startVal = (this.props.event.start) * 1000;\n    const endVal = (this.props.event.end) * 1000;\n    return (\n      <div className=\"row time\">\n        {this._renderIcon(\"ic-eventcard-time@2x.png\")}\n        <span>\n          <TimePicker\n            value={startVal}\n            onChange={this._onChangeStartTime}\n          />\n          to\n          <TimePicker\n            value={endVal}\n            relativeTo={startVal}\n            onChange={this._onChangeEndTime}\n          />\n          <span className=\"timezone\">\n            {moment().tz(DateUtils.timeZone).format(\"z\")}\n          </span>\n          &nbsp;\n          on\n          &nbsp;\n          <DatePicker value={startVal} onChange={this._onChangeDay} />\n        </span>\n      </div>\n    )\n  }\n\n  _renderSuggestPrompt() {\n    const metadata = this.props.draft.metadataForPluginId(PLUGIN_ID);\n    if (metadata && metadata.proposals) {\n      return (\n        <div className=\"suggest-times\">\n          <a onClick={this._onProposeTimes}>Select different times…</a>\n        </div>\n      )\n    }\n    return (\n      <div className=\"suggest-times\">\n        or: <a onClick={this._onProposeTimes}>Suggest several times…</a>\n      </div>\n    )\n  }\n\n  render() {\n    return (\n      <div className=\"new-event-card\">\n        <TabGroupRegion>\n          <div className=\"remove-button\" onClick={this.props.onRemove}>✕</div>\n          <div className=\"row title\">\n            {this._renderIcon(\"ic-eventcard-description@2x.png\")}\n            <input\n              type=\"text\"\n              name=\"title\"\n              className=\"event-title\"\n              placeholder=\"Add an event title\"\n              value={this.props.event.title || \"\"}\n              onChange={e => this.props.onChange({title: e.target.value})}\n            />\n          </div>\n\n          {this._renderTimePicker()}\n\n          {this._renderSuggestPrompt()}\n\n          {this._renderCalendarPicker()}\n\n          <div className=\"row recipients\">\n            {this._renderIcon(\"ic-eventcard-people@2x.png\")}\n            <div onClick={this.props.onParticipantsClick()}>{this._renderParticipants()}</div>\n          </div>\n\n          <div className=\"row location\">\n            {this._renderIcon(\"ic-eventcard-location@2x.png\")}\n            <input\n              type=\"text\"\n              name=\"location\"\n              placeholder=\"Add a location\"\n              value={this.props.event.location}\n              onChange={e => this.props.onChange({location: e.target.value})}\n            />\n          </div>\n\n          <div className=\"row description\">\n            {this._renderIcon(\"ic-eventcard-notes@2x.png\")}\n\n            <textarea\n              ref=\"description\"\n              name=\"description\"\n              placeholder=\"Add notes\"\n              value={this.props.event.description}\n              onChange={e => this.props.onChange({description: e.target.value})}\n            />\n          </div>\n        </TabGroupRegion>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/new-event-helper.es6",
    "content": "import moment from 'moment-round'\nimport {\n  Event,\n  Calendar,\n  DatabaseStore,\n} from 'nylas-exports'\n\nimport {PLUGIN_ID} from '../scheduler-constants'\n\nexport default class NewEventHelper {\n\n  // Extra level of indirection for testing\n  static now() {\n    return moment()\n  }\n\n  static launchCalendarWindow(draftClientId) {\n    NylasEnv.newWindow({\n      title: \"Calendar\",\n      hidden: true, // Displayed by ProposedTimePicker::componentDidMount\n      windowType: \"scheduler-calendar\",\n      windowKey: `scheduler-calendar-${draftClientId}`,\n      windowProps: {draftClientId},\n    });\n  }\n\n  // Sometimes we simply hide event data instead of fully destroying it.\n  // This happens when users toggle the scheduler icon or cut and paste\n  // the anchor in the contenteditable.\n  //\n  // We've kept the data on the metadata via the `RemoveEventHelper.hideEventData` method.\n  //\n  // If we can't find restoration data, we'll create a new event via\n  // `NewEventHelper.createNewEvent`\n  static restoreOrCreateEvent(session) {\n    const draft = session.draft()\n    const metadata = draft.metadataForPluginId(PLUGIN_ID);\n    if (metadata && (metadata.hiddenPendingEvent || metadata.pendingEvent)) {\n      metadata.pendingEvent = metadata.pendingEvent || metadata.hiddenPendingEvent\n      metadata.proposals = metadata.proposals || metadata.hiddenProposals\n      delete metadata.hiddenPendingEvent;\n      delete metadata.hiddenProposals\n      return session.changes.addPluginMetadata(PLUGIN_ID, metadata);\n    }\n    return NewEventHelper.createNewEvent(session)\n  }\n\n  static createNewEvent(session) {\n    if (!session) { return Promise.reject(\"Need session\") }\n    const draft = session.draft()\n    return DatabaseStore.findAll(Calendar, {accountId: draft.accountId})\n    .then((allCalendars) => {\n      if (allCalendars.length === 0) {\n        throw new Error(`Can't create an event. The Account \\\n${draft.accountId} has no calendars.`);\n      }\n\n      const cals = allCalendars.filter(c => !c.readOnly);\n\n      if (cals.length === 0) {\n        NylasEnv.showErrorDialog(`This account has no editable \\\ncalendars. We can't create an event for you. Please make sure you have an \\\neditable calendar with your account provider.`);\n        return Promise.reject();\n      }\n\n      const start = NewEventHelper.now().ceil(30, 'minutes');\n      const metadata = draft.metadataForPluginId(PLUGIN_ID) || {};\n      metadata.pendingEvent = new Event({\n        calendarId: cals[0].id,\n        start: start.unix(),\n        end: moment(start).add(1, 'hour').unix(),\n      }).toJSON();\n      return session.changes.addPluginMetadata(PLUGIN_ID, metadata);\n    })\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/new-event-preview.jsx",
    "content": "import moment from 'moment-timezone'\nimport React from 'react'\nimport {Event, DateUtils} from 'nylas-exports'\nimport b64Imgs from './email-images.json'\nimport {PLUGIN_ID} from '../scheduler-constants'\nimport {prepareEvent} from './event-prep-helper'\nimport ProposedTimeList from './proposed-time-list'\n\nconst TZ = moment.tz(DateUtils.timeZone).format(\"z\");\n\nexport default class NewEventPreview extends React.Component {\n  static propTypes = {\n    draft: React.PropTypes.object,\n  }\n\n  static defaultProps = {\n    draft: {},\n  }\n\n  static displyName = \"NewEventPreview\";\n\n  constructor(props) {\n    super(props);\n    this.state = this._stateFromProps(props)\n  }\n\n  componentWillReceiveProps(props) {\n    this.setState(this._stateFromProps(props))\n  }\n\n  _stateFromProps(props) {\n    const metadata = props.draft.metadataForPluginId(PLUGIN_ID);\n    const eventData = metadata.pendingEvent || metadata.hiddenPendingEvent;\n    const proposals = metadata.proposals || metadata.hiddenProposals || [];\n    let event;\n    if (eventData) {\n      event = prepareEvent(new Event().fromJSON(eventData), props.draft, proposals)\n    } else {\n      event = null\n    }\n    return {event, proposals}\n  }\n\n  _renderB64Img(name, styles = {}) {\n    let imgStyles = {\n      width: \"16px\",\n      height: \"16px\",\n      display: \"inline-block\",\n      marginRight: \"10px\",\n      backgroundRepeat: \"no-repeat\",\n      backgroundImage: `url('${b64Imgs[name]}')`,\n    }\n    imgStyles = Object.assign(imgStyles, styles);\n    return <div style={imgStyles} />\n  }\n\n  _renderEventInfo() {\n    const styles = {\n      fontSize: \"20px\",\n      fontWeight: 400,\n      margin: \"0 10px 15px 10px\",\n    }\n    const noteStyles = {\n      marginTop: \"12px\",\n      paddingLeft: \"40px\",\n    }\n    return (\n      <div className=\"new-event-preview\">\n        <h2 style={styles}>\n          {this._renderB64Img(\"description\", {verticalAlign: \"middle\"})}\n          {this.state.event.title}\n        </h2>\n        <span style={{margin: \"0 10px\"}}>\n          {this._renderB64Img(\"time\", {verticalAlign: \"super\"})}\n          {this._renderEventTime()}\n        </span>\n        <div style={noteStyles}>You will receive a calendar invite for this event shortly.</div>\n      </div>\n    )\n  }\n\n  _renderEventTime() {\n    const start = moment.unix(this.state.event.start)\n    const end = moment.unix(this.state.event.end).add(1, 'second')\n    const dayTxt = start.format(DateUtils.DATE_FORMAT_LLLL_NO_TIME)\n    const tz = (<span style={{fontSize: \"10px\", color: \"#aaa\"}}>{TZ}</span>);\n    const styles = {\n      display: \"inline-block\",\n    }\n    return <span style={styles}>{dayTxt}<br />{`${start.format(\"LT\")} – ${end.format(\"LT\")}`}{tz}</span>\n  }\n\n  _sEventPreviewWrap() {\n    return {\n      borderRadius: \"4px\",\n      border: \"1px solid rgba(0,0,0,0.15)\",\n      padding: \"15px\",\n      margin: \"10px 0\",\n      position: \"relative\",\n    }\n  }\n\n  render() {\n    if (!this.state.event) {\n      return false\n    }\n    if (this.state.proposals.length > 0) {\n      return (\n        <ProposedTimeList\n          draft={this.props.draft}\n          event={this.state.event}\n          inEmail\n          proposals={this.state.proposals}\n        />\n      )\n    }\n    return (\n      <div style={this._sEventPreviewWrap()}>\n        {this._renderEventInfo()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/proposed-time-list.jsx",
    "content": "import _ from 'underscore'\nimport moment from 'moment-timezone'\nimport React from 'react'\nimport {DateUtils} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit'\nimport b64Imgs from './email-images.json'\nimport {PLUGIN_URL} from '../scheduler-constants'\n\nconst TZ = moment.tz(DateUtils.timeZone).format(\"z\");\n\nexport default class ProposedTimeList extends React.Component {\n  static propTypes = {\n    draft: React.PropTypes.object,\n    event: React.PropTypes.object,\n    inEmail: React.PropTypes.bool,\n    proposals: React.PropTypes.array.isRequired,\n  }\n\n  static defaultProps = {\n    draft: {},\n    inEmail: false,\n  }\n\n  static displayName = \"ProposedTimeList\";\n\n  _proposalUrl(proposalId) {\n    const {clientId, accountId} = this.props.draft\n    return `${PLUGIN_URL}/scheduler/${accountId}/${clientId}/${proposalId}`\n  }\n\n  _renderB64Img(name) {\n    const imgStyles = {\n      width: \"16px\",\n      height: \"16px\",\n      display: \"inline-block\",\n      marginRight: \"10px\",\n      backgroundRepeat: \"no-repeat\",\n      backgroundImage: `url('${b64Imgs[name]}')`,\n    }\n    return <div style={imgStyles} />\n  }\n\n  _renderHeaderInEmail() {\n    const styles = {\n      fontSize: \"16px\",\n      fontWeight: 400,\n      margin: \"0 10px 15px 10px\",\n    }\n    return (\n      <div>\n        <h2 style={styles}>\n          {this._renderB64Img(\"description\")}\n          {this.props.event.title || this.props.draft.subject}\n        </h2>\n        <span style={{margin: \"0 10px\"}}>\n          {this._renderB64Img(\"time\")}\n          Select a time to schedule instantly:\n        </span>\n      </div>\n    )\n  }\n\n  _renderHeaderInCard() {\n    return (\n      <span>\n        <span className=\"field-icon\">\n          <RetinaImg\n            name=\"ic-eventcard-time.png\"\n            mode={RetinaImg.Mode.ContentPreserve}\n          />\n        </span>\n        <span>Proposed times:</span>\n      </span>\n    )\n  }\n\n  _sProposalTimeList() {\n    if (this.props.inEmail) {\n      return {\n        borderRadius: \"4px\",\n        border: \"1px solid rgba(0,0,0,0.15)\",\n        padding: \"15px\",\n        margin: \"10px 0\",\n        position: \"relative\",\n      }\n    }\n    return {\n      display: \"block\",\n      position: \"relative\",\n    }\n  }\n\n  _sProposalWrap() {\n    return {\n\n    }\n  }\n\n  _proposalsByDay() {\n    return _.groupBy(this.props.proposals, (p) => {\n      return moment.unix(p.start).dayOfYear()\n    })\n  }\n\n  _sProposalTable() {\n    return {\n      width: \"100%\",\n      textAlign: \"center\",\n      borderSpacing: \"0px\",\n    }\n  }\n\n  _sTD() {\n    return {\n      padding: \"0 10px\",\n    }\n  }\n\n  _sTH() {\n    return Object.assign({}, this._sTD(), {\n      fontSize: \"12px\",\n      color: \"#333333\",\n      textTransform: \"uppercase\",\n      fontWeight: 400,\n    });\n  }\n\n  _sTDInner(isLast) {\n    const styles = {\n      borderBottom: \"1px solid rgba(0,0,0,0.15)\",\n      borderRight: \"1px solid rgba(0,0,0,0.15)\",\n      borderLeft: \"1px solid rgba(0,0,0,0.15)\",\n      padding: \"10px 5px\",\n    }\n    if (isLast) {\n      styles.borderRadius = \"0 0 4px 4px\";\n    }\n    return styles\n  }\n\n  _sTHInner() {\n    return Object.assign({}, this._sTDInner(), {\n      borderTop: \"1px solid rgba(0,0,0,0.15)\",\n      borderRadius: \"4px 4px 0 0\",\n    });\n  }\n\n  _renderProposalTable() {\n    const byDay = this._proposalsByDay();\n    let maxLen = 0;\n    _.each(byDay, (ps) => {\n      maxLen = Math.max(maxLen, ps.length)\n    });\n\n    const trs = []\n    for (let i = -1; i < maxLen; i++) {\n      const tds = []\n      for (const dayNum of Object.keys(byDay)) {\n        if (i === -1) {\n          tds.push(\n            <th key={dayNum} style={this._sTH()}>\n              <div style={this._sTHInner()}>\n                {this._headerTextFromDay(dayNum)}\n              </div>\n            </th>\n          )\n        } else {\n          const proposal = byDay[dayNum][i]\n          if (proposal) {\n            const isLast = (i === maxLen - 1) || !byDay[dayNum][i + 1]\n\n            let timeText;\n            if (this.props.inEmail) {\n              const url = this._proposalUrl(proposal.id)\n              timeText = (\n                <a href={url} style={{textDecoration: \"none\"}}>\n                  {this._renderProposalTimeText(proposal)}\n                </a>\n              )\n            } else {\n              timeText = this._renderProposalTimeText(proposal)\n            }\n\n            tds.push(\n              <td key={proposal.id} style={this._sTD()}>\n                <div style={this._sTDInner(isLast)}>{timeText}</div>\n              </td>\n            )\n          } else {\n            tds.push(\n              <td key={i + dayNum} style={this._sTD()} />\n            )\n          }\n        }\n      }\n      trs.push(\n        <tr key={i}>{tds}</tr>\n      )\n    }\n\n    return (\n      <table\n        style={this._sProposalTable()}\n        className=\"proposed-time-table\"\n      >\n        <tbody>\n          {trs}\n        </tbody>\n      </table>\n    )\n  }\n\n  _renderProposalTimeText(proposal) {\n    const start = moment.unix(proposal.start).format(\"LT\")\n    const end = moment.unix(proposal.end).add(1, 'second').format(\"LT\")\n    const tz = <span style={{fontSize: \"10px\", color: \"#aaa\"}}>{TZ}</span>\n    const timestr = `${start} — ${end}`\n    return <span>{timestr}&nbsp;&nbsp;{tz}</span>\n  }\n\n  _headerTextFromDay(dayNum) {\n    return moment().dayOfYear(dayNum).format(\"ddd, MMM D\")\n  }\n\n  _sProposalsWrap() {\n    const styles = {\n      margin: \"10px 0\",\n    }\n    if (!this.props.inEmail) { styles.paddingLeft = \"48px\"; }\n    return styles\n  }\n\n  render() {\n    let header;\n\n    if (this.props.inEmail) {\n      header = this._renderHeaderInEmail()\n    } else {\n      header = this._renderHeaderInCard()\n    }\n\n    return (\n      <div style={this._sProposalTimeList()}>\n        {header}\n        <div style={this._sProposalsWrap()}>\n          {this._renderProposalTable()}\n        </div>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/remove-event-helper.es6",
    "content": "import {PLUGIN_ID} from '../scheduler-constants'\n\n/**\n * Removing a proposed event can happen through various mechansism.\n *\n * 1. The user click the \"X\" on the event card\n * 2. The user re-clicks the \"Proposed Event\" time card\n * 3. The user removes (permanently or temporarily) the event card's\n *    anchor from the contenteditable DOM\n *\n * In scenario 1, we want to fully remove the metadata from the object.\n *\n * In scenarios 2, and 3, we want to keep a copy of the data around since\n * it's likely the user will want to at some point restore their work.\n */\nexport default class RemoveEventHelper {\n  static deleteEventData(session) {\n    session.changes.addPluginMetadata(PLUGIN_ID, {});\n  }\n\n  static hideEventData(session) {\n    const draft = session.draft()\n    const metadata = draft.metadataForPluginId(PLUGIN_ID);\n    if (metadata) {\n      metadata.hiddenPendingEvent = metadata.pendingEvent\n      metadata.hiddenProposals = metadata.proposals\n      delete metadata.pendingEvent;\n      delete metadata.proposals\n      session.changes.addPluginMetadata(PLUGIN_ID, metadata);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/scheduler-composer-button.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport {\n  Actions,\n  APIError,\n  NylasAPI,\n  NylasAPIHelpers,\n} from 'nylas-exports'\nimport {Menu, RetinaImg} from 'nylas-component-kit'\nimport moment from 'moment'\n\nimport {PLUGIN_ID, PLUGIN_NAME} from '../scheduler-constants'\nimport NewEventHelper from './new-event-helper'\nimport SchedulerActions from '../scheduler-actions'\n\n// moment-round upon require patches `moment` with new functions.\nrequire('moment-round')\n\nconst MEETING_REQUEST = \"Send a meeting request…\"\nconst PROPOSAL = \"Propose times to meet…\"\n\nexport default class SchedulerComposerButton extends React.Component {\n  static displayName = \"SchedulerComposerButton\";\n\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = {enabled: false};\n    this._unsubscribes = [];\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (this.state !== nextState) ||\n      (this._hasPendingEvent(nextProps) !== this._hasPendingEvent(this.props));\n  }\n\n  _hasPendingEvent(props) {\n    const metadata = props.draft.metadataForPluginId(PLUGIN_ID);\n    return metadata && metadata.pendingEvent\n  }\n\n  // Helper method that will render the contents of our popover.\n  _renderPopover() {\n    const headerComponents = [\n      <span key=\"header\">I&#39;d like to:</span>,\n    ];\n    const items = [\n      MEETING_REQUEST,\n      PROPOSAL,\n    ];\n    const idFn = (item) => item\n    return (\n      <Menu\n        className=\"scheduler-picker\"\n        items={items}\n        itemKey={idFn}\n        itemContent={idFn}\n        headerComponents={headerComponents}\n        defaultSelectedIndex={-1}\n        onSelect={this._onSelectItem}\n      />\n    )\n  }\n\n  _onSelectItem = (item) => {\n    // We unfortunately can't pass the draft and the session since the\n    // NewEventCard will be rendered in a location that won't get\n    // top-down prop-based updates when the draft changes. Downstream\n    // we use the `InflatesDraftClientId` decorator to make the component\n    // responsive to draft updates.\n    //\n    // The insertNewEventCard action will run\n    // `EditorAPI::insertOverlaidComponent`. This will render the\n    // corresponding anchor into the contenteditable. Once the anchor\n    // has been placed, `NewEventCardContainer` will render and on\n    // mount, ensure the metadata is there.\n    SchedulerActions.insertNewEventCard({\n      draftClientId: this.props.draft.clientId,\n    })\n\n    if (item === PROPOSAL) {\n      NewEventHelper.launchCalendarWindow(this.props.draft.clientId)\n    }\n    Actions.closePopover()\n  }\n\n  _onClick = () => {\n    if (this._hasEvent()) {\n      // When the event anchor is removed, the `NewEventCardContainer`\n      // will unmount. When it unmounts, it will call\n      // `RemoveEventHelper.hideEventData`\n      SchedulerActions.removeEventCard()\n    } else {\n      NylasAPIHelpers.authPlugin(PLUGIN_ID, PLUGIN_NAME,\n          this.props.draft.accountId).catch(this._onPluginAuthError);\n\n      const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()\n      Actions.openPopover(\n        this._renderPopover(),\n        {originRect: buttonRect, direction: 'up'}\n      )\n    }\n  }\n\n  _onPluginAuthError(error) {\n    let title = \"Error\"\n    let msg = `Unfortunately scheduling is not currently available. \\\nPlease try again later.\\n\\nError: ${error}`\n    if (!(error instanceof APIError)) {\n      NylasEnv.reportError(error);\n    } else if (error.statusCode === 400) {\n      NylasEnv.reportError(error);\n    } else if (NylasAPI.TimeoutErrorCodes.includes(error.statusCode)) {\n      title = \"Offline\"\n      msg = `Scheduling does not work offline. Please try again when you come back online.`\n    }\n    Actions.closePopover()\n    NylasEnv.showErrorDialog({title, message: msg});\n  }\n\n  _now() {\n    return moment()\n  }\n\n  _hasEvent() {\n    return this._hasPendingEvent(this.props);\n  }\n\n  render() {\n    let more = \"\"\n    if (!this._hasEvent()) {\n      more = (\n        <span>\n          &nbsp;\n          <RetinaImg\n            name=\"icon-composer-dropdown.png\"\n            mode={RetinaImg.Mode.ContentIsMask}\n          />\n        </span>\n      )\n    }\n    return (\n      <button\n        className={`btn btn-toolbar btn-scheduler ${this._hasEvent() ? \"btn-enabled\" : \"\"}`}\n        onClick={this._onClick}\n        title=\"Schedule an event…\"\n      >\n        <RetinaImg\n          url=\"nylas://composer-scheduler/assets/ic-composer-scheduler@2x.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        {more}\n      </button>\n    )\n  }\n}\n\nSchedulerComposerButton.containerRequired = false;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/composer/scheduler-composer-extension.es6",
    "content": "import {\n  Event,\n  Actions,\n  ComposerExtension,\n} from 'nylas-exports'\n\nimport {PLUGIN_ID} from '../scheduler-constants'\nimport SchedulerActions from '../scheduler-actions'\nimport {prepareEvent} from './event-prep-helper'\n\n/**\n * Inserts the set of Proposed Times into the body of the HTML email.\n *\n */\nexport default class SchedulerComposerExtension extends ComposerExtension {\n\n  static TAG_NAME = \"scheduler-card\";\n\n  static editingActions() {\n    return [{\n      action: SchedulerActions.insertNewEventCard,\n      callback: SchedulerComposerExtension._insertNewEventCard,\n    }, {\n      action: SchedulerActions.removeEventCard,\n      callback: SchedulerComposerExtension._removeEventCard,\n    }]\n  }\n\n  static _removeEventCard({editor}) {\n    const el = editor.rootNode.querySelector(\".new-event-card-container\")\n    if (el) {\n      el.parentNode.removeChild(el);\n    }\n  }\n\n  static _insertNewEventCard({editor, actionArg}) {\n    if (editor.draftClientId === actionArg.draftClientId) { return }\n    if (editor.rootNode.querySelector('.new-event-card-container')) { return }\n    editor.rootNode.focus()\n    const containerRect = editor.rootNode.getBoundingClientRect()\n    editor.insertCustomComponent(\"NewEventCardContainer\", {\n      className: \"new-event-card-container\",\n      style: {width: containerRect.width - 44},\n    })\n  }\n\n  // We must set the `preparedEvent` to be exactly what could be posted to\n  // the /events endpoint of the API.\n  static _cleanEventJSON(rawJSON) {\n    const json = rawJSON;\n    delete json.client_id;\n    delete json.id;\n    json.when = {\n      start_time: json._start,\n      end_time: json._end,\n    }\n    delete json._start\n    delete json._end\n    return json\n  }\n\n  static applyTransformsForSending({draft}) {\n    const metadata = draft.metadataForPluginId(PLUGIN_ID)\n    if (metadata && metadata.pendingEvent) {\n      if (metadata.proposals && metadata.proposals.length > 0) {\n        Actions.recordUserEvent(\"Meeting Times Proposed\", {\n          numItems: metadata.proposals.length,\n        })\n      } else {\n        Actions.recordUserEvent(\"Meeting Request Scheduled\")\n      }\n      const nextEvent = new Event().fromJSON(metadata.pendingEvent);\n      const nextEventPrepared = prepareEvent(nextEvent, draft, metadata.proposals);\n      metadata.pendingEvent = SchedulerComposerExtension._cleanEventJSON(nextEventPrepared.toJSON());\n      metadata.uid = draft.clientId;\n      draft.applyPluginMetadata(PLUGIN_ID, metadata);\n    }\n  }\n\n  static unapplyTransformsForSending() {\n    return;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/main.es6",
    "content": "import {\n  WorkspaceStore,\n  ComponentRegistry,\n  ExtensionRegistry,\n  CustomContenteditableComponents,\n} from 'nylas-exports'\n\nimport {HasTutorialTip} from 'nylas-component-kit';\n\nimport ProposedTimeEvent from './calendar/proposed-time-event'\nimport ProposedTimePicker from './calendar/proposed-time-picker'\nimport NewEventCardContainer from './composer/new-event-card-container'\nimport SchedulerComposerButton from './composer/scheduler-composer-button';\nimport ProposedTimeCalendarStore from './proposed-time-calendar-store'\nimport SchedulerComposerExtension from './composer/scheduler-composer-extension';\n\nconst SchedulerComposerButtonWithTip = HasTutorialTip(SchedulerComposerButton, {\n  title: \"Create a new meeting request\",\n  instructions: \"Click the <b>calendar icon</b> to send calendar invites or propose times to meet&mdash;all without leaving the inbox!\",\n});\n\nexport function activate() {\n  if (NylasEnv.getWindowType() === 'scheduler-calendar') {\n    ProposedTimeCalendarStore.activate()\n\n    NylasEnv.getCurrentWindow().setMinimumSize(480, 250)\n\n    ComponentRegistry.register(ProposedTimeEvent, {\n      role: 'Calendar:Event',\n    });\n\n    ComponentRegistry.register(ProposedTimePicker, {\n      location: WorkspaceStore.Location.Center,\n    });\n  } else {\n    ComponentRegistry.register(SchedulerComposerButtonWithTip, {\n      role: 'Composer:ActionButton',\n    });\n\n    ExtensionRegistry.Composer.register(SchedulerComposerExtension);\n\n    CustomContenteditableComponents.register(\"NewEventCardContainer\", NewEventCardContainer);\n  }\n}\n\nexport function serialize() {\n}\n\nexport function deactivate() {\n  if (NylasEnv.getWindowType() === 'scheduler-calendar') {\n    ProposedTimeCalendarStore.deactivate()\n    ComponentRegistry.unregister(ProposedTimeEvent);\n    ComponentRegistry.unregister(ProposedTimePicker);\n  } else {\n    ComponentRegistry.unregister(SchedulerComposerButtonWithTip);\n    ExtensionRegistry.Composer.unregister(SchedulerComposerExtension);\n    CustomContenteditableComponents.unregister(\"NewEventCardContainer\");\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/proposal.es6",
    "content": "import {Utils} from 'nylas-exports'\n\nexport default class Proposal {\n  constructor(args = {}) {\n    this.id = Utils.generateFakeServerId();\n    Object.assign(this, args);\n\n    // This field is used by edgehill-server to lookup the proposals.\n    this.proposalId = this.id;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/proposed-time-calendar-store.es6",
    "content": "import _ from 'underscore'\nimport NylasStore from 'nylas-store'\nimport {Event, Utils} from 'nylas-exports'\n\nimport Proposal from './proposal'\nimport SchedulerActions from './scheduler-actions'\nimport {CALENDAR_ID} from './scheduler-constants'\n\n// moment-round upon require patches `moment` with new functions.\nconst moment = require('moment-round')\n\n/**\n * Maintains the creation of \"Proposed Times\" when scheduling with people.\n *\n * The proposed times are displayed in various calendar views.\n *\n */\nclass ProposedTimeCalendarStore extends NylasStore {\n  DURATIONS = [\n    [30, 'minutes', '30 min'],\n    [1, 'hour', '1 hr'],\n    [1.5, 'hours', '1½ hr'],\n    [2, 'hours', '2 hr'],\n    [2.5, 'hours', '2½ hr'],\n    [3, 'hours', '3 hr'],\n  ]\n\n  activate() {\n    this._proposals = []\n    this._resetDragBuffer();\n    this._pendingSave = false;\n    this._duration = this.DURATIONS[0] // 30 min\n    this.unsubscribers = [\n      SchedulerActions.changeDuration.listen(this._onChangeDuration),\n      SchedulerActions.clearProposals.listen(this._onClearProposals),\n      SchedulerActions.addToProposedTimeBlock.listen(this._onAddToBlock),\n      SchedulerActions.startProposedTimeBlock.listen(this._onStartBlock),\n      SchedulerActions.endProposedTimeBlock.listen(this._onEndBlock),\n      SchedulerActions.removeProposedTime.listen(this._onRemoveProposedTime),\n    ]\n  }\n\n  pendingSave() {\n    return this._pendingSave\n  }\n\n  deactivate() {\n    this.unsubscribers.forEach(unsub => unsub())\n  }\n\n  currentDuration() {\n    return this._duration\n  }\n\n  _dragBufferAsEvent() {\n    if (!this._dragBuffer.anchor) {\n      return []\n    }\n    const {start, end} = this._dragBuffer;\n    const event = new Event().fromJSON({\n      title: \"Availability Block\",\n      calendar_id: CALENDAR_ID,\n      when: {\n        object: \"timespan\",\n        start_time: start,\n        end_time: end,\n      },\n    })\n    event.proposalType = \"availability\"\n    return [event];\n  }\n\n  proposalsAsEvents() {\n    return _.map(this._proposals, (p) => {\n      const event = new Event().fromJSON({\n        title: \"Proposed Time\",\n        calendar_id: CALENDAR_ID,\n        when: {\n          object: \"timespan\",\n          start_time: p.start,\n          end_time: p.end,\n        },\n      })\n      event.proposalType = \"proposal\";\n      return event\n    }).concat(this._dragBufferAsEvent());\n  }\n\n  _convertBufferToProposedTimes() {\n    const bounds = this._dragBuffer;\n    const minMoment = moment.unix(bounds.start);\n    minMoment.floor(30, 'minutes');\n\n    const maxMoment = moment.unix(bounds.end);\n    maxMoment.ceil(30, 'minutes');\n\n    if (maxMoment.isSame(minMoment)) {\n      maxMoment.add(30, 'minutes')\n    }\n\n    const overlapBoundsTest = {start: bounds.start + 1, end: bounds.end - 1}\n    this._proposals = _.reject(this._proposals, (p) =>\n      Utils.overlapsBounds(overlapBoundsTest, p)\n    )\n\n    const blockSize = this._duration.slice(0, 2)\n    blockSize[0] /= 1; // moment requires a number\n    const isMinBlockSize = (bounds.end - bounds.start) >= moment.duration(...blockSize).as('seconds');\n    while (minMoment.isBefore(maxMoment)) {\n      const start = minMoment.unix();\n      minMoment.add(blockSize[0], blockSize[1]);\n      const end = minMoment.unix();\n      if (end > bounds.end && isMinBlockSize) { break; }\n      this._proposals.push(new Proposal({start, end}))\n    }\n  }\n\n  _resetDragBuffer() {\n    this._dragBuffer = {\n      anchor: null,\n      start: Number.MAX_SAFE_INTEGER,\n      end: 0,\n    }\n  }\n\n  _updateDragBuffer(newT) {\n    const {anchor, start, end} = this._dragBuffer\n\n    // Ensure that the drag buffer stays within the same day\n    newT.dayOfYear(anchor.dayOfYear())\n    newT.year(anchor.year())\n\n    const newTUnix = newT.unix()\n    const anchorUnix = anchor.unix();\n    this._dragBuffer = {\n      anchor,\n      start: Math.min(newTUnix, anchorUnix),\n      end: Math.max(newTUnix, anchorUnix),\n    }\n    if (this._dragBuffer.start !== start || this._dragBuffer.end !== end) {\n      this.trigger()\n    }\n  }\n\n  _onStartBlock = (newT) => {\n    this._resetDragBuffer();\n    this._dragBuffer.anchor = newT.floor(30, 'minutes')\n  }\n\n  _onAddToBlock = (newT) => {\n    this._updateDragBuffer(newT.round(30, 'minutes'));\n  }\n\n  _onEndBlock = () => {\n    if (this._dragBuffer.anchor) {\n      this._convertBufferToProposedTimes()\n      this._resetDragBuffer();\n      this.trigger();\n    }\n  }\n\n  _onChangeDuration = (newDuration) => {\n    this._duration = newDuration\n    this.trigger()\n  }\n\n  _onClearProposals = () => {\n    this._proposals = [];\n    this.trigger();\n  }\n\n  _onRemoveProposedTime = ({start}) => {\n    const startInt = parseInt(start, 10);\n    this._proposals = _.reject(this._proposals, (p) =>\n      p.start <= startInt && p.end > startInt\n    )\n    this.trigger()\n  }\n\n  proposals() {\n    return this._proposals\n  }\n}\n\nexport default new ProposedTimeCalendarStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/scheduler-actions.es6",
    "content": "import _ from 'underscore'\nimport {Reflux} from 'nylas-exports'\n\nconst globalSchedulerActions = Reflux.createActions([\n  'confirmChoices',\n])\n\nconst localSchedulerActions = Reflux.createActions([\n  'changeDuration',\n  'clearProposals',\n  'removeEventCard',\n  'insertNewEventCard',\n  'removeProposedTime',\n  'addToProposedTimeBlock',\n  'startProposedTimeBlock',\n  'endProposedTimeBlock',\n])\n\nconst SchedulerActions = _.extend(localSchedulerActions, globalSchedulerActions)\n\nfor (const key of Object.keys(SchedulerActions)) {\n  SchedulerActions[key].sync = true\n}\n\nNylasEnv.registerGlobalActions({\n  pluginName: \"SchedulerActions\",\n  actions: globalSchedulerActions,\n});\n\nexport default SchedulerActions\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/lib/scheduler-constants.es6",
    "content": "import plugin from '../package.json'\n\nlet pluginId = plugin.name;\nlet pluginUrl = plugin.serverUrl[NylasEnv.config.get(\"env\")];\n\nif (NylasEnv.inSpecMode()) {\n  pluginId = \"TEST_SCHEDULER_PLUGIN_ID\"\n  pluginUrl = \"https://edgehill-test.nylas.com\"\n}\n\nexport const PLUGIN_ID = pluginId;\nexport const PLUGIN_URL = pluginUrl;\nexport const PLUGIN_NAME = \"Quick Schedule\"\nexport const CALENDAR_ID = \"QUICK SCHEDULE\"\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/package.json",
    "content": "{\n  \"name\": \"composer-scheduler\",\n  \"title\":\"Scheduler\",\n  \"description\": \"Add event invites to emails easily.\",\n  \"main\": \"./lib/main\",\n  \"isHiddenOnPluginsPage\": true,\n  \"version\": \"0.1.0\",\n  \"serverUrl\": {\n    \"development\": \"http://localhost:5009\",\n    \"staging\": \"https://edgehill-staging.nylas.com\",\n    \"production\": \"https://edgehill.nylas.com\"\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"icon\": \"./icon.png\",\n  \"isOptional\": true,\n  \"supportedEnvs\": [\"development\", \"staging\", \"production\"],\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"scheduler-calendar\": true,\n    \"thread-popout\": true\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/spec/composer-scheduler-spec-helper.es6",
    "content": "import {\n  DatabaseStore,\n  DraftStore,\n  Message,\n  Actions,\n  Calendar,\n  Contact,\n} from 'nylas-exports'\nimport {activate, deactivate} from '../lib/main'\nimport {PLUGIN_ID} from '../lib/scheduler-constants'\n\nexport const DRAFT_CLIENT_ID = \"draft-client-id\"\n\nexport const testCalendars = () => [new Calendar({\n  clientId: \"client-1\",\n  servierId: \"server-1\",\n  name: \"Test Calendar\",\n})]\n\n// Must be a `function` so `this` can be overridden by caller's `apply`\nexport const prepareDraft = function prepareDraft() {\n  spyOn(NylasEnv, \"isMainWindow\").andReturn(true);\n  spyOn(NylasEnv, \"getWindowType\").andReturn(\"root\");\n  spyOn(Actions, \"setMetadata\").andCallFake((draft, pluginId, metadata) => {\n    if (!this.session) {\n      throw new Error(\"Setup test session first\")\n    }\n    this.session.changes.addPluginMetadata(PLUGIN_ID, metadata);\n  })\n  activate();\n\n  const draft = new Message({\n    clientId: DRAFT_CLIENT_ID,\n    draft: true,\n    body: \"\",\n    accountId: window.TEST_ACCOUNT_ID,\n    from: [new Contact({email: window.TEST_ACCOUNT_EMAIL})],\n  })\n\n  spyOn(DatabaseStore, \"run\").andCallFake((query) => {\n    if (query.objectClass() === Calendar.name) {\n      return Promise.resolve(testCalendars())\n    } else if (query.objectClass() === Message.name) {\n      return Promise.resolve(draft)\n    }\n    return Promise.resolve()\n  })\n  this.session = DraftStore._createSession(DRAFT_CLIENT_ID, draft);\n}\n\nexport const cleanupDraft = function cleanupDraft() {\n  DraftStore._cleanupAllSessions()\n  deactivate()\n}\n\nexport const setupCalendars = function setupCalendars() {\n  const aid = window.TEST_ACCOUNT_ID\n  spyOn(DatabaseStore, \"findAll\").andCallFake((klass, {accountId}) => {\n    expect(klass).toBe(Calendar);\n    expect(accountId).toBe(aid);\n    const cals = [\n      new Calendar({accountId: aid, readOnly: false, name: 'a'}),\n      new Calendar({accountId: aid, readOnly: true, name: 'b'}),\n    ]\n    return Promise.resolve(cals);\n  })\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/spec/new-event-card-spec.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport ReactTestUtils from 'react-addons-test-utils';\n\nimport {\n  Event,\n  DatabaseStore,\n} from 'nylas-exports'\n\nimport {PLUGIN_ID} from '../lib/scheduler-constants'\nimport NewEventCard from '../lib/composer/new-event-card'\nimport NewEventCardContainer from '../lib/composer/new-event-card-container'\n\nimport Proposal from '../lib/proposal'\nimport SchedulerActions from '../lib/scheduler-actions'\n\nimport {\n  DRAFT_CLIENT_ID,\n  prepareDraft,\n  testCalendars,\n  cleanupDraft,\n} from './composer-scheduler-spec-helper'\n\nconst now = window.testNowMoment\n\nxdescribe('NewEventCard', function newEventCard() {\n  beforeEach(() => {\n    this.session = null\n    prepareDraft.call(this)\n\n    waitsFor(() => {\n      return this.session._draft\n    })\n\n    runs(() => {\n      this.eventCardContainer = ReactTestUtils.renderIntoDocument(\n        <NewEventCardContainer draft={this.session.draft()} session={this.session} />\n      );\n    })\n  });\n\n  afterEach(() => {\n    cleanupDraft()\n  })\n\n  const setNewTestEvent = () => {\n    if (!this.session) {\n      throw new Error(\"Setup test session first\")\n    }\n    this.session.changes.addPluginMetadata(PLUGIN_ID, {\n      uid: DRAFT_CLIENT_ID,\n      pendingEvent: new Event({\n        calendarId: \"TEST_CALENDAR_ID\",\n        title: \"\",\n        start: now().unix(),\n        end: now().add(1, 'hour').unix(),\n      }).toJSON(),\n    });\n\n    this.eventCardContainer = ReactTestUtils.renderIntoDocument(\n      <NewEventCardContainer draft={this.session.draft()} session={this.session} />\n    );\n  }\n\n  const getPendingEvent = () =>\n    this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent\n\n  it(\"creates a new event card\", () => {\n    const el = ReactTestUtils.findRenderedComponentWithType(this.eventCardContainer,\n        NewEventCardContainer);\n    expect(el instanceof NewEventCardContainer).toBe(true)\n  });\n\n  it(\"doesn't render if there's no event on metadata\", () => {\n    expect(this.eventCardContainer.refs.newEventCard).not.toBeDefined();\n  });\n\n  it(\"renders the event card when an event is created\", () => {\n    setNewTestEvent()\n    expect(this.eventCardContainer.refs.newEventCard).toBeDefined();\n    expect(this.eventCardContainer.refs.newEventCard instanceof NewEventCard).toBe(true);\n  });\n\n  it(\"loads the calendars for email\", () => {\n    setNewTestEvent()\n    waitsFor(() =>\n      this.eventCardContainer.refs.newEventCard.state.calendars.length > 0\n    )\n    runs(() => {\n      const newCardRef = this.eventCardContainer.refs.newEventCard;\n      expect(newCardRef.state.calendars).toEqual(testCalendars());\n    });\n  });\n\n  it(\"removes the event and clears metadata\", () => {\n    setNewTestEvent()\n\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass,\n      this.eventCardContainer);\n    const rmBtn = ReactDOM.findDOMNode($(\"remove-button\")[0]);\n\n    // The event is there before clicking remove\n    expect(this.eventCardContainer.refs.newEventCard).toBeDefined()\n    expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).toBeDefined()\n\n    ReactTestUtils.Simulate.click(rmBtn);\n\n    // The event has been removed from metadata\n    expect(this.session.draft().metadataForPluginId(PLUGIN_ID).pendingEvent).not.toBeDefined()\n  });\n\n  it(\"properly updates the event\", () => {\n    setNewTestEvent()\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass,\n      this.eventCardContainer);\n    const title = ReactDOM.findDOMNode($(\"event-title\")[0]);\n\n    // The event has the old title\n    expect(getPendingEvent().title).toBe(\"\")\n\n    title.value = \"Test\"\n    ReactTestUtils.Simulate.change(title);\n\n    // The event has the new title\n    expect(getPendingEvent().title).toBe(\"Test\")\n  });\n\n  it(\"updates the day\", () => {\n    setNewTestEvent()\n    const eventCard = this.eventCardContainer.refs.newEventCard;\n\n    // The event has the default day\n    const nowUnix = now().unix()\n    expect(getPendingEvent()._start).toBe(nowUnix)\n\n    // The event has the new day\n    const newDay = now().add(2, 'days');\n    eventCard._onChangeDay(newDay.valueOf());\n\n    expect(getPendingEvent()._start).toBe(newDay.unix())\n  });\n\n  it(\"updates the time properly\", () => {\n    setNewTestEvent()\n    const eventCard = this.eventCardContainer.refs.newEventCard;\n\n    const oldEnd = now().add(1, 'hour').unix()\n    expect(getPendingEvent()._start).toBe(now().unix())\n    expect(getPendingEvent()._end).toBe(oldEnd)\n\n    const newStart = now().subtract(1, 'hour');\n    eventCard._onChangeStartTime(newStart.valueOf());\n\n    expect(getPendingEvent()._start).toBe(newStart.unix())\n    expect(getPendingEvent()._end).toBe(oldEnd)\n  });\n\n  it(\"adjusts the times to prevent invalid times\", () => {\n    setNewTestEvent()\n    const eventCard = this.eventCardContainer.refs.newEventCard;\n\n    const start0 = now();\n    const end0 = now().add(1, 'hour');\n\n    const start1 = now().add(2, 'hours');\n    const expectedEnd1 = now().add(3, 'hours');\n\n    const expectedStart2 = now().subtract(3, 'hours');\n    const end2 = now().subtract(2, 'hours');\n\n    // The event has the start times\n    expect(getPendingEvent()._start).toBe(start0.unix())\n    expect(getPendingEvent()._end).toBe(end0.unix())\n\n    eventCard._onChangeStartTime(start1.valueOf());\n\n    // The event the new start time and also moved the end to match\n    expect(getPendingEvent()._start).toBe(start1.unix())\n    expect(getPendingEvent()._end).toBe(expectedEnd1.unix())\n\n    eventCard._onChangeEndTime(end2.valueOf());\n\n    // The event the new end time and also moved the start to match\n    expect(getPendingEvent()._start).toBe(expectedStart2.unix())\n    expect(getPendingEvent()._end).toBe(end2.unix())\n  });\n\n  it(\"switches calendars when the from account changes\", () => {\n    // TODO\n  });\n\n  describe(\"Inserting proposed times\", () => {\n    beforeEach(() => {\n      const draft = this.session.draft()\n      spyOn(DatabaseStore, \"find\").andReturn(Promise.resolve(draft));\n      const start = now().add(1, 'hour').unix();\n      const end = now().add(2, 'hours').unix();\n      this.proposals = [new Proposal({start, end})]\n\n      runs(() => {\n        SchedulerActions.confirmChoices({\n          proposals: this.proposals,\n          draftClientId: DRAFT_CLIENT_ID,\n        });\n      })\n      waitsFor(() => {\n        const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);\n        return (metadata.proposals || []).length > 0;\n      })\n    });\n\n    it(\"inserts proposed times on metadata\", () => {\n      const metadata = this.session.draft().metadataForPluginId(PLUGIN_ID);\n      expect(JSON.stringify(metadata.proposals)).toEqual(JSON.stringify(this.proposals));\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/spec/proposed-time-picker-spec.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport ReactTestUtils from 'react-addons-test-utils'\nimport {NylasCalendar} from 'nylas-component-kit'\nimport {WorkspaceStore} from 'nylas-exports'\n\nimport ProposedTimePicker from '../lib/calendar/proposed-time-picker'\nimport TestProposalDataSource from './test-proposal-data-source'\nimport ProposedTimeCalendarStore from '../lib/proposed-time-calendar-store'\nimport {activate, deactivate} from '../lib/main'\n\nconst WeekView = NylasCalendar.WeekView;\nconst now = window.testNowMoment\n\n/**\n * This tests the ProposedTimePicker as an integration test of the picker,\n * associated calendar object, the ProposedTimeCalendarStore, and stubbed\n * ProposedTimeCalendarDataSource\n *\n */\ndescribe('ProposedTimePicker', function proposedTimePicker() {\n  beforeEach(() => {\n    WorkspaceStore.defineSheet('Main', {root: true},\n      {popout: ['Center']})\n    spyOn(NylasEnv, \"getWindowType\").andReturn(\"scheduler-calendar\");\n    spyOn(WeekView.prototype, \"_now\").andReturn(now());\n    spyOn(NylasCalendar.prototype, \"_now\").andReturn(now());\n    activate()\n\n    this.testSrc = new TestProposalDataSource()\n    spyOn(ProposedTimePicker.prototype, \"_dataSource\").andReturn(this.testSrc)\n    this.picker = ReactTestUtils.renderIntoDocument(\n      <ProposedTimePicker />\n    )\n    this.weekView = ReactTestUtils.findRenderedComponentWithType(this.picker, WeekView);\n  });\n\n  afterEach(() => {\n    deactivate()\n  })\n\n  it(\"renders a proposed time picker in week view\", () => {\n    const picker = ReactTestUtils.findRenderedComponentWithType(this.picker, ProposedTimePicker);\n    const weekView = ReactTestUtils.findRenderedComponentWithType(this.picker, WeekView);\n    expect(picker instanceof ProposedTimePicker).toBe(true);\n    expect(weekView instanceof WeekView).toBe(true);\n  });\n\n  // NOTE: We manually fire the SchedulerActions since we've tested the\n  // mouse click to time conversion in the nylas-calendar\n\n  it(\"creates a proposal on click\", () => {\n    this.picker._onCalendarMouseDown({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n    expect(ProposedTimeCalendarStore.proposals().length).toBe(1)\n    expect(ProposedTimeCalendarStore.proposalsAsEvents().length).toBe(1)\n    const proposals = $(\"proposal\");\n    const events = $(\"calendar-event\");\n    expect(events.length).toBe(1);\n    expect(proposals.length).toBe(1);\n\n    // It's not an availability block but a full blown proposal\n    expect($(\"availability\").length).toBe(0);\n  });\n\n  it(\"creates the time picker for the correct timespan\", () => {\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n    const title = $(\"title\");\n    expect(ReactDOM.findDOMNode(title[0]).innerHTML).toBe(\"March 13 - March 19 2016\");\n  });\n\n  it(\"creates a block of proposals on drag down\", () => {\n    this.picker._onCalendarMouseDown({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseMove({\n      time: now().add(30, 'minutes'),\n      mouseIsDown: true,\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseMove({\n      time: now().add(60, 'minutes'),\n      mouseIsDown: true,\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n\n    // Ensure that we don't see any proposals\n    expect(ProposedTimeCalendarStore.proposals().length).toBe(0)\n    let proposalEls = $(\"proposal\");\n    expect(proposalEls.length).toBe(0);\n\n    // But we DO see the drag block event\n    expect(ProposedTimeCalendarStore.proposalsAsEvents().length).toBe(1)\n    let events = $(\"calendar-event\");\n    expect($(\"availability\").length).toBe(1);\n    expect(events.length).toBe(1);\n\n    this.picker._onCalendarMouseUp({\n      time: now().add(90, 'minutes'),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    // Now that we've moused up, this should convert them into proposals\n    const proposals = ProposedTimeCalendarStore.proposals()\n    expect(proposals.length).toBe(3)\n    expect(ProposedTimeCalendarStore.proposalsAsEvents().length).toBe(3)\n    proposalEls = $(\"proposal\");\n    events = $(\"calendar-event\");\n    expect(events.length).toBe(3);\n    expect(proposalEls.length).toBe(3);\n\n    const times = proposals.map((p) =>\n      [p.start, p.end]\n    );\n\n    expect(times).toEqual([\n      [now().unix(), now().add(30, 'minutes').unix()],\n      [now().add(30, 'minutes').unix(),\n        now().add(60, 'minutes').unix()],\n      [now().add(60, 'minutes').unix(),\n        now().add(90, 'minutes').unix()],\n    ]);\n  });\n\n  it(\"creates a block of proposals on drag up\", () => {\n    this.picker._onCalendarMouseDown({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseMove({\n      time: now().subtract(30, 'minutes'),\n      mouseIsDown: true,\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseMove({\n      time: now().subtract(60, 'minutes'),\n      mouseIsDown: true,\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now().subtract(90, 'minutes'),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    const proposals = ProposedTimeCalendarStore.proposals()\n    const times = proposals.map((p) =>\n      [p.start, p.end]\n    );\n\n    expect(times).toEqual([\n      [now().subtract(90, 'minutes').unix(),\n        now().subtract(60, 'minutes').unix()],\n      [now().subtract(60, 'minutes').unix(),\n        now().subtract(30, 'minutes').unix()],\n      [now().subtract(30, 'minutes').unix(),\n        now().unix()],\n    ]);\n  });\n\n  it(\"removes proposals when clicked on\", () => {\n    // This created a proposal\n    this.picker._onCalendarMouseDown({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    // See the proposal is there\n    expect(ProposedTimeCalendarStore.proposals().length).toBe(1)\n\n    // Now let's find and click it.\n    // This also tests to make sure it actually rendered\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n    const removeBtn = $(\"rm-time proposal\");\n    expect(removeBtn.length).toBe(1)\n    ReactTestUtils.Simulate.mouseDown(ReactDOM.findDOMNode(removeBtn[0]))\n\n    // Now see that it's gone!\n    expect(ProposedTimeCalendarStore.proposals().length).toBe(0)\n    // And gone from the DOM too.\n    expect($(\"proposal\").length).toBe(0);\n    // And didn't turn into an availability block or something dumb\n    expect($(\"availability\").length).toBe(0);\n  });\n\n  it(\"can clear all of the proposals\", () => {\n    // This created a proposal\n    this.picker._onCalendarMouseDown({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    // See the proposal is there\n    expect(ProposedTimeCalendarStore.proposals().length).toBe(1)\n\n    // Find the clear button\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n    const clearBtns = $(\"clear-proposed-times\");\n    expect(clearBtns.length).toBe(1);\n\n    // Click it\n    ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(clearBtns[0]))\n\n    // Ensure no more proposals\n    expect(ProposedTimeCalendarStore.proposals().length).toBe(0)\n    // And nothing still rendered\n    expect($(\"proposal\").length).toBe(0);\n    expect($(\"availability\").length).toBe(0);\n  });\n\n  it(\"can change the duration\", () => {\n    // Find the duration picker.\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n    const pickerEls = $(\"duration-picker-select\");\n    expect(pickerEls.length).toBe(1);\n\n    // Starts with default duration\n    const d30Min = ProposedTimeCalendarStore.DURATIONS[0]\n    expect(ProposedTimeCalendarStore._duration).toEqual(d30Min)\n\n    const pickerEl = ReactDOM.findDOMNode(pickerEls[0]);\n    pickerEl.value = \"1.5|hours|1½ hr\"\n    ReactTestUtils.Simulate.change(pickerEl)\n\n    const dHrHalf = ProposedTimeCalendarStore.DURATIONS[2]\n    dHrHalf[0] = `${dHrHalf[0]}` // convert to string\n    expect(ProposedTimeCalendarStore._duration).toEqual(dHrHalf)\n  });\n\n  it(\"creates a block of proposals with a longer duration\", () => {\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n\n    // Create a single proposal with the default 30 min duration.\n    this.picker._onCalendarMouseDown({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    // It's 30 min long\n    const proposals = ProposedTimeCalendarStore.proposals()\n    const times = proposals.map((p) => [p.start, p.end]);\n    expect(times).toEqual([\n      [now().unix(),\n        now().add(30, 'minutes').unix()],\n    ]);\n\n    // Change duration to 2.5 hours\n    const pickerEl = ReactDOM.findDOMNode($(\"duration-picker-select\")[0]);\n    pickerEl.value = \"2.5|hours|2½ hr\"\n    ReactTestUtils.Simulate.change(pickerEl)\n\n    // Click a new event\n    this.picker._onCalendarMouseDown({\n      time: now().add(2, 'hours'),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now().add(2, 'hours'),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    // It should have added a 2.5 hour long event and left the original\n    // event alone\n    const p2 = ProposedTimeCalendarStore.proposals()\n    const t2 = p2.map((p) => [p.start, p.end]);\n    expect(t2).toEqual([\n      [now().unix(),\n        now().add(30, 'minutes').unix()],\n      [now().add(2, 'hours').unix(),\n        now().add(4.5, 'hours').unix()],\n    ]);\n  });\n\n  it(\"overrides events so they don't overlap\", () => {\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.picker);\n    this.picker._onCalendarMouseDown({\n      time: now(),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now().add(1, 'hour'),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    // Creates two proposals.\n    const proposals = ProposedTimeCalendarStore.proposals()\n    const times = proposals.map((p) => [p.start, p.end]);\n    expect(times).toEqual([\n      [now().unix(),\n        now().add(30, 'minutes').unix()],\n      [now().add(30, 'minutes').unix(),\n        now().add(60, 'minutes').unix()],\n    ]);\n\n    // Change the duration to 2 hours\n    const pickerEl = ReactDOM.findDOMNode($(\"duration-picker-select\")[0]);\n    pickerEl.value = \"2|hours|2 hr\"\n    ReactTestUtils.Simulate.change(pickerEl)\n\n    // Click and drag overlapping the first of the original events.\n    this.picker._onCalendarMouseDown({\n      time: now().subtract(1.5, 'hours'),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n    this.picker._onCalendarMouseUp({\n      time: now().add(20, 'minutes'),\n      currentView: NylasCalendar.WEEK_VIEW,\n    })\n\n    // See that there's only 1 new event with the correct time and it\n    // exhchanged it with the old one.\n    //\n    // It left the non overlapping one alone.\n    const p2 = ProposedTimeCalendarStore.proposals()\n    const t2 = p2.map((p) => [p.start, p.end]);\n    expect(t2).toEqual([\n      [now().add(30, 'minutes').unix(),\n        now().add(60, 'minutes').unix()],\n      [now().subtract(1.5, 'hours').unix(),\n        now().add(30, 'minutes').unix()],\n    ]);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/spec/scheduler-composer-button-spec.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport ReactTestUtils from 'react-addons-test-utils'\n\nimport {APIError, Actions, NylasAPIHelpers} from 'nylas-exports'\nimport {PLUGIN_ID, PLUGIN_NAME} from '../lib/scheduler-constants'\n\nimport SchedulerComposerButton from '../lib/composer/scheduler-composer-button'\n\nimport NewEventHelper from '../lib/composer/new-event-helper'\n\nimport SchedulerActions from '../lib/scheduler-actions'\n\nimport {\n  prepareDraft,\n  cleanupDraft,\n  setupCalendars,\n} from './composer-scheduler-spec-helper'\n\nconst now = window.testNowMoment;\n\nxdescribe('SchedulerComposerButton', function schedulerComposerButton() {\n  beforeEach(() => {\n    this.session = null\n    spyOn(Actions, \"openPopover\").andCallThrough();\n    spyOn(Actions, \"closePopover\").andCallThrough();\n    spyOn(NylasEnv, \"reportError\")\n    spyOn(NylasEnv, \"showErrorDialog\")\n    spyOn(NewEventHelper, \"now\").andReturn(now())\n\n    prepareDraft.call(this)\n\n    waitsFor(() => {\n      return this.session._draft\n    })\n\n    runs(() => {\n      this.schedulerBtn = ReactTestUtils.renderIntoDocument(\n        <SchedulerComposerButton draft={this.session.draft()} session={this.session} />\n      );\n    })\n  });\n\n  afterEach(() => {\n    cleanupDraft()\n  })\n\n  const spyAuthSuccess = () => {\n    spyOn(NylasAPIHelpers, \"authPlugin\").andCallFake((pluginId, pluginName, accountId) => {\n      expect(pluginId).toBe(PLUGIN_ID);\n      expect(pluginName).toBe(PLUGIN_NAME);\n      expect(accountId).toBe(window.TEST_ACCOUNT_ID);\n      return Promise.resolve();\n    })\n  }\n\n  it(\"loads the draft and renders the button\", () => {\n    const el = ReactTestUtils.findRenderedComponentWithType(this.schedulerBtn,\n        SchedulerComposerButton);\n    expect(el instanceof SchedulerComposerButton).toBe(true)\n  });\n\n  const testForError = () => {\n    runs(() => {\n      ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(this.schedulerBtn));\n    })\n    waitsFor(() =>\n      NylasEnv.showErrorDialog.calls.length > 0\n    );\n    runs(() => {\n      const picker = document.querySelector(\".scheduler-picker\")\n      expect(Actions.openPopover).toHaveBeenCalled();\n      expect(Actions.closePopover).toHaveBeenCalled();\n      expect(picker).toBe(null);\n    })\n  }\n\n  it(\"errors on 400 error and reports\", () => {\n    const err = new APIError({statusCode: 400});\n    spyOn(NylasAPIHelpers, \"authPlugin\").andReturn(Promise.reject(err));\n    testForError(err);\n    runs(() => {\n      expect(NylasEnv.reportError).toHaveBeenCalledWith(err);\n    })\n  });\n\n  it(\"errors on unexpected errors and reports\", () => {\n    const err = new Error(\"OH NO\");\n    spyOn(NylasAPIHelpers, \"authPlugin\").andReturn(Promise.reject(err));\n    testForError(err);\n    runs(() => {\n      expect(NylasEnv.reportError).toHaveBeenCalledWith(err);\n    })\n  });\n\n  it(\"errors on offline, but doesn't report\", () => {\n    const err = new APIError({statusCode: 0});\n    spyOn(NylasAPIHelpers, \"authPlugin\").andReturn(Promise.reject(err));\n    testForError(err);\n    runs(() => {\n      expect(NylasEnv.reportError).not.toHaveBeenCalled();\n    })\n  });\n\n  describe(\"auth success\", () => {\n    beforeEach(() => {\n      spyAuthSuccess();\n      ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(this.schedulerBtn));\n      const items = document.querySelectorAll(\".scheduler-picker .item\");\n      this.meetingRequestBtn = items[0];\n      this.proposalBtn = items[1];\n    });\n\n    it(\"renders the popover on click\", () => {\n      // The popover renders outside the scope of the component.\n      const picker = document.querySelector(\".scheduler-picker\")\n      expect(Actions.openPopover).toHaveBeenCalled();\n      expect(picker).toBeDefined();\n    });\n\n    it(\"auths the plugin on click\", () => {\n      expect(NylasAPIHelpers.authPlugin).toHaveBeenCalled()\n      expect(NylasAPIHelpers.authPlugin.calls.length).toBe(1)\n    });\n\n    it(\"fires the scheduler action to insert the anchor into the contenteditable\", () => {\n      setupCalendars();\n      spyOn(SchedulerActions, \"insertNewEventCard\")\n      runs(() => {\n        ReactTestUtils.Simulate.mouseDown(this.meetingRequestBtn);\n      })\n      waitsFor(() =>\n        SchedulerActions.insertNewEventCard.calls.length > 0\n      );\n      runs(() => {\n        expect(Actions.closePopover).toHaveBeenCalled();\n        expect(SchedulerActions.insertNewEventCard.calls.length).toBe(1)\n        expect(NylasEnv.showErrorDialog).not.toHaveBeenCalled()\n      })\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/spec/scheduler-composer-extension-spec.es6",
    "content": "import {PLUGIN_ID} from '../lib/scheduler-constants'\nimport {\n  prepareDraft,\n  setupCalendars,\n  cleanupDraft,\n} from './composer-scheduler-spec-helper'\nimport NewEventHelper from '../lib/composer/new-event-helper'\nimport SchedulerComposerExtension from '../lib/composer/scheduler-composer-extension'\n\nconst now = window.testNowMoment;\n\nxdescribe('SchedulerComposerExtension', function schedulerComposerExtension() {\n  beforeEach(() => {\n    this.session = null\n    // Will eventually fill this.session\n    prepareDraft.call(this);\n\n    waitsFor(() => {\n      return this.session._draft\n    })\n\n    // Note: Needs to be in a `runs` block so it happens after the async\n    // activities of `prepareDraft`\n    runs(() => {\n      setupCalendars.call(this);\n      spyOn(NewEventHelper, \"now\").andReturn(now())\n      NewEventHelper.restoreOrCreateEvent(this.session)\n    })\n    waitsFor(() =>\n      this.session.draft().metadataForPluginId(PLUGIN_ID)\n    )\n  });\n\n  afterEach(() => {\n    cleanupDraft()\n  })\n\n  // TODO: This moved to OverlaidComponents\n  // describe(\"Inserting a new event\", () => {\n  //   beforeEach(() => {\n  //     this.nextDraft = SchedulerComposerExtension.applyTransformsForSending({\n  //       draft: this.session.draft(),\n  //     });\n  //   });\n  //\n  //   it(\"Inserts the proposted-time-list\", () => {\n  //     expect(this.nextDraft.body).toMatch(/new-event-preview/);\n  //   });\n  //\n  //   it(\"Has the correct start and end times in the body\", () => {\n  //     const startStr = moment.unix(now().unix()).format(\"LT\")\n  //     const endStr = moment.unix(now().add(1, 'hour').unix()).format(\"LT\")\n  //\n  //     const re = new RegExp(`Tuesday, March 15, 2016 <br\\/>${startStr} – ${endStr}`)\n  //\n  //     // NOTE: These are supposed to render in local time. Make sure we\n  //     // test for the local timezone of the test setup.\n  //     expect(this.nextDraft.body).toMatch(re);\n  //   });\n  //\n  //   it(\"Doesn't include proposed times\", () => {\n  //     expect(this.nextDraft.body).not.toMatch(/proposed-time-table/);\n  //   });\n  // });\n  //\n  // describe(\"When proposals are prsent\", () => {\n  //   it(\"inserts the proposals into the draft body\", () => {\n  //     const start = now().add(1, 'hour').unix();\n  //     const end = now().add(2, 'hours').unix();\n  //\n  //     const startStr = moment.unix(start).format(\"LT\")\n  //     const endStr = moment.unix(end).format(\"LT\")\n  //\n  //     const re = new RegExp(`${startStr} — ${endStr}`)\n  //\n  //     const draft = new Message({body: ''})\n  //     draft.applyPluginMetadata(PLUGIN_ID, {\n  //       pendingEvent: new Event(),\n  //       proposals: [new Proposal({start, end})],\n  //     })\n  //\n  //     const nextDraft = SchedulerComposerExtension.applyTransformsForSending({draft});\n  //     expect(nextDraft.body).not.toMatch(/new-event-preview/);\n  //     expect(nextDraft.body).toMatch(/proposed-time-table/);\n  //     expect(nextDraft.body).toMatch(re);\n  //   });\n  // });\n\n  // The backend will use whatever is stored in the `pendingEvent` field\n  // to POST to the /events API endpoint. This means the data must be\n  // a valid event. Verify that it meets Nylas API specs\n  describe(\"When setting the event JSON to match server requirements\", () => {\n    beforeEach(() => {\n      this.draft = this.session.draft();\n      SchedulerComposerExtension.applyTransformsForSending({draft: this.draft});\n      this.metadata = this.draft.metadataForPluginId(PLUGIN_ID);\n      this.pendingEvent = this.metadata.pendingEvent\n    });\n\n    it(\"doesn't have a clientId\", () => {\n      expect(this.pendingEvent.client_id).not.toBeDefined();\n      expect(this.pendingEvent.clientId).not.toBeDefined();\n    });\n\n    it(\"doesn't have an id\", () => {\n      expect(this.pendingEvent.id).not.toBeDefined();\n      expect(this.pendingEvent.serverId).not.toBeDefined();\n      expect(this.pendingEvent.server_id).not.toBeDefined();\n    });\n\n    it(\"has the correct `when` block\", () => {\n      expect(this.pendingEvent.when).toEqual({\n        start_time: now().unix(),\n        end_time: now().add(1, 'hour').unix(),\n      })\n      expect(this.pendingEvent.when.object).not.toBeDefined();\n    });\n\n    it(\"doesn't have _start or _end blocks\", () => {\n      expect(this.pendingEvent._start).not.toBeDefined();\n      expect(this.pendingEvent._end).not.toBeDefined();\n    });\n\n    it(\"has the correct participants\", () => {\n      const from = this.draft.from[0]\n      expect(this.pendingEvent.participants.length).toBe(1);\n      expect(this.pendingEvent.participants[0].name).toBe(from.name);\n      expect(this.pendingEvent.participants[0].email).toBe(from.email);\n      expect(this.pendingEvent.participants[0].status).toBe(\"noreply\");\n    });\n\n    // NOTE: The backend requires a `uid` key on the metadata in order to\n    // properly look up the pending event. This must be present in order\n    // for the service to work.\n    it(\"IMPORTANT: Puts the draft client ID on the `uid` key\", () => {\n      const id = this.draft.clientId\n      expect(this.metadata.uid).toBe(id);\n    })\n\n    it(\"only has appropriate keys\", () => {\n      expect(Object.keys(this.pendingEvent)).toEqual([\n        \"calendar_id\",\n        \"title\",\n        \"participants\",\n        \"when\",\n      ])\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/spec/test-proposal-data-source.es6",
    "content": "import {CalendarDataSource} from 'nylas-exports'\nimport ProposedTimeCalendarStore from '../lib/proposed-time-calendar-store'\n\nexport default class TestProposalDataSource extends CalendarDataSource {\n  buildObservable({startTime, endTime}) {\n    this.endTime = endTime\n    this.startTime = startTime\n    this._usub = ProposedTimeCalendarStore.listen(this.manuallyTrigger)\n    return this\n  }\n\n  manuallyTrigger = () => {\n    this.onNext({events: ProposedTimeCalendarStore.proposalsAsEvents()})\n  }\n\n  subscribe(onNext) {\n    this.onNext = onNext\n    this.manuallyTrigger()\n    const dispose = () => {\n      this._usub()\n    }\n    return {dispose}\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-scheduler/stylesheets/scheduler.less",
    "content": "@import 'ui-variables';\n@import \"ui-mixins\";\n\n.calendar-event {\n  .rm-time {\n    font-size: 13px;\n    padding: 3px 5px 10px 10px;\n    position: absolute;\n    right: 0;\n    left: 0;\n    top: 0;\n    bottom: 0;\n    text-align: right;\n    cursor: default;\n    color: @text-color-very-subtle;\n    &:hover {\n      color: @text-color-subtle;\n    }\n  }\n\n  .event-injected-components {\n    position: absolute;\n    left: 0;\n    top: 0;\n    right: 0;\n    bottom: 0;\n  }\n}\n\n.scheduler-picker {\n  min-width: 186px;\n}\n\nimg[data-overlay-id].new-event-card-container {\n  display: block;\n}\n\n.new-event-card-container {\n  .new-event-card {\n    border-radius: 4px;\n    box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.28), 0 1px 1.5px rgba(0, 0, 0, 0.08);\n    padding: 10px 0;\n    padding-right: 30px;\n\n    .remove-button {\n      position: absolute;\n      right: 5px;\n      top: 0;\n      color: #CCC;\n      padding: 10px;\n      cursor: default;\n    }\n    .remove-button:hover {\n      color: @text-color;\n    }\n    .row {\n      position: relative;\n      display: flex;\n      align-items: baseline;\n      padding: 10px 0;\n    }\n    .row.title {\n      font-size: @font-size-h3;\n    }\n    .suggest-times {\n      padding-left: 56px;\n      color: #A9A9A9;\n      padding-bottom: 10px;\n      a {\n        &:hover {\n          cursor: default;\n        }\n        margin-left: 0.5em;\n      }\n    }\n    .row.expand, .row.time {\n      color: #A9A9A9;\n\n      .timezone {\n        font-size: 10px;\n        font-weight: 600;\n      }\n    }\n    .row.time {\n      .time-picker {\n        text-align: center;\n      }\n      .time-picker-wrap {\n        margin-right: 5px;\n\n        .time-options {\n          z-index: 10; // So the time pickers show over\n        }\n      }\n    }\n    .datetime-input-container {\n      padding: 0 5px;\n      display: inline-block;\n      input {\n        margin: 0;\n      }\n    }\n    .datetime-input-container:first-child {\n      padding-left: 0;\n      input[type=date] {\n        padding-left: 0;\n      }\n    }\n\n    .row textarea {\n      border-bottom: 1px solid #EEE;\n      &:focus {\n        box-shadow: 0 0 0;\n      }\n    }\n\n    input[type=text] {\n      border-radius: 0;\n      display: inline-block;\n      flex: 1;\n      border-bottom: 1px solid #EEE;\n    }\n    input[type=text]:focus {\n      border: none;\n      box-shadow: none;\n      border-bottom: 1px solid #EEE;\n    }\n\n    .field-icon {\n      padding: 0 20px;\n\n      img.content-mask {\n        background: #CCC;\n      }\n    }\n\n    .proposals-wrap {\n      margin-top: 10px;\n      display: flex;\n      padding-left: 58px;\n    }\n    .proposal-day {\n      flex: 1;\n      border: 1px solid @border-color-divider;\n      border-radius: @border-radius-base;\n      margin-right: 15px;\n      text-align: center;\n      &:last-child {\n        margin-right: 0;\n      }\n    }\n\n    .day-header {\n      padding: 10px;\n      border-bottom: 1px solid @border-color-divider;\n    }\n    .proposal {\n      padding: 10px;\n      border-bottom: 1px solid @border-color-divider;\n      &:last-child {\n        border-bottom: 0;\n      }\n    }\n\n    .day-header {\n      text-transform: uppercase;\n      color: @text-color-very-subtle;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/lib/main.es6",
    "content": "import {PreferencesUIStore, ExtensionRegistry, ComponentRegistry} from 'nylas-exports';\n\nimport SignatureComposerExtension from './signature-composer-extension';\nimport SignatureComposerDropdown from './signature-composer-dropdown';\nimport PreferencesSignatures from \"./preferences-signatures\";\n\nexport function activate() {\n  this.preferencesTab = new PreferencesUIStore.TabItem({\n    tabId: \"Signatures\",\n    displayName: \"Signatures\",\n    component: PreferencesSignatures,\n  });\n\n  ExtensionRegistry.Composer.register(SignatureComposerExtension);\n  PreferencesUIStore.registerPreferencesTab(this.preferencesTab);\n\n  ComponentRegistry.register(SignatureComposerDropdown, {\n    role: 'Composer:FromFieldComponents',\n  });\n}\n\nexport function deactivate() {\n  ExtensionRegistry.Composer.unregister(SignatureComposerExtension);\n  PreferencesUIStore.unregisterPreferencesTab(this.preferencesTab.sectionId);\n\n  ComponentRegistry.unregister(SignatureComposerDropdown);\n}\n\nexport function serialize() {\n  return {};\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/lib/preferences-signatures.jsx",
    "content": "import React from 'react';\nimport _ from 'underscore';\nimport {\n    Flexbox,\n    RetinaImg,\n    EditableList,\n    Contenteditable,\n    ScrollRegion,\n    MultiselectDropdown,\n} from 'nylas-component-kit';\nimport {AccountStore, SignatureStore, Actions} from 'nylas-exports';\n\n\nexport default class PreferencesSignatures extends React.Component {\n  static displayName = 'PreferencesSignatures';\n\n  constructor() {\n    super()\n    this.state = this._getStateFromStores()\n  }\n\n  componentDidMount() {\n    this.unsubscribers = [\n      SignatureStore.listen(this._onChange),\n    ]\n  }\n\n  componentWillUnmount() {\n    this.unsubscribers.forEach(unsubscribe => unsubscribe());\n  }\n\n\n  _onChange = () => {\n    this.setState(this._getStateFromStores())\n  }\n\n  _getStateFromStores() {\n    const signatures = SignatureStore.getSignatures()\n    const accountsAndAliases = AccountStore.aliases()\n    const selected = SignatureStore.selectedSignature()\n    const defaults = SignatureStore.getDefaults()\n    return {\n      signatures: signatures,\n      selectedSignature: selected,\n      defaults: defaults,\n      accountsAndAliases: accountsAndAliases,\n      editAsHTML: this.state ? this.state.editAsHTML : false,\n    }\n  }\n\n\n  _onCreateButtonClick = () => {\n    this._onAddSignature()\n  }\n\n  _onAddSignature = () => {\n    Actions.addSignature()\n  }\n\n  _onDeleteSignature = (signature) => {\n    Actions.removeSignature(signature)\n  }\n\n  _onEditSignature = (edit) => {\n    let editedSig;\n    if (typeof edit === \"object\") {\n      editedSig = {\n        title: this.state.selectedSignature.title,\n        body: edit.target.value,\n      }\n    } else {\n      editedSig = {\n        title: edit,\n        body: this.state.selectedSignature.body,\n      }\n    }\n    Actions.updateSignature(editedSig, this.state.selectedSignature.id)\n  }\n\n  _onSelectSignature = (sig) => {\n    Actions.selectSignature(sig.id)\n  }\n\n  _onToggleAccount = (account) => {\n    Actions.toggleAccount(account.email)\n  }\n\n  _onToggleEditAsHTML = () => {\n    const toggled = !this.state.editAsHTML\n    this.setState({editAsHTML: toggled})\n  }\n\n  _renderListItemContent = (sig) => {\n    return sig.title\n  }\n\n  _renderSignatureToolbar() {\n    return (\n      <div className=\"editable-toolbar\">\n        <div className=\"account-picker\">\n          Default for: {this._renderAccountPicker()}\n        </div>\n        <div className=\"render-mode\">\n          <input\n            type=\"checkbox\"\n            id=\"render-mode\"\n            checked={this.state.editAsHTML}\n            onClick={this._onToggleEditAsHTML}\n          />\n          <label htmlFor=\"render-mode\">Edit raw HTML</label>\n        </div>\n      </div>\n    )\n  }\n\n  _selectItemKey = (accountOrAlias) => {\n    return accountOrAlias.clientId\n  }\n\n  _isChecked = (accountOrAlias) => {\n    if (!this.state.selectedSignature) {\n      return false;\n    }\n    return (this.state.defaults[accountOrAlias.email] === this.state.selectedSignature.id);\n  }\n\n  _labelForAccountPicker() {\n    const sel = _.filter(this.state.accountsAndAliases, (accountOrAlias) => {\n      return this._isChecked(accountOrAlias)\n    })\n    const numSelected = sel.length;\n    return numSelected.toString() + (numSelected === 1 ? \" Account\" : \" Accounts\")\n  }\n\n  _renderAccountPicker() {\n    const buttonText = this._labelForAccountPicker()\n\n    return (\n      <MultiselectDropdown\n        className=\"account-dropdown\"\n        items={this.state.accountsAndAliases}\n        itemChecked={this._isChecked}\n        onToggleItem={this._onToggleAccount}\n        itemKey={this._selectItemKey}\n        current={this.selectedSignature}\n        buttonText={buttonText}\n        itemContent={(accountOrAlias) => accountOrAlias.email}\n      />\n    )\n  }\n\n  _renderEditableSignature() {\n    const selectedBody = this.state.selectedSignature ? this.state.selectedSignature.body : \"\"\n    return (\n      <Contenteditable\n        ref=\"signatureInput\"\n        value={selectedBody}\n        spellcheck={false}\n        onChange={this._onEditSignature}\n      />\n    )\n  }\n\n  _renderHTMLSignature() {\n    return (\n      <textarea\n        value={this.state.selectedSignature.body}\n        onChange={this._onEditSignature}\n      />\n    );\n  }\n\n  _renderSignatures() {\n    const sigArr = _.values(this.state.signatures)\n    if (sigArr.length === 0) {\n      return (\n        <div className=\"empty-list\">\n          <RetinaImg\n            className=\"icon-signature\"\n            name=\"signatures-big.png\"\n            mode={RetinaImg.Mode.ContentDark}\n          />\n          <h2>No signatures</h2>\n          <button\n            className=\"btn btn-small btn-create-signature\"\n            onClick={this._onCreateButtonClick}\n          >\n            Create a new signature\n          </button>\n        </div>\n      );\n    }\n    return (\n      <Flexbox>\n        <EditableList\n          showEditIcon\n          className=\"signature-list\"\n          items={sigArr}\n          itemContent={this._renderListItemContent}\n          onCreateItem={this._onAddSignature}\n          onDeleteItem={this._onDeleteSignature}\n          onItemEdited={this._onEditSignature}\n          onSelectItem={this._onSelectSignature}\n          selected={this.state.selectedSignature}\n        />\n        <div className=\"signature-wrap\">\n          <ScrollRegion className=\"signature-scroll-region\">\n            {this.state.editAsHTML ? this._renderHTMLSignature() : this._renderEditableSignature()}\n          </ScrollRegion>\n          {this._renderSignatureToolbar()}\n        </div>\n      </Flexbox>\n    )\n  }\n\n  render() {\n    return (\n      <div className=\"preferences-signatures-container\">\n        <section>\n          {this._renderSignatures()}\n        </section>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/lib/signature-composer-dropdown.jsx",
    "content": "import {\n  React,\n  Actions,\n  SignatureStore,\n} from 'nylas-exports'\nimport {\n  Menu,\n  RetinaImg,\n  ButtonDropdown,\n} from 'nylas-component-kit'\nimport _ from 'underscore'\n\nimport SignatureUtils from './signature-utils'\n\n\nexport default class SignatureComposerDropdown extends React.Component {\n  static displayName = 'SignatureComposerDropdown'\n\n  static containerRequired = false\n\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n    currentAccount: React.PropTypes.object,\n    accounts: React.PropTypes.array,\n  }\n\n  constructor() {\n    super()\n    this.state = this._getStateFromStores()\n  }\n\n  componentDidMount = () => {\n    this.unsubscribers = [\n      SignatureStore.listen(this._onChange),\n    ]\n  }\n\n  componentDidUpdate(previousProps) {\n    if (previousProps.currentAccount.clientId !== this.props.currentAccount.clientId) {\n      const nextDefaultSignature = SignatureStore.signatureForEmail(this.props.currentAccount.email)\n      this._changeSignature(nextDefaultSignature)\n    }\n  }\n\n  componentWillUnmount() {\n    this.unsubscribers.forEach(unsubscribe => unsubscribe())\n  }\n\n  _onChange = () => {\n    this.setState(this._getStateFromStores())\n  }\n\n\n  _getStateFromStores() {\n    const signatures = SignatureStore.getSignatures()\n    return {\n      signatures: signatures,\n    }\n  }\n\n  _renderSigItem = (sigItem) => {\n    return (\n      <span className={`signature-title-${sigItem.title}`}>{sigItem.title}</span>\n    )\n  }\n\n  _changeSignature = (sig) => {\n    let body;\n    if (sig) {\n      body = SignatureUtils.applySignature(this.props.draft.body, sig.body)\n    } else {\n      body = SignatureUtils.applySignature(this.props.draft.body, '')\n    }\n    this.props.session.changes.add({body})\n  }\n\n  _isSelected = (sigObj) => {\n    // http://stackoverflow.com/questions/3446170/escape-string-for-use-in-javascript-regex\n    const escapeRegExp = (str) => {\n      return str.replace(/[-[\\]/}{)(*+?.\\\\^$|]/g, \"\\\\$&\");\n    }\n    const signatureRegex = new RegExp(escapeRegExp(`<signature>${sigObj.body}</signature>`))\n    const signatureLocation = signatureRegex.exec(this.props.draft.body)\n    if (signatureLocation) return true\n    return false\n  }\n\n  _onClickNoSignature = () => {\n    this._changeSignature({body: ''})\n  }\n\n  _onClickEditSignatures() {\n    Actions.switchPreferencesTab('Signatures')\n    Actions.openPreferences()\n  }\n\n  _renderSignatures() {\n    const header = [<div className=\"item item-none\" key=\"none\" onMouseDown={this._onClickNoSignature}><span>No signature</span></div>]\n    const footer = [<div className=\"item item-edit\" key=\"edit\" onMouseDown={this._onClickEditSignatures}><span>Edit Signatures...</span></div>]\n\n    const sigItems = _.values(this.state.signatures)\n    return (\n      <Menu\n        headerComponents={header}\n        footerComponents={footer}\n        items={sigItems}\n        itemKey={sigItem => sigItem.id}\n        itemContent={this._renderSigItem}\n        onSelect={this._changeSignature}\n        itemChecked={this._isSelected}\n      />\n    )\n  }\n\n  _renderSignatureIcon() {\n    return (\n      <RetinaImg\n        className=\"signature-button\"\n        name=\"top-signature-dropdown.png\"\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n    )\n  }\n\n  render() {\n    const sigs = this.state.signatures;\n    const icon = this._renderSignatureIcon()\n\n    if (!_.isEmpty(sigs)) {\n      return (\n        <div className=\"signature-button-dropdown\">\n          <ButtonDropdown\n            primaryItem={icon}\n            menu={this._renderSignatures()}\n            bordered={false}\n          />\n        </div>\n      )\n    }\n    return null\n  }\n\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/lib/signature-composer-extension.es6",
    "content": "import {ComposerExtension, SignatureStore} from 'nylas-exports';\nimport SignatureUtils from './signature-utils';\n\nexport default class SignatureComposerExtension extends ComposerExtension {\n  static prepareNewDraft = ({draft}) => {\n    const signatureObj = draft.from && draft.from[0] ? SignatureStore.signatureForEmail(draft.from[0].email) : null;\n    if (!signatureObj) {\n      return;\n    }\n    draft.body = SignatureUtils.applySignature(draft.body, signatureObj.body);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/lib/signature-utils.es6",
    "content": "import {RegExpUtils} from 'nylas-exports'\n\nexport default {\n  applySignature(body, signature) {\n    // https://regex101.com/r/nC0qL2/2\n    const signatureRegex = RegExpUtils.signatureRegex();\n\n    let newBody = body;\n    let paddingBefore = '';\n\n    // Remove any existing signature in the body\n    newBody = newBody.replace(signatureRegex, \"\");\n    const signatureInPrevious = newBody !== body\n\n    // http://www.regexpal.com/?fam=94390\n    // prefer to put the signature one <br> before the beginning of the quote,\n    // if possible.\n    let insertionPoint = newBody.search(RegExpUtils.n1QuoteStartRegex());\n    if (insertionPoint === -1) {\n      insertionPoint = newBody.length;\n      if (!signatureInPrevious) paddingBefore = '<br><br>'\n    }\n\n    const contentBefore = newBody.slice(0, insertionPoint);\n    const contentAfter = newBody.slice(insertionPoint);\n    return `${contentBefore}${paddingBefore}<signature>${signature}</signature>${contentAfter}`;\n  },\n};\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/package.json",
    "content": "{\n  \"name\": \"composer-signature\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"A small extension to the draft store that implements signatures\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/spec/preferences-signatures-spec.jsx",
    "content": "/* eslint quote-props: 0 */\nimport ReactTestUtils from 'react-addons-test-utils';\nimport React from 'react';\nimport {SignatureStore, Actions} from 'nylas-exports';\nimport PreferencesSignatures from '../lib/preferences-signatures';\n\n\nconst SIGNATURES = {\n  '1': {\n    id: '1',\n    title: 'one',\n    body: 'first test signature!',\n  },\n  '2': {\n    id: '2',\n    title: 'two',\n    body: 'Here is my second sig!',\n  },\n}\n\nconst DEFAULTS = {\n  'one@nylas.com': '1',\n  'two@nylas.com': '2',\n}\n\n\nconst makeComponent = (props = {}) => {\n  return ReactTestUtils.renderIntoDocument(<PreferencesSignatures {...props} />)\n}\n\ndescribe('PreferencesSignatures', function preferencesSignatures() {\n  this.component = null\n\n  describe('when there are no signatures', () => {\n    it('should add a signature when you click the button', () => {\n      spyOn(SignatureStore, 'getSignatures').andReturn({})\n      spyOn(SignatureStore, 'selectedSignature')\n      spyOn(SignatureStore, 'getDefaults').andReturn({})\n      this.component = makeComponent()\n      spyOn(Actions, 'addSignature')\n      this.button = ReactTestUtils.findRenderedDOMComponentWithClass(this.component, 'btn-create-signature')\n      ReactTestUtils.Simulate.click(this.button)\n      expect(Actions.addSignature).toHaveBeenCalled()\n    })\n  })\n\n  describe('when there are signatures', () => {\n    beforeEach(() => {\n      spyOn(SignatureStore, 'getSignatures').andReturn(SIGNATURES)\n      spyOn(SignatureStore, 'selectedSignature').andReturn(SIGNATURES['1'])\n      spyOn(SignatureStore, 'getDefaults').andReturn(DEFAULTS)\n      this.component = makeComponent()\n    })\n    it('should add a signature when you click the plus button', () => {\n      spyOn(Actions, 'addSignature')\n      this.plus = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'btn-editable-list')[0]\n      ReactTestUtils.Simulate.click(this.plus)\n      expect(Actions.addSignature).toHaveBeenCalled()\n    })\n    it('should delete a signature when you click the minus button', () => {\n      spyOn(Actions, 'removeSignature')\n      this.minus = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'btn-editable-list')[1]\n      ReactTestUtils.Simulate.click(this.minus)\n      expect(Actions.removeSignature).toHaveBeenCalledWith(SIGNATURES['1'])\n    })\n    it('should toggle default status when you click an email on the dropdown', () => {\n      spyOn(Actions, 'toggleAccount')\n      this.account = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'item')[0]\n      ReactTestUtils.Simulate.mouseDown(this.account)\n      expect(Actions.toggleAccount).toHaveBeenCalledWith('tester@nylas.com')\n    })\n    it('should set the selected signature when you click on one that is not currently selected', () => {\n      spyOn(Actions, 'selectSignature')\n      this.item = ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'list-item')[1]\n      ReactTestUtils.Simulate.click(this.item)\n      expect(Actions.selectSignature).toHaveBeenCalledWith('2')\n    })\n    it('should modify the signature body when edited', () => {\n      spyOn(Actions, 'updateSignature')\n      const newText = 'Changed <strong>NEW 1 HTML</strong><br>'\n      this.component._onEditSignature({target: {value: newText}});\n      expect(Actions.updateSignature).toHaveBeenCalled()\n    })\n    it('should modify the signature title when edited', () => {\n      spyOn(Actions, 'updateSignature')\n      const newTitle = 'Changed'\n      this.component._onEditSignature(newTitle)\n      expect(Actions.updateSignature).toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/spec/signature-composer-dropdown-spec.jsx",
    "content": "/* eslint quote-props: 0 */\n\nimport React from 'react';\nimport ReactTestUtils from 'react-addons-test-utils'\nimport {SignatureStore} from 'nylas-exports';\nimport SignatureComposerDropdown from '../lib/signature-composer-dropdown'\nimport {renderIntoDocument} from '../../../spec/nylas-test-utils'\n\nconst SIGNATURES = {\n  '1': {\n    id: '1',\n    title: 'one',\n    body: 'first test signature!',\n  },\n  '2': {\n    id: '2',\n    title: 'two',\n    body: 'Here is my second sig!',\n  },\n}\n\ndescribe('SignatureComposerDropdown', function signatureComposerDropdown() {\n  beforeEach(() => {\n    spyOn(SignatureStore, 'getSignatures').andReturn(SIGNATURES)\n    spyOn(SignatureStore, 'selectedSignature')\n    this.session = {\n      changes: {\n        add: jasmine.createSpy('add'),\n      },\n    }\n    this.draft = {\n      body: \"draft body\",\n    }\n    this.button = renderIntoDocument(<SignatureComposerDropdown draft={this.draft} session={this.session} />)\n  })\n  describe('the button dropdown', () => {\n    it('calls add signature with the correct signature', () => {\n      const sigToAdd = SIGNATURES['2']\n      ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithClass(this.button, 'only-item'))\n      this.signature = ReactTestUtils.findRenderedDOMComponentWithClass(this.button, `signature-title-${sigToAdd.title}`)\n      ReactTestUtils.Simulate.mouseDown(this.signature)\n      expect(this.button.props.session.changes.add).toHaveBeenCalledWith({body: `${this.button.props.draft.body}<br><br><signature>${sigToAdd.body}</signature>`})\n    })\n    it('calls add signature with nothing when no signature is clicked and there is no current signature', () => {\n      ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithClass(this.button, 'only-item'))\n      this.noSignature = ReactTestUtils.findRenderedDOMComponentWithClass(this.button, 'item-none')\n      ReactTestUtils.Simulate.mouseDown(this.noSignature)\n      expect(this.button.props.session.changes.add).toHaveBeenCalledWith({body: `${this.button.props.draft.body}<br><br><signature></signature>`})\n    })\n    it('finds and removes the signature when no signature is clicked and there is a current signature', () => {\n      this.draft = 'draft body<signature>Remove me</signature>'\n      ReactTestUtils.Simulate.click(ReactTestUtils.findRenderedDOMComponentWithClass(this.button, 'only-item'))\n      this.noSignature = ReactTestUtils.findRenderedDOMComponentWithClass(this.button, 'item-none')\n      ReactTestUtils.Simulate.mouseDown(this.noSignature)\n      expect(this.button.props.session.changes.add).toHaveBeenCalledWith({body: `${this.button.props.draft.body}<br><br><signature></signature>`})\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/spec/signature-composer-extension-spec.es6",
    "content": "import {Message, SignatureStore} from 'nylas-exports';\nimport SignatureComposerExtension from '../lib/signature-composer-extension';\n\nconst TEST_ID = 1\nconst TEST_SIGNATURE = {\n  id: TEST_ID,\n  title: 'test-sig',\n  body: '<div class=\"something\">This is my signature.</div>',\n}\n\nconst TEST_SIGNATURES = {}\nTEST_SIGNATURES[TEST_ID] = TEST_SIGNATURE\n\ndescribe('SignatureComposerExtension', function signatureComposerExtension() {\n  describe(\"prepareNewDraft\", () => {\n    describe(\"when a signature is defined\", () => {\n      beforeEach(() => {\n        spyOn(NylasEnv.config, 'get').andCallFake((key) =>\n          (key === 'nylas.signatures' ? TEST_SIGNATURES : null)\n        );\n        spyOn(SignatureStore, 'signatureForEmail').andReturn(TEST_SIGNATURE)\n        SignatureStore.activate()\n      });\n\n      it(\"should insert the signature at the end of the message or before the first quoted text block and have a newline\", () => {\n        const a = new Message({\n          draft: true,\n          from: ['one@nylas.com'],\n          accountId: TEST_ACCOUNT_ID,\n          body: 'This is a test! <div class=\"gmail_quote\">Hello world</div>',\n        });\n        const b = new Message({\n          draft: true,\n          from: ['one@nylas.com'],\n          accountId: TEST_ACCOUNT_ID,\n          body: 'This is a another test.',\n        });\n\n        SignatureComposerExtension.prepareNewDraft({draft: a});\n        expect(a.body).toEqual(`This is a test! <signature>${TEST_SIGNATURE.body}</signature><div class=\"gmail_quote\">Hello world</div>`);\n        SignatureComposerExtension.prepareNewDraft({draft: b});\n        expect(b.body).toEqual(`This is a another test.<br><br><signature>${TEST_SIGNATURE.body}</signature>`);\n      });\n\n      const scenarios = [\n        {\n          name: 'With blockquote',\n          body: `This is a test! <signature><div>SIG</div></signature><div class=\"gmail_quote\">Hello world</div>`,\n          expected: `This is a test! <signature>${TEST_SIGNATURE.body}</signature><div class=\"gmail_quote\">Hello world</div>`,\n        },\n        {\n          name: 'Populated signature div',\n          body: `This is a test! <signature><div>SIG</div></signature>`,\n          expected: `This is a test! <signature>${TEST_SIGNATURE.body}</signature>`,\n        },\n        {\n          name: 'Empty signature div',\n          body: 'This is a test! <signature></signature>',\n          expected: `This is a test! <signature>${TEST_SIGNATURE.body}</signature>`,\n        },\n        {\n          name: 'With newlines',\n          body: 'This is a test!<br/> <signature>\\n<br>\\n<div>SIG</div>\\n</signature>',\n          expected: `This is a test!<br/> <signature>${TEST_SIGNATURE.body}</signature>`,\n        },\n      ]\n\n      scenarios.forEach((scenario) => {\n        it(`should replace the signature if a signature is already present (${scenario.name})`, () => {\n          const message = new Message({\n            draft: true,\n            from: ['one@nylas.com'],\n            body: scenario.body,\n            accountId: TEST_ACCOUNT_ID,\n          })\n          SignatureComposerExtension.prepareNewDraft({draft: message});\n          expect(message.body).toEqual(scenario.expected)\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/spec/signature-store-spec.jsx",
    "content": "/* eslint quote-props: 0 */\nimport {SignatureStore} from 'nylas-exports'\n\nlet SIGNATURES = {\n  '1': {\n    id: '1',\n    title: 'one',\n    body: 'first test signature!',\n  },\n  '2': {\n    id: '2',\n    title: 'two',\n    body: 'Here is my second sig!',\n  },\n}\n\nconst DEFAULTS = {\n  'one@nylas.com': '2',\n  'two@nylas.com': '2',\n  'three@nylas.com': null,\n}\n\ndescribe('SignatureStore', function signatureStore() {\n  beforeEach(() => {\n    spyOn(NylasEnv.config, 'get').andCallFake((key) => (key === 'nylas.signatures' ? SIGNATURES : null))\n\n    spyOn(SignatureStore, '_saveSignatures').andCallFake(() => {\n      NylasEnv.config.set(`nylas.signatures`, SignatureStore.signatures)\n    })\n    spyOn(SignatureStore, 'signatureForEmail').andCallFake((email) => SIGNATURES[DEFAULTS[email]])\n    spyOn(SignatureStore, 'selectedSignature').andCallFake(() => SIGNATURES['1'])\n    SignatureStore.activate()\n  })\n\n\n  describe('signatureForAccountId', () => {\n    it('should return the default signature for that account', () => {\n      const titleForAccount1 = SignatureStore.signatureForEmail('one@nylas.com').title\n      expect(titleForAccount1).toEqual(SIGNATURES['2'].title)\n      const account2Def = SignatureStore.signatureForEmail('three@nylas.com')\n      expect(account2Def).toEqual(undefined)\n    })\n  })\n\n  describe('removeSignature', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv.config, 'set').andCallFake((key, newObject) => {\n        if (key === 'nylas.signatures') {\n          SIGNATURES = newObject;\n        }\n      })\n    })\n    it('should remove the signature from our list of signatures', () => {\n      const toRemove = SIGNATURES[SignatureStore.selectedSignatureId]\n      SignatureStore._onRemoveSignature(toRemove)\n      expect(SIGNATURES['1']).toEqual(undefined)\n    })\n    it('should reset selectedSignatureId to a different signature', () => {\n      const toRemove = SIGNATURES[SignatureStore.selectedSignatureId]\n      SignatureStore._onRemoveSignature(toRemove)\n      expect(SignatureStore.selectedSignatureId).toNotEqual('1')\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-signature/styles/composer-signature.less",
    "content": "@import \"ui-variables\";\n\n@blurred-primary-color: mix(@background-primary, #ffbb00, 96%);\n// Styles for Preferences Signatures\n.preferences-signatures-container {\n  max-width: 800px;\n  margin: 0 auto;\n\n  .empty-list {\n    height: 300px;\n    width: inherit;\n    background-color: @background-primary;\n    border: 1px solid @border-color-divider;\n    text-align: center;\n    padding: 60px;\n\n    .icon-signature {\n      height: 120px;\n    }\n\n    h2 {\n      color: @text-color-very-subtle;\n    }\n\n    .btn {\n      margin-top: 10px;\n    }\n\n  }\n\n\n  .signature-list {\n    position: relative;\n    height: inherit;\n    width: inherit;\n\n    .items-wrapper {\n      min-width:200px;\n      height: 262px;\n    }\n    .item-rule-disabled {\n      color: @color-error;\n      padding: 4px 10px;\n      border-bottom: 1px solid @border-color-divider;\n    }\n    .selected .item-rule-disabled {\n      color: @component-active-bg;\n    }\n\n    .editable-item {\n      padding: 8px 10px;\n      border-bottom: none;\n    }\n\n    .btn-editable-list {\n      height: 37px;\n      width: 37px;\n      line-height: 37px;\n      font-size: 1em;\n    }\n  }\n\n  .signature-wrap {\n    position: relative;\n    border: 1px solid @input-border-color;\n    background-color: @white;\n    min-height: 200px;\n    height: 300px;\n    display: flex;\n    flex-direction: column;\n    width: 100%;\n    border-left: none;\n\n    .signature-scroll-region {\n      width: 100%;\n      height: 100%;\n\n      .scroll-region-content-inner {\n        min-height: 100%;\n        height: 100%;\n      }\n    }\n\n    textarea {\n      padding: 10px;\n      border: 0;\n      flex: 1;\n      font-family: monospace;\n      font-size: 0.9em;\n    }\n\n    .contenteditable {\n      padding: 10px;\n    }\n\n    .editable-toolbar {\n      border-top: 1px solid @input-border-color;\n      padding: 6px 10px;\n\n      .render-mode {\n        float: right;\n      }\n\n      .account-picker {\n        display: inline-block;\n        color: @text-color-subtle;\n\n        .button-dropdown {\n          .secondary-items {\n            .menu {\n              .item {\n                  padding: 5px 10px 5px 25px;\n              }\n              .item.checked {\n                background-image: url(images/menu/osx-checkmark@2x.svg);\n                background-position: left;\n                background-position-x: 6%;\n                background-size: 10px;\n                margin: 0;\n                padding: 5px 10px 5px 25px;\n                background-repeat: no-repeat;\n              }\n            }\n          }\n        }\n      }\n    }\n\n  }\n\n}\n\n\n// Styles for account-contact-field signature selector\n.message-item-white-wrap.composer-outer-wrap{\n  .composer-participant-field {\n\n    .dropdown-component {\n      display: inline-block;\n      float: right;\n\n      .signature-button-dropdown {\n        .only-item {\n          background: @blurred-primary-color;\n          box-shadow: none;\n          padding-right: 0;\n        }\n\n        img.content-mask {\n          display: none;\n        }\n        img.signature-button{\n          display: inline-block;\n          background-color: @btn-icon-color;\n        }\n\n\n        .button-dropdown {\n          &.open {\n            img.signature-button {\n              background-color: @component-active-color;\n            }\n          }\n          .secondary-items {\n            right:0;\n            left:auto;\n            .menu {\n              .header-container {\n                display:inline-block;\n                padding: 0;\n              }\n              .footer-container {\n                display: inline-block;\n                border-top: 1px solid #dddddd;\n              }\n              .item {\n                padding: 5px 20px 5px 20px;\n              }\n              .item.checked {\n                background-image: url(images/menu/osx-checkmark@2x.svg);\n                background-position: left;\n                background-position-x: 5%;\n                background-size: 10px;\n                background-repeat: no-repeat;\n              }\n\n            }\n          }\n        }\n\n      }\n    }\n  }\n  &.focused {\n    .composer-participant-field {\n      .dropdown-component {\n        .only-item {\n          background: @background-primary;\n        }\n      }\n    }\n  }\n}\n.composer-full-window {\n  .message-item-white-wrap.composer-outer-wrap .composer-participant-field\n  .dropdown-component .signature-button-dropdown .only-item {\n    background: @background-primary;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-spellcheck/lib/main.es6",
    "content": "import {ExtensionRegistry} from 'nylas-exports';\nimport SpellcheckComposerExtension from './spellcheck-composer-extension';\n\nexport function activate() {\n  if (NylasEnv.config.get(\"core.composing.spellcheck\")) {\n    ExtensionRegistry.Composer.register(SpellcheckComposerExtension);\n  }\n}\n\nexport function deactivate() {\n  if (NylasEnv.config.get(\"core.composing.spellcheck\")) {\n    ExtensionRegistry.Composer.unregister(SpellcheckComposerExtension);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-spellcheck/lib/spellcheck-composer-extension.es6",
    "content": "import _ from 'underscore'\nimport {DOMUtils, ComposerExtension, Spellchecker} from 'nylas-exports';\n\nconst recycled = [];\nconst MAX_MISPELLINGS = 10\n\nfunction getSpellingNodeForText(text) {\n  let node = recycled.pop();\n  if (!node) {\n    node = document.createElement('spelling');\n    node.classList.add('misspelled');\n  }\n  node.textContent = text;\n  return node;\n}\n\nfunction recycleSpellingNode(node) {\n  recycled.push(node);\n}\n\nfunction whileApplyingSelectionChanges(rootNode, cb) {\n  const selection = document.getSelection();\n  const selectionSnapshot = {\n    anchorNode: selection.anchorNode,\n    anchorOffset: selection.anchorOffset,\n    focusNode: selection.focusNode,\n    focusOffset: selection.focusOffset,\n    modified: false,\n  };\n\n  rootNode.style.display = 'none'\n  cb(selectionSnapshot);\n  rootNode.style.display = 'block'\n\n  if (selectionSnapshot.modified) {\n    selection.setBaseAndExtent(selectionSnapshot.anchorNode, selectionSnapshot.anchorOffset, selectionSnapshot.focusNode, selectionSnapshot.focusOffset);\n  }\n}\n\n// Removes all of the <spelling> nodes found in the provided `editor`.\n// It normalizes the DOM after removing spelling nodes to ensure that words\n// are not split between text nodes. (ie: doesn, 't => doesn't)\nfunction unwrapWords(rootNode) {\n  whileApplyingSelectionChanges(rootNode, (selectionSnapshot) => {\n    const spellingNodes = rootNode.querySelectorAll('spelling');\n    for (let ii = 0; ii < spellingNodes.length; ii++) {\n      const node = spellingNodes[ii];\n      if (selectionSnapshot.anchorNode === node) {\n        selectionSnapshot.anchorNode = node.firstChild;\n      }\n      if (selectionSnapshot.focusNode === node) {\n        selectionSnapshot.focusNode = node.firstChild;\n      }\n      selectionSnapshot.modified = true;\n      while (node.firstChild) {\n        node.parentNode.insertBefore(node.firstChild, node);\n      }\n      recycleSpellingNode(node);\n      node.parentNode.removeChild(node);\n    }\n  });\n  rootNode.normalize();\n}\n\n// Traverses all of the text nodes within the provided `editor`. If it finds a\n// text node with a misspelled word, it splits it, wraps the misspelled word\n// with a <spelling> node and updates the selection to account for the change.\nexport function wrapMisspelledWords(rootNode) {\n  whileApplyingSelectionChanges(rootNode, (selectionSnapshot) => {\n    const treeWalker = document.createTreeWalker(rootNode, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, {\n      acceptNode: (node) => {\n        // skip the entire subtree inside <code> tags and <a> tags...\n        if ((node.nodeType === Node.ELEMENT_NODE) && ([\"CODE\", \"A\", \"PRE\"].includes(node.tagName))) {\n          return NodeFilter.FILTER_REJECT;\n        }\n        return (node.nodeType === Node.TEXT_NODE) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;\n      },\n    });\n\n    const nodeList = [];\n\n    while (treeWalker.nextNode()) {\n      nodeList.push(treeWalker.currentNode);\n    }\n\n    // Note: As a performance optimization, we stop spellchecking after encountering\n    // 10 misspelled words. This keeps the runtime of this method bounded!\n    let nodeMisspellingsFound = 0;\n\n    while (true) {\n      const node = nodeList.shift();\n      if ((node === undefined) || (nodeMisspellingsFound > MAX_MISPELLINGS)) {\n        break;\n      }\n\n      const nodeContent = node.textContent;\n      const nodeWordRegexp = /(\\w[\\w'’-]*\\w|\\w)/g; // https://regex101.com/r/bG5yC4/1\n\n      while (true) {\n        const match = nodeWordRegexp.exec(nodeContent);\n        if ((match === null) || (nodeMisspellingsFound > MAX_MISPELLINGS)) {\n          break;\n        }\n\n        if (Spellchecker.isMisspelled(match[0])) {\n          // The insertion point is currently at the end of this misspelled word.\n          // Do not mark it until the user types a space or leaves.\n          if ((selectionSnapshot.focusNode === node) && (selectionSnapshot.focusOffset === match.index + match[0].length)) {\n            continue;\n          }\n\n          const matchNode = (match.index === 0) ? node : node.splitText(match.index);\n          const afterMatchNode = matchNode.splitText(match[0].length);\n\n          const spellingSpan = getSpellingNodeForText(match[0]);\n          matchNode.parentNode.replaceChild(spellingSpan, matchNode);\n\n          for (const prop of ['anchor', 'focus']) {\n            if (selectionSnapshot[`${prop}Node`] === node) {\n              if (selectionSnapshot[`${prop}Offset`] > match.index + match[0].length) {\n                selectionSnapshot[`${prop}Node`] = afterMatchNode;\n                selectionSnapshot[`${prop}Offset`] -= match.index + match[0].length;\n                selectionSnapshot.modified = true;\n              } else if (selectionSnapshot[`${prop}Offset`] >= match.index) {\n                selectionSnapshot[`${prop}Node`] = spellingSpan.childNodes[0];\n                selectionSnapshot[`${prop}Offset`] -= match.index;\n                selectionSnapshot.modified = true;\n              }\n            }\n          }\n\n          nodeMisspellingsFound += 1;\n          nodeList.unshift(afterMatchNode);\n          break;\n        }\n      }\n    }\n  });\n}\n\nlet currentlyRunningSpellChecker = false;\nconst runSpellChecker = _.debounce((editor) => {\n  if (!editor.currentSelection().isInScope()) return;\n  currentlyRunningSpellChecker = true;\n  unwrapWords(editor.rootNode);\n  Spellchecker.handler.provideHintText(editor.rootNode.textContent).then(() => {\n    wrapMisspelledWords(editor.rootNode)\n\n    // We defer here so that when the MutationObserver fires the\n    // SpellcheckComposerExtension.onContentChanged callback we will properly\n    // observe that we just ran the spellchecker and won't schedule another\n    // spellcheck pass (which would cause an infinite loop of spellchecking\n    // once every second)\n    _.defer(() => {\n      currentlyRunningSpellChecker = false;\n    });\n  })\n}, 1000)\n\n\nexport default class SpellcheckComposerExtension extends ComposerExtension {\n\n  static onContentChanged({editor}) {\n    if (!currentlyRunningSpellChecker) {\n      runSpellChecker(editor);\n    }\n  }\n\n  static onShowContextMenu({editor, menu}) {\n    const selection = editor.currentSelection();\n    const range = DOMUtils.Mutating.getRangeAtAndSelectWord(selection, 0);\n    const word = range.toString();\n\n    Spellchecker.appendSpellingItemsToMenu({\n      menu,\n      word,\n      onCorrect: (correction) => {\n        DOMUtils.Mutating.applyTextInRange(range, selection, correction);\n        SpellcheckComposerExtension.onContentChanged({editor});\n      },\n      onDidLearn: () => {\n        SpellcheckComposerExtension.onContentChanged({editor});\n      },\n    });\n  }\n\n  static applyTransformsForSending({draftBodyRootNode}) {\n    const spellingEls = draftBodyRootNode.querySelectorAll('spelling');\n    for (const spellingEl of Array.from(spellingEls)) {\n      // move contents out of the spelling node, remove the node\n      const parent = spellingEl.parentNode;\n      while (spellingEl.firstChild) {\n        parent.insertBefore(spellingEl.firstChild, spellingEl);\n      }\n      parent.removeChild(spellingEl);\n    }\n  }\n\n  static unapplyTransformsForSending() {\n    // no need to put spelling nodes back!\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-spellcheck/package.json",
    "content": "{\n  \"name\": \"composer-spellcheck\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"A small extension to the draft store that implements spellcheck\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-spellcheck/spec/fixtures/california-spelling-lookup.json",
    "content": "{\n    \"1\": false,\n    \"5\": false,\n    \"9\": false,\n    \"13\": false,\n    \"14\": false,\n    \"15\": false,\n    \"16\": false,\n    \"37\": false,\n    \"58\": false,\n    \"1821\": false,\n    \"1848\": false,\n    \"1850\": false,\n    \"34th\": false,\n    \"most\": false,\n    \"populous\": false,\n    \"and\": false,\n    \"the\": false,\n    \"8th\": false,\n    \"or\": false,\n    \"9th\": false,\n    \"largest\": false,\n    \"economy\": false,\n    \"in\": false,\n    \"world\": false,\n    \"If\": false,\n    \"it\": false,\n    \"were\": false,\n    \"a\": false,\n    \"country\": false,\n    \"California\": false,\n    \"would\": false,\n    \"be\": false,\n    \"California's\": false,\n    \"agriculture\": false,\n    \"industry\": false,\n    \"has\": false,\n    \"highest\": false,\n    \"output\": false,\n    \"of\": false,\n    \"any\": false,\n    \"U\": false,\n    \"S\": false,\n    \"State\": false,\n    \"Although\": false,\n    \"only\": false,\n    \"State's\": false,\n    \"together\": false,\n    \"comprising\": false,\n    \"business\": false,\n    \"services\": false,\n    \"government\": false,\n    \"professional\": false,\n    \"scientific\": false,\n    \"technical\": false,\n    \"real\": false,\n    \"estate\": false,\n    \"finance\": false,\n    \"technology\": false,\n    \"is\": false,\n    \"centered\": false,\n    \"on\": false,\n    \"About\": false,\n    \"000\": false,\n    \"earthquakes\": false,\n    \"are\": false,\n    \"recorded\": false,\n    \"each\": false,\n    \"year\": false,\n    \"but\": false,\n    \"too\": false,\n    \"small\": false,\n    \"to\": false,\n    \"felt\": false,\n    \"Pacific\": false,\n    \"Ring\": false,\n    \"Fire\": false,\n    \"Earthquakes\": false,\n    \"common\": false,\n    \"because\": false,\n    \"state's\": false,\n    \"location\": false,\n    \"along\": false,\n    \"Florida\": false,\n    \"all\": false,\n    \"states\": false,\n    \"after\": false,\n    \"Alaska\": false,\n    \"3rd\": false,\n    \"longest\": false,\n    \"coastline\": false,\n    \"contiguous\": false,\n    \"United\": false,\n    \"States\": false,\n    \"Death\": false,\n    \"Valley\": false,\n    \"lowest\": false,\n    \"point\": false,\n    \"Mount\": false,\n    \"Whitney\": false,\n    \"major\": false,\n    \"agricultural\": false,\n    \"area\": false,\n    \"contains\": false,\n    \"both\": false,\n    \"Central\": false,\n    \"areas\": false,\n    \"southeast\": false,\n    \"The\": false,\n    \"center\": false,\n    \"state\": false,\n    \"dominated\": false,\n    \"by\": false,\n    \"Mojave\": false,\n    \"Desert\": false,\n    \"forests\": false,\n    \"northwest\": false,\n    \"Douglas\": false,\n    \"fir\": false,\n    \"Redwood\": false,\n    \"west\": false,\n    \"from\": false,\n    \"Coast\": false,\n    \"east\": false,\n    \"Sierra\": false,\n    \"Nevada\": false,\n    \"diverse\": false,\n    \"geography\": false,\n    \"ranges\": false,\n    \"starting\": false,\n    \"led\": false,\n    \"dramati\": true,\n    \"dramatic\": false,\n    \"sociaal\": true,\n    \"social\": false,\n    \"demographic\": false,\n    \"change\": false,\n    \"with\": false,\n    \"large-scale\": false,\n    \"immigration\": false,\n    \"abroad\": false,\n    \"an\": false,\n    \"accompanying\": false,\n    \"economic\": false,\n    \"boom\": false,\n    \"Gold\": false,\n    \"Rush\": false,\n    \"western\": false,\n    \"portion\": false,\n    \"Alta\": false,\n    \"was\": false,\n    \"organized\": false,\n    \"as\": false,\n    \"which\": false,\n    \"admitted\": false,\n    \"31st\": false,\n    \"September\": false,\n    \"Mexican\": false,\n    \"American\": false,\n    \"War\": false,\n    \"ceded\": false,\n    \"war\": false,\n    \"for\": false,\n    \"independance\": true,\n    \"following\": false,\n    \"its\": false,\n    \"successful\": false,\n    \"Mexico\": false,\n    \"became\": false,\n    \"part\": false,\n    \"New\": false,\n    \"Spain\": false,\n    \"larger\": false,\n    \"territory\": false,\n    \"Spanish\": false,\n    \"Empire\": false,\n    \"before\": false,\n    \"being\": false,\n    \"explored\": false,\n    \"number\": false,\n    \"European\": false,\n    \"expeditions\": false,\n    \"during\": false,\n    \"16th\": false,\n    \"17th\": false,\n    \"centuries\": false,\n    \"It\": false,\n    \"then\": false,\n    \"claimed\": false,\n    \"various\": false,\n    \"Native\": false,\n    \"tribes\": false,\n    \"What\": false,\n    \"now\": false,\n    \"first\": false,\n    \"setttled\": true,\n    \"this\": false,\n    \"it's\": false,\n    \"doesn't\": false,\n\n    \"testst\": true,\n    \"have\": false,\n    \"a\": false,\n    \"misspellled\": true,\n    \"words\": false,\n    \"myvariable\": true,\n    \"fragmen\": true,\n    \"document\": false,\n    \"applieed\": true,\n    \"like\": false,\n    \"appples\": true,\n    \"is\": false,\n    \"back\": false,\n    \"normall\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-after.html",
    "content": "<p>What is now California was first <spelling class=\"misspelled\">setttled</spelling> by this it's doesn't <a href=\"/wiki/Indigenous_peoples_of_California\" title=\"Indigenous peoples of California\">various Native American tribes</a> before being explored by a number of European expeditions during the 16th and 17th centuries. It was then claimed by the <a href=\"/wiki/Spanish_Empire\" title=\"Spanish Empire\">Spanish Empire</a> as part of <a href=\"/wiki/Alta_California\" title=\"Alta California\">Alta California</a> in the larger territory of <a href=\"/wiki/New_Spain\" title=\"New Spain\">New Spain</a>. Alta California became a part of <a href=\"/wiki/Mexico\" title=\"Mexico\">Mexico</a> in 1821 following its successful <a href=\"/wiki/Mexican_War_of_Independence\" title=\"Mexican War of Independence\">war for independance</a>, but was ceded to the <a href=\"/wiki/United_States\" title=\"United States\">United States</a> in 1848 after the <a href=\"/wiki/Mexican%E2%80%93American_War\" title=\"Mexican–American War\">Mexican–American War</a>. The western portion of Alta California was organized as the State of California, which was admitted as the 31st state on September 9, 1850. The <a href=\"/wiki/California_Gold_Rush\" title=\"California Gold Rush\">California Gold Rush</a> starting in 1848 led to <spelling class=\"misspelled\">dramati</spelling> <spelling class=\"misspelled\">sociaal</spelling> and demographic change, with large-scale immigration from the east and abroad with an accompanying economic boom.</p><p>California's diverse geography ranges from the <a href=\"/wiki/Sierra_Nevada_(U.S.)\" title=\"Sierra Nevada (U.S.)\">Sierra Nevada</a> in the east to the <a href=\"/wiki/West_Coast_of_the_United_States\" title=\"West Coast of the United States\">Pacific Coast</a> in the west, from the <a href=\"/wiki/Sequoia_sempervirens\" title=\"Sequoia sempervirens\">Redwood</a>–<a href=\"/wiki/Douglas_fir\" title=\"Douglas fir\">Douglas fir</a> forests of the northwest, to the <a href=\"/wiki/Mojave_Desert\" title=\"Mojave Desert\">Mojave Desert</a> areas in the southeast. The center of the state is dominated by the <a href=\"/wiki/California_Central_Valley\" title=\"California Central Valley\" class=\"mw-redirect\">Central Valley</a>, a major agricultural area. California contains both the highest point (<a href=\"/wiki/Mount_Whitney\" title=\"Mount Whitney\">Mount Whitney</a>) and the lowest point (<a href=\"/wiki/Death_Valley\" title=\"Death Valley\">Death Valley</a>), in the <a href=\"/wiki/Contiguous_United_States\" title=\"Contiguous United States\">contiguous United States</a> and it has the <a href=\"/wiki/List_of_U.S._states_by_coastline\" title=\"List of U.S. states by coastline\">3rd longest coastline</a> of all states (after Alaska and <a href=\"/wiki/Florida\" title=\"Florida\">Florida</a>). Earthquakes are common because of the state's location along the <a href=\"/wiki/Pacific_Ring_of_Fire\" title=\"Pacific Ring of Fire\" class=\"mw-redirect\">Pacific Ring of Fire</a>. About 37,000 earthquakes are recorded each year, but most are too small to be felt.<sup id=\"cite_ref-13\" class=\"reference\"><a href=\"#cite_note-13\"><span>[</span>13<span>]</span></a></sup></p><p>California's economy is centered on <a href=\"/wiki/Technology\" title=\"Technology\">technology</a>, <a href=\"/wiki/Finance\" title=\"Finance\">finance</a>, <a href=\"/wiki/Real_estate\" title=\"Real estate\">real estate services</a>, government, and professional, scientific and technical <a href=\"/wiki/Business_services\" title=\"Businaaaaasess services\" class=\"mw-redirect\">business services</a>; together comprising 58% of the State economy.<sup id=\"cite_ref-BEA_14-0\" class=\"reference\"><a href=\"#cite_note-BEA-14\"><span>[</span>14<span>]</span></a></sup> Although only 1.5% of the State's economy,<sup id=\"cite_ref-BEA_14-1\" class=\"reference\"><a href=\"#cite_note-BEA-14\"><span>[</span>14<span>]</span></a></sup> California's agriculture industry has the highest output of any U.S. State.<sup id=\"cite_ref-15\" class=\"reference\"><a href=\"#cite_note-15\"><span>[</span>15<span>]</span></a></sup> If it were a country, California would be the <a href=\"/wiki/Comparison_between_U.S._states_and_countries_nominal_GDP\" title=\"Comparison between U.S. states and countries nominal GDP\" class=\"mw-redirect\">8th or 9th largest economy in the world</a><sup id=\"cite_ref-16\" class=\"reference\"><a href=\"#cite_note-16\"><span>[</span>16<span>]</span></a></sup> and the <a href=\"/wiki/List_of_countries_by_population\" title=\"List of countries by population\" class=\"mw-redirect\">34th most populous</a>.</p>\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-spellcheck/spec/fixtures/california-with-misspellings-before.html",
    "content": "<p>What is now California was first setttled by this it's doesn't <a href=\"/wiki/Indigenous_peoples_of_California\" title=\"Indigenous peoples of California\">various Native American tribes</a> before being explored by a number of European expeditions during the 16th and 17th centuries. It was then claimed by the <a href=\"/wiki/Spanish_Empire\" title=\"Spanish Empire\">Spanish Empire</a> as part of <a href=\"/wiki/Alta_California\" title=\"Alta California\">Alta California</a> in the larger territory of <a href=\"/wiki/New_Spain\" title=\"New Spain\">New Spain</a>. Alta California became a part of <a href=\"/wiki/Mexico\" title=\"Mexico\">Mexico</a> in 1821 following its successful <a href=\"/wiki/Mexican_War_of_Independence\" title=\"Mexican War of Independence\">war for independance</a>, but was ceded to the <a href=\"/wiki/United_States\" title=\"United States\">United States</a> in 1848 after the <a href=\"/wiki/Mexican%E2%80%93American_War\" title=\"Mexican–American War\">Mexican–American War</a>. The western portion of Alta California was organized as the State of California, which was admitted as the 31st state on September 9, 1850. The <a href=\"/wiki/California_Gold_Rush\" title=\"California Gold Rush\">California Gold Rush</a> starting in 1848 led to dramati sociaal and demographic change, with large-scale immigration from the east and abroad with an accompanying economic boom.</p><p>California's diverse geography ranges from the <a href=\"/wiki/Sierra_Nevada_(U.S.)\" title=\"Sierra Nevada (U.S.)\">Sierra Nevada</a> in the east to the <a href=\"/wiki/West_Coast_of_the_United_States\" title=\"West Coast of the United States\">Pacific Coast</a> in the west, from the <a href=\"/wiki/Sequoia_sempervirens\" title=\"Sequoia sempervirens\">Redwood</a>–<a href=\"/wiki/Douglas_fir\" title=\"Douglas fir\">Douglas fir</a> forests of the northwest, to the <a href=\"/wiki/Mojave_Desert\" title=\"Mojave Desert\">Mojave Desert</a> areas in the southeast. The center of the state is dominated by the <a href=\"/wiki/California_Central_Valley\" title=\"California Central Valley\" class=\"mw-redirect\">Central Valley</a>, a major agricultural area. California contains both the highest point (<a href=\"/wiki/Mount_Whitney\" title=\"Mount Whitney\">Mount Whitney</a>) and the lowest point (<a href=\"/wiki/Death_Valley\" title=\"Death Valley\">Death Valley</a>), in the <a href=\"/wiki/Contiguous_United_States\" title=\"Contiguous United States\">contiguous United States</a> and it has the <a href=\"/wiki/List_of_U.S._states_by_coastline\" title=\"List of U.S. states by coastline\">3rd longest coastline</a> of all states (after Alaska and <a href=\"/wiki/Florida\" title=\"Florida\">Florida</a>). Earthquakes are common because of the state's location along the <a href=\"/wiki/Pacific_Ring_of_Fire\" title=\"Pacific Ring of Fire\" class=\"mw-redirect\">Pacific Ring of Fire</a>. About 37,000 earthquakes are recorded each year, but most are too small to be felt.<sup id=\"cite_ref-13\" class=\"reference\"><a href=\"#cite_note-13\"><span>[</span>13<span>]</span></a></sup></p><p>California's economy is centered on <a href=\"/wiki/Technology\" title=\"Technology\">technology</a>, <a href=\"/wiki/Finance\" title=\"Finance\">finance</a>, <a href=\"/wiki/Real_estate\" title=\"Real estate\">real estate services</a>, government, and professional, scientific and technical <a href=\"/wiki/Business_services\" title=\"Businaaaaasess services\" class=\"mw-redirect\">business services</a>; together comprising 58% of the State economy.<sup id=\"cite_ref-BEA_14-0\" class=\"reference\"><a href=\"#cite_note-BEA-14\"><span>[</span>14<span>]</span></a></sup> Although only 1.5% of the State's economy,<sup id=\"cite_ref-BEA_14-1\" class=\"reference\"><a href=\"#cite_note-BEA-14\"><span>[</span>14<span>]</span></a></sup> California's agriculture industry has the highest output of any U.S. State.<sup id=\"cite_ref-15\" class=\"reference\"><a href=\"#cite_note-15\"><span>[</span>15<span>]</span></a></sup> If it were a country, California would be the <a href=\"/wiki/Comparison_between_U.S._states_and_countries_nominal_GDP\" title=\"Comparison between U.S. states and countries nominal GDP\" class=\"mw-redirect\">8th or 9th largest economy in the world</a><sup id=\"cite_ref-16\" class=\"reference\"><a href=\"#cite_note-16\"><span>[</span>16<span>]</span></a></sup> and the <a href=\"/wiki/List_of_countries_by_population\" title=\"List of countries by population\" class=\"mw-redirect\">34th most populous</a>.</p>\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-spellcheck/spec/spellcheck-composer-extension-spec.es6",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport {Spellchecker, Message} from 'nylas-exports';\n\nimport SpellcheckComposerExtension, {wrapMisspelledWords} from '../lib/spellcheck-composer-extension';\n\nconst initialPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-before.html');\nconst initialHTML = fs.readFileSync(initialPath).toString();\nconst afterPath = path.join(__dirname, 'fixtures', 'california-with-misspellings-after.html');\nconst afterHTML = fs.readFileSync(afterPath).toString();\n\ndescribe('SpellcheckComposerExtension', function spellcheckComposerExtension() {\n  beforeEach(() => {\n    // Avoid differences between node-spellcheck on different platforms\n    const lookupPath = path.join(__dirname, 'fixtures', 'california-spelling-lookup.json');\n    const spellings = JSON.parse(fs.readFileSync(lookupPath));\n    spyOn(Spellchecker, 'isMisspelled').andCallFake(word => spellings[word])\n    spyOn(Spellchecker.handler, 'provideHintText').andReturn({\n      then(cb) {\n        cb()\n      },\n    })\n  });\n\n  describe(\"wrapMisspelledWords\", () => {\n    it(\"correctly walks a DOM tree and surrounds mispelled words\", () => {\n      const node = document.createElement('div');\n      node.innerHTML = initialHTML;\n      wrapMisspelledWords(node);\n      advanceClock(1000) // Wait for debounce\n      advanceClock(1) // Wait for defer\n      expect(node.innerHTML).toEqual(afterHTML);\n    });\n\n    it(\"does not mark misspelled words inside A, CODE and PRE tags\", () => {\n      const node = document.createElement('div');\n      node.innerHTML = `\n      <br>\n      This is a testst! I have a few misspellled words.\n      <code>myvariable</code>\n      <pre>\n         fragmen = document.applieed();\n      </pre>\n      <a href=\"apple.com\">I like appples!</a>\n      <br>\n      This is back to normall.\n      `;\n      wrapMisspelledWords(node);\n      advanceClock(1000) // Wait for debounce\n      advanceClock(1) // Wait for defer\n      expect(node.innerHTML).toEqual(`\n      <br>\n      This is a <spelling class=\"misspelled\">testst</spelling>! I have a few <spelling class=\"misspelled\">misspellled</spelling> words.\n      <code>myvariable</code>\n      <pre>         fragmen = document.applieed();\n      </pre>\n      <a href=\"apple.com\">I like appples!</a>\n      <br>\n      This is back to <spelling class=\"misspelled\">normall</spelling>.\n      `);\n    });\n  });\n\n  describe(\"applyTransformsForSending\", () => {\n    it(\"removes the spelling annotations it inserted\", () => {\n      const draft = new Message({ body: afterHTML });\n      const fragment = document.createDocumentFragment();\n      const draftBodyRootNode = document.createElement('root')\n      fragment.appendChild(draftBodyRootNode)\n      draftBodyRootNode.innerHTML = afterHTML\n      SpellcheckComposerExtension.applyTransformsForSending({draftBodyRootNode, draft});\n      expect(draftBodyRootNode.innerHTML).toEqual(initialHTML);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/README.md",
    "content": "# Composer Templates\n\nCreate templates you can use to pre-fill the N1 composer - never type the same\nemail again! Templates live in the ~/.nylas-mail/templates directory on your computer.\nEach template is an HTML file - the name of the\nfile is the name of the template, and it's contents are the default message body.\n\nIf you include HTML &lt;code&gt; tags in your template, you can create\nregions that you can jump between and fill easily.\nGive &lt;code&gt; tags the `var` class to mark them as template regions. Add\nthe `empty` class to make them dark yellow. When you send your message, &lt;code&gt;\ntags are always stripped so the recipient never sees any highlighting.\n\nThis example is a good starting point for plugins that want to extend the composer\nexperience.\n\n<img src=\"https://raw.githubusercontent.com/nylas/nylas-mail/master/internal_packages/composer-templates/screenshot.png\">\n\n#### Install this plugin\n\n1. Download and run N1\n\n2. From the menu, select `Developer > Install a Plugin Manually...`\n   The dialog will default to this examples directory. Just choose the\n   package to install it!\n\n   > When you install packages, they're moved to `~/.nylas-mail/packages`,\n   > and N1 runs `apm install` on the command line to fetch dependencies\n   > listed in the package's `package.json`\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/assets/Welcome to Quick Replies.html",
    "content": "Hey, <code class=\"var empty\">{{First Name}}</code>!\n<br/>\n<br/>\nWelcome to the Quick Replies plugin! Here you can create email templates with\n<code class=\"var empty\">{{variables}}</code> in double curly braces that you can quickly fill\nbefore you send your email.\n<br/>\n<br/>\nWhen you send your message, the highlighting is always removed so the recipient\nnever sees it.\n<br/>\n<br/>\nEnjoy!\n<br/>\n<br/>\n-Nylas Team\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/lib/main.es6",
    "content": "/* eslint global-require: 0 */\nimport {PreferencesUIStore, ComponentRegistry, ExtensionRegistry} from 'nylas-exports';\nimport TemplatePicker from './template-picker';\nimport TemplateStatusBar from './template-status-bar';\nimport TemplateComposerExtension from './template-composer-extension';\n\nexport function activate(state = {}) {\n  this.state = state;\n  this.preferencesTab = new PreferencesUIStore.TabItem({\n    tabId: 'Quick Replies',\n    displayName: 'Quick Replies',\n    component: require('./preferences-templates').default,\n  });\n  ComponentRegistry.register(TemplatePicker, {role: 'Composer:ActionButton'});\n  ComponentRegistry.register(TemplateStatusBar, {role: 'Composer:Footer'});\n  PreferencesUIStore.registerPreferencesTab(this.preferencesTab);\n  ExtensionRegistry.Composer.register(TemplateComposerExtension);\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(TemplatePicker);\n  ComponentRegistry.unregister(TemplateStatusBar);\n  PreferencesUIStore.unregisterPreferencesTab(this.preferencesTab.tabId);\n  ExtensionRegistry.Composer.unregister(TemplateComposerExtension);\n}\n\nexport function serialize() {\n  return this.state;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/lib/preferences-templates.jsx",
    "content": "import _ from 'underscore';\nimport {Contenteditable, RetinaImg} from 'nylas-component-kit';\nimport {React} from 'nylas-exports';\n\nimport TemplateStore from './template-store';\nimport TemplateEditor from './template-editor';\n\n\nclass PreferencesTemplates extends React.Component {\n  static displayName = 'PreferencesTemplates';\n\n  constructor() {\n    super();\n    this._templateSaveQueue = {};\n\n    const {templates, selectedTemplate, selectedTemplateName} = this._getStateFromStores();\n    this.state = {\n      editAsHTML: false,\n      editState: templates.length === 0 ? \"new\" : null,\n      templates: templates,\n      selectedTemplate: selectedTemplate,\n      selectedTemplateName: selectedTemplateName,\n      contents: null,\n    };\n  }\n\n  componentDidMount() {\n    this.unsub = TemplateStore.listen(this._onChange);\n  }\n\n  componentWillUnmount() {\n    this.unsub();\n    if (this.state.selectedTemplate) {\n      this._saveTemplateNow(this.state.selectedTemplate.name, this.state.contents);\n    }\n  }\n\n  // SAVING AND LOADING TEMPLATES\n  _loadTemplateContents = (template) => {\n    if (template) {\n      TemplateStore.getTemplateContents(template.id, (contents) => {\n        this.setState({contents: contents});\n      });\n    }\n  }\n\n  _saveTemplateNow(name, contents, callback) {\n    TemplateStore.saveTemplate(name, contents, callback);\n  }\n\n  _saveTemplateSoon(name, contents) {\n    this._templateSaveQueue[name] = contents;\n    this._saveTemplatesFromCache();\n  }\n\n  __saveTemplatesFromCache() {\n    for (const name of Object.keys(this._templateSaveQueue)) {\n      this._saveTemplateNow(name, this._templateSaveQueue[name]);\n    }\n    this._templateSaveQueue = {};\n  }\n\n  _saveTemplatesFromCache = _.debounce(PreferencesTemplates.prototype.__saveTemplatesFromCache, 500);\n\n  // OVERALL STATE HANDLING\n  _onChange = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  _getStateFromStores() {\n    const templates = TemplateStore.items();\n    let selectedTemplate = this.state ? this.state.selectedTemplate : null;\n    if (selectedTemplate && !_.pluck(templates, \"id\").includes(selectedTemplate.id)) {\n      selectedTemplate = null;\n    } else if (!selectedTemplate) {\n      selectedTemplate = templates.length > 0 ? templates[0] : null;\n    }\n    this._loadTemplateContents(selectedTemplate);\n    let selectedTemplateName = null;\n    if (selectedTemplate) {\n      selectedTemplateName = this.state ? this.state.selectedTemplateName : selectedTemplate.name;\n    }\n    return {templates, selectedTemplate, selectedTemplateName};\n  }\n\n  // TEMPLATE CONTENT EDITING\n  _onEditTemplate = (event) => {\n    const html = event.target.value;\n    this.setState({contents: html});\n    if (this.state.selectedTemplate) {\n      this._saveTemplateSoon(this.state.selectedTemplate.name, html);\n    }\n  }\n\n  _onSelectTemplate = (event) => {\n    if (this.state.selectedTemplate) {\n      this._saveTemplateNow(this.state.selectedTemplate.name, this.state.contents);\n    }\n\n    const selectedId = event.target.value;\n    const selectedTemplate = this.state.templates.find((template) =>\n      template.id === selectedId\n    );\n\n    this.setState({\n      selectedTemplate: selectedTemplate,\n      selectedTemplateName: selectedTemplate ? selectedTemplate.name : null,\n      contents: null,\n    });\n    this._loadTemplateContents(selectedTemplate);\n  }\n\n  _renderTemplatePicker() {\n    const options = this.state.templates.map((template) => {\n      return <option value={template.id} key={template.id}>{template.name}</option>\n    });\n\n    return (\n      <select value={this.state.selectedTemplate ? this.state.selectedTemplate.id : null} onChange={this._onSelectTemplate}>\n        {options}\n      </select>\n    );\n  }\n\n  _renderEditableTemplate() {\n    return (\n      <Contenteditable\n        ref=\"templateInput\"\n        value={this.state.contents || \"\"}\n        onChange={this._onEditTemplate}\n        extensions={[TemplateEditor]}\n        spellcheck={false}\n      />\n    );\n  }\n\n  _renderHTMLTemplate() {\n    return (\n      <textarea\n        ref=\"templateHTMLInput\"\n        value={this.state.contents || \"\"}\n        onChange={this._onEditTemplate}\n      />\n    );\n  }\n\n  _renderModeToggle() {\n    if (this.state.editAsHTML) {\n      return (<a onClick={() => { this.setState({editAsHTML: false}); }}>Edit live preview</a>);\n    }\n    return (<a onClick={() => { this.setState({editAsHTML: true}); }}>Edit raw HTML</a>);\n  }\n\n  _onEnter(action) {\n    return (event) => {\n      if (event.key === \"Enter\") {\n        action()\n      }\n    }\n  }\n\n  // TEMPLATE NAME EDITING\n  _renderEditName() {\n    return (\n      <div className=\"section-title\">\n        Template Name: <input type=\"text\" className=\"template-name-input\" value={this.state.selectedTemplateName} onChange={this._onEditName} onKeyDown={this._onEnter(this._saveName)} />\n        <button className=\"btn template-name-btn\" onClick={this._saveName}>Save Name</button>\n        <button className=\"btn template-name-btn\" onClick={this._cancelEditName}>Cancel</button>\n      </div>\n    );\n  }\n\n  _renderName() {\n    const rawText = this.state.editAsHTML ? \"Raw HTML \" : \"\";\n    return (\n      <div className=\"section-title\">\n        {rawText}Template: {this._renderTemplatePicker()}\n        <button className=\"btn template-name-btn\" title=\"New template\" onClick={this._startNewTemplate}>New</button>\n        <button className=\"btn template-name-btn\" onClick={() => { this.setState({editState: \"name\"}); }}>Rename</button>\n      </div>\n    );\n  }\n\n  _onEditName = (event) => {\n    this.setState({selectedTemplateName: event.target.value});\n  }\n\n  _cancelEditName = () => {\n    this.setState({\n      selectedTemplateName: this.state.selectedTemplate ? this.state.selectedTemplate.name : null,\n      editState: null,\n    });\n  }\n\n  _saveName = () => {\n    if (this.state.selectedTemplate && this.state.selectedTemplate.name !== this.state.selectedTemplateName) {\n      TemplateStore.renameTemplate(this.state.selectedTemplate.name, this.state.selectedTemplateName, (renamedTemplate) => {\n        this.setState({\n          selectedTemplate: renamedTemplate,\n          editState: null,\n        });\n      });\n    } else {\n      this.setState({\n        editState: null,\n      });\n    }\n  }\n\n  // DELETE AND NEW\n  _deleteTemplate = () => {\n    const numTemplates = this.state.templates.length;\n    if (this.state.selectedTemplate) {\n      TemplateStore.deleteTemplate(this.state.selectedTemplate.name);\n    }\n    if (numTemplates === 1) {\n      this.setState({\n        editState: \"new\",\n        selectedTemplate: null,\n        selectedTemplateName: \"\",\n        contents: \"\",\n      });\n    }\n  }\n\n  _startNewTemplate = () => {\n    this.setState({\n      editState: \"new\",\n      selectedTemplate: null,\n      selectedTemplateName: \"\",\n      contents: \"\",\n    });\n  }\n\n  _saveNewTemplate = () => {\n    this.setState({contents: \"\"})\n    TemplateStore.saveNewTemplate(this.state.selectedTemplateName, \"\", (template) => {\n      this.setState({\n        selectedTemplate: template,\n        editState: null,\n      });\n    });\n  }\n\n  _cancelNewTemplate = () => {\n    const template = this.state.templates.length > 0 ? this.state.templates[0] : null;\n    this.setState({\n      selectedTemplate: template,\n      selectedTemplateName: template ? template.name : null,\n      editState: null,\n    });\n    this._loadTemplateContents(template);\n  }\n\n  _renderCreateNew() {\n    const cancel = (<button className=\"btn template-name-btn\" onClick={this._cancelNewTemplate}>Cancel</button>);\n    return (\n      <div className=\"section-title\">\n        Template Name: <input type=\"text\" className=\"template-name-input\" value={this.state.selectedTemplateName} onChange={this._onEditName} onKeyDown={this._onEnter(this._saveNewTemplate)} />\n        <button className=\"btn btn-emphasis template-name-btn\" onClick={this._saveNewTemplate}>Save</button>\n        {this.state.templates.length ? cancel : null}\n      </div>\n    );\n  }\n\n  // MAIN RENDER\n  render() {\n    const deleteBtn = (\n      <button className=\"btn\" title=\"Delete template\" onClick={this._deleteTemplate}>\n        <RetinaImg name=\"icon-composer-trash.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n\n    const editor = (\n      <div>\n        <div className=\"template-wrap\">\n          {this.state.editAsHTML ? this._renderHTMLTemplate() : this._renderEditableTemplate()}\n        </div>\n        <div style={{marginTop: \"5px\"}}>\n          <span className=\"editor-note\">\n            {_.size(this._templateSaveQueue) === 0 ? \"Changes saved.\" : \"\"}\n            &nbsp;\n          </span>\n          <span style={{\"float\": \"right\"}}>{this.state.editState === null ? deleteBtn : \"\"}</span>\n        </div>\n        <div className=\"toggle-mode\" style={{marginTop: \"1em\"}}>\n          {this._renderModeToggle()}\n        </div>\n      </div>\n    );\n\n    let editContainer = this._renderName();\n    if (this.state.editState === \"name\") {\n      editContainer = this._renderEditName();\n    } else if (this.state.editState === \"new\") {\n      editContainer = this._renderCreateNew();\n    }\n\n    const noTemplatesMessage = (\n      <div className=\"template-status-bar no-templates-message\">\n        {`You don't have any templates! Enter a template name and press save to create one.`}\n      </div>\n    );\n\n    return (\n      <div className=\"container-templates\">\n        <section style={this.state.editState === \"new\" ? {marginBottom: 50} : null}>\n          {editContainer}\n          {this.state.editState !== \"new\" ? editor : null}\n          {this.state.templates.length === 0 ? noTemplatesMessage : null}\n        </section>\n\n        <section className=\"templates-instructions\">\n          <p>\n            {`To create a variable, type a set of double curly\n            brackets wrapping the variable's name, like this`}: <strong>{\"{{\"}variable_name{\"}}\"}</strong>. The highlighting in the variable regions will be removed before the message is\n            sent.\n          </p>\n          <p>\n            Reply templates are saved as HTML files in the <strong>~/.nylas-mail/templates</strong> directory on your computer. In raw HTML, variables are defined as HTML &lt;code&gt; tags with class &quot;var empty&quot;.\n          </p>\n        </section>\n      </div>\n    );\n  }\n\n}\n\nexport default PreferencesTemplates;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/lib/template-composer-extension.es6",
    "content": "import {DOMUtils, ComposerExtension} from 'nylas-exports';\n\nexport default class TemplatesComposerExtension extends ComposerExtension {\n\n  static warningsForSending({draft}) {\n    const warnings = [];\n    if (draft.body.search(/<code[^>]*empty[^>]*>/i) > 0) {\n      warnings.push('with an empty template area');\n    }\n    return warnings;\n  }\n\n  static applyTransformsForSending = ({draftBodyRootNode}) => {\n    draftBodyRootNode.innerHTML = draftBodyRootNode.innerHTML.replace(/<\\/?code[^>]*>/g, (match) =>\n      `<!-- ${match} -->`\n    );\n  }\n\n  static unapplyTransformsForSending = ({draftBodyRootNode}) => {\n    draftBodyRootNode.innerHTML = draftBodyRootNode.innerHTML.replace(/<!-- (<\\/?code[^>]*>) -->/g, (match, node) =>\n      node\n    );\n  }\n\n  static onClick({editor, event}) {\n    const node = event.target;\n    if (node.nodeName === 'CODE' && node.classList.contains('var') && node.classList.contains('empty')) {\n      editor.selectAllChildren(node);\n    }\n  }\n\n  static onKeyDown({editor, event}) {\n    const editableNode = editor.rootNode;\n    if (event.key === 'Tab') {\n      const nodes = editableNode.querySelectorAll('code.var');\n      if (nodes.length > 0) {\n        const sel = editor.currentSelection();\n        let found = false;\n\n        // First, try to find a <code> that the selection is within. If found,\n        // select the next/prev node if the selection ends at the end of the\n        // <code>'s text, otherwise select the <code>'s contents.\n        for (let i = 0; i < nodes.length; i++) {\n          const node = nodes[i];\n          if (DOMUtils.selectionIsWithin(node)) {\n            const selIndex = editor.getSelectionTextIndex(node);\n            const length = DOMUtils.getIndexedTextContent(node).slice(-1)[0].end;\n            let nextIndex = i;\n            if (selIndex.endIndex === length) {\n              nextIndex = event.shiftKey ? i - 1 : i + 1;\n            }\n            nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions\n            sel.selectAllChildren(nodes[nextIndex]);\n            found = true;\n            break;\n          }\n        }\n\n        // If we failed to find a <code> that the selection is within, select the\n        // nearest <code> before/after the selection (depending on shift).\n        if (!found) {\n          const treeWalker = document.createTreeWalker(editableNode, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT);\n          let curIndex = 0;\n          let nextIndex = null;\n          let node = treeWalker.nextNode();\n          while (node) {\n            if (sel.anchorNode === node || sel.focusNode === node) break;\n            if (node.nodeName === 'CODE' && node.classList.contains('var')) curIndex++;\n            node = treeWalker.nextNode();\n          }\n          nextIndex = event.shiftKey ? curIndex - 1 : curIndex;\n          nextIndex = (nextIndex + nodes.length) % nodes.length; // allow wraparound in both directions\n          sel.selectAllChildren(nodes[nextIndex]);\n        }\n\n        event.preventDefault();\n        event.stopPropagation();\n      }\n    } else if (event.key === 'Enter') {\n      const nodes = editableNode.querySelectorAll('code.var');\n      for (let i = 0; i < nodes.length; i++) {\n        if (DOMUtils.selectionStartsOrEndsIn(nodes[i])) {\n          event.preventDefault();\n          event.stopPropagation();\n          break;\n        }\n      }\n    }\n  }\n\n  static onContentChanged({editor}) {\n    const editableNode = editor.rootNode;\n    const selection = editor.currentSelection().rawSelection;\n    const isWithinNode = (node) => {\n      let test = selection.baseNode;\n      while (test !== editableNode) {\n        if (test === node) { return true; }\n        test = test.parentNode;\n      }\n      return false;\n    };\n\n    const codeTags = editableNode.querySelectorAll('code.var.empty');\n    for (let i = 0, codeTag; i < codeTags.length; i++) {\n      codeTag = codeTags[i];\n      // sets node contents to just its textContent, strips HTML\n      codeTag.textContent = codeTag.textContent;\n      if (selection.containsNode(codeTag) || isWithinNode(codeTag)) {\n        codeTag.classList.remove('empty');\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/lib/template-editor.es6",
    "content": "import {DOMUtils, ContenteditableExtension} from 'nylas-exports';\n\nexport default class TemplateEditor extends ContenteditableExtension {\n\n  static onContentChanged = ({editor}) => {\n    // Run through and remove all code nodes that are invalid\n    const codeNodes = editor.rootNode.querySelectorAll(\"code.var.empty\");\n    for (let ii = 0; ii < codeNodes.length; ii++) {\n      const codeNode = codeNodes[ii];\n\n      // remove any style that was added by contenteditable\n      codeNode.removeAttribute(\"style\");\n\n      // grab the text content and the indexable text content\n      const codeNodeText = codeNode.textContent;\n      const indexText = DOMUtils.getIndexedTextContent(codeNode).map(({text}) => text).join(\"\");\n\n      // unwrap any code nodes that don't start/end with {{}}, and any with line breaks inside\n      if ((!codeNodeText.startsWith(\"{{\")) || (!codeNodeText.endsWith(\"}}\")) || (indexText.indexOf(\"\\n\") > -1)) {\n        editor.whilePreservingSelection(() => {\n          DOMUtils.unwrapNode(codeNode);\n        });\n      }\n    }\n\n    // Attempt to sanitize extra nodes that may have been created by contenteditable on certain text editing\n    // operations (insertion/deletion of line breaks, etc.). These are generally <span>, but can also be\n    // <font>, <b>, and possibly others. The extra nodes often grab CSS styles from neighboring elements\n    // as inline style, including the yellow text from <code> nodes that we insert. This is contenteditable\n    // trying to be \"smart\" and preserve styles, which is very undesirable for the <code> node styles. The\n    // below code is a hack to prevent yellow text from appearing.\n    const starNodes = editor.rootNode.querySelectorAll(\"*\");\n    for (let ii = 0; ii < starNodes.length; ii++) {\n      const node = starNodes[ii];\n      if ((!node.className) && (node.style.color === \"#c79b11\")) {\n        editor.whilePreservingSelection(() => {\n          DOMUtils.unwrapNode(node);\n        });\n      }\n    }\n\n    const fontNodes = editor.rootNode.querySelectorAll(\"font\");\n    for (let ii = 0; ii < fontNodes.length; ii++) {\n      const node = fontNodes[ii];\n      if (node.color === \"#c79b11\") {\n        editor.whilePreservingSelection(() => {\n          DOMUtils.unwrapNode(node);\n        });\n      }\n    }\n\n    // Find all {{}} and wrap them in code nodes if they aren't already\n    // Regex finds any {{ <contents> }} that doesn't contain {, }, or \\n\n    // https://regex101.com/r/jF2oF4/1\n    for (const range of editor.regExpSelectorAll(/\\{\\{[^\\n{}]*?\\}\\}/g)) {\n      if (!DOMUtils.isWrapped(range, \"CODE\")) {\n        // Preserve the selection based on text index within the range matched by the regex\n        const selIndex = editor.getSelectionTextIndex(range);\n        const codeNode = DOMUtils.wrap(range, \"CODE\");\n        codeNode.className = \"var empty\";\n\n        // Sets node contents to just its textContent, strips HTML\n        codeNode.textContent = codeNode.textContent;\n\n        if (selIndex != null) {\n          editor.restoreSelectionByTextIndex(codeNode, selIndex.startIndex, selIndex.endIndex);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/lib/template-picker.jsx",
    "content": "/* eslint jsx-a11y/tabindex-no-positive: 0 */\nimport {Actions, React, ReactDOM} from 'nylas-exports';\nimport {Menu, RetinaImg} from 'nylas-component-kit';\nimport TemplateStore from './template-store';\n\nclass TemplatePopover extends React.Component {\n  static displayName = 'TemplatePopover';\n\n  static propTypes = {\n    draftClientId: React.PropTypes.string,\n  };\n\n  constructor() {\n    super();\n    this.state = {\n      searchValue: '',\n      templates: TemplateStore.items(),\n    };\n  }\n\n  componentDidMount() {\n    this.unsubscribe = TemplateStore.listen(() => {\n      this.setState({templates: TemplateStore.items()});\n    });\n  }\n\n  componentWillUnmount() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n  }\n\n  _filteredTemplates() {\n    const {searchValue, templates} = this.state;\n\n    if (!searchValue.length) { return templates; }\n\n    return templates.filter((t) => {\n      return t.name.toLowerCase().indexOf(searchValue.toLowerCase()) === 0;\n    });\n  }\n\n  _onSearchValueChange = (event) => {\n    this.setState({searchValue: event.target.value});\n  };\n\n  _onChooseTemplate = (template) => {\n    Actions.insertTemplateId({templateId: template.id, draftClientId: this.props.draftClientId});\n    Actions.closePopover();\n  }\n\n  _onManageTemplates = () => {\n    Actions.showTemplates();\n  };\n\n  _onNewTemplate = () => {\n    Actions.createTemplate({draftClientId: this.props.draftClientId});\n  };\n\n  _onClickButton = () => {\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()\n    Actions.openPopover(\n      this._renderPopover(),\n      {originRect: buttonRect, direction: 'up'}\n    )\n  };\n\n  render() {\n    const filteredTemplates = this._filteredTemplates();\n\n    const headerComponents = [\n      <input\n        type=\"text\"\n        tabIndex=\"1\"\n        key=\"textfield\"\n        className=\"search\"\n        value={this.state.searchValue}\n        onChange={this._onSearchValueChange}\n      />,\n    ];\n\n    const footerComponents = [\n      <div className=\"item\" key=\"new\" onMouseDown={this._onNewTemplate}>Save Draft as Template...</div>,\n      <div className=\"item\" key=\"manage\" onMouseDown={this._onManageTemplates}>Manage Templates...</div>,\n    ];\n\n    return (\n      <Menu\n        className=\"template-picker\"\n        headerComponents={headerComponents}\n        footerComponents={footerComponents}\n        items={filteredTemplates}\n        itemKey={(item) => item.id}\n        itemContent={(item) => item.name}\n        onSelect={this._onChooseTemplate}\n      />\n    );\n  }\n\n}\n\nclass TemplatePicker extends React.Component {\n  static displayName = 'TemplatePicker';\n\n  static propTypes = {\n    draftClientId: React.PropTypes.string,\n  };\n\n  _onClickButton = () => {\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()\n    Actions.openPopover(\n      <TemplatePopover draftClientId={this.props.draftClientId} />,\n      {originRect: buttonRect, direction: 'up'}\n    )\n  };\n\n  render() {\n    return (\n      <button\n        tabIndex={-1}\n        className=\"btn btn-toolbar btn-templates narrow pull-right\"\n        onClick={this._onClickButton}\n        title=\"Insert quick reply…\"\n      >\n        <RetinaImg\n          url=\"nylas://composer-templates/assets/icon-composer-templates@2x.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        &nbsp;\n        <RetinaImg\n          name=\"icon-composer-dropdown.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </button>\n    );\n  }\n}\n\nexport default TemplatePicker;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/lib/template-status-bar.jsx",
    "content": "import {React} from 'nylas-exports';\n\nclass TemplateStatusBar extends React.Component {\n  static displayName = 'TemplateStatusBar';\n\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n  };\n\n  shouldComponentUpdate(nextProps) {\n    return (this._usingTemplate(nextProps) !== this._usingTemplate(this.props));\n  }\n\n  _usingTemplate({draft}) {\n    return draft && draft.body.search(/<code[^>]*class=\"var[^>]*>/i) > 0;\n  }\n\n  render() {\n    if (this._usingTemplate(this.props)) {\n      return (\n        <div className=\"template-status-bar\">\n          Press &quot;tab&quot; to quickly move between the blanks - highlighting will not be visible to recipients.\n        </div>\n      );\n    }\n    return <div />;\n  }\n\n}\n\nTemplateStatusBar.containerStyles = {\n  textAlign: 'center',\n  width: 580,\n  margin: 'auto',\n};\n\nexport default TemplateStatusBar;\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/lib/template-store.es6",
    "content": "/* eslint global-require: 0*/\n\nimport {DraftStore, Actions, QuotedHTMLTransformer} from 'nylas-exports';\nimport NylasStore from 'nylas-store';\nimport path from 'path';\nimport fs from 'fs';\n\nclass TemplateStore extends NylasStore {\n\n  // Support accented characters in template names\n  // https://regex101.com/r/nD3eY8/1\n  static INVALID_TEMPLATE_NAME_REGEX = /[^a-zA-Z\\u00C0-\\u017F0-9_\\- ]+/g;\n\n  constructor() {\n    super();\n    this._init();\n  }\n\n  _init(templatesDir = path.join(NylasEnv.getConfigDirPath(), 'templates')) {\n    this.items = this.items.bind(this);\n    this.templatesDirectory = this.templatesDirectory.bind(this);\n    this._setStoreDefaults = this._setStoreDefaults.bind(this);\n    this._registerListeners = this._registerListeners.bind(this);\n    this._populate = this._populate.bind(this);\n    this._onCreateTemplate = this._onCreateTemplate.bind(this);\n    this._onShowTemplates = this._onShowTemplates.bind(this);\n    this._displayDialog = this._displayDialog.bind(this);\n    this._displayError = this._displayError.bind(this);\n    this.saveNewTemplate = this.saveNewTemplate.bind(this);\n    this.saveTemplate = this.saveTemplate.bind(this);\n    this.deleteTemplate = this.deleteTemplate.bind(this);\n    this.renameTemplate = this.renameTemplate.bind(this);\n    this.getTemplateContents = this.getTemplateContents.bind(this);\n    this._onInsertTemplateId = this._onInsertTemplateId.bind(this);\n    this._setStoreDefaults();\n    this._registerListeners();\n\n    this._templatesDir = templatesDir;\n    this._welcomeName = 'Welcome to Quick Replies.html';\n    this._welcomePath = path.join(__dirname, '..', 'assets', this._welcomeName);\n    this._watcher = null;\n\n    // I know this is a bit of pain but don't do anything that\n    // could possibly slow down app launch\n    fs.exists(this._templatesDir, (exists) => {\n      if (exists) {\n        this._populate();\n        this.watch();\n      } else {\n        fs.mkdir(this._templatesDir, () => {\n          fs.readFile(this._welcomePath, (err, welcome) => {\n            fs.writeFile(path.join(this._templatesDir, this._welcomeName), welcome, () => {\n              this.watch();\n            });\n          });\n        });\n      }\n    });\n  }\n\n  watch() {\n    if (!this._watcher) {\n      this._watcher = fs.watch(this._templatesDir, () => this._populate());\n    }\n  }\n  unwatch() {\n    if (this._watcher) {\n      this._watcher.close();\n    }\n    this._watcher = null;\n  }\n\n  items() {\n    return this._items;\n  }\n\n  templatesDirectory() {\n    return this._templatesDir;\n  }\n\n  _setStoreDefaults() {\n    this._items = [];\n  }\n\n  _registerListeners() {\n    this.listenTo(Actions.insertTemplateId, this._onInsertTemplateId);\n    this.listenTo(Actions.createTemplate, this._onCreateTemplate);\n    this.listenTo(Actions.showTemplates, this._onShowTemplates);\n  }\n\n  _populate() {\n    fs.readdir(this._templatesDir, (err, filenames) => {\n      if (err) {\n        NylasEnv.showErrorDialog({\n          title: \"Cannot scan templates directory\",\n          message: `N1 was unable to read the contents of your templates directory (${this._templatesDir}). You may want to delete this folder or ensure filesystem permissions are set correctly.`,\n        });\n        return;\n      }\n      this._items = [];\n      for (let i = 0, filename; i < filenames.length; i++) {\n        filename = filenames[i];\n        if (filename[0] === '.') { continue; }\n        const displayname = path.basename(filename, path.extname(filename));\n        this._items.push({\n          id: filename,\n          name: displayname,\n          path: path.join(this._templatesDir, filename),\n        });\n      }\n      this.trigger(this);\n    });\n  }\n\n  _onCreateTemplate({draftClientId, name, contents} = {}) {\n    if (draftClientId) {\n      DraftStore.sessionForClientId(draftClientId).then((session) => {\n        const draft = session.draft();\n        const draftName = name || draft.subject.replace(TemplateStore.INVALID_TEMPLATE_NAME_REGEX, '');\n        let draftContents = contents || QuotedHTMLTransformer.removeQuotedHTML(draft.body);\n\n        const sigIndex = draftContents.indexOf('<signature>');\n        draftContents = sigIndex > -1 ? draftContents.slice(0, sigIndex) : draftContents;\n        if (!draftName || draftName.length === 0) {\n          this._displayError('Give your draft a subject to name your template.');\n        }\n        if (!draftContents || draftContents.length === 0) {\n          this._displayError('To create a template you need to fill the body of the current draft.');\n        }\n        this.saveNewTemplate(draftName, draftContents, this._onShowTemplates);\n      });\n      return;\n    }\n    if (!name || name.length === 0) {\n      this._displayError('You must provide a name for your template.');\n    }\n    if (!contents || contents.length === 0) {\n      this._displayError('You must provide contents for your template.');\n    }\n    this.saveNewTemplate(name, contents, this._onShowTemplates);\n  }\n\n  _onShowTemplates() {\n    Actions.switchPreferencesTab('Quick Replies');\n    Actions.openPreferences();\n  }\n\n  _displayError(message) {\n    const dialog = require('electron').remote.dialog;\n    dialog.showErrorBox('Template Creation Error', message);\n  }\n  _displayDialog(title, message, buttons) {\n    const dialog = require('electron').remote.dialog;\n    return (dialog.showMessageBox({\n      title: title,\n      message: title,\n      detail: message,\n      buttons: buttons,\n      type: 'info',\n    }) === 0);\n  }\n\n  saveNewTemplate(name, contents, callback) {\n    if (!name || name.length === 0) {\n      this._displayError('You must provide a template name.');\n      return;\n    }\n\n    if (name.match(TemplateStore.INVALID_TEMPLATE_NAME_REGEX)) {\n      this._displayError('Invalid template name! Names can only contain letters, numbers, spaces, dashes, and underscores.');\n      return;\n    }\n\n    const template = this._getTemplate(name);\n    if (template) {\n      this._displayError('A template with that name already exists!');\n      return;\n    }\n    this.saveTemplate(name, contents, callback);\n    this.trigger(this);\n  }\n\n  _getTemplate(name, id) {\n    for (const template of this._items) {\n      if ((template.name === name || name == null) && (template.id === id || id == null)) {\n        return template;\n      }\n    }\n    return null;\n  }\n\n  saveTemplate(name, contents, callback) {\n    const filename = `${name}.html`;\n    const templatePath = path.join(this._templatesDir, filename);\n\n    let template = this._getTemplate(name);\n    this.unwatch();\n    fs.writeFile(templatePath, contents, (err) => {\n      this.watch();\n      if (err) { this._displayError(err); }\n      if (template === null) {\n        template = {\n          id: filename,\n          name: name,\n          path: templatePath,\n        };\n        this._items.unshift(template);\n      }\n      if (callback) {\n        callback(template);\n      }\n    });\n  }\n\n  deleteTemplate(name, callback) {\n    const template = this._getTemplate(name);\n    if (!template) { return; }\n\n    if (this._displayDialog(\n        'Delete this template?',\n        'The template and its file will be permanently deleted.',\n        ['Delete', 'Cancel']\n    )) {\n      fs.unlink(template.path, () => {\n        this._populate();\n        if (callback) {\n          callback();\n        }\n      });\n    }\n  }\n\n  renameTemplate(oldName, newName, callback) {\n    const template = this._getTemplate(oldName);\n    if (!template) { return; }\n\n    if (newName.match(TemplateStore.INVALID_TEMPLATE_NAME_REGEX)) {\n      this._displayError('Invalid template name! Names can only contain letters, numbers, spaces, dashes, and underscores.');\n      return;\n    }\n    if (newName.length === 0) {\n      this._displayError('You must provide a template name.');\n      return;\n    }\n\n    const newFilename = `${newName}.html`;\n    const oldPath = path.join(this._templatesDir, `${oldName}.html`);\n    const newPath = path.join(this._templatesDir, newFilename);\n    fs.rename(oldPath, newPath, () => {\n      template.name = newName;\n      template.id = newFilename;\n      template.path = newPath;\n      this.trigger(this);\n      callback(template);\n    });\n  }\n\n  _onInsertTemplateId({templateId, draftClientId} = {}) {\n    this.getTemplateContents(templateId, (templateBody) => {\n      DraftStore.sessionForClientId(draftClientId).then((session) => {\n        let proceed = true;\n        if (!session.draft().pristine && !session.draft().hasEmptyBody()) {\n          proceed = this._displayDialog(\n              'Replace draft contents?',\n              'It looks like your draft already has some content. Loading this template will ' +\n              'overwrite all draft contents.',\n              ['Replace contents', 'Cancel']\n          );\n        }\n\n        if (proceed) {\n          const draftContents = QuotedHTMLTransformer.removeQuotedHTML(session.draft().body);\n          const sigIndex = draftContents.indexOf('<signature>');\n          const signature = sigIndex > -1 ? draftContents.slice(sigIndex) : '';\n\n          const draftHtml = QuotedHTMLTransformer.appendQuotedHTML(templateBody + signature, session.draft().body);\n          Actions.recordUserEvent(\"Email Template Inserted\")\n          session.changes.add({body: draftHtml});\n        }\n      });\n    });\n  }\n\n  getTemplateContents(templateId, callback) {\n    const template = this._getTemplate(null, templateId);\n    if (!template) { return; }\n\n    fs.readFile(template.path, (err, data) => {\n      const body = data.toString();\n      callback(body);\n    });\n  }\n}\n\nconst store = new TemplateStore();\nexport default store\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/package.json",
    "content": "{\n  \"name\": \"composer-templates\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n\n  \"isOptional\": true,\n\n  \"title\": \"Quick Replies\",\n  \"description\": \"Create templated messages you can use to pre-fill the composer.\",\n  \"icon\": \"./icon.png\",\n\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/spec/template-store-spec.es6",
    "content": "import fs from 'fs';\nimport { remote } from 'electron';\nimport {Message, DraftStore} from 'nylas-exports';\nimport TemplateStore from '../lib/template-store';\n\nconst { shell } = remote;\n\nconst stubTemplatesDir = '~/.nylas-mail/templates';\n\nconst stubTemplateFiles = {\n  'template1.html': '<p>bla1</p>',\n  'template2.html': '<p>bla2</p>',\n};\n\nconst stubTemplates = [\n  {id: 'template1.html', name: 'template1', path: `${stubTemplatesDir}/template1.html`},\n  {id: 'template2.html', name: 'template2', path: `${stubTemplatesDir}/template2.html`},\n];\n\nxdescribe('TemplateStore', function templateStore() {\n  beforeEach(() => {\n    spyOn(fs, 'mkdir');\n    spyOn(shell, 'showItemInFolder').andCallFake(() => {});\n    spyOn(fs, 'writeFile').andCallFake((path, contents, callback) => {\n      callback(null);\n    });\n    spyOn(fs, 'readFile').andCallFake((path, callback) => {\n      const filename = path.split('/').pop();\n      callback(null, stubTemplateFiles[filename]);\n    });\n  });\n\n  it('should create the templates folder if it does not exist', () => {\n    spyOn(fs, 'exists').andCallFake((path, callback) => callback(false));\n    TemplateStore._init(stubTemplatesDir);\n    expect(fs.mkdir).toHaveBeenCalled();\n  });\n\n  it('should expose templates in the templates directory', () => {\n    let watchCallback;\n    spyOn(fs, 'exists').andCallFake((path, callback) => { callback(true); });\n    spyOn(fs, 'watch').andCallFake((path, callback) => { watchCallback = callback });\n    spyOn(fs, 'readdir').andCallFake((path, callback) => { callback(null, Object.keys(stubTemplateFiles)); });\n    TemplateStore._init(stubTemplatesDir);\n    watchCallback();\n    expect(TemplateStore.items()).toEqual(stubTemplates);\n  });\n\n  it('should watch the templates directory and reflect changes', () => {\n    let watchCallback = null;\n    let watchFired = false;\n\n    spyOn(fs, 'exists').andCallFake((path, callback) => callback(true));\n    spyOn(fs, 'watch').andCallFake((path, callback) => { watchCallback = callback });\n    spyOn(fs, 'readdir').andCallFake((path, callback) => {\n      if (watchFired) {\n        callback(null, Object.keys(stubTemplateFiles));\n      } else {\n        callback(null, []);\n      }\n    });\n    TemplateStore._init(stubTemplatesDir);\n    expect(TemplateStore.items()).toEqual([]);\n\n    watchFired = true;\n    watchCallback();\n    expect(TemplateStore.items()).toEqual(stubTemplates);\n  });\n\n  describe('insertTemplateId', () => {\n    xit('should insert the template with the given id into the draft with the given id', () => {\n      let watchCallback;\n      spyOn(fs, 'exists').andCallFake((path, callback) => { callback(true); });\n      spyOn(fs, 'watch').andCallFake((path, callback) => { watchCallback = callback });\n      spyOn(fs, 'readdir').andCallFake((path, callback) => { callback(null, Object.keys(stubTemplateFiles)); });\n      TemplateStore._init(stubTemplatesDir);\n      watchCallback();\n      const add = jasmine.createSpy('add');\n      spyOn(DraftStore, 'sessionForClientId').andCallFake(() => {\n        return Promise.resolve({changes: {add}});\n      });\n\n      runs(() => {\n        TemplateStore._onInsertTemplateId({\n          templateId: 'template1.html',\n          draftClientId: 'localid-draft',\n        });\n      });\n      waitsFor(() => add.calls.length > 0);\n      runs(() => {\n        expect(add).toHaveBeenCalledWith({\n          body: stubTemplateFiles['template1.html'],\n        });\n      });\n    });\n  });\n\n  describe('onCreateTemplate', () => {\n    beforeEach(() => {\n      let d;\n      spyOn(DraftStore, 'sessionForClientId').andCallFake((draftClientId) => {\n        if (draftClientId === 'localid-nosubject') {\n          d = new Message({subject: '', body: '<p>Body</p>'});\n        } else {\n          d = new Message({subject: 'Subject', body: '<p>Body</p>'});\n        }\n        const session = {draft() { return d; }};\n        return Promise.resolve(session);\n      });\n      TemplateStore._init(stubTemplatesDir);\n    });\n\n    xit('should create a template with the given name and contents', () => {\n      const ref = TemplateStore.items();\n      TemplateStore._onCreateTemplate({name: '123', contents: 'bla'});\n      const item = (ref != null ? ref[0] : undefined);\n      expect(item.id).toBe('123.html');\n      expect(item.name).toBe('123');\n      expect(item.path.split('/').pop()).toBe('123.html');\n    });\n\n    xit('should display an error if no name is provided', () => {\n      spyOn(TemplateStore, '_displayError');\n      TemplateStore._onCreateTemplate({contents: 'bla'});\n      expect(TemplateStore._displayError).toHaveBeenCalled();\n    });\n\n    xit('should display an error if no content is provided', () => {\n      spyOn(TemplateStore, '_displayError');\n      TemplateStore._onCreateTemplate({name: 'bla'});\n      expect(TemplateStore._displayError).toHaveBeenCalled();\n    });\n\n    xit('should save the template file to the templates folder', () => {\n      TemplateStore._onCreateTemplate({name: '123', contents: 'bla'});\n      const path = `${stubTemplatesDir}/123.html`;\n      expect(fs.writeFile).toHaveBeenCalled();\n      expect(fs.writeFile.mostRecentCall.args[0]).toEqual(path);\n      expect(fs.writeFile.mostRecentCall.args[1]).toEqual('bla');\n    });\n\n    xit('should open the template so you can see it', () => {\n      TemplateStore._onCreateTemplate({name: '123', contents: 'bla'});\n      expect(shell.showItemInFolder).toHaveBeenCalled();\n    });\n\n    describe('when given a draft id', () => {\n      xit('should create a template from the name and contents of the given draft', () => {\n        spyOn(TemplateStore, 'trigger');\n        spyOn(TemplateStore, '_populate');\n        runs(() => {\n          TemplateStore._onCreateTemplate({draftClientId: 'localid-b'});\n        });\n        waitsFor(() => TemplateStore.trigger.callCount > 0);\n        runs(() => {\n          expect(TemplateStore.items().length).toEqual(1);\n        });\n      });\n\n      it('should display an error if the draft has no subject', () => {\n        spyOn(TemplateStore, '_displayError');\n        spyOn(fs, 'watch');\n        runs(() => {\n          TemplateStore._onCreateTemplate({draftClientId: 'localid-nosubject'});\n        });\n        waitsFor(() => TemplateStore._displayError.callCount > 0);\n        runs(() => {\n          expect(TemplateStore._displayError).toHaveBeenCalled();\n        });\n      });\n    });\n  });\n\n  describe('onShowTemplates', () => {\n    xit('should open the templates folder in the Finder', () => {\n      TemplateStore._onShowTemplates();\n      expect(shell.showItemInFolder).toHaveBeenCalled();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-templates/stylesheets/message-templates.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n@code-bg-color: #fcf4db;\n\n.template-picker {\n  .content-container {\n    height:150px;\n    width: 210px;\n    overflow-y:scroll;\n  }\n  .footer-container {\n    border-top: 1px solid @border-color-secondary;\n  }\n}\n\n.template-status-bar {\n  background-color: @code-bg-color;\n  color: darken(@code-bg-color, 70%);\n  border: 1.5px solid darken(@code-bg-color, 10%);\n  border-radius: @border-radius-small;\n  padding-top: @padding-small-vertical @padding-small-horizontal @padding-small-vertical @padding-small-horizontal;\n  font-size: @font-size-small;\n  margin-bottom: 10px;\n}\n\n.compose-body,.container-templates .contenteditable {\n  code.var {\n    font: inherit;\n    padding:0;\n    padding-left:2px;\n    padding-right:2px;\n    border-bottom: 1.5px solid darken(@code-bg-color, 10%);\n    background-color: @code-bg-color;\n    &.empty {\n      color:darken(@code-bg-color, 50%);\n    }\n  }\n}\n\n\n.container-templates {\n  max-width: 640px;\n  margin: 0 auto;\n\n  .no-templates-message {\n    text-align: center;\n    margin-top: 50px;\n  }\n\n  .template-wrap {\n    position: relative;\n    border: 1px solid @input-border-color;\n    background-color: @white;\n    padding: 10px;\n    margin-top: 20px;\n    min-height: 200px;\n    display: flex;\n\n    .contenteditable-container {\n      min-height: 200px;\n    }\n\n    textarea {\n      border: 0;\n      padding: 0;\n      flex: 1;\n      font-family: monospace;\n      font-size: 0.9em;\n    }\n  }\n\n  .section-title {\n    display: inline-flex;\n    width: 100%;\n    flex-wrap: wrap;\n    align-items: center;\n\n    span {\n      line-height: 1.7;\n    }\n    select {\n      flex-basis: 100px;\n      flex-grow: 1;\n      margin: 0 0 0 8px;\n    }\n    input {\n      flex-grow: 1;\n      margin: 0 0 0 8px;\n    }\n  }\n\n\n  .section-body {\n    padding: 10px 0 0 0;\n\n    .menu {\n      border: solid thin #CCC;\n      margin-right: 5px;\n      min-height: 200px;\n      .menu-items {\n        margin:0;\n        padding:0;\n        list-style: none;\n\n        li { padding: 6px; }\n      }\n    }\n    .menu-horizontal {\n      height: 100%;\n      .menu-items {\n        height:100%;\n        margin: 0;\n        padding: 0;\n        list-style: none;\n        li {\n          text-align:center;\n          width:40px;\n          display:inline-block;\n          padding:8px 16px 8px 16px;\n          border-right: solid thin #CCC;\n        }\n      }\n    }\n    .template-area {\n      border: solid thin #CCC;\n      min-height: 200px;\n    }\n    .menu-footer {\n      border: solid thin #CCC;\n      overflow: auto;\n    }\n    .template-footer {\n      border: solid thin #CCC;\n      overflow: auto;\n\n      .edit-html-button {\n        float: right;\n        margin: 6px;\n      }\n    }\n  }\n\n  .template-name-btn {\n    float: right;\n    margin: 0 6px;\n  }\n  .template-name-input {\n    display: inline-block;\n    width: 100px;\n  }\n  .editor-note {\n    color: #AAA;\n    font-size: small;\n    margin-top: 5px;\n  }\n\n  .templates-instructions {\n    color: #333;\n    font-size: small;\n    margin-top: 20px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-translate/README.md",
    "content": "\n## Translate\n\nA package for N1 that translates draft text into other languages using the Yandex Translation API.\n\n<img src=\"https://raw.githubusercontent.com/nylas/nylas-mail/master/internal_packages/composer-translate/examples-screencap-translate.png\"/>\n\n#### Enable this plugin\n\n1. Download and run N1\n\n2. Navigate to Preferences > Plugins and click \"Enable\" beside the plugin.\n\n#### Build documentation\n\n```\ncjsx-transform lib/main.cjsx > docs/main.coffee\ndocco docs/main.coffee\nrm docs/main.coffee\n```\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-translate/lib/main.jsx",
    "content": "/* eslint global-require: \"off\" */\n\n// // Translation Plugin\n// Last Revised: Feb. 29, 2016 by Ben Gotow\n\n// TranslateButton is a simple React component that allows you to select\n// a language from a popup menu and translates draft text into that language.\n\nimport request from 'request'\n\nimport {\n  React,\n  ReactDOM,\n  ComponentRegistry,\n  QuotedHTMLTransformer,\n  Actions,\n} from 'nylas-exports';\n\nimport {\n  Menu,\n  RetinaImg,\n} from 'nylas-component-kit';\n\nconst YandexTranslationURL = 'https://translate.yandex.net/api/v1.5/tr.json/translate';\nconst YandexTranslationKey = 'trnsl.1.1.20150415T044616Z.24814c314120d022.0a339e2bc2d2337461a98d5ec9863fc46e42735e';\nconst YandexLanguages = {\n  English: 'en',\n  Spanish: 'es',\n  Russian: 'ru',\n  Chinese: 'zh',\n  French: 'fr',\n  German: 'de',\n  Italian: 'it',\n  Japanese: 'ja',\n  Portuguese: 'pt',\n  Korean: 'ko',\n};\n\nclass TranslateButton extends React.Component {\n\n  // Adding a `displayName` makes debugging React easier\n  static displayName = 'TranslateButton';\n\n  // Since our button is being injected into the Composer Footer,\n  // we receive the local id of the current draft as a `prop` (a read-only\n  // property). Since our code depends on this prop, we mark it as a requirement.\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n  };\n\n  shouldComponentUpdate(nextProps) {\n    // Our render method doesn't use the provided `draft`, and the draft changes\n    // constantly (on every keystroke!) `shouldComponentUpdate` helps keep N1 fast.\n    return nextProps.session !== this.props.session;\n  }\n\n  _onError(error) {\n    Actions.closePopover()\n    const dialog = require('electron').remote.dialog;\n    dialog.showErrorBox('Language Conversion Failed', error.toString());\n  }\n\n  _onTranslate = (lang) => {\n    Actions.closePopover()\n\n    // Obtain the session for the current draft. The draft session provides us\n    // the draft object and also manages saving changes to the local cache and\n    // Nilas API as multiple parts of the application touch the draft.\n    const draftHtml = this.props.draft.body;\n    const text = QuotedHTMLTransformer.removeQuotedHTML(draftHtml);\n\n    Actions.recordUserEvent(\"Email Translated\", {\n      language: YandexLanguages[lang],\n    })\n\n    const query = {\n      key: YandexTranslationKey,\n      lang: YandexLanguages[lang],\n      text: text,\n      format: 'html',\n    };\n\n    // Use Node's `request` library to perform the translation using the Yandex API.\n    request({url: YandexTranslationURL, qs: query}, (error, resp, data) => {\n      if (resp.statusCode !== 200) {\n        this._onError(error);\n        return;\n      }\n\n      const json = JSON.parse(data);\n      let translated = json.text.join('');\n\n      // The new text of the draft is our translated response, plus any quoted text\n      // that we didn't process.\n      translated = QuotedHTMLTransformer.appendQuotedHTML(translated, draftHtml);\n\n      // To update the draft, we add the new body to it's session. The session object\n      // automatically marshalls changes to the database and ensures that others accessing\n      // the same draft are notified of changes.\n      this.props.session.changes.add({body: translated});\n      this.props.session.changes.commit();\n    });\n  };\n\n  _onClickTranslateButton = () => {\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()\n    Actions.openPopover(\n      this._renderPopover(),\n      {originRect: buttonRect, direction: 'up'}\n    )\n  };\n\n  // Helper method that will render the contents of our popover.\n  _renderPopover() {\n    const headerComponents = [\n      <span>Translate:</span>,\n    ];\n    return (\n      <Menu\n        className=\"translate-language-picker\"\n        items={Object.keys(YandexLanguages)}\n        itemKey={(item) => item}\n        itemContent={(item) => item}\n        headerComponents={headerComponents}\n        defaultSelectedIndex={-1}\n        onSelect={this._onTranslate}\n      />\n    )\n  }\n\n  // The `render` method returns a React Virtual DOM element. This code looks\n  // like HTML, but don't be fooled. The JSX preprocessor converts\n  // `<a href=\"http://facebook.github.io/react/\">Hello!</a>`\n  // into Javascript objects which describe the HTML you want:\n  // `React.createElement('a', {href: 'http://facebook.github.io/react/'}, 'Hello!')`\n\n  // We're rendering a `Menu` inside our Popover, and using a `RetinaImg` for the button.\n  // These components are part of N1's standard `nylas-component-kit` library,\n  // and make it easy to build interfaces that match the rest of N1's UI.\n  //\n  // For example, using the `RetinaImg` component makes it easy to display an\n  // image from our package. `RetinaImg` will automatically chose the best image\n  // format for our display.\n  render() {\n    return (\n      <button\n        tabIndex={-1}\n        className=\"btn btn-toolbar btn-translate pull-right\"\n        onClick={this._onClickTranslateButton}\n        title=\"Translate email body…\"\n      >\n        <RetinaImg\n          mode={RetinaImg.Mode.ContentIsMask}\n          url=\"nylas://composer-translate/assets/icon-composer-translate@2x.png\"\n        />\n        &nbsp;\n        <RetinaImg\n          name=\"icon-composer-dropdown.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </button>\n    );\n  }\n}\n\n/*\nAll packages must export a basic object that has at least the following 3\nmethods:\n\n1. `activate` - Actions to take once the package gets turned on.\nPre-enabled packages get activated on N1 bootup. They can also be\nactivated manually by a user.\n\n2. `deactivate` - Actions to take when a package gets turned off. This can\nhappen when a user manually disables a package.\n\n3. `serialize` - A simple serializable object that gets saved to disk\nbefore N1 quits. This gets passed back into `activate` next time N1 boots\nup or your package is manually activated.\n*/\n\nexport function activate() {\n  ComponentRegistry.register(TranslateButton, {\n    role: 'Composer:ActionButton',\n  });\n}\n\nexport function serialize() {\n\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(TranslateButton);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-translate/package.json",
    "content": "{\n  \"name\": \"composer-translate\",\n  \"version\": \"0.2.0\",\n  \"main\": \"./lib/main\",\n\n  \"isOptional\": true,\n\n  \"title\": \"Translation\",\n  \"description\": \"Translate your drafts in the composer into other languages using the Yandex Translation API.\",\n  \"icon\": \"./icon.png\",\n\n  \"license\": \"GPL-3.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nylas/nylas-mail\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/composer-translate/stylesheets/translate.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.translate-language-picker {\n  .footer-container {\n    display: none;\n  }\n  .content-container {\n    height:185px;\n    width:170px;\n    overflow:scroll;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/contact-rankings/lib/contact-rankings-cache.es6",
    "content": "import _ from 'underscore'\nimport moment from 'moment-timezone'\nimport {\n  IdentityStore,\n  AccountStore,\n  NylasAPI,\n  NylasAPIRequest,\n  FolderSyncProgressStore,\n} from 'nylas-exports'\nimport RefreshingJSONCache from './refreshing-json-cache'\n\n// Stores contact rankings\nclass ContactRankingsCache extends RefreshingJSONCache {\n  constructor(accountId) {\n    super({\n      key: `ContactRankingsFor${accountId}`,\n      version: 1,\n      refreshInterval: moment.duration(60, 'seconds').asMilliseconds(),\n      maxRefreshInterval: moment.duration(24, 'hours').asMilliseconds(),\n    })\n    this._accountId = accountId\n  }\n\n  _nextRefreshInterval() {\n    // For the first 15 minutes, refresh roughly once every minute so that the\n    // experience of composing drafts during initial is less annoying.\n    const initialLimit = (60 * 1000) + 15;\n    if (this.refreshInterval < initialLimit) {\n      return this.refreshInterval + 1;\n    }\n    // After the first 15 minutes, refresh twice as long each time up to the max.\n    return Math.min(this.refreshInterval * 2, this.maxRefreshInterval);\n  }\n\n  fetchData = (callback) => {\n    if (NylasEnv.inSpecMode()) { return }\n    if (!IdentityStore.identity()) { return }\n\n    const request = new NylasAPIRequest({\n      api: NylasAPI,\n      options: {\n        accountId: this._accountId,\n        path: \"/contacts/rankings\",\n      },\n    })\n\n    request.run()\n    .then((json) => {\n      if (!json || !(json instanceof Array)) return\n\n      // Convert rankings into the format needed for quick lookup\n      const rankings = {}\n      for (const [email, rank] of json) {\n        rankings[email.toLowerCase()] = rank\n      }\n      callback(rankings)\n\n      this.refreshInterval = this._nextRefreshInterval();\n    })\n    .catch((err) => {\n      console.warn(`Request for Contact Rankings failed for\n                    account ${this._accountId}. ${err}`)\n    })\n  }\n}\n\nclass ContactRankingsCacheManager {\n  constructor() {\n    this.accountCaches = {};\n    this.unsubscribers = [];\n    this.onAccountsChanged = _.debounce(this.onAccountsChanged, 100);\n  }\n\n  activate() {\n    this.onAccountsChanged();\n    this.unsubscribers = [AccountStore.listen(this.onAccountsChanged)];\n  }\n\n  deactivate() {\n    this.unsubscribers.forEach(unsub => unsub());\n  }\n\n  onAccountsChanged = async () => {\n    const previousIDs = Object.keys(this.accountCaches);\n    const latestIDs = AccountStore.accounts().map(a => a.id);\n    if (_.isEqual(previousIDs, latestIDs)) {\n      return;\n    }\n\n    const newIDs = _.difference(latestIDs, previousIDs);\n    const removedIDs = _.difference(previousIDs, latestIDs);\n\n    console.log(`ContactRankingsCache: Updating contact rankings; added = ${latestIDs}, removed = ${removedIDs}`);\n\n    for (const newID of newIDs) {\n      // Wait until the account has started syncing before trying to fetch\n      // contact rankings\n      await FolderSyncProgressStore.whenCategoryListSynced(newID)\n      this.accountCaches[newID] = new ContactRankingsCache(newID);\n      this.accountCaches[newID].start();\n    }\n\n    for (const removedID of removedIDs) {\n      if (this.accountCaches[removedID]) {\n        this.accountCaches[removedID].end();\n        this.accountCaches[removedID] = null;\n      }\n    }\n  }\n}\n\nexport default new ContactRankingsCacheManager();\n"
  },
  {
    "path": "packages/client-app/internal_packages/contact-rankings/lib/main.es6",
    "content": "import ContactRankingsCache from './contact-rankings-cache'\n\n\nexport function activate() {\n  ContactRankingsCache.activate();\n}\n\nexport function deactivate() {\n  ContactRankingsCache.deactivate();\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/contact-rankings/lib/refreshing-json-cache.coffee",
    "content": "_ = require 'underscore'\n{NylasStore, DatabaseStore} = require 'nylas-exports'\n\n\nclass RefreshingJSONCache\n\n  constructor: ({@key, @version, @refreshInterval, @maxRefreshInterval}) ->\n    @_timeoutId = null\n\n  start: ->\n    # Clear any scheduled actions\n    @end()\n\n    # Look up existing data from db\n    DatabaseStore.findJSONBlob(@key).then (json) =>\n      if json? and json.refreshInterval\n        @refreshInterval = json.refreshInterval\n\n      # Refresh immediately if json is missing or version is outdated. Otherwise,\n      # compute next refresh time and schedule\n      timeUntilRefresh = 0\n      if json? and json.version is @version\n        timeUntilRefresh = Math.max(0, @refreshInterval - (Date.now() - json.time))\n\n      @_timeoutId = setTimeout(@refresh, timeUntilRefresh)\n\n  reset: ->\n    # Clear db value, turn off any scheduled actions\n    DatabaseStore.inTransaction (t) => t.persistJSONBlob(@key, {})\n    @end()\n\n  end: ->\n    # Turn off any scheduled actions\n    clearInterval(@_timeoutId) if @_timeoutId\n    @_timeoutId = null\n\n  refresh: =>\n    # Set up next refresh call\n    clearTimeout(@_timeoutId) if @_timeoutId\n    @_timeoutId = setTimeout(@refresh, @refreshInterval)\n\n    # Call fetch data function, save it to the database\n    @fetchData (newValue) =>\n      DatabaseStore.inTransaction (t) =>\n        t.persistJSONBlob(@key, {\n          version: @version\n          time: Date.now()\n          value: newValue\n          refreshInterval: @refreshInterval\n        })\n\n  fetchData: (callback) =>\n    throw new Error(\"Subclasses should override this method.\")\n\n\n\nmodule.exports = RefreshingJSONCache\n"
  },
  {
    "path": "packages/client-app/internal_packages/contact-rankings/package.json",
    "content": "{\n  \"name\": \"deltas\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Delta connection handling in Nylas Mail\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"work\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/draft-list-columns.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\n{Actions, Utils} = require 'nylas-exports'\n{InjectedComponentSet, ListTabular} = require 'nylas-component-kit'\n\n\nsnippet = (html) =>\n  return \"\" unless html and typeof(html) is 'string'\n  try\n    text = Utils.extractTextFromHtml(html, maxLength: 400)\n    text[0..200]\n  catch\n    return \"\"\n\nsubject = (subj) ->\n  if (subj ? \"\").trim().length is 0\n    return <span className=\"no-subject\">(No Subject)</span>\n  else\n    return text = Utils.extractTextFromHtml(subj)\n\nParticipantsColumn = new ListTabular.Column\n  name: \"Participants\"\n  width: 200\n  resolver: (draft) =>\n    list = [].concat(draft.to, draft.cc, draft.bcc)\n\n    if list.length > 0\n      <div className=\"participants\">\n        <span>{list.map((p) => p.displayName()).join(', ')}</span>\n      </div>\n    else\n      <div className=\"participants no-recipients\">\n        (No Recipients)\n      </div>\n\nContentsColumn = new ListTabular.Column\n  name: \"Contents\"\n  flex: 4\n  resolver: (draft) =>\n    attachments = []\n    if draft.files?.length > 0\n      attachments = <div className=\"thread-icon thread-icon-attachment\"></div>\n    <span className=\"details\">\n      <span className=\"subject\">{subject(draft.subject)}</span>\n      <span className=\"snippet\">{snippet(draft.body)}</span>\n      {attachments}\n    </span>\n\nStatusColumn = new ListTabular.Column\n  name: \"State\"\n  resolver: (draft) =>\n    <InjectedComponentSet\n      inline={true}\n      containersRequired={false}\n      matching={role: \"DraftList:DraftStatus\"}\n      className=\"draft-list-injected-state\"\n      exposedProps={{draft}}/>\n\nmodule.exports =\n  Wide: [ParticipantsColumn, ContentsColumn, StatusColumn]\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/draft-list-send-status.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {DateUtils} from 'nylas-exports'\nimport {Flexbox} from 'nylas-component-kit'\nimport SendingProgressBar from './sending-progress-bar'\n\nexport default class DraftListSendStatus extends Component {\n  static displayName = 'DraftListSendStatus';\n\n  static propTypes = {\n    draft: PropTypes.object,\n  };\n\n  static containerRequired = false;\n\n  render() {\n    const {draft} = this.props\n    if (draft.uploadTaskId) {\n      return (\n        <Flexbox style={{width: 150, whiteSpace: 'nowrap'}}>\n          <SendingProgressBar\n            style={{flex: 1, marginRight: 10}}\n            progress={draft.uploadProgress * 100}\n          />\n        </Flexbox>\n      )\n    }\n    return <span className=\"timestamp\">{DateUtils.shortTimeString(draft.date)}</span>\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/draft-list-store.coffee",
    "content": "NylasStore = require 'nylas-store'\n_ = require 'underscore'\n{Rx,\n Message,\n OutboxStore,\n AccountStore,\n MutableQueryResultSet,\n MutableQuerySubscription,\n ObservableListDataSource,\n FocusedPerspectiveStore,\n DatabaseStore} = require 'nylas-exports'\n{ListTabular} = require 'nylas-component-kit'\n\nclass DraftListStore extends NylasStore\n  constructor: ->\n    @listenTo FocusedPerspectiveStore, @_onPerspectiveChanged\n    @_createListDataSource()\n\n  dataSource: =>\n    @_dataSource\n\n  selectionObservable: =>\n    return Rx.Observable.fromListSelection(@)\n\n  # Inbound Events\n\n  _onPerspectiveChanged: =>\n    @_createListDataSource()\n\n  # Internal\n\n  _createListDataSource: =>\n    mailboxPerspective = FocusedPerspectiveStore.current()\n\n    if mailboxPerspective.drafts\n      query = DatabaseStore.findAll(Message)\n        .include(Message.attributes.body)\n        .order(Message.attributes.date.descending())\n        .where(draft: true)\n        .page(0, 1)\n\n      # Adding a \"account_id IN (a,b,c)\" clause to our query can result in a full\n      # table scan. Don't add the where clause if we know we want results from all.\n      if mailboxPerspective.accountIds.length < AccountStore.accounts().length\n        query.where(accountId: mailboxPerspective.accountIds)\n\n      subscription = new MutableQuerySubscription(query, {emitResultSet: true})\n      $resultSet = Rx.Observable.fromNamedQuerySubscription('draft-list', subscription)\n      $resultSet = Rx.Observable.combineLatest [\n        $resultSet,\n        Rx.Observable.fromStore(OutboxStore)\n      ], (resultSet, outbox) =>\n\n        # Generate a new result set that includes additional information on\n        # the draft objects. This is similar to what we do in the thread-list,\n        # where we set thread.__messages to the message array.\n        resultSetWithTasks = new MutableQueryResultSet(resultSet)\n\n        mailboxPerspective.accountIds.forEach (aid) =>\n          OutboxStore.itemsForAccount(aid).forEach (task) =>\n            draft = resultSet.modelWithId(task.draftClientId)\n            if draft\n              draft = draft.clone()\n              draft.uploadTaskId = task.id\n              draft.uploadProgress = task.progress\n              resultSetWithTasks.updateModel(draft)\n\n        return resultSetWithTasks.immutableClone()\n\n      @_dataSource = new ObservableListDataSource($resultSet, subscription.replaceRange)\n    else\n      @_dataSource = new ListTabular.DataSource.Empty()\n\n    @trigger(@)\n\nmodule.exports = new DraftListStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/draft-list-toolbar.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {ListensToObservable, MultiselectToolbar, InjectedComponentSet} from 'nylas-component-kit'\n\nimport DraftListStore from './draft-list-store'\n\n\nfunction getObservable() {\n  return DraftListStore.selectionObservable()\n}\n\nfunction getStateFromObservable(items) {\n  if (!items) {\n    return {items: []}\n  }\n  return {items}\n}\n\nclass DraftListToolbar extends Component {\n  static displayName = 'DraftListToolbar';\n\n  static propTypes = {\n    items: PropTypes.array,\n  };\n\n  onClearSelection = () => {\n    DraftListStore.dataSource().selection.clear()\n  };\n\n  render() {\n    const {selection} = DraftListStore.dataSource()\n    const {items} = this.props\n\n    // Keep all of the exposed props from deprecated regions that now map to this one\n    const toolbarElement = (\n      <InjectedComponentSet\n        matching={{role: \"DraftActionsToolbarButton\"}}\n        exposedProps={{selection, items}}\n      />\n    )\n\n    return (\n      <MultiselectToolbar\n        collection=\"draft\"\n        selectionCount={items.length}\n        toolbarElement={toolbarElement}\n        onClearSelection={this.onClearSelection}\n      />\n    )\n  }\n}\n\nexport default ListensToObservable(DraftListToolbar, {getObservable, getStateFromObservable})\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/draft-list.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\n{Actions} = require 'nylas-exports'\n{FluxContainer,\n FocusContainer,\n EmptyListState,\n MultiselectList} = require 'nylas-component-kit'\nDraftListStore = require './draft-list-store'\nDraftListColumns = require './draft-list-columns'\n\nclass DraftList extends React.Component\n  @displayName: 'DraftList'\n  @containerRequired: false\n\n  render: =>\n    <FluxContainer\n      stores=[DraftListStore]\n      getStateFromStores={ -> dataSource: DraftListStore.dataSource() }>\n      <FocusContainer collection=\"draft\">\n        <MultiselectList\n          className=\"draft-list\"\n          columns={DraftListColumns.Wide}\n          onDoubleClick={@_onDoubleClick}\n          EmptyComponent={EmptyListState}\n          keymapHandlers={@_keymapHandlers()}\n          itemPropsProvider={@_itemPropsProvider}\n          itemHeight={39}\n        />\n      </FocusContainer>\n    </FluxContainer>\n\n  _itemPropsProvider: (draft) ->\n    props = {}\n    props.className = 'sending' if draft.uploadTaskId\n    props\n\n  _keymapHandlers: =>\n    'core:delete-item': @_onRemoveFromView\n    'core:gmail-remove-from-view': @_onRemoveFromView\n    'core:remove-from-view': @_onRemoveFromView\n\n  _onDoubleClick: (draft) =>\n    unless draft.uploadTaskId\n      Actions.composePopoutDraft(draft.clientId)\n\n  # Additional Commands\n\n  _onRemoveFromView: =>\n    drafts = DraftListStore.dataSource().selection.items()\n    Actions.destroyDraft(draft.clientId) for draft in drafts\n\nmodule.exports = DraftList\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/draft-toolbar-buttons.cjsx",
    "content": "React = require \"react\"\n{RetinaImg} = require 'nylas-component-kit'\n{Actions, FocusedContentStore} = require \"nylas-exports\"\n\nclass DraftDeleteButton extends React.Component\n  @displayName: 'DraftDeleteButton'\n  @containerRequired: false\n\n  @propTypes:\n    selection: React.PropTypes.object.isRequired\n\n  render: ->\n    <button style={order:-100}\n            className=\"btn btn-toolbar\"\n            title=\"Delete\"\n            onClick={@_destroySelected}>\n      <RetinaImg name=\"icon-composer-trash.png\" mode={RetinaImg.Mode.ContentIsMask} />\n    </button>\n\n  _destroySelected: =>\n    for item in @props.selection.items()\n      Actions.destroyDraft(item.clientId)\n    @props.selection.clear()\n    return\n\nmodule.exports = {DraftDeleteButton}\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/main.es6",
    "content": "import {WorkspaceStore, ComponentRegistry, Actions} from 'nylas-exports'\nimport DraftList from './draft-list'\nimport DraftListToolbar from './draft-list-toolbar'\nimport DraftListSendStatus from './draft-list-send-status'\nimport {DraftDeleteButton} from \"./draft-toolbar-buttons\"\n\n\nexport function activate() {\n  WorkspaceStore.defineSheet(\n    'Drafts',\n    {root: true},\n    {list: ['RootSidebar', 'DraftList']}\n  );\n  if (NylasEnv.savedState.perspective &&\n    NylasEnv.savedState.perspective.type === \"DraftsMailboxPerspective\") {\n    Actions.selectRootSheet(WorkspaceStore.Sheet.Drafts);\n  }\n\n  ComponentRegistry.register(DraftList, {location: WorkspaceStore.Location.DraftList})\n  ComponentRegistry.register(DraftListToolbar, {location: WorkspaceStore.Location.DraftList.Toolbar})\n  ComponentRegistry.register(DraftDeleteButton, {role: 'DraftActionsToolbarButton'})\n  ComponentRegistry.register(DraftListSendStatus, {role: 'DraftList:DraftStatus'})\n}\n\n\nexport function deactivate() {\n  ComponentRegistry.unregister(DraftList)\n  ComponentRegistry.unregister(DraftListToolbar)\n  ComponentRegistry.unregister(DraftDeleteButton)\n  ComponentRegistry.unregister(DraftListSendStatus)\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/sending-cancel-button.cjsx",
    "content": "React = require 'react'\n{Actions} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\n\nclass SendingCancelButton extends React.Component\n  @displayName: 'SendingCancelButton'\n\n  @propTypes:\n    taskId: React.PropTypes.string.isRequired\n\n  constructor: (@props) ->\n    @state =\n      cancelling: false\n\n  render: =>\n    if @state.cancelling\n      <RetinaImg\n        style={width: 20, height: 20, marginTop: 2}\n        name=\"inline-loading-spinner.gif\"\n        mode={RetinaImg.Mode.ContentPreserve} />\n    else\n      <div onClick={@_onClick} style={marginTop: 1}>\n        <RetinaImg\n          name=\"image-cancel-button.png\"\n          mode={RetinaImg.Mode.ContentPreserve} />\n      </div>\n\n  _onClick: =>\n    Actions.dequeueTask(@props.taskId)\n    @setState(cancelling: true)\n\nmodule.exports = SendingCancelButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/lib/sending-progress-bar.cjsx",
    "content": "React = require 'react'\n{Utils} = require 'nylas-exports'\n\nclass SendingProgressBar extends React.Component\n  @propTypes:\n    progress: React.PropTypes.number.isRequired\n\n  render: ->\n    otherProps = Utils.fastOmit(@props, Object.keys(@constructor.propTypes))\n    if 0 < @props.progress < 99\n      <div className=\"sending-progress\" {...otherProps}>\n        <div className=\"filled\"\n             style={width:\"#{Math.min(100, @props.progress)}%\"}>\n        </div>\n      </div>\n    else\n      <div className=\"sending-progress\" {...otherProps}>\n        <div className=\"indeterminate\"></div>\n      </div>\n\nmodule.exports = SendingProgressBar\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/package.json",
    "content": "{\n  \"name\": \"draft-list\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"View drafts using React\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/draft-list/stylesheets/draft-list.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n\n@keyframes sending-progress-move {\n  0% {\n    background-position: 0 0;\n  }\n  100% {\n    background-position: 50px 50px;\n  }\n}\n\n.draft-list {\n  .sending {\n    background-color: @background-primary;\n    &:hover {\n      background-color: @background-primary;\n    }\n  }\n\n  .sending-progress {\n    display: block;\n    height: 7px;\n    align-self: center;\n    background-color: @background-primary;\n    border-bottom:1px solid @border-color-divider;\n    position: relative;\n\n    .filled {\n      display: block;\n      background: @component-active-color;\n      height:6px;\n      width: 0; //overridden by style\n      transition: width 1000ms linear;\n    }\n    .indeterminate {\n      display: block;\n      background: @component-active-color;\n      height:6px;\n      width: 100%;\n    }\n    .indeterminate:after {\n      content: \"\";\n      position: absolute;\n      top: 0; left: 0; bottom: 0; right: 0;\n      background-image: linear-gradient(\n        -45deg,\n        rgba(255, 255, 255, .2) 25%,\n        transparent 25%,\n        transparent 50%,\n        rgba(255, 255, 255, .2) 50%,\n        rgba(255, 255, 255, .2) 75%,\n        transparent 75%,\n        transparent\n      );\n      background-size: 50px 50px;\n      animation: sending-progress-move 2s linear infinite;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/events/lib/event-header.cjsx",
    "content": "_ = require 'underscore'\npath = require 'path'\nReact = require 'react'\n{RetinaImg} = require 'nylas-component-kit'\n{Actions,\n DateUtils,\n Message,\n Event,\n ComponentRegistry,\n EventRSVPTask,\n DatabaseStore,\n AccountStore} = require 'nylas-exports'\nmoment = require 'moment-timezone'\n\nclass EventHeader extends React.Component\n  @displayName: 'EventHeader'\n\n  @propTypes:\n    message: React.PropTypes.instanceOf(Message).isRequired\n\n  constructor: (@props) ->\n    @state =\n      event: @props.message.events[0]\n\n  _onChange: =>\n    return unless @state.event\n    DatabaseStore.find(Event, @state.event.id).then (event) =>\n      return unless event\n      @setState({event})\n\n  componentDidMount: =>\n    # TODO: This should use observables!\n    @_unlisten = DatabaseStore.listen (change) =>\n      if @state.event and change.objectClass is Event.name\n        updated = _.find change.objects, (o) => o.id is @state.event.id\n        @setState({event: updated}) if updated\n    @_onChange()\n\n  componentWillReceiveProps: (nextProps) =>\n    @setState({event:nextProps.message.events[0]})\n    @_onChange()\n\n  componentWillUnmount: =>\n    @_unlisten?()\n\n  render: =>\n    timeFormat = DateUtils.getTimeFormat({timeZone: true})\n    if @state.event?\n      <div className=\"event-wrapper\">\n        <div className=\"event-header\">\n          <RetinaImg name=\"icon-RSVP-calendar-mini@2x.png\"\n                     mode={RetinaImg.Mode.ContentPreserve}/>\n          <span className=\"event-title-text\">Event: </span><span className=\"event-title\">{@state.event.title}</span>\n        </div>\n        <div className=\"event-body\">\n          <div className=\"event-date\">\n            <div className=\"event-day\">\n              {moment(@state.event.start*1000).tz(DateUtils.timeZone).format(\"dddd, MMMM Do\")}\n            </div>\n            <div>\n              <div className=\"event-time\">\n                {moment(@state.event.start*1000).tz(DateUtils.timeZone).format(timeFormat)}\n              </div>\n              {@_renderEventActions()}\n            </div>\n          </div>\n        </div>\n      </div>\n    else\n      <div></div>\n\n  _renderEventActions: =>\n    me = @state.event.participantForMe()\n    return false unless me\n\n    actions = [[\"yes\", \"Accept\"], [\"maybe\", \"Maybe\"], [\"no\", \"Decline\"]]\n\n    <div className=\"event-actions\">\n      {actions.map ([status, label]) =>\n        classes = \"btn-rsvp \"\n        classes += status if me.status is status\n        <div key={status} className={classes} onClick={=> @_rsvp(status)}>\n          {label}\n        </div>\n      }\n    </div>\n\n  _rsvp: (status) =>\n    me = @state.event.participantForMe()\n    Actions.queueTask(new EventRSVPTask(@state.event, me.email, status))\n\nmodule.exports = EventHeader\n"
  },
  {
    "path": "packages/client-app/internal_packages/events/lib/main.cjsx",
    "content": "{ComponentRegistry, WorkspaceStore} = require 'nylas-exports'\nEventHeader = require \"./event-header\"\n\nmodule.exports =\n  activate: (@state={}) ->\n    ComponentRegistry.register EventHeader,\n      role: 'message:BodyHeader'\n\n  deactivate: ->\n    ComponentRegistry.unregister(EventHeader)\n\n  serialize: -> @state\n"
  },
  {
    "path": "packages/client-app/internal_packages/events/package.json",
    "content": "{\n  \"name\": \"events\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"View events on message\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/events/stylesheets/events.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.event-wrapper {\n  cursor: default;\n  position: relative;\n  font-size: @font-size-small;\n  margin-top: @spacing-standard;\n  overflow: hidden;\n  text-overflow: ellipsis;\n  white-space: nowrap;\n  width: 100%;\n  display: inline-block;\n  border-radius: @border-radius-base;\n  border: 1px solid @border-color-divider;\n\n  .event-header {\n    border-bottom: 1px solid @border-color-divider;\n    padding: 10px;\n    overflow: hidden;\n    text-overflow: ellipsis;\n\n    img {\n      margin-right: 20px;\n    }\n\n    .event-title-text {\n      color: @text-color-very-subtle;\n    }\n\n    .event-title {\n      color: @text-color;\n    }\n  }\n\n  .event-body {\n    padding: @padding-small-horizontal;\n\n    .event-date {\n      display: inline-block;\n      width: 100%;\n\n      .event-day {\n        display: block;\n        font-size: @font-size-large;\n        color: #e64d65;\n      }\n\n      .event-time {\n        display: inline-block;\n        font-size: @font-size-h3;\n        font-weight: @font-weight-blond;\n      }\n    }\n\n    .event-actions {\n      display: inline-block;\n      float: right;\n\n      z-index: 4;\n      text-align: center;\n\n      .btn-rsvp {\n        float: left;\n        padding: @spacing-three-quarters @spacing-standard * 1.75 @spacing-three-quarters @spacing-standard * 1.75;\n        line-height: 10px;\n        color: @text-color;\n        border-radius: 3px;\n        background: @background-primary;\n        box-shadow: @standard-shadow;\n        margin: 0 7.5px 0 7.5px;\n        &:active {background: transparent;}\n\n        &.no {\n          background: @color-error;\n          color: @white;\n        }\n\n        &.yes {\n          background: @color-success;\n          color: @white;\n        }\n\n        &.maybe {\n          background: @gray-light;\n          color: @white;\n        }\n      }\n    }\n  }\n}\nbody.platform-win32 {\n  .event-wrapper {\n    border-radius: 0;\n    .event-body {\n      .event-actions {\n        .btn-rsvp {\n          border-radius: 0;\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/github-contact-card/README.md",
    "content": "# Github Contact Card Section\n\nExtends the contact card in the sidebar to show public repos of the people you email.\nUses GitHub's public API to look up a GitHub user based on their email address,\nand then displays public repos and their stars.\n\nThis example is a good starting point for plugins that want to display data from\nexternal sources in the sidebar. If you want to see some more advanced plugins, have a look through [all the internal plugins, here on Github](https://github.com/nylas/nylas-mail/tree/master/internal_packages).\n\n<img src=\"https://raw.githubusercontent.com/nylas/nylas-mail/master/internal_packages/github-contact-card/screenshot.png\">\n\n#### Install this plugin\n\n1. Download and run N1\n\n2. From the menu, select `Developer > Install a Plugin Manually...`\n   The dialog will default to this examples directory. Just choose the\n   package to install it!\n\n   > When you install packages, they're moved to `~/.nylas-mail/packages`,\n   > and N1 runs `apm install` on the command line to fetch dependencies\n   > listed in the package's `package.json`\n"
  },
  {
    "path": "packages/client-app/internal_packages/github-contact-card/lib/github-contact-card-section.jsx",
    "content": "import _ from 'underscore';\nimport {React} from 'nylas-exports';\nimport GithubUserStore from \"./github-user-store\";\n\n// Small React component that renders a single Github repository\nconst GithubRepo = function GithubRepo(props) {\n  const {repo} = props;\n\n  return (\n    <div className=\"repo\">\n      <div className=\"stars\">{repo.stargazers_count}</div>\n      <a href={repo.html_url}>{repo.full_name}</a>\n    </div>\n  );\n}\nGithubRepo.propTypes = {\n  // This component takes a `repo` object as a prop. Listing props is optional\n  // but enables nice React warnings when our expectations aren't met\n  repo: React.PropTypes.object.isRequired,\n};\n\n// Small React component that renders the user's Github profile.\nconst GithubProfile = function GithubProfile(props) {\n  const {profile} = props;\n\n  // Transform the profile's array of repos into an array of React <GithubRepo> elements\n  const repoElements = _.map(profile.repos, (repo) => {\n    return <GithubRepo key={repo.id} repo={repo} />\n  });\n\n  // Remember - this looks like HTML, but it's actually CJSX, which is converted into\n  // Coffeescript at transpile-time. We're actually creating a nested tree of Javascript\n  // objects here that *represent* the DOM we want.\n  return (\n    <div className=\"profile\">\n      <img className=\"logo\" alt=\"github logo\" src=\"nylas://github-contact-card/assets/github.png\" />\n      <a href={profile.html_url}>{profile.login}</a>\n      <div>{repoElements}</div>\n    </div>\n  );\n}\nGithubProfile.propTypes = {\n  // This component takes a `profile` object as a prop. Listing props is optional\n  // but enables nice React warnings when our expectations aren't met.\n  profile: React.PropTypes.object.isRequired,\n}\n\nexport default class GithubContactCardSection extends React.Component {\n  static displayName = 'GithubContactCardSection';\n\n  static containerStyles = {\n    order: 10,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    // When our component mounts, start listening to the GithubUserStore.\n    // When the store `triggers`, our `_onChange` method will fire and allow\n    // us to replace our state.\n    this._unsubscribe = GithubUserStore.listen(this._onChange);\n  }\n\n  componentWillUnmount() {\n    this._unsubscribe();\n  }\n\n  _getStateFromStores = () => {\n    return {\n      profile: GithubUserStore.profileForFocusedContact(),\n      loading: GithubUserStore.loading(),\n    };\n  }\n\n  // The data vended by the GithubUserStore has changed. Calling `setState:`\n  // will cause React to re-render our view to reflect the new values.\n  _onChange = () => {\n    this.setState(this._getStateFromStores())\n  }\n\n  _renderInner() {\n    // Handle various loading states by returning early\n    if (this.state.loading) {\n      return (<div className=\"pending\">Loading...</div>);\n    }\n\n    if (!this.state.profile) {\n      return (<div className=\"pending\">No Matching Profile</div>);\n    }\n\n    return (\n      <GithubProfile profile={this.state.profile} />\n    );\n  }\n\n  render() {\n    return (\n      <div className=\"sidebar-github-profile\">\n        <h2>Github</h2>\n        {this._renderInner()}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/github-contact-card/lib/github-user-store.es6",
    "content": "import _ from 'underscore';\nimport request from 'request';\nimport NylasStore from 'nylas-store';\nimport {FocusedContactsStore} from 'nylas-exports';\n\n// This package uses the Flux pattern - our Store is a small singleton that\n// observes other parts of the application and vends data to our React\n// component. If the user could interact with the GithubSidebar, this store\n// would also listen for `Actions` emitted by our React components.\nclass GithubUserStore extends NylasStore {\n  constructor() {\n    super();\n\n    this._profile = null;\n    this._cache = {};\n    this._loading = false;\n    this._error = null;\n\n    // Register a callback with the FocusedContactsStore. This will tell us\n    // whenever the selected person has changed so we can refresh our data.\n    this.listenTo(FocusedContactsStore, this._onFocusedContactChanged);\n  }\n\n  // Getter Methods\n\n  profileForFocusedContact() {\n    return this._profile;\n  }\n\n  loading() {\n    return this._loading;\n  }\n\n  error() {\n    return this._error;\n  }\n\n  // Called when the FocusedContactStore `triggers`, notifying us that the data\n  // it vends has changed.\n  _onFocusedContactChanged = () => {\n    // Grab the new focused contact\n    const contact = FocusedContactsStore.focusedContact();\n\n    // First, clear the contact that we're currently showing and `trigger`. Since\n    // our React component observes our store, `trigger` causes our React component\n    // to re-render.\n    this._error = null;\n    this._profile = null;\n\n    if (contact) {\n      this._profile = this._cache[contact.email];\n      if (this._profile === undefined) {\n        // Make a Github search request to find the matching user profile\n        this._githubFetchProfile(contact.email);\n      }\n    }\n\n    this.trigger(this);\n  }\n\n  _githubFetchProfile(email) {\n    this._loading = true\n    this._githubRequest(`https://api.github.com/search/users?q=${email}`, (err, resp, data) => {\n      if (err || !data) {\n        return;\n      }\n\n      if (data.message !== undefined) {\n        console.warn(data.message);\n      }\n\n      // Sometimes we get rate limit errors, etc., so we need to check and make\n      // sure we've gotten items before pulling the first one.\n      let profile = false;\n      if (data && data.items && data.items[0]) {\n        profile = data.items[0];\n      }\n\n      // If a profile was found, make a second request for the user's public\n      // repositories.\n      if (profile !== false) {\n        profile.repos = [];\n        this._githubRequest(`https://api.github.com/search/repositories?q=user:${profile.login}&sort=stars&order=desc`, (reposErr, reposResp, repos) => {\n          // Sort the repositories by their stars (`-` for descending order)\n          profile.repos = _.sortBy(repos.items, (repo) => -repo.stargazers_count);\n          // Trigger so that our React components refresh their state and display\n          // the updated data.\n          this.trigger(this);\n        });\n      }\n\n      this._loading = false;\n      this._profile = this._cache[email] = profile;\n      this.trigger(this);\n    });\n  }\n\n  // Wrap the Node `request` library and pass the User-Agent header, which is required\n  // by Github's API. Also pass `json:true`, which causes responses to be automatically\n  // parsed.\n  _githubRequest(url, callback) {\n    return request({url: url, headers: {'User-Agent': 'request'}, json: true}, callback);\n  }\n}\n\nexport default new GithubUserStore();\n"
  },
  {
    "path": "packages/client-app/internal_packages/github-contact-card/lib/main.jsx",
    "content": "import {\n  ComponentRegistry,\n} from \"nylas-exports\";\n\nimport GithubContactCardSection from \"./github-contact-card-section\";\n\n/*\nAll packages must export a basic object that has at least the following 3\nmethods:\n\n1. `activate` - Actions to take once the package gets turned on.\nPre-enabled packages get activated on N1 bootup. They can also be\nactivated manually by a user.\n\n2. `deactivate` - Actions to take when a package gets turned off. This can\nhappen when a user manually disables a package.\n\n3. `serialize` - A simple serializable object that gets saved to disk\nbefore N1 quits. This gets passed back into `activate` next time N1 boots\nup or your package is manually activated.\n*/\nexport function activate() {\n  // Register our sidebar so that it appears in the Message List sidebar.\n  // This sidebar is to the right of the Message List in both split pane mode\n  // and list mode.\n  ComponentRegistry.register(GithubContactCardSection, {\n    role: \"MessageListSidebar:ContactCard\",\n  });\n}\n\nexport function serialize() {\n  return {};\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(GithubContactCardSection);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/github-contact-card/package.json",
    "content": "{\n  \"name\": \"github-contact-card\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"isHiddenOnPluginsPage\": true,\n\n  \"isOptional\": true,\n\n  \"title\": \"GitHub Sidebar Info\",\n  \"description\": \"Extend the contact card in the sidebar to show public repos of the people you email.\",\n  \"icon\": \"./icon.png\",\n\n  \"license\": \"GPL-3.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/bengotow/nylas-sidebar-github-profile\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/github-contact-card/stylesheets/sidebar-github-profile.less",
    "content": "@import \"ui-variables\";\n\n.sidebar-github-profile {\n  margin-bottom: 20px;\n\n  a{ text-decoration: none; }\n\n  .pending {\n    font-size: 12px;\n    color: @text-color-very-subtle;\n  }\n\n  .logo {\n    float: left;\n    width:15px;\n    height:15px;\n    opacity:0.4;\n    margin-right: @spacing-half;\n  }\n\n  .repo {\n    padding-left: @spacing-standard;\n    border-left:1px solid @border-color-divider;\n    margin-left: @spacing-standard/2;\n    font-size: @font-size-smaller;\n    display: flex;\n\n    a {\n      overflow: hidden;\n      text-overflow: ellipsis;\n      flex: 1;\n      white-space: nowrap;\n    }\n    .stars {\n      order: 2;\n      margin-left:@spacing-standard;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/README.md",
    "content": "## Keybase Plugin\n\nTODO:\n-----\n* final refactor\n* tests\n\nWISHLIST:\n-----\n* message signing\n* encrypted file handling\n* integrate MIT PGP Keyserver search into Keybase searchbar\n* make the decrypt interface a message body overlay instead of a button in the header\n* improve search result deduping with keys on file\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/decrypt-button.cjsx",
    "content": "{MessageStore, React, ReactDOM, FileDownloadStore, MessageBodyProcessor, Actions} = require 'nylas-exports'\nPGPKeyStore = require './pgp-key-store'\n{remote} = require 'electron'\nPassphrasePopover = require './passphrase-popover'\nPrivateKeyPopover = require './private-key-popover'\npgp = require 'kbpgp'\n_ = require 'underscore'\n\nclass DecryptMessageButton extends React.Component\n\n  @displayName: 'DecryptMessageButton'\n\n  @propTypes:\n    message: React.PropTypes.object.isRequired\n\n  constructor: (props) ->\n    super(props)\n    @state = @_getStateFromStores()\n\n  _getStateFromStores: ->\n    return {\n      isDecrypted: PGPKeyStore.isDecrypted(@props.message)\n      wasEncrypted: PGPKeyStore.hasEncryptedComponent(@props.message)\n      encryptedAttachments: PGPKeyStore.fetchEncryptedAttachments(@props.message)\n      status: PGPKeyStore.msgStatus(@props.message)\n    }\n\n  componentDidMount: ->\n    @unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange, @)\n\n  componentWillUnmount: ->\n    @unlistenKeystore()\n\n  _onKeystoreChange: ->\n    # every time a new key gets unlocked/fetched, try to decrypt this message\n    if not @state.isDecrypted\n      PGPKeyStore.decrypt(@props.message)\n    @setState(@_getStateFromStores())\n\n  _onClickDecrypt: (event) =>\n    popoverTarget = event.target.getBoundingClientRect()\n    if @_noPrivateKeys()\n      Actions.openPopover(\n        <PrivateKeyPopover\n          addresses={_.pluck(@props.message.to, \"email\")}\n          callback={=> @_openPassphrasePopover(popoverTarget, @decryptPopoverDone)}/>,\n        {originRect: popoverTarget, direction: 'down'}\n      )\n    else\n      @_openPassphrasePopover(popoverTarget, @decryptPopoverDone)\n\n  _displayError: (err) ->\n    dialog = remote.dialog\n    dialog.showErrorBox('Decryption Error', err.toString())\n\n  _onClickDecryptAttachments: (event) =>\n    popoverTarget = event.target.getBoundingClientRect()\n    if @_noPrivateKeys()\n      Actions.openPopover(\n        <PrivateKeyPopover\n          addresses={_.pluck(@props.message.to, \"email\")}\n          callback={=> @_openPassphrasePopover(popoverTarget, @decryptAttachmentsPopoverDone)}/>,\n        {originRect: popoverTarget, direction: 'down'}\n      )\n    else\n      @_openPassphrasePopover(popoverTarget, @decryptAttachmentsPopoverDone)\n\n  decryptPopoverDone: (passphrase) =>\n    for recipient in @props.message.to\n      # right now, just try to unlock all possible keys\n      # (many will fail - TODO?)\n      privateKeys = PGPKeyStore.privKeys(address: recipient.email, timed: false)\n      for privateKey in privateKeys\n        PGPKeyStore.getKeyContents(key: privateKey, passphrase: passphrase)\n\n  decryptAttachmentsPopoverDone: (passphrase) =>\n    for recipient in @props.message.to\n      privateKeys = PGPKeyStore.privKeys(address: recipient.email, timed: false)\n      for privateKey in privateKeys\n        PGPKeyStore.getKeyContents(key: privateKey, passphrase: passphrase, callback: (identity) => PGPKeyStore.decryptAttachments(identity, @state.encryptedAttachments))\n\n  _openPassphrasePopover: (target, callback) =>\n    Actions.openPopover(\n      <PassphrasePopover addresses={_.pluck(@props.message.to, \"email\")} onPopoverDone={callback} />,\n      {originRect: target, direction: 'down'}\n    )\n\n  _noPrivateKeys: =>\n    numKeys = 0\n    for recipient in @props.message.to\n      numKeys = numKeys + PGPKeyStore.privKeys(address: recipient.email, timed: false).length\n    return numKeys < 1\n\n  render: =>\n    if not (@state.wasEncrypted or @state.encryptedAttachments.length > 0)\n      return false\n\n    title = \"Message Encrypted\"\n    decryptLabel = \"Decrypt\"\n    borderClass = \"border\"\n    decryptClass = \"decrypt-bar\"\n    if @state.status?\n      if @state.status.indexOf(\"Message decrypted\") >= 0\n        title = @state.status\n        borderClass = \"border done-border\"\n        decryptClass = \"decrypt-bar done-decrypt-bar\"\n      else if @state.status.indexOf(\"Unable to decrypt message.\") >= 0\n        title = @state.status\n        borderClass = \"border error-border\"\n        decryptClass = \"decrypt-bar error-decrypt-bar\"\n        decryptLabel = \"Try Again\"\n\n    decryptBody = false\n    if !@state.isDecrypted and !(@state.status?.indexOf(\"malformed\") >= 0)\n      decryptBody = <button title=\"Decrypt email body\" className=\"btn btn-toolbar\" onClick={@_onClickDecrypt} ref=\"button\">{decryptLabel}</button>\n\n    decryptAttachments = false\n    if @state.encryptedAttachments?.length >= 1\n      title = if @state.encryptedAttachments.length == 1 then \"Attachment Encrypted\" else \"Attachments Encrypted\"\n      buttonLabel = if @state.encryptedAttachments.length == 1 then \"Decrypt Attachment\" else \"Decrypt Attachments\"\n      decryptAttachments = <button onClick={ @_onClickDecryptAttachments } className=\"btn btn-toolbar\">{buttonLabel}</button>\n\n    if decryptAttachments or decryptBody\n      decryptionInterface =\n        <div className=\"decryption-interface\">\n          {decryptBody}\n          {decryptAttachments}\n        </div>\n\n    <div className=\"keybase-decrypt\">\n      <div className=\"line-w-label\">\n        <div className={borderClass}></div>\n        <div className={decryptClass}>\n          <div className=\"title-text\">\n            {title}\n          </div>\n          {decryptionInterface}\n        </div>\n        <div className={borderClass}></div>\n      </div>\n    </div>\n\nmodule.exports = DecryptMessageButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/decryption-preprocess.coffee",
    "content": "{MessageViewExtension, Actions} = require 'nylas-exports'\nPGPKeyStore = require './pgp-key-store'\n\nclass DecryptPGPExtension extends MessageViewExtension\n  @formatMessageBody: ({message}) =>\n    if not PGPKeyStore.hasEncryptedComponent(message)\n      return message\n    if PGPKeyStore.isDecrypted(message)\n      message.body = PGPKeyStore.getDecrypted(message)\n    else\n      # trigger a decryption\n      PGPKeyStore.decrypt(message)\n    message\n\nmodule.exports = DecryptPGPExtension\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/email-popover.cjsx",
    "content": "{React, Actions} = require 'nylas-exports'\n{ParticipantsTextField} = require 'nylas-component-kit'\nIdentity = require './identity'\n_ = require 'underscore'\n\nmodule.exports =\nclass EmailPopover extends React.Component\n  constructor: ->\n    @state = {to: [], cc: [], bcc: []}\n\n  @propTypes:\n    profile: React.PropTypes.instanceOf(Identity).isRequired\n\n  render: ->\n    participants = @state\n\n    <div className=\"keybase-import-popover\">\n      <ParticipantsTextField\n        field=\"to\"\n        className=\"keybase-participant-field\"\n        participants={ participants }\n        change={ @_onRecipientFieldChange } />\n      <button className=\"btn btn-toolbar\" onClick={ @_onDone }>Associate Emails with Key</button>\n    </div>\n\n  _onRecipientFieldChange: (contacts) =>\n    @setState(contacts)\n\n  _onDone: =>\n    @props.onPopoverDone(_.pluck(@state.to, 'email'), @props.profile)\n    Actions.closePopover()\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/encrypt-button.cjsx",
    "content": "{Utils, DraftStore, React, Actions, DatabaseStore, Contact, ReactDOM} = require 'nylas-exports'\nPGPKeyStore = require './pgp-key-store'\nIdentity = require './identity'\nModalKeyRecommender = require './modal-key-recommender'\n{RetinaImg} = require 'nylas-component-kit'\n{remote} = require 'electron'\npgp = require 'kbpgp'\n_ = require 'underscore'\n\nclass EncryptMessageButton extends React.Component\n\n  @displayName: 'EncryptMessageButton'\n\n  # require that we have a draft object available\n  @propTypes:\n    draft: React.PropTypes.object.isRequired\n    session: React.PropTypes.object.isRequired\n\n  constructor: (props) ->\n    super(props)\n\n    # plaintext: store the message's plaintext in case the user wants to edit\n    # further after hitting the \"encrypt\" button (i.e. so we can \"undo\" the\n    # encryption)\n\n    # cryptotext: store the message's body here, for comparison purposes (so\n    # that if the user edits an encrypted message, we can revert it)\n    @state = {plaintext: \"\", cryptotext: \"\", currentlyEncrypted: false}\n\n  componentDidMount: ->\n    @unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange, @)\n\n  componentWillUnmount: ->\n    @unlistenKeystore()\n\n  componentWillReceiveProps: (nextProps) ->\n    if @state.currentlyEncrypted and nextProps.draft.body != @props.draft.body and nextProps.draft.body != @state.cryptotext\n      # A) we're encrypted\n      # B) someone changed something\n      # C) the change was AWAY from the \"correct\" cryptotext\n      body = @state.cryptotext\n      @props.session.changes.add({body: body})\n\n  _getKeys: ->\n    keys = []\n    for recipient in @props.draft.participants({includeFrom: false, includeBcc: true})\n      publicKeys = PGPKeyStore.pubKeys(recipient.email)\n      if publicKeys.length < 1\n        # no key for this user\n        keys.push(new Identity({addresses: [recipient.email]}))\n      else\n        # note: this, by default, encrypts using every public key associated\n        # with the address\n        for publicKey in publicKeys\n          if not publicKey.key?\n            PGPKeyStore.getKeyContents(key: publicKey)\n          else\n            keys.push(publicKey)\n\n    return keys\n\n  _onKeystoreChange: =>\n    # if something changes with the keys, check to make sure the recipients\n    # haven't changed (thus invalidating our encrypted message)\n    if @state.currentlyEncrypted\n      newKeys = _.map(@props.draft.participants(), (participant) ->\n        return PGPKeyStore.pubKeys(participant.email)\n      )\n      newKeys = _.flatten(newKeys)\n\n      oldKeys = _.map(@props.draft.participants(), (participant) ->\n        return PGPKeyStore.pubKeys(participant.email)\n      )\n      oldKeys = _.flatten(oldKeys)\n\n      if newKeys.length != oldKeys.length\n        # someone added/removed a key - our encrypted body is now out of date\n        @_toggleCrypt()\n\n  render: ->\n    classnames = \"btn btn-toolbar\"\n    if @state.currentlyEncrypted\n      classnames += \" btn-enabled\"\n\n    <div className=\"n1-keybase\">\n      <button title=\"Encrypt email body\" className={ classnames } onClick={ => @_onClick()} ref=\"button\">\n        <RetinaImg url=\"nylas://keybase/encrypt-composer-button@2x.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    </div>\n\n  _onClick: =>\n    @_toggleCrypt()\n\n  _toggleCrypt: =>\n    # if decrypted, encrypt, and vice versa\n    # addresses which don't have a key\n    if @state.currentlyEncrypted\n      # if the message is already encrypted, place the stored plaintext back\n      # in the draft (i.e. un-encrypt)\n      @props.session.changes.add({body: @state.plaintext})\n      @setState({currentlyEncrypted: false})\n    else\n      # if not encrypted, save the plaintext, then encrypt\n      plaintext = @props.draft.body\n      identities = @_getKeys()\n      @_checkKeysAndEncrypt(plaintext, identities, (err, cryptotext) =>\n        if err\n          console.warn err\n          Actions.recordUserEvent(\"Email Encryption Errored\", {error: err})\n          NylasEnv.showErrorDialog(err)\n        if cryptotext? and cryptotext != \"\"\n          Actions.recordUserEvent(\"Email Encrypted\")\n          # <pre> tag prevents gross HTML formatting in-flight\n          cryptotext = \"<pre>#{cryptotext}</pre>\"\n          @setState({\n            currentlyEncrypted: true\n            plaintext: plaintext\n            cryptotext: cryptotext\n          })\n          @props.session.changes.add({body: cryptotext})\n      )\n\n  _encrypt: (text, identities, cb) =>\n    # get the actual key objects\n    keys = _.pluck(identities, \"key\")\n    # remove the nulls\n    kms = _.compact(keys)\n    if kms.length == 0\n      NylasEnv.showErrorDialog(\"There are no PGP public keys loaded, so the message cannot be\n       encrypted. Compose a message, add recipients in the To: field, and try again.\")\n      return\n    params =\n      encrypt_for: kms\n      msg: text\n    pgp.box(params, cb)\n\n  _checkKeysAndEncrypt: (text, identities, cb) =>\n    emails = _.chain(identities)\n      .pluck(\"addresses\")\n      .flatten()\n      .uniq()\n      .value()\n\n    if _.every(identities, (identity) -> identity.key?)\n      # every key is present and valid\n      @_encrypt(text, identities, cb)\n    else\n      # open a popover to correct null keys\n      DatabaseStore.findAll(Contact, {email: emails}).then((contacts) =>\n        component = (<ModalKeyRecommender contacts={contacts} emails={emails} callback={ (newIdentities) => @_encrypt(text, newIdentities, cb) }/>)\n        Actions.openPopover(\n          component,\n        {\n          originRect: ReactDOM.findDOMNode(@).getBoundingClientRect(),\n          direction: 'up',\n          closeOnAppBlur: false,\n        })\n      )\n\nmodule.exports = EncryptMessageButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/identity.coffee",
    "content": "# A single user identity: a key, a way to find that key, one or more email\n# addresses, and a keybase profile\n\n{Utils} = require 'nylas-exports'\npath = require 'path'\n\nmodule.exports =\nclass Identity\n  constructor: ({key, addresses, isPriv, keybase_profile}) ->\n    @clientId = Utils.generateTempId()\n    @key = key ? null # keybase keymanager object\n    @isPriv = isPriv ? false # is this a private key?\n    @timeout = null # the time after which this key (if private) needs to be unlocked again\n    @addresses = addresses ? [] # email addresses associated with this identity\n    @keybase_profile = keybase_profile ? null # a kb profile object associated with this identity\n\n    Object.defineProperty(@, 'keyPath', {\n      get: ->\n        if @addresses.length > 0\n          keyDir = path.join(NylasEnv.getConfigDirPath(), 'keys')\n          thisDir = if @isPriv then path.join(keyDir, 'private') else path.join(keyDir, 'public')\n          keyPath = path.join(thisDir, @addresses.join(\" \"))\n        else\n          keyPath = null\n        return keyPath\n    })\n\n    if @isPriv\n      @setTimeout()\n\n  fingerprint: ->\n    if @key?\n      return @key.get_pgp_fingerprint().toString('hex')\n    return null\n\n  setTimeout: ->\n    delay = 1000 * 60 * 30 # 30 minutes in ms\n    @timeout = Date.now() + delay\n\n  isTimedOut: ->\n    return @timeout < Date.now()\n\n  uid: ->\n    if @key?\n      uid = @key.get_pgp_fingerprint().toString('hex')\n    else if @keybase_profile?\n      uid = @keybase_profile.components.username.val\n    else if @addresses.length > 0\n      uid = @addresses.join('')\n    else\n      uid = @clientId\n\n    return uid\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/key-adder.cjsx",
    "content": "{Utils, React, RegExpUtils} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\nPGPKeyStore = require './pgp-key-store'\nIdentity = require './identity'\nkb = require './keybase'\npgp = require 'kbpgp'\n_ = require 'underscore'\nfs = require 'fs'\n\nmodule.exports =\nclass KeyAdder extends React.Component\n  @displayName: 'KeyAdder'\n\n  constructor: (props) ->\n    @state =\n      address: \"\"\n      keyContents: \"\"\n      passphrase: \"\"\n\n      generate: false\n      paste: false\n      import: false\n\n      isPriv: false\n      loading: false\n\n      validAddress: false\n      validKeyBody: false\n\n  _onPasteButtonClick: (event) =>\n    @setState\n      generate: false\n      paste: !@state.paste\n      import: false\n      address: \"\"\n      validAddress: false\n      keyContents: \"\"\n\n  _onGenerateButtonClick: (event) =>\n    @setState\n      generate: !@state.generate\n      paste: false\n      import: false\n      address: \"\"\n      validAddress: false\n      keyContents: \"\"\n      passphrase: \"\"\n\n  _onImportButtonClick: (event) =>\n    NylasEnv.showOpenDialog({\n      title: \"Import PGP Key\",\n      buttonLabel: \"Import\",\n      properties: ['openFile']\n    }, (filepath) =>\n      if filepath?\n        @setState\n          generate: false\n          paste: false\n          import: true\n          address: \"\"\n          validAddress: false\n          passphrase: \"\"\n        fs.readFile(filepath[0], (err, data) =>\n          pgp.KeyManager.import_from_armored_pgp {\n            armored: data\n          }, (err, km) =>\n            if err\n              PGPKeyStore._displayError(\"File is not a valid PGP key.\")\n              return\n            else\n              privateStart = \"-----BEGIN PGP PRIVATE KEY BLOCK-----\"\n              keyBody = if km.armored_pgp_private? then km.armored_pgp_private else km.armored_pgp_public\n              @setState\n                keyContents: keyBody\n                isPriv: keyBody.indexOf(privateStart) >= 0\n                validKeyBody: true\n      )\n    )\n\n  _onInnerGenerateButtonClick: (event) =>\n    @setState\n      loading: true\n    @_generateKeypair()\n\n  _generateKeypair: =>\n    pgp.KeyManager.generate_rsa { userid : @state.address }, (err, km) =>\n      km.sign {}, (err) =>\n        if err\n          console.warn(err)\n        km.export_pgp_private {passphrase: @state.passphrase}, (err, pgp_private) =>\n          ident = new Identity({\n            addresses: [@state.address]\n            isPriv: true\n          })\n          PGPKeyStore.saveNewKey(ident, pgp_private)\n        km.export_pgp_public {}, (err, pgp_public) =>\n          ident = new Identity({\n            addresses: [@state.address]\n            isPriv: false\n          })\n          PGPKeyStore.saveNewKey(ident, pgp_public)\n          @setState\n            keyContents: pgp_public\n            loading: false\n\n  _saveNewKey: =>\n    ident = new Identity({\n      addresses: [@state.address]\n      isPriv: @state.isPriv\n    })\n    PGPKeyStore.saveNewKey(ident, @state.keyContents)\n\n  _onAddressChange: (event) =>\n    address = event.target.value\n    valid = false\n    if (address and address.length > 0 and RegExpUtils.emailRegex().test(address))\n      valid = true\n    @setState\n      address: event.target.value\n      validAddress: valid\n\n  _onPassphraseChange: (event) =>\n    @setState\n      passphrase: event.target.value\n\n  _onKeyChange: (event) =>\n    privateStart = \"-----BEGIN PGP PRIVATE KEY BLOCK-----\"\n    @setState\n      keyContents: event.target.value\n      isPriv: event.target.value.indexOf(privateStart) >= 0\n    pgp.KeyManager.import_from_armored_pgp {\n      armored: event.target.value\n    }, (err, km) =>\n      if err\n        valid = false\n      else\n        valid = true\n      @setState\n        validKeyBody: valid\n\n  _renderAddButtons: ->\n    <div>\n      Add a PGP Key:\n      <button className=\"btn key-creation-button\" title=\"Paste\" onClick={@_onPasteButtonClick}>Paste in a New Key</button>\n      <button className=\"btn key-creation-button\" title=\"Import\" onClick={@_onImportButtonClick}>Import a Key From File</button>\n      <button className=\"btn key-creation-button\" title=\"Generate\" onClick={@_onGenerateButtonClick}>Generate a New Keypair</button>\n    </div>\n\n  _renderManualKey: ->\n    if !@state.validAddress and @state.address.length > 0\n      invalidMsg = <span className=\"invalid-msg\">Invalid email address</span>\n    else if !@state.validKeyBody and @state.keyContents.length > 0\n      invalidMsg = <span className=\"invalid-msg\">Invalid key body</span>\n    else\n      invalidMsg = <span className=\"invalid-msg\"> </span>\n    invalidInputs = !(@state.validAddress and @state.validKeyBody)\n\n    buttonClass = if invalidInputs then \"btn key-add-btn btn-disabled\" else \"btn key-add-btn\"\n\n    passphraseInput = <input type=\"password\" value={@state.passphrase} placeholder=\"Private Key Password\" className=\"key-passphrase-input\" onChange={@_onPassphraseChange} />\n\n    <div className=\"key-adder\">\n      <div className=\"key-text\">\n        <textarea ref=\"key-input\"\n                value={@state.keyContents || \"\"}\n                onChange={@_onKeyChange}\n                placeholder=\"Paste in your PGP key here!\"/>\n      </div>\n      <div className=\"credentials\">\n        <input type=\"text\" value={@state.address} placeholder=\"Email Address\" className=\"key-email-input\" onChange={@_onAddressChange} />\n        {if @state.isPriv then passphraseInput}\n        {invalidMsg}\n        <button className={buttonClass} disabled={invalidInputs} title=\"Save\" onClick={@_saveNewKey}>Save</button>\n      </div>\n    </div>\n\n  _renderGenerateKey: ->\n    if !@state.validAddress and @state.address.length > 0\n      invalidMsg = <span className=\"invalid-msg\">Invalid email address</span>\n    else\n      invalidMsg = <span className=\"invalid-msg\"> </span>\n\n    loading = <RetinaImg style={width: 20, height: 20} name=\"inline-loading-spinner.gif\" mode={RetinaImg.Mode.ContentPreserve} />\n    if @state.loading\n      keyPlaceholder = \"Generating your key now. This could take a while.\"\n    else\n      keyPlaceholder = \"Your generated public key will appear here. Share it with your friends!\"\n\n    buttonClass = if !@state.validAddress then \"btn key-add-btn btn-disabled\" else \"btn key-add-btn\"\n\n    <div className=\"key-adder\">\n      <div className=\"credentials\">\n        <input type=\"text\" value={@state.address} placeholder=\"Email Address\" className=\"key-email-input\" onChange={@_onAddressChange} />\n        <input type=\"password\" value={@state.passphrase} placeholder=\"Private Key Password\" className=\"key-passphrase-input\" onChange={@_onPassphraseChange} />\n        {invalidMsg}\n        <button className={buttonClass} disabled={!(@state.validAddress)} title=\"Generate\" onClick={@_onInnerGenerateButtonClick}>Generate</button>\n      </div>\n      <div className=\"key-text\">\n        <div className=\"loading\">{if @state.loading then loading}</div>\n        <textarea ref=\"key-output\"\n              value={@state.keyContents || \"\"}\n              disabled\n              placeholder={keyPlaceholder}/>\n      </div>\n    </div>\n\n  render: ->\n\n    <div>\n      {@_renderAddButtons()}\n      {if @state.generate then @_renderGenerateKey()}\n      {if @state.paste or @state.import then @_renderManualKey()}\n    </div>\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/key-manager.cjsx",
    "content": "{Utils, React, Actions} = require 'nylas-exports'\nPGPKeyStore = require './pgp-key-store'\nKeybaseUser = require './keybase-user'\nPassphrasePopover = require './passphrase-popover'\nkb = require './keybase'\n_ = require 'underscore'\npgp = require 'kbpgp'\nfs = require 'fs'\n\nmodule.exports =\nclass KeyManager extends React.Component\n  @displayName: 'KeyManager'\n\n  @propTypes:\n    pubKeys: React.PropTypes.array.isRequired\n    privKeys: React.PropTypes.array.isRequired\n\n  constructor: (props) ->\n    super(props)\n\n  _exportPopoverDone: (passphrase, identity) =>\n    # check the passphrase before opening the save dialog\n    fs.readFile(identity.keyPath, (err, data) =>\n      pgp.KeyManager.import_from_armored_pgp {\n        armored: data\n      }, (err, km) =>\n        if err\n          console.warn err\n        else\n          km.unlock_pgp { passphrase: passphrase }, (err) =>\n            if err\n              PGPKeyStore._displayError(err)\n            else\n              PGPKeyStore.exportKey({identity: identity, passphrase: passphrase})\n    )\n\n  _exportPrivateKey: (identity, event) =>\n    popoverTarget = event.target.getBoundingClientRect()\n\n    Actions.openPopover(\n      <PassphrasePopover identity={identity} addresses={identity.addresses} onPopoverDone={ @_exportPopoverDone } />,\n      {originRect: popoverTarget, direction: 'left'}\n    )\n\n  render: ->\n    {pubKeys, privKeys} = @props\n\n    pubKeys = pubKeys.map (identity) =>\n      deleteButton = (<button title=\"Delete Public\" className=\"btn btn-toolbar btn-danger\" onClick={ => PGPKeyStore.deleteKey(identity) } ref=\"button\">\n        Delete Key\n      </button>\n      )\n      exportButton = (<button title=\"Export Public\" className=\"btn btn-toolbar\" onClick={ => PGPKeyStore.exportKey({identity: identity}) } ref=\"button\">\n        Export Key\n      </button>\n      )\n      actionButton = (<div className=\"key-actions\">\n        {exportButton}\n        {deleteButton}\n      </div>\n      )\n      return <KeybaseUser profile={identity} key={identity.clientId} actionButton={actionButton}/>\n\n    privKeys = privKeys.map (identity) =>\n      deleteButton = (<button title=\"Delete Private\" className=\"btn btn-toolbar btn-danger\" onClick={ => PGPKeyStore.deleteKey(identity) } ref=\"button\">\n        Delete Key\n      </button>\n      )\n      exportButton = (<button title=\"Export Private\" className=\"btn btn-toolbar\" onClick={ (event) => @_exportPrivateKey(identity, event) } ref=\"button\">\n        Export Key\n      </button>\n      )\n      actionButton = (<div className=\"key-actions\">\n        {exportButton}\n        {deleteButton}\n      </div>\n      )\n      return <KeybaseUser profile={identity} key={identity.clientId} actionButton={actionButton}/>\n\n    <div className=\"key-manager\">\n      <div className=\"line-w-label\">\n        <div className=\"border\"></div>\n        <div className=\"title-text\">Saved Public Keys</div>\n        <div className=\"border\"></div>\n      </div>\n      <div>\n        { pubKeys }\n      </div>\n      <div className=\"line-w-label\">\n        <div className=\"border\"></div>\n        <div className=\"title-text\">Saved Private Keys</div>\n        <div className=\"border\"></div>\n      </div>\n      <div>\n        { privKeys }\n      </div>\n    </div>\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/keybase-search.cjsx",
    "content": "{Utils,\n React,\n ReactDOM,\n Actions,\n RegExpUtils,\n IdentityStore,\n AccountStore} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\nEmailPopover = require './email-popover'\nPGPKeyStore = require './pgp-key-store'\nKeybaseUser = require '../lib/keybase-user'\nIdentity = require './identity'\nkb = require './keybase'\n_ = require 'underscore'\n\nmodule.exports =\nclass KeybaseSearch extends React.Component\n  @displayName: 'KeybaseSearch'\n\n  @propTypes:\n    initialSearch: React.PropTypes.string\n    # importFunc: a alternate function to execute when the \"import\" button is\n    # clicked instead of the \"please specify an email\" popover\n    importFunc: React.PropTypes.func\n    # TODO consider just passing in a pre-specified email instead of a func?\n    inPreferences: React.PropTypes.bool\n\n  @defaultProps:\n    initialSearch: \"\"\n    importFunc: null\n    inPreferences: false\n\n  constructor: (props) ->\n    super(props)\n    @state = {\n      query: props.initialSearch\n      results: []\n      loading: false\n      searchedByEmail: false\n    }\n\n    @debouncedSearch =  _.debounce(@_search, 300)\n\n  componentDidMount: ->\n    @_search()\n\n  componentWillReceiveProps: (props) ->\n    @setState({query: props.initialSearch})\n\n  _search: ->\n    oldquery = @state.query\n    if @state.query != \"\" and @state.loading == false\n      @setState({loading: true})\n      kb.autocomplete(@state.query, (error, profiles) =>\n        if profiles?\n          profiles = _.map(profiles, (profile) ->\n            return new Identity({keybase_profile: profile, isPriv: false})\n          )\n          @setState({results: profiles, loading: false})\n        else\n          @setState({results: [], loading: false})\n        if @state.query != oldquery\n          @debouncedSearch()\n      )\n    else\n      # no query - empty out the results\n      @setState({results: []})\n\n  _importKey: (profile, event) =>\n    # opens a popover requesting user to enter 1+ emails to associate with a\n    # key - a button in the popover then calls _save to actually import the key\n    popoverTarget = event.target.getBoundingClientRect()\n\n    Actions.openPopover(\n      <EmailPopover profile={profile} onPopoverDone={ @_popoverDone } />,\n      {originRect: popoverTarget, direction: 'left'}\n    )\n\n  _popoverDone: (addresses, identity) =>\n    if addresses.length < 1\n      # no email addresses added, noop\n      return\n    else\n      identity.addresses = addresses\n      # TODO validate the addresses?\n      @_save(identity)\n\n  _save: (identity) =>\n    # save/import a key from keybase\n    keybaseUsername = identity.keybase_profile.components.username.val\n\n    kb.getKey(keybaseUsername, (error, key) =>\n      if error\n        console.error error\n      else\n        PGPKeyStore.saveNewKey(identity, key)\n    )\n\n  _queryChange: (event) =>\n    emailQuery = RegExpUtils.emailRegex().test(event.target.value)\n    @setState({query: event.target.value, searchedByEmail: emailQuery})\n    @debouncedSearch()\n\n  render: ->\n    profiles = _.map(@state.results, (profile) =>\n\n      # allow for overriding the import function\n      if typeof @props.importFunc is \"function\"\n        boundFunc = @props.importFunc\n      else\n        boundFunc = @_importKey\n\n      saveButton = (<button title=\"Import\" className=\"btn btn-toolbar\" onClick={ (event) => boundFunc(profile, event) } ref=\"button\">\n        Import Key\n      </button>\n      )\n\n      # TODO improved deduping? tricky because of the kbprofile - email association\n      if not profile.keyPath?\n        return <KeybaseUser profile={profile} actionButton={ saveButton } />\n    )\n\n    if not profiles? or profiles.length < 1\n      profiles = []\n\n    badSearch = null\n    loading = null\n    empty = null\n\n    if profiles.length < 1 and @state.searchedByEmail\n      badSearch = <span className=\"bad-search-msg\">Keybase cannot be searched by email address. <br/>Try entering a name, or a username from GitHub, Keybase or Twitter.</span>\n\n    if @state.loading\n      loading = <RetinaImg style={width: 20, height: 20, marginTop: 2} name=\"inline-loading-spinner.gif\" mode={RetinaImg.Mode.ContentPreserve} />\n\n    <div className=\"keybase-search\">\n      <div className=\"searchbar\">\n        <input type=\"text\" value={ @state.query } placeholder=\"Search for PGP public keys on Keybase\" ref=\"searchbar\" onChange={@_queryChange} />\n        {empty}\n        <div className=\"loading\">{ loading }</div>\n      </div>\n      <div className=\"results\" ref=\"results\">\n        { profiles }\n        { badSearch }\n      </div>\n    </div>\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/keybase-user.cjsx",
    "content": "{Utils, React, Actions} = require 'nylas-exports'\n{ParticipantsTextField} = require 'nylas-component-kit'\nPGPKeyStore = require './pgp-key-store'\nEmailPopover = require './email-popover'\nIdentity = require './identity'\nkb = require './keybase'\n_ = require 'underscore'\n\nmodule.exports =\nclass KeybaseUser extends React.Component\n  @displayName: 'KeybaseUserProfile'\n\n  @propTypes:\n    profile: React.PropTypes.instanceOf(Identity).isRequired\n    actionButton: React.PropTypes.node\n    displayEmailList: React.PropTypes.bool\n\n  @defaultProps:\n    actionButton: false\n    displayEmailList: true\n\n  constructor: (props) ->\n    super(props)\n\n  componentDidMount: ->\n    PGPKeyStore.getKeybaseData(@props.profile)\n\n  _addEmail: (email) =>\n    PGPKeyStore.addAddressToKey(@props.profile, email)\n\n  _addEmailClick: (event) =>\n    popoverTarget = event.target.getBoundingClientRect()\n\n    Actions.openPopover(\n      <EmailPopover profile={@props.profile} onPopoverDone={ @_popoverDone } />,\n      {originRect: popoverTarget, direction: 'left'}\n    )\n\n  _popoverDone: (addresses, identity) =>\n    if addresses.length < 1\n      # no email addresses added, noop\n      return\n    else\n      _.each(addresses, (address) =>\n        @_addEmail(address))\n\n  _removeEmail: (email) =>\n    PGPKeyStore.removeAddressFromKey(@props.profile, email)\n\n  render: =>\n    {profile} = @props\n\n    keybaseDetails = <div className=\"details\"></div>\n    if profile.keybase_profile?\n      keybase = profile.keybase_profile\n\n      # profile picture\n      if keybase.thumbnail?\n        picture = <img className=\"user-picture\" src={ keybase.thumbnail }/>\n      else\n        hue = Utils.hueForString(\"Keybase\")\n        bgColor = \"hsl(#{hue}, 50%, 45%)\"\n        abv = \"K\"\n        picture = <div className=\"default-profile-image\" style={{backgroundColor: bgColor}}>{abv}</div>\n\n      # full name\n      if keybase.components.full_name?.val?\n        fullname = keybase.components.full_name.val\n      else\n        fullname = username\n        username = false\n\n      # link to keybase profile\n      keybase_url = \"keybase.io/#{keybase.components.username.val}\"\n      if keybase_url.length > 25\n        keybase_string = keybase_url.slice(0, 23).concat('...')\n      else\n        keybase_string = keybase_url\n      username = <a href=\"https://#{keybase_url}\">{keybase_string}</a>\n\n      # TODO: potentially display confirmation on keybase-user objects\n      ###\n      possible_profiles = [\"twitter\", \"github\", \"coinbase\"]\n      profiles = _.map(possible_profiles, (possible) =>\n        if keybase.components[possible]?.val?\n          # TODO icon instead of weird \"service: username\" text\n          return (<span key={ possible }><b>{ possible }</b>: { keybase.components[possible].val }</span>)\n      )\n      profiles = _.reject(profiles, (profile) -> profile is undefined)\n\n      profiles =  _.map(profiles, (profile) ->\n        return <span key={ profile.key }>{ profile } </span>)\n      profileList = (<span>{ profiles }</span>)\n      ###\n\n      keybaseDetails = (<div className=\"details\">\n        <div className=\"profile-name\">\n        { fullname }\n        </div>\n        <div className=\"profile-username\">\n          { username }\n        </div>\n      </div>)\n    else\n      # if no keybase profile, default image is based on email address\n      hue = Utils.hueForString(@props.profile.addresses[0])\n      bgColor = \"hsl(#{hue}, 50%, 45%)\"\n      abv = @props.profile.addresses[0][0].toUpperCase()\n      picture = <div className=\"default-profile-image\" style={{backgroundColor: bgColor}}>{abv}</div>\n\n    # email addresses\n    if profile.addresses?.length > 0\n      emails = _.map(profile.addresses, (email) =>\n        # TODO make that remove button not terrible\n        return <li key={ email }>{ email } <small><a onClick={ => @_removeEmail(email) }>(X)</a></small></li>)\n      emailList = (<ul> { emails }\n          <a ref=\"addEmail\" onClick={ @_addEmailClick }>+ Add Email</a>\n          </ul>)\n\n    emailListDiv = (<div className=\"email-list\">\n        <ul>\n          { emailList }\n        </ul>\n      </div>)\n\n    <div className=\"keybase-profile\">\n      <div className=\"profile-photo-wrap\">\n        <div className=\"profile-photo\">\n        { picture }\n        </div>\n      </div>\n      { keybaseDetails }\n      {if @props.displayEmailList then emailListDiv}\n      { @props.actionButton }\n    </div>\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/keybase.coffee",
    "content": "_ = require 'underscore'\nrequest = require 'request'\n\nclass KeybaseAPI\n  constructor: ->\n    @baseUrl = \"https://keybase.io\"\n\n  getUser: (key, keyType, callback) =>\n    if not keyType in ['usernames', 'domain', 'twitter', 'github', 'reddit',\n                       'hackernews', 'coinbase', 'key_fingerprint']\n      console.error 'keyType must be a supported Keybase query type.'\n\n    this._keybaseRequest(\"/_/api/1.0/user/lookup.json?#{keyType}=#{key}\", (err, resp, obj) =>\n      return callback(err, null) if err\n      return callback(new Error(\"Empty response!\"), null) if not obj? or not obj.them?\n      if obj.status?\n        return callback(new Error(obj.status.desc), null) if obj.status.name != \"OK\"\n\n      callback(null, _.map(obj.them, @_regularToAutocomplete))\n    )\n\n  getKey: (username, callback) =>\n    request({url: @baseUrl + \"/#{username}/key.asc\", headers: {'User-Agent': 'request'}}, (err, resp, obj) =>\n      return callback(err, null) if err\n      return callback(new Error(\"No key found for #{username}\"), null) if not obj?\n      return callback(new Error(\"No key returned from keybase for #{username}\"), null) if not obj.startsWith(\"-----BEGIN PGP PUBLIC KEY BLOCK-----\")\n      callback(null, obj)\n    )\n\n  autocomplete: (query, callback) =>\n    url = \"/_/api/1.0/user/autocomplete.json\"\n    request({url: @baseUrl + url, form: {q: query}, headers: {'User-Agent': 'request'}, json: true}, (err, resp, obj) =>\n      return callback(err, null) if err\n      if obj.status?\n        return callback(new Error(obj.status.desc), null) if obj.status.name != \"OK\"\n\n      callback(null, obj.completions)\n    )\n\n  _keybaseRequest: (url, callback) =>\n    return request({url: @baseUrl + url, headers: {'User-Agent': 'request'}, json: true}, callback)\n\n  _regularToAutocomplete: (profile) ->\n    # converts a keybase profile to the weird format used in the autocomplete\n    # endpoint for backward compatability\n    # (does NOT translate accounts - e.g. twitter, github - yet)\n    # TODO this should be the other way around\n    cleanedProfile = {components: {}}\n    cleanedProfile.thumbnail = null\n    if profile.pictures?.primary?\n      cleanedProfile.thumbnail = profile.pictures.primary.url\n    safe_name = if profile.profile? then profile.profile.full_name else \"\"\n    cleanedProfile.components = {full_name: {val: safe_name }, username: {val: profile.basics.username}}\n    _.each(profile.proofs_summary.all, (connectedAccount) =>\n      component = {}\n      component[connectedAccount.proof_type] = {val: connectedAccount.nametag}\n      cleanedProfile.components = _.extend(cleanedProfile.components, component)\n    )\n    return cleanedProfile\n\nmodule.exports = new KeybaseAPI()\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/main.es6",
    "content": "import {PreferencesUIStore, ComponentRegistry, ExtensionRegistry} from 'nylas-exports';\n\nimport EncryptMessageButton from './encrypt-button';\nimport DecryptMessageButton from './decrypt-button';\nimport DecryptPGPExtension from './decryption-preprocess';\nimport RecipientKeyChip from './recipient-key-chip';\nimport PreferencesKeybase from './preferences-keybase';\n\nconst PREFERENCE_TAB_ID = 'Encryption'\n\nexport function activate() {\n  const preferencesTab = new PreferencesUIStore.TabItem({\n    tabId: PREFERENCE_TAB_ID,\n    displayName: 'Encryption',\n    component: PreferencesKeybase,\n  });\n  ComponentRegistry.register(EncryptMessageButton, {role: 'Composer:ActionButton'});\n  ComponentRegistry.register(DecryptMessageButton, {role: 'message:BodyHeader'});\n  ComponentRegistry.register(RecipientKeyChip, {role: 'Composer:RecipientChip'});\n  ExtensionRegistry.MessageView.register(DecryptPGPExtension);\n  PreferencesUIStore.registerPreferencesTab(preferencesTab);\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(EncryptMessageButton);\n  ComponentRegistry.unregister(DecryptMessageButton);\n  ComponentRegistry.unregister(RecipientKeyChip);\n  ExtensionRegistry.MessageView.unregister(DecryptPGPExtension);\n  PreferencesUIStore.unregisterPreferencesTab(PREFERENCE_TAB_ID);\n}\n\nexport function serialize() {\n  return {};\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/modal-key-recommender.cjsx",
    "content": "{Utils, React, Actions} = require 'nylas-exports'\nPGPKeyStore = require './pgp-key-store'\nKeybaseSearch = require './keybase-search'\nKeybaseUser = require './keybase-user'\nkb = require './keybase'\n_ = require 'underscore'\n\nmodule.exports =\nclass ModalKeyRecommender extends React.Component\n\n  @displayName: 'ModalKeyRecommender'\n\n  @propTypes:\n    contacts: React.PropTypes.array.isRequired\n    emails: React.PropTypes.array\n    callback: React.PropTypes.func\n\n  @defaultProps:\n    callback: -> return # NOP\n\n  constructor: (props) ->\n    super(props)\n    @state = Object.assign({\n      currentContact: 0},\n      @_getStateFromStores())\n\n  componentDidMount: ->\n    @unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange)\n\n  componentWillUnmount: ->\n    @unlistenKeystore()\n\n  _onKeystoreChange: =>\n    @setState(@_getStateFromStores())\n\n  _getStateFromStores: =>\n    identities: PGPKeyStore.pubKeys(@props.emails)\n\n  _selectProfile: (address, identity) =>\n    # TODO this is an almost exact duplicate of keybase-search.cjsx:_save\n    keybaseUsername = identity.keybase_profile.components.username.val\n    identity.addresses.push(address)\n    kb.getKey(keybaseUsername, (error, key) =>\n      if error\n        console.error error\n      else\n        PGPKeyStore.saveNewKey(identity, key)\n    )\n\n  _onNext: =>\n    # NOTE: this doesn't do bounds checks! you must do that in render()!\n    @setState({currentContact: @state.currentContact + 1})\n\n  _onPrev: =>\n    # NOTE: this doesn't do bounds checks! you must do that in render()!\n    @setState({currentContact: @state.currentContact - 1})\n\n  _setPage: (page) =>\n    # NOTE: this doesn't do bounds checks! you must do that in render()!\n    @setState({currentContact: page})\n    # indexes from 0 because what kind of monster doesn't\n\n  _onDone: =>\n    if @state.identities.length < @props.emails.length\n      if !PGPKeyStore._displayDialog(\n        'Encrypt without keys for all recipients?',\n        'Some recipients are missing PGP public keys. They will not be able to decrypt this message.',\n        ['Encrypt', 'Cancel']\n      )\n        return\n\n    emptyIdents = _.filter(@state.identities, (identity) -> !identity.key?)\n    if emptyIdents.length == 0\n      Actions.closePopover()\n      @props.callback(@state.identities)\n    else\n      newIdents = []\n      for idIndex of emptyIdents\n        identity = emptyIdents[idIndex]\n        if idIndex < emptyIdents.length - 1\n          PGPKeyStore.getKeyContents(key: identity, callback: (identity) => newIdents.push(identity))\n        else\n          PGPKeyStore.getKeyContents(key: identity, callback: (identity) =>\n            newIdents.push(identity)\n            @props.callback(newIdents)\n            Actions.closePopover()\n          )\n\n  _onManageKeys: =>\n    Actions.switchPreferencesTab('Encryption')\n    Actions.openPreferences()\n\n  render: ->\n    # find the email we're dealing with now\n    email = @props.emails[@state.currentContact]\n    # and a corresponding contact\n    contact = _.findWhere(@props.contacts, {'email': email})\n    contactString = if contact? then contact.toString() else email\n    # find the identity object that goes with this email (if any)\n    identity = _.find(@state.identities, (identity) ->\n      return email in identity.addresses\n    )\n\n    if @state.currentContact == (@props.emails.length - 1)\n      # last one\n      if @props.emails.length == 1\n        # only one\n        backButton = false\n      else\n        backButton = <button className=\"btn modal-back-button\" onClick={ @_onPrev }>Back</button>\n      nextButton = <button className=\"btn modal-next-button\" onClick={ @_onDone }>Done</button>\n    else if @state.currentContact == 0\n      # first one\n      backButton = false\n      nextButton = <button className=\"btn modal-next-button\" onClick={ @_onNext }>Next</button>\n    else\n      # somewhere in the middle\n      backButton = <button className=\"btn modal-back-button\" onClick={ @_onPrev }>Back</button>\n      nextButton = <button className=\"btn modal-next-button\" onClick={ @_onNext }>Next</button>\n\n    if identity?\n      deleteButton = (<button title=\"Delete Public\" className=\"btn btn-toolbar btn-danger\" onClick={ => PGPKeyStore.deleteKey(identity) } ref=\"button\">\n        Delete Key\n      </button>\n      )\n      body = [\n        <div key=\"title\" className=\"picker-title\">This PGP public key has been saved for <br/><b>{ contactString }.</b></div>\n        <div className=\"keybase-profile-solo\">\n          <KeybaseUser key=\"keybase-user\" profile={ identity }, displayEmailList={false}, actionButton={deleteButton}/>\n        </div>\n      ]\n    else\n      if contact?\n        query = contact.fullName()\n        # don't search Keybase for emails, won't work anyways\n        if not query.match(/\\s/)?\n          query = \"\"\n      else\n        query = \"\"\n      importFunc = ((identity) => @_selectProfile(email, identity))\n\n      body = [\n        <div key=\"title\" className=\"picker-title\">There is no PGP public key saved for <br/><b>{ contactString }.</b></div>\n        <KeybaseSearch key=\"keybase-search\" initialSearch={ query }, importFunc={ importFunc } />\n      ]\n\n    prefsButton = <button className=\"btn modal-prefs-button\" onClick={@_onManageKeys}>Advanced Key Management</button>\n\n    <div className=\"key-picker-modal\">\n      { body }\n      <div style={{flex:1}}></div>\n      <div className=\"picker-controls\">\n        <div style={{width: 60}}> { backButton } </div>\n        { prefsButton }\n        <div style={{width: 60}}> { nextButton } </div>\n      </div>\n    </div>\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/passphrase-popover.cjsx",
    "content": "{React, Actions} = require 'nylas-exports'\nIdentity = require './identity'\nPGPKeyStore = require './pgp-key-store'\n_ = require 'underscore'\nfs = require 'fs'\npgp = require 'kbpgp'\n\nmodule.exports =\nclass PassphrasePopover extends React.Component\n  constructor: ->\n    @state = {\n      passphrase: \"\"\n      placeholder: \"PGP private key password\"\n      error: false\n      mounted: true\n    }\n\n  componentDidMount: ->\n    @_mounted = true\n\n  componentWillUnmount: ->\n    @_mounted = false\n\n  @propTypes:\n    identity: React.PropTypes.instanceOf(Identity)\n    addresses: React.PropTypes.array\n\n  render: ->\n    classNames = if @state.error then \"key-passphrase-input form-control bad-passphrase\" else \"key-passphrase-input form-control\"\n    <div className=\"passphrase-popover\">\n      <input type=\"password\" value={@state.passphrase} placeholder={@state.placeholder} className={classNames} onChange={@_onPassphraseChange} onKeyUp={@_onKeyUp} />\n      <button className=\"btn btn-toolbar\" onClick={@_validatePassphrase}>Done</button>\n    </div>\n\n  _onPassphraseChange: (event) =>\n    @setState\n      passphrase: event.target.value\n      placeholder: \"PGP private key password\"\n      error: false\n\n  _onKeyUp: (event) =>\n    if event.keyCode == 13\n      @_validatePassphrase()\n\n  _validatePassphrase: =>\n    passphrase = @state.passphrase\n    for emailIndex of @props.addresses\n      email = @props.addresses[emailIndex]\n      privateKeys = PGPKeyStore.privKeys(address: email, timed: false)\n      for keyIndex of privateKeys\n        # check to see if the password unlocks the key\n        key = privateKeys[keyIndex]\n        fs.readFile(key.keyPath, (err, data) =>\n          pgp.KeyManager.import_from_armored_pgp {\n            armored: data\n          }, (err, km) =>\n            if err\n              console.warn err\n            else\n              km.unlock_pgp { passphrase: passphrase }, (err) =>\n                if err\n                  if parseInt(keyIndex, 10) == privateKeys.length - 1\n                    if parseInt(emailIndex, 10) == @props.addresses.length - 1\n                      # every key has been tried, the password failed on all of them\n                      if @_mounted\n                        @setState\n                          passphrase: \"\"\n                          placeholder: \"Incorrect password\"\n                          error: true\n                else\n                  # the password unlocked a key; that key should be used\n                  @_onDone()\n        )\n\n  _onDone: =>\n    if @props.identity?\n      @props.onPopoverDone(@state.passphrase, @props.identity)\n    else\n      @props.onPopoverDone(@state.passphrase)\n    Actions.closePopover()\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/pgp-key-store.cjsx",
    "content": "NylasStore = require 'nylas-store'\n{Actions, FileDownloadStore, DraftStore, MessageBodyProcessor, RegExpUtils} = require 'nylas-exports'\n{remote, shell} = require 'electron'\nIdentity = require './identity'\nkb = require './keybase'\npgp = require 'kbpgp'\n_ = require 'underscore'\npath = require 'path'\nfs = require 'fs'\nos = require 'os'\n\nclass PGPKeyStore extends NylasStore\n\n  constructor: ->\n    super()\n\n    @_identities = {}\n\n    @_msgCache = []\n    @_msgStatus = []\n\n    # Recursive subdir watching only works on OSX / Windows. annoying\n    @_pubWatcher = null\n    @_privWatcher = null\n\n    @_keyDir = path.join(NylasEnv.getConfigDirPath(), 'keys')\n    @_pubKeyDir = path.join(@_keyDir, 'public')\n    @_privKeyDir = path.join(@_keyDir, 'private')\n\n    # Create the key storage file system if it doesn't already exist\n    fs.access(@_keyDir, fs.R_OK | fs.W_OK, (err) =>\n      if err\n        fs.mkdir(@_keyDir, (err) =>\n          if err\n            console.warn err\n          else\n            fs.mkdir(@_pubKeyDir, (err) =>\n              if err\n                console.warn err\n              else\n                fs.mkdir(@_privKeyDir, (err) =>\n                  if err\n                    console.warn err\n                  else\n                    @watch())))\n      else\n        fs.access(@_pubKeyDir, fs.R_OK | fs.W_OK, (err) =>\n          if err\n            fs.mkdir(@_pubKeyDir, (err) =>\n              if err\n                console.warn err))\n        fs.access(@_privKeyDir, fs.R_OK | fs.W_OK, (err) =>\n          if err\n            fs.mkdir(@_privKeyDir, (err) =>\n              if err\n                console.warn err))\n        @_populate()\n        @watch())\n\n  validAddress: (address, isPub) =>\n    if (!address || address.length == 0)\n      @_displayError('You must provide an email address.')\n      return false\n    if not (RegExpUtils.emailRegex().test(address))\n      @_displayError('Invalid email address.')\n      return false\n    keys = if isPub then @pubKeys(address) else @privKeys({address: address, timed: false})\n    keystate = if isPub then 'public' else 'private'\n    if (keys.length > 0)\n      @_displayError(\"A PGP #{keystate} key for that email address already exists.\")\n      return false\n    return true\n\n  ### I/O and File Tracking ###\n\n  watch: =>\n    if (!@_pubWatcher)\n      @_pubWatcher = fs.watch(@_pubKeyDir, @_populate)\n    if (!@_privWatcher)\n      @_privWatcher = fs.watch(@_privKeyDir, @_populate)\n\n  unwatch: =>\n    if (@_pubWatcher)\n      @_pubWatcher.close()\n    @_pubWatcher = null\n    if (@_privWatcher)\n      @_privWatcher.close()\n    @_privWatcher = null\n\n  _populate: =>\n    # add identity elements to later be populated with keys from disk\n    # TODO if this function is called multiple times in quick succession it\n    # will duplicate keys - need to do deduplication on add\n    fs.readdir(@_pubKeyDir, (err, pubFilenames) =>\n      fs.readdir(@_privKeyDir, (err, privFilenames) =>\n        @_identities = {}\n        _.each([[pubFilenames, false], [privFilenames, true]], (readresults) =>\n          filenames = readresults[0]\n          i = 0\n          if filenames.length == 0\n            @trigger(@)\n          while i < filenames.length\n            filename = filenames[i]\n            if filename[0] == '.'\n              continue\n            ident = new Identity({\n              addresses: filename.split(\" \")\n              isPriv: readresults[1]\n            })\n            @_identities[ident.clientId] = ident\n            @trigger(@)\n            i++)\n      )\n    )\n\n  getKeyContents: ({key, passphrase, callback}) =>\n    # Reads an actual PGP key from disk and adds it to the preexisting metadata\n    if not key.keyPath?\n      console.error \"Identity has no path for key!\", key\n      return\n    fs.readFile(key.keyPath, (err, data) =>\n      pgp.KeyManager.import_from_armored_pgp {\n        armored: data\n      }, (err, km) =>\n        if err\n          console.warn err\n        else\n          if km.is_pgp_locked()\n            # private key - check passphrase\n            passphrase ?= \"\"\n            km.unlock_pgp { passphrase: passphrase }, (err) =>\n              if err\n                # decrypt checks all keys, so DON'T open an error dialog\n                console.warn err\n                return\n              else\n                key.key = km\n                key.setTimeout()\n                if callback?\n                  callback(key)\n          else\n            # public key - get keybase data\n            key.key = km\n            key.setTimeout()\n            @getKeybaseData(key)\n            if callback?\n              callback(key)\n        @trigger(@)\n    )\n\n  getKeybaseData: (identity) =>\n    # Given a key, fetches metadata from keybase about that key\n    # TODO currently only works for public keys\n    if not identity.key? and not identity.isPriv and not identity.keybase_profile\n      @getKeyContents(key: identity)\n    else\n      fingerprint = identity.fingerprint()\n      if fingerprint?\n        kb.getUser(fingerprint, 'key_fingerprint', (err, user) =>\n          if err\n            console.error(err)\n          if user?.length == 1\n            identity.keybase_profile = user[0]\n          @trigger(@)\n        )\n\n  saveNewKey: (identity, contents) =>\n    # Validate the email address(es), then write to file.\n    if not identity instanceof Identity\n      console.error \"saveNewKey requires an identity object\"\n      return\n    addresses = identity.addresses\n    if addresses.length < 1\n      console.error \"Identity must have at least one email address to save key\"\n      return\n    if _.every(addresses, (address) => @validAddress(address, !identity.isPriv))\n      # Just say no to trailing whitespace.\n      if contents.charAt(contents.length - 1) != '-'\n        contents = contents.slice(0, -1)\n      fs.writeFile(identity.keyPath, contents, (err) =>\n        if (err)\n          @_displayError(err)\n      )\n\n  exportKey: ({identity, passphrase}) =>\n    atIndex = identity.addresses[0].indexOf(\"@\")\n    suffix = if identity.isPriv then \"-private.asc\" else \".asc\"\n    shortName = identity.addresses[0].slice(0, atIndex).concat(suffix)\n    NylasEnv.savedState.lastKeybaseDownloadDirectory ?= os.homedir()\n    savePath = path.join(NylasEnv.savedState.lastKeybaseDownloadDirectory, shortName)\n    @getKeyContents(key: identity, passphrase: passphrase, callback: ( (identity) =>\n      NylasEnv.showSaveDialog({\n        title: \"Export PGP Key\",\n        defaultPath: savePath,\n      }, (keyPath) =>\n        if (!keyPath)\n          return\n        NylasEnv.savedState.lastKeybaseDownloadDirectory = keyPath.slice(0, keyPath.length - shortName.length)\n        if passphrase?\n          identity.key.export_pgp_private {passphrase: passphrase}, (err, pgp_private) =>\n            if (err)\n              @_displayError(err)\n            fs.writeFile(keyPath, pgp_private, (err) =>\n              if (err)\n                @_displayError(err)\n              shell.showItemInFolder(keyPath)\n            )\n        else\n          identity.key.export_pgp_public {}, (err, pgp_public) =>\n            fs.writeFile(keyPath, pgp_public, (err) =>\n              if (err)\n                @_displayError(err)\n              shell.showItemInFolder(keyPath)\n            )\n      )\n    )\n    )\n\n  deleteKey: (key) =>\n    if this._displayDialog(\n      'Delete this key?',\n      'The key will be permanently deleted.',\n      ['Delete', 'Cancel']\n    )\n      fs.unlink(key.keyPath, (err) =>\n        if (err)\n          @_displayError(err)\n        @_populate()\n      )\n\n  addAddressToKey: (profile, address) =>\n    if @validAddress(address, !profile.isPriv)\n      oldPath = profile.keyPath\n      profile.addresses.push(address)\n      fs.rename(oldPath, profile.keyPath, (err) =>\n        if (err)\n          @_displayError(err)\n        )\n\n  removeAddressFromKey: (profile, address) =>\n    if profile.addresses.length > 1\n      oldPath = profile.keyPath\n      profile.addresses = _.without(profile.addresses, address)\n      fs.rename(oldPath, profile.keyPath, (err) =>\n        if (err)\n          @_displayError(err)\n        )\n    else\n      @deleteKey(profile)\n\n  ### Internal Key Management ###\n\n  pubKeys: (addresses) =>\n    # fetch public identity/ies for an address (synchronous)\n    # if no address, return them all\n    identities = _.where(_.values(@_identities), {isPriv: false})\n\n    if not addresses?\n      return identities\n\n    if typeof addresses is \"string\"\n      addresses = [addresses]\n\n    identities = _.filter(identities, (identity) ->\n      return _.intersection(addresses, identity.addresses).length > 0\n    )\n    return identities\n\n  privKeys: ({address, timed} = {timed: true}) =>\n    # fetch private identity/ies for an address (synchronous).\n    # by default, only return non-timed-out keys\n    # if no address, return them all\n    identities = _.where(_.values(@_identities), {isPriv: true})\n\n    if address?\n      identities = _.filter(identities, (identity) ->\n        return address in identity.addresses\n      )\n\n    if timed\n      identities = _.reject(identities, (identity) ->\n        return identity.isTimedOut()\n      )\n\n    return identities\n\n  _displayError: (err) ->\n    dialog = remote.dialog\n    dialog.showErrorBox('Key Management Error', err.toString())\n\n  _displayDialog: (title, message, buttons) ->\n    dialog = remote.dialog\n    return (dialog.showMessageBox({\n      title: title,\n      message: title,\n      detail: message,\n      buttons: buttons,\n      type: 'info',\n    }) == 0)\n\n  msgStatus: (msg) ->\n    # fetch the latest status of a message\n    if not msg?\n      return null\n    else\n      clientId = msg.clientId\n      statuses = _.filter @_msgStatus, (status) ->\n        return status.clientId == clientId\n      status = _.max statuses, (stat) ->\n        return stat.time\n    return status.message\n\n  isDecrypted: (message) ->\n    # if the message is already decrypted, return true\n    # if the message has no encrypted component, return true\n    # if the message has an encrypted component that is not yet decrypted, return false\n    if not @hasEncryptedComponent(message)\n      return true\n    else if @getDecrypted(message)?\n      return true\n    else\n      return false\n\n  getDecrypted: (message) =>\n    # Fetch a cached decrypted message\n    # (synchronous)\n\n    if message.clientId in _.pluck(@_msgCache, 'clientId')\n      msg = _.findWhere(@_msgCache, {clientId: message.clientId})\n      if msg.timeout > Date.now()\n        return msg.body\n\n    # otherwise\n    return null\n\n  hasEncryptedComponent: (message) ->\n    if not message.body?\n      return false\n\n    # find a PGP block\n    pgpStart = \"-----BEGIN PGP MESSAGE-----\"\n    pgpEnd = \"-----END PGP MESSAGE-----\"\n\n    blockStart = message.body.indexOf(pgpStart)\n    blockEnd = message.body.indexOf(pgpEnd)\n    # if they're both present, assume an encrypted block\n    return (blockStart >= 0 and blockEnd >= 0)\n\n  fetchEncryptedAttachments: (message) ->\n    encrypted = _.map(message.files, (file) =>\n      # calendars don't have filenames\n      if file.filename?\n        tokenized = file.filename.split('.')\n        extension = tokenized[tokenized.length - 1]\n        if extension == \"asc\" or extension == \"pgp\"\n          # something.asc or something.pgp -> assume encrypted attachment\n          return file\n        else\n          return null\n      else\n        return null\n      )\n    # NOTE for now we don't verify that the .asc/.pgp files actually have a PGP\n    # block inside\n\n    return _.compact(encrypted)\n\n  decrypt: (message) =>\n    # decrypt a message, cache the result\n    # (asynchronous)\n\n    # check to make sure we haven't already decrypted and cached the message\n    # note: could be a race condition here causing us to decrypt multiple times\n    # (not that that's a big deal other than minor resource wastage)\n    if @getDecrypted(message)?\n      return\n\n    if not @hasEncryptedComponent(message)\n      return\n\n    # fill our keyring with all possible private keys\n    ring = new pgp.keyring.KeyRing\n    # (the unbox function will use the right one)\n\n    for key in @privKeys({timed: true})\n      if key.key?\n        ring.add_key_manager(key.key)\n\n    # find a PGP block\n    pgpStart = \"-----BEGIN PGP MESSAGE-----\"\n    blockStart = message.body.indexOf(pgpStart)\n\n    pgpEnd = \"-----END PGP MESSAGE-----\"\n    blockEnd = message.body.indexOf(pgpEnd) + pgpEnd.length\n\n    # if we don't find those, it isn't encrypted\n    return unless (blockStart >= 0 and blockEnd >= 0)\n\n    pgpMsg = message.body.slice(blockStart, blockEnd)\n\n    # Some users may send messages from sources that pollute the encrypted block.\n    pgpMsg = pgpMsg.replace(/&#43;/gm,'+')\n    pgpMsg = pgpMsg.replace(/(<br>)/g, '\\n')\n    pgpMsg = pgpMsg.replace(/<\\/(blockquote|div|dl|dt|dd|form|h1|h2|h3|h4|h5|h6|hr|ol|p|pre|table|tr|td|ul|li|section|header|footer)>/g, '\\n')\n    pgpMsg = pgpMsg.replace(/<(.+?)>/g, '')\n    pgpMsg = pgpMsg.replace(/&nbsp;/g, ' ')\n\n    pgp.unbox { keyfetch: ring, armored: pgpMsg }, (err, literals, warnings, subkey) =>\n      if err\n        console.warn err\n        errMsg = \"Unable to decrypt message.\"\n        if err.toString().indexOf(\"tailer found\") >= 0 or err.toString().indexOf(\"checksum mismatch\") >= 0\n          errMsg = \"Unable to decrypt message. Encrypted block is malformed.\"\n        else if err.toString().indexOf(\"key not found:\") >= 0\n          errMsg = \"Unable to decrypt message. Private key does not match encrypted block.\"\n          if !@msgStatus(message)?\n            errMsg = \"Decryption preprocessing failed.\"\n        Actions.recordUserEvent(\"Email Decryption Errored\", {error: errMsg})\n        @_msgStatus.push({\"clientId\": message.clientId, \"time\": Date.now(), \"message\": errMsg})\n      else\n        if warnings._w.length > 0\n          console.warn warnings._w\n\n        if literals.length > 0\n          plaintext = literals[0].toString('utf8')\n\n          # <pre> tag for consistent styling\n          if plaintext.indexOf(\"<pre>\") == -1\n            plaintext = \"<pre>\\n\" + plaintext + \"\\n</pre>\"\n\n          # can't use _.template :(\n          body = message.body.slice(0, blockStart) + plaintext + message.body.slice(blockEnd)\n\n          # TODO if message is already in the cache, consider updating its TTL\n          timeout = 1000 * 60 * 30 # 30 minutes in ms\n          @_msgCache.push({clientId: message.clientId, body: body, timeout: Date.now() + timeout})\n          keyprint = subkey.get_fingerprint().toString('hex')\n          @_msgStatus.push({\"clientId\": message.clientId, \"time\": Date.now(), \"message\": \"Message decrypted with key #{keyprint}\"})\n          # re-render messages\n          Actions.recordUserEvent(\"Email Decrypted\")\n          MessageBodyProcessor.resetCache()\n          @trigger(@)\n        else\n          console.warn \"Unable to decrypt message.\"\n          @_msgStatus.push({\"clientId\": message.clientId, \"time\": Date.now(), \"message\": \"Unable to decrypt message.\"})\n\n  decryptAttachments: (identity, files) =>\n    # fill our keyring with all possible private keys\n    keyring = new pgp.keyring.KeyRing\n    # (the unbox function will use the right one)\n\n    if identity.key?\n      keyring.add_key_manager(identity.key)\n\n    FileDownloadStore._fetchAndSaveAll(files).then((filepaths) ->\n      # open, decrypt, and resave each of the newly-downloaded files in place\n      _.each(filepaths, (filepath) =>\n        fs.readFile(filepath, (err, data) =>\n          # find a PGP block\n          pgpStart = \"-----BEGIN PGP MESSAGE-----\"\n          blockStart = data.indexOf(pgpStart)\n\n          pgpEnd = \"-----END PGP MESSAGE-----\"\n          blockEnd = data.indexOf(pgpEnd) + pgpEnd.length\n\n          # if we don't find those, it isn't encrypted\n          return unless (blockStart >= 0 and blockEnd >= 0)\n\n          pgpMsg = data.slice(blockStart, blockEnd)\n\n          # decrypt the file\n          pgp.unbox({ keyfetch: keyring, armored: pgpMsg }, (err, literals, warnings, subkey) =>\n            if err\n              console.warn err\n            else\n              if warnings._w.length > 0\n                console.warn warnings._w\n\n            literalLen = literals?.length\n            # if we have no literals, failed to decrypt and should abort\n            return unless literalLen?\n\n            if literalLen == 1\n              # success! replace old encrypted file with awesome decrypted file\n              filepath = filepath.slice(0, filepath.length-3).concat(\"txt\")\n              fs.writeFile(filepath, literals[0].toBuffer(), (err) =>\n                if err\n                  console.warn err\n              )\n            else\n              console.warn \"Attempt to decrypt attachment failed: #{literalLen} literals found, expected 1.\"\n          )\n        )\n      )\n    )\n\n\nmodule.exports = new PGPKeyStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/preferences-keybase.cjsx",
    "content": "{React, RegExpUtils} = require 'nylas-exports'\nPGPKeyStore = require './pgp-key-store'\nKeybaseSearch = require './keybase-search'\nKeyManager = require './key-manager'\nKeyAdder = require './key-adder'\n\nclass PreferencesKeybase extends React.Component\n  @displayName: 'PreferencesKeybase'\n\n  constructor: (@props) ->\n    @_keySaveQueue = {}\n\n    {pubKeys, privKeys} = @_getStateFromStores()\n    @state =\n      pubKeys: pubKeys\n      privKeys: privKeys\n\n  componentDidMount: =>\n    @unlistenKeystore = PGPKeyStore.listen(@_onChange, @)\n\n  componentWillUnmount: =>\n    @unlistenKeystore()\n\n  _onChange: =>\n    @setState @_getStateFromStores()\n\n  _getStateFromStores: ->\n    pubKeys = PGPKeyStore.pubKeys()\n    privKeys = PGPKeyStore.privKeys(timed: false)\n    return {pubKeys, privKeys}\n\n  render: =>\n    noKeysMessage =\n    <div className=\"key-status-bar no-keys-message\">\n      You have no saved PGP keys!\n    </div>\n\n    keyManager = <KeyManager pubKeys={@state.pubKeys} privKeys={@state.privKeys}/>\n\n    <div className=\"container-keybase\">\n      <section className=\"key-add\">\n        <KeyAdder/>\n      </section>\n      <section className=\"keybase\">\n        <KeybaseSearch inPreferences={true} />\n        {if @state.pubKeys.length == 0 and @state.privKeys.length == 0 then noKeysMessage else keyManager}\n      </section>\n    </div>\n\nmodule.exports = PreferencesKeybase\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/private-key-popover.cjsx",
    "content": "{React, Actions, AccountStore} = require 'nylas-exports'\n{remote} = require 'electron'\nIdentity = require './identity'\nPGPKeyStore = require './pgp-key-store'\nPassphrasePopover = require './passphrase-popover'\n_ = require 'underscore'\nfs = require 'fs'\npgp = require 'kbpgp'\n\nmodule.exports =\nclass PrivateKeyPopover extends React.Component\n  constructor: ->\n    @state = {\n      selectedAddress: \"0\"\n      keyBody: \"\"\n      paste: false\n      import: false\n      validKeyBody: false\n    }\n\n  @propTypes:\n    addresses: React.PropTypes.array\n\n  render: =>\n    errorBar = <div className=\"invalid-key-body\">Invalid key body.</div>\n    keyArea = <textarea value={@state.keyBody || \"\"} onChange={@_onKeyChange} placeholder=\"Paste in your PGP key here!\"/>\n\n    saveBtnClass = if !(@state.validKeyBody) then \"btn modal-done-button btn-disabled\" else \"btn modal-done-button\"\n    saveButton = <button className={saveBtnClass} disabled={!(@state.validKeyBody)} onClick={@_onDone}>Save</button>\n\n    <div className=\"private-key-popover\" tabIndex=0>\n      <span key=\"title\" className=\"picker-title\"><b>No PGP private key found.<br/>Add a key for {@_renderAddresses()}</b></span>\n      <div className=\"key-add-buttons\">\n        <button className=\"btn btn-toolbar paste-btn\" onClick={@_onClickPaste}>Paste in a Key</button>\n        <button className=\"btn btn-toolbar import-btn\" onClick={@_onClickImport}>Import from File</button>\n      </div>\n      {if (@state.import or @state.paste) and !@state.validKeyBody and @state.keyBody != \"\" then errorBar}\n      {if @state.import or @state.paste then keyArea}\n      <div className=\"picker-controls\">\n        <div style={{width: 80}}><button className=\"btn modal-cancel-button\" onClick={=> Actions.closePopover()}>Cancel</button></div>\n        <button className=\"btn modal-prefs-button\" onClick={@_onClickAdvanced}>Advanced</button>\n        <div style={{width: 80}}>{saveButton}</div>\n      </div>\n    </div>\n\n  _renderAddresses: =>\n    signedIn = _.pluck(AccountStore.accounts(), \"emailAddress\")\n    suggestions = _.intersection(signedIn, @props.addresses)\n\n    if suggestions.length == 1\n      addresses = <span>{suggestions[0]}.</span>\n    else if suggestions.length > 1\n      options = suggestions.map((address) => <option value={suggestions.indexOf(address)} key={suggestions.indexOf(address)}>{address}</option>)\n      addresses =\n        <select value={@state.selectedAddress} onChange={@_onSelectAddress} style={{minWidth: 150}}>\n          {options}\n        </select>\n    else\n      throw new Error(\"How did you receive a message that you're not in the TO field for?\")\n\n  _onSelectAddress: (event) =>\n    @setState\n      selectedAddress: parseInt(event.target.value, 10)\n\n  _displayError: (err) ->\n    dialog = remote.dialog\n    dialog.showErrorBox('Private Key Error', err.toString())\n\n  _onClickAdvanced: =>\n    Actions.switchPreferencesTab('Encryption')\n    Actions.openPreferences()\n\n  _onClickImport: (event) =>\n    NylasEnv.showOpenDialog({\n      title: \"Import PGP Key\",\n      buttonLabel: \"Import\",\n      properties: ['openFile']\n    }, (filepath) =>\n      if filepath?\n        fs.readFile(filepath[0], (err, data) =>\n          pgp.KeyManager.import_from_armored_pgp {\n            armored: data\n          }, (err, km) =>\n            if err\n              @_displayError(\"File is not a valid PGP private key.\")\n              return\n            else\n              privateStart = \"-----BEGIN PGP PRIVATE KEY BLOCK-----\"\n              if km.armored_pgp_public.indexOf(privateStart) >= 0\n                @setState\n                  paste: false\n                  import: true\n                  keyBody: km.armored_pgp_public\n                  validKeyBody: true\n              else\n                @_displayError(\"File is not a valid PGP private key.\")\n      )\n    )\n\n  _onClickPaste: (event) =>\n    @setState\n      paste: !@state.paste\n      import: false\n      keyBody: \"\"\n      validKeyBody: false\n\n  _onKeyChange: (event) =>\n    @setState\n      keyBody: event.target.value\n    pgp.KeyManager.import_from_armored_pgp {\n      armored: event.target.value\n    }, (err, km) =>\n      if err\n        valid = false\n      else\n        privateStart = \"-----BEGIN PGP PRIVATE KEY BLOCK-----\"\n        if km.armored_pgp_public.indexOf(privateStart) >= 0\n          valid = true\n        else\n          valid = false\n      @setState\n        validKeyBody: valid\n\n  _onDone: =>\n    signedIn = _.pluck(AccountStore.accounts(), \"emailAddress\")\n    suggestions = _.intersection(signedIn, @props.addresses)\n    selectedAddress = suggestions[@state.selectedAddress]\n    ident = new Identity({\n      addresses: [selectedAddress]\n      isPriv: true\n    })\n    @unlistenKeystore = PGPKeyStore.listen(@_onKeySaved, @)\n    PGPKeyStore.saveNewKey(ident, @state.keyBody)\n\n  _onKeySaved: =>\n    @unlistenKeystore()\n    Actions.closePopover()\n    @props.callback()\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/lib/recipient-key-chip.cjsx",
    "content": "{MessageStore, React} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\nPGPKeyStore = require './pgp-key-store'\npgp = require 'kbpgp'\n_ = require 'underscore'\n\n# Sits next to recipient chips in the composer and turns them green/red\n# depending on whether or not there's a PGP key present for that user\nclass RecipientKeyChip extends React.Component\n\n  @displayName: 'RecipientKeyChip'\n\n  @propTypes:\n    contact: React.PropTypes.object.isRequired\n\n  constructor: (props) ->\n    super(props)\n    @state = @_getStateFromStores()\n\n  componentDidMount: ->\n    # fetch the actual key(s) from disk\n    keys = PGPKeyStore.pubKeys(@props.contact.email)\n    _.each(keys, (key) ->\n      PGPKeyStore.getKeyContents(key: key)\n    )\n    @unlistenKeystore = PGPKeyStore.listen(@_onKeystoreChange, @)\n\n  componentWillUnmount: ->\n    @unlistenKeystore()\n\n  _getStateFromStores: ->\n    return {\n      # true if there is at least one loaded key for the account\n      keys: PGPKeyStore.pubKeys(@props.contact.email).some((cv, ind, arr) =>\n        cv.hasOwnProperty('key')\n      )\n    }\n\n  _onKeystoreChange: ->\n    @setState(@_getStateFromStores())\n\n  render: ->\n    if @state.keys\n      <div className=\"n1-keybase-recipient-key-chip\">\n        <RetinaImg url=\"nylas://keybase/key-present@2x.png\" mode={RetinaImg.Mode.ContentPreserve} ref=\"keyIcon\" />\n      </div>\n    else\n      <div className=\"n1-keybase-recipient-key-chip\">\n        <span ref=\"noKeyIcon\"></span>\n      </div>\n\n\nmodule.exports = RecipientKeyChip\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/package.json",
    "content": "{\n  \"name\": \"keybase\",\n  \"main\": \"./lib/main\",\n  \"version\": \"0.1.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"isOptional\": true,\n  \"isHiddenOnPluginsPage\": true,\n\n  \"title\": \"Encryption\",\n  \"description\": \"Send and receive encrypted messages using Keybase for public key exchange.\",\n  \"icon\": \"./icon.png\",\n  \"license\": \"GPL-3.0\",\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/decrypt-buttons-spec.cjsx",
    "content": "{React, ReactTestUtils, DraftStore, Message} = require 'nylas-exports'\npgp = require 'kbpgp'\n\nDecryptMessageButton = require '../lib/decrypt-button'\nPGPKeyStore = require '../lib/pgp-key-store'\n\ndescribe \"DecryptMessageButton\", ->\n  beforeEach ->\n    @unencryptedMsg = new Message({clientId: 'test', subject: 'Subject', body: '<p>Body</p>'})\n    body = \"\"\"-----BEGIN PGP MESSAGE-----\n    Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto\n\n    wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=\n    =1aPN\n    -----END PGP MESSAGE-----\"\"\"\n    @encryptedMsg = new Message({clientId: 'test2', subject: 'Subject', body: body})\n\n    @msg = new Message({subject: 'Subject', body: '<p>Body</p>'})\n    @component = ReactTestUtils.renderIntoDocument(\n      <DecryptMessageButton message={@msg} />\n    )\n\n  xit \"should try to decrypt the message whenever a new key is unlocked\", ->\n    spyOn(PGPKeyStore, \"decrypt\")\n    spyOn(PGPKeyStore, \"isDecrypted\").andCallFake((message) =>\n      return false\n    )\n    spyOn(PGPKeyStore, \"hasEncryptedComponent\").andCallFake((message) =>\n      return true\n    )\n\n    PGPKeyStore.trigger(PGPKeyStore)\n    expect(PGPKeyStore.decrypt).toHaveBeenCalled()\n\n  xit \"should not try to decrypt the message whenever a new key is unlocked\n       if the message is already decrypted\", ->\n    spyOn(PGPKeyStore, \"decrypt\")\n    spyOn(PGPKeyStore, \"isDecrypted\").andCallFake((message) =>\n      return true)\n    spyOn(PGPKeyStore, \"hasEncryptedComponent\").andCallFake((message) =>\n      return true)\n\n    # TODO for some reason the above spyOn calls aren't working and false is\n    # being returned from isDecrypted, causing this test to fail\n    PGPKeyStore.trigger(PGPKeyStore)\n\n    expect(PGPKeyStore.decrypt).not.toHaveBeenCalled()\n\n  it \"should have a button to decrypt a message\", ->\n    @component = ReactTestUtils.renderIntoDocument(\n      <DecryptMessageButton message=@encryptedMsg />\n    )\n\n    expect(@component.refs.button).toBeDefined()\n\n  it \"should not allow for the unlocking of a message with no encrypted component\", ->\n    @component = ReactTestUtils.renderIntoDocument(\n      <DecryptMessageButton message=@unencryptedMsg />\n    )\n\n    expect(@component.refs.button).not.toBeDefined()\n\n  it \"should indicate when a message has been decrypted\", ->\n    spyOn(PGPKeyStore, \"isDecrypted\").andCallFake((message) =>\n      return true)\n\n    @component = ReactTestUtils.renderIntoDocument(\n      <DecryptMessageButton message=@encryptedMsg />\n    )\n\n    expect(@component.refs.button).not.toBeDefined()\n\n  it \"should open a popover when clicked\", ->\n    spyOn(DecryptMessageButton.prototype, \"_onClickDecrypt\")\n\n    msg = @encryptedMsg\n    msg.to = [{email: \"test@example.com\"}]\n    @component = ReactTestUtils.renderIntoDocument(\n      <DecryptMessageButton message=msg />\n    )\n    expect(@component.refs.button).toBeDefined()\n    ReactTestUtils.Simulate.click(@component.refs.button)\n    expect(DecryptMessageButton.prototype._onClickDecrypt).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/encrypt-button-spec.cjsx",
    "content": "{React, ReactDOM, ReactTestUtils, DraftStore, Message} = require 'nylas-exports'\npgp = require 'kbpgp'\n\nEncryptMessageButton = require '../lib/encrypt-button'\nPGPKeyStore = require '../lib/pgp-key-store'\n\ndescribe \"EncryptMessageButton\", ->\n  beforeEach ->\n    key = \"\"\"-----BEGIN PGP PRIVATE KEY BLOCK-----\n      Version: GnuPG v1\n\n      lQOYBFbgdCwBCADP7pEHzySjYHIlQK7T3XlqfFaot7VAgwmBUmXwFNRsYxGFj5sC\n      qEvhcw3nGvhVOul9A5S3yDZCtEDMqZSFDXNNIptpbhJgEqae0stfmHzHNUJSz+3w\n      ZE8Bvz1D5MU8YsMCUbt/wM/dBsp0EdbCS+zWIfM7Gzhb5vYOYx/wAeUxORCljQ6i\n      E80iGKII7EYmpscIOjb6QgaM7wih6GT3GWFYOMRG0uKGDVGWgWQ3EJgdcJq6Dvmx\n      GgrEQL7R8chtuLn9iyG3t5ZUfNvoH6PM7L7ei2ceMjxvLOfaHWNVKc+9YPeEOcvB\n      uQi5NEqSEZOSqd1jPPaOiSTnIOCeVXXMyZVlABEBAAEAB/0Q2OWLWm8/hYr6FbmU\n      lPdHd3eWB/x5k6Rrg/+aajWj6or65V3L41Lym13fAcJpNXLBnE6qbWBoGy685miQ\n      NzzGXS12Z2K5wgkaCT5NKo/BnEEZcJt4xMfZ/mK6Y4jPkbj3MSQd/8NXxzsUGHXs\n      HDa+StXoThZM6/O3yrRFwAGP8UhMVYOSwZB0u+DZ8EFaImqKJmznRvyNOaaGDrI5\n      cNdB4Xkk7L/tDxUxqc60WMQ49BEA9HW7miqymb3MEBA4Gd931pGYRM3hzQDhg+VI\n      oGlw2Xl9YjUGWVHMyufKzxTYhWWHDSpfjSVikeKwqbJWVqZ0a9/4GghhQRMdo2ho\n      AerpBADeXox+MRdbf2SgerxN4dPMBL5A5LD89Cu8AeY+6Ae1KlvGQFEOOQlW6Cwh\n      R1Tqn1p8JFG8jr7zg/nbPcIvOH/F00Dozfe+BW4BPJ8uv1E0ON/p54Bnp/XaNlGM\n      KyCDqRK+KDVpMXgP+rFK94+xLOuimMU3PhIDq623mezc8+u2CwQA72ELj49/OtqD\n      6VzEG6MKGfAOkW8l0xuxqo3SgLBU2E45zA9JYaocQ+z1fzFTUmMruFQaD1SxX7kr\n      Ml1s0BBiiEh323Cf01y1DXWQhWtw0s5phSzfzgB5GFZV42xtyQ+qZqf20TihJ8/O\n      b56J1tM7DsVXbVtcZdKRtUbRZ8vuOE8D/1oIuDT1a8Eqzl0KuS5VLOuVYvl8pbMc\n      aRkPtSkG4+nRw3LTQb771M39HpjgEv2Jw9aACHsWZ8DnNtoc8DA7UUeAouCT+Ev4\n      u3o9LrQ/+A/NUSLwBibViflo/gsR5L8tYn51zhJ3573FucFJP9ej7JncSL9x615q\n      Il2+Ry2pfUUZRj20OURha290YSBOZWxzb24gKFRlc3QgS2V5IEZvciBOMSBQbHVn\n      aW4pIDxkYWtvdGFAbnlsYXMuY29tPokBOAQTAQIAIgUCVuB0LAIbAwYLCQgHAwIG\n      FQgCCQoLBBYCAwECHgECF4AACgkQJgGhql9yqOCb5wgAqATlYC2ysjyUN66IfatW\n      rZij5lbIcjZyq5an1fxW9J0ofxeOIQ2duqnwoLFoDS2lNz4/kFlOn8vyvApsSfzC\n      +Gy1T46rc32CUBMjtD5Lh5fQ7fSNysii813MZAwfhdR0H6XO6kFj4RTJe4nzKnmM\n      sSSBbS/kbl9ZWZ993gisun8/PyDO4/1Yon8BDHABaJRJD5rqd1ZwtMIZguSgipXu\n      HqrdLpDxNUPr+YQ0C5r0kVJLFu0TVIz9grjV+MMCNVlDJvFla7vvRTdnym3HnbZo\n      XBeq/8zEnFcDWQC9Gkl4TrcuIwUYvcaO9j5V/E2fN+3b7YQp/0iwjZCHe+BgK5Hd\n      TJ0DmARW4HQsAQgAtSb1ove+EOJEspTwFP2gmnZ32SF6qGLcXkZkHJ7bYzudoKrQ\n      rkYcs61foyyeH/UrvOdHWsEOFnekE44oA/y7dGZiHAUcuYrqxtEF7QhmbcK0aRKS\n      JqmjO17rZ4Xz2MXsFxnGup5D94ZLxv2XktZX8EexMjdfU5Zdx1wu0GsMZX5Gj6AP\n      lQb0E1KDDnFII2uRs32j6GuO5WZJk1hdvz0DSTaaJ2pY3/WtMiUEBap9qSRR8WIK\n      kUO+TbzeogDXW10EiRyhIQadnfQTFjSVpGEos9b1k7zNNk/hb7yvlNL+pRY+8UcH\n      zRRMjC9wv6V7xmVOF/GhdGLLwzs36lxCbeheWQARAQABAAf/Vua0qZQtUo4pJH48\n      WeV9uPuh7MCZxdN/IZ6lAfHXDtiXem7XIvMxa6R9H5sU1AHaFInieg/owTBtvo/Q\n      dHE2P9WptQVizUNt8yhsrlP8RyVDRLCK+g8g5idXyFbDLrdr1X0hD39C3ahIC9K1\n      dtRqZTMPNybHDSMyI6P+NS9VSA4naigzzIzz4GLUgnzI/55M6QFcWxrnXc8B3XPQ\n      QxerSL3UseuNNr6nRhYt5arPpD7YhgmRakib+guPnmD5ZIbHOVFqS6RCkNkQ91zJ\n      nCo+o72gHbUDupEo8l/739k2SknWrNFt4S+mrvBM3c29cCnFaKQyRBNNGXtwmNnE\n      Dwr8DQQAxvQ+6Ijh4so4mdlI4+UT0d50gYQcnjz6BLtcRfewpT/EadIb0OuVS1Eh\n      MxM9QN5hXFKzT7GRS+nuk4NvrGr8aJ7mDPXzOHE/rnnAuikMuB1F13I8ELbya36B\n      j5wTvOBBjtNkcA1e9wX+iN4PyBVpzRUZZY6y0Xcyp9DsQwVpMvcEAOkYAeg4UCfO\n      PumYjdBRqcAuCKSQ8/UOrTOu5BDiIoyYBD3mrWSe61zZTuR7kb8/IkGHDTC7tLVZ\n      vKzdkRinh+qISpjI5OHSsITBV1uh/iko+K2rKca8gonjQBsxeAPMZwvMfUROGKkS\n      eXm/5sLUWlRtGkfVED1rYwUkE720tFUvBACGilgE7ezuoH7ZukyPPw9RziI7/CQp\n      u0KhFTGzLMGJWfiGgMC7l1jnS0EJxvs3ZpBme//vsKCjPGVg3/OqOHqCY0p9Uqjt\n      7v8o7y62AMzHKEGuMubSzDZZalo0515HQilfwnOGTHN14693icg1W/daB8aGI+Uz\n      cH3NziXnu23zc0VMiQEfBBgBAgAJBQJW4HQsAhsMAAoJECYBoapfcqjghFEH/ioJ\n      c4jot40O3Xa0K9ZFXol2seUHIf5rLgvcnwAKEiibK81/cZzlL6uXpgxVA4GOgdw5\n      nfGVd7b9jB7S6aUKcVoLDmy47qmJkWvZ45cjgv+K+ZoV22IN0J9Hhhdnqe+QJd4A\n      vIqb67gb9cw0xUDqcLdYywsXHoF9WkAYpIvBw4klHgd77XTzYz6xv4vVl469CPdk\n      +1dlOKpCHTLh7t38StP/rSu4ZrAYGET0e2+Ayqj44VHS9VwEbR/D2xrbjo43URZB\n      VsVlQKtXimFLpck1z0BPQ0NmRdEzRHQwP2WNYfxdNCeFAGDL4tpblBzw/vp/CFTO\n      217s2OKjpJqtpHPf2vY=\n      =UY7Y\n      -----END PGP PRIVATE KEY BLOCK-----\"\"\"\n\n    pgp.KeyManager.import_from_armored_pgp {\n      armored: key\n    }, (err, km) =>\n      @km = km\n\n    waitsFor (=> @km?), \"getting a key took too long\", 1000\n\n    @msg = new Message({subject: 'Subject', body: '<p>Body</p>', draft: true})\n    @session =\n      draft: =>\n        return @msg\n      changes:\n        add: (changes) =>\n          @output = changes\n\n    @output = null\n\n    add = jasmine.createSpy('add')\n    spyOn(DraftStore, 'sessionForClientId').andCallFake((draftClientId) =>\n      return Promise.resolve(@session)\n    )\n\n    @component = ReactTestUtils.renderIntoDocument(\n      <EncryptMessageButton draft={@msg} session={@session} />\n    )\n\n  it \"should render into the page\", ->\n    expect(@component).toBeDefined()\n\n  it \"should have a displayName\", ->\n    expect(EncryptMessageButton.displayName).toBe('EncryptMessageButton')\n\n  it \"should have an onClick behavior which encrypts the message\", ->\n    spyOn(@component, '_onClick')\n    buttonNode = ReactDOM.findDOMNode(@component.refs.button)\n    ReactTestUtils.Simulate.click(buttonNode)\n    expect(@component._onClick).toHaveBeenCalled()\n\n  it \"should store the message body's plaintext on encryption\", ->\n    spyOn(@component, '_onClick')\n    buttonNode = ReactDOM.findDOMNode(@component.refs.button)\n    ReactTestUtils.Simulate.click(buttonNode)\n    expect(@component.plaintext is @msg.body)\n\n  it \"should mark itself as encrypted\", ->\n    spyOn(@component, '_onClick')\n    buttonNode = ReactDOM.findDOMNode(@component.refs.button)\n    ReactTestUtils.Simulate.click(buttonNode)\n    expect(@component.currentlyEncrypted is true)\n\n  xit \"should be able to encrypt messages\", ->\n    # NOTE: this doesn't work.\n    # As best I can tell, something is wrong with the pgp.box function -\n    # nothing seems to get it to complete. Weird.\n\n    runs( =>\n      console.log @km\n      @component._encrypt(\"test text\", [@km])\n\n      @flag = false\n      pgp.box {encrypt_for: [@km], msg: \"test text\"}, (err, result_string) =>\n        expect(not err?)\n        @err = err\n        @result_string = result_string\n        @flag = true\n    )\n\n    waitsFor (=> console.log @flag; @flag), \"encryption took too long\", 5000\n\n    runs( =>\n      console.log @err\n      console.log @result_string\n      console.log @output\n\n      expect(@output is @result_string))\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/keybase-profile-spec.cjsx",
    "content": "{React, ReactTestUtils, Message} = require 'nylas-exports'\n\nKeybaseUser = require '../lib/keybase-user'\n\ndescribe \"KeybaseUserProfile\", ->\n  it \"should have a displayName\", ->\n    expect(KeybaseUser.displayName).toBe('KeybaseUserProfile')\n\n# behold, the most comprehensive test suite of all time\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/keybase-search-spec.cjsx",
    "content": "{React, ReactTestUtils, Message} = require 'nylas-exports'\n\nKeybaseSearch = require '../lib/keybase-search'\n\ndescribe \"KeybaseSearch\", ->\n  it \"should have a displayName\", ->\n    expect(KeybaseSearch.displayName).toBe('KeybaseSearch')\n\n  it \"should have no results when rendered\", ->\n    @component = ReactTestUtils.renderIntoDocument(\n      <KeybaseSearch />\n    )\n\n    expect(@component.state.results).toEqual([])\n\n# behold, the most comprehensive test suite of all time\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/keybase-spec.coffee",
    "content": "kb = require '../lib/keybase'\n\nxdescribe \"keybase lib\", ->\n  # TODO stub keybase calls?\n  it \"should be able to fetch an account by username\", ->\n    @them = null\n    runs( =>\n      kb.getUser('dakota', 'usernames', (err, them) =>\n        @them = them\n      )\n    )\n    waitsFor((=> @them != null), 2000)\n    runs( =>\n      expect(@them?[0].components.username.val).toEqual(\"dakota\")\n    )\n\n  it \"should be able to fetch an account by key fingerprint\", ->\n    @them = null\n    runs( =>\n      kb.getUser('7FA5A43BBF2BAD1845C8D0E8145FCCD989968E3B', 'key_fingerprint', (err, them) =>\n        @them = them\n      )\n    )\n    waitsFor((=> @them != null), 2000)\n    runs( =>\n      expect(@them?[0].components.username.val).toEqual(\"dakota\")\n    )\n\n  it \"should be able to fetch a user's key\", ->\n    @key = null\n    runs( =>\n      kb.getKey('dakota', (error, key) =>\n        @key = key\n      )\n    )\n    waitsFor((=> @key != null), 2000)\n    runs( =>\n      expect(@key?.startsWith('-----BEGIN PGP PUBLIC KEY BLOCK-----'))\n    )\n\n  it \"should be able to return an autocomplete query\", ->\n    @completions = null\n    runs( =>\n      kb.autocomplete('dakota', (error, completions) =>\n        @completions = completions\n      )\n    )\n    waitsFor((=> @completions != null), 2000)\n    runs( =>\n      expect(@completions[0].components.username.val).toEqual(\"dakota\")\n    )\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/main-spec.coffee",
    "content": "{ComponentRegistry, ExtensionRegistry} = require 'nylas-exports'\n{activate, deactivate} = require '../lib/main'\n\nEncryptMessageButton = require '../lib/encrypt-button'\nDecryptMessageButton = require '../lib/decrypt-button'\nDecryptPGPExtension = require '../lib/decryption-preprocess'\n\ndescribe \"activate\", ->\n  it \"should register the encryption button\", ->\n    spyOn(ComponentRegistry, 'register')\n    activate()\n    expect(ComponentRegistry.register).toHaveBeenCalledWith(EncryptMessageButton, {role: 'Composer:ActionButton'})\n\n  it \"should register the decryption button\", ->\n    spyOn(ComponentRegistry, 'register')\n    activate()\n    expect(ComponentRegistry.register).toHaveBeenCalledWith(DecryptMessageButton, {role: 'message:BodyHeader'})\n\n  it \"should register the decryption processor\", ->\n    spyOn(ExtensionRegistry.MessageView, 'register')\n    activate()\n    expect(ExtensionRegistry.MessageView.register).toHaveBeenCalledWith(DecryptPGPExtension)\n\n\ndescribe \"deactivate\", ->\n  it \"should unregister the encrypt button\", ->\n    spyOn(ComponentRegistry, 'unregister')\n    deactivate()\n    expect(ComponentRegistry.unregister).toHaveBeenCalledWith(EncryptMessageButton)\n\n  it \"should unregister the decryption button\", ->\n    spyOn(ComponentRegistry, 'unregister')\n    deactivate()\n    expect(ComponentRegistry.unregister).toHaveBeenCalledWith(DecryptMessageButton)\n\n  it \"should unregister the decryption processor\", ->\n    spyOn(ExtensionRegistry.MessageView, 'unregister')\n    deactivate()\n    expect(ExtensionRegistry.MessageView.unregister).toHaveBeenCalledWith(DecryptPGPExtension)\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/pgp-key-store-spec.cjsx",
    "content": "{React, ReactTestUtils, DraftStore, Message} = require 'nylas-exports'\npgp = require 'kbpgp'\n_ = require 'underscore'\nfs = require 'fs'\n\nIdentity = require '../lib/identity'\nPGPKeyStore = require '../lib/pgp-key-store'\n\ndescribe \"PGPKeyStore\", ->\n  beforeEach ->\n    @TEST_KEY = \"\"\"-----BEGIN PGP PRIVATE KEY BLOCK-----\n      Version: GnuPG v1\n\n      lQOYBFbgdCwBCADP7pEHzySjYHIlQK7T3XlqfFaot7VAgwmBUmXwFNRsYxGFj5sC\n      qEvhcw3nGvhVOul9A5S3yDZCtEDMqZSFDXNNIptpbhJgEqae0stfmHzHNUJSz+3w\n      ZE8Bvz1D5MU8YsMCUbt/wM/dBsp0EdbCS+zWIfM7Gzhb5vYOYx/wAeUxORCljQ6i\n      E80iGKII7EYmpscIOjb6QgaM7wih6GT3GWFYOMRG0uKGDVGWgWQ3EJgdcJq6Dvmx\n      GgrEQL7R8chtuLn9iyG3t5ZUfNvoH6PM7L7ei2ceMjxvLOfaHWNVKc+9YPeEOcvB\n      uQi5NEqSEZOSqd1jPPaOiSTnIOCeVXXMyZVlABEBAAEAB/0Q2OWLWm8/hYr6FbmU\n      lPdHd3eWB/x5k6Rrg/+aajWj6or65V3L41Lym13fAcJpNXLBnE6qbWBoGy685miQ\n      NzzGXS12Z2K5wgkaCT5NKo/BnEEZcJt4xMfZ/mK6Y4jPkbj3MSQd/8NXxzsUGHXs\n      HDa+StXoThZM6/O3yrRFwAGP8UhMVYOSwZB0u+DZ8EFaImqKJmznRvyNOaaGDrI5\n      cNdB4Xkk7L/tDxUxqc60WMQ49BEA9HW7miqymb3MEBA4Gd931pGYRM3hzQDhg+VI\n      oGlw2Xl9YjUGWVHMyufKzxTYhWWHDSpfjSVikeKwqbJWVqZ0a9/4GghhQRMdo2ho\n      AerpBADeXox+MRdbf2SgerxN4dPMBL5A5LD89Cu8AeY+6Ae1KlvGQFEOOQlW6Cwh\n      R1Tqn1p8JFG8jr7zg/nbPcIvOH/F00Dozfe+BW4BPJ8uv1E0ON/p54Bnp/XaNlGM\n      KyCDqRK+KDVpMXgP+rFK94+xLOuimMU3PhIDq623mezc8+u2CwQA72ELj49/OtqD\n      6VzEG6MKGfAOkW8l0xuxqo3SgLBU2E45zA9JYaocQ+z1fzFTUmMruFQaD1SxX7kr\n      Ml1s0BBiiEh323Cf01y1DXWQhWtw0s5phSzfzgB5GFZV42xtyQ+qZqf20TihJ8/O\n      b56J1tM7DsVXbVtcZdKRtUbRZ8vuOE8D/1oIuDT1a8Eqzl0KuS5VLOuVYvl8pbMc\n      aRkPtSkG4+nRw3LTQb771M39HpjgEv2Jw9aACHsWZ8DnNtoc8DA7UUeAouCT+Ev4\n      u3o9LrQ/+A/NUSLwBibViflo/gsR5L8tYn51zhJ3573FucFJP9ej7JncSL9x615q\n      Il2+Ry2pfUUZRj20OURha290YSBOZWxzb24gKFRlc3QgS2V5IEZvciBOMSBQbHVn\n      aW4pIDxkYWtvdGFAbnlsYXMuY29tPokBOAQTAQIAIgUCVuB0LAIbAwYLCQgHAwIG\n      FQgCCQoLBBYCAwECHgECF4AACgkQJgGhql9yqOCb5wgAqATlYC2ysjyUN66IfatW\n      rZij5lbIcjZyq5an1fxW9J0ofxeOIQ2duqnwoLFoDS2lNz4/kFlOn8vyvApsSfzC\n      +Gy1T46rc32CUBMjtD5Lh5fQ7fSNysii813MZAwfhdR0H6XO6kFj4RTJe4nzKnmM\n      sSSBbS/kbl9ZWZ993gisun8/PyDO4/1Yon8BDHABaJRJD5rqd1ZwtMIZguSgipXu\n      HqrdLpDxNUPr+YQ0C5r0kVJLFu0TVIz9grjV+MMCNVlDJvFla7vvRTdnym3HnbZo\n      XBeq/8zEnFcDWQC9Gkl4TrcuIwUYvcaO9j5V/E2fN+3b7YQp/0iwjZCHe+BgK5Hd\n      TJ0DmARW4HQsAQgAtSb1ove+EOJEspTwFP2gmnZ32SF6qGLcXkZkHJ7bYzudoKrQ\n      rkYcs61foyyeH/UrvOdHWsEOFnekE44oA/y7dGZiHAUcuYrqxtEF7QhmbcK0aRKS\n      JqmjO17rZ4Xz2MXsFxnGup5D94ZLxv2XktZX8EexMjdfU5Zdx1wu0GsMZX5Gj6AP\n      lQb0E1KDDnFII2uRs32j6GuO5WZJk1hdvz0DSTaaJ2pY3/WtMiUEBap9qSRR8WIK\n      kUO+TbzeogDXW10EiRyhIQadnfQTFjSVpGEos9b1k7zNNk/hb7yvlNL+pRY+8UcH\n      zRRMjC9wv6V7xmVOF/GhdGLLwzs36lxCbeheWQARAQABAAf/Vua0qZQtUo4pJH48\n      WeV9uPuh7MCZxdN/IZ6lAfHXDtiXem7XIvMxa6R9H5sU1AHaFInieg/owTBtvo/Q\n      dHE2P9WptQVizUNt8yhsrlP8RyVDRLCK+g8g5idXyFbDLrdr1X0hD39C3ahIC9K1\n      dtRqZTMPNybHDSMyI6P+NS9VSA4naigzzIzz4GLUgnzI/55M6QFcWxrnXc8B3XPQ\n      QxerSL3UseuNNr6nRhYt5arPpD7YhgmRakib+guPnmD5ZIbHOVFqS6RCkNkQ91zJ\n      nCo+o72gHbUDupEo8l/739k2SknWrNFt4S+mrvBM3c29cCnFaKQyRBNNGXtwmNnE\n      Dwr8DQQAxvQ+6Ijh4so4mdlI4+UT0d50gYQcnjz6BLtcRfewpT/EadIb0OuVS1Eh\n      MxM9QN5hXFKzT7GRS+nuk4NvrGr8aJ7mDPXzOHE/rnnAuikMuB1F13I8ELbya36B\n      j5wTvOBBjtNkcA1e9wX+iN4PyBVpzRUZZY6y0Xcyp9DsQwVpMvcEAOkYAeg4UCfO\n      PumYjdBRqcAuCKSQ8/UOrTOu5BDiIoyYBD3mrWSe61zZTuR7kb8/IkGHDTC7tLVZ\n      vKzdkRinh+qISpjI5OHSsITBV1uh/iko+K2rKca8gonjQBsxeAPMZwvMfUROGKkS\n      eXm/5sLUWlRtGkfVED1rYwUkE720tFUvBACGilgE7ezuoH7ZukyPPw9RziI7/CQp\n      u0KhFTGzLMGJWfiGgMC7l1jnS0EJxvs3ZpBme//vsKCjPGVg3/OqOHqCY0p9Uqjt\n      7v8o7y62AMzHKEGuMubSzDZZalo0515HQilfwnOGTHN14693icg1W/daB8aGI+Uz\n      cH3NziXnu23zc0VMiQEfBBgBAgAJBQJW4HQsAhsMAAoJECYBoapfcqjghFEH/ioJ\n      c4jot40O3Xa0K9ZFXol2seUHIf5rLgvcnwAKEiibK81/cZzlL6uXpgxVA4GOgdw5\n      nfGVd7b9jB7S6aUKcVoLDmy47qmJkWvZ45cjgv+K+ZoV22IN0J9Hhhdnqe+QJd4A\n      vIqb67gb9cw0xUDqcLdYywsXHoF9WkAYpIvBw4klHgd77XTzYz6xv4vVl469CPdk\n      +1dlOKpCHTLh7t38StP/rSu4ZrAYGET0e2+Ayqj44VHS9VwEbR/D2xrbjo43URZB\n      VsVlQKtXimFLpck1z0BPQ0NmRdEzRHQwP2WNYfxdNCeFAGDL4tpblBzw/vp/CFTO\n      217s2OKjpJqtpHPf2vY=\n      =UY7Y\n      -----END PGP PRIVATE KEY BLOCK-----\"\"\"\n\n    # mock getKeyContents to get rid of all the fs.readFiles\n    spyOn(PGPKeyStore, \"getKeyContents\").andCallFake( ({key, passphrase, callback}) =>\n      data = @TEST_KEY\n      pgp.KeyManager.import_from_armored_pgp {\n        armored: data\n      }, (err, km) =>\n        expect(err).toEqual(null)\n        if km.is_pgp_locked()\n          expect(passphrase).toBeDefined()\n          km.unlock_pgp { passphrase: passphrase }, (err) =>\n            expect(err).toEqual(null)\n        key.key = km\n        key.setTimeout()\n        if callback?\n          callback()\n    )\n\n    # define an encrypted and an unencrypted message\n    @unencryptedMsg = new Message({clientId: 'test', subject: 'Subject', body: '<p>Body</p>'})\n    body = \"\"\"-----BEGIN PGP MESSAGE-----\n    Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto\n\n    wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=\n    =1aPN\n    -----END PGP MESSAGE-----\"\"\"\n    @encryptedMsg = new Message({clientId: 'test2', subject: 'Subject', body: body})\n\n    # blow away the saved identities and set up a test pub/priv keypair\n    PGPKeyStore._identities = {}\n    pubIdent = new Identity({\n      addresses: [\"benbitdiddle@icloud.com\"]\n      isPriv: false\n    })\n    PGPKeyStore._identities[pubIdent.clientId] = pubIdent\n    privIdent = new Identity({\n      addresses: [\"benbitdiddle@icloud.com\"]\n      isPriv: true\n    })\n    PGPKeyStore._identities[privIdent.clientId] = privIdent\n\n  describe \"when handling private keys\", ->\n    it 'should be able to retrieve and unlock a private key', ->\n      expect(PGPKeyStore.privKeys().some((cv, index, array) =>\n        cv.hasOwnProperty(\"key\"))).toBeFalsey\n      key = PGPKeyStore.privKeys(address: \"benbitdiddle@icloud.com\", timed: false)[0]\n      PGPKeyStore.getKeyContents(key: key, passphrase: \"\", callback: =>\n        expect(PGPKeyStore.privKeys({timed: false}).some((cv, index, array) =>\n          cv.hasOwnProperty(\"key\"))).toBeTruthy\n      )\n\n    it 'should not return a private key after its timeout has passed', ->\n      expect(PGPKeyStore.privKeys({address: \"benbitdiddle@icloud.com\", timed: false}).length).toEqual(1)\n      PGPKeyStore.privKeys({address: \"benbitdiddle@icloud.com\", timed: false})[0].timeout = Date.now() - 5\n      expect(PGPKeyStore.privKeys(address: \"benbitdiddle@icloud.com\", timed: true).length).toEqual(0)\n      PGPKeyStore.privKeys({address: \"benbitdiddle@icloud.com\", timed: false})[0].setTimeout()\n\n    it 'should only return the key(s) corresponding to a supplied email address', ->\n      expect(PGPKeyStore.privKeys(address: \"wrong@example.com\", timed: true).length).toEqual(0)\n\n    it 'should return all private keys when an address is not supplied', ->\n      expect(PGPKeyStore.privKeys({timed: false}).length).toEqual(1)\n\n    it 'should update an existing key when it is unlocked, not add a new one', ->\n      timeout = PGPKeyStore.privKeys({address: \"benbitdiddle@icloud.com\", timed: false})[0].timeout\n      PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: \"\", callback: =>\n        # expect no new keys to have been added\n        expect(PGPKeyStore.privKeys({timed: false}).length).toEqual(1)\n        # make sure the timeout is updated\n        expect(timeout < PGPKeyStore.privKeys({address: \"benbitdiddle@icloud.com\", timed: false}).timeout)\n      )\n\n  describe \"when decrypting messages\", ->\n    xit 'should be able to decrypt a message', ->\n      # TODO for some reason, the pgp.unbox has a problem with the message body\n      runs( =>\n        spyOn(PGPKeyStore, 'trigger')\n        PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: \"\", callback: =>\n          PGPKeyStore.decrypt(@encryptedMsg)\n        )\n      )\n      waitsFor((=> PGPKeyStore.trigger.callCount > 0), 'message to decrypt')\n      runs( =>\n        expect(_.findWhere(PGPKeyStore._msgCache,\n               {clientId: @encryptedMsg.clientId})).toExist()\n      )\n\n    it 'should be able to handle an unencrypted message', ->\n      PGPKeyStore.decrypt(@unencryptedMsg)\n      expect(_.findWhere(PGPKeyStore._msgCache,\n             {clientId: @unencryptedMsg.clientId})).not.toBeDefined()\n\n    it 'should be able to tell when a message has no encrypted component', ->\n      expect(PGPKeyStore.hasEncryptedComponent(@unencryptedMsg)).not\n      expect(PGPKeyStore.hasEncryptedComponent(@encryptedMsg))\n\n    it 'should be able to handle a message with no BEGIN PGP MESSAGE block', ->\n      body = \"\"\"Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto\n\n      wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=\n      =1aPN\n      -----END PGP MESSAGE-----\"\"\"\n      badMsg = new Message({clientId: 'test2', subject: 'Subject', body: body})\n\n      PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: \"\", callback: =>\n        PGPKeyStore.decrypt(badMsg)\n        expect(_.findWhere(PGPKeyStore._msgCache,\n               {clientId: badMsg.clientId})).not.toBeDefined()\n      )\n\n    it 'should be able to handle a message with no END PGP MESSAGE block', ->\n      body = \"\"\"-----BEGIN PGP MESSAGE-----\n      Version: Keybase OpenPGP v2.0.52 Comment: keybase.io/crypto\n\n      wcBMA5nwa6GWVDOUAQf+MjiVRIBWJyM6The6/h2MgSJTDyrN9teFFJTizOvgHNnD W4EpEmmhShNyERI67qXhC03lFczu2Zp2Qofgs8YePIEv7wwb27/cviODsE42YJvX 1zGir+jBp81s9ZiF4dex6Ir9XfiZJlypI2QV2dHjO+5pstW+XhKIc1R5vKvoFTGI 1XmZtL3EgtKfj/HkPUkq2N0G5kAoB2MTTQuurfXm+3TRkftqesyTKlek652sFjCv nSF+LQ1GYq5hI4YaUBiHnZd7wKUgDrIh2rzbuGq+AHjrHdVLMfRTbN0Xsy3OWRcC 9uWU8Nln00Ly6KbTqPXKcBDcMrOJuoxYcpmLlhRds9JoAY7MyIsj87M2mkTtAtMK hqK0PPvJKfepV+eljDhQ7y0TQ0IvNtO5/pcY2CozbFJncm/ToxxZPNJueKRcz+EH M9uBvrWNTwfHj26g405gpRDN1T8CsY5ZeiaDHduIKnBWd4za0ak0Xfw=\n      =1aPN\"\"\"\n      badMsg = new Message({clientId: 'test2', subject: 'Subject', body: body})\n\n      PGPKeyStore.getKeyContents(key: PGPKeyStore.privKeys({timed: false})[0], passphrase: \"\", callback: =>\n        PGPKeyStore.decrypt(badMsg)\n        expect(_.findWhere(PGPKeyStore._msgCache,\n               {clientId: badMsg.clientId})).not.toBeDefined()\n      )\n\n    it 'should not return a decrypted message which has timed out', ->\n      PGPKeyStore._msgCache.push({clientId: \"testID\", body: \"example body\", timeout: Date.now()})\n\n      msg = new Message({clientId: \"testID\"})\n      expect(PGPKeyStore.getDecrypted(msg)).toEqual(null)\n\n    it 'should return a decrypted message', ->\n      timeout = Date.now() + (1000*60*60)\n      PGPKeyStore._msgCache.push({clientId: \"testID2\", body: \"example body\", timeout: timeout})\n\n      msg = new Message({clientId: \"testID2\", body: \"example body\"})\n      expect(PGPKeyStore.getDecrypted(msg)).toEqual(msg.body)\n\n  describe \"when handling public keys\", ->\n\n    it \"should immediately return a pre-cached key\", ->\n      expect(PGPKeyStore.pubKeys('benbitdiddle@icloud.com').length).toEqual(1)\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/spec/recipient-key-chip-spec.cjsx",
    "content": "{React, ReactTestUtils, DraftStore, Contact} = require 'nylas-exports'\npgp = require 'kbpgp'\n\nRecipientKeyChip = require '../lib/recipient-key-chip'\nPGPKeyStore = require '../lib/pgp-key-store'\n\ndescribe \"DecryptMessageButton\", ->\n  beforeEach ->\n    @contact = new Contact({email: \"test@example.com\"})\n    @component = ReactTestUtils.renderIntoDocument(\n      <RecipientKeyChip contact=@contact />\n    )\n\n  it \"should render into the page\", ->\n    expect(@component).toBeDefined()\n\n  it \"should have a displayName\", ->\n    expect(RecipientKeyChip.displayName).toBe('RecipientKeyChip')\n\n  xit \"should indicate when a recipient has a PGP key available\", ->\n    spyOn(PGPKeyStore, \"pubKeys\").andCallFake((address) =>\n      return [{'key':0}])\n    key = PGPKeyStore.pubKeys(@contact.email)\n    expect(key).toBeDefined()\n\n    # TODO these calls crash the tester because they require a call to getKeyContents\n    expect(@component.refs.keyIcon).toBeDefined()\n    expect(@component.refs.noKeyIcon).not.toBeDefined()\n\n  xit \"should indicate when a recipient does not have a PGP key available\", ->\n    component = ReactTestUtils.renderIntoDocument(\n      <RecipientKeyChip contact=@contact />\n    )\n\n    key = PGPKeyStore.pubKeys(@contact.email)\n    expect(key).toEqual([])\n\n    # TODO these calls crash the tester because they require a call to getKeyContents\n    expect(component.refs.keyIcon).not.toBeDefined()\n    expect(component.refs.noKeyIcon).toBeDefined()\n"
  },
  {
    "path": "packages/client-app/internal_packages/keybase/stylesheets/main.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n@code-bg-color: #fcf4db;\n\n.keybase {\n\n  .no-keys-message {\n    text-align: center;\n  }\n\n}\n\n.container-keybase {\n  max-width: 640px;\n  margin: 0 auto;\n}\n\n.keybase-profile {\n  border: 1px solid @border-color-primary;\n  border-top: 0;\n  background: @background-primary;\n  padding: 10px;\n  overflow: auto;\n  display: flex;\n\n  .profile-photo-wrap {\n    width: 50px;\n    height: 50px;\n    border-radius: @border-radius-base;\n    padding: 3px;\n    box-shadow: 0 0 1px rgba(0,0,0,0.5);\n    background: @background-primary;\n\n    .profile-photo {\n      border-radius: @border-radius-small;\n      overflow: hidden;\n      text-align: center;\n      width: 44px;\n      height: 44px;\n\n      img, .default-profile-image {\n        width: 44px;\n        height: 44px;\n      }\n\n      .default-profile-image {\n        line-height: 44px;\n        font-size: 18px;\n        font-weight: 500;\n        color: white;\n        box-shadow: inset 0 0 1px rgba(0,0,0,0.18);\n        background-image: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 100%);\n      }\n\n      .user-picture {\n        background: @background-secondary;\n        width: 44px;\n        height: 44px;\n      }\n    }\n  }\n\n  .key-actions {\n    display: flex;\n    flex-direction: column;\n\n    button {\n      margin: 2px 0 2px 10px;\n      white-space: nowrap;\n      display: inline-block;\n      float: right;\n    }\n  }\n\n  .details {\n    margin-left: 10px;\n    flex: 1;\n  }\n\n  button {\n    margin: 10px 0 10px 10px;\n    white-space: nowrap;\n    display: inline-block;\n    float: right;\n  }\n\n  keybase-participant-field {\n    float: right;\n  }\n\n  ul {\n    list-style-type: none;\n  }\n\n  .email-list {\n    padding-left: 10px;\n    word-break: break-all;\n    flex-grow: 3;\n    text-align: right;\n  }\n}\n\n.keybase-profile:first-child {\n  border-top: 1px solid @border-color-primary;\n}\n\n.fixed-popover-container, .email-list {\n  .keybase-participant-field {\n    margin-bottom: 10px;\n\n    .n1-keybase-recipient-key-chip {\n      display: none;\n    }\n\n    .tokenizing-field-label {\n      display: none;\n      padding-top: 0;\n    }\n\n    .tokenizing-field-input {\n      padding-left: 0;\n      padding-top: 0;\n\n      input {\n        border: none;\n      }\n    }\n  }\n}\n\n.fixed-popover-container {\n  .keybase-participant-field {\n    width: 300px;\n    background: @input-bg;\n    border: 1px solid @input-border-color;\n\n      .menu .content-container {\n        background: @background-secondary;\n      }\n  }\n\n  .passphrase-popover {\n    margin: 10px;\n    display: flex;\n\n    button {\n      margin-left: 5px;\n      flex: 0;\n    }\n\n    input {\n      min-width: 180px;\n      flex: 1;\n    }\n\n    .bad-passphrase {\n      border-color: @color-error;\n    }\n  }\n\n  .keybase-import-popover {\n    margin: 10px;\n\n    button {\n      width: 100%;\n    }\n\n    .title {\n      margin: 0 auto;\n      white-space: nowrap;\n    }\n  }\n\n  .private-key-popover {\n    display: flex;\n    flex-direction: column;\n    width: 300px;\n    margin: 5px 10px;\n\n    .picker-title {\n      margin-left: auto;\n      margin-right: auto;\n      text-align: center;\n    }\n\n    textarea {\n      margin-top: 5px;\n    }\n\n    .invalid-key-body {\n      background-color: @code-bg-color;\n      color: darken(@code-bg-color, 70%);\n      border: 1.5px solid darken(@code-bg-color, 10%);\n      border-radius: @border-radius-small;\n      font-size: @font-size-small;\n      margin: 5px 0 0 0;\n      text-align: center;\n    }\n\n    .key-add-buttons {\n      display: flex;\n      flex-direction: row;\n\n      button {\n        width: 147px;\n        margin: 5px 0 0 0;\n      }\n\n      .paste-btn {\n        margin-right: 6px;\n      }\n    }\n\n    .picker-controls {\n      width: 100%;\n      margin: 5px auto;\n      display: flex;\n      flex-shrink: 0;\n      flex-direction: row;\n\n      .modal-cancel-button {\n        float: left;\n      }\n\n      .modal-prefs-button {\n        flex: 1;\n        margin: 0 35px;\n      }\n\n      .modal-done-button {\n        float: right;\n      }\n    }\n  }\n}\n\n.email-list {\n  .keybase-participant-field {\n    width: 200px;\n    border-bottom: 1px solid @gray-light;\n  }\n}\n\n.keybase-decrypt {\n\n  div.line-w-label {\n    display: flex;\n    align-items: center;\n    color: rgba(128, 128, 128, 0.5);\n  }\n\n  div.decrypt-bar {\n    padding: 5px;\n    border: 1.5px solid rgba(128, 128, 128, 0.5);\n    border-radius: @border-radius-large;\n    align-items: center;\n    display: flex;\n\n    .title-text {\n      flex: 1;\n      margin: auto 0;\n    }\n\n    .decryption-interface {\n      button {\n        margin-left: 5px;\n      }\n    }\n  }\n\n  div.error-decrypt-bar {\n    border: 1.5px solid @color-error;\n\n    .title-text {\n      color: @color-error;\n    }\n  }\n\n  div.done-decrypt-bar {\n    border: 1.5px solid @color-success;\n\n    .title-text {\n      color: @color-success;\n    }\n  }\n\n  div.border {\n    height: 1px;\n    background: rgba(128, 128, 128, 0.5);\n    flex: 1;\n  }\n\n  div.error-border {\n    background: @color-error;\n  }\n\n  div.done-border {\n    background: @color-success;\n  }\n}\n\n.key-manager {\n\n  div.line-w-label {\n    display: flex;\n    align-items: center;\n    color: rgba(128, 128, 128, 0.5);\n    margin: 10px 0;\n  }\n  div.title-text {\n    padding: 0 10px;\n  }\n  div.border {\n    height: 1px;\n    background: rgba(128, 128, 128, 0.5);\n    flex: 1;\n  }\n}\n\n.key-status-bar {\n  background-color: @code-bg-color;\n  color: darken(@code-bg-color, 70%);\n  border: 1.5px solid darken(@code-bg-color, 10%);\n  border-radius: @border-radius-small;\n  font-size: @font-size-small;\n  margin-bottom: 10px;\n}\n\n.key-add {\n    padding-top:10px;\n\n    .no-keys-message {\n      text-align: center;\n    }\n\n    .key-adder {\n      position: relative;\n      border: 1px solid @input-border-color;\n      padding: 10px;\n      padding-top: 0;\n      margin-bottom: 10px;\n\n    .key-text {\n      margin-top: 10px;\n      min-height: 200px;\n      display: flex;\n\n      .loading {\n        position: absolute;\n        left: 50%;\n        top: 50%;\n        transform: translate(-50%, 50%);\n      }\n\n      textarea {\n        border: 0;\n        padding: 0;\n        font-size: 0.9em;\n        flex: 1;\n      }\n    }\n  }\n\n  .credentials {\n    display: flex;\n    flex-direction: row;\n\n    .key-add-btn {\n      margin: 10px 5px 0 0;\n      flex: 0;\n    }\n\n    .key-email-input {\n      margin: 10px 5px 0 0;\n      flex: 1;\n    }\n\n    .key-passphrase-input {\n      margin: 10px 5px 0 0;\n      flex: 1;\n    }\n\n    .invalid-msg {\n      color: #AAA;\n      white-space: nowrap;\n      text-align: right;\n      margin: 12px 5px 0 0;\n      flex: 1;\n    }\n  }\n\n  .key-creation-button {\n    display: inline-block;\n    margin: 0 5px 10px 5px;\n  }\n\n  .editor-note {\n    color: #AAA;\n  }\n}\n\n.key-instructions {\n  color: #333;\n  font-size: small;\n  margin-top: 20px;\n}\n\n.keybase-search {\n  margin-top: 15px;\n  margin-bottom: 15px;\n  overflow: scroll;\n  position: relative;\n\n  input {\n    padding: 10px;\n    margin-bottom: 10px;\n  }\n\n  .empty {\n    text-align: center;\n  }\n\n  .loading {\n    position: absolute;\n    right: 10px;\n    top: 8px; // lol I wonder how long until this is a problem\n  }\n\n  .bad-search-msg {\n    display: inline-block;\n    width: 100%;\n    text-align: center;\n    color: rgba(128, 128, 128, 0.5);\n\n    br {\n      display: none;\n    }\n  }\n}\n\n.key-picker-modal {\n  width: 400px;\n  height: 400px;\n  display: flex;\n  flex-direction: column;\n\n  .keybase-search {\n    display: flex;\n    flex-direction: column;\n    height: 100%;\n    max-width: 400px;\n    overflow: hidden;\n    margin-bottom: 0;\n    margin-top: 10px;\n\n    .searchbar {\n      width: 380px;\n      margin-left: auto;\n      margin-right: auto;\n    }\n\n    .loading {\n      right: 20px;\n    }\n\n    .results {\n      overflow: auto;\n      height: 100%;\n      width: 100%;\n    }\n\n    .bad-search-msg {\n      br {\n        display: inline;\n      }\n    }\n  }\n\n  .picker-controls {\n    width: 380px;\n    margin: 5px auto 10px auto;\n    display: flex;\n    flex-shrink: 0;\n    flex-direction: row;\n\n    .modal-back-button {\n      float: left;\n    }\n\n    .modal-prefs-button {\n      flex: 1;\n      margin: 0 35px;\n    }\n\n    .modal-next-button {\n      float: right;\n    }\n  }\n\n  .keybase-profile-solo {\n    border: 1px solid @border-color-primary;\n    margin-top: 10px;\n  }\n\n  .picker-title {\n    margin-top: 10px;\n    margin-left: auto;\n    margin-right: auto;\n    text-align: center;\n  }\n}\n\n.decrypted {\n  display: block;\n  box-sizing: border-box;\n  -webkit-print-color-adjust: exact;\n  padding: 8px 12px;\n  margin-bottom: 5px;\n  border: 1px solid rgb(235, 204, 209);\n  border-radius: 4px;\n  background-color: rgb(121, 212, 91);\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/README.md",
    "content": "\n## Open Tracking\n\nAdds tracking pixels to messages and tracks whether they have been opened.\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/lib/link-tracking-button.jsx",
    "content": "// import {DraftStore, React, Actions, NylasAPI, DatabaseStore, Message, Rx} from 'nylas-exports'\nimport {React, APIError, NylasAPI} from 'nylas-exports'\nimport {MetadataComposerToggleButton} from 'nylas-component-kit'\nimport {PLUGIN_ID, PLUGIN_NAME} from './link-tracking-constants'\n\nexport default class LinkTrackingButton extends React.Component {\n  static displayName = 'LinkTrackingButton';\n\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n  };\n\n  shouldComponentUpdate(nextProps) {\n    return (nextProps.draft.metadataForPluginId(PLUGIN_ID) !== this.props.draft.metadataForPluginId(PLUGIN_ID));\n  }\n\n  _title(enabled) {\n    const dir = enabled ? \"Disable\" : \"Enable\";\n    return `${dir} link tracking`\n  }\n\n  _errorMessage(error) {\n    if (error instanceof APIError && NylasAPI.TimeoutErrorCodes.includes(error.statusCode)) {\n      return `Link tracking does not work offline. Please re-enable when you come back online.`\n    }\n    return `Unfortunately, link tracking servers are currently not available. Please try again later. Error: ${error.message}`\n  }\n\n  render() {\n    return (\n      <MetadataComposerToggleButton\n        title={this._title}\n        iconName=\"icon-composer-linktracking.png\"\n        pluginId={PLUGIN_ID}\n        pluginName={PLUGIN_NAME}\n        metadataEnabledValue={{tracked: true}}\n        stickyToggle\n        errorMessage={this._errorMessage}\n        draft={this.props.draft}\n        session={this.props.session}\n      />\n    )\n  }\n}\n\nLinkTrackingButton.containerRequired = false;\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/lib/link-tracking-composer-extension.es6",
    "content": "import {ComposerExtension, RegExpUtils} from 'nylas-exports';\nimport {PLUGIN_ID, PLUGIN_URL} from './link-tracking-constants'\n\nfunction forEachATagInBody(draftBodyRootNode, callback) {\n  const treeWalker = document.createTreeWalker(draftBodyRootNode, NodeFilter.SHOW_ELEMENT, {\n    acceptNode: (node) => {\n      if (node.classList.contains('gmail_quote')) {\n        return NodeFilter.FILTER_REJECT; // skips the entire subtree\n      }\n      return (node.hasAttribute('href')) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;\n    },\n  })\n\n  while (treeWalker.nextNode()) {\n    callback(treeWalker.currentNode);\n  }\n}\n\n/**\n * This replaces all links with a new url that redirects through our\n * cloud-api servers (see cloud-api/routes/link-tracking)\n *\n * This redirect link href is NOT complete at this stage. It requires\n * substantial post processing just before send. This happens in iso-core\n * since sending can happen immediately or later in cloud-workers.\n *\n * See isomorphic-core tracking-utils.es6\n *\n * We don't have a Message Id yet since this is still a draft. We generate\n * and replace `MESSAGE_ID` later with the correct one.\n *\n * We also need to add individualized recipients to each tracking pixel\n * for each message sent to each person.\n *\n * We finally need to put the original url back for the message that ends\n * up in the users's sent folder. This ensures the sender doesn't trip\n * their own link tracks.\n */\nexport default class LinkTrackingComposerExtension extends ComposerExtension {\n  static applyTransformsForSending({draftBodyRootNode, draft}) {\n    const metadata = draft.metadataForPluginId(PLUGIN_ID);\n    if (metadata) {\n      const messageUid = draft.clientId;\n      const links = [];\n\n      forEachATagInBody(draftBodyRootNode, (el) => {\n        const url = el.getAttribute('href');\n        if (!RegExpUtils.urlRegex().test(url)) {\n          return;\n        }\n        const encoded = encodeURIComponent(url);\n        const redirectUrl = `${PLUGIN_URL}/link/MESSAGE_ID/${links.length}?redirect=${encoded}`;\n\n        links.push({\n          url,\n          click_count: 0,\n          click_data: [],\n          redirect_url: redirectUrl,\n        });\n\n        el.setAttribute('href', redirectUrl);\n      });\n\n      // save the link info to draft metadata\n      metadata.uid = messageUid;\n      metadata.links = links;\n      draft.applyPluginMetadata(PLUGIN_ID, metadata);\n    }\n  }\n\n  static unapplyTransformsForSending({draftBodyRootNode}) {\n    forEachATagInBody(draftBodyRootNode, (el) => {\n      const url = el.getAttribute('href');\n      if (url.indexOf(PLUGIN_URL) !== -1) {\n        const userURLEncoded = url.split('?redirect=')[1];\n        el.setAttribute('href', decodeURIComponent(userURLEncoded));\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/lib/link-tracking-constants.es6",
    "content": "import plugin from '../package.json'\n\nexport const PLUGIN_NAME = plugin.title\nexport const PLUGIN_ID = plugin.name;\nexport const PLUGIN_URL = plugin.serverUrl[NylasEnv.config.get(\"env\")];\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/lib/link-tracking-message-extension.jsx",
    "content": "import {React, MessageViewExtension, Actions} from 'nylas-exports'\nimport LinkTrackingMessagePopover from './link-tracking-message-popover'\nimport {PLUGIN_ID} from './link-tracking-constants'\n\nexport default class LinkTrackingMessageExtension extends MessageViewExtension {\n\n  static renderedMessageBodyIntoDocument({document, message, iframe}) {\n    const metadata = message.metadataForPluginId(PLUGIN_ID) || {};\n    if ((metadata.links || []).length === 0) { return }\n\n    const links = {}\n    for (const link of metadata.links) {\n      links[link.url] = link\n      links[link.redirect_url] = link\n    }\n\n    const trackedLinksWalker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {\n      acceptNode: (node) => {\n        if ((node.nodeName === 'A') && links[node.getAttribute('href')]) {\n          return NodeFilter.FILTER_ACCEPT;\n        }\n        return NodeFilter.FILTER_SKIP;\n      },\n    });\n\n    while (trackedLinksWalker.nextNode()) {\n      const node = trackedLinksWalker.currentNode;\n      const nodeHref = node.getAttribute('href');\n      const originalHref = links[nodeHref].url;\n\n      const dotNode = document.createElement('img');\n      dotNode.className = 'link-tracking-dot';\n      dotNode.style = 'margin-bottom: 0.75em; margin-left: 1px; margin-right: 1px; vertical-align: text-bottom; width: 6px;';\n      if (links[nodeHref].click_count > 0) {\n        dotNode.title = `${links[nodeHref].click_count} click${links[nodeHref].click_count === 1 ? \"\" : \"s\"} (${originalHref})`;\n        dotNode.src = 'nylas://link-tracking/assets/ic-tracking-visited@2x.png';\n        dotNode.style = 'margin-bottom: 0.75em; margin-left: 1px; margin-right: 1px; vertical-align: text-bottom; width: 6px; cursor: pointer;'\n        dotNode.onmousedown = () => {\n          const dotRect = dotNode.getBoundingClientRect();\n          const iframeRect = iframe.getBoundingClientRect();\n          const rect = {\n            top: dotRect.top + iframeRect.top,\n            bottom: dotRect.bottom + iframeRect.top,\n            left: dotRect.left + iframeRect.left,\n            right: dotRect.right + iframeRect.left,\n            width: dotRect.width,\n            height: dotRect.height,\n          };\n          Actions.openPopover(\n            <LinkTrackingMessagePopover\n              message={message}\n              linkMetadata={links[nodeHref]}\n            />,\n            {\n              originRect: rect,\n              direction: 'down',\n            }\n          );\n        }\n      } else {\n        dotNode.title = `This link has not been clicked (${originalHref})`;\n        dotNode.src = 'nylas://link-tracking/assets/ic-tracking-unvisited@2x.png';\n      }\n      node.href = originalHref;\n      node.title = originalHref;\n      node.parentNode.insertBefore(dotNode, node.nextSibling);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/lib/link-tracking-message-popover.jsx",
    "content": "import React from 'react';\nimport {DateUtils} from 'nylas-exports';\nimport {Flexbox} from 'nylas-component-kit';\nimport ActivityListStore from '../../activity-list/lib/activity-list-store';\n\n\nclass LinkTrackingMessagePopover extends React.Component {\n  static displayName = 'LinkTrackingMessagePopover';\n\n  static propTypes = {\n    message: React.PropTypes.object,\n    linkMetadata: React.PropTypes.object,\n  };\n\n  renderClickActions() {\n    const clicks = this.props.linkMetadata.click_data;\n    return clicks.map((click) => {\n      const recipients = this.props.message.to.concat(this.props.message.cc, this.props.message.bcc);\n      const recipient = ActivityListStore.getRecipient(click.recipient, recipients);\n      const date = new Date(0);\n      date.setUTCSeconds(click.timestamp);\n      return (\n        <Flexbox key={`${click.timestamp}`} className=\"click-action\">\n          <div className=\"recipient\">\n            {recipient ? recipient.displayName() : \"Someone\"}\n          </div>\n          <div className=\"spacer\" />\n          <div className=\"timestamp\">\n            {DateUtils.shortTimeString(date)}\n          </div>\n        </Flexbox>\n      );\n    });\n  }\n\n  render() {\n    return (\n      <div\n        className=\"link-tracking-message-popover\"\n        tabIndex=\"-1\"\n      >\n        <div className=\"link-tracking-header\">Clicked by:</div>\n        <div className=\"click-history-container\">\n          {this.renderClickActions()}\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default LinkTrackingMessagePopover;\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/lib/main.es6",
    "content": "import {\n  ComponentRegistry,\n  ExtensionRegistry,\n} from 'nylas-exports';\nimport {HasTutorialTip} from 'nylas-component-kit';\n\nimport LinkTrackingButton from './link-tracking-button';\nimport LinkTrackingComposerExtension from './link-tracking-composer-extension';\nimport LinkTrackingMessageExtension from './link-tracking-message-extension';\n\nconst LinkTrackingButtonWithTutorialTip = HasTutorialTip(LinkTrackingButton, {\n  title: \"Track links in this email\",\n  instructions: \"When link tracking is turned on, Nylas Mail will notify you when recipients click links in this email.\",\n});\n\nexport function activate() {\n  ComponentRegistry.register(LinkTrackingButtonWithTutorialTip, {\n    role: 'Composer:ActionButton',\n  });\n\n  ExtensionRegistry.Composer.register(LinkTrackingComposerExtension);\n\n  ExtensionRegistry.MessageView.register(LinkTrackingMessageExtension);\n}\n\nexport function serialize() {}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(LinkTrackingButtonWithTutorialTip);\n  ExtensionRegistry.Composer.unregister(LinkTrackingComposerExtension);\n  ExtensionRegistry.MessageView.unregister(LinkTrackingMessageExtension);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/package.json",
    "content": "{\n  \"name\": \"link-tracking\",\n  \"main\": \"./lib/main\",\n  \"version\": \"0.1.0\",\n  \"serverUrl\": {\n    \"local\": \"https://local-n1.nylas.com\",\n    \"development\": \"https://local-n1.nylas.com\",\n    \"staging\": \"https://n1-staging.nylas.com\",\n    \"production\": \"https://n1.nylas.com\"\n  },\n\n  \"title\": \"Link Tracking\",\n  \"description\": \"Track when links in an email have been clicked by recipients.\",\n  \"icon\": \"./icon.png\",\n  \"isOptional\": true,\n  \"supportedEnvs\": [\"local\", \"development\", \"staging\", \"production\"],\n\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"\"\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  },\n  \"dependencies\": {},\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/spec/link-tracking-composer-extension-spec.es6",
    "content": "import {Message} from 'nylas-exports';\n\nimport LinkTrackingComposerExtension from '../lib/link-tracking-composer-extension'\nimport {PLUGIN_ID, PLUGIN_URL} from '../lib/link-tracking-constants';\n\nconst beforeBody = `TEST_BODY<br>\n<a href=\"www.replaced.com\">test</a>\n<a style=\"color: #aaa\" href=\"http://replaced.com\">asdad</a>\n<a hre=\"www.stillhere.com\">adsasd</a>\n<a stillhere=\"\">stillhere</a>\n<div href=\"stillhere\"></div>\nhttp://www.stillhere.com\n<blockquote class=\"gmail_quote\">twst<a style=\"color: #aaa\" href=\"http://untouched.com\">asdad</a></blockquote>`;\n\nconst afterBodyFactory = (accountId, messageUid) => `TEST_BODY<br>\n<a href=\"${PLUGIN_URL}/link/${accountId}/${messageUid}/0?redirect=www.replaced.com\">test</a>\n<a style=\"color: #aaa\" href=\"${PLUGIN_URL}/link/${accountId}/${messageUid}/1?redirect=http%3A%2F%2Freplaced.com\">asdad</a>\n<a hre=\"www.stillhere.com\">adsasd</a>\n<a stillhere=\"\">stillhere</a>\n<div href=\"stillhere\"></div>\nhttp://www.stillhere.com\n<blockquote class=\"gmail_quote\">twst<a style=\"color: #aaa\" href=\"http://untouched.com\">asdad</a></blockquote>`;\n\nconst nodeForHTML = (html) => {\n  const fragment = document.createDocumentFragment();\n  const node = document.createElement('root');\n  fragment.appendChild(node);\n  node.innerHTML = html;\n  return node;\n}\n\nxdescribe('Link tracking composer extension', function linkTrackingComposerExtension() {\n  describe(\"applyTransformsForSending\", () => {\n    beforeEach(() => {\n      this.draft = new Message({accountId: \"test\"});\n      this.draft.body = beforeBody;\n      this.draftBodyRootNode = nodeForHTML(this.draft.body);\n    });\n\n    it(\"takes no action if there is no metadata\", () => {\n      LinkTrackingComposerExtension.applyTransformsForSending({\n        draftBodyRootNode: this.draftBodyRootNode,\n        draft: this.draft,\n      });\n      const afterBody = this.draftBodyRootNode.innerHTML;\n      expect(afterBody).toEqual(beforeBody);\n    });\n\n    describe(\"With properly formatted metadata and correct params\", () => {\n      beforeEach(() => {\n        this.metadata = {tracked: true};\n        this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);\n      });\n\n      it(\"replaces links in the unquoted portion of the body\", () => {\n        LinkTrackingComposerExtension.applyTransformsForSending({\n          draftBodyRootNode: this.draftBodyRootNode,\n          draft: this.draft,\n        });\n\n        const metadata = this.draft.metadataForPluginId(PLUGIN_ID);\n        const afterBody = this.draftBodyRootNode.innerHTML;\n        expect(afterBody).toEqual(afterBodyFactory(this.draft.accountId, metadata.uid));\n      });\n\n      it(\"sets a uid and list of links on the metadata\", () => {\n        LinkTrackingComposerExtension.applyTransformsForSending({\n          draftBodyRootNode: this.draftBodyRootNode,\n          draft: this.draft,\n        });\n        const metadata = this.draft.metadataForPluginId(PLUGIN_ID);\n        expect(metadata.uid).not.toBeUndefined();\n        expect(metadata.links).not.toBeUndefined();\n        expect(metadata.links.length).toEqual(2);\n\n        for (const link of metadata.links) {\n          expect(link.click_count).toEqual(0);\n        }\n      });\n    });\n  });\n\n  describe(\"unapplyTransformsForSending\", () => {\n    beforeEach(() => {\n      this.metadata = {tracked: true, uid: '123'};\n      this.draft = new Message({accountId: \"test\"});\n      this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);\n    });\n\n    it(\"takes no action if there are no tracked links in the body\", () => {\n      this.draft.body = beforeBody;\n      this.draftBodyRootNode = nodeForHTML(this.draft.body);\n\n      LinkTrackingComposerExtension.unapplyTransformsForSending({\n        draftBodyRootNode: this.draftBodyRootNode,\n        draft: this.draft,\n      });\n      const afterBody = this.draftBodyRootNode.innerHTML;\n      expect(afterBody).toEqual(beforeBody);\n    });\n\n    it(\"replaces tracked links with the original links, restoring the body exactly\", () => {\n      this.draft.body = afterBodyFactory(this.draft.accountId, this.metadata.uid);\n      this.draftBodyRootNode = nodeForHTML(this.draft.body);\n\n      LinkTrackingComposerExtension.unapplyTransformsForSending({\n        draftBodyRootNode: this.draftBodyRootNode,\n        draft: this.draft,\n      });\n      const afterBody = this.draftBodyRootNode.innerHTML;\n      expect(afterBody).toEqual(beforeBody);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/link-tracking/stylesheets/main.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n\n.link-tracking-icon img.content-mask {\n  background-color: #AAA;\n  vertical-align: text-bottom;\n}\n.link-tracking-icon img.content-mask.clicked {\n  background-color: #CCC;\n}\n.link-tracking-icon .link-click-count {\n  display: inline-block;\n  position: relative;\n  left: -16px;\n  text-align: center;\n  \n  color: #3187e1;\n  font-size: 12px;\n  font-weight: bold;\n}\n.link-tracking-icon {\n  width: 16px;\n  margin-right: 4px;\n}\n\n\n.link-tracking-panel {\n  background: #DDF6FF;\n  border: 1px solid #ACD;\n  padding: 5px;\n  border-radius: 5px;\n}\n\n.link-tracking-panel h4{\n  text-align: center;\n  margin-top: 0;\n}\n.link-tracking-panel table{\n  width: 100%;\n}\n.link-tracking-panel td {\n  border-bottom: 1px solid #D5EAF5;\n  border-top: 1px solid #D5EAF5;\n  padding: 0 10px;\n  text-align: left;\n}\n\n.link-tracking-message-popover {\n  width: 200px;\n  max-height: 134px;\n  .link-tracking-header {\n    padding: @padding-base-vertical @padding-base-horizontal 0 @padding-base-horizontal;\n    text-align: center;\n    color: @text-color-subtle;\n    font-weight: 600;\n  }\n  .click-history-container {\n    max-height: 112px;\n    padding: 0 @padding-base-horizontal @padding-base-vertical @padding-base-horizontal;\n    overflow: auto;\n    .click-action {\n      color: @text-color-subtle;\n      .recipient {\n        text-overflow: ellipsis;\n        overflow: hidden;\n      }\n      .spacer {\n        flex: 1 1 0;\n      }\n      .timestamp {\n        color: @text-color-very-subtle;\n        flex-shrink: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/README.md",
    "content": "# composer package\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/lib/calendar-wrapper.jsx",
    "content": "import {\n  Actions,\n  DestroyModelTask,\n  CalendarDataSource,\n} from 'nylas-exports';\nimport {\n  NylasCalendar,\n  KeyCommandsRegion,\n  CalendarEventPopover,\n} from 'nylas-component-kit';\nimport React from 'react';\nimport {remote} from 'electron';\n\n\nexport default class CalendarWrapper extends React.Component {\n  static displayName = 'CalendarWrapper';\n  static containerRequired = false;\n\n  constructor(props) {\n    super(props);\n    this._dataSource = new CalendarDataSource();\n    this.state = {selectedEvents: []};\n  }\n\n  _openEventPopover(eventModel) {\n    const eventEl = document.getElementById(eventModel.id);\n    if (!eventEl) { return; }\n    const eventRect = eventEl.getBoundingClientRect()\n\n    Actions.openPopover(\n      <CalendarEventPopover event={eventModel} />\n    , {\n      originRect: eventRect,\n      direction: 'right',\n      fallbackDirection: 'left',\n    })\n  }\n\n  _onEventClick = (e, event) => {\n    let next = [].concat(this.state.selectedEvents);\n\n    if (e.shiftKey || e.metaKey) {\n      const idx = next.findIndex(({id}) => event.id === id)\n      if (idx === -1) {\n        next.push(event)\n      } else {\n        next.splice(idx, 1)\n      }\n    } else {\n      next = [event];\n    }\n\n    this.setState({\n      selectedEvents: next,\n    });\n  }\n\n  _onEventDoubleClick = (eventModel) => {\n    this._openEventPopover(eventModel)\n  }\n\n  _onEventFocused = (eventModel) => {\n    this._openEventPopover(eventModel)\n  }\n\n  _onDeleteSelectedEvents = () => {\n    if (this.state.selectedEvents.length === 0) {\n      return;\n    }\n    const response = remote.dialog.showMessageBox(remote.getCurrentWindow(), {\n      type: 'warning',\n      buttons: ['Delete', 'Cancel'],\n      message: 'Delete or decline these events?',\n      detail: `Are you sure you want to delete or decline invitations for the selected event(s)?`,\n    });\n    if (response === 0) { // response is button array index\n      for (const event of this.state.selectedEvents) {\n        const task = new DestroyModelTask({\n          clientId: event.clientId,\n          modelName: event.constructor.name,\n          endpoint: '/events',\n          accountId: event.accountId,\n        })\n        Actions.queueTask(task);\n      }\n    }\n  }\n\n  render() {\n    return (\n      <KeyCommandsRegion\n        className=\"main-calendar\"\n        localHandlers={{\n          'core:remove-from-view': this._onDeleteSelectedEvents,\n        }}\n      >\n        <NylasCalendar\n          dataSource={this._dataSource}\n          onEventClick={this._onEventClick}\n          onEventDoubleClick={this._onEventDoubleClick}\n          onEventFocused={this._onEventFocused}\n          selectedEvents={this.state.selectedEvents}\n        />\n      </KeyCommandsRegion>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/lib/event-description-frame.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport {EventedIFrame} from 'nylas-component-kit';\nimport {Utils} from 'nylas-exports';\n\nexport default class EmailFrame extends React.Component {\n  static displayName = 'EmailFrame';\n\n  static propTypes = {\n    content: React.PropTypes.string.isRequired,\n  };\n\n  componentDidMount() {\n    this._mounted = true;\n    this._writeContent();\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state));\n  }\n\n  componentDidUpdate() {\n    this._writeContent();\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n    if (this._unlisten) {\n      this._unlisten();\n    }\n  }\n\n  _writeContent = () => {\n    const iframeNode = ReactDOM.findDOMNode(this.refs.iframe);\n    const doc = iframeNode.contentDocument;\n    if (!doc) { return; }\n    doc.open();\n\n    // NOTE: The iframe must have a modern DOCTYPE. The lack of this line\n    // will cause some bizzare non-standards compliant rendering with the\n    // message bodies. This is particularly felt with <table> elements use\n    // the `border-collapse: collapse` css property while setting a\n    // `padding`.\n    doc.write(\"<!DOCTYPE html>\");\n    doc.write(`<div id='inbox-html-wrapper'>${this.props.content}</div>`);\n    doc.close();\n\n    // autolink(doc, {async: true});\n    // autoscaleImages(doc);\n    // addInlineDownloadPrompts(doc);\n\n    // Notify the EventedIFrame that we've replaced it's document (with `open`)\n    // so it can attach event listeners again.\n    this.refs.iframe.didReplaceDocument();\n    this._onMustRecalculateFrameHeight();\n  }\n\n  _onMustRecalculateFrameHeight = () => {\n    this.refs.iframe.setHeightQuietly(0);\n    this._lastComputedHeight = 0;\n    this._setFrameHeight();\n  }\n\n  _getFrameHeight = (doc) => {\n    let height = 0;\n\n    if (doc && doc.body) {\n      // Why reset the height? body.scrollHeight will always be 0 if the height\n      // of the body is dependent on the iframe height e.g. if height ===\n      // 100% in inline styles or an email stylesheet\n      const style = window.getComputedStyle(doc.body)\n      if (style.height === '0px') {\n        doc.body.style.height = \"auto\"\n      }\n      height = doc.body.scrollHeight;\n    }\n\n    if (doc && doc.documentElement) {\n      height = doc.documentElement.scrollHeight;\n    }\n\n    // scrollHeight does not include space required by scrollbar\n    return height + 25;\n  }\n\n  _setFrameHeight = () => {\n    if (!this._mounted) {\n      return;\n    }\n\n    // Q: What's up with this holder?\n    // A: If you resize the window, or do something to trigger setFrameHeight\n    // on an already-loaded message view, all the heights go to zero for a brief\n    // second while the heights are recomputed. This causes the ScrollRegion to\n    // reset it's scrollTop to ~0 (the new combined heiht of all children).\n    // To prevent this, the holderNode holds the last computed height until\n    // the new height is computed.\n    const holderNode = ReactDOM.findDOMNode(this.refs.iframeHeightHolder);\n    const iframeNode = ReactDOM.findDOMNode(this.refs.iframe);\n    const height = this._getFrameHeight(iframeNode.contentDocument);\n\n    // Why 5px? Some emails have elements with a height of 100%, and then put\n    // tracking pixels beneath that. In these scenarios, the scrollHeight of the\n    // message is always <100% + 1px>, which leads us to resize them constantly.\n    // This is a hack, but I'm not sure of a better solution.\n    if (Math.abs(height - this._lastComputedHeight) > 5) {\n      this.refs.iframe.setHeightQuietly(height);\n      holderNode.style.height = `${height}px`;\n      this._lastComputedHeight = height;\n    }\n\n    if (iframeNode.contentDocument.readyState !== 'complete') {\n      setTimeout(() => this._setFrameHeight(), 0);\n    }\n  }\n\n  render() {\n    return (\n      <div\n        className=\"iframe-container\"\n        ref=\"iframeHeightHolder\"\n        style={{height: this._lastComputedHeight}}\n      >\n        <EventedIFrame\n          ref=\"iframe\"\n          seamless=\"seamless\"\n          searchable\n          onResize={this._onMustRecalculateFrameHeight}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/lib/main.jsx",
    "content": "// import {exec} from 'child_process';\n// import fs from 'fs';\n// import path from 'path';\n// import {WorkspaceStore, ComponentRegistry} from 'nylas-exports';\n// import CalendarWrapper from './calendar-wrapper';\n// import QuickEventButton from './quick-event-button';\n\n//\n// function resolveHelperPath(callback) {\n//   const resourcesPath = NylasEnv.getLoadSettings().resourcePath;\n//   let pathToCalendarApp = path.join(resourcesPath, '..', 'Nylas Calendar.app');\n//\n//   fs.exists(pathToCalendarApp, (exists) => {\n//     if (exists) {\n//       callback(pathToCalendarApp);\n//       return;\n//     }\n//\n//     pathToCalendarApp = path.join(resourcesPath, 'build', 'resources', 'mac', 'Nylas Calendar.app');\n//     fs.exists(pathToCalendarApp, (fallbackExists) => {\n//       if (fallbackExists) {\n//         callback(pathToCalendarApp);\n//         return;\n//       }\n//       callback(null);\n//     });\n//   });\n// }\n\nexport function activate() {\n  return;\n  // WorkspaceStore.defineSheet('Main', {root: true}, {list: ['Center']});\n  //\n  // if (process.platform === 'darwin') {\n  //   resolveHelperPath((helperPath) => {\n  //     if (!helperPath) {\n  //       return;\n  //     }\n  //\n  //     exec(`chmod +x \"${helperPath}/Contents/MacOS/Nylas Calendar\"`, () => {\n  //       exec(`open \"${helperPath}\"`);\n  //     });\n  //\n  //     if (!NylasEnv.config.get('addedToDockCalendar')) {\n  //       exec(`defaults write com.apple.dock persistent-apps -array-add \"<dict><key>tile-data</key><dict><key>file-data</key><dict><key>_CFURLString</key><string>${helperPath}/</string><key>_CFURLStringType</key><integer>0</integer></dict></dict></dict>\"`, () => {\n  //         NylasEnv.config.set('addedToDockCalendar', true);\n  //         exec(`killall Dock`);\n  //       });\n  //     }\n  //   });\n  //\n  //   NylasEnv.onBeforeUnload(() => {\n  //     exec('killall \"Nylas Calendar\"');\n  //     return true;\n  //   });\n  // }\n  //\n  // ComponentRegistry.register(CalendarWrapper, {\n  //   location: WorkspaceStore.Location.Center,\n  // });\n  // ComponentRegistry.register(QuickEventButton, {\n  //   location: WorkspaceStore.Location.Center.Toolbar,\n  // });\n}\n\nexport function deactivate() {\n  return;\n  // ComponentRegistry.unregister(CalendarWrapper);\n  // ComponentRegistry.unregister(QuickEventButton);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/lib/quick-event-button.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport {Actions} from 'nylas-exports';\nimport QuickEventPopover from './quick-event-popover';\n\nexport default class QuickEventButton extends React.Component {\n  static displayName = \"QuickEventButton\";\n\n  onClick = (event) => {\n    event.stopPropagation()\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()\n    Actions.openPopover(\n      <QuickEventPopover />,\n      {originRect: buttonRect, direction: 'down'}\n    )\n  };\n\n  render() {\n    return (\n      <button\n        style={{order: -50}}\n        tabIndex={-1}\n        className=\"btn btn-toolbar\"\n        onClick={this.onClick}\n      >\n      +\n      </button>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/lib/quick-event-popover.jsx",
    "content": "import React from 'react';\nimport {\n  Actions,\n  Calendar,\n  DatabaseStore,\n  DateUtils,\n  Event,\n  SyncbackEventTask,\n} from 'nylas-exports'\n\nexport default class QuickEventPopover extends React.Component {\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      start: null,\n      end: null,\n      leftoverText: null,\n    }\n  }\n\n  onInputKeyDown = (event) => {\n    const {key, target: {value}} = event;\n    if (value.length > 0 && [\"Enter\", \"Return\"].includes(key)) {\n      // This prevents onInputChange from being fired\n      event.stopPropagation();\n      this.createEvent(DateUtils.parseDateString(value));\n      Actions.closePopover();\n    }\n  };\n\n  onInputChange = (event) => {\n    this.setState(DateUtils.parseDateString(event.target.value));\n  };\n\n  createEvent = ({leftoverText, start, end}) => {\n    DatabaseStore.findAll(Calendar).then((allCalendars) => {\n      if (allCalendars.length === 0) {\n        throw new Error(\"Can't create an event, you have no calendars\");\n      }\n      const cals = allCalendars.filter(c => !c.readOnly);\n      if (cals.length === 0) {\n        NylasEnv.showErrorDialog(\"This account has no editable calendars. We can't \" +\n          \"create an event for you. Please make sure you have an editable calendar \" +\n          \"with your account provider.\");\n        return Promise.reject();\n      }\n\n      const event = new Event({\n        calendarId: cals[0].id,\n        accountId: cals[0].accountId,\n        start: start.unix(),\n        end: end.unix(),\n        when: {\n          start_time: start.unix(),\n          end_time: end.unix(),\n        },\n        title: leftoverText,\n      })\n\n      return DatabaseStore.inTransaction((t) => {\n        return t.persistModel(event)\n      }).then(() => {\n        const task = new SyncbackEventTask(event.clientId);\n        Actions.queueTask(task);\n      })\n    })\n  }\n\n\n  render() {\n    let dateInterpretation;\n    if (this.state.start) {\n      dateInterpretation = (\n        <span className=\"date-interpretation\">\n          Title: {this.state.leftoverText} <br />\n          Start: {DateUtils.format(this.state.start, DateUtils.DATE_FORMAT_SHORT)} <br />\n          End: {DateUtils.format(this.state.end, DateUtils.DATE_FORMAT_SHORT)}\n        </span>\n      );\n    }\n\n    return (\n      <div className=\"quick-event-popover nylas-date-input\">\n        <input\n          tabIndex=\"0\"\n          type=\"text\"\n          placeholder=\"Coffee next Monday at 9AM'\"\n          onKeyDown={this.onInputKeyDown}\n          onChange={this.onInputChange}\n        />\n        {dateInterpretation}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/package.json",
    "content": "{\n  \"name\": \"main-calendar\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Nylas Calendar Sidebar\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"scripts\": {\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"calendar\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/main-calendar/stylesheets/main-calendar.less",
    "content": "// The ui-variables file is provided by base themes provided by N1.\n@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.main-calendar {\n  height: 100%;\n\n  .event-grid-legend {\n    border-left: 1px solid @border-color-divider;\n  }\n}\n\n.calendar-event-popover {\n  color: fadeout(@text-color, 20%);\n  background-color: @background-primary;\n  display: flex;\n  flex-direction: column;\n  font-size: @font-size-small;\n  width: 300px;\n\n  .location {\n    color: @text-color-very-subtle;\n    padding: @padding-base-vertical @padding-base-horizontal;\n    word-wrap: break-word;\n  }\n  .title-wrapper {\n    color: @text-color-inverse;\n    display: flex;\n    font-size: @font-size-larger;\n    background-color: @accent-primary;\n    border-top-left-radius: @border-radius-base;\n    border-top-right-radius: @border-radius-base;\n    padding: @padding-base-vertical @padding-base-horizontal;\n  }\n  .edit-icon {\n    background-color: @text-color-inverse;\n    cursor: pointer;\n  }\n  .description .scroll-region-content {\n    max-height:300px;\n    word-wrap: break-word;\n    position: relative;\n  }\n  .label {\n    color: @text-color-very-subtle;\n  }\n  .section {\n    border-top: 1px solid @border-color-divider;\n    padding: @padding-base-vertical @padding-base-horizontal;\n  }\n  .row.time {\n    .time-picker {\n      text-align: center;\n    }\n    .time-picker-wrap {\n      margin-right: 5px;\n\n      .time-options {\n        z-index: 10; // So the time pickers show over\n      }\n    }\n  }\n}\n\n.quick-event-popover {\n  width: 250px;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/lib/autoload-images-actions.es6",
    "content": "import Reflux from 'reflux';\n\nconst ActionNames = [\n  'temporarilyEnableImages',\n  'permanentlyEnableImages',\n];\n\nconst Actions = Reflux.createActions(ActionNames);\nActionNames.forEach((name) => {\n  Actions[name].sync = true;\n});\n\nexport default Actions;\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/lib/autoload-images-extension.es6",
    "content": "import {MessageViewExtension} from 'nylas-exports';\nimport AutoloadImagesStore from './autoload-images-store';\n\nexport default class AutoloadImagesExtension extends MessageViewExtension {\n  static formatMessageBody = ({message}) => {\n    if (AutoloadImagesStore.shouldBlockImagesIn(message)) {\n      message.body = message.body.replace(AutoloadImagesStore.ImagesRegexp, (match, prefix) => {\n        return `${prefix}#`;\n      });\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/lib/autoload-images-header.jsx",
    "content": "import React from 'react';\nimport {Message} from 'nylas-exports';\n\nimport AutoloadImagesStore from './autoload-images-store';\nimport Actions from './autoload-images-actions';\n\nexport default class AutoloadImagesHeader extends React.Component {\n  static displayName = 'AutoloadImagesHeader';\n\n  static propTypes = {\n    message: React.PropTypes.instanceOf(Message).isRequired,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      blocking: AutoloadImagesStore.shouldBlockImagesIn(this.props.message),\n    };\n  }\n\n  componentDidMount() {\n    this._unlisten = AutoloadImagesStore.listen(() => {\n      const blocking = AutoloadImagesStore.shouldBlockImagesIn(this.props.message);\n      if (blocking !== this.state.blocking) {\n        this.setState({blocking});\n      }\n    });\n  }\n\n  componentWillUnmount() {\n    this._unlisten();\n  }\n\n  render() {\n    const {message} = this.props;\n    const {blocking} = this.state;\n\n    if (blocking === false) {\n      return (\n        <div />\n      );\n    }\n\n    return (\n      <div className=\"autoload-images-header\">\n        <a className=\"option\" onClick={() => Actions.temporarilyEnableImages(message)}>\n          Show Images\n        </a>\n        <span style={{paddingLeft: 10, paddingRight: 10}}>|</span>\n        <a className=\"option\" onClick={() => Actions.permanentlyEnableImages(message)}>\n          Always show images from {message.fromContact().toString()}\n        </a>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/lib/autoload-images-store.es6",
    "content": "import NylasStore from 'nylas-store';\nimport fs from 'fs';\nimport path from 'path';\nimport {Utils, MessageBodyProcessor} from 'nylas-exports';\nimport AutoloadImagesActions from './autoload-images-actions';\n\nconst ImagesRegexp = /((?:src|background|placeholder|icon|background|poster|srcset)\\s*=\\s*['\"]?(?=\\w*:\\/\\/)|:\\s*url\\()+([^\"')]*)/gi;\n\nclass AutoloadImagesStore extends NylasStore {\n\n  constructor() {\n    super();\n\n    this.ImagesRegexp = ImagesRegexp;\n\n    this._whitelistEmails = {}\n    this._whitelistMessageIds = {}\n\n    const filename = 'autoload-images-whitelist.txt';\n    this._whitelistEmailsPath = path.join(NylasEnv.getConfigDirPath(), filename);\n\n    this._loadWhitelist();\n\n    this.listenTo(AutoloadImagesActions.temporarilyEnableImages, this._onTemporarilyEnableImages);\n    this.listenTo(AutoloadImagesActions.permanentlyEnableImages, this._onPermanentlyEnableImages);\n\n    NylasEnv.config.onDidChange('core.reading.autoloadImages', () => {\n      MessageBodyProcessor.resetCache();\n    });\n  }\n\n  shouldBlockImagesIn = (message) => {\n    if (NylasEnv.config.get('core.reading.autoloadImages') === true) {\n      return false;\n    }\n    if (this._whitelistEmails[Utils.toEquivalentEmailForm(message.fromContact().email)]) {\n      return false;\n    }\n    if (this._whitelistMessageIds[message.id]) {\n      return false;\n    }\n\n    return ImagesRegexp.test(message.body);\n  }\n\n  _loadWhitelist = () => {\n    fs.exists(this._whitelistEmailsPath, (exists) => {\n      if (!exists) { return; }\n\n      fs.readFile(this._whitelistEmailsPath, (err, body) => {\n        if (err || !body) {\n          console.log(err);\n          return;\n        }\n\n        this._whitelistEmails = {}\n        body.toString().split(/[\\n\\r]+/).forEach((email) => {\n          this._whitelistEmails[Utils.toEquivalentEmailForm(email)] = true;\n        });\n        this.trigger();\n      });\n    });\n  }\n\n  _saveWhitelist = () => {\n    const data = Object.keys(this._whitelistEmails).join('\\n');\n    fs.writeFile(this._whitelistEmailsPath, data, (err) => {\n      if (err) {\n        console.error(`AutoloadImagesStore could not save whitelist: ${err.toString()}`);\n      }\n    });\n  }\n\n  _onTemporarilyEnableImages = (message) => {\n    this._whitelistMessageIds[message.id] = true;\n    MessageBodyProcessor.resetCache();\n    this.trigger();\n  }\n\n  _onPermanentlyEnableImages = (message) => {\n    const email = Utils.toEquivalentEmailForm(message.fromContact().email);\n    this._whitelistEmails[email] = true;\n    MessageBodyProcessor.resetCache();\n    setTimeout(this._saveWhitelist, 1);\n    this.trigger();\n  }\n}\n\nexport default new AutoloadImagesStore();\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/lib/main.es6",
    "content": "import {\n  ComponentRegistry,\n  ExtensionRegistry,\n} from 'nylas-exports';\n\nimport AutoloadImagesExtension from './autoload-images-extension';\nimport AutoloadImagesHeader from './autoload-images-header';\n\n/*\nAll packages must export a basic object that has at least the following 3\nmethods:\n\n1. `activate` - Actions to take once the package gets turned on.\nPre-enabled packages get activated on N1 bootup. They can also be\nactivated manually by a user.\n\n2. `deactivate` - Actions to take when a package gets turned off. This can\nhappen when a user manually disables a package.\n\n3. `serialize` - A simple serializable object that gets saved to disk\nbefore N1 quits. This gets passed back into `activate` next time N1 boots\nup or your package is manually activated.\n*/\nexport function activate() {\n  // Register Message List Actions we provide globally\n  ExtensionRegistry.MessageView.register(AutoloadImagesExtension);\n  ComponentRegistry.register(AutoloadImagesHeader, {\n    role: 'message:BodyHeader',\n  });\n}\n\nexport function serialize() {}\n\nexport function deactivate() {\n  ExtensionRegistry.MessageView.unregister(AutoloadImagesExtension);\n  ComponentRegistry.unregister(AutoloadImagesHeader);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/package.json",
    "content": "{\n  \"name\": \"message-autoload-images\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Option to conditionally load the images in messages\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/autoload-images-extension-spec.es6",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport AutoloadImagesExtension from '../lib/autoload-images-extension';\nimport AutoloadImagesStore from '../lib/autoload-images-store';\n\ndescribe('AutoloadImagesExtension', function autoloadImagesExtension() {\n  describe(\"formatMessageBody\", () => {\n    const scenarios = [];\n    const fixtures = path.resolve(path.join(__dirname, 'fixtures'));\n\n    fs.readdirSync(fixtures).forEach((filename) => {\n      if (filename.endsWith('-in.html')) {\n        const name = filename.replace('-in.html', '');\n\n        scenarios.push({\n          'name': name,\n          'in': fs.readFileSync(path.join(fixtures, filename)).toString(),\n          'out': fs.readFileSync(path.join(fixtures, `${name}-out.html`)).toString(),\n        });\n      }\n    });\n\n    scenarios.forEach((scenario) => {\n      it(`should process ${scenario.name}`, () => {\n        spyOn(AutoloadImagesStore, 'shouldBlockImagesIn').andReturn(true);\n\n        const message = {\n          body: scenario.in,\n        };\n        AutoloadImagesExtension.formatMessageBody({message});\n\n        expect(message.body === scenario.out).toBe(true);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/linkedin-in.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"-webkit-text-size-adjust:none;\">\n <head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\" />\n  <meta name=\"HandheldFriendly\" content=\"true\" />\n  <meta name=\"viewport\" content=\"width=device-width; initial-scale=0.666667; maximum-scale=0.666667; user-scalable=0\" />\n  <meta name=\"viewport\" content=\"width=device-width\" />\n  <title></title>\n  <style type=\"text/css\">@media all and (max-width:590px) { *[class].responsive { width:290px !important; } *[id]#center { width:50%; margin:0 auto; display:table; } *[class].display-none { display:none !important; } *[class].display-block { display:block !important; } *[class].fix-table-content { table-layout:fixed; } *[class].hide-for-mobile { display:none !important; } *[class].show-for-mobile { width:auto !important; max-height:none !important; visibility:visible !important; overflow:visible !important; float:none !important; height:auto !important; display:block !important; } *[class].responsive_header { display:table-cell !important; width:100% !important; color:#0077b5 !important; font-size:12px !important; } *[class].res-font10 { font-size:10px !important; } *[class].res-font12 { font-size:12px !important; } *[class].res-font13 { font-size:13px !important; } *[class].res-font14 { font-size:14px !important; } *[class].res-font16 { font-size:16px !important; } *[class].res-font18 { font-size:18px !important; } *[class].res-font20 { font-size:20px !important; } *[class].res-width10 { width:10px !important; } *[class].res-width20 { width:20px !important; } *[class].res-width25 { width:25px !important; } *[class].res-width120 { width:120px !important; } *[class].res-height0 { height:0px !important; } *[class].res-height10 { height:10px !important; } *[class].res-height20 { height:20px !important; } *[class].res-height30 { height:30px !important; } *[class].res-img40 { width:40px !important; height:40px !important; } *[class].res-img60 { width:60px !important; height:60px !important; } *[class].res-img75 { width:75px !important; height:75px !important; } *[class].res-img100 { width:100px !important; height:100px !important; } *[class].res-img150 { width:150px !important; height:150px !important; } *[class].res-img320 { width:320px !important; height:auto !important; } *[class].hideIMG { display:none; height:0px !important; width:0px !important; } *[class].responsive-spacer { width:10px !important; } *[class].header-spacer { table-layout:auto !important; width:250px !important; } *[class].header-spacer td, *[class].header-spacer div { width:250px !important; } *[class].center-content { text-align:center !important; } *[class].responsive-fullwidth { width:100% !important; } *[class].cellpadding-none, *[class].cellpadding-none table, *[class].cellpadding-none table td { border-collapse:collapse !important; padding:0 !important; } *[class].remove-margin { margin:0 !important; } *[class].remove-border { border:none !important; } table[class].responsive:not(.responsive-footer) { width:100% !important; } *[class].responsive-hidden { display:none !important; max-height:0 !important; font-size:0 !important; overflow:hidden !important; mso-hide:all !important; } *[class].responsive-body { width:100% !important; } *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { margin:0 auto; text-align:0 left; width:268px !important; } *[class].text-link { color:#008CC9; text-decoration:none; } *[class].res-font16 { font-size:16px !important; line-height:20px !important; } @media only screen and (max-width:532px) { *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { width:96% !important; max-width:532px; text-align:left; } } *[class].responsive-promo { margin:0 auto; text-align:0 left; width:100% !important; max-width:532px; text-align:left; } *[class].mobile-hidden { display:none; } } @media all and (-webkit-min-device-pixel-ratio:1.5) { *[id]#base-header-logo { background-image:url(https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_email_197x48_v1.png) !important; background-size:95px; background-repeat:no-repeat; width:95px !important; height:21px !important; } *[id]#base-header-logo img { display:none; } *[id]#base-header-logo a { height:21px !important; } *[id]#base-header-logo-china { background-image:url(https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_china_email_266x42_v1.png) !important; background-size:133px; background-repeat:no-repeat; width:133px !important; height:21px !important; } *[id]#base-header-logo-china img { display:none; } *[id]#base-header-logo-china a { height:21px !important; } } </style>\n </head>\n <body style=\"background-color:#dfdfdf;padding:0;margin:0 auto;width:100%;\">\n  <span style=\"visibility:hidden;color:transparent;display:none !important;mso-hide:all;width:0;font-size:1px;opacity:0;height:0;\"></span>\n  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" bgcolor=\"#dfdfdf\">\n   <tbody>\n    <tr>\n     <td>\n      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\" width=\"1\" bgcolor=\"#dfdfdf\">\n       <tbody>\n        <tr>\n         <td>\n          <div style=\"height:5px;font-size:5px;line-height:5px;\">\n           &nbsp;\n          </div></td>\n        </tr>\n       </tbody>\n      </table></td>\n    </tr>\n    <tr>\n     <td>\n      <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" align=\"center\" width=\"100%\" style=\"table-layout:fixed;font-family:Helvetica,Arial,sans-serif;\">\n       <tbody>\n        <tr>\n         <td align=\"center\">\n          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;min-width:290px;\" width=\"100%\" class=\"responsive\">\n           <tbody>\n            <tr>\n             <td align=\"left\">\n              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-body\">\n               <tbody>\n                <tr>\n                 <td align=\"center\">\n                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-logo\">\n                   <tbody>\n                    <tr>\n                     <td align=\"left\">\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:7px;font-size:7px;line-height:7px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\" class=\"responsive-logo\" bgcolor=\"#DFDFDF\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                           <tbody>\n                            <tr>\n                             <td>\n                              <div style=\"height:8px;font-size:8px;line-height:8px\">\n                               &nbsp;\n                              </div></td>\n                            </tr>\n                           </tbody>\n                          </table></td>\n                        </tr>\n                        <tr>\n                         <td valign=\"middle\" align=\"left\" height=\"35\" width=\"260\">\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-logo\">\n                           <tbody>\n                            <tr>\n                             <td align=\"left\" valign=\"middle\" width=\"95\" height=\"21\" id=\"base-header-logo\"><a style=\"text-decoration:none;cursor:pointer;border:none;display:block;height:21px;width:100%;\" href=\"https://www.linkedin.com/comm/nhome/?midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-null-10-null&amp;trkEmail=eml-b2_content_ecosystem_digest-null-10-null-null-6m81ta%7Eihd6z5md%7E7\"><img src=\"https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_email_95x21_v1.png\" width=\"95\" height=\"21\" alt=\"LinkedIn\" style=\"border:none;text-decoration:none;\" /></a></td>\n                            </tr>\n                           </tbody>\n                          </table></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"responsive-logo\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:8px;font-size:8px;line-height:8px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:12px;font-size:12px;line-height:12px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n               </tbody>\n              </table>\n              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" bgcolor=\"#ffffff\" class=\"responsive-body\" align=\"center\">\n               <tbody>\n                <tr>\n                 <td align=\"center\">\n                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n                   <tbody>\n                    <tr>\n                     <td align=\"center\">\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"1\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:25px;font-size:25px;line-height:25px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\" align=\"center\">\n                       <tbody>\n                        <tr>\n                         <td align=\"center\">\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\" data-qa=\"section_votd\">\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"left\">\n                                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n                                   <tbody>\n                                    <tr>\n                                     <td align=\"left\">\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=11&amp;m=hero&amp;urlhash=EPM7&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Ftoday%2Fauthor%2F83392170\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Jake Anderson, Co-Founder at FertilityIQ</a></td>\n                                        </tr>\n                                        <tr>\n                                         <td colspan=\"3\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=12&amp;m=hero&amp;urlhash=Os_Q&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Fpulse%2Fpaternity-leave-its-harder-than-ever-mark-zuckerberg-you-anderson\" title=\"Paternity Leave Or Not, It’s Harder Than Ever For Zuckerberg (And You) To Be A Good Father\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Paternity Leave Or Not, It’s Harder Than Ever For Zuckerberg (And You) To Be A Good Father</a></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n                                        </tr>\n                                        <tr>\n                                         <td data-qa=\"_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=13&amp;m=hero&amp;urlhash=Os_Q&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Fpulse%2Fpaternity-leave-its-harder-than-ever-mark-zuckerberg-you-anderson\" style=\"color:#868686;font-weight:100;text-decoration:none;\"></a></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:20px;font-size:20px;line-height:20px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_summary_link\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=14&amp;m=hero&amp;urlhash=Os_Q&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Fpulse%2Fpaternity-leave-its-harder-than-ever-mark-zuckerberg-you-anderson\" title=\"Paternity Leave Or Not, It’s Harder Than Ever For Zuckerberg (And You) To Be A Good Father\"><img src=\"https://media.licdn.com/media/AAEAAQAAAAAAAAYvAAAAJDQzOThlNzFhLTYwM2YtNDZjZC1iZGE5LWZlZTgxNTI2NDgwYg.png\" alt=\"Highlight of the day\" style=\"max-width:100%;border-width:0;\" /></a></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:20px;font-size:20px;line-height:20px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table>\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\" data-qa=\"section_promo\">\n                              <!-- Deprecated i18n Tags -->\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-promo\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse-fe-storeLink&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=15&amp;m=footer_promo&amp;ts=eml-ced-appstore-body\" target=\"_blank\" style=\"margin:0;color:#262626;font-weight:100;text-decoration:none;font-size:16px;text-align:center;\"><p style=\"margin:0;color:#262626;font-weight:100;text-decoration:none;font-size:16px;text-align:center;\">Download LinkedIn Pulse to get more top stories of the day</p></a></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:15px;font-size:15px;line-height:15px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table>\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-promo\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"right\" width=\"45%\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse-fe-storeLink&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=15&amp;m=footer_promo&amp;ts=eml-ced-appstore-body\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/images/email/campaigns/pulse_ced/apple_appstore_v1.png\" height=\"90%\" alt=\"Get the App\" style=\"outline: none; border: none;\" /></a></td>\n                                 <td width=\"1%\">\n                                  <table width=\"8\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                 <td align=\"left\" width=\"42%\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse-fe-storeLink&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=15&amp;m=footer_promo&amp;ts=eml-ced-appstore-body\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/images/email/campaigns/pulse_ced/google_appstore_v1.png\" height=\"90%\" alt=\"Get the App\" style=\"outline: none; border: none;\" /></a></td>\n                                </tr>\n                               </tbody>\n                              </table>\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-promo\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td width=\"520\" bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table>\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\">\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"left\">\n                                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n                                   <tbody>\n                                    <tr>\n                                     <td align=\"left\">\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" align=\"center\">\n                                           <tbody>\n                                            <tr>\n                                             <td align=\"left\" data-qa=\"section_network_publishes\">\n                                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                                               <tbody>\n                                                <tr>\n                                                 <td valign=\"middle\" align=\"left\" data-qa=\"network_publishes_section_header\" style=\"margin-left:0;color:#666666;font-weight:100;text-decoration:none;font-size:16px;line-height:20px;text-align:left;\">Published by your network</td>\n                                                </tr>\n                                                <tr>\n                                                 <td>\n                                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                                   <tbody>\n                                                    <tr>\n                                                     <td>\n                                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                                       &nbsp;\n                                                      </div></td>\n                                                    </tr>\n                                                   </tbody>\n                                                  </table></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/5/005/030/230/060551c.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"network_publishes_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAACl_IUB62sJErqek1a4XmvD4rSXrL20rQM&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-network_publishes-33-null&amp;trkEmail=eml-b2_content_ecosystem_digest-network_publishes-33-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Richard A. Moran</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"network_publishes_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=31&amp;m=network_publishes&amp;permLink=steve-jobs-again-still-richard-a-moran\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Steve Jobs - Again or Still? </a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"network_publishes_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=32&amp;m=network_publishes&amp;permLink=steve-jobs-again-still-richard-a-moran\" style=\"color:#868686;font-weight:100;text-decoration:none;\">Hard to believe, but Steve Jobs has been gone for four years.&nbsp; But he's really not gone.&nbsp; His influence on leadership, technology, innovation and jus</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/1/000/1c8/0ad/1ad0d0d.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"network_publishes_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAtR9cBIf5DoWAFDE3YQ2jjApX2h4Refjs&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-network_publishes-39-null&amp;trkEmail=eml-b2_content_ecosystem_digest-network_publishes-39-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Ryan Holmes</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"network_publishes_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=37&amp;m=network_publishes&amp;permLink=3-ways-tap-hottest-marketing-tactic-right-now-ryan-holmes\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">3 Ways to Tap Into the Hottest Marketing Tactic Right Now </a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"network_publishes_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=38&amp;m=network_publishes&amp;permLink=3-ways-tap-hottest-marketing-tactic-right-now-ryan-holmes\" style=\"color:#868686;font-weight:100;text-decoration:none;\">Southern Biscuits and Gravy is one of the more interesting potato chip flavors to hit American snack aisles recently. Last month, Lay’s announced tha</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                    <tr>\n                                     <td>\n                                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                       <tbody>\n                                        <tr>\n                                         <td>\n                                          <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                           &nbsp;\n                                          </div></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table>\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"left\">\n                                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n                                   <tbody>\n                                    <tr>\n                                     <td align=\"left\">\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" align=\"center\">\n                                           <tbody>\n                                            <tr>\n                                             <td align=\"left\" data-qa=\"section_recommended_articles\">\n                                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                                               <tbody>\n                                                <tr>\n                                                 <td valign=\"middle\" align=\"left\" data-qa=\"recommended_articles_section_header\" style=\"margin-left:0;color:#666666;font-weight:100;text-decoration:none;font-size:16px;line-height:20px;text-align:left;\">Recommended for you</td>\n                                                </tr>\n                                                <tr>\n                                                 <td>\n                                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                                   <tbody>\n                                                    <tr>\n                                                     <td>\n                                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                                       &nbsp;\n                                                      </div></td>\n                                                    </tr>\n                                                   </tbody>\n                                                  </table></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/6/000/1cd/2c8/0b00d8a.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAExb3oBO9pn_apeWKktkimPWp12DcbRGQs&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-75-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-75-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Jeff Haden</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=73&amp;m=recommended_articles&amp;permLink=why-doesnt-anyone-ever-feel-rich-even-happy-jeff-haden\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Why Doesn't Anyone Ever Feel Rich? (Or Even Happy?)</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=74&amp;m=recommended_articles&amp;permLink=why-doesnt-anyone-ever-feel-rich-even-happy-jeff-haden\" style=\"color:#868686;font-weight:100;text-decoration:none;\">One day I'd like to meet someone who is actually rich. Sometimes I think I've found one but it always turns out I'm wrong. No matter how rich I assume</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/8/005/093/3ae/01eefce.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAMDxhcBDYxYriW1LuClTZtD2HPi26f38E4&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-81-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-81-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Dr. Travis Bradberry</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=79&amp;m=recommended_articles&amp;permLink=why-smart-people-act-so-stupid-dr-travis-bradberry\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Why Smart People Act So Stupid</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=80&amp;m=recommended_articles&amp;permLink=why-smart-people-act-so-stupid-dr-travis-bradberry\" style=\"color:#868686;font-weight:100;text-decoration:none;\">It’s good to be smart. After all, intelligent people earn more money, accumulate more wealth, and even live longer. On the surface, being smart looks </a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/AAEAAQAAAAAAAAP_AAAAJDAzZTEwODg4LTQ1MjAtNDQzMC1iNGI5LTM4ZDFjOTg2MWY5ZA.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAGF3QQBt058ke-fjSJf3Lv1mQVTQNvy8n4&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-87-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-87-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Katie Carroll</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=85&amp;m=recommended_articles&amp;permLink=daily-pulse-china-responds-isis-biggest-pharma-deal-ever-carroll\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Daily Pulse: BREAKING: Paris Attacks Leader Killed, China Responds to...</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=86&amp;m=recommended_articles&amp;permLink=daily-pulse-china-responds-isis-biggest-pharma-deal-ever-carroll\" style=\"color:#868686;font-weight:100;text-decoration:none;\">BREAKING:&nbsp;Abdelhamid Abaooud, the purported leader of the Paris attacks,&nbsp;was confirmed dead by French authorities. His body was identified using papil</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/8/005/093/3ae/01eefce.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAMDxhcBDYxYriW1LuClTZtD2HPi26f38E4&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-93-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-93-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Dr. Travis Bradberry</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=91&amp;m=recommended_articles&amp;permLink=how-make-yourself-work-when-youre-mood-dr-travis-bradberry\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">How To Make Yourself Work When You're Not In The Mood</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=92&amp;m=recommended_articles&amp;permLink=how-make-yourself-work-when-youre-mood-dr-travis-bradberry\" style=\"color:#868686;font-weight:100;text-decoration:none;\">Procrastination affects everyone. It sneaks up on most people when they’re tired or bored, but for some, procrastination can be a full-fledged addicti</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/8/000/285/228/33907b3.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAAzXIBrf8iPyriw5pG3z8vxMDDp-pdANE&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-99-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-99-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Liz Ryan</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=97&amp;m=recommended_articles&amp;permLink=real-reason-you-didnt-get-job-liz-ryan\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">The Real Reason You Didn't Get That Job</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=98&amp;m=recommended_articles&amp;permLink=real-reason-you-didnt-get-job-liz-ryan\" style=\"color:#868686;font-weight:100;text-decoration:none;\">At Human Workplace we teach&nbsp;our clients a five-step protocol to follow after every job interview. The first step is to go home (or back to the office,</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                    <tr>\n                                     <td>\n                                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                       <tbody>\n                                        <tr>\n                                         <td>\n                                          <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                           &nbsp;\n                                          </div></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table>\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\" align=\"center\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\" data-qa=\"section_start_writing\" class=\"mobile-hidden\">\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"center\"></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:5px;font-size:5px;line-height:5px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td align=\"center\" style=\"color:#666666;font-weight:100;font-size:18px;line-height:20px;text-align:center;\">Have your own perspective to share?</td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:20px;font-size:20px;line-height:20px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td align=\"center\" width=\"100%\" height=\"40\" bgcolor=\"#008CC9\" class=\"res-font16\" style=\"font-weight:200;width:100%;font-size:20px;text-align:center;\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_create_article&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=102&amp;m=footer_promo&amp;ts=pub_upsell\" target=\"_blank\" style=\"color:#FFFFFF;text-decoration:none;\">Start writing on LinkedIn</a></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:40px;font-size:40px;line-height:40px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n               </tbody>\n              </table></td>\n            </tr>\n            <tr>\n             <td></td>\n            </tr>\n            <tr>\n             <td align=\"left\">\n              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive\">\n               <tbody>\n                <tr>\n                 <td>\n                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                   <tbody>\n                    <tr>\n                     <td>\n                      <div style=\"height:10px;font-size:10px;line-height:10px\">\n                       &nbsp;\n                      </div></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n                <tr>\n                 <td align=\"left\">\n                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"color:#999999;font-size:11px;font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                   <tbody>\n                    <tr>\n                     <td>You are receiving notification emails from LinkedIn. <a style=\"color:#0077B5;text-decoration:none;\" href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;t=lun&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;loid=AQFuT4vuXECaYwAAAVE53xL5xFgXxKXqjevO9odGDbZLvLwW0KFPs2TIv9hn3SeTRG7xg9zK6p6NfgqsH56X&amp;eid=6m81ta-ihd6z5md-7\">Unsubscribe</a></td>\n                    </tr>\n                    <tr>\n                     <td></td>\n                    </tr>\n                    <tr>\n                     <td>This email was intended for Benjamin Hartester (Software Developer). <a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=customerServiceUrl&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;articleId=4788\" style=\"color:#2e8dd7;text-decoration:none;\">Learn why we included this.</a></td>\n                    </tr>\n                    <tr>\n                     <td>If you need assistance or have questions, please contact <a target=\"_blank\" href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=customerServiceUrl&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest\" style=\"color:#2e8dd7;text-decoration:none;\">LinkedIn Customer Service</a>.</td>\n                    </tr>\n                    <tr>\n                     <td>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:10px;font-size:10px;line-height:10px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                    <tr>\n                     <td>&copy; 2015 LinkedIn Corporation, 2029 Stierlin Court, Mountain View CA 94043. LinkedIn and the LinkedIn logo are registered trademarks of LinkedIn.</td>\n                    </tr>\n                    <tr>\n                     <td>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:10px;font-size:10px;line-height:10px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                    <tr>\n                     <td></td>\n                    </tr>\n                    <tr>\n                     <td>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:10px;font-size:10px;line-height:10px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n                <tr>\n                 <td>\n                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                   <tbody>\n                    <tr>\n                     <td>\n                      <div style=\"height:20px;font-size:20px;line-height:20px\">\n                       &nbsp;\n                      </div></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n               </tbody>\n              </table></td>\n            </tr>\n           </tbody>\n          </table></td>\n        </tr>\n       </tbody>\n      </table></td>\n    </tr>\n    <tr>\n     <td>\n      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n       <tbody>\n        <tr>\n         <td>\n          <div style=\"height:20px;font-size:20px;line-height:20px\">\n           &nbsp;\n          </div></td>\n        </tr>\n       </tbody>\n      </table></td>\n    </tr>\n   </tbody>\n  </table>\n  <img src=\"http://www.linkedin.com/emimp/6m81ta-ihd6z5md-7.gif\" style=\"width:1px; height:1px;\" />\n </body>\n</html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/linkedin-out.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\" style=\"-webkit-text-size-adjust:none;\">\n <head>\n  <meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\" />\n  <meta name=\"HandheldFriendly\" content=\"true\" />\n  <meta name=\"viewport\" content=\"width=device-width; initial-scale=0.666667; maximum-scale=0.666667; user-scalable=0\" />\n  <meta name=\"viewport\" content=\"width=device-width\" />\n  <title></title>\n  <style type=\"text/css\">@media all and (max-width:590px) { *[class].responsive { width:290px !important; } *[id]#center { width:50%; margin:0 auto; display:table; } *[class].display-none { display:none !important; } *[class].display-block { display:block !important; } *[class].fix-table-content { table-layout:fixed; } *[class].hide-for-mobile { display:none !important; } *[class].show-for-mobile { width:auto !important; max-height:none !important; visibility:visible !important; overflow:visible !important; float:none !important; height:auto !important; display:block !important; } *[class].responsive_header { display:table-cell !important; width:100% !important; color:#0077b5 !important; font-size:12px !important; } *[class].res-font10 { font-size:10px !important; } *[class].res-font12 { font-size:12px !important; } *[class].res-font13 { font-size:13px !important; } *[class].res-font14 { font-size:14px !important; } *[class].res-font16 { font-size:16px !important; } *[class].res-font18 { font-size:18px !important; } *[class].res-font20 { font-size:20px !important; } *[class].res-width10 { width:10px !important; } *[class].res-width20 { width:20px !important; } *[class].res-width25 { width:25px !important; } *[class].res-width120 { width:120px !important; } *[class].res-height0 { height:0px !important; } *[class].res-height10 { height:10px !important; } *[class].res-height20 { height:20px !important; } *[class].res-height30 { height:30px !important; } *[class].res-img40 { width:40px !important; height:40px !important; } *[class].res-img60 { width:60px !important; height:60px !important; } *[class].res-img75 { width:75px !important; height:75px !important; } *[class].res-img100 { width:100px !important; height:100px !important; } *[class].res-img150 { width:150px !important; height:150px !important; } *[class].res-img320 { width:320px !important; height:auto !important; } *[class].hideIMG { display:none; height:0px !important; width:0px !important; } *[class].responsive-spacer { width:10px !important; } *[class].header-spacer { table-layout:auto !important; width:250px !important; } *[class].header-spacer td, *[class].header-spacer div { width:250px !important; } *[class].center-content { text-align:center !important; } *[class].responsive-fullwidth { width:100% !important; } *[class].cellpadding-none, *[class].cellpadding-none table, *[class].cellpadding-none table td { border-collapse:collapse !important; padding:0 !important; } *[class].remove-margin { margin:0 !important; } *[class].remove-border { border:none !important; } table[class].responsive:not(.responsive-footer) { width:100% !important; } *[class].responsive-hidden { display:none !important; max-height:0 !important; font-size:0 !important; overflow:hidden !important; mso-hide:all !important; } *[class].responsive-body { width:100% !important; } *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { margin:0 auto; text-align:0 left; width:268px !important; } *[class].text-link { color:#008CC9; text-decoration:none; } *[class].res-font16 { font-size:16px !important; line-height:20px !important; } @media only screen and (max-width:532px) { *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { width:96% !important; max-width:532px; text-align:left; } } *[class].responsive-promo { margin:0 auto; text-align:0 left; width:100% !important; max-width:532px; text-align:left; } *[class].mobile-hidden { display:none; } } @media all and (-webkit-min-device-pixel-ratio:1.5) { *[id]#base-header-logo { background-image:url(#) !important; background-size:95px; background-repeat:no-repeat; width:95px !important; height:21px !important; } *[id]#base-header-logo img { display:none; } *[id]#base-header-logo a { height:21px !important; } *[id]#base-header-logo-china { background-image:url(#) !important; background-size:133px; background-repeat:no-repeat; width:133px !important; height:21px !important; } *[id]#base-header-logo-china img { display:none; } *[id]#base-header-logo-china a { height:21px !important; } } </style>\n </head>\n <body style=\"background-color:#dfdfdf;padding:0;margin:0 auto;width:100%;\">\n  <span style=\"visibility:hidden;color:transparent;display:none !important;mso-hide:all;width:0;font-size:1px;opacity:0;height:0;\"></span>\n  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" bgcolor=\"#dfdfdf\">\n   <tbody>\n    <tr>\n     <td>\n      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\" width=\"1\" bgcolor=\"#dfdfdf\">\n       <tbody>\n        <tr>\n         <td>\n          <div style=\"height:5px;font-size:5px;line-height:5px;\">\n           &nbsp;\n          </div></td>\n        </tr>\n       </tbody>\n      </table></td>\n    </tr>\n    <tr>\n     <td>\n      <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" align=\"center\" width=\"100%\" style=\"table-layout:fixed;font-family:Helvetica,Arial,sans-serif;\">\n       <tbody>\n        <tr>\n         <td align=\"center\">\n          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;min-width:290px;\" width=\"100%\" class=\"responsive\">\n           <tbody>\n            <tr>\n             <td align=\"left\">\n              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-body\">\n               <tbody>\n                <tr>\n                 <td align=\"center\">\n                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-logo\">\n                   <tbody>\n                    <tr>\n                     <td align=\"left\">\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:7px;font-size:7px;line-height:7px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\" class=\"responsive-logo\" bgcolor=\"#DFDFDF\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                           <tbody>\n                            <tr>\n                             <td>\n                              <div style=\"height:8px;font-size:8px;line-height:8px\">\n                               &nbsp;\n                              </div></td>\n                            </tr>\n                           </tbody>\n                          </table></td>\n                        </tr>\n                        <tr>\n                         <td valign=\"middle\" align=\"left\" height=\"35\" width=\"260\">\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-logo\">\n                           <tbody>\n                            <tr>\n                             <td align=\"left\" valign=\"middle\" width=\"95\" height=\"21\" id=\"base-header-logo\"><a style=\"text-decoration:none;cursor:pointer;border:none;display:block;height:21px;width:100%;\" href=\"https://www.linkedin.com/comm/nhome/?midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-null-10-null&amp;trkEmail=eml-b2_content_ecosystem_digest-null-10-null-null-6m81ta%7Eihd6z5md%7E7\"><img src=\"#\" width=\"95\" height=\"21\" alt=\"LinkedIn\" style=\"border:none;text-decoration:none;\" /></a></td>\n                            </tr>\n                           </tbody>\n                          </table></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"responsive-logo\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:8px;font-size:8px;line-height:8px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:12px;font-size:12px;line-height:12px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n               </tbody>\n              </table>\n              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" bgcolor=\"#ffffff\" class=\"responsive-body\" align=\"center\">\n               <tbody>\n                <tr>\n                 <td align=\"center\">\n                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n                   <tbody>\n                    <tr>\n                     <td align=\"center\">\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"1\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:25px;font-size:25px;line-height:25px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table>\n                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\" align=\"center\">\n                       <tbody>\n                        <tr>\n                         <td align=\"center\">\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\" data-qa=\"section_votd\">\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"left\">\n                                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n                                   <tbody>\n                                    <tr>\n                                     <td align=\"left\">\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=11&amp;m=hero&amp;urlhash=EPM7&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Ftoday%2Fauthor%2F83392170\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Jake Anderson, Co-Founder at FertilityIQ</a></td>\n                                        </tr>\n                                        <tr>\n                                         <td colspan=\"3\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=12&amp;m=hero&amp;urlhash=Os_Q&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Fpulse%2Fpaternity-leave-its-harder-than-ever-mark-zuckerberg-you-anderson\" title=\"Paternity Leave Or Not, It’s Harder Than Ever For Zuckerberg (And You) To Be A Good Father\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Paternity Leave Or Not, It’s Harder Than Ever For Zuckerberg (And You) To Be A Good Father</a></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n                                        </tr>\n                                        <tr>\n                                         <td data-qa=\"_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=13&amp;m=hero&amp;urlhash=Os_Q&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Fpulse%2Fpaternity-leave-its-harder-than-ever-mark-zuckerberg-you-anderson\" style=\"color:#868686;font-weight:100;text-decoration:none;\"></a></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:20px;font-size:20px;line-height:20px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\" data-qa=\"votd_summary_link\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;t=plh&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=14&amp;m=hero&amp;urlhash=Os_Q&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Fpulse%2Fpaternity-leave-its-harder-than-ever-mark-zuckerberg-you-anderson\" title=\"Paternity Leave Or Not, It’s Harder Than Ever For Zuckerberg (And You) To Be A Good Father\"><img src=\"#\" alt=\"Highlight of the day\" style=\"max-width:100%;border-width:0;\" /></a></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:20px;font-size:20px;line-height:20px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table>\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\" data-qa=\"section_promo\">\n                              <!-- Deprecated i18n Tags -->\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-promo\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse-fe-storeLink&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=15&amp;m=footer_promo&amp;ts=eml-ced-appstore-body\" target=\"_blank\" style=\"margin:0;color:#262626;font-weight:100;text-decoration:none;font-size:16px;text-align:center;\"><p style=\"margin:0;color:#262626;font-weight:100;text-decoration:none;font-size:16px;text-align:center;\">Download LinkedIn Pulse to get more top stories of the day</p></a></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:15px;font-size:15px;line-height:15px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table>\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-promo\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"right\" width=\"45%\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse-fe-storeLink&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=15&amp;m=footer_promo&amp;ts=eml-ced-appstore-body\" target=\"_blank\"><img src=\"#\" height=\"90%\" alt=\"Get the App\" style=\"outline: none; border: none;\" /></a></td>\n                                 <td width=\"1%\">\n                                  <table width=\"8\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                 <td align=\"left\" width=\"42%\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse-fe-storeLink&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=15&amp;m=footer_promo&amp;ts=eml-ced-appstore-body\" target=\"_blank\"><img src=\"#\" height=\"90%\" alt=\"Get the App\" style=\"outline: none; border: none;\" /></a></td>\n                                </tr>\n                               </tbody>\n                              </table>\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-promo\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td width=\"520\" bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table>\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\">\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"left\">\n                                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n                                   <tbody>\n                                    <tr>\n                                     <td align=\"left\">\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" align=\"center\">\n                                           <tbody>\n                                            <tr>\n                                             <td align=\"left\" data-qa=\"section_network_publishes\">\n                                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                                               <tbody>\n                                                <tr>\n                                                 <td valign=\"middle\" align=\"left\" data-qa=\"network_publishes_section_header\" style=\"margin-left:0;color:#666666;font-weight:100;text-decoration:none;font-size:16px;line-height:20px;text-align:left;\">Published by your network</td>\n                                                </tr>\n                                                <tr>\n                                                 <td>\n                                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                                   <tbody>\n                                                    <tr>\n                                                     <td>\n                                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                                       &nbsp;\n                                                      </div></td>\n                                                    </tr>\n                                                   </tbody>\n                                                  </table></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"#\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"network_publishes_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAACl_IUB62sJErqek1a4XmvD4rSXrL20rQM&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-network_publishes-33-null&amp;trkEmail=eml-b2_content_ecosystem_digest-network_publishes-33-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Richard A. Moran</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"network_publishes_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=31&amp;m=network_publishes&amp;permLink=steve-jobs-again-still-richard-a-moran\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Steve Jobs - Again or Still? </a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"network_publishes_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=32&amp;m=network_publishes&amp;permLink=steve-jobs-again-still-richard-a-moran\" style=\"color:#868686;font-weight:100;text-decoration:none;\">Hard to believe, but Steve Jobs has been gone for four years.&nbsp; But he's really not gone.&nbsp; His influence on leadership, technology, innovation and jus</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"#\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"network_publishes_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAtR9cBIf5DoWAFDE3YQ2jjApX2h4Refjs&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-network_publishes-39-null&amp;trkEmail=eml-b2_content_ecosystem_digest-network_publishes-39-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Ryan Holmes</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"network_publishes_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=37&amp;m=network_publishes&amp;permLink=3-ways-tap-hottest-marketing-tactic-right-now-ryan-holmes\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">3 Ways to Tap Into the Hottest Marketing Tactic Right Now </a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"network_publishes_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=38&amp;m=network_publishes&amp;permLink=3-ways-tap-hottest-marketing-tactic-right-now-ryan-holmes\" style=\"color:#868686;font-weight:100;text-decoration:none;\">Southern Biscuits and Gravy is one of the more interesting potato chip flavors to hit American snack aisles recently. Last month, Lay’s announced tha</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                    <tr>\n                                     <td>\n                                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                       <tbody>\n                                        <tr>\n                                         <td>\n                                          <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                           &nbsp;\n                                          </div></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table>\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"left\">\n                                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n                                   <tbody>\n                                    <tr>\n                                     <td align=\"left\">\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" align=\"center\">\n                                           <tbody>\n                                            <tr>\n                                             <td align=\"left\" data-qa=\"section_recommended_articles\">\n                                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                                               <tbody>\n                                                <tr>\n                                                 <td valign=\"middle\" align=\"left\" data-qa=\"recommended_articles_section_header\" style=\"margin-left:0;color:#666666;font-weight:100;text-decoration:none;font-size:16px;line-height:20px;text-align:left;\">Recommended for you</td>\n                                                </tr>\n                                                <tr>\n                                                 <td>\n                                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                                   <tbody>\n                                                    <tr>\n                                                     <td>\n                                                      <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                                       &nbsp;\n                                                      </div></td>\n                                                    </tr>\n                                                   </tbody>\n                                                  </table></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table>\n                                      <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                       <tbody>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"#\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAExb3oBO9pn_apeWKktkimPWp12DcbRGQs&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-75-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-75-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Jeff Haden</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=73&amp;m=recommended_articles&amp;permLink=why-doesnt-anyone-ever-feel-rich-even-happy-jeff-haden\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Why Doesn't Anyone Ever Feel Rich? (Or Even Happy?)</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=74&amp;m=recommended_articles&amp;permLink=why-doesnt-anyone-ever-feel-rich-even-happy-jeff-haden\" style=\"color:#868686;font-weight:100;text-decoration:none;\">One day I'd like to meet someone who is actually rich. Sometimes I think I've found one but it always turns out I'm wrong. No matter how rich I assume</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"#\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAMDxhcBDYxYriW1LuClTZtD2HPi26f38E4&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-81-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-81-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Dr. Travis Bradberry</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=79&amp;m=recommended_articles&amp;permLink=why-smart-people-act-so-stupid-dr-travis-bradberry\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Why Smart People Act So Stupid</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=80&amp;m=recommended_articles&amp;permLink=why-smart-people-act-so-stupid-dr-travis-bradberry\" style=\"color:#868686;font-weight:100;text-decoration:none;\">It’s good to be smart. After all, intelligent people earn more money, accumulate more wealth, and even live longer. On the surface, being smart looks </a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"#\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAGF3QQBt058ke-fjSJf3Lv1mQVTQNvy8n4&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-87-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-87-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Katie Carroll</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=85&amp;m=recommended_articles&amp;permLink=daily-pulse-china-responds-isis-biggest-pharma-deal-ever-carroll\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">Daily Pulse: BREAKING: Paris Attacks Leader Killed, China Responds to...</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=86&amp;m=recommended_articles&amp;permLink=daily-pulse-china-responds-isis-biggest-pharma-deal-ever-carroll\" style=\"color:#868686;font-weight:100;text-decoration:none;\">BREAKING:&nbsp;Abdelhamid Abaooud, the purported leader of the Paris attacks,&nbsp;was confirmed dead by French authorities. His body was identified using papil</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"#\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAMDxhcBDYxYriW1LuClTZtD2HPi26f38E4&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-93-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-93-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Dr. Travis Bradberry</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=91&amp;m=recommended_articles&amp;permLink=how-make-yourself-work-when-youre-mood-dr-travis-bradberry\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">How To Make Yourself Work When You're Not In The Mood</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=92&amp;m=recommended_articles&amp;permLink=how-make-yourself-work-when-youre-mood-dr-travis-bradberry\" style=\"color:#868686;font-weight:100;text-decoration:none;\">Procrastination affects everyone. It sneaks up on most people when they’re tired or bored, but for some, procrastination can be a full-fledged addicti</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td align=\"left\">\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td width=\"24\"><img width=\"24\" height=\"24\" src=\"#\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\" /></td>\n                                             <td width=\"7\">\n                                              <table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:0px;font-size:0px;line-height:0px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                             <td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAAzXIBrf8iPyriw5pG3z8vxMDDp-pdANE&amp;ref=CONTENT&amp;midToken=AQHp7WCc2qHESg&amp;trk=eml-b2_content_ecosystem_digest-recommended_articles-99-null&amp;trkEmail=eml-b2_content_ecosystem_digest-recommended_articles-99-null-null-6m81ta%7Eihd6z5md%7E7\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">Liz Ryan</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table>\n                                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=97&amp;m=recommended_articles&amp;permLink=real-reason-you-didnt-get-job-liz-ryan\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">The Real Reason You Didn't Get That Job</a></td>\n                                            </tr>\n                                            <tr>\n                                             <td>\n                                              <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                               <tbody>\n                                                <tr>\n                                                 <td>\n                                                  <div style=\"height:10px;font-size:10px;line-height:10px\">\n                                                   &nbsp;\n                                                  </div></td>\n                                                </tr>\n                                               </tbody>\n                                              </table></td>\n                                            </tr>\n                                            <tr>\n                                             <td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_view_article_detail_new_url&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=98&amp;m=recommended_articles&amp;permLink=real-reason-you-didnt-get-job-liz-ryan\" style=\"color:#868686;font-weight:100;text-decoration:none;\">At Human Workplace we teach&nbsp;our clients a five-step protocol to follow after every job interview. The first step is to go home (or back to the office,</a></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td>\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:30px;font-size:30px;line-height:30px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                        <tr>\n                                         <td bgcolor=\"#dddddd\" style=\"background-color:#dddddd;\">\n                                          <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                           <tbody>\n                                            <tr>\n                                             <td>\n                                              <div style=\"height:1px;font-size:1px;line-height:1px\">\n                                               &nbsp;\n                                              </div></td>\n                                            </tr>\n                                           </tbody>\n                                          </table></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                    <tr>\n                                     <td>\n                                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                       <tbody>\n                                        <tr>\n                                         <td>\n                                          <div style=\"height:25px;font-size:25px;line-height:25px\">\n                                           &nbsp;\n                                          </div></td>\n                                        </tr>\n                                       </tbody>\n                                      </table></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table>\n                          <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\" align=\"center\">\n                           <tbody>\n                            <tr>\n                             <td align=\"center\" data-qa=\"section_start_writing\" class=\"mobile-hidden\">\n                              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\" align=\"center\">\n                               <tbody>\n                                <tr>\n                                 <td align=\"center\"></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:5px;font-size:5px;line-height:5px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td align=\"center\" style=\"color:#666666;font-weight:100;font-size:18px;line-height:20px;text-align:center;\">Have your own perspective to share?</td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:20px;font-size:20px;line-height:20px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                                <tr>\n                                 <td align=\"center\" width=\"100%\" height=\"40\" bgcolor=\"#008CC9\" class=\"res-font16\" style=\"font-weight:200;width:100%;font-size:20px;text-align:center;\"><a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=pulse_web_create_article&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;li=102&amp;m=footer_promo&amp;ts=pub_upsell\" target=\"_blank\" style=\"color:#FFFFFF;text-decoration:none;\">Start writing on LinkedIn</a></td>\n                                </tr>\n                                <tr>\n                                 <td>\n                                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                                   <tbody>\n                                    <tr>\n                                     <td>\n                                      <div style=\"height:40px;font-size:40px;line-height:40px\">\n                                       &nbsp;\n                                      </div></td>\n                                    </tr>\n                                   </tbody>\n                                  </table></td>\n                                </tr>\n                               </tbody>\n                              </table></td>\n                            </tr>\n                           </tbody>\n                          </table></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n               </tbody>\n              </table></td>\n            </tr>\n            <tr>\n             <td></td>\n            </tr>\n            <tr>\n             <td align=\"left\">\n              <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive\">\n               <tbody>\n                <tr>\n                 <td>\n                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                   <tbody>\n                    <tr>\n                     <td>\n                      <div style=\"height:10px;font-size:10px;line-height:10px\">\n                       &nbsp;\n                      </div></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n                <tr>\n                 <td align=\"left\">\n                  <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"color:#999999;font-size:11px;font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n                   <tbody>\n                    <tr>\n                     <td>You are receiving notification emails from LinkedIn. <a style=\"color:#0077B5;text-decoration:none;\" href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;t=lun&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;loid=AQFuT4vuXECaYwAAAVE53xL5xFgXxKXqjevO9odGDbZLvLwW0KFPs2TIv9hn3SeTRG7xg9zK6p6NfgqsH56X&amp;eid=6m81ta-ihd6z5md-7\">Unsubscribe</a></td>\n                    </tr>\n                    <tr>\n                     <td></td>\n                    </tr>\n                    <tr>\n                     <td>This email was intended for Benjamin Hartester (Software Developer). <a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=customerServiceUrl&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;articleId=4788\" style=\"color:#2e8dd7;text-decoration:none;\">Learn why we included this.</a></td>\n                    </tr>\n                    <tr>\n                     <td>If you need assistance or have questions, please contact <a target=\"_blank\" href=\"https://www.linkedin.com/e/v2?e=6m81ta-ihd6z5md-7&amp;a=customerServiceUrl&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest\" style=\"color:#2e8dd7;text-decoration:none;\">LinkedIn Customer Service</a>.</td>\n                    </tr>\n                    <tr>\n                     <td>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:10px;font-size:10px;line-height:10px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                    <tr>\n                     <td>&copy; 2015 LinkedIn Corporation, 2029 Stierlin Court, Mountain View CA 94043. LinkedIn and the LinkedIn logo are registered trademarks of LinkedIn.</td>\n                    </tr>\n                    <tr>\n                     <td>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:10px;font-size:10px;line-height:10px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                    <tr>\n                     <td></td>\n                    </tr>\n                    <tr>\n                     <td>\n                      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                       <tbody>\n                        <tr>\n                         <td>\n                          <div style=\"height:10px;font-size:10px;line-height:10px\">\n                           &nbsp;\n                          </div></td>\n                        </tr>\n                       </tbody>\n                      </table></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n                <tr>\n                 <td>\n                  <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n                   <tbody>\n                    <tr>\n                     <td>\n                      <div style=\"height:20px;font-size:20px;line-height:20px\">\n                       &nbsp;\n                      </div></td>\n                    </tr>\n                   </tbody>\n                  </table></td>\n                </tr>\n               </tbody>\n              </table></td>\n            </tr>\n           </tbody>\n          </table></td>\n        </tr>\n       </tbody>\n      </table></td>\n    </tr>\n    <tr>\n     <td>\n      <table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n       <tbody>\n        <tr>\n         <td>\n          <div style=\"height:20px;font-size:20px;line-height:20px\">\n           &nbsp;\n          </div></td>\n        </tr>\n       </tbody>\n      </table></td>\n    </tr>\n   </tbody>\n  </table>\n  <img src=\"#\" style=\"width:1px; height:1px;\" />\n </body>\n</html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/marketing-email-in.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional //EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"><html><head>\n    <title></title>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n    <style type=\"text/css\">\n.font-sans-serif {\n  font-family: sans-serif;\n}\n.font-avenir {\n  font-family: Avenir, sans-serif;\n}\n.mso .wrapper .font-avenir {\n  font-family: sans-serif !important;\n}\n.font-lato {\n  font-family: Lato, Tahoma, sans-serif;\n}\n.mso .wrapper .font-lato {\n  font-family: Tahoma, sans-serif !important;\n}\n.font-cabin {\n  font-family: Cabin, Avenir, sans-serif;\n}\n.mso .wrapper .font-cabin {\n  font-family: sans-serif !important;\n}\n.font-open-Sans {\n  font-family: \"Open Sans\", sans-serif;\n}\n.mso .wrapper .font-open-Sans {\n  font-family: sans-serif !important;\n}\n.font-roboto {\n  font-family: Roboto, Tahoma, sans-serif;\n}\n.mso .wrapper .font-roboto {\n  font-family: Tahoma, sans-serif !important;\n}\n.font-ubuntu {\n  font-family: Ubuntu, sans-serif;\n}\n.mso .wrapper .font-ubuntu {\n  font-family: sans-serif !important;\n}\n.font-pt-sans {\n  font-family: \"PT Sans\", \"Trebuchet MS\", sans-serif;\n}\n.mso .wrapper .font-pt-sans {\n  font-family: \"Trebuchet MS\", sans-serif !important;\n}\n.font-georgia {\n  font-family: Georgia, serif;\n}\n.font-merriweather {\n  font-family: Merriweather, Georgia, serif;\n}\n.mso .wrapper .font-merriweather {\n  font-family: Georgia, serif !important;\n}\n.font-bitter {\n  font-family: Bitter, Georgia, serif;\n}\n.mso .wrapper .font-bitter {\n  font-family: Georgia, serif !important;\n}\n.font-pt-serif {\n  font-family: \"PT Serif\", Georgia, serif;\n}\n.mso .wrapper .font-pt-serif {\n  font-family: Georgia, serif !important;\n}\n.font-pompiere {\n  font-family: Pompiere, \"Trebuchet MS\", sans-serif;\n}\n.mso .wrapper .font-pompiere {\n  font-family: \"Trebuchet MS\", sans-serif !important;\n}\n.font-roboto-slab {\n  font-family: \"Roboto Slab\", Georgia, serif;\n}\n.mso .wrapper .font-roboto-slab {\n  font-family: Georgia, serif !important;\n}\n@media only screen and (max-width: 620px) {\n  .wrapper .column .size-8 {\n    font-size: 8px !important;\n    line-height: 14px !important;\n  }\n  .wrapper .column .size-9 {\n    font-size: 9px !important;\n    line-height: 16px !important;\n  }\n  .wrapper .column .size-10 {\n    font-size: 10px !important;\n    line-height: 18px !important;\n  }\n  .wrapper .column .size-11 {\n    font-size: 11px !important;\n    line-height: 19px !important;\n  }\n  .wrapper .column .size-12 {\n    font-size: 12px !important;\n    line-height: 19px !important;\n  }\n  .wrapper .column .size-13 {\n    font-size: 13px !important;\n    line-height: 21px !important;\n  }\n  .wrapper .column .size-14 {\n    font-size: 14px !important;\n    line-height: 21px !important;\n  }\n  .wrapper .column .size-15 {\n    font-size: 15px !important;\n    line-height: 23px !important;\n  }\n  .wrapper .column .size-16 {\n    font-size: 16px !important;\n    line-height: 24px !important;\n  }\n  .wrapper .column .size-17 {\n    font-size: 17px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-18 {\n    font-size: 17px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-20 {\n    font-size: 17px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-22 {\n    font-size: 18px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-24 {\n    font-size: 20px !important;\n    line-height: 28px !important;\n  }\n  .wrapper .column .size-26 {\n    font-size: 22px !important;\n    line-height: 31px !important;\n  }\n  .wrapper .column .size-28 {\n    font-size: 24px !important;\n    line-height: 32px !important;\n  }\n  .wrapper .column .size-30 {\n    font-size: 26px !important;\n    line-height: 34px !important;\n  }\n  .wrapper .column .size-32 {\n    font-size: 28px !important;\n    line-height: 36px !important;\n  }\n  .wrapper .column .size-34 {\n    font-size: 30px !important;\n    line-height: 38px !important;\n  }\n  .wrapper .column .size-36 {\n    font-size: 30px !important;\n    line-height: 38px !important;\n  }\n  .wrapper .column .size-40 {\n    font-size: 32px !important;\n    line-height: 40px !important;\n  }\n  .wrapper .column .size-44 {\n    font-size: 34px !important;\n    line-height: 43px !important;\n  }\n  .wrapper .column .size-48 {\n    font-size: 36px !important;\n    line-height: 43px !important;\n  }\n  .wrapper .column .size-56 {\n    font-size: 40px !important;\n    line-height: 47px !important;\n  }\n  .wrapper .column .size-64 {\n    font-size: 44px !important;\n    line-height: 50px !important;\n  }\n}\nbody {\n  margin: 0;\n  padding: 0;\n  min-width: 100%;\n}\n.mso body {\n  mso-line-height-rule: exactly;\n}\n.no-padding .wrapper .column .column-top,\n.no-padding .wrapper .column .column-bottom {\n  font-size: 0px;\n  line-height: 0px;\n}\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\ntd {\n  padding: 0;\n  vertical-align: top;\n}\n.spacer,\n.border {\n  font-size: 1px;\n  line-height: 1px;\n}\n.spacer {\n  width: 100%;\n}\nimg {\n  border: 0;\n  -ms-interpolation-mode: bicubic;\n}\n.image {\n  font-size: 12px;\n  mso-line-height-rule: at-least;\n}\n.image img {\n  display: block;\n}\n.logo {\n  mso-line-height-rule: at-least;\n}\n.logo img {\n  display: block;\n}\nstrong {\n  font-weight: bold;\n}\nh1,\nh2,\nh3,\np,\nol,\nul,\nblockquote,\n.image {\n  font-style: normal;\n  font-weight: 400;\n}\nol,\nul,\nli {\n  padding-left: 0;\n}\nblockquote {\n  Margin-left: 0;\n  Margin-right: 0;\n  padding-right: 0;\n}\n.column-top,\n.column-bottom {\n  font-size: 20px;\n  line-height: 20px;\n  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);\n  transition-duration: 150ms;\n  transition-property: all;\n}\n.half-padding .column .column-top,\n.half-padding .column .column-bottom {\n  font-size: 10px;\n  line-height: 10px;\n}\n.column {\n  text-align: left;\n}\n.contents {\n  table-layout: fixed;\n  width: 100%;\n}\n.padded {\n  padding-left: 20px;\n  padding-right: 20px;\n  word-break: break-word;\n  word-wrap: break-word;\n}\n.wrapper {\n  display: table;\n  table-layout: fixed;\n  width: 100%;\n  min-width: 620px;\n  -webkit-text-size-adjust: 100%;\n  -ms-text-size-adjust: 100%;\n}\n.wrapper a {\n  transition: opacity 0.2s ease-in;\n}\ntable.wrapper {\n  table-layout: fixed;\n}\n.one-col,\n.two-col,\n.three-col {\n  Margin-left: auto;\n  Margin-right: auto;\n  width: 600px;\n}\n.centered {\n  Margin-left: auto;\n  Margin-right: auto;\n}\n.btn a {\n  border-radius: 3px;\n  display: inline-block;\n  font-size: 14px;\n  font-weight: 700;\n  line-height: 24px;\n  padding: 13px 35px 12px 35px;\n  text-align: center;\n  text-decoration: none !important;\n}\n.btn a:hover {\n  opacity: 0.8;\n}\n.two-col .btn a {\n  font-size: 12px;\n  line-height: 22px;\n  padding: 10px 28px;\n}\n.three-col .btn a {\n  font-size: 11px;\n  line-height: 19px;\n  padding: 6px 18px 5px 18px;\n}\n@media only screen and (max-width: 620px) {\n  .btn a {\n    display: block !important;\n    font-size: 14px !important;\n    line-height: 24px !important;\n    padding: 13px 10px 12px 10px !important;\n  }\n}\n.two-col .column {\n  width: 300px;\n}\n.three-col .column {\n  width: 200px;\n}\n@media only screen and (min-width: 0) {\n  .wrapper {\n    text-rendering: optimizeLegibility;\n  }\n}\n@media only screen and (max-width: 620px) {\n  [class=wrapper] {\n    min-width: 320px !important;\n    width: 100% !important;\n  }\n  [class=wrapper] .one-col,\n  [class=wrapper] .two-col,\n  [class=wrapper] .three-col {\n    width: 320px !important;\n  }\n  [class=wrapper] .column,\n  [class=wrapper] .gutter {\n    display: block;\n    float: left;\n    width: 320px !important;\n  }\n  [class=wrapper] .padded {\n    padding-left: 20px !important;\n    padding-right: 20px !important;\n  }\n  [class=wrapper] .block {\n    display: block !important;\n  }\n  [class=wrapper] .hide {\n    display: none !important;\n  }\n  [class=wrapper] .image img {\n    height: auto !important;\n    width: 100% !important;\n  }\n}\n.footer {\n  width: 100%;\n}\n.footer .inner {\n  padding: 58px 0 29px 0;\n  width: 600px;\n}\n.footer .left td,\n.footer .right td {\n  font-size: 12px;\n  line-height: 22px;\n}\n.footer .left td {\n  text-align: left;\n  width: 400px;\n}\n.footer .right td {\n  max-width: 200px;\n  mso-line-height-rule: at-least;\n}\n.footer .links {\n  line-height: 26px;\n  Margin-bottom: 26px;\n  mso-line-height-rule: at-least;\n}\n.footer .links a:hover {\n  opacity: 0.8;\n}\n.footer .links img {\n  vertical-align: middle;\n}\n.footer .address {\n  Margin-bottom: 18px;\n}\n.footer .campaign {\n  Margin-bottom: 18px;\n}\n.footer .campaign a {\n  font-weight: bold;\n  text-decoration: none;\n}\n.footer .sharing div {\n  Margin-bottom: 5px;\n}\n.wrapper .footer .fblike,\n.wrapper .footer .tweet,\n.wrapper .footer .linkedinshare,\n.wrapper .footer .forwardtoafriend {\n  background-repeat: no-repeat;\n  background-size: 200px 56px;\n  border-radius: 2px;\n  color: #ffffff;\n  display: block;\n  font-size: 11px;\n  font-weight: bold;\n  line-height: 11px;\n  padding: 8px 11px 7px 28px;\n  text-align: left;\n  text-decoration: none;\n}\n.wrapper .footer .fblike:hover,\n.wrapper .footer .tweet:hover,\n.wrapper .footer .linkedinshare:hover,\n.wrapper .footer .forwardtoafriend:hover {\n  color: #ffffff !important;\n  opacity: 0.8;\n}\n.footer .fblike {\n  background-image: url(http://i3.cmail20.com/static/eb/master/04-broadsheet/imgf/fblike.png);\n}\n.footer .tweet {\n  background-image: url(http://i4.cmail20.com/static/eb/master/04-broadsheet/imgf/tweet.png);\n}\n.footer .linkedinshare {\n  background-image: url(http://i8.cmail20.com/static/eb/master/04-broadsheet/imgf/lishare.png);\n}\n.footer .forwardtoafriend {\n  background-image: url(http://i5.cmail20.com/static/eb/master/04-broadsheet/imgf/forward.png);\n}\n@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {\n  .footer .fblike {\n    background-image: url(http://i6.cmail20.com/static/eb/master/04-broadsheet/imgf/fblike@2x.png) !important;\n  }\n  .footer .tweet {\n    background-image: url(http://i7.cmail20.com/static/eb/master/04-broadsheet/imgf/tweet@2x.png) !important;\n  }\n  .footer .linkedinshare {\n    background-image: url(http://i9.cmail20.com/static/eb/master/04-broadsheet/imgf/lishare@2x.png) !important;\n  }\n  .footer .forwardtoafriend {\n    background-image: url(http://i10.cmail20.com/static/eb/master/04-broadsheet/imgf/forward@2x.png) !important;\n  }\n}\n@media only screen and (max-width: 620px) {\n  .footer {\n    width: 320px !important;\n  }\n  .footer td {\n    display: none;\n  }\n  .footer .inner,\n  .footer .inner td {\n    display: block;\n    text-align: center !important;\n    max-width: 320px !important;\n    width: 320px !important;\n  }\n  .footer .sharing {\n    Margin-bottom: 40px;\n  }\n  .footer .sharing div {\n    display: inline-block;\n  }\n  .footer .fblike,\n  .footer .tweet,\n  .footer .linkedinshare,\n  .footer .forwardtoafriend {\n    display: inline-block !important;\n  }\n}\n.wrapper h1,\n.wrapper h2,\n.wrapper h3,\n.wrapper p,\n.wrapper ol,\n.wrapper ul,\n.wrapper li,\n.wrapper blockquote,\n.image,\n.btn,\n.divider {\n  Margin-bottom: 0;\n  Margin-top: 0;\n}\n.wrapper .column h1 + * {\n  Margin-top: 24px;\n}\n.wrapper .column h2 + * {\n  Margin-top: 16px;\n}\n.wrapper .column h3 + * {\n  Margin-top: 12px;\n}\n.wrapper .column p + *,\n.wrapper .column ol + *,\n.wrapper .column ul + *,\n.wrapper .column blockquote + *,\n.image + .contents td > :first-child {\n  Margin-top: 27px;\n}\n.wrapper .column li + * {\n  Margin-top: 14px;\n}\n.contents:nth-last-child(n+3) h1:last-child,\n.no-padding .contents:nth-last-child(n+2) h1:last-child {\n  Margin-bottom: 24px;\n}\n.contents:nth-last-child(n+3) h2:last-child,\n.no-padding .contents:nth-last-child(n+2) h2:last-child {\n  Margin-bottom: 16px;\n}\n.contents:nth-last-child(n+3) h3:last-child,\n.no-padding .contents:nth-last-child(n+2) h3:last-child {\n  Margin-bottom: 12px;\n}\n.contents:nth-last-child(n+3) p:last-child,\n.no-padding .contents:nth-last-child(n+2) p:last-child,\n.contents:nth-last-child(n+3) ol:last-child,\n.no-padding .contents:nth-last-child(n+2) ol:last-child,\n.contents:nth-last-child(n+3) ul:last-child,\n.no-padding .contents:nth-last-child(n+2) ul:last-child,\n.contents:nth-last-child(n+3) blockquote:last-child,\n.no-padding .contents:nth-last-child(n+2) blockquote:last-child,\n.contents:nth-last-child(n+3) .image,\n.no-padding .contents:nth-last-child(n+2) .image,\n.contents:nth-last-child(n+3) .divider,\n.no-padding .contents:nth-last-child(n+2) .divider,\n.contents:nth-last-child(n+3) .btn,\n.no-padding .contents:nth-last-child(n+2) .btn {\n  Margin-bottom: 27px;\n}\n.two-col .column p + *,\n.two-col .column ol + *,\n.two-col .column ul + *,\n.two-col .column blockquote + *,\n.two-col .image + .contents td > :first-child {\n  Margin-top: 24px;\n}\n.two-col .column li + * {\n  Margin-top: 12px;\n}\n.two-col .contents:nth-last-child(n+3) p:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) p:last-child,\n.two-col .contents:nth-last-child(n+3) ol:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) ol:last-child,\n.two-col .contents:nth-last-child(n+3) ul:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) ul:last-child,\n.two-col .contents:nth-last-child(n+3) blockquote:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) blockquote:last-child,\n.two-col .contents:nth-last-child(n+3) .image,\n.no-padding .two-col .contents:nth-last-child(n+2) .image,\n.two-col .contents:nth-last-child(n+3) .divider,\n.no-padding .two-col .contents:nth-last-child(n+2) .divider,\n.two-col .contents:nth-last-child(n+3) .btn,\n.no-padding .two-col .contents:nth-last-child(n+2) .btn {\n  Margin-bottom: 24px;\n}\n.three-col .column p + *,\n.three-col .column ol + *,\n.three-col .column ul + *,\n.three-col .column blockquote + *,\n.three-col .image + .contents td > :first-child {\n  Margin-top: 21px;\n}\n.three-col .column li + * {\n  Margin-top: 10px;\n}\n.three-col .contents:nth-last-child(n+3) p:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) p:last-child,\n.three-col .contents:nth-last-child(n+3) ol:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) ol:last-child,\n.three-col .contents:nth-last-child(n+3) ul:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) ul:last-child,\n.three-col .contents:nth-last-child(n+3) blockquote:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) blockquote:last-child,\n.three-col .contents:nth-last-child(n+3) .image,\n.no-padding .three-col .contents:nth-last-child(n+2) .image,\n.three-col .contents:nth-last-child(n+3) .divider,\n.no-padding .three-col .contents:nth-last-child(n+2) .divider,\n.three-col .contents:nth-last-child(n+3) .btn,\n.no-padding .three-col .contents:nth-last-child(n+2) .btn {\n  Margin-bottom: 21px;\n}\n@media only screen and (max-width: 620px) {\n  .wrapper p + *,\n  .wrapper ol + *,\n  .wrapper ul + *,\n  .wrapper blockquote + *,\n  .image + .contents td > :first-child {\n    Margin-top: 27px !important;\n  }\n  .contents:nth-last-child(n+3) p:last-child,\n  .no-padding .contents:nth-last-child(n+2) p:last-child,\n  .contents:nth-last-child(n+3) ol:last-child,\n  .no-padding .contents:nth-last-child(n+2) ol:last-child,\n  .contents:nth-last-child(n+3) ul:last-child,\n  .no-padding .contents:nth-last-child(n+2) ul:last-child,\n  .contents:nth-last-child(n+3) blockquote:last-child,\n  .no-padding .contents:nth-last-child(n+2) blockquote:last-child,\n  .contents:nth-last-child(n+3) .image:last-child,\n  .no-padding .contents:nth-last-child(n+2) .image:last-child,\n  .contents:nth-last-child(n+3) .divider:last-child,\n  .no-padding .contents:nth-last-child(n+2) .divider:last-child,\n  .contents:nth-last-child(n+3) .btn:last-child,\n  .no-padding .contents:nth-last-child(n+2) .btn:last-child {\n    Margin-bottom: 27px !important;\n  }\n  .column li + * {\n    Margin-top: 14px !important;\n  }\n}\n.divider {\n  font-size: 4px;\n  line-height: 4px;\n}\n.contents .divider {\n  Margin-bottom: 27px;\n}\n.divider .bullet {\n  border-radius: 2px;\n  display: inline-block;\n  font-size: 4px;\n  height: 4px;\n  line-height: 4px;\n  width: 4px;\n}\n.mso .bullet {\n  display: none;\n}\n.one-col,\ntwo-col,\n.three-col {\n  table-layout: fixed;\n}\ntable.spacer {\n  height: 54px;\n}\n.wrapper a {\n  text-decoration: underline;\n}\n.wrapper h1 {\n  font-size: 32px;\n  line-height: 40px;\n}\n.wrapper h2 {\n  font-size: 22px;\n  line-height: 30px;\n}\n.wrapper h3 {\n  font-size: 18px;\n  line-height: 24px;\n}\n.wrapper h1 a,\n.wrapper h2 a,\n.wrapper h3 a {\n  text-decoration: none;\n}\n.wrapper p,\n.wrapper ol,\n.wrapper ul {\n  font-size: 17px;\n  line-height: 25px;\n}\n.wrapper ol {\n  Margin-left: 24px;\n}\n.wrapper ol li {\n  padding-left: 4px;\n}\n.wrapper ul {\n  Margin-left: 18px;\n}\n.wrapper ul li {\n  padding-left: 9px;\n}\n.wrapper blockquote {\n  Margin-left: 0;\n  padding-left: 17px;\n}\n.two-col ol {\n  Margin-left: 21px;\n}\n.two-col ol li {\n  padding-left: 3px;\n}\n.two-col ul {\n  Margin-left: 18px;\n}\n.two-col ul li {\n  padding-left: 6px;\n}\n.two-col blockquote {\n  border-left-width: 3px;\n  padding-left: 15px;\n}\n.three-col ul {\n  Margin-left: 16px;\n}\n.three-col ul li {\n  padding-left: 6px;\n}\n.three-col ol {\n  Margin-left: 18px;\n}\n.three-col ol li {\n  padding-left: 4px;\n}\n.three-col blockquote {\n  border-left-width: 2px;\n  padding-left: 13px;\n}\n.wrapper h2 {\n  font-weight: 700;\n}\n.wrapper h3 {\n  font-weight: 700;\n}\n.wrapper blockquote {\n  font-style: italic;\n}\n.header {\n  Margin-left: auto;\n  Margin-right: auto;\n  width: 560px;\n}\n.preheader table {\n  width: 560px;\n}\n.preheader .title,\n.preheader .webversion {\n  padding-bottom: 12px;\n  font-size: 12px;\n  line-height: 21px;\n}\n.preheader .title {\n  text-align: left;\n}\n.preheader .webversion {\n  text-align: right;\n  width: 300px;\n}\n.header td {\n  font-size: 24px;\n}\n.header .logo div {\n  font-weight: bold;\n}\n.header .logo div a {\n  text-decoration: none;\n}\n.header .logo div.logo-center {\n  text-align: center;\n}\n.header .logo div.logo-center img {\n  Margin-left: auto;\n  Margin-right: auto;\n}\n@media only screen and (max-width: 620px) {\n  [class=wrapper] table.spacer {\n    height: 28px !important;\n  }\n  [class=wrapper] .header {\n    width: 280px !important;\n  }\n  [class=wrapper] .webversion {\n    display: none;\n  }\n  [class=wrapper] .title {\n    padding-left: 20px !important;\n    padding-right: 20px !important;\n  }\n  [class=wrapper] .preheader table {\n    width: 320px !important;\n  }\n  [class=wrapper] .logo img {\n    max-width: 280px !important;\n    height: auto !important;\n  }\n  [class=wrapper] blockquote {\n    border-left-width: 4px !important;\n    margin-left: 0 !important;\n    padding-left: 14px !important;\n  }\n  [class=wrapper] h1 {\n    font-size: 32px !important;\n    line-height: 42px !important;\n  }\n  [class=wrapper] h2 {\n    font-size: 22px !important;\n    line-height: 30px !important;\n  }\n  [class=wrapper] h3 {\n    font-size: 18px !important;\n    line-height: 26px !important;\n  }\n  [class=wrapper] .one-col p,\n  [class=wrapper] .two-col p,\n  [class=wrapper] .three-col p,\n  [class=wrapper] .one-col ol,\n  [class=wrapper] .two-col ol,\n  [class=wrapper] .three-col ol,\n  [class=wrapper] .one-col ul,\n  [class=wrapper] .two-col ul,\n  [class=wrapper] .three-col ul {\n    font-size: 17px !important;\n    line-height: 27px !important;\n  }\n  [class=wrapper] ol {\n    margin-left: 24px !important;\n  }\n  [class=wrapper] ol li {\n    padding-left: 4px !important;\n  }\n  [class=wrapper] ul {\n    margin-left: 19px !important;\n  }\n  [class=wrapper] ul li {\n    padding-left: 9px !important;\n  }\n  [class=wrapper] .second .column-top,\n  [class=wrapper] .third .column-top {\n    display: none;\n  }\n  [class=wrapper] .show {\n    display: block !important;\n    font-size: 1px;\n    line-height: 1px;\n  }\n  [class=wrapper] .hide {\n    display: none !important;\n  }\n}\n</style>\n  <!--[if !mso]><!--><style type=\"text/css\">\n@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);\n</style><link href=\"https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic\" rel=\"stylesheet\" type=\"text/css\"><!--<![endif]--><style type=\"text/css\">\n.wrapper h1{}.wrapper h1{font-family:Lato,Tahoma,sans-serif}.mso .wrapper h1{font-family:Tahoma,sans-serif !important}.wrapper h2{}.wrapper h2{font-family:Lato,Tahoma,sans-serif}.mso .wrapper h2{font-family:Tahoma,sans-serif !important}.wrapper h3{}.wrapper h3{font-family:Lato,Tahoma,sans-serif}.mso .wrapper h3{font-family:Tahoma,sans-serif !important}.wrapper p,.wrapper ol,.wrapper ul,.wrapper .image{}.wrapper p,.wrapper ol,.wrapper ul,.wrapper .image{font-family:Lato,Tahoma,sans-serif}.mso .wrapper p,.mso .wrapper ol,.mso .wrapper ul,.mso .wrapper .image{font-family:Tahoma,sans-serif !important}.wrapper .btn a{}.wrapper .btn a{font-family:Lato,Tahoma,sans-serif}.mso .wrapper .btn a{font-family:Tahoma,sans-serif !important}.logo div{}.logo div{font-family:Merriweather,Georgia,serif}.mso .logo div{font-family:Georgia,serif\n!important}.title,.webversion,.fblike,.tweet,.linkedinshare,.forwardtoafriend,.link,.address,.permission,.campaign{}.title,.webversion,.fblike,.tweet,.linkedinshare,.forwardtoafriend,.link,.address,.permission,.campaign{font-family:Lato,Tahoma,sans-serif}.mso .title,.mso .webversion,.mso .fblike,.mso .tweet,.mso .linkedinshare,.mso .forwardtoafriend,.mso .link,.mso .address,.mso .permission,.mso .campaign{font-family:Tahoma,sans-serif !important}body,.wrapper,.emb-editor-canvas{background-color:#fafafa}blockquote{border-left:4px solid #c7c7c7}.wrapper h1{color:#b8bdc9}.wrapper h2{color:#8690a8}.wrapper h3{color:#8690a8}.wrapper p,.wrapper ol,.wrapper ul{color:#595959}.wrapper .image{color:#595959}.wrapper a{color:#6b7489}.wrapper a:hover{color:#555c6c !important}.wrapper .btn a{background-color:#6b7489;color:#fefefe}.wrapper .btn a:hover{color:#fefefe !important}.logo\ndiv{color:#202020}.logo div a{color:#202020}.logo div a:hover{color:#202020 !important}.divider .bullet{background-color:#c7c7c7}.bullet-light{display:none}.title,.webversion,.header,.footer .inner td{color:#bbb}.wrapper .preheader a,.wrapper .header a,.wrapper .footer a{color:#bbb}.wrapper .preheader a:hover,.wrapper .header a:hover,.wrapper .footer a:hover{color:#959595 !important}.wrapper .footer .fblike,.wrapper .footer .tweet,.wrapper .footer .linkedinshare,.wrapper .footer .forwardtoafriend{background-color:#7d7d7d}\n</style><!--[if mso]>\n<style type=\"text/css\">\n@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);\n</style>\n<![endif]--></head>\n<!--[if mso]>\n  <body class=\"mso\">\n<![endif]-->\n<!--[if !mso]><!-->\n  <body class=\"ff-spacing full-padding\" style=\"margin: 0;padding: 0;min-width: 100%;background-color: #fafafa;\">\n<!--<![endif]-->\n    <center class=\"wrapper\" style=\"display: table;table-layout: fixed;width: 100%;min-width: 620px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;background-color: #fafafa;\">\n      <table class=\"spacer\" style=\"border-collapse: collapse;border-spacing: 0;font-size: 1px;line-height: 1px;width: 100%;height: 54px;\"><tbody><tr><td style=\"padding: 0;vertical-align: top;\">&nbsp;</td></tr></tbody></table>\n      <table class=\"preheader centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;\">\n        <tbody><tr>\n          <td style=\"padding: 0;vertical-align: top;\">\n            <table style=\"border-collapse: collapse;border-spacing: 0;width: 560px;\">\n              <tbody><tr>\n                <td class=\"title\" style=\"padding: 0;vertical-align: top;font-family: Lato,Tahoma,sans-serif;color: #bbb;padding-bottom: 12px;font-size: 12px;line-height: 21px;text-align: left;\">\n                  &nbsp;\n                </td>\n                <td class=\"webversion\" style=\"padding: 0;vertical-align: top;font-family: Lato,Tahoma,sans-serif;color: #bbb;padding-bottom: 12px;font-size: 12px;line-height: 21px;text-align: right;width: 300px;\">\n                  <div>No Images? <a style=\"transition: opacity 0.2s ease-in;text-decoration: none;color: #bbb;font-weight: bold;\" href=\"http://campaigns.nylas.com/t/d-e-jidutik-juyklhmj-z/\">Click here</a></div>\n                </td>\n              </tr>\n            </tbody></table>\n          </td>\n        </tr>\n      </tbody></table>\n      <table class=\"header centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;width: 560px;color: #bbb;\">\n        <tbody><tr>\n          <td class=\"logo emb-logo-padding-box\" style=\"padding: 0;vertical-align: top;mso-line-height-rule: at-least;font-size: 24px;padding-top: 2px;padding-bottom: 27px;\">\n            <div class=\"logo-center\" style=\"font-family: Merriweather,Georgia,serif;color: #202020;font-weight: bold;text-align: center;font-size: 0px !important;line-height: 0 !important;\" align=\"center\" id=\"emb-email-header\"><a style=\"transition: opacity 0.2s ease-in;text-decoration: none;color: #bbb;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-r/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;Margin-left: auto;Margin-right: auto;max-width: 435px;\" src=\"http://i1.cmail20.com/ei/d/A8/57E/075/060154/csfinal/68747470733a2f2f6564676568696c6c2e73332e616d617a6f1.png\" alt=\"\" width=\"435\" height=\"80\"></a></div>\n          </td>\n        </tr>\n      </tbody></table>\n\n          <table class=\"one-col centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;width: 600px;table-layout: fixed;\" emb-background-style>\n            <tbody><tr>\n              <td class=\"column\" style=\"padding: 0;vertical-align: top;text-align: left;\">\n                <div><div class=\"column-top\" style=\"font-size: 20px;line-height: 20px;transition-timing-function: cubic-bezier(0, 0, 0.2, 1);transition-duration: 150ms;transition-property: all;\">&nbsp;</div></div>\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-18\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 0;font-size: 18px;line-height: 26px;color: #595959;text-align: center;\"><em><span style=\"color:rgb(89, 89, 89)\">The&nbsp;extensible, open source mail client.</span></em></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:33px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <h1 class=\"font-avenir size-32\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 24px;Margin-top: 0;font-size: 32px;line-height: 40px;color: #b8bdc9;text-align: center;\"><span style=\"color:#2b2b2b\">New features, speed &amp; plugins for N1.</span></h1>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 0;font-size: 20px;line-height: 28px;color: #595959;\">It's been almost 2 months since we released Nylas Mail. Our team has been hard at work on this latest update, including awesome new plugins, a beautiful Windows version, and details of our roadmap. Read on for full details!</p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #46a837;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-y/\">Download N1 for Free!</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-y/\" style=\"width:214px\" arcsize=\"7%\" fillcolor=\"#46A837\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">Download N1 for Free!</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:61px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"http://i1.cmail20.com/ei/d/A8/57E/075/060154/csfinal/ScreenShot2015-11-23at8.04.40PM1.png\" alt=\"\" width=\"600\" height=\"340\">\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">Tame your calendar with QuickSchedule.</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Say goodbye to the hassle of scheduling! This new&nbsp;plugin lets you avoid the typical back-and-forth of picking a time to meet. Just select a few options, and your recipient confirms with one click. It's the best way to instantly schedule meetings.</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #8254ff;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-j/\">Upgrade to QuickSchedule</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-j/\" style=\"width:237px\" arcsize=\"7%\" fillcolor=\"#8254FF\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">Upgrade to QuickSchedule</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:62px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-t/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"http://i2.cmail20.com/ei/d/A8/57E/075/060154/csfinal/github.png\" alt=\"\" width=\"600\" height=\"203\"></a>\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\">It's full of stars!</p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:rgb(64, 64, 64)\">Starring the N1 repo is a great way to show your support and bookmark the codebase for later. It also means you'll see pre-release product updates in your GitHub feed.</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #141414 !important;font-family: Lato,Tahoma,sans-serif;background-color: #fcdd14;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-i/\">&#9733;     Star N1 on GitHub Now     &#9733;</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-i/\" style=\"width:250px\" arcsize=\"7%\" fillcolor=\"#FCDD14\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#141414;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">&#9733;<br>\n   <br>\nStar N1 on GitHub Now<br>\n   <br>\n&#9733;</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:66px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-d/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"http://i3.cmail20.com/ei/d/A8/57E/075/060154/csfinal/cN1.png\" alt=\"\" width=\"600\" height=\"403\"></a>\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">N1 is now available on Windows!</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Are you tired of Outlook and looking for something fresh? N1 now works great on Windows with all the same features available on Mac and Linux. Plus you can connect both your Gmail and Exchange accounts.&nbsp;</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #ffffff !important;font-family: Lato,Tahoma,sans-serif;background-color: #5f7ec7;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-h/\">Download N1 for Windows (64-bit)</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-h/\" style=\"width:289px\" arcsize=\"7%\" fillcolor=\"#5F7EC7\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">Download N1 for Windows (64-bit)</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:53px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-k/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"http://i4.cmail20.com/ei/d/A8/57E/075/060154/csfinal/ModernWeb1.png\" alt=\"\" width=\"600\" height=\"304\"></a>\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"Margin-top: 27px;line-height: 20px;font-size: 1px;\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 0;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">Faster and with fewer bugs!</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\"><span>We've closed hundreds of bug reports and made big improvements to speed and memory usage of N1. If you've already downloaded, make sure to update the app.</span></span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #7d7d7d;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-x/\">See the full CHANGELOG</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-x/\" style=\"width:229px\" arcsize=\"7%\" fillcolor=\"#7D7D7D\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">See the full CHANGELOG</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:82px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"http://i5.cmail20.com/ei/d/A8/57E/075/060154/csfinal/PuzzlePiece.png\" alt=\"\" width=\"600\" height=\"268\">\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">Features On Deck</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Our team is hard at work on features including unified inbox, mail rules, and support for aliases and signatures. To stay up to date, you should follow us on Twitter\n</span><span style=\"color:#404040\"><a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-m/\">here</a>. You can also vote up features on our\n<a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-c/\">open roadmap</a>.</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Those are the latest updates. Thanks for trying N1 and for the continued feedback!&nbsp;If you'd like to join the experimental beta channel for N1, just reply to this message with your address. (We'll push more frequent updates, but they might have occasional issues.)</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:37px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"http://i6.cmail20.com/ei/d/A8/57E/075/060154/csfinal/11.png\" alt=\"\" width=\"600\" height=\"308\">\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-26\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 26px;line-height: 34px;color: #595959;text-align: center;\"><strong style=\"font-weight: bold;\">Want to create the future of email?</strong></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><font color=\"#080808\"><strong style=\"font-weight: bold;\">Nylas is hiring!</strong>\nOur small team in SF is growing, and we're looking for great engineers, designers, PMs, and more to help shape the future of email. </font></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:rgb(8, 8, 8)\">Curious? Learn a bit more </span><a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-q/\">about the team</a><span style=\"color:rgb(8, 8, 8)\">&nbsp;behind N1,</span><span style=\"color:rgb(8, 8, 8)\">\nand see </span><a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-a/\">what it's like to work at Nylas</a><span style=\"color:rgb(8, 8, 8)\">. We welcome applications from those of all background.</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #46a837;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-f/\">View open positions at Nylas</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-f/\" style=\"width:250px\" arcsize=\"7%\" fillcolor=\"#46A837\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">View open positions at Nylas</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:20px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p style=\"font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-size: 17px;line-height: 25px;font-family: Lato,Tahoma,sans-serif;color: #595959;\">&nbsp;</p><p style=\"font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 27px;font-size: 17px;line-height: 25px;font-family: Lato,Tahoma,sans-serif;color: #595959;\"><em>PS: Not sure why you're receiving this message?&nbsp;</em><strong style=\"font-weight: bold;\">Nylas was previously called InboxApp and launched last year</strong><em>. You probably signed up then</em></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                <div class=\"column-bottom\" style=\"font-size: 20px;line-height: 20px;transition-timing-function: cubic-bezier(0, 0, 0.2, 1);transition-duration: 150ms;transition-property: all;\">&nbsp;</div>\n              </td>\n            </tr>\n          </tbody></table>\n\n      <table class=\"footer centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;width: 100%;\">\n        <tbody><tr>\n          <td style=\"padding: 0;vertical-align: top;\">&nbsp;</td>\n          <td class=\"inner\" style=\"padding: 58px 0 29px 0;vertical-align: top;width: 600px;\">\n            <table class=\"right\" style=\"border-collapse: collapse;border-spacing: 0;\" align=\"right\">\n              <tbody><tr>\n                <td style=\"padding: 0;vertical-align: top;color: #bbb;font-size: 12px;line-height: 22px;max-width: 200px;mso-line-height-rule: at-least;\">\n                  <div class=\"sharing\">\n                    <div style=\"Margin-bottom: 5px;\">\n                      <![if !mso]><a class=\"fblike\" style=\"font-family: Lato,Tahoma,sans-serif;transition: opacity 0.2s ease-in;text-decoration: none;color: #ffffff;background-image: url(http://i3.cmail20.com/static/eb/master/04-broadsheet/imgf/fblike.png);background-repeat: no-repeat;background-size: 200px 56px;border-radius: 2px;display: block;font-size: 11px;font-weight: bold;line-height: 11px;padding: 8px 11px 7px 28px;text-align: left;background-color: #7d7d7d;\" href=\"http://campaigns.nylas.com/t/d-fb-jidutik-juyklhmj-v/\" likeurl=\"www.facebook.com/InboxApp\" left-align-text=\"true\" rel=\"cs_facebox\">\n                        Like\n                      </a><![endif]>\n                    <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-fb-jidutik-juyklhmj-v/\" style=\"width:81px\" arcsize=\"8%\" fill=\"t\" stroke=\"f\"><v:fill type=\"tile\" src=\"http://i3.cmail20.com/static/eb/master/04-broadsheet/imgf/fblike.png\" color=\"#7D7D7D\"></v:fill><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"27px,7px,0,6px\"><p style=\"font-size:11px;line-height:11px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:-1px\">                        Like                      </p></v:textbox></v:roundrect><![endif]--></div>\n                    <div style=\"Margin-bottom: 5px;\">\n                      <![if !mso]><a class=\"tweet\" style=\"font-family: Lato,Tahoma,sans-serif;transition: opacity 0.2s ease-in;text-decoration: none;color: #ffffff;background-image: url(http://i4.cmail20.com/static/eb/master/04-broadsheet/imgf/tweet.png);background-repeat: no-repeat;background-size: 200px 56px;border-radius: 2px;display: block;font-size: 11px;font-weight: bold;line-height: 11px;padding: 8px 11px 7px 28px;text-align: left;background-color: #7d7d7d;\" href=\"http://campaigns.nylas.com/t/d-tw-jidutik-juyklhmj-e/\" left-align-text=\"true\">\n                        Tweet\n                      </a><![endif]>\n                    <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-tw-jidutik-juyklhmj-e/\" style=\"width:81px\" arcsize=\"8%\" fill=\"t\" stroke=\"f\"><v:fill type=\"tile\" src=\"http://i4.cmail20.com/static/eb/master/04-broadsheet/imgf/tweet.png\" color=\"#7D7D7D\"></v:fill><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"27px,7px,0,6px\"><p style=\"font-size:11px;line-height:11px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:-1px\">                        Tweet                      </p></v:textbox></v:roundrect><![endif]--></div>\n\n                    <div style=\"Margin-bottom: 5px;\">\n                      <![if !mso]><a class=\"forwardtoafriend\" style=\"font-family: Lato,Tahoma,sans-serif;transition: opacity 0.2s ease-in;text-decoration: none;color: #ffffff;background-image: url(http://i5.cmail20.com/static/eb/master/04-broadsheet/imgf/forward.png);background-repeat: no-repeat;background-size: 200px 56px;border-radius: 2px;display: block;font-size: 11px;font-weight: bold;line-height: 11px;padding: 8px 11px 7px 28px;text-align: left;background-color: #7d7d7d;\" href=\"http://inbox.forwardtomyfriend.com/d-juyklhmj-B1CA593A-jidutik-l-s\" left-align-text=\"true\">\n                        Forward\n                      </a><![endif]>\n                    <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://inbox.forwardtomyfriend.com/d-juyklhmj-B1CA593A-jidutik-l-s\" style=\"width:81px\" arcsize=\"8%\" fill=\"t\" stroke=\"f\"><v:fill type=\"tile\" src=\"http://i5.cmail20.com/static/eb/master/04-broadsheet/imgf/forward.png\" color=\"#7D7D7D\"></v:fill><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"27px,7px,0,6px\"><p style=\"font-size:11px;line-height:11px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:-1px\">                        Forward                      </p></v:textbox></v:roundrect><![endif]--></div>\n                  </div>\n                </td>\n              </tr>\n            </tbody></table>\n            <table class=\"left\" style=\"border-collapse: collapse;border-spacing: 0;\" align=\"left\">\n              <tbody><tr>\n                <td style=\"padding: 0;vertical-align: top;color: #bbb;font-size: 12px;line-height: 22px;text-align: left;width: 400px;\">\n\n                  <div class=\"address\" style=\"font-family: Lato,Tahoma,sans-serif;Margin-bottom: 18px;\">\n                    <div>Nylas, Inc.<br>\n2030 Harrison St.<br>\nSan Francisco, CA</div>\n                  </div>\n                  <div class=\"permission\" style=\"font-family: Lato,Tahoma,sans-serif;\">\n                    <div>You're receiving this email because you expressed interest in InboxApp, the Nylas Developer Program, or registered as a developer.</div>\n                  </div>\n                  <div class=\"campaign\" style=\"font-family: Lato,Tahoma,sans-serif;Margin-bottom: 18px;\">\n\n                    <a style=\"transition: opacity 0.2s ease-in;text-decoration: none;color: #bbb;font-weight: bold;\" href=\"http://campaigns.nylas.com/t/d-u-jidutik-juyklhmj-g/\">Unsubscribe</a>\n                  </div>\n                </td>\n              </tr>\n            </tbody></table>\n          </td>\n          <td style=\"padding: 0;vertical-align: top;\">&nbsp;</td>\n        </tr>\n      </tbody></table>\n      <table class=\"spacer\" style=\"border-collapse: collapse;border-spacing: 0;font-size: 1px;line-height: 1px;width: 100%;height: 54px;\"><tbody><tr><td style=\"padding: 0;vertical-align: top;\">&nbsp;</td></tr></tbody></table>\n    </center>\n  <img style=\"border: 0 !important;-ms-interpolation-mode: bicubic;visibility: hidden !important;display: block !important;height: 1px !important;width: 1px !important;margin: 0 !important;padding: 0 !important;\" src=\"https://inbox.cmail20.com/t/d-o-jidutik-juyklhmj/o.gif\" width=\"1\" height=\"1\" border=\"0\" alt=\"\">\n</body></html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/marketing-email-out.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional //EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"><html><head>\n    <title></title>\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n    <style type=\"text/css\">\n.font-sans-serif {\n  font-family: sans-serif;\n}\n.font-avenir {\n  font-family: Avenir, sans-serif;\n}\n.mso .wrapper .font-avenir {\n  font-family: sans-serif !important;\n}\n.font-lato {\n  font-family: Lato, Tahoma, sans-serif;\n}\n.mso .wrapper .font-lato {\n  font-family: Tahoma, sans-serif !important;\n}\n.font-cabin {\n  font-family: Cabin, Avenir, sans-serif;\n}\n.mso .wrapper .font-cabin {\n  font-family: sans-serif !important;\n}\n.font-open-Sans {\n  font-family: \"Open Sans\", sans-serif;\n}\n.mso .wrapper .font-open-Sans {\n  font-family: sans-serif !important;\n}\n.font-roboto {\n  font-family: Roboto, Tahoma, sans-serif;\n}\n.mso .wrapper .font-roboto {\n  font-family: Tahoma, sans-serif !important;\n}\n.font-ubuntu {\n  font-family: Ubuntu, sans-serif;\n}\n.mso .wrapper .font-ubuntu {\n  font-family: sans-serif !important;\n}\n.font-pt-sans {\n  font-family: \"PT Sans\", \"Trebuchet MS\", sans-serif;\n}\n.mso .wrapper .font-pt-sans {\n  font-family: \"Trebuchet MS\", sans-serif !important;\n}\n.font-georgia {\n  font-family: Georgia, serif;\n}\n.font-merriweather {\n  font-family: Merriweather, Georgia, serif;\n}\n.mso .wrapper .font-merriweather {\n  font-family: Georgia, serif !important;\n}\n.font-bitter {\n  font-family: Bitter, Georgia, serif;\n}\n.mso .wrapper .font-bitter {\n  font-family: Georgia, serif !important;\n}\n.font-pt-serif {\n  font-family: \"PT Serif\", Georgia, serif;\n}\n.mso .wrapper .font-pt-serif {\n  font-family: Georgia, serif !important;\n}\n.font-pompiere {\n  font-family: Pompiere, \"Trebuchet MS\", sans-serif;\n}\n.mso .wrapper .font-pompiere {\n  font-family: \"Trebuchet MS\", sans-serif !important;\n}\n.font-roboto-slab {\n  font-family: \"Roboto Slab\", Georgia, serif;\n}\n.mso .wrapper .font-roboto-slab {\n  font-family: Georgia, serif !important;\n}\n@media only screen and (max-width: 620px) {\n  .wrapper .column .size-8 {\n    font-size: 8px !important;\n    line-height: 14px !important;\n  }\n  .wrapper .column .size-9 {\n    font-size: 9px !important;\n    line-height: 16px !important;\n  }\n  .wrapper .column .size-10 {\n    font-size: 10px !important;\n    line-height: 18px !important;\n  }\n  .wrapper .column .size-11 {\n    font-size: 11px !important;\n    line-height: 19px !important;\n  }\n  .wrapper .column .size-12 {\n    font-size: 12px !important;\n    line-height: 19px !important;\n  }\n  .wrapper .column .size-13 {\n    font-size: 13px !important;\n    line-height: 21px !important;\n  }\n  .wrapper .column .size-14 {\n    font-size: 14px !important;\n    line-height: 21px !important;\n  }\n  .wrapper .column .size-15 {\n    font-size: 15px !important;\n    line-height: 23px !important;\n  }\n  .wrapper .column .size-16 {\n    font-size: 16px !important;\n    line-height: 24px !important;\n  }\n  .wrapper .column .size-17 {\n    font-size: 17px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-18 {\n    font-size: 17px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-20 {\n    font-size: 17px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-22 {\n    font-size: 18px !important;\n    line-height: 26px !important;\n  }\n  .wrapper .column .size-24 {\n    font-size: 20px !important;\n    line-height: 28px !important;\n  }\n  .wrapper .column .size-26 {\n    font-size: 22px !important;\n    line-height: 31px !important;\n  }\n  .wrapper .column .size-28 {\n    font-size: 24px !important;\n    line-height: 32px !important;\n  }\n  .wrapper .column .size-30 {\n    font-size: 26px !important;\n    line-height: 34px !important;\n  }\n  .wrapper .column .size-32 {\n    font-size: 28px !important;\n    line-height: 36px !important;\n  }\n  .wrapper .column .size-34 {\n    font-size: 30px !important;\n    line-height: 38px !important;\n  }\n  .wrapper .column .size-36 {\n    font-size: 30px !important;\n    line-height: 38px !important;\n  }\n  .wrapper .column .size-40 {\n    font-size: 32px !important;\n    line-height: 40px !important;\n  }\n  .wrapper .column .size-44 {\n    font-size: 34px !important;\n    line-height: 43px !important;\n  }\n  .wrapper .column .size-48 {\n    font-size: 36px !important;\n    line-height: 43px !important;\n  }\n  .wrapper .column .size-56 {\n    font-size: 40px !important;\n    line-height: 47px !important;\n  }\n  .wrapper .column .size-64 {\n    font-size: 44px !important;\n    line-height: 50px !important;\n  }\n}\nbody {\n  margin: 0;\n  padding: 0;\n  min-width: 100%;\n}\n.mso body {\n  mso-line-height-rule: exactly;\n}\n.no-padding .wrapper .column .column-top,\n.no-padding .wrapper .column .column-bottom {\n  font-size: 0px;\n  line-height: 0px;\n}\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\ntd {\n  padding: 0;\n  vertical-align: top;\n}\n.spacer,\n.border {\n  font-size: 1px;\n  line-height: 1px;\n}\n.spacer {\n  width: 100%;\n}\nimg {\n  border: 0;\n  -ms-interpolation-mode: bicubic;\n}\n.image {\n  font-size: 12px;\n  mso-line-height-rule: at-least;\n}\n.image img {\n  display: block;\n}\n.logo {\n  mso-line-height-rule: at-least;\n}\n.logo img {\n  display: block;\n}\nstrong {\n  font-weight: bold;\n}\nh1,\nh2,\nh3,\np,\nol,\nul,\nblockquote,\n.image {\n  font-style: normal;\n  font-weight: 400;\n}\nol,\nul,\nli {\n  padding-left: 0;\n}\nblockquote {\n  Margin-left: 0;\n  Margin-right: 0;\n  padding-right: 0;\n}\n.column-top,\n.column-bottom {\n  font-size: 20px;\n  line-height: 20px;\n  transition-timing-function: cubic-bezier(0, 0, 0.2, 1);\n  transition-duration: 150ms;\n  transition-property: all;\n}\n.half-padding .column .column-top,\n.half-padding .column .column-bottom {\n  font-size: 10px;\n  line-height: 10px;\n}\n.column {\n  text-align: left;\n}\n.contents {\n  table-layout: fixed;\n  width: 100%;\n}\n.padded {\n  padding-left: 20px;\n  padding-right: 20px;\n  word-break: break-word;\n  word-wrap: break-word;\n}\n.wrapper {\n  display: table;\n  table-layout: fixed;\n  width: 100%;\n  min-width: 620px;\n  -webkit-text-size-adjust: 100%;\n  -ms-text-size-adjust: 100%;\n}\n.wrapper a {\n  transition: opacity 0.2s ease-in;\n}\ntable.wrapper {\n  table-layout: fixed;\n}\n.one-col,\n.two-col,\n.three-col {\n  Margin-left: auto;\n  Margin-right: auto;\n  width: 600px;\n}\n.centered {\n  Margin-left: auto;\n  Margin-right: auto;\n}\n.btn a {\n  border-radius: 3px;\n  display: inline-block;\n  font-size: 14px;\n  font-weight: 700;\n  line-height: 24px;\n  padding: 13px 35px 12px 35px;\n  text-align: center;\n  text-decoration: none !important;\n}\n.btn a:hover {\n  opacity: 0.8;\n}\n.two-col .btn a {\n  font-size: 12px;\n  line-height: 22px;\n  padding: 10px 28px;\n}\n.three-col .btn a {\n  font-size: 11px;\n  line-height: 19px;\n  padding: 6px 18px 5px 18px;\n}\n@media only screen and (max-width: 620px) {\n  .btn a {\n    display: block !important;\n    font-size: 14px !important;\n    line-height: 24px !important;\n    padding: 13px 10px 12px 10px !important;\n  }\n}\n.two-col .column {\n  width: 300px;\n}\n.three-col .column {\n  width: 200px;\n}\n@media only screen and (min-width: 0) {\n  .wrapper {\n    text-rendering: optimizeLegibility;\n  }\n}\n@media only screen and (max-width: 620px) {\n  [class=wrapper] {\n    min-width: 320px !important;\n    width: 100% !important;\n  }\n  [class=wrapper] .one-col,\n  [class=wrapper] .two-col,\n  [class=wrapper] .three-col {\n    width: 320px !important;\n  }\n  [class=wrapper] .column,\n  [class=wrapper] .gutter {\n    display: block;\n    float: left;\n    width: 320px !important;\n  }\n  [class=wrapper] .padded {\n    padding-left: 20px !important;\n    padding-right: 20px !important;\n  }\n  [class=wrapper] .block {\n    display: block !important;\n  }\n  [class=wrapper] .hide {\n    display: none !important;\n  }\n  [class=wrapper] .image img {\n    height: auto !important;\n    width: 100% !important;\n  }\n}\n.footer {\n  width: 100%;\n}\n.footer .inner {\n  padding: 58px 0 29px 0;\n  width: 600px;\n}\n.footer .left td,\n.footer .right td {\n  font-size: 12px;\n  line-height: 22px;\n}\n.footer .left td {\n  text-align: left;\n  width: 400px;\n}\n.footer .right td {\n  max-width: 200px;\n  mso-line-height-rule: at-least;\n}\n.footer .links {\n  line-height: 26px;\n  Margin-bottom: 26px;\n  mso-line-height-rule: at-least;\n}\n.footer .links a:hover {\n  opacity: 0.8;\n}\n.footer .links img {\n  vertical-align: middle;\n}\n.footer .address {\n  Margin-bottom: 18px;\n}\n.footer .campaign {\n  Margin-bottom: 18px;\n}\n.footer .campaign a {\n  font-weight: bold;\n  text-decoration: none;\n}\n.footer .sharing div {\n  Margin-bottom: 5px;\n}\n.wrapper .footer .fblike,\n.wrapper .footer .tweet,\n.wrapper .footer .linkedinshare,\n.wrapper .footer .forwardtoafriend {\n  background-repeat: no-repeat;\n  background-size: 200px 56px;\n  border-radius: 2px;\n  color: #ffffff;\n  display: block;\n  font-size: 11px;\n  font-weight: bold;\n  line-height: 11px;\n  padding: 8px 11px 7px 28px;\n  text-align: left;\n  text-decoration: none;\n}\n.wrapper .footer .fblike:hover,\n.wrapper .footer .tweet:hover,\n.wrapper .footer .linkedinshare:hover,\n.wrapper .footer .forwardtoafriend:hover {\n  color: #ffffff !important;\n  opacity: 0.8;\n}\n.footer .fblike {\n  background-image: url(#);\n}\n.footer .tweet {\n  background-image: url(#);\n}\n.footer .linkedinshare {\n  background-image: url(#);\n}\n.footer .forwardtoafriend {\n  background-image: url(#);\n}\n@media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) {\n  .footer .fblike {\n    background-image: url(#) !important;\n  }\n  .footer .tweet {\n    background-image: url(#) !important;\n  }\n  .footer .linkedinshare {\n    background-image: url(#) !important;\n  }\n  .footer .forwardtoafriend {\n    background-image: url(#) !important;\n  }\n}\n@media only screen and (max-width: 620px) {\n  .footer {\n    width: 320px !important;\n  }\n  .footer td {\n    display: none;\n  }\n  .footer .inner,\n  .footer .inner td {\n    display: block;\n    text-align: center !important;\n    max-width: 320px !important;\n    width: 320px !important;\n  }\n  .footer .sharing {\n    Margin-bottom: 40px;\n  }\n  .footer .sharing div {\n    display: inline-block;\n  }\n  .footer .fblike,\n  .footer .tweet,\n  .footer .linkedinshare,\n  .footer .forwardtoafriend {\n    display: inline-block !important;\n  }\n}\n.wrapper h1,\n.wrapper h2,\n.wrapper h3,\n.wrapper p,\n.wrapper ol,\n.wrapper ul,\n.wrapper li,\n.wrapper blockquote,\n.image,\n.btn,\n.divider {\n  Margin-bottom: 0;\n  Margin-top: 0;\n}\n.wrapper .column h1 + * {\n  Margin-top: 24px;\n}\n.wrapper .column h2 + * {\n  Margin-top: 16px;\n}\n.wrapper .column h3 + * {\n  Margin-top: 12px;\n}\n.wrapper .column p + *,\n.wrapper .column ol + *,\n.wrapper .column ul + *,\n.wrapper .column blockquote + *,\n.image + .contents td > :first-child {\n  Margin-top: 27px;\n}\n.wrapper .column li + * {\n  Margin-top: 14px;\n}\n.contents:nth-last-child(n+3) h1:last-child,\n.no-padding .contents:nth-last-child(n+2) h1:last-child {\n  Margin-bottom: 24px;\n}\n.contents:nth-last-child(n+3) h2:last-child,\n.no-padding .contents:nth-last-child(n+2) h2:last-child {\n  Margin-bottom: 16px;\n}\n.contents:nth-last-child(n+3) h3:last-child,\n.no-padding .contents:nth-last-child(n+2) h3:last-child {\n  Margin-bottom: 12px;\n}\n.contents:nth-last-child(n+3) p:last-child,\n.no-padding .contents:nth-last-child(n+2) p:last-child,\n.contents:nth-last-child(n+3) ol:last-child,\n.no-padding .contents:nth-last-child(n+2) ol:last-child,\n.contents:nth-last-child(n+3) ul:last-child,\n.no-padding .contents:nth-last-child(n+2) ul:last-child,\n.contents:nth-last-child(n+3) blockquote:last-child,\n.no-padding .contents:nth-last-child(n+2) blockquote:last-child,\n.contents:nth-last-child(n+3) .image,\n.no-padding .contents:nth-last-child(n+2) .image,\n.contents:nth-last-child(n+3) .divider,\n.no-padding .contents:nth-last-child(n+2) .divider,\n.contents:nth-last-child(n+3) .btn,\n.no-padding .contents:nth-last-child(n+2) .btn {\n  Margin-bottom: 27px;\n}\n.two-col .column p + *,\n.two-col .column ol + *,\n.two-col .column ul + *,\n.two-col .column blockquote + *,\n.two-col .image + .contents td > :first-child {\n  Margin-top: 24px;\n}\n.two-col .column li + * {\n  Margin-top: 12px;\n}\n.two-col .contents:nth-last-child(n+3) p:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) p:last-child,\n.two-col .contents:nth-last-child(n+3) ol:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) ol:last-child,\n.two-col .contents:nth-last-child(n+3) ul:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) ul:last-child,\n.two-col .contents:nth-last-child(n+3) blockquote:last-child,\n.no-padding .two-col .contents:nth-last-child(n+2) blockquote:last-child,\n.two-col .contents:nth-last-child(n+3) .image,\n.no-padding .two-col .contents:nth-last-child(n+2) .image,\n.two-col .contents:nth-last-child(n+3) .divider,\n.no-padding .two-col .contents:nth-last-child(n+2) .divider,\n.two-col .contents:nth-last-child(n+3) .btn,\n.no-padding .two-col .contents:nth-last-child(n+2) .btn {\n  Margin-bottom: 24px;\n}\n.three-col .column p + *,\n.three-col .column ol + *,\n.three-col .column ul + *,\n.three-col .column blockquote + *,\n.three-col .image + .contents td > :first-child {\n  Margin-top: 21px;\n}\n.three-col .column li + * {\n  Margin-top: 10px;\n}\n.three-col .contents:nth-last-child(n+3) p:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) p:last-child,\n.three-col .contents:nth-last-child(n+3) ol:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) ol:last-child,\n.three-col .contents:nth-last-child(n+3) ul:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) ul:last-child,\n.three-col .contents:nth-last-child(n+3) blockquote:last-child,\n.no-padding .three-col .contents:nth-last-child(n+2) blockquote:last-child,\n.three-col .contents:nth-last-child(n+3) .image,\n.no-padding .three-col .contents:nth-last-child(n+2) .image,\n.three-col .contents:nth-last-child(n+3) .divider,\n.no-padding .three-col .contents:nth-last-child(n+2) .divider,\n.three-col .contents:nth-last-child(n+3) .btn,\n.no-padding .three-col .contents:nth-last-child(n+2) .btn {\n  Margin-bottom: 21px;\n}\n@media only screen and (max-width: 620px) {\n  .wrapper p + *,\n  .wrapper ol + *,\n  .wrapper ul + *,\n  .wrapper blockquote + *,\n  .image + .contents td > :first-child {\n    Margin-top: 27px !important;\n  }\n  .contents:nth-last-child(n+3) p:last-child,\n  .no-padding .contents:nth-last-child(n+2) p:last-child,\n  .contents:nth-last-child(n+3) ol:last-child,\n  .no-padding .contents:nth-last-child(n+2) ol:last-child,\n  .contents:nth-last-child(n+3) ul:last-child,\n  .no-padding .contents:nth-last-child(n+2) ul:last-child,\n  .contents:nth-last-child(n+3) blockquote:last-child,\n  .no-padding .contents:nth-last-child(n+2) blockquote:last-child,\n  .contents:nth-last-child(n+3) .image:last-child,\n  .no-padding .contents:nth-last-child(n+2) .image:last-child,\n  .contents:nth-last-child(n+3) .divider:last-child,\n  .no-padding .contents:nth-last-child(n+2) .divider:last-child,\n  .contents:nth-last-child(n+3) .btn:last-child,\n  .no-padding .contents:nth-last-child(n+2) .btn:last-child {\n    Margin-bottom: 27px !important;\n  }\n  .column li + * {\n    Margin-top: 14px !important;\n  }\n}\n.divider {\n  font-size: 4px;\n  line-height: 4px;\n}\n.contents .divider {\n  Margin-bottom: 27px;\n}\n.divider .bullet {\n  border-radius: 2px;\n  display: inline-block;\n  font-size: 4px;\n  height: 4px;\n  line-height: 4px;\n  width: 4px;\n}\n.mso .bullet {\n  display: none;\n}\n.one-col,\ntwo-col,\n.three-col {\n  table-layout: fixed;\n}\ntable.spacer {\n  height: 54px;\n}\n.wrapper a {\n  text-decoration: underline;\n}\n.wrapper h1 {\n  font-size: 32px;\n  line-height: 40px;\n}\n.wrapper h2 {\n  font-size: 22px;\n  line-height: 30px;\n}\n.wrapper h3 {\n  font-size: 18px;\n  line-height: 24px;\n}\n.wrapper h1 a,\n.wrapper h2 a,\n.wrapper h3 a {\n  text-decoration: none;\n}\n.wrapper p,\n.wrapper ol,\n.wrapper ul {\n  font-size: 17px;\n  line-height: 25px;\n}\n.wrapper ol {\n  Margin-left: 24px;\n}\n.wrapper ol li {\n  padding-left: 4px;\n}\n.wrapper ul {\n  Margin-left: 18px;\n}\n.wrapper ul li {\n  padding-left: 9px;\n}\n.wrapper blockquote {\n  Margin-left: 0;\n  padding-left: 17px;\n}\n.two-col ol {\n  Margin-left: 21px;\n}\n.two-col ol li {\n  padding-left: 3px;\n}\n.two-col ul {\n  Margin-left: 18px;\n}\n.two-col ul li {\n  padding-left: 6px;\n}\n.two-col blockquote {\n  border-left-width: 3px;\n  padding-left: 15px;\n}\n.three-col ul {\n  Margin-left: 16px;\n}\n.three-col ul li {\n  padding-left: 6px;\n}\n.three-col ol {\n  Margin-left: 18px;\n}\n.three-col ol li {\n  padding-left: 4px;\n}\n.three-col blockquote {\n  border-left-width: 2px;\n  padding-left: 13px;\n}\n.wrapper h2 {\n  font-weight: 700;\n}\n.wrapper h3 {\n  font-weight: 700;\n}\n.wrapper blockquote {\n  font-style: italic;\n}\n.header {\n  Margin-left: auto;\n  Margin-right: auto;\n  width: 560px;\n}\n.preheader table {\n  width: 560px;\n}\n.preheader .title,\n.preheader .webversion {\n  padding-bottom: 12px;\n  font-size: 12px;\n  line-height: 21px;\n}\n.preheader .title {\n  text-align: left;\n}\n.preheader .webversion {\n  text-align: right;\n  width: 300px;\n}\n.header td {\n  font-size: 24px;\n}\n.header .logo div {\n  font-weight: bold;\n}\n.header .logo div a {\n  text-decoration: none;\n}\n.header .logo div.logo-center {\n  text-align: center;\n}\n.header .logo div.logo-center img {\n  Margin-left: auto;\n  Margin-right: auto;\n}\n@media only screen and (max-width: 620px) {\n  [class=wrapper] table.spacer {\n    height: 28px !important;\n  }\n  [class=wrapper] .header {\n    width: 280px !important;\n  }\n  [class=wrapper] .webversion {\n    display: none;\n  }\n  [class=wrapper] .title {\n    padding-left: 20px !important;\n    padding-right: 20px !important;\n  }\n  [class=wrapper] .preheader table {\n    width: 320px !important;\n  }\n  [class=wrapper] .logo img {\n    max-width: 280px !important;\n    height: auto !important;\n  }\n  [class=wrapper] blockquote {\n    border-left-width: 4px !important;\n    margin-left: 0 !important;\n    padding-left: 14px !important;\n  }\n  [class=wrapper] h1 {\n    font-size: 32px !important;\n    line-height: 42px !important;\n  }\n  [class=wrapper] h2 {\n    font-size: 22px !important;\n    line-height: 30px !important;\n  }\n  [class=wrapper] h3 {\n    font-size: 18px !important;\n    line-height: 26px !important;\n  }\n  [class=wrapper] .one-col p,\n  [class=wrapper] .two-col p,\n  [class=wrapper] .three-col p,\n  [class=wrapper] .one-col ol,\n  [class=wrapper] .two-col ol,\n  [class=wrapper] .three-col ol,\n  [class=wrapper] .one-col ul,\n  [class=wrapper] .two-col ul,\n  [class=wrapper] .three-col ul {\n    font-size: 17px !important;\n    line-height: 27px !important;\n  }\n  [class=wrapper] ol {\n    margin-left: 24px !important;\n  }\n  [class=wrapper] ol li {\n    padding-left: 4px !important;\n  }\n  [class=wrapper] ul {\n    margin-left: 19px !important;\n  }\n  [class=wrapper] ul li {\n    padding-left: 9px !important;\n  }\n  [class=wrapper] .second .column-top,\n  [class=wrapper] .third .column-top {\n    display: none;\n  }\n  [class=wrapper] .show {\n    display: block !important;\n    font-size: 1px;\n    line-height: 1px;\n  }\n  [class=wrapper] .hide {\n    display: none !important;\n  }\n}\n</style>\n  <!--[if !mso]><!--><style type=\"text/css\">\n@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);\n</style><link href=\"https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic\" rel=\"stylesheet\" type=\"text/css\"><!--<![endif]--><style type=\"text/css\">\n.wrapper h1{}.wrapper h1{font-family:Lato,Tahoma,sans-serif}.mso .wrapper h1{font-family:Tahoma,sans-serif !important}.wrapper h2{}.wrapper h2{font-family:Lato,Tahoma,sans-serif}.mso .wrapper h2{font-family:Tahoma,sans-serif !important}.wrapper h3{}.wrapper h3{font-family:Lato,Tahoma,sans-serif}.mso .wrapper h3{font-family:Tahoma,sans-serif !important}.wrapper p,.wrapper ol,.wrapper ul,.wrapper .image{}.wrapper p,.wrapper ol,.wrapper ul,.wrapper .image{font-family:Lato,Tahoma,sans-serif}.mso .wrapper p,.mso .wrapper ol,.mso .wrapper ul,.mso .wrapper .image{font-family:Tahoma,sans-serif !important}.wrapper .btn a{}.wrapper .btn a{font-family:Lato,Tahoma,sans-serif}.mso .wrapper .btn a{font-family:Tahoma,sans-serif !important}.logo div{}.logo div{font-family:Merriweather,Georgia,serif}.mso .logo div{font-family:Georgia,serif\n!important}.title,.webversion,.fblike,.tweet,.linkedinshare,.forwardtoafriend,.link,.address,.permission,.campaign{}.title,.webversion,.fblike,.tweet,.linkedinshare,.forwardtoafriend,.link,.address,.permission,.campaign{font-family:Lato,Tahoma,sans-serif}.mso .title,.mso .webversion,.mso .fblike,.mso .tweet,.mso .linkedinshare,.mso .forwardtoafriend,.mso .link,.mso .address,.mso .permission,.mso .campaign{font-family:Tahoma,sans-serif !important}body,.wrapper,.emb-editor-canvas{background-color:#fafafa}blockquote{border-left:4px solid #c7c7c7}.wrapper h1{color:#b8bdc9}.wrapper h2{color:#8690a8}.wrapper h3{color:#8690a8}.wrapper p,.wrapper ol,.wrapper ul{color:#595959}.wrapper .image{color:#595959}.wrapper a{color:#6b7489}.wrapper a:hover{color:#555c6c !important}.wrapper .btn a{background-color:#6b7489;color:#fefefe}.wrapper .btn a:hover{color:#fefefe !important}.logo\ndiv{color:#202020}.logo div a{color:#202020}.logo div a:hover{color:#202020 !important}.divider .bullet{background-color:#c7c7c7}.bullet-light{display:none}.title,.webversion,.header,.footer .inner td{color:#bbb}.wrapper .preheader a,.wrapper .header a,.wrapper .footer a{color:#bbb}.wrapper .preheader a:hover,.wrapper .header a:hover,.wrapper .footer a:hover{color:#959595 !important}.wrapper .footer .fblike,.wrapper .footer .tweet,.wrapper .footer .linkedinshare,.wrapper .footer .forwardtoafriend{background-color:#7d7d7d}\n</style><!--[if mso]>\n<style type=\"text/css\">\n@import url(https://fonts.googleapis.com/css?family=Lato:400,700,400italic,700italic);\n</style>\n<![endif]--></head>\n<!--[if mso]>\n  <body class=\"mso\">\n<![endif]-->\n<!--[if !mso]><!-->\n  <body class=\"ff-spacing full-padding\" style=\"margin: 0;padding: 0;min-width: 100%;background-color: #fafafa;\">\n<!--<![endif]-->\n    <center class=\"wrapper\" style=\"display: table;table-layout: fixed;width: 100%;min-width: 620px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;background-color: #fafafa;\">\n      <table class=\"spacer\" style=\"border-collapse: collapse;border-spacing: 0;font-size: 1px;line-height: 1px;width: 100%;height: 54px;\"><tbody><tr><td style=\"padding: 0;vertical-align: top;\">&nbsp;</td></tr></tbody></table>\n      <table class=\"preheader centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;\">\n        <tbody><tr>\n          <td style=\"padding: 0;vertical-align: top;\">\n            <table style=\"border-collapse: collapse;border-spacing: 0;width: 560px;\">\n              <tbody><tr>\n                <td class=\"title\" style=\"padding: 0;vertical-align: top;font-family: Lato,Tahoma,sans-serif;color: #bbb;padding-bottom: 12px;font-size: 12px;line-height: 21px;text-align: left;\">\n                  &nbsp;\n                </td>\n                <td class=\"webversion\" style=\"padding: 0;vertical-align: top;font-family: Lato,Tahoma,sans-serif;color: #bbb;padding-bottom: 12px;font-size: 12px;line-height: 21px;text-align: right;width: 300px;\">\n                  <div>No Images? <a style=\"transition: opacity 0.2s ease-in;text-decoration: none;color: #bbb;font-weight: bold;\" href=\"http://campaigns.nylas.com/t/d-e-jidutik-juyklhmj-z/\">Click here</a></div>\n                </td>\n              </tr>\n            </tbody></table>\n          </td>\n        </tr>\n      </tbody></table>\n      <table class=\"header centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;width: 560px;color: #bbb;\">\n        <tbody><tr>\n          <td class=\"logo emb-logo-padding-box\" style=\"padding: 0;vertical-align: top;mso-line-height-rule: at-least;font-size: 24px;padding-top: 2px;padding-bottom: 27px;\">\n            <div class=\"logo-center\" style=\"font-family: Merriweather,Georgia,serif;color: #202020;font-weight: bold;text-align: center;font-size: 0px !important;line-height: 0 !important;\" align=\"center\" id=\"emb-email-header\"><a style=\"transition: opacity 0.2s ease-in;text-decoration: none;color: #bbb;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-r/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;Margin-left: auto;Margin-right: auto;max-width: 435px;\" src=\"#\" alt=\"\" width=\"435\" height=\"80\"></a></div>\n          </td>\n        </tr>\n      </tbody></table>\n\n          <table class=\"one-col centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;width: 600px;table-layout: fixed;\" emb-background-style>\n            <tbody><tr>\n              <td class=\"column\" style=\"padding: 0;vertical-align: top;text-align: left;\">\n                <div><div class=\"column-top\" style=\"font-size: 20px;line-height: 20px;transition-timing-function: cubic-bezier(0, 0, 0.2, 1);transition-duration: 150ms;transition-property: all;\">&nbsp;</div></div>\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-18\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 0;font-size: 18px;line-height: 26px;color: #595959;text-align: center;\"><em><span style=\"color:rgb(89, 89, 89)\">The&nbsp;extensible, open source mail client.</span></em></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:33px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <h1 class=\"font-avenir size-32\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 24px;Margin-top: 0;font-size: 32px;line-height: 40px;color: #b8bdc9;text-align: center;\"><span style=\"color:#2b2b2b\">New features, speed &amp; plugins for N1.</span></h1>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 0;font-size: 20px;line-height: 28px;color: #595959;\">It's been almost 2 months since we released Nylas Mail. Our team has been hard at work on this latest update, including awesome new plugins, a beautiful Windows version, and details of our roadmap. Read on for full details!</p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #46a837;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-y/\">Download N1 for Free!</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-y/\" style=\"width:214px\" arcsize=\"7%\" fillcolor=\"#46A837\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">Download N1 for Free!</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:61px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"#\" alt=\"\" width=\"600\" height=\"340\">\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">Tame your calendar with QuickSchedule.</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Say goodbye to the hassle of scheduling! This new&nbsp;plugin lets you avoid the typical back-and-forth of picking a time to meet. Just select a few options, and your recipient confirms with one click. It's the best way to instantly schedule meetings.</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #8254ff;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-j/\">Upgrade to QuickSchedule</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-j/\" style=\"width:237px\" arcsize=\"7%\" fillcolor=\"#8254FF\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">Upgrade to QuickSchedule</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:62px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-t/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"#\" alt=\"\" width=\"600\" height=\"203\"></a>\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\">It's full of stars!</p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:rgb(64, 64, 64)\">Starring the N1 repo is a great way to show your support and bookmark the codebase for later. It also means you'll see pre-release product updates in your GitHub feed.</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #141414 !important;font-family: Lato,Tahoma,sans-serif;background-color: #fcdd14;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-i/\">&#9733;     Star N1 on GitHub Now     &#9733;</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-i/\" style=\"width:250px\" arcsize=\"7%\" fillcolor=\"#FCDD14\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#141414;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">&#9733;<br>\n   <br>\nStar N1 on GitHub Now<br>\n   <br>\n&#9733;</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:66px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-d/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"#\" alt=\"\" width=\"600\" height=\"403\"></a>\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">N1 is now available on Windows!</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Are you tired of Outlook and looking for something fresh? N1 now works great on Windows with all the same features available on Mac and Linux. Plus you can connect both your Gmail and Exchange accounts.&nbsp;</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #ffffff !important;font-family: Lato,Tahoma,sans-serif;background-color: #5f7ec7;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-h/\">Download N1 for Windows (64-bit)</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-h/\" style=\"width:289px\" arcsize=\"7%\" fillcolor=\"#5F7EC7\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">Download N1 for Windows (64-bit)</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:53px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-k/\"><img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"#\" alt=\"\" width=\"600\" height=\"304\"></a>\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"Margin-top: 27px;line-height: 20px;font-size: 1px;\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 0;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">Faster and with fewer bugs!</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\"><span>We've closed hundreds of bug reports and made big improvements to speed and memory usage of N1. If you've already downloaded, make sure to update the app.</span></span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #7d7d7d;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-x/\">See the full CHANGELOG</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-x/\" style=\"width:229px\" arcsize=\"7%\" fillcolor=\"#7D7D7D\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">See the full CHANGELOG</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:82px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"#\" alt=\"\" width=\"600\" height=\"268\">\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-30\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 30px;line-height: 38px;color: #595959;text-align: center;\"><span style=\"color:#404040\">Features On Deck</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Our team is hard at work on features including unified inbox, mail rules, and support for aliases and signatures. To stay up to date, you should follow us on Twitter\n</span><span style=\"color:#404040\"><a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-m/\">here</a>. You can also vote up features on our\n<a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-c/\">open roadmap</a>.</span></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:#404040\">Those are the latest updates. Thanks for trying N1 and for the continued feedback!&nbsp;If you'd like to join the experimental beta channel for N1, just reply to this message with your address. (We'll push more frequent updates, but they might have occasional issues.)</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:37px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n            <div class=\"image\" style=\"font-size: 12px;mso-line-height-rule: at-least;font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-family: Lato,Tahoma,sans-serif;color: #595959;\" align=\"center\">\n              <img style=\"border: 0;-ms-interpolation-mode: bicubic;display: block;max-width: 900px;\" src=\"#\" alt=\"\" width=\"600\" height=\"308\">\n            </div>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p class=\"font-avenir size-26\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 26px;line-height: 34px;color: #595959;text-align: center;\"><strong style=\"font-weight: bold;\">Want to create the future of email?</strong></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 0;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><font color=\"#080808\"><strong style=\"font-weight: bold;\">Nylas is hiring!</strong>\nOur small team in SF is growing, and we're looking for great engineers, designers, PMs, and more to help shape the future of email. </font></p><p class=\"font-avenir size-20\" style=\"font-style: normal;font-weight: 400;font-family: avenir,sans-serif;Margin-bottom: 27px;Margin-top: 27px;font-size: 20px;line-height: 28px;color: #595959;\"><span style=\"color:rgb(8, 8, 8)\">Curious? Learn a bit more </span><a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-q/\">about the team</a><span style=\"color:rgb(8, 8, 8)\">&nbsp;behind N1,</span><span style=\"color:rgb(8, 8, 8)\">\nand see </span><a style=\"transition: opacity 0.2s ease-in;text-decoration: underline;color: #6b7489;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-a/\">what it's like to work at Nylas</a><span style=\"color:rgb(8, 8, 8)\">. We welcome applications from those of all background.</span></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div class=\"btn btn--center\" style=\"Margin-bottom: 27px;Margin-top: 0;text-align: center;\">\n              <![if !mso]><a style=\"border-radius: 3px;display: inline-block;font-size: 14px;font-weight: 700;line-height: 24px;padding: 13px 35px 12px 35px;text-align: center;text-decoration: none !important;transition: opacity 0.2s ease-in;color: #fefefe;font-family: Lato,Tahoma,sans-serif;background-color: #46a837;\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-f/\">View open positions at Nylas</a><![endif]>\n            <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-l-jidutik-juyklhmj-f/\" style=\"width:250px\" arcsize=\"7%\" fillcolor=\"#46A837\" stroke=\"f\"><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"0px,12px,0px,11px\"><center style=\"font-size:14px;line-height:24px;color:#FEFEFE;font-family:Tahoma,sans-serif;font-weight:700;mso-line-height-rule:exactly;mso-text-raise:4px\">View open positions at Nylas</center></v:textbox></v:roundrect><![endif]--></div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <div style=\"line-height:20px;font-size:1px\">&nbsp;</div>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                  <table class=\"contents\" style=\"border-collapse: collapse;border-spacing: 0;table-layout: fixed;width: 100%;\">\n                    <tbody><tr>\n                      <td class=\"padded\" style=\"padding: 0;vertical-align: top;padding-left: 20px;padding-right: 20px;word-break: break-word;word-wrap: break-word;\">\n\n            <p style=\"font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 0;font-size: 17px;line-height: 25px;font-family: Lato,Tahoma,sans-serif;color: #595959;\">&nbsp;</p><p style=\"font-style: normal;font-weight: 400;Margin-bottom: 0;Margin-top: 27px;font-size: 17px;line-height: 25px;font-family: Lato,Tahoma,sans-serif;color: #595959;\"><em>PS: Not sure why you're receiving this message?&nbsp;</em><strong style=\"font-weight: bold;\">Nylas was previously called InboxApp and launched last year</strong><em>. You probably signed up then</em></p>\n\n                      </td>\n                    </tr>\n                  </tbody></table>\n\n                <div class=\"column-bottom\" style=\"font-size: 20px;line-height: 20px;transition-timing-function: cubic-bezier(0, 0, 0.2, 1);transition-duration: 150ms;transition-property: all;\">&nbsp;</div>\n              </td>\n            </tr>\n          </tbody></table>\n\n      <table class=\"footer centered\" style=\"border-collapse: collapse;border-spacing: 0;Margin-left: auto;Margin-right: auto;width: 100%;\">\n        <tbody><tr>\n          <td style=\"padding: 0;vertical-align: top;\">&nbsp;</td>\n          <td class=\"inner\" style=\"padding: 58px 0 29px 0;vertical-align: top;width: 600px;\">\n            <table class=\"right\" style=\"border-collapse: collapse;border-spacing: 0;\" align=\"right\">\n              <tbody><tr>\n                <td style=\"padding: 0;vertical-align: top;color: #bbb;font-size: 12px;line-height: 22px;max-width: 200px;mso-line-height-rule: at-least;\">\n                  <div class=\"sharing\">\n                    <div style=\"Margin-bottom: 5px;\">\n                      <![if !mso]><a class=\"fblike\" style=\"font-family: Lato,Tahoma,sans-serif;transition: opacity 0.2s ease-in;text-decoration: none;color: #ffffff;background-image: url(#);background-repeat: no-repeat;background-size: 200px 56px;border-radius: 2px;display: block;font-size: 11px;font-weight: bold;line-height: 11px;padding: 8px 11px 7px 28px;text-align: left;background-color: #7d7d7d;\" href=\"http://campaigns.nylas.com/t/d-fb-jidutik-juyklhmj-v/\" likeurl=\"www.facebook.com/InboxApp\" left-align-text=\"true\" rel=\"cs_facebox\">\n                        Like\n                      </a><![endif]>\n                    <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-fb-jidutik-juyklhmj-v/\" style=\"width:81px\" arcsize=\"8%\" fill=\"t\" stroke=\"f\"><v:fill type=\"tile\" src=\"#\" color=\"#7D7D7D\"></v:fill><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"27px,7px,0,6px\"><p style=\"font-size:11px;line-height:11px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:-1px\">                        Like                      </p></v:textbox></v:roundrect><![endif]--></div>\n                    <div style=\"Margin-bottom: 5px;\">\n                      <![if !mso]><a class=\"tweet\" style=\"font-family: Lato,Tahoma,sans-serif;transition: opacity 0.2s ease-in;text-decoration: none;color: #ffffff;background-image: url(#);background-repeat: no-repeat;background-size: 200px 56px;border-radius: 2px;display: block;font-size: 11px;font-weight: bold;line-height: 11px;padding: 8px 11px 7px 28px;text-align: left;background-color: #7d7d7d;\" href=\"http://campaigns.nylas.com/t/d-tw-jidutik-juyklhmj-e/\" left-align-text=\"true\">\n                        Tweet\n                      </a><![endif]>\n                    <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://campaigns.nylas.com/t/d-tw-jidutik-juyklhmj-e/\" style=\"width:81px\" arcsize=\"8%\" fill=\"t\" stroke=\"f\"><v:fill type=\"tile\" src=\"#\" color=\"#7D7D7D\"></v:fill><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"27px,7px,0,6px\"><p style=\"font-size:11px;line-height:11px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:-1px\">                        Tweet                      </p></v:textbox></v:roundrect><![endif]--></div>\n\n                    <div style=\"Margin-bottom: 5px;\">\n                      <![if !mso]><a class=\"forwardtoafriend\" style=\"font-family: Lato,Tahoma,sans-serif;transition: opacity 0.2s ease-in;text-decoration: none;color: #ffffff;background-image: url(#);background-repeat: no-repeat;background-size: 200px 56px;border-radius: 2px;display: block;font-size: 11px;font-weight: bold;line-height: 11px;padding: 8px 11px 7px 28px;text-align: left;background-color: #7d7d7d;\" href=\"http://inbox.forwardtomyfriend.com/d-juyklhmj-B1CA593A-jidutik-l-s\" left-align-text=\"true\">\n                        Forward\n                      </a><![endif]>\n                    <!--[if mso]><v:roundrect xmlns:v=\"urn:schemas-microsoft-com:vml\" href=\"http://inbox.forwardtomyfriend.com/d-juyklhmj-B1CA593A-jidutik-l-s\" style=\"width:81px\" arcsize=\"8%\" fill=\"t\" stroke=\"f\"><v:fill type=\"tile\" src=\"#\" color=\"#7D7D7D\"></v:fill><v:textbox style=\"mso-fit-shape-to-text:t\" inset=\"27px,7px,0,6px\"><p style=\"font-size:11px;line-height:11px;color:#FFFFFF;font-family:Tahoma,sans-serif;font-weight:bold;mso-line-height-rule:exactly;mso-text-raise:-1px\">                        Forward                      </p></v:textbox></v:roundrect><![endif]--></div>\n                  </div>\n                </td>\n              </tr>\n            </tbody></table>\n            <table class=\"left\" style=\"border-collapse: collapse;border-spacing: 0;\" align=\"left\">\n              <tbody><tr>\n                <td style=\"padding: 0;vertical-align: top;color: #bbb;font-size: 12px;line-height: 22px;text-align: left;width: 400px;\">\n\n                  <div class=\"address\" style=\"font-family: Lato,Tahoma,sans-serif;Margin-bottom: 18px;\">\n                    <div>Nylas, Inc.<br>\n2030 Harrison St.<br>\nSan Francisco, CA</div>\n                  </div>\n                  <div class=\"permission\" style=\"font-family: Lato,Tahoma,sans-serif;\">\n                    <div>You're receiving this email because you expressed interest in InboxApp, the Nylas Developer Program, or registered as a developer.</div>\n                  </div>\n                  <div class=\"campaign\" style=\"font-family: Lato,Tahoma,sans-serif;Margin-bottom: 18px;\">\n\n                    <a style=\"transition: opacity 0.2s ease-in;text-decoration: none;color: #bbb;font-weight: bold;\" href=\"http://campaigns.nylas.com/t/d-u-jidutik-juyklhmj-g/\">Unsubscribe</a>\n                  </div>\n                </td>\n              </tr>\n            </tbody></table>\n          </td>\n          <td style=\"padding: 0;vertical-align: top;\">&nbsp;</td>\n        </tr>\n      </tbody></table>\n      <table class=\"spacer\" style=\"border-collapse: collapse;border-spacing: 0;font-size: 1px;line-height: 1px;width: 100%;height: 54px;\"><tbody><tr><td style=\"padding: 0;vertical-align: top;\">&nbsp;</td></tr></tbody></table>\n    </center>\n  <img style=\"border: 0 !important;-ms-interpolation-mode: bicubic;visibility: hidden !important;display: block !important;height: 1px !important;width: 1px !important;margin: 0 !important;padding: 0 !important;\" src=\"#\" width=\"1\" height=\"1\" border=\"0\" alt=\"\">\n</body></html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/no-image-extensions-in.html",
    "content": "<!doctype html>\n<html id=\"email\" style=\"background:#fff\">\n        <head>\n          <meta charset=\"utf-8\">\n          <meta content=\"width=device-width\" name=\"viewport\">\n          <title>Google Play</title>\n        <style>@import url(<a href=\"https://fonts.googleapis.com/css?family=Roboto:300,400\" title=\"https://fonts.googleapis.com/css?family=Roboto:300,400\"  target=\"_blank\">fonts.googleapis.com/css?family=Roboto:300,400</a>);</style>\n</head>\n        <body style=\"-ms-text-size-adjust:100%; -webkit-text-size-adjust:100%; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:24px; margin:0; min-width:100%; padding:0; text-align:left; width:100%\" align=\"left\" width=\"100%\">\n          <table style=\"border-collapse:collapse; border-spacing:0; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n            <tbody style=\"width:100%\" width=\"100%\">\n              <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:center; vertical-align:top; word-break:break-word\" align=\"center\" valign=\"top\">\n                  <center style=\"background:#e5e5e5; margin:0 auto; width:600px\" width=\"600\">\n\n<!--\n\n-->\n\n<!---- Element: Header ----><!-- Logo -->\n<!--\n<table class=\"container logo vertical En\">\n  <tbody>\n    <tr>\n      <td class=\"center\">\n        <a href=\"https://www.google.com/appserve/mkt/p/fNOMGzHPj10D6yJ55kIL5WtTkJNBsiBffQQqY…xq08_OyAB_MJCHiYLWNYGwVSqo9Rr0CskMnPCQdvff-RpcSEuqLH6cYNE81spIIlD963du-w==\" title=\"https://www.google.com/appserve/mkt/p/fNOMGzHPj10D6yJ55kIL5WtTkJNBsiBffQQqY…xq08_OyAB_MJCHiYLWNYGwVSqo9Rr0CskMnPCQdvff-RpcSEuqLH6cYNE81spIIlD963du-w==\" >\n\n            <img alt=\"Google Play\" src=\"https://lh3.ggpht.com/Nmax7S1VmSbeBPpAIKOJg2OU74l67OLmQkv22wOUBwSaK4AzCRH0skdObQ75NZh9zcg=w350\">\n\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n-->\n\n<!-- Subject -->\n<table style=\"background:#e5e5e5; border-collapse:collapse; border-spacing:0; margin:0 auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:url(https://lh6.ggpht.com/CKwgKS7m3a5fNeq-QMQzKKIeRxGUUh8x6Qhz_IxVUGh0L-LYYyxKZpvd_O5UxI2ZdpI=w600) #FF8D1C bottom left no-repeat; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:30px 0 5px; text-align:left; vertical-align:top; width:100%; word-break:break-word\" align=\"left\" valign=\"top\" width=\"100%\">\n        <h1 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:30px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:0 10px; text-align:center; width:auto; word-break:break-word\" align=\"center\" width=\"auto\">\n          <a href=\"https://www.google.com/appserve/mkt/p/e6HiYpv26sL9mFqIIgWM6se-DPkp7HE9YRTkq…cP8KDJnXRS2O1q6MeeYBJE3jLx6F49EjFASGrGvIjOGhgkrODhhxfgPJdcp4meBCX9MI-NGQ==\" title=\"https://www.google.com/appserve/mkt/p/e6HiYpv26sL9mFqIIgWM6se-DPkp7HE9YRTkq…cP8KDJnXRS2O1q6MeeYBJE3jLx6F49EjFASGrGvIjOGhgkrODhhxfgPJdcp4meBCX9MI-NGQ==\"  style=\"color:#fff; text-decoration:none\">Album of the Week</a>\n        </h1>\n      </td>\n    </tr>\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:url(https://lh5.ggpht.com/A_xVlTxHZI_Qka_D-C4chqT5xcf_Gfx2BHk4XEi1O-JmNBpVzHgTyctVKw_YMRFxIMw=w600) top left no-repeat; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:55px; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:center; vertical-align:top; width:100%; word-break:break-word\" height=\"55\" align=\"center\" valign=\"top\" width=\"100%\">\n        <a href=\"https://www.google.com/appserve/mkt/p/PMAjz4TGGt1Tchz6z_tPE4evzfMiN2W0w348d…ZRF7Bh63Hn6LJIoOc_KCsQC__1_eaq7P4sJ8QR9UNPidl519mA_QLN0lzWl0gs9Mdk0BK6Evrd\" title=\"https://www.google.com/appserve/mkt/p/PMAjz4TGGt1Tchz6z_tPE4evzfMiN2W0w348d…ZRF7Bh63Hn6LJIoOc_KCsQC__1_eaq7P4sJ8QR9UNPidl519mA_QLN0lzWl0gs9Mdk0BK6Evrd\"  style=\"color:#1ABDD4; display:block; height:50px; margin:0 auto; text-decoration:none; width:50px\" height=\"50\" width=\"50\">\n          <img alt=\"Google Play Music\" src=\"https://lh6.ggpht.com/YTdAK700NZW1Jt5U7Q0kmYQFl1w5aPZVh89jqF5vyZYPZ2nLQhgguZmY07tr8YbgmPQ\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n<!-- Element: Text --><table style=\"background:#e5e5e5; border-collapse:collapse; border-spacing:0; margin:0 auto 5px; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\"><tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 10px; text-align:left; vertical-align:middle; width:40%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"40%\">\n\n\n            <a href=\"https://www.google.com/appserve/mkt/p/GzRjOEZZFlCnx5uuzOpjtk0sppniyYqy7pEsc…URW_lr1MiKVdShxC6awps3uvbPgJUaRxqKV7PKFEL8O7JktbnOcmwiE6KZ4MI8ncDxhWehvTzZ\" title=\"https://www.google.com/appserve/mkt/p/GzRjOEZZFlCnx5uuzOpjtk0sppniyYqy7pEsc…URW_lr1MiKVdShxC6awps3uvbPgJUaRxqKV7PKFEL8O7JktbnOcmwiE6KZ4MI8ncDxhWehvTzZ\"  style=\"color:#1ABDD4; text-decoration:none\"><img src=\"https://lh3.googleusercontent.com/IMEazKbS1jIFRKKH_QkMjhP0y5yVmc2Vv-9raG0puYpeuVS1Ml01_1MEnnmNn56S6bY=c\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\"></a>\n\n\n      </td>\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 10px; text-align:left; vertical-align:middle; width:60%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"60%\">\n\n\n\n          <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:14px; font-weight:300; line-height:20px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">Cowboy hat? Check. Crushin&#39; it? Check. Now all you need is Brad Paisley&#39;s latest album, Moonshine in the Trunk. Lucky for you, it’s free on Google Play for a limited time.*</p>\n\n\n\n          <a href=\"https://www.google.com/appserve/mkt/p/UO6vTvTLoG5BzPQTiW5l7xdTVD8KReLPIizUq…FelDRdpvg8zJHW0CgRmCCJD-5WgvbFz1XF7B3TOEEXTVCH-QDfhF0-jSMYtdjr0JLEeEezZ7Q=\" title=\"https://www.google.com/appserve/mkt/p/UO6vTvTLoG5BzPQTiW5l7xdTVD8KReLPIizUq…FelDRdpvg8zJHW0CgRmCCJD-5WgvbFz1XF7B3TOEEXTVCH-QDfhF0-jSMYtdjr0JLEeEezZ7Q=\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:15px; font-weight:normal; line-height:1; margin:10px 0 15px; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase\" align=\"center\">Get It Free</a>\n\n      </td>\n    </tr></tbody>\n</table>\n<!-- Element: Text --><table style=\"background:#FD992E; border-collapse:collapse; border-spacing:0; margin:10px auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n      <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n        <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 20px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\"><h2 style=\"color:#fff; font-family:Roboto, sans-serif-light, sans-serif; font-size:24px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:20px 0; text-align:center; width:auto; word-break:break-word\" align=\"center\" width=\"auto\"><a href=\"https://www.google.com/appserve/mkt/p/SVsU1sviGu_iJ0RZUhJKcMD-ZsNDwZpb32tD_…74KSLiQYsgzu_daLsgmPYVdnddyJYyfFA3s2K6jPVIs4c3I792KFrUzTSmpiSi4HWcoleE0LP1\" title=\"https://www.google.com/appserve/mkt/p/SVsU1sviGu_iJ0RZUhJKcMD-ZsNDwZpb32tD_…74KSLiQYsgzu_daLsgmPYVdnddyJYyfFA3s2K6jPVIs4c3I792KFrUzTSmpiSi4HWcoleE0LP1\"  style=\"color:#fff; text-decoration:none\">On sale: Country greats</a></h2></td>\n\n          <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 20px; padding-left:0; text-align:right; vertical-align:middle; word-break:break-word\" align=\"right\" valign=\"middle\"><a href=\"https://www.google.com/appserve/mkt/p/IcqipRC60sigr_KABSeXuJfYijqVCBV0uYixa…Sy7gdnud9ELh9vEN7R-E7Zxl4FHG6KW4E6jIK-2J1UmnBvWVYEm_C6KDblkSIvLgFxVotoT9g=\" title=\"https://www.google.com/appserve/mkt/p/IcqipRC60sigr_KABSeXuJfYijqVCBV0uYixa…Sy7gdnud9ELh9vEN7R-E7Zxl4FHG6KW4E6jIK-2J1UmnBvWVYEm_C6KDblkSIvLgFxVotoT9g=\"  style=\"background:transparent; border:1px solid #fff; border-radius:3px; color:#fff; display:block; font-size:14px; font-weight:normal; line-height:1; margin:0; min-width:60px; padding:10px 20px; text-align:center; text-decoration:none; text-transform:uppercase; white-space:nowrap\" align=\"center\">SEE ALL</a></td>\n\n      </tr>\n\n\n    </tbody>\n</table>\n<!---- Element: Cluster ---->\n\n<table style=\"background:#e5e5e5; border-collapse:collapse; border-spacing:0; margin:10px auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n<!-- 3 Across -->\n        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; margin-bottom:5px; max-width:600px; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n          <tbody style=\"width:100%\" width=\"100%\">\n            <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n<!-- Cluster Item --><td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; hyphens:auto; line-height:24px; margin:0; padding:0 5px; text-align:left; vertical-align:top; width:190px; word-break:break-word\" height=\"100%\" align=\"left\" valign=\"top\" width=\"190\">\n                <table style=\"background:#fff; border-collapse:collapse; border-radius:3px; border-spacing:0; height:100%; min-height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                  <tbody style=\"width:100%\" width=\"100%\">\n                    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:#f5f5f5; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:middle; width:50%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"50%\">\n                        <a href=\"https://www.google.com/appserve/mkt/p/zgknewdZyNxLoFrq66voips4aGwP5MTvgRD61…ciGJAHqfLiXsPKvDNxV9vbtyMhdGPdDcVmOUSj3dEHETCpEYCNOpOXzyLzOamiOheDBJH6Ew==\" title=\"https://www.google.com/appserve/mkt/p/zgknewdZyNxLoFrq66voips4aGwP5MTvgRD61…ciGJAHqfLiXsPKvDNxV9vbtyMhdGPdDcVmOUSj3dEHETCpEYCNOpOXzyLzOamiOheDBJH6Ew==\"  style=\"color:#1ABDD4; text-decoration:none\">\n                          <img alt=\"Pageant Material\" src=\"https://lh3.googleusercontent.com/i5O0AOOWVsbVlbpGr0HNMV7U118NU3nQMQ82NKzhVpXLiQMOr_7RpXV9N9UHfLxrhhWhhzPERuk=w500\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n                        </a>\n                      </td>\n                      </tr>\n<tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:10px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                              <h3 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:5px 0 5px; text-align:left; width:auto; word-break:break-word\" align=\"left\" width=\"auto\">\n                                <a href=\"https://www.google.com/appserve/mkt/p/0OQWP8RWdTE7gcgLuR3k7LoJ-TVYsraVxnd7K…9UANvNCxMvpkXbJERBDSiearz0RxCPOhKbKkBIrOGXzeeD7RSMuua4IXFjWnbhMtOMlT8QDg==\" title=\"https://www.google.com/appserve/mkt/p/0OQWP8RWdTE7gcgLuR3k7LoJ-TVYsraVxnd7K…9UANvNCxMvpkXbJERBDSiearz0RxCPOhKbKkBIrOGXzeeD7RSMuua4IXFjWnbhMtOMlT8QDg==\"  style=\"color:#333; text-decoration:none\">Pageant Material</a>\n                              </h3>\n<p style=\"color:#8d8d8d; font-family:Roboto, sans-serif-light, sans-serif; font-size:13px; font-weight:300; line-height:16px; margin:0; padding:5px 0 5px; padding-bottom:10px; text-align:inherit\" align=\"inherit\">\n                                Kacey Musgraves\n                              </p>\n</td>\n                          </tr>\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; padding-top:10px; text-align:left; vertical-align:bottom; word-break:break-word\" align=\"left\" valign=\"bottom\">\n\n                              <a href=\"https://www.google.com/appserve/mkt/p/sfCqRhS_X9NrYQXQdMjLtsR-h-QxhvfeQlipQ…MSZJGINmdkAUPlFTx1sNP1fNOzrIQ_6BdpQD8ieIMDI83S3_8o9kePbRaJGtQFOk8ETQk1xNxV\" title=\"https://www.google.com/appserve/mkt/p/sfCqRhS_X9NrYQXQdMjLtsR-h-QxhvfeQlipQ…MSZJGINmdkAUPlFTx1sNP1fNOzrIQ_6BdpQD8ieIMDI83S3_8o9kePbRaJGtQFOk8ETQk1xNxV\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:14px; font-weight:normal; line-height:1; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase; width:81%\" align=\"center\" width=\"81%\">\n\n                                Buy</a>\n\n                            </td>\n                          </tr>\n                        </table>\n                      </td>\n                    </tr>\n                  </tbody>\n                </table>\n              </td>\n<!-- Cluster Item --><td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; hyphens:auto; line-height:24px; margin:0; padding:0 5px; text-align:left; vertical-align:top; width:190px; word-break:break-word\" height=\"100%\" align=\"left\" valign=\"top\" width=\"190\">\n                <table style=\"background:#fff; border-collapse:collapse; border-radius:3px; border-spacing:0; height:100%; min-height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                  <tbody style=\"width:100%\" width=\"100%\">\n                    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:#f5f5f5; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:middle; width:50%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"50%\">\n                        <a href=\"https://www.google.com/appserve/mkt/p/oHAK9eKmrFGNkUQozxikBRP1y4TaQSkDY8XUC…i2Q4JwZ1VNAVkEwF8hi3kgeNaRW1TIqk9P_jmGGl2qTVr5A9cQiNwfrM3T0SOARWRRt8lbZg==\" title=\"https://www.google.com/appserve/mkt/p/oHAK9eKmrFGNkUQozxikBRP1y4TaQSkDY8XUC…i2Q4JwZ1VNAVkEwF8hi3kgeNaRW1TIqk9P_jmGGl2qTVr5A9cQiNwfrM3T0SOARWRRt8lbZg==\"  style=\"color:#1ABDD4; text-decoration:none\">\n                          <img alt=\"Illinois\" src=\"https://lh3.googleusercontent.com/Z1S6NcmAgaCC47U4fl-Tq1r_lRUDv9U9NGOUverSwkJsDQdoj-q2RBh_RfdRPSjPaVKfQVUx=w500\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n                        </a>\n                      </td>\n                      </tr>\n<tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:10px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                              <h3 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:5px 0 5px; text-align:left; width:auto; word-break:break-word\" align=\"left\" width=\"auto\">\n                                <a href=\"https://www.google.com/appserve/mkt/p/ppS-1gIlYMfLgyeVD5OKnNFrpirfN7p5imAw_…vDEIFLenFqBTvBhdUjX2hqCvpUFtLnHyVAzdYxLXRrVUhUwv93KSXKaBxU4USkOs0z0uQaZQ==\" title=\"https://www.google.com/appserve/mkt/p/ppS-1gIlYMfLgyeVD5OKnNFrpirfN7p5imAw_…vDEIFLenFqBTvBhdUjX2hqCvpUFtLnHyVAzdYxLXRrVUhUwv93KSXKaBxU4USkOs0z0uQaZQ==\"  style=\"color:#333; text-decoration:none\">Illinois</a>\n                              </h3>\n<p style=\"color:#8d8d8d; font-family:Roboto, sans-serif-light, sans-serif; font-size:13px; font-weight:300; line-height:16px; margin:0; padding:5px 0 5px; padding-bottom:10px; text-align:inherit\" align=\"inherit\">\n                                Brett Eldredge\n                              </p>\n</td>\n                          </tr>\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; padding-top:10px; text-align:left; vertical-align:bottom; word-break:break-word\" align=\"left\" valign=\"bottom\">\n\n                              <a href=\"https://www.google.com/appserve/mkt/p/Wq-bg8wtXPt4TKN3is_53PAhEgZkvRxYfN7xL…oNLXCE_RH6bwkUgA1mUpu-wf8srq5HRk63kpBTZo7G290VlqHsuMAd2yvxzRCQXzcS3maQSOOy\" title=\"https://www.google.com/appserve/mkt/p/Wq-bg8wtXPt4TKN3is_53PAhEgZkvRxYfN7xL…oNLXCE_RH6bwkUgA1mUpu-wf8srq5HRk63kpBTZo7G290VlqHsuMAd2yvxzRCQXzcS3maQSOOy\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:14px; font-weight:normal; line-height:1; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase; width:81%\" align=\"center\" width=\"81%\">\n\n                                Buy</a>\n\n                            </td>\n                          </tr>\n                        </table>\n                      </td>\n                    </tr>\n                  </tbody>\n                </table>\n              </td>\n<!-- Cluster Item --><td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; hyphens:auto; line-height:24px; margin:0; padding:0 5px; text-align:left; vertical-align:top; width:190px; word-break:break-word\" height=\"100%\" align=\"left\" valign=\"top\" width=\"190\">\n                <table style=\"background:#fff; border-collapse:collapse; border-radius:3px; border-spacing:0; height:100%; min-height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                  <tbody style=\"width:100%\" width=\"100%\">\n                    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:#f5f5f5; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:middle; width:50%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"50%\">\n                        <a href=\"https://www.google.com/appserve/mkt/p/AiSUyy4ybefhQTROU3JEt5eeWnt6MB3Ij8plE…TVU76M_BCQWUneOBGDobhQtCx4lTV0_qcE7F_JdNj-8Tb6ujt1jt2nejXqEnz1F9Lenp29cg==\" title=\"https://www.google.com/appserve/mkt/p/AiSUyy4ybefhQTROU3JEt5eeWnt6MB3Ij8plE…TVU76M_BCQWUneOBGDobhQtCx4lTV0_qcE7F_JdNj-8Tb6ujt1jt2nejXqEnz1F9Lenp29cg==\"  style=\"color:#1ABDD4; text-decoration:none\">\n                          <img alt=\"Platinum\" src=\"https://lh6.googleusercontent.com/vCfbHsYIJAY46Xfb4NBwmLti1WvKtwIgsDn5RgxVmpIeWNH0vqLzcmCYfTa-_Qr_MvH8IMC2WA=w500\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n                        </a>\n                      </td>\n                      </tr>\n<tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:10px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                              <h3 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:5px 0 5px; text-align:left; width:auto; word-break:break-word\" align=\"left\" width=\"auto\">\n                                <a href=\"https://www.google.com/appserve/mkt/p/nNftpHKPSDvnCZpWbciHvxWtcXELwq_DduYmE…TWlK99PigJBcD9Bn4zxdPhgf1MU2acgbHyB3rR8eD4b4ptkojDzbx6TIx7KfMWN5RtvZAXfw==\" title=\"https://www.google.com/appserve/mkt/p/nNftpHKPSDvnCZpWbciHvxWtcXELwq_DduYmE…TWlK99PigJBcD9Bn4zxdPhgf1MU2acgbHyB3rR8eD4b4ptkojDzbx6TIx7KfMWN5RtvZAXfw==\"  style=\"color:#333; text-decoration:none\">Platinum</a>\n                              </h3>\n<p style=\"color:#8d8d8d; font-family:Roboto, sans-serif-light, sans-serif; font-size:13px; font-weight:300; line-height:16px; margin:0; padding:5px 0 5px; padding-bottom:10px; text-align:inherit\" align=\"inherit\">\n                                Miranda Lambert\n                              </p>\n</td>\n                          </tr>\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; padding-top:10px; text-align:left; vertical-align:bottom; word-break:break-word\" align=\"left\" valign=\"bottom\">\n\n                              <a href=\"https://www.google.com/appserve/mkt/p/uN0F5UdQxhGGBWbHAV4NbXOtXdCdo8PcBvfXs…hZCMkTJAWAw-eFtmRU_OmYdZUlkdFC3M_cMp7KZ7aG8IrhiEAh8gHezWG4uK2pk1EC6umyZBzO\" title=\"https://www.google.com/appserve/mkt/p/uN0F5UdQxhGGBWbHAV4NbXOtXdCdo8PcBvfXs…hZCMkTJAWAw-eFtmRU_OmYdZUlkdFC3M_cMp7KZ7aG8IrhiEAh8gHezWG4uK2pk1EC6umyZBzO\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:14px; font-weight:normal; line-height:1; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase; width:81%\" align=\"center\" width=\"81%\">\n\n                                Buy</a>\n\n                            </td>\n                          </tr>\n                        </table>\n                      </td>\n                    </tr>\n                  </tbody>\n                </table>\n              </td>\n</tr>\n          </tbody>\n        </table>\n      </td>\n    </tr>\n  </tbody>\n</table>\n<!-- Element: Footer -->\n<table style=\"background:#fff; border-collapse:collapse; border-spacing:0; border-top:15px solid #fff; margin:0 auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n\n\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 10px; text-align:center; vertical-align:top; word-break:break-word\" align=\"center\" valign=\"top\">\n\n\n        <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">© 2015 Google Inc. 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</p>\n\n\n\n            <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">This message was sent to <a href=\"mailto:careless@foundry376.com\" title=\"mailto:careless@foundry376.com\"  target=\"_blank\">careless@foundry376.com</a> because you asked us to keep you up to date with the latest news and offers from Google Play. If you do not wish to receive these emails, please <a href=\"https://www.google.com/appserve/mkt/optout/1drUTvkDwpclMwiR4pPjqEX3g2tEksB-2fJ9zMfp-oWM?hl=en_US\" title=\"https://www.google.com/appserve/mkt/optout/1drUTvkDwpclMwiR4pPjqEX3g2tEksB-2fJ9zMfp-oWM?hl=en_US\"  style=\"color:#FD992E; text-decoration:underline\">unsubscribe here</a>. You can also change your email preferences on Google Play by logging in at <a href=\"https://www.google.com/appserve/mkt/p/AxB-dqY5N8vKFEjajb5YTgw3oVy1twMcyGqgACzQ1vZBtozpjKO27T_OoxtmGF25\" title=\"https://www.google.com/appserve/mkt/p/AxB-dqY5N8vKFEjajb5YTgw3oVy1twMcyGqgACzQ1vZBtozpjKO27T_OoxtmGF25\"  style=\"color:#FD992E; text-decoration:underline\">https://play.google.com/settings</a>.</p>\n\n\n\n        <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">Downloading free music, TV shows, and certain free books and magazines is still considered a transaction, even when the price of the item is $0.00. If you don&#39;t have a credit card associated with your Google Payments account or if you haven&#39;t set up a <a href=\"https://www.google.com/appserve/mkt/p/FQQJhahIGEeJ_mOZVxqDB78ei8gY948agLuTmOmy4dczruybryPE5ce3hw==\" title=\"https://www.google.com/appserve/mkt/p/FQQJhahIGEeJ_mOZVxqDB78ei8gY948agLuTmOmy4dczruybryPE5ce3hw==\"  style=\"color:#FD992E; text-decoration:underline\">Google Payments</a> account, you&#39;ll be prompted to add a new payment method upon downloading some types of content on Google Play.   </p>\n<p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">*Promotion valid while supplies last. Promotion is not transferable, cannot be sold or bartered, has no cash value, and is non-refundable. Promotion is void where prohibited by law. Requires Google Play account. Offer good for users 13+ in United States only. Compatible internet connected devices required. Google reserves the right to terminate or modify this promotion. © Google Inc.</p>\n      </td>\n    </tr>\n\n  </tbody>\n</table>\n                  </center>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        <img height=\"1\" width=\"3\" src=\"https://#\"></body>\n      </html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/no-image-extensions-out.html",
    "content": "<!doctype html>\n<html id=\"email\" style=\"background:#fff\">\n        <head>\n          <meta charset=\"utf-8\">\n          <meta content=\"width=device-width\" name=\"viewport\">\n          <title>Google Play</title>\n        <style>@import url(<a href=\"https://fonts.googleapis.com/css?family=Roboto:300,400\" title=\"https://fonts.googleapis.com/css?family=Roboto:300,400\"  target=\"_blank\">fonts.googleapis.com/css?family=Roboto:300,400</a>);</style>\n</head>\n        <body style=\"-ms-text-size-adjust:100%; -webkit-text-size-adjust:100%; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:24px; margin:0; min-width:100%; padding:0; text-align:left; width:100%\" align=\"left\" width=\"100%\">\n          <table style=\"border-collapse:collapse; border-spacing:0; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n            <tbody style=\"width:100%\" width=\"100%\">\n              <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:center; vertical-align:top; word-break:break-word\" align=\"center\" valign=\"top\">\n                  <center style=\"background:#e5e5e5; margin:0 auto; width:600px\" width=\"600\">\n\n<!--\n\n-->\n\n<!---- Element: Header ----><!-- Logo -->\n<!--\n<table class=\"container logo vertical En\">\n  <tbody>\n    <tr>\n      <td class=\"center\">\n        <a href=\"https://www.google.com/appserve/mkt/p/fNOMGzHPj10D6yJ55kIL5WtTkJNBsiBffQQqY…xq08_OyAB_MJCHiYLWNYGwVSqo9Rr0CskMnPCQdvff-RpcSEuqLH6cYNE81spIIlD963du-w==\" title=\"https://www.google.com/appserve/mkt/p/fNOMGzHPj10D6yJ55kIL5WtTkJNBsiBffQQqY…xq08_OyAB_MJCHiYLWNYGwVSqo9Rr0CskMnPCQdvff-RpcSEuqLH6cYNE81spIIlD963du-w==\" >\n\n            <img alt=\"Google Play\" src=\"#\">\n\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n-->\n\n<!-- Subject -->\n<table style=\"background:#e5e5e5; border-collapse:collapse; border-spacing:0; margin:0 auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:url(#) #FF8D1C bottom left no-repeat; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:30px 0 5px; text-align:left; vertical-align:top; width:100%; word-break:break-word\" align=\"left\" valign=\"top\" width=\"100%\">\n        <h1 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:30px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:0 10px; text-align:center; width:auto; word-break:break-word\" align=\"center\" width=\"auto\">\n          <a href=\"https://www.google.com/appserve/mkt/p/e6HiYpv26sL9mFqIIgWM6se-DPkp7HE9YRTkq…cP8KDJnXRS2O1q6MeeYBJE3jLx6F49EjFASGrGvIjOGhgkrODhhxfgPJdcp4meBCX9MI-NGQ==\" title=\"https://www.google.com/appserve/mkt/p/e6HiYpv26sL9mFqIIgWM6se-DPkp7HE9YRTkq…cP8KDJnXRS2O1q6MeeYBJE3jLx6F49EjFASGrGvIjOGhgkrODhhxfgPJdcp4meBCX9MI-NGQ==\"  style=\"color:#fff; text-decoration:none\">Album of the Week</a>\n        </h1>\n      </td>\n    </tr>\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:url(#) top left no-repeat; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:55px; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:center; vertical-align:top; width:100%; word-break:break-word\" height=\"55\" align=\"center\" valign=\"top\" width=\"100%\">\n        <a href=\"https://www.google.com/appserve/mkt/p/PMAjz4TGGt1Tchz6z_tPE4evzfMiN2W0w348d…ZRF7Bh63Hn6LJIoOc_KCsQC__1_eaq7P4sJ8QR9UNPidl519mA_QLN0lzWl0gs9Mdk0BK6Evrd\" title=\"https://www.google.com/appserve/mkt/p/PMAjz4TGGt1Tchz6z_tPE4evzfMiN2W0w348d…ZRF7Bh63Hn6LJIoOc_KCsQC__1_eaq7P4sJ8QR9UNPidl519mA_QLN0lzWl0gs9Mdk0BK6Evrd\"  style=\"color:#1ABDD4; display:block; height:50px; margin:0 auto; text-decoration:none; width:50px\" height=\"50\" width=\"50\">\n          <img alt=\"Google Play Music\" src=\"#\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n        </a>\n      </td>\n    </tr>\n  </tbody>\n</table>\n<!-- Element: Text --><table style=\"background:#e5e5e5; border-collapse:collapse; border-spacing:0; margin:0 auto 5px; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\"><tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 10px; text-align:left; vertical-align:middle; width:40%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"40%\">\n\n\n            <a href=\"https://www.google.com/appserve/mkt/p/GzRjOEZZFlCnx5uuzOpjtk0sppniyYqy7pEsc…URW_lr1MiKVdShxC6awps3uvbPgJUaRxqKV7PKFEL8O7JktbnOcmwiE6KZ4MI8ncDxhWehvTzZ\" title=\"https://www.google.com/appserve/mkt/p/GzRjOEZZFlCnx5uuzOpjtk0sppniyYqy7pEsc…URW_lr1MiKVdShxC6awps3uvbPgJUaRxqKV7PKFEL8O7JktbnOcmwiE6KZ4MI8ncDxhWehvTzZ\"  style=\"color:#1ABDD4; text-decoration:none\"><img src=\"#\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\"></a>\n\n\n      </td>\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 10px; text-align:left; vertical-align:middle; width:60%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"60%\">\n\n\n\n          <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:14px; font-weight:300; line-height:20px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">Cowboy hat? Check. Crushin&#39; it? Check. Now all you need is Brad Paisley&#39;s latest album, Moonshine in the Trunk. Lucky for you, it’s free on Google Play for a limited time.*</p>\n\n\n\n          <a href=\"https://www.google.com/appserve/mkt/p/UO6vTvTLoG5BzPQTiW5l7xdTVD8KReLPIizUq…FelDRdpvg8zJHW0CgRmCCJD-5WgvbFz1XF7B3TOEEXTVCH-QDfhF0-jSMYtdjr0JLEeEezZ7Q=\" title=\"https://www.google.com/appserve/mkt/p/UO6vTvTLoG5BzPQTiW5l7xdTVD8KReLPIizUq…FelDRdpvg8zJHW0CgRmCCJD-5WgvbFz1XF7B3TOEEXTVCH-QDfhF0-jSMYtdjr0JLEeEezZ7Q=\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:15px; font-weight:normal; line-height:1; margin:10px 0 15px; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase\" align=\"center\">Get It Free</a>\n\n      </td>\n    </tr></tbody>\n</table>\n<!-- Element: Text --><table style=\"background:#FD992E; border-collapse:collapse; border-spacing:0; margin:10px auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n      <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n        <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 20px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\"><h2 style=\"color:#fff; font-family:Roboto, sans-serif-light, sans-serif; font-size:24px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:20px 0; text-align:center; width:auto; word-break:break-word\" align=\"center\" width=\"auto\"><a href=\"https://www.google.com/appserve/mkt/p/SVsU1sviGu_iJ0RZUhJKcMD-ZsNDwZpb32tD_…74KSLiQYsgzu_daLsgmPYVdnddyJYyfFA3s2K6jPVIs4c3I792KFrUzTSmpiSi4HWcoleE0LP1\" title=\"https://www.google.com/appserve/mkt/p/SVsU1sviGu_iJ0RZUhJKcMD-ZsNDwZpb32tD_…74KSLiQYsgzu_daLsgmPYVdnddyJYyfFA3s2K6jPVIs4c3I792KFrUzTSmpiSi4HWcoleE0LP1\"  style=\"color:#fff; text-decoration:none\">On sale: Country greats</a></h2></td>\n\n          <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 20px; padding-left:0; text-align:right; vertical-align:middle; word-break:break-word\" align=\"right\" valign=\"middle\"><a href=\"https://www.google.com/appserve/mkt/p/IcqipRC60sigr_KABSeXuJfYijqVCBV0uYixa…Sy7gdnud9ELh9vEN7R-E7Zxl4FHG6KW4E6jIK-2J1UmnBvWVYEm_C6KDblkSIvLgFxVotoT9g=\" title=\"https://www.google.com/appserve/mkt/p/IcqipRC60sigr_KABSeXuJfYijqVCBV0uYixa…Sy7gdnud9ELh9vEN7R-E7Zxl4FHG6KW4E6jIK-2J1UmnBvWVYEm_C6KDblkSIvLgFxVotoT9g=\"  style=\"background:transparent; border:1px solid #fff; border-radius:3px; color:#fff; display:block; font-size:14px; font-weight:normal; line-height:1; margin:0; min-width:60px; padding:10px 20px; text-align:center; text-decoration:none; text-transform:uppercase; white-space:nowrap\" align=\"center\">SEE ALL</a></td>\n\n      </tr>\n\n\n    </tbody>\n</table>\n<!---- Element: Cluster ---->\n\n<table style=\"background:#e5e5e5; border-collapse:collapse; border-spacing:0; margin:10px auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n<!-- 3 Across -->\n        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; margin-bottom:5px; max-width:600px; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n          <tbody style=\"width:100%\" width=\"100%\">\n            <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n<!-- Cluster Item --><td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; hyphens:auto; line-height:24px; margin:0; padding:0 5px; text-align:left; vertical-align:top; width:190px; word-break:break-word\" height=\"100%\" align=\"left\" valign=\"top\" width=\"190\">\n                <table style=\"background:#fff; border-collapse:collapse; border-radius:3px; border-spacing:0; height:100%; min-height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                  <tbody style=\"width:100%\" width=\"100%\">\n                    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:#f5f5f5; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:middle; width:50%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"50%\">\n                        <a href=\"https://www.google.com/appserve/mkt/p/zgknewdZyNxLoFrq66voips4aGwP5MTvgRD61…ciGJAHqfLiXsPKvDNxV9vbtyMhdGPdDcVmOUSj3dEHETCpEYCNOpOXzyLzOamiOheDBJH6Ew==\" title=\"https://www.google.com/appserve/mkt/p/zgknewdZyNxLoFrq66voips4aGwP5MTvgRD61…ciGJAHqfLiXsPKvDNxV9vbtyMhdGPdDcVmOUSj3dEHETCpEYCNOpOXzyLzOamiOheDBJH6Ew==\"  style=\"color:#1ABDD4; text-decoration:none\">\n                          <img alt=\"Pageant Material\" src=\"#\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n                        </a>\n                      </td>\n                      </tr>\n<tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:10px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                              <h3 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:5px 0 5px; text-align:left; width:auto; word-break:break-word\" align=\"left\" width=\"auto\">\n                                <a href=\"https://www.google.com/appserve/mkt/p/0OQWP8RWdTE7gcgLuR3k7LoJ-TVYsraVxnd7K…9UANvNCxMvpkXbJERBDSiearz0RxCPOhKbKkBIrOGXzeeD7RSMuua4IXFjWnbhMtOMlT8QDg==\" title=\"https://www.google.com/appserve/mkt/p/0OQWP8RWdTE7gcgLuR3k7LoJ-TVYsraVxnd7K…9UANvNCxMvpkXbJERBDSiearz0RxCPOhKbKkBIrOGXzeeD7RSMuua4IXFjWnbhMtOMlT8QDg==\"  style=\"color:#333; text-decoration:none\">Pageant Material</a>\n                              </h3>\n<p style=\"color:#8d8d8d; font-family:Roboto, sans-serif-light, sans-serif; font-size:13px; font-weight:300; line-height:16px; margin:0; padding:5px 0 5px; padding-bottom:10px; text-align:inherit\" align=\"inherit\">\n                                Kacey Musgraves\n                              </p>\n</td>\n                          </tr>\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; padding-top:10px; text-align:left; vertical-align:bottom; word-break:break-word\" align=\"left\" valign=\"bottom\">\n\n                              <a href=\"https://www.google.com/appserve/mkt/p/sfCqRhS_X9NrYQXQdMjLtsR-h-QxhvfeQlipQ…MSZJGINmdkAUPlFTx1sNP1fNOzrIQ_6BdpQD8ieIMDI83S3_8o9kePbRaJGtQFOk8ETQk1xNxV\" title=\"https://www.google.com/appserve/mkt/p/sfCqRhS_X9NrYQXQdMjLtsR-h-QxhvfeQlipQ…MSZJGINmdkAUPlFTx1sNP1fNOzrIQ_6BdpQD8ieIMDI83S3_8o9kePbRaJGtQFOk8ETQk1xNxV\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:14px; font-weight:normal; line-height:1; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase; width:81%\" align=\"center\" width=\"81%\">\n\n                                Buy</a>\n\n                            </td>\n                          </tr>\n                        </table>\n                      </td>\n                    </tr>\n                  </tbody>\n                </table>\n              </td>\n<!-- Cluster Item --><td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; hyphens:auto; line-height:24px; margin:0; padding:0 5px; text-align:left; vertical-align:top; width:190px; word-break:break-word\" height=\"100%\" align=\"left\" valign=\"top\" width=\"190\">\n                <table style=\"background:#fff; border-collapse:collapse; border-radius:3px; border-spacing:0; height:100%; min-height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                  <tbody style=\"width:100%\" width=\"100%\">\n                    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:#f5f5f5; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:middle; width:50%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"50%\">\n                        <a href=\"https://www.google.com/appserve/mkt/p/oHAK9eKmrFGNkUQozxikBRP1y4TaQSkDY8XUC…i2Q4JwZ1VNAVkEwF8hi3kgeNaRW1TIqk9P_jmGGl2qTVr5A9cQiNwfrM3T0SOARWRRt8lbZg==\" title=\"https://www.google.com/appserve/mkt/p/oHAK9eKmrFGNkUQozxikBRP1y4TaQSkDY8XUC…i2Q4JwZ1VNAVkEwF8hi3kgeNaRW1TIqk9P_jmGGl2qTVr5A9cQiNwfrM3T0SOARWRRt8lbZg==\"  style=\"color:#1ABDD4; text-decoration:none\">\n                          <img alt=\"Illinois\" src=\"#\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n                        </a>\n                      </td>\n                      </tr>\n<tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:10px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                              <h3 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:5px 0 5px; text-align:left; width:auto; word-break:break-word\" align=\"left\" width=\"auto\">\n                                <a href=\"https://www.google.com/appserve/mkt/p/ppS-1gIlYMfLgyeVD5OKnNFrpirfN7p5imAw_…vDEIFLenFqBTvBhdUjX2hqCvpUFtLnHyVAzdYxLXRrVUhUwv93KSXKaBxU4USkOs0z0uQaZQ==\" title=\"https://www.google.com/appserve/mkt/p/ppS-1gIlYMfLgyeVD5OKnNFrpirfN7p5imAw_…vDEIFLenFqBTvBhdUjX2hqCvpUFtLnHyVAzdYxLXRrVUhUwv93KSXKaBxU4USkOs0z0uQaZQ==\"  style=\"color:#333; text-decoration:none\">Illinois</a>\n                              </h3>\n<p style=\"color:#8d8d8d; font-family:Roboto, sans-serif-light, sans-serif; font-size:13px; font-weight:300; line-height:16px; margin:0; padding:5px 0 5px; padding-bottom:10px; text-align:inherit\" align=\"inherit\">\n                                Brett Eldredge\n                              </p>\n</td>\n                          </tr>\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; padding-top:10px; text-align:left; vertical-align:bottom; word-break:break-word\" align=\"left\" valign=\"bottom\">\n\n                              <a href=\"https://www.google.com/appserve/mkt/p/Wq-bg8wtXPt4TKN3is_53PAhEgZkvRxYfN7xL…oNLXCE_RH6bwkUgA1mUpu-wf8srq5HRk63kpBTZo7G290VlqHsuMAd2yvxzRCQXzcS3maQSOOy\" title=\"https://www.google.com/appserve/mkt/p/Wq-bg8wtXPt4TKN3is_53PAhEgZkvRxYfN7xL…oNLXCE_RH6bwkUgA1mUpu-wf8srq5HRk63kpBTZo7G290VlqHsuMAd2yvxzRCQXzcS3maQSOOy\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:14px; font-weight:normal; line-height:1; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase; width:81%\" align=\"center\" width=\"81%\">\n\n                                Buy</a>\n\n                            </td>\n                          </tr>\n                        </table>\n                      </td>\n                    </tr>\n                  </tbody>\n                </table>\n              </td>\n<!-- Cluster Item --><td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; height:100%; hyphens:auto; line-height:24px; margin:0; padding:0 5px; text-align:left; vertical-align:top; width:190px; word-break:break-word\" height=\"100%\" align=\"left\" valign=\"top\" width=\"190\">\n                <table style=\"background:#fff; border-collapse:collapse; border-radius:3px; border-spacing:0; height:100%; min-height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                  <tbody style=\"width:100%\" width=\"100%\">\n                    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; background:#f5f5f5; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:middle; width:50%; word-break:break-word\" align=\"left\" valign=\"middle\" width=\"50%\">\n                        <a href=\"https://www.google.com/appserve/mkt/p/AiSUyy4ybefhQTROU3JEt5eeWnt6MB3Ij8plE…TVU76M_BCQWUneOBGDobhQtCx4lTV0_qcE7F_JdNj-8Tb6ujt1jt2nejXqEnz1F9Lenp29cg==\" title=\"https://www.google.com/appserve/mkt/p/AiSUyy4ybefhQTROU3JEt5eeWnt6MB3Ij8plE…TVU76M_BCQWUneOBGDobhQtCx4lTV0_qcE7F_JdNj-8Tb6ujt1jt2nejXqEnz1F9Lenp29cg==\"  style=\"color:#1ABDD4; text-decoration:none\">\n                          <img alt=\"Platinum\" src=\"#\" style=\"-ms-interpolation-mode:bicubic; border:none; clear:both; display:block; max-width:100%; outline:none; text-decoration:none; width:100%\" width=\"100%\">\n                        </a>\n                      </td>\n                      </tr>\n<tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:10px; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                        <table style=\"border-collapse:collapse; border-spacing:0; height:100%; padding:0; text-align:left; vertical-align:top; width:100%\" height=\"100%\" align=\"left\" valign=\"top\" width=\"100%\">\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; text-align:left; vertical-align:top; word-break:break-word\" align=\"left\" valign=\"top\">\n                              <h3 style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; line-height:1.3; margin:0; max-width:inherit; padding:5px 0 5px; text-align:left; width:auto; word-break:break-word\" align=\"left\" width=\"auto\">\n                                <a href=\"https://www.google.com/appserve/mkt/p/nNftpHKPSDvnCZpWbciHvxWtcXELwq_DduYmE…TWlK99PigJBcD9Bn4zxdPhgf1MU2acgbHyB3rR8eD4b4ptkojDzbx6TIx7KfMWN5RtvZAXfw==\" title=\"https://www.google.com/appserve/mkt/p/nNftpHKPSDvnCZpWbciHvxWtcXELwq_DduYmE…TWlK99PigJBcD9Bn4zxdPhgf1MU2acgbHyB3rR8eD4b4ptkojDzbx6TIx7KfMWN5RtvZAXfw==\"  style=\"color:#333; text-decoration:none\">Platinum</a>\n                              </h3>\n<p style=\"color:#8d8d8d; font-family:Roboto, sans-serif-light, sans-serif; font-size:13px; font-weight:300; line-height:16px; margin:0; padding:5px 0 5px; padding-bottom:10px; text-align:inherit\" align=\"inherit\">\n                                Miranda Lambert\n                              </p>\n</td>\n                          </tr>\n                          <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n                            <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; display:table-cell; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0; padding-top:10px; text-align:left; vertical-align:bottom; word-break:break-word\" align=\"left\" valign=\"bottom\">\n\n                              <a href=\"https://www.google.com/appserve/mkt/p/uN0F5UdQxhGGBWbHAV4NbXOtXdCdo8PcBvfXs…hZCMkTJAWAw-eFtmRU_OmYdZUlkdFC3M_cMp7KZ7aG8IrhiEAh8gHezWG4uK2pk1EC6umyZBzO\" title=\"https://www.google.com/appserve/mkt/p/uN0F5UdQxhGGBWbHAV4NbXOtXdCdo8PcBvfXs…hZCMkTJAWAw-eFtmRU_OmYdZUlkdFC3M_cMp7KZ7aG8IrhiEAh8gHezWG4uK2pk1EC6umyZBzO\"  style=\"background:#FD992E; border:none; border-radius:3px; color:#fff; display:inline-block; font-size:14px; font-weight:normal; line-height:1; min-width:60px; padding:8px 16px; text-align:center; text-decoration:none; text-transform:uppercase; width:81%\" align=\"center\" width=\"81%\">\n\n                                Buy</a>\n\n                            </td>\n                          </tr>\n                        </table>\n                      </td>\n                    </tr>\n                  </tbody>\n                </table>\n              </td>\n</tr>\n          </tbody>\n        </table>\n      </td>\n    </tr>\n  </tbody>\n</table>\n<!-- Element: Footer -->\n<table style=\"background:#fff; border-collapse:collapse; border-spacing:0; border-top:15px solid #fff; margin:0 auto; padding:0; text-align:inherit; vertical-align:top; width:600px\" align=\"inherit\" valign=\"top\" width=\"600\">\n  <tbody style=\"width:100%\" width=\"100%\">\n\n\n    <tr style=\"padding:0; text-align:left; vertical-align:top\" align=\"left\" valign=\"top\">\n      <td style=\"-moz-hyphens:auto; -webkit-hyphens:auto; border-collapse:collapse; color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:16px; font-weight:300; hyphens:auto; line-height:24px; margin:0; padding:0 10px; text-align:center; vertical-align:top; word-break:break-word\" align=\"center\" valign=\"top\">\n\n\n        <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">© 2015 Google Inc. 1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</p>\n\n\n\n            <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">This message was sent to <a href=\"mailto:careless@foundry376.com\" title=\"mailto:careless@foundry376.com\"  target=\"_blank\">careless@foundry376.com</a> because you asked us to keep you up to date with the latest news and offers from Google Play. If you do not wish to receive these emails, please <a href=\"https://www.google.com/appserve/mkt/optout/1drUTvkDwpclMwiR4pPjqEX3g2tEksB-2fJ9zMfp-oWM?hl=en_US\" title=\"https://www.google.com/appserve/mkt/optout/1drUTvkDwpclMwiR4pPjqEX3g2tEksB-2fJ9zMfp-oWM?hl=en_US\"  style=\"color:#FD992E; text-decoration:underline\">unsubscribe here</a>. You can also change your email preferences on Google Play by logging in at <a href=\"https://www.google.com/appserve/mkt/p/AxB-dqY5N8vKFEjajb5YTgw3oVy1twMcyGqgACzQ1vZBtozpjKO27T_OoxtmGF25\" title=\"https://www.google.com/appserve/mkt/p/AxB-dqY5N8vKFEjajb5YTgw3oVy1twMcyGqgACzQ1vZBtozpjKO27T_OoxtmGF25\"  style=\"color:#FD992E; text-decoration:underline\">https://play.google.com/settings</a>.</p>\n\n\n\n        <p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">Downloading free music, TV shows, and certain free books and magazines is still considered a transaction, even when the price of the item is $0.00. If you don&#39;t have a credit card associated with your Google Payments account or if you haven&#39;t set up a <a href=\"https://www.google.com/appserve/mkt/p/FQQJhahIGEeJ_mOZVxqDB78ei8gY948agLuTmOmy4dczruybryPE5ce3hw==\" title=\"https://www.google.com/appserve/mkt/p/FQQJhahIGEeJ_mOZVxqDB78ei8gY948agLuTmOmy4dczruybryPE5ce3hw==\"  style=\"color:#FD992E; text-decoration:underline\">Google Payments</a> account, you&#39;ll be prompted to add a new payment method upon downloading some types of content on Google Play.   </p>\n<p style=\"color:#606060; font-family:Roboto, sans-serif-light, sans-serif; font-size:10px; font-weight:300; line-height:16px; margin:0; padding:0; padding-bottom:10px; text-align:inherit\" align=\"inherit\">*Promotion valid while supplies last. Promotion is not transferable, cannot be sold or bartered, has no cash value, and is non-refundable. Promotion is void where prohibited by law. Requires Google Play account. Offer good for users 13+ in United States only. Compatible internet connected devices required. Google reserves the right to terminate or modify this promotion. © Google Inc.</p>\n      </td>\n    </tr>\n\n  </tbody>\n</table>\n                  </center>\n                </td>\n              </tr>\n            </tbody>\n          </table>\n        <img height=\"1\" width=\"3\" src=\"#\"></body>\n      </html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/table-body-in.html",
    "content": "<table width=\"598\" class=\"deviceWidth\" height=\"403\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" background=\"https://services.google.com/fh/files/emails/bkg_billboard_v2.png\" bgcolor=\"#42a0fc\" style=\"-webkit-text-size-adjust: none; border-collapse: collapse !important; mso-table-lspace: 0pt !important; mso-table-rspace: 0pt !important; border: 0px; display:block;width:598px;  min-width:598px;\">\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/spec/fixtures/table-body-out.html",
    "content": "<table width=\"598\" class=\"deviceWidth\" height=\"403\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" background=\"#\" bgcolor=\"#42a0fc\" style=\"-webkit-text-size-adjust: none; border-collapse: collapse !important; mso-table-lspace: 0pt !important; mso-table-rspace: 0pt !important; border: 0px; display:block;width:598px;  min-width:598px;\">\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-autoload-images/stylesheets/message-autoload-images.less",
    "content": "@import \"ui-variables\";\n\n.autoload-images-header {\n  background-color: mix(@background-primary, #FFCC11, 80%);\n  border: 1px solid darken(mix(@background-primary, #FFCC11, 50%), 25%);\n  color: mix(@text-color-subtle, #FFCC11, 40%);\n  margin: @padding-base-vertical 0;\n  padding: @padding-base-vertical @padding-base-horizontal;\n  white-space: nowrap;\n  overflow: hidden;\n  text-overflow: ellipsis;\n\n  .option {\n    color: fade(mix(@text-color-subtle, #FFCC11, 80%), 70%);\n  }\n  .option:hover {\n    color: mix(@text-color-subtle, #FFCC11, 80%);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/autolinker.es6",
    "content": "import {RegExpUtils, DOMUtils} from 'nylas-exports';\n\nfunction _matchesAnyRegexp(text, regexps) {\n  for (const excludeRegexp of regexps) {\n    if (excludeRegexp.test(text)) {\n      return true;\n    }\n  }\n  return false;\n}\n\nfunction _runOnTextNode(node, matchers) {\n  if (node.parentElement) {\n    const withinScript = node.parentElement.tagName === \"SCRIPT\";\n    const withinStyle = node.parentElement.tagName === \"STYLE\";\n    const withinA = (node.parentElement.closest('a') !== null);\n    if (withinScript || withinA || withinStyle) {\n      return;\n    }\n  }\n  if (node.textContent.trim().length < 4) {\n    return;\n  }\n\n  let longest = null;\n  let longestLength = null;\n  for (const [prefix, regex, options = {}] of matchers) {\n    regex.lastIndex = 0;\n    const match = regex.exec(node.textContent);\n    if (match !== null) {\n      if (options.exclude && _matchesAnyRegexp(match[0], options.exclude)) {\n        continue;\n      }\n      if (match[0].length > longestLength) {\n        longest = [prefix, match];\n        longestLength = match[0].length;\n      }\n    }\n  }\n\n  if (longest) {\n    const [prefix, match] = longest;\n    const href = `${prefix}${match[0]}`;\n    const range = document.createRange();\n    range.setStart(node, match.index);\n    range.setEnd(node, match.index + match[0].length);\n    const aTag = DOMUtils.wrap(range, 'A');\n    aTag.href = href;\n    aTag.title = href;\n    return;\n  }\n}\n\nexport function autolink(doc, {async} = {}) {\n  // Traverse the new DOM tree and make things that look like links clickable,\n  // and ensure anything with an href has a title attribute.\n  const textWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_TEXT);\n  const matchers = [\n    ['mailto:', RegExpUtils.emailRegex(), {\n      // Technically, gmail.com/bengotow@gmail.com is an email address. After\n      // matching, manully exclude any email that follows the .*[/?].*@ pattern.\n      exclude: [/\\..*[/|?].*@/],\n    }],\n    ['tel:', RegExpUtils.phoneRegex()],\n    ['', RegExpUtils.nylasCommandRegex()],\n    ['', RegExpUtils.urlRegex({matchEntireString: false})],\n  ];\n\n  if (async) {\n    const fn = (deadline) => {\n      while (textWalker.nextNode()) {\n        _runOnTextNode(textWalker.currentNode, matchers);\n        if (deadline.timeRemaining() <= 0) {\n          window.requestIdleCallback(fn, {timeout: 500});\n          return;\n        }\n      }\n    };\n    window.requestIdleCallback(fn, {timeout: 500});\n  } else {\n    while (textWalker.nextNode()) {\n      _runOnTextNode(textWalker.currentNode, matchers);\n    }\n  }\n\n  // Traverse the new DOM tree and make sure everything with an href has a title.\n  const aTagWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, {\n    acceptNode: (node) =>\n      (node.href ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP)\n    ,\n  });\n  while (aTagWalker.nextNode()) {\n    aTagWalker.currentNode.title = aTagWalker.currentNode.getAttribute('href');\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/autoscale-images.es6",
    "content": "\nfunction _getDimension(node, dim) {\n  const raw = node.style[dim] || node[dim];\n  if (!raw) {\n    return [null, ''];\n  }\n  const valueRegexp = /(\\d*)(.*)/;\n  const match = valueRegexp.exec(raw);\n  if (!match) {\n    return [null, ''];\n  }\n\n  const value = match[1];\n  const units = match[2] || 'px';\n  return [value / 1, units];\n}\n\nfunction _runOnImageNode(node) {\n  const [width, widthUnits] = _getDimension(node, 'width');\n  const [height, heightUnits] = _getDimension(node, 'height');\n\n  if (node.style.maxWidth || node.style.maxHeight) {\n    return;\n  }\n  // VW is like %, but always basd on the iframe width, regardless of whether\n  // a container is position: relative.\n  // https://web-design-weekly.com/2014/11/18/viewport-units-vw-vh-vmin-vmax/\n  if (width && height && (widthUnits === heightUnits)) {\n    node.style.maxWidth = '100vw';\n    node.style.maxHeight = `${100 * height / width}vw`;\n  } else if (!height) {\n    node.style.maxWidth = '100vw';\n  } else {\n    // If your image has a width and height in different units, or a height and\n    // no width, we don't want to screw with it because it would change the\n    // aspect ratio.\n  }\n}\n\nexport function autoscaleImages(doc) {\n  const imgTagWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, {\n    acceptNode: (node) => {\n      if (node.nodeName === 'IMG') {\n        return NodeFilter.FILTER_ACCEPT;\n      }\n      return NodeFilter.FILTER_SKIP;\n    },\n  });\n\n  while (imgTagWalker.nextNode()) {\n    _runOnImageNode(imgTagWalker.currentNode);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/email-frame-styles-store.coffee",
    "content": "NylasStore = require 'nylas-store'\n\nclass EmailFrameStylesStore extends NylasStore\n\n  constructor: ->\n\n  styles: =>\n    if not @_styles\n      @_findStyles()\n      @_listenToStyles()\n    @_styles\n\n  _findStyles: =>\n    @_styles = \"\"\n    for sheet in document.querySelectorAll('[source-path*=\"email-frame.less\"]')\n      @_styles += \"\\n\"+sheet.innerText\n    @_styles = @_styles.replace(/.ignore-in-parent-frame/g, '')\n    @trigger()\n\n  _listenToStyles: =>\n    target = document.getElementsByTagName('nylas-styles')[0]\n    @_mutationObserver = new MutationObserver(@_findStyles)\n    @_mutationObserver.observe(target, attributes: true, subtree: true, childList: true)\n\n  _unlistenToStyles: =>\n    @_mutationObserver?.disconnect()\n\n  module.exports = new EmailFrameStylesStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/email-frame.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport _ from \"underscore\";\nimport {EventedIFrame} from 'nylas-component-kit';\nimport {Utils, QuotedHTMLTransformer, MessageStore} from 'nylas-exports';\nimport {autolink} from './autolinker';\nimport {autoscaleImages} from './autoscale-images';\nimport {addInlineImageListeners} from './inline-image-listeners';\nimport EmailFrameStylesStore from './email-frame-styles-store';\n\nexport default class EmailFrame extends React.Component {\n  static displayName = 'EmailFrame';\n\n  static propTypes = {\n    content: React.PropTypes.string.isRequired,\n    message: React.PropTypes.object,\n    showQuotedText: React.PropTypes.bool,\n    onLoad: React.PropTypes.func,\n  };\n\n  componentDidMount() {\n    this._mounted = true;\n    this._writeContent();\n    this._unlisten = EmailFrameStylesStore.listen(this._writeContent);\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) ||\n            !Utils.isEqualReact(nextState, this.state));\n  }\n\n  componentDidUpdate() {\n    this._writeContent();\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n    if (this._unlisten) {\n      this._unlisten();\n    }\n  }\n\n  _emailContent = () => {\n    // When showing quoted text, always return the pure content\n    if (this.props.showQuotedText) {\n      return this.props.content;\n    }\n    return QuotedHTMLTransformer.removeQuotedHTML(this.props.content, {\n      keepIfWholeBodyIsQuote: true,\n    });\n  }\n\n  _writeContent = () => {\n    const iframeNode = ReactDOM.findDOMNode(this.refs.iframe);\n    const doc = iframeNode.contentDocument;\n    if (!doc) { return; }\n    doc.open();\n\n    // NOTE: The iframe must have a modern DOCTYPE. The lack of this line\n    // will cause some bizzare non-standards compliant rendering with the\n    // message bodies. This is particularly felt with <table> elements use\n    // the `border-collapse: collapse` css property while setting a\n    // `padding`.\n    doc.write(\"<!DOCTYPE html>\");\n    const styles = EmailFrameStylesStore.styles();\n    if (styles) {\n      doc.write(`<style>${styles}</style>`);\n    }\n    doc.write(`<div id='inbox-html-wrapper'>${this._emailContent()}</div>`);\n    doc.close();\n\n    autolink(doc, {async: true});\n    autoscaleImages(doc);\n    addInlineImageListeners(doc);\n\n    for (const extension of MessageStore.extensions()) {\n      if (!extension.renderedMessageBodyIntoDocument) {\n        continue;\n      }\n      try {\n        extension.renderedMessageBodyIntoDocument({\n          document: doc,\n          message: this.props.message,\n          iframe: iframeNode,\n        });\n      } catch (e) {\n        NylasEnv.reportError(e);\n      }\n    }\n\n    // Notify the EventedIFrame that we've replaced it's document (with `open`)\n    // so it can attach event listeners again.\n    this.refs.iframe.didReplaceDocument();\n    this._onMustRecalculateFrameHeight();\n  }\n\n  _onMustRecalculateFrameHeight = () => {\n    this.refs.iframe.setHeightQuietly(0);\n    this._lastComputedHeight = 0;\n    this._setFrameHeight();\n  }\n\n  _getFrameHeight = (doc) => {\n    let height = 0;\n\n    // If documentElement has a scroll height, prioritize that as height\n    // If not, fall back to body scroll height by setting it to auto\n    if (doc && doc.documentElement && doc.documentElement.scrollHeight > 0) {\n      height = doc.documentElement.scrollHeight;\n    } else if (doc && doc.body) {\n      const style = window.getComputedStyle(doc.body);\n      if (style.height === '0px') {\n        doc.body.style.height = \"auto\";\n      }\n      height = doc.body.scrollHeight;\n    }\n\n    // scrollHeight does not include space required by scrollbar\n    return height + 25;\n  }\n\n  _setFrameHeight = () => {\n    if (!this._mounted) {\n      return;\n    }\n\n    // Q: What's up with this holder?\n    // A: If you resize the window, or do something to trigger setFrameHeight\n    // on an already-loaded message view, all the heights go to zero for a brief\n    // second while the heights are recomputed. This causes the ScrollRegion to\n    // reset it's scrollTop to ~0 (the new combined heiht of all children).\n    // To prevent this, the holderNode holds the last computed height until\n    // the new height is computed.\n    const holderNode = ReactDOM.findDOMNode(this.refs.iframeHeightHolder);\n    const iframeNode = ReactDOM.findDOMNode(this.refs.iframe);\n    const height = this._getFrameHeight(iframeNode.contentDocument);\n\n    // Why 5px? Some emails have elements with a height of 100%, and then put\n    // tracking pixels beneath that. In these scenarios, the scrollHeight of the\n    // message is always <100% + 1px>, which leads us to resize them constantly.\n    // This is a hack, but I'm not sure of a better solution.\n    if (Math.abs(height - this._lastComputedHeight) > 5) {\n      this.refs.iframe.setHeightQuietly(height);\n      holderNode.style.height = `${height}px`;\n      this._lastComputedHeight = height;\n    }\n\n    if (iframeNode.contentDocument.readyState !== 'complete') {\n      _.defer(() => this._setFrameHeight());\n    }\n  }\n\n  render() {\n    return (\n      <div\n        className=\"iframe-container\"\n        ref=\"iframeHeightHolder\"\n        style={{height: this._lastComputedHeight}}\n      >\n        <EventedIFrame\n          ref=\"iframe\"\n          seamless=\"seamless\"\n          searchable\n          onLoad={this.props.onLoad}\n          onResize={this._onMustRecalculateFrameHeight}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/find-in-thread.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport classnames from 'classnames'\nimport {Actions, MessageStore, SearchableComponentStore} from 'nylas-exports'\nimport {RetinaImg, KeyCommandsRegion} from 'nylas-component-kit'\n\nexport default class FindInThread extends React.Component {\n  static displayName = \"FindInThread\";\n\n  constructor(props) {\n    super(props);\n    this.state = SearchableComponentStore.getCurrentSearchData()\n  }\n\n  componentDidMount() {\n    this._usub = SearchableComponentStore.listen(this._onSearchableChange)\n  }\n\n  componentWillUnmount() {\n    this._usub()\n  }\n\n  _globalKeymapHandlers() {\n    return {\n      'core:find-in-thread': this._onFindInThread,\n      'core:find-in-thread-next': this._onNextResult,\n      'core:find-in-thread-previous': this._onPrevResult,\n    }\n  }\n\n  _onFindInThread = () => {\n    if (this.state.searchTerm === null) {\n      Actions.findInThread(\"\");\n      if (MessageStore.hasCollapsedItems()) {\n        Actions.toggleAllMessagesExpanded()\n      }\n    }\n    this._focusSearch()\n  }\n\n  _onSearchableChange = () => {\n    this.setState(SearchableComponentStore.getCurrentSearchData())\n  }\n\n  _onFindChange = (event) => {\n    Actions.findInThread(event.target.value)\n  }\n\n  _onFindKeyDown = (event) => {\n    if (event.key === \"Enter\") {\n      return event.shiftKey ? this._onPrevResult() : this._onNextResult()\n    } else if (event.key === \"Escape\") {\n      this._clearSearch()\n      ReactDOM.findDOMNode(this.refs.searchBox).blur()\n    }\n    return null\n  }\n\n  _selectionText() {\n    if (this.state.globalIndex !== null && this.state.resultsLength > 0) {\n      return `${this.state.globalIndex + 1} of ${this.state.resultsLength}`\n    }\n    return \"\"\n  }\n\n  _navEnabled() {\n    return this.state.resultsLength > 0;\n  }\n\n  _onPrevResult = () => {\n    if (this._navEnabled()) { Actions.previousSearchResult() }\n  }\n\n  _onNextResult = () => {\n    if (this._navEnabled()) { Actions.nextSearchResult() }\n  }\n\n  _clearSearch = () => {\n    Actions.findInThread(null)\n  }\n\n  _focusSearch = (event) => {\n    const cw = ReactDOM.findDOMNode(this.refs.controlsWrap)\n    if (!event || !(cw && cw.contains(event.target))) {\n      ReactDOM.findDOMNode(this.refs.searchBox).focus()\n    }\n  }\n\n  render() {\n    const rootCls = classnames({\n      \"find-in-thread\": true,\n      \"enabled\": this.state.searchTerm !== null,\n    })\n    const btnCls = \"btn btn-find-in-thread\";\n    return (\n      <div className={rootCls} onClick={this._focusSearch}>\n        <KeyCommandsRegion globalHandlers={this._globalKeymapHandlers()}>\n          <div className=\"controls-wrap\" ref=\"controlsWrap\">\n            <div className=\"input-wrap\">\n\n              <input\n                type=\"text\"\n                ref=\"searchBox\"\n                placeholder=\"Find in thread\"\n                onChange={this._onFindChange}\n                onKeyDown={this._onFindKeyDown}\n                value={this.state.searchTerm || \"\"}\n              />\n\n              <div className=\"selection-progress\">{this._selectionText()}</div>\n\n              <div className=\"btn-wrap\">\n                <button\n                  tabIndex={-1}\n                  className={btnCls}\n                  disabled={!this._navEnabled()}\n                  onClick={this._onPrevResult}\n                >\n                  <RetinaImg\n                    name=\"ic-findinthread-previous.png\"\n                    mode={RetinaImg.Mode.ContentIsMask}\n                  />\n                </button>\n\n                <button\n                  className={btnCls}\n                  tabIndex={-1}\n                  disabled={!this._navEnabled()}\n                  onClick={this._onNextResult}\n                >\n                  <RetinaImg\n                    name=\"ic-findinthread-next.png\"\n                    mode={RetinaImg.Mode.ContentIsMask}\n                  />\n                </button>\n              </div>\n\n            </div>\n\n            <button className={btnCls} onClick={this._clearSearch} >\n              <RetinaImg\n                name=\"ic-findinthread-close.png\"\n                mode={RetinaImg.Mode.ContentIsMask}\n              />\n            </button>\n          </div>\n        </KeyCommandsRegion>\n      </div>\n    )\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/inline-image-listeners.es6",
    "content": "import {Actions, Utils} from 'nylas-exports';\n\nfunction safeEncode(str) {\n  return btoa(unescape(encodeURIComponent(str)));\n}\n\nfunction safeDecode(str) {\n  return atob(decodeURIComponent(escape(str)))\n}\n\nfunction _runOnImageNode(node) {\n  if (node.src && node.dataset.nylasFile) {\n    node.addEventListener('error', () => {\n      const file = JSON.parse(safeDecode(node.dataset.nylasFile), Utils.registeredObjectReviver);\n      const initialDisplay = node.style.display;\n      const downloadButton = document.createElement('a');\n      downloadButton.classList.add('inline-download-prompt')\n      downloadButton.textContent = \"Click to download inline image\";\n      downloadButton.addEventListener('click', () => {\n        Actions.fetchFile(file);\n        node.parentNode.removeChild(downloadButton);\n        node.addEventListener('load', () => {\n          node.style.display = initialDisplay;\n        });\n      });\n      node.style.display = 'none';\n      node.parentNode.insertBefore(downloadButton, node);\n    });\n\n    node.addEventListener('load', () => {\n      const file = JSON.parse(safeDecode(node.dataset.nylasFile), Utils.registeredObjectReviver);\n      node.addEventListener('dblclick', () => {\n        Actions.fetchAndOpenFile(file);\n      });\n    });\n  }\n}\n\nexport function encodedAttributeForFile(file) {\n  return safeEncode(JSON.stringify(file, Utils.registeredObjectReplacer));\n}\n\nexport function addInlineImageListeners(doc) {\n  const imgTagWalker = document.createTreeWalker(doc.body, NodeFilter.SHOW_ELEMENT, {\n    acceptNode: (node) => {\n      if (node.nodeName === 'IMG') {\n        return NodeFilter.FILTER_ACCEPT;\n      }\n      return NodeFilter.FILTER_SKIP;\n    },\n  });\n\n  while (imgTagWalker.nextNode()) {\n    _runOnImageNode(imgTagWalker.currentNode);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/main.cjsx",
    "content": "{MailboxPerspective,\n ComponentRegistry,\n ExtensionRegistry,\n WorkspaceStore,\n DatabaseStore,\n Actions,\n Thread} = require 'nylas-exports'\n\nMessageList = require(\"./message-list\")\nMessageListHiddenMessagesToggle = require('./message-list-hidden-messages-toggle').default\n\nSidebarPluginContainer = require \"./sidebar-plugin-container\"\nSidebarParticipantPicker = require('./sidebar-participant-picker').default\n\nmodule.exports =\n  activate: ->\n    if NylasEnv.isMainWindow()\n      # Register Message List Actions we provide globally\n      ComponentRegistry.register MessageList,\n        location: WorkspaceStore.Location.MessageList\n\n      ComponentRegistry.register SidebarParticipantPicker,\n        location: WorkspaceStore.Location.MessageListSidebar\n\n      ComponentRegistry.register SidebarPluginContainer,\n        location: WorkspaceStore.Location.MessageListSidebar\n\n      ComponentRegistry.register MessageListHiddenMessagesToggle,\n        role: 'MessageListHeaders'\n    else\n      # This is for the thread-popout window.\n      {threadId, perspectiveJSON} = NylasEnv.getWindowProps()\n      ComponentRegistry.register(MessageList, {location: WorkspaceStore.Location.Center})\n      # We need to locate the thread and focus it so that the MessageList displays it\n      DatabaseStore.find(Thread, threadId).then((thread) =>\n        Actions.setFocus({collection: 'thread', item: thread})\n      )\n      # Set the focused perspective and hide the proper messages\n      # (e.g. we should hide deleted items from the inbox, but not from trash)\n      Actions.focusMailboxPerspective(MailboxPerspective.fromJSON(perspectiveJSON))\n      ComponentRegistry.register MessageListHiddenMessagesToggle,\n        role: 'MessageListHeaders'\n\n  deactivate: ->\n    ComponentRegistry.unregister MessageList\n    ComponentRegistry.unregister SidebarPluginContainer\n    ComponentRegistry.unregister SidebarParticipantPicker\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-controls.cjsx",
    "content": "React = require 'react'\n{remote} = require 'electron'\n{Actions, NylasAPI, NylasAPIRequest, AccountStore} = require 'nylas-exports'\n{RetinaImg, ButtonDropdown, Menu} = require 'nylas-component-kit'\n\nclass MessageControls extends React.Component\n  @displayName: \"MessageControls\"\n  @propTypes:\n    thread: React.PropTypes.object.isRequired\n    message: React.PropTypes.object.isRequired\n\n  constructor: (@props) ->\n\n  render: =>\n    items = @_items()\n\n    <div className=\"message-actions-wrap\">\n      <ButtonDropdown\n        primaryItem={<RetinaImg name={items[0].image} mode={RetinaImg.Mode.ContentIsMask}/>}\n        primaryTitle={items[0].name}\n        primaryClick={items[0].select}\n        closeOnMenuClick={true}\n        menu={@_dropdownMenu(items[1..-1])}/>\n      <div className=\"message-actions-ellipsis\" onClick={@_onShowActionsMenu}>\n        <RetinaImg name={\"message-actions-ellipsis.png\"} mode={RetinaImg.Mode.ContentIsMask}/>\n      </div>\n    </div>\n\n  _items: ->\n    reply =\n      name: 'Reply',\n      image: 'ic-dropdown-reply.png'\n      select: @_onReply\n    replyAll =\n      name: 'Reply All',\n      image: 'ic-dropdown-replyall.png'\n      select: @_onReplyAll\n    forward =\n      name: 'Forward',\n      image: 'ic-dropdown-forward.png'\n      select: @_onForward\n\n    if @props.message.canReplyAll()\n      defaultReplyType = NylasEnv.config.get('core.sending.defaultReplyType')\n      if defaultReplyType is 'reply-all'\n        return [replyAll, reply, forward]\n      else\n        return [reply, replyAll, forward]\n    else\n      return [reply, forward]\n\n  _account: =>\n    AccountStore.accountForId(@props.message.accountId)\n\n  _dropdownMenu: (items) ->\n    itemContent = (item) ->\n      <span>\n        <RetinaImg name={item.image} mode={RetinaImg.Mode.ContentIsMask}/>\n        &nbsp;&nbsp;{item.name}\n      </span>\n\n    <Menu items={items}\n          itemKey={ (item) -> item.name }\n          itemContent={itemContent}\n          onSelect={ (item) => item.select() }\n          />\n\n  _onReply: =>\n    {thread, message} = @props\n    Actions.composeReply({thread, message, type: 'reply', behavior: 'prefer-existing-if-pristine'})\n\n  _onReplyAll: =>\n    {thread, message} = @props\n    Actions.composeReply({thread, message, type: 'reply-all', behavior: 'prefer-existing-if-pristine'})\n\n  _onForward: =>\n    {thread, message} = @props\n    Actions.composeForward({thread, message})\n\n  _onShowActionsMenu: =>\n    SystemMenu = remote.Menu\n    SystemMenuItem = remote.MenuItem\n\n    # Todo: refactor this so that message actions are provided\n    # dynamically. Waiting to see if this will be used often.\n    menu = new SystemMenu()\n    menu.append(new SystemMenuItem({ label: 'Log Data', click: => @_onLogData()}))\n    menu.append(new SystemMenuItem({ label: 'Show Original', click: => @_onShowOriginal()}))\n    menu.append(new SystemMenuItem({ label: 'Copy Debug Info to Clipboard', click: => @_onCopyToClipboard()}))\n    menu.popup(remote.getCurrentWindow())\n\n  _onShowOriginal: =>\n    fs = require 'fs'\n    path = require 'path'\n    BrowserWindow = remote.BrowserWindow\n    app = remote.app\n    tmpfile = path.join(app.getPath('temp'), @props.message.id)\n\n    request = new NylasAPIRequest\n      api: NylasAPI\n      options:\n        headers:\n          Accept: 'message/rfc822'\n        path: \"/messages/#{@props.message.id}\"\n        accountId: @props.message.accountId\n        json:false\n    request.run()\n    .then((body) =>\n      fs.writeFile tmpfile, body, =>\n        window = new BrowserWindow(width: 800, height: 600, title: \"#{@props.message.subject} - RFC822\")\n        window.loadURL('file://'+tmpfile)\n    )\n\n  _onLogData: =>\n    console.log @props.message\n    window.__message = @props.message\n    window.__thread = @props.thread\n    console.log \"Also now available in window.__message and window.__thread\"\n\n  _onCopyToClipboard: =>\n    clipboard = require('electron').clipboard\n    data = \"AccountID: #{@props.message.accountId}\\n\"+\n      \"Message ID: #{@props.message.serverId}\\n\"+\n      \"Message Metadata: #{JSON.stringify(@props.message.pluginMetadata, null, '  ')}\\n\"+\n      \"Thread ID: #{@props.thread.serverId}\\n\"+\n      \"Thread Metadata: #{JSON.stringify(@props.thread.pluginMetadata, null, '  ')}\\n\"\n\n    clipboard.writeText(data)\n\nmodule.exports = MessageControls\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-item-body.cjsx",
    "content": "React = require 'react'\n_ = require 'underscore'\nEmailFrame = require('./email-frame').default\n{encodedAttributeForFile} = require('./inline-image-listeners')\n{\n  DraftHelpers,\n  CanvasUtils,\n  NylasAPI,\n  NylasAPIRequest,\n  MessageUtils,\n  MessageBodyProcessor,\n  QuotedHTMLTransformer,\n  FileDownloadStore\n} = require 'nylas-exports'\n{\n  InjectedComponentSet,\n  RetinaImg\n} = require 'nylas-component-kit'\n\nTransparentPixel = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII=\"\n\nclass MessageItemBody extends React.Component\n  @displayName: 'MessageItemBody'\n  @propTypes:\n    message: React.PropTypes.object.isRequired\n    downloads: React.PropTypes.object.isRequired\n    onLoad: React.PropTypes.func\n\n  constructor: (@props) ->\n    @_mounted = false\n    @state =\n      showQuotedText: DraftHelpers.isForwardedMessage(@props.message)\n      processedBody: null\n\n  componentWillMount: =>\n    @_unsub = MessageBodyProcessor.subscribe @props.message, (processedBody) =>\n      @setState({processedBody})\n\n  componentDidMount: =>\n    @_mounted = true\n\n  componentWillReceiveProps: (nextProps) ->\n    if nextProps.message.id isnt @props.message.id\n      @_unsub?()\n      @_unsub = MessageBodyProcessor.subscribe nextProps.message, (processedBody) =>\n        @setState({processedBody})\n\n  componentWillUnmount: =>\n    @_mounted = false\n    @_unsub?()\n\n  render: =>\n    <span>\n      <InjectedComponentSet\n        matching={role: \"message:BodyHeader\"}\n        exposedProps={message: @props.message}\n        direction=\"column\"\n        style={width:'100%'}/>\n      {@_renderBody()}\n      {@_renderQuotedTextControl()}\n    </span>\n\n  _renderBody: =>\n    if _.isString(@props.message.body) and _.isString(@state.processedBody)\n      <EmailFrame\n        showQuotedText={@state.showQuotedText}\n        content={@_mergeBodyWithFiles(@state.processedBody)}\n        message={@props.message}\n        onLoad={@props.onLoad}\n      />\n    else\n      <div className=\"message-body-loading\">\n        <RetinaImg\n          name=\"inline-loading-spinner.gif\"\n          mode={RetinaImg.Mode.ContentDark}\n          style={{width: 14, height: 14}}/>\n      </div>\n\n  _renderQuotedTextControl: =>\n    return null unless QuotedHTMLTransformer.hasQuotedHTML(@props.message.body)\n    <a className=\"quoted-text-control\" onClick={@_toggleQuotedText}>\n      <span className=\"dots\">&bull;&bull;&bull;</span>\n    </a>\n\n  _toggleQuotedText: =>\n    @setState\n      showQuotedText: !@state.showQuotedText\n\n  _mergeBodyWithFiles: (body) =>\n    # Replace cid: references with the paths to downloaded files\n    for file in @props.message.files\n      download = @props.downloads[file.id]\n\n      # Note: I don't like doing this with RegExp before the body is inserted into\n      # the DOM, but we want to avoid \"could not load cid://\" in the console.\n\n      if download and download.state isnt 'finished'\n        inlineImgRegexp = new RegExp(\"<\\s*img.*src=['\\\"]cid:#{file.contentId}['\\\"][^>]*>\", 'gi')\n        # Render a spinner\n        body = body.replace inlineImgRegexp, =>\n          '<img alt=\"spinner.gif\" src=\"nylas://message-list/assets/spinner.gif\" style=\"-webkit-user-drag: none;\">'\n      else\n        # Render the completed download. We include data-nylas-file so that if the image fails\n        # to load, we can parse the file out and call `Actions.fetchFile` to retrieve it.\n        # (Necessary when attachment download mode is set to \"manual\")\n        cidRegexp = new RegExp(\"cid:#{file.contentId}(['\\\"])\", 'gi')\n        body = body.replace cidRegexp, (text, quoteCharacter) ->\n          \"file://#{FileDownloadStore.pathForFile(file)}#{quoteCharacter} data-nylas-file=\\\"#{encodedAttributeForFile(file)}\\\" \"\n\n    # Replace remaining cid: references - we will not display them since they'll\n    # throw \"unknown ERR_UNKNOWN_URL_SCHEME\". Show a transparent pixel so that there's\n    # no \"missing image\" region shown, just a space.\n    body = body.replace(MessageUtils.cidRegex, \"src=\\\"#{TransparentPixel}\\\"\")\n\n    return body\n\nmodule.exports = MessageItemBody\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-item-container.cjsx",
    "content": "React = require 'react'\nclassNames = require 'classnames'\n\nMessageItem = require './message-item'\n\n{Utils,\n DraftStore,\n ComponentRegistry,\n MessageStore} = require 'nylas-exports'\n\n\nclass MessageItemContainer extends React.Component\n  @displayName = 'MessageItemContainer'\n\n  @propTypes =\n    thread: React.PropTypes.object.isRequired\n    message: React.PropTypes.object.isRequired\n    messages: React.PropTypes.array.isRequired\n    collapsed: React.PropTypes.bool\n    isLastMsg: React.PropTypes.bool\n    isBeforeReplyArea: React.PropTypes.bool\n    scrollTo: React.PropTypes.func\n    onLoad: React.PropTypes.func\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentWillReceiveProps: (newProps) ->\n    @setState(@_getStateFromStores(newProps))\n\n  componentDidMount: =>\n    if @props.message.draft\n      @_unlisten = DraftStore.listen @_onSendingStateChanged\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  componentWillUnmount: =>\n    @_unlisten() if @_unlisten\n\n  focus: =>\n    @refs.message.focus()\n\n  render: =>\n    if @state.isSending\n      @_renderMessage(pending: true)\n    else if @props.message.draft\n      @_renderComposer()\n    else\n      @_renderMessage(pending: false)\n\n  _renderMessage: ({pending}) =>\n    <MessageItem\n      ref=\"message\"\n      pending={pending}\n      thread={@props.thread}\n      message={@props.message}\n      messages={@props.messages}\n      className={@_classNames()}\n      collapsed={@props.collapsed}\n      isLastMsg={@props.isLastMsg}\n      onLoad={@props.onLoad}\n    />\n\n  _renderComposer: =>\n    Composer = ComponentRegistry.findComponentsMatching(role: 'Composer')[0]\n    if (!Composer)\n      return <span></span>\n\n    <Composer\n      ref=\"message\"\n      draftClientId={@props.message.clientId}\n      className={@_classNames()}\n      mode={\"inline\"}\n      threadId={@props.thread.id}\n      scrollTo={@props.scrollTo}\n    />\n\n  _classNames: => classNames\n    \"draft\": @props.message.draft\n    \"unread\": @props.message.unread\n    \"collapsed\": @props.collapsed\n    \"message-item-wrap\": true\n    \"before-reply-area\": @props.isBeforeReplyArea\n\n  _onSendingStateChanged: (draftClientId) =>\n    if draftClientId is @props.message.clientId\n      @setState(@_getStateFromStores())\n\n  _getStateFromStores: (props = @props) ->\n    isSending: DraftStore.isSendingDraft(props.message.clientId)\n\nmodule.exports = MessageItemContainer\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-item.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\nclassNames = require 'classnames'\n_ = require 'underscore'\nMessageParticipants = require \"./message-participants\"\nMessageItemBody = require \"./message-item-body\"\nMessageTimestamp = require(\"./message-timestamp\").default\nMessageControls = require './message-controls'\n{Utils,\n Actions,\n MessageUtils,\n AccountStore,\n MessageBodyProcessor,\n QuotedHTMLTransformer,\n ComponentRegistry,\n FileDownloadStore} = require 'nylas-exports'\n{RetinaImg,\n InjectedComponentSet,\n InjectedComponent} = require 'nylas-component-kit'\n\nTransparentPixel = \"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNikAQAACIAHF/uBd8AAAAASUVORK5CYII=\"\n\nclass MessageItem extends React.Component\n  @displayName = 'MessageItem'\n\n  @propTypes =\n    thread: React.PropTypes.object.isRequired\n    message: React.PropTypes.object.isRequired\n    messages: React.PropTypes.array.isRequired\n    collapsed: React.PropTypes.bool\n    onLoad: React.PropTypes.func\n\n  constructor: (@props) ->\n    fileIds = @props.message.fileIds()\n    @state =\n      # Holds the downloadData (if any) for all of our files. It's a hash\n      # keyed by a fileId. The value is the downloadData.\n      downloads: FileDownloadStore.getDownloadDataForFiles(fileIds)\n      filePreviewPaths: FileDownloadStore.previewPathsForFiles(fileIds)\n      detailedHeaders: false\n      detailedHeadersTogglePos: {top: 18}\n\n  componentDidMount: =>\n    @_storeUnlisten = FileDownloadStore.listen(@_onDownloadStoreChange)\n    @_setDetailedHeadersTogglePos()\n\n  componentDidUpdate: =>\n    @_setDetailedHeadersTogglePos()\n\n  componentWillUnmount: =>\n    @_storeUnlisten() if @_storeUnlisten\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  render: =>\n    if @props.collapsed\n      @_renderCollapsed()\n    else\n      @_renderFull()\n\n  _renderCollapsed: =>\n    attachmentIcon = []\n    if Utils.showIconForAttachments(@props.message.files)\n      attachmentIcon = <div className=\"collapsed-attachment\"></div>\n\n    <div className={@props.className} onClick={@_toggleCollapsed}>\n      <div className=\"message-item-white-wrap\">\n        <div className=\"message-item-area\">\n          <div className=\"collapsed-from\">\n            {@props.message.from?[0]?.displayName(compact: true)}\n          </div>\n          <div className=\"collapsed-snippet\">\n            {@props.message.snippet}\n          </div>\n          <div className=\"collapsed-timestamp\">\n            <MessageTimestamp date={@props.message.date} />\n          </div>\n          {attachmentIcon}\n        </div>\n      </div>\n    </div>\n\n  _renderFull: =>\n    <div className={@props.className}>\n      <div className=\"message-item-white-wrap\">\n        <div className=\"message-item-area\">\n          {@_renderHeader()}\n          <MessageItemBody\n            message={@props.message}\n            downloads={@state.downloads}\n            onLoad={@props.onLoad}\n          />\n          {@_renderAttachments()}\n          {@_renderFooterStatus()}\n        </div>\n      </div>\n    </div>\n\n  _renderHeader: =>\n    classes = classNames\n      \"message-header\": true\n      \"pending\": @props.pending\n\n    <header ref=\"header\" className={classes} onClick={@_onClickHeader}>\n      <InjectedComponent\n        matching={{role: \"MessageHeader\"}}\n        exposedProps={{message: @props.message, thread: @props.thread, messages: @props.messages}}\n      />\n      <div className=\"pending-spinner\" style={{position: 'absolute', marginTop: -2}}>\n        <RetinaImg\n          ref=\"spinner\"\n          name=\"sending-spinner.gif\"\n          mode={RetinaImg.Mode.ContentPreserve}\n        />\n      </div>\n      <div className=\"message-header-right\">\n        <MessageTimestamp\n          className=\"message-time\"\n          isDetailed={@state.detailedHeaders}\n          date={@props.message.date}\n        />\n        <InjectedComponentSet\n          className=\"message-header-status\"\n          matching={role: \"MessageHeaderStatus\"}\n          exposedProps={message: @props.message, thread: @props.thread, detailedHeaders: @state.detailedHeaders}\n        />\n        <MessageControls thread={@props.thread} message={@props.message}/>\n      </div>\n      <MessageParticipants\n        from={@props.message.from}\n        onClick={@_onClickParticipants}\n        isDetailed={@state.detailedHeaders}\n      />\n      <MessageParticipants\n        to={@props.message.to}\n        cc={@props.message.cc}\n        bcc={@props.message.bcc}\n        onClick={@_onClickParticipants}\n        isDetailed={@state.detailedHeaders}\n      />\n      {@_renderFolder()}\n      {@_renderHeaderDetailToggle()}\n    </header>\n\n  _renderFolder: =>\n    return [] unless @state.detailedHeaders\n    acct = AccountStore.accountForId(@props.message.accountId)\n    acctUsesFolders = acct and acct.usesFolders()\n    folder = @props.message.categories?[0]\n    return unless folder and acctUsesFolders\n    <div className=\"header-row\">\n      <div className=\"header-label\">Folder:&nbsp;</div>\n      <div className=\"header-name\">{folder.displayName}</div>\n    </div>\n\n  _onClickParticipants: (e) =>\n    el = e.target\n    while el isnt e.currentTarget\n      if \"collapsed-participants\" in el.classList\n        @setState(detailedHeaders: true)\n        e.stopPropagation()\n        return\n      el = el.parentElement\n    return\n\n  _onClickHeader: (e) =>\n    return if @state.detailedHeaders\n    el = e.target\n    while el isnt e.currentTarget\n      wl = [\"message-header-right\",\n            \"collapsed-participants\",\n            \"header-toggle-control\"]\n      if \"message-header-right\" in el.classList then return\n      if \"collapsed-participants\" in el.classList then return\n      el = el.parentElement\n    @_toggleCollapsed()\n\n  _onDownloadAll: =>\n    Actions.fetchAndSaveAllFiles(@props.message.files)\n\n  _renderDownloadAllButton: =>\n    <div className=\"download-all\">\n      <div className=\"attachment-number\">\n        <RetinaImg\n          name=\"ic-attachments-all-clippy.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        <span>{@props.message.files.length} attachments</span>\n      </div>\n      <div className=\"separator\">-</div>\n      <div className=\"download-all-action\" onClick={@_onDownloadAll}>\n        <RetinaImg\n          name=\"ic-attachments-download-all.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        <span>Download all</span>\n      </div>\n    </div>\n\n\n  _renderAttachments: =>\n    files = (@props.message.files ? []).filter((f) => @_isRealFile(f))\n    messageClientId = @props.message.clientId\n    {filePreviewPaths, downloads} = @state\n    if files.length > 0\n      <div>\n        {if files.length > 1 then @_renderDownloadAllButton()}\n        <div className=\"attachments-area\">\n          <InjectedComponent\n            matching={{role: 'MessageAttachments'}}\n            exposedProps={{files, downloads, filePreviewPaths, messageClientId, canRemoveAttachments: false}}\n          />\n        </div>\n      </div>\n    else\n      <div />\n\n  _renderFooterStatus: =>\n    <InjectedComponentSet\n      className=\"message-footer-status\"\n      matching={role:\"MessageFooterStatus\"}\n      exposedProps={message: @props.message, thread: @props.thread, detailedHeaders: @state.detailedHeaders}\n    />\n\n  _setDetailedHeadersTogglePos: =>\n    header = ReactDOM.findDOMNode(@refs.header)\n    if !header\n      return\n    fromNode = header.querySelector('.participant-name.from-contact,.participant-primary')\n    if !fromNode\n      return\n    fromRect = fromNode.getBoundingClientRect()\n    topPos = Math.floor(fromNode.offsetTop + (fromRect.height / 2) - 10)\n    if topPos isnt @state.detailedHeadersTogglePos.top\n      @setState({detailedHeadersTogglePos: {top: topPos}})\n\n  _renderHeaderDetailToggle: =>\n    return null if @props.pending\n    {top} = @state.detailedHeadersTogglePos\n    if @state.detailedHeaders\n      <div\n        className=\"header-toggle-control\"\n        style={{top, left: \"-14px\"}}\n        onClick={(e) => @setState(detailedHeaders: false); e.stopPropagation()}\n      >\n        <RetinaImg\n          name={\"message-disclosure-triangle-active.png\"}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </div>\n    else\n      <div\n        className=\"header-toggle-control inactive\"\n        style={{top}}\n        onClick={(e) => @setState(detailedHeaders: true); e.stopPropagation()}\n      >\n        <RetinaImg\n          name={\"message-disclosure-triangle.png\"}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </div>\n\n  _toggleCollapsed: =>\n    return if @props.isLastMsg\n    Actions.toggleMessageIdExpanded(@props.message.id)\n\n  _isRealFile: (file) ->\n    hasCIDInBody = file.contentId? and @props.message.body?.indexOf(file.contentId) > 0\n    return not hasCIDInBody\n\n  _onDownloadStoreChange: =>\n    fileIds = @props.message.fileIds()\n    @setState\n      downloads: FileDownloadStore.getDownloadDataForFiles(fileIds)\n      filePreviewPaths: FileDownloadStore.previewPathsForFiles(fileIds)\n\nmodule.exports = MessageItem\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-list-hidden-messages-toggle.jsx",
    "content": "import {\n  React,\n  Actions,\n  MessageStore,\n  FocusedPerspectiveStore,\n} from 'nylas-exports';\n\nexport default class MessageListHiddenMessagesToggle extends React.Component {\n\n  static displayName = 'MessageListHiddenMessagesToggle';\n\n  constructor() {\n    super();\n    this.state = {\n      numberOfHiddenItems: MessageStore.numberOfHiddenItems(),\n    };\n  }\n\n  componentDidMount() {\n    this._unlisten = MessageStore.listen(() => {\n      this.setState({\n        numberOfHiddenItems: MessageStore.numberOfHiddenItems(),\n      });\n    });\n  }\n\n  componentWillUnmount() {\n    this._unlisten();\n  }\n\n  render() {\n    const {numberOfHiddenItems} = this.state;\n    if (numberOfHiddenItems === 0) {\n      return (<span />);\n    }\n\n\n    const viewing = FocusedPerspectiveStore.current().categoriesSharedName();\n    let message = null;\n\n    if (MessageStore.CategoryNamesHiddenByDefault.includes(viewing)) {\n      if (numberOfHiddenItems > 1) {\n        message = `There are ${numberOfHiddenItems} more messages in this thread that are not in spam or trash.`;\n      } else {\n        message = `There is one more message in this thread that is not in spam or trash.`;\n      }\n    } else {\n      if (numberOfHiddenItems > 1) {\n        message = `${numberOfHiddenItems} messages in this thread are hidden because it was moved to trash or spam.`;\n      } else {\n        message = `One message in this thread is hidden because it was moved to trash or spam.`;\n      }\n    }\n\n    return (\n      <div className=\"show-hidden-messages\">\n        {message}\n        <a onClick={function toggle() { Actions.toggleHiddenMessages() }}>Show all messages</a>\n      </div>\n    );\n  }\n}\n\nMessageListHiddenMessagesToggle.containerRequired = false;\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-list.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\nclassNames = require 'classnames'\nFindInThread = require('./find-in-thread').default\nMessageItemContainer = require './message-item-container'\n\n{Utils,\n Actions,\n Message,\n DraftStore,\n MessageStore,\n AccountStore,\n DatabaseStore,\n WorkspaceStore,\n ChangeLabelsTask,\n ComponentRegistry,\n SearchableComponentStore\n SearchableComponentMaker} = require(\"nylas-exports\")\n\n{Spinner,\n RetinaImg,\n MailLabelSet,\n ScrollRegion,\n MailImportantIcon,\n InjectedComponent,\n KeyCommandsRegion,\n InjectedComponentSet} = require('nylas-component-kit')\n\nclass MessageListScrollTooltip extends React.Component\n  @displayName: 'MessageListScrollTooltip'\n  @propTypes:\n    viewportCenter: React.PropTypes.number.isRequired\n    totalHeight: React.PropTypes.number.isRequired\n\n  componentWillMount: =>\n    @setupForProps(@props)\n\n  componentWillReceiveProps: (newProps) =>\n    @setupForProps(newProps)\n\n  shouldComponentUpdate: (newProps, newState) =>\n    not _.isEqual(@state,newState)\n\n  setupForProps: (props) ->\n    # Technically, we could have MessageList provide the currently visible\n    # item index, but the DOM approach is simple and self-contained.\n    #\n    els = document.querySelectorAll('.message-item-wrap')\n    idx = _.findIndex els, (el) -> el.offsetTop > props.viewportCenter\n    if idx is -1\n      idx = els.length\n\n    @setState\n      idx: idx\n      count: els.length\n\n  render: ->\n    <div className=\"scroll-tooltip\">\n      {@state.idx} of {@state.count}\n    </div>\n\nclass MessageList extends React.Component\n  @displayName: 'MessageList'\n  @containerRequired: false\n  @containerStyles:\n    minWidth: 500\n    maxWidth: 999999\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n    @state.minified = true\n    @_draftScrollInProgress = false\n    @MINIFY_THRESHOLD = 3\n\n  componentDidMount: =>\n    @_unsubscribers = []\n    @_unsubscribers.push MessageStore.listen @_onChange\n    @_unsubscribers.push Actions.focusDraft.listen ({draftClientId}) =>\n      Utils.waitFor( => @_getMessageContainer(draftClientId)?).then =>\n        @_focusDraft(@_getMessageContainer(draftClientId))\n      .catch =>\n\n  componentWillUnmount: =>\n    unsubscribe() for unsubscribe in @_unsubscribers\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  componentDidUpdate: (prevProps, prevState) =>\n\n  _globalMenuItems: ->\n    toggleExpandedLabel = if @state.hasCollapsedItems then \"Expand\" else \"Collapse\"\n    [\n      {\n        \"label\": \"Thread\",\n        \"submenu\": [{\n          \"label\": \"#{toggleExpandedLabel} conversation\",\n          \"command\": \"message-list:toggle-expanded\",\n          \"position\": \"endof=view-actions\",\n        }]\n      }\n    ]\n\n  _globalKeymapHandlers: ->\n    handlers =\n      'core:reply': =>\n        Actions.composeReply({\n          thread: @state.currentThread,\n          message: @_lastMessage(),\n          type: 'reply',\n          behavior: 'prefer-existing',\n        })\n      'core:reply-all': =>\n        Actions.composeReply({\n          thread: @state.currentThread,\n          message: @_lastMessage(),\n          type: 'reply-all',\n          behavior: 'prefer-existing',\n        })\n      'core:forward': => @_onForward()\n      'core:print-thread': => @_onPrintThread()\n      'core:messages-page-up': => @_onScrollByPage(-1)\n      'core:messages-page-down': => @_onScrollByPage(1)\n\n    if @state.canCollapse\n      handlers['message-list:toggle-expanded'] = => @_onToggleAllMessagesExpanded()\n\n    handlers\n\n  _getMessageContainer: (clientId) =>\n    @refs[\"message-container-#{clientId}\"]\n\n  _focusDraft: (draftElement) =>\n    # Note: We don't want the contenteditable view competing for scroll offset,\n    # so we block incoming childScrollRequests while we scroll to the new draft.\n    @_draftScrollInProgress = true\n    draftElement.focus()\n    @refs.messageWrap.scrollTo(draftElement, {\n      position: ScrollRegion.ScrollPosition.Top,\n      settle: true,\n      done: =>\n        @_draftScrollInProgress = false\n    })\n\n  _onForward: =>\n    return unless @state.currentThread\n    Actions.composeForward(thread: @state.currentThread)\n\n  render: =>\n    if not @state.currentThread\n      return <span />\n\n    wrapClass = classNames\n      \"messages-wrap\": true\n      \"ready\": not @state.loading\n\n    messageListClass = classNames\n      \"message-list\": true\n      \"height-fix\": SearchableComponentStore.searchTerm isnt null\n\n    <KeyCommandsRegion\n      globalHandlers={@_globalKeymapHandlers()}\n      globalMenuItems={@_globalMenuItems()}>\n      <FindInThread ref=\"findInThread\" />\n      <div className={messageListClass} id=\"message-list\">\n        <ScrollRegion tabIndex=\"-1\"\n             className={wrapClass}\n             scrollbarTickProvider={SearchableComponentStore}\n             scrollTooltipComponent={MessageListScrollTooltip}\n             ref=\"messageWrap\">\n          {@_renderSubject()}\n          <div className=\"headers\" style={position:'relative'}>\n            <InjectedComponentSet\n              className=\"message-list-headers\"\n              matching={{role: \"MessageListHeaders\"}}\n              exposedProps={{thread: @state.currentThread, messages: @state.messages}}\n              direction=\"column\"\n            />\n          </div>\n          {@_messageElements()}\n        </ScrollRegion>\n        <Spinner visible={@state.loading} />\n      </div>\n    </KeyCommandsRegion>\n\n  _renderSubject: ->\n    subject = @state.currentThread.subject\n    subject = \"(No Subject)\" if not subject or subject.length is 0\n\n    <div className=\"message-subject-wrap\">\n      <MailImportantIcon thread={@state.currentThread}/>\n      <div style={flex: 1}>\n        <span className=\"message-subject\">{subject}</span>\n        <MailLabelSet\n          removable={true}\n          messages={@state.messages}\n          thread={@state.currentThread}\n          includeCurrentCategories={true}\n        />\n      </div>\n      {@_renderIcons()}\n    </div>\n\n  _renderIcons: =>\n    <div className=\"message-icons-wrap\">\n      {@_renderExpandToggle()}\n      <div onClick={@_onPrintThread}>\n        <RetinaImg name=\"print.png\" title=\"Print Thread\" mode={RetinaImg.Mode.ContentIsMask}/>\n      </div>\n      {@_renderPopoutToggle()}\n    </div>\n\n  _renderExpandToggle: =>\n    return <span/> unless @state.canCollapse\n\n    if @state.hasCollapsedItems\n      <div onClick={@_onToggleAllMessagesExpanded}>\n        <RetinaImg name={\"expand.png\"} title={\"Expand All\"} mode={RetinaImg.Mode.ContentIsMask}/>\n      </div>\n    else\n      <div onClick={@_onToggleAllMessagesExpanded}>\n        <RetinaImg name={\"collapse.png\"} title={\"Collapse All\"} mode={RetinaImg.Mode.ContentIsMask}/>\n      </div>\n\n  _renderPopoutToggle: =>\n    if NylasEnv.isThreadWindow()\n      <div onClick={@_onPopThreadIn}>\n        <RetinaImg name=\"thread-popin.png\" title=\"Pop thread in\" mode={RetinaImg.Mode.ContentIsMask}/>\n      </div>\n    else\n      <div onClick={@_onPopoutThread}>\n        <RetinaImg name=\"thread-popout.png\" title=\"Popout thread\" mode={RetinaImg.Mode.ContentIsMask}/>\n      </div>\n\n\n  _renderReplyArea: =>\n    <div className=\"footer-reply-area-wrap\" onClick={@_onClickReplyArea} key='reply-area'>\n      <div className=\"footer-reply-area\">\n        <RetinaImg name=\"#{@_replyType()}-footer.png\" mode={RetinaImg.Mode.ContentIsMask}/>\n        <span className=\"reply-text\">Write a reply…</span>\n      </div>\n    </div>\n\n  _lastMessage: =>\n    _.last(_.filter((@state.messages ? []), (m) -> not m.draft))\n\n  # Returns either \"reply\" or \"reply-all\"\n  _replyType: =>\n    defaultReplyType = NylasEnv.config.get('core.sending.defaultReplyType')\n    lastMessage = @_lastMessage()\n    return 'reply' unless lastMessage\n\n    if lastMessage.canReplyAll()\n      if defaultReplyType is 'reply-all'\n        return 'reply-all'\n      else\n        return 'reply'\n    else\n      return 'reply'\n\n  _onToggleAllMessagesExpanded: ->\n    Actions.toggleAllMessagesExpanded()\n\n  _onPrintThread: =>\n    node = ReactDOM.findDOMNode(@)\n    Actions.printThread(@state.currentThread, node.innerHTML)\n\n  _onPopThreadIn: =>\n    return unless @state.currentThread\n    Actions.focusThreadMainWindow(@state.currentThread)\n    NylasEnv.close()\n\n  _onPopoutThread: =>\n    return unless @state.currentThread\n    Actions.popoutThread(@state.currentThread)\n    # This returns the single-pane view to the inbox, and does nothing for\n    # double-pane view because we're at the root sheet.\n    Actions.popSheet()\n\n  _onClickReplyArea: =>\n    return unless @state.currentThread\n    Actions.composeReply({\n      thread: @state.currentThread,\n      message: @_lastMessage(),\n      type: @_replyType(),\n      behavior: 'prefer-existing-if-pristine',\n    })\n\n  _messageElements: =>\n    elements = []\n\n    hasReplyArea = not _.last(@state.messages)?.draft\n    messages = @_messagesWithMinification(@state.messages)\n    messages.forEach (message, idx) =>\n\n      if message.type is \"minifiedBundle\"\n        elements.push(@_renderMinifiedBundle(message))\n        return\n\n      collapsed = !@state.messagesExpandedState[message.id]\n      isLastMsg = (messages.length - 1 is idx)\n      isBeforeReplyArea = isLastMsg and hasReplyArea\n\n      elements.push(\n        <MessageItemContainer\n          key={message.clientId}\n          ref={\"message-container-#{message.clientId}\"}\n          thread={@state.currentThread}\n          message={message}\n          messages={@state.messages}\n          collapsed={collapsed}\n          isLastMsg={isLastMsg}\n          isBeforeReplyArea={isBeforeReplyArea}\n          scrollTo={@_scrollTo}\n          onLoad={@_onMessageLoaded}\n        />\n      )\n\n    if hasReplyArea\n      elements.push(@_renderReplyArea())\n\n    return elements\n\n  _renderMinifiedBundle: (bundle) ->\n    BUNDLE_HEIGHT = 36\n    lines = bundle.messages[0...10]\n    h = Math.round(BUNDLE_HEIGHT / lines.length)\n\n    <div className=\"minified-bundle\"\n         onClick={ => @setState minified: false }\n         key={Utils.generateTempId()}>\n      <div className=\"num-messages\">{bundle.messages.length} older messages</div>\n      <div className=\"msg-lines\" style={height: h*lines.length}>\n        {lines.map (msg, i) ->\n          <div key={msg.id} style={height: h*2, top: -h*i} className=\"msg-line\"></div>}\n      </div>\n    </div>\n\n  _messagesWithMinification: (messages=[]) =>\n    return messages unless @state.minified\n\n    messages = _.clone(messages)\n    minifyRanges = []\n    consecutiveCollapsed = 0\n\n    messages.forEach (message, idx) =>\n      return if idx is 0 # Never minify the 1st message\n\n      expandState = @state.messagesExpandedState[message.id]\n\n      if not expandState\n        consecutiveCollapsed += 1\n      else\n        # We add a +1 because we don't minify the last collapsed message,\n        # but the MINIFY_THRESHOLD refers to the smallest N that can be in\n        # the \"N older messages\" minified block.\n        if expandState is \"default\"\n          minifyOffset = 1\n        else # if expandState is \"explicit\"\n          minifyOffset = 0\n\n        if consecutiveCollapsed >= @MINIFY_THRESHOLD + minifyOffset\n          minifyRanges.push\n            start: idx - consecutiveCollapsed\n            length: (consecutiveCollapsed - minifyOffset)\n        consecutiveCollapsed = 0\n\n    indexOffset = 0\n    for range in minifyRanges\n      start = range.start - indexOffset\n      minified =\n        type: \"minifiedBundle\"\n        messages: messages[start...(start+range.length)]\n      messages.splice(start, range.length, minified)\n\n      # While we removed `range.length` items, we also added 1 back in.\n      indexOffset += (range.length - 1)\n\n    return messages\n\n  # Some child components (like the composer) might request that we scroll\n  # to a given location. If `selectionTop` is defined that means we should\n  # scroll to that absolute position.\n  #\n  # If messageId and location are defined, that means we want to scroll\n  # smoothly to the top of a particular message.\n  _scrollTo: ({clientId, rect, position}={}) =>\n    return if @_draftScrollInProgress\n    if clientId\n      messageElement = @_getMessageContainer(clientId)\n      return unless messageElement\n      pos = position ? ScrollRegion.ScrollPosition.Visible\n      @refs.messageWrap.scrollTo(messageElement, {\n        position: pos\n      })\n    else if rect\n      @refs.messageWrap.scrollToRect(rect, {\n        position: ScrollRegion.ScrollPosition.CenterIfInvisible\n      })\n    else\n      throw new Error(\"onChildScrollRequest: expected clientId or rect\")\n\n  _onMessageLoaded: =>\n    if @state.currentThread\n      timerKey = \"select-thread-#{@state.currentThread.id}\"\n      if NylasEnv.timer.isPending(timerKey)\n        actionTimeMs = NylasEnv.timer.stop(timerKey)\n        messageCount = (@state.messages || []).length\n        Actions.recordPerfMetric({\n          sample: 0.1,\n          action: 'select-thread',\n          actionTimeMs,\n          messageCount,\n        })\n\n  _onScrollByPage: (direction) =>\n    height = ReactDOM.findDOMNode(@refs.messageWrap).clientHeight\n    @refs.messageWrap.scrollTop += height * direction\n\n  _onChange: =>\n    newState = @_getStateFromStores()\n    if @state.currentThread?.id isnt newState.currentThread?.id\n      newState.minified = true\n    @setState(newState)\n\n  _getStateFromStores: =>\n    messages: (MessageStore.items() ? [])\n    messagesExpandedState: MessageStore.itemsExpandedState()\n    canCollapse: MessageStore.items().length > 1\n    hasCollapsedItems: MessageStore.hasCollapsedItems()\n    currentThread: MessageStore.thread()\n    loading: MessageStore.itemsLoading()\n\nmodule.exports = SearchableComponentMaker.extend(MessageList)\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-participants.cjsx",
    "content": "_ = require 'underscore'\nReact = require \"react\"\nclassnames = require 'classnames'\n{Actions, Contact} = require 'nylas-exports'\n{Menu, MenuItem} = require('electron').remote\n\n\nMAX_COLLAPSED = 5\n\nclass MessageParticipants extends React.Component\n  @displayName: 'MessageParticipants'\n\n  @propTypes:\n    to: React.PropTypes.array\n    cc: React.PropTypes.array\n    bcc: React.PropTypes.array\n    from: React.PropTypes.array\n    onClick: React.PropTypes.func\n    isDetailed: React.PropTypes.bool\n\n  @defaultProps:\n    to: []\n    cc: []\n    bcc: []\n    from: []\n\n\n  # Helpers\n\n  _allToParticipants: =>\n    _.union(@props.to, @props.cc, @props.bcc)\n\n  _selectText: (e) =>\n    textNode = e.currentTarget.childNodes[0]\n\n    range = document.createRange()\n    range.setStart(textNode, 0)\n    range.setEnd(textNode, textNode.length)\n    selection = document.getSelection()\n    selection.removeAllRanges()\n    selection.addRange(range)\n\n  _shortNames: (contacts = [], max = MAX_COLLAPSED) =>\n    names = _.map(contacts, (c) -> c.displayName(includeAccountLabel: true, compact: true))\n    if names.length > max\n      extra = names.length - max\n      names = names.slice(0, max)\n      names.push(\"and #{extra} more\")\n    names.join(\", \")\n\n  _onContactContextMenu: (contact) =>\n    menu = new Menu()\n    menu.append(new MenuItem({role: 'copy'}))\n    menu.append(new MenuItem({\n      label: \"Email #{contact.email}\",\n      click: => Actions.composeNewDraftToRecipient(contact)\n    }))\n    menu.popup(NylasEnv.getCurrentWindow())\n\n  # Renderers\n\n  _renderFullContacts: (contacts = []) =>\n    _.map(contacts, (c, i) =>\n      if contacts.length is 1 then comma = \"\"\n      else if i is contacts.length-1 then comma = \"\"\n      else comma = \",\"\n\n      if c.name?.length > 0 and c.name isnt c.email\n        <div key={\"#{c.email}-#{i}\"} className=\"participant selectable\">\n          <div className=\"participant-primary\" onClick={@_selectText}>\n            {c.fullName()}\n          </div>\n          <div className=\"participant-secondary\">\n            {\" <\"}\n            <span\n              onClick={@_selectText}\n              onContextMenu={=> @_onContactContextMenu(c)}\n            >\n              {c.email}\n            </span>\n            {\">#{comma}\"}\n          </div>\n        </div>\n      else\n        <div key={\"#{c.email}-#{i}\"} className=\"participant selectable\">\n          <div className=\"participant-primary\">\n            <span\n              onClick={@_selectText}\n              onContextMenu={=> @_onContactContextMenu(c)}\n            >\n              {c.email}\n            </span>\n            {comma}\n          </div>\n        </div>\n    )\n\n  _renderExpandedField: (name, field, {includeLabel} = {}) =>\n    includeLabel ?= true\n    <div className=\"participant-type\" key={\"participant-type-#{name}\"}>\n      {\n        if includeLabel\n          <div className={\"participant-label #{name}-label\"}>{name}:&nbsp;</div>\n        else\n          undefined\n      }\n      <div className={\"participant-name #{name}-contact\"}>\n        {@_renderFullContacts(field)}\n      </div>\n    </div>\n\n  _renderExpanded: =>\n    expanded = []\n\n    if @props.from.length > 0\n      expanded.push(\n        @_renderExpandedField('from', @props.from, includeLabel: false)\n      )\n\n    if @props.to.length > 0\n      expanded.push(\n        @_renderExpandedField('to', @props.to)\n      )\n\n    if @props.cc.length > 0\n      expanded.push(\n        @_renderExpandedField('cc', @props.cc)\n      )\n\n    if @props.bcc.length > 0\n      expanded.push(\n        @_renderExpandedField('bcc', @props.bcc)\n      )\n\n    <div className=\"expanded-participants\">\n      {expanded}\n    </div>\n\n  _renderCollapsed: =>\n    childSpans = []\n    toParticipants = @_allToParticipants()\n\n    if @props.from.length > 0\n      childSpans.push(\n        <span className=\"participant-name from-contact\" key=\"from\">{@_shortNames(@props.from)}</span>\n      )\n\n    if toParticipants.length > 0\n      childSpans.push(\n        <span className=\"participant-label to-label\" key=\"to-label\">To:&nbsp;</span>\n        <span className=\"participant-name to-contact\" key=\"to-value\">{@_shortNames(toParticipants)}</span>\n      )\n\n    <span className=\"collapsed-participants\">\n      {childSpans}\n    </span>\n\n  render: =>\n    classSet = classnames\n      \"participants\": true\n      \"message-participants\": true\n      \"collapsed\": not @props.isDetailed\n      \"from-participants\": @props.from.length > 0\n      \"to-participants\": @_allToParticipants().length > 0\n\n    <div className={classSet} onClick={@props.onClick}>\n      {if @props.isDetailed then @_renderExpanded() else @_renderCollapsed()}\n    </div>\n\nmodule.exports = MessageParticipants\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/message-timestamp.jsx",
    "content": "import React from 'react'\nimport {DateUtils} from 'nylas-exports'\n\nclass MessageTimestamp extends React.Component {\n  static displayName = 'MessageTimestamp'\n\n  static propTypes = {\n    date: React.PropTypes.object.isRequired,\n    className: React.PropTypes.string,\n    isDetailed: React.PropTypes.bool,\n    onClick: React.PropTypes.func,\n  }\n\n  shouldComponentUpdate(nextProps) {\n    return (\n      nextProps.date !== this.props.date ||\n      nextProps.isDetailed !== this.props.isDetailed\n    )\n  }\n\n  render() {\n    let formattedDate = null\n    if (this.props.isDetailed) {\n      formattedDate = DateUtils.mediumTimeString(this.props.date)\n    } else {\n      formattedDate = DateUtils.shortTimeString(this.props.date)\n    }\n    return (\n      <div\n        className={this.props.className}\n        title={DateUtils.fullTimeString(this.props.date)}\n        onClick={this.props.onClick}\n      >\n        {formattedDate}\n      </div>\n    )\n  }\n}\n\nexport default MessageTimestamp\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/sidebar-participant-picker.jsx",
    "content": "import React from 'react';\nimport {Actions, FocusedContactsStore} from 'nylas-exports'\n\nconst SPLIT_KEY = \"---splitvalue---\"\n\nexport default class SidebarParticipantPicker extends React.Component {\n  static displayName = 'SidebarParticipantPicker';\n\n  static containerStyles = {\n    order: 0,\n    flexShrink: 0,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._usub = FocusedContactsStore.listen(() => {\n      return this.setState(this._getStateFromStores());\n    });\n  }\n\n  componentWillUnmount() {\n    this._usub();\n  }\n\n  _getStateFromStores() {\n    return {\n      sortedContacts: FocusedContactsStore.sortedContacts(),\n      focusedContact: FocusedContactsStore.focusedContact(),\n    };\n  }\n\n  _getKeyForContact(contact) {\n    if (!contact) {\n      return null\n    }\n    return contact.email + SPLIT_KEY + contact.name\n  }\n\n  _onSelectContact = (event) => {\n    const {sortedContacts} = this.state\n    const [email, name] = event.target.value.split(SPLIT_KEY);\n    const contact = sortedContacts.find((c) => c.name === name && c.email === email)\n    return Actions.focusContact(contact);\n  }\n\n  _renderSortedContacts() {\n    return this.state.sortedContacts.map((contact) => {\n      const key = this._getKeyForContact(contact)\n\n      return (\n        <option value={key} key={key}>\n          {contact.displayName({includeAccountLabel: true, forceAccountLabel: true})}\n        </option>\n      )\n    });\n  }\n\n  render() {\n    const {sortedContacts, focusedContact} = this.state\n    const value = this._getKeyForContact(focusedContact)\n    if (sortedContacts.length === 0 || !value) {\n      return false\n    }\n    return (\n      <div className=\"sidebar-participant-picker\">\n        <select tabIndex={-1} value={value} onChange={this._onSelectContact}>\n          {this._renderSortedContacts()}\n        </select>\n      </div>\n    )\n  }\n\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/sidebar-plugin-container.cjsx",
    "content": "_ = require 'underscore'\nReact = require \"react\"\n{FocusedContactsStore} = require(\"nylas-exports\")\n{InjectedComponentSet} = require(\"nylas-component-kit\")\n\nclass FocusedContactStorePropsContainer extends React.Component\n  @displayName: 'FocusedContactStorePropsContainer'\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @unsubscribe = FocusedContactsStore.listen(@_onChange)\n\n  componentWillUnmount: =>\n    @unsubscribe()\n\n  render: ->\n    classname = \"sidebar-section\"\n    if @state.focusedContact\n      classname += \" visible\"\n      inner = React.cloneElement(@props.children, @state)\n\n    <div className={classname}>{inner}</div>\n\n  _onChange: =>\n    @setState(@_getStateFromStores())\n\n  _getStateFromStores: =>\n    sortedContacts: FocusedContactsStore.sortedContacts()\n    focusedContact: FocusedContactsStore.focusedContact()\n    focusedContactThreads: FocusedContactsStore.focusedContactThreads()\n\nclass SidebarPluginContainer extends React.Component\n  @displayName: 'SidebarPluginContainer'\n\n  @containerStyles:\n    order: 1\n    flexShrink: 0\n    minWidth:200\n    maxWidth:300\n\n  constructor: (@props) ->\n\n  render: ->\n    <FocusedContactStorePropsContainer>\n      <SidebarPluginContainerInner />\n    </FocusedContactStorePropsContainer>\n\nclass SidebarPluginContainerInner extends React.Component\n  constructor: (@props) ->\n\n  render: ->\n    <InjectedComponentSet\n      className=\"sidebar-contact-card\"\n      key={@props.focusedContact.email}\n      matching={role: \"MessageListSidebar:ContactCard\"}\n      direction=\"column\"\n      exposedProps={contact: @props.focusedContact, contactThreads: @props.focusedContactThreads}/>\n\nmodule.exports = SidebarPluginContainer\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/thread-archive-button.cjsx",
    "content": "{RetinaImg} = require 'nylas-component-kit'\n{Actions,\n React,\n TaskFactory,\n DOMUtils,\n AccountStore,\n FocusedPerspectiveStore} = require 'nylas-exports'\n\nclass ThreadArchiveButton extends React.Component\n  @displayName: \"ThreadArchiveButton\"\n  @containerRequired: false\n\n  @propTypes:\n    thread: React.PropTypes.object.isRequired\n\n  render: =>\n    canArchiveThreads = FocusedPerspectiveStore.current().canArchiveThreads([@props.thread])\n    return <span /> unless canArchiveThreads\n\n    <button className=\"btn btn-toolbar btn-archive\"\n            style={order: -107}\n            title=\"Archive\"\n            onClick={@_onArchive}>\n      <RetinaImg name=\"toolbar-archive.png\" mode={RetinaImg.Mode.ContentIsMask}/>\n    </button>\n\n  _onArchive: (e) =>\n    return unless DOMUtils.nodeIsVisible(e.currentTarget)\n    Actions.archiveThreads({\n      threads: [@props.thread],\n      source: 'Toolbar Button: Message List',\n    })\n    Actions.popSheet()\n    e.stopPropagation()\n\nmodule.exports = ThreadArchiveButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/thread-star-button.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\n{Actions, Utils} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\n\nclass StarButton extends React.Component\n  @displayName: \"StarButton\"\n  @containerRequired: false\n  @propTypes:\n    thread: React.PropTypes.object\n\n  render: =>\n    selected = @props.thread? and @props.thread.starred\n    <button className=\"btn btn-toolbar\"\n            style={order: -104}\n            title={if selected then \"Remove star\" else \"Add star\"}\n            onClick={@_onStarToggle}>\n      <RetinaImg name=\"toolbar-star.png\" mode={RetinaImg.Mode.ContentIsMask} selected={selected} />\n    </button>\n\n  _onStarToggle: (e) =>\n    Actions.toggleStarredThreads({\n      source: \"Toolbar Button: Message List\",\n      threads: [@props.thread]\n    })\n    e.stopPropagation()\n\n\nmodule.exports = StarButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/thread-toggle-unread-button.cjsx",
    "content": "{Actions, React, FocusedContentStore} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\n\nclass ThreadToggleUnreadButton extends React.Component\n  @displayName: \"ThreadToggleUnreadButton\"\n  @containerRequired: false\n\n  render: =>\n    fragment = if @props.thread?.unread then \"read\" else \"unread\"\n    <button className=\"btn btn-toolbar\"\n            style={order: -105}\n            title=\"Mark as #{fragment}\"\n            onClick={@_onClick}>\n      <RetinaImg name=\"toolbar-markas#{fragment}.png\"\n                 mode={RetinaImg.Mode.ContentIsMask} />\n    </button>\n\n  _onClick: (e) =>\n    Actions.toggleUnreadThreads({\n      source: \"Toolbar Button: Thread List\",\n      threads: [@props.thread],\n    })\n    Actions.popSheet()\n    e.stopPropagation()\n\nmodule.exports = ThreadToggleUnreadButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/lib/thread-trash-button.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\n{Actions,\n DOMUtils,\n TaskFactory,\n AccountStore,\n FocusedPerspectiveStore} = require 'nylas-exports'\n{RetinaImg} = require 'nylas-component-kit'\n\nclass ThreadTrashButton extends React.Component\n  @displayName: \"ThreadTrashButton\"\n  @containerRequired: false\n\n  @propTypes:\n    thread: React.PropTypes.object.isRequired\n\n  render: =>\n    allowed = FocusedPerspectiveStore.current().canMoveThreadsTo([@props.thread], 'trash')\n    return <span /> unless allowed\n\n    <button className=\"btn btn-toolbar\"\n            style={order: -106}\n            title=\"Move to Trash\"\n            onClick={@_onRemove}>\n      <RetinaImg name=\"toolbar-trash.png\" mode={RetinaImg.Mode.ContentIsMask}/>\n    </button>\n\n  _onRemove: (e) =>\n    return unless DOMUtils.nodeIsVisible(e.currentTarget)\n    Actions.trashThreads({\n      source: \"Toolbar Button: Thread List\",\n      threads: [@props.thread],\n    })\n    Actions.popSheet()\n    e.stopPropagation()\n\n\nmodule.exports = ThreadTrashButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/package.json",
    "content": "{\n  \"name\": \"message-list\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"View messages for a thread\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/both-email-and-url-in.html",
    "content": "To test this, send https://www.google.com/search?q=test@example.com or gmail.com?q=bengotow@gmail.com\nto yourself from a client that allows plaintext or html editing.\n\nWhat about gmail.com/bengotow@gmail.com - Oh man you're asking for trouble.\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/both-email-and-url-out.html",
    "content": "To test this, send <a href=\"https://www.google.com/search?q=test@example.com\" title=\"https://www.google.com/search?q=test@example.com\">https://www.google.com/search?q=test@example.com</a> or <a href=\"gmail.com?q=bengotow@gmail.com\" title=\"gmail.com?q=bengotow@gmail.com\">gmail.com?q=bengotow@gmail.com</a>\nto yourself from a client that allows plaintext or html editing.\n\nWhat about <a href=\"gmail.com/bengotow@gmail.com\" title=\"gmail.com/bengotow@gmail.com\">gmail.com/bengotow@gmail.com</a> - Oh man you're asking for trouble.\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/gmail-in.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<title></title>\n</head>\n<body style=\"margin: 0; padding: 0;\" bgcolor=\"#FFFFFF\">\n<table width=\"100%\" height=\"100%\" style=\"min-width: 348px;\" border=\n\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tr height=\"32px\">\n<td></td>\n</tr>\n<tr align=\"center\">\n<td width=\"32px\"></td>\n<td>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"max-width: 600px;\">\n<tr>\n<td>\n<table width=\"100%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tr>\n<td align=\"left\"><img width=\"92px\" height=\"32px\" src=\n\"cid:google_logo\" style=\"display: block;\"></td>\n<td align=\"right\"><img width=\"32px\" height=\"32px\" style=\n\"display: block;\" src=\"cid:keyhole\"></td>\n</tr>\n</table>\n</td>\n</tr>\n<tr height=\"16\">\n<td></td>\n</tr>\n<tr>\n<td>\n<table bgcolor=\"#4184F3\" width=\"100%\" border=\"0\" cellspacing=\"0\"\ncellpadding=\"0\" style=\n\"min-width: 332px; max-width: 600px; border: 1px solid #E0E0E0; border-bottom: 0; border-top-left-radius: 3px; border-top-right-radius: 3px;\">\n<tr>\n<td height=\"72px\" colspan=\"3\"></td>\n</tr>\n<tr>\n<td width=\"32px\"></td>\n<td style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 24px; color: #FFFFFF; line-height: 1.25;\">\nNew sign-in from Chrome on Mac</td>\n<td width=\"32px\"></td>\n</tr>\n<tr>\n<td height=\"18px\" colspan=\"3\"></td>\n</tr>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table bgcolor=\"#FAFAFA\" width=\"100%\" border=\"0\" cellspacing=\"0\"\ncellpadding=\"0\" style=\n\"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;\">\n<tr height=\"16px\">\n<td width=\"32px\" rowspan=\"3\"></td>\n<td></td>\n<td width=\"32px\" rowspan=\"3\"></td>\n</tr>\n<tr>\n<td>\n<table style=\"min-width: 300px;\" border=\"0\" cellspacing=\"0\"\ncellpadding=\"0\">\n<tr>\n<td style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;\">\nHi Ben,</td>\n</tr>\n<tr>\n<td style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;\">\nYour Google Account careless@foundry376.com was just used to sign\nin from <span style=\"white-space:nowrap;\">Chrome</span> on\n<span style=\"white-space:nowrap;\">Mac</span>.\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"margin-top: 48px; margin-bottom: 48px;\">\n<tr valign=\"middle\">\n<td width=\"32px\"></td>\n<td align=\"center\"><img src=\"cid:profilephoto\" width=\"48px\" height=\n\"48px\" style=\"display: block; border-radius: 50%;\"></td>\n<td width=\"16px\"></td>\n<td style=\"line-height: 1;\"><span style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 20px; color: #202020;\">\nBen Gotow (Careless)</span><br>\n<span style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 13px; color: #727272;\">\ncareless@foundry376.com</span></td>\n</tr>\n<tr valign=\"middle\">\n<td width=\"32px\" height=\"24px\"></td>\n<td align=\"center\" height=\"24px\"><img src=\"cid:down_arrow\" width=\n\"4px\" height=\"10px\" style=\"display: block;\"></td>\n</tr>\n<tr valign=\"top\">\n<td width=\"32px\"></td>\n<td align=\"center\"><img src=\"cid:osx\" width=\"48px\" height=\"48px\"\nstyle=\"display: block;\"></td>\n<td width=\"16px\"></td>\n<td style=\"line-height: 1.5;\"><span style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 16px; color: #202020;\">\nMac</span><br>\n<span style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #727272;\">\nMonday, July 13, 2015 3:49 PM (Pacific Daylight Time)<br>\nSan Francisco, CA, USA*<br>\nChrome</span></td>\n</tr>\n</table>\n<b>Don't recognize this activity?</b><br>\nReview your <a href=\n\"https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&amp;am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31\"\nstyle=\"text-decoration: none; color: #4285F4;\" target=\n\"_blank\">recently used devices</a> now.<br>\n<br>\nWhy are we sending this? We take security very seriously and we\nwant to keep you in the loop on important actions in your\naccount.<br>\nWe were unable to determine whether you have used this browser or\ndevice with your account before. This can happen when you sign in\nfor the first time on a new computer, phone or browser, when you\nuse your browser's incognito or private browsing mode or clear your\ncookies, or when somebody else is accessing your account.</td>\n</tr>\n<tr height=\"32px\">\n<td></td>\n</tr>\n<tr>\n<td style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;\">\nBest,<br>\nThe Google Accounts team</td>\n</tr>\n<tr height=\"16px\">\n<td></td>\n</tr>\n<tr>\n<td style=\n\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: #B9B9B9; line-height: 1.5;\">\n*The location is approximate and determined by the IP address it\nwas coming from.<br>\nThis email can't receive replies. To give us feedback on this\nalert, <a href=\n\"https://support.google.com/accounts/contact/device_alert_feedback?hl=en\"\nstyle=\"text-decoration: none; color: #4285F4;\" target=\n\"_blank\">click here</a>.<br>\nFor more information, visit the <a href=\n\"https://support.google.com/accounts/answer/2733203\" style=\n\"text-decoration: none; color: #4285F4;\" target=\"_blank\">Google\nAccounts Help Center</a>.</td>\n</tr>\n</table>\n</td>\n</tr>\n<tr height=\"32px\">\n<td></td>\n</tr>\n</table>\n</td>\n</tr>\n<tr height=\"16\">\n<td></td>\n</tr>\n<tr>\n<td style=\n\"max-width: 600px; font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 10px; color: #BCBCBC; line-height: 1.5;\">\nYou received this mandatory email service announcement to update\nyou about important changes to your Google product or account.<br>\n<div style=\"direction: ltr; text-align: left\">© 2015 Google Inc.,\n1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</div>\n</td>\n</tr>\n</table>\n</td>\n<td width=\"32px\"></td>\n</tr>\n<tr height=\"32px\">\n<td></td>\n</tr>\n</table>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/gmail-out.html",
    "content": "\n\n\n<title></title>\n\n\n<table width=\"100%\" height=\"100%\" style=\"min-width: 348px;\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody><tr height=\"32px\">\n<td></td>\n</tr>\n<tr align=\"center\">\n<td width=\"32px\"></td>\n<td>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"max-width: 600px;\">\n<tbody><tr>\n<td>\n<table width=\"100%\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody><tr>\n<td align=\"left\"><img width=\"92px\" height=\"32px\" src=\"cid:google_logo\" style=\"display: block;\"></td>\n<td align=\"right\"><img width=\"32px\" height=\"32px\" style=\"display: block;\" src=\"cid:keyhole\"></td>\n</tr>\n</tbody></table>\n</td>\n</tr>\n<tr height=\"16\">\n<td></td>\n</tr>\n<tr>\n<td>\n<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;\">\n<tbody><tr>\n<td height=\"72px\" colspan=\"3\"></td>\n</tr>\n<tr>\n<td width=\"32px\"></td>\n<td style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 24px; color: #FFFFFF; line-height: 1.25;\">\nNew sign-in from Chrome on Mac</td>\n<td width=\"32px\"></td>\n</tr>\n<tr>\n<td height=\"18px\" colspan=\"3\"></td>\n</tr>\n</tbody></table>\n</td>\n</tr>\n<tr>\n<td>\n<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;\">\n<tbody><tr height=\"16px\">\n<td width=\"32px\" rowspan=\"3\"></td>\n<td></td>\n<td width=\"32px\" rowspan=\"3\"></td>\n</tr>\n<tr>\n<td>\n<table style=\"min-width: 300px;\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody><tr>\n<td style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;\">\nHi Ben,</td>\n</tr>\n<tr>\n<td style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;\">\nYour Google Account <a href=\"mailto:careless@foundry376.com\" title=\"mailto:careless@foundry376.com\">careless@foundry376.com</a> was just used to sign\nin from <span style=\"white-space:nowrap;\">Chrome</span> on\n<span style=\"white-space:nowrap;\">Mac</span>.\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"margin-top: 48px; margin-bottom: 48px;\">\n<tbody><tr valign=\"middle\">\n<td width=\"32px\"></td>\n<td align=\"center\"><img src=\"cid:profilephoto\" width=\"48px\" height=\"48px\" style=\"display: block; border-radius: 50%;\"></td>\n<td width=\"16px\"></td>\n<td style=\"line-height: 1;\"><span style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 20px; color: #202020;\">\nBen Gotow (Careless)</span><br>\n<span style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif;font-size: 13px; color: #727272;\">\n<a href=\"mailto:careless@foundry376.com\" title=\"mailto:careless@foundry376.com\">careless@foundry376.com</a></span></td>\n</tr>\n<tr valign=\"middle\">\n<td width=\"32px\" height=\"24px\"></td>\n<td align=\"center\" height=\"24px\"><img src=\"cid:down_arrow\" width=\"4px\" height=\"10px\" style=\"display: block;\"></td>\n</tr>\n<tr valign=\"top\">\n<td width=\"32px\"></td>\n<td align=\"center\"><img src=\"cid:osx\" width=\"48px\" height=\"48px\" style=\"display: block;\"></td>\n<td width=\"16px\"></td>\n<td style=\"line-height: 1.5;\"><span style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 16px; color: #202020;\">\nMac</span><br>\n<span style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #727272;\">\nMonday, July 13, 2015 3:49 PM (Pacific Daylight Time)<br>\nSan Francisco, CA, USA*<br>\nChrome</span></td>\n</tr>\n</tbody></table>\n<b>Don't recognize this activity?</b><br>\nReview your <a href=\"https://accounts.google.com/AccountChooser?Email=careless@foundry376.com&amp;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&amp;am%E2%80%A6//security.google.com/settings/security/activity/nt/1436827773000?rfn%3D31\">recently used devices</a> now.<br>\n<br>\nWhy are we sending this? We take security very seriously and we\nwant to keep you in the loop on important actions in your\naccount.<br>\nWe were unable to determine whether you have used this browser or\ndevice with your account before. This can happen when you sign in\nfor the first time on a new computer, phone or browser, when you\nuse your browser's incognito or private browsing mode or clear your\ncookies, or when somebody else is accessing your account.</td>\n</tr>\n<tr height=\"32px\">\n<td></td>\n</tr>\n<tr>\n<td style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 13px; color: #202020; line-height: 1.5;\">\nBest,<br>\nThe Google Accounts team</td>\n</tr>\n<tr height=\"16px\">\n<td></td>\n</tr>\n<tr>\n<td style=\"font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 12px; color: #B9B9B9; line-height: 1.5;\">\n*The location is approximate and determined by the IP address it\nwas coming from.<br>\nThis email can't receive replies. To give us feedback on this\nalert, <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>\nFor 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\nAccounts Help Center</a>.</td>\n</tr>\n</tbody></table>\n</td>\n</tr>\n<tr height=\"32px\">\n<td></td>\n</tr>\n</tbody></table>\n</td>\n</tr>\n<tr height=\"16\">\n<td></td>\n</tr>\n<tr>\n<td style=\"max-width: 600px; font-family: Roboto-Regular,Helvetica,Arial,sans-serif; font-size: 10px; color: #BCBCBC; line-height: 1.5;\">\nYou received this mandatory email service announcement to update\nyou about important changes to your Google product or account.<br>\n<div style=\"direction: ltr; text-align: left\">© 2015 Google Inc.,\n1600 Amphitheatre Parkway, Mountain View, CA 94043, USA</div>\n</td>\n</tr>\n</tbody></table>\n</td>\n<td width=\"32px\"></td>\n</tr>\n<tr height=\"32px\">\n<td></td>\n</tr>\n</tbody></table>\n\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/linkedin-in.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\"\n    \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\n<head>\n<title></title>\n</head>\n<body>\n\"\n<meta http-equiv=\"Content-Type\" content=\n\"text/html;charset=utf-8\" />\n<meta name=\"HandheldFriendly\" content=\"true\" />\n<meta name=\"viewport\" content=\n\"width=device-width; initial-scale=0.666667; maximum-scale=0.666667; user-scalable=0\" />\n<meta name=\"viewport\" content=\"width=device-width\" />\n<style type=\"text/css\">\n/*<![CDATA[*/\n@media all and (max-width:590px) { *[class].responsive { width:290px !important; } *[id]#center { width:50%; margin:0 auto; display:table; } *[class].display-none { display:none !important; } *[class].display-block { display:block !important; } *[class].fix-table-content { table-layout:fixed; } *[class].hide-for-mobile { display:none !important; } *[class].show-for-mobile { width:auto !important; max-height:none !important; visibility:visible !important; overflow:visible !important; float:none !important; height:auto !important; display:block !important; } *[class].responsive_header { display:table-cell !important; width:100% !important; color:#0077b5 !important; font-size:12px !important; } *[class].res-font10 { font-size:10px !important; } *[class].res-font12 { font-size:12px !important; } *[class].res-font13 { font-size:13px !important; } *[class].res-font14 { font-size:14px !important; } *[class].res-font16 { font-size:16px !important; } *[class].res-font18 { font-size:18px !important; } *[class].res-font20 { font-size:20px !important; } *[class].res-width10 { width:10px !important; } *[class].res-width20 { width:20px !important; } *[class].res-width25 { width:25px !important; } *[class].res-width120 { width:120px !important; } *[class].res-height0 { height:0px !important; } *[class].res-height10 { height:10px !important; } *[class].res-height20 { height:20px !important; } *[class].res-height30 { height:30px !important; } *[class].res-img40 { width:40px !important; height:40px !important; } *[class].res-img60 { width:60px !important; height:60px !important; } *[class].res-img75 { width:75px !important; height:75px !important; } *[class].res-img100 { width:100px !important; height:100px !important; } *[class].res-img150 { width:150px !important; height:150px !important; } *[class].res-img320 { width:320px !important; height:auto !important; } *[class].hideIMG { display:none; height:0px !important; width:0px !important; } *[class].responsive-spacer { width:10px !important; } *[class].header-spacer { table-layout:auto !important; width:250px !important; } *[class].header-spacer td, *[class].header-spacer div { width:250px !important; } *[class].center-content { text-align:center !important; } *[class].responsive-fullwidth { width:100% !important; } *[class].cellpadding-none, *[class].cellpadding-none table, *[class].cellpadding-none table td { border-collapse:collapse !important; padding:0 !important; } *[class].remove-margin { margin:0 !important; } *[class].remove-border { border:none !important; } table[class].responsive:not(.responsive-footer) { width:100% !important; } *[class].responsive-hidden { display:none !important; max-height:0 !important; font-size:0 !important; overflow:hidden !important; mso-hide:all !important; } *[class].responsive-body { width:100% !important; } *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { margin:0 auto; text-align:0 left; width:268px !important; } *[class].text-link { color:#008CC9; text-decoration:none; } *[class].res-font16 { font-size:16px !important; line-height:20px !important; } @media only screen and (max-width:532px) { *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { width:96% !important; max-width:532px; text-align:left; } } *[class].responsive-promo { margin:0 auto; text-align:0 left; width:100% !important; max-width:532px; text-align:left; } *[class].mobile-hidden { display:none; } } @media all and (-webkit-min-device-pixel-ratio:1.5) { *[id]#base-header-logo { background-image:url(https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_email_197x48_v1.png) !important; background-size:95px; background-repeat:no-repeat; width:95px !important; height:21px !important; } *[id]#base-header-logo img { display:none; } *[id]#base-header-logo a { height:21px !important; } *[id]#base-header-logo-china { background-image:url(https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_china_email_266x42_v1.png) !important; background-size:133px; background-repeat:no-repeat; width:133px !important; height:21px !important; } *[id]#base-header-logo-china img { display:none; } *[id]#base-header-logo-china a { height:21px !important; } }\n/*]]>*/\n</style>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\"\nwidth=\"100%\" bgcolor=\"#DFDFDF\">\n<tbody>\n<tr>\n<td>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\"\nwidth=\"1\" bgcolor=\"#DFDFDF\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:5px;font-size:5px;line-height:5px;\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" align=\"center\"\nwidth=\"100%\" style=\n\"table-layout:fixed;font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;min-width:290px;\" width=\n\"100%\" class=\"responsive\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-logo\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:7px;font-size:7px;line-height:7px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\"\nclass=\"responsive-logo\" bgcolor=\"#DFDFDF\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:8px;font-size:8px;line-height:8px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td valign=\"middle\" align=\"left\" height=\"35\" width=\"260\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\n\"responsive-logo\">\n<tbody>\n<tr>\n<td align=\"left\" valign=\"middle\" width=\"95\" height=\"21\" id=\n\"base-header-logo\"><a style=\n\"text-decoration:none;cursor:pointer;border:none;display:block;height:21px;width:100%;\"\nhref=\n\"https://www.linkedin.com/comm/nhome/?midToken=AQHp7WCc2qHESg&amp;trk=eml-b2%E2%80%A6l=eml-b2_content_ecosystem_digest-null-10-null-null-6m81ta%7Eilj7lvs1%7Eit\">\n<img src=\n\"https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_email_95x21_v1.png\"\nwidth=\"95\" height=\"21\" alt=\"LinkedIn\" style=\n\"border:none;text-decoration:none;\" /></a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\n\"responsive-logo\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:8px;font-size:8px;line-height:8px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:12px;font-size:12px;line-height:12px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" bgcolor=\n\"#FFFFFF\" class=\"responsive-body\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\n\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"1\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\n\"responsive-article\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n<tbody>\n<tr>\n<td align=\"center\" data-qa=\"section_votd\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\" align=\"left\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_author_link\" style=\n\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;t=plh&amp;midToken=A%E2%80%A6HkG&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Ftoday%2Fauthor%2F21685101\"\nstyle=\n\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">\nLarry Wilmore, Host and Executive Producer at \"The Nightly Show\nwith Larry Wilmore\"</a></td>\n</tr>\n<tr>\n<td colspan=\"3\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n</tr>\n<tr>\n<td align=\"left\" style=\n\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\"\ntitle=\"Why I Haven’t Worked in 30 Years\" style=\n\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">\nWhy I Haven’t Worked in 30 Years</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n</tr>\n<tr>\n<td data-qa=\"_summary_link\" align=\"left\" style=\n\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\"\nstyle=\n\"color:#868686;font-weight:100;text-decoration:none;\"></a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_summary_link\"><a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\"\ntitle=\"Why I Haven’t Worked in 30 Years\"><img src=\n\"https://media.licdn.com/media/AAEAAQAAAAAAAAXcAAAAJDNhYzZmNjQzLTdiNzEtNDliMi05ZWIxLWQ1OGQ0MmE1YmJkOQ.png\"\nalt=\"Highlight of the day\" style=\n\"max-width:100%;border-width:0;\" /></a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td bgcolor=\"#DDDDDD\" style=\"background-color:#dddddd;\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:1px;font-size:1px;line-height:1px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\n\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\" align=\"left\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" align=\n\"center\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"section_recommended_articles\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n<tbody>\n<tr>\n<td valign=\"middle\" align=\"left\" data-qa=\n\"recommended_articles_section_header\" style=\n\"margin-left:0;color:#666666;font-weight:100;text-decoration:none;font-size:16px;line-height:20px;text-align:left;\">\nRecommended for you</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\n\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\n\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/5/000/24a/3ec/1b1ee8f.jpg\"\nstyle=\"border:none; text-decoration:none; outline:hidden;\" alt=\n\"linkedin.com\" /></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\"\nstyle=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\n\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/comm/profile/view?id=AAsAAA7nVsoB_DQj7aOCUmGDxO0ro%E2%80%A6_ecosystem_digest-recommended_articles-51-null-null-6m81ta%7Eilj7lvs1%7Eit\"\nstyle=\n\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">\nLiz Claman</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\n\"recommended_articles_article_title_link\" align=\"left\" style=\n\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6=49&amp;m=recommended_articles&amp;permLink=letter-buffetts-law-liz-claman\"\nstyle=\n\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">\nThe Letter of Buffett’s Law</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\n\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6=50&amp;m=recommended_articles&amp;permLink=letter-buffetts-law-liz-claman\"\nstyle=\"color:#868686;font-weight:100;text-decoration:none;\">In a\nworld where people gravitate today to “shorter is better,” a world\nwhere a tweet of 140...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\n\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/8/000/202/01b/38c9a44.jpg\"\nstyle=\"border:none; text-decoration:none; outline:hidden;\" alt=\n\"linkedin.com\" /></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\"\nstyle=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\n\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAAOCMBF9g5NOCyznm5m9sou%E2%80%A6_ecosystem_digest-recommended_articles-57-null-null-6m81ta%7Eilj7lvs1%7Eit\"\nstyle=\n\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">\nTrish Nicolas</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\n\"recommended_articles_article_title_link\" align=\"left\" style=\n\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=what-do-when-your-facebook-feed-full-friends-selling-stuff-nicolas\"\nstyle=\n\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">\nWhat To Do When Your Facebook Feed Is Full of Friends Selling\nStuff</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\n\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=what-do-when-your-facebook-feed-full-friends-selling-stuff-nicolas\"\nstyle=\n\"color:#868686;font-weight:100;text-decoration:none;\">&nbsp;There\nhas been blog post after article after video after rant about how\nFacebook and other social...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\n\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/5/005/053/130/2d406a4.jpg\"\nstyle=\"border:none; text-decoration:none; outline:hidden;\" alt=\n\"linkedin.com\" /></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\"\nstyle=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\n\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAANXUBkcXuMdK34Tlpnoa8N%E2%80%A6_ecosystem_digest-recommended_articles-63-null-null-6m81ta%7Eilj7lvs1%7Eit\"\nstyle=\n\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">\nJosh Kopelman</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\n\"recommended_articles_article_title_link\" align=\"left\" style=\n\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6articles&amp;permLink=watney-rule-startups-return-old-normal-josh-kopelman\"\nstyle=\n\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">\nThe Watney Rule for Startups — and the Return to the ‘Old\nNormal’</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\n\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6articles&amp;permLink=watney-rule-startups-return-old-normal-josh-kopelman\"\nstyle=\n\"color:#868686;font-weight:100;text-decoration:none;\">Founders are\nrealizing the need to rethink prior assumptions about prioritizing\ngrowth above all...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\n\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/8/005/093/3ae/01eefce.jpg\"\nstyle=\"border:none; text-decoration:none; outline:hidden;\" alt=\n\"linkedin.com\" /></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\"\nstyle=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\n\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/comm/profile/view?id=AAsAAAMDxhcBDYxYriW1LuClTZtD2%E2%80%A6_ecosystem_digest-recommended_articles-69-null-null-6m81ta%7Eilj7lvs1%7Eit\"\nstyle=\n\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">\nDr. Travis Bradberry</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\n\"recommended_articles_article_title_link\" align=\"left\" style=\n\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=critical-skills-you-should-learn-pay-dividends-dr-travis-bradberry\"\nstyle=\n\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">\nCritical Skills You Should Learn That Pay Dividends\nForever</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\n\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=critical-skills-you-should-learn-pay-dividends-dr-travis-bradberry\"\nstyle=\"color:#868686;font-weight:100;text-decoration:none;\">The\nfurther along you are in your career, the easier it is to fall back\non the mistaken assumption...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\n\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/AAEAAQAAAAAAAAToAAAAJGJiMjhiYTBlLWVlZjktNGUyYS1hMjNmLTliOGUyZTU1MzBjZg.jpg\"\nstyle=\"border:none; text-decoration:none; outline:hidden;\" alt=\n\"linkedin.com\" /></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\"\nstyle=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\n\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/comm/profile/view?id=AAsAAACoOUgBv6MMrsHAs8f7frGan%E2%80%A6_ecosystem_digest-recommended_articles-75-null-null-6m81ta%7Eilj7lvs1%7Eit\"\nstyle=\n\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\">\nAlex Baydin</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\n\"recommended_articles_article_title_link\" align=\"left\" style=\n\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6permLink=what-every-startup-ceo-needs-know-go-down-like-parker-alex-baydin\"\nstyle=\n\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">\nWhat Every Startup CEO Needs to Know to Not Go Down like Parker\nConrad</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\n\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\n\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6permLink=what-every-startup-ceo-needs-know-go-down-like-parker-alex-baydin\"\nstyle=\"color:#868686;font-weight:100;text-decoration:none;\">Much\nhas been made of the recent resignation of Zenefits’ CEO, Parker\nConrad – not because the CEO...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td bgcolor=\"#DDDDDD\" style=\"background-color:#dddddd;\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:1px;font-size:1px;line-height:1px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\n\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\n\"responsive-article\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\" data-qa=\"section_start_writing\" class=\n\"mobile-hidden\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\n\"responsive-article\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\n\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:5px;font-size:5px;line-height:5px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"center\" style=\n\"color:#666666;font-weight:100;font-size:18px;line-height:20px;text-align:center;\">\nHave your own perspective to share?</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\n\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"center\" width=\"100%\" height=\"40\" bgcolor=\"#008CC9\"\nclass=\"res-font16\" style=\n\"font-weight:200;width:100%;font-size:20px;text-align:center;\">\n<a href=\n\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_create_a%E2%80%A6b2_content_ecosystem_digest&amp;li=78&amp;m=footer_promo&amp;ts=pub_upsell\"\ntarget=\"_blank\" style=\"color:#FFFFFF;text-decoration:none;\">Start\nwriting on LinkedIn</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\n\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:40px;font-size:40px;line-height:40px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td></td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\n\"responsive\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"color:#999999;font-size:11px;font-family:Helvetica,Arial,sans-serif;\"\nwidth=\"100%\">\n<tbody>\n<tr>\n<td>You are receiving notification emails from LinkedIn. <a style=\n\"color:#0077B5;text-decoration:none;\" href=\n\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;t=lun&amp;midToken=A%E2%80%A6NyKVukKWbPt8yA7mBX-kIWaokn7YiB8E4inwd2pXH8snw1h&amp;eid=6m81ta-ilj7lvs1-it\">\nUnsubscribe</a></td>\n</tr>\n<tr>\n<td></td>\n</tr>\n<tr>\n<td>This email was intended for Benjamin Hartester (Software\nDeveloper). <a href=\n\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=customerServiceUrl%E2%80%A6Token=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;articleId=4788\"\nstyle=\"color:#2e8dd7;text-decoration:none;\">Learn why we included\nthis.</a></td>\n</tr>\n<tr>\n<td>If you need assistance or have questions, please contact\n<a target=\"_blank\" href=\n\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=customerServiceUrl&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest\"\nstyle=\"color:#2e8dd7;text-decoration:none;\">LinkedIn Customer\nService</a>.</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>© 2016 LinkedIn Corporation, 2029 Stierlin Court, Mountain View\nCA 94043. LinkedIn and the LinkedIn logo are registered trademarks\nof LinkedIn.</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\n\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<img src=\"http://www.linkedin.com/emimp/6m81ta-ilj7lvs1-it.gif\"\nstyle=\"width:1px; height:1px;\" />\"\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/linkedin-out.html",
    "content": "\n\n\n<title></title>\n\n\n\"\n<meta http-equiv=\"Content-Type\" content=\"text/html;charset=utf-8\">\n<meta name=\"HandheldFriendly\" content=\"true\">\n<meta name=\"viewport\" content=\"width=device-width; initial-scale=0.666667; maximum-scale=0.666667; user-scalable=0\">\n<meta name=\"viewport\" content=\"width=device-width\">\n<style type=\"text/css\">\n/*<![CDATA[*/\n@media all and (max-width:590px) { *[class].responsive { width:290px !important; } *[id]#center { width:50%; margin:0 auto; display:table; } *[class].display-none { display:none !important; } *[class].display-block { display:block !important; } *[class].fix-table-content { table-layout:fixed; } *[class].hide-for-mobile { display:none !important; } *[class].show-for-mobile { width:auto !important; max-height:none !important; visibility:visible !important; overflow:visible !important; float:none !important; height:auto !important; display:block !important; } *[class].responsive_header { display:table-cell !important; width:100% !important; color:#0077b5 !important; font-size:12px !important; } *[class].res-font10 { font-size:10px !important; } *[class].res-font12 { font-size:12px !important; } *[class].res-font13 { font-size:13px !important; } *[class].res-font14 { font-size:14px !important; } *[class].res-font16 { font-size:16px !important; } *[class].res-font18 { font-size:18px !important; } *[class].res-font20 { font-size:20px !important; } *[class].res-width10 { width:10px !important; } *[class].res-width20 { width:20px !important; } *[class].res-width25 { width:25px !important; } *[class].res-width120 { width:120px !important; } *[class].res-height0 { height:0px !important; } *[class].res-height10 { height:10px !important; } *[class].res-height20 { height:20px !important; } *[class].res-height30 { height:30px !important; } *[class].res-img40 { width:40px !important; height:40px !important; } *[class].res-img60 { width:60px !important; height:60px !important; } *[class].res-img75 { width:75px !important; height:75px !important; } *[class].res-img100 { width:100px !important; height:100px !important; } *[class].res-img150 { width:150px !important; height:150px !important; } *[class].res-img320 { width:320px !important; height:auto !important; } *[class].hideIMG { display:none; height:0px !important; width:0px !important; } *[class].responsive-spacer { width:10px !important; } *[class].header-spacer { table-layout:auto !important; width:250px !important; } *[class].header-spacer td, *[class].header-spacer div { width:250px !important; } *[class].center-content { text-align:center !important; } *[class].responsive-fullwidth { width:100% !important; } *[class].cellpadding-none, *[class].cellpadding-none table, *[class].cellpadding-none table td { border-collapse:collapse !important; padding:0 !important; } *[class].remove-margin { margin:0 !important; } *[class].remove-border { border:none !important; } table[class].responsive:not(.responsive-footer) { width:100% !important; } *[class].responsive-hidden { display:none !important; max-height:0 !important; font-size:0 !important; overflow:hidden !important; mso-hide:all !important; } *[class].responsive-body { width:100% !important; } *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { margin:0 auto; text-align:0 left; width:268px !important; } *[class].text-link { color:#008CC9; text-decoration:none; } *[class].res-font16 { font-size:16px !important; line-height:20px !important; } @media only screen and (max-width:532px) { *[class].responsive-logo, *[class].responsive-article, *[class].responsive-footer, *[class].responsive-headline { width:96% !important; max-width:532px; text-align:left; } } *[class].responsive-promo { margin:0 auto; text-align:0 left; width:100% !important; max-width:532px; text-align:left; } *[class].mobile-hidden { display:none; } } @media all and (-webkit-min-device-pixel-ratio:1.5) { *[id]#base-header-logo { background-image:url(https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_email_197x48_v1.png) !important; background-size:95px; background-repeat:no-repeat; width:95px !important; height:21px !important; } *[id]#base-header-logo img { display:none; } *[id]#base-header-logo a { height:21px !important; } *[id]#base-header-logo-china { background-image:url(https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_china_email_266x42_v1.png) !important; background-size:133px; background-repeat:no-repeat; width:133px !important; height:21px !important; } *[id]#base-header-logo-china img { display:none; } *[id]#base-header-logo-china a { height:21px !important; } }\n/*]]>*/\n</style>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" bgcolor=\"#DFDFDF\">\n<tbody>\n<tr>\n<td>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"background-color:#dfdfdf;font-family:Helvetica,Arial,sans-serif;\" width=\"1\" bgcolor=\"#DFDFDF\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:5px;font-size:5px;line-height:5px;\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" align=\"center\" width=\"100%\" style=\"table-layout:fixed;font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;min-width:290px;\" width=\"100%\" class=\"responsive\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-logo\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:7px;font-size:7px;line-height:7px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\" class=\"responsive-logo\" bgcolor=\"#DFDFDF\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:8px;font-size:8px;line-height:8px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td valign=\"middle\" align=\"left\" height=\"35\" width=\"260\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-logo\">\n<tbody>\n<tr>\n<td align=\"left\" valign=\"middle\" width=\"95\" height=\"21\" id=\"base-header-logo\"><a style=\"text-decoration:none;cursor:pointer;border:none;display:block;height:21px;width:100%;\" href=\"https://www.linkedin.com/comm/nhome/?midToken=AQHp7WCc2qHESg&amp;trk=eml-b2%E2%80%A6l=eml-b2_content_ecosystem_digest-null-10-null-null-6m81ta%7Eilj7lvs1%7Eit\" title=\"https://www.linkedin.com/comm/nhome/?midToken=AQHp7WCc2qHESg&amp;trk=eml-b2%E2%80%A6l=eml-b2_content_ecosystem_digest-null-10-null-null-6m81ta%7Eilj7lvs1%7Eit\">\n<img src=\"https://static.licdn.com/scds/common/u/images/email/logos/logo_linkedin_tm_email_95x21_v1.png\" width=\"95\" height=\"21\" alt=\"LinkedIn\" style=\"border:none;text-decoration:none;\"></a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"responsive-logo\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:8px;font-size:8px;line-height:8px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:12px;font-size:12px;line-height:12px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" bgcolor=\"#FFFFFF\" class=\"responsive-body\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"1\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n<tbody>\n<tr>\n<td align=\"center\" data-qa=\"section_votd\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;t=plh&amp;midToken=A%E2%80%A6HkG&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Ftoday%2Fauthor%2F21685101\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\" title=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;t=plh&amp;midToken=A%E2%80%A6HkG&amp;url=https%3A%2F%2Fwww%2Elinkedin%2Ecom%2Ftoday%2Fauthor%2F21685101\">\nLarry Wilmore, Host and Executive Producer at \"The Nightly Show\nwith Larry Wilmore\"</a></td>\n</tr>\n<tr>\n<td colspan=\"3\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n</tr>\n<tr>\n<td align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\">\nWhy I Haven’t Worked in 30 Years</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_article_title_link\"></td>\n</tr>\n<tr>\n<td data-qa=\"_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\" style=\"color:#868686;font-weight:100;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\"></a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"votd_summary_link\"><a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6p;li=14&amp;m=hero&amp;permLink=why-i-havent-worked-30-years-larry-wilmore\"><img src=\"https://media.licdn.com/media/AAEAAQAAAAAAAAXcAAAAJDNhYzZmNjQzLTdiNzEtNDliMi05ZWIxLWQ1OGQ0MmE1YmJkOQ.png\" alt=\"Highlight of the day\" style=\"max-width:100%;border-width:0;\"></a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td bgcolor=\"#DDDDDD\" style=\"background-color:#dddddd;\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:1px;font-size:1px;line-height:1px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\">\n<tbody>\n<tr>\n<td align=\"center\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\" align=\"left\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"left\" data-qa=\"section_recommended_articles\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n<tbody>\n<tr>\n<td valign=\"middle\" align=\"left\" data-qa=\"recommended_articles_section_header\" style=\"margin-left:0;color:#666666;font-weight:100;text-decoration:none;font-size:16px;line-height:20px;text-align:left;\">\nRecommended for you</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/5/000/24a/3ec/1b1ee8f.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\"></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAA7nVsoB_DQj7aOCUmGDxO0ro%E2%80%A6_ecosystem_digest-recommended_articles-51-null-null-6m81ta%7Eilj7lvs1%7Eit\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\" title=\"https://www.linkedin.com/comm/profile/view?id=AAsAAA7nVsoB_DQj7aOCUmGDxO0ro%E2%80%A6_ecosystem_digest-recommended_articles-51-null-null-6m81ta%7Eilj7lvs1%7Eit\">\nLiz Claman</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6=49&amp;m=recommended_articles&amp;permLink=letter-buffetts-law-liz-claman\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6=49&amp;m=recommended_articles&amp;permLink=letter-buffetts-law-liz-claman\">\nThe Letter of Buffett’s Law</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6=50&amp;m=recommended_articles&amp;permLink=letter-buffetts-law-liz-claman\" style=\"color:#868686;font-weight:100;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6=50&amp;m=recommended_articles&amp;permLink=letter-buffetts-law-liz-claman\">In a\nworld where people gravitate today to “shorter is better,” a world\nwhere a tweet of 140...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/8/000/202/01b/38c9a44.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\"></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAAOCMBF9g5NOCyznm5m9sou%E2%80%A6_ecosystem_digest-recommended_articles-57-null-null-6m81ta%7Eilj7lvs1%7Eit\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\" title=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAAOCMBF9g5NOCyznm5m9sou%E2%80%A6_ecosystem_digest-recommended_articles-57-null-null-6m81ta%7Eilj7lvs1%7Eit\">\nTrish Nicolas</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=what-do-when-your-facebook-feed-full-friends-selling-stuff-nicolas\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=what-do-when-your-facebook-feed-full-friends-selling-stuff-nicolas\">\nWhat To Do When Your Facebook Feed Is Full of Friends Selling\nStuff</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=what-do-when-your-facebook-feed-full-friends-selling-stuff-nicolas\" style=\"color:#868686;font-weight:100;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=what-do-when-your-facebook-feed-full-friends-selling-stuff-nicolas\">&nbsp;There\nhas been blog post after article after video after rant about how\nFacebook and other social...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/5/005/053/130/2d406a4.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\"></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAANXUBkcXuMdK34Tlpnoa8N%E2%80%A6_ecosystem_digest-recommended_articles-63-null-null-6m81ta%7Eilj7lvs1%7Eit\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\" title=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAAANXUBkcXuMdK34Tlpnoa8N%E2%80%A6_ecosystem_digest-recommended_articles-63-null-null-6m81ta%7Eilj7lvs1%7Eit\">\nJosh Kopelman</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6articles&amp;permLink=watney-rule-startups-return-old-normal-josh-kopelman\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6articles&amp;permLink=watney-rule-startups-return-old-normal-josh-kopelman\">\nThe Watney Rule for Startups — and the Return to the ‘Old\nNormal’</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6articles&amp;permLink=watney-rule-startups-return-old-normal-josh-kopelman\" style=\"color:#868686;font-weight:100;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6articles&amp;permLink=watney-rule-startups-return-old-normal-josh-kopelman\">Founders are\nrealizing the need to rethink prior assumptions about prioritizing\ngrowth above all...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/p/8/005/093/3ae/01eefce.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\"></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAMDxhcBDYxYriW1LuClTZtD2%E2%80%A6_ecosystem_digest-recommended_articles-69-null-null-6m81ta%7Eilj7lvs1%7Eit\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\" title=\"https://www.linkedin.com/comm/profile/view?id=AAsAAAMDxhcBDYxYriW1LuClTZtD2%E2%80%A6_ecosystem_digest-recommended_articles-69-null-null-6m81ta%7Eilj7lvs1%7Eit\">\nDr. Travis Bradberry</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=critical-skills-you-should-learn-pay-dividends-dr-travis-bradberry\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=critical-skills-you-should-learn-pay-dividends-dr-travis-bradberry\">\nCritical Skills You Should Learn That Pay Dividends\nForever</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=critical-skills-you-should-learn-pay-dividends-dr-travis-bradberry\" style=\"color:#868686;font-weight:100;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6ermLink=critical-skills-you-should-learn-pay-dividends-dr-travis-bradberry\">The\nfurther along you are in your career, the easier it is to fall back\non the mistaken assumption...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td width=\"24\"><img width=\"24\" height=\"24\" src=\"https://media.licdn.com/mpr/mpr/shrinknp_100_100/AAEAAQAAAAAAAAToAAAAJGJiMjhiYTBlLWVlZjktNGUyYS1hMjNmLTliOGUyZTU1MzBjZg.jpg\" style=\"border:none; text-decoration:none; outline:hidden;\" alt=\"linkedin.com\"></td>\n<td width=\"7\">\n<table width=\"7px\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:0px;font-size:0px;line-height:0px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n<td align=\"left\" data-qa=\"recommended_articles_author_link\" style=\"color:#666666;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/comm/profile/view?id=AAsAAACoOUgBv6MMrsHAs8f7frGan%E2%80%A6_ecosystem_digest-recommended_articles-75-null-null-6m81ta%7Eilj7lvs1%7Eit\" style=\"color:#303030;font-weight:100;text-decoration:none;font-size:13px;line-height:20px;text-align:right;\" title=\"https://www.linkedin.com/comm/profile/view?id=AAsAAACoOUgBv6MMrsHAs8f7frGan%E2%80%A6_ecosystem_digest-recommended_articles-75-null-null-6m81ta%7Eilj7lvs1%7Eit\">\nAlex Baydin</a></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-body\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td class=\"res-font16\" data-qa=\"recommended_articles_article_title_link\" align=\"left\" style=\"margin-left:0;color:#008CC9;font-weight:100;font-size:22px;line-height:26px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6permLink=what-every-startup-ceo-needs-know-go-down-like-parker-alex-baydin\" style=\"margin-left:0;color:#008CC9;font-weight:100;text-decoration:none;text-align:left;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6permLink=what-every-startup-ceo-needs-know-go-down-like-parker-alex-baydin\">\nWhat Every Startup CEO Needs to Know to Not Go Down like Parker\nConrad</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td data-qa=\"recommended_articles_summary_link\" align=\"left\" style=\"margin:0;color:#666666;font-weight:100;font-size:16px;line-height:20px;text-align:left;\">\n<a href=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6permLink=what-every-startup-ceo-needs-know-go-down-like-parker-alex-baydin\" style=\"color:#868686;font-weight:100;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2/pulse?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_vi%E2%80%A6permLink=what-every-startup-ceo-needs-know-go-down-like-parker-alex-baydin\">Much\nhas been made of the recent resignation of Zenefits’ CEO, Parker\nConrad – not because the CEO...</a></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:30px;font-size:30px;line-height:30px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td bgcolor=\"#DDDDDD\" style=\"background-color:#dddddd;\">\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:1px;font-size:1px;line-height:1px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" class=\"res-height10\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:25px;font-size:25px;line-height:25px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive-article\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\" data-qa=\"section_start_writing\" class=\"mobile-hidden\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"520\" class=\"responsive-article\" align=\"center\">\n<tbody>\n<tr>\n<td align=\"center\"></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:5px;font-size:5px;line-height:5px\">&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"center\" style=\"color:#666666;font-weight:100;font-size:18px;line-height:20px;text-align:center;\">\nHave your own perspective to share?</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"center\" width=\"100%\" height=\"40\" bgcolor=\"#008CC9\" class=\"res-font16\" style=\"font-weight:200;width:100%;font-size:20px;text-align:center;\">\n<a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_create_a%E2%80%A6b2_content_ecosystem_digest&amp;li=78&amp;m=footer_promo&amp;ts=pub_upsell\" target=\"_blank\" style=\"color:#FFFFFF;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=pulse_web_create_a%E2%80%A6b2_content_ecosystem_digest&amp;li=78&amp;m=footer_promo&amp;ts=pub_upsell\">Start\nwriting on LinkedIn</a></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:40px;font-size:40px;line-height:40px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td></td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\" width=\"100%\" class=\"responsive\">\n<tbody>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td align=\"left\">\n<table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"color:#999999;font-size:11px;font-family:Helvetica,Arial,sans-serif;\" width=\"100%\">\n<tbody>\n<tr>\n<td>You are receiving notification emails from LinkedIn. <a style=\"color:#0077B5;text-decoration:none;\" href=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;t=lun&amp;midToken=A%E2%80%A6NyKVukKWbPt8yA7mBX-kIWaokn7YiB8E4inwd2pXH8snw1h&amp;eid=6m81ta-ilj7lvs1-it\" title=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;t=lun&amp;midToken=A%E2%80%A6NyKVukKWbPt8yA7mBX-kIWaokn7YiB8E4inwd2pXH8snw1h&amp;eid=6m81ta-ilj7lvs1-it\">\nUnsubscribe</a></td>\n</tr>\n<tr>\n<td></td>\n</tr>\n<tr>\n<td>This email was intended for Benjamin Hartester (Software\nDeveloper). <a href=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=customerServiceUrl%E2%80%A6Token=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;articleId=4788\" style=\"color:#2e8dd7;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=customerServiceUrl%E2%80%A6Token=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest&amp;articleId=4788\">Learn why we included\nthis.</a></td>\n</tr>\n<tr>\n<td>If you need assistance or have questions, please contact\n<a target=\"_blank\" href=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=customerServiceUrl&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest\" style=\"color:#2e8dd7;text-decoration:none;\" title=\"https://www.linkedin.com/e/v2?e=6m81ta-ilj7lvs1-it&amp;a=customerServiceUrl&amp;midToken=AQHp7WCc2qHESg&amp;ek=b2_content_ecosystem_digest\">LinkedIn Customer\nService</a>.</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>© 2016 LinkedIn Corporation, 2029 Stierlin Court, Mountain View\nCA 94043. LinkedIn and the LinkedIn logo are registered trademarks\nof LinkedIn.</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td></td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:10px;font-size:10px;line-height:10px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n<tr>\n<td>\n<table width=\"1\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"font-family:Helvetica,Arial,sans-serif;\">\n<tbody>\n<tr>\n<td>\n<div style=\"height:20px;font-size:20px;line-height:20px\">\n&nbsp;</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<img src=\"http://www.linkedin.com/emimp/6m81ta-ilj7lvs1-it.gif\" style=\"width:1px; height:1px;\">\"\n\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/medium-post-in.html",
    "content": "Reported on GitHub:\n\nhttps://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.3fela2o72\n\nGeez they have messy URLs.\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/medium-post-out.html",
    "content": "Reported on GitHub:\n\n<a href=\"https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.3fela2o72\" title=\"https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.3fela2o72\">https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0#.3fela2o72</a>\n\nGeez they have messy URLs.\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/nylas-url-in.html",
    "content": "Hello world\n\nnylas is cool.\n\nnylas://plugins?test=stuff\n\nnylas:plugins?test=stuff\n\n<strong>nylas://plugins?test=stuff</strong>\n\nDon't you like nylas?\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/nylas-url-out.html",
    "content": "Hello world\n\nnylas is cool.\n\n<a href=\"nylas://plugins?test=stuff\" title=\"nylas://plugins?test=stuff\">nylas://plugins?test=stuff</a>\n\n<a href=\"nylas:plugins?test=stuff\" title=\"nylas:plugins?test=stuff\">nylas:plugins?test=stuff</a>\n\n<strong><a href=\"nylas://plugins?test=stuff\" title=\"nylas://plugins?test=stuff\">nylas://plugins?test=stuff</a></strong>\n\nDon't you like nylas?\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/plaintext-in.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<title></title>\n</head>\n<body>\nhttp://apple.com/\n<div><br></div>\n<div>https://dropbox.com/</div>\n<div><br></div>\n<div>whatever.com</div>\n<div><br></div>\n<div>kinda-looks-like-a-link.com</div>\n<div><br></div>\n<div>ftp://helloworld.com/asd</div>\n<div><br></div>\n<div>540-250-2334</div>\n<div><br></div>\n<div>+1-524-123-3333</div>\n<div><br></div>\n<div>550.555.1234</div>\n<div><br></div>\n<div>bengotow@gmail.com</div>\n<div><br></div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/plaintext-out.html",
    "content": "\n\n\n<title></title>\n\n\n<a href=\"http://apple.com/\" title=\"http://apple.com/\">http://apple.com/</a>\n<div><br></div>\n<div><a href=\"https://dropbox.com/\" title=\"https://dropbox.com/\">https://dropbox.com/</a></div>\n<div><br></div>\n<div><a href=\"whatever.com\" title=\"whatever.com\">whatever.com</a></div>\n<div><br></div>\n<div><a href=\"kinda-looks-like-a-link.com\" title=\"kinda-looks-like-a-link.com\">kinda-looks-like-a-link.com</a></div>\n<div><br></div>\n<div><a href=\"ftp://helloworld.com/asd\" title=\"ftp://helloworld.com/asd\">ftp://helloworld.com/asd</a></div>\n<div><br></div>\n<div><a href=\"tel:540-250-2334\" title=\"tel:540-250-2334\">540-250-2334</a></div>\n<div><br></div>\n<div><a href=\"tel:+1-524-123-3333\" title=\"tel:+1-524-123-3333\">+1-524-123-3333</a></div>\n<div><br></div>\n<div>550.555.1234</div>\n<div><br></div>\n<div><a href=\"mailto:bengotow@gmail.com\" title=\"mailto:bengotow@gmail.com\">bengotow@gmail.com</a></div>\n<div><br></div>\n\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/readme-in.html",
    "content": "To get up and running with the api you'll need to follow these steps.\n\n1. Once you feel familiar with the endpoints and http responses go ham!!!\n\n> [ ID] Interval Transfer Bandwidth Jitter Lost/Total\n> [ 4] 0.00-10.00 sec 11.8 MBytes 9.90 Mbits/sec 0.687 ms 1397/1497 (93%)\n\ndiff --git a/drivers/video/fbdev/nvidia/nv_local.h b/drivers/video/fbdev/nvidia/nv_local.h\nindex 68e508d..2c6baa1 100644\n--- a/drivers/video/fbdev/nvidia/nv_local.h\n+++ b/drivers/video/fbdev/nvidia/nv_local.h\n\nThis is the correct solution as there is really no imx6sl-fox-p1.dts file.\n\n## Support\n\nPlease visit https://github.com/a/b/issues/new.\n\nPlease [open an issue](https://github.com/a/b/issues/new) for support.\n\nAlso see https://nylas.com/cloud/docs#receiving_notifications\n\nAlso see https://nylas.com/tag#about%20me\n\nAlso see https://nylas.com/tag#about%20\n\ndev.tellform.com/#!/verify/xcFfUbvQL0FG298GsB0nBJGS7QRi7nsWVjS9iSyaeyBCFgUv\n\n## Contributing\nIf you would like to contribute to the axefax api (which you are encouraged to do) here are the basics.\n\n- Ruby version: 2.2.2\n- System dependencies: Rails, AWS, postgres, eb-cli\n- Configuration:\n```bash\n$ git clone https://github.com/a/b.git\n$ bundle install\n```\n- Database intitialization/creation:\n```bash\n$ rake db:reset db:setup db:seed\n```\n- How to run the test suite:\n```bash\n$ rspec spec\n```\n- or alternatively:\n```bash\n$ guard\n```\n- run the server:\n```bash\n$ rails server\n```\n- Git Guidelines:\n - Please create a contributor/feature branch for any changes you make.\n - Be sure to always pull down the latest master branch before pushing.\n - etc...\n- Generating Documentation:\n - This app makes use of the (Apipie Gem)[https://github.com/Apipie/apipie-rails]\n - To Auto/Re-Generate Documentation for API Endpoints based on config/routes.rb\n   and the spec suite run...\n```bash\n$ APIPIE_RECORD=params rake spec:controllers\n$ APIPIE_RECORD=examples rake spec:controllers\n```\n - Then to generate static HTML files for production...\n```bash\n$ rake apipie:static\n```\n- Deployment instructions:\n - If the test suite is passing and you've successfully merged to master and pushed up to github...\n```bash\n$ eb deploy\n```\n - Hopefully you won't need to ssh into the remote server to run migrations but if you do...\n```bash\n$ eb ssh\nremote:ec2 ~ $ cd /var/app/current/\n```\n - From here you have access to a limited set of railsy stuffs. But for example rake db:migrate\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/readme-out.html",
    "content": "To get up and running with the api you'll need to follow these steps.\n\n1. Once you feel familiar with the endpoints and http responses go ham!!!\n\n&gt; [ ID] Interval Transfer Bandwidth Jitter Lost/Total\n&gt; [ 4] 0.00-10.00 sec 11.8 MBytes 9.90 Mbits/sec 0.687 ms 1397/1497 (93%)\n\ndiff --git a/drivers/video/fbdev/nvidia/nv_local.h b/drivers/video/fbdev/nvidia/nv_local.h\nindex 68e508d..2c6baa1 100644\n--- a/drivers/video/fbdev/nvidia/nv_local.h\n+++ b/drivers/video/fbdev/nvidia/nv_local.h\n\nThis is the correct solution as there is really no imx6sl-fox-p1.dts file.\n\n## Support\n\nPlease visit <a href=\"https://github.com/a/b/issues/new\" title=\"https://github.com/a/b/issues/new\">https://github.com/a/b/issues/new</a>.\n\nPlease [open an issue](<a href=\"https://github.com/a/b/issues/new\" title=\"https://github.com/a/b/issues/new\">https://github.com/a/b/issues/new</a>) for support.\n\nAlso see <a href=\"https://nylas.com/cloud/docs#receiving_notifications\" title=\"https://nylas.com/cloud/docs#receiving_notifications\">https://nylas.com/cloud/docs#receiving_notifications</a>\n\nAlso see <a href=\"https://nylas.com/tag#about%20me\" title=\"https://nylas.com/tag#about%20me\">https://nylas.com/tag#about%20me</a>\n\nAlso see <a href=\"https://nylas.com/tag#about%20\" title=\"https://nylas.com/tag#about%20\">https://nylas.com/tag#about%20</a>\n\n<a href=\"dev.tellform.com/#!/verify/xcFfUbvQL0FG298GsB0nBJGS7QRi7nsWVjS9iSyaeyBCFgUv\" title=\"dev.tellform.com/#!/verify/xcFfUbvQL0FG298GsB0nBJGS7QRi7nsWVjS9iSyaeyBCFgUv\">dev.tellform.com/#!/verify/xcFfUbvQL0FG298GsB0nBJGS7QRi7nsWVjS9iSyaeyBCFgUv</a>\n\n## Contributing\nIf you would like to contribute to the axefax api (which you are encouraged to do) here are the basics.\n\n- Ruby version: 2.2.2\n- System dependencies: Rails, AWS, postgres, eb-cli\n- Configuration:\n```bash\n$ git clone <a href=\"https://github.com/a/b.git\" title=\"https://github.com/a/b.git\">https://github.com/a/b.git</a>\n$ bundle install\n```\n- Database intitialization/creation:\n```bash\n$ rake db:reset db:setup db:seed\n```\n- How to run the test suite:\n```bash\n$ rspec spec\n```\n- or alternatively:\n```bash\n$ guard\n```\n- run the server:\n```bash\n$ rails server\n```\n- Git Guidelines:\n - Please create a contributor/feature branch for any changes you make.\n - Be sure to always pull down the latest master branch before pushing.\n - etc...\n- Generating Documentation:\n - This app makes use of the (Apipie Gem)[<a href=\"https://github.com/Apipie/apipie-rails\" title=\"https://github.com/Apipie/apipie-rails\">https://github.com/Apipie/apipie-rails</a>]\n - To Auto/Re-Generate Documentation for API Endpoints based on config/routes.rb\n   and the spec suite run...\n```bash\n$ APIPIE_RECORD=params rake spec:controllers\n$ APIPIE_RECORD=examples rake spec:controllers\n```\n - Then to generate static HTML files for production...\n```bash\n$ rake apipie:static\n```\n- Deployment instructions:\n - If the test suite is passing and you've successfully merged to master and pushed up to github...\n```bash\n$ eb deploy\n```\n - Hopefully you won't need to ssh into the remote server to run migrations but if you do...\n```bash\n$ eb ssh\nremote:ec2 ~ $ cd /var/app/current/\n```\n - From here you have access to a limited set of railsy stuffs. But for example rake db:migrate\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/strangeemails-in.html",
    "content": "1/ Regarding the duplicated notifications, did you send an email\nfrom \"joshua90@gmail.com\" to \"joshua@drntric.com\"?  Since we're\nsyncing those two accounts, you should be receiving webhooks for\nboth of them.\n\nmailbox+tag@hostanme.com\n\nMiles.O'Brian@example.com\n\n785ee39055efcd86359b6e05a9bef0e7@example.com\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/strangeemails-out.html",
    "content": "1/ Regarding the duplicated notifications, did you send an email\nfrom \"<a href=\"mailto:joshua90@gmail.com\" title=\"mailto:joshua90@gmail.com\">joshua90@gmail.com</a>\" to \"<a href=\"mailto:joshua@drntric.com\" title=\"mailto:joshua@drntric.com\">joshua@drntric.com</a>\"?  Since we're\nsyncing those two accounts, you should be receiving webhooks for\nboth of them.\n\n<a href=\"mailto:mailbox+tag@hostanme.com\" title=\"mailto:mailbox+tag@hostanme.com\">mailbox+tag@hostanme.com</a>\n\n<a href=\"mailto:Miles.O'Brian@example.com\" title=\"mailto:Miles.O'Brian@example.com\">Miles.O'Brian@example.com</a>\n\n<a href=\"mailto:785ee39055efcd86359b6e05a9bef0e7@example.com\" title=\"mailto:785ee39055efcd86359b6e05a9bef0e7@example.com\">785ee39055efcd86359b6e05a9bef0e7@example.com</a>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/strangephones-in.html",
    "content": "Give us a call at 540-250-1231. Thanks!\nGive us a call at +1540-250-1231. Thanks!\nGive us a call at +1-540-250-1231. Thanks!\nGive us a call at 1-540-250-1231. Thanks!\nGive us a call at +1-(540)-250-1231. Thanks!\nGive us a call at (540)-250-1231. Thanks!\nGive us a call at (540) 250 1231. Thanks!\nGive us a call at 540 250 1231. Thanks!\nGive us a call at +1 540 250 1231. Thanks!\nGive us a call at 6641234567. Thanks!\nGive us a call at 664 123 4567. Thanks!\nGive us a call at (044) 664 123 4567. Thanks!\nGive us a call at 0333 320 1030. Thanks!\n\n123123-1223-12-312-31-23-123123-12341515124124-123124\n1111123123123-1231\n123123123123123123123123123123123\n\nHere's the number:(540) 250 1231\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/strangephones-out.html",
    "content": "Give us a call at <a href=\"tel:540-250-1231\" title=\"tel:540-250-1231\">540-250-1231</a>. Thanks!\nGive us a call at <a href=\"tel:+1540-250-1231\" title=\"tel:+1540-250-1231\">+1540-250-1231</a>. Thanks!\nGive us a call at <a href=\"tel:+1-540-250-1231\" title=\"tel:+1-540-250-1231\">+1-540-250-1231</a>. Thanks!\nGive us a call at <a href=\"tel:1-540-250-1231\" title=\"tel:1-540-250-1231\">1-540-250-1231</a>. Thanks!\nGive us a call at <a href=\"tel:+1-(540)-250-1231\" title=\"tel:+1-(540)-250-1231\">+1-(540)-250-1231</a>. Thanks!\nGive us a call at <a href=\"tel:(540)-250-1231\" title=\"tel:(540)-250-1231\">(540)-250-1231</a>. Thanks!\nGive us a call at <a href=\"tel:(540) 250 1231\" title=\"tel:(540) 250 1231\">(540) 250 1231</a>. Thanks!\nGive us a call at <a href=\"tel:540 250 1231\" title=\"tel:540 250 1231\">540 250 1231</a>. Thanks!\nGive us a call at <a href=\"tel:+1 540 250 1231\" title=\"tel:+1 540 250 1231\">+1 540 250 1231</a>. Thanks!\nGive us a call at 6641234567. Thanks!\nGive us a call at <a href=\"tel:664 123 4567\" title=\"tel:664 123 4567\">664 123 4567</a>. Thanks!\nGive us a call at <a href=\"tel:(044) 664 123 4567\" title=\"tel:(044) 664 123 4567\">(044) 664 123 4567</a>. Thanks!\nGive us a call at <a href=\"tel:0333 320 1030\" title=\"tel:0333 320 1030\">0333 320 1030</a>. Thanks!\n\n123123-1223-12-312-31-23-123123-12341515124124-123124\n1111123123123-1231\n123123123123123123123123123123123\n\nHere's the number:<a href=\"tel:(540) 250 1231\" title=\"tel:(540) 250 1231\">(540) 250 1231</a>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/twitter-in.html",
    "content": "Reported on GitHub:\n\nhttps://twitter.com/SF_emergency/status/714901408298893317\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/twitter-out.html",
    "content": "Reported on GitHub:\n\n<a href=\"https://twitter.com/SF_emergency/status/714901408298893317\" title=\"https://twitter.com/SF_emergency/status/714901408298893317\">https://twitter.com/SF_emergency/status/714901408298893317</a>\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/url-with-port-in.html",
    "content": "HTTP links with port in them don't link correctly either,\ne.g. http://example.com:8080/path/ only links http://example.com.\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-fixtures/url-with-port-out.html",
    "content": "HTTP links with port in them don't link correctly either,\ne.g. <a href=\"http://example.com:8080/path/\" title=\"http://example.com:8080/path/\">http://example.com:8080/path/</a> only links <a href=\"http://example.com\" title=\"http://example.com\">http://example.com</a>.\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/autolinker-spec.es6",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport {autolink} from '../lib/autolinker';\n\ndescribe('autolink', function autolinkSpec() {\n  const fixturesDir = path.join(__dirname, 'autolinker-fixtures');\n  fs.readdirSync(fixturesDir).filter(filename =>\n    filename.indexOf('-in.html') !== -1\n  ).forEach((filename) => {\n    it(`should properly autolink a variety of email bodies ${filename}`, () => {\n      const div = document.createElement('div');\n      const inputPath = path.join(fixturesDir, filename);\n      const expectedPath = inputPath.replace('-in', '-out');\n\n      const input = fs.readFileSync(inputPath).toString();\n      const expected = fs.readFileSync(expectedPath).toString();\n\n      div.innerHTML = input;\n      autolink({body: div});\n\n      expect(div.innerHTML).toEqual(expected);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/message-item-body-spec.cjsx",
    "content": "proxyquire = require 'proxyquire'\nReact = require \"react\"\nReactDOM = require \"react-dom\"\nReactTestUtils = require('react-addons-test-utils')\n\n{Contact,\n Message,\n File,\n FileDownloadStore,\n MessageBodyProcessor} = require \"nylas-exports\"\n\nEmailFrameStub = React.createClass({render: -> <div></div>})\n\n{InjectedComponent} = require 'nylas-component-kit'\n\nfile = new File\n  id: 'file_1_id'\n  filename: 'a.png'\n  contentType: 'image/png'\n  size: 10\nfile_not_downloaded = new File\n  id: 'file_2_id'\n  filename: 'b.png'\n  contentType: 'image/png'\n  size: 10\nfile_inline = new File\n  id: 'file_inline_id'\n  filename: 'c.png'\n  contentId: 'file_inline_id'\n  contentType: 'image/png'\n  size: 10\nfile_inline_downloading = new File\n  id: 'file_inline_downloading_id'\n  filename: 'd.png'\n  contentId: 'file_inline_downloading_id'\n  contentType: 'image/png'\n  size: 10\nfile_inline_not_downloaded = new File\n  id: 'file_inline_not_downloaded_id'\n  filename: 'e.png'\n  contentId: 'file_inline_not_downloaded_id'\n  contentType: 'image/png'\n  size: 10\nfile_cid_but_not_referenced = new File\n  id: 'file_cid_but_not_referenced'\n  filename: 'f.png'\n  contentId: 'file_cid_but_not_referenced'\n  contentType: 'image/png'\n  size: 10\nfile_cid_but_not_referenced_or_image = new File\n  id: 'file_cid_but_not_referenced_or_image'\n  filename: 'ansible notes.txt'\n  contentId: 'file_cid_but_not_referenced_or_image'\n  contentType: 'text/plain'\n  size: 300\nfile_without_filename = new File\n  id: 'file_without_filename'\n  contentType: 'image/png'\n  size: 10\n\ndownload =\n  fileId: 'file_1_id'\ndownload_inline =\n  fileId: 'file_inline_downloading_id'\n\nuser_1 = new Contact\n  name: \"User One\"\n  email: \"user1@nylas.com\"\nuser_2 = new Contact\n  name: \"User Two\"\n  email: \"user2@nylas.com\"\nuser_3 = new Contact\n  name: \"User Three\"\n  email: \"user3@nylas.com\"\nuser_4 = new Contact\n  name: \"User Four\"\n  email: \"user4@nylas.com\"\n\nMessageItemBody = proxyquire '../lib/message-item-body',\n  './email-frame': {default: EmailFrameStub}\n\n\nxdescribe \"MessageItem\", ->\n  beforeEach ->\n    spyOn(FileDownloadStore, 'pathForFile').andCallFake (f) ->\n      return '/fake/path.png' if f.id is file.id\n      return '/fake/path-inline.png' if f.id is file_inline.id\n      return '/fake/path-downloading.png' if f.id is file_inline_downloading.id\n      return null\n    spyOn(MessageBodyProcessor, '_addToCache').andCallFake ->\n\n    @downloads =\n      'file_1_id': download,\n      'file_inline_downloading_id': download_inline\n\n    @message = new Message\n      id: \"111\"\n      from: [user_1]\n      to: [user_2]\n      cc: [user_3, user_4]\n      bcc: null\n      body: \"Body One\"\n      date: new Date(1415814587)\n      draft: false\n      files: []\n      unread: false\n      snippet: \"snippet one...\"\n      subject: \"Subject One\"\n      threadId: \"thread_12345\"\n      accountId: window.TEST_ACCOUNT_ID\n\n    # Generate the test component. Should be called after @message is configured\n    # for the test, since MessageItem assumes attributes of the message will not\n    # change after getInitialState runs.\n    @createComponent = ({collapsed} = {}) =>\n      collapsed ?= false\n      @component = ReactTestUtils.renderIntoDocument(\n        <MessageItemBody message={@message} downloads={@downloads} />\n      )\n      advanceClock()\n\n  describe \"when the message contains attachments\", ->\n    beforeEach ->\n      @message.files = [\n        file,\n        file_not_downloaded,\n        file_cid_but_not_referenced,\n        file_cid_but_not_referenced_or_image,\n\n        file_inline,\n        file_inline_downloading,\n        file_inline_not_downloaded,\n        file_without_filename\n      ]\n\n    describe \"inline\", ->\n      beforeEach ->\n        @message.body = \"\"\"\n          <img alt=\\\"A\\\" src=\\\"cid:#{file_inline.contentId}\\\"/>\n          <img alt=\\\"B\\\" src=\\\"cid:#{file_inline_downloading.contentId}\\\"/>\n          <img alt=\\\"C\\\" src=\\\"cid:#{file_inline_not_downloaded.contentId}\\\"/>\n          <img src=\\\"cid:missing-attachment\\\"/>\n          Hello world!\n          \"\"\"\n        @createComponent()\n        waitsFor =>\n          ReactTestUtils.scryRenderedComponentsWithType(@component, EmailFrameStub).length\n\n      it \"should never leave src=cid: in the message body\", ->\n        runs =>\n          body = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub).props.content\n          expect(body.indexOf('cid')).toEqual(-1)\n\n      it \"should replace cid:<file.contentId> with the FileDownloadStore's path for the file\", ->\n        runs =>\n          body = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub).props.content\n          expect(body.indexOf('alt=\"A\" src=\"file:///fake/path-inline.png\"')).toEqual(@message.body.indexOf('alt=\"A\"'))\n\n      it \"should not replace cid:<file.contentId> with the FileDownloadStore's path if the download is in progress\", ->\n        runs =>\n          body = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub).props.content\n          expect(body.indexOf('/fake/path-downloading.png')).toEqual(-1)\n\n  describe \"showQuotedText\", ->\n    it \"should be initialized to false\", ->\n      @createComponent()\n      expect(@component.state.showQuotedText).toBe(false)\n\n    it \"shouldn't render the quoted text control if there's no quoted text\", ->\n      @message.body = \"no quotes here!\"\n      @createComponent()\n      toggles = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, 'quoted-text-control')\n      expect(toggles.length).toBe 0\n\n    describe 'quoted text control toggle button', ->\n      beforeEach ->\n        @message.body = \"\"\"\n          Message\n          <blockquote class=\"gmail_quote\">\n            Quoted message\n          </blockquote>\n          \"\"\"\n        @createComponent()\n        @toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')\n\n      it 'should be rendered', ->\n        expect(@toggle).toBeDefined()\n\n    it \"should be initialized to true if the message contains `Forwarded`...\", ->\n      @message.body = \"\"\"\n        Hi guys, take a look at this. Very relevant. -mg\n        <br>\n        <br>\n        <div class=\"gmail_quote\">\n          ---- Forwarded Message -----\n          blablalba\n        </div>\n        \"\"\"\n      @createComponent()\n      expect(@component.state.showQuotedText).toBe(true)\n\n    it \"should be initialized to false if the message is a response to a Forwarded message\", ->\n      @message.body = \"\"\"\n        Thanks mg, that indeed looks very relevant. Will bring it up\n        with the rest of the team.\n\n        On Sunday, March 4th at 12:32AM, Michael Grinich Wrote:\n        <div class=\"gmail_quote\">\n          Hi guys, take a look at this. Very relevant. -mg\n          <br>\n          <br>\n          <div class=\"gmail_quote\">\n            ---- Forwarded Message -----\n            blablalba\n          </div>\n        </div>\n        \"\"\"\n      @createComponent()\n      expect(@component.state.showQuotedText).toBe(false)\n\n    describe \"when showQuotedText is true\", ->\n      beforeEach ->\n        @message.body = \"\"\"\n          Message\n          <blockquote class=\"gmail_quote\">\n            Quoted message\n          </blockquote>\n          \"\"\"\n        @createComponent()\n        @component.state.showQuotedText = true\n        waitsFor =>\n          ReactTestUtils.scryRenderedComponentsWithType(@component, EmailFrameStub).length\n\n      describe 'quoted text control toggle button', ->\n        beforeEach ->\n          @toggle = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'quoted-text-control')\n\n        it 'should be rendered', ->\n          expect(@toggle).toBeDefined()\n\n      it \"should pass the value into the EmailFrame\", ->\n        runs =>\n          frame = ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)\n          expect(frame.props.showQuotedText).toBe(true)\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/message-item-container-spec.cjsx",
    "content": "React = require \"react\"\nproxyquire = require(\"proxyquire\").noPreserveCache()\nReactTestUtils = require('react-addons-test-utils')\n\n{Thread,\n Message,\n ComponentRegistry,\n DraftStore} = require 'nylas-exports'\n\nclass StubMessageItem extends React.Component\n  @displayName: \"StubMessageItem\"\n  render: -> <span></span>\n\nclass StubComposer extends React.Component\n  @displayName: \"StubComposer\"\n  render: -> <span></span>\n\nMessageItemContainer = proxyquire '../lib/message-item-container',\n  \"./message-item\": StubMessageItem\n\ntestThread = new Thread(id: \"t1\", accountId: TEST_ACCOUNT_ID)\ntestClientId = \"local-id\"\ntestMessage = new Message(id: \"m1\", draft: false, unread: true, accountId: TEST_ACCOUNT_ID)\ntestDraft = new Message(id: \"d1\", draft: true, unread: true, accountId: TEST_ACCOUNT_ID)\n\nxdescribe 'MessageItemContainer', ->\n\n  beforeEach ->\n    @isSendingDraft = false\n    spyOn(DraftStore, \"isSendingDraft\").andCallFake => @isSendingDraft\n    ComponentRegistry.register(StubComposer, role: 'Composer')\n\n  afterEach ->\n    ComponentRegistry.register(StubComposer, role: 'Composer')\n\n  renderContainer = (message) ->\n    ReactTestUtils.renderIntoDocument(\n      <MessageItemContainer thread={testThread}\n                            message={message}\n                            draftClientId={testClientId} />\n    )\n\n  it \"shows composer if it's a draft\", ->\n    @isSendingDraft = false\n    doc = renderContainer(testDraft)\n    items = ReactTestUtils.scryRenderedComponentsWithType(doc, StubComposer)\n    expect(items.length).toBe 1\n\n  it \"renders a message if it's a draft that is sending\", ->\n    @isSendingDraft = true\n    doc = renderContainer(testDraft)\n    items = ReactTestUtils.scryRenderedComponentsWithType(doc, StubMessageItem)\n    expect(items.length).toBe 1\n    expect(items[0].props.pending).toBe true\n\n  it \"renders a message if it's not a draft\", ->\n    @isSendingDraft = false\n    doc = renderContainer(testMessage)\n    items = ReactTestUtils.scryRenderedComponentsWithType(doc, StubMessageItem)\n    expect(items.length).toBe 1\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/message-item-spec.cjsx",
    "content": "proxyquire = require 'proxyquire'\nReact = require \"react\"\nReactDOM = require \"react-dom\"\nReactTestUtils = require 'react-addons-test-utils'\n\n{Contact,\n Message,\n File,\n Thread,\n Utils,\n QuotedHTMLTransformer,\n FileDownloadStore,\n MessageBodyProcessor} = require \"nylas-exports\"\n\nMessageItemBody = React.createClass({render: -> <div></div>})\n\n{InjectedComponent} = require 'nylas-component-kit'\n\nfile = new File\n  id: 'file_1_id'\n  filename: 'a.png'\n  contentType: 'image/png'\n  size: 10\nfile_not_downloaded = new File\n  id: 'file_2_id'\n  filename: 'b.png'\n  contentType: 'image/png'\n  size: 10\nfile_inline = new File\n  id: 'file_inline_id'\n  filename: 'c.png'\n  contentId: 'file_inline_id'\n  contentType: 'image/png'\n  size: 10\nfile_inline_downloading = new File\n  id: 'file_inline_downloading_id'\n  filename: 'd.png'\n  contentId: 'file_inline_downloading_id'\n  contentType: 'image/png'\n  size: 10\nfile_inline_not_downloaded = new File\n  id: 'file_inline_not_downloaded_id'\n  filename: 'e.png'\n  contentId: 'file_inline_not_downloaded_id'\n  contentType: 'image/png'\n  size: 10\nfile_cid_but_not_referenced = new File\n  id: 'file_cid_but_not_referenced'\n  filename: 'f.png'\n  contentId: 'file_cid_but_not_referenced'\n  contentType: 'image/png'\n  size: 10\nfile_cid_but_not_referenced_or_image = new File\n  id: 'file_cid_but_not_referenced_or_image'\n  filename: 'ansible notes.txt'\n  contentId: 'file_cid_but_not_referenced_or_image'\n  contentType: 'text/plain'\n  size: 300\nfile_without_filename = new File\n  id: 'file_without_filename'\n  contentType: 'image/png'\n  size: 10\n\ndownload =\n  fileId: 'file_1_id'\ndownload_inline =\n  fileId: 'file_inline_downloading_id'\n\nuser_1 = new Contact\n  name: \"User One\"\n  email: \"user1@nylas.com\"\nuser_2 = new Contact\n  name: \"User Two\"\n  email: \"user2@nylas.com\"\nuser_3 = new Contact\n  name: \"User Three\"\n  email: \"user3@nylas.com\"\nuser_4 = new Contact\n  name: \"User Four\"\n  email: \"user4@nylas.com\"\nuser_5 = new Contact\n  name: \"User Five\"\n  email: \"user5@nylas.com\"\n\n\nMessageItem = proxyquire '../lib/message-item',\n  './message-item-body': MessageItemBody\n\nMessageTimestamp = require('../lib/message-timestamp').default\n\n\nxdescribe \"MessageItem\", ->\n  beforeEach ->\n    spyOn(FileDownloadStore, 'pathForFile').andCallFake (f) ->\n      return '/fake/path.png' if f.id is file.id\n      return '/fake/path-inline.png' if f.id is file_inline.id\n      return '/fake/path-downloading.png' if f.id is file_inline_downloading.id\n      return null\n    spyOn(FileDownloadStore, 'getDownloadDataForFiles').andCallFake (ids) ->\n      return {'file_1_id': download, 'file_inline_downloading_id': download_inline}\n\n    spyOn(MessageBodyProcessor, '_addToCache').andCallFake ->\n\n    @message = new Message\n      id: \"111\"\n      from: [user_1]\n      to: [user_2]\n      cc: [user_3, user_4]\n      bcc: null\n      body: \"Body One\"\n      date: new Date(1415814587)\n      draft: false\n      files: []\n      unread: false\n      snippet: \"snippet one...\"\n      subject: \"Subject One\"\n      threadId: \"thread_12345\"\n      accountId: TEST_ACCOUNT_ID\n\n    @thread = new Thread\n      id: 'thread-111'\n      accountId: TEST_ACCOUNT_ID\n\n    @threadParticipants = [user_1, user_2, user_3, user_4]\n\n    # Generate the test component. Should be called after @message is configured\n    # for the test, since MessageItem assumes attributes of the message will not\n    # change after getInitialState runs.\n    @createComponent = ({collapsed} = {}) =>\n      collapsed ?= false\n      @component = ReactTestUtils.renderIntoDocument(\n        <MessageItem key={@message.id}\n                     message={@message}\n                     thread={@thread}\n                     collapsed={collapsed} />\n      )\n\n  # TODO: We currently don't support collapsed messages\n  # describe \"when collapsed\", ->\n  #   beforeEach ->\n  #     @createComponent({collapsed: true})\n  #\n  #   it \"should not render the EmailFrame\", ->\n  #     expect( -> ReactTestUtils.findRenderedComponentWithType(@component, EmailFrameStub)).toThrow()\n  #\n  #   it \"should have the `collapsed` class\", ->\n  #     expect(ReactDOM.findDOMNode(@component).className.indexOf('collapsed') >= 0).toBe(true)\n\n  describe \"when displaying detailed headers\", ->\n    beforeEach ->\n      @createComponent({collapsed: false})\n      @component.setState detailedHeaders: true\n\n    it \"correctly sets the participant states\", ->\n      participants = ReactTestUtils.scryRenderedDOMComponentsWithClass(@component, \"expanded-participants\")\n      expect(participants.length).toBe 2\n      expect(-> ReactTestUtils.findRenderedDOMComponentWithClass(@component, \"collapsed-participants\")).toThrow()\n\n    it \"correctly sets the timestamp\", ->\n      ts = ReactTestUtils.findRenderedComponentWithType(@component, MessageTimestamp)\n      expect(ts.props.isDetailed).toBe true\n\n  describe \"when not collapsed\", ->\n    beforeEach ->\n      @createComponent({collapsed: false})\n\n    it \"should render the MessageItemBody\", ->\n      frame = ReactTestUtils.findRenderedComponentWithType(@component, MessageItemBody)\n      expect(frame).toBeDefined()\n\n    it \"should not have the `collapsed` class\", ->\n      expect(ReactDOM.findDOMNode(@component).className.indexOf('collapsed') >= 0).toBe(false)\n\n  xdescribe \"when the message contains attachments\", ->\n    beforeEach ->\n      @message.files = [\n        file,\n        file_not_downloaded,\n        file_cid_but_not_referenced,\n        file_cid_but_not_referenced_or_image,\n\n        file_inline,\n        file_inline_downloading,\n        file_inline_not_downloaded,\n        file_without_filename\n      ]\n      @message.body = \"\"\"\n        <img alt=\\\"A\\\" src=\\\"cid:#{file_inline.contentId}\\\"/>\n        <img alt=\\\"B\\\" src=\\\"cid:#{file_inline_downloading.contentId}\\\"/>\n        <img alt=\\\"C\\\" src=\\\"cid:#{file_inline_not_downloaded.contentId}\\\"/>\n        <img src=\\\"cid:missing-attachment\\\"/>\n        \"\"\"\n      @createComponent()\n\n    it \"should include the attachments area\", ->\n      attachments = ReactTestUtils.findRenderedDOMComponentWithClass(@component, 'attachments-area')\n      expect(attachments).toBeDefined()\n\n    it 'injects a MessageAttachments component for any present attachments', ->\n      els = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@component, InjectedComponent, matching: {role: \"MessageAttachments\"})\n      expect(els.length).toBe 1\n\n    it \"should list attachments that are not mentioned in the body via cid\", ->\n      els = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@component, InjectedComponent, matching: {role: \"MessageAttachments\"})\n      attachments = els[0].props.exposedProps.files\n      expect(attachments.length).toEqual(5)\n      expect(attachments[0]).toBe(file)\n      expect(attachments[1]).toBe(file_not_downloaded)\n      expect(attachments[2]).toBe(file_cid_but_not_referenced)\n      expect(attachments[3]).toBe(file_cid_but_not_referenced_or_image)\n\n    it \"should provide the correct file download state for each attachment\", ->\n      els = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@component, InjectedComponent, matching: {role: \"MessageAttachments\"})\n      {downloads} = els[0].props.exposedProps\n      expect(downloads['file_1_id']).toBe(download)\n      expect(downloads['file_not_downloaded']).toBe(undefined)\n\n    it \"should still list attachments when the message has no body\", ->\n      @message.body = \"\"\n      @createComponent()\n      els = ReactTestUtils.scryRenderedComponentsWithTypeAndProps(@component, InjectedComponent, matching: {role: \"MessageAttachments\"})\n      attachments = els[0].props.exposedProps.files\n      expect(attachments.length).toEqual(8)\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/message-list-spec.cjsx",
    "content": "_ = require \"underscore\"\nmoment = require \"moment\"\nproxyquire = require(\"proxyquire\").noPreserveCache()\n\nReact = require \"react\"\nReactDOM = require \"react-dom\"\nReactTestUtils = require 'react-addons-test-utils'\n\n{Thread,\n Contact,\n Actions,\n Message,\n Account,\n DraftStore,\n MessageStore,\n AccountStore,\n NylasTestUtils,\n ComponentRegistry} = require \"nylas-exports\"\n\nMessageParticipants = require \"../lib/message-participants\"\nMessageItemContainer = require \"../lib/message-item-container\"\nMessageList = require '../lib/message-list'\n\n# User_1 needs to be \"me\" so that when we calculate who we should reply\n# to, it properly matches the AccountStore\nuser_1 = new Contact\n  name: TEST_ACCOUNT_NAME\n  email: TEST_ACCOUNT_EMAIL\nuser_2 = new Contact\n  name: \"User Two\"\n  email: \"user2@nylas.com\"\nuser_3 = new Contact\n  name: \"User Three\"\n  email: \"user3@nylas.com\"\nuser_4 = new Contact\n  name: \"User Four\"\n  email: \"user4@nylas.com\"\nuser_5 = new Contact\n  name: \"User Five\"\n  email: \"user5@nylas.com\"\n\nm1 = (new Message).fromJSON({\n  \"id\"   : \"111\",\n  \"from\" : [ user_1 ],\n  \"to\"   : [ user_2 ],\n  \"cc\"   : [ user_3, user_4 ],\n  \"bcc\"  : null,\n  \"body\"      : \"Body One\",\n  \"date\"      : 1415814587,\n  \"draft\"     : false\n  \"files\"     : [],\n  \"unread\"    : false,\n  \"object\"    : \"message\",\n  \"snippet\"   : \"snippet one...\",\n  \"subject\"   : \"Subject One\",\n  \"thread_id\" : \"thread_12345\",\n  \"account_id\" : TEST_ACCOUNT_ID\n})\nm2 = (new Message).fromJSON({\n  \"id\"   : \"222\",\n  \"from\" : [ user_2 ],\n  \"to\"   : [ user_1 ],\n  \"cc\"   : [ user_3, user_4 ],\n  \"bcc\"  : null,\n  \"body\"      : \"Body Two\",\n  \"date\"      : 1415814587,\n  \"draft\"     : false\n  \"files\"     : [],\n  \"unread\"    : false,\n  \"object\"    : \"message\",\n  \"snippet\"   : \"snippet Two...\",\n  \"subject\"   : \"Subject Two\",\n  \"thread_id\" : \"thread_12345\",\n  \"account_id\" : TEST_ACCOUNT_ID\n})\nm3 = (new Message).fromJSON({\n  \"id\"   : \"333\",\n  \"from\" : [ user_3 ],\n  \"to\"   : [ user_1 ],\n  \"cc\"   : [ user_2, user_4 ],\n  \"bcc\"  : [],\n  \"body\"      : \"Body Three\",\n  \"date\"      : 1415814587,\n  \"draft\"     : false\n  \"files\"     : [],\n  \"unread\"    : false,\n  \"object\"    : \"message\",\n  \"snippet\"   : \"snippet Three...\",\n  \"subject\"   : \"Subject Three\",\n  \"thread_id\" : \"thread_12345\",\n  \"account_id\" : TEST_ACCOUNT_ID\n})\nm4 = (new Message).fromJSON({\n  \"id\"   : \"444\",\n  \"from\" : [ user_4 ],\n  \"to\"   : [ user_1 ],\n  \"cc\"   : [],\n  \"bcc\"  : [ user_5 ],\n  \"body\"      : \"Body Four\",\n  \"date\"      : 1415814587,\n  \"draft\"     : false\n  \"files\"     : [],\n  \"unread\"    : false,\n  \"object\"    : \"message\",\n  \"snippet\"   : \"snippet Four...\",\n  \"subject\"   : \"Subject Four\",\n  \"thread_id\" : \"thread_12345\",\n  \"account_id\" : TEST_ACCOUNT_ID\n})\nm5 = (new Message).fromJSON({\n  \"id\"   : \"555\",\n  \"from\" : [ user_1 ],\n  \"to\"   : [ user_4 ],\n  \"cc\"   : [],\n  \"bcc\"  : [],\n  \"body\"      : \"Body Five\",\n  \"date\"      : 1415814587,\n  \"draft\"     : false\n  \"files\"     : [],\n  \"unread\"    : false,\n  \"object\"    : \"message\",\n  \"snippet\"   : \"snippet Five...\",\n  \"subject\"   : \"Subject Five\",\n  \"thread_id\" : \"thread_12345\",\n  \"account_id\" : TEST_ACCOUNT_ID\n})\ntestMessages = [m1, m2, m3, m4, m5]\ndraftMessages = [\n  (new Message).fromJSON({\n    \"id\"   : \"666\",\n    \"from\" : [ user_1 ],\n    \"to\"   : [ ],\n    \"cc\"   : [ ],\n    \"bcc\"  : null,\n    \"body\"      : \"Body One\",\n    \"date\"      : 1415814587,\n    \"draft\"     : true\n    \"files\"     : [],\n    \"unread\"    : false,\n    \"object\"    : \"draft\",\n    \"snippet\"   : \"draft snippet one...\",\n    \"subject\"   : \"Draft One\",\n    \"thread_id\" : \"thread_12345\",\n    \"account_id\" : TEST_ACCOUNT_ID\n  }),\n]\n\ntest_thread = (new Thread).fromJSON({\n  \"id\": \"12345\"\n  \"id\" : \"thread_12345\"\n  \"subject\" : \"Subject 12345\",\n  \"account_id\" : TEST_ACCOUNT_ID\n})\n\ndescribe \"MessageList\", ->\n  beforeEach ->\n    MessageStore._items = []\n    MessageStore._threadId = null\n    spyOn(MessageStore, \"itemsLoading\").andCallFake ->\n      false\n\n    @messageList = ReactTestUtils.renderIntoDocument(<MessageList />)\n    @messageList_node = ReactDOM.findDOMNode(@messageList)\n\n  it \"renders into the document\", ->\n    expect(ReactTestUtils.isCompositeComponentWithType(@messageList,\n           MessageList)).toBe true\n\n  it \"by default has zero children\", ->\n    items = ReactTestUtils.scryRenderedComponentsWithType(@messageList,\n            MessageItemContainer)\n\n    expect(items.length).toBe 0\n\n  describe \"Populated Message list\", ->\n    beforeEach ->\n      MessageStore._items = testMessages\n      MessageStore._expandItemsToDefault()\n      MessageStore.trigger(MessageStore)\n      @messageList.setState(currentThread: test_thread)\n      NylasTestUtils.loadKeymap(\"keymaps/base\")\n\n    it \"renders all the correct number of messages\", ->\n      items = ReactTestUtils.scryRenderedComponentsWithType(@messageList,\n              MessageItemContainer)\n      expect(items.length).toBe 5\n\n    it \"renders the correct number of expanded messages\", ->\n      msgs = ReactTestUtils.scryRenderedDOMComponentsWithClass(@messageList, \"collapsed message-item-wrap\")\n      expect(msgs.length).toBe 4\n\n    it \"displays lists of participants on the page\", ->\n      items = ReactTestUtils.scryRenderedComponentsWithType(@messageList,\n              MessageParticipants)\n      expect(items.length).toBe 2\n\n    it \"includes drafts as message item containers\", ->\n      msgs = @messageList.state.messages\n      @messageList.setState\n        messages: msgs.concat(draftMessages)\n      items = ReactTestUtils.scryRenderedComponentsWithType(@messageList,\n              MessageItemContainer)\n      expect(items.length).toBe 6\n\n  describe \"reply type\", ->\n    it \"prompts for a reply when there's only one participant\", ->\n      MessageStore._items = [m3, m5]\n      MessageStore._thread = test_thread\n      MessageStore.trigger()\n      expect(@messageList._replyType()).toBe \"reply\"\n      cs = ReactTestUtils.scryRenderedDOMComponentsWithClass(@messageList, \"footer-reply-area\")\n      expect(cs.length).toBe 1\n\n    it \"prompts for a reply-all when there's more than one participant and the default is reply-all\", ->\n      spyOn(NylasEnv.config, \"get\").andReturn \"reply-all\"\n      MessageStore._items = [m5, m3]\n      MessageStore._thread = test_thread\n      MessageStore.trigger()\n      expect(@messageList._replyType()).toBe \"reply-all\"\n      cs = ReactTestUtils.scryRenderedDOMComponentsWithClass(@messageList, \"footer-reply-area\")\n      expect(cs.length).toBe 1\n\n    it \"prompts for a reply-all when there's more than one participant and the default is reply\", ->\n      spyOn(NylasEnv.config, \"get\").andReturn \"reply\"\n      MessageStore._items = [m5, m3]\n      MessageStore._thread = test_thread\n      MessageStore.trigger()\n      expect(@messageList._replyType()).toBe \"reply\"\n      cs = ReactTestUtils.scryRenderedDOMComponentsWithClass(@messageList, \"footer-reply-area\")\n      expect(cs.length).toBe 1\n\n    it \"hides the reply type if the last message is a draft\", ->\n      MessageStore._items = [m5, m3, draftMessages[0]]\n      MessageStore._thread = test_thread\n      MessageStore.trigger()\n      cs = ReactTestUtils.scryRenderedDOMComponentsWithClass(@messageList, \"footer-reply-area\")\n      expect(cs.length).toBe 0\n\n  describe \"Message minification\", ->\n    beforeEach ->\n      @messageList.MINIFY_THRESHOLD = 3\n      @messageList.setState minified: true\n      @messages = [\n        {id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'}, {id: 'g'}\n      ]\n\n    it \"ignores the first message if it's collapsed\", ->\n      @messageList.setState messagesExpandedState:\n        a: false, b: false, c: false, d: false, e: false, f: false, g: \"default\"\n\n      out = @messageList._messagesWithMinification(@messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]\n        },\n        {id: 'f'},\n        {id: 'g'}\n      ]\n\n    it \"ignores the first message if it's expanded\", ->\n      @messageList.setState messagesExpandedState:\n        a: \"default\", b: false, c: false, d: false, e: false, f: false, g: \"default\"\n\n      out = @messageList._messagesWithMinification(@messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]\n        },\n        {id: 'f'},\n        {id: 'g'}\n      ]\n\n    it \"doesn't minify the last collapsed message\", ->\n      @messageList.setState messagesExpandedState:\n        a: false, b: false, c: false, d: false, e: false, f: \"default\", g: \"default\"\n\n      out = @messageList._messagesWithMinification(@messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]\n        },\n        {id: 'e'},\n        {id: 'f'},\n        {id: 'g'}\n      ]\n\n    it \"allows explicitly expanded messages\", ->\n      @messageList.setState messagesExpandedState:\n        a: false, b: false, c: false, d: false, e: false, f: \"explicit\", g: \"default\"\n\n      out = @messageList._messagesWithMinification(@messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}]\n        },\n        {id: 'f'},\n        {id: 'g'}\n      ]\n\n    it \"doesn't minify if the threshold isn't reached\", ->\n      @messageList.setState messagesExpandedState:\n        a: false, b: \"default\", c: false, d: \"default\", e: false, f: \"default\", g: \"default\"\n\n      out = @messageList._messagesWithMinification(@messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {id: 'b'},\n        {id: 'c'},\n        {id: 'd'},\n        {id: 'e'},\n        {id: 'f'},\n        {id: 'g'}\n      ]\n\n    it \"doesn't minify if the threshold isn't reached due to the rule about not minifying the last collapsed messages\", ->\n      @messageList.setState messagesExpandedState:\n        a: false, b: false, c: false, d: false, e: \"default\", f: \"default\", g: \"default\"\n\n      out = @messageList._messagesWithMinification(@messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {id: 'b'},\n        {id: 'c'},\n        {id: 'd'},\n        {id: 'e'},\n        {id: 'f'},\n        {id: 'g'}\n      ]\n\n    it \"minifies at the threshold if the message is explicitly expanded\", ->\n      @messageList.setState messagesExpandedState:\n        a: false, b: false, c: false, d: false, e: \"explicit\", f: \"default\", g: \"default\"\n\n      out = @messageList._messagesWithMinification(@messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]\n        },\n        {id: 'e'},\n        {id: 'f'},\n        {id: 'g'}\n      ]\n\n    it \"can have multiple minification blocks\", ->\n      messages = [\n        {id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'},\n        {id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}, {id: 'k'}, {id: 'l'}\n      ]\n\n      @messageList.setState messagesExpandedState:\n        a: false, b: false, c: false, d: false, e: false, f: \"default\",\n        g: false, h: false, i: false, j: false, k: false, l: \"default\"\n\n      out = @messageList._messagesWithMinification(messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]\n        },\n        {id: 'e'},\n        {id: 'f'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}]\n        },\n        {id: 'k'},\n        {id: 'l'}\n      ]\n\n    it \"can have multiple minification blocks next to explicitly expanded messages\", ->\n      messages = [\n        {id: 'a'}, {id: 'b'}, {id: 'c'}, {id: 'd'}, {id: 'e'}, {id: 'f'},\n        {id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}, {id: 'k'}, {id: 'l'}\n      ]\n\n      @messageList.setState messagesExpandedState:\n        a: false, b: false, c: false, d: false, e: \"explicit\", f: \"default\",\n        g: false, h: false, i: false, j: false, k: \"explicit\", l: \"default\"\n\n      out = @messageList._messagesWithMinification(messages)\n      expect(out).toEqual [\n        {id: 'a'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'b'}, {id: 'c'}, {id: 'd'}]\n        },\n        {id: 'e'},\n        {id: 'f'},\n        {\n          type: \"minifiedBundle\"\n          messages: [{id: 'g'}, {id: 'h'}, {id: 'i'}, {id: 'j'}]\n        },\n        {id: 'k'},\n        {id: 'l'}\n      ]\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/message-participants-spec.cjsx",
    "content": "_ = require 'underscore'\nReact = require \"react\"\nReactDOM = require \"react-dom\"\nReactTestUtils = require 'react-addons-test-utils'\n{Contact, Message, DOMUtils} = require \"nylas-exports\"\nMessageParticipants = require \"../lib/message-participants\"\n\nuser_1 =\n  name: \"User One\"\n  email: \"user1@nylas.com\"\nuser_2 =\n  name: \"User Two\"\n  email: \"user2@nylas.com\"\nuser_3 =\n  name: \"User Three\"\n  email: \"user3@nylas.com\"\nuser_4 =\n  name: \"User Four\"\n  email: \"user4@nylas.com\"\nuser_5 =\n  name: \"User Five\"\n  email: \"user5@nylas.com\"\n\nmany_users = (new Contact({name: \"User #{i}\", email:\"#{i}@app.com\"}) for i in [0..100])\n\ntest_message = (new Message).fromJSON({\n  \"id\"   : \"111\",\n  \"from\" : [ user_1 ],\n  \"to\"   : [ user_2 ],\n  \"cc\"   : [ user_3, user_4 ],\n  \"bcc\"  : [ user_5 ]\n})\n\nbig_test_message = (new Message).fromJSON({\n  \"id\"   : \"222\",\n  \"from\" : [ user_1 ],\n  \"to\"   : many_users\n})\n\nmany_thread_users = [user_1].concat(many_users)\n\ndescribe \"MessageParticipants\", ->\n  describe \"when collapsed\", ->\n    makeParticipants = (props) ->\n      ReactTestUtils.renderIntoDocument(\n        <MessageParticipants {...props} />\n      )\n\n    it \"renders into the document\", ->\n      participants = makeParticipants(to: test_message.to, cc: test_message.cc,\n                                      from: test_message.from, message_participants: test_message.participants())\n      expect(participants).toBeDefined()\n\n    it \"uses short names\", ->\n      actualOut = makeParticipants(to: test_message.to)\n      to = ReactTestUtils.findRenderedDOMComponentWithClass(actualOut, \"to-contact\")\n      expect(ReactDOM.findDOMNode(to).innerHTML).toBe \"User\"\n\n    it \"doesn't render any To nodes if To array is empty\", ->\n      actualOut = makeParticipants(to: [])\n      findToField = ->\n        ReactTestUtils.findRenderedDOMComponentWithClass(actualOut, \"to-contact\")\n      expect(findToField).toThrow()\n\n    it \"doesn't render any Cc nodes if Cc array is empty\", ->\n      actualOut = makeParticipants(cc: [])\n      findCcField = ->\n        ReactTestUtils.findRenderedDOMComponentWithClass(actualOut, \"cc-contact\")\n      expect(findCcField).toThrow()\n\n    it \"doesn't render any Bcc nodes if Bcc array is empty\", ->\n      actualOut = makeParticipants(bcc: [])\n      findBccField = ->\n        ReactTestUtils.findRenderedDOMComponentWithClass(actualOut, \"bcc-contact\")\n      expect(findBccField).toThrow()\n\n  describe \"when expanded\", ->\n    beforeEach ->\n      @participants = ReactTestUtils.renderIntoDocument(\n        <MessageParticipants to={test_message.to}\n                             cc={test_message.cc}\n                             from={test_message.from}\n                             isDetailed={true}\n                             message_participants={test_message.participants()} />\n      )\n\n    it \"renders into the document\", ->\n      participants = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, \"expanded-participants\")\n      expect(participants).toBeDefined()\n\n    it \"uses full names\", ->\n      to = ReactTestUtils.findRenderedDOMComponentWithClass(@participants, \"to-contact\")\n      expect(ReactDOM.findDOMNode(to).innerText.trim()).toEqual \"User Two <user2@nylas.com>\"\n\n\n  # TODO: We no longer display \"to everyone\"\n  #\n  # it \"determines the message is to everyone\", ->\n  #   p1 = TestUtils.renderIntoDocument(\n  #     <MessageParticipants to={big_test_message.to}\n  #                          cc={big_test_message.cc}\n  #                          from={big_test_message.from}\n  #                          message_participants={big_test_message.participants()} />\n  #   )\n  #   expect(p1._isToEveryone()).toBe true\n  #\n  # it \"knows when the message isn't to everyone due to participant mismatch\", ->\n  #   p2 = TestUtils.renderIntoDocument(\n  #     <MessageParticipants to={test_message.to}\n  #                          cc={test_message.cc}\n  #                          from={test_message.from}\n  #                          message_participants={test_message.participants()} />\n  #   )\n  #   # this should be false because we don't count bccs\n  #   expect(p2._isToEveryone()).toBe false\n  #\n  # it \"knows when the message isn't to everyone due to participant size\", ->\n  #   p2 = TestUtils.renderIntoDocument(\n  #     <MessageParticipants to={test_message.to}\n  #                          cc={test_message.cc}\n  #                          from={test_message.from}\n  #                          message_participants={test_message.participants()} />\n  #   )\n  #   # this should be false because we don't count bccs\n  #   expect(p2._isToEveryone()).toBe false\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/spec/message-timestamp-spec.cjsx",
    "content": "moment = require 'moment'\nReact = require \"react\"\nReactDOM = require \"react-dom\"\nReactTestUtils = require 'react-addons-test-utils'\nMessageTimestamp = require('../lib/message-timestamp').default\n\nmsgTime = ->\n  moment([2010, 1, 14, 15, 25, 50, 125]) # Feb 14, 2010 at 3:25 PM\n\ndescribe \"MessageTimestamp\", ->\n  beforeEach ->\n    @item = ReactTestUtils.renderIntoDocument(\n      <MessageTimestamp date={msgTime()} />\n    )\n\n  it \"still processes one day, even if it crosses a month divider\", ->\n    # this should be tested in moment.js, but we add a test here for our own sanity too\n    feb28 = moment([2015, 1, 28])\n    mar01 = moment([2015, 2, 1])\n    expect(mar01.diff(feb28, 'days')).toBe 1\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/stylesheets/find-in-thread.less",
    "content": "@import 'ui-variables';\n\nbody.platform-win32 {\n  .find-in-thread {\n  }\n}\n\n.find-in-thread {\n  background: @background-secondary;\n  text-align: right;\n  overflow: hidden;\n\n  height: 0;\n  padding: 0 8px;\n  transition: all 125ms ease-in-out;\n  border-bottom: 0;\n  &.enabled {\n    padding: 4px 8px;\n    height: 35px;\n    border-bottom: 1px solid @border-color-secondary;\n  }\n\n  .controls-wrap {\n    display: inline-block;\n  }\n\n  .selection-progress {\n    color: @text-color-very-subtle;\n    position: absolute;\n    top: 4px;\n    right: 54px;\n    font-size: 12px;\n  }\n\n  .btn.btn-find-in-thread {\n    border: 0;\n    box-shadow: 0 0 0;\n    border-radius: 0;\n    background: transparent;\n    display: inline-block;\n  }\n  .input-wrap {\n    display: inline-block;\n    position: relative;\n    input {\n      height: 26px;\n      width: 230px;\n      padding-left: 8px;\n      font-size: 12px;\n    }\n    .btn-wrap {\n      width: 54px;\n      position: absolute;\n      top: 0;\n      right: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-list/stylesheets/message-list.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n@message-max-width: 800px;\n@message-spacing: 6px;\n\n.tag-picker {\n    .menu {\n        .content-container {\n            height:250px;\n            overflow-y:scroll;\n        }\n    }\n}\n\nbody.platform-win32 {\n  .sheet-toolbar {\n    .message-toolbar-arrow.down {\n      margin: 0 0 0 1px;\n      padding: 0 5px;\n      .windows-btn-bg;\n      &:hover {\n        background: #e5e5e5;\n      }\n      &.btn-icon:hover {\n        color: @text-color;\n        img.content-mask { background: rgba(35, 31, 32, 0.8); }\n      }\n    }\n    .message-toolbar-arrow.up {\n      margin: 0 0 0 1px;\n      padding: 0 5px;\n      .windows-btn-bg;\n      &.btn-icon:hover {\n        color: @text-color;\n        img.content-mask { background: rgba(35, 31, 32, 0.8); }\n      }\n    }\n    .message-toolbar-arrow.disabled {\n      &:hover {\n        background: transparent;\n      }\n    }\n  }\n\n  #message-list {\n    .message-item-wrap {\n      .message-item-white-wrap {\n        border-radius: 0;\n      }\n    }\n    .minified-bundle {\n      .num-messages {\n        border-radius: 0;\n      }\n      .msg-line {\n        border-radius: 0;\n      }\n    }\n    .footer-reply-area-wrap {\n      border-radius: 0;\n    }\n  }\n\n  .sidebar-section {\n    border-radius: 0;\n  }\n}\n\n.sheet-toolbar {\n  // This class wraps the items that appear above the message list in the\n  // toolbar. We want the toolbar items to sit right above the centered\n  // content, so we need another 800px-wide container in the toolbar...\n  .message-toolbar-items {\n    order: 200;\n    flex-grow: 0;\n    flex-shrink: 0;\n  }\n\n  .message-toolbar-arrow.down {\n    order:201;\n    margin-right: 0;\n    margin-left: @spacing-standard * 1.5;\n  }\n  .message-toolbar-arrow.up {\n    order:202;\n    // <1 because of hit region padding on the button\n    margin-right: @spacing-standard * 0.75;\n  }\n  .message-toolbar-arrow.disabled {\n    opacity: 0.3;\n  }\n}\n\n.mode-split {\n  .message-nav-title {\n    display: none;\n  }\n}\n\n.hide-sidebar-button {\n  font-size: @font-size-small;\n  color: @text-color-subtle;\n  margin-left: @spacing-standard;\n  cursor:default;\n  -webkit-user-select: none;\n  .img-wrap {\n    margin-right: @spacing-half;\n    position: relative;\n    top: -1px;\n  }\n  img { background: @text-color-subtle; }\n}\n\n#message-list.height-fix {\n  height: calc(~\"100% - 35px\");\n  min-height: calc(~\"100% - 35px\");\n}\n\n#message-list {\n  display: flex;\n  flex-direction: column;\n  position: relative;\n  background: @background-secondary;\n\n  transition: all 125ms ease-in-out;\n  width: 100%;\n  height: 100%;\n  min-height: 100%;\n  padding: 0;\n  order: 2;\n\n  search-match, .search-match {\n    background: @text-color-search-match;\n    border-radius: @border-radius-base;\n    box-shadow: 0 0.5px 0.5px rgba(0,0,0,0.25);\n    &.current-match {\n      background: @text-color-search-current-match;\n    }\n  }\n\n  .show-hidden-messages {\n    background-color: darken(@background-secondary, 4%);\n    border: 1px solid darken(@background-secondary, 8%);\n    border-radius: @border-radius-base;\n    color: @text-color-very-subtle;\n    margin-bottom: @padding-large-vertical;\n    cursor: default;\n    padding: @padding-base-vertical @padding-base-horizontal;\n    a { float: right; }\n  }\n\n  .message-body-error {\n    background-color: @background-secondary;\n    border: 1px solid darken(@background-secondary, 8%);\n    color: @text-color-very-subtle;\n    margin-top: @padding-large-vertical;\n    cursor: default;\n    padding: @padding-base-vertical @padding-base-horizontal;\n    a { float: right; }\n  }\n\n  .message-body-loading {\n    height: 1em;\n    align-content: center;\n    margin-top: @padding-large-vertical;\n    margin-bottom: @padding-large-vertical;\n  }\n\n  .message-subject-wrap {\n    max-width: @message-max-width;\n    margin: 5px auto 10px auto;\n    -webkit-user-select: text;\n    line-height: @font-size-large * 1.8;\n    display: flex;\n    align-items: center;\n    padding: 0 @padding-base-horizontal;\n  }\n  .mail-important-icon {\n    margin-right:@spacing-half;\n    margin-bottom:1px;\n    flex-shrink: 0;\n  }\n  .message-subject {\n    font-size: @font-size-large;\n    color: @text-color;\n    margin-right: @spacing-standard;\n  }\n  .message-icons-wrap {\n     flex-shrink: 0;\n     cursor: pointer;\n     -webkit-user-select: none;\n     margin-left: auto;\n     display: flex;\n     align-items: center;\n\n     img {\n       background: @text-color-subtle;\n     }\n     div + div {\n       margin-left: @padding-small-horizontal;\n     }\n  }\n  .thread-injected-mail-labels {\n    vertical-align: top;\n  }\n  .message-list-headers {\n    margin: 0 auto;\n    width: 100%;\n    max-width: @message-max-width;\n    display:block;\n\n    .participants {\n      .contact-chip {\n        display:inline-block;\n      }\n    }\n  }\n\n  .messages-wrap {\n    flex: 1;\n    opacity:0;\n    transition: opacity 0s;\n\n    &.ready {\n      opacity:1;\n      transition: opacity .1s linear;\n    }\n\n    .scroll-region-content-inner {\n      padding: 6px;\n    }\n  }\n\n  .minified-bundle + .message-item-wrap {\n    margin-top: -5px;\n  }\n\n  .message-item-wrap {\n    transition: height 0.1s;\n    position: relative;\n    max-width: @message-max-width;\n    margin: 0 auto;\n\n    .message-item-white-wrap {\n      background: @background-primary;\n      border: 0;\n      box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.28), 0 1px 1.5px rgba(0, 0, 0, 0.08);\n      border-radius: 4px;\n    }\n\n    padding-bottom: @message-spacing * 2;\n    &.before-reply-area { padding-bottom: 0; }\n\n    &.collapsed {\n      .message-item-white-wrap {\n        background-color: darken(@background-primary, 2%);\n        padding-top: 19px;\n        padding-bottom: 8px;\n        margin-bottom: 0;\n      }\n\n      &+.minified-bundle {\n        margin-top: -@message-spacing\n      }\n    }\n\n    &.collapsed .message-item-area {\n      padding-bottom: 10px;\n      display: flex;\n      flex-direction: row;\n      font-size: @font-size-small;\n\n      .collapsed-snippet {\n        flex: 1;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        overflow: hidden;\n        cursor: default;\n        color: @text-color-very-subtle;\n      }\n\n      .collapsed-attachment {\n        width:15px;\n        height:15px;\n        background-size: 15px;\n        background-repeat: no-repeat;\n        background-position:center;\n        padding:12px;\n        margin-left: 0.5em;\n        background-image:url(../static/images/message-list/icon-attachment-@2x.png);\n        position: relative;\n        top: -2px;\n      }\n\n      .collapsed-from {\n        font-weight: @font-weight-semi-bold;\n        color: @text-color-very-subtle;\n        // min-width: 60px;\n        margin-right: 1em;\n      }\n\n      .collapsed-timestamp {\n        margin-left: 0.5em;\n        color: @text-color-very-subtle;\n      }\n    }\n\n  }\n\n\n  .message-item-divider {\n    border:0; // remove default hr border left, right\n    border-top: 2px solid @border-color-secondary;\n    height: 3px;\n    background: @background-secondary;\n    border-bottom: 1px solid @border-color-primary;\n    margin: 0;\n\n    &.collapsed {\n      height: 0;\n      border-bottom: 0;\n    }\n  }\n\n  .minified-bundle {\n    position: relative;\n    .num-messages {\n      position: absolute;\n      top: 50%;\n      left: 50%;\n      margin-left: -80px;\n      margin-top: -15px;\n      border-radius: 15px;\n      border: 1px solid @border-color-divider;\n      width: 160px;\n      background: @background-primary;\n      text-align: center;\n      color: @text-color-very-subtle;\n      z-index: 2;\n      background: @background-primary;\n      &:hover {\n        cursor: default;\n      }\n    }\n    .msg-lines {\n      max-width: @message-max-width;\n      margin: 0 auto;\n      width: 100%;\n      margin-top: -13px;\n    }\n    .msg-line {\n      border-radius: 4px 4px 0 0;\n      position: relative;\n      border-top: 1px solid @border-color-divider;\n      background-color: darken(@background-primary, 2%);\n      box-shadow: 0 0.5px 0 rgba(0,0,0,0.1), 0 -0.5px 0 rgba(0,0,0,0.1), 0.5px 0 0 rgba(0,0,0,0.1), -0.5px 0 0 rgba(0,0,0,0.1);\n    }\n  }\n\n  .message-header {\n    position: relative;\n    font-size: @font-size-small;\n    padding-bottom: 0;\n    padding-top: 19px;\n\n    &.pending {\n      .message-actions-wrap {\n        width: 0;\n        opacity: 0;\n        position: absolute;\n      }\n      .pending-spinner {\n        opacity: 1;\n      }\n    }\n\n    .pending-spinner {\n      transition: opacity 100ms;\n      transition-delay: 50ms, 0ms;\n      transition-timing-function: ease-in;\n      opacity: 0;\n    }\n\n    .header-row {\n      margin-top: 0.5em;\n      color: @text-color-very-subtle;\n\n      .header-label {\n        float: left;\n        display: block;\n        font-weight: @font-weight-normal;\n        margin-left: 0;\n      }\n\n      .header-name {\n      }\n    }\n\n    .message-actions-wrap {\n      transition: opacity 100ms, width 150ms;\n      transition-delay: 50ms, 0ms;\n      transition-timing-function: ease-in-out;\n      opacity: 1;\n      text-align: left;\n    }\n\n    .message-actions-ellipsis {\n      display: block;\n      float: left;\n    }\n\n    .message-actions {\n      display: inline-block;\n      height: 23px;\n      border: 1px solid lighten(@border-color-divider, 6%);\n      border-radius: 11px;\n\n      z-index: 4;\n      margin-top: 0.35em;\n      margin-left: 0.5em;\n      text-align: center;\n\n      .btn-icon {\n        opacity: 0.75;\n        padding: 0 @spacing-half;\n        height: 20px;\n        line-height: 10px;\n        border-radius: 0;\n        border-right: 1px solid lighten(@border-color-divider, 6%);\n        &:last-child { border-right: 0; }\n        margin: 0;\n        &:active {background: transparent;}\n      }\n    }\n\n    .message-time {\n      padding-top: 4px;\n      z-index: 2; position: relative;\n      display: inline-block;\n      min-width: 125px;\n      cursor: default;\n    }\n    .msg-actions-tooltip {\n      display: inline-block;\n      margin-left: 1em;\n    }\n\n    .message-time, .message-indicator {\n      color: @text-color-very-subtle;\n    }\n\n    .message-header-right {\n      z-index: 4;\n      position: relative;\n      top: -5px;\n      float: right;\n      text-align: right;\n      display: flex;\n      height: 2em;\n    }\n\n  }\n\n  .message-item-area {\n    width: 100%;\n    max-width: @message-max-width;\n    margin: 0 auto;\n    padding: 0 20px @spacing-standard 20px;\n\n    .iframe-container {\n      margin-top: 10px;\n      width: 100%;\n\n      iframe {\n        width: 100%;\n        border: 0;\n        padding: 0;\n        overflow: auto;\n      }\n    }\n  }\n\n  .collapse-region {\n    width: calc(~\"100% - 30px\");\n    height: 56px;\n    position: absolute;\n    top: 0;\n  }\n\n  .header-toggle-control {\n    &.inactive { display: none; }\n    z-index: 3;\n    position: absolute;\n    top: 0;\n    left: -1 * 13px;\n    img { background: @text-color-very-subtle; }\n  }\n  .message-item-wrap:hover {\n    .header-toggle-control.inactive { display: block; }\n  }\n\n  .footer-reply-area-wrap {\n    overflow: hidden;\n\n    max-width: @message-max-width;\n    margin: -3px auto 0 auto;\n\n    position: relative;\n    z-index: 2;\n\n    border: 0;\n    box-shadow: 0 0 0.5px rgba(0, 0, 0, 0.28), 0 1px 1.5px rgba(0, 0, 0, 0.08);\n    border-top: 1px dashed @border-color-divider;\n    border-radius: 0 0 4px 4px;\n    background: @background-primary;\n\n    color: @text-color-very-subtle;\n    img.content-mask { background-color:@text-color-very-subtle; }\n\n    &:hover {\n      cursor: default;\n    }\n\n    .footer-reply-area {\n      width: 100%;\n      max-width: @message-max-width;\n      margin: 0 auto;\n      padding: 12px @spacing-standard * 1.5;\n    }\n    .reply-text {\n      display: inline-block;\n      vertical-align: middle;\n      margin-left: 0.5em;\n    }\n  }\n\n}\n\n.download-all {\n  @download-btn-color: fadeout(#929292, 20%);\n  @download-hover-color: fadeout(@component-active-color, 20%);\n\n  display: flex;\n  align-items: center;\n  color: @download-btn-color;\n  font-size: 0.9em;\n  cursor: default;\n  margin-top: @spacing-three-quarters;\n\n  .separator {\n    margin: 0 5px;\n  }\n\n  .attachment-number {\n    display: flex;\n    align-items: center;\n  }\n\n  img {\n    vertical-align: middle;\n    margin-right: @spacing-half;\n    background-color: @download-btn-color;\n  }\n\n  .download-all-action:hover {\n    color: @download-hover-color;\n    img {\n      background-color: @download-hover-color;\n    }\n  }\n}\n\n.attachments-area {\n  padding-top: @spacing-half + 2;\n\n  // attachments are padded on both sides so that things like the remove \"X\" can\n  // overhang them. To make the attachments line up with the body, we need to outdent\n  margin-left: -@spacing-standard;\n  margin-right: -@spacing-standard;\n\n  cursor:default;\n}\n\n\n///////////////////////////////\n// message-participants.cjsx //\n///////////////////////////////\n.pending {\n  .message-participants {\n    padding-left: 34px;\n  }\n}\n.message-participants {\n  z-index: 1;\n  display: flex;\n  transition: padding-left 150ms;\n  transition-timing-function: ease-in-out;\n\n  &.collapsed:hover {cursor: default;}\n\n  .from-contact {\n    font-weight: @headings-font-weight;\n    color: @text-color;\n  }\n  .from-label, .to-label, .cc-label, .bcc-label {\n    color: @text-color-very-subtle;\n  }\n  .to-contact, .cc-contact, .bcc-contact, .to-everyone {\n    color: @text-color-very-subtle;\n  }\n\n  &.to-participants {\n    width: 100%;\n\n    .collapsed-participants {\n      width: 100%;\n      margin-top: -6px;\n    }\n  }\n\n  .collapsed-participants {\n    display: flex;\n    align-items: center;\n\n    .to-contact {\n      display: inline-block;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n      overflow: hidden;\n    }\n  }\n\n  .expanded-participants {\n    padding-right: 1.2em;\n    width: 100%;\n\n    .participant {\n      display: inline-block;\n      margin-right: 0.25em;\n    }\n\n    .participant-type {\n      margin-top: 0.5em;\n      &:first-child {margin-top: 0;}\n    }\n\n    .from-label, .to-label, .cc-label, .bcc-label {\n      float: left;\n      display: block;\n      text-transform: capitalize;\n      font-weight: @font-weight-normal;\n      margin-left: 0;\n    }\n\n    .from-contact, .subject {\n      font-weight: @font-weight-semi-bold;\n    }\n\n    // .from-label { margin-right: 1em; }\n    .to-label, .cc-label { margin-right: 0.5em; }\n    .bcc-label { margin-right: 0; }\n\n    .participant-primary {\n      color: @text-color-very-subtle;\n      margin-right: 0.15em;\n      display:inline-block;\n    }\n    .participant-secondary {\n      color: @text-color-very-subtle;\n      display:inline-block;\n    }\n\n    .from-contact {\n      .participant-primary {\n        color: @text-color;\n      }\n      .participant-secondary {\n        color: @text-color;\n      }\n    }\n  }\n}\n\n///////////////////////////////\n// sidebar-contact-card.cjsx //\n///////////////////////////////\n.sidebar-section {\n  opacity: 0;\n  margin: 5px;\n  cursor: default;\n  border: 1px solid @border-color-primary;\n  border-radius: @border-radius-large;\n  background: @background-primary;\n  padding: 15px;\n\n  &.visible {\n    transition: opacity 0.1s ease-out;\n    opacity: 1;\n  }\n\n  h2 {\n    font-size: 11px;\n    font-weight: @font-weight-semi-bold;\n    text-transform: uppercase;\n    color: @text-color-very-subtle;\n    margin: 0 0 18px 0;\n    position: relative;\n\n    &:after {\n      content: \" \";\n      background-image: url(images/sidebar/sidebar-section-divider@2x.png);\n      background-size: 100%;\n      background-repeat: repeat-x;\n      background-color: transparent;\n      position: absolute;\n      left: -15px;\n      bottom: -10px;\n      width: calc(~\"100% + 30px\");\n      height: 3px;\n    }\n    &:first-child {\n      margin-top: 0;\n    }\n  }\n\n  .sidebar-contact-card {\n  }\n}\n.sidebar-participant-picker {\n  padding: 10px 5px 20px 5px;\n  text-align: right;\n  select {\n    max-width: 100%;\n    width: 100%;\n  }\n}\n\n.column-MessageListSidebar {\n  background-color: @background-secondary;\n  overflow: auto;\n  border-left: 1px solid @border-color-divider;\n  color: @text-color-subtle;\n  .flexbox-handle-horizontal div {\n    border-right: 0;\n    width: 1px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-view-on-github/README.md",
    "content": "# View on GitHub\n\nThe \"View on GitHub\" plugin adds a button to the toolbar above the message view.\nWhen you view a message from GitHub that contains a \"View on GitHub\" link,\nthe button appears and makes it easy to jump to the issue / pull request / comment\non GitHub.\n\nThis example is a good starting point for plugins that want to create custom\nactions.\n\n#### Install this plugin\n\n1. Download and run N1\n\n2. From the menu, select `Developer > Install a Plugin Manually...`\n   The dialog will default to this examples directory. Just choose the\n   package to install it!\n\n   > When you install packages, they're moved to `~/.nylas-mail/packages`,\n   > and N1 runs `apm install` on the command line to fetch dependencies\n   > listed in the package's `package.json`\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-view-on-github/keymaps/github.json",
    "content": "{\n  \"github:open\": \"mod-G\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-view-on-github/lib/github-store.es6",
    "content": "import _ from 'underscore';\nimport NylasStore from 'nylas-store';\nimport {MessageStore} from 'nylas-exports';\n\nclass GithubStore extends NylasStore {\n  // It's very common practive for {NylasStore}s to listen to other parts of N1.\n  // Since Stores are singletons and constructed once on `require`, there is no\n  // teardown step to turn off listeners.\n  constructor() {\n    super();\n    this.listenTo(MessageStore, this._onMessageStoreChanged);\n  }\n\n  // This is the only public method on `GithubStore` and it's read only.\n  // All {NylasStore}s ONLY have reader methods. No setter methods. Use an\n  // `Action` instead!\n  //\n  // This is the computed & cached value that our `ViewOnGithubButton` will\n  // render.\n  link() {\n    return this._link;\n  }\n\n  // Private methods\n\n  _onMessageStoreChanged() {\n    if (!MessageStore.threadId()) {\n      return;\n    }\n\n    const itemIds = _.pluck(MessageStore.items(), \"id\");\n    if ((itemIds.length === 0) || _.isEqual(itemIds, this._lastItemIds)) {\n      return;\n    }\n\n    this._lastItemIds = itemIds;\n    this._link = this._isRelevantThread() ? this._findGitHubLink() : null;\n    this.trigger();\n  }\n\n  _findGitHubLink() {\n    let msg = MessageStore.items()[0];\n    if (!msg.body) {\n      // The msg body may be null if it's collapsed. In that case, use the\n      // last message. This may be less relaiable since the last message\n      // might be a side-thread that doesn't contain the link in the quoted\n      // text.\n      msg = _.last(MessageStore.items());\n    }\n\n    // Use a regex to parse the message body for GitHub URLs - this is a quick\n    // and dirty method to determine the GitHub object the email is about:\n    // https://regex101.com/r/aW8bI4/2\n    const re = /<a.*?href=['\"](.*?)['\"].*?view.*?it.*?on.*?github.*?\\/a>/gmi;\n    const firstMatch = re.exec(msg.body);\n    if (firstMatch) {\n      // [0] is the full match and [1] is the matching group\n      return firstMatch[1];\n    }\n\n    return null;\n  }\n\n  _isRelevantThread() {\n    const participants = MessageStore.thread().participants || [];\n    const githubDomainRegex = /@github\\.com/gi;\n    return _.any(participants, contact => githubDomainRegex.test(contact.email));\n  }\n}\n\n/*\nIMPORTANT NOTE:\n\nAll {NylasStore}s are constructed upon their first `require` by another\nmodule.  Since `require` is cached, they are only constructed once and\nare therefore singletons.\n*/\nexport default new GithubStore();\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-view-on-github/lib/main.jsx",
    "content": "/*\nThis package displays a \"Vew on Github Button\" whenever the message you're\nlooking at contains a \"view it on Github\" link.\n\nThis is the entry point of an N1 package. All packages must have a file\ncalled `main` in their `/lib` folder.\n\nThe `activate` method of the package gets called when it is activated.\nThis happens during N1's bootup. It can also happen when a user manually\nenables your package.\n\nNearly all N1 packages have similar `activate` methods. The most common\naction is to register a {React} component with the {ComponentRegistry}\n\nSee more details about how this works in the {ComponentRegistry}\ndocumentation.\n\nIn this case the `ViewOnGithubButton` React Component will get rendered\nwhenever the `\"MessageList:ThreadActionsToolbarButton\"` region gets rendered.\n\nSince the `ViewOnGithubButton` doesn't know who owns the\n`\"MessageList:ThreadActionsToolbarButton\"` region, or even when or where it will be rendered, it\nhas to load its internal `state` from the `GithubStore`.\n\nThe `GithubStore` is responsible for figuring out what message you're\nlooking at, if it has a relevant Github link, and what that link is. Once\nit figures that out, it makes that data available for the\n`ViewOnGithubButton` to display.\n*/\n\nimport {ComponentRegistry} from 'nylas-exports';\nimport ViewOnGithubButton from \"./view-on-github-button\";\n\n/*\nAll packages must export a basic object that has at least the following 3\nmethods:\n\n1. `activate` - Actions to take once the package gets turned on.\nPre-enabled packages get activated on N1 bootup. They can also be\nactivated manually by a user.\n\n2. `deactivate` - Actions to take when a package gets turned off. This can\nhappen when a user manually disables a package.\n\n3. `serialize` - A simple serializable object that gets saved to disk\nbefore N1 quits. This gets passed back into `activate` next time N1 boots\nup or your package is manually activated.\n*/\nexport function activate() {\n  ComponentRegistry.register(ViewOnGithubButton, {\n    role: 'ThreadActionsToolbarButton',\n  });\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ViewOnGithubButton);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-view-on-github/lib/view-on-github-button.jsx",
    "content": "import {shell} from 'electron'\nimport {Actions, React} from 'nylas-exports'\nimport {RetinaImg, KeyCommandsRegion} from 'nylas-component-kit'\n\nimport GithubStore from './github-store'\n\n/**\nThe `ViewOnGithubButton` displays a button whenever there's a relevant\nGithub asset to link to.\n\nWhen creating this React component the first consideration was when &\nwhere we'd be rendered. The next consideration was what data we need to\ndisplay.\n\nUnlike a traditional React application, N1 components have very few\nguarantees on who will render them and where they will be rendered. In our\n`lib/main.cjsx` file we registered this component with our\n{ComponentRegistry} for the `\"ThreadActionsToolbarButton\"` role. That means that\nwhenever the \"ThreadActionsToolbarButton\" region gets rendered, we'll render\neverything registered with that area. Other buttons, such as \"Archive\" and\nthe \"Change Label\" button are reigstered with that role, so we should\nexpect ourselves to showup alongside them.\n\nThe only data we need is a single relevant to Github. If we have one,\nwe'll open it up in a browser. If we don't have one, we'll hide the\ncomponent.\n\nGetting that url takes a bit of message parsing. We need to retrieve a\nmessage body then implement some kind of regex to find and parse out that\nlink.\n\nWe could have put all of that logic in this React Component, but that's\nnot what React components should be doing. In N1 a component's only job is\nto display known data and be the first responders to user interaction.\n\nWe instead create a {GithubStore} to handle the fetching and preparation\nof the data. See that file's documentation for more on how that works.\n\nAs far as this component is concerned, there will be an entity called\n`GitHubStore` that will expose the correct `link`. That store will then\nnotify us when the `link` changes so we can update our state.\n\nOnce we know our `link` our `render` method can simply be a description of\nhow we want to display that link. In this case we're going to make a\nsimple button with a GitHub logo in it.\n\nWe'll also display nothing if there is no link.\n*/\nexport default class ViewOnGithubButton extends React.Component {\n  static displayName = \"ViewOnGithubButton\"\n\n  static containerRequired = false\n\n  static propTypes = {\n    items: React.PropTypes.array,\n  }\n\n  /** ** React methods ****\n  * The following methods are React methods that we override. See {React}\n  * documentation for more info\n  */\n\n  constructor(props) {\n    super(props)\n    this.state = this._getStateFromStores()\n  }\n\n  /*\n   * When components mount, it's very common to have them listen to a\n   * `Store`. Since most of our React Components in N1 are registered into\n   * {ComponentRegistry} regions instead of manually rendered top-down much\n   * of our data is side-loaded from stores instead of passed in as props.\n  */\n  componentDidMount() {\n    /*\n    * The `listen` method of {NylasStore}s (which {GithubStore}\n    * subclasses) returns an \"unlistener\" function. When the unlistener is\n    * invoked (as it is in `componentWillUnmount`) the listener references\n    * are cleaned up. Every time the `GithubStore` calls its `trigger`\n    * method, the `_onStoreChanged` callback will be fired.\n    */\n    this._unlisten = GithubStore.listen(this._onStoreChanged)\n  }\n\n  componentWillUnmount() {\n    this._unlisten()\n  }\n\n  _keymapHandlers() {\n    return {\n      'github:open': this._openLink,\n    }\n  }\n\n  /** ** Super common N1 Component private methods ****\n  /*\n  * An extremely common pattern for all N1 components are the methods\n  * `onStoreChanged` and `getStateFromStores`.\n  *\n  * Most N1 components listen to some source of data, which is usally a\n  * Store. When the store notifies that something has changed, we need to\n  * fetch the fresh data and updated our state.\n  *\n  * Note that when a Store updates it does not let us know what changed.\n  * This is intentional! This forces us to fresh the full latest state\n  * from the stores in a more declarative, easy-to-follow way. There are a\n  * couple rare exceptions that are only used for performance\n  * optimizations.\n\n  * Note that we bind this method to the class instance's `this`. Any\n  * method used as a callback must be bound. In Coffeescript we use the\n  * fat arrow (`=>`)\n  */\n  _onStoreChanged = () => {\n    this.setState(this._getStateFromStores())\n  }\n\n  /*\n  * getStateFromStores fetches the data the view needs from the\n  * appropriate data source (our GithubStore). We return a basic object\n  * that can be passed directly into `setState`.\n  */\n  _getStateFromStores() {\n    return {\n      link: GithubStore.link(),\n    }\n  }\n\n  /** ** Other utility \"private\" methods ****\n  /*\n  * This responds to user interaction. Since it's a callback we have to\n  * bind it to the instances's `this` (Coffeescript fat arrow `=>`)\n  *\n  * In the case of this component we use the Electron `shell` module to\n  * request the computer to open the default browser.\n  *\n  * In other very common cases, user interaction handlers may fire an\n  * `Action` across the system for other Stores to respond to. They may\n  * also queue a {Task} to eventually perform a mutating API POST or PUT\n  * request.\n  */\n  _openLink = () => {\n    Actions.recordUserEvent(\"Github Thread Opened\", {pageUrl: this.state.link})\n    if (this.state.link) {\n      shell.openExternal(this.state.link)\n    }\n  }\n\n  render() {\n    if (this.props.items.length !== 1) { return false }\n    if (!this.state.link) { return false }\n    return (\n      <KeyCommandsRegion globalHandlers={this._keymapHandlers()}>\n        <button\n          className=\"btn btn-toolbar btn-view-on-github\"\n          onClick={this._openLink}\n          title={\"Visit Thread on GitHub\"}\n        >\n          <RetinaImg\n            mode={RetinaImg.Mode.ContentIsMask}\n            url=\"nylas://message-view-on-github/assets/github@2x.png\"\n          />\n        </button>\n      </KeyCommandsRegion>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-view-on-github/package.json",
    "content": "{\n  \"name\": \"message-view-on-github\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"View on Github button\",\n  \"isHiddenOnPluginsPage\": true,\n  \"license\": \"GPL-3.0\",\n\n  \"title\":\"View on GitHub\",\n  \"description\": \"Add a \\\"View On GitHub\\\" button that appears when viewing GitHub emails.\",\n  \"icon\": \"./icon.png\",\n  \"isOptional\": true,\n\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/message-view-on-github/stylesheets/github.less",
    "content": "\n.btn.btn-toolbar.btn-view-on-github {\n  &:only-of-type {\n    margin-right: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/mode-switch/lib/main.es6",
    "content": "import {ComponentRegistry, WorkspaceStore} from 'nylas-exports';\nimport {HasTutorialTip} from 'nylas-component-kit';\n\nimport ModeToggle from './mode-toggle';\n\nconst ToggleWithTutorialTip = HasTutorialTip(ModeToggle, {\n  title: 'Compose with context',\n  instructions: \"Nylas Mail shows you everything about your contacts right inside your inbox. See LinkedIn profiles, Twitter bios, message history, and more.\",\n});\n\n// NOTE: this is a hack to allow ComponentRegistry\n// to register the same component multiple times in\n// different areas. if we do this more than once, let's\n// dry this out.\nclass ToggleWithTutorialTipList extends ToggleWithTutorialTip {\n  static displayName = 'ModeToggleList'\n}\n\nexport function activate() {\n  ComponentRegistry.register(ToggleWithTutorialTipList, {\n    location: WorkspaceStore.Sheet.Thread.Toolbar.Right,\n    modes: ['list'],\n  });\n\n  ComponentRegistry.register(ToggleWithTutorialTip, {\n    location: WorkspaceStore.Sheet.Threads.Toolbar.Right,\n    modes: ['split'],\n  });\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ToggleWithTutorialTip);\n  ComponentRegistry.unregister(ToggleWithTutorialTipList);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/mode-switch/lib/mode-toggle.cjsx",
    "content": "{ComponentRegistry,\n WorkspaceStore,\n Actions} = require \"nylas-exports\"\n{RetinaImg} = require 'nylas-component-kit'\nReact = require \"react\"\n_ = require \"underscore\"\n\nclass ModeToggle extends React.Component\n  @displayName: 'ModeToggle'\n\n  constructor: (@props) ->\n    @column = WorkspaceStore.Location.MessageListSidebar\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @_unsubscriber = WorkspaceStore.listen(@_onStateChanged)\n    @_mounted = true\n\n  componentWillUnmount: =>\n    @_mounted = false\n    @_unsubscriber?()\n\n  render: =>\n    <button\n         className=\"btn btn-toolbar mode-toggle mode-#{@state.hidden}\"\n         style={order:500}\n         title={if @state.hidden then \"Show sidebar\" else \"Hide sidebar\"}\n         onClick={@_onToggleMode}>\n      <RetinaImg\n        name=\"toolbar-person-sidebar.png\"\n        mode={RetinaImg.Mode.ContentIsMask} />\n    </button>\n\n  _onStateChanged: =>\n    # We need to keep track of this because our parent unmounts us in the same\n    # event listener cycle that we receive the event in. ie:\n    #\n    #   for listener in listeners\n    #      # 1. workspaceView remove left column\n    #      # ---- Mode toggle unmounts, listeners array mutated in place\n    #      # 2. ModeToggle update\n    return unless @_mounted\n    @setState(@_getStateFromStores())\n\n  _getStateFromStores: =>\n    {hidden: WorkspaceStore.isLocationHidden(@column)}\n\n  _onToggleMode: =>\n    Actions.toggleWorkspaceLocationHidden(@column)\n\n\nmodule.exports = ModeToggle\n"
  },
  {
    "path": "packages/client-app/internal_packages/mode-switch/package.json",
    "content": "{\n  \"name\": \"mode-switch\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Mode switch\",\n  \"main\": \"./lib/main\",\n  \"license\": \"GPL-3.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/mode-switch/stylesheets/mode-switch.less",
    "content": "@import 'ui-variables';\n\n.btn-toolbar.mode-toggle {\n  z-index: 1000;\n  position: relative;\n}\n.btn-toolbar.mode-toggle.mode-false {\n  img.content-mask {\n    background-color: @component-active-color;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/items/account-error-notif.jsx",
    "content": "import {shell, ipcRenderer} from 'electron';\nimport {React, Account, AccountStore, Actions} from 'nylas-exports';\nimport {Notification} from 'nylas-component-kit';\n\nexport default class AccountErrorNotification extends React.Component {\n  static displayName = 'AccountErrorNotification';\n\n  constructor() {\n    super();\n    this._checkingTimeout = null\n    this.state = {\n      checking: false,\n      debugKeyPressed: false,\n      accounts: AccountStore.accounts(),\n    }\n  }\n\n  componentDidMount() {\n    this.unlisten = AccountStore.listen(() => this.setState({\n      accounts: AccountStore.accounts(),\n    }));\n  }\n\n  componentWillUnmount() {\n    this.unlisten();\n  }\n\n  _onContactSupport = (erroredAccount) => {\n    let url = 'https://support.nylas.com/hc/en-us/requests/new'\n    if (erroredAccount) {\n      url += `?email=${encodeURIComponent(erroredAccount.emailAddress)}`\n      const {syncError} = erroredAccount\n      if (syncError != null) {\n        url += `&subject=${encodeURIComponent('Sync Error')}`\n        const description = encodeURIComponent(\n          `Sync Error:\\n\\`\\`\\`\\n${JSON.stringify(syncError, null, 2)}\\n\\`\\`\\``\n        )\n        url += `&description=${description}`\n      }\n    }\n    shell.openExternal(url);\n  }\n\n  _onReconnect = (existingAccount) => {\n    ipcRenderer.send('command', 'application:add-account', {existingAccount, source: 'Reconnect from error notification'});\n  }\n\n  _onOpenAccountPreferences = () => {\n    Actions.switchPreferencesTab('Accounts');\n    Actions.openPreferences()\n  }\n\n  _onCheckAgain(event, account) {\n    if (event.metaKey) {\n      Actions.debugSync()\n      return\n    }\n    clearTimeout(this._checkingTimeout)\n    this.setState({checking: true})\n    this._checkingTimeout = setTimeout(() => this.setState({checking: false}), 10000)\n\n    if (account) {\n      Actions.wakeLocalSyncWorkerForAccount(account.id)\n      return\n    }\n    const erroredAccounts = this.state.accounts.filter(a => a.hasSyncStateError());\n    erroredAccounts.forEach(acc => Actions.wakeLocalSyncWorkerForAccount(acc.id))\n  }\n\n  render() {\n    const erroredAccounts = this.state.accounts.filter(a =>\n      a.hasN1CloudError() || a.hasSyncStateError()\n    );\n    const checkAgainLabel = this.state.checking ? 'Checking...' : 'Check Again'\n    let title;\n    let subtitle;\n    let subtitleAction;\n    let actions;\n    if (erroredAccounts.length === 0) {\n      return <span />\n    } else if (erroredAccounts.length > 1) {\n      title = \"Several of your accounts are having issues\";\n      actions = [{\n        label: checkAgainLabel,\n        fn: (e) => this._onCheckAgain(e),\n      }, {\n        label: \"Manage\",\n        fn: this._onOpenAccountPreferences,\n      }];\n    } else {\n      const erroredAccount = erroredAccounts[0];\n      if (erroredAccount.hasN1CloudError()) {\n        title = `Cannot authenticate Nylas Mail Cloud Services with ${erroredAccount.emailAddress}`;\n        actions = [{\n          label: checkAgainLabel,\n          fn: (e) => this._onCheckAgain(e, erroredAccount),\n        }, {\n          label: 'Reconnect',\n          fn: () => this._onReconnect(erroredAccount),\n        }];\n      } else {\n        switch (erroredAccount.syncState) {\n          case Account.SYNC_STATE_AUTH_FAILED:\n            title = `Cannot authenticate with ${erroredAccount.emailAddress}`;\n            actions = [{\n              label: checkAgainLabel,\n              fn: (e) => this._onCheckAgain(e, erroredAccount),\n            }, {\n              label: 'Reconnect',\n              fn: () => this._onReconnect(erroredAccount),\n            }];\n            break;\n          default: {\n            title = `Encountered an error while syncing ${erroredAccount.emailAddress}`;\n            let label = this.state.checking ? 'Retrying...' : 'Try Again'\n            if (this.state.debugKeyPressed) {\n              label = 'Debug'\n            }\n            actions = [{\n              label,\n              fn: (e) => this._onCheckAgain(e, erroredAccount),\n              props: {\n                onMouseEnter: (e) => this.setState({debugKeyPressed: e.metaKey}),\n                onMouseLeave: () => this.setState({debugKeyPressed: false}),\n              },\n            }];\n          }\n        }\n      }\n    }\n\n    return (\n      <Notification\n        priority=\"3\"\n        isError\n        title={title}\n        subtitle={subtitle}\n        subtitleAction={subtitleAction}\n        actions={actions}\n        icon=\"volstead-error.png\"\n      />\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/items/default-client-notif.jsx",
    "content": "import {React, DefaultClientHelper} from 'nylas-exports';\nimport {Notification} from 'nylas-component-kit';\n\nconst SETTINGS_KEY = 'nylas.mailto.prompted-about-default'\n\nexport default class DefaultClientNotification extends React.Component {\n  static displayName = 'DefaultClientNotification';\n\n  constructor() {\n    super();\n    this.helper = new DefaultClientHelper();\n    this.state = this.getStateFromStores();\n    this.state.initializing = true;\n    this.mounted = false;\n  }\n\n  componentDidMount() {\n    this.mounted = true;\n    this.helper.isRegisteredForURLScheme('mailto', (registered) => {\n      if (this.mounted) {\n        this.setState({\n          initializing: false,\n          registered: registered,\n        })\n      }\n    })\n    this.disposable = NylasEnv.config.onDidChange(SETTINGS_KEY,\n      () => this.setState(this.getStateFromStores()));\n  }\n\n  componentWillUnmount() {\n    this.mounted = false;\n    this.disposable.dispose();\n  }\n\n  getStateFromStores() {\n    return {\n      alreadyPrompted: NylasEnv.config.get(SETTINGS_KEY),\n    }\n  }\n\n  _onAccept = () => {\n    this.helper.registerForURLScheme('mailto', (err) => {\n      if (err) {\n        NylasEnv.reportError(err)\n      }\n    });\n    NylasEnv.config.set(SETTINGS_KEY, true)\n  }\n\n  _onDecline = () => {\n    NylasEnv.config.set(SETTINGS_KEY, true)\n  }\n\n  render() {\n    if (this.state.initializing || this.state.alreadyPrompted || this.state.registered) {\n      return <span />\n    }\n    return (\n      <Notification\n        title=\"Would you like to make Nylas Mail your default mail client?\"\n        priority=\"1\"\n        icon=\"volstead-defaultclient.png\"\n        actions={[{\n          label: \"Yes\",\n          fn: this._onAccept,\n        }, {\n          label: \"No\",\n          fn: this._onDecline,\n        }]}\n      />\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/items/dev-mode-notif.jsx",
    "content": "import {React} from 'nylas-exports';\nimport {Notification} from 'nylas-component-kit';\n\nexport default class DevModeNotification extends React.Component {\n  static displayName = 'DevModeNotification';\n\n  constructor() {\n    super();\n    // Don't need listeners to update this, since toggling dev mode reloads\n    // the entire window anyway\n    this.state = {\n      inDevMode: NylasEnv.inDevMode(),\n    }\n  }\n\n  render() {\n    if (!this.state.inDevMode) {\n      return <span />\n    }\n    return (\n      <Notification\n        priority=\"0\"\n        title=\"Nylas Mail is running in dev mode!\"\n      />\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/items/disabled-mail-rules-notif.jsx",
    "content": "import {React, MailRulesStore, Actions} from 'nylas-exports';\nimport {Notification} from 'nylas-component-kit';\n\nexport default class DisabledMailRulesNotification extends React.Component {\n  static displayName = 'DisabledMailRulesNotification';\n\n  constructor() {\n    super();\n    this.state = this.getStateFromStores();\n  }\n\n  componentDidMount() {\n    this.unlisten = MailRulesStore.listen(() => this.setState(this.getStateFromStores()));\n  }\n\n  componentWillUnmount() {\n    this.unlisten();\n  }\n\n  getStateFromStores() {\n    return {\n      disabledRules: MailRulesStore.disabledRules(),\n    }\n  }\n\n  _onOpenMailRulesPreferences = () => {\n    Actions.switchPreferencesTab('Mail Rules', {accountId: this.state.disabledRules[0].accountId})\n    Actions.openPreferences()\n  }\n\n  render() {\n    if (this.state.disabledRules.length === 0) {\n      return <span />\n    }\n    return (\n      <Notification\n        priority=\"2\"\n        title=\"One or more of your mail rules have been disabled.\"\n        icon=\"volstead-defaultclient.png\"\n        isError\n        actions={[{\n          label: 'View Mail Rules',\n          fn: this._onOpenMailRulesPreferences,\n        }]}\n      />\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/items/offline-notification.jsx",
    "content": "import {OnlineStatusStore, React, Actions} from 'nylas-exports';\nimport {Notification, ListensToFluxStore} from 'nylas-component-kit';\n\n\nfunction OfflineNotification({isOnline, retryingInSeconds}) {\n  if (isOnline) {\n    return false\n  }\n  const subtitle = retryingInSeconds ?\n    `Retrying in ${retryingInSeconds} second${retryingInSeconds > 1 ? 's' : ''}` :\n    `Retrying now...`;\n\n  return (\n    <Notification\n      className=\"offline\"\n      title=\"Nylas Mail is offline\"\n      subtitle={subtitle}\n      priority=\"5\"\n      icon=\"volstead-offline.png\"\n      actions={[{\n        id: 'try_now',\n        label: 'Try now',\n        fn: () => Actions.checkOnlineStatus(),\n      }]}\n    />\n  )\n}\nOfflineNotification.displayName = 'OfflineNotification'\nOfflineNotification.propTypes = {\n  isOnline: React.PropTypes.bool,\n  retryingInSeconds: React.PropTypes.number,\n}\n\nexport default ListensToFluxStore(OfflineNotification, {\n  stores: [OnlineStatusStore],\n  getStateFromStores() {\n    return {\n      isOnline: OnlineStatusStore.isOnline(),\n      retryingInSeconds: OnlineStatusStore.retryingInSeconds(),\n    }\n  },\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/main.es6",
    "content": "/* eslint no-unused-vars:0 */\n\nimport {ComponentRegistry, WorkspaceStore} from 'nylas-exports';\nimport ActivitySidebar from \"./sidebar/activity-sidebar\";\nimport NotifWrapper from \"./notif-wrapper\";\n\nimport AccountErrorNotification from \"./items/account-error-notif\";\nimport DefaultClientNotification from \"./items/default-client-notif\";\nimport DevModeNotification from \"./items/dev-mode-notif\";\nimport DisabledMailRulesNotification from \"./items/disabled-mail-rules-notif\";\nimport OfflineNotification from \"./items/offline-notification\";\n\nconst notifications = [\n  AccountErrorNotification,\n  DefaultClientNotification,\n  DevModeNotification,\n  DisabledMailRulesNotification,\n  OfflineNotification,\n]\n\nexport function activate() {\n  ComponentRegistry.register(ActivitySidebar, {location: WorkspaceStore.Location.RootSidebar});\n  ComponentRegistry.register(NotifWrapper, {location: WorkspaceStore.Location.RootSidebar});\n\n  for (const notification of notifications) {\n    ComponentRegistry.register(notification, {role: 'RootSidebar:Notifications'});\n  }\n}\n\nexport function serialize() {}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ActivitySidebar);\n  ComponentRegistry.unregister(NotifWrapper);\n\n  for (const notification of notifications) {\n    ComponentRegistry.unregister(notification)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/notif-wrapper.jsx",
    "content": "import _ from 'underscore';\nimport {React, ReactDOM} from 'nylas-exports'\nimport {InjectedComponentSet} from 'nylas-component-kit'\n\nconst ROLE = \"RootSidebar:Notifications\";\n\nexport default class NotifWrapper extends React.Component {\n  static displayName = 'NotifWrapper';\n\n  componentDidMount() {\n    this.observer = new MutationObserver(this.update);\n    this.observer.observe(ReactDOM.findDOMNode(this), {childList: true})\n    this.update() // Necessary if notifications are already mounted\n  }\n\n  componentWillUnmount() {\n    this.observer.disconnect();\n  }\n\n  update = () => {\n    const className = \"highest-priority\";\n    const node = ReactDOM.findDOMNode(this);\n\n    const oldHighestPriorityElems = node.querySelectorAll(`.${className}`);\n    for (const oldElem of oldHighestPriorityElems) {\n      oldElem.classList.remove(className)\n    }\n\n    const elemsWithPriority = node.querySelectorAll(\"[data-priority]\")\n    if (elemsWithPriority.length === 0) {\n      return;\n    }\n\n    const highestPriorityElem = _.max(elemsWithPriority,\n        (elem) => parseInt(elem.dataset.priority, 10))\n\n    highestPriorityElem.classList.add(className);\n  }\n\n  render() {\n    return (\n      <InjectedComponentSet\n        className=\"notifications\"\n        matching={{role: ROLE}}\n        direction=\"column\"\n        containersRequired={false}\n      />\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/sidebar/activity-sidebar.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\nReactCSSTransitionGroup = require 'react-addons-css-transition-group'\n_ = require 'underscore'\nclassNames = require 'classnames'\n\nSyncActivity = require(\"./sync-activity\").default\nSyncbackActivity = require(\"./syncback-activity\").default\n\n{Utils,\n Actions,\n TaskQueue,\n AccountStore,\n FolderSyncProgressStore,\n TaskQueueStatusStore\n PerformSendActionTask,\n SendDraftTask} = require 'nylas-exports'\n\nSEND_TASK_CLASSES = [PerformSendActionTask, SendDraftTask]\n\nclass ActivitySidebar extends React.Component\n  @displayName: 'ActivitySidebar'\n\n  @containerRequired: false\n  @containerStyles:\n    minWidth: 165\n    maxWidth: 400\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  componentDidMount: =>\n    @_unlisteners = []\n    @_unlisteners.push TaskQueueStatusStore.listen @_onDataChanged\n    @_unlisteners.push FolderSyncProgressStore.listen @_onDataChanged\n\n  componentWillUnmount: =>\n    unlisten() for unlisten in @_unlisteners\n\n  render: =>\n    sendTasks = []\n    nonSendTasks = []\n    @state.tasks.forEach (task) ->\n      if SEND_TASK_CLASSES.some(((taskClass) -> task instanceof taskClass ))\n        sendTasks.push(task)\n      else\n        nonSendTasks.push(task)\n\n\n    names = classNames\n      \"sidebar-activity\": true\n      \"sidebar-activity-error\": error?\n\n    wrapperClass = \"sidebar-activity-transition-wrapper \"\n\n    inside = <ReactCSSTransitionGroup\n      className={names}\n      transitionLeaveTimeout={625}\n      transitionEnterTimeout={125}\n      transitionName=\"activity-opacity\">\n        <SyncbackActivity syncbackTasks={sendTasks} />\n        <SyncActivity\n          initialSync={!@state.isInitialSyncComplete}\n          syncbackTasks={nonSendTasks}\n        />\n    </ReactCSSTransitionGroup>\n\n    <ReactCSSTransitionGroup\n      className={wrapperClass}\n      transitionLeaveTimeout={625}\n      transitionEnterTimeout={125}\n      transitionName=\"activity-opacity\">\n        {inside}\n    </ReactCSSTransitionGroup>\n\n  _onDataChanged: =>\n    @setState(@_getStateFromStores())\n\n  _getStateFromStores: =>\n    tasks: TaskQueueStatusStore.queue()\n    isInitialSyncComplete: FolderSyncProgressStore.isSyncComplete()\n\nmodule.exports = ActivitySidebar\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/sidebar/initial-sync-activity.jsx",
    "content": "import _ from 'underscore';\nimport _str from 'underscore.string';\nimport {Utils, AccountStore, FolderSyncProgressStore, React} from 'nylas-exports';\n\nconst MONTH_SHORT_FORMATS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul',\n  'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\nexport default class InitialSyncActivity extends React.Component {\n  static displayName = 'InitialSyncActivity';\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      syncState: FolderSyncProgressStore.getSyncState(),\n    }\n    this.mounted = false;\n  }\n\n  componentDidMount() {\n    this.mounted = true;\n    this.unsub = FolderSyncProgressStore.listen(this.onDataChanged)\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) ||\n      !Utils.isEqualReact(nextState, this.state);\n  }\n\n  componentWillUnmount() {\n    this.unsub();\n    this.mounted = false;\n  }\n\n  onDataChanged = () => {\n    const syncState = Utils.deepClone(FolderSyncProgressStore.getSyncState())\n    this.setState({syncState});\n  }\n\n  renderFolderProgress(name, progress, oldestProcessedDate) {\n    let status = 'busy';\n    let progressLabel = 'In Progress'\n    let syncedThrough = 'Syncing this past month';\n    if (progress === 1) {\n      status = 'complete';\n      progressLabel = '';\n      syncedThrough = 'Up to date'\n    } else {\n      let month = oldestProcessedDate.getMonth();\n      let year = oldestProcessedDate.getFullYear();\n      const currentDate = new Date();\n      if (month !== currentDate.getMonth() || year !== currentDate.getFullYear()) {\n        // We're currently syncing in `month`, which mean's we've synced through all\n        // of the month *after* it.\n        month++;\n        if (month === 12) {\n          month = 0;\n          year++;\n        }\n        syncedThrough = `Synced through ${MONTH_SHORT_FORMATS[month]} ${year}`;\n      }\n    }\n\n    return (\n      <div className={`model-progress ${status}`} key={name} title={syncedThrough}>\n        {_str.titleize(name)} <span className=\"progress-label\">{progressLabel}</span>\n      </div>\n    )\n  }\n\n  render() {\n    if (!AccountStore.accountsAreSyncing() || FolderSyncProgressStore.isSyncComplete()) {\n      return false;\n    }\n\n    let maxHeight = 0;\n    let accounts = _.map(this.state.syncState, (accountSyncState, accountId) => {\n      const account = _.findWhere(AccountStore.accounts(), {id: accountId});\n      if (!account) {\n        return false;\n      }\n\n      const {folderSyncProgress} = accountSyncState\n      let folderStates = _.map(folderSyncProgress, ({progress, oldestProcessedDate}, name) => {\n        return this.renderFolderProgress(name, progress, oldestProcessedDate)\n      })\n\n      if (folderStates.length === 0) {\n        folderStates = <div><br />Gathering folders...</div>\n      }\n\n      // A row for the account email address plus a row for each folder state,\n      const numRows = 1 + (folderStates.length || 1)\n      maxHeight += 50 * numRows;\n\n      return (\n        <div className=\"account\" key={accountId}>\n          <h2>{account.emailAddress}</h2>\n          {folderStates}\n        </div>\n      )\n    });\n\n    if (accounts.length === 0) {\n      accounts = <div><br />Looking for accounts...</div>\n    }\n\n    return (\n      <div\n        className=\"account-detail-area\"\n        key=\"expanded-sync-state\"\n        style={{maxHeight: `${maxHeight + 500}px`}}\n      >\n        {accounts}\n      </div>\n    )\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/sidebar/sync-activity.jsx",
    "content": "import classNames from 'classnames';\nimport {Actions, React, Utils} from 'nylas-exports';\n\nimport InitialSyncActivity from './initial-sync-activity';\nimport SyncbackActivity from './syncback-activity';\n\nexport default class SyncActivity extends React.Component {\n\n  static propTypes = {\n    initialSync: React.PropTypes.bool,\n    syncbackTasks: React.PropTypes.array,\n  }\n\n  constructor() {\n    super()\n    this.state = {\n      expanded: false,\n      blink: false,\n    }\n    this.mounted = false;\n  }\n\n  componentDidMount() {\n    this.mounted = true;\n    this.unsub = Actions.expandInitialSyncState.listen(this.showExpandedState);\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) ||\n    !Utils.isEqualReact(nextState, this.state);\n  }\n\n  componentWillUnmount() {\n    this.mounted = false;\n    this.unsub();\n  }\n\n  showExpandedState = () => {\n    if (!this.state.expanded) {\n      this.setState({expanded: true});\n    } else {\n      this.setState({blink: true});\n      setTimeout(() => {\n        if (this.mounted) {\n          this.setState({blink: false});\n        }\n      }, 1000)\n    }\n  }\n\n  hideExpandedState = () => {\n    this.setState({expanded: false});\n  }\n\n  _renderInitialSync() {\n    if (!this.props.initialSync) { return false; }\n    return <InitialSyncActivity />\n  }\n\n  _renderSyncbackTasks() {\n    return <SyncbackActivity syncbackTasks={this.props.syncbackTasks} />\n  }\n\n  _renderExpandedDetails() {\n    return (\n      <div>\n        <a className=\"close-expanded\" onClick={this.hideExpandedState}>Hide</a>\n        {this._renderSyncbackTasks()}\n        {this._renderInitialSync()}\n      </div>\n    )\n  }\n\n  render() {\n    const {initialSync, syncbackTasks} = this.props;\n    if (!initialSync && (!syncbackTasks || syncbackTasks.length === 0)) {\n      return false;\n    }\n\n    const classSet = classNames({\n      'item': true,\n      'expanded-sync': this.state.expanded,\n      'blink': this.state.blink,\n    });\n\n    const ellipses = [1, 2, 3].map((i) => (\n      <span key={`ellipsis${i}`} className={`ellipsis${i}`}>.</span>)\n    );\n\n    return (\n      <div\n        className={classSet}\n        key=\"sync-activity\"\n        onClick={() => (this.setState({expanded: !this.state.expanded}))}\n      >\n        <div className=\"inner clickable\">Syncing your mailbox{ellipses}</div>\n        {this.state.expanded ? this._renderExpandedDetails() : false}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/lib/sidebar/syncback-activity.jsx",
    "content": "import _ from 'underscore';\nimport {React, Utils} from 'nylas-exports';\n\nexport default class SyncbackActivity extends React.Component {\n  static propTypes = {\n    syncbackTasks: React.PropTypes.array,\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) ||\n      !Utils.isEqualReact(nextState, this.state);\n  }\n\n  render() {\n    const {syncbackTasks} = this.props;\n    if (!syncbackTasks || syncbackTasks.length === 0) { return false; }\n\n    const counts = {}\n    this.props.syncbackTasks.forEach((task) => {\n      const label = task.label ? task.label() : null;\n      if (!label) { return; }\n      if (!counts[label]) {\n        counts[label] = 0;\n      }\n      counts[label] += +task.numberOfImpactedItems()\n    });\n\n    const ellipses = [1, 2, 3].map((i) => (\n      <span key={`ellipsis${i}`} className={`ellipsis${i}`}>.</span>)\n    );\n\n    const items = _.pairs(counts).map(([label, count]) => {\n      return (\n        <div className=\"item\" key={label}>\n          <div className=\"inner\">\n            <span className=\"count\">({count.toLocaleString()})</span>\n            {label}{ellipses}\n          </div>\n        </div>\n      )\n    });\n\n    if (items.length === 0) {\n      items.push(\n        <div className=\"item\" key=\"no-labels\">\n          <div className=\"inner\">\n            Applying tasks\n          </div>\n        </div>\n      )\n    }\n\n    return (\n      <div>\n        {items}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/package.json",
    "content": "{\n  \"name\": \"notifications\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Notifications\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/spec/account-error-notif-spec.jsx",
    "content": "import {mount} from 'enzyme';\nimport {AccountStore, Account, Actions, React} from 'nylas-exports';\nimport {ipcRenderer} from 'electron';\n\nimport AccountErrorNotification from '../lib/items/account-error-notif';\n\ndescribe(\"AccountErrorNotif\", function AccountErrorNotifTests() {\n  describe(\"when one account is in the `invalid` state\", () => {\n    beforeEach(() => {\n      spyOn(AccountStore, 'accounts').andReturn([\n        new Account({id: 'A', syncState: 'invalid', emailAddress: '123@gmail.com'}),\n        new Account({id: 'B', syncState: 'running', emailAddress: 'other@gmail.com'}),\n      ])\n    });\n\n    it(\"renders an error bar that mentions the account email\", () => {\n      const notif = mount(<AccountErrorNotification />);\n      expect(notif.find('.title').text().indexOf('123@gmail.com') > 0).toBe(true);\n    });\n\n    it(\"allows the user to refresh the account\", () => {\n      const notif = mount(<AccountErrorNotification />);\n      spyOn(Actions, 'wakeLocalSyncWorkerForAccount').andReturn(Promise.resolve());\n      notif.find('#action-0').simulate('click'); // Expects first action to be the refresh action\n      expect(Actions.wakeLocalSyncWorkerForAccount).toHaveBeenCalled();\n    });\n\n    it(\"allows the user to reconnect the account\", () => {\n      const notif = mount(<AccountErrorNotification />);\n      spyOn(ipcRenderer, 'send');\n      notif.find('#action-1').simulate('click'); // Expects second action to be the reconnect action\n      expect(ipcRenderer.send).toHaveBeenCalledWith('command', 'application:add-account', {\n        existingAccount: AccountStore.accounts()[0],\n        source: 'Reconnect from error notification',\n      });\n    });\n  });\n\n  describe(\"when more than one account is in the `invalid` state\", () => {\n    beforeEach(() => {\n      spyOn(AccountStore, 'accounts').andReturn([\n        new Account({id: 'A', syncState: 'invalid', emailAddress: '123@gmail.com'}),\n        new Account({id: 'B', syncState: 'invalid', emailAddress: 'other@gmail.com'}),\n      ])\n    });\n\n    it(\"renders an error bar\", () => {\n      const notif = mount(<AccountErrorNotification />);\n      expect(notif.find('.notification').exists()).toEqual(true);\n    });\n\n    it(\"allows the user to refresh the accounts\", () => {\n      const notif = mount(<AccountErrorNotification />);\n      spyOn(Actions, 'wakeLocalSyncWorkerForAccount').andReturn(Promise.resolve());\n      notif.find('#action-0').simulate('click'); // Expects first action to be the refresh action\n      expect(Actions.wakeLocalSyncWorkerForAccount).toHaveBeenCalled();\n    });\n\n    it(\"allows the user to open preferences\", () => {\n      spyOn(Actions, 'switchPreferencesTab')\n      spyOn(Actions, 'openPreferences')\n      const notif = mount(<AccountErrorNotification />);\n      notif.find('#action-1').simulate('click'); // Expects second action to be the preferences action\n      expect(Actions.openPreferences).toHaveBeenCalled();\n      expect(Actions.switchPreferencesTab).toHaveBeenCalledWith('Accounts');\n    });\n  });\n\n  describe(\"when all accounts are fine\", () => {\n    beforeEach(() => {\n      spyOn(AccountStore, 'accounts').andReturn([\n        new Account({id: 'A', syncState: 'running', emailAddress: '123@gmail.com'}),\n        new Account({id: 'B', syncState: 'running', emailAddress: 'other@gmail.com'}),\n      ])\n    });\n\n    it(\"renders nothing\", () => {\n      const notif = mount(<AccountErrorNotification />);\n      expect(notif.find('.notification').exists()).toEqual(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/spec/default-client-notif-spec.jsx",
    "content": "import {mount} from 'enzyme';\nimport proxyquire from 'proxyquire';\nimport {React} from 'nylas-exports';\n\nlet stubIsRegistered = null;\nlet stubRegister = () => {};\nconst patched = proxyquire('../lib/items/default-client-notif',\n  {\n    'nylas-exports': {\n      DefaultClientHelper: class {\n        constructor() {\n          this.isRegisteredForURLScheme = (urlScheme, callback) => { callback(stubIsRegistered) };\n          this.registerForURLScheme = (urlScheme) => { stubRegister(urlScheme) };\n        }\n      },\n    },\n  }\n)\nconst DefaultClientNotification = patched.default;\nconst SETTINGS_KEY = 'nylas.mailto.prompted-about-default';\n\ndescribe(\"DefaultClientNotif\", function DefaultClientNotifTests() {\n  describe(\"when N1 isn't the default mail client\", () => {\n    beforeEach(() => {\n      stubIsRegistered = false;\n    })\n    describe(\"when the user has already responded\", () => {\n      beforeEach(() => {\n        spyOn(NylasEnv.config, \"get\").andReturn(true);\n        this.notif = mount(<DefaultClientNotification />);\n        expect(NylasEnv.config.get).toHaveBeenCalledWith(SETTINGS_KEY);\n      });\n      it(\"renders nothing\", () => {\n        expect(this.notif.find('.notification').exists()).toEqual(false);\n      });\n    });\n\n    describe(\"when the user has yet to respond\", () => {\n      beforeEach(() => {\n        spyOn(NylasEnv.config, \"get\").andReturn(false);\n        this.notif = mount(<DefaultClientNotification />);\n        expect(NylasEnv.config.get).toHaveBeenCalledWith(SETTINGS_KEY);\n      });\n      it(\"renders a notification\", () => {\n        expect(this.notif.find('.notification').exists()).toEqual(true);\n      });\n\n      it(\"allows the user to set N1 as the default client\", () => {\n        let scheme = null;\n        stubRegister = (urlScheme) => { scheme = urlScheme };\n        this.notif.find('#action-0').simulate('click'); // Expects first action to set N1 as default\n        expect(scheme).toEqual('mailto');\n      });\n\n      it(\"allows the user to decline\", () => {\n        spyOn(NylasEnv.config, \"set\")\n        this.notif.find('#action-1').simulate('click'); // Expects second action to decline\n        expect(NylasEnv.config.set).toHaveBeenCalledWith(SETTINGS_KEY, true);\n      });\n    })\n  });\n\n  describe(\"when N1 is the default mail client\", () => {\n    beforeEach(() => {\n      stubIsRegistered = true;\n      this.notif = mount(<DefaultClientNotification />)\n    })\n    it(\"renders nothing\", () => {\n      expect(this.notif.find('.notification').exists()).toEqual(false);\n    });\n  })\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/spec/dev-mode-notif-spec.jsx",
    "content": "import {mount} from 'enzyme';\nimport {React} from 'nylas-exports';\nimport DevModeNotification from '../lib/items/dev-mode-notif';\n\ndescribe(\"DevModeNotif\", function DevModeNotifTests() {\n  describe(\"When the window is in dev mode\", () => {\n    beforeEach(() => {\n      spyOn(NylasEnv, \"inDevMode\").andReturn(true);\n      this.notif = mount(<DevModeNotification />);\n    })\n    it(\"displays a notification\", () => {\n      expect(this.notif.find('.notification').exists()).toEqual(true);\n    })\n  })\n\n  describe(\"When the window is not in dev mode\", () => {\n    beforeEach(() => {\n      spyOn(NylasEnv, \"inDevMode\").andReturn(false);\n      this.notif = mount(<DevModeNotification />);\n    })\n    it(\"doesn't display a notification\", () => {\n      expect(this.notif.find('.notification').exists()).toEqual(false);\n    })\n  })\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/spec/disabled-mail-rules-notif-spec.jsx",
    "content": "import {mount} from 'enzyme';\nimport {React, AccountStore, Account, Actions, MailRulesStore} from 'nylas-exports';\nimport DisabledMailRulesNotification from '../lib/items/disabled-mail-rules-notif';\n\ndescribe(\"DisabledMailRulesNotification\", function DisabledMailRulesNotifTests() {\n  beforeEach(() => {\n    spyOn(AccountStore, 'accounts').andReturn([\n      new Account({id: 'A', syncState: 'running', emailAddress: '123@gmail.com'}),\n    ])\n  })\n  describe(\"When there is one disabled mail rule\", () => {\n    beforeEach(() => {\n      spyOn(MailRulesStore, \"disabledRules\").andReturn([{accountId: 'A'}])\n      this.notif = mount(<DisabledMailRulesNotification />)\n    })\n    it(\"displays a notification\", () => {\n      expect(this.notif.find('.notification').exists()).toEqual(true);\n    })\n\n    it(\"allows users to open the preferences\", () => {\n      spyOn(Actions, \"switchPreferencesTab\")\n      spyOn(Actions, \"openPreferences\")\n      this.notif.find('#action-0').simulate('click');\n      expect(Actions.switchPreferencesTab).toHaveBeenCalledWith('Mail Rules', {accountId: 'A'})\n      expect(Actions.openPreferences).toHaveBeenCalled();\n    })\n  });\n\n  describe(\"When there are multiple disabled mail rules\", () => {\n    beforeEach(() => {\n      spyOn(MailRulesStore, \"disabledRules\").andReturn([{accountId: 'A'},\n        {accountId: 'A'}])\n      this.notif = mount(<DisabledMailRulesNotification />)\n    })\n    it(\"displays a notification\", () => {\n      expect(this.notif.find('.notification').exists()).toEqual(true);\n    })\n\n    it(\"allows users to open the preferences\", () => {\n      spyOn(Actions, \"switchPreferencesTab\")\n      spyOn(Actions, \"openPreferences\")\n      this.notif.find('#action-0').simulate('click');\n      expect(Actions.switchPreferencesTab).toHaveBeenCalledWith('Mail Rules', {accountId: 'A'})\n      expect(Actions.openPreferences).toHaveBeenCalled();\n    })\n  });\n\n  describe(\"When there are no disabled mail rules\", () => {\n    beforeEach(() => {\n      spyOn(MailRulesStore, \"disabledRules\").andReturn([])\n      this.notif = mount(<DisabledMailRulesNotification />)\n    })\n    it(\"does not display a notification\", () => {\n      expect(this.notif.find('.notification').exists()).toEqual(false);\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/spec/priority-spec.jsx",
    "content": "import {mount} from 'enzyme';\nimport {ComponentRegistry, React} from 'nylas-exports';\nimport {Notification} from 'nylas-component-kit';\n\nimport NotifWrapper from '../lib/notif-wrapper';\n\nconst stubNotif = (priority) => {\n  return class extends React.Component {\n    static displayName = `NotifPriority${priority}`;\n    static containerRequired = false;\n    render() { return <Notification priority={`${priority}`} title={`Priority ${priority}`} /> }\n  }\n};\n\nconst checkHighestPriority = (expectedPriority, wrapper) => {\n  const visibleElems = wrapper.find(\".highest-priority\")\n  expect(visibleElems.exists()).toEqual(true);\n  const titleElem = visibleElems.first().find('.title');\n  expect(titleElem.exists()).toEqual(true);\n  expect(titleElem.text().trim()).toEqual(`Priority ${expectedPriority}`);\n  // Make sure there's only one highest-priority elem\n  expect(visibleElems.get(1)).toEqual(undefined);\n}\n\ndescribe(\"NotifPriority\", function notifPriorityTests() {\n  beforeEach(() => {\n    this.wrapper = mount(<NotifWrapper />)\n    this.trigger = () => {\n      ComponentRegistry.trigger();\n      this.wrapper.get(0).update();\n    }\n  })\n  describe(\"When there is only one notification\", () => {\n    beforeEach(() => {\n      ComponentRegistry._clear();\n      ComponentRegistry.register(stubNotif(5), {role: 'RootSidebar:Notifications'})\n      this.trigger();\n    })\n    it(\"should mark it as highest-priority\", () => {\n      checkHighestPriority(5, this.wrapper);\n    })\n  })\n  describe(\"when there are multiple notifications\", () => {\n    beforeEach(() => {\n      this.components = [stubNotif(5), stubNotif(7), stubNotif(3), stubNotif(2)]\n      ComponentRegistry._clear();\n      this.components.forEach((item) => {\n        ComponentRegistry.register(item, {role: 'RootSidebar:Notifications'})\n      })\n      this.trigger();\n    })\n    it(\"should mark the proper one as highest-priority\", () => {\n      checkHighestPriority(7, this.wrapper);\n    })\n    it(\"properly updates when a highest-priority notification is removed\", () => {\n      ComponentRegistry.unregister(this.components[1])\n      this.trigger();\n      checkHighestPriority(5, this.wrapper);\n    })\n    it(\"properly updates when a higher priority notifcation is added\", () => {\n      ComponentRegistry.register(stubNotif(10), {role: 'RootSidebar:Notifications'});\n      this.trigger();\n      checkHighestPriority(10, this.wrapper);\n    })\n  })\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/stylesheets/notifications.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.sidebar-activity-transition-wrapper {\n  order: 2;\n  z-index: 2;\n  overflow-y: auto;\n}\n\n.sidebar-activity {\n  display: block;\n  width: 100%;\n  bottom: 0;\n  background: @background-off-primary;\n  font-size: @font-size-small;\n  color: @text-color-subtle;\n  line-height:@line-height-computed * 0.95;\n  box-shadow:inset 0 1px 0 @border-color-divider;\n  &:hover { cursor: default }\n\n\n  .item {\n    &:hover { cursor: default }\n    .clickable { cursor: pointer; }\n\n    .inner {\n      padding: @padding-large-vertical @padding-base-horizontal @padding-large-vertical @padding-base-horizontal;\n      border-bottom: 1px solid rgba(0,0,0,0.1);\n\n      .ellipsis1 {\n        animation: show-ellipsis 3s 0s infinite;\n      }\n      .ellipsis2 {\n        animation: show-ellipsis 3s 250ms infinite;\n      }\n      .ellipsis3 {\n        animation: show-ellipsis 3s 500ms infinite;\n      }\n    }\n    .count {\n      color: @text-color-very-subtle;\n      float:right;\n    }\n    .btn {\n      display:block;\n      text-align:center;\n      margin-top:4px;\n      margin-bottom:4px;\n      font-size: @font-size-small;\n    }\n    // TODO: Necessary for Chromium 42 to render `activity-opacity-leave` animation\n    // properly. Removing position relative causes the div to remain visible\n    position:relative;\n    opacity: 1;\n\n    .account-detail-area {\n      max-height: 0;\n      overflow: hidden;\n      transition: max-height 0.2s;\n    }\n    &.expanded-sync {\n      .account {\n        padding: @padding-base-vertical @padding-base-horizontal @padding-large-vertical*1.5 @padding-base-horizontal;\n        border-bottom: 1px solid rgba(0,0,0,0.1);\n\n        .model-progress::before {\n          height: 10px;\n          width: 10px;\n          background-color: #00dd00;\n          content: '';\n          display: inline-block;\n          border-radius: 5px;\n          margin-right: 5px;\n          box-sizing: border-box;\n        }\n\n        .model-progress.busy::before {\n          background-color: transparent;\n          border: solid 2px #00dd00;\n          animation: border-pulse 3s infinite;\n        }\n\n        .model-progress {\n          font-size: @font-size-base;\n          margin: 3px;\n          max-width: 100%;\n          overflow: hidden;\n          white-space: nowrap;\n          text-overflow: ellipsis;\n\n          .progress-label {\n            color: @text-color-very-subtle;\n            margin-left: 2px;\n            font-size: @font-size-smaller;\n          }\n        }\n      }\n    }\n    h2 {\n      font-size: 14px;\n      margin: 0;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      font-weight: 500;\n      margin-bottom: @padding-large-vertical;\n    }\n    h3 {\n      font-size: 14px;\n      margin: 10px 0 4px 0;\n    }\n    .amount {\n      margin-top: 2px;\n      font-size: 12px;\n      color: @text-color-subtle;\n    }\n    .close-expanded {\n      padding: @padding-large-vertical @padding-base-horizontal;\n      position: absolute;\n      top: 0;\n      right: 0;\n      cursor: pointer;\n    }\n  }\n\n  transition: height 0.4s;\n  transition-delay: 2s;\n  &.sidebar-activity-error {\n    .progress {\n      background-color: @color-error;\n    }\n  }\n}\n\n.activity-opacity-enter {\n  opacity:0;\n  transition: opacity .125s ease-out;\n}\n\n.activity-opacity-enter.activity-opacity-enter-active {\n  opacity:1;\n}\n\n.activity-opacity-leave {\n  opacity:1;\n  transition: opacity .125s ease-in;\n  transition-delay: 0.5s;\n}\n\n.activity-opacity-leave.activity-opacity-leave-active {\n  transition-delay: 0.5s;\n  opacity:0;\n}\n\n.notifications-sticky {\n  width:100%;\n\n  .notification-info {\n    background-color: @background-color-info;\n  }\n  .notification-developer {\n    background-color: #615396;\n  }\n  .notification-upgrade {\n    background-image: -webkit-linear-gradient(bottom, #429E91, #40b1ac);\n    img { background-color: @text-color-inverse; }\n  }\n  .notification-error {\n    background: linear-gradient(to top, darken(@background-color-error, 4%) 0%, @background-color-error 100%);\n    border-color: @background-color-error;\n    color: @color-error;\n  }\n  .notification-offline {\n    background: linear-gradient(to top, darken(#CC9900, 4%) 0%, #CC9900 100%);\n    border-color: darken(#CC9900, 5%);\n  }\n\n  .notifications-sticky-item {\n    display:flex;\n    font-size: @font-size-base;\n    color: @text-color-inverse;\n    border-bottom:1px solid rgba(0,0,0,0.25);\n    padding-left: @padding-base-horizontal;\n    line-height: @line-height-base * 1.5;\n    align-items: baseline;\n    a {\n      flex-shrink: 0;\n      color:@text-color-inverse;\n      padding: 0 @padding-base-horizontal;\n    }\n    a:hover {\n      background-color: rgba(255,255,255,0.15);\n      text-decoration:none;\n      color:@text-color-inverse;\n    }\n    a.default {\n      background-color: rgba(0,0,0,0.15);\n    }\n    a.default:hover {\n      background-color: rgba(255,255,255,0.15);\n    }\n    i {\n      margin-right:@padding-base-horizontal;\n    }\n    .icon {\n      display: inline-block;\n      align-self: center;\n      line-height: 16px;\n      margin-right:@padding-base-horizontal;\n\n      img {\n        vertical-align: initial;\n      }\n    }\n\n    div.message {\n      flex: 1;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      line-height: @line-height-base * 1.1;\n      padding: @padding-small-vertical 0;\n    }\n\n    &.has-default-action:hover {\n      -webkit-filter: brightness(110%);\n      cursor:default;\n    }\n  }\n}\n\n.blink {\n  animation: blink 1s ease;\n}\n\n@-webkit-keyframes blink {\n  0%, 100%{\n    box-shadow: none;\n  }\n  50% {\n    box-shadow: 5px 5px 1px rgba(37, 143, 225, 1) inset,\n               -5px -5px 1px rgba(37, 143, 225, 1) inset;\n  }\n}\n\n@-webkit-keyframes border-pulse {\n  0%, 100%{\n    border-color: #00dd00;\n  }\n  50% {\n    border-color: transparent;\n  }\n}\n\n@-webkit-keyframes show-ellipsis {\n  0%, 100% {opacity: 0;}\n  50%, {opacity: 1.0;}\n}\n\n// Windows Changes\n\nbody.platform-win32 {\n  .notifications-sticky {\n    .notifications-sticky-item {\n      a {\n        border-radius: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/notifications/stylesheets/styles.less",
    "content": "@import 'ui-variables';\n\n.notifications {\n  background-color: @panel-background-color;\n  box-shadow: 0 -6px 4px @panel-background-color;\n  z-index: 2;\n}\n\n.notification {\n  background: @background-color-info;\n  display: none;\n  color: @text-color-inverse;\n  margin: 10px;\n  margin-top: 0;\n  border-radius: @border-radius-large;\n}\n\n.notification.error {\n  background: @background-color-error;\n}\n\n.notification.offline {\n  background: linear-gradient(to top, darken(#CC9900, 4%) 0%, #CC9900 100%);\n}\n\n.notification.highest-priority {\n  display: block;\n}\n\n.notif-top {\n  display: flex;\n  align-items: flex-start;\n  padding: 10px;\n}\n\n.notification .icon {\n  margin-right: 10px;\n}\n\n.notification .title {\n  padding: 10px;\n}\n\n.notification .subtitle {\n  font-size: @font-size-smaller;\n  position: relative;\n  opacity: 0.8;\n}\n\n.notification .subtitle.has-action {\n  cursor: pointer;\n}\n\n.notification .subtitle.has-action::after {\n  content:'';\n  background: url(nylas://notifications/assets/minichevron@2x.png) top left no-repeat;\n  background-size: 4.5px 7px;\n  margin-left:3px;\n  display: inline-block;\n  width:4.5px;\n  height:7px;\n  vertical-align: baseline;\n}\n\n.notification .actions-wrapper {\n  display: flex;\n}\n\n.notification .action {\n  text-align: center;\n  flex: 1;\n  border-top: solid rgba(255, 255, 255, 0.5) 1px;\n  border-left: solid rgba(255, 255, 255, 0.5) 1px;\n  padding: 10px;\n  cursor: pointer;\n\n  /* The semi-transparent backgrounds that can be layered on top\n     of this class shouldn't have sharp corners on the bottom */\n  border-bottom-left-radius: @border-radius-large;\n  border-bottom-right-radius: @border-radius-large;\n}\n\n.notification .action:first-child {\n  border-left: none;\n}\n\n.notification .action:hover {\n  background-color: rgba(255, 255, 255, 0.2);\n  box-shadow: @standard-shadow inset;\n}\n\n.notification .action.loading {\n  cursor: progress;\n  background-color: rgba(0, 0, 0, 0.2);\n  box-shadow: @standard-shadow inset;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-fonts/lib/main.es6",
    "content": "export function activate() {}\nexport function deactivate() {}\nexport function serialize() {}\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-fonts/package.json",
    "content": "{\n  \"name\": \"nylas-private-fonts\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Nylas Fonts\",\n  \"license\": \"Proprietary\",\n  \"private\": true,\n  \"windowTypes\": {\n    \"all\": true\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-fonts/stylesheets/nylas-fonts.less",
    "content": "// ----- Font Families -----\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 200;\n  src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Thin.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 300;\n  src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Blond.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 400;\n  src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Normal.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 500;\n  src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Medium.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 600;\n  src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-SemiBold.otf');\n}\n\n// Pro-SemiBold doesn't render emoji properly. Override the emjoi unicode\n// block so that it uses the \"Normal\" weight even at font-weight:600.\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 600;\n  src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Normal.otf'), Helvetica, sans-serif;\n  unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/.gitignore",
    "content": ".arcconfig\n.arclint\narclib\n*.swp\n*~\n.DS_Store\nThumbs.db\n.project\n.svn\n.nvm-version\nnode_modules\nnpm-debug.log\ndebug.log\n/tags\n/electron/\ndocs/output\ndocs/includes\nspec/fixtures/evil-files/\n/_site\n/.sass-cache\n.integration-test-config\n.idea/\nspec-saved-state.json\n\n!spec/fixtures/packages/package-with-incompatible-native-module/node_modules\n\n#emacs\n*~\n*#\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/README.md",
    "content": "# Nylas Mail Salesforce Integration\n\nSee [+N1 Salesforce](https://paper.dropbox.com/doc/N1-Salesforce-tIXHxx0fSDJSnxdxAx1rS) on Paper\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/keymaps/salesforce.json",
    "content": "{\n  \"salesforce:show-relate-thread-popover\": \"mod+l\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/composer/contact-search-results.jsx",
    "content": "import React from 'react'\nimport {Menu} from 'nylas-component-kit'\nimport {Contact, DatabaseStore} from 'nylas-exports'\n\nimport SalesforceIcon from '../shared-components/salesforce-icon'\nimport SalesforceObject from '../models/salesforce-object'\n\nexport function ContactSearchResult({token}) {\n  return (\n    <span key={token.id} className=\"salesforce-contact-search-result\">\n      <SalesforceIcon objectType=\"Contact\" />\n      <Menu.NameEmailItem name={token.name} email={token.email} />\n    </span>\n  )\n}\nContactSearchResult.propTypes = {\n  token: React.PropTypes.instanceOf(Contact),\n}\n\n/**\n * Registers as \"ContactSearchResults\"\n */\nexport default class ContactSearchResults extends React.Component {\n  static displayName = \"ContactSearchResults\"\n\n  static containerRequired = false\n\n  static propTypes = {\n    token: React.PropTypes.instanceOf(SalesforceObject),\n  }\n\n  /**\n   * Finds Salesforce contacts and replaces any pre-found and sorted\n   * nylasContacts with the corresponding Salesforce contact.\n   */\n  static findAdditionalContacts(search, nylasContacts) {\n    return DatabaseStore.findAll(SalesforceObject)\n    .search(search).then((results) => {\n      const sfContacts = results.filter(c => c.type === \"Contact\")\n        .map(o => {\n          const c = new Contact({name: o.name, email: o.identifier})\n          c.customComponent = ContactSearchResult\n          return c;\n        });\n\n      const sfEmails = {}\n      sfContacts.forEach((c, i) => {\n        sfEmails[c.email.toLowerCase()] = i\n      });\n\n      const combinedContacts = []\n      nylasContacts.forEach((c) => {\n        const i = sfEmails[c.email.toLowerCase()]\n        if (i >= 0) {\n          combinedContacts.push(sfContacts.splice(i, 1)[0])\n        } else {\n          combinedContacts.push(c)\n        }\n      })\n\n      return combinedContacts.concat(sfContacts);\n    })\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/composer/participant-decorator.jsx",
    "content": "import React from 'react'\nimport {Rx, DatabaseStore} from 'nylas-exports'\n\nimport SalesforceIcon from '../shared-components/salesforce-icon'\nimport SalesforceObject from '../models/salesforce-object'\n\nexport default class ParticipantDecorator extends React.Component {\n\n  static displayName = \"ParticipantDecorator\"\n  static containerRequired = false\n\n  static propTypes = {\n    contact: React.PropTypes.object,\n    collapsed: React.PropTypes.bool,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = { sfContacts: [] }\n  }\n\n  componentWillMount() {\n    this._setupObserver(this.props)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this._setupObserver(nextProps)\n  }\n\n  componentWillUnmount() {\n    this._disposable.dispose();\n  }\n\n  _setupObserver(props) {\n    if (this._disposable) this._disposable.dispose();\n    const email = (props.contact.email || \"\").toLowerCase().trim()\n    if (email.length === 0) return;\n    const query = DatabaseStore.findAll(SalesforceObject)\n    .where({type: \"Contact\", identifier: email})\n\n    this._disposable = Rx.Observable.fromQuery(query)\n    .subscribe((sfContacts = []) => {\n      this.setState({sfContacts})\n    })\n  }\n\n  render() {\n    if (this.props.collapsed) return false;\n    if (this.state.sfContacts.length === 0) return false\n    return <SalesforceIcon objectType=\"Contact\" />\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/composer/salesforce-composer-picker.jsx",
    "content": "import React from 'react'\nimport {Popover, RetinaImg} from 'nylas-component-kit'\nimport SalesforceObjectPicker from '../form/salesforce-object-picker'\n\n// TODO: Add to composer\nclass SalesforceComposerPicker extends React.Component {\n  static displayName = \"SalesforceComposerPicker\"\n\n  // Inline composers will have threadIds.\n  // Popout composers will not and the threadId will be null.\n  static propTypes= {\n    threadId: React.PropTypes.string,\n    draftClientId: React.PropTypes.string.isRequired,\n  }\n\n  static containerStyles = {\n    order: 2,\n  }\n\n  _defaultObjectType() {\n    return \"Opportunity\"\n  }\n\n  _pickerId() {\n    return `${this.props.draftClientId}-Picker`\n  }\n\n  _renderPicker() {\n    const button = (\n      <button className=\"btn btn-toolbar narrow\">\n        <RetinaImg\n          name=\"nylas://salesforce/static/images/salesforce-icon.png\"\n          style={{position: \"relative\", top: \"-2px\"}}\n          mode={RetinaImg.Mode.ContentPreserve}\n        />\n        <RetinaImg\n          name=\"nylas://salesforce/static/images/toolbar-chevron.png\"\n          style={{position: \"relative\", top: \"-2px\"}}\n          mode={RetinaImg.Mode.ContentPreserve}\n        />\n      </button>\n    )\n\n    return (\n      <Popover ref=\"popover\" className=\"salesforce-composer-picker pull-right\" buttonComponent={button}>\n        <h2 className=\"picker-h2\">Sync with Salesforce {this._defaultObjectType()}</h2>\n        <SalesforceObjectPicker\n          id={this._pickerId()}\n          objectType={this._defaultObjectType()}\n        />\n      </Popover>\n    )\n  }\n\n  render() {\n    return this._renderPicker()\n  }\n}\n\nexport default SalesforceComposerPicker\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/contact/salesforce-contact-info.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\n\nimport {FocusedContentStore} from 'nylas-exports'\n\nimport SalesforceIcon from '../shared-components/salesforce-icon'\nimport * as dataHelpers from '../salesforce-object-helpers'\nimport SalesforceActions from '../salesforce-actions'\nimport OpenInSalesforceBtn from '../shared-components/open-in-salesforce-btn'\nimport SalesforceRelatedObjectCache from '../salesforce-related-object-cache'\n\nclass SalesforceContactInfo extends React.Component {\n  static displayName = \"SalesforceContactInfo\";\n\n  static containerStyles = {\n    order: 97,\n  }\n\n  static propTypes = {\n    contact: React.PropTypes.object.isRequired,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = { leads: [], contacts: [] }\n  }\n\n  componentDidMount() {\n    this._fetchLead(this.props)\n    this._disposable = SalesforceRelatedObjectCache.observeDirectlyRelatedSObjectsByEmail(this.props.contact.email).subscribe((sObjectsById = {}) => {\n      const objsByType = _.groupBy(_.values(sObjectsById), \"type\");\n      this.setState({\n        leads: objsByType.Lead || [],\n        contacts: objsByType.Contact || [],\n      })\n    })\n  }\n\n  componentWillReceiveProps(nextProps = {}) {\n    this._fetchLead(nextProps)\n  }\n\n  componentWillUnmount() {\n    this._disposable.dispose()\n  }\n\n  /**\n   * We don't initial sync Leads because there are usually too many of\n   * them (Millions). If a user inspects a contact, then we'll fetch leads\n   * on demand. If we find a lead, it'll save to the Database which will\n   * cause the SalesforceRelatedObjectCache to trigger for our observable.\n   */\n  _fetchLead(props) {\n    const email = props.contact.email.toLowerCase().trim()\n    return dataHelpers.loadBasicObjectsByField({\n      objectType: \"Lead\",\n      where: {Email: email},\n    }).then(dataHelpers.upsertBasicObjects)\n  }\n\n  _requestNew(objectType, objectInitialData = {}) {\n    const thread = FocusedContentStore.focused('thread')\n    return SalesforceActions.openObjectForm({\n      objectType: objectType,\n      objectInitialData: objectInitialData,\n      contextData: {\n        nylasObjectId: thread.id,\n        nylasObjectType: \"Thread\",\n        focusedNylasContactData: {\n          id: this.props.contact.id,\n          name: this.props.contact.name,\n          email: this.props.contact.email,\n        },\n      },\n    })\n  }\n\n  _requestEdit(object) {\n    const thread = FocusedContentStore.focused('thread')\n    SalesforceActions.openObjectForm({\n      objectId: object.id,\n      objectType: object.type,\n      objectInitialData: object,\n      contextData: {\n        nylasObjectId: thread.id,\n        nylasObjectType: \"Thread\",\n      },\n    })\n  }\n\n  _renderObjectCreators() {\n    const headers = []\n\n    if (this.state.leads.length === 0) {\n      headers.push(this._renderCreateObj(\"Lead\"))\n      if (this.state.contacts.length === 0) {\n        headers.push(this._renderCreateObj(\"Contact\"))\n      }\n    }\n\n    if (headers.length === 0) { return false; }\n\n    return <div className=\"related-sf-creators\">{headers}</div>\n  }\n\n  _hasRelatedObjects() {\n    return (this.state.leads.length > 0 || this.state.contacts.length > 0)\n  }\n\n  _renderRelatedObjects() {\n    if (!this._hasRelatedObjects()) { return false; }\n    return (\n      <div className={`cell-container`}>\n        {[this._renderRelatedSFObjects(\"Lead\", this.state.leads),\n          this._renderRelatedSFObjects(\"Contact\", this.state.contacts)]}\n      </div>\n    )\n  }\n\n  _renderRelatedSFObjects(objectType, sfObjects = []) {\n    const objDoms = []\n    sfObjects.forEach((object) => {\n      const reqEdit = _.debounce(() => this._requestEdit(object), 1000, true);\n      const objDom = (\n        <div\n          key={object.id}\n          onClick={reqEdit}\n          title={`Edit ${objectType}`}\n          className={`cell-item sf-profile ${objectType}-profile sf-related-object`}\n        >\n          <div className=\"main-cell-wrap\">\n            <SalesforceIcon objectType={objectType} />\n            <span className=\"linkable-object-name\">{object.name}</span>\n            <OpenInSalesforceBtn objectId={object.id} />\n          </div>\n        </div>\n      )\n      objDoms.push(objDom)\n      if (objectType === \"Lead\") {\n        objDoms.push(this._renderConvertLead(object))\n      }\n    });\n    return objDoms;\n  }\n\n  _renderCreateObj(objType) {\n    const reqNew = () => this._requestNew(objType)\n    return (\n      <a\n        key={`create-${objType}`}\n        className={`create-${objType} create-sf-obj-link`}\n        onClick={reqNew}\n      >\n        Create {objType} from {this.props.contact.firstName()}\n      </a>\n    )\n  }\n\n  _renderConvertLead(lead) {\n    if (this.state.contacts.length > 0) { return false; }\n    const convert = _.debounce(() =>\n        this._requestNew(\"Contact\", {\n          Name: lead.name,\n          Email: lead.identifier,\n        }), 1000, true);\n    return (\n      <div\n        className=\"cell-item action-item\"\n        title=\"Convert lead to contact\"\n        onClick={convert}\n      >\n        <SalesforceIcon objectType=\"lead_convert\" className=\"round\" />\n        <span>Convert Lead to Contact</span>\n      </div>\n    )\n  }\n\n  render() {\n    if (!this.props.contact) return false;\n    if (this.props.contact.isMe()) return false;\n    let h2 = false;\n    if (this._hasRelatedObjects()) {\n      h2 = <h2 className=\"sidebar-h2\">Salesforce</h2>\n    }\n    return (\n      <div className=\"salesforce-contact-info salesforce\">\n        {h2}\n        {this._renderRelatedObjects()}\n        {this._renderObjectCreators()}\n      </div>\n    )\n  }\n}\n\nexport default SalesforceContactInfo\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/fetch-empty-schema-for-type.es6",
    "content": "import {DatabaseStore} from 'nylas-exports';\nimport SalesforceAPI from '../salesforce-api'\nimport SalesforceSchema from '../models/salesforce-schema';\nimport SalesforceActions from '../salesforce-actions';\nimport SalesforceObjectPicker from './salesforce-object-picker'\nimport SalesforceSchemaAdapter from './salesforce-schema-adapter';\n\n/**\n * Given a Salesforce object type, we resolve a GeneratedForm schema.\n */\nclass FetchEmptySchemaForType {\n  run(objectType) {\n    return Promise.resolve(objectType)\n    .then(this._loadSchemaFromDatabase)\n    .then(this._addCustomFormTypes)\n    .then(this._verifySchemaValidity)\n    .then(({genFormSchema, isValid}) => {\n      if (isValid) return genFormSchema;\n      return Promise.resolve(objectType)\n      .then(this._describeLayouts)\n      .then(this._fetchDefaultLayout)\n      .then(SalesforceSchemaAdapter.convertFullEditLayout.bind(SalesforceSchemaAdapter))\n      .then(this._saveGenFormSchema)\n    })\n    // We allow all errors to propagate up so they can be caught by the\n    // caller and displayed to the user.\n  }\n\n  _loadSchemaFromDatabase = (objectType) => {\n    return DatabaseStore.findBy(SalesforceSchema, {objectType})\n    .order(SalesforceSchema.attributes.createdAt.descending())\n    .limit(1)\n  }\n\n  _addCustomFormTypes(formSchema = {}) {\n    const fieldsets = formSchema.fieldsets || []\n    for (const fieldset of fieldsets) {\n      const formItems = fieldset.formItems || []\n      for (const formItem of formItems) {\n        if (formItem.type === \"reference\") {\n          formItem.customComponent = SalesforceObjectPicker\n        }\n      }\n    }\n    return formSchema\n  }\n\n  _verifySchemaValidity = (genFormSchema = {}) => {\n    if (!(genFormSchema instanceof SalesforceSchema)) {\n      return {genFormSchema, isValid: false}\n    }\n    const noData = (genFormSchema.fieldsets || []).length === 0;\n    const fieldError = this._hasInvalidFields(genFormSchema);\n\n    if (noData || fieldError) {\n      console.warn(\"The schema in the DB is malformed!\", genFormSchema, {noData, fieldError});\n      return DatabaseStore.inTransaction(t => t.unpersistModel(genFormSchema))\n      .then(() => {\n        return {genFormSchema, isValid: false}\n      })\n    }\n    return {genFormSchema, isValid: true}\n  }\n\n  _hasInvalidFields = (genFormSchema) => {\n    const fieldsets = genFormSchema.fieldsets || []\n    if (fieldsets.length === 0) return \"no fieldsets\";\n    for (const fieldset of fieldsets) {\n      if (!fieldset.id) return \"no fieldset id\";\n      const formItems = fieldset.formItems || []\n      if (formItems.length === 0) return \"empty form items\";\n\n      for (const formItem of formItems) {\n        if (!formItem.id) return \"formItem with no Id\";\n\n        if (formItem.type !== \"EmptySpace\" && !formItem.name) {\n          return \"formItem has no name\";\n        }\n\n        if (formItem.type === \"reference\") {\n          /**\n           * We enfore the Id format since we use that format to\n           * pre-populate fields from existing objects in the\n           * SalesforceObjectPicker.\n           */\n          if (formItem.referenceTo.length === 0) return \"empty referenceTo\";\n          if (formItem.referenceType === \"hasMany\") {\n            if (!/.+Ids$/.test(formItem.name)) {\n              return `Invalid hasMany name: ${formItem.name}`\n            }\n          } else if (formItem.referenceType === \"hasManyThrough\") {\n            if (!/.+Ids$/.test(formItem.name)) {\n              return `Invalid hasManyThrough name: ${formItem.name}`\n            }\n            if (!formItem.referenceThrough) return \"No referenceThough\";\n            if (!formItem.referenceThroughSelfKey) return \"No SelfKey\";\n            if (!formItem.referenceThroughForeignKey) return \"No ForeignKey\";\n          } else {\n            if (!/.+Id$/.test(formItem.name)) {\n              return `Invalid belongsTo name: ${formItem.name}`\n            }\n          }\n        }\n      }\n    }\n    return false;\n  }\n\n  _describeLayouts = (objectType) => {\n    return SalesforceAPI.makeRequest({\n      path: `/sobjects/${objectType}/describe/layouts`,\n    }).then((layoutDescription) => {\n      return {layoutDescription, objectType}\n    })\n  }\n\n  // The /describe endpoint returns a list of `recordTypeMappings` that\n  // may include one or more layouts. In many cases there will only be 1\n  // layout and 1 default to choose from. We can immediately return the\n  // layout in this case.\n  //\n  // In other cases we will need to separately fetch the raw layout from\n  // the API\n  _fetchDefaultLayout = ({layoutDescription, objectType}) => {\n    try {\n      const rawLayout = SalesforceSchemaAdapter.defaultLayout(layoutDescription);\n      if (rawLayout) return {rawLayout, objectType}\n\n      const path = SalesforceSchemaAdapter.pathForDefaultLayout(layoutDescription)\n\n      return SalesforceAPI.makeRequest({path: path})\n      .then((rl) => { return {rawLayout: rl, objectType} })\n    } catch (error) {\n      error.reportedToSentry = true;\n      SalesforceActions.reportError(error, {objectType, layoutDescription});\n      throw error;\n    }\n  }\n\n  _saveGenFormSchema = (genFormSchemaJSON) => {\n    const genFormSchema = new SalesforceSchema(genFormSchemaJSON);\n    const schema = this._addCustomFormTypes(genFormSchema)\n    return DatabaseStore.inTransaction(t => {\n      return t.persistModel(schema).then(() => schema);\n    });\n  }\n}\nexport default new FetchEmptySchemaForType()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/form-data-helpers.es6",
    "content": "import _ from 'underscore';\nimport {Utils} from 'nylas-exports'\nimport SalesforceObject from '../models/salesforce-object'\nimport PendingSalesforceObject from './pending-salesforce-object'\n\nexport function formItemEach(formData, eachFn) {\n  if (!_.isFunction(eachFn)) { return; }\n  const fieldsets = formData.fieldsets || []\n  for (const fieldset of fieldsets) {\n    const formItems = fieldset.formItems || []\n    for (const formItem of formItems) {\n      eachFn(formItem);\n    }\n  }\n}\n\n/**\n * Many forms will want to initialize new forms. When we do this we need\n * to pass along serialized data of the current form state to a new\n * window. Form values can be full of PendingSalesforceObjects and\n * SalesforceObjects that we'll need to serialize properly.\n */\nexport function serializeRawFormValue(value) {\n  if (value === null || value === undefined) return value;\n  if (typeof value === \"string\") return value;\n  if (value instanceof SalesforceObject) return value.id\n  if (value instanceof PendingSalesforceObject) return value.toJSON();\n  if (value.pendingSalesforceObject) return value;\n  if (value.id) return value.id;\n  if (_.isArray(value)) return _.compact(value.map(serializeRawFormValue))\n  return value\n}\n\n/**\n * A Salesforce REQUIRED_FIELD_MISSING API error has the following\n * schema:\n *\n * rawError = [\n *   {\n *     errorCode: \"REQUIRED_FIELD_MISSING\",\n *     fields: [\"AccountId\", \"LastName\"]\n *   }\n * ]\n */\nexport function validateForm(formData) {\n  const validationErrors = {}\n  let valid = true;\n  formItemEach(formData, (formItem) => {\n    if (formItem.required &&\n        ((formItem.value === null || formItem.value === undefined) ||\n         formItem.value.length === 0)) {\n      valid = false;\n      validationErrors[formItem.id] = {\n        id: formItem.id,\n        message: \"This is a required field\",\n      }\n    }\n  })\n  if (valid) return Promise.resolve();\n  return Promise.reject(validationErrors)\n}\n\n\n/**\n * A frontend form validation error has the following schema:\n *\n * validationErrors = {\n *   \"local-123\": {\n *     id: \"local-123\",\n *     message: \"This is a required field\",\n *   }\n *   \"some-form-item-id\": {\n *     id: \"some-form-item-id\",\n *     message: \"Some error message\",\n *   }\n * }\n */\nexport function formDataWithValidationErrors(_formData, validationErrors = {}) {\n  const formData = Utils.deepClone(_formData);\n  formData.errors.formItemErrors = validationErrors;\n  return formData\n}\n\nexport function cloneFormWithoutErrors(formData) {\n  const newFormData = Utils.deepClone(formData);\n  newFormData.errors = {};\n  return newFormData;\n}\n\nexport function mergeSalesforceError(formData, error) {\n  if (error.errorCode !== \"REQUIRED_FIELD_MISSING\") {\n    return formData;\n  }\n  formData.errors.formItemErrors = {};\n  formItemEach(formData, (formItem) => {\n    if (!error.fields.includes(formItem.name)) return;\n    formData.errors.formItemErrors[formItem.id] = {\n      id: formItem.id,\n      message: \"This is a required field\",\n    };\n  })\n  return formData\n}\n\n// Merges errors with formData and returns a new shallow of formData\n// See the generated form error data schema in:\n// src/components/generated-form\nexport function formDataWithAPIErrors(_formData, error = {}) {\n  let formData = Utils.deepClone(_formData);\n  // Came from Edgehill API\n  if (error.errorCode === \"REQUIRED_FIELD_MISSING\") {\n    formData.errors = {};\n    formData = mergeSalesforceError(formData, error.body[0]);\n    return formData;\n  }\n  const msg = error.message || \"Unknown error with the Salesforce API\"\n  if (error.name === \"APIError\") {\n    formData.errors = {\n      formError: { message: msg },\n      formItemErrors: {},\n    };\n  } else {\n    console.log(\"An unexpected error occurred\", error);\n    formData.errors = {\n      formError: { message: (error.message || \"An unexpected error occurred\") },\n      formItemErrors: {},\n    };\n  }\n  return formData;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/generated-form-to-salesforce-adapter.es6",
    "content": "/**\n * Converts the schema of a GeneratedForm to a data format that the\n * Salesforce object creation API understands\n *\n * https://www.salesforce.com/us/developer/docs/api_rest/\n * See:\n *   Using REST Resources > Using REST API Resources > Working with Records\n *   > Create a Record\n */\nimport _ from \"underscore\"\nimport SalesforceObject from '../models/salesforce-object'\n\nclass GeneratedFormToSalesforceAdapter {\n\n  static extract(formData) {\n    const relatedObjectsData = {};\n    const formPostData = {};\n    const fieldSets = formData.fieldsets || []\n\n    fieldSets.forEach((fieldset) => {\n      const formItems = fieldset.formItems || []\n      formItems.forEach((formItem) => {\n        if (formItem.type === \"EmptySpace\") { return; }\n\n        if (!formItem.name) {\n          console.error(formItem);\n          throw new Error(\"This formItem doesnt have a name\");\n        }\n\n        if (formItem.type === \"reference\") {\n          if (_.isString(formItem.value)) {\n            console.error(formItem);\n            throw new Error(\"Invalid value for reference type\")\n          }\n\n          const objIds = (formItem.value || []).filter((obj) => {\n            return obj instanceof SalesforceObject\n          }).map(obj => obj.id)\n\n          if (formItem.referenceType === \"hasMany\") {\n            relatedObjectsData[formItem.name] = objIds;\n          } else if (formItem.referenceType === \"hasManyThrough\") {\n            if (!formItem.referenceThrough) {\n              console.error(formItem);\n              throw new Error(\"Must specify referenceThrough\")\n            }\n            relatedObjectsData[formItem.name] = objIds;\n          } else {\n            // This is a standards Salesforce \"reference\" type. In the\n            // Nylas language it is a \"belongsTo\" `referenceType`\n            let value = objIds[0]\n            if (value === null || value === undefined) value = \"\";\n            formPostData[formItem.name] = value;\n          }\n        } else {\n          formPostData[formItem.name] = formItem.value;\n        }\n      })\n    })\n\n    return {\n      formPostData,\n      relatedObjectsData,\n    };\n  }\n}\n\nexport default GeneratedFormToSalesforceAdapter\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/pending-salesforce-object.es6",
    "content": "import {Utils} from 'nylas-exports'\n\n/*\nThere are many places we want to have a SalesforceObject for which we\ndon't yet have the full data.\n\nAn example is when we're created a linked SalesforceObject in a\nSalesforceForm. It may take a user a long time to create that object. In\nthe meantime, we stub in a PendingSalesforceObject to indicate that such\nan activity is in progress.\n*/\n\nexport default class PendingSalesforceObject {\n  constructor({id, type, name}) {\n    this.id = id || Utils.generateTempId();\n    this.type = type;\n    this.name = name;\n  }\n\n  toJSON() {\n    return {\n      id: this.id,\n      type: this.type,\n      name: this.name,\n      pendingSalesforceObject: true,\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/remove-controls.jsx",
    "content": "import React from 'react'\nimport _str from 'underscore.string'\nimport {Actions} from 'nylas-exports'\nimport OpenInSalesforceBtn from '../shared-components/open-in-salesforce-btn'\nimport DestroySalesforceObjectTask from '../tasks/destroy-salesforce-object-task'\n\nexport default class RemoveControls extends React.Component {\n  static propTypes = {\n    objectId: React.PropTypes.string,\n    objectType: React.PropTypes.string,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {confirmDelete: false}\n  }\n\n  _renderOpenInSalesforce() {\n    return [\n      <OpenInSalesforceBtn objectId={this.props.objectId} />,\n      <span>&nbsp;&nbsp;|&nbsp;&nbsp;</span>,\n    ]\n  }\n\n  _deleteObject = () => {\n    Actions.recordUserEvent(\"Salesforce Object Delete Submitted\", {\n      sObjectId: this.props.objectId,\n      sObjectType: this.props.objectType,\n    });\n    const task = new DestroySalesforceObjectTask({\n      sObjectId: this.props.objectId,\n      sObjectType: this.props.objectType,\n    })\n    Actions.queueTask(task);\n    setTimeout(() => { NylasEnv.close() }, 20)\n  }\n\n  render() {\n    const confirm = () => this.setState({confirmDelete: true});\n    const cancel = () => this.setState({confirmDelete: false});\n\n    let confirmControl\n    let confirmControlClass = \"\"\n    if (this.state.confirmDelete) {\n      confirmControlClass = \"confirm-control\"\n      confirmControl = (<span>\n        Are you sure? This will permanently delete on force.com.\n        <br />\n        <a onClick={this._deleteObject}>Yes delete</a>\n        &nbsp;&nbsp;|&nbsp;&nbsp;\n        <a onClick={cancel}>No cancel</a>\n      </span>)\n    } else {\n      const objectName = _str.titleize(_str.humanize(this.props.objectType))\n      confirmControl = (\n        <span>\n          <a onClick={confirm}>Delete {objectName}</a>\n        </span>\n      )\n    }\n    return (\n      <div className={`salesforce-delete-object ${confirmControlClass}`}>\n        {this._renderOpenInSalesforce()}\n        {confirmControl}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/salesforce-object-form.jsx",
    "content": "import React from 'react'\nimport _ from 'underscore'\nimport _str from 'underscore.string'\nimport {Utils, Actions} from 'nylas-exports'\nimport {Spinner, GeneratedForm} from 'nylas-component-kit'\n\nimport SmartFields from './smart-fields';\nimport RemoveControls from './remove-controls'\nimport * as dataHelpers from '../salesforce-object-helpers'\nimport SalesforceActions from '../salesforce-actions'\nimport * as formDataHelpers from './form-data-helpers'\nimport SalesforceObjectPicker from './salesforce-object-picker'\nimport FetchEmptySchemaForType from './fetch-empty-schema-for-type'\nimport SyncbackSalesforceObjectTask from '../tasks/syncback-salesforce-object-task';\nimport GeneratedFormToSalesforceAdapter from './generated-form-to-salesforce-adapter';\n\nclass SalesforceObjectForm extends React.Component {\n  static displayName = \"SalesforceObjectForm\"\n  static containerRequired = false\n\n  static propTypes = {\n    /**\n     * The Salesforce Object ID. This is only given if we're editing an\n     * existing object. If it's blank that means we're creating a new\n     * object.\n     */\n    objectId: React.PropTypes.string,\n\n    // The type of Salesforce Object\n    objectType: React.PropTypes.string.isRequired,\n\n    /**\n     * When the object form is created, it's passed in contextData. We use\n     * this data to help us intelligently fill out the form. We also pass\n     * the contextData onto the form so any downstream objects get created\n     * accordingly.\n     */\n    contextData: SalesforceObjectPicker.propTypes.contextData,\n\n    // Any initial data we get passed when creating this object.\n    objectInitialData: React.PropTypes.object,\n  }\n\n  static defaultProps = {\n    contextData: {},\n    objectInitialData: {},\n  }\n\n  constructor(props) {\n    super(props);\n    this.formId = props.contextData.formId || Utils.generateTempId()\n    this.state = {\n      formData: null,\n      submitting: false,\n      formLoadingErrorMsg: null,\n    }\n  }\n\n  componentWillMount() {\n    this._usubs = [\n      SalesforceActions.deleteSuccess.listen(this._onDeleteSuccess),\n      SalesforceActions.syncbackFailed.listen(this._onSyncbackFailed),\n      SalesforceActions.syncbackSuccess.listen(this._onSyncbackSuccess),\n    ]\n\n    NylasEnv.onBeforeUnload(this._onBeforeUnload);\n\n    this._initializeNewFormData().then(formData => {\n      this.setState({formData})\n    })\n  }\n\n  componentWillUnmount() {\n    for (const usub of this._usubs) { usub() }\n    return NylasEnv.removeUnloadCallback(this._onBeforeUnload);\n  }\n\n  _initializeNewFormData() {\n    return FetchEmptySchemaForType.run(this.props.objectType)\n    .then((emptySchema) => {\n      const initialSchema = this._addContextData(emptySchema)\n      return Promise.props({\n        initialSchema: initialSchema,\n        objectInitialData: this._initialData(),\n      }).then(SmartFields.fillForm)\n    })\n    .catch(this._handleFormLoadingErrors);\n  }\n\n  _addContextData(emptySchema) {\n    return Object.assign({}, emptySchema, {\n      formType: this.props.objectId ? \"update\" : \"new\",\n      contextData: Object.assign({}, this.props.contextData, {\n        formId: this.formId,\n        objectId: this.props.objectId,\n        objectType: this.props.objectType,\n      }),\n    })\n  }\n\n  _initialData = () => {\n    if (!this.props.objectId) return this.props.objectInitialData;\n    return this._loadFullObject().then(object => {\n      return Object.assign({}, this.props.objectInitialData,\n          (object.rawData || {}))\n    })\n  }\n\n  _loadFullObject = () => {\n    return dataHelpers.loadFullObject(this.props).then(object => {\n      if (!object) {\n        const err = new Error();\n        err.formErrorMessage = `The ${this._objectName()} you attempted to access with ID ${this.props.objectId} has been deleted. The user who deleted this record may be able to recover it from the Salesforce.com Recycle Bin. Deleted data is stored in the Recycle Bin for 15 days.`\n        throw err\n      }\n      return object\n    })\n  }\n\n  _handleFormLoadingErrors = (error) => {\n    let msg = `Unable to load the form for ${this._objectName()}`\n    if (error.formErrorMessage) msg = error.formErrorMessage;\n    if (!error.reportedToSentry) {\n      SalesforceActions.reportError(error, this.props);\n    }\n    return this.setState({formLoadingErrorMsg: msg});\n  }\n\n  _onSubmit = () => {\n    const formData = formDataHelpers.cloneFormWithoutErrors(this.state.formData);\n    this.setState({submitting: true, formData: formData})\n    const {formPostData, relatedObjectsData} = GeneratedFormToSalesforceAdapter.extract(formData);\n\n    this._submittedName = formPostData.Name || formPostData.Email\n    this._action = this.props.objectId ? \"Edit\" : \"Create\";\n    Actions.recordUserEvent(`Salesforce Object ${this._action} Submitted`, {\n      sObjectId: this.props.objectId,\n      sObjectType: this.props.objectType,\n      sObjectName: this._submittedName,\n    });\n\n    formDataHelpers.validateForm(formData).then(() => {\n      const t = new SyncbackSalesforceObjectTask({\n        objectId: this.props.objectId,\n        objectType: this.props.objectType,\n        contextData: formData.contextData,\n        formPostData: formPostData,\n        relatedObjectsData: relatedObjectsData,\n      });\n      Actions.queueTask(t);\n    }).catch((validationErrors = {}) => {\n      const newData = formDataHelpers.formDataWithValidationErrors(formData, validationErrors);\n      Actions.recordUserEvent(`Salesforce Object ${this._action} Errored`, {\n        errorType: \"LocalValidationError\",\n        errorCode: \"LOCAL_FORM_VALIDATION_ERROR\",\n        errorMessage: this._localErrorsForAnalytics(validationErrors),\n        sObjectId: this.props.objectId,\n        sObjectType: this.props.objectType,\n        sObjectName: this._submittedName,\n      });\n      this.setState({formData: newData, submitting: false})\n      // Don't rethrow\n    })\n  }\n\n  _localErrorsForAnalytics(validationErrors = {}) {\n    return _.uniq(_.values(validationErrors).map(({message}) => message))\n    .sort().join(\", \")\n  }\n\n  _remoteErrorsForAnalytics(apiError = {}) {\n    const msg = (apiError.body || [])[0] || apiError.message\n    return [`${apiError.errorCode}: ${msg}`]\n  }\n\n  _onDeleteSuccess = ({objectId}) => {\n    if (this.props.objectId === objectId) { NylasEnv.close() }\n  }\n\n  _onSyncbackFailed = ({contextData, error}) => {\n    if (contextData.formId !== this.formId) return;\n    if (!this.state.formData) return;\n    Actions.recordUserEvent(`Salesforce Object ${this._action} Errored`, {\n      errorType: error.constructor.name,\n      errorCode: error.errorCode,\n      errorMessage: error.message,\n      sObjectId: this.props.objectId,\n      sObjectType: this.props.objectType,\n      sObjectName: this._submittedName,\n    });\n    this.setState({\n      submitting: false,\n      formData: formDataHelpers.formDataWithAPIErrors(this.state.formData, error),\n    })\n  }\n\n  _onSyncbackSuccess = ({contextData} = {}) => {\n    if (contextData.formId !== this.formId) return;\n    this._closingDueToObjectSuccess = true;\n    Actions.recordUserEvent(`Salesforce Object ${this._action} Succeeded`, {\n      sObjectId: this.props.objectId,\n      sObjectType: this.props.objectType,\n      sObjectName: this._submittedName,\n    });\n    setTimeout(() => { NylasEnv.close(); }, 20)\n  }\n\n  _onBeforeUnload = () => {\n    SalesforceActions.salesforceWindowClosing({\n      contextData: this.state.formData.contextData,\n      closingDueToObjectSuccess: this._closingDueToObjectSuccess,\n    });\n    return true;\n  }\n\n  _objectName() {\n    return _str.titleize(_str.humanize(this.props.objectType))\n  }\n\n  render() {\n    if (!this.state.formData) {\n      return (\n        <div className=\"salesforce-object-form\">\n          <Spinner visible withCover />\n        </div>\n      )\n    }\n\n    if (this.state.formLoadingErrorMsg) {\n      return (\n        <div className=\"salesforce-object-form schema-error\">\n          {this.state.formLoadingErrorMsg}\n        </div>\n      )\n    }\n\n    return (\n      <div className=\"salesforce-object-form-wrap\">\n        <div className=\"salesforce-object-form\">\n          <RemoveControls\n            objectId={this.props.objectId}\n            objectType={this.props.objectType}\n          />\n          <GeneratedForm\n            {...this.state.formData}\n            style={{zIndex: 0}}\n            onSubmit={this._onSubmit}\n            onChange={formData => this.setState({formData})}\n          />\n          <Spinner visible={this.state.submitting} withCover />\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default SalesforceObjectForm\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/salesforce-object-picker.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport titleize from 'underscore.string/titleize'\nimport {Actions, Utils, DatabaseStore} from 'nylas-exports'\nimport {FormItem, BoldedSearchResult, TokenizingTextField} from 'nylas-component-kit'\n\nimport SalesforceIcon from '../shared-components/salesforce-icon'\nimport SalesforceObject from '../models/salesforce-object'\nimport {loadBasicObject} from '../salesforce-object-helpers';\nimport SalesforceActions from '../salesforce-actions'\nimport PendingSalesforceObject from './pending-salesforce-object'\n\nimport * as formDataHelpers from './form-data-helpers'\n\nconst MAX_RESULTS = 100;\n\n/*\nThis creates a selectable dropdown that lets you choose and create new\nSalesforce objects.\n\nIt behaves like a standard formItem with a `value` prop and `onChange`. The value is always an array of SalesforceObject or PendingSalesforceObjects\n*/\nclass SalesforceObjectPicker extends React.Component {\n  static displayName = \"SalesforceObjectPicker\"\n\n  static extendedPropTypes = {\n    // Zero or more SalesforceObject's or PendingSalesforceObjects to turn\n    // into `tokens` in the TokenizingTextField\n    value: React.PropTypes.arrayOf(\n      React.PropTypes.oneOfType([\n        React.PropTypes.instanceOf(SalesforceObject),\n        React.PropTypes.instanceOf(PendingSalesforceObject),\n      ])\n    ),\n\n    /**\n     * This is extra environment data about the form we're creating. It\n     * includes data about the thread that we're creating this object in\n     * context with, or with extra contact details used to help enrich the\n     * form.\n     */\n    contextData: React.PropTypes.shape({\n      /**\n       * The unique ID of the form the picker is in. This is used when\n       * creating \"PendingSalesforceObject\" that are linked to forms. When\n       * those forms close, we can know what PendingSalesforceObjects to\n       * remove.\n       */\n      formId: React.PropTypes.string,\n\n      /**\n       * The Salesforce object this form is about.\n       * If the objectId is present, then we're editing an existing\n       * object.\n       */\n      objectId: React.PropTypes.string,\n      objectType: React.PropTypes.string,\n\n      /**\n       * The thread (or threads) that were selected when this object form\n       * was requested. We use this to pre-fill contact form fields and\n       * for analytics.\n       */\n      nylasObjectId: React.PropTypes.string,\n      nylasObjectIds: React.PropTypes.array,\n      nylasObjectType: React.PropTypes.string,\n\n      /**\n       * Used by SmartFields to pre-fill in data given the contact that\n       * was focused when the object form was requested.\n       */\n      focusedNylasContactData: React.PropTypes.object,\n    }),\n  }\n\n  static propTypes = Object.assign({},\n      FormItem.propTypes, SalesforceObjectPicker.extendedPropTypes);\n\n  static defaultProps = {\n    value: [], // Does not protect if parent sets this to `null`\n    onChange: () => {},\n    contextData: {\n      nylasObjectId: null,\n      nylasObjectIds: [],\n      nylasObjectType: null,\n      focusedNylasContactData: null,\n    },\n  }\n\n  componentWillMount() {\n    this._usubs = [\n      SalesforceActions.salesforceWindowClosing.listen(this._onSalesforceWindowClosing),\n      SalesforceActions.syncbackSuccess.listen(this._onSyncbackSuccess),\n    ]\n  }\n\n  componentWillUnmount() {\n    for (const usub of this._usubs) { usub() }\n  }\n\n  focus() {\n    this.refs.tokenizingTextField.focus()\n  }\n\n  _tokens() {\n    return this.props.value || []\n  }\n\n  _onSyncbackSuccess = ({objectType, objectId, contextData = {}} = {}) => {\n    return loadBasicObject(objectType, objectId)\n    .then((sObject) => {\n      this.props.onChange(this._tokens().map((o) => {\n        if (o.id === contextData.formId) return sObject;\n        return o;\n      }));\n    });\n  }\n\n  _onSalesforceWindowClosing = (args) => {\n    if (args.closingDueToObjectSuccess) { return; }\n    this.props.onChange(this._tokens().filter((o) => {\n      return (o.id !== args.contextData.formId)\n    }));\n  }\n\n  // Returns a salesforce object given the input\n  _lookupSalesforceObject = (input = \"\", {clear} = {}) => {\n    return new Promise((resolve, reject) => {\n      let referenceTo = this.props.referenceTo;\n      if (_.isString(this.props.referenceTo)) {\n        referenceTo = [this.props.referenceTo]\n      }\n      if (clear) return resolve([])\n      if (input.length > 0) {\n        return DatabaseStore.findAll(SalesforceObject,\n            {type: referenceTo})\n        .where([SalesforceObject.attributes.name.like(input)])\n        .then((objects = []) => {\n          const re = Utils.wordSearchRegExp(input);\n          const inputLower = input.toLowerCase()\n\n          const sortedObjs = objects.sort((o1, o2) => {\n            const o1Name = o1.name.toLowerCase()\n            const o2Name = o2.name.toLowerCase()\n            const i1 = re.test(o1.name) ? o1Name.search(inputLower) : 999\n            const i2 = re.test(o2.name) ? o2Name.search(inputLower) : 999\n            return i1 - i2\n          })\n\n          for (const referenceType of referenceTo) {\n            /**\n             * Note that we do NOT set an id for these objects. They will\n             * be assigned random IDs that we'll use to set the formIDs of\n             * the downstream forms that each of these represents.\n             */\n            const obj = new PendingSalesforceObject({\n              type: referenceType,\n              name: input,\n            })\n            sortedObjs.push(obj)\n          }\n          return resolve(sortedObjs.slice(0, MAX_RESULTS))\n        })\n        .catch(reject)\n      }\n      return resolve([])\n    })\n  }\n\n  // An autocomplete suggestion item\n  _renderObjectSuggestion = (obj, {inputValue} = {}) => {\n    if (obj instanceof PendingSalesforceObject) {\n      return (\n        <div className=\"salesforce-suggestion new-object\">\n          <SalesforceIcon\n            objectType={obj.type}\n            className=\"round-create\"\n          />\n          Create new {titleize(obj.type)} &ldquo;{obj.name}&rdquo;\n        </div>\n      )\n    }\n    return (\n      <div className=\"salesforce-suggestion\" title={obj.name}>\n        <SalesforceIcon objectType={obj.type} />\n        <BoldedSearchResult query={inputValue} value={obj.name} />\n      </div>\n    )\n  }\n\n  // Called with either a found object or a new value\n  _onTokensAdd = (objs = []) => {\n    objs.filter(o => o instanceof PendingSalesforceObject)\n    .forEach(this._createNew)\n\n    this.props.onChange(this._tokens().concat(objs))\n    Actions.closePopover()\n  }\n\n  _onEditMotion = (object) => {\n    if (!(object instanceof SalesforceObject)) return;\n    SalesforceActions.openObjectForm({\n      objectId: object.id,\n      objectType: object.type,\n      objectInitialData: object,\n      contextData: this.props.contextData,\n    })\n  }\n\n  _createNew = (pendingObj = {}) => {\n    if ((pendingObj.name || \"\").trim().length === 0) return\n\n    /**\n     * When we create a PendingSalesforceObject, that means we want to\n     * create a whole new form from that object. We use the\n     * PendingSalesforceObject's id as the formId of the newly generated\n     * form. The constructor of salesforce-object-form will detect the\n     * formId in the passed-in contextData and initialize with that ID. By\n     * letting us set the ID from here, we know what form to listen to\n     * when the downstream form closes or saves.\n     */\n    const contextData = Object.assign({}, this.props.contextData, {\n      formId: pendingObj.id,\n    })\n\n    SalesforceActions.openObjectForm({\n      objectType: pendingObj.type,\n      contextData: contextData,\n      objectInitialData: this._initialDataForNewObject(pendingObj),\n    });\n  }\n\n  /**\n   * When you're going to create a new object there is a lot of\n   * information we can give you a head start on that new object.\n   *\n   * First when you create a new object through the Salesforce Object\n   * Picker, we have the name you just typed.\n   *\n   * Second, the GeneratedForm also passes to each formItem (including\n   * this one) the currentFormValues. We pass those along as initial data.\n   * If we're editing a Contact and we create a new Opportunity, the\n   * Opportunity will want the same AccountId as the Contact's AccountID.\n   * By passing along the currentFormValues, we can pre-fill the\n   * Opportunity with what we have already.\n   *\n   * Third, we create a backRefObj to the current form you have open. If\n   * we're creating a brand new Contact, and also start creating an\n   * Account, the Account form can have a back reference to the in-flight\n   * Contact we're creating or the existing Contact we already have.\n   *\n   * Fourth, we pass along all additional contextData. That contextData\n   * includes the Nylas Thread & Contact in scope when we create this\n   * object. Our SmartFields adapter will use that information to query\n   * Clearbit and other data sources to fill in as much as possible. See\n   * SmartFields for a variety of other techniques we use to pre-fill the\n   * form.\n   */\n  _initialDataForNewObject = (pendingObj = {}) => {\n    const rawForm = this.props.currentFormValues || {}\n    const initialData = {}\n    for (const name of Object.keys(rawForm)) {\n      initialData[name] = formDataHelpers.serializeRawFormValue(rawForm[name])\n    }\n\n    initialData.Name = pendingObj.name;\n\n    const selfType = this.props.contextData.objectType;\n\n    let backRef = null\n    if (this.props.contextData.objectId) {\n      /**\n       * For existing objects, we just need the ID of the object. It will\n       * be re-inflated when the downstream form loads.\n       */\n      backRef = this.props.contextData.objectId\n    } else {\n      /**\n       * The id needs to be the formId of the object that created us.\n       * When we send this initialData to a new form,\n       * SmartFields._resolveInitialRefs will unpack the\n       * PendingSalesforceObject JSON and create the appropriate\n       * PendingSalesforceObject with the given ID.\n       */\n      backRef = new PendingSalesforceObject({\n        id: this.props.contextData.formId,\n        type: selfType,\n      }).toJSON()\n    }\n\n    let key = null\n    if (this.props.referenceType === \"hasManyThrough\") {\n      // Back-reference will be a hasManyThrough since this is a\n      // hasManyThrough\n      key = `${selfType}Ids`\n    } else if (this.props.referenceType === \"hasMany\") {\n      // Back-reference will be a belongsTo since this is a hasMany\n      key = `${selfType}Id`\n    } else {\n      // Back-reference will be a hasMany since this is a belongsTo\n      key = `${selfType}Ids`\n    }\n    if (!initialData[key]) initialData[key] = [];\n    initialData[key].push(backRef);\n    return initialData\n  }\n\n  // The found token object\n  _renderFoundObject = (props) => {\n    if (props.token instanceof SalesforceObject) {\n      return (\n        <div className=\"salesforce-object\">\n          <SalesforceIcon objectType={props.token.type} />\n          {props.token.name}\n        </div>\n      )\n    } else if (props.token instanceof PendingSalesforceObject) {\n      return (\n        <div className=\"salesforce-object token-pending\">\n          <SalesforceIcon objectType={props.token.type} pending />\n          Creating {props.token.type}…\n        </div>\n      )\n    }\n    return false\n  }\n\n  _onTokensRemoved = (objs = []) => {\n    const toRemoveIds = objs.map(o => o.id);\n    const val = this._tokens().filter(o => !toRemoveIds.includes(o.id));\n    this.props.onChange(val)\n  }\n\n  render() {\n    const objId = (obj) => obj.id\n    return (\n      <div className=\"salesforce-object-picker\">\n        <TokenizingTextField\n          ref=\"tokenizingTextField\"\n          onAdd={this._onTokensAdd}\n          tokens={this._tokens()}\n          onRemove={this._onTokensRemoved}\n          tokenKey={objId}\n          disabled={this.props.disabled}\n          tabIndex={this.props.tabIndex}\n          maxTokens={this.props.multiple ? null : 1}\n          placeholder={this.props.placeholder}\n          defaultValue={this.props.defaultValue}\n          onEditMotion={this._onEditMotion}\n          tokenRenderer={this._renderFoundObject}\n          completionNode={this._renderObjectSuggestion}\n          onRequestCompletions={this._lookupSalesforceObject}\n        />\n      </div>\n    )\n  }\n}\n\nexport default SalesforceObjectPicker\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/salesforce-schema-adapter.es6",
    "content": "import { Utils } from 'nylas-exports';\nimport _ from 'underscore';\nimport SalesforceActions from '../salesforce-actions'\n\n// Salesforce provides \"Layouts\", which are custom specifications on what\n// various object creation / edit forms look like. The data here includes\n// row & column information and fieldset data. Layouts are available at\n// the: /sobjects/{OBJECT_TYPE}/describe/layouts endpoint\n//\n// Salesforce also provides a separate schema that defines the total fields\n// of a particular object type. This is the ultimate truth on what the API\n// will and will not accept. It lists all fields of an object and crucially\n// indicates if it is `updateable` (aka user editable), and if it's\n// `nillable` (aka required). The set of fields are availabe at the:\n// /sobjects/{OBJECT_TYPE}/describe endpoint.\n//\n// We need to look at both the layout and the schema to determine the\n// proper way to display the form (via the layout) and what to mark as\n// required & editable (via the schema).\n//\n//\n// This class converts Schemas and Layouts into an object that\n// the GeneratedForm component can understand.\n//\n// A Salesforce /describe block (the schema) looks like (as of API v37 Sept\n// 2016):\n// rawData = {\n//   \"actionOverrides\": [],\n//   \"activateable\": false,\n//   \"childRelationships\": [],\n//   \"compactLayoutable\": true,\n//   \"createable\": true,\n//   \"custom\": false,\n//   \"customSetting\": false,\n//   \"deletable\": true,\n//   \"deprecatedAndHidden\": false,\n//   \"feedEnabled\": true,\n//   \"fields\": [\n//     {\n//       \"aggregatable\": true,\n//       \"autoNumber\": false,\n//       \"byteLength\": 18,\n//       \"calculated\": false,\n//       \"calculatedFormula\": null,\n//       \"cascadeDelete\": false,\n//       \"caseSensitive\": false,\n//       \"controllerName\": null,\n//       \"createable\": false,\n//       \"custom\": false,\n//       \"defaultValue\": null,\n//       \"defaultValueFormula\": null,\n//       \"defaultedOnCreate\": true,\n//       \"dependentPicklist\": false,\n//       \"deprecatedAndHidden\": false,\n//       \"digits\": 0,\n//       \"displayLocationInDecimal\": false,\n//       \"encrypted\": false,\n//       \"externalId\": false,\n//       \"extraTypeInfo\": null,\n//       \"filterable\": true,\n//       \"filteredLookupInfo\": null,\n//       \"groupable\": true,\n//       \"highScaleNumber\": false,\n//       \"htmlFormatted\": false,\n//       \"idLookup\": true,\n//       \"inlineHelpText\": null,\n//       \"label\": \"Lead ID\",\n//       \"length\": 18,\n//       \"mask\": null,\n//       \"maskType\": null,\n//       \"name\": \"Id\",\n//       \"nameField\": false,\n//       \"namePointing\": false,\n//       \"nillable\": false,\n//       \"permissionable\": false,\n//       \"picklistValues\": [],\n//       \"precision\": 0,\n//       \"queryByDistance\": false,\n//       \"referenceTargetField\": null,\n//       \"referenceTo\": [],\n//       \"relationshipName\": null,\n//       \"relationshipOrder\": null,\n//       \"restrictedDelete\": false,\n//       \"restrictedPicklist\": false,\n//       \"scale\": 0,\n//       \"soapType\": \"tns:ID\",\n//       \"sortable\": true,\n//       \"type\": \"id\",\n//       \"unique\": false,\n//       \"updateable\": false,\n//       \"writeRequiresMasterRead\": false\n//     }\n//     {}\n//     ...\n//   ],\n//   \"keyPrefix\": \"00Q\",\n//   \"label\": \"Lead\",\n//   \"labelPlural\": \"Leads\",\n//   \"layoutable\": true,\n//   \"listviewable\": null,\n//   \"lookupLayoutable\": null,\n//   \"mergeable\": true,\n//   \"mruEnabled\": true,\n//   \"name\": \"Lead\",\n//   \"namedLayoutInfos\": [],\n//   \"networkScopeFieldName\": null,\n//   \"queryable\": true,\n//   \"recordTypeInfos\": [],\n//   \"replicateable\": true,\n//   \"retrieveable\": true,\n//   \"searchLayoutable\": true,\n//   \"searchable\": true,\n//   \"supportedScopes\": [\n//     {\n//       \"label\": \"All leads\",\n//       \"name\": \"everything\"\n//     },\n//   ],\n//   \"triggerable\": true,\n//   \"undeletable\": true,\n//   \"updateable\": true,\n//   \"urls\": {}\n// }\n//\n// A Salesforce full layout looks like (as of API v37 Aug 2016):\n// rawData = {\n//   recordTypeMappings: [\n//     {\n//       \"available\": true,\n//       \"defaultRecordTypeMapping\": true,\n//       \"layoutId\": \"00h41000000TRMmAAO\",\n//       \"master\": false,\n//       \"name\": \"Master\",\n//       \"picklistsForRecordType\": [],\n//       \"recordTypeId\": \"01241000000Yg3MAAS\",\n//       \"urls\": {\n//         \"layout\": \"/services/data/v37.0/sobjects/Opportunity/describe/layouts/01241000000Yg3MAAS\"\n//       }\n//     },\n//   ],\n//   recordTypeSelectorRequired: []\n//   layouts: [\n//     { // layout\n//       id : \"00h41000000TRMtAAO\"\n//       buttonLayoutSection : {}\n//       detailLayoutSections : []\n//       feedView : null\n//       highlightsPanelLayoutSection : null\n//       multirowEditLayoutSections : []\n//       offlineLinks : []\n//       quickActionList : {}\n//       relatedContent : {}\n//       relatedLists : []\n//       editLayoutSections: [ // may be many layoutSections aka fieldsets\n//         { // layoutSection\n//           rows: 8\n//           columns: 2\n//           heading: \"Contact Information\"\n//           parentLayoutId: \"00h41000000TRMt\"\n//           tabOrder: \"TopToBottom\"\n//           useCollapsibleSection: false\n//           useHeading: true\n//           layoutRows: [\n//             { // layoutRow\n//               numItems: 2\n//               layoutItems: [\n//                 { // layoutItem\n//                   editableForNew: false\n//                   editableForUpdate: false\n//                   label: \"Contact Owner\"\n//                   placeholder: false\n//                   required: true\n//                   layoutComponents: [\n//                     { // layoutComponent\n//                       displayLines: 1\n//                       fieldType: \"string\"\n//                       tabOrder: 32\n//                       type: \"Field\"\n//                       value: \"Name\"\n//\n//                       components: [\n//                         {LAYOUT_COMPONENT}\n//                         ... a couple layoutComponent\n//                       ]\n//\n//                       details: {\n//                         aggregatable : true\n//                         autoNumber : false\n//                         byteLength : 18\n//                         calculated : false\n//                         calculatedFormula : null\n//                         cascadeDelete : false\n//                         caseSensitive : false\n//                         controllerName : null\n//                         createable : true\n//                         custom : false\n//                         defaultValue : null\n//                         defaultValueFormula : null\n//                         defaultedOnCreate : false\n//                         dependentPicklist : false\n//                         deprecatedAndHidden : false\n//                         digits : 0\n//                         displayLocationInDecimal : false\n//                         encrypted : false\n//                         externalId : false\n//                         extraTypeInfo : null\n//                         filterable : true\n//                         filteredLookupInfo : null\n//                         groupable : true\n//                         highScaleNumber : false\n//                         htmlFormatted : false\n//                         idLookup : false\n//                         inlineHelpText : null\n//                         label : \"Account ID\"\n//                         length : 18\n//                         mask : null\n//                         maskType : null\n//                         name : \"AccountId\"\n//                         nameField : false\n//                         namePointing : false\n//                         nillable : true\n//                         permissionable : true\n//                         picklistValues : [\n//                           {\n//                             active : true\n//                             defaultValue : false\n//                             label : \"Mr.\"\n//                             validFor : null\n//                             value : \"Mr.\"\n//                           }\n//                           ...\n//                         ]\n//                         precision : 0\n//                         queryByDistance : false\n//                         referenceTargetField : null\n//                         referenceTo : [\n//                           \"Account\"\n//                         ]\n//                         relationshipName : \"Account\"\n//                         relationshipOrder : null\n//                         restrictedDelete : false\n//                         restrictedPicklist : false\n//                         scale : 0\n//                         soapType : \"tns:ID\"\n//                         sortable : true\n//                         type : \"reference\"\n//                         unique : false\n//                         updateable : true\n//                         writeRequiresMasterRead : false\n//                       } // details\n//                     } // layoutComponent. Usually only 1 layoutComponent\n//                   ]\n//                 }\n//                 {LAYOUT_ITEM} // layoutItem. Exactly num of columns\n//               ]\n//             } // layoutRow\n//             ... many layoutRows\n//           ]\n//         } // layoutSection\n//         ... many layoutSections (aka fieldsets)\n//       ]\n//     } // layout. Usually only 1 layout\n//   ]\n// } // rawData\n//\n//\nexport default class SalesforceSchemaAdapter {\n\n  // The /describe endpoint actually returns a listing of available\n  // \"Record Layout\" objects. For a given Salesforce record (like an\n  // opportunity), there may be many different layouts (aka record\n  // layouts) exposed to many different types of users. SREs, Account\n  // Managers, and Marketers may have different fields to fill out for the\n  // same Opportunity.\n  //\n  // We first attempt to find the \"default\" layout for the user. This is\n  // annotated by the `defaultRecordTypeMapping` attribute of each of the\n  // record types in the `recordTypeMappings` field.\n  //\n  // If we can't find one, we fall back to the record mapping labeled as\n  // \"master\".\n  //\n  // Sometimes the layouts automatically come down with the describe\n  // block. If there are 3 or more record mappings, they need to be\n  // separately fetched. We detect this and return the url of the layout\n  // to fetch so we can asynchronously grab that later.\n  static defaultLayout(layoutDescription = {}) {\n    if (!_.isArray(layoutDescription.layouts)) return null;\n    if (layoutDescription.layouts.length === 1) {\n      return layoutDescription.layouts[0]\n    }\n    const defaultRecordType = this.defaultRecordType(layoutDescription);\n    const id = defaultRecordType.layoutId;\n    return _.findWhere(layoutDescription.layouts, {id: id})\n  }\n\n  static defaultRecordType(layoutDescription = {}) {\n    const recordTypes = layoutDescription.recordTypeMappings\n    if (!_.isArray(recordTypes)) {\n      throw new Error(\"Unsupported Salesforce layout: No Record Type mappings\")\n    }\n    let defaultRecordType = _.findWhere(recordTypes, {defaultRecordTypeMapping: true});\n\n    if (defaultRecordType) return defaultRecordType\n\n    defaultRecordType = _.findWhere(recordTypes, {master: true});\n\n    if (!defaultRecordType) {\n      throw new Error(\"Unsupported Salesforce layout: No default Record Type nor Master Record Type \")\n    }\n\n    return defaultRecordType\n  }\n\n  static pathForDefaultLayout(layoutDescription = {}) {\n    const recordType = this.defaultRecordType(layoutDescription) || {}\n    const path = (recordType.urls || {}).layout;\n    if (!path) {\n      throw new Error(\"Unsupported Salesforce layout: No url for default record type\")\n    }\n    return path.slice(path.search(\"/sobjects\"))\n  }\n\n  // As returned from the SObject Layouts endpoint\n  // https://www.salesforce.com/us/developer/docs/api_rest/\n  //\n  // See ../spec/fixtures/opportunity-layouts.json for an example schema\n  //\n  // See /src/components/generated-form.cjsx for the output schema\n  static convertFullEditLayout({objectType, rawLayout = {}}) {\n    try {\n      if ((rawLayout.editLayoutSections || []).length === 0) {\n        throw new Error(\"Unsupported Salesforce layout: No editLayoutSections\")\n      }\n\n      if (!rawLayout.id) {\n        throw new Error(\"Unsupported Salesforce layout: No layout Id\")\n      }\n\n      let fieldsets = rawLayout.editLayoutSections\n      fieldsets = fieldsets.map((layoutSection) => {\n        return this.normalizeFieldset(layoutSection);\n      });\n      fieldsets = this.addCustomFieldsets(objectType, fieldsets);\n\n      const genFormSchemaJSON = {\n        id: rawLayout.id,\n        schemaType: \"full\",\n        objectType,\n        fieldsets,\n        createdAt: new Date(),\n      };\n      return genFormSchemaJSON;\n    } catch (error) {\n      error.reportedToSentry = true;\n      SalesforceActions.reportError(error, {objectType, rawLayout});\n      throw error;\n    }\n  }\n\n  // A layoutSection (aka fieldset) needs to be normalized and flattened\n  // from the Salesforce schema\n  static normalizeFieldset(layoutSection = {}) {\n    // We flatten all layoutItems in all rows to a single array and record\n    // the row and column they're supposed to appear.\n    let normalizedLayoutItems = [];\n    const layoutRows = layoutSection.layoutRows || []\n    for (let rowIndex = 0; rowIndex < layoutRows.length; rowIndex++) {\n      const layoutRow = layoutRows[rowIndex]\n      const layoutItems = layoutRow.layoutItems || []\n      for (let colIndex = 0; colIndex < layoutItems.length; colIndex++) {\n        const layoutItem = layoutItems[colIndex];\n        layoutItem.row = rowIndex;\n        layoutItem.column = colIndex;\n        normalizedLayoutItems.push(layoutItem);\n      }\n    }\n\n    // Since some layoutItems contain one or more layoutComponents (e.g.\n    // the Mailing Address layoutItem has 5 layoutComponents for the\n    // Street, City, State, etc), we flatten them all out.\n    normalizedLayoutItems = normalizedLayoutItems.map(this.normalizeLayoutItem);\n    const flattenedLayoutItems = _.compact(_.flatten(normalizedLayoutItems));\n    const formItems = flattenedLayoutItems.map(this.layoutItemToFormItem.bind(this));\n\n    return {\n      id: Utils.generateTempId(),\n      rows: layoutSection.rows,\n      columns: layoutSection.columns,\n      heading: layoutSection.heading,\n      formItems: formItems,\n      useHeading: layoutSection.useHeading,\n    };\n  }\n\n  static normalizeLayoutItem(rawLayoutItem = {}) {\n    const layoutItem = _.clone(rawLayoutItem);\n    const layoutComponent = (layoutItem.layoutComponents || [])[0];\n    if (!layoutComponent) { return null; }\n    delete layoutItem.layoutComponents\n\n    const components = layoutComponent.components || []\n    if (components.length > 0) {\n      return components.map((_component = {}) => {\n        const component = _.extend({},\n          layoutItem, // NOTE: We want the 'label' of the layoutItem overridden\n          _component,\n          _component.details);\n        delete component.details;\n        return component;\n      });\n    }\n    const normalizedLayoutItem = _.extend({},\n      layoutComponent,\n      (layoutComponent.details || {}),\n      layoutItem); // NOTE: we want to use the 'label' of the layoutItem\n    delete normalizedLayoutItem.details;\n    return normalizedLayoutItem;\n  }\n\n  static layoutItemToFormItem(layoutItem = {}) {\n    return {\n      id: Utils.generateTempId(),\n      row: layoutItem.row || 0,\n      type: this.typeMap(layoutItem.type),\n      name: layoutItem.name, // can be null in the EmptySpace case.\n      label: layoutItem.label,\n      column: layoutItem.column,\n      length: layoutItem.length,\n      multiple: layoutItem.type === \"multipicklist\",\n      tabIndex: layoutItem.tabOrder || 0,\n      required: this._isRequired(layoutItem),\n      placeholder: this._placeholder(layoutItem),\n      referenceTo: this._referenceTo(layoutItem),\n      defaultValue: layoutItem.defaultValue, // Used in SmartFields\n      selectOptions: (layoutItem.picklistValues || []).map(this.picklistOptionToFormOption),\n      editableForNew: layoutItem.editableForNew,\n      editableForUpdate: layoutItem.editableForUpdate,\n      // Note the disabled field is calculated in the generatedForm via\n      // `editableForNew` and `editableForUpdate`\n    };\n  }\n\n  static picklistOptionToFormOption(picklistOption = {}) {\n    return {\n      label: picklistOption.label,\n      value: picklistOption.value,\n      validFor: picklistOption.validFor,\n      defaultValue: picklistOption.defaultValue,\n    };\n  }\n\n  static _isRequired(layoutItem) {\n    if (layoutItem.type === \"EmptySpace\") return false;\n\n    // It doesn't make sense to have a checkbox be required since when\n    // displayed it always deafults to \"false\". HTML forms erroneously bug\n    // you when a checkbox's value is null, when in reality users perceive\n    // that to simply be \"unchecked\" aka \"false\".\n    if (layoutItem.type === \"boolean\") return false;\n\n    return layoutItem.nillable === false\n  }\n\n  static _referenceTo(layoutItem) {\n    return layoutItem.referenceTo || [];\n  }\n\n  static _placeholder(layoutItem) {\n    if (layoutItem.type === \"reference\") {\n      let label = (layoutItem.label || \"\").toLowerCase();\n      if (label.slice(-3) === \" id\") { label = label.slice(0, -3); }\n      const a = label[0] === \"a\" || label[0] === \"e\" || label[0] === \"i\" || label[0] === \"o\" || label[0] === \"u\" ? \"an\" : \"a\";\n      return `Create or search for ${a} ${label}`;\n    }\n    return layoutItem.label;\n  }\n\n  static typeMap(type) {\n    const knownTypes = {\n      \"int\": \"number\",\n      \"phone\": \"tel\",\n      \"string\": \"text\",\n      \"address\": \"textarea\",\n      \"boolean\": \"checkbox\",\n      \"percent\": \"number\",\n      \"currency\": \"number\",\n      \"picklist\": \"select\",\n      \"textarea\": \"textarea\",\n      \"EmptySpace\": \"EmptySpace\",\n      \"multipicklist\": \"select\",\n    };\n    return knownTypes[type] != null ? knownTypes[type] : type;\n  }\n\n  static addCustomFieldsets(objectType, fieldsets = []) {\n    if (objectType === \"Contact\") {\n      fieldsets.unshift(this._opportunitiesInContacts());\n    } else if (objectType === \"Opportunity\") {\n      fieldsets.unshift(this._contactsInOpportunities());\n    } else if (objectType === \"Account\") {\n      fieldsets.unshift(this._contactsInAccounts());\n    }\n    return fieldsets;\n  }\n\n  // Contacts are linked to Opportunities through a trivial object called\n  // an OpportunityContactRole. Instead of popping up a whole new object\n  // creator, we provide a more user-friendly interface to pick an\n  // Opportunity through a standard picker and create the association\n  // object in the background for the users.\n  //\n  // Since any additional data will throw an error if fully submitted to\n  // the SalesforceAPI, we use the \"hasManyThrough\" `refereneType`\n  static _opportunitiesInContacts() {\n    return {\n      id: Utils.generateTempId(),\n      heading: \"Related Opportunities\",\n      useHeading: true,\n      formItems: [{\n        id: Utils.generateTempId(),\n        row: 0,\n        type: \"reference\",\n        name: \"OpportunityIds\",\n        label: \"Opportunities\",\n        column: 0,\n        tabIndex: 0,\n        required: false,\n        multiple: true,\n        placeholder: \"Create or search for opportunities\",\n        defaultValue: null,\n        referenceTo: [\"Opportunity\"],\n        referenceType: \"hasManyThrough\",\n        referenceThrough: \"OpportunityContactRole\",\n        referenceThroughSelfKey: \"identifier\",\n        referenceThroughForeignKey: \"relatedToId\",\n      }],\n    };\n  }\n\n  static _contactsInOpportunities() {\n    return {\n      id: Utils.generateTempId(),\n      heading: \"Contacts for Opportunity\",\n      useHeading: true,\n      formItems: [{\n        id: Utils.generateTempId(),\n        row: 0,\n        type: \"reference\",\n        name: \"ContactIds\",\n        label: \"Contacts\",\n        column: 0,\n        tabIndex: 0,\n        required: false,\n        multiple: true,\n        placeholder: \"Create or search for contacts\",\n        defaultValue: null,\n        referenceTo: [\"Contact\"],\n        referenceType: \"hasManyThrough\",\n        referenceThrough: \"OpportunityContactRole\",\n        referenceThroughSelfKey: \"relatedToId\",\n        referenceThroughForeignKey: \"identifier\",\n      }],\n    };\n  }\n\n  static _contactsInAccounts() {\n    return {\n      id: Utils.generateTempId(),\n      heading: \"Contacts for Account\",\n      useHeading: true,\n      formItems: [{\n        id: Utils.generateTempId(),\n        row: 0,\n        type: \"reference\",\n        name: \"ContactIds\",\n        label: \"Contacts\",\n        column: 0,\n        tabIndex: 0,\n        required: false,\n        multiple: true,\n        placeholder: \"Create or search for contacts\",\n        defaultValue: null,\n        referenceTo: [\"Contact\"],\n        referenceType: \"hasMany\",\n      }],\n    };\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/salesforce-window-launcher.es6",
    "content": "import _str from 'underscore.string'\nimport {remote} from 'electron'\nimport SalesforceActions from '../salesforce-actions'\n\nconst WINDOW_TYPE = \"SalesforceObjectForm\"\nconst WIN_WIDTH = 600\nconst WIN_HEIGHT = 800\n\nfunction isFormWindow(windowKey) {\n  const re = new RegExp(WINDOW_TYPE);\n  return re.test(windowKey)\n}\n\nfunction getScreenSize() {\n  return remote.screen.getPrimaryDisplay().workAreaSize\n}\n\nfunction findSpotOn(dir = \"right\") {\n  const defaultStart = dir === \"right\" ? 0 : 9999\n  let adjustedX = defaultStart;\n  let adjustedY = defaultStart;\n  const allWindowDimensions = NylasEnv.getAllWindowDimensions();\n\n  const testFn = dir === \"right\" ? Math.max : Math.min;\n\n  for (const windowKey of Object.keys(allWindowDimensions)) {\n    if (!isFormWindow(windowKey)) continue;\n    const dims = allWindowDimensions[windowKey];\n    const newX = dir === \"right\" ? dims.x + dims.width : dims.x - dims.width\n    adjustedX = testFn(adjustedX, newX);\n    adjustedY = testFn(adjustedY, dims.y);\n  }\n  return {adjustedX, adjustedY}\n}\n\nfunction calcBoundsForNextWindow() {\n  const {width, height} = getScreenSize();\n  const screenWidth = width\n  const screenHeight = height\n\n  // By default, center window in the screen.\n  let winY = Math.round((screenHeight / 2) - (WIN_HEIGHT / 2))\n  let winX = Math.round((screenWidth / 2) - (WIN_WIDTH / 2))\n\n  let {adjustedX, adjustedY} = findSpotOn('right');\n  if (adjustedX + WIN_WIDTH > screenWidth) {\n    const newDims = findSpotOn('left');\n    adjustedX = newDims.adjustedX\n    adjustedY = newDims.adjustedY\n  }\n  adjustedX = Math.min(adjustedX, screenWidth - WIN_WIDTH);\n  adjustedY = Math.min(adjustedY, screenHeight - WIN_HEIGHT);\n\n  // If there are other windows, place to the right of that window.\n  if (adjustedX > 0 || adjustedY > 0) {\n    winX = adjustedX;\n    winY = adjustedY;\n  }\n\n  return {\n    x: winX,\n    y: winY,\n    width: WIN_WIDTH,\n    height: WIN_HEIGHT,\n  }\n}\n\nclass SalesforceWindowLauncher {\n\n  activate() {\n    this._usub = SalesforceActions.openObjectForm.listen(this._newForm)\n  }\n\n  deactivate() {\n    this._usub();\n  }\n\n  /**\n   * This will create a new Salesforce Object form in a popout window\n   *\n   * Options:\n   *   - objectType: The Salesforce objectType i.e. \"Opportunity\" or \"Account\"\n   *   - objectId: OPTIONAL- If present that means we want to open an\n   *     a form to edit the given objectId\n   *   - objectInitialData: Some initial data to seed the creation with.\n   *     It's a hash whose keys are the SalesforceObject data keys and\n   *     whose values are the default values for that.\n   *   - contextData: The entity that originated the call to create a\n   *     new window may wish to pass some identifying information to be\n   *     passed along with the call. This is useful to let the caller know\n   *     when a separate window closed, or an object was created, etc.\n   */\n  _newForm({objectId, objectType, objectInitialData, contextData}) {\n    const objName = _str.titleize(_str.humanize(objectType))\n    let title = `Create New ${objName}`\n    if (objectId) {\n      title = `Update ${(objectInitialData || {}).Name || objName}`\n    }\n\n    NylasEnv.newWindow({\n      title: title,\n      bounds: calcBoundsForNextWindow(),\n      windowType: WINDOW_TYPE,\n      windowProps: {objectId, objectType, objectInitialData, contextData},\n    })\n  }\n}\n\nexport default new SalesforceWindowLauncher()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/form/smart-fields.es6",
    "content": "import _ from 'underscore'\nimport moment from 'moment'\nimport {Utils, Contact, DatabaseStore} from 'nylas-exports'\n\nimport SalesforceEnv from '../salesforce-env'\nimport SalesforceObject from '../models/salesforce-object'\nimport PendingSalesforceObject from './pending-salesforce-object'\n\n/**\n * This attempts to pre-fill as much data as possible in generic\n * Salesforce forms. We keep a known mapping of Nylas Fields to common\n * Salesforce fields.\n *\n * The initialSchema is the blank schema loaded via\n * `FetchEmptySchemaForType`\n *\n * contextData is data passed into a form from the form's creator. This\n * provides context and additional data (like related Contacts) to the\n * form so we can have something to pre-fill from.\n *\n * objectInitialData is the combination of initially passed in data to the\n * form and existing data if the object already exists and we're editing\n * it.\n *\n * This uses the Clearbit API to load contextual information about various\n * contacts.\n */\nclass SmartFields {\n  fillForm = (args = {}) => {\n    return Promise.resolve(this.checkArgs(args))\n    .then(this.addSelfAsOwner)\n    .then(this.loadAssociatedContact)\n    .then(this.formItemEach((formItem) => {\n      if (this.isUnfillable(formItem)) return formItem;\n      return Promise.resolve(formItem)\n      .then(this.fillFromInitialData(args.objectInitialData))\n      .then(this.fillJoinedReferences(args))\n      .then(this.fillFromDefaultValue)\n      .then(this.fillFromKnownFields)\n      .then(this.fillFromClearbit(args))\n      .then(this.normalizeValue)\n    })).then((formData) => formData)\n  }\n\n  checkArgs(args) {\n    if (!args.initialSchema) {\n      throw new Error(\"Need initial schema\")\n    }\n    return args\n  }\n\n  addSelfAsOwner = (args) => {\n    return SalesforceEnv.loadIdentity().then(identity => {\n      args.objectInitialData.OwnerId = identity.id;\n      return args\n    });\n  }\n\n  loadAssociatedContact = (args) => {\n    const {focusedNylasContactData} = args.initialSchema.contextData;\n    if (!focusedNylasContactData) return args;\n    if (focusedNylasContactData.id) {\n      return DatabaseStore.find(Contact, focusedNylasContactData.id)\n      .then((contact) => {\n        args.initialSchema.contextData.contact = contact;\n        return args\n      })\n    }\n    args.initialSchema.contextData.contact = new Contact({\n      name: focusedNylasContactData.name,\n      email: focusedNylasContactData.email,\n    });\n    return Promise.resolve(args)\n  }\n\n  formItemEach(eachFn) {\n    return (args) => {\n      const formData = Utils.deepClone(args.initialSchema);\n      return Promise.each(formData.fieldsets, (fieldset) => {\n        return Promise.each(fieldset.formItems, (formItem) => {\n          // Designed to update formData via each formItem in place\n          return eachFn(formItem)\n        })\n      }).then(() => formData)\n    }\n  }\n\n  hasEmptyValue(formItem) {\n    if (typeof formItem.value === 'string' || _.isArray(formItem.value)) {\n      return formItem.value.length === 0\n    }\n    return (formItem.value === null || formItem.value === undefined)\n  }\n\n  isUnfillable = (formItem) => {\n    return formItem.type === \"EmptySpace\" || !formItem.name\n  }\n\n  fillFromInitialData = (objectInitialData = {}) => {\n    return (formItem) => {\n      if (formItem.name in objectInitialData) {\n        formItem.value = objectInitialData[formItem.name];\n      }\n      return formItem;\n    }\n  }\n\n  /**\n   * For \"hasMany\" and \"hasManyThrough\" reference types. Based on the\n   * objectId we lookup all related objects for that object given the\n   * reference flags stored on the formItem's value.\n   *\n   * The formItem's value for a type reference must always be an array.\n   * The array must end up filled with zero or more SalesforceObjects\n   *\n   * To resolve these references properly we make use of the following\n   * formItem fields:\n   *\n   * - type === \"reference\"\n   * - referenceTo\n   * - referenceType\n   * - referenceThrough\n   * - referenceThroughSelfKey\n   * - referenceThroughForeignKey\n   *\n   * See SalesforceSchemaAdapter for a place we insert objects with\n   * more complex referenceTypes\n   */\n  fillJoinedReferences = (args) => {\n    const objectId = args.initialSchema.contextData.objectId;\n\n    return (formItem) => {\n      if (formItem.type !== \"reference\") return formItem;\n      if (!formItem.referenceType) formItem.referenceType = \"belongsTo\";\n\n      return this._resolveInitialRefs(formItem.value, formItem.referenceTo, args.initialSchema.contextData).then((value) => {\n        formItem.value = value;\n\n        if (!objectId) return formItem;\n\n        if (formItem.referenceType === \"hasMany\") {\n          // Example: An Account hasMany Contacts. Each Contact has an\n          // AccountId pointer in their \"relatedToId\" field\n          return DatabaseStore.findAll(SalesforceObject, {\n            type: formItem.referenceTo,\n            relatedToId: objectId,\n          }).then((objs = []) => {\n            formItem.value = formItem.value.concat(objs);\n            return formItem\n          })\n        } else if (formItem.referenceType === \"hasManyThrough\") {\n          // Example: A Contact hasMany Opportunities through\n          // OpportunityContactRoles.\n          //\n          // For a given Contact, we can lookup OpportunityContactRoles by\n          // the Contact's id. The referenceThroughSelfKey for a Contact is\n          // the \"identifier\" field of an OpportunityContactRole. The\n          // referenceThroughForeignKey for a Contact is the \"relatedToId\"\n          // field of an OpportunityContactRole.\n          const joinWhere = {type: formItem.referenceThrough}\n          joinWhere[formItem.referenceThroughSelfKey] = objectId;\n          return DatabaseStore.findAll(SalesforceObject, joinWhere)\n          .then((joinItems = []) => {\n            if (joinItems.length === 0) return [];\n            const objIds = _.pluck(joinItems, formItem.referenceThroughForeignKey)\n            return DatabaseStore.findAll(SalesforceObject, {\n              type: formItem.referenceTo, id: objIds,\n            })\n          })\n          .then((objs = []) => {\n            formItem.value = formItem.value.concat(objs);\n            return formItem\n          })\n        }\n\n        // We get here if it's a \"belongsTo\" referenceType and the field\n        // is blank & empty. In the \"belongsTo\" case there's nothing to\n        // lookup, so we simply return the formItem.\n        return formItem\n      })\n    }\n  }\n\n  /**\n   * When we fill reference types, the key may already have a value\n   * associated with it. That value represents objects we want to pre-fill\n   * into a field, in addition to those we find already related to the\n   * object. The value may come to us in a variety of formats.\n   *\n   * We return an array of zero or more SalesforceObject or\n   * PendingSalesforceObject types.\n   */\n  _resolveInitialRefs = (rawValue = [], referenceTo) => {\n    const ids = []\n    const outValue = []\n    const pendingJSON = []\n    if (rawValue === null || rawValue === undefined) {\n      return Promise.resolve(outValue)\n    } else if (typeof rawValue === \"string\") {\n      ids.push(rawValue)\n    } else if (_.isArray(rawValue)) {\n      for (const val of rawValue) {\n        if (typeof val == \"string\") ids.push(val);\n        if (val.pendingSalesforceObject) pendingJSON.push(val);\n        if (val instanceof SalesforceObject) outValue.push(val);\n        if (val instanceof PendingSalesforceObject) outValue.push(val);\n      }\n    } else if (rawValue.pendingSalesforceObject) {\n      pendingJSON.push(rawValue)\n    } else {\n      return Promise.resolve(outValue)\n    }\n\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: referenceTo,\n      id: ids,\n    }).then((objs = []) => {\n      return outValue.concat(objs).concat(pendingJSON.map((objJSON) => {\n        /**\n         * As initialData we can pass in the JSON of a\n         * PendingSalesforceObject. We do this to initialize back\n         * references to forms. The id of the JSON has been set to the\n         * formId of the form creating the backref. That way if the\n         * creating form closes, the creating form's ID will match with\n         * our PendingSalesforceObject's ID, and we'll properly dismiss\n         * the PendingSalesforceObject in the form.\n         */\n        return new PendingSalesforceObject(objJSON)\n      }));\n    })\n  }\n\n  fillFromDefaultValue = (formItem) => {\n    if (!this.hasEmptyValue(formItem)) return formItem\n    if (formItem.defaultValue && formItem.defaultValue.length > 0) {\n      formItem.value = formItem.defaultValue\n    }\n    if (formItem.type === \"checkbox\") { formItem.value = false; }\n    return formItem\n  }\n\n  fillFromKnownFields = (formItem) => {\n    if (!this.hasEmptyValue(formItem)) return formItem;\n    const knownFields = {\n      CloseDate: () => moment().add(1, 'month').format(\"YYYY-MM-DD\"),\n      StageName: () => \"Prospecting\",\n      ForecastCategoryName: () => \"Pipeline\",\n      LeadStatus: () => \"Working - Contacted\",\n    }\n    if (formItem.name in knownFields) {\n      formItem.value = knownFields[formItem.name]()\n    }\n    if (!this.hasEmptyValue(formItem)) formItem.prefilled = true;\n    return formItem;\n  }\n\n  fillFromClearbit = (args) => {\n    return (formItem) => {\n      if (!this.hasEmptyValue(formItem)) return formItem;\n      const contact = args.initialSchema.contextData.contact;\n      const objectType = args.initialSchema.contextData.objectType;\n      if (!contact || !this.hasEmptyValue(formItem)) return formItem;\n      formItem.value = this.getFieldFromClearbit(contact, objectType, formItem.name)\n      if (!this.hasEmptyValue(formItem)) formItem.prefilled = true;\n      return formItem;\n    }\n  }\n\n  normalizeValue = (formItem) => {\n    if (this.hasEmptyValue(formItem)) return formItem;\n    if (formItem.name.includes(\"LinkedIn\")) {\n      formItem.value = `https://linkedin.com/${formItem.value}`\n    } else if (formItem.name === \"FirstName\") {\n      if (formItem.value.includes(\"@\")) {\n        formItem.value = null\n      }\n    }\n    if (this.hasEmptyValue(formItem)) formItem.prefilled = false;\n    return formItem;\n  }\n\n  /**\n   * This is the default field mapping between Clearbit's Enrichment\n   * API for Persons (version 2016-01-04) and Companies\n   * (version 2016-05-18), and a standard uncustomized Salesforce\n   * environment\n   *\n   * TODO: Load a custom config from the `SalesforceEnv` that lets users\n   * customize their field mappings.\n   *\n   * See https://dashboard.clearbit.com/docs#enrichment-api-company-api-attributes\n   *\n   */\n  getFieldFromClearbit(contact, objectType, formItemName) {\n    const cbPerson = \"thirdPartyData.clearbit.rawClearbitData.person\"\n    const cbCompany = \"thirdPartyData.clearbit.rawClearbitData.company\"\n    const personMapping = {\n      Name: \"name\",\n      Email: \"email\",\n      Phone: \"phone\",\n\n      Salutation: \"\",\n      FirstName: \"firstName\",\n      MiddleName: \"\",\n      LastName: \"lastName\",\n      Suffix: \"\",\n\n      Company: `company,${cbPerson}.employment.name,${cbCompany}.name,guessCompanyFromEmail`,\n      Title: `${cbPerson}.employment.title`,\n      Department: `${cbPerson}.employment.role`,\n      Website: `${cbCompany}.url`,\n\n      Street: \"\",\n      City: `${cbPerson}.city,${cbCompany}.city`,\n      // StateCode: `${cbPerson}.stateCode,${cbCompany}.stateCode`,\n      PostalCode: \"\",\n      // CountryCode: `${cbPerson}.countryCode,${cbCompany}.countryCode`,\n\n      LinkedIn__c: `${cbPerson}.linkedin.handle`,\n      LinkedIn_personal_url__c: `${cbPerson}.linkedin.handle`,\n    }\n    const companyMapping = {\n      Name: `${cbCompany}.name`,\n      Website: `${cbCompany}.url`,\n      Phone: `${cbCompany}.phone`,\n      Description: `${cbCompany}.description`,\n      Industry: `${cbCompany}.category.industry`,\n      NumberOfEmployees: `${cbCompany}.metrics.employees`,\n      BillingStreet: `${cbCompany}.geo.streetNumber+${cbCompany}.geo.streetName`,\n      BillingCity: `${cbCompany}.geo.city`,\n      // BillingStateCode: `${cbCompany}.geo.state`,\n      BillingPostalCode: `${cbCompany}.geo.postalCode`,\n      // BillingCountryCode: `${cbCompany}.geo.country`,\n      ShippingStreet: `${cbCompany}.geo.streetNumber+${cbCompany}.geo.streetName`,\n      ShippingCity: `${cbCompany}.geo.city`,\n      // ShippingStateCode: `${cbCompany}.geo.state`,\n      ShippingPostalCode: `${cbCompany}.geo.postalCode`,\n      // ShippingCountryCode: `${cbCompany}.geo.country`,\n    }\n    const mapping = {\n      Lead: personMapping,\n      Contact: personMapping,\n      Account: companyMapping,\n      Opportunity: companyMapping,\n    }\n    const lookupPath = (mapping[objectType] || {})[formItemName]\n    return Utils.resolvePath(lookupPath, contact)\n  }\n}\n\nexport default new SmartFields()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/main.jsx",
    "content": "import React from 'react'\nimport {Rx, ComponentRegistry, WorkspaceStore} from 'nylas-exports'\n\n// Worker to fetch new Salesforce objects\nimport SalesforceDataReset from './salesforce-data-reset'\nimport SalesforceSyncWorker from './salesforce-sync-worker'\n\n// Plugin-wide environment and object store\nimport SalesforceEnv from './salesforce-env'\nimport SalesforceAPIError from './salesforce-api-error'\nimport SalesforceErrorReporter from './salesforce-error-reporter'\nimport SalesforceNewMailListener from './salesforce-new-mail-listener'\n// import SalesforceIntroNotification from './salesforce-intro-notification'\n\n// Database Objects\nimport SalesforceSchema from './models/salesforce-schema'\nimport SalesforceObject from './models/salesforce-object'\n\n// Salesforce Create / Update Forms\nimport SalesforceObjectForm from './form/salesforce-object-form'\nimport SalesforceWindowLauncher from './form/salesforce-window-launcher'\n\n// Enhancements to Thread\nimport SalesforceSyncLabel from './thread/salesforce-sync-label'\nimport RelatedObjectsForThread from './thread/related-objects-for-thread'\nimport SalesforceSyncMessageStatus from './thread/salesforce-sync-message-status'\nimport SalesforceManuallyRelateThreadButton from './thread/salesforce-manually-relate-thread-button'\n\n// Enhancements to Sidebar Contact info\nimport SalesforceContactInfo from './contact/salesforce-contact-info'\n\n// Enhancements to Search\nimport SalesforceSearchIndexer from './search/salesforce-search-indexer'\nimport SalesforceSearchBarResults from './search/salesforce-search-bar-results'\n\n// Enhancements to Composer\nimport ParticipantDecorator from './composer/participant-decorator'\nimport ContactSearchResults from './composer/contact-search-results'\n\n// Tasks to sync emails back to Salesforce\nimport SyncSalesforceObjectsTask from './tasks/sync-salesforce-objects-task'\nimport DestroySalesforceObjectTask from './tasks/destroy-salesforce-object-task'\nimport SyncbackSalesforceObjectTask from './tasks/syncback-salesforce-object-task'\nimport EnsureMessageOnSalesforceTask from './tasks/ensure-message-on-salesforce-task'\nimport DestroyMessageOnSalesforceTask from './tasks/destroy-message-on-salesforce-task'\nimport UpsertOpportunityContactRoleTask from './tasks/upsert-opportunity-contact-role-task'\nimport ManuallyRelateSalesforceObjectTask from './tasks/manually-relate-salesforce-object-task'\nimport SyncThreadActivityToSalesforceTask from './tasks/sync-thread-activity-to-salesforce-task'\nimport RemoveManualRelationToSalesforceObjectTask from './tasks/remove-manual-relation-to-salesforce-object-task'\n\nimport SalesforceIntroNotification from './salesforce-intro-notification'\nimport SalesforceRelatedObjectCache from './salesforce-related-object-cache'\n\nfunction SalesforceObjectFormWithWindowProps() {\n  return <SalesforceObjectForm {...NylasEnv.getWindowProps()} />\n}\nSalesforceObjectFormWithWindowProps.containerRequired = false\nSalesforceObjectFormWithWindowProps.displayName = \"SalesforceObjectFormWithWindowProps\"\n\n\n// This special `modelConstructors` key will add the following\n// constructors to the `DatabaseObjectRegistry`. This will enable model\n// serialization across IPC as well as SQL Table construction.\nexport const modelConstructors = [\n  SalesforceSchema,\n  SalesforceObject,\n]\n\n// This special `taskConstructors` key will add the following\n// constructors to the `TaskRegistry`. This will enable task serialization\nexport const taskConstructors = [\n  SalesforceAPIError, // So it can go across the action bridge\n  SyncSalesforceObjectsTask,\n  DestroySalesforceObjectTask,\n  SyncbackSalesforceObjectTask,\n  EnsureMessageOnSalesforceTask,\n  DestroyMessageOnSalesforceTask,\n  UpsertOpportunityContactRoleTask,\n  SyncThreadActivityToSalesforceTask,\n  ManuallyRelateSalesforceObjectTask,\n  RemoveManualRelationToSalesforceObjectTask,\n]\n\nconst components = [\n  {\n    component: SalesforceSyncLabel,\n    role: \"Thread:MailLabel\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: SalesforceManuallyRelateThreadButton,\n    role: \"ThreadActionsToolbarButton\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: RelatedObjectsForThread,\n    role: \"MessageListHeaders\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: SalesforceSyncMessageStatus,\n    role: \"MessageFooterStatus\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: SalesforceContactInfo,\n    role: \"MessageListSidebar:ContactCard\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: SalesforceSearchBarResults,\n    role: \"SearchBarResults\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: ParticipantDecorator,\n    role: \"Composer:RecipientChip\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: ContactSearchResults,\n    role: \"ContactSearchResults\",\n    window: \"default\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: SalesforceIntroNotification,\n    role: \"RootSidebar:Notifications\",\n    window: \"default\",\n    onlyWhenLoggedIn: false,\n  },\n  {\n    component: ParticipantDecorator,\n    role: \"Composer:RecipientChip\",\n    window: \"composer\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: ContactSearchResults,\n    role: \"ContactSearchResults\",\n    window: \"composer\",\n    onlyWhenLoggedIn: true,\n  },\n  {\n    component: SalesforceObjectFormWithWindowProps,\n    location: WorkspaceStore.Location.Center,\n    window: \"SalesforceObjectForm\",\n    onlyWhenLoggedIn: false,\n  },\n]\nfunction setComponentActivation() {\n  components.forEach((opts) => {\n    if (NylasEnv.getWindowType() !== opts.window) return;\n    if (opts.onlyWhenLoggedIn && !SalesforceEnv.isLoggedIn()) {\n      ComponentRegistry.unregister(opts.component);\n    } else {\n      ComponentRegistry.register(opts.component, opts)\n    }\n  })\n}\n\n\nconst stores = [\n  {store: SalesforceEnv, window: \"all\"},\n  {store: SalesforceWindowLauncher, window: \"all\"},\n  {store: SalesforceErrorReporter, window: \"default\"},\n  {store: SalesforceNewMailListener, window: \"default\"},\n  {store: SalesforceRelatedObjectCache, window: \"default\"},\n  {store: SalesforceDataReset, window: \"work\"},\n  {store: SalesforceSyncWorker, window: \"work\"},\n  {store: SalesforceSearchIndexer, window: \"work\"},\n]\nfunction storesForWindow() {\n  return stores.filter(({window}) => {\n    return window === NylasEnv.getWindowType() || window === \"all\"\n  }).map(({store}) => store)\n}\n\n\nlet disp = {dispose: () => {}}\nexport function activate() {\n  if (NylasEnv.getWindowType() === 'SalesforceObjectForm') {\n    WorkspaceStore.defineSheet(\n      'Main',\n      {root: true},\n      {popout: ['Center']},\n    )\n  }\n  disp = Rx.Observable.fromConfig('salesforce.id').subscribe(setComponentActivation)\n  storesForWindow().forEach(s => s.activate())\n}\n\nexport function deactivate() {\n  disp.dispose()\n  components.forEach(opts => ComponentRegistry.unregister(opts.component))\n  storesForWindow().forEach(s => s.deactivate())\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/metadata-helpers.es6",
    "content": "import {Utils} from 'nylas-exports'\nimport {PLUGIN_ID} from './salesforce-constants'\n\n// When we attach metadata to Nylas objects we save SalesforceObjects\n// according to the following schemas.\n//\n// thread.metadata = {\n//   manuallyRelatedTo: {\n//     \"salesforceAccountId\": {\n//       id: \"salesforceAccountId\",\n//       type: \"Account\",\n//     }\n//     \"salesforceOpportunityId\": {\n//       id: \"salesforceOpportunityId\",\n//       type: \"Opportunity\",\n//     }\n//     \"salesforceCaseId\": {\n//       id: \"salesforceCaseId\",\n//       type: \"Case\",\n//     }\n//   }\n//\n//   syncActivityTo: {\n//     \"salesforceOpportunityId\": {\n//       id: \"salesforceOpportunityId\",\n//       type: \"Opportunity\",\n//     }\n//   }\n// }\n//\n// message.metadata = {\n//   clonedAs: {\n//     \"opportunityId\": {\n//       \"taskId1\": {\n//         id: \"taskId1\",\n//         type: \"Task\",\n//         relatedToId: \"opportunityId\",\n//       },\n//       \"emailMessageId\": {\n//         id: null,\n//         errorCode: \"FILE_TOO_LARGE\"\n//         errorMessage: \"Body too large\"\n//         type: \"EmailMessage\",\n//         relatedToId: \"opportunityId\",\n//       },\n//     },\n//     \"accountId\": {\n//       \"taskId2\": {\n//         id: \"taskId2\",\n//         type: \"Task\",\n//         relatedToId: \"accountId\",\n//       },\n//       \"emailMessageId2\": {\n//         id: \"emailMessageId2\",\n//         type: \"EmailMessage\",\n//         relatedToId: \"accountId\",\n//       },\n//     },\n//   }\n// }\n//\n// Schema before 2016-10-18 the schema used to look like:\n// nylasObject.metadata = {\n//   sObjects: {\n//     \"salesforceID1\": {\n//       id: \"salesforceID1\",\n//       type: \"Opportunity\",\n//     },\n//     \"salesforceID2\": {\n//       id: \"salesforceID2\",\n//       type: \"Account\",\n//     },\n//     \"salesforceID3\": {\n//       id: \"salesforceID3\",\n//       type: \"Task\",\n//       relatedToId: \"salesforceID1\"\n//     },\n//     \"salesforceID4\": {\n//       id: \"salesforceID4\",\n//       type: \"EmailMessage\",\n//       relatedToId: \"salesforceID1\"\n//     }\n//   }\n// }\n\nfunction metadataClone(nylasObject) {\n  return Utils.deepClone(nylasObject.metadataForPluginId(PLUGIN_ID) || {});\n}\n\nexport function getManuallyRelatedObjects(nylasObject) {\n  const metadata = metadataClone(nylasObject);\n  return metadata.manuallyRelatedTo || {}\n}\n\n/**\n * Note we only store the id and type in the metadata.\n */\nexport function setManuallyRelatedObject(nylasObject, {id, type} = {}) {\n  if (!id || !type) throw new Error(\"Must provide id and type of object\");\n  const metadata = metadataClone(nylasObject);\n  const manuallyRelatedTo = metadata.manuallyRelatedTo || {};\n  manuallyRelatedTo[id] = {id, type, name};\n  metadata.manuallyRelatedTo = manuallyRelatedTo;\n  nylasObject.applyPluginMetadata(PLUGIN_ID, metadata);\n  return metadata\n}\n\nexport function removeManuallyRelatedObject(nylasObject, {id} = {}) {\n  if (!id) throw new Error(\"Must provide id\");\n  const metadata = metadataClone(nylasObject);\n  const manuallyRelatedTo = metadata.manuallyRelatedTo || {};\n  delete manuallyRelatedTo[id];\n  metadata.manuallyRelatedTo = manuallyRelatedTo;\n  nylasObject.applyPluginMetadata(PLUGIN_ID, metadata);\n  return metadata\n}\n\n\nexport function getSObjectsToSyncActivityTo(nylasObject) {\n  const metadata = metadataClone(nylasObject);\n  return metadata.syncActivityTo || {}\n}\n\nexport function addActivitySyncSObject(nylasObject, {id, type} = {}) {\n  if (!id || !type) throw new Error(\"Must provide id and type of object\");\n  const metadata = metadataClone(nylasObject);\n  const syncActivityTo = metadata.syncActivityTo || {};\n  syncActivityTo[id] = {id, type};\n  metadata.syncActivityTo = syncActivityTo;\n  nylasObject.applyPluginMetadata(PLUGIN_ID, metadata);\n  return metadata\n}\n\nexport function removeActivitySyncSObject(nylasObject, {id} = {}) {\n  if (!id) throw new Error(\"Must provide id of object\");\n  const metadata = metadataClone(nylasObject);\n  const syncActivityTo = metadata.syncActivityTo || {};\n  delete syncActivityTo[id]\n  metadata.syncActivityTo = syncActivityTo;\n  nylasObject.applyPluginMetadata(PLUGIN_ID, metadata);\n  return metadata\n}\n\n\nexport function getClonedAsForSObject(nylasObject, relatedSObject = {}) {\n  const metadata = metadataClone(nylasObject);\n  const clonedAs = metadata.clonedAs || {}\n  return clonedAs[relatedSObject.id] || {}\n}\n\nexport function getClonedAs(nylasObject) {\n  const metadata = metadataClone(nylasObject);\n  return metadata.clonedAs || {}\n}\n\n// A Nylas Message may be replicated as a Salesforce Task on multiple\n// Salesforce Opportunities. Given a Salesforce Task sObject, this will\n// look through all opportunities until we find one with that Task id,\n// then if so, return the corresponding opportunityId it's found under.\n//\n// This is useful when we discover that a Salesforce Task has been deleted\n// and we need to cleanup references to that Task.\nexport function relatedIdForClonedSObject(nylasObject, sObject = {}) {\n  const metadata = metadataClone(nylasObject);\n  const clonedAs = metadata.clonedAs || {}\n  for (const relatedToId of Object.keys(clonedAs)) {\n    if (clonedAs[relatedToId][sObject.id]) return relatedToId;\n  }\n  return null;\n}\n\nexport function addClonedSObject(nylasObject, relatedSObject = {}, {id, type, relatedToId} = {}) {\n  if (!id || !type || !relatedToId) throw new Error(\"Must provide id, type, and relatedToId of object\");\n  if (!relatedSObject.id) throw new Error(\"Must provide a related sObject with an id\")\n\n  const metadata = metadataClone(nylasObject);\n  const clonedAs = metadata.clonedAs || {};\n  const clonedAsForObj = clonedAs[relatedSObject.id] || {};\n\n  clonedAsForObj[id] = {id, type, relatedToId};\n  clonedAs[relatedSObject.id] = clonedAsForObj;\n  clonedAs[relatedSObject.id].type = relatedSObject.type;\n  metadata.clonedAs = clonedAs;\n\n  nylasObject.applyPluginMetadata(PLUGIN_ID, metadata);\n  return metadata\n}\n\nexport function removeClonedSObject(nylasObject, relatedSObject = {}, {id}) {\n  if (!id) throw new Error(\"Must provide id of cloned object\");\n  if (!relatedSObject.id) throw new Error(\"Must provide a related sObject with an id\")\n\n  const metadata = metadataClone(nylasObject);\n  const clonedAs = metadata.clonedAs || {};\n  const clonedAsForObj = clonedAs[relatedSObject.id] || {};\n\n  delete clonedAsForObj[id]\n  clonedAs[relatedSObject.id] = clonedAsForObj;\n  if (Object.keys(clonedAsForObj).length === 0) {\n    delete clonedAs[relatedSObject.id]\n  }\n  metadata.clonedAs = clonedAs;\n\n  nylasObject.applyPluginMetadata(PLUGIN_ID, metadata);\n  return metadata\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/models/salesforce-object.es6",
    "content": "import {Model, Attributes} from 'nylas-exports'\n\nclass SalesforceObject extends Model {\n\n  static searchable = true\n\n  static searchFields = ['content']\n\n  // This intentionally does NOT use _.extend(... Model.Attributes)\n  // because we do NOT want most of those attributes.\n  // So, we pick the relevant attributes by hand.\n  static attributes = {\n    id: Attributes.String({\n      queryable: true,\n      modelKey: 'id',\n    }),\n\n    clientId: Attributes.String({\n      queryable: true,\n      modelKey: 'clientId',\n      jsonKey: 'client_id',\n    }),\n\n    serverId: Attributes.ServerId({\n      queryable: true,\n      modelKey: 'serverId',\n      jsonKey: 'server_id',\n    }),\n\n    type: Attributes.String({\n      queryable: true,\n      modelKey: 'type',\n      jsonKey: 'type',\n    }),\n\n    name: Attributes.String({\n      queryable: true,\n      modelKey: 'name',\n      jsonKey: 'name',\n    }),\n\n    // Can optionally be used to query for objects if the name is not\n    // sufficient. For example, a Contact object might want to put the\n    // `email` field here. A Task might want to put the `description`\n    // field here.\n    // We also downcase and trim the data before it goes into the\n    // \"identifier\" field.\n    identifier: Attributes.String({\n      queryable: true,\n      modelKey: 'identifier',\n      jsonKey: 'identifier',\n    }),\n\n    relatedToId: Attributes.String({\n      queryable: true,\n      modelKey: 'relatedToId',\n      jsonKey: 'relatedToId',\n    }),\n\n    updatedAt: Attributes.DateTime({\n      queryable: true,\n      modelKey: 'updatedAt',\n      jsonKey: 'updatedAt',\n    }),\n\n    // NOTE: We always expect that rawData is filled with the complete\n    // object (not a partial object). We use that to determine if we have\n    // enough information to display a SalesforceObject Edit field.\n    rawData: Attributes.Object({\n      modelKey: 'rawData',\n      jsonKey: 'rawData',\n    }),\n\n    isSearchIndexed: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'isSearchIndexed',\n      jsonKey: 'is_search_indexed',\n      defaultValue: false,\n      loadFromColumn: true,\n    }),\n\n    // This corresponds to the rowid in the FTS table. We need to use the FTS\n    // rowid when updating and deleting items in the FTS table because otherwise\n    // these operations would be way too slow on large FTS tables.\n    searchIndexId: Attributes.Number({\n      modelKey: 'searchIndexId',\n      jsonKey: 'search_index_id',\n    }),\n  }\n\n  static sortOrderAttribute = () => {\n    return SalesforceObject.attributes.name\n  }\n\n  static naturalSortOrder = () => {\n    return SalesforceObject.sortOrderAttribute().descending()\n  }\n\n  static additionalSQLiteConfig = {\n    setup: () => [\n      'CREATE INDEX IF NOT EXISTS TypeIdentifierIndex ON `SalesforceObject` (type, identifier)',\n      'CREATE INDEX IF NOT EXISTS TypeRelatedToIdIndex ON `SalesforceObject` (type, relatedToId)',\n    ],\n  }\n}\n\nexport default SalesforceObject\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/models/salesforce-schema.es6",
    "content": "import {Model, Attributes} from 'nylas-exports'\n\n\nclass SalesforceSchema extends Model {\n\n  static attributes = {\n    id: Attributes.String({\n      queryable: true,\n      modelKey: 'id',\n      jsonKey: 'id',\n    }),\n\n    schemaType: Attributes.String({\n      queryable: true,\n      modelKey: 'schemaType',\n      jsonKey: 'schemaType',\n    }),\n\n    objectType: Attributes.String({\n      queryable: true,\n      modelKey: 'objectType',\n      jsonKey: 'objectType',\n    }),\n\n    fieldsets: Attributes.Object({\n      modelKey: 'fieldsets',\n      jsonKey: 'fieldsets',\n    }),\n\n    createdAt: Attributes.DateTime({\n      queryable: true,\n      modelKey: 'createdAt',\n      jsonKey: 'createdAt',\n    }),\n  }\n}\n\nexport default SalesforceSchema\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/related-object-helpers.es6",
    "content": "import _ from 'underscore'\n\nimport {\n  Rx,\n  DatabaseStore,\n  Thread,\n} from 'nylas-exports'\nimport * as mdHelpers from './metadata-helpers'\nimport SalesforceRelatedObjectCache from './salesforce-related-object-cache'\n\nexport function getUniqueRelatedSObjects(directObjects, manualObjects) {\n  const sObjects = []\n  const addedObjectIds = []\n  for (const obj of directObjects.concat(manualObjects)) {\n    if (!addedObjectIds.includes(obj.id)) {\n      sObjects.push(obj)\n      addedObjectIds.push(obj.id)\n    }\n  }\n  return sObjects\n}\n\nexport function relatedSObjectsForThread(thread) {\n  const direct = _.values(SalesforceRelatedObjectCache.directlyRelatedSObjectsForThread(thread))\n  const manual = _.values(mdHelpers.getManuallyRelatedObjects(thread))\n  if (!direct) return []\n  return getUniqueRelatedSObjects(direct, manual)\n}\n\nexport function observeRelatedSObjectsForThread(thread) {\n  const directSource = SalesforceRelatedObjectCache.observeDirectlyRelatedSObjectsForThread(thread)\n  const manualSource = Rx.Observable.fromQuery(DatabaseStore.find(Thread, thread.id))\n  return Rx.Observable.combineLatest(directSource, manualSource).map((objects) => {\n    const [directObjectMap, observedThread] = objects\n    const directObjects = _.values(directObjectMap);\n    let manualObjects = _.values(mdHelpers.getManuallyRelatedObjects(observedThread))\n    manualObjects = manualObjects.map((manualObject) => {\n      manualObject.manuallyRelated = true\n      return manualObject\n    })\n    return getUniqueRelatedSObjects(directObjects, manualObjects)\n  })\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-actions.es6",
    "content": "import _ from 'underscore'\nimport {Reflux} from 'nylas-exports'\n\nconst globalSFActions = Reflux.createActions([\n  \"deleteSuccess\",\n\n  \"syncbackSuccess\",\n  \"syncbackFailed\",\n\n  \"salesforceWindowClosing\",\n\n  \"loginToSalesforce\",\n  \"logoutOfSalesforce\",\n\n  \"syncSalesforce\",\n\n  \"reportError\",\n])\n\nconst localSFActions = Reflux.createActions([\n  \"openObjectForm\",\n])\n\nconst SalesforceActions = _.extend({}, localSFActions, globalSFActions)\n\nfor (const actionName of Object.keys(SalesforceActions)) {\n  SalesforceActions[actionName].sync = true\n}\n\nNylasEnv.registerGlobalActions({\n  pluginName: \"Salesforce\",\n  actions: globalSFActions,\n});\n\nexport default SalesforceActions\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-api-error.es6",
    "content": "import _ from 'underscore'\nimport {APIError} from 'nylas-exports'\n\n// Salesforce errors have a JSON body of the following format:\n//\n// error.message = [\n//   {\n//     message: \"Some Salesforce error message.\"\n//     errorCode: \"SOME_SALESFORCE_ERROR_CODE_STRING\"\n//   }\n// ]\nexport default class SalesforceAPIError extends APIError {\n  constructor(args) {\n    super(args);\n    if (_.isArray(this.body)) {\n      this.messages = _.pluck(this.body, \"message\")\n      this.errorCodes = _.pluck(this.body, \"errorCode\")\n      this.message = this.messages[0]\n      this.errorCode = this.errorCodes[0]\n    } else if (_.isString(this.body)) {\n      this.messages = [this.body]\n      this.errorCodes = []\n      this.message = this.body\n      this.errorCode = null\n    } else {\n      this.messages = []\n      this.errorCodes = []\n      this.message = \"Unknown Salesforce Error\"\n      this.errorCode = null\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-api.jsx",
    "content": "import {shell, remote} from 'electron'\nimport React from 'react'\nimport {Actions, NylasAPIRequest} from 'nylas-exports'\nimport SalesforceActions from './salesforce-actions'\nimport SalesforceOAuth from './salesforce-oauth'\nimport SalesforceAPIError from './salesforce-api-error'\n\nclass SalesforceAPI {\n  constructor() {\n    this.VERSION = \"v37.0\"\n    NylasEnv.config.onDidChange('salesforce.instance_url', this._setAPIRoot)\n    this._setAPIRoot()\n    this._apiDisabled = false\n    this._tokenRefreshPromise = null\n  }\n\n  _setAPIRoot = () => {\n    const instanceUrl = NylasEnv.config.get(\"salesforce.instance_url\");\n    if (instanceUrl) {\n      this.APIRoot = `${instanceUrl}/services/data/${this.VERSION}`;\n      this._apiDisabled = false\n    } else {\n      this.APIRoot = null\n    }\n  }\n\n  makeRequest(options = {}) {\n    if (NylasEnv.getLoadSettings().isSpec) {\n      return Promise.resolve();\n    }\n    if (this._apiDisabled === true) { return Promise.resolve() }\n    if (!this.APIRoot) {\n      return Promise.reject(new Error(\"Please authenticate Salesforce first\"))\n    }\n\n    // Always refresh since the accessToken may have changed.\n    options.auth = {\n      bearer: SalesforceOAuth.accessToken(),\n    }\n\n    const req = new NylasAPIRequest({\n      api: this,\n      options,\n    });\n\n    return req.run()\n    .catch((apiError) => {\n      const salesforceAPIError = new SalesforceAPIError(apiError)\n      if (this._isBadTokenError(salesforceAPIError)) {\n        if (!this._tokenRefreshPromise) {\n          this._tokenRefreshPromise = this._refreshToken()\n        }\n        return this._tokenRefreshPromise.then(() => {\n          this._tokenRefreshPromise = null;\n        }).then(this._retry(options))\n      } else if (salesforceAPIError.errorCode === \"API_DISABLED_FOR_ORG\") {\n        Actions.recordUserEvent(\"Salesforce Connect Errored\", {\n          errorType: salesforceAPIError.constructor.name,\n          errorCode: salesforceAPIError.errorCode,\n          errorMessage: salesforceAPIError.message,\n        })\n        this._handleAPIDisabled()\n        return Promise.reject(salesforceAPIError)\n      }\n      return Promise.reject(salesforceAPIError)\n    })\n  }\n\n  _retry(options) {\n    return () => {\n      if (options.retries >= 2) {\n        this._unableToAuth()\n        return Promise.reject(new Error(\"Unable to refresh token\"))\n      }\n      options.retries = (options.retries || 0) + 1\n      return this.makeRequest(options) // Try one more time\n    }\n  }\n\n  _isBadTokenError(salesforceAPIError) {\n    const statusCode = salesforceAPIError.statusCode\n    if (statusCode === 401 || statusCode === 403) {\n      if (salesforceAPIError.errorCode === \"INVALID_SESSION_ID\") return true;\n      if (salesforceAPIError.message === \"Bad_OAuth_Token\") return true;\n      SalesforceActions.reportError(salesforceAPIError);\n      return false\n    }\n    return false\n  }\n\n  _isLimitExceeded(salesforceAPIError) {\n    return (salesforceAPIError.errorCode === \"REQUEST_LIMIT_EXCEEDED\")\n  }\n\n  _handleAPIDisabled() {\n    if (this._apiDisabled) return;\n    this._apiDisabled = true\n    const openLink = () => shell.openExternal(\"https://help.salesforce.com/HTViewSolution?id=000005140\")\n    Actions.openModal({\n      component: (\n        <div className=\"salesforce-welcome\" tabIndex=\"0\">\n          <h2>We can&rsquo;t connect to your Salesforce environment</h2>\n          <p>Your Salesforce environment does not have API access enabled. If you are using Group or Professional editions, you must either add API access or upgrade to Enterprise edition. If you are already using Enterprise or Ultimate editions please check your installation settings to ensure API access is turned on.</p>\n          <p>See <a onClick={openLink}>this Salesforce support article</a> for more information regarding API access.</p>\n        </div>\n      ),\n      height: 290,\n      width: 700,\n    })\n    SalesforceActions.logoutOfSalesforce()\n  }\n\n  _unableToAuth() {\n    SalesforceActions.logoutOfSalesforce()\n    const response = remote.dialog.showMessageBox({\n      message: 'Salesforce Connection Problem',\n      detail: `We could no longer access your Salesforce environment. Please reconnect Salesforce.`,\n      buttons: ['Connect Salesforce', 'Dismiss'],\n      type: 'warning',\n    });\n    if (response === 0) {\n      SalesforceActions.loginToSalesforce()\n    }\n  }\n\n  _refreshToken() {\n    return SalesforceOAuth.fetchNewToken()\n    .catch((apiError) => {\n      this._unableToAuth()\n      throw apiError\n    })\n  }\n}\n\nexport default new SalesforceAPI()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-constants.es6",
    "content": "import plugin from '../package.json'\nexport const PLUGIN_NAME = plugin.title\nexport const PLUGIN_ID = plugin.name;\nexport const INFO_DOC_URL = \"https://paper.dropbox.com/doc/N1-Salesforce-Alpha-Program-XVthfW6SeEei8RoKr9X1i\"\nexport const CORE_RELATEABLE_OBJECT_TYPES = [\"Opportunity\", \"Account\", \"Case\"]\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-contact-crawler.es6",
    "content": "import _ from 'underscore'\n\nimport {\n  Contact,\n  DatabaseStore,\n  Utils,\n} from 'nylas-exports'\nimport SalesforceEnv from './salesforce-env'\nimport SalesforceActions from './salesforce-actions'\nimport SalesforceObject from './models/salesforce-object'\n\n\n// Check for new contacts once a day\nconst REFRESH_INTERVAL = 1000 * 60 * 60 * 24\nconst JSOB_BLOB_KEY = \"SalesforceContactCrawler\"\n\nclass SalesforceContactCrawler {\n\n  activate() {\n    this._unsubscribe = SalesforceActions.syncSalesforce.listen(this._run)\n    this._interval = setInterval(this._run, REFRESH_INTERVAL)\n    setTimeout(this._run, 3000)\n\n    this._sContacts = []\n    this._domains = []\n    this._suggestedContacts = []\n  }\n\n  deactivate() {\n    this._unsubscribe()\n    clearInterval(this._interval)\n  }\n\n  _run = () => {\n    if (!SalesforceEnv.isLoggedIn()) return\n    DatabaseStore.findAll(SalesforceObject, {\n      type: \"Contact\",\n    })\n    .then((sContacts) => {\n      this._sContacts = sContacts\n      for (const sContact of sContacts) {\n        this._addToDomains(sContact.identifier)\n      }\n      return Promise.resolve()\n    })\n    .then(() => {\n      return DatabaseStore.findAll(Contact)\n    })\n    .then((contacts) => {\n      for (const contact of contacts) {\n        this._addToSuggestedContacts(contact)\n      }\n      return Promise.resolve()\n    })\n    .then(() => {\n      DatabaseStore.inTransaction((t) => {\n        return t.persistJSONBlob(JSOB_BLOB_KEY, this._suggestedContacts)\n      })\n    })\n  }\n\n  _getDomain(email) {\n    return _.last(email.toLowerCase().trim().split(\"@\"))\n  }\n\n  // Check if domain is probably a company (i.e., not Gmail) and unique\n  _addToDomains(email) {\n    const domain = this._getDomain(email)\n    if (domain.length > 0 &&\n      !Utils.emailHasCommonDomain(email) &&\n      !this._domains.includes(domain)) {\n      this._domains.push(domain)\n    }\n  }\n\n  // Check if contact is real person and is from same company as a Salesforce contact\n  _addToSuggestedContacts(contact) {\n    if (contact.email &&\n      !Utils.likelyNonHumanEmail(contact.email) &&\n      this._domains.includes(this._getDomain(contact.email))) {\n      this._suggestedContacts.push(contact)\n    }\n  }\n\n}\n\nexport default new SalesforceContactCrawler()\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-data-reset.es6",
    "content": "import {DatabaseStore} from 'nylas-exports'\nimport SalesforceObject from './models/salesforce-object'\nimport SalesforceSchema from './models/salesforce-schema'\nimport SalesforceActions from './salesforce-actions'\n\nconst SALESFORCE_DATA_VERSION = 1;\nconst CONFIG_KEY = \"salesforceDataVersion\"\n\nclass SalesforceDataReset {\n  activate() {\n    const version = NylasEnv.config.get(CONFIG_KEY);\n    if (version !== SALESFORCE_DATA_VERSION) {\n      this.deleteAllData().then(() => {\n        SalesforceActions.syncSalesforce();\n        NylasEnv.config.set(CONFIG_KEY, SALESFORCE_DATA_VERSION)\n      })\n    }\n  }\n\n  deactivate() {\n    return true\n  }\n\n  deleteAllData() {\n    return DatabaseStore.inTransaction((t) => {\n      return Promise.all([\n        t.removeAllOfClass(SalesforceObject),\n        t.removeAllOfClass(SalesforceSchema),\n      ])\n    });\n  }\n}\nexport default new SalesforceDataReset()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-env.es6",
    "content": "import NylasStore from 'nylas-store'\nimport {DatabaseStore} from 'nylas-exports'\nimport SalesforceAPI from './salesforce-api'\nimport SalesforceOAuth from './salesforce-oauth'\nimport SalesforceObject from './models/salesforce-object'\nimport SalesforceActions from './salesforce-actions'\nimport SalesforceDataReset from './salesforce-data-reset'\n\n// const loggedInMenu = require('../menus/salesforce-logged-in.json');\n// const loggedOutMenu = require('../menus/salesforce-logged-out.json');\n\n/**\n * The Salesforce environment. Requires config to be populated from an\n * Oauth request with the following information:\n *\n * Note we store the access_token and refresh_token in the system keychain\n *\n * \"salesforce\": {\n *   \"instance_url\": \"\",\n *   \"id\": \"\"\n * },\n *\n */\nclass SalesforceEnv extends NylasStore {\n  constructor() {\n    super()\n    this._menuListeners = []\n    this._subs = []\n    this._lastIdentityUrl = this._getIdentityUrl()\n  }\n\n  activate() {\n    if (NylasEnv.isMainWindow()) {\n      SalesforceOAuth.activate()\n      this.listenTo(SalesforceActions.loginToSalesforce, this._login)\n      this.listenTo(SalesforceActions.logoutOfSalesforce, this._logout)\n    }\n\n    this._subs.push(NylasEnv.config.onDidChange('salesforce.id', this._onIdentityChange));\n    this._listenForLoginState()\n    this._onIdentityChange()\n  }\n\n  deactivate() {\n    for (const sub of this._subs) { sub.dispose() }\n    for (const sub of this._menuListeners) { sub.dispose() }\n    this.stopListeningToAll();\n    if (NylasEnv.isMainWindow()) {\n      SalesforceOAuth.deactivate()\n    }\n  }\n\n  isLoggedIn() {\n    return this._getIdentityUrl() && this._getIdentityUrl().length > 0\n  }\n\n  // menuForLoginState() {\n  //   return this.isLoggedIn() ? loggedInMenu : loggedOutMenu\n  // }\n\n  _listenForLoginState() {\n    for (const sub of this._menuListeners) { sub.dispose() }\n\n    this._menuListeners = [\n      NylasEnv.commands.add(document.body, \"salesforce:sync\", () => SalesforceActions.syncSalesforce()),\n    ]\n    if (this.isLoggedIn()) {\n      this._menuListeners.push(NylasEnv.commands.add(document.body, \"salesforce:disconnect\", this._logout));\n    } else {\n      this._menuListeners.push(NylasEnv.commands.add(document.body, \"salesforce:connect\", this._login));\n    }\n  }\n\n  instanceUrl() {\n    return NylasEnv.config.get(\"salesforce.instance_url\")\n  }\n\n  loadIdentity() {\n    const idUrl = this._getIdentityUrl();\n    if (!idUrl) return Promise.resolve(null);\n\n    return DatabaseStore.findBy(SalesforceObject, {\n      identifier: idUrl,\n    }).then((identity) => {\n      if (!identity) {\n        return this._fetchIdentityFromAPI().then(this._saveIdentity)\n      }\n      return identity\n    })\n  }\n\n  _getIdentityUrl() {\n    return NylasEnv.config.get(\"salesforce.id\")\n  }\n\n  _onIdentityChange = () => {\n    if (this._lastIdentityUrl !== this._getIdentityUrl()) {\n      this._lastIdentityUrl = this._getIdentityUrl()\n      this._listenForLoginState();\n    }\n    this.trigger()\n  }\n\n  _fetchIdentityFromAPI = () => {\n    const idUrl = this._getIdentityUrl();\n    if (!idUrl) return Promise.resolve(null);\n    return SalesforceAPI.makeRequest({\n      APIRoot: idUrl,\n      path: \"/\",\n    })\n  }\n\n  _saveIdentity = (identityJSON) => {\n    if (!identityJSON) {\n      SalesforceActions.reportError(new Error(\"Could not load Identity\"), {\n        APIRoot: this._getIdentityUrl(),\n      });\n      return {};\n    }\n\n    const user = new SalesforceObject({\n      id: identityJSON.user_id,\n      type: \"User\",\n      name: identityJSON.display_name,\n      identifier: this._getIdentityUrl(),\n      object: \"SalesforceObject\",\n      rawData: identityJSON,\n    })\n    return DatabaseStore.inTransaction(t => t.persistModel(user))\n    .then(() => user)\n  }\n\n  _login = () => {\n    SalesforceOAuth.connect()\n  }\n\n  _logout = () => {\n    return SalesforceDataReset.deleteAllData()\n    .then(() => {\n      NylasEnv.config.set(\"salesforce\", {});\n      SalesforceOAuth.clearTokens()\n    });\n  }\n}\n\nexport default new SalesforceEnv()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-error-reporter.es6",
    "content": "import SalesforceEnv from './salesforce-env'\nimport SalesforceActions from './salesforce-actions'\n\nclass SalesforceErrorReporter {\n  activate() {\n    this._usub = SalesforceActions.reportError.listen(this._onError)\n  }\n\n  deactivate() {\n    this._usub();\n  }\n\n  _onError = (error, extraInfo = {}) => {\n    SalesforceEnv.loadIdentity().then((identity) => {\n      NylasEnv.reportError(error, Object.assign({}, extraInfo, {\n        identity: identity,\n        instanceUrl: SalesforceEnv.instanceUrl(),\n      }));\n    })\n  }\n}\nexport default new SalesforceErrorReporter()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-intro-notification.jsx",
    "content": "import React from 'react'\nimport {shell} from 'electron'\n\nimport {Notification} from 'nylas-component-kit'\nimport SalesforceEnv from './salesforce-env'\nimport {INFO_DOC_URL} from './salesforce-constants'\nimport SalesforceActions from './salesforce-actions'\n\nexport default class SalesforceIntroNotification extends React.Component {\n\n  static displayName = \"SalesforceIntroNotification\"\n  static containerRequired = false\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      isLoggedIn: SalesforceEnv.isLoggedIn(),\n    }\n  }\n\n  componentDidMount() {\n    this._unsub = SalesforceEnv.listen(this._onLoginStateChanged)\n  }\n\n  componentWillUnmount() {\n    this._unsub()\n  }\n\n  _onLoginStateChanged = () => {\n    this.setState({isLoggedIn: SalesforceEnv.isLoggedIn()})\n  }\n\n  render() {\n    let actions = [{\n      label: \"Connect Salesforce\",\n      fn: () => {\n        SalesforceActions.loginToSalesforce()\n      },\n    }]\n\n    if (this.state.isLoggedIn) {\n      actions = [{\n        label: \"Learn More\",\n        fn: () => {\n          shell.openExternal(INFO_DOC_URL)\n        },\n      }]\n    }\n\n    return (\n      <Notification\n        priority=\"1\"\n        displayName={SalesforceIntroNotification.displayName}\n        title=\"Welcome to the Nylas Salesforce trial!\"\n        actions={actions}\n        isDismissable\n        isPermanentlyDismissable\n      />\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-metadata-cleanup-listener.es6",
    "content": "import {\n  Thread,\n  Message,\n  Actions,\n  DatabaseStore,\n  SyncbackMetadataTask,\n} from 'nylas-exports'\nimport {PLUGIN_ID} from './salesforce-constants'\nimport SalesforceObject from './models/salesforce-object'\nimport * as mdHelpers from './metadata-helpers'\nimport SyncThreadActivityToSalesforceTask from './tasks/sync-thread-activity-to-salesforce-task'\nimport RemoveManualRelationToSalesforceObjectTask from './tasks/remove-manual-relation-to-salesforce-object-task'\n\n\n/**\n * When sObjects get deleted from the database, we need to cleanup their\n * references in our metadata.\n *\n * If we've manually related sObjects and/or are trying to sync thread\n * activity with them, we need to spawn tasks that clear these when\n * sObjects get deleted.\n *\n */\nclass SalesforceMetadataCleanupListener {\n  constructor() {\n    this._unsubscribers = []\n  }\n\n  activate() {\n    this._unsubscribers = [\n      DatabaseStore.listen(this._onDataChanged),\n    ]\n  }\n\n  deactivate() {\n    this._unsubscribers.forEach((usub) => usub())\n  }\n\n  _onDataChanged = (change) => {\n    if (change.objectClass !== SalesforceObject.name) return;\n    if (change.type !== 'unpersist') {\n      this._onSObjectsDeleted(change.objects)\n    }\n  }\n\n  _onSObjectsDeleted = (deletedSObjects) => {\n    DatabaseStore.findAll(Thread)\n    .where(Thread.attributes.pluginMetadata.contains(PLUGIN_ID))\n    .then((threads) => {\n      for (const thread of threads) {\n        for (const deletedSObject of deletedSObjects) {\n          this._cleanupThread(thread, deletedSObject)\n        }\n      }\n    })\n\n    DatabaseStore.findAll(Message)\n    .where(Message.attributes.pluginMetadata.contains(PLUGIN_ID))\n    .then((messages) => {\n      for (const message of messages) {\n        for (const deletedSObject of deletedSObjects) {\n          this._cleanupMessage(message, deletedSObject)\n        }\n      }\n    })\n  }\n\n  // If we're syncing a thread with an sObject that just got deleted, stop\n  // syncing with that sObject.\n  _cleanupThread = (thread, deletedSObject) => {\n    // A thread might be manually related to the recently deleted sObject.\n    // Be sure to clean that up\n    if (mdHelpers.getManuallyRelatedObjects(thread)[deletedSObject.id]) {\n      const t = new RemoveManualRelationToSalesforceObjectTask({\n        sObjectId: deletedSObject.id,\n        sObjectType: deletedSObject.type,\n        nylasObjectId: thread.id,\n        nylasObjectType: thread.type,\n      })\n      Actions.queueTask(t)\n    } else if (mdHelpers.getSObjectsToSyncActivityTo(thread)[deletedSObject.id]) {\n      // Note, this is an ELSE if, because the\n      // RemoveManualRelationToSalesforceObjectTask automatically checks\n      // if we've enabled sync witht that thread. If so it'll do the same\n      // thing and mark the thread to stop syncing.\n      //\n      // It's common for us to be syncing with threads that were\n      // automatically related and have no records in our \"manually\n      // related\" objects.\n      const t = new SyncThreadActivityToSalesforceTask({\n        threadId: thread.id,\n        threadClientId: thread.clientId,\n        sObjectsToStopSyncing: [deletedSObject],\n      })\n      Actions.queueTask(t)\n    }\n    return Promise.resolve()\n  }\n\n  _cleanupMessage = (message, deletedSObject) => {\n    const relatedToId = mdHelpers.relatedIdForClonedSObject(message, deletedSObject)\n    if (relatedToId) {\n      const relatedSObject = {id: relatedToId}\n      const sObjectToRemove = {id: relatedToId}\n      mdHelpers.removeClonedSObject(message, relatedSObject, sObjectToRemove);\n      return DatabaseStore.inTransaction(t => t.persistModel(message))\n      .then(() => {\n        const t = new SyncbackMetadataTask(message.clientId, message.constructor.name, PLUGIN_ID);\n        Actions.queueTask(t);\n      })\n    }\n    return Promise.resolve()\n  }\n}\nexport default new SalesforceMetadataCleanupListener()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-new-mail-listener.es6",
    "content": "import _ from 'underscore';\n\nimport NylasStore from 'nylas-store';\nimport { Thread, Actions, DatabaseStore } from 'nylas-exports';\n\nimport SalesforceEnv from './salesforce-env';\n\nimport SyncThreadActivityToSalesforceTask from './tasks/sync-thread-activity-to-salesforce-task';\n\nimport * as mdHelpers from './metadata-helpers'\n\nclass SalesforceNewMailListener extends NylasStore {\n\n  activate() {\n    this.listenTo(Actions.onNewMailDeltas, this._newMailReceived);\n    this.listenTo(Actions.draftDeliverySucceeded, this._onSendDraftSuccess);\n  }\n\n  deactivate() {\n    return this.stopListeningToAll();\n  }\n\n  _ensureThreadSynced = (thread) => {\n    if (!thread) return;\n    const ids = Object.keys(mdHelpers.getSObjectsToSyncActivityTo(thread));\n    if (ids.length === 0) return;\n    const task = new SyncThreadActivityToSalesforceTask({\n      threadId: thread.id, threadClientId: thread.clientId,\n    });\n    Actions.queueTask(task);\n  }\n\n  /**\n   * For replies, the thread will exist. For new messages, the thread will\n   * not exist, but will come in shortly via\n   * `didPassivelyReceivedNewModels`.\n   */\n  _onSendDraftSuccess = ({message}) => {\n    return DatabaseStore.find(Thread, message.threadId)\n    .then(this._ensureThreadSynced)\n  }\n\n  _newMailReceived = (incoming) => {\n    if (!SalesforceEnv.isLoggedIn()) return;\n    if (!incoming.message || incoming.message.length <= 0) { return; }\n    const tids = _.pluck(incoming.message, \"threadId\");\n    const incomingThreads = incoming.thread || [];\n    Promise.map(tids, (tid) => {\n      const thread = _.findWhere(incomingThreads, {id: tid});\n      if (thread) return thread;\n      return DatabaseStore.find(Thread, tid)\n    }).each(this._ensureThreadSynced);\n  }\n}\n\nexport default new SalesforceNewMailListener();\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-oauth.jsx",
    "content": "import {shell} from 'electron'\nimport crypto from 'crypto';\nimport {Actions, NylasAPIRequest, React, KeyManager} from 'nylas-exports'\n\nconst ACCESS_TOKEN_KEY_NAME = 'Nylas Salesforce Token';\nconst REFRESH_TOKEN_KEY_NAME = 'Nylas Salesforce Refresh Token';\n\nclass SalesforceOAuth {\n  constructor() {\n    this._onConfigChanged()\n    this._resetDelay()\n    this.MAX_POLLS = 100;\n    this._connectionAttempt = 0\n  }\n\n  activate() {\n    this._usub = NylasEnv.config.onDidChange('env', this._onConfigChanged)\n  }\n\n  deactivate() {\n    this._usub.dispose()\n  }\n\n  connect() {\n    if (NylasEnv.getLoadSettings().isSpec) { return Promise.resolve() }\n    this._connectionAttempt += 1\n    this._resetDelay()\n    Actions.recordUserEvent(\"Salesforce Connect Started\")\n    shell.openExternal(`${this.APIRoot}/connect/salesforce?state=${this.state}`)\n    this._numPolls = 0;\n    return this._pollForToken(this._connectionAttempt)\n  }\n\n  fetchNewToken() {\n    const req = new NylasAPIRequest({\n      api: this,\n      options: {\n        path: `/salesforce/token/refresh`,\n        method: \"POST\",\n        body: {refresh_token: this.refreshToken()},\n        auth: {user: \"\", pass: \"\", sendImmediately: true},\n      },\n    });\n    return req.run().then((tokenData) => {\n      const configData = this._extractAndSetTokens(tokenData)\n\n      const oldConfig = NylasEnv.config.get(\"salesforce\") || {}\n      NylasEnv.config.set(\"salesforce\", Object.assign({}, oldConfig, configData))\n      Actions.recordUserEvent(\"Salesforce Token Refreshed\", {\n        instanceUrl: configData.instance_url,\n      })\n    })\n  }\n\n  clearTokens() {\n    KeyManager.deletePassword(ACCESS_TOKEN_KEY_NAME);\n    KeyManager.deletePassword(REFRESH_TOKEN_KEY_NAME);\n  }\n\n  accessToken() {\n    return KeyManager.getPassword(ACCESS_TOKEN_KEY_NAME, {migrateFromService: \"Nylas Salesforce\"})\n  }\n\n  refreshToken() {\n    return KeyManager.getPassword(REFRESH_TOKEN_KEY_NAME, {migrateFromService: \"Nylas Salesforce\"})\n  }\n\n  _resetDelay() {\n    this.state = (new Buffer(crypto.randomBytes(40))).toString('base64');\n    this.delay = 1000;\n    if (this.currentTimeout) clearTimeout(this.currentTimeout);\n  }\n\n  _onConfigChanged = () => {\n    const env = NylasEnv.config.get('env')\n    if (['development', 'local'].includes(env)) {\n      this.APIRoot = \"http://localhost:3000\"\n    } else if (env === 'staging') {\n      this.APIRoot = \"https://nylas-salesforce.herokuapp.com\"\n    } else {\n      this.APIRoot = \"https://nylas-salesforce.herokuapp.com\"\n    }\n  }\n\n  _onConnection = (tokenData) => {\n    Actions.recordUserEvent(\"Salesforce Connected\", {\n      instanceUrl: tokenData.instance_url,\n    })\n\n    const configData = this._extractAndSetTokens(tokenData)\n\n    NylasEnv.config.set(\"salesforce\", configData)\n\n    NylasEnv.show()\n    Actions.openModal({\n      component: (\n        <div className=\"salesforce-welcome\" tabIndex=\"0\">\n          <h2>Success! Nylas Mail and Salesforce are now connected.</h2>\n          <p>Select a message to create or edit contact and lead records or to sync the thread with an opportunity. Here&rsquo;s how it works!</p>\n          <iframe width=\"560\" height=\"315\" src=\"https://www.youtube.com/embed/5ziK7lCdTjA\" />\n        </div>\n      ),\n      height: 520,\n      width: 700,\n    })\n  }\n\n  _extractAndSetTokens = (tokenData) => {\n    const clonedData = Object.assign({}, tokenData);\n    const accessToken = tokenData.access_token\n    const refreshToken = tokenData.refresh_token\n    delete clonedData.access_token\n    delete clonedData.refresh_token\n\n    if (accessToken) {\n      KeyManager.replacePassword(ACCESS_TOKEN_KEY_NAME, accessToken)\n    }\n\n    if (refreshToken) {\n      KeyManager.replacePassword(REFRESH_TOKEN_KEY_NAME, refreshToken)\n    }\n\n    return clonedData\n  }\n\n  _pollForToken(connectionAttempt) {\n    const req = new NylasAPIRequest({\n      api: this,\n      options: {\n        auth: {user: \"\", pass: \"\", sendImmediately: true},\n        path: `/salesforce/token?state=${this.state}`,\n      },\n    });\n    return req.run()\n    .then(this._onConnection)\n    .catch((apiError) => {\n      if (apiError.statusCode === 404) {\n        if (this._connectionAttempt === connectionAttempt) {\n          return this._tryAgain(() => this._pollForToken(connectionAttempt))\n        }\n        return Promise.resolve()\n      }\n      return Promise.reject(apiError)\n    })\n  }\n\n  _tryAgain(fn) {\n    return new Promise((resolve, reject) => {\n      if (this._numPolls > this.MAX_POLLS) {\n        reject()\n        return\n      }\n      this.currentTimeout = setTimeout(() => {\n        fn.call(this).then(resolve).catch(reject)\n      }, this.delay);\n    })\n  }\n}\n\nexport default new SalesforceOAuth();\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-object-helpers.es6",
    "content": "import _ from 'underscore'\nimport moment from 'moment';\nimport querystring from \"querystring\";\nimport {DatabaseStore} from 'nylas-exports'\n\nimport SalesforceEnv from './salesforce-env';\nimport SalesforceAPI from './salesforce-api';\nimport SalesforceObject from './models/salesforce-object'\nimport SalesforceActions from './salesforce-actions'\n\nimport * as mdHelpers from './metadata-helpers'\n\nfunction _defaultFieldMapping() {\n  return {\n    Id: \"id\",\n    Name: \"name\",\n    LastModifiedDate: \"updatedAt\",\n  };\n}\n\nfunction _fieldMapping() {\n  return {\n    User: _defaultFieldMapping(),\n    Account: _defaultFieldMapping(),\n    Opportunity: {\n      Id: \"id\",\n      Name: \"name\",\n      AccountId: \"relatedToId\",\n      LastModifiedDate: \"updatedAt\",\n    },\n    Contact: {\n      Id: \"id\",\n      Name: \"name\",\n      Email: \"identifier\",\n      AccountId: \"relatedToId\",\n      LastModifiedDate: \"updatedAt\",\n    },\n    Lead: {\n      Id: \"id\",\n      Name: \"name\",\n      Email: \"identifier\",\n      LastModifiedDate: \"updatedAt\",\n    },\n    Case: {\n      Id: \"id\",\n      Subject: \"name\",\n      CaseNumber: \"identifier\",\n      AccountId: \"relatedToId\",\n      LastModifiedDate: \"updatedAt\",\n    },\n    EmailMessage: {\n      Id: \"id\",\n      Subject: \"identifier\",\n      LastModifiedDate: \"updatedAt\",\n    },\n    OpportunityContactRole: {\n      Id: \"id\",\n      ContactId: \"identifier\",\n      OpportunityId: \"relatedToId\",\n      LastModifiedDate: \"updatedAt\",\n    },\n  };\n}\n\nfunction _rawSalesforceDataAdapter(rawData, objectType) {\n  if (!objectType) {\n    console.error(rawData);\n    throw new Error(\"Requested Salesforce object does not have a objectType\");\n  }\n\n  let fieldMapping = _fieldMapping()[objectType];\n  if (!fieldMapping) { fieldMapping = _defaultFieldMapping(); }\n\n  let attrs = {};\n  if (_.isFunction(fieldMapping)) {\n    attrs = fieldMapping(rawData)\n  } else {\n    for (const sfKey of Object.keys(fieldMapping)) {\n      const nyKey = fieldMapping[sfKey];\n      let val;\n      if (nyKey === \"updatedAt\") {\n        val = moment(rawData[sfKey]).toDate();\n      } else if (nyKey === \"identifier\") {\n        if (sfKey === \"Email\") {\n          val = (rawData[sfKey] || \"\").toLowerCase().trim();\n        } else {\n          val = rawData[sfKey];\n        }\n      } else {\n        val = rawData[sfKey];\n      }\n\n      attrs[nyKey] = val;\n    }\n  }\n\n  const obj = new SalesforceObject(Object.assign(attrs, {\n    type: objectType,\n    object: \"SalesforceObject\",\n  }))\n\n  return obj;\n}\n\nexport function newBasicObjectsQuery(objectType, where = \"\", fields = []) {\n  let fieldsStr = \"\"\n  if (fields.length === 0) {\n    let fieldMapping = _fieldMapping()[objectType];\n    if (!fieldMapping) {\n      fieldMapping = _defaultFieldMapping();\n    }\n    fieldsStr = Object.keys(fieldMapping).join(',');\n  } else {\n    fieldsStr = fields.join(',')\n  }\n  return querystring.stringify({q: `SELECT ${fieldsStr} FROM ${objectType} WHERE ${where}`});\n}\n\nexport function loadBasicObjectsByField({objectType, where = {}, fields = []}) {\n  if (!SalesforceEnv.isLoggedIn()) { return Promise.resolve(); }\n  const wheres = []\n  for (const field of Object.keys(where)) {\n    wheres.push(`${field} = '${where[field]}'`)\n  }\n  const whereStr = wheres.join(\" AND \");\n  const query = newBasicObjectsQuery(objectType, whereStr, fields);\n  return SalesforceAPI.makeRequest({path: `/query/?${query}`});\n}\n\nexport function requestFullObjectFromAPI({objectType, objectId}) {\n  return SalesforceAPI.makeRequest({\n    path: `/sobjects/${objectType}/${objectId}`})\n  .then((rawFullData) => {\n    const obj = _rawSalesforceDataAdapter(rawFullData, objectType);\n    // Note: The presence of rawData being filled is what makes this a\n    // \"full\" object instead of a \"basic\" object.\n    obj.rawData = rawFullData;\n    return DatabaseStore.inTransaction(t => t.persistModel(obj).then(() => obj));\n  })\n  .catch((apiError = {}) => {\n    if (apiError.statusCode !== 404) {\n      // We don't re-throw since we've already reported the error and\n      // don't want to take down the app at this point.\n      SalesforceActions.reportError(apiError, {objectType, objectId})\n      return null\n    }\n    return null\n  });\n}\n\n// Attempts to fetch the given object from the Database. If the `rawData`\n// field isn't populated or if the object doesn't exist, then we grab it\n// from Salesforce\nexport function loadFullObject({objectType, objectId}) {\n  return DatabaseStore.findBy(SalesforceObject, {id: objectId, type: objectType})\n  .then((object = {}) => {\n    if (object && object.rawData && _.size(object.rawData) > 0) { return object; }\n    return requestFullObjectFromAPI({objectType, objectId});\n  });\n}\n\nexport function loadManuallyRelatedObjects(nylasObject) {\n  const sObjects = _.values(mdHelpers.getManuallyRelatedObjects(nylasObject));\n  return Promise.map(sObjects, (sObject) => {\n    return loadFullObject({objectType: sObject.type, objectId: sObject.id});\n  });\n}\n\n\n// Supports an array of objectTypes. This is useful when trying to look\n// up an object that may be a reference to multiple things\nexport function loadBasicObject(objectTypes, objectId) {\n  let types = objectTypes;\n  if (_.isString(types)) types = [objectTypes];\n  return DatabaseStore.findBy(SalesforceObject, {id: objectId, type: types})\n  .then(object => {\n    if (object) { return object; }\n    return Promise.map(types, (type) => {\n      return requestFullObjectFromAPI({objectType: type, objectId});\n    }).then((objects = []) => {\n      // There's only 1 ID, but we're searching across multiple object\n      // types. There should only be 1 value returned.\n      return _.compact(objects)[0]\n    })\n  });\n}\n\nexport function upsertBasicObjects(data = {}) {\n  const records = data.records || []\n  if (records.length === 0) { return Promise.resolve([]); }\n  try {\n    const models = records.map((rawBasicData) => {\n      const objectType = rawBasicData.attributes.type;\n      return _rawSalesforceDataAdapter(rawBasicData, objectType)\n    });\n\n    if (models.length === 0) return Promise.resolve([]);\n\n    return DatabaseStore.inTransaction(t => t.persistModels(models))\n    .thenReturn(models)\n  } catch (err) {\n    SalesforceActions.reportError(err, {rawApiData: data})\n    return Promise.reject(err)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-related-object-cache.es6",
    "content": "import _ from 'underscore'\nimport { Rx, AccountStore, DatabaseStore } from 'nylas-exports'\nimport SalesforceObject from './models/salesforce-object'\n\nclass SalesforceRelatedObjectCache {\n\n  constructor() {\n    this._unsubscribers = []\n    this._observers = {}\n    this._sObjectsByEmail = new Map();\n    this._changes = []\n    this._updateCache = _.debounce(this._updateCache, 100);\n  }\n\n  activate() {\n    this._initializeCache()\n    this._unsubscribers = [\n      DatabaseStore.listen(this._onDataChanged),\n    ]\n  }\n\n  directlyRelatedSObjectsForThread(thread) {\n    const objs = {}\n    const myEmails = AccountStore.emailAddresses().map((em) => em.toLowerCase())\n    for (const participant of thread.participants) {\n      if (myEmails.includes(participant.email.toLowerCase())) continue;\n      Object.assign(objs, this.directlyRelatedSObjectsByEmail(participant.email))\n    }\n    return objs;\n  }\n\n  directlyRelatedSObjectsByEmail(rawEmail) {\n    const email = rawEmail.trim().toLowerCase()\n    return this._sObjectsByEmail.get(email) || {}\n  }\n\n  observeDirectlyRelatedSObjectsByEmail(email) {\n    return Rx.Observable.create((observer) => {\n      if (!this._observers[email]) this._observers[email] = [];\n      this._observers[email].push({\n        observer: observer,\n        observerType: \"email\",\n      })\n      observer.onNext(this.directlyRelatedSObjectsByEmail(email))\n      return Rx.Disposable.create(() => {\n        if (!this._observers[email]) return;\n        this._observers[email] = this._observers[email].filter(obs => obs.observer !== observer)\n        if (this._observers[email].length === 0) {\n          delete this._observers[email]\n        }\n      })\n    })\n  }\n\n  observeDirectlyRelatedSObjectsForThread(thread) {\n    return Rx.Observable.create((observer) => {\n      const myEmails = AccountStore.emailAddresses().map((em) => em.toLowerCase())\n      observer.alsoFireFor = thread.participants\n      for (const participant of thread.participants) {\n        if (myEmails.includes(participant.email.toLowerCase())) continue;\n        if (!this._observers[participant.email]) {\n          this._observers[participant.email] = []\n        }\n        this._observers[participant.email].push({\n          observer: observer,\n          observerType: \"thread\",\n          thread: thread,\n        })\n      }\n\n      observer.onNext(this.directlyRelatedSObjectsForThread(thread))\n\n      return Rx.Disposable.create(() => {\n        for (const participant of thread.participants) {\n          if (!this._observers[participant.email]) {\n            continue\n          }\n          this._observers[participant.email] = this._observers[participant.email].filter(obs => obs.observer !== observer)\n          if (this._observers[participant.email].length === 0) {\n            delete this._observers[participant.email]\n          }\n        }\n      })\n    })\n  }\n\n  _initializeCache() {\n    return DatabaseStore.findAll(SalesforceObject, {type: \"Contact\"})\n    .then(this._updateContacts)\n  }\n\n  _onDataChanged = (change) => {\n    if (change.objectClass !== SalesforceObject.name) return\n    this._changes = this._changes.concat(change);\n    this._updateCache()\n  }\n\n  _getBasicObject = (sObject) => {\n    const objForCache = Object.assign({}, sObject);\n    delete objForCache.rawData;\n    objForCache.id = sObject.id;\n    return objForCache\n  }\n\n  _changeObjectInCache = (sObject, rawEmail, changeType) => {\n    if (!rawEmail || !sObject) return\n    const email = rawEmail.trim().toLowerCase()\n    let relatedObjects = this._sObjectsByEmail.get(email)\n    const objectToUpdate = this._getBasicObject(sObject)\n    if (!relatedObjects) relatedObjects = {}\n    if (changeType === \"unpersist\") {\n      delete relatedObjects[sObject.id]\n    } else {\n      relatedObjects[sObject.id] = objectToUpdate\n    }\n    this._sObjectsByEmail.set(email, relatedObjects)\n    if (this._observers[email]) {\n      for (const {observer, observerType, thread} of this._observers[email]) {\n        if (observerType === \"email\") {\n          observer.onNext(this.directlyRelatedSObjectsByEmail(email))\n        } else if (observerType === \"thread\") {\n          observer.onNext(this.directlyRelatedSObjectsForThread(thread))\n        }\n      }\n    }\n  }\n\n  // Contact: Find accounts and opportunities using email from contacts\n  // and add to cache. This is optimized for many contacts at once since\n  // we rebuild the cache on every launch.\n  _updateContacts = (contacts = [], changeType) => {\n    if (contacts.length === 0) return Promise.resolve()\n    const contactIds = _.compact(_.pluck(contacts, \"id\"))\n    const relatedToIds = _.compact(_.pluck(contacts, \"relatedToId\"))\n\n    const objectsToUpdate = {}\n    for (const contact of contacts) {\n      objectsToUpdate[contact.identifier] = [contact]\n    }\n\n    if (relatedToIds.length === 0) {\n      for (const email of Object.keys(objectsToUpdate)) {\n        for (const sObject of objectsToUpdate[email]) {\n          this._changeObjectInCache(sObject, email, changeType)\n        }\n      }\n      return Promise.resolve()\n    }\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"Account\",\n      id: relatedToIds,\n    })\n    .then((accounts) => {\n      const accById = _.groupBy(accounts, \"id\");\n      for (const contact of contacts) {\n        const account = (accById[contact.relatedToId] || [])[0]\n        if (!account) continue;\n        objectsToUpdate[contact.identifier].push(account)\n      }\n      return Promise.resolve()\n    })\n    .then(() => {\n      if (contactIds.length === 0) return {opportunityContactRoles: []}\n      return DatabaseStore.findAll(SalesforceObject, {\n        type: \"OpportunityContactRole\",\n        identifier: contactIds,\n      })\n    })\n    .then((opportunityContactRoles = []) => {\n      const oppIds = _.compact(_.pluck(opportunityContactRoles, \"relatedToId\"));\n      if (oppIds.length === 0) return {opportunities: [], opportunityContactRoles}\n      return DatabaseStore.findAll(SalesforceObject, {\n        type: \"Opportunity\",\n        id: oppIds,\n      }).then((opportunities = []) => {\n        return {opportunities, opportunityContactRoles}\n      })\n    })\n    .then(({opportunities, opportunityContactRoles}) => {\n      const roleByCid = _.groupBy(opportunityContactRoles, \"identifier\");\n      const oppById = _.groupBy(opportunities, \"id\")\n\n      for (const contact of contacts) {\n        const role = (roleByCid[contact.id] || [])[0]\n        if (!role) continue;\n        const opp = (oppById[role.relatedToId] || [])[0];\n        if (!opp) continue;\n        objectsToUpdate[contact.identifier].push(opp)\n      }\n    })\n    .then(() => {\n      for (const email of Object.keys(objectsToUpdate)) {\n        for (const sObject of objectsToUpdate[email]) {\n          this._changeObjectInCache(sObject, email, changeType)\n        }\n      }\n    })\n  }\n\n  // Account: Add accounts to cache using email from contact\n  _updateAccounts = (accounts = [], changeType) => {\n    if (accounts.length === 0) return Promise.resolve();\n    const aids = _.pluck(accounts, \"id\");\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"Contact\",\n      relatedToId: aids,\n    }).then((contacts) => {\n      const accById = _.groupBy(accounts, \"id\");\n      for (const contact of contacts) {\n        const account = (accById[contact.relatedToId] || [])[0]\n        this._changeObjectInCache(account, contact.identifier, changeType)\n      }\n    })\n  }\n\n  // Opportunity: Add opportunities to cache using email from contact\n  _updateOpportunities = (opportunities = [], changeType) => {\n    if (opportunities.length === 0) return Promise.resolve();\n    const oids = _.pluck(opportunities, \"id\");\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"OpportunityContactRole\",\n      relatedToId: oids,\n    })\n    .then((opportunityContactRoles) => {\n      const contactIds = _.pluck(opportunityContactRoles, \"identifier\");\n      const roleByCid = _.groupBy(opportunityContactRoles, \"identifier\");\n      const oppById = _.groupBy(opportunities, \"id\");\n      return DatabaseStore.findAll(SalesforceObject, {\n        type: \"Contact\",\n        id: contactIds,\n      }).then((contacts) => {\n        for (const contact of contacts) {\n          const role = (roleByCid[contact.id] || [])[0]\n          if (!role) continue;\n          const opp = (oppById[role.relatedToId] || [])[0];\n          if (!opp) continue;\n          this._changeObjectInCache(opp, contact.identifier, changeType)\n        }\n      })\n    })\n  }\n\n  // OpportunityContactRole: Add opportunities to cache using email from contact\n  _updateOpportunityContactRoles = (opportunityContactRoles = [], changeType) => {\n    if (opportunityContactRoles.length === 0) return Promise.resolve();\n    const cids = _.pluck(opportunityContactRoles, \"identifier\");\n    const roleByCid = _.groupBy(opportunityContactRoles, \"identifier\");\n    const oppIds = _.pluck(opportunityContactRoles, \"relatedToId\")\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"Contact\",\n      id: cids,\n    })\n    .then((contacts) => {\n      return DatabaseStore.findAll(SalesforceObject, {\n        type: \"Opportunity\",\n        id: oppIds,\n      })\n      .then((opportunities) => {\n        const oppById = _.groupBy(opportunities, \"id\");\n        for (const contact of contacts) {\n          const role = (roleByCid[contact.id] || [])[0]\n          if (!role) continue;\n          const opp = (oppById[role.relatedToId] || [])[0];\n          if (!opp) continue;\n          this._changeObjectInCache(opp, contact.identifier, changeType)\n        }\n      })\n    })\n  }\n\n  _updateLeads = (leads = [], changeType) => {\n    if (leads.length === 0) return Promise.resolve()\n    for (const lead of leads) {\n      this._changeObjectInCache(lead, lead.identifier, changeType)\n    }\n    return Promise.resolve();\n  }\n\n  /*\n  The cache is keyed by email and the values represent the related Salesforce\n  objects (opportunities and accounts).\n\n  This method id debounced and loads the latest from _changes.\n\n  this._sObjectsByEmail = {\n    \"jackie@nylas.com\": {\n      \"ACCOUNT_ID\": {\n        name: \"\",\n        type: \"\",\n        identifier: \"\",\n        relatedToId: \"\",\n      },\n      \"OPPORTUNITY_ID\": {\n        ...\n      }\n    }\n  }\n  */\n  _updateCache = () => {\n    const changes = this._changes;\n    this._changes = [];\n    const changeByType = _.groupBy(changes, \"type\");\n    for (const changeType of Object.keys(changeByType)) {\n      const sObjects = _.flatten(changeByType[changeType].map(c => c.objects));\n      if (sObjects.length === 0) continue;\n      const objsByType = _.groupBy(sObjects, \"type\");\n      Promise.all([\n        this._updateLeads(objsByType.Lead, changeType),\n        this._updateContacts(objsByType.Contact, changeType),\n        this._updateAccounts(objsByType.Account, changeType),\n        this._updateOpportunities(objsByType.Opportunity, changeType),\n        this._updateOpportunityContactRoles(objsByType.OpportunityContactRole, changeType),\n      ])\n    }\n  }\n\n  deactivate() {\n    this._unsubscribers.forEach((usub) => usub())\n  }\n}\n\n\nexport default new SalesforceRelatedObjectCache()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/salesforce-sync-worker.es6",
    "content": "import {DatabaseStore, Actions} from 'nylas-exports'\nimport SalesforceEnv from './salesforce-env'\nimport SalesforceActions from './salesforce-actions'\nimport SalesforceObject from './models/salesforce-object'\nimport SalesforceDataReset from './salesforce-data-reset'\nimport SyncSalesforceObjectsTask from './tasks/sync-salesforce-objects-task'\n\n\n// How often we poll Salesforce and pull down all new objects (in sec)\nconst REFRESH_INTERVAL = 1000 * 60 * 10; // (10 minutes)\n\n// The list of objects we optimistically pull down a full set of\nconst ObjectTypes = [\n  \"User\",\n  \"Case\",\n  \"Contact\",\n  \"Account\",\n  \"Opportunity\",\n  \"OpportunityContactRole\",\n]\n\nfunction getMostRecentUpdateTime(objectType) {\n  return DatabaseStore.findBy(SalesforceObject, {type: objectType})\n  .order(SalesforceObject.attributes.updatedAt.descending())\n  .limit(1)\n  .then((obj = {}) => obj.updatedAt);\n}\n\nclass SalesforceSyncWorker {\n\n  activate() {\n    this._disposables = [\n      NylasEnv.config.onDidChange('salesforce.id', this._resetLocalData),\n    ]\n    this._unsubscribers = [\n      SalesforceActions.syncSalesforce.listen(this._run),\n      SalesforceActions.logoutOfSalesforce.listen(this._onLogout),\n    ]\n\n    this._interval = setInterval(this._run, REFRESH_INTERVAL);\n\n    // Give the app time to bootup before queuing these resource-intensive\n    // tasks.\n    setTimeout(this._run, 3000)\n  }\n\n  deactivate() {\n    this._disposables.forEach((disp) => disp.dispose())\n    this._unsubscribers.forEach((usub) => usub())\n    clearInterval(this._interval)\n  }\n\n  _run = () => {\n    if (!SalesforceEnv.isLoggedIn()) { return; }\n    ObjectTypes.forEach((objectType) => {\n      getMostRecentUpdateTime(objectType)\n      .then((lastUpdateTime = 0) => {\n        const task = new SyncSalesforceObjectsTask({objectType, lastUpdateTime})\n        Actions.queueTask(task)\n      })\n    })\n  }\n\n  _onLogout = () => {\n    clearInterval(this._interval)\n  }\n\n  _resetLocalData = () => {\n    if (!SalesforceEnv.isLoggedIn()) { return Promise.resolve(); }\n\n    clearInterval(this._interval)\n    this._interval = setInterval(this._run, REFRESH_INTERVAL);\n    return SalesforceDataReset.deleteAllData().then(this._run);\n  }\n}\n\nexport default new SalesforceSyncWorker()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/search/salesforce-search-bar-results.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport {Rx, Thread, DatabaseStore} from 'nylas-exports'\nimport SalesforceIcon from '../shared-components/salesforce-icon'\nimport SalesforceObject from '../models/salesforce-object'\nimport {relatedSObjectsForThread} from '../related-object-helpers'\n\nfunction SearchBarResult(sObject) {\n  return (\n    <span className=\"salesforce-search-bar-result\">\n      <SalesforceIcon objectType={sObject.type} />\n      <span>{sObject.name}</span>\n    </span>\n  )\n}\n\nfunction _searchObjects(name) {\n  return DatabaseStore.findAll(SalesforceObject).search(name)\n}\n\nlet idleCallback = -1;\n\nfunction _forAllPages(fn, offset = 0) {\n  const SERACH_SIZE = 100;\n  return DatabaseStore.findAll(Thread)\n  .limit(SERACH_SIZE)\n  .offset(offset)\n  .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n  .then((threads) => {\n    if (!fn(threads)) return;\n    window.cancelIdleCallback(idleCallback)\n    idleCallback = window.requestIdleCallback(() => {\n      _forAllPages(fn, offset + SERACH_SIZE);\n    })\n  })\n}\n\nexport default class SalesforceSearchBarResults extends React.Component {\n  static displayName = \"SalesforceSearchBarResults\";\n\n  static searchLabel() { return \"Salesforce Objects\" }\n\n  static fetchSearchSuggestions(searchQuery) {\n    return Promise.map(_searchObjects(searchQuery), (sObject) => {\n      return {\n        customElement: SearchBarResult(sObject),\n        label: sObject.id,\n        value: sObject.name,\n      }\n    })\n  }\n\n  static observeThreadIdsForQuery(searchQuery) {\n    let cancelPagination = false;\n    return Rx.Observable.create((observer) => {\n      _searchObjects(searchQuery)\n      .then((sObjects => {\n        if (sObjects.length === 0) {\n          observer.onCompleted();\n          return;\n        }\n        const ids = new Set(sObjects.map(o => o.id))\n        _forAllPages((threads) => {\n          if (cancelPagination) return false;\n          if (threads.length === 0) return false;\n          const threadIds = threads.filter((thread) => {\n            return _.any(relatedSObjectsForThread(thread),\n              (sObject) => ids.has(sObject.id))\n          }).map(t => t.id);\n          observer.onNext(threadIds);\n          return true;\n        })\n      }))\n      return Rx.Disposable.create(() => {\n        window.cancelIdleCallback(idleCallback)\n        cancelPagination = true;\n      })\n    })\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/search/salesforce-search-indexer.es6",
    "content": "import { ModelSearchIndexer } from 'nylas-exports';\nimport SalesforceObject from '../models/salesforce-object'\n\nclass SalesforceSearchIndexer extends ModelSearchIndexer {\n\n  get MaxIndexSize() {\n    return 10000\n  }\n\n  get ConfigKey() {\n    return \"salesforce.searchIndexVersion\"\n  }\n\n  get IndexVersion() {\n    return 1\n  }\n\n  get ModelClass() {\n    return SalesforceObject\n  }\n\n  getIndexDataForModel(sObject) {\n    return {\n      content: [\n        sObject.name ? sObject.name : '',\n        sObject.identifier ? sObject.identifier : '',\n        sObject.identifier ? sObject.identifier.replace('@', ' ') : '',\n      ].join(' '),\n    };\n  }\n}\n\nexport default new SalesforceSearchIndexer()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/shared-components/open-in-salesforce-btn.jsx",
    "content": "import React from 'react'\nimport {shell} from 'electron'\nimport {RetinaImg} from 'nylas-component-kit'\nimport SalesforceEnv from '../salesforce-env'\n\nexport default function OpenInSalesforceBtn({objectId, size = \"small\"}) {\n  const openLink = (event) => {\n    event.stopPropagation()\n    event.preventDefault()\n    shell.openExternal(`${SalesforceEnv.instanceUrl()}/${objectId}`)\n  }\n\n  return (\n    <div\n      className={`open-in-salesforce-btn action-icon ${size}`}\n      onClick={openLink}\n      title=\"Open in Salesforce.com\"\n    >\n      <RetinaImg\n        mode={RetinaImg.Mode.ContentPreserve}\n        url={`nylas://nylas-private-salesforce/static/images/ic-salesforce-cloud-btn-${size}@2x.png`}\n      />\n    </div>\n  )\n}\nOpenInSalesforceBtn.propTypes = {\n  objectId: React.PropTypes.string,\n  size: React.PropTypes.string,\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/shared-components/salesforce-icon.jsx",
    "content": "import React from 'react'\nimport {RetinaImg} from 'nylas-component-kit'\n\nexport default function SalesforceIcon(props = {}) {\n  const DEFAULT_COLOR = \"#8199af\"\n  const {objectType, className, onClick} = props\n  // See https://www.lightningdesignsystem.com/icons/\n  const type = objectType.toLowerCase();\n  const colorMap = {\n    \"lead\": \"#f88962\",\n    \"task\": \"#4bc076\",\n    \"case\": \"#f2cf5b\",\n    \"account\": \"#7f8de1\",\n    \"contact\": \"#a094ed\",\n    \"pending\": DEFAULT_COLOR,\n    \"opportunity\": \"#fcb95b\",\n    \"lead_convert\": \"#f88962\",\n    \"emailmessage\": \"#95aec5\",\n  }\n  const clickFn = onClick || (() => {});\n  const color = props.pending ? DEFAULT_COLOR : (colorMap[type] || DEFAULT_COLOR);\n  return (\n    <span\n      onClick={clickFn}\n      title={props.title || \"\"}\n      className={`sf-icon-wrap sf-icon-wrap-${type} ${className || \"\"}`}\n      style={{backgroundColor: color}}\n    >\n      <RetinaImg\n        className=\"sf-icon-img\"\n        mode={RetinaImg.Mode.ContentPreserve}\n        url={`nylas://nylas-private-salesforce/static/images/icons/${type}_120.png`}\n      />\n    </span>\n  )\n}\nSalesforceIcon.propTypes = {\n  title: React.PropTypes.string,\n  pending: React.PropTypes.bool,\n  onClick: React.PropTypes.func,\n  className: React.PropTypes.string,\n  objectType: React.PropTypes.string.isRequired,\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/shared-components/salesforce-login-prompt.jsx",
    "content": "import React from 'react'\nimport {shell} from 'electron'\nimport {RetinaImg} from 'nylas-component-kit'\n\nimport {INFO_DOC_URL} from '../salesforce-constants'\nimport SalesforceActions from '../salesforce-actions'\n\n\nclass SalesforceLoginPrompt extends React.Component {\n  static displayName = \"SalesforceLoginPrompt\"\n\n  _connectSalesforce() {\n    SalesforceActions.loginToSalesforce()\n  }\n\n  render() {\n    const onClick = () => shell.openExternal(INFO_DOC_URL)\n    return (\n      <div className=\"salesforce-login salesforce\">\n        <div onClick={this._connectSalesforce} className=\"salesforce-no-connect-placeholder\">\n          <RetinaImg\n            url=\"nylas://nylas-private-salesforce/static/images/salesforce-logo@2x.png\"\n            className=\"salesforce-empty-img\"\n            mode={RetinaImg.Mode.ContentDark}\n          />\n        </div>\n        <div className=\"salesforce-prompt\">\n          <button className=\"btn\" onClick={this._connectSalesforce}>Connect Salesforce</button>\n          <p><a onClick={onClick}>Learn More</a></p>\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default SalesforceLoginPrompt\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/destroy-message-on-salesforce-task.es6",
    "content": "import _ from 'underscore'\nimport {\n  Task,\n  Utils,\n  Message,\n  Actions,\n  DatabaseStore,\n  SyncbackMetadataTask,\n} from 'nylas-exports'\n\nimport {PLUGIN_ID} from '../salesforce-constants'\nimport SalesforceAPI from '../salesforce-api'\nimport * as mdHelpers from '../metadata-helpers'\n\nexport default class DestroyMessageOnSalesforceTask extends Task {\n  constructor({messageId, sObjectId} = {}) {\n    super()\n    this.messageId = messageId;\n    this.sObjectId = sObjectId;\n    this.isCanceled = false;\n  }\n\n  isSameAndOlderTask(other) {\n    return other instanceof DestroyMessageOnSalesforceTask &&\n      other.messageId === this.messageId &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  isComplementTask(other) {\n    return other.constructor.name === \"EnsureMessageOnSalesforceTask\" &&\n      other.messageId === this.messageId &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  shouldDequeueOtherTask(other) {\n    return this.isSameAndOlderTask(other) || this.isComplementTask(other);\n  }\n\n  isDependentOnTask(other) {\n    return this.isSameAndOlderTask(other) || this.isComplementTask(other);\n  }\n\n  performLocal() {\n    return DatabaseStore.find(Message, this.messageId)\n    .then(this._markPendingStatus)\n  }\n\n  performRemote() {\n    return DatabaseStore.find(Message, this.messageId)\n    .then(this._deleteClonedSObjects)\n    .thenReturn(Task.Status.Success)\n  }\n\n  cancel() {\n    this.isCanceled = true;\n  }\n\n  _deleteClonedSObjects = (message) => {\n    if (this.isCanceled) return Promise.resolve();\n    const clonedAs = _.values(mdHelpers.getClonedAsForSObject(message, {\n      id: this.sObjectId}));\n    if (clonedAs.length === 0) return Promise.resolve();\n    return Promise.each(clonedAs, (clonedSObject) => {\n      if (this.isCanceled) return Promise.resolve();\n      return SalesforceAPI.makeRequest({\n        method: \"DELETE\",\n        path: `/sobjects/${clonedSObject.type}/${clonedSObject.id}`,\n      })\n      .catch((apiError) => {\n        if (apiError.errorCode === \"ENTITY_IS_DELETED\") {\n          return Promise.resolve(); // go ahead and remove from metadata\n        }\n        throw apiError\n      })\n      .then(() => {\n        mdHelpers.removeClonedSObject(message,\n            {id: this.sObjectId}, {id: clonedSObject.id})\n      })\n    }).finally(() => { this._syncbackMetadata(message) })\n  }\n\n  _syncbackMetadata = (message) => {\n    const metadata = Utils.deepClone(message.metadataForPluginId(PLUGIN_ID));\n    metadata.pendingSync = false;\n    message.applyPluginMetadata(PLUGIN_ID, metadata);\n    return DatabaseStore.inTransaction(t => t.persistModel(message))\n    .then(() => {\n      const task = new SyncbackMetadataTask(message.clientId, \"Message\", PLUGIN_ID);\n      Actions.queueTask(task);\n    })\n  }\n\n  _markPendingStatus = (message) => {\n    const metadata = Utils.deepClone(message.metadataForPluginId(PLUGIN_ID) || {});\n    metadata.pendingSync = true\n    message.applyPluginMetadata(PLUGIN_ID, metadata);\n    return DatabaseStore.inTransaction(t => t.persistModel(message))\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/destroy-salesforce-object-task.es6",
    "content": "import { Task, DatabaseStore } from 'nylas-exports'\nimport SalesforceAPI from '../salesforce-api'\nimport SalesforceObject from '../models/salesforce-object'\n\n/**\n * Attempts to delete a Salesforce Object remotely and then locally from\n * N1.\n *\n * Note that when sObjects get deleted from our Database, the\n * SalesforceRelatedObjectCache listens for those changes and queues the\n * appropriate cleanup tasks.\n *\n * For example, if we delete an Opportunity, we want to cleanup any manual\n * relations we've setup and stop trying to sync emails to it.\n *\n * If we delete a Salesforce Task, we want to remove that Task from the\n * corresponding paired Message (if any).\n */\nexport default class DestroySalesforceObjectTask extends Task {\n  constructor(args = {}) {\n    super();\n    this.args = args;\n    this.sObjectId = args.sObjectId\n    this.sObjectType = args.sObjectType\n  }\n\n  isSameAndOlderTask(other) {\n    return other instanceof DestroySalesforceObjectTask &&\n      other.sObjectId === this.sObjectId &&\n      other.sObjectType === this.sObjectType &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  shouldDequeueOtherTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  isDependentOnTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  performLocal() {\n    return Promise.resolve()\n  }\n\n  performRemote() {\n    return SalesforceAPI.makeRequest({\n      method: \"DELETE\",\n      path: `/sobjects/${this.sObjectType}/${this.sObjectId}`,\n    })\n    .then(this._removeLocally)\n    .catch((err = {}) => {\n      if (err.statusCode === 404) return this._removeLocally()\n      throw err\n    })\n    .then(() => Task.Status.Success);\n  }\n\n  _removeLocally = () => {\n    return DatabaseStore.findBy(SalesforceObject,\n        {id: this.sObjectId, type: this.sObjectType})\n    .then((obj) => (\n      DatabaseStore.inTransaction(t => t.unpersistModel(obj))\n    ))\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/ensure-message-on-salesforce-task.es6",
    "content": "import _ from 'underscore'\nimport {\n  Task,\n  Utils,\n  Message,\n  Actions,\n  DatabaseStore,\n  SyncbackMetadataTask,\n} from 'nylas-exports'\nimport moment from 'moment'\n\nimport {PLUGIN_ID} from '../salesforce-constants'\nimport SalesforceAPI from '../salesforce-api'\nimport * as mdHelpers from '../metadata-helpers'\nimport SalesforceObject from '../models/salesforce-object'\nimport SalesforceActions from '../salesforce-actions'\n\nexport default class EnsureMessageOnSalesforceTask extends Task {\n  constructor({messageId, sObjectId, sObjectType} = {}) {\n    super()\n    this.messageId = messageId;\n    this.sObjectId = sObjectId;\n    this.sObjectType = sObjectType;\n    this.isCanceled = false;\n  }\n\n  isSameAndOlderTask(other) {\n    return other instanceof EnsureMessageOnSalesforceTask &&\n      other.messageId === this.messageId &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  isComplementTask(other) {\n    return other.constructor.name === \"DestroyMessageOnSalesforceTask\" &&\n      other.messageId === this.messageId &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  shouldDequeueOtherTask(other) {\n    return this.isSameAndOlderTask(other) || this.isComplementTask(other);\n  }\n\n  isDependentOnTask(other) {\n    return this.isSameAndOlderTask(other) || this.isComplementTask(other);\n  }\n\n  performLocal() {\n    return DatabaseStore.find(Message, this.messageId)\n    .then(this._markPendingStatus)\n  }\n\n  performRemote() {\n    return this._checkIfFullySynced()\n    .then((fullySynced) => {\n      if (fullySynced) {\n        return DatabaseStore.find(Message, this.messageId)\n        .then(this._unmarkPendingStatus)\n        .then(() => Task.Status.Success)\n      }\n\n      if (this.isCanceled) return Promise.resolve(Task.Status.Success);\n\n      return DatabaseStore.find(Message, this.messageId)\n      .include(Message.attributes.body)\n      .then(this._prepareMessageBody)\n      .then(this._createNewActivityObjects)\n      .thenReturn(Task.Status.Success)\n    })\n  }\n\n  cancel() {\n    this.isCanceled = true;\n  }\n\n  _markPendingStatus = (message) => {\n    const metadata = Utils.deepClone(message.metadataForPluginId(PLUGIN_ID) || {});\n    metadata.pendingSync = true\n    message.applyPluginMetadata(PLUGIN_ID, metadata);\n    return DatabaseStore.inTransaction(t => t.persistModel(message))\n  }\n\n  _unmarkPendingStatus = (message) => {\n    const metadata = Utils.deepClone(message.metadataForPluginId(PLUGIN_ID) || {});\n    metadata.pendingSync = false\n    message.applyPluginMetadata(PLUGIN_ID, metadata);\n    return DatabaseStore.inTransaction(t => t.persistModel(message))\n  }\n\n  // We do an initial check here to see if we need to fully load the\n  // message's body and go through the effort of creating objects.\n  _checkIfFullySynced() {\n    return DatabaseStore.find(Message, this.messageId).then((message) => {\n      return this._typesToClone(message).length === 0\n    })\n  }\n\n  _typesToClone(message) {\n    const clonedAs = _.values(mdHelpers.getClonedAsForSObject(message, {\n      id: this.sObjectId}));\n\n    return _.difference([\"Task\", \"EmailMessage\"], _.pluck(clonedAs, \"type\"))\n  }\n\n  _prepareMessageBody = (message) => {\n    if (this.isCanceled) return Promise.resolve();\n    const mDom = message.computeDOMWithoutQuotes();\n    const bodies = {\n      plainTextUnquoted: message.cleanPlainTextBody(mDom.body.innerText),\n      htmlUnquoted: mDom.body.innerHTML,\n    }\n    return Promise.resolve({message, bodies})\n  }\n\n  _createNewActivityObjects = ({message, bodies}) => {\n    if (this.isCanceled) return Promise.resolve();\n\n    const clonedAs = mdHelpers.getClonedAsForSObject(message, {id: this.sObjectId});\n    const clonedAsTypes = _.pluck(_.values(clonedAs), \"type\")\n\n    return Promise.resolve()\n    .then(() => {\n      if (clonedAsTypes.includes(\"EmailMessage\")) { return Promise.resolve() }\n      return this._newEmailMessage({message, bodies})\n      .then((sfCreatedObj = {}) => {\n        mdHelpers.addClonedSObject(message, {id: this.sObjectId}, {\n          id: sfCreatedObj.id,\n          type: \"EmailMessage\",\n          relatedToId: this.sObjectId,\n        })\n        return sfCreatedObj.id\n      })\n    })\n    .catch((err) => {\n      // If we can't create the EmailMessage object, attempt to\n      // manually create a Task instead.\n      //\n      // This happens fairly frequently because the EmailMessage\n      // object type has fairly strict limits on the size of the HTML\n      // body you can upload to Salesforce.\n      //\n      // We store the error if it's permanent so we don't keep retrying\n      // the same error\n\n      if (clonedAsTypes.includes(\"Task\")) { return Promise.resolve() }\n\n      return this._newTask({message, bodies})\n      .then((sfCreatedObj) => {\n        mdHelpers.addClonedSObject(message, {id: this.sObjectId}, {\n          id: sfCreatedObj.id,\n          type: \"Task\",\n          relatedToId: this.sObjectId,\n        })\n      }).then(() => {\n        // Be sure to re-throw the error\n        throw err\n      })\n    })\n    .then((newEmailMessageId) => {\n      if (clonedAsTypes.includes(\"Task\")) { return Promise.resolve() }\n\n      // Once an EmailMessage object is created, it'll automatically\n      // also create a corresponding Task object.\n      return SalesforceAPI.makeRequest({\n        path: `/sobjects/EmailMessage/${newEmailMessageId}`,\n      })\n    })\n    .then((rawEmailMessage) => {\n      if (clonedAsTypes.includes(\"Task\")) { return Promise.resolve() }\n      // Load the raw Task.\n      return SalesforceAPI.makeRequest({\n        path: `/sobjects/Task/${rawEmailMessage.ActivityId}`,\n      })\n    })\n    .then((rawTask) => {\n      if (clonedAsTypes.includes(\"Task\")) { return Promise.resolve() }\n      return this._updateTask({rawTask, message, bodies})\n      .then(() => {\n        mdHelpers.addClonedSObject(message, {id: this.sObjectId}, {\n          id: rawTask.Id,\n          type: \"Task\",\n          relatedToId: this.sObjectId,\n        })\n      })\n    })\n    .finally(() => { this._syncbackMetadata(message) })\n  }\n\n  _syncbackMetadata = (message) => {\n    return this._unmarkPendingStatus(message).then(() => {\n      const task = new SyncbackMetadataTask(message.clientId, \"Message\", PLUGIN_ID);\n      Actions.queueTask(task);\n    })\n  }\n\n  _updateTask({rawTask, message}) {\n    return DatabaseStore.findBy(SalesforceObject,\n        {type: \"Contact\", identifier: message.fromContact().email})\n    .then((contact) => {\n      const updates = {\n        Status: \"Completed\",\n      }\n      if (contact) { updates.WhoId = contact.id }\n      return SalesforceAPI.makeRequest({\n        method: \"PATCH\",\n        path: `/sobjects/Task/${rawTask.Id}`,\n        body: updates,\n      }).catch((apiError) => {\n        // These shouldn't fail. If they do we need to look on Sentry.\n        // Unfortunately there's no user feedback yet, so report and\n        // re-throw so the task fails.\n        SalesforceActions.reportError(apiError, {rawPostData: updates})\n        throw apiError\n      });\n    })\n  }\n\n  // https://na16.salesforce.com/services/data/v37.0/query?q=SELECT id, subject FROM EmailMessage WHERE sObjectIdToSync='006j000000T2I08AAF'\n  // Example Task:\n  // https://na16.salesforce.com/services/data/v37.0/sobjects/Task/00Tj000001M3XtrEAF\n  _newTask({message, bodies}) {\n    const to = message.to.map((p) => p.email)\n    const cc = message.cc.map((p) => p.email)\n    const bcc = message.bcc.map((p) => p.email)\n    const fileNames = message.files.map((f) => f.filename)\n    const task = {\n      Subject: `Email: ${message.subject}`,\n      Description: `Date: ${moment(message.date).format('MMMM Do YYYY, h:mm:ss a')}\\nAdditional To: ${to.join(\", \")}\\nCC: ${cc.join(\", \")}\\nBCC: ${bcc.join(\", \")}\\nAttachment: ${fileNames.join(\", \")}\\n\\nSubject: ${message.subject}\\nBody:\\n${bodies.plainTextUnquoted}`,\n      WhatId: this.sObjectId,\n      TaskSubtype: \"Email\",\n      ActivityDate: moment(message.date).format(\"YYYY-MM-DD\"),\n      Status: \"Completed\",\n      Priority: \"Normal\",\n    }\n\n    return DatabaseStore.findBy(SalesforceObject,\n        {type: \"Contact\", identifier: message.fromContact().email})\n    .then((contact) => {\n      if (contact) { task.WhoId = contact.id }\n      return SalesforceAPI.makeRequest({\n        method: \"POST\",\n        path: `/sobjects/Task/`,\n        body: task,\n      }).catch((apiError) => {\n        // These shouldn't fail. If they do we need to look on Sentry.\n        // Unfortunately there's no user feedback yet, so report and\n        // re-throw so the task fails.\n        SalesforceActions.reportError(apiError, {rawPostData: task})\n        throw apiError\n      });\n    })\n  }\n\n  // Note this invisible Task:\n  // https://na16.salesforce.com/services/data/v37.0/sobjects/Task/00Tj000001LLhZbEAL\n  // Is a duplicate of this EmailMessage:\n  // Example EmailMessage:\n  // https://na16.salesforce.com/services/data/v37.0/sobjects/EmailMessage/02sj0000006tyw6AAA\n  _newEmailMessage({message, bodies}) {\n    const emailMessage = {\n      TextBody: bodies.plainTextUnquoted,\n      HtmlBody: bodies.htmlUnquoted,\n      Headers: null,\n      Subject: message.subject,\n      FromName: message.fromContact().name,\n      FromAddress: message.fromContact().email,\n      ToAddress: message.to.map((p) => p.email).join(\";\"),\n      CcAddress: message.cc.map((p) => p.email).join(\";\"),\n      BccAddress: message.bcc.map((p) => p.email).join(\";\"),\n      MessageDate: moment(message.date).format(),\n      ReplyToEmailMessageId: null,\n      RelatedToId: this.sObjectId,\n    }\n    return SalesforceAPI.makeRequest({\n      method: \"POST\",\n      path: `/sobjects/EmailMessage/`,\n      body: emailMessage,\n    }).catch((apiError) => {\n      // These shouldn't fail. If they do we need to look on Sentry.\n      // Unfortunately there's no user feedback yet, so report and\n      // re-throw so the task fails.\n      SalesforceActions.reportError(apiError, {rawPostData: emailMessage})\n      throw apiError\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/manually-relate-salesforce-object-task.es6",
    "content": "import _ from 'underscore'\nimport {\n  Task,\n  Actions,\n  DatabaseStore,\n  SyncbackMetadataTask,\n  DatabaseObjectRegistry,\n} from 'nylas-exports'\nimport { PLUGIN_ID } from '../salesforce-constants';\nimport * as mdHelpers from \"../metadata-helpers\";\nimport * as dataHelpers from \"../salesforce-object-helpers\";\n\nimport UpsertOpportunityContactRoleTask from './upsert-opportunity-contact-role-task'\nimport SyncThreadActivityToSalesforceTask from './sync-thread-activity-to-salesforce-task'\n\nexport default class ManuallyRelateSalesforceObjectTask extends Task {\n  constructor(args = {}) {\n    super();\n    this.args = args;\n    this.sObjectId = args.sObjectId\n    this.sObjectType = args.sObjectType\n    this.nylasObjectId = args.nylasObjectId\n    this.syncbackThread = args.syncbackThread\n    this.nylasObjectType = args.nylasObjectType\n  }\n\n  isSameAndOlderTask(other) {\n    return other instanceof ManuallyRelateSalesforceObjectTask &&\n      other.sObjectId === this.sObjectId &&\n      other.sObjectType === this.sObjectType &&\n      other.nylasObjectId === this.nylasObjectId &&\n      other.nylasObjectType === this.nylasObjectType &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  isDependentOnTask(other) {\n    return ((other instanceof SyncbackMetadataTask) &&\n        (other.modelClassName === \"Thread\") &&\n        (other.pluginId === PLUGIN_ID)) ||\n        (other.constructor.name === \"SyncbackSalesforceObjectTask\" &&\n         other.objectId === this.sObjectId &&\n         other.objectType === this.sObjectType) ||\n        this.isSameAndOlderTask(other)\n  }\n\n  shouldDequeueOtherTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  performLocal() {\n    return Promise.resolve({objectId: this.sObjectId, objectType: this.sObjectType})\n    .then(dataHelpers.loadFullObject)\n    .then(this._loadNylasObject)\n    .then(this._updateMetadata)\n  }\n\n  performRemote() {\n    return Promise.resolve({objectId: this.sObjectId, objectType: this.sObjectType})\n    .then(dataHelpers.loadFullObject)\n    .then(this._loadNylasObject)\n    .then(this._queueSyncbackMetadata)\n    .then(this._queueSyncThreadActivity)\n    .then(this._queueRelatedObjectUpsert)\n    .then(() => Task.Status.Success)\n  }\n\n  _loadNylasObject = (fullSObject) => {\n    const klass = DatabaseObjectRegistry.get(this.nylasObjectType);\n    return DatabaseStore.find(klass, this.nylasObjectId)\n    .then((nylasObject) => { return {nylasObject, fullSObject} })\n  }\n\n  _updateMetadata = ({fullSObject, nylasObject}) => {\n    mdHelpers.setManuallyRelatedObject(nylasObject, fullSObject);\n    return DatabaseStore.inTransaction(t => t.persistModel(nylasObject))\n    .then(() => { return {fullSObject, nylasObject} })\n  }\n\n  _queueSyncbackMetadata = ({fullSObject, nylasObject}) => {\n    const task = new SyncbackMetadataTask(nylasObject.clientId, nylasObject.constructor.name, PLUGIN_ID);\n    Actions.queueTask(task);\n    return Promise.resolve({fullSObject, nylasObject})\n  }\n\n  // When manually relating an sObject, we can optional auto-enable whether\n  // we syncback activity to that sObject. If we didn't have this feature\n  // users would always have to manually flip the \"Sync to this sObject\"\n  // toggle.\n  _queueSyncThreadActivity = ({fullSObject, nylasObject}) => {\n    // Make sure we haven't already flagged this as a thread to sync\n    if (!mdHelpers.getSObjectsToSyncActivityTo(nylasObject)[fullSObject.id]) {\n      if (this.syncbackThread && this.nylasObjectType === \"Thread\") {\n        const t = new SyncThreadActivityToSalesforceTask({\n          threadId: this.nylasObjectId,\n          threadClientId: nylasObject.clientId,\n          newSObjectsToSync: [fullSObject],\n          sObjectsToStopSyncing: [],\n        });\n\n        Actions.queueTask(t);\n      }\n    }\n    return Promise.resolve({fullSObject, nylasObject})\n  }\n\n  // This is reciprocal to code in SyncbackSalesforceObjectTask\n  //\n  // When we link a Thread to an Opportunity and we know there are\n  // Contacts on the Thread, we can link them to this opportunity if\n  // they're not attached already. Contacts and Opportunities are\n  // connected through OpportunityContactRole objects.\n  _queueRelatedObjectUpsert = ({fullSObject, nylasObject}) => {\n    if (this.sObjectType === \"Opportunity\" && this.nylasObjectType === \"Thread\") {\n      return this._queueUpsertOpportunityContactRole({fullSObject, nylasObject})\n    }\n    return Promise.resolve()\n  }\n\n  _queueUpsertOpportunityContactRole = ({nylasObject}) => {\n    const contacts = nylasObject.participants.filter((contact) => {\n      return !contact.isMe() && !contact.hasSameDomainAsMe()\n    })\n    const t = new UpsertOpportunityContactRoleTask({\n      opportunityId: this.sObjectId,\n      emails: _.pluck(contacts, \"email\"),\n    })\n    Actions.queueTask(t)\n    return Promise.resolve()\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/remove-manual-relation-to-salesforce-object-task.es6",
    "content": "import {\n  Task,\n  Actions,\n  DatabaseStore,\n  SyncbackMetadataTask,\n  DatabaseObjectRegistry,\n} from 'nylas-exports'\nimport { PLUGIN_ID } from '../salesforce-constants';\nimport * as mdHelpers from \"../metadata-helpers\";\n\nimport SyncThreadActivityToSalesforceTask from './sync-thread-activity-to-salesforce-task'\n\nexport default class RemoveManualRelationToSalesforceObjectTask extends Task {\n  constructor({sObjectId, nylasObjectId, nylasObjectType} = {}) {\n    super();\n    this.sObjectId = sObjectId\n    this.nylasObjectId = nylasObjectId\n    this.nylasObjectType = nylasObjectType\n    this.metadataUpdated = false\n  }\n\n  isSameAndOlderTask(other) {\n    return other instanceof RemoveManualRelationToSalesforceObjectTask &&\n      other.sObjectId === this.sObjectId &&\n      other.nylasObjectId === this.nylasObjectId &&\n      other.nylasObjectType === this.nylasObjectType &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  isDependentOnTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  shouldDequeueOtherTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  performLocal() {\n    return this._loadNylasObject()\n    .then(this._updateMetadata)\n  }\n\n  performRemote() {\n    if (this.metadataUpdated) {\n      return this._loadNylasObject\n      .then(this._queueSyncbackMetadata)\n      .then(this._queueSyncThreadActivity)\n      .then(() => Task.Status.Success)\n    }\n    return Promise.resolve(Task.Status.Success)\n  }\n\n  _loadNylasObject() {\n    const klass = DatabaseObjectRegistry.get(this.nylasObjectType);\n    return DatabaseStore.find(klass, this.nylasObjectId)\n  }\n\n  _updateMetadata = (nylasObject) => {\n    if (mdHelpers.getManuallyRelatedObjects(nylasObject)[this.sObjectId]) {\n      mdHelpers.removeManuallyRelatedObject(nylasObject, {id: this.sObjectId});\n      this.metadataUpdated = true\n      return DatabaseStore.inTransaction(t => t.persistModel(nylasObject))\n    }\n    return Promise.resolve()\n  }\n\n  _queueSyncbackMetadata = (nylasObject) => {\n    const task = new SyncbackMetadataTask(nylasObject.clientId, nylasObject.constructor.name, PLUGIN_ID);\n    Actions.queueTask(task);\n    return Promise.resolve(nylasObject)\n  }\n\n  // When removing a manually related sObject, we also want to stop\n  // syncing the thread to it (if we marked it to sync).\n  _queueSyncThreadActivity = (nylasObject) => {\n    if (mdHelpers.getSObjectsToSyncActivityTo(nylasObject)[this.sObjectId]) {\n      const t = new SyncThreadActivityToSalesforceTask({\n        threadId: nylasObject.id,\n        threadClientId: nylasObject.clientId,\n        sObjectsToStopSyncing: [{id: this.sObjectId}],\n      });\n\n      Actions.queueTask(t);\n    }\n    return Promise.resolve()\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/sync-salesforce-objects-task.es6",
    "content": "import _ from 'underscore'\nimport moment from 'moment'\nimport querystring from \"querystring\";\nimport {Task, TaskQueue, DatabaseStore} from 'nylas-exports'\nimport SalesforceActions from '../salesforce-actions'\nimport SalesforceAPI from '../salesforce-api'\nimport SalesforceEnv from '../salesforce-env'\nimport SalesforceObject from '../models/salesforce-object'\nimport {upsertBasicObjects, newBasicObjectsQuery} from '../salesforce-object-helpers'\n\n\nclass SyncSalesforceObjectsTask extends Task {\n\n  constructor({objectType, lastUpdateTime} = {}) {\n    super()\n    this._objectType = objectType\n    this._lastUpdateTime = lastUpdateTime\n  }\n\n  get objectType() {\n    return this._objectType\n  }\n\n  performLocal() {\n    if (!this._objectType) {\n      return Promise.reject(new Error('SyncSalesforceObjectsTask: Must provide an objectType'))\n    }\n    return Promise.resolve()\n  }\n\n  performRemote() {\n    if (!SalesforceEnv.isLoggedIn()) { return Promise.resolve(Task.Status.Continue) }\n\n    const queuedSyncs = TaskQueue.findTasks(SyncSalesforceObjectsTask, {objectType: this._objectType})\n    if (queuedSyncs.length > 1) {\n      return Promise.resolve(Task.Status.Continue)\n    }\n\n    console.log(`Salesforce: Syncing ${this._objectType}...`)\n    return Promise.all([\n      this._fetchNewOrUpdatedObjects(this._objectType, this._lastUpdateTime),\n      this._removeOldObjects(this._objectType, this._lastUpdateTime),\n    ])\n    .then(() => console.log(`Salesforce: Done syncing ${this._objectType}`))\n    .then(() => Promise.resolve(Task.Status.Success))\n    .catch((err) => {\n      SalesforceActions.reportError(err)\n      return Promise.resolve([Task.Status.Failed, err])\n    })\n  }\n\n  _handleFetchResponse = (data) => {\n    return upsertBasicObjects(data)\n    .then(() => {\n      const {done, nextRecordsUrl} = data\n      if (!done) {\n        const nextPath = nextRecordsUrl.match(/\\/query\\/.*/)[0];\n        if (!nextPath) {\n          return Promise.reject(\n            new Error(`SyncSalesforceObjectsTask: Could not load all objects of type ${this._objectType}. Invalid nextRecordsUrl: ${nextRecordsUrl}`)\n          )\n        }\n        return SalesforceAPI.makeRequest({\n          path: nextPath,\n        })\n        .then(this._handleFetchResponse)\n      }\n      return Promise.resolve()\n    })\n  }\n\n  _fetchNewOrUpdatedObjects(objectType, lastUpdateTime) {\n    const lastModifiedDate = moment(+lastUpdateTime).utc().format();\n    const where = `LastModifiedDate > ${lastModifiedDate}`;\n    const query = newBasicObjectsQuery(objectType, where);\n\n    return SalesforceAPI.makeRequest({\n      path: `/query/?${query}`,\n    })\n    .then(this._handleFetchResponse)\n  }\n\n  // See Salesforce API documentation for\n  // Geting a List of Deleted Records Within the past 30 days\n  _removeOldObjects(objectType, lastUpdateTime) {\n    if (lastUpdateTime === 0) { return Promise.resolve(); }\n\n    let start = moment()\n    const isTooOld = moment(lastUpdateTime).add(29, 'days').isBefore(start);\n    start = isTooOld ?\n      start.subtract(29, 'days') : moment(lastUpdateTime);\n    const end = moment()\n    const query = querystring.stringify({\n      start: start.utc().format(),\n      end: end.utc().format(),\n    });\n\n    return SalesforceAPI.makeRequest({\n      path: `/sobjects/${objectType}/deleted/?${query}`,\n    })\n    .then((data) => {\n      const deletedRecords = data.deletedRecords || []\n      if (deletedRecords.length === 0) { return Promise.resolve(); }\n      const ids = _.pluck(deletedRecords, \"id\");\n      return Promise.all(ids.map((id) =>\n        DatabaseStore.find(SalesforceObject, id).then((model) => {\n          if (!model) { return Promise.resolve() }\n          return DatabaseStore.inTransaction(t => t.unpersistModel(model));\n        })\n      ));\n    })\n  }\n\n}\n\nexport default SyncSalesforceObjectsTask\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/sync-thread-activity-to-salesforce-task.es6",
    "content": "import {\n  Task,\n  Thread,\n  Message,\n  Actions,\n  DatabaseStore,\n  SyncbackMetadataTask,\n} from 'nylas-exports'\nimport {PLUGIN_ID} from '../salesforce-constants'\nimport * as mdHelpers from '../metadata-helpers'\nimport EnsureMessageOnSalesforceTask from './ensure-message-on-salesforce-task'\nimport DestroyMessageOnSalesforceTask from './destroy-message-on-salesforce-task'\n\n/**\n * Given a threadId, this will load all of the messages on the thread and\n * make sure that there are EmailMessages associated with each of the\n * correspondingly linked SalesforceObjects\n *\n * See lib/metadata-helpers.es6 for documentation on what metadata on the\n * object looks like.\n *\n */\nexport default class SyncThreadActivityToSalesforceTask extends Task {\n  constructor({threadId, threadClientId, newSObjectsToSync, sObjectsToStopSyncing} = {}) {\n    super();\n    this.threadId = threadId;\n    this.isCanceled = false;\n    this.threadClientId = threadClientId;\n    this.newSObjectsToSync = newSObjectsToSync || [];\n    this.sObjectsToStopSyncing = sObjectsToStopSyncing || [];\n  }\n\n  isSameAndOlderTask(other) {\n    return other instanceof SyncThreadActivityToSalesforceTask &&\n      other.threadId === this.threadId &&\n      other.threadClientId === this.threadClientId &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  shouldDequeueOtherTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  isDependentOnTask(other) {\n    return (other instanceof SyncbackMetadataTask) &&\n      (other.modelClassName === \"Thread\") &&\n      (other.clientId === this.threadClientId) &&\n      (other.pluginId === PLUGIN_ID) ||\n      this.isSameAndOlderTask(other);\n  }\n\n  performLocal() {\n    return this._loadThread()\n    .then(this._updateMetadata)\n  }\n\n  performRemote() {\n    return this._loadThread()\n    .then(this._queueSyncbackMetadata)\n    .then(this._queueMessageTasks)\n    .thenReturn(Task.Status.Success)\n  }\n\n  cancel() {\n    this.isCanceled = true;\n  }\n\n  _loadThread = () => {\n    return DatabaseStore.find(Thread, this.threadId)\n  }\n\n  _updateMetadata = (thread) => {\n    for (const newSObject of this.newSObjectsToSync) {\n      mdHelpers.addActivitySyncSObject(thread, newSObject);\n    }\n\n    for (const sObject of this.sObjectsToStopSyncing) {\n      mdHelpers.removeActivitySyncSObject(thread, sObject);\n    }\n\n    return DatabaseStore.inTransaction(t => t.persistModel(thread))\n    .then(() => thread)\n  }\n\n  _queueSyncbackMetadata = (thread) => {\n    if (this.isCanceled) return Promise.resolve(thread);\n    const task = new SyncbackMetadataTask(thread.clientId, thread.constructor.name, PLUGIN_ID);\n    Actions.queueTask(task);\n    return Promise.resolve(thread)\n  }\n\n  _queueMessageTasks = (thread) => {\n    if (this.isCanceled) return Promise.resolve(thread);\n    const sObjectsToSync = mdHelpers.getSObjectsToSyncActivityTo(thread);\n    if (Object.keys(sObjectsToSync).length === 0 && this.sObjectsToStopSyncing.length === 0) {\n      return Promise.resolve()\n    }\n\n    // Since we don't need the very expensive bodies!\n    const basicMsgQuery = DatabaseStore.findAll(Message).where({threadId: thread.id})\n    return Promise.each(basicMsgQuery, (message) => {\n      if (this.isCanceled) return;\n      for (const sObjectToStopSyncing of this.sObjectsToStopSyncing) {\n        const t = new DestroyMessageOnSalesforceTask({\n          messageId: message.id,\n          sObjectId: sObjectToStopSyncing.id,\n        })\n        Actions.queueTask(t);\n      }\n\n      for (const sObjectId of Object.keys(sObjectsToSync)) {\n        const t = new EnsureMessageOnSalesforceTask({\n          messageId: message.id,\n          sObjectId: sObjectId,\n          sObjectType: sObjectsToSync[sObjectId].type,\n        })\n        Actions.queueTask(t);\n      }\n    })\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/syncback-salesforce-object-task.es6",
    "content": "import _ from 'underscore'\nimport {Task, Utils, Actions, DatabaseStore} from 'nylas-exports'\nimport { PLUGIN_ID } from '../salesforce-constants';\nimport SalesforceAPI from '../salesforce-api'\nimport SalesforceObject from '../models/salesforce-object'\nimport SalesforceActions from '../salesforce-actions'\nimport * as dataHelpers from '../salesforce-object-helpers'\nimport DestroySalesforceObjectTask from './destroy-salesforce-object-task'\n\nexport default class SyncbackSalesforceObjectTask extends Task {\n  constructor({objectId, objectType, formPostData, contextData, relatedObjectsData} = {}) {\n    super()\n    this.objectId = objectId\n    this.objectType = objectType\n    this.contextData = contextData || {}\n    this.formPostData = formPostData || {}\n    this.relatedObjectsData = relatedObjectsData || {}\n  }\n\n  isDependentOnTask(other) {\n    return ((other.constructor.name === \"SyncbackMetadataTask\") &&\n        (other.modelClassName === \"Thread\") &&\n        (other.pluginId === PLUGIN_ID))\n  }\n\n  shouldDequeueOtherTask(other) {\n    return other instanceof SyncbackSalesforceObjectTask &&\n      other.objectId === this.objectId &&\n      other.objectType === this.objectType &&\n      Utils.isEqual(other.contextData, this.contextData) &&\n      Utils.isEqual(other.formPostData, this.formPostData) &&\n      Utils.isEqual(other.relatedObjectsData, this.relatedObjectsData)\n  }\n\n  performRemote() {\n    return Promise.resolve()\n    .then(this.submitToSalesforce)\n    .then(this.loadAndSaveFullObject)\n    .then(this.upsertRelatedObjects)\n    .then(this.notifySuccess)\n    .then(() => Task.Status.Success)\n    .catch(this.handleError)\n  }\n\n  submitToSalesforce = () => {\n    // If the objectId is present that means we're updating with new data.\n    // If it's blank, that means we're creating a new one.\n    const method = this.objectId ? \"PATCH\" : \"POST\";\n    const oidPath = this.objectId != null ? this.objectId : \"\";\n    const path = `/sobjects/${this.objectType}/${oidPath}`;\n    return SalesforceAPI.makeRequest({\n      path,\n      method,\n      body: this.formPostData,\n    })\n  }\n\n  // When you create an object on Salesforce, it returns a stub object\n  // with the new id according to the schema here:\n  // https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_create.htm\n  loadAndSaveFullObject = (sfCreatedObj = {}) => {\n    const objectId = this.objectId || sfCreatedObj.id\n    // Note: After we request the full object from the API we save it to\n    // the Database here:\n    return dataHelpers.requestFullObjectFromAPI({objectType: this.objectType, objectId});\n  }\n\n  // When we Create a Contact and there's an Opportunity present, we also\n  // connect the newly created Contact to that Opportunity via a special\n  // `OpportunityContactRole` object.\n  //\n  // This is reciprocal to code in ManuallyRelateSalesforceObjectTask\n  upsertRelatedObjects = (sObject) => {\n    const updates = []\n    if (sObject.type === \"Contact\" &&\n        this.relatedObjectsData.OpportunityIds) {\n      updates.push(this._setOpportunitiesForContact(sObject))\n    } else if (sObject.type === \"Opportunity\" &&\n        this.relatedObjectsData.ContactIds) {\n      updates.push(this._setContactsForOpportunity(sObject))\n    } else if (sObject.type === \"Account\" &&\n        this.relatedObjectsData.ContactIds) {\n      updates.push(this._setContactsForAccount(sObject))\n    }\n    return Promise.all(updates).then(() => sObject)\n  }\n\n  _setOpportunitiesForContact(contact) {\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"OpportunityContactRole\",\n      identifier: contact.id,\n    }).then((roles = []) => {\n      const existingOppIds = _.pluck(roles, \"relatedToId\");\n      const desiredOppIds = this.relatedObjectsData.OpportunityIds;\n\n      const rolesToDelete = roles.filter(role => {\n        return !(desiredOppIds.includes(role.relatedToId))\n      })\n\n      const oppIdsToCreate = desiredOppIds.filter((oid) => {\n        return !(existingOppIds.includes(oid))\n      })\n\n      const tasks = []\n      for (const oppId of oppIdsToCreate) {\n        tasks.push(new SyncbackSalesforceObjectTask({\n          objectType: \"OpportunityContactRole\",\n          formPostData: {\n            OpportunityId: oppId,\n            ContactId: contact.id,\n          },\n        }))\n      }\n      for (const role of rolesToDelete) {\n        tasks.push(new DestroySalesforceObjectTask({\n          sObjectType: \"OpportunityContactRole\",\n          sObjectId: role.id,\n        }))\n      }\n      if (tasks.length === 0) return;\n      Actions.queueTasks(tasks);\n    })\n  }\n\n  _setContactsForOpportunity(opp) {\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"OpportunityContactRole\",\n      relatedToId: opp.id,\n    }).then((roles = []) => {\n      const existingContactIds = _.pluck(roles, \"identifier\");\n      const desiredContactIds = this.relatedObjectsData.ContactIds;\n\n      const rolesToDelete = roles.filter(role => {\n        return !(desiredContactIds.includes(role.identifier))\n      })\n\n      const contactIdsToCreate = desiredContactIds.filter((cid) => {\n        return !(existingContactIds.includes(cid))\n      })\n\n      const tasks = []\n      for (const cid of contactIdsToCreate) {\n        tasks.push(new SyncbackSalesforceObjectTask({\n          objectType: \"OpportunityContactRole\",\n          formPostData: {\n            OpportunityId: opp.id,\n            ContactId: cid,\n          },\n        }))\n      }\n      for (const role of rolesToDelete) {\n        tasks.push(new DestroySalesforceObjectTask({\n          sObjectType: \"OpportunityContactRole\",\n          sObjectId: role.id,\n        }))\n      }\n      if (tasks.length === 0) return;\n      Actions.queueTasks(tasks);\n    })\n  }\n\n  // An Account must have the following Contacts. Therefore we need to\n  // update Contact objects to have the correct AccountId\n  _setContactsForAccount(account) {\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"Contact\",\n      relatedToId: account.id,\n    }).then((contacts = []) => {\n      const existingContactIds = _.pluck(contacts, \"id\");\n      const desiredContactIds = this.relatedObjectsData.ContactIds;\n\n      const contactsToRemoveAccount = contacts.filter(c => {\n        return !(desiredContactIds.includes(c.relatedToId))\n      })\n\n      const contactsToAddAccount = desiredContactIds.filter((cid) => {\n        return !(existingContactIds.includes(cid))\n      })\n\n      const tasks = []\n      for (const cid of contactsToAddAccount) {\n        tasks.push(new SyncbackSalesforceObjectTask({\n          objectType: \"Contact\",\n          objectId: cid,\n          formPostData: { AccountId: account.id },\n        }))\n      }\n      for (const contact of contactsToRemoveAccount) {\n        tasks.push(new SyncbackSalesforceObjectTask({\n          objectType: \"Contact\",\n          objectId: contact.id,\n          formPostData: { AccountId: \"\" },\n        }))\n      }\n      if (tasks.length === 0) return;\n      Actions.queueTasks(tasks);\n    })\n  }\n\n  notifySuccess = (sObject) => {\n    SalesforceActions.syncbackSuccess({\n      objectId: sObject.id,\n      objectType: sObject.type,\n      contextData: this.contextData,\n      formPostData: this.formPostData,\n      relatedObjectsData: this.relatedObjectsData,\n    })\n  }\n\n  handleError = (apiError = {}) => {\n    const name = this.formPostData.Name || this.formPostData.Email\n    SalesforceActions.reportError(apiError, {\n      sObjectId: this.objectId,\n      sObjectType: this.objectType,\n      sObjectName: name,\n      contextData: this.contextData,\n      formPostData: this.formPostData,\n      relatedObjectsData: this.relatedObjectsData,\n    });\n    SalesforceActions.syncbackFailed({\n      objectType: this.objectType,\n      contextData: this.contextData,\n      error: apiError,\n    });\n    return Promise.resolve([Task.Status.Failed, apiError])\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/tasks/upsert-opportunity-contact-role-task.es6",
    "content": "import _ from 'underscore'\nimport {Task, Utils, DatabaseStore} from 'nylas-exports'\nimport SalesforceAPI from '../salesforce-api'\nimport SalesforceObject from '../models/salesforce-object'\nimport * as dataHelpers from '../salesforce-object-helpers'\n\n\nclass UpsertOpportunityContactRoleTask extends Task {\n  constructor({opportunityId, emails} = {}) {\n    super()\n    this.opportunityId = opportunityId\n    this.emails = emails\n  }\n\n  isSameAndOlderTask(other) {\n    return other instanceof UpsertOpportunityContactRoleTask &&\n      other.opportunityId === this.opportunityId &&\n      Utils.isEqual(other.emails, this.emails) &&\n      other.sequentialId < this.sequentialId;\n  }\n\n  shouldDequeueOtherTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  isDependentOnTask(other) {\n    return this.isSameAndOlderTask(other)\n  }\n\n  performRemote() {\n    return Promise.resolve()\n    .then(this._fetchAndSaveContactsFromEmails)\n    .then(this._fetchAndSaveRolesFromContacts)\n    .then(this._calculateMissingRoles)\n    .then(this._submitMissingRoles)\n    .then(() => Task.Status.Success)\n  }\n\n  _identifier(contact) {\n    return `${this.opportunityId}-${contact.id}`\n  }\n\n  _fetchAndSaveContactsFromEmails = () => {\n    // console.log(\"---> Finding Contacts from emails\")\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"Contact\",\n      identifier: this.emails,\n    }).then((sfContactModels = []) => {\n      const toFetch = _.difference(this.emails, _.pluck(sfContactModels, \"identifier\"));\n      return Promise.map(toFetch, (emailToFetch) => {\n        return dataHelpers.loadBasicObjectsByField({\n          objectType: \"Contact\",\n          where: {Email: emailToFetch},\n        })\n        .then(dataHelpers.upsertBasicObjects)\n      }).then((savedContactsFromAPI = []) => {\n        return sfContactModels.concat(_.compact(_.flatten(savedContactsFromAPI)))\n      })\n    })\n  }\n\n  _fetchAndSaveRolesFromContacts = (sfContactModels = []) => {\n    // console.log(\"---> Found Contats\", sfContactModels)\n    const identifiers = sfContactModels.map((sfContact) => {\n      return this._identifier(sfContact);\n    })\n    // console.log(\"---> Finding OpportunityContactRoles from Contats\")\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"OpportunityContactRole\",\n      identifier: identifiers,\n    }).then((roles = []) => {\n      const toFetch = _.difference(identifiers, _.pluck(roles, \"identifier\"));\n      return Promise.map(toFetch, (identifier) => {\n        return dataHelpers.loadBasicObjectsByField({\n          objectType: \"OpportunityContactRole\",\n          fields: [\"Id\", \"OpportunityId\", \"ContactId\"],\n          where: {\n            OpportunityId: this.opportunityId,\n            ContactId: identifier.split(\"-\")[1],\n          },\n        })\n        .then(dataHelpers.upsertBasicObjects)\n      })\n      .then((savedOpportunityContactRoles = []) => {\n        return roles.concat(_.compact(_.flatten(savedOpportunityContactRoles)))\n      })\n      .then((sfOpportunityContactRoles = []) => {\n        return {sfContactModels, sfOpportunityContactRoles}\n      })\n    })\n  }\n\n  _calculateMissingRoles = ({sfContactModels, sfOpportunityContactRoles}) => {\n    // console.log(\"---> Found OpportunityContatRoles\", sfOpportunityContactRoles)\n    const contactIds = sfContactModels.map((sfContact) => {\n      return this._identifier(sfContact);\n    })\n    const roleIds = _.pluck(sfOpportunityContactRoles, \"identifier\");\n    return _.difference(contactIds, roleIds).map((ident) => {\n      return ident.split(\"-\")[1]\n    })\n  }\n\n  _submitMissingRoles = (missingContactIds = []) => {\n    // console.log(`---> ${missingContactIds.length} missing Roles`, missingContactIds)\n    if (missingContactIds.length === 0) return Promise.resolve();\n    return Promise.each(missingContactIds, (contactId) => {\n      return SalesforceAPI.makeRequest({\n        path: `/sobjects/OpportunityContactRole`,\n        method: \"POST\",\n        body: {\n          OpportunityId: this.opportunityId,\n          ContactId: contactId,\n        },\n      }).then((sfCreatedObj) => {\n        return dataHelpers.requestFullObjectFromAPI({\n          objectType: \"OpportunityContactRole\",\n          objectId: sfCreatedObj.id,\n        })\n      })\n    }).then(() => {\n      // console.log(\"Saved OpportunityContactRoles\")\n    })\n  }\n}\n\nexport default UpsertOpportunityContactRoleTask\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/thread/related-objects-for-thread.jsx",
    "content": "import _ from 'underscore'\nimport _str from 'underscore.string'\nimport React from 'react'\nimport moment from 'moment'\nimport ReactDOM from 'react-dom'\nimport {Utils, DatabaseStore, AccountStore, FocusedContactsStore} from 'nylas-exports'\n\nimport SalesforceIcon from '../shared-components/salesforce-icon'\nimport SalesforceObject from '../models/salesforce-object'\nimport SyncThreadToggle from './sync-thread-toggle'\nimport SalesforceActions from '../salesforce-actions'\nimport OpenInSalesforceBtn from '../shared-components/open-in-salesforce-btn'\n\nimport * as dataHelpers from '../salesforce-object-helpers'\nimport * as relatedHelpers from '../related-object-helpers'\nimport {CORE_RELATEABLE_OBJECT_TYPES} from '../salesforce-constants'\n\nclass RelatedObjectsForThread extends React.Component {\n  static displayName = \"RelatedObjectsForThread\"\n\n  static containerStyles = {\n    order: 2,\n    flexShrink: 0,\n  }\n\n  static propTypes = {\n    thread: React.PropTypes.object,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = this._initialState();\n  }\n\n  componentWillMount() {\n    this._setupDataSource(this.props);\n    this._usub = FocusedContactsStore.listen(this._onContactChange)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this._setupDataSource(nextProps)\n  }\n\n  componentWillUnmount() {\n    if (this.disposable && this.disposable.dispose) {\n      this.disposable.dispose()\n    }\n    this._usub()\n  }\n\n  _initialState() {\n    return {\n      expanded: false,\n      subObjects: {},\n      relatedObjects: [],\n      focusedContact: FocusedContactsStore.focusedContact(),\n      focusedContacts: FocusedContactsStore.sortedContacts(),\n    }\n  }\n\n  _onContactChange = () => {\n    this.setState({\n      focusedContact: FocusedContactsStore.focusedContact(),\n      focusedContacts: FocusedContactsStore.sortedContacts(),\n    })\n  }\n\n  _setupDataSource(props) {\n    if (this.disposable && this.disposable.dispose) {\n      this.disposable.dispose()\n    }\n    this.setState(this._initialState())\n    this.disposable = relatedHelpers.observeRelatedSObjectsForThread(props.thread).subscribe((relatedObjects) => {\n      return Promise.map(relatedObjects, (relatedObj) => {\n        return dataHelpers.loadFullObject({\n          objectId: relatedObj.id,\n          objectType: relatedObj.type,\n        })\n      }).then((fullObjs) => {\n        const objs = fullObjs.filter(obj =>\n            CORE_RELATEABLE_OBJECT_TYPES.includes(obj.type))\n        this.setState({relatedObjects: objs})\n        return Promise.each(this._mainObjects(objs), this._loadSubObjects)\n      })\n    })\n  }\n\n  // TODO: Do in more extensible way when SalesforceConfig Main Object\n  // types come into play\n  // TODO: Make a subObject observer;\n  _loadSubObjects = (mainObj) => {\n    if (mainObj.type === \"Opportunity\") {\n      return DatabaseStore.findAll(SalesforceObject,\n        {type: \"OpportunityContactRole\", relatedToId: mainObj.id})\n      .then((roles = []) => {\n        if (roles.length === 0) return []\n        return DatabaseStore.findAll(SalesforceObject, {\n          type: \"Contact\", id: roles.map(r => r.identifier),\n        })\n      }).then((contacts = []) => {\n        let p = Promise.resolve([]);\n        if (mainObj.relatedToId) {\n          p = DatabaseStore.findAll(SalesforceObject, {type: \"Account\", id: mainObj.relatedToId})\n        }\n        return p.then((accounts = []) => {\n          const subObjs = Utils.deepClone(this.state.subObjects);\n          subObjs[mainObj.id] = accounts.concat(contacts);\n          this.setState({subObjects: subObjs})\n        })\n      })\n    } else if (mainObj.type === \"Account\") {\n      return DatabaseStore.findAll(SalesforceObject,\n        {type: \"Contact\", relatedToId: mainObj.id})\n      .then((objs = []) => {\n        const subObjs = Utils.deepClone(this.state.subObjects);\n        subObjs[mainObj.id] = objs;\n        this.setState({subObjects: subObjs})\n      })\n    }\n    return Promise.resolve()\n  }\n\n  _extraInfoForObj(obj) {\n    if (obj.type === \"Opportunity\" && obj.rawData) {\n      const opp = obj.rawData\n      const info = []\n      if (opp.Amount) {\n        const amnt = opp.Amount.toFixed(0).replace(/(\\d)(?=(\\d\\d\\d)+(?!\\d))/g, \"$1,\")\n        info.push(`$${amnt}`)\n      }\n      if (opp.StageName) {\n        info.push(`${opp.StageName}`)\n      }\n      if (opp.Probability) {\n        info.push(`${opp.Probability}%`)\n      }\n      if (opp.CloseDate) {\n        info.push(`Close ${opp.CloseDate}`)\n      }\n      if (opp.LastActivityDate) {\n        info.push(`Last activity: ${moment(opp.LastActivityDate).fromNow()}`)\n      }\n      return info.join(\" • \")\n    } else if (obj.type === \"Contact\") {\n      return obj.identifier || \"\"\n    }\n    return \"\"\n  }\n\n  _requestEdit(object) {\n    let focusedNylasContactData = null;\n    if (this.state.focusedContact) {\n      focusedNylasContactData = {\n        id: this.state.focusedContact.id,\n        name: this.state.focusedContact.name,\n        email: this.state.focusedContact.email,\n      }\n    }\n    SalesforceActions.openObjectForm({\n      objectId: object.id,\n      objectType: object.type,\n      objectInitialData: object,\n      contextData: {\n        nylasObjectId: this.props.thread.id,\n        nylasObjectType: \"Thread\",\n        focusedNylasContactData: focusedNylasContactData,\n      },\n    })\n  }\n\n  _createNewContact(participant, mainObj) {\n    const objectInitialData = {}\n    if (mainObj.type === \"Opportunity\") {\n      objectInitialData.OpportunityIds = [mainObj.id]\n    }\n    const subObjs = this._subObjects(mainObj);\n    const account = subObjs.filter(o => o.type === \"Account\")[0]\n    if (account) {\n      objectInitialData.AccountId = account.id;\n    }\n    SalesforceActions.openObjectForm({\n      objectType: \"Contact\",\n      objectInitialData: objectInitialData,\n      contextData: {\n        nylasObjectId: this.props.thread.id,\n        nylasObjectType: \"Thread\",\n        focusedNylasContactData: {\n          name: participant.name,\n          email: participant.email,\n        },\n      },\n    })\n  }\n\n  _editObj = (obj) => {\n    const reqEdit = _.debounce(() => this._requestEdit(obj), 1000, true);\n    return (event) => {\n      const wrap = ReactDOM.findDOMNode(this.refs.relObjects);\n      const toggles = Array.from(wrap.querySelectorAll(\".thread-toggles\"));\n      for (const toggle of toggles) {\n        if (toggle.contains(event.target)) {\n          return\n        }\n      }\n      reqEdit()\n    }\n  }\n\n  _humanize(type) {\n    return _str.titleize(_str.humanize(type))\n  }\n\n  _renderMainObject = (obj) => {\n    if (!obj) return null\n    return (\n      <div\n        className=\"cell-item sf-related-object large-cell\"\n        key={obj.id}\n      >\n        <div\n          className=\"main-cell-wrap\"\n          title={`Edit ${this._humanize(obj.type)}`}\n          onClick={this._editObj(obj)}\n        >\n          <SalesforceIcon objectType={obj.type} />\n\n          <span className=\"synced-wrap\">\n            <span className={`linkable-object-name ${obj.type}`}>\n              {obj.name}\n            </span>\n            <span className=\"linkable-object-details\">\n              {this._extraInfoForObj(obj)}\n            </span>\n          </span>\n\n          <span ref=\"syncThreadToggle\" className=\"thread-toggles\">\n            <SyncThreadToggle\n              thread={this.props.thread}\n              sObjectId={obj.id}\n              sObjectType={obj.type}\n            />\n          </span>\n\n          <OpenInSalesforceBtn objectId={obj.id} size=\"large\" />\n        </div>\n\n        {this._renderSubObjects(obj)}\n      </div>\n    )\n  }\n\n  _renderSubObjects(obj) {\n    const PADDING = 5 + 5 + 1; // paddings + border-bottom\n    const SUB_OBJ_HEIGHT = 26;\n    const NUM_TO_SHOW = 3;\n\n    const subObjs = this._subObjects(obj);\n    if (subObjs.length === 0) return false;\n    const participants = this._remainingParticipants(subObjs);\n\n    let numParticipants = participants.length;\n    if (this.state.focusedContacts.length === 0) {\n      // This means we're still loading participants. Guess box height\n      // from thread participants so we don't reflow the message list of\n      // the thread for users.\n      numParticipants = this.props.thread.participants.length\n    }\n\n    const numSubObjs = subObjs.length + numParticipants;\n    const numToShow = this.state.expanded ? numSubObjs : Math.min(numSubObjs, NUM_TO_SHOW);\n\n    const onToggle = () => this.setState({expanded: !this.state.expanded})\n    const hasToggle = (numSubObjs > NUM_TO_SHOW)\n    const msg = this.state.expanded ? \"Collapse\" : \"Show more\"\n    const toggle = (\n      <div className=\"toggle\" key={`toggle-${obj.id}`} onClick={onToggle}>{msg}</div>\n    )\n\n    // Since you can't animate to height: auto\n    let height = numToShow * SUB_OBJ_HEIGHT + PADDING;\n\n    // Otherwise the base height overflows\n    if (hasToggle) height -= 2;\n\n    if (this.state.expanded) {\n      height = numToShow * SUB_OBJ_HEIGHT + PADDING;\n    }\n    return [\n      <div key={`subItemsWrap-${obj.id}`} className=\"sub-items-wrap\" style={{height}}>\n        {subObjs.map(this._renderSubObject)}\n        {participants.map(this._renderSuggestedContact(obj))}\n      </div>,\n      (hasToggle ? toggle : false),\n    ]\n  }\n\n  _remainingParticipants = (subObjs) => {\n    const emails = new Set(subObjs.map(o => (o.identifier || \"\")))\n    return this.state.focusedContacts.filter(p =>\n      !emails.has(p.email) && !AccountStore.accountForEmail(p.email)\n    )\n  }\n\n  _renderSuggestedContact = (mainObj) => {\n    return (participant) => {\n      const reqCreate = _.debounce(() =>\n        this._createNewContact(participant, mainObj), 1000, true\n      );\n      return (\n        <div\n          className={`sub-item`}\n          key={`${participant.email}-${participant.name}`}\n          title={`Create Contact for ${participant.email}`}\n          onClick={reqCreate}\n        >\n          <SalesforceIcon objectType=\"Contact\" className=\"round-create\" />\n          <div className=\"synced-wrap\">\n            Add:&nbsp;\n            <span className=\"linkable-object-name\">\n              {participant.fullName()}\n            </span>\n            <span className=\"linkable-object-details\">\n              {participant.email}\n            </span>\n          </div>\n        </div>\n      )\n    }\n  }\n\n  _renderSubObject = (subObj) => {\n    return (\n      <div\n        className={`sub-item ${subObj.type}`}\n        key={`subItem-${subObj.id}`}\n        title={`Edit ${this._humanize(subObj.type)}`}\n        onClick={this._editObj(subObj)}\n      >\n        <SalesforceIcon objectType={subObj.type} />\n        <div className=\"synced-wrap\">\n          <span className=\"linkable-object-name\">{subObj.name}</span>\n          <span className=\"linkable-object-details\">\n            {this._extraInfoForObj(subObj)}\n          </span>\n        </div>\n        <OpenInSalesforceBtn objectId={subObj.id} />\n      </div>\n    )\n  }\n\n  // _renderNewPrompt() {\n  //   const forWhom = this.state.focusedContact;\n  //   let company = null;\n  //   let text = \"\"\n  //   if (forWhom) {\n  //     company = SmartFields.getFieldFromClearbit(forWhom, \"Contact\", \"Company\");\n  //     text = `for ${company || forWhom.firstName()}`;\n  //   }\n  //   return (\n  //     <div className=\"cell-container inline\">\n  //       <div className=\"cell-item new-item\">\n  //         <SalesforceIcon objectType=\"Opportunity\" className=\"round-create\" />\n  //         <span>Create Opportunity {text}</span>\n  //       </div>\n  //     </div>\n  //   )\n  // }\n\n  // TODO: Replace with SalesforceConfig\n  _mainObjects = (objs = []) => {\n    const opps = objs.filter(o => o.type === \"Opportunity\");\n    if (opps.length > 0) return opps;\n    const accounts = objs.filter(o => o.type === \"Account\");\n    if (accounts.length > 0) return accounts;\n    return [];\n  }\n\n  _subObjects(obj) {\n    return this.state.subObjects[obj.id] || []\n  }\n\n  _renderMainObjects() {\n    const mainObjects = this._mainObjects(this.state.relatedObjects);\n    if (mainObjects.length > 0) {\n      return (\n        <div className=\"cell-container\">\n          {mainObjects.map(this._renderMainObject)}\n        </div>\n      )\n    }\n    return false;\n  }\n\n  render() {\n    if (!this.props.thread) return false\n    return (\n      <div className=\"salesforce related-objects-wrap\" ref=\"relObjects\">\n        {this._renderMainObjects()}\n      </div>\n    )\n  }\n}\n\nexport default RelatedObjectsForThread\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/thread/salesforce-manually-relate-thread-button.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nimport {RetinaImg, KeyCommandsRegion} from 'nylas-component-kit'\nimport {Rx, Actions, FocusedContactsStore} from 'nylas-exports'\n\nimport SalesforceEnv from '../salesforce-env'\nimport SalesforceObject from '../models/salesforce-object'\nimport SalesforceActions from '../salesforce-actions'\nimport PendingSalesforceObject from '../form/pending-salesforce-object'\nimport ManuallyRelateSalesforceObjectTask from '../tasks/manually-relate-salesforce-object-task'\nimport SalesforceManuallyRelateThreadPopover from './salesforce-manually-relate-thread-popover'\n\nexport default class SalesforceManuallyRelateThreadButton extends React.Component {\n  static displayName = \"SalesforceManuallyRelateThreadButton\"\n\n  static containerRequired = false\n\n  static propTypes = {\n    items: React.PropTypes.array,\n  }\n\n  static defaultProps = {\n    items: [],\n  }\n\n  constructor(props) {\n    super(props)\n    this._pendingPickerObjs = {};\n    this.state = {\n      isLoggedIn: SalesforceEnv.isLoggedIn(),\n      focusedContact: FocusedContactsStore.focusedContact(),\n    }\n  }\n\n  componentWillMount() {\n    this._usubs = [\n      SalesforceActions.syncbackSuccess.listen(this._onObjectCreate),\n      SalesforceActions.salesforceWindowClosing.listen(this._onWinClose),\n    ];\n\n    this.disposable = Rx.Observable.combineLatest([\n      Rx.Observable.fromStore(FocusedContactsStore),\n      Rx.Observable.fromStore(SalesforceEnv),\n    ]).subscribe(() => {\n      this.setState({\n        isLoggedIn: SalesforceEnv.isLoggedIn(),\n        focusedContact: FocusedContactsStore.focusedContact(),\n      })\n    })\n  }\n\n  componentWillUnmount() {\n    this._pendingPickerObjs = {};\n    for (const usub of this._usubs) { usub() }\n    this.disposable.dispose()\n  }\n\n  /**\n   * When you create a new object with the picker, we drop a\n   * PendingSalesforceObject in the picker before closing the popover and\n   * unmounting the picker. If that objects ends up getting created, we\n   * want to make sure we catch that and finish the intended user action\n   * of manually relating the salesforce object.\n   */\n  _onObjectCreate = ({objectType, objectId, contextData = {}} = {}) => {\n    const formId = contextData.formId\n    const threadIds = this._pendingPickerObjs[formId] || []\n    delete this._pendingPickerObjs[formId]\n    const tasks = threadIds.map((threadId) => {\n      Actions.recordUserEvent(\"Salesforce Manually Related\", {\n        existingObject: false,\n        sObjectId: objectId,\n        sObjectType: objectType,\n        nylasObjectId: threadId,\n        nylasObjectType: \"Thread\",\n      });\n      return new ManuallyRelateSalesforceObjectTask({\n        sObjectId: objectId,\n        sObjectType: objectType,\n        nylasObjectId: threadId,\n        nylasObjectType: \"Thread\",\n      })\n    })\n\n    if (tasks.length > 0) Actions.queueTasks(tasks);\n  }\n\n  _onWinClose = ({contextData = {}, closingDueToObjectSuccess} = {}) => {\n    if (!closingDueToObjectSuccess) {\n      delete this._pendingPickerObjs[contextData.formId]\n    }\n  }\n\n  _emails(props) {\n    _.uniq(_.flatten(props.items.map((thread) => {\n      return _.pluck((thread.participants || []), \"email\")\n    })))\n  }\n\n  _openPopover = () => {\n    const buttonRect = ReactDOM.findDOMNode(this.refs.button).getBoundingClientRect()\n    Actions.openPopover(\n      <SalesforceManuallyRelateThreadPopover\n        threads={this.props.items}\n        isLoggedIn={this.state.isLoggedIn}\n        focusedContact={this.state.focusedContact}\n        onObjectsPicked={this._onObjectsPicked}\n      />,\n      {\n        originRect: buttonRect,\n        direction: 'down',\n      }\n    )\n    return\n  }\n\n  _onObjectsPicked = (pickerObjects = []) => {\n    const tasks = []\n    const threadIds = this.props.items.map(thread => thread.id)\n    for (const pickerObj of pickerObjects) {\n      if (pickerObj instanceof SalesforceObject) {\n        for (const threadId of threadIds) {\n          Actions.recordUserEvent(\"Salesforce Manually Related\", {\n            existingObject: true,\n            sObjectId: pickerObj.id,\n            sObjectType: pickerObj.type,\n            nylasObjectId: threadId,\n            nylasObjectType: \"Thread\",\n          });\n          const task = new ManuallyRelateSalesforceObjectTask({\n            sObjectId: pickerObj.id,\n            sObjectType: pickerObj.type,\n            nylasObjectId: threadId,\n            nylasObjectType: \"Thread\",\n          })\n          tasks.push(task);\n        }\n      } else if (pickerObj instanceof PendingSalesforceObject) {\n        this._pendingPickerObjs[pickerObj.id] = threadIds\n      } else {\n        console.error(pickerObj)\n        throw new Error(\"Invalid picker object type\")\n      }\n    }\n\n    if (tasks.length > 0) Actions.queueTasks(tasks);\n  }\n\n  _keymapHandlers() {\n    return {\n      \"salesforce:show-relate-thread-popover\": this._openPopover,\n    }\n  }\n\n  _menuItems() {\n    return [{\n      label: \"Thread\",\n      submenu: [{\n        label: \"Relate With Salesforce Objects...\",\n        command: \"salesforce:show-relate-thread-popover\",\n        position: \"endof=thread-actions\",\n      }],\n    }]\n  }\n\n  render() {\n    const title = \"Relate thread to Salesforce objects\"\n    return (\n      <KeyCommandsRegion\n        globalHandlers={this._keymapHandlers()}\n        globalMenuItems={this._menuItems()}\n      >\n        <button\n          ref=\"button\"\n          style={{marginRight: 0}}\n          title={title}\n          onClick={this._openPopover}\n          tabIndex={-1}\n          className=\"btn btn-toolbar btn-salesforce\"\n        >\n          <RetinaImg\n            url=\"nylas://nylas-private-salesforce/static/images/ic-salesforce-cloud-btn-large@2x.png\"\n            mode={RetinaImg.Mode.ContentLight}\n          />\n        </button>\n      </KeyCommandsRegion>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/thread/salesforce-manually-relate-thread-popover.jsx",
    "content": "import React from 'react'\nimport _ from 'underscore'\nimport classNames from 'classnames'\nimport {RetinaImg} from 'nylas-component-kit'\n\nimport SmartFields from '../form/smart-fields'\nimport SalesforceLoginPrompt from '../shared-components/salesforce-login-prompt'\nimport SalesforceObjectPicker from '../form/salesforce-object-picker'\nimport {CORE_RELATEABLE_OBJECT_TYPES} from '../salesforce-constants'\n\nconst PICKER_ID = \"manually-relate-thread-popover\"\n\nclass SalesforceManuallyRelateThreadPopover extends React.Component {\n  static displayName = \"SalesforceManuallyRelateThreadPopover\"\n\n  static propTypes = {\n    threads: React.PropTypes.array,\n    isLoggedIn: React.PropTypes.bool,\n    focusedContact: React.PropTypes.object,\n    onObjectsPicked: React.PropTypes.func,\n  }\n\n  static containerStyles = {\n    order: 2,\n    flexShrink: 0,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      pickerValue: [],\n    }\n  }\n\n  _threadIds() {\n    return _.pluck(this.props.threads, \"id\")\n  }\n\n  _placeholder() {\n    return (\n      <span>\n        <RetinaImg\n          mode={RetinaImg.Mode.ContentPreserve}\n          name=\"searchloupe.png\"\n        />\n        &nbsp;&nbsp;<span>Create or search for objects</span>\n      </span>\n    )\n  }\n\n  _onChange = (pickerObjects = []) => {\n    this.props.onObjectsPicked(pickerObjects);\n    this.setState({pickerValue: []})\n  }\n\n  _renderAssociationPicker() {\n    let focusedNylasContactData = null;\n    let company = null\n    if (this.props.focusedContact) {\n      company = SmartFields.getFieldFromClearbit(this.props.focusedContact, \"Contact\", \"Company\");\n    }\n    if (this.props.focusedContact) {\n      focusedNylasContactData = {\n        id: this.props.focusedContact.id,\n        name: this.props.focusedContact.name,\n        email: this.props.focusedContact.email,\n      }\n    }\n    return [\n      <h5 key=\"relate\">Relate Object to Thread</h5>,\n      <SalesforceObjectPicker\n        id={PICKER_ID}\n        key=\"picker\"\n        ref=\"objectPicker\"\n        value={this.state.pickerValue}\n        onChange={this._onChange}\n        placeholder={this._placeholder()}\n        referenceTo={CORE_RELATEABLE_OBJECT_TYPES}\n        defaultValue={company}\n        nylasObjectIds={this._threadIds()}\n        nylasObjectType=\"Thread\"\n        focusedNylasContactData={focusedNylasContactData}\n      />,\n    ]\n  }\n\n  _renderSalesforce() {\n    if (!this.props.isLoggedIn) {\n      return <SalesforceLoginPrompt />\n    }\n\n    const classes = classNames({\n      \"salesforce\": true,\n      \"salesforce-manually-relate-popover\": true,\n    })\n\n    return (\n      <div className={classes}>\n        <div className=\"visible association-picker\">\n          {this._renderAssociationPicker()}\n        </div>\n      </div>\n    )\n  }\n\n  render() {\n    if (!this.props.threads) return false\n    return (\n      <div className=\"related-objects-wrap\" tabIndex=\"-1\">\n        {this._renderSalesforce()}\n      </div>\n    )\n  }\n}\n\nexport default SalesforceManuallyRelateThreadPopover\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/thread/salesforce-sync-label.jsx",
    "content": "import React from 'react';\nimport SalesforceIcon from '../shared-components/salesforce-icon'\nimport SalesforceActions from '../salesforce-actions'\nimport {CORE_RELATEABLE_OBJECT_TYPES} from '../salesforce-constants'\n\nimport * as relatedHelpers from '../related-object-helpers'\nimport * as metadataHelpers from '../metadata-helpers'\n\nclass SalesforceSyncLabel extends React.Component {\n\n  static displayName = 'SalesforceSyncLabel'\n  static containerRequired = false\n\n  static propTypes = {\n    thread: React.PropTypes.object,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = this._initialState(props)\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    this._setupDataSource(this.props)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this._setupDataSource(nextProps);\n    this.setState(this._initialState(nextProps))\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n    if (this.disposable && this.disposable.dispose) {\n      this.disposable.dispose()\n    }\n  }\n\n  _initialState(props) {\n    return {\n      relatedObjects: relatedHelpers.relatedSObjectsForThread(props.thread),\n    }\n  }\n\n  _setupDataSource() {\n    if (this.disposable && this.disposable.dispose) {\n      this.disposable.dispose()\n    }\n    clearTimeout(this.observableTimeout)\n\n    this.observableTimeout = setTimeout(() => {\n      if (!this._mounted) return;\n      this.disposable = relatedHelpers.observeRelatedSObjectsForThread(this.props.thread).subscribe((relatedObjects) => {\n        this.setState({relatedObjects: relatedObjects})\n      })\n    }, 3000)\n  }\n\n  _requestEdit(object) {\n    SalesforceActions.openObjectForm({\n      objectId: object.id,\n      objectType: object.type,\n      objectInitialData: object,\n    })\n  }\n\n  render() {\n    const syncingWith = metadataHelpers.getSObjectsToSyncActivityTo(this.props.thread);\n    const objs = this.state.relatedObjects\n    .filter(o => CORE_RELATEABLE_OBJECT_TYPES.includes(o.type))\n    .map((sObject) => {\n      const syncing = syncingWith[sObject.id] ? \"and syncing with \" : \"\"\n      const title = `Related to ${syncing}${sObject.type}`\n      return (\n        <SalesforceIcon\n          key={`salesforce-label-${sObject.id}`}\n          title={title}\n          objectType={sObject.type}\n          className={`${syncingWith[sObject.id] ? \"checked\" : \"\"}`}\n        />\n      )\n    })\n    return (\n      <span style={{marginRight: 6}} className=\"salesforce-thread-icons\">\n        {objs}\n      </span>\n    )\n  }\n}\n\nexport default SalesforceSyncLabel\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/thread/salesforce-sync-message-status.jsx",
    "content": "import {React} from 'nylas-exports'\nimport {PLUGIN_ID} from '../salesforce-constants'\nimport * as mdHelpers from '../metadata-helpers'\nimport SalesforceActions from '../salesforce-actions'\nimport SalesforceIcon from '../shared-components/salesforce-icon'\n\nexport default class SalesforceSyncMessageStatus extends React.Component {\n  static displayName = \"SalesforceSyncMessageStatus\";\n  static containerRequired = false;\n\n  static propTypes = {\n    message: React.PropTypes.object.isRequired,\n  };\n\n  static containerStyles = {\n    paddingTop: 4,\n  };\n\n  _getRelatedIds() {\n    const taskIds = []\n    const emailMessageIds = []\n\n    const clonedAs = mdHelpers.getClonedAs(this.props.message);\n    for (const relatedToId of Object.keys(clonedAs)) {\n      for (const clonedSObjectId of Object.keys(clonedAs[relatedToId])) {\n        const clonedObj = clonedAs[relatedToId][clonedSObjectId] || {}\n        if (clonedObj.type === \"Task\") taskIds.push(clonedSObjectId)\n        if (clonedObj.type === \"EmailMessage\") emailMessageIds.push(clonedSObjectId)\n      }\n    }\n\n    return {taskIds, emailMessageIds}\n  }\n\n  _hasRelatedSObject() {\n    const {taskIds, emailMessageIds} = this._getRelatedIds();\n    return taskIds.length > 0 || emailMessageIds.length > 0\n  }\n\n  _editActivityBtn(type, id) {\n    const onClick = () => {\n      SalesforceActions.openObjectForm({\n        objectId: id,\n        objectType: type,\n      })\n    }\n    return <SalesforceIcon className=\"inline\" objectType={type} onClick={onClick} />\n  }\n\n  _editEmailMessageFn(id) {\n    SalesforceActions.openObjectForm({\n      objectId: id,\n      objectType: \"EmailMessage\",\n    })\n  }\n\n  _isPendingSync() {\n    return (this.props.message.metadataForPluginId(PLUGIN_ID) || {}).pendingSync\n  }\n\n  _renderPendingSync() {\n    return <div className=\"salesforce-sync-message-status\">Syncing to Salesforce…</div>\n  }\n\n  render() {\n    if (this._isPendingSync()) return this._renderPendingSync();\n    if (!this._hasRelatedSObject()) return false;\n    const {taskIds, emailMessageIds} = this._getRelatedIds();\n    const id = emailMessageIds[0] || taskIds[0];\n\n    const tasks = taskIds.map((taskId) => {\n      return this._editActivityBtn(\"Task\", taskId)\n    })\n    const emailMessages = emailMessageIds.map((emailMessageId) => {\n      return this._editActivityBtn(\"EmailMessage\", emailMessageId)\n    })\n\n    if (!id) return false;\n    return (\n      <div className=\"salesforce-sync-message-status\">\n        Synced to Salesforce:\n        {tasks}\n        {emailMessages}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/lib/thread/sync-thread-toggle.jsx",
    "content": "import React from 'react'\nimport _str from 'underscore.string'\nimport {Switch} from 'nylas-component-kit'\nimport {Actions} from 'nylas-exports'\nimport * as mdHelpers from '../metadata-helpers'\nimport SyncThreadActivityToSalesforceTask from '../tasks/sync-thread-activity-to-salesforce-task'\n\nexport default function SyncThreadToggle(props) {\n  const checked = mdHelpers.getSObjectsToSyncActivityTo(props.thread)[props.sObjectId]\n\n  const onChange = () => {\n    const newSObjectsToSync = []\n    const sObjectsToStopSyncing = []\n    const obj = {id: props.sObjectId, type: props.sObjectType}\n\n    let mixpanelEvent;\n    if (checked) {\n      mixpanelEvent = \"Salesforce Thread Unsynced\";\n      sObjectsToStopSyncing.push(obj)\n    } else {\n      mixpanelEvent = \"Salesforce Thread Synced\";\n      newSObjectsToSync.push(obj)\n    }\n    const task = new SyncThreadActivityToSalesforceTask({\n      threadId: props.thread.id,\n      threadClientId: props.thread.clientId,\n      newSObjectsToSync: newSObjectsToSync,\n      sObjectsToStopSyncing: sObjectsToStopSyncing,\n    })\n\n    Actions.queueTask(task);\n\n    Actions.recordUserEvent(mixpanelEvent, {\n      threadId: props.thread.id,\n      sObjectId: obj.id,\n      sObjectType: obj.type,\n    });\n  }\n\n  const objName = _str.titleize(_str.humanize(props.sObjectType))\n  const msgOn = `Upload all messages to this ${objName}`\n  const msgOff = `Remove all messages from this ${objName}`\n  const title = checked ? msgOff : msgOn\n\n  return (\n    <span className=\"sync-thread-toggle\" title={title}>\n      Sync:&nbsp;&nbsp;&nbsp;&nbsp;\n      <Switch\n        onChange={onChange}\n        checked={checked}\n      />\n    </span>\n  )\n}\nSyncThreadToggle.displayName = \"SyncThreadToggle\"\nSyncThreadToggle.propTypes = {\n  thread: React.PropTypes.object,\n  sObjectId: React.PropTypes.string,\n  sObjectType: React.PropTypes.string,\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/menus/salesforce.json",
    "content": "{\n  \"menu\": [\n    {\n      \"label\": \"Salesforce\",\n      \"submenu\": [\n        { \"label\": \"Refresh Salesforce Data\", \"command\": \"salesforce:sync\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Connect Salesforce\",\n          \"command\": \"salesforce:connect\",\n          \"hideWhenDisabled\": true\n        },\n        { \"label\": \"Disconnect Salesforce\",\n          \"command\": \"salesforce:disconnect\",\n          \"hideWhenDisabled\": true\n        }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/package.json",
    "content": "{\n  \"name\": \"salesforce\",\n  \"title\": \"Salesforce\",\n  \"description\": \"Nylas Mail and Salesforce integration\",\n  \"isHiddenOnPluginsPage\": true,\n  \"isOptional\": true,\n  \"icon\": \"./icon.png\",\n\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n\n  \"license\": \"Proprietary\",\n\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"work\": true,\n    \"SalesforceObjectForm\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/spec/fixtures/opportunity-layouts-alt.json",
    "content": "{\"buttonLayoutSection\":{\"detailButtons\":[{\"custom\":false,\"label\":\"Edit\",\"name\":\"Edit\"},{\"custom\":false,\"label\":\"Delete\",\"name\":\"Delete\"},{\"custom\":false,\"label\":\"Clone\",\"name\":\"Clone\"},{\"custom\":false,\"label\":\"Sharing\",\"name\":\"Share\"}]},\"detailLayoutSections\":[{\"columns\":2,\"heading\":\"Opportunity Information\",\"layoutRows\":[{\"layoutItems\":[{\"editable\":false,\"label\":\"Opportunity Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":360,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":true,\"inlineHelpText\":null,\"label\":\"Name\",\"length\":120,\"name\":\"Name\",\"nameField\":true,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":1,\"type\":\"Field\",\"value\":\"Name\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Opportunity Owner\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Owner ID\",\"length\":18,\"name\":\"OwnerId\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"User\"],\"relationshipName\":\"Owner\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":7,\"type\":\"Field\",\"value\":\"OwnerId\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":false,\"label\":\"Account Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Account ID\",\"length\":18,\"name\":\"AccountId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"Account\"],\"relationshipName\":\"Account\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":2,\"type\":\"Field\",\"value\":\"AccountId\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Close Date\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Close Date\",\"length\":0,\"name\":\"CloseDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:date\",\"sortable\":true,\"type\":\"date\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":8,\"type\":\"Field\",\"value\":\"CloseDate\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":false,\"label\":\"Type\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Opportunity Type\",\"length\":40,\"name\":\"Type\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Existing Business\",\"validFor\":null,\"value\":\"Existing Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"New Business\",\"validFor\":null,\"value\":\"New Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other Business\",\"validFor\":null,\"value\":\"Other Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Not Your Business\",\"validFor\":null,\"value\":\"Not Your Business\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":3,\"type\":\"Field\",\"value\":\"Type\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Stage\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Stage\",\"length\":40,\"name\":\"StageName\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Prospecting\",\"validFor\":null,\"value\":\"Prospecting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Qualification\",\"validFor\":null,\"value\":\"Qualification\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Needs Analysis\",\"validFor\":null,\"value\":\"Needs Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Value Proposition\",\"validFor\":null,\"value\":\"Value Proposition\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Id. Decision Makers\",\"validFor\":null,\"value\":\"Id. Decision Makers\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Perception Analysis\",\"validFor\":null,\"value\":\"Perception Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Proposal/Price Quote\",\"validFor\":null,\"value\":\"Proposal/Price Quote\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Negotiation/Review\",\"validFor\":null,\"value\":\"Negotiation/Review\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Won\",\"validFor\":null,\"value\":\"Closed Won\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Lost\",\"validFor\":null,\"value\":\"Closed Lost\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":9,\"type\":\"Field\",\"value\":\"StageName\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":false,\"label\":\"Primary Campaign Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Campaign ID\",\"length\":18,\"name\":\"CampaignId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"Campaign\"],\"relationshipName\":\"Campaign\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":4,\"type\":\"Field\",\"value\":\"CampaignId\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Probability (%)\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Probability (%)\",\"length\":0,\"name\":\"Probability\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":3,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":10,\"type\":\"Field\",\"value\":\"Probability\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":false,\"label\":\"CustomOppType\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CustomOppType\",\"length\":255,\"name\":\"CustomOppType__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":true,\"label\":\"Outbound Sales\",\"validFor\":null,\"value\":\"Outbound Sales\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Incoming Requests\",\"validFor\":null,\"value\":\"Incoming Requests\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Customer Support\",\"validFor\":null,\"value\":\"Customer Support\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Developer Platform\",\"validFor\":null,\"value\":\"Developer Platform\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Recruiting\",\"validFor\":null,\"value\":\"Recruiting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Events\",\"validFor\":null,\"value\":\"Events\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Office Operations\",\"validFor\":null,\"value\":\"Office Operations\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":5,\"type\":\"Field\",\"value\":\"CustomOppType__c\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Amount\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Amount\",\"length\":0,\"name\":\"Amount\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":18,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"currency\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":11,\"type\":\"Field\",\"value\":\"Amount\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":false,\"label\":\"CoolnessPercent\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":\"50\",\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CoolnessPercent\",\"length\":0,\"name\":\"CoolnessPercent__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":5,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":6,\"type\":\"Field\",\"value\":\"CoolnessPercent__c\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Forecast Category\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Forecast Category\",\"length\":40,\"name\":\"ForecastCategoryName\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Omitted\",\"validFor\":null,\"value\":\"Omitted\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Pipeline\",\"validFor\":null,\"value\":\"Pipeline\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Best Case\",\"validFor\":null,\"value\":\"Best Case\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Commit\",\"validFor\":null,\"value\":\"Commit\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed\",\"validFor\":null,\"value\":\"Closed\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":true,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":12,\"type\":\"Field\",\"value\":\"ForecastCategoryName\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2}],\"rows\":6,\"useCollapsibleSection\":false,\"useHeading\":false},{\"columns\":2,\"heading\":\"Additional Information\",\"layoutRows\":[{\"layoutItems\":[{\"editable\":false,\"label\":\"Next Step\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Next Step\",\"length\":255,\"name\":\"NextStep\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":25,\"type\":\"Field\",\"value\":\"NextStep\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Lead Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Lead Source\",\"length\":40,\"name\":\"LeadSource\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Advertisement\",\"validFor\":null,\"value\":\"Advertisement\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Employee Referral\",\"validFor\":null,\"value\":\"Employee Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"External Referral\",\"validFor\":null,\"value\":\"External Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Partner\",\"validFor\":null,\"value\":\"Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Public Relations\",\"validFor\":null,\"value\":\"Public Relations\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Internal\",\"validFor\":null,\"value\":\"Seminar - Internal\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Partner\",\"validFor\":null,\"value\":\"Seminar - Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Trade Show\",\"validFor\":null,\"value\":\"Trade Show\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Web\",\"validFor\":null,\"value\":\"Web\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Word of mouth\",\"validFor\":null,\"value\":\"Word of mouth\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other\",\"validFor\":null,\"value\":\"Other\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":26,\"type\":\"Field\",\"value\":\"LeadSource\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":false,\"label\":\"Description\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":96000,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":false,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Description\",\"length\":32000,\"name\":\"Description\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"textarea\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":4,\"tabOrder\":27,\"type\":\"Field\",\"value\":\"Description\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"\",\"layoutComponents\":[],\"placeholder\":true,\"required\":false}],\"numItems\":2}],\"rows\":2,\"useCollapsibleSection\":true,\"useHeading\":true},{\"columns\":2,\"heading\":\"System Information\",\"layoutRows\":[{\"layoutItems\":[{\"editable\":false,\"label\":\"Created By\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Created By ID\",\"length\":18,\"name\":\"CreatedById\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"User\"],\"relationshipName\":\"CreatedBy\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":31,\"type\":\"Field\",\"value\":\"CreatedById\"},{\"displayLines\":1,\"tabOrder\":32,\"type\":\"Separator\",\"value\":\", \"},{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Created Date\",\"length\":0,\"name\":\"CreatedDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:dateTime\",\"sortable\":true,\"type\":\"datetime\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":33,\"type\":\"Field\",\"value\":\"CreatedDate\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"Last Modified By\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Last Modified By ID\",\"length\":18,\"name\":\"LastModifiedById\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"User\"],\"relationshipName\":\"LastModifiedBy\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":34,\"type\":\"Field\",\"value\":\"LastModifiedById\"},{\"displayLines\":1,\"tabOrder\":35,\"type\":\"Separator\",\"value\":\", \"},{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Last Modified Date\",\"length\":0,\"name\":\"LastModifiedDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:dateTime\",\"sortable\":true,\"type\":\"datetime\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":36,\"type\":\"Field\",\"value\":\"LastModifiedDate\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2}],\"rows\":1,\"useCollapsibleSection\":true,\"useHeading\":true}],\"editLayoutSections\":[{\"columns\":2,\"heading\":\"Opportunity Information\",\"layoutRows\":[{\"layoutItems\":[{\"editable\":true,\"label\":\"Opportunity Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":360,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":true,\"inlineHelpText\":null,\"label\":\"Name\",\"length\":120,\"name\":\"Name\",\"nameField\":true,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":13,\"type\":\"Field\",\"value\":\"Name\"}],\"placeholder\":false,\"required\":true},{\"editable\":false,\"label\":\"Opportunity Owner\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Owner ID\",\"length\":18,\"name\":\"OwnerId\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"User\"],\"relationshipName\":\"Owner\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":19,\"type\":\"Field\",\"value\":\"OwnerId\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":true,\"label\":\"Account Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Account ID\",\"length\":18,\"name\":\"AccountId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"Account\"],\"relationshipName\":\"Account\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":14,\"type\":\"Field\",\"value\":\"AccountId\"}],\"placeholder\":false,\"required\":true},{\"editable\":true,\"label\":\"Close Date\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Close Date\",\"length\":0,\"name\":\"CloseDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:date\",\"sortable\":true,\"type\":\"date\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":20,\"type\":\"Field\",\"value\":\"CloseDate\"}],\"placeholder\":false,\"required\":true}],\"numItems\":2},{\"layoutItems\":[{\"editable\":true,\"label\":\"Type\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Opportunity Type\",\"length\":40,\"name\":\"Type\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Existing Business\",\"validFor\":null,\"value\":\"Existing Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"New Business\",\"validFor\":null,\"value\":\"New Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other Business\",\"validFor\":null,\"value\":\"Other Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Not Your Business\",\"validFor\":null,\"value\":\"Not Your Business\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":15,\"type\":\"Field\",\"value\":\"Type\"}],\"placeholder\":false,\"required\":false},{\"editable\":true,\"label\":\"Stage\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Stage\",\"length\":40,\"name\":\"StageName\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Prospecting\",\"validFor\":null,\"value\":\"Prospecting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Qualification\",\"validFor\":null,\"value\":\"Qualification\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Needs Analysis\",\"validFor\":null,\"value\":\"Needs Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Value Proposition\",\"validFor\":null,\"value\":\"Value Proposition\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Id. Decision Makers\",\"validFor\":null,\"value\":\"Id. Decision Makers\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Perception Analysis\",\"validFor\":null,\"value\":\"Perception Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Proposal/Price Quote\",\"validFor\":null,\"value\":\"Proposal/Price Quote\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Negotiation/Review\",\"validFor\":null,\"value\":\"Negotiation/Review\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Won\",\"validFor\":null,\"value\":\"Closed Won\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Lost\",\"validFor\":null,\"value\":\"Closed Lost\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":21,\"type\":\"Field\",\"value\":\"StageName\"}],\"placeholder\":false,\"required\":true}],\"numItems\":2},{\"layoutItems\":[{\"editable\":true,\"label\":\"Primary Campaign Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Campaign ID\",\"length\":18,\"name\":\"CampaignId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[\"Campaign\"],\"relationshipName\":\"Campaign\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":16,\"type\":\"Field\",\"value\":\"CampaignId\"}],\"placeholder\":false,\"required\":false},{\"editable\":true,\"label\":\"Probability (%)\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Probability (%)\",\"length\":0,\"name\":\"Probability\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":3,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":22,\"type\":\"Field\",\"value\":\"Probability\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":true,\"label\":\"CustomOppType\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CustomOppType\",\"length\":255,\"name\":\"CustomOppType__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":true,\"label\":\"Outbound Sales\",\"validFor\":null,\"value\":\"Outbound Sales\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Incoming Requests\",\"validFor\":null,\"value\":\"Incoming Requests\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Customer Support\",\"validFor\":null,\"value\":\"Customer Support\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Developer Platform\",\"validFor\":null,\"value\":\"Developer Platform\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Recruiting\",\"validFor\":null,\"value\":\"Recruiting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Events\",\"validFor\":null,\"value\":\"Events\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Office Operations\",\"validFor\":null,\"value\":\"Office Operations\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":17,\"type\":\"Field\",\"value\":\"CustomOppType__c\"}],\"placeholder\":false,\"required\":false},{\"editable\":true,\"label\":\"Amount\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Amount\",\"length\":0,\"name\":\"Amount\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":18,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"currency\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":23,\"type\":\"Field\",\"value\":\"Amount\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":true,\"label\":\"CoolnessPercent\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":\"50\",\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CoolnessPercent\",\"length\":0,\"name\":\"CoolnessPercent__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":5,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":18,\"type\":\"Field\",\"value\":\"CoolnessPercent__c\"}],\"placeholder\":false,\"required\":false},{\"editable\":true,\"label\":\"Forecast Category\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Forecast Category\",\"length\":40,\"name\":\"ForecastCategoryName\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Omitted\",\"validFor\":null,\"value\":\"Omitted\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Pipeline\",\"validFor\":null,\"value\":\"Pipeline\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Best Case\",\"validFor\":null,\"value\":\"Best Case\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Commit\",\"validFor\":null,\"value\":\"Commit\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed\",\"validFor\":null,\"value\":\"Closed\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":true,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":24,\"type\":\"Field\",\"value\":\"ForecastCategoryName\"}],\"placeholder\":false,\"required\":true}],\"numItems\":2}],\"rows\":6,\"useCollapsibleSection\":false,\"useHeading\":true},{\"columns\":2,\"heading\":\"Additional Information\",\"layoutRows\":[{\"layoutItems\":[{\"editable\":true,\"label\":\"Next Step\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Next Step\",\"length\":255,\"name\":\"NextStep\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":28,\"type\":\"Field\",\"value\":\"NextStep\"}],\"placeholder\":false,\"required\":false},{\"editable\":true,\"label\":\"Lead Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":true,\"groupable\":true,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Lead Source\",\"length\":40,\"name\":\"LeadSource\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Advertisement\",\"validFor\":null,\"value\":\"Advertisement\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Employee Referral\",\"validFor\":null,\"value\":\"Employee Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"External Referral\",\"validFor\":null,\"value\":\"External Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Partner\",\"validFor\":null,\"value\":\"Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Public Relations\",\"validFor\":null,\"value\":\"Public Relations\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Internal\",\"validFor\":null,\"value\":\"Seminar - Internal\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Partner\",\"validFor\":null,\"value\":\"Seminar - Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Trade Show\",\"validFor\":null,\"value\":\"Trade Show\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Web\",\"validFor\":null,\"value\":\"Web\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Word of mouth\",\"validFor\":null,\"value\":\"Word of mouth\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other\",\"validFor\":null,\"value\":\"Other\"}],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":29,\"type\":\"Field\",\"value\":\"LeadSource\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editable\":true,\"label\":\"Description\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":96000,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"filterable\":false,\"groupable\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Description\",\"length\":32000,\"name\":\"Description\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"textarea\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":4,\"tabOrder\":30,\"type\":\"Field\",\"value\":\"Description\"}],\"placeholder\":false,\"required\":false},{\"editable\":false,\"label\":\"\",\"layoutComponents\":[],\"placeholder\":true,\"required\":false}],\"numItems\":2}],\"rows\":2,\"useCollapsibleSection\":false,\"useHeading\":true}],\"id\":\"00hj0000000wT8jAAE\",\"multirowEditLayoutSections\":[],\"offlineLinks\":[],\"quickActionList\":{\"quickActionListItems\":[{\"iconUrl\":null,\"label\":\"Post\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.TextPost\",\"targetSobjectType\":null,\"type\":\"Post\"},{\"iconUrl\":null,\"label\":\"File\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.ContentPost\",\"targetSobjectType\":null,\"type\":\"Post\"},{\"iconUrl\":\"https://na16.salesforce.com/img/icon/home32.png\",\"label\":\"Task\",\"miniIconUrl\":\"https://na16.salesforce.com/img/icon/tasks16.png\",\"quickActionName\":\"Opportunity.Task\",\"targetSobjectType\":\"Task\",\"type\":\"Create\"},{\"iconUrl\":\"https://na16.salesforce.com/img/icon/home32.png\",\"label\":\"Log a Call\",\"miniIconUrl\":\"https://na16.salesforce.com/img/icon/tasks16.png\",\"quickActionName\":\"Opportunity.Log_a_Call\",\"targetSobjectType\":\"Task\",\"type\":\"Create\"},{\"iconUrl\":\"https://na16.salesforce.com/img/icon/home32.png\",\"label\":\"Event\",\"miniIconUrl\":\"https://na16.salesforce.com/img/icon/calendar16.png\",\"quickActionName\":\"Opportunity.Event\",\"targetSobjectType\":\"Event\",\"type\":\"Create\"},{\"iconUrl\":null,\"label\":\"Link\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.LinkPost\",\"targetSobjectType\":null,\"type\":\"Post\"},{\"iconUrl\":null,\"label\":\"Poll\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.PollPost\",\"targetSobjectType\":null,\"type\":\"Post\"}]},\"relatedLists\":[{\"columns\":[{\"field\":\"OpenActivity.Subject\",\"format\":null,\"label\":\"Subject\",\"lookupId\":\"Id\",\"name\":\"Subject\"},{\"field\":\"Name.Name\",\"format\":null,\"label\":\"Name\",\"lookupId\":\"WhoId\",\"name\":\"Who.Name\"},{\"field\":\"OpenActivity.IsTask\",\"format\":null,\"label\":\"Task\",\"lookupId\":null,\"name\":\"IsTask\"},{\"field\":\"OpenActivity.ActivityDate\",\"format\":\"date\",\"label\":\"Due Date\",\"lookupId\":null,\"name\":\"ActivityDate\"},{\"field\":\"OpenActivity.Status\",\"format\":null,\"label\":\"Status\",\"lookupId\":null,\"name\":\"toLabel(Status)\"},{\"field\":\"OpenActivity.Priority\",\"format\":null,\"label\":\"Priority\",\"lookupId\":null,\"name\":\"toLabel(Priority)\"},{\"field\":\"User.Name\",\"format\":null,\"label\":\"Assigned To\",\"lookupId\":\"Owner.Id\",\"name\":\"Owner.Name\"}],\"custom\":false,\"field\":\"WhatId\",\"label\":\"Open Activities\",\"limitRows\":5,\"name\":\"OpenActivities\",\"sobject\":\"OpenActivity\",\"sort\":[{\"ascending\":true,\"column\":\"ActivityDate\"},{\"ascending\":false,\"column\":\"LastModifiedDate\"}]},{\"columns\":[{\"field\":\"ActivityHistory.Subject\",\"format\":null,\"label\":\"Subject\",\"lookupId\":\"Id\",\"name\":\"Subject\"},{\"field\":\"Name.Name\",\"format\":null,\"label\":\"Name\",\"lookupId\":\"WhoId\",\"name\":\"Who.Name\"},{\"field\":\"ActivityHistory.IsTask\",\"format\":null,\"label\":\"Task\",\"lookupId\":null,\"name\":\"IsTask\"},{\"field\":\"ActivityHistory.ActivityDate\",\"format\":\"date\",\"label\":\"Due Date\",\"lookupId\":null,\"name\":\"ActivityDate\"},{\"field\":\"User.Name\",\"format\":null,\"label\":\"Assigned To\",\"lookupId\":\"Owner.Id\",\"name\":\"Owner.Name\"},{\"field\":\"ActivityHistory.LastModifiedDate\",\"format\":\"datetime\",\"label\":\"Last Modified Date/Time\",\"lookupId\":null,\"name\":\"LastModifiedDate\"}],\"custom\":false,\"field\":\"WhatId\",\"label\":\"Activity History\",\"limitRows\":5,\"name\":\"ActivityHistories\",\"sobject\":\"ActivityHistory\",\"sort\":[{\"ascending\":false,\"column\":\"ActivityDate\"},{\"ascending\":false,\"column\":\"LastModifiedDate\"}]},{\"columns\":[{\"field\":\"NoteAndAttachment.IsNote\",\"format\":null,\"label\":\"Type\",\"lookupId\":null,\"name\":\"IsNote\"},{\"field\":\"Note.Title\",\"format\":null,\"label\":\"Title\",\"lookupId\":null,\"name\":\"Title\"},{\"field\":\"Note.LastModifiedDate\",\"format\":null,\"label\":\"Last Modified\",\"lookupId\":null,\"name\":\"LastModifiedDate\"},{\"field\":\"Note.CreatedById\",\"format\":null,\"label\":\"Created By\",\"lookupId\":null,\"name\":\"CreatedBy.Name\"}],\"custom\":false,\"field\":\"ParentId\",\"label\":\"Notes & Attachments\",\"limitRows\":5,\"name\":\"NotesAndAttachments\",\"sobject\":\"NoteAndAttachment\",\"sort\":[{\"ascending\":false,\"column\":\"LastModifiedDate\"}]},{\"columns\":[{\"field\":\"Contact.Name\",\"format\":null,\"label\":\"Contact Name\",\"lookupId\":null,\"name\":\"Contact.Name\"},{\"field\":\"Account.Name\",\"format\":null,\"label\":\"Account Name\",\"lookupId\":null,\"name\":\"Contact.Account.Name\"},{\"field\":\"Contact.Email\",\"format\":null,\"label\":\"Email\",\"lookupId\":null,\"name\":\"Contact.Email\"},{\"field\":\"Contact.Phone\",\"format\":null,\"label\":\"Phone\",\"lookupId\":null,\"name\":\"Contact.Phone\"},{\"field\":\"OpportunityContactRole.Role\",\"format\":null,\"label\":\"Role\",\"lookupId\":null,\"name\":\"Role\"},{\"field\":\"OpportunityContactRole.IsPrimary\",\"format\":null,\"label\":\"Primary\",\"lookupId\":null,\"name\":\"IsPrimary\"}],\"custom\":false,\"field\":\"OpportunityId\",\"label\":\"Contact Roles\",\"limitRows\":5,\"name\":\"OpportunityContactRoles\",\"sobject\":\"OpportunityContactRole\",\"sort\":[{\"ascending\":true,\"column\":\"Contact.Name\"}]},{\"columns\":[{\"field\":\"Product2.Name\",\"format\":null,\"label\":\"Product\",\"lookupId\":null,\"name\":\"PricebookEntry.Product2.Name\"},{\"field\":\"OpportunityLineItem.Quantity\",\"format\":null,\"label\":\"Quantity\",\"lookupId\":null,\"name\":\"Quantity\"},{\"field\":\"OpportunityLineItem.UnitPrice\",\"format\":null,\"label\":\"Sales Price\",\"lookupId\":null,\"name\":\"UnitPrice\"},{\"field\":\"OpportunityLineItem.ServiceDate\",\"format\":\"date\",\"label\":\"Date\",\"lookupId\":null,\"name\":\"ServiceDate\"},{\"field\":\"OpportunityLineItem.Description\",\"format\":null,\"label\":\"Line Description\",\"lookupId\":null,\"name\":\"Description\"},{\"field\":\"PricebookEntry.UnitPrice\",\"format\":null,\"label\":\"List Price\",\"lookupId\":null,\"name\":\"PricebookEntry.UnitPrice\"}],\"custom\":false,\"field\":\"OpportunityId\",\"label\":\"Products\",\"limitRows\":5,\"name\":\"OpportunityLineItems\",\"sobject\":\"OpportunityLineItem\",\"sort\":[{\"ascending\":true,\"column\":\"SortOrder\"}]},{\"columns\":[{\"field\":\"Quote.QuoteNumber\",\"format\":null,\"label\":\"Quote Number\",\"lookupId\":\"Id\",\"name\":\"QuoteNumber\"},{\"field\":\"Quote.Name\",\"format\":null,\"label\":\"Quote Name\",\"lookupId\":\"Id\",\"name\":\"Name\"},{\"field\":\"Quote.IsSyncing\",\"format\":null,\"label\":\"Syncing\",\"lookupId\":null,\"name\":\"IsSyncing\"},{\"field\":\"Quote.ExpirationDate\",\"format\":\"date\",\"label\":\"Expiration Date\",\"lookupId\":null,\"name\":\"ExpirationDate\"},{\"field\":\"Quote.Discount\",\"format\":null,\"label\":\"Discount\",\"lookupId\":null,\"name\":\"Discount\"},{\"field\":\"Quote.GrandTotal\",\"format\":null,\"label\":\"Grand Total\",\"lookupId\":null,\"name\":\"GrandTotal\"},{\"field\":\"User.Name\",\"format\":null,\"label\":\"Created By\",\"lookupId\":\"CreatedBy.Id\",\"name\":\"CreatedBy.Name\"}],\"custom\":false,\"field\":\"OpportunityId\",\"label\":\"Quotes\",\"limitRows\":5,\"name\":\"Quotes\",\"sobject\":\"Quote\",\"sort\":[{\"ascending\":true,\"column\":\"Name\"}]}]}"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/spec/fixtures/opportunity-layouts.json",
    "content": "{\"layouts\":[{\"buttonLayoutSection\":{\"detailButtons\":[{\"behavior\":null,\"colors\":[{\"color\":\"1DCCBF\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/edit.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/edit_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/edit_120.png\",\"width\":120}],\"label\":\"Edit\",\"menubar\":false,\"name\":\"Edit\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":[{\"color\":\"E6717C\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/delete.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/delete_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/delete_120.png\",\"width\":120}],\"label\":\"Delete\",\"menubar\":false,\"name\":\"Delete\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":[{\"color\":\"6CA1E9\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/clone.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/clone_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/clone_120.png\",\"width\":120}],\"label\":\"Clone\",\"menubar\":false,\"name\":\"Clone\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"Sharing\",\"menubar\":false,\"name\":\"Share\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null}]},\"detailLayoutSections\":[{\"columns\":2,\"heading\":\"Opportunity Information\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Opportunity Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":360,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":true,\"inlineHelpText\":null,\"label\":\"Name\",\"length\":120,\"mask\":null,\"maskType\":null,\"name\":\"Name\",\"nameField\":true,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":1,\"type\":\"Field\",\"value\":\"Name\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Opportunity Owner\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Owner ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"OwnerId\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"User\"],\"relationshipName\":\"Owner\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":8,\"type\":\"Field\",\"value\":\"OwnerId\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Account Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Account ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"AccountId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"Account\"],\"relationshipName\":\"Account\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":2,\"type\":\"Field\",\"value\":\"AccountId\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Close Date\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Close Date\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"CloseDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:date\",\"sortable\":true,\"type\":\"date\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":9,\"type\":\"Field\",\"value\":\"CloseDate\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Type\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Opportunity Type\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"Type\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Existing Business\",\"validFor\":null,\"value\":\"Existing Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"New Business\",\"validFor\":null,\"value\":\"New Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other Business\",\"validFor\":null,\"value\":\"Other Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Not Your Business\",\"validFor\":null,\"value\":\"Not Your Business\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":3,\"type\":\"Field\",\"value\":\"Type\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Stage\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Stage\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"StageName\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Prospecting\",\"validFor\":null,\"value\":\"Prospecting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Qualification\",\"validFor\":null,\"value\":\"Qualification\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Needs Analysis\",\"validFor\":null,\"value\":\"Needs Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Value Proposition\",\"validFor\":null,\"value\":\"Value Proposition\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Id. Decision Makers\",\"validFor\":null,\"value\":\"Id. Decision Makers\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Perception Analysis\",\"validFor\":null,\"value\":\"Perception Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Proposal/Price Quote\",\"validFor\":null,\"value\":\"Proposal/Price Quote\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Negotiation/Review\",\"validFor\":null,\"value\":\"Negotiation/Review\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Won\",\"validFor\":null,\"value\":\"Closed Won\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Lost\",\"validFor\":null,\"value\":\"Closed Lost\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":10,\"type\":\"Field\",\"value\":\"StageName\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Primary Campaign Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Campaign ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"CampaignId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"Campaign\"],\"relationshipName\":\"Campaign\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":4,\"type\":\"Field\",\"value\":\"CampaignId\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Probability (%)\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Probability (%)\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Probability\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":3,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":11,\"type\":\"Field\",\"value\":\"Probability\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"CustomOppType\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CustomOppType\",\"length\":255,\"mask\":null,\"maskType\":null,\"name\":\"CustomOppType__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":true,\"label\":\"Outbound Sales\",\"validFor\":null,\"value\":\"Outbound Sales\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Incoming Requests\",\"validFor\":null,\"value\":\"Incoming Requests\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Customer Support\",\"validFor\":null,\"value\":\"Customer Support\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Developer Platform\",\"validFor\":null,\"value\":\"Developer Platform\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Recruiting\",\"validFor\":null,\"value\":\"Recruiting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Events\",\"validFor\":null,\"value\":\"Events\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Office Operations\",\"validFor\":null,\"value\":\"Office Operations\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":5,\"type\":\"Field\",\"value\":\"CustomOppType__c\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"\",\"layoutComponents\":[{\"displayLines\":1,\"tabOrder\":12,\"type\":\"EmptySpace\",\"value\":null}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"CoolnessPercent\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":\"50\",\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CoolnessPercent\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"CoolnessPercent__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":5,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":6,\"type\":\"Field\",\"value\":\"CoolnessPercent__c\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Amount\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Amount\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Amount\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":18,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"currency\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":13,\"type\":\"Field\",\"value\":\"Amount\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Sell To\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":4099,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"Select all that apply\",\"label\":\"Sell To\",\"length\":4099,\"mask\":null,\"maskType\":null,\"name\":\"All_the_things__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"San Francisco\",\"validFor\":null,\"value\":\"San Francisco\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Boston\",\"validFor\":null,\"value\":\"Boston\"},{\"active\":true,\"defaultValue\":false,\"label\":\"London\",\"validFor\":null,\"value\":\"London\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Tokyo\",\"validFor\":null,\"value\":\"Tokyo\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Shanghai\",\"validFor\":null,\"value\":\"Shanghai\"},{\"active\":true,\"defaultValue\":false,\"label\":\"San Diego\",\"validFor\":null,\"value\":\"San Diego\"}],\"precision\":4,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"multipicklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":4,\"tabOrder\":7,\"type\":\"Field\",\"value\":\"All_the_things__c\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Forecast Category\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Forecast Category\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"ForecastCategoryName\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Omitted\",\"validFor\":null,\"value\":\"Omitted\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Pipeline\",\"validFor\":null,\"value\":\"Pipeline\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Best Case\",\"validFor\":null,\"value\":\"Best Case\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Commit\",\"validFor\":null,\"value\":\"Commit\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed\",\"validFor\":null,\"value\":\"Closed\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":true,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":14,\"type\":\"Field\",\"value\":\"ForecastCategoryName\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2}],\"rows\":7,\"tabOrder\":\"TopToBottom\",\"useCollapsibleSection\":false,\"useHeading\":false},{\"columns\":2,\"heading\":\"Additional Information\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Next Step\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Next Step\",\"length\":255,\"mask\":null,\"maskType\":null,\"name\":\"NextStep\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":29,\"type\":\"Field\",\"value\":\"NextStep\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Lead Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Lead Source\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"LeadSource\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Advertisement\",\"validFor\":null,\"value\":\"Advertisement\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Employee Referral\",\"validFor\":null,\"value\":\"Employee Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"External Referral\",\"validFor\":null,\"value\":\"External Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Partner\",\"validFor\":null,\"value\":\"Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Public Relations\",\"validFor\":null,\"value\":\"Public Relations\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Internal\",\"validFor\":null,\"value\":\"Seminar - Internal\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Partner\",\"validFor\":null,\"value\":\"Seminar - Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Trade Show\",\"validFor\":null,\"value\":\"Trade Show\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Web\",\"validFor\":null,\"value\":\"Web\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Word of mouth\",\"validFor\":null,\"value\":\"Word of mouth\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other\",\"validFor\":null,\"value\":\"Other\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":30,\"type\":\"Field\",\"value\":\"LeadSource\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Description\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":96000,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":\"plaintextarea\",\"filterable\":false,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Description\",\"length\":32000,\"mask\":null,\"maskType\":null,\"name\":\"Description\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"textarea\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":4,\"tabOrder\":31,\"type\":\"Field\",\"value\":\"Description\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"\",\"layoutComponents\":[],\"placeholder\":true,\"required\":false}],\"numItems\":2}],\"rows\":2,\"tabOrder\":\"LeftToRight\",\"useCollapsibleSection\":true,\"useHeading\":true},{\"columns\":1,\"heading\":\"Even More Info\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Checkout\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"We encourage you to check more than one\",\"label\":\"Checkout\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Checkout__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:boolean\",\"sortable\":true,\"type\":\"boolean\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":35,\"type\":\"Field\",\"value\":\"Checkout__c\"}],\"placeholder\":false,\"required\":false}],\"numItems\":1},{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Send Date Time\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"Please pick a time too\",\"label\":\"Send Date Time\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Send_Date_Time__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:dateTime\",\"sortable\":true,\"type\":\"datetime\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":36,\"type\":\"Field\",\"value\":\"Send_Date_Time__c\"}],\"placeholder\":false,\"required\":false}],\"numItems\":1}],\"rows\":2,\"tabOrder\":\"TopToBottom\",\"useCollapsibleSection\":true,\"useHeading\":true},{\"columns\":1,\"heading\":\"Description Information\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Other thoughts\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":98304,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":\"\\\"Once upon a time…\\\"\",\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":\"plaintextarea\",\"filterable\":false,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"More things to say\",\"label\":\"Other thoughts\",\"length\":32768,\"mask\":null,\"maskType\":null,\"name\":\"Other_thoughts__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"textarea\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":10,\"tabOrder\":39,\"type\":\"Field\",\"value\":\"Other_thoughts__c\"}],\"placeholder\":false,\"required\":false}],\"numItems\":1}],\"rows\":1,\"tabOrder\":\"TopToBottom\",\"useCollapsibleSection\":false,\"useHeading\":false},{\"columns\":2,\"heading\":\"System Information\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Created By\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Created By ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"CreatedById\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"User\"],\"relationshipName\":\"CreatedBy\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":41,\"type\":\"Field\",\"value\":\"CreatedById\"},{\"displayLines\":1,\"tabOrder\":42,\"type\":\"Separator\",\"value\":\", \"},{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Created Date\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"CreatedDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:dateTime\",\"sortable\":true,\"type\":\"datetime\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":43,\"type\":\"Field\",\"value\":\"CreatedDate\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Last Modified By\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Last Modified By ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"LastModifiedById\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"User\"],\"relationshipName\":\"LastModifiedBy\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":44,\"type\":\"Field\",\"value\":\"LastModifiedById\"},{\"displayLines\":1,\"tabOrder\":45,\"type\":\"Separator\",\"value\":\", \"},{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":false,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Last Modified Date\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"LastModifiedDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:dateTime\",\"sortable\":true,\"type\":\"datetime\",\"unique\":false,\"updateable\":false,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":46,\"type\":\"Field\",\"value\":\"LastModifiedDate\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2}],\"rows\":1,\"tabOrder\":\"TopToBottom\",\"useCollapsibleSection\":true,\"useHeading\":true}],\"editLayoutSections\":[{\"columns\":2,\"heading\":\"Opportunity Information\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Opportunity Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":360,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":true,\"inlineHelpText\":null,\"label\":\"Name\",\"length\":120,\"mask\":null,\"maskType\":null,\"name\":\"Name\",\"nameField\":true,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":15,\"type\":\"Field\",\"value\":\"Name\"}],\"placeholder\":false,\"required\":true},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Opportunity Owner\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Owner ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"OwnerId\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"User\"],\"relationshipName\":\"Owner\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":22,\"type\":\"Field\",\"value\":\"OwnerId\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Account Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Account ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"AccountId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"Account\"],\"relationshipName\":\"Account\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":16,\"type\":\"Field\",\"value\":\"AccountId\"}],\"placeholder\":false,\"required\":true},{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Close Date\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Close Date\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"CloseDate\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:date\",\"sortable\":true,\"type\":\"date\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":23,\"type\":\"Field\",\"value\":\"CloseDate\"}],\"placeholder\":false,\"required\":true}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Type\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Opportunity Type\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"Type\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Existing Business\",\"validFor\":null,\"value\":\"Existing Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"New Business\",\"validFor\":null,\"value\":\"New Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other Business\",\"validFor\":null,\"value\":\"Other Business\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Not Your Business\",\"validFor\":null,\"value\":\"Not Your Business\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":17,\"type\":\"Field\",\"value\":\"Type\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Stage\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Stage\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"StageName\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Prospecting\",\"validFor\":null,\"value\":\"Prospecting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Qualification\",\"validFor\":null,\"value\":\"Qualification\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Needs Analysis\",\"validFor\":null,\"value\":\"Needs Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Value Proposition\",\"validFor\":null,\"value\":\"Value Proposition\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Id. Decision Makers\",\"validFor\":null,\"value\":\"Id. Decision Makers\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Perception Analysis\",\"validFor\":null,\"value\":\"Perception Analysis\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Proposal/Price Quote\",\"validFor\":null,\"value\":\"Proposal/Price Quote\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Negotiation/Review\",\"validFor\":null,\"value\":\"Negotiation/Review\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Won\",\"validFor\":null,\"value\":\"Closed Won\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed Lost\",\"validFor\":null,\"value\":\"Closed Lost\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":24,\"type\":\"Field\",\"value\":\"StageName\"}],\"placeholder\":false,\"required\":true}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Primary Campaign Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Campaign ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"CampaignId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"Campaign\"],\"relationshipName\":\"Campaign\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":18,\"type\":\"Field\",\"value\":\"CampaignId\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Probability (%)\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Probability (%)\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Probability\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":3,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":25,\"type\":\"Field\",\"value\":\"Probability\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"CustomOppType\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CustomOppType\",\"length\":255,\"mask\":null,\"maskType\":null,\"name\":\"CustomOppType__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":true,\"label\":\"Outbound Sales\",\"validFor\":null,\"value\":\"Outbound Sales\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Incoming Requests\",\"validFor\":null,\"value\":\"Incoming Requests\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Customer Support\",\"validFor\":null,\"value\":\"Customer Support\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Developer Platform\",\"validFor\":null,\"value\":\"Developer Platform\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Recruiting\",\"validFor\":null,\"value\":\"Recruiting\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Events\",\"validFor\":null,\"value\":\"Events\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Office Operations\",\"validFor\":null,\"value\":\"Office Operations\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":19,\"type\":\"Field\",\"value\":\"CustomOppType__c\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"\",\"layoutComponents\":[{\"displayLines\":1,\"tabOrder\":26,\"type\":\"EmptySpace\",\"value\":null}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"CoolnessPercent\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":\"50\",\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"CoolnessPercent\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"CoolnessPercent__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":5,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"percent\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":20,\"type\":\"Field\",\"value\":\"CoolnessPercent__c\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Amount\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Amount\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Amount\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":18,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":2,\"soapType\":\"xsd:double\",\"sortable\":true,\"type\":\"currency\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":27,\"type\":\"Field\",\"value\":\"Amount\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Sell To\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":4099,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"Select all that apply\",\"label\":\"Sell To\",\"length\":4099,\"mask\":null,\"maskType\":null,\"name\":\"All_the_things__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"San Francisco\",\"validFor\":null,\"value\":\"San Francisco\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Boston\",\"validFor\":null,\"value\":\"Boston\"},{\"active\":true,\"defaultValue\":false,\"label\":\"London\",\"validFor\":null,\"value\":\"London\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Tokyo\",\"validFor\":null,\"value\":\"Tokyo\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Shanghai\",\"validFor\":null,\"value\":\"Shanghai\"},{\"active\":true,\"defaultValue\":false,\"label\":\"San Diego\",\"validFor\":null,\"value\":\"San Diego\"}],\"precision\":4,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"multipicklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":4,\"tabOrder\":21,\"type\":\"Field\",\"value\":\"All_the_things__c\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Forecast Category\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Forecast Category\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"ForecastCategoryName\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":false,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Omitted\",\"validFor\":null,\"value\":\"Omitted\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Pipeline\",\"validFor\":null,\"value\":\"Pipeline\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Best Case\",\"validFor\":null,\"value\":\"Best Case\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Commit\",\"validFor\":null,\"value\":\"Commit\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Closed\",\"validFor\":null,\"value\":\"Closed\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":true,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":28,\"type\":\"Field\",\"value\":\"ForecastCategoryName\"}],\"placeholder\":false,\"required\":true}],\"numItems\":2}],\"rows\":7,\"tabOrder\":\"TopToBottom\",\"useCollapsibleSection\":false,\"useHeading\":true},{\"columns\":2,\"heading\":\"Additional Information\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Next Step\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":765,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Next Step\",\"length\":255,\"mask\":null,\"maskType\":null,\"name\":\"NextStep\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"string\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":32,\"type\":\"Field\",\"value\":\"NextStep\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Lead Source\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":120,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Lead Source\",\"length\":40,\"mask\":null,\"maskType\":null,\"name\":\"LeadSource\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[{\"active\":true,\"defaultValue\":false,\"label\":\"Advertisement\",\"validFor\":null,\"value\":\"Advertisement\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Employee Referral\",\"validFor\":null,\"value\":\"Employee Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"External Referral\",\"validFor\":null,\"value\":\"External Referral\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Partner\",\"validFor\":null,\"value\":\"Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Public Relations\",\"validFor\":null,\"value\":\"Public Relations\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Internal\",\"validFor\":null,\"value\":\"Seminar - Internal\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Seminar - Partner\",\"validFor\":null,\"value\":\"Seminar - Partner\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Trade Show\",\"validFor\":null,\"value\":\"Trade Show\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Web\",\"validFor\":null,\"value\":\"Web\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Word of mouth\",\"validFor\":null,\"value\":\"Word of mouth\"},{\"active\":true,\"defaultValue\":false,\"label\":\"Other\",\"validFor\":null,\"value\":\"Other\"}],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":true,\"type\":\"picklist\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":33,\"type\":\"Field\",\"value\":\"LeadSource\"}],\"placeholder\":false,\"required\":false}],\"numItems\":2},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Description\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":96000,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":\"plaintextarea\",\"filterable\":false,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Description\",\"length\":32000,\"mask\":null,\"maskType\":null,\"name\":\"Description\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"textarea\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":4,\"tabOrder\":34,\"type\":\"Field\",\"value\":\"Description\"}],\"placeholder\":false,\"required\":false},{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"\",\"layoutComponents\":[],\"placeholder\":true,\"required\":false}],\"numItems\":2}],\"rows\":2,\"tabOrder\":\"LeftToRight\",\"useCollapsibleSection\":false,\"useHeading\":true},{\"columns\":1,\"heading\":\"Even More Info\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Checkout\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"We encourage you to check more than one\",\"label\":\"Checkout\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Checkout__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:boolean\",\"sortable\":true,\"type\":\"boolean\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":37,\"type\":\"Field\",\"value\":\"Checkout__c\"}],\"placeholder\":false,\"required\":false}],\"numItems\":1},{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Send Date Time\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":0,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"Please pick a time too\",\"label\":\"Send Date Time\",\"length\":0,\"mask\":null,\"maskType\":null,\"name\":\"Send_Date_Time__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:dateTime\",\"sortable\":true,\"type\":\"datetime\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":38,\"type\":\"Field\",\"value\":\"Send_Date_Time__c\"}],\"placeholder\":false,\"required\":false}],\"numItems\":1}],\"rows\":2,\"tabOrder\":\"TopToBottom\",\"useCollapsibleSection\":false,\"useHeading\":true},{\"columns\":1,\"heading\":\"Description Information\",\"layoutRows\":[{\"layoutItems\":[{\"editableForNew\":true,\"editableForUpdate\":true,\"label\":\"Other thoughts\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":98304,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":true,\"defaultValue\":null,\"defaultValueFormula\":\"\\\"Once upon a time…\\\"\",\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":\"plaintextarea\",\"filterable\":false,\"filteredLookupInfo\":null,\"groupable\":false,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":\"More things to say\",\"label\":\"Other thoughts\",\"length\":32768,\"mask\":null,\"maskType\":null,\"name\":\"Other_thoughts__c\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[],\"relationshipName\":null,\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"xsd:string\",\"sortable\":false,\"type\":\"textarea\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":10,\"tabOrder\":40,\"type\":\"Field\",\"value\":\"Other_thoughts__c\"}],\"placeholder\":false,\"required\":false}],\"numItems\":1}],\"rows\":1,\"tabOrder\":\"TopToBottom\",\"useCollapsibleSection\":false,\"useHeading\":true}],\"highlightsPanelLayoutSection\":null,\"id\":\"00hj0000000wT8jAAE\",\"multirowEditLayoutSections\":[],\"offlineLinks\":[],\"quickActionList\":{\"quickActionListItems\":[{\"accessLevelRequired\":null,\"colors\":[{\"color\":\"65CAE4\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"iconUrl\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_post.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_post_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_post_120.png\",\"width\":120}],\"label\":\"Post\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.TextPost\",\"targetSobjectType\":null,\"type\":\"Post\",\"urls\":{}},{\"accessLevelRequired\":null,\"colors\":[{\"color\":\"BAAC93\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"iconUrl\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_file.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_file_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_file_120.png\",\"width\":120}],\"label\":\"File\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.ContentPost\",\"targetSobjectType\":null,\"type\":\"Post\",\"urls\":{}},{\"accessLevelRequired\":null,\"colors\":[{\"color\":\"4BC076\",\"context\":\"primary\",\"theme\":\"theme4\"},{\"color\":\"1797C0\",\"context\":\"primary\",\"theme\":\"theme3\"}],\"iconUrl\":\"https://na16.salesforce.com/img/icon/home32.png\",\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task_120.png\",\"width\":120}],\"label\":\"Task\",\"miniIconUrl\":\"https://na16.salesforce.com/img/icon/tasks16.png\",\"quickActionName\":\"Opportunity.Task\",\"targetSobjectType\":\"Task\",\"type\":\"Create\",\"urls\":{\"defaultValuesTemplate\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Task/defaultValues/{ID}\",\"quickAction\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Task\",\"defaultValues\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Task/defaultValues\",\"describe\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Task/describe\"}},{\"accessLevelRequired\":null,\"colors\":[{\"color\":\"4BC076\",\"context\":\"primary\",\"theme\":\"theme4\"},{\"color\":\"1797C0\",\"context\":\"primary\",\"theme\":\"theme3\"}],\"iconUrl\":\"https://na16.salesforce.com/img/icon/home32.png\",\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task_120.png\",\"width\":120}],\"label\":\"Log a Call\",\"miniIconUrl\":\"https://na16.salesforce.com/img/icon/tasks16.png\",\"quickActionName\":\"Opportunity.Log_a_Call\",\"targetSobjectType\":\"Task\",\"type\":\"Create\",\"urls\":{\"defaultValuesTemplate\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Log_a_Call/defaultValues/{ID}\",\"quickAction\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Log_a_Call\",\"defaultValues\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Log_a_Call/defaultValues\",\"describe\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Log_a_Call/describe\"}},{\"accessLevelRequired\":null,\"colors\":[{\"color\":\"EB7092\",\"context\":\"primary\",\"theme\":\"theme4\"},{\"color\":\"1797C0\",\"context\":\"primary\",\"theme\":\"theme3\"}],\"iconUrl\":\"https://na16.salesforce.com/img/icon/home32.png\",\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_event.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_event_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_event_120.png\",\"width\":120}],\"label\":\"Event\",\"miniIconUrl\":\"https://na16.salesforce.com/img/icon/calendar16.png\",\"quickActionName\":\"Opportunity.Event\",\"targetSobjectType\":\"Event\",\"type\":\"Create\",\"urls\":{\"defaultValuesTemplate\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Event/defaultValues/{ID}\",\"quickAction\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Event\",\"defaultValues\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Event/defaultValues\",\"describe\":\"/services/data/v33.0/sobjects/Opportunity/quickActions/Event/describe\"}},{\"accessLevelRequired\":null,\"colors\":[{\"color\":\"7A9AE6\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"iconUrl\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_link.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_link_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_link_120.png\",\"width\":120}],\"label\":\"Link\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.LinkPost\",\"targetSobjectType\":null,\"type\":\"Post\",\"urls\":{}},{\"accessLevelRequired\":null,\"colors\":[{\"color\":\"699BE1\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"iconUrl\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_poll.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_poll_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/share_poll_120.png\",\"width\":120}],\"label\":\"Poll\",\"miniIconUrl\":\"\",\"quickActionName\":\"FeedItem.PollPost\",\"targetSobjectType\":null,\"type\":\"Post\",\"urls\":{}}]},\"relatedContent\":{\"relatedContentItems\":[{\"describeLayoutItem\":{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Account Name\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":false,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Account ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"AccountId\",\"nameField\":false,\"namePointing\":false,\"nillable\":true,\"permissionable\":true,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"Account\"],\"relationshipName\":\"Account\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":47,\"type\":\"ExpandedLookup\",\"value\":\"AccountId\"}],\"placeholder\":false,\"required\":false}},{\"describeLayoutItem\":{\"editableForNew\":false,\"editableForUpdate\":false,\"label\":\"Opportunity Owner\",\"layoutComponents\":[{\"details\":{\"autoNumber\":false,\"byteLength\":18,\"calculated\":false,\"calculatedFormula\":null,\"cascadeDelete\":false,\"caseSensitive\":false,\"controllerName\":null,\"createable\":true,\"custom\":false,\"defaultValue\":null,\"defaultValueFormula\":null,\"defaultedOnCreate\":true,\"dependentPicklist\":false,\"deprecatedAndHidden\":false,\"digits\":0,\"displayLocationInDecimal\":false,\"externalId\":false,\"extraTypeInfo\":null,\"filterable\":true,\"filteredLookupInfo\":null,\"groupable\":true,\"highScaleNumber\":false,\"htmlFormatted\":false,\"idLookup\":false,\"inlineHelpText\":null,\"label\":\"Owner ID\",\"length\":18,\"mask\":null,\"maskType\":null,\"name\":\"OwnerId\",\"nameField\":false,\"namePointing\":false,\"nillable\":false,\"permissionable\":false,\"picklistValues\":[],\"precision\":0,\"queryByDistance\":false,\"referenceTargetField\":null,\"referenceTo\":[\"User\"],\"relationshipName\":\"Owner\",\"relationshipOrder\":null,\"restrictedDelete\":false,\"restrictedPicklist\":false,\"scale\":0,\"soapType\":\"tns:ID\",\"sortable\":true,\"type\":\"reference\",\"unique\":false,\"updateable\":true,\"writeRequiresMasterRead\":false},\"displayLines\":1,\"tabOrder\":48,\"type\":\"ExpandedLookup\",\"value\":\"OwnerId\"}],\"placeholder\":false,\"required\":false}}]},\"relatedLists\":[{\"accessLevelRequiredForCreate\":null,\"buttons\":[{\"behavior\":null,\"colors\":[{\"color\":\"4BC076\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_task_120.png\",\"width\":120}],\"label\":\"New Task\",\"menubar\":false,\"name\":\"NewTask\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":[{\"color\":\"EB7092\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_event.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_event_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/new_event_120.png\",\"width\":120}],\"label\":\"New Event\",\"menubar\":false,\"name\":\"NewEvent\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"New Meeting Request\",\"menubar\":false,\"name\":\"NewProposeMeeting\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null}],\"columns\":[{\"field\":\"OpenActivity.Subject\",\"format\":null,\"label\":\"Subject\",\"lookupId\":\"Id\",\"name\":\"Subject\"},{\"field\":\"Name.Name\",\"format\":null,\"label\":\"Name\",\"lookupId\":\"WhoId\",\"name\":\"Who.Name\"},{\"field\":\"OpenActivity.IsTask\",\"format\":null,\"label\":\"Task\",\"lookupId\":null,\"name\":\"IsTask\"},{\"field\":\"OpenActivity.ActivityDate\",\"format\":\"date\",\"label\":\"Due Date\",\"lookupId\":null,\"name\":\"ActivityDate\"},{\"field\":\"OpenActivity.Status\",\"format\":null,\"label\":\"Status\",\"lookupId\":null,\"name\":\"toLabel(Status)\"},{\"field\":\"OpenActivity.Priority\",\"format\":null,\"label\":\"Priority\",\"lookupId\":null,\"name\":\"toLabel(Priority)\"},{\"field\":\"User.Name\",\"format\":null,\"label\":\"Assigned To\",\"lookupId\":\"Owner.Id\",\"name\":\"Owner.Name\"}],\"custom\":false,\"field\":\"WhatId\",\"label\":\"Open Activities\",\"limitRows\":5,\"name\":\"OpenActivities\",\"sobject\":\"OpenActivity\",\"sort\":[{\"ascending\":true,\"column\":\"ActivityDate\"},{\"ascending\":false,\"column\":\"LastModifiedDate\"}]},{\"accessLevelRequiredForCreate\":null,\"buttons\":[{\"behavior\":null,\"colors\":[{\"color\":\"48C3CC\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/log_a_call.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/log_a_call_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/log_a_call_120.png\",\"width\":120}],\"label\":\"Log a Call\",\"menubar\":false,\"name\":\"LogCall\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"Mail Merge\",\"menubar\":false,\"name\":\"MailMerge\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":[{\"color\":\"95AEC5\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/email.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/email_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/email_120.png\",\"width\":120}],\"label\":\"Send an Email\",\"menubar\":false,\"name\":\"SendEmail\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"Compose Gmail\",\"menubar\":false,\"name\":\"ComposeGmail\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"Request Update\",\"menubar\":false,\"name\":\"RequestUpdate\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"View All\",\"menubar\":false,\"name\":\"ViewAll\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null}],\"columns\":[{\"field\":\"ActivityHistory.Subject\",\"format\":null,\"label\":\"Subject\",\"lookupId\":\"Id\",\"name\":\"Subject\"},{\"field\":\"Name.Name\",\"format\":null,\"label\":\"Name\",\"lookupId\":\"WhoId\",\"name\":\"Who.Name\"},{\"field\":\"ActivityHistory.IsTask\",\"format\":null,\"label\":\"Task\",\"lookupId\":null,\"name\":\"IsTask\"},{\"field\":\"ActivityHistory.ActivityDate\",\"format\":\"date\",\"label\":\"Due Date\",\"lookupId\":null,\"name\":\"ActivityDate\"},{\"field\":\"User.Name\",\"format\":null,\"label\":\"Assigned To\",\"lookupId\":\"Owner.Id\",\"name\":\"Owner.Name\"},{\"field\":\"ActivityHistory.LastModifiedDate\",\"format\":\"datetime\",\"label\":\"Last Modified Date/Time\",\"lookupId\":null,\"name\":\"LastModifiedDate\"}],\"custom\":false,\"field\":\"WhatId\",\"label\":\"Activity History\",\"limitRows\":5,\"name\":\"ActivityHistories\",\"sobject\":\"ActivityHistory\",\"sort\":[{\"ascending\":false,\"column\":\"ActivityDate\"},{\"ascending\":false,\"column\":\"LastModifiedDate\"}]},{\"accessLevelRequiredForCreate\":null,\"buttons\":null,\"columns\":[{\"field\":\"CombinedAttachment.Title\",\"format\":null,\"label\":\"Title\",\"lookupId\":\"Id\",\"name\":\"Title\"},{\"field\":\"CombinedAttachment.RecordType\",\"format\":null,\"label\":\"Type\",\"lookupId\":null,\"name\":\"RecordType\"},{\"field\":\"CombinedAttachment.LastModifiedDate\",\"format\":null,\"label\":\"Last Modified\",\"lookupId\":null,\"name\":\"LastModifiedDate\"},{\"field\":\"CombinedAttachment.CreatedById\",\"format\":null,\"label\":\"Created By\",\"lookupId\":null,\"name\":\"CreatedBy.Name\"},{\"field\":\"CombinedAttachment.FileType\",\"format\":null,\"label\":\"File Type\",\"lookupId\":null,\"name\":\"FileType\"},{\"field\":\"CombinedAttachment.ContentSize\",\"format\":null,\"label\":\"Content Size\",\"lookupId\":null,\"name\":\"ContentSize\"},{\"field\":\"CombinedAttachment.FileExtension\",\"format\":null,\"label\":\"File Extension\",\"lookupId\":null,\"name\":\"FileExtension\"},{\"field\":\"CombinedAttachment.ContentUrl\",\"format\":null,\"label\":\"Content URL\",\"lookupId\":null,\"name\":\"ContentUrl\"}],\"custom\":false,\"field\":\"ParentId\",\"label\":\"Notes & Attachments\",\"limitRows\":5,\"name\":\"CombinedAttachments\",\"sobject\":\"CombinedAttachment\",\"sort\":[{\"ascending\":false,\"column\":\"LastModifiedDate\"}]},{\"accessLevelRequiredForCreate\":null,\"buttons\":null,\"columns\":[{\"field\":\"Contact.Name\",\"format\":null,\"label\":\"Contact Name\",\"lookupId\":null,\"name\":\"Contact.Name\"},{\"field\":\"Account.Name\",\"format\":null,\"label\":\"Account Name\",\"lookupId\":null,\"name\":\"Contact.Account.Name\"},{\"field\":\"Contact.Email\",\"format\":null,\"label\":\"Email\",\"lookupId\":null,\"name\":\"Contact.Email\"},{\"field\":\"Contact.Phone\",\"format\":null,\"label\":\"Phone\",\"lookupId\":null,\"name\":\"Contact.Phone\"},{\"field\":\"OpportunityContactRole.Role\",\"format\":null,\"label\":\"Role\",\"lookupId\":null,\"name\":\"Role\"},{\"field\":\"OpportunityContactRole.IsPrimary\",\"format\":null,\"label\":\"Primary\",\"lookupId\":null,\"name\":\"IsPrimary\"}],\"custom\":false,\"field\":\"OpportunityId\",\"label\":\"Contact Roles\",\"limitRows\":5,\"name\":\"OpportunityContactRoles\",\"sobject\":\"OpportunityContactRole\",\"sort\":[{\"ascending\":true,\"column\":\"Contact.Name\"}]},{\"accessLevelRequiredForCreate\":null,\"buttons\":[{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"Add Product\",\"menubar\":false,\"name\":\"AddProduct\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"Edit All\",\"menubar\":false,\"name\":\"EditAllProduct\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"Choose Price Book\",\"menubar\":false,\"name\":\"ChoosePricebook\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null},{\"behavior\":null,\"colors\":[{\"color\":\"FAB9A5\",\"context\":\"primary\",\"theme\":\"theme4\"}],\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":[{\"contentType\":\"image/svg+xml\",\"height\":0,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/sort.svg\",\"width\":0},{\"contentType\":\"image/png\",\"height\":60,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/sort_60.png\",\"width\":60},{\"contentType\":\"image/png\",\"height\":120,\"theme\":\"theme4\",\"url\":\"https://na16.salesforce.com/img/icon/t4v32/action/sort_120.png\",\"width\":120}],\"label\":\"Sort\",\"menubar\":false,\"name\":\"Sort\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null}],\"columns\":[{\"field\":\"Product2.Name\",\"format\":null,\"label\":\"Product\",\"lookupId\":null,\"name\":\"PricebookEntry.Product2.Name\"},{\"field\":\"OpportunityLineItem.Quantity\",\"format\":null,\"label\":\"Quantity\",\"lookupId\":null,\"name\":\"Quantity\"},{\"field\":\"OpportunityLineItem.UnitPrice\",\"format\":null,\"label\":\"Sales Price\",\"lookupId\":null,\"name\":\"UnitPrice\"},{\"field\":\"OpportunityLineItem.ServiceDate\",\"format\":\"date\",\"label\":\"Date\",\"lookupId\":null,\"name\":\"ServiceDate\"},{\"field\":\"OpportunityLineItem.Description\",\"format\":null,\"label\":\"Line Description\",\"lookupId\":null,\"name\":\"Description\"},{\"field\":\"PricebookEntry.UnitPrice\",\"format\":null,\"label\":\"List Price\",\"lookupId\":null,\"name\":\"PricebookEntry.UnitPrice\"}],\"custom\":false,\"field\":\"OpportunityId\",\"label\":\"Products\",\"limitRows\":5,\"name\":\"OpportunityLineItems\",\"sobject\":\"OpportunityLineItem\",\"sort\":[{\"ascending\":true,\"column\":\"SortOrder\"}]},{\"accessLevelRequiredForCreate\":null,\"buttons\":[{\"behavior\":null,\"colors\":null,\"content\":null,\"contentSource\":null,\"custom\":false,\"encoding\":null,\"height\":null,\"icons\":null,\"label\":\"New Quote\",\"menubar\":false,\"name\":\"NewQuote\",\"overridden\":false,\"resizeable\":false,\"scrollbars\":false,\"showsLocation\":false,\"showsStatus\":false,\"toolbar\":false,\"url\":null,\"width\":null,\"windowPosition\":null}],\"columns\":[{\"field\":\"Quote.QuoteNumber\",\"format\":null,\"label\":\"Quote Number\",\"lookupId\":\"Id\",\"name\":\"QuoteNumber\"},{\"field\":\"Quote.Name\",\"format\":null,\"label\":\"Quote Name\",\"lookupId\":\"Id\",\"name\":\"Name\"},{\"field\":\"Quote.IsSyncing\",\"format\":null,\"label\":\"Syncing\",\"lookupId\":null,\"name\":\"IsSyncing\"},{\"field\":\"Quote.ExpirationDate\",\"format\":\"date\",\"label\":\"Expiration Date\",\"lookupId\":null,\"name\":\"ExpirationDate\"},{\"field\":\"Quote.Discount\",\"format\":null,\"label\":\"Discount\",\"lookupId\":null,\"name\":\"Discount\"},{\"field\":\"Quote.GrandTotal\",\"format\":null,\"label\":\"Grand Total\",\"lookupId\":null,\"name\":\"GrandTotal\"},{\"field\":\"User.Name\",\"format\":null,\"label\":\"Created By\",\"lookupId\":\"CreatedBy.Id\",\"name\":\"CreatedBy.Name\"}],\"custom\":false,\"field\":\"OpportunityId\",\"label\":\"Quotes\",\"limitRows\":5,\"name\":\"Quotes\",\"sobject\":\"Quote\",\"sort\":[{\"ascending\":true,\"column\":\"Name\"}]}]}],\"recordTypeMappings\":[{\"available\":true,\"defaultRecordTypeMapping\":true,\"layoutId\":\"00hj0000000wT8jAAE\",\"name\":\"Master\",\"picklistsForRecordType\":[],\"recordTypeId\":\"012000000000000AAA\",\"urls\":{\"layout\":\"/services/data/v33.0/sobjects/Opportunity/describe/layouts/012000000000000AAA\"}}],\"recordTypeSelectorRequired\":[false]}"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/spec/form-builder-spec.jsx",
    "content": "import React from 'react'\nimport ReactTestUtils from 'react-addons-test-utils'\nimport {\n  FormItem,\n  GeneratedForm,\n  GeneratedFieldset,\n} from 'nylas-component-kit'\nimport SalesforceSchemaAdapter from '../lib/form/salesforce-schema-adapter'\nimport rawData from './fixtures/opportunity-layouts.json'\n\nconst rawLayout = SalesforceSchemaAdapter.defaultLayout(rawData)\nconst testData = SalesforceSchemaAdapter.convertFullEditLayout({objectType: \"opportunity\", rawLayout: rawLayout})\n\nfunction StubDiv() {\n  return <div />\n}\n\nxdescribe('Form Builder', function describeBlock() {\n  beforeEach(() => {\n    for (let i = 0; i < testData.fieldsets.length; i++) {\n      const fieldset = testData.fieldsets[i];\n      for (let j = 0; j < fieldset.formItems.length; j++) {\n        const formItem = fieldset.formItems[j];\n        if (formItem.type === \"reference\") {\n          formItem.type = StubDiv\n        }\n      }\n    }\n    this.form = ReactTestUtils.renderIntoDocument(\n      <GeneratedForm {...testData} onSubmit={() => {}} onChange={() => {}} />\n    )\n  })\n\n  it(\"generates a form\", () => {\n    const forms = ReactTestUtils.scryRenderedComponentsWithType(this.form, GeneratedForm);\n    const $forms = ReactTestUtils.scryRenderedDOMComponentsWithTag(this.form, \"form\");\n    expect(forms.length).toBeGreaterThan(0);\n    expect($forms.length).toBeGreaterThan(0);\n  });\n\n  it(\"generates a fieldset\", () => {\n    const fieldsets = ReactTestUtils.scryRenderedComponentsWithType(this.form, GeneratedFieldset);\n    const $fieldsets = ReactTestUtils.scryRenderedDOMComponentsWithTag(this.form, \"fieldset\");\n    expect(fieldsets.length).toBeGreaterThan(0);\n    expect($fieldsets.length).toBeGreaterThan(0);\n  });\n\n  it(\"generates a form item\", () => {\n    const items = ReactTestUtils.scryRenderedComponentsWithType(this.form, FormItem);\n    expect(items.length).toBeGreaterThan(0);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/spec/generate-test-data.es6",
    "content": "\nimport {\n  DatabaseStore,\n  Thread,\n} from 'nylas-exports'\n\nimport SalesforceObject from '../lib/models/salesforce-object'\nimport SalesforceAPI from '../lib/salesforce-api'\n\nconst companyNames = [\n  \"Lyft\",\n  \"Airbnb\",\n  \"Salesforce\",\n  \"Facebook\",\n  \"Google\",\n  \"Apple\",\n  \"Tesla\",\n  \"SpaceX\",\n  \"Dropbox\",\n  \"Snap\",\n  \"Twitter\",\n  \"Oracle\",\n  \"Sequoia Capital\",\n  \"KPCB\",\n  \"Andreessen Horowitz\",\n]\n\nconst oppNames = [\n  \"Lyft Sales\",\n  \"Airbnb Marketing\",\n  \"Salesforce Recruiting\",\n  \"Facebook Sales\",\n  \"Google Marketing\",\n  \"Apple Recruiting\",\n  \"Tesla Sales\",\n  \"SpaceX Marketing\",\n  \"Dropbox Recruiting\",\n  \"Snap Sales\",\n  \"Twitter Marketing\",\n  \"Oracle Recruiting\",\n  \"Sequoia Capital Sales\",\n  \"KPCB Marketing\",\n  \"Andreessen Horowitz Recruiting\",\n]\n\n\nclass GenerateTestData {\n\n  constructor() {\n    this._threads = []\n    this._contacts = []\n    this._accounts = []\n    this._index = 29\n  }\n\n  populateAccounts() {\n    DatabaseStore.findAll(SalesforceObject, {\n      type: \"Account\",\n    })\n    .then((accounts) => {\n      console.log(accounts)\n      this._accounts = accounts\n    })\n  }\n\n  createSalesforceContacts() {\n    DatabaseStore.findAll(Thread)\n    .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n    .limit(2000)\n    .then((threads) => {\n      this._threads = threads\n      const contactEmails = []\n      this._threads.forEach((thread) => {\n        thread.participants.forEach((contact) => {\n          if (!contactEmails.includes(contact.email)) {\n            this._contacts.push(contact)\n            contactEmails.push(contact.email)\n          }\n        })\n      })\n\n      for (const contact of this._contacts) {\n        const formPostData = {\n          Email: contact.email,\n          FirstName: contact.firstName,\n          LastName: contact.lastName,\n          OwnerId: \"00541000000ohxCAAQ\",\n        }\n        SalesforceAPI.makeRequest({\n          path: \"/sobjects/Contact/\",\n          method: \"POST\",\n          body: formPostData,\n        })\n      }\n    })\n  }\n\n  createSalesforceAccounts() {\n    for (const companyName of companyNames) {\n      const formPostData = {\n        Name: companyName,\n        OwnerId: \"00541000000ohxCAAQ\",\n      }\n      SalesforceAPI.makeRequest({\n        path: \"/sobjects/Account/\",\n        method: \"POST\",\n        body: formPostData,\n      })\n    }\n  }\n\n  createSalesforceOpportunities() {\n    for (const oppName of oppNames) {\n      const formPostData = {\n        CloseDate: \"2016-11-20\",\n        Name: oppName,\n        StageName: \"Prospecting\",\n        Probability: \"20\",\n        Amount: \"15000\",\n        OwnerId: \"00541000000ohxCAAQ\",\n      }\n      SalesforceAPI.makeRequest({\n        path: \"/sobjects/Opportunity/\",\n        method: \"POST\",\n        body: formPostData,\n      })\n    }\n  }\n\n  // Adding to accounts didn't work for some reason\n  addContactsToAccountsAndOpportunities() {\n    DatabaseStore.findAll(SalesforceObject, {\n      type: \"Contact\",\n    })\n    .then((contacts) => {\n      for (const contact of contacts) {\n        if (this._isLucky()) {\n          this._chooseAccountAndOpportunity()\n          .then(({account, opportunity}) => {\n            const accountData = {\n              Email: contact.email,\n              FirstName: contact.firstName,\n              LastName: contact.lastName,\n              AccountId: account.id,\n              OwnerId: \"00541000000ohxCAAQ\",\n            }\n            SalesforceAPI.makeRequest({\n              path: \"/sobjects/Contact/\",\n              method: \"PATCH\",\n              body: accountData,\n            })\n            const opportunityData = {\n              ContactId: contact.id,\n              OpportunityId: opportunity.id,\n            }\n            SalesforceAPI.makeRequest({\n              path: \"/sobjects/OpportunityContactRole/\",\n              method: \"POST\",\n              body: opportunityData,\n            })\n          })\n        }\n      }\n    })\n  }\n\n  _chooseAccountAndOpportunity() {\n    const account = this._accounts[this._getIndex()]\n    return DatabaseStore.findAll(SalesforceObject, {\n      type: \"Opportunity\",\n    })\n    .where(SalesforceObject.attributes.name.like(account.name))\n    .then((opportunities) => {\n      return Promise.resolve({\n        account: account,\n        opportunity: opportunities[0],\n      })\n    })\n  }\n\n  _isLucky() {\n    return Math.floor((Math.random() * 100)) < 75\n  }\n\n  _getIndex() {\n    if (this._index === 44) {\n      this._index = 29\n    }\n    this._index++\n    return this._index\n  }\n}\n\nexport default new GenerateTestData()\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/spec/salesforce-schema-adapter-spec.es6",
    "content": "import _ from 'underscore';\nimport fs from 'fs';\nimport path from 'path';\nimport {GeneratedForm, GeneratedFieldset, FormItem} from 'nylas-component-kit';\n\nimport SalesforceSchemaAdapter from '../lib/form/salesforce-schema-adapter';\n\nconst fpath = path.resolve(__dirname, 'fixtures/opportunity-layouts.json');\nconst opportunityLayouts = JSON.parse(fs.readFileSync(fpath, 'utf-8'));\n\n\ndescribe(\"SalesforceSchemaAdapter\", function describeBlock() {\n  beforeEach(() => {\n    const rawLayout = SalesforceSchemaAdapter.defaultLayout(opportunityLayouts);\n    this.schema = SalesforceSchemaAdapter.convertFullEditLayout({objectType: \"opportunity\", rawLayout});\n  });\n\n  it(\"gets the values into the schema correctly\", () => {\n    expect(this.schema.id).toBeDefined();\n    expect(this.schema.objectType).toBe(\"opportunity\");\n\n    const {fieldsets} = this.schema;\n    expect(fieldsets.length).toBe(4);\n\n    const fieldset = fieldsets[0];\n    expect(fieldset.heading).toBe(\"Opportunity Information\");\n    expect(fieldset.formItems.length).toBe(14);\n\n    const formItem = fieldset.formItems[11];\n    expect(formItem.label).toBe(\"Amount\");\n    expect(formItem.type).toBe(\"number\");\n    expect(formItem.row).toBe(5);\n    expect(formItem.column).toBe(1);\n    expect(fieldset.formItems[0].row).toBe(0);\n    expect(fieldset.formItems[0].column).toBe(0);\n\n    const {selectOptions} = fieldset.formItems[5];\n    expect(selectOptions.length).toBe(10);\n    expect(selectOptions[0].value).toBe(\"Prospecting\");\n  });\n\n  it(\"only uses valid form types\", () => {\n    const {formItems} = this.schema.fieldsets[0];\n    const types = _.pluck(formItems, \"type\");\n    const validTypes = Object.keys(FormItem.inputElementTypes);\n\n    // Code elsewhere will custom handle these types\n    const customTypes = [\"reference\", \"textarea\", \"select\", \"EmptySpace\"];\n\n    expect(_.difference(types, validTypes.concat(customTypes))).toEqual([]);\n  });\n\n  it(\"generates the correct schema\", () => {\n    expect(_.difference(Object.keys(this.schema),\n                        Object.keys(GeneratedForm.propTypes)))\n          .toEqual([\"schemaType\", \"objectType\", \"createdAt\"]); // Leftovers not used in element\n\n    const fieldset = this.schema.fieldsets[0];\n    expect(_.difference(Object.keys(fieldset),\n                        Object.keys(GeneratedFieldset.propTypes)))\n          .toEqual([\"rows\", \"columns\"]); // Leftovers not used in element\n\n    const formItem = fieldset.formItems[5];\n    expect(_.difference(Object.keys(formItem),\n                        Object.keys(FormItem.propTypes)))\n          .toEqual([\"length\"]); // Leftovers not used in element\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/spec/syncback-salesforce-object-task-spec.es6",
    "content": "describe(\"SyncbackSalesforceObjectTask\", function SyncbackSalesforceObjectTaskSpec() {\n  describe(\"when saving an opportunity\", () => {\n    it(\"keeps contacts when they're the same\");\n    it(\"creates new contacts when added to field\");\n    it(\"removes contats when removed from field\");\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/open-in-salesforce-btn.less",
    "content": "@import \"ui-variables\";\n.open-in-salesforce-btn {\n  padding: 3px 4px 5px 4px;\n  box-shadow: inset 0 0 0 0.5px rgba(0,0,0,0.15), 0 0.5px 0.5px 0.5px rgba(0,0,0,0.07);\n  border-radius: 3px;\n  background-color: @background-primary;\n  background-image: linear-gradient(to top, fadeout(difference(@background-primary, white), 97), @background-primary 100%);\n  line-height: 12px;\n  display: inline-block;\n  height: 20px;\n\n  &.large {\n    padding: 5px 4px 6px 5px;\n  }\n}\n\n.large-cell .open-in-salesforce-btn.large {\n  margin-top: -1px;\n  height: 24px;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-association.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.message-list-salesforce-opportunity-bar {\n  background: @background-primary;\n  border-bottom: 1px solid @border-color-primary;\n  padding: 4px 15px 5px 15px;\n\n  .opp-picker {\n    padding-left: 15px;\n  }\n  .opp-name {\n    font-weight:@headings-font-weight;\n    padding-left: 0.5em;\n  }\n  .unlink {\n    font-size: @font-size-small;\n    padding-left: 0.5em;\n    a {\n      color: @text-color-subtle;\n      border-bottom: 1px solid @text-color-subtle;\n      &:hover {\n        color: darken(@text-color-link, 10%);\n        border-bottom: 1px solid darken(@text-color-link, 10%);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-composer.less",
    "content": ".token .participant .sf-icon-wrap, .collapsed-contact .sf-icon-wrap {\n  position: relative;\n  left: 0;\n  top: 0;\n  margin: 0 6px 0 0;\n}\n.salesforce-contact-search-result {\n  .sf-icon-wrap {\n    top: 0;\n    left: 0;\n    margin-right: 6px;\n    position: relative;\n  }\n}\n.item.selected .salesforce-contact-search-result .sf-icon-wrap {\n  background: transparent !important;\n}\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-contact.less",
    "content": "@import \"ui-variables\";\n\n.salesforce-contact-info {\n  font-size: 12px;\n  margin-bottom: 22px;\n  .sf-profile {\n    .profile-source-icon {\n      width: 18px;\n      padding-top: 0;\n      float: left;\n    }\n    .profile-source-link {\n      color: @text-color-link;\n      a {\n        text-decoration: none;\n        &:hover {\n          color: @text-color-link;\n        }\n      }\n    }\n  }\n  .create-sf-obj-link {\n    display: block;\n    position: relative;\n    margin-top: 0.5em;\n    &:first-child {\n      margin-top: 0;\n    }\n    color: @text-color-link;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-icon.less",
    "content": "@import \"ui-variables\";\n@icon-size: 20px;\n\n.sf-icon-wrap {\n  position: absolute;\n  left: 10px;\n  top: 50%;\n  margin-top: @icon-size / -2;\n  box-shadow: inset 0 0 0 0.5px rgba(0,0,0,0.1);\n  background-image: linear-gradient(to top, rgba(255,255,255,0) 0%, rgba(255,255,255,0.11) 100%);\n\n  display: inline-block;\n  width: @icon-size;\n  height: @icon-size;\n  line-height: @icon-size - 1px;\n  border-radius: 2px;\n\n  &.checked {\n    &:after {\n      content: \"✓\";\n      width: 8px;\n      height: 8px;\n      font-size: 6px;\n      position: absolute;\n      color: #fff;\n      background: @color-success;\n      bottom: 0;\n      right: 0;\n      border-radius: 2px 0 2px 0;\n      line-height: 6px;\n      text-align: center;\n      padding-top: 1px;\n      padding-left: 1px;\n      box-shadow: 0 0 0 0.5px rgba(0,0,0,0.1);\n    }\n  }\n\n  &.round {\n    border-radius: 50%;\n    padding: 5px;\n    .sf-icon-img {\n      width: @icon-size * 0.58;\n      height: @icon-size * 0.58;\n      vertical-align: top;\n    }\n  }\n\n  &.round-create {\n    border-radius: 50%;\n    padding: 1px;\n    .sf-icon-img {\n      width: @icon-size * 0.9;\n      height: @icon-size * 0.9;\n      vertical-align: text-top;\n    }\n  }\n\n  &.inline {\n    position: relative;\n    left: 0;\n    top: 0;\n    margin-top: 0;\n    margin-left: 10px;\n  }\n\n  .sf-icon-img {\n    width: @icon-size;\n    height: @icon-size;\n    position: relative;\n    margin-top: -1px;\n  }\n}\n\n.cell-item.action-item .sf-icon-wrap {\n  width: 14px;\n  height: 14px;\n  margin-top: -7px;\n  left: 16px;\n  padding: 4px 3px;\n  .sf-icon-img {\n    width: 8px;\n    height: 8px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-object-form.less",
    "content": "@import \"ui-variables\";\n\n.salesforce-object-form-wrap {\n  overflow-y: auto;\n}\n.salesforce-object-form {\n  flex: 1;\n  position: relative;\n  .spinner {\n    z-index: 1001;\n  }\n  .form-name {\n    padding: 0 22px;\n    h1 {\n      margin: 22px 0;\n    }\n  }\n  .form-footer {\n    position: fixed;\n    width: 100%;\n    bottom: 0;\n    z-index: 9999;\n  }\n  fieldset {\n    &:last-child {\n      margin-bottom: 44px;\n    }\n    position: relative;\n    // z-index set by generated-form.cjsx\n    border-bottom: 0;\n  }\n\n  &.schema-error {\n    display: flex;\n    color: @color-error;\n    align-items: center;\n    justify-content: center;\n    max-width: 540px;\n    margin: 0 auto;\n    padding: 20px;\n  }\n}\n.salesforce-delete-object {\n  position: absolute;\n  right: 0;\n  padding: 22px 22px 0 22px;\n  text-align: right;\n  z-index: 10;\n  background: white;\n  &.confirm-control {\n    padding-top: 12px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-object-picker.less",
    "content": "@import \"ui-variables\";\n\n@lead: #f88962;\n@task: #4bc076;\n@case: #f2cf5b;\n@account: #7f8de1;\n@contact: #a094ed;\n@opportunity: #fcb95b;\n@lead_convert: #f88962;\n@emailmessage: #95aec5;\n\nbody.platform-win32 {\n  .salesforce .cell-container {\n    border-radius: 0;\n  }\n}\n\n.salesforce {\n\n  .linkable-object-name {\n    flex: 1;\n    font-weight: 600;\n    font-size: 13.6px;\n    margin-right: 18px;\n    &.Lead { color: @lead; }\n    &.Task { color: @task; }\n    &.Case { color: @case; }\n    &.Account { color: @account; }\n    &.Contact { color: @contact; }\n    &.Opportunity { color: @opportunity; }\n    &.EmailMessage { color: @emailmessage; }\n  }\n\n  h2.sidebar-h2 {\n    // font-size: 11px;\n    // font-weight: @font-weight-semi-bold;\n    // text-transform: uppercase;\n    // color: @text-color-very-subtle;\n    // border-bottom: 1px solid @border-color-divider;\n    // border-bottom: 0;\n    // margin: 1.5em 0 1em 0;\n    // &:after {\n    //   background-image: url(nylas://nylas-private-salesforce/static/images/sidebar-section-divider@2x.png);\n    //   background-repeat: repeat-x;\n    //   width: 100%;\n    //   height: 3px;\n    // }\n    // &:first-child {\n    //   margin-top: 0;\n    // }\n  }\n\n  .cell-container {\n    position: relative;\n    font-size: 13px;\n    box-shadow: inset 0 0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.08);\n    border-radius: 4px;\n    margin: 0 0 12px 0;\n  }\n\n  .cell-item {\n    &:hover {\n      cursor: default;\n    }\n    padding: 8px 8.5px 8px 8.5px;\n    border-top: 1px solid rgba(0,0,0,0.1);\n    position: relative;\n    &.large-cell {\n      padding: 14px 12px 14px 12px;\n    }\n    &:first-child {\n      border-top: 0;\n    }\n\n    &.action-item {\n      padding-left: 38px;\n      padding-right: 32px;\n      font-size: 11px;\n      color: @text-color-subtle;\n      box-shadow: inset 0 0 0.5px 0 rgba(0,0,0,0.15);\n    }\n  }\n\n  .action-icon {\n    position: relative;\n    top: -1px;\n  }\n\n  .linkable-object-details {\n    font-size: 11px;\n    font-weight: @font-weight-medium;\n    color: @text-color-subtle;\n  }\n\n}\n\n.salesforce-object-picker {\n  .item {\n    padding-left: 5px;\n    padding-right: 5px;\n  }\n\n  .salesforce-suggestion {\n    position: relative;\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    padding-left: 25px;\n  }\n\n  .sf-icon-wrap {\n    left: 0;\n  }\n\n  .tokenizing-field {\n    .token {\n      .sf-icon-wrap {\n        left: 3px;\n      }\n    }\n  }\n}\n\n.salesforce-login {\n  margin: 0 10px 10px 10px;\n}\n\n.salesforce-manually-relate-popover {\n  width: 280px;\n  padding: 10px;\n\n  h5 {\n    margin: 0;\n    color: #afafaf;\n  }\n\n  input {\n    border: none !important;\n  }\n\n  .header-container {\n    padding: 0 !important;\n  }\n\n  .content-container {\n    background: white !important;\n    max-height: 300px;\n  }\n\n  .salesforce-object-picker {\n    padding: 10px 0 0 0;\n  }\n\n  .placeholder {\n    font-weight: normal !important;\n  }\n}\n\n.related-objects-wrap {\n  a{text-decoration: none}\n\n  .replica-status {\n    position: absolute;\n    right: 15px;\n    top: 8px;\n    .synced { display: none; }\n    .syncing { display: block; }\n  }\n  .has-replica {\n    .replica-status {\n      .synced { display: block; }\n      .syncing { display: none; }\n    }\n  }\n\n  .salesforce-prompt {\n    text-align: center;\n    color: fade(@text-color, 30%);\n    ul {\n      list-style: none;\n      padding-left: 0;\n    }\n    p {\n      margin: 10px 0 0 0;\n    }\n  }\n\n  .salesforce-no-connect-placeholder {\n    text-align: center;\n    margin: 25px 0;\n  }\n\n  h3.sidebar-h3 {\n    font-size: @font-size-smaller;\n    font-weight: @font-weight-normal;\n    margin: 1em 0 0.4em 0;\n  }\n  .tokenizing-field-input, .menu .header-container, .tokenizing-field {\n    border-bottom: 0;\n  }\n\n  .tokenizing-field-input {\n    background: @white;\n\n    overflow: hidden;\n    padding: 0 5px 5px 5px;\n    margin-left: 0;\n    .placeholder {\n      font-size: 14px;\n    }\n\n    .token {\n      padding: 0 20px 0 28px;\n      margin: 5px 5px 0 0;\n\n      &.selected {\n        border: 0;\n      }\n    }\n\n    input[type=text] {\n      border: 0;\n      box-shadow: none;\n      margin: 5px 0 0 0;\n      padding: 0;\n      line-height: 26px;\n    }\n  }\n\n  .sidebar-item {\n    position: relative;\n  }\n\n  .cell-container:hover {\n    .unassociate-object {\n      display: block;\n    }\n  }\n  .unassociate-object {\n    display: none;\n    cursor: default;\n    line-height: 15px;\n    font-size: 15px;\n    position: absolute;\n    right: -10px;\n    top: -10px;\n  }\n  img.colorfill {\n    background: @source-list-active-color;\n  }\n\n  .association-picker {\n    display: flex;\n    flex-direction: column;\n\n    .tokenizing-field-input {\n      border: 1px solid rgba(0,0,0,0.11);\n    }\n\n    .spacer {\n      padding: 5px;\n    }\n  }\n\n  .new-links {\n    margin-top: 10px;\n  }\n}\n\n.salesforce-sync-message-status {\n  padding-left: 20px;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-picker.less",
    "content": "@import \"ui-variables\";\n\n.salesforce-composer-picker {\n  h2.picker-h2 {\n    font-size: 11px;\n    font-weight: @font-weight-semi-bold;\n    text-transform: uppercase;\n    color: @text-color-very-subtle;\n    margin: 0;\n  }\n  .popover {\n    padding: @spacing-standard;\n  }\n\n  .tokenizing-field-input {\n    padding: 0 0.5em;\n    padding-top: 5px;\n    background: @white;\n    margin-top: @spacing-half;\n    border: 1px solid @input-border-color;\n  }\n  .tokenizing-field {\n    border: 0;\n  }\n\n  .menu .content-container {\n    background: @white;\n    position: absolute;\n    left: -235px;\n    bottom: -15px;\n    max-height: 300px;\n    overflow-y: auto;\n  }\n  img.colorfill {\n    background: @source-list-active-color;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-related-object.less",
    "content": "@import \"ui-variables\";\n\n.salesforce .cell-item.sf-related-object {\n  position: relative;\n  display: flex;\n  flex-direction: column;\n  padding: 0;\n\n  &.create-sf-obj-link {\n    border-top: 0;\n    margin-top: 0;\n  }\n\n  .synced-wrap {\n    flex: 1;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n    overflow: hidden;\n    padding-right: 9px;\n  }\n\n  .main-cell-wrap {\n    padding: 10px 10px 10px 37px;\n    box-shadow: inset 0 0 0 0.5px rgba(0,0,0,0.14), 0 1px 1px rgba(0,0,0,0.08);\n    border-radius: 4px 4px 0 0;\n    display: flex;\n    position: relative;\n    z-index: 2;\n    background: @background-off-primary;\n\n    .sf-icon-wrap {\n      left: 10px;\n    }\n  }\n\n  .sub-items-wrap {\n    padding: 5px 0;\n    position: relative;\n    border-bottom: 1px solid rgba(0,0,0,0.1);\n    overflow: auto;\n    z-index: 1;\n    transition: height 200ms ease-in-out;\n  }\n  .sub-item {\n    height: 26px;\n    padding: 3px 10px 3px 37px;\n    position: relative;\n    display: flex;\n  }\n  .toggle {\n    font-size: 12px;\n    text-align: center;\n    padding: 0.5em 15px;\n    border-top: 1px solid rgba(0,0,0,0.08);\n    color: @text-color-link;\n  }\n}\n.cell-container .new-item {\n  position: relative;\n  padding: 7px 10px 8px 37px;\n}\n.cell-container.inline {\n  display: inline-block;\n}\n\nbody.platform-win32 {\n  .salesforce .cell-item.sf-related-object .main-cell-wrap {\n    border-radius: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-sync-label.less",
    "content": ".list-item.focused .salesforce-thread-icons .sf-icon-wrap {\n  background: transparent !important;\n}\n\n.salesforce-thread-icons {\n  position: relative;\n  line-height: 24px;\n\n  .sf-icon-wrap {\n    position: relative;\n    top: -1px;\n    left: 0;\n    width: 21px;\n    height: 21px;\n    margin-right: 6px;\n    line-height: 19px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-sync-message-status.less",
    "content": "@import \"ui-variables\";\n\n.salesforce-sync-message-status {\n  width: 100%;\n  text-align: right;\n  color: @text-color-very-subtle;\n  font-size: @font-size-tiny;\n  &:hover {\n    cursor: default;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/salesforce-welcome-view.less",
    "content": "@import \"ui-variables\";\n\n.salesforce-welcome {\n    padding: 40px;\n    text-align: center;\n\n    h2 {\n        margin-top: -10px;\n    }\n\n    p {\n        margin: 15px auto;\n        width: 500px;\n    }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/search-results.less",
    "content": ".salesforce-search-bar-result {\n  position: relative;\n  padding-left: 26px;\n  .sf-icon-wrap {\n    left: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-salesforce/stylesheets/sync-thread-toggle.less",
    "content": "@import \"ui-variables\";\n.sync-thread-toggle {\n  font-size: @font-size-tiny;\n  margin-right: 10px;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-sounds/lib/main.es6",
    "content": "import {SoundRegistry} from 'nylas-exports'\n\nexport function activate() {\n  // FIXME: Use the nylas:// protocol handlers once we upgrade Electron past\n  // v30.0\n  // See: https://github.com/atom/electron/issues/1123\n  SoundRegistry.register({\n    \"send\": [\"internal_packages\", \"nylas-private-sounds\", \"NYLAS_UI_Send_v1.ogg\"],\n    \"confirm\": [\"internal_packages\", \"nylas-private-sounds\", \"NYLAS_UI_Confirm_v1.ogg\"],\n    \"hit-send\": [\"internal_packages\", \"nylas-private-sounds\", \"NYLAS_UI_HitSend_v1.ogg\"],\n    \"new-mail\": [\"internal_packages\", \"nylas-private-sounds\", \"NYLAS_UI_NewMail_v1.ogg\"],\n  })\n}\n\nexport function deactivate() {\n  SoundRegistry.unregister([\"send\", \"confirm\", \"hit-send\", \"new-mail\"])\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/nylas-private-sounds/package.json",
    "content": "{\n  \"name\": \"nylas-private-sounds\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Nylas Sounds\",\n  \"license\": \"Proprietary\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"all\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/account-types.es6",
    "content": "// const TODO_ACCOUNT_TYPES = [\n//   {\n//     type: 'exchange',\n//     displayName: 'Microsoft Exchange',\n//     icon: 'ic-settings-account-eas.png',\n//     headerIcon: 'setup-icon-provider-exchange.png',\n//     color: '#1ea2a3',\n//   },\n//   {\n//     type: 'outlook',\n//     displayName: 'Outlook.com',\n//     icon: 'ic-settings-account-outlook.png',\n//     headerIcon: 'setup-icon-provider-outlook.png',\n//     color: '#1174c3',\n//   },\n// ]\n\nconst AccountTypes = [\n  {\n    type: 'gmail',\n    displayName: 'Gmail or G Suite',\n    icon: 'ic-settings-account-gmail.png',\n    headerIcon: 'setup-icon-provider-gmail.png',\n    color: '#e99999',\n  },\n  {\n    type: 'office365',\n    displayName: 'Office 365',\n    icon: 'ic-settings-account-outlook.png',\n    headerIcon: 'setup-icon-provider-outlook.png',\n    color: '#0078d7',\n  },\n  {\n    type: 'yahoo',\n    displayName: 'Yahoo',\n    icon: 'ic-settings-account-yahoo.png',\n    headerIcon: 'setup-icon-provider-yahoo.png',\n    color: '#a76ead',\n  },\n  {\n    type: 'icloud',\n    displayName: 'iCloud',\n    icon: 'ic-settings-account-icloud.png',\n    headerIcon: 'setup-icon-provider-icloud.png',\n    color: '#61bfe9',\n  },\n  {\n    type: 'fastmail',\n    displayName: 'FastMail',\n    title: 'Set up your account',\n    icon: 'ic-settings-account-fastmail.png',\n    headerIcon: 'setup-icon-provider-fastmail.png',\n    color: '#24345a',\n  },\n  {\n    type: 'imap',\n    displayName: 'IMAP / SMTP',\n    title: 'Set up your IMAP account',\n    icon: 'ic-settings-account-imap.png',\n    headerIcon: 'setup-icon-provider-imap.png',\n    color: '#aaa',\n  },\n]\n\nexport default AccountTypes;\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/decorators/create-page-for-form.jsx",
    "content": "import {shell} from 'electron'\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport {RetinaImg} from 'nylas-component-kit';\nimport {NylasAPI, Actions} from 'nylas-exports';\n\nimport OnboardingActions from '../onboarding-actions';\nimport {runAuthRequest} from '../onboarding-helpers';\nimport FormErrorMessage from '../form-error-message';\nimport AccountTypes from '../account-types'\n\nconst CreatePageForForm = (FormComponent) => {\n  return class Composed extends React.Component {\n    static displayName = FormComponent.displayName;\n\n    static propTypes = {\n      accountInfo: React.PropTypes.object,\n    };\n\n    constructor(props) {\n      super(props);\n\n      this.state = Object.assign({\n        accountInfo: JSON.parse(JSON.stringify(this.props.accountInfo)),\n        errorFieldNames: [],\n        errorMessage: null,\n      }, FormComponent.validateAccountInfo(this.props.accountInfo));\n    }\n\n    componentDidMount() {\n      this._applyFocus();\n    }\n\n    componentDidUpdate() {\n      this._applyFocus();\n    }\n\n    _applyFocus() {\n      const anyInputFocused = document.activeElement && document.activeElement.nodeName === 'INPUT';\n      if (anyInputFocused) {\n        return;\n      }\n\n      const inputs = Array.from(ReactDOM.findDOMNode(this).querySelectorAll('input'));\n      if (inputs.length === 0) {\n        return;\n      }\n\n      for (const input of inputs) {\n        if (input.value === '') {\n          input.focus();\n          return;\n        }\n      }\n      inputs[0].focus();\n    }\n\n    _isValid() {\n      const {populated, errorFieldNames} = this.state\n      return errorFieldNames.length === 0 && populated\n    }\n\n    onFieldChange = (event) => {\n      const changes = {};\n      if (event.target.type === 'checkbox') {\n        changes[event.target.id] = event.target.checked;\n      } else {\n        changes[event.target.id] = event.target.value;\n        if (event.target.id === 'email') {\n          changes[event.target.id] = event.target.value.trim();\n        }\n      }\n\n      const accountInfo = Object.assign({}, this.state.accountInfo, changes);\n      const {errorFieldNames, errorMessage, populated} = FormComponent.validateAccountInfo(accountInfo);\n\n      this.setState({accountInfo, errorFieldNames, errorMessage, populated, errorStatusCode: null});\n    }\n\n    onSubmit = () => {\n      OnboardingActions.setAccountInfo(this.state.accountInfo);\n      this.refs.form.submit();\n    }\n\n    onFieldKeyPress = (event) => {\n      if (!this._isValid()) { return }\n      if (['Enter', 'Return'].includes(event.key)) {\n        this.onSubmit();\n      }\n    }\n\n    onBack = () => {\n      OnboardingActions.setAccountInfo(this.state.accountInfo);\n      OnboardingActions.moveToPreviousPage();\n    }\n\n    onConnect = (updatedAccountInfo) => {\n      const accountInfo = updatedAccountInfo || this.state.accountInfo;\n      const {errorStatusCode: statusCode} = this.state\n\n      this.setState({submitting: true});\n\n      const reqOptions = {}\n      const isCertificateError = statusCode === 495\n      if (isCertificateError) {\n        reqOptions.forceTrustCertificate = true\n      }\n\n      runAuthRequest(accountInfo, reqOptions)\n      .then((json) => {\n        OnboardingActions.moveToPage('account-onboarding-success')\n        OnboardingActions.accountJSONReceived(json, json.localToken, json.cloudToken)\n      })\n      .catch((err) => {\n        Actions.recordUserEvent('Email Account Auth Failed', {\n          erroredEmail: accountInfo.email,\n          errorMessage: err.message,\n          errorLocation: err.location,\n          provider: accountInfo.type,\n        })\n\n        const errorFieldNames = err.body ? (err.body.missing_fields || err.body.missing_settings || []) : []\n        let errorMessage = err.message;\n        const errorStatusCode = err.statusCode\n\n        if (err.errorType === \"setting_update_error\") {\n          errorMessage = 'The IMAP/SMTP servers for this account do not match our records. Please verify that any server names you entered are correct. If your IMAP/SMTP server has changed, first remove this account from Nylas Mail, then try logging in again.';\n        }\n        if (err.errorType && err.errorType.includes(\"autodiscover\") && (accountInfo.type === 'exchange')) {\n          errorFieldNames.push('eas_server_host')\n          errorFieldNames.push('username');\n        }\n        if (err.statusCode === 401) {\n          if (/smtp/i.test(err.message)) {\n            errorFieldNames.push('smtp_username');\n            errorFieldNames.push('smtp_password');\n          }\n          if (/imap/i.test(err.message)) {\n            errorFieldNames.push('imap_username');\n            errorFieldNames.push('imap_password');\n          }\n          // not sure what these are for -- backcompat?\n          errorFieldNames.push('password')\n          errorFieldNames.push('email');\n          errorFieldNames.push('username');\n        }\n        if (NylasAPI.TimeoutErrorCodes.includes(err.statusCode)) { // timeout\n          errorMessage = \"We were unable to reach your mail provider. Please try again.\"\n        }\n\n        this.setState({errorMessage, errorStatusCode, errorFieldNames, submitting: false});\n      });\n    }\n\n    _renderButton() {\n      const {accountInfo, submitting, errorStatusCode} = this.state;\n      let buttonLabel = FormComponent.submitLabel(accountInfo);\n      const isCertificateError = errorStatusCode === 495\n      if (isCertificateError) {\n        buttonLabel = 'Connect anyway'\n      }\n\n      // We're not on the last page.\n      if (submitting) {\n        return (\n          <button className=\"btn btn-large btn-disabled btn-add-account spinning\">\n            <RetinaImg name=\"sending-spinner.gif\" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} />\n            Adding account&hellip;\n          </button>\n        );\n      }\n\n      if (!this._isValid()) {\n        return (\n          <button className=\"btn btn-large btn-gradient btn-disabled btn-add-account\">{buttonLabel}</button>\n        );\n      }\n\n      return (\n        <button className=\"btn btn-large btn-gradient btn-add-account\" onClick={this.onSubmit}>{buttonLabel}</button>\n      );\n    }\n\n    // When a user enters the wrong credentials, show a message that could\n    // help with common problems. For instance, they may need an app password,\n    // or to enable specific settings with their provider.\n    _renderCredentialsNote() {\n      const {errorStatusCode, accountInfo} = this.state;\n      if (errorStatusCode !== 401) { return false; }\n      let message;\n      let articleURL;\n      if (accountInfo.email.includes(\"@yahoo.com\")) {\n        message = \"Have you enabled access through Yahoo?\";\n        articleURL = \"https://support.nylas.com/hc/en-us/articles/115001076128\";\n      } else {\n        message = \"Some providers require an app password.\"\n        articleURL = \"https://support.nylas.com/hc/en-us/articles/115001056608\";\n      }\n      // We don't use a FormErrorMessage component because the content\n      // we need to display has HTML.\n      return (\n        <div className=\"message error\">\n          {message}&nbsp;\n          <a\n            href=\"\"\n            style={{cursor: 'pointer'}}\n            onClick={() => { shell.openExternal(articleURL) }}\n          >\n            Learn more.\n          </a>\n        </div>\n      );\n    }\n\n    render() {\n      const {accountInfo, errorMessage, errorStatusCode, errorFieldNames, submitting} = this.state;\n      const AccountType = AccountTypes.find(a => a.type === accountInfo.type);\n\n      if (!AccountType) {\n        throw new Error(`Cannot find account type ${accountInfo.type}`);\n      }\n\n      const hideTitle = errorMessage && errorMessage.length > 120;\n\n      return (\n        <div className={`page account-setup ${FormComponent.displayName}`}>\n          <div className=\"logo-container\">\n            <RetinaImg\n              style={{backgroundColor: AccountType.color, borderRadius: 44}}\n              name={AccountType.headerIcon}\n              mode={RetinaImg.Mode.ContentPreserve}\n              className=\"logo\"\n            />\n          </div>\n          {hideTitle ? <div style={{height: 20}} /> : <h2>{FormComponent.titleLabel(AccountType)}</h2>}\n          <FormErrorMessage\n            message={errorMessage}\n            statusCode={errorStatusCode}\n            empty={FormComponent.subtitleLabel(AccountType)}\n          />\n          { this._renderCredentialsNote() }\n          <FormComponent\n            ref=\"form\"\n            accountInfo={accountInfo}\n            errorFieldNames={errorFieldNames}\n            submitting={submitting}\n            onFieldChange={this.onFieldChange}\n            onFieldKeyPress={this.onFieldKeyPress}\n            onConnect={this.onConnect}\n          />\n          <div>\n            <div className=\"btn btn-large btn-gradient\" onClick={this.onBack}>Back</div>\n            {this._renderButton()}\n          </div>\n        </div>\n      );\n    }\n  }\n}\n\nexport default CreatePageForForm;\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/form-error-message.jsx",
    "content": "import React from 'react';\nimport {RegExpUtils} from 'nylas-exports';\n\nconst FormErrorMessage = (props) => {\n  const {message, statusCode, empty} = props;\n  if (!message) {\n    return <div className=\"message empty\">{empty}</div>;\n  }\n\n  const isCertificateError = statusCode === 495\n  if (isCertificateError) {\n    return (\n      <div className=\"error-region\" style={{maxHeight: 21}}>\n        <p className=\"message error error-message\">{message}</p>\n        <p className=\"message error error-message\">\n          The certificate for this server is invalid. Would you like to connect to the server anyway?\n        </p>\n      </div>\n    );\n  }\n\n  const result = RegExpUtils.urlRegex({matchEntireString: false}).exec(message);\n  if (result) {\n    const link = result[0];\n    return (\n      <div className=\"message error\">\n        {message.substr(0, result.index)}\n        <a href={link}>{link}</a>\n        {message.substr(result.index + link.length)}\n      </div>\n    );\n  }\n\n  return (\n    <div className=\"message error\">\n      {message}\n    </div>\n  );\n}\n\nFormErrorMessage.propTypes = {\n  empty: React.PropTypes.string,\n  message: React.PropTypes.string,\n  statusCode: React.PropTypes.number,\n};\n\nexport default FormErrorMessage;\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/form-field.jsx",
    "content": "import React from 'react';\n\nconst FormField = (props) => {\n  return (\n    <span>\n      <label htmlFor={props.field}>{props.title}:</label>\n      <input\n        type={props.type || \"text\"}\n        id={props.field}\n        style={props.style}\n        className={(props.accountInfo[props.field] && props.errorFieldNames.includes(props.field)) ? 'error' : ''}\n        disabled={props.submitting}\n        value={props.accountInfo[props.field] || ''}\n        onKeyPress={props.onFieldKeyPress}\n        onChange={props.onFieldChange}\n      />\n    </span>\n  );\n}\n\nFormField.propTypes = {\n  field: React.PropTypes.string,\n  title: React.PropTypes.string,\n  type: React.PropTypes.string,\n  style: React.PropTypes.object,\n  submitting: React.PropTypes.bool,\n  onFieldKeyPress: React.PropTypes.func,\n  onFieldChange: React.PropTypes.func,\n  errorFieldNames: React.PropTypes.array,\n  accountInfo: React.PropTypes.object,\n}\n\nexport default FormField;\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/main.es6",
    "content": "import {SystemStartService, WorkspaceStore, ComponentRegistry} from 'nylas-exports';\nimport OnboardingRoot from './onboarding-root';\n\nexport function activate() {\n  WorkspaceStore.defineSheet('Main', {root: true}, {list: ['Center']});\n\n  ComponentRegistry.register(OnboardingRoot, {\n    location: WorkspaceStore.Location.Center,\n  });\n\n  const accounts = NylasEnv.config.get('nylas.accounts') || [];\n\n  if (accounts.length === 0) {\n    const startService = new SystemStartService();\n    startService.checkAvailability().then((available) => {\n      if (!available) {\n        return;\n      }\n      startService.doesLaunchOnSystemStart().then((launchesOnStart) => {\n        if (!launchesOnStart) {\n          startService.configureToLaunchOnSystemStart();\n        }\n      });\n    });\n  }\n}\n\nexport function deactivate() {\n\n}\n\nexport function serialize() {\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/onboarding-actions.es6",
    "content": "import Reflux from 'reflux';\n\nconst OnboardingActions = Reflux.createActions([\n  \"setAccountInfo\",\n  \"setAccountType\",\n  \"moveToPreviousPage\",\n  \"moveToPage\",\n  \"authenticationJSONReceived\",\n  \"accountJSONReceived\",\n]);\n\nfor (const key of Object.keys(OnboardingActions)) {\n  OnboardingActions[key].sync = true;\n}\n\nexport default OnboardingActions;\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/onboarding-helpers.es6",
    "content": "/* eslint global-require: 0 */\n\nimport crypto from 'crypto';\nimport {CommonProviderSettings} from 'isomorphic-core'\nimport {\n  N1CloudAPI,\n  NylasAPI,\n  NylasAPIRequest,\n  RegExpUtils,\n} from 'nylas-exports';\n\nconst IMAP_FIELDS = new Set([\n  \"imap_host\",\n  \"imap_port\",\n  \"imap_username\",\n  \"imap_password\",\n  \"imap_security\",\n  \"imap_allow_insecure_ssl\",\n  \"smtp_host\",\n  \"smtp_port\",\n  \"smtp_username\",\n  \"smtp_password\",\n  \"smtp_security\",\n  \"smtp_allow_insecure_ssl\",\n]);\n\nfunction base64url(inBuffer) {\n  let buffer;\n  if (typeof inBuffer === \"string\") {\n    buffer = new Buffer(inBuffer);\n  } else if (inBuffer instanceof Buffer) {\n    buffer = inBuffer;\n  } else {\n    throw new Error(`${inBuffer} must be a string or Buffer`)\n  }\n  return buffer.toString('base64')\n    .replace(/\\+/g, '-')  // Convert '+' to '-'\n    .replace(/\\//g, '_'); // Convert '/' to '_'\n}\n\nconst NO_AUTH = { user: '', pass: '', sendImmediately: true };\n\nexport async function makeGmailOAuthRequest(sessionKey) {\n  const remoteRequest = new NylasAPIRequest({\n    api: N1CloudAPI,\n    options: {\n      path: `/auth/gmail/token?key=${sessionKey}`,\n      method: 'GET',\n      auth: NO_AUTH,\n    },\n  });\n  return remoteRequest.run()\n}\n\nexport async function authIMAPForGmail(tokenData, {forceTrustCertificate = false} = {}) {\n  const localRequest = new NylasAPIRequest({\n    api: NylasAPI,\n    options: {\n      path: `/auth`,\n      method: 'POST',\n      auth: NO_AUTH,\n      timeout: 1000 * 90, // Connecting to IMAP could take up to 90 seconds, so we don't want to hang up too soon\n      body: {\n        email: tokenData.email_address,\n        name: tokenData.name,\n        provider: 'gmail',\n        settings: {\n          xoauth2: tokenData.resolved_settings.xoauth2,\n          expiry_date: tokenData.resolved_settings.expiry_date,\n          imap_allow_insecure_ssl: forceTrustCertificate,\n          smtp_allow_insecure_ssl: forceTrustCertificate,\n        },\n      },\n    },\n  })\n  const localJSON = await localRequest.run()\n  const account = Object.assign({}, localJSON);\n  account.localToken = localJSON.account_token;\n  account.cloudToken = tokenData.account_token;\n  return account\n}\n\nexport function buildGmailSessionKey(identityId) {\n  return `${identityId}-----${base64url(crypto.randomBytes(40))}`;\n}\n\nexport function buildGmailAuthURL(sessionKey) {\n  return `${N1CloudAPI.APIRoot}/auth/gmail?state=${sessionKey}`;\n}\n\nexport function runAuthRequest(accountInfo, {forceTrustCertificate = false} = {}) {\n  const {username, type, email, name} = accountInfo;\n\n  const settings = Object.assign({}, accountInfo)\n  if (forceTrustCertificate) {\n    settings.imap_allow_insecure_ssl = true\n    settings.smtp_allow_insecure_ssl = true\n  }\n  const data = {\n    provider: type,\n    email: email,\n    name: name,\n    settings,\n  };\n\n  // handle special case for exchange/outlook/hotmail username field\n  data.settings.username = username || email;\n\n  if (data.settings.imap_port) {\n    data.settings.imap_port /= 1;\n  }\n  if (data.settings.smtp_port) {\n    data.settings.smtp_port /= 1;\n  }\n  // if there's an account with this email, get the ID for it to notify the backend of re-auth\n  // const account = AccountStore.accountForEmail(accountInfo.email);\n  // const reauthParam = account ? `&reauth=${account.id}` : \"\";\n\n  /**\n   * Only include the required IMAP fields. Auth validation does not allow\n   * extra fields\n   */\n  if (type !== \"gmail\" && type !== \"office365\") {\n    for (const key of Object.keys(data.settings)) {\n      if (!IMAP_FIELDS.has(key)) {\n        delete data.settings[key]\n      }\n    }\n  }\n\n  const noauth = {\n    user: '',\n    pass: '',\n    sendImmediately: true,\n  };\n\n  // Send the form data directly to Nylas to get code\n  // If this succeeds, send the received code to N1 server to register the account\n  // Otherwise process the error message from the server and highlight UI as needed\n  const n1CloudIMAPAuthRequest = new NylasAPIRequest({\n    api: N1CloudAPI,\n    options: {\n      path: '/auth',\n      method: 'POST',\n      timeout: 1000 * 180, // Same timeout as server timeout (most requests are faster than 90s, but server validation can be slow in some cases)\n      body: data,\n      auth: noauth,\n    },\n  })\n  return n1CloudIMAPAuthRequest.run()\n  .catch((err) => {\n    err.location = \"cloud\"\n    throw err\n  })\n  .then((remoteJSON) => {\n    const localSyncIMAPAuthRequest = new NylasAPIRequest({\n      api: NylasAPI,\n      options: {\n        path: `/auth`,\n        method: 'POST',\n        timeout: 1000 * 180, // Same timeout as server timeout (most requests are faster than 90s, but server validation can be slow in some cases)\n        body: data,\n        auth: noauth,\n      },\n    })\n    return localSyncIMAPAuthRequest.run()\n    .catch((err) => {\n      err.location = \"client\"\n      throw err\n    })\n    .then((localJSON) => {\n      const accountWithTokens = Object.assign({}, localJSON);\n      accountWithTokens.localToken = localJSON.account_token;\n      accountWithTokens.cloudToken = remoteJSON.account_token;\n      return accountWithTokens\n    })\n  })\n}\n\nexport function isValidHost(value) {\n  return RegExpUtils.domainRegex().test(value) || RegExpUtils.ipAddressRegex().test(value);\n}\n\nexport function accountInfoWithIMAPAutocompletions(existingAccountInfo) {\n  const {email, type} = existingAccountInfo;\n  const domain = email.split('@').pop().toLowerCase();\n  let template = CommonProviderSettings[domain] || CommonProviderSettings[type] || {};\n  if (template.alias) {\n    template = CommonProviderSettings[template.alias];\n  }\n\n  const usernameWithFormat = (format) => {\n    if (format === 'email') {\n      return email\n    }\n    if (format === 'email-without-domain') {\n      return email.split('@').shift();\n    }\n    return undefined;\n  }\n\n  const defaults = {\n    imap_host: template.imap_host,\n    imap_port: template.imap_port || 993,\n    imap_username: usernameWithFormat(template.imap_user_format),\n    imap_password: existingAccountInfo.password,\n    imap_security: template.imap_security || \"SSL / TLS\",\n    imap_allow_insecure_ssl: template.imap_allow_insecure_ssl || false,\n    smtp_host: template.smtp_host,\n    smtp_port: template.smtp_port || 587,\n    smtp_username: usernameWithFormat(template.smtp_user_format),\n    smtp_password: existingAccountInfo.password,\n    smtp_security: template.smtp_security || \"STARTTLS\",\n    smtp_allow_insecure_ssl: template.smtp_allow_insecure_ssl || false,\n  }\n\n  return Object.assign({}, existingAccountInfo, defaults);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/onboarding-root.jsx",
    "content": "import React from 'react';\nimport ReactCSSTransitionGroup from 'react-addons-css-transition-group';\nimport {Actions} from 'nylas-exports'\nimport OnboardingStore from './onboarding-store';\nimport PageTopBar from './page-top-bar';\n\nimport WelcomePage from './page-welcome';\nimport AccountChoosePage from './page-account-choose';\nimport AccountSettingsPage from './page-account-settings';\nimport AccountSettingsPageGmail from './page-account-settings-gmail';\nimport AccountSettingsPageIMAP from './page-account-settings-imap';\nimport AccountOnboardingSuccess from './page-account-onboarding-success';\nimport AccountSettingsPageExchange from './page-account-settings-exchange';\nimport InitialPreferencesPage from './page-initial-preferences';\n\n\nconst PageComponents = {\n  \"welcome\": WelcomePage,\n  \"account-choose\": AccountChoosePage,\n  \"account-settings\": AccountSettingsPage,\n  \"account-settings-gmail\": AccountSettingsPageGmail,\n  \"account-settings-imap\": AccountSettingsPageIMAP,\n  \"account-settings-exchange\": AccountSettingsPageExchange,\n  \"account-onboarding-success\": AccountOnboardingSuccess,\n  \"initial-preferences\": InitialPreferencesPage,\n}\n\nexport default class OnboardingRoot extends React.Component {\n  static displayName = 'OnboardingRoot';\n  static containerRequired = false;\n\n  constructor(props) {\n    super(props);\n    this.state = this._getStateFromStore();\n  }\n\n  componentDidMount() {\n    this.unsubscribe = OnboardingStore.listen(this._onStateChanged, this);\n    NylasEnv.center();\n    NylasEnv.displayWindow();\n\n    if (NylasEnv.timer.isPending('open-add-account-window')) {\n      const {source} = NylasEnv.getWindowProps()\n      Actions.recordPerfMetric({\n        source,\n        action: 'open-add-account-window',\n        actionTimeMs: NylasEnv.timer.stop('open-add-account-window'),\n        maxValue: 4 * 1000,\n      })\n    }\n\n    if (NylasEnv.timer.isPending('app-boot')) {\n      // If this component is mounted and we are /still/ timing `app-boot`, it\n      // means that the app booted for an unauthenticated user and we are\n      // showing the onboarding window for the first time.\n      // In this case, we can't report `app-boot` time because we don't have a\n      // nylasId or accountId required to report a metric.\n      // However, we do want to clear the timer by stopping it\n      NylasEnv.timer.stop('app-boot')\n    }\n  }\n\n  componentWillUnmount() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n  }\n\n  _getStateFromStore = () => {\n    return {\n      page: OnboardingStore.page(),\n      pageDepth: OnboardingStore.pageDepth(),\n      accountInfo: OnboardingStore.accountInfo(),\n    };\n  }\n\n  _onStateChanged = () => {\n    this.setState(this._getStateFromStore());\n  }\n\n  render() {\n    const Component = PageComponents[this.state.page];\n    if (!Component) {\n      throw new Error(`Cannot find component for page: ${this.state.page}`);\n    }\n\n    return (\n      <div className=\"page-frame\">\n        <PageTopBar\n          pageDepth={this.state.pageDepth}\n          allowMoveBack={!['initial-preferences', 'account-choose'].includes(this.state.page)}\n        />\n        <ReactCSSTransitionGroup\n          transitionName=\"alpha-fade\"\n          transitionLeaveTimeout={150}\n          transitionEnterTimeout={150}\n        >\n          <div key={this.state.page} className=\"page-container\">\n            <Component accountInfo={this.state.accountInfo} ref=\"activePage\" />\n          </div>\n        </ReactCSSTransitionGroup>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/onboarding-store.es6",
    "content": "import {AccountStore, Actions, IdentityStore, FolderSyncProgressStore} from 'nylas-exports';\nimport {ipcRenderer} from 'electron';\nimport NylasStore from 'nylas-store';\n\nimport OnboardingActions from './onboarding-actions';\n\nfunction accountTypeForProvider(provider) {\n  if (provider === 'eas') {\n    return 'exchange';\n  }\n  if (provider === 'custom') {\n    return 'imap';\n  }\n  return provider;\n}\n\nclass OnboardingStore extends NylasStore {\n  constructor() {\n    super();\n\n    this.listenTo(OnboardingActions.moveToPreviousPage, this._onMoveToPreviousPage)\n    this.listenTo(OnboardingActions.moveToPage, this._onMoveToPage)\n    this.listenTo(OnboardingActions.accountJSONReceived, this._onAccountJSONReceived)\n    this.listenTo(OnboardingActions.authenticationJSONReceived, this._onAuthenticationJSONReceived)\n    this.listenTo(OnboardingActions.setAccountInfo, this._onSetAccountInfo);\n    this.listenTo(OnboardingActions.setAccountType, this._onSetAccountType);\n    ipcRenderer.on('set-account-type', (e, type) => {\n      if (type) {\n        this._onSetAccountType(type)\n      } else {\n        this._pageStack = ['account-choose']\n        this.trigger()\n      }\n    })\n\n    const {existingAccount, addingAccount, accountType} = NylasEnv.getWindowProps();\n    this._accountInfo = {};\n\n    if (existingAccount) {\n      // Used when re-adding an account after re-connecting\n      const existingAccountType = accountTypeForProvider(existingAccount.provider);\n      this._pageStack = ['account-choose']\n      this._accountInfo = {\n        name: existingAccount.name,\n        email: existingAccount.emailAddress,\n      };\n      this._onSetAccountType(existingAccountType);\n    } else if (addingAccount) {\n      // Adding a new, unknown account\n      this._pageStack = ['account-choose'];\n      if (accountType) {\n        this._onSetAccountType(accountType);\n      }\n    } else {\n      // Standard new user onboarding flow.\n      this._pageStack = ['welcome'];\n    }\n  }\n\n  _onOnboardingComplete = () => {\n    // When account JSON is received, we want to notify external services\n    // that it succeeded. Unfortunately in this case we're likely to\n    // close the window before those requests can be made. We add a short\n    // delay here to ensure that any pending requests have a chance to\n    // clear before the window closes.\n    setTimeout(() => {\n      ipcRenderer.send('account-setup-successful');\n    }, 100);\n  }\n\n  _onSetAccountType = (type) => {\n    let nextPage = \"account-settings\";\n    if (type === 'gmail') {\n      nextPage = \"account-settings-gmail\";\n    } else if (type === 'exchange') {\n      nextPage = \"account-settings-exchange\";\n    }\n\n    Actions.recordUserEvent('Selected Account Type', {\n      provider: type,\n    });\n\n    // Don't carry over any type-specific account information\n    const {email, name, password} = this._accountInfo;\n    this._onSetAccountInfo({email, name, password, type});\n    this._onMoveToPage(nextPage);\n  }\n\n  _onSetAccountInfo = (info) => {\n    this._accountInfo = info;\n    this.trigger();\n  }\n\n  _onMoveToPreviousPage = () => {\n    this._pageStack.pop();\n    this.trigger();\n  }\n\n  _onMoveToPage = (page) => {\n    this._pageStack.push(page)\n    this.trigger();\n  }\n\n  _onAuthenticationJSONReceived = async (json) => {\n    const isFirstAccount = AccountStore.accounts().length === 0;\n\n    await IdentityStore.saveIdentity(json);\n\n    setTimeout(() => {\n      if (isFirstAccount) {\n        this._onSetAccountInfo(Object.assign({}, this._accountInfo, {\n          name: `${json.firstname || \"\"} ${json.lastname || \"\"}`,\n          email: json.email,\n        }));\n        OnboardingActions.moveToPage('account-choose');\n      } else {\n        this._onOnboardingComplete();\n      }\n    }, 1000);\n  }\n\n  _onAccountJSONReceived = async (json, localToken, cloudToken) => {\n    try {\n      const isFirstAccount = AccountStore.accounts().length === 0;\n\n      AccountStore.addAccountFromJSON(json, localToken, cloudToken);\n      this._accountFromAuth = AccountStore.accountForEmail(json.email_address);\n\n      Actions.recordUserEvent('Email Account Auth Succeeded', {\n        provider: this._accountFromAuth.provider,\n      });\n      ipcRenderer.send('new-account-added');\n      NylasEnv.displayWindow();\n\n      if (isFirstAccount) {\n        this._onMoveToPage('initial-preferences');\n        Actions.recordUserEvent('First Account Linked', {\n          provider: this._accountFromAuth.provider,\n        });\n      } else {\n        await FolderSyncProgressStore.whenCategoryListSynced(json.id)\n        this._onOnboardingComplete();\n      }\n    } catch (e) {\n      NylasEnv.reportError(e);\n      NylasEnv.showErrorDialog(\"Unable to Connect Account\", \"Sorry, something went wrong on the Nylas server. Please try again later.\");\n    }\n  }\n\n  page() {\n    return this._pageStack[this._pageStack.length - 1];\n  }\n\n  pageDepth() {\n    return this._pageStack.length;\n  }\n\n  accountInfo() {\n    return this._accountInfo;\n  }\n\n  accountFromAuth() {\n    return this._accountFromAuth;\n  }\n}\n\nexport default new OnboardingStore();\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-account-choose.jsx",
    "content": "import React from 'react';\nimport {RetinaImg} from 'nylas-component-kit';\nimport OnboardingActions from './onboarding-actions';\nimport AccountTypes from './account-types';\n\nexport default class AccountChoosePage extends React.Component {\n  static displayName = \"AccountChoosePage\";\n\n  static propTypes = {\n    accountInfo: React.PropTypes.object,\n  }\n\n  _renderAccountTypes() {\n    return AccountTypes.map((accountType) =>\n      <div\n        key={accountType.type}\n        className={`provider ${accountType.type}`}\n        onClick={() => OnboardingActions.setAccountType(accountType.type)}\n      >\n        <div className=\"icon-container\">\n          <RetinaImg\n            name={accountType.icon}\n            mode={RetinaImg.Mode.ContentPreserve}\n            className=\"icon\"\n          />\n        </div>\n        <span className=\"provider-name\">{accountType.displayName}</span>\n      </div>\n    );\n  }\n\n  render() {\n    return (\n      <div className=\"page account-choose\">\n        <h2>\n          Connect an email account\n        </h2>\n        <div className=\"provider-list\">\n          {this._renderAccountTypes()}\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-account-onboarding-success.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport {RetinaImg} from 'nylas-component-kit';\nimport AccountTypes from './account-types'\n\n\nclass AccountOnboardingSuccess extends Component { // eslint-disable-line\n  static displayName = 'AccountOnboardingSuccess'\n\n  static propTypes = {\n    accountInfo: PropTypes.object,\n  }\n\n  render() {\n    const {accountInfo} = this.props\n    const accountType = AccountTypes.find(a => a.type === accountInfo.type);\n    return (\n      <div className={`page account-setup AccountOnboardingSuccess`}>\n        <div className=\"logo-container\">\n          <RetinaImg\n            style={{backgroundColor: accountType.color, borderRadius: 44}}\n            name={accountType.headerIcon}\n            mode={RetinaImg.Mode.ContentPreserve}\n            className=\"logo\"\n          />\n        </div>\n        <div>\n          <h2>Successfully connected to {accountType.displayName}!</h2>\n          <h3>Adding your account to Nylas Mail…</h3>\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default AccountOnboardingSuccess\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-account-settings-exchange.jsx",
    "content": "import React from 'react';\nimport {RegExpUtils} from 'nylas-exports';\nimport {isValidHost} from './onboarding-helpers';\nimport CreatePageForForm from './decorators/create-page-for-form';\nimport FormField from './form-field';\n\nclass AccountExchangeSettingsForm extends React.Component {\n  static displayName = 'AccountExchangeSettingsForm';\n\n  static propTypes = {\n    accountInfo: React.PropTypes.object,\n    errorFieldNames: React.PropTypes.array,\n    submitting: React.PropTypes.bool,\n    onConnect: React.PropTypes.func,\n    onFieldChange: React.PropTypes.func,\n    onFieldKeyPress: React.PropTypes.func,\n  };\n\n  static submitLabel = () => {\n    return 'Connect Account';\n  }\n\n  static titleLabel = () => {\n    return 'Add your Exchange account';\n  }\n\n  static subtitleLabel = () => {\n    return 'Enter your Exchange credentials to get started.';\n  }\n\n  static validateAccountInfo = (accountInfo) => {\n    const {email, password, name} = accountInfo;\n    const errorFieldNames = [];\n    let errorMessage = null;\n\n    if (!email || !password || !name) {\n      return {errorMessage, errorFieldNames, populated: false};\n    }\n\n    if (!RegExpUtils.emailRegex().test(accountInfo.email)) {\n      errorFieldNames.push('email')\n      errorMessage = \"Please provide a valid email address.\"\n    }\n    if (!accountInfo.password) {\n      errorFieldNames.push('password')\n      errorMessage = \"Please provide a password for your account.\"\n    }\n    if (!accountInfo.name) {\n      errorFieldNames.push('name')\n      errorMessage = \"Please provide your name.\"\n    }\n    if (accountInfo.eas_server_host && !isValidHost(accountInfo.eas_server_host)) {\n      errorFieldNames.push('eas_server_host')\n      errorMessage = \"Please provide a valid host name.\"\n    }\n\n    return {errorMessage, errorFieldNames, populated: true};\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {showAdvanced: false};\n  }\n\n  submit() {\n    this.props.onConnect();\n  }\n\n  render() {\n    const {errorFieldNames, accountInfo} = this.props;\n    const showAdvanced = (\n      this.state.showAdvanced ||\n      errorFieldNames.includes('eas_server_host') ||\n      errorFieldNames.includes('username') ||\n      accountInfo.eas_server_host ||\n      accountInfo.username\n    );\n\n    let classnames = \"twocol\";\n    if (!showAdvanced) {\n      classnames += \" hide-second-column\";\n    }\n\n    return (\n      <div className={classnames}>\n        <div className=\"col\">\n          <FormField field=\"name\" title=\"Name\" {...this.props} />\n          <FormField field=\"email\" title=\"Email\" {...this.props} />\n          <FormField field=\"password\" title=\"Password\" type=\"password\" {...this.props} />\n          <a className=\"toggle-advanced\" onClick={() => this.setState({showAdvanced: !this.state.showAdvanced})}>\n            {showAdvanced ? \"Hide Advanced Options\" : \"Show Advanced Options\"}\n          </a>\n        </div>\n        <div className=\"col\">\n          <FormField field=\"username\" title=\"Username (Optional)\" {...this.props} />\n          <FormField field=\"eas_server_host\" title=\"Exchange Server (Optional)\" {...this.props} />\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default CreatePageForForm(AccountExchangeSettingsForm);\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-account-settings-gmail.jsx",
    "content": "import React from 'react';\nimport {OAuthSignInPage} from 'nylas-component-kit';\nimport {IdentityStore} from 'nylas-exports'\n\nimport {\n  makeGmailOAuthRequest,\n  authIMAPForGmail,\n  buildGmailSessionKey,\n  buildGmailAuthURL,\n} from './onboarding-helpers';\n\nimport OnboardingActions from './onboarding-actions';\nimport AccountTypes from './account-types';\n\n\nexport default class AccountSettingsPageGmail extends React.Component {\n  static displayName = \"AccountSettingsPageGmail\";\n\n  static propTypes = {\n    accountInfo: React.PropTypes.object,\n  };\n\n  constructor() {\n    super()\n    this._sessionKey = buildGmailSessionKey(IdentityStore.identityId());\n    this._gmailAuthUrl = buildGmailAuthURL(this._sessionKey)\n  }\n\n  onSuccess(account) {\n    OnboardingActions.accountJSONReceived(account, account.localToken, account.cloudToken);\n  }\n\n  render() {\n    const {accountInfo} = this.props;\n    const accountType = AccountTypes.find(a => a.type === accountInfo.type)\n    const {headerIcon} = accountType;\n    const goBack = () => OnboardingActions.moveToPreviousPage()\n\n    return (\n      <OAuthSignInPage\n        serviceName=\"Google\"\n        providerAuthPageUrl={this._gmailAuthUrl}\n        iconName={headerIcon}\n        accountInfo={accountInfo}\n        tokenRequestPollFn={makeGmailOAuthRequest}\n        accountFromTokenFn={authIMAPForGmail}\n        onSuccess={this.onSuccess}\n        onTryAgain={goBack}\n        sessionKey={this._sessionKey}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-account-settings-imap.jsx",
    "content": "import React from 'react';\nimport {isValidHost} from './onboarding-helpers';\nimport CreatePageForForm from './decorators/create-page-for-form';\nimport FormField from './form-field';\n\nclass AccountIMAPSettingsForm extends React.Component {\n  static displayName = 'AccountIMAPSettingsForm';\n\n  static propTypes = {\n    accountInfo: React.PropTypes.object,\n    errorFieldNames: React.PropTypes.array,\n    submitting: React.PropTypes.bool,\n    onConnect: React.PropTypes.func,\n    onFieldChange: React.PropTypes.func,\n    onFieldKeyPress: React.PropTypes.func,\n  };\n\n  static submitLabel = () => {\n    return 'Connect Account';\n  }\n\n  static titleLabel = () => {\n    return 'Set up your account';\n  }\n\n  static subtitleLabel = () => {\n    return 'Complete the IMAP and SMTP settings below to connect your account.';\n  }\n\n  static validateAccountInfo = (accountInfo) => {\n    let errorMessage = null;\n    const errorFieldNames = [];\n\n    for (const type of ['imap', 'smtp']) {\n      if (!accountInfo[`${type}_host`] || !accountInfo[`${type}_username`] || !accountInfo[`${type}_password`]) {\n        return {errorMessage, errorFieldNames, populated: false};\n      }\n      if (!isValidHost(accountInfo[`${type}_host`])) {\n        errorMessage = \"Please provide a valid hostname or IP adddress.\";\n        errorFieldNames.push(`${type}_host`);\n      }\n      if (accountInfo[`${type}_host`] === 'imap.gmail.com') {\n        errorMessage = \"Please link Gmail accounts by choosing 'Google' on the account type screen.\";\n        errorFieldNames.push(`${type}_host`);\n      }\n      if (!Number.isInteger(accountInfo[`${type}_port`] / 1)) {\n        errorMessage = \"Please provide a valid port number.\";\n        errorFieldNames.push(`${type}_port`);\n      }\n    }\n\n    return {errorMessage, errorFieldNames, populated: true};\n  }\n\n  submit() {\n    this.props.onConnect();\n  }\n\n  renderPortDropdown(protocol) {\n    if (![\"imap\", \"smtp\"].includes(protocol)) {\n      throw new Error(`Can't render port dropdown for protocol '${protocol}'`);\n    }\n    const {accountInfo, submitting, onFieldKeyPress, onFieldChange} = this.props;\n\n    if (protocol === \"imap\") {\n      return (\n        <span>\n          <label htmlFor=\"imap_port\">Port:</label>\n          <select\n            id=\"imap_port\"\n            tabIndex={0}\n            value={accountInfo.imap_port}\n            disabled={submitting}\n            onKeyPress={onFieldKeyPress}\n            onChange={onFieldChange}\n          >\n            <option value=\"143\" key=\"143\">143</option>\n            <option value=\"993\" key=\"993\">993</option>\n          </select>\n        </span>\n      )\n    }\n    if (protocol === \"smtp\") {\n      return (\n        <span>\n          <label htmlFor=\"smtp_port\">Port:</label>\n          <select\n            id=\"smtp_port\"\n            tabIndex={0}\n            value={accountInfo.smtp_port}\n            disabled={submitting}\n            onKeyPress={onFieldKeyPress}\n            onChange={onFieldChange}\n          >\n            <option value=\"25\" key=\"25\">25</option>\n            <option value=\"465\" key=\"465\">465</option>\n            <option value=\"587\" key=\"587\">587</option>\n          </select>\n        </span>\n      )\n    }\n    return \"\";\n  }\n\n  renderSecurityDropdown(protocol) {\n    const {accountInfo, submitting, onFieldKeyPress, onFieldChange} = this.props;\n\n    return (\n      <div>\n        <span>\n          <label htmlFor={`${protocol}_security`}>Security:</label>\n          <select\n            id={`${protocol}_security`}\n            tabIndex={0}\n            value={accountInfo[`${protocol}_security`]}\n            disabled={submitting}\n            onKeyPress={onFieldKeyPress}\n            onChange={onFieldChange}\n          >\n            <option value=\"SSL / TLS\" key=\"SSL\">SSL / TLS</option>\n            <option value=\"STARTTLS\" key=\"STARTTLS\">STARTTLS</option>\n            <option value=\"none\" key=\"none\">none</option>\n          </select>\n        </span>\n        <span style={{paddingLeft: '20px', paddingTop: '10px'}}>\n          <input\n            type=\"checkbox\"\n            id={`${protocol}_allow_insecure_ssl`}\n            disabled={submitting}\n            checked={accountInfo[`${protocol}_allow_insecure_ssl`] || false}\n            onKeyPress={onFieldKeyPress}\n            onChange={onFieldChange}\n          />\n          <label htmlFor={`${protocol}_allow_insecure_ssl\"`} className=\"checkbox\">Allow insecure SSL</label>\n        </span>\n      </div>\n    )\n  }\n\n  renderFieldsForType(type) {\n    return (\n      <div>\n        <FormField field={`${type}_host`} title={\"Server\"} {...this.props} />\n        <div style={{textAlign: 'left'}}>\n          {this.renderPortDropdown(type)}\n          {this.renderSecurityDropdown(type)}\n        </div>\n        <FormField field={`${type}_username`} title={\"Username\"} {...this.props} />\n        <FormField field={`${type}_password`} title={\"Password\"} type=\"password\" {...this.props} />\n      </div>\n    );\n  }\n\n  render() {\n    return (\n      <div className=\"twocol\">\n        <div className=\"col\">\n          <div className=\"col-heading\">Incoming Mail (IMAP):</div>\n          {this.renderFieldsForType('imap')}\n        </div>\n        <div className=\"col\">\n          <div className=\"col-heading\">Outgoing Mail (SMTP):</div>\n          {this.renderFieldsForType('smtp')}\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default CreatePageForForm(AccountIMAPSettingsForm);\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-account-settings.jsx",
    "content": "import React from 'react';\nimport {RegExpUtils} from 'nylas-exports';\n\nimport OnboardingActions from './onboarding-actions';\nimport CreatePageForForm from './decorators/create-page-for-form';\nimport {accountInfoWithIMAPAutocompletions} from './onboarding-helpers';\nimport FormField from './form-field';\n\nclass AccountBasicSettingsForm extends React.Component {\n  static displayName = 'AccountBasicSettingsForm';\n\n  static propTypes = {\n    accountInfo: React.PropTypes.object,\n    errorFieldNames: React.PropTypes.array,\n    submitting: React.PropTypes.bool,\n    onConnect: React.PropTypes.func,\n    onFieldChange: React.PropTypes.func,\n    onFieldKeyPress: React.PropTypes.func,\n  };\n\n  static submitLabel = (accountInfo) => {\n    return (accountInfo.type === 'imap') ? 'Continue' : 'Connect Account';\n  }\n\n  static titleLabel = (AccountType) => {\n    return AccountType.title || `Add your ${AccountType.displayName} account`;\n  }\n\n  static subtitleLabel = () => {\n    return 'Enter your email account credentials to get started.';\n  }\n\n  static validateAccountInfo = (accountInfo) => {\n    const {email, password, name} = accountInfo;\n    const errorFieldNames = [];\n    let errorMessage = null;\n\n    if (!email || !password || !name) {\n      return {errorMessage, errorFieldNames, populated: false};\n    }\n\n    if (!RegExpUtils.emailRegex().test(accountInfo.email)) {\n      errorFieldNames.push('email')\n      errorMessage = \"Please provide a valid email address.\"\n    }\n    if (!accountInfo.password) {\n      errorFieldNames.push('password')\n      errorMessage = \"Please provide a password for your account.\"\n    }\n    if (!accountInfo.name) {\n      errorFieldNames.push('name')\n      errorMessage = \"Please provide your name.\"\n    }\n\n    return {errorMessage, errorFieldNames, populated: true};\n  }\n\n  submit() {\n    if (!['gmail', 'office365'].includes(this.props.accountInfo.type)) {\n      const accountInfo = accountInfoWithIMAPAutocompletions(this.props.accountInfo);\n      OnboardingActions.setAccountInfo(accountInfo);\n      if (this.props.accountInfo.type === 'imap') {\n        OnboardingActions.moveToPage('account-settings-imap');\n      } else {\n        // We have to pass in the updated accountInfo, because the onConnect()\n        // we're calling exists on a component that won't have had it's state\n        // updated from the OnboardingStore change yet.\n        this.props.onConnect(accountInfo);\n      }\n    } else {\n      this.props.onConnect();\n    }\n  }\n\n  render() {\n    return (\n      <form className=\"settings\">\n        <FormField field=\"name\" title=\"Name\" {...this.props} />\n        <FormField field=\"email\" title=\"Email\" {...this.props} />\n        <FormField field=\"password\" title=\"Password\" type=\"password\" {...this.props} />\n      </form>\n    )\n  }\n}\n\nexport default CreatePageForForm(AccountBasicSettingsForm);\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-initial-preferences.cjsx",
    "content": "React = require 'react'\npath = require 'path'\nfs = require 'fs'\n_ = require 'underscore'\n{RetinaImg, Flexbox, ConfigPropContainer} = require 'nylas-component-kit'\n{AccountStore} = require 'nylas-exports'\nOnboardingActions = require('./onboarding-actions').default\n\n# NOTE: Temporarily copied from preferences module\nclass AppearanceModeOption extends React.Component\n  @propTypes:\n    mode: React.PropTypes.string.isRequired\n    active: React.PropTypes.bool\n    onClick: React.PropTypes.func\n\n  render: =>\n    classname = \"appearance-mode\"\n    classname += \" active\" if @props.active\n\n    label = {\n      'list': 'Reading Pane Off'\n      'split': 'Reading Pane On'\n    }[@props.mode]\n\n    <div className={classname} onClick={@props.onClick}>\n      <RetinaImg name={\"appearance-mode-#{@props.mode}.png\"} mode={RetinaImg.Mode.ContentIsMask}/>\n      <div>{label}</div>\n    </div>\n\n\nclass InitialPreferencesOptions extends React.Component\n  @propTypes:\n    config: React.PropTypes.object\n\n  constructor: (@props) ->\n    @state =\n      templates: []\n    @_loadTemplates()\n\n  _loadTemplates: =>\n    templatesDir = path.join(NylasEnv.getLoadSettings().resourcePath, 'keymaps', 'templates')\n    fs.readdir templatesDir, (err, files) =>\n      return unless files and files instanceof Array\n      templates = files.filter (filename) =>\n        path.extname(filename) is '.cson' or path.extname(filename) is '.json'\n      templates = templates.map (filename) =>\n        path.parse(filename).name\n      @setState(templates: templates)\n      @_setConfigDefaultsForAccount(templates)\n\n  _setConfigDefaultsForAccount: (templates) =>\n    return unless @props.account\n\n    templateWithBasename = (name) =>\n      _.find templates, (t) -> t.indexOf(name) is 0\n\n    if @props.account.provider is 'gmail'\n      @props.config.set('core.workspace.mode', 'list')\n      @props.config.set('core.keymapTemplate', templateWithBasename('Gmail'))\n    else if @props.account.provider is 'eas' or @props.account.provider is 'office365'\n      @props.config.set('core.workspace.mode', 'split')\n      @props.config.set('core.keymapTemplate', templateWithBasename('Outlook'))\n    else\n      @props.config.set('core.workspace.mode', 'split')\n      if process.platform is 'darwin'\n        @props.config.set('core.keymapTemplate', templateWithBasename('Apple Mail'))\n      else\n        @props.config.set('core.keymapTemplate', templateWithBasename('Outlook'))\n\n  render: =>\n    return false unless @props.config\n\n    <div style={display:'flex', width:600, marginBottom: 50, marginLeft:150, marginRight: 150, textAlign: 'left'}>\n      <div style={flex:1}>\n        <p>\n          Do you prefer a single panel layout (like Gmail)\n          or a two panel layout?\n        </p>\n        <Flexbox direction=\"row\" style={alignItems: \"center\"}>\n          {['list', 'split'].map (mode) =>\n            <AppearanceModeOption\n              mode={mode} key={mode}\n              active={@props.config.get('core.workspace.mode') is mode}\n              onClick={ => @props.config.set('core.workspace.mode', mode)} />\n          }\n        </Flexbox>\n      </div>\n      <div key=\"divider\" style={marginLeft:20, marginRight:20, borderLeft:'1px solid #ccc'}></div>\n      <div style={flex:1}>\n        <p>\n          We've picked a set of keyboard shortcuts based on your email\n          account and platform. You can also pick another set:\n        </p>\n        <select\n          style={margin:0}\n          value={@props.config.get('core.keymapTemplate')}\n          onChange={ (event) => @props.config.set('core.keymapTemplate', event.target.value) }>\n        { @state.templates.map (template) =>\n          <option key={template} value={template}>{template}</option>\n        }\n        </select>\n      </div>\n\n    </div>\n\n\nclass InitialPreferencesPage extends React.Component\n  @displayName: \"InitialPreferencesPage\"\n\n  constructor:(@props) ->\n    @state = {account: AccountStore.accounts()[0]}\n\n  componentDidMount: =>\n    @_unlisten = AccountStore.listen(@_onAccountStoreChange)\n\n  componentWillUnmount: =>\n    @_unlisten?()\n\n  _onAccountStoreChange: =>\n    @setState(account: AccountStore.accounts()[0])\n\n  render: =>\n    <div className=\"page opaque\" style={width:900, height:620}>\n      <h1 style={paddingTop: 100}>Welcome to Nylas Mail</h1>\n      <h4 style={marginBottom: 70}>Let's set things up to your liking.</h4>\n      <ConfigPropContainer>\n        <InitialPreferencesOptions account={@state.account} />\n      </ConfigPropContainer>\n      <button\n        className=\"btn btn-large btn-get-started\"\n        style={marginBottom:60}\n        onClick={@_onFinished}>\n        Looks Good!\n        </button>\n    </div>\n\n  _onFinished: =>\n    require('electron').ipcRenderer.send('account-setup-successful')\n\nmodule.exports = InitialPreferencesPage\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-top-bar.jsx",
    "content": "import React from 'react';\nimport {AccountStore} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\nimport OnboardingActions from './onboarding-actions';\n\nconst PageTopBar = (props) => {\n  const {pageDepth} = props;\n\n  const closeClass = (pageDepth > 1) ? 'back' : 'close';\n  const closeIcon = (pageDepth > 1) ? 'onboarding-back.png' : 'onboarding-close.png';\n  const closeAction = () => {\n    const webview = document.querySelector('webview');\n    if (webview && webview.canGoBack()) {\n      webview.goBack();\n    } else if (pageDepth > 1) {\n      OnboardingActions.moveToPreviousPage();\n    } else {\n      if (AccountStore.accounts().length === 0) {\n        NylasEnv.quit();\n      } else {\n        NylasEnv.close();\n      }\n    }\n  }\n\n  let backButton = (\n    <div className={closeClass} onClick={closeAction}>\n      <RetinaImg name={closeIcon} mode={RetinaImg.Mode.ContentPreserve} />\n    </div>\n  )\n  if (props.pageDepth > 1 && !props.allowMoveBack) {\n    backButton = null;\n  }\n\n  return (\n    <div\n      className=\"dragRegion\"\n      style={{\n        top: 0,\n        left: 26,\n        right: 0,\n        height: 27,\n        zIndex: 100,\n        position: 'absolute',\n        WebkitAppRegion: \"drag\",\n      }}\n    >\n      {backButton}\n    </div>\n  )\n}\n\nPageTopBar.propTypes = {\n  pageDepth: React.PropTypes.number,\n  allowMoveBack: React.PropTypes.bool,\n};\n\nexport default PageTopBar;\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/lib/page-welcome.jsx",
    "content": "import React from 'react';\nimport {RetinaImg} from 'nylas-component-kit';\nimport OnboardingActions from './onboarding-actions';\n\nexport default class WelcomePage extends React.Component {\n  static displayName = \"WelcomePage\";\n\n  _onContinue = () => {\n    OnboardingActions.moveToPage(\"account-choose\");\n  }\n\n  render() {\n    return (\n      <div className=\"page welcome\">\n        <div className=\"steps-container\">\n          <div>\n            <RetinaImg className=\"logo\" style={{marginTop: 166}} url=\"nylas://onboarding/assets/nylas-logo@2x.png\" mode={RetinaImg.Mode.ContentPreserve} />\n            <p className=\"hero-text\" style={{fontSize: 46, marginTop: 57}}>Welcome to Nylas Mail</p>\n            <RetinaImg className=\"icons\" url=\"nylas://onboarding/assets/icons-bg@2x.png\" mode={RetinaImg.Mode.ContentPreserve} />\n          </div>\n        </div>\n        <div className=\"footer\">\n          <button key=\"next\" className=\"btn btn-large btn-continue\" onClick={this._onContinue}>Get Started</button>\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/package.json",
    "content": "{\n  \"name\": \"onboarding\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"The sign in experience\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"onboarding\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/stylesheets/onboarding-reset.less",
    "content": "@import \"ui-variables\";\n\n/* The Onboarding window should never adopt theme styles. This re-assigns UI\nvariables and resets commonly overridden styles to ensure the onboarding window\nalways looks good. Previously we tried to make the theme just not load in the\nwindow, but it uses a hot window which makes that difficult now. */\n\n@black:        #231f20;\n@gray-base:    #0a0b0c;\n@gray-darker:  lighten(@gray-base, 13.5%); // #222\n@gray-dark:    lighten(@gray-base, 20%);   // #333\n@gray:         lighten(@gray-base, 33.5%); // #555\n@gray-light:   lighten(@gray-base, 46.7%); // #777\n@gray-lighter: lighten(@gray-base, 92.5%); // #eee\n@white:        #ffffff;\n\n@blue-dark:    #3187e1;\n@blue:         #419bf9;\n\n//== Color Descriptors\n@accent-primary:      @blue;\n@accent-primary-dark: @blue-dark;\n\n@background-primary:     @white;\n@background-off-primary: #fdfdfd;\n@background-secondary:   #f6f6f6;\n@background-tertiary:    #6d7987;\n\n@text-color:                     @black;\n@text-color-subtle:              fadeout(@text-color, 20%);\n@text-color-very-subtle:         fadeout(@text-color, 50%);\n@text-color-inverse:             @white;\n@text-color-inverse-subtle:      fadeout(@text-color-inverse, 20%);\n@text-color-inverse-very-subtle: fadeout(@text-color-inverse, 50%);\n\n@text-color-heading:      #434648;\n@font-family-sans-serif:  \"Nylas-Pro\", \"Helvetica\", sans-serif;\n@font-family-serif:       Georgia, \"Times New Roman\", Times, serif;\n@font-family-monospace:   Menlo, Monaco, Consolas, \"Courier New\", monospace;\n\n@font-family:             @font-family-sans-serif;\n@font-family-heading:     @font-family-sans-serif;\n@font-size-base:          14px;\n\n@line-height-base:         1.5; // 22.5/15\n@line-height-computed:     floor((@font-size-base * @line-height-base)); // ~20px\n@line-height-heading:      1.1;\n\n@component-active-color: @accent-primary-dark;\n@component-active-bg:    @background-primary;\n\n@input-bg:                       @white;\n@input-bg-disabled:              @gray-lighter;\n\nh1, h2, h3, h4, h5, h6 {\n  font-family: @font-family-heading;\n  line-height: @line-height-heading;\n  color: @text-color-heading;\n\n  small,\n  .small {\n    line-height: 1;\n  }\n}\n\nh1 {\n  font-size:   @font-size-h1;\n  font-weight: @font-weight-semi-bold;\n}\nh2 {\n  font-size:   @font-size-h2;\n  font-weight: @font-weight-blond;\n}\nh3 {\n  font-size:   @font-size-h3;\n  font-weight: @font-weight-blond;\n}\nh4 { font-size: @font-size-h4; }\nh5 { font-size: @font-size-h5; }\nh6 { font-size: @font-size-h6; }\n\nh1, h2, h3{\n  margin-top: @line-height-computed;\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 65%;\n  }\n}\nh4, h5, h6 {\n  margin-top: (@line-height-computed / 2);\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 75%;\n  }\n}\n\n\n.btn {\n  padding: 0 0.8em;\n  border-radius: @border-radius-base;\n  border: 0;\n  cursor: default;\n  display:inline-block;\n  color: @btn-default-text-color;\n  background: @background-primary;\n\n  img.content-mask { background-color: @btn-default-text-color; }\n\n  // Use 4 box shadows to create a 0.5px hairline around the button, and another\n  // for the actual shadow. Pending https://code.google.com/p/chromium/issues/detail?id=236371\n  // Yes, 1px border looks really bad on retina.\n  box-shadow: 0 0.5px 0 rgba(0,0,0,0.15), 0 -0.5px 0 rgba(0,0,0,0.15), 0.5px 0 0 rgba(0,0,0,0.15), -0.5px 0 0 rgba(0,0,0,0.15), 0 0.5px 1px rgba(0, 0, 0, 0.15);\n\n  height: 1.9em;\n  line-height: 1.9em;\n\n  .text {\n    margin-left: 6px;\n  }\n\n  &:active {\n    cursor: default;\n    background: darken(@btn-default-bg-color, 9%);\n  }\n  &:focus {\n    outline: none\n  }\n\n  font-size: @font-size-small;\n\n  &.btn-small {\n    font-size: @font-size-smaller;\n  }\n  &.btn-large {\n    font-size: @font-size-base;\n    padding: 0 1.3em;\n    line-height: 2.2em;\n    height: 2.3em;\n  }\n  &.btn-larger {\n    font-size: @font-size-large;\n    padding: 0 1.6em;\n  }\n\n  &.btn-disabled {\n    color:      fadeout(@btn-default-text-color, 40%);\n    background: fadeout(@btn-default-bg-color, 15%);\n    &:active {\n      background: fadeout(@btn-default-bg-color, 15%);\n    }\n  }\n\n  &.btn-emphasis {\n    position: relative;\n    color: @btn-emphasis-text-color;\n    font-weight: @font-weight-medium;\n\n    img.content-mask { background-color:@btn-emphasis-text-color; }\n\n    background: linear-gradient(to bottom, #6bb1f9 0%, #0a80ff 100%);\n    box-shadow: none;\n    border: 1px solid darken(@btn-emphasis-bg-color, 7%);\n\n    &.btn-disabled {\n      opacity: 0.4;\n    }\n\n    &:before {\n      content: ' ';\n      width: calc(~\"100% + 2px\");\n      height: calc(~\"100% + 2px\");\n      border-radius: @border-radius-base + 1;\n      top: -1px;\n      left: -1px;\n      position: absolute;\n      z-index: -1;\n      background: linear-gradient(to bottom, #4ca2f9 0%, #015cff 100%);\n    }\n    &:active {\n      background: -webkit-gradient(linear, left top, left bottom, from(darken(@btn-emphasis-bg-color,10%)), to(darken(@btn-emphasis-bg-color, 4%)));\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/onboarding/stylesheets/onboarding.less",
    "content": "@import \"onboarding-reset\";\n\n@-webkit-keyframes fadein {\n  from { opacity: 0; }\n    to { opacity: 1; }\n}\n\n.alpha-fade-enter {\n  opacity: 0.01;\n  transition: all .15s ease-out;\n}\n\n.alpha-fade-enter.alpha-fade-enter-active {\n  opacity: 1;\n}\n.alpha-fade-leave {\n  opacity: 1;\n  transition: all .15s ease-in;\n}\n\n.alpha-fade-leave.alpha-fade-leave-active {\n  opacity: 0.01;\n}\n\n.page-frame {\n  text-align: center;\n  flex: 1;\n\n  .page-container {\n    display: flex;\n    position: absolute;\n    top: 0;\n    bottom: 0;\n    left: 0;\n    right: 0;\n  }\n\n  .page {\n    background-color: #F3F3F3;\n    flex: 1;\n  }\n\n  h1 {\n    font-weight: 100;\n    font-size: 40pt;\n    margin:0;\n  }\n\n  h2 {\n    line-height: 1.3em;\n    font-size: 30pt;\n    font-weight: 200;\n  }\n\n  h4 {\n    font-weight: 400;\n    font-size: 20pt;\n  }\n\n  .logo-container {\n    width: 117px;\n    height: 117px;\n    display: inline-block;\n    padding-top: 40px;\n    box-sizing: content-box;\n  }\n\n  .btn-add-account {\n    width: 170px;\n    margin-left: 10px;\n    transition: width 150ms ease-in-out;\n  }\n  .btn-add-account.spinning {\n    img { vertical-align: middle; margin-right: 5px; margin-bottom: 2px; }\n  }\n\n  .prompt {\n    color:#5D5D5D;\n    font-size:1.07em;\n    font-weight:300;\n    margin-top:20px;\n    margin-bottom:14px;\n  }\n\n  .close {\n    position: fixed;\n    z-index: 100;\n    top: 1px;\n    left: 6px;\n  }\n\n  .back {\n    position: fixed;\n    top: 15px;\n    left: 15px;\n    padding: 10px;\n  }\n\n  .message {\n    margin-bottom:15px;\n    max-width: 600px;\n    margin: auto;\n\n    &.error {\n      color: #A33;\n      -webkit-user-select: text;\n      a {\n        color: #A33;\n      }\n    }\n    &.empty {\n      color: gray;\n    }\n  }\n\n  form.settings {\n    padding: 0 20px;\n    padding-bottom: 20px;\n  }\n  input {\n    display: inline-block;\n    width: 100%;\n    padding: 7px;\n    margin-bottom: 10px;\n    background: #FFF;\n    color: #333;\n    text-align: left;\n    border: 1px solid #AAA;\n\n    &::-webkit-input-placeholder {\n      color: #C6C6C6;\n    }\n\n    &[type=checkbox] {\n      width: initial;\n      margin-right: 5px;\n    }\n\n    &:disabled {\n      background: fadeout(@input-bg, 40%);\n    }\n    &.error {\n      border: 1px solid #A33;\n    }\n  }\n\n  label {\n    display: inline-block;\n    white-space: nowrap;\n    width: 100%;\n    color: #888;\n    text-align: left;\n    padding:3px 0;\n  }\n\n  label[for=subscribe-check] {\n    color: black;\n    white-space: inherit;\n  }\n\n  label.checkbox {\n    width: inherit;\n  }\n\n  .toggle-advanced {\n    display: inline-block;\n    width: 100%;\n    font-size: 0.94em;\n    text-align: right;\n    padding: 0;\n  }\n\n  .btn {\n    margin-top:8px;\n  }\n}\n\n.page.authenticate {\n  flex: 1;\n  display: flex;\n\n  webview {\n    display: flex;\n    flex: 1;\n  }\n\n  .webview-loading-spinner {\n    position: absolute;\n    right: 17px;\n    top: 17px;\n    opacity: 0;\n    transition: opacity 200ms ease-in-out;\n    transition-delay: 200ms;\n    &.loading-true {\n      opacity: 1;\n    }\n  }\n\n  .webview-cover {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background-color: #F3F3F3;\n    opacity: 1;\n    transition: opacity 200ms ease-out;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    .message {\n      color: #444;\n      opacity: 0;\n      margin-top: 20px;\n      transition: opacity 200ms ease-out;\n    }\n    .try-again {\n      opacity: 0;\n      transition: opacity 200ms ease-out;\n    }\n  }\n  .webview-cover.slow,\n  .webview-cover.error {\n    .message {\n      opacity: 1;\n      max-width: 400px;\n    }\n  }\n  .webview-cover.error {\n    .spinner { visibility: hidden;}\n    .try-again {\n      opacity: 1;\n    }\n  }\n  .webview-cover.ready {\n    pointer-events: none;\n    opacity: 0;\n  }\n}\n\n.page.account-choose {\n  h2 {\n    margin-top: 90px;\n    margin-bottom: 20px;\n  }\n\n  .provider-list {\n    margin:auto;\n    width: 280px;\n  }\n  .cloud-sync-note {\n    margin-bottom: 20px;\n    cursor: default;\n    color: @text-color-very-subtle;\n  }\n  .provider-name {\n    font-size: 18px;\n    font-weight: 300;\n    color: rgba(0,0,0,0.7);\n  }\n\n  .provider {\n    text-align: left;\n    cursor: default;\n    line-height: 63px;\n\n    .icon-container {\n      width: 50px;\n      height: 50px;\n      display: inline-block;\n      box-sizing: content-box;\n      padding: 0 15px 0 20px;\n      vertical-align: top;\n      zoom: 0.9;\n    }\n  }\n  .provider:hover{\n    background: rgba(255,255,255,0.4);\n  }\n}\n\n.page.account-setup {\n  form {\n    width: 400px;\n    padding-top: 20px;\n    margin: auto;\n  }\n  .twocol {\n    display: flex;\n    flex-direction: row;\n    width: 700px;\n    margin: auto;\n    transition: width 400ms ease-in-out;\n  }\n  .twocol.hide-second-column {\n    width: 400px;\n    .col:nth-child(2) {\n      opacity: 0;\n      flex: 0;\n      padding: 0;\n      flex-shrink: 1;\n    }\n    .col:first-child {\n    }\n  }\n  .col {\n    flex: 1;\n    padding: 0 20px;\n    opacity: 1;\n    border-left: 1px solid #ddd;\n    overflow: hidden;\n    transition: all 400ms ease-in-out;\n  }\n  .col:first-child {\n    border-left: none;\n  }\n  .col-heading {\n    text-align: left;\n    padding-bottom: 15px;\n  }\n}\n\n.page.account-setup.AccountExchangeSettingsForm {\n  .logo-container {\n    padding-top: 36px;\n  }\n  .twocol {\n    padding-top: 10px;\n    padding-bottom: 10px;\n  }\n}\n\n.page.account-setup.AccountIMAPSettingsForm {\n  h2 {\n    padding-top: 36px;\n  }\n  .logo-container {\n    display: none;\n  }\n  .twocol {\n    padding-top: 20px;\n    padding-bottom: 10px;\n  }\n}\n.page.account-setup.google, .page.account-setup.AccountOnboardingSuccess {\n  .logo-container {\n    padding-top: 160px;\n  }\n}\n\n.page.tutorial {\n  display: flex;\n  flex-direction: column;\n\n  &.appeared-false {\n    .tutorial-container .left {\n      transform: translate3d(-30px, 0, 0);\n      opacity: 0;\n    }\n    .tutorial-container .right {\n      transform: translate3d(30px, 0, 0);\n      opacity: 0;\n    }\n  }\n\n  .tutorial-container {\n    background-color: #F9F9F9;\n    display: flex;\n    flex-direction: row;\n    flex: 1;\n\n    .left {\n      align-self: center;\n      flex: 2;\n      opacity: 1;\n      transform: translate3d(0, 0, 0);\n      transition: all ease-in-out 400ms;\n\n      .screenshot {\n        width: 523px;\n        height: 385px;\n        background:url(nylas://onboarding/assets/app-screenshot@2x.png) top left no-repeat;\n        background-size: contain;\n        margin:auto;\n        position: relative;\n\n        .overlay {\n          position: absolute;\n          width:40px;\n          height:40px;\n          border: 2px solid rgba(0,0,0,0.7);\n          border-radius: 20px;\n          transform:translate3d(-50%, -50%, 0);\n          transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 260ms;\n\n          .overlay-content {\n            transition: all cubic-bezier(0.65, 0.05, 0.36, 1) 260ms;\n            transform: translate3d(-67px,-67px,0) scale(0.21);\n            background:url(nylas://onboarding/assets/app-screenshot@2x.png) top left no-repeat;\n            background-position: 10% 20%;\n            border-radius: 73px;\n            width: 146px;\n            height: 146px;\n            opacity: 0;\n            display: block;\n            position: absolute;\n          }\n        }\n        .overlay.seen {\n          border: 2px solid rgba(0,0,0,0.3);\n        }\n        .overlay.expanded {\n          width:150px;\n          height:150px;\n          border: 2px solid rgba(0,0,0,0.7);\n          border-radius: 75px;\n          box-shadow: 0 0 15px fade(#2673D1, 50%);\n          z-index: 2;\n\n          .overlay-content {\n            transform:scale(1);\n            opacity: 1;\n          }\n        }\n      }\n    }\n\n    .right {\n      flex: 1;\n      padding: 30px;\n      padding-left: 0;\n      opacity: 1;\n      transform:translate3d(0, 0, 0);\n      transition: all ease-in-out 400ms;\n\n      h2 {\n        font-size: 28px;\n        font-weight: 300;\n        text-align: center;\n      }\n      p {\n        font-size: 16px;\n        line-height: 1.85em;\n        text-align: left;\n        padding: 10px 0;\n        color: #333;\n      }\n    }\n  }\n}\n\n.page.welcome {\n  display: flex;\n  flex-direction: column;\n\n  .footer {\n    background-image: linear-gradient(to right, rgba(167,214,134,1) 0%,rgba(122,201,201,1) 100%);\n  }\n\n  @-webkit-keyframes slideIn {\n    from {\n      transform: translate3d(20,0,0);\n      opacity: 0;\n    }\n    to {\n      transform: translate3d(0,0,0);\n      opacity: 1;\n    }\n  }\n\n  a {\n    color: white;\n    border-bottom: 1px solid white;\n    text-decoration: none;\n    font-weight: 300;\n    &:hover {\n      background-color: rgba(255,255,255,0.1);\n    }\n  }\n\n  .steps-container {\n    position: relative;\n    flex: 1;\n    background-image: linear-gradient(to right, rgba(149,205,107,1) 0%,rgba(60,176,176,1) 100%);\n    color: white;\n    overflow: hidden;\n  }\n\n  .hero-text {\n    font-size: 34px;\n    line-height: 41px;\n    font-weight: 200;\n    cursor: default;\n    -webkit-font-smoothing: subpixel-antialiased;\n  }\n\n  .sub-text {\n    font-size: 17px;\n    font-weight: 300;\n  }\n\n  img.icons {\n    position: absolute;\n    top: 0;\n    left: 0;\n    pointer-events: none;\n  }\n}\n\n.page.welcome,\n.page.tutorial {\n  .footer {\n    text-align: center;\n    background-color: #ececec;\n    border-top: 1px solid rgba(0,0,0,0.10);\n    box-shadow: 0 1px 1px solid rgba(255,255,255,0.25);\n\n    .btn-next,\n    .btn-prev {\n      width: 160px;\n      margin: 20px 10px;\n    }\n    .btn-continue {\n      font-weight: 300;\n      margin: 20px 0;\n      width: 296px;\n      line-height: 2.5em;\n      height: 2.5em;\n    }\n  }\n}\n\nbody.platform-win32 {\n  .page-frame {\n    .alpha-fade-enter {\n      transition: all .01s ease-out;\n    }\n    .alpha-fade-leave {\n      transition: opacity .01s ease-in;\n    }\n  }\n}\n\n\n// Individual Components\n\n.appearance-mode {\n  background-color:#f7f9f9;\n  border-radius: 10px;\n  border: 1px solid #c6c7c7;\n  text-align: center;\n  flex: 1;\n  padding:9px;\n  padding-top:10px;\n  margin:10px;\n  margin-top:0;\n  img {\n    background-color: #c6c7c7;\n  }\n  div {\n    margin-top: 10px;\n    text-transform: capitalize;\n    cursor: default;\n  }\n}\n.appearance-mode.active {\n  border:1px solid @component-active-color;\n  color: @component-active-color;\n  img { background-color: @component-active-color; }\n}\n\n\n.alternative-auth {\n  p {\n    color: @text-color-heading;\n  }\n  .url-copy-target {\n    width: 50%;\n    border: 1px solid #c6c7c7;\n    margin: 10px;\n  }\n  .copy-to-clipboard {\n    display: inline-block;\n    cursor: pointer;\n    img {\n      background-color: @btn-icon-color;\n    }\n    img:active {\n      background-color: @black;\n    }\n  }\n\n  .hidden {\n    opacity: 0;\n  }\n\n  .visible {\n    opacity: 1;\n    margin-bottom: 0;\n  }\n\n  .fadein {\n    opacity: 1;\n    transition: opacity 2s linear;\n  }\n\n  .fadeout {\n    opacity: 0;\n    transition: opacity 1s linear;\n  }\n\n  input {\n    margin-top: 0;\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/README.md",
    "content": "\n## Open Tracking\n\nAdds tracking pixels to messages and tracks whether they have been opened.\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/lib/main.es6",
    "content": "import {\n  ComponentRegistry,\n  ExtensionRegistry,\n} from 'nylas-exports';\nimport {HasTutorialTip} from 'nylas-component-kit';\nimport OpenTrackingButton from './open-tracking-button';\nimport OpenTrackingIcon from './open-tracking-icon';\nimport OpenTrackingMessageStatus from './open-tracking-message-status';\nimport OpenTrackingComposerExtension from './open-tracking-composer-extension';\n\nconst OpenTrackingButtonWithTutorialTip = HasTutorialTip(OpenTrackingButton, {\n  title: \"See when recipients open this email\",\n  instructions: \"When enabled, Nylas Mail will notify you as soon as someone reads this message. Sending to a group? Nylas Mail shows you which recipients opened your email so you can follow up with precision.\",\n});\n\nexport function activate() {\n  ComponentRegistry.register(OpenTrackingButtonWithTutorialTip,\n    {role: 'Composer:ActionButton'});\n\n  ComponentRegistry.register(OpenTrackingIcon,\n    {role: 'ThreadListIcon'});\n\n  ComponentRegistry.register(OpenTrackingMessageStatus,\n    {role: 'MessageHeaderStatus'});\n\n  ExtensionRegistry.Composer.register(OpenTrackingComposerExtension);\n}\n\nexport function serialize() {}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(OpenTrackingButtonWithTutorialTip);\n  ComponentRegistry.unregister(OpenTrackingIcon);\n  ComponentRegistry.unregister(OpenTrackingMessageStatus);\n  ExtensionRegistry.Composer.unregister(OpenTrackingComposerExtension);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/lib/open-tracking-button.jsx",
    "content": "// import {DraftStore, React, Actions, NylasAPI, DatabaseStore, Message, Rx} from 'nylas-exports'\nimport {React, APIError, NylasAPI} from 'nylas-exports'\nimport {MetadataComposerToggleButton} from 'nylas-component-kit'\nimport {PLUGIN_ID, PLUGIN_NAME} from './open-tracking-constants'\n\nexport default class OpenTrackingButton extends React.Component {\n  static displayName = 'OpenTrackingButton';\n\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n  };\n\n  shouldComponentUpdate(nextProps) {\n    return (nextProps.draft.metadataForPluginId(PLUGIN_ID) !== this.props.draft.metadataForPluginId(PLUGIN_ID));\n  }\n\n  _title(enabled) {\n    const dir = enabled ? \"Disable\" : \"Enable\";\n    return `${dir} open tracking`\n  }\n\n  _errorMessage(error) {\n    if (error instanceof APIError && NylasAPI.TimeoutErrorCodes.includes(error.statusCode)) {\n      return `Open tracking does not work offline. Please re-enable when you come back online.`\n    }\n    return `Unfortunately, open tracking is currently not available. Please try again later. Error: ${error.message}`\n  }\n\n  render() {\n    const enabledValue = {\n      open_count: 0,\n      open_data: [],\n    };\n\n    return (\n      <MetadataComposerToggleButton\n        title={this._title}\n        iconUrl=\"nylas://open-tracking/assets/icon-composer-eye@2x.png\"\n        pluginId={PLUGIN_ID}\n        pluginName={PLUGIN_NAME}\n        metadataEnabledValue={enabledValue}\n        stickyToggle\n        errorMessage={this._errorMessage}\n        draft={this.props.draft}\n        session={this.props.session}\n      />\n    )\n  }\n}\n\nOpenTrackingButton.containerRequired = false;\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/lib/open-tracking-composer-extension.es6",
    "content": "import {ComposerExtension} from 'nylas-exports';\nimport {PLUGIN_ID, PLUGIN_URL} from './open-tracking-constants';\n\nexport default class OpenTrackingComposerExtension extends ComposerExtension {\n\n  /**\n   * This inserts a placeholder image tag to serve as our open tracking\n   * pixel.\n   *\n   * See cloud-api/routes/open-tracking\n   *\n   * This image tag is NOT complete at this stage. It requires substantial\n   * post processing just before send. This happens in iso-core since\n   * sending can happen immediately or later in cloud-workers.\n   *\n   * See isomorphic-core tracking-utils.es6\n   *\n   * We don't add a `src` parameter here since we don't want the tracking\n   * pixel to prematurely load with an incorrect url.\n   *\n   * We also don't have a Message Id yet since this is still a draft. We\n   * generate and replace `MESSAGE_ID` later with the correct one.\n   *\n   * We also need to add individualized recipients to each tracking pixel\n   * for each message sent to each person.\n   *\n   * We finally need to remove the tracking pixel from the message that\n   * ends up in the users's sent folder. This ensures the sender doesn't\n   * trip their own open track.\n   */\n  static applyTransformsForSending({draftBodyRootNode, draft}) {\n    // grab message metadata, if any\n    const messageUid = draft.clientId;\n    const metadata = draft.metadataForPluginId(PLUGIN_ID);\n    if (!metadata) {\n      return;\n    }\n\n    // insert a tracking pixel <img> into the message\n    const serverUrl = `${PLUGIN_URL}/open/MESSAGE_ID`\n    const imgFragment = document.createRange().createContextualFragment(`<img class=\"n1-open\" width=\"0\" height=\"0\" style=\"border:0; width:0; height:0;\" data-open-tracking-src=\"${serverUrl}\">`);\n    const beforeEl = draftBodyRootNode.querySelector('.gmail_quote');\n    if (beforeEl) {\n      beforeEl.parentNode.insertBefore(imgFragment, beforeEl);\n    } else {\n      draftBodyRootNode.appendChild(imgFragment);\n    }\n\n    // save the uid info to draft metadata\n    metadata.uid = messageUid;\n    draft.applyPluginMetadata(PLUGIN_ID, metadata);\n  }\n\n  static unapplyTransformsForSending({draftBodyRootNode}) {\n    const imgEl = draftBodyRootNode.querySelector('.n1-open');\n    if (imgEl) {\n      imgEl.parentNode.removeChild(imgEl);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/lib/open-tracking-constants.es6",
    "content": "import plugin from '../package.json'\n\nexport const PLUGIN_NAME = plugin.title\nexport const PLUGIN_ID = plugin.name;\nexport const PLUGIN_URL = plugin.serverUrl[NylasEnv.config.get(\"env\")];\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/lib/open-tracking-icon.jsx",
    "content": "import {React, ReactDOM, Actions} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\nimport OpenTrackingMessagePopover from './open-tracking-message-popover';\nimport {PLUGIN_ID} from './open-tracking-constants';\n\n\nexport default class OpenTrackingIcon extends React.Component {\n  static displayName = 'OpenTrackingIcon';\n\n  static propTypes = {\n    thread: React.PropTypes.object.isRequired,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = this._getStateFromThread(props.thread)\n  }\n\n  componentWillReceiveProps(newProps) {\n    this.setState(this._getStateFromThread(newProps.thread));\n  }\n\n  onMouseDown = () => {\n    const rect = ReactDOM.findDOMNode(this).getBoundingClientRect();\n    Actions.openPopover(\n      <OpenTrackingMessagePopover\n        message={this.state.message}\n        openMetadata={this.state.message.metadataForPluginId(PLUGIN_ID)}\n      />,\n      {originRect: rect, direction: 'down'}\n    )\n  }\n\n  _getStateFromThread(thread) {\n    const messages = thread.__messages || []\n\n    let lastMessage = null;\n    for (let i = messages.length - 1; i >= 0; i--) {\n      if (!messages[i].draft) {\n        lastMessage = messages[i];\n        break;\n      }\n    }\n\n    if (!lastMessage) {\n      return {\n        message: null,\n        opened: false,\n        openCount: null,\n        hasMetadata: false,\n      };\n    }\n\n    const lastMessageMeta = lastMessage.metadataForPluginId(PLUGIN_ID);\n    const hasMetadata = lastMessageMeta != null && lastMessageMeta.open_count != null;\n\n    return {\n      message: lastMessage,\n      opened: hasMetadata && lastMessageMeta.open_count > 0,\n      openCount: hasMetadata ? lastMessageMeta.open_count : null,\n      hasMetadata: hasMetadata,\n    };\n  }\n\n  _renderImage() {\n    return (\n      <RetinaImg\n        className={this.state.opened ? \"opened\" : \"unopened\"}\n        url=\"nylas://open-tracking/assets/icon-tracking-opened@2x.png\"\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n    );\n  }\n\n  render() {\n    if (!this.state.hasMetadata) return <span style={{width: \"19px\"}} />;\n    const openedTitle = `${this.state.openCount} open${this.state.openCount === 1 ? \"\" : \"s\"}`;\n    const title = this.state.opened ? openedTitle : \"This message has not been opened\";\n    return (\n      <div\n        title={title}\n        className=\"open-tracking-icon\"\n        onMouseDown={this.state.opened ? this.onMouseDown : null}\n      >\n        {this._renderImage()}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/lib/open-tracking-message-popover.jsx",
    "content": "import React from 'react';\nimport {DateUtils} from 'nylas-exports';\nimport {Flexbox} from 'nylas-component-kit';\nimport ActivityListStore from '../../activity-list/lib/activity-list-store';\n\n\nclass OpenTrackingMessagePopover extends React.Component {\n  static displayName = 'OpenTrackingMessagePopover';\n\n  static propTypes = {\n    message: React.PropTypes.object,\n    openMetadata: React.PropTypes.object,\n  };\n\n  renderOpenActions() {\n    const opens = this.props.openMetadata.open_data;\n    return opens.map((open) => {\n      const recipients = this.props.message.to.concat(this.props.message.cc, this.props.message.bcc);\n      const recipient = ActivityListStore.getRecipient(open.recipient, recipients);\n      const date = new Date(0);\n      date.setUTCSeconds(open.timestamp);\n      return (\n        <Flexbox key={`${open.timestamp}`} className=\"open-action\">\n          <div className=\"recipient\">\n            {recipient ? recipient.displayName() : \"Someone\"}\n          </div>\n          <div className=\"spacer\" />\n          <div className=\"timestamp\">\n            {DateUtils.shortTimeString(date)}\n          </div>\n        </Flexbox>\n      );\n    });\n  }\n\n  render() {\n    return (\n      <div\n        className=\"open-tracking-message-popover\"\n        tabIndex=\"-1\"\n      >\n        <div className=\"open-tracking-header\">Opened by:</div>\n        <div className=\"open-history-container\">\n          {this.renderOpenActions()}\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default OpenTrackingMessagePopover;\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/lib/open-tracking-message-status.jsx",
    "content": "import {React, ReactDOM, Actions} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit'\nimport OpenTrackingMessagePopover from './open-tracking-message-popover'\nimport {PLUGIN_ID} from './open-tracking-constants'\n\n\nexport default class OpenTrackingMessageStatus extends React.Component {\n  static displayName = \"OpenTrackingMessageStatus\";\n\n  static propTypes = {\n    message: React.PropTypes.object.isRequired,\n  };\n\n  static containerStyles = {\n    paddingTop: 4,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = this._getStateFromMessage(props.message)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState(this._getStateFromMessage(nextProps.message))\n  }\n\n  onMouseDown = () => {\n    const rect = ReactDOM.findDOMNode(this).getBoundingClientRect();\n    Actions.openPopover(\n      <OpenTrackingMessagePopover\n        message={this.props.message}\n        openMetadata={this.props.message.metadataForPluginId(PLUGIN_ID)}\n      />,\n      {originRect: rect, direction: 'down'}\n    )\n  }\n\n  _getStateFromMessage(message) {\n    const metadata = message.metadataForPluginId(PLUGIN_ID);\n    if (!metadata || metadata.open_count == null) {\n      return {\n        hasMetadata: false,\n        openCount: null,\n        opened: false,\n      };\n    }\n    return {\n      hasMetadata: true,\n      openCount: metadata.open_count,\n      opened: metadata.open_count > 0,\n    };\n  }\n\n  renderImage() {\n    return (\n      <RetinaImg\n        className={this.state.opened ? \"opened\" : \"unopened\"}\n        style={{position: 'relative', top: -1}}\n        url=\"nylas://open-tracking/assets/InMessage-opened@2x.png\"\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n    );\n  }\n\n  render() {\n    if (!this.state.hasMetadata) return false;\n    let openedCount = `${this.state.openCount} open${this.state.openCount === 1 ? \"\" : \"s\"}`;\n    if (this.state.openCount > 999) openedCount = \"999+ opens\";\n    const text = this.state.opened ? openedCount : \"No opens\";\n    return (\n      <span\n        className={`open-tracking-message-status ${this.state.opened ? \"opened\" : \"unopened\"}`}\n        onMouseDown={this.state.opened ? this.onMouseDown : null}\n      >\n        {this.renderImage()}&nbsp;&nbsp;{text}\n      </span>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/package.json",
    "content": "{\n  \"name\": \"open-tracking\",\n  \"main\": \"./lib/main\",\n  \"version\": \"0.1.0\",\n  \"serverUrl\": {\n    \"local\": \"https://local-n1.nylas.com\",\n    \"development\": \"https://local-n1.nylas.com\",\n    \"staging\": \"https://n1-staging.nylas.com\",\n    \"production\": \"https://n1.nylas.com\"\n  },\n\n  \"title\": \"Open Tracking\",\n  \"description\": \"Track when email messages have been opened by recipients.\",\n  \"icon\": \"./icon.png\",\n  \"isOptional\": true,\n  \"supportedEnvs\": [\"local\", \"development\", \"staging\", \"production\"],\n\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"\"\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  },\n  \"dependencies\": {},\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/spec/open-tracking-composer-extension-spec.es6",
    "content": "import {Message} from 'nylas-exports';\nimport OpenTrackingComposerExtension from '../lib/open-tracking-composer-extension'\nimport {PLUGIN_ID, PLUGIN_URL} from '../lib/open-tracking-constants';\n\nconst accountId = 'fake-accountId';\nconst clientId = 'local-31d8df57-1442';\nconst beforeBody = `TEST_BODY <blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\"> On Feb 25 2016, at 3:38 pm, Drew &lt;drew@nylas.com&gt; wrote: <br> twst </blockquote>`;\nconst afterBody = `TEST_BODY <img class=\"n1-open\" width=\"0\" height=\"0\" style=\"border:0; width:0; height:0;\" src=\"${PLUGIN_URL}/open/${accountId}/${clientId}\"><blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\"> On Feb 25 2016, at 3:38 pm, Drew &lt;drew@nylas.com&gt; wrote: <br> twst </blockquote>`;\n\nconst nodeForHTML = (html) => {\n  const fragment = document.createDocumentFragment();\n  const node = document.createElement('root');\n  fragment.appendChild(node);\n  node.innerHTML = html;\n  return node;\n}\n\nxdescribe('Open tracking composer extension', function openTrackingComposerExtension() {\n  describe(\"applyTransformsForSending\", () => {\n    beforeEach(() => {\n      this.draftBodyRootNode = nodeForHTML(beforeBody);\n      this.draft = new Message({\n        clientId: clientId,\n        accountId: accountId,\n        body: beforeBody,\n      });\n    });\n\n    it(\"takes no action if there is no metadata\", () => {\n      OpenTrackingComposerExtension.applyTransformsForSending({\n        draftBodyRootNode: this.draftBodyRootNode,\n        draft: this.draft,\n      });\n      const actualAfterBody = this.draftBodyRootNode.innerHTML;\n      expect(actualAfterBody).toEqual(beforeBody);\n    });\n\n    describe(\"With properly formatted metadata and correct params\", () => {\n      beforeEach(() => {\n        this.metadata = {open_count: 0};\n        this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);\n\n        OpenTrackingComposerExtension.applyTransformsForSending({\n          draftBodyRootNode: this.draftBodyRootNode,\n          draft: this.draft,\n        });\n        this.metadata = this.draft.metadataForPluginId(PLUGIN_ID);\n      });\n\n      it(\"appends an image with the correct server URL to the unquoted body\", () => {\n        const actualAfterBody = this.draftBodyRootNode.innerHTML;\n        expect(actualAfterBody).toEqual(afterBody);\n      });\n    });\n  });\n\n  describe(\"unapplyTransformsForSending\", () => {\n    it(\"takes no action if the img tag is missing\", () => {\n      this.draftBodyRootNode = nodeForHTML(beforeBody);\n      this.draft = new Message({\n        clientId: clientId,\n        accountId: accountId,\n        body: beforeBody,\n      });\n      OpenTrackingComposerExtension.unapplyTransformsForSending({\n        draftBodyRootNode: this.draftBodyRootNode,\n        draft: this.draft,\n      });\n      const actualAfterBody = this.draftBodyRootNode.innerHTML;\n      expect(actualAfterBody).toEqual(beforeBody);\n    });\n\n    it(\"removes the image from the body and restore the body to it's exact original content\", () => {\n      this.metadata = {open_count: 0};\n      this.draft.applyPluginMetadata(PLUGIN_ID, this.metadata);\n\n      this.draftBodyRootNode = nodeForHTML(afterBody);\n      this.draft = new Message({\n        clientId: clientId,\n        accountId: accountId,\n        body: afterBody,\n      });\n      OpenTrackingComposerExtension.unapplyTransformsForSending({\n        draftBodyRootNode: this.draftBodyRootNode,\n        draft: this.draft,\n      });\n      const actualAfterBody = this.draftBodyRootNode.innerHTML;\n      expect(actualAfterBody).toEqual(beforeBody);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/spec/open-tracking-icon-spec.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport {findRenderedDOMComponentWithClass} from 'react-addons-test-utils';\n\nimport {Message, NylasTestUtils} from 'nylas-exports'\nimport OpenTrackingIcon from '../lib/open-tracking-icon'\nimport {PLUGIN_ID} from '../lib/open-tracking-constants'\n\nconst {renderIntoDocument} = NylasTestUtils;\n\nfunction makeIcon(thread, props = {}) {\n  return renderIntoDocument(<OpenTrackingIcon {...props} thread={thread} />);\n}\n\nfunction find(component, className) {\n  return ReactDOM.findDOMNode(findRenderedDOMComponentWithClass(component, className))\n}\n\nfunction addOpenMetadata(obj, openCount) {\n  obj.applyPluginMetadata(PLUGIN_ID, {open_count: openCount});\n}\n\ndescribe('Open tracking icon', function openTrackingIcon() {\n  beforeEach(() => {\n    this.thread = {__messages: []};\n  });\n\n\n  it(\"shows no icon if the thread has no messages\", () => {\n    const icon = ReactDOM.findDOMNode(makeIcon(this.thread));\n    expect(icon.children.length).toEqual(0);\n  });\n\n  it(\"shows no icon if the thread messages have no metadata\", () => {\n    this.thread.__messages.push(new Message());\n    this.thread.__messages.push(new Message());\n    const icon = ReactDOM.findDOMNode(makeIcon(this.thread));\n    expect(icon.children.length).toEqual(0);\n  });\n\n  describe(\"With messages and metadata\", () => {\n    beforeEach(() => {\n      this.messages = [new Message(), new Message(), new Message({draft: true})];\n      this.thread.__messages.push(...this.messages);\n    });\n\n    it(\"shows no icon if metadata is malformed\", () => {\n      this.messages[0].applyPluginMetadata(PLUGIN_ID, {gar: \"bage\"});\n      const icon = ReactDOM.findDOMNode(makeIcon(this.thread));\n      expect(icon.children.length).toEqual(0);\n    });\n\n    it(\"shows an unopened icon if last non draft message has metadata and is unopened\", () => {\n      addOpenMetadata(this.messages[0], 1);\n      addOpenMetadata(this.messages[1], 0);\n      const icon = find(makeIcon(this.thread), \"open-tracking-icon\");\n      expect(icon.children.length).toEqual(1);\n      expect(icon.querySelector(\"img.unopened\")).not.toBeNull();\n      expect(icon.querySelector(\"img.opened\")).toBeNull();\n    });\n\n    it(\"shows an opened icon if last non draft message with metadata is opened\", () => {\n      addOpenMetadata(this.messages[0], 0);\n      addOpenMetadata(this.messages[1], 1);\n      const icon = find(makeIcon(this.thread), \"open-tracking-icon\");\n      expect(icon.children.length).toEqual(1);\n      expect(icon.querySelector(\"img.unopened\")).toBeNull();\n      expect(icon.querySelector(\"img.opened\")).not.toBeNull();\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/spec/open-tracking-message-status-spec.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\n\nimport {Message, NylasTestUtils} from 'nylas-exports'\nimport OpenTrackingMessageStatus from '../lib/open-tracking-message-status'\nimport {PLUGIN_ID} from '../lib/open-tracking-constants'\n\nconst {renderIntoDocument} = NylasTestUtils;\n\nfunction makeIcon(message, props = {}) {\n  return renderIntoDocument(<div className=\"temp\"><OpenTrackingMessageStatus {...props} message={message} /></div>);\n}\n\nfunction addOpenMetadata(obj, openCount) {\n  obj.applyPluginMetadata(PLUGIN_ID, {open_count: openCount});\n}\n\ndescribe('Open tracking message status', function openTrackingMessageStatus() {\n  beforeEach(() => {\n    this.message = new Message();\n  });\n\n\n  it(\"shows nothing if the message has no metadata\", () => {\n    const icon = ReactDOM.findDOMNode(makeIcon(this.message));\n    expect(icon.querySelector(\".open-tracking-message-status\")).toBeNull();\n  });\n\n\n  it(\"shows nothing if metadata is malformed\", () => {\n    this.message.applyPluginMetadata(PLUGIN_ID, {gar: \"bage\"});\n    const icon = ReactDOM.findDOMNode(makeIcon(this.message));\n    expect(icon.querySelector(\".open-tracking-message-status\")).toBeNull();\n  });\n\n  it(\"shows an unopened icon if the message has metadata and is unopened\", () => {\n    addOpenMetadata(this.message, 0);\n    const icon = ReactDOM.findDOMNode(makeIcon(this.message));\n    expect(icon.querySelector(\"img.unopened\")).not.toBeNull();\n    expect(icon.querySelector(\"img.opened\")).toBeNull();\n  });\n\n  it(\"shows an opened icon if the message has metadata and is opened\", () => {\n    addOpenMetadata(this.message, 1);\n    const icon = ReactDOM.findDOMNode(makeIcon(this.message));\n    expect(icon.querySelector(\"img.unopened\")).toBeNull();\n    expect(icon.querySelector(\"img.opened\")).not.toBeNull();\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/open-tracking/stylesheets/main.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n@open-tracking-color: #7C19CC;\n\n.open-tracking-icon img {\n  vertical-align: initial;\n}\n\n.open-tracking-icon img.content-mask.unopened {\n  background-color: fadeout(@open-tracking-color, 80%);\n  cursor: default;\n}\n.open-tracking-icon img.content-mask.opened {\n  background-color: @open-tracking-color;\n  cursor: pointer;\n}\n\n.list-item.focused, .list-item.selected {\n  .open-tracking-icon img.content-mask.unopened {\n    background-color: fadeout(@text-color-inverse, 70%);\n  }\n  .open-tracking-icon img.content-mask.opened {\n    background-color: @text-color-inverse;\n  }\n}\n\n.open-tracking-icon .open-count {\n  display: inline-block;\n  position: relative;\n  left: -16px;\n  text-align: center;\n\n  background-color: @text-color-link;\n  font-size: 12px;\n  font-weight: bold;\n}\n\n.open-tracking-icon {\n  width: 15px;\n  margin: 0 2px;\n}\n\n.open-tracking-message-status {\n  color: @text-color-very-subtle;\n  margin-left: 10px;\n  &.unopened {\n    img.content-mask {\n      background-color: @text-color-very-subtle;\n    }\n  }\n  &.opened {\n    cursor: pointer;\n    img.content-mask {\n      background-color: @open-tracking-color;\n    }\n  }\n}\n\n.open-tracking-message-popover {\n  width: 200px;\n  max-height: 240px;\n  .open-tracking-header {\n    padding: @padding-base-vertical @padding-base-horizontal 0 @padding-base-horizontal;\n    text-align: center;\n    color: @text-color-subtle;\n    font-weight: 600;\n  }\n  .open-history-container {\n    max-height: 216px;\n    padding: 0 @padding-base-horizontal @padding-base-vertical @padding-base-horizontal;\n    overflow: auto;\n    .open-action {\n      color: @text-color-subtle;\n      .recipient {\n        text-overflow: ellipsis;\n        overflow: hidden;\n      }\n      .spacer {\n        flex: 1 1 0;\n      }\n      .timestamp {\n        color: @text-color-very-subtle;\n        flex-shrink: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/participant-profile/lib/clearbit-data-source.coffee",
    "content": "# This file is in coffeescript just to use the existential operator!\n{AccountStore} = require 'nylas-exports'\n\nMAX_RETRY = 10\n\nmodule.exports = class ClearbitDataSource\n  clearbitAPI: ->\n    return \"https://person.clearbit.com/v2/combined\"\n\n  find: ({email, tryCount}) ->\n    # TODO: If you have a Clearbit API key, insert the request to clearbit here!\n    return Promise.resolve({})\n\n  # The clearbit -> Nylas adapater\n  parseResponse: (body={}, statusCode, requestedEmail, tryCount=0) =>\n    new Promise (resolve, reject) =>\n      # This means it's in the process of fetching. Return null so we don't\n      # cache and try again.\n      if statusCode is 202\n        setTimeout =>\n          @find({email: requestedEmail, tryCount: tryCount+1}).then(resolve).catch(reject)\n        , 1000\n        return\n      else if statusCode isnt 200\n        resolve(null)\n        return\n\n      person = body.person\n\n      # This means there was no data about the person available. Return a\n      # valid, but empty object for us to cache. This can happen when we\n      # have company data, but no personal data.\n      if not person\n        person = {email: requestedEmail}\n\n      resolve({\n        cacheDate: Date.now()\n        email: requestedEmail # Used as checksum\n        bio: person.bio ? person.twitter?.bio ? person.aboutme?.bio,\n        location: person.location ? person.geo?.city\n        currentTitle: person.employment?.title,\n        currentEmployer: person.employment?.name,\n        profilePhotoUrl: person.avatar,\n        rawClearbitData: body,\n        socialProfiles: @_socialProfiles(person)\n      })\n\n  _socialProfiles: (person={}) ->\n    profiles = {}\n    if (person.twitter?.handle ? \"\").length > 0\n      profiles.twitter =\n        handle: person.twitter.handle\n        url: \"https://twitter.com/#{person.twitter.handle}\"\n    if (person.facebook?.handle ? \"\").length > 0\n      profiles.facebook =\n        handle: person.facebook.handle\n        url: \"https://facebook.com/#{person.facebook.handle}\"\n    if (person.linkedin?.handle ? \"\").length > 0\n      profiles.linkedin =\n        handle: person.linkedin.handle\n        url: \"https://linkedin.com/#{person.linkedin.handle}\"\n\n    return profiles\n"
  },
  {
    "path": "packages/client-app/internal_packages/participant-profile/lib/main.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports'\nimport ParticipantProfileStore from './participant-profile-store'\nimport SidebarParticipantProfile from './sidebar-participant-profile'\nimport SidebarRelatedThreads from './sidebar-related-threads'\n\nexport function activate() {\n  ParticipantProfileStore.activate()\n  ComponentRegistry.register(SidebarParticipantProfile, {role: 'MessageListSidebar:ContactCard'})\n  ComponentRegistry.register(SidebarRelatedThreads, {role: 'MessageListSidebar:ContactCard'})\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(SidebarParticipantProfile)\n  ComponentRegistry.unregister(SidebarRelatedThreads)\n  ParticipantProfileStore.deactivate()\n}\n\nexport function serialize() {\n\n}\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/participant-profile/lib/participant-profile-store.es6",
    "content": "import {DatabaseStore, Utils} from 'nylas-exports'\nimport NylasStore from 'nylas-store'\nimport ClearbitDataSource from './clearbit-data-source'\n\n// TODO: Back with Metadata\nconst contactCache = {}\nconst CACHE_SIZE = 100\nconst contactCacheKeyIndex = []\n\nclass ParticipantProfileStore extends NylasStore {\n  activate() {\n    this.cacheExpiry = 1000 * 60 * 60 * 24 // 1 day\n    this.dataSource = new ClearbitDataSource()\n  }\n\n  dataForContact(contact) {\n    if (!contact) {\n      return {}\n    }\n\n    if (Utils.likelyNonHumanEmail(contact.email)) {\n      return {}\n    }\n\n    if (this.inCache(contact)) {\n      const data = this.getCache(contact);\n      if (data.cacheDate) {\n        return data\n      }\n      return {}\n    }\n\n    this.dataSource.find({email: contact.email}).then((data) => {\n      if (data && data.email === contact.email) {\n        this.saveDataToContact(contact, data)\n        this.setCache(contact, data);\n        this.trigger()\n      }\n    }).catch((err = {}) => {\n      if (err.statusCode !== 404) {\n        throw err\n      }\n    })\n    return {}\n  }\n\n  // TODO: Back by metadata.\n  getCache(contact) {\n    return contactCache[contact.email]\n  }\n\n  inCache(contact) {\n    const cache = contactCache[contact.email]\n    if (!cache) { return false }\n    if (!cache.cacheDate || Date.now() - cache.cacheDate > this.cacheExpiry) {\n      return false\n    }\n    return true\n  }\n\n  setCache(contact, value) {\n    contactCache[contact.email] = value\n    contactCacheKeyIndex.push(contact.email)\n    if (contactCacheKeyIndex.length > CACHE_SIZE) {\n      delete contactCache[contactCacheKeyIndex.shift()]\n    }\n    return value\n  }\n\n  /**\n   * We save the clearbit data to the contat object in the database.\n   * This lets us load extra Clearbit data from other windows without\n   * needing to call a very expensive API again.\n   */\n  saveDataToContact(contact, data) {\n    return DatabaseStore.inTransaction((t) => {\n      if (!contact.thirdPartyData) contact.thirdPartyData = {};\n      contact.thirdPartyData.clearbit = data\n      return t.persistModel(contact)\n    })\n  }\n\n  deactivate() {\n    // no op\n  }\n}\nconst store = new ParticipantProfileStore()\nexport default store\n"
  },
  {
    "path": "packages/client-app/internal_packages/participant-profile/lib/sidebar-participant-profile.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport {DOMUtils, RegExpUtils, Utils} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit'\nimport ParticipantProfileStore from './participant-profile-store'\n\nexport default class SidebarParticipantProfile extends React.Component {\n  static displayName = \"SidebarParticipantProfile\";\n\n  static propTypes = {\n    contact: React.PropTypes.object,\n    contactThreads: React.PropTypes.array,\n  }\n\n  static containerStyles = {\n    order: 0,\n  }\n\n  constructor(props) {\n    super(props);\n\n    /* We expect ParticipantProfileStore.dataForContact to return the\n     * following schema:\n     * {\n     *    profilePhotoUrl: string\n     *    bio: string\n     *    location: string\n     *    currentTitle: string\n     *    currentEmployer: string\n     *    socialProfiles: hash keyed by type: ('twitter', 'facebook' etc)\n     *      url: string\n     *      handle: string\n     * }\n     */\n    this.state = ParticipantProfileStore.dataForContact(props.contact)\n  }\n\n  componentDidMount() {\n    this.usub = ParticipantProfileStore.listen(() => {\n      this.setState(ParticipantProfileStore.dataForContact(this.props.contact))\n    })\n  }\n\n  componentWillUnmount() {\n    this.usub()\n  }\n\n  _renderProfilePhoto() {\n    if (this.state.profilePhotoUrl) {\n      return (\n        <div className=\"profile-photo-wrap\">\n          <div className=\"profile-photo\">\n            <img alt=\"Profile\" src={this.state.profilePhotoUrl} />\n          </div>\n        </div>\n      )\n    }\n    return this._renderDefaultProfileImage()\n  }\n\n  _renderDefaultProfileImage() {\n    const hue = Utils.hueForString(this.props.contact.email);\n    const bgColor = `hsl(${hue}, 50%, 45%)`\n    const abv = this.props.contact.nameAbbreviation()\n    return (\n      <div className=\"profile-photo-wrap\">\n        <div className=\"profile-photo\">\n          <div\n            className=\"default-profile-image\"\n            style={{backgroundColor: bgColor}}\n          >\n            {abv}\n          </div>\n        </div>\n      </div>\n    )\n  }\n\n  _renderCorePersonalInfo() {\n    const fullName = this.props.contact.fullName();\n    let renderName = false;\n    if (fullName !== this.props.contact.email) {\n      renderName = <div className=\"selectable full-name\" onClick={this._select}>{this.props.contact.fullName()}</div>\n    }\n    return (\n      <div className=\"core-personal-info\">\n        {renderName}\n        <div className=\"selectable email\" onClick={this._select}>{this.props.contact.email}</div>\n        {this._renderSocialProfiles()}\n      </div>\n    )\n  }\n\n  _renderSocialProfiles() {\n    if (!this.state.socialProfiles) { return false }\n    const profiles = _.map(this.state.socialProfiles, (profile, type) => {\n      return (\n        <a\n          className=\"social-profile-item\"\n          key={type}\n          title={profile.url}\n          href={profile.url}\n        >\n          <RetinaImg\n            url={`nylas://participant-profile/assets/${type}-sidebar-icon@2x.png`}\n            mode={RetinaImg.Mode.ContentPreserve}\n          />\n        </a>\n      )\n    });\n    return <div className=\"social-profiles-wrap\">{profiles}</div>\n  }\n\n  _renderAdditionalInfo() {\n    return (\n      <div className=\"additional-info\">\n        {this._renderCurrentJob()}\n        {this._renderBio()}\n        {this._renderLocation()}\n      </div>\n    )\n  }\n\n  _renderCurrentJob() {\n    if (!this.state.employer) { return false; }\n    let title = false;\n    if (this.state.title) {\n      title = <span>{this.state.title},&nbsp;</span>\n    }\n    return (\n      <p className=\"selectable current-job\">{title}{this.state.employer}</p>\n    )\n  }\n\n  _renderBio() {\n    if (!this.state.bio) { return false; }\n\n    const bioNodes = [];\n    const hashtagOrMentionRegex = RegExpUtils.hashtagOrMentionRegex();\n\n    let bioRemainder = this.state.bio;\n    let match = null;\n    let count = 0;\n\n    /* I thought we were friends. */\n    /* eslint no-cond-assign: 0 */\n    while (match = hashtagOrMentionRegex.exec(bioRemainder)) {\n      // the first char of the match is whitespace, match[1] is # or @, match[2] is the tag itself.\n      bioNodes.push(bioRemainder.substr(0, match.index + 1));\n      if (match[1] === '#') {\n        bioNodes.push(<a key={count} href={`https://twitter.com/hashtag/${match[2]}`}>{`#${match[2]}`}</a>);\n      }\n      if (match[1] === '@') {\n        bioNodes.push(<a key={count} href={`https://twitter.com/${match[2]}`}>{`@${match[2]}`}</a>);\n      }\n      bioRemainder = bioRemainder.substr(match.index + match[0].length);\n      count += 1;\n    }\n    bioNodes.push(bioRemainder);\n\n    return (\n      <p className=\"selectable bio\">{bioNodes}</p>\n    )\n  }\n\n  _renderLocation() {\n    if (!this.state.location) { return false; }\n    return (\n      <p className=\"location\">\n        <RetinaImg\n          url={`nylas://participant-profile/assets/location-icon@2x.png`}\n          mode={RetinaImg.Mode.ContentPreserve}\n          style={{\"float\": \"left\"}}\n        />\n        <span className=\"selectable\" style={{display: \"block\", marginLeft: 20}}>{this.state.location}</span>\n      </p>\n    )\n  }\n\n  _select(event) {\n    const el = event.target;\n    const sel = document.getSelection()\n    if (el.contains(sel.anchorNode) && !sel.isCollapsed) {\n      return\n    }\n    const anchor = DOMUtils.findFirstTextNode(el)\n    const focus = DOMUtils.findLastTextNode(el)\n    if (anchor && focus && focus.data) {\n      sel.setBaseAndExtent(anchor, 0, focus, focus.data.length)\n    }\n  }\n\n  render() {\n    return (\n      <div className=\"participant-profile\">\n        {this._renderProfilePhoto()}\n        {this._renderCorePersonalInfo()}\n        {this._renderAdditionalInfo()}\n      </div>\n    )\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/participant-profile/lib/sidebar-related-threads.jsx",
    "content": "import React from 'react'\nimport {Actions, DateUtils} from 'nylas-exports'\n\nexport default class RelatedThreads extends React.Component {\n  static displayName = \"RelatedThreads\";\n\n  static propTypes = {\n    contact: React.PropTypes.object,\n    contactThreads: React.PropTypes.array,\n  }\n\n  static containerStyles = {\n    order: 99,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {expanded: false}\n    this.DEFAULT_NUM = 3\n  }\n\n  _onClick(thread) {\n    Actions.setFocus({collection: 'thread', item: thread})\n  }\n\n  _toggle = () => {\n    this.setState({expanded: !this.state.expanded})\n  }\n\n  _renderToggle() {\n    if (!this._hasToggle()) { return false; }\n    const msg = this.state.expanded ? \"Collapse\" : \"Show more\"\n    return (\n      <div className=\"toggle\" onClick={this._toggle}>{msg}</div>\n    )\n  }\n\n  _hasToggle() {\n    return (this.props.contactThreads.length > this.DEFAULT_NUM)\n  }\n\n  render() {\n    let limit;\n    if (this.state.expanded) {\n      limit = this.props.contactThreads.length;\n    } else {\n      limit = Math.min(this.props.contactThreads.length, this.DEFAULT_NUM)\n    }\n\n    const height = ((limit + (this._hasToggle() ? 1 : 0)) * 31);\n    const shownThreads = this.props.contactThreads.slice(0, limit)\n    const threads = shownThreads.map((thread) => {\n      const {snippet, subject, lastMessageReceivedTimestamp} = thread;\n      const snippetStyles = (subject && subject.length) ? {marginLeft: '1em'} : {};\n      const onClick = () => { this._onClick(thread) }\n\n      return (\n        <div key={thread.id} className=\"related-thread\" onClick={onClick} >\n          <span className=\"content\" title={subject}>\n            {subject}\n            <span className=\"snippet\" style={snippetStyles}>{snippet}</span>\n          </span>\n          <span className=\"timestamp\" title={DateUtils.fullTimeString(lastMessageReceivedTimestamp)}>{DateUtils.shortTimeString(lastMessageReceivedTimestamp)}</span>\n        </div>\n      )\n    })\n\n    return (\n      <div className=\"related-threads\" style={{height}}>\n        {threads}\n        {this._renderToggle()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/participant-profile/package.json",
    "content": "{\n  \"name\": \"participant-profile\",\n  \"version\": \"0.1.0\",\n  \"title\": \"Participant Profile\",\n  \"description\": \"Information about a participant\",\n  \"isOptional\": false,\n  \"main\": \"lib/main\",\n  \"windowTypes\": {\n    \"default\": true\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/participant-profile/stylesheets/participant-profile.less",
    "content": "@import 'ui-variables';\n\n.related-threads {\n  width: calc(~\"100% + 30px\");\n  position: relative;\n  left: -15px;\n  border-top: 1px solid rgba(0,0,0,0.15);\n  transition: height 150ms ease-in-out;\n  top: 15px;\n  margin-top: -15px;\n  overflow: hidden;\n  border-radius: 0 0 @border-radius-large @border-radius-large;\n\n  .related-thread {\n    display: flex;\n    font-size: 12px;\n    color: @text-color-very-subtle;\n    width: 100%;\n    padding: 0.5em 15px;\n    border-top: 1px solid rgba(0,0,0,0.08);\n\n    &:hover {\n      background: @list-hover-bg;\n    }\n\n    .content {\n      flex: 1;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      padding-right: 1em;\n\n      .snippet {\n        opacity: 0.5;\n      }\n    }\n  }\n\n  .toggle {\n    font-size: 12px;\n    text-align: center;\n    padding: 0.5em 15px;\n    border-top: 1px solid rgba(0,0,0,0.08);\n    color: @text-color-link;\n  }\n}\n\n.participant-profile {\n  margin-bottom: 22px;\n\n  .profile-photo-wrap {\n    width: 50px;\n    height: 50px;\n    border-radius: @border-radius-base;\n    padding: 3px;\n    box-shadow: 0 0 1px rgba(0,0,0,0.5);\n    position: absolute;\n    left: calc(~\"50% - 25px\");\n    top: -31px;\n    background: @background-primary;\n\n    .profile-photo {\n      border-radius: @border-radius-small;\n      overflow: hidden;\n      text-align: center;\n      width: 44px;\n      height: 44px;\n\n      img, .default-profile-image {\n        width: 44px;\n        height: 44px;\n      }\n\n      .default-profile-image {\n        line-height: 44px;\n        font-size: 18px;\n        font-weight: 500;\n        color: white;\n        box-shadow: inset 0 0 1px rgba(0,0,0,0.18);\n        background-image: linear-gradient(to bottom, rgba(255,255,255,0.15) 0%, rgba(255,255,255,0) 100%);\n      }\n    }\n  }\n\n  .core-personal-info {\n    padding-top: 30px;\n    text-align: center;\n    margin-bottom: @spacing-standard;\n\n    .full-name, .email {\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n    }\n\n    .full-name {\n      font-size: 16px;\n    }\n    .email {\n      color: @text-color-very-subtle;\n      margin-bottom: @spacing-standard;\n    }\n    .social-profiles-wrap {\n      margin-bottom: @spacing-standard;\n    }\n    .social-profile-item {\n      margin: 0 10px;\n    }\n  }\n  .additional-info {\n    font-size: 12px;\n    p {\n      margin-bottom: 15px;\n    }\n    .bio {\n      color: @text-color-very-subtle;\n    }\n  }\n}\n\nbody.platform-win32 {\n  .participant-profile {\n    border-radius: 0;\n    .profile-photo {\n      border-radius: 0;\n    }\n  }\n  .related-threads {\n    border-radius: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/README.md",
    "content": "# Personal Level Icon\n\nAn icon to indicate whether an email was sent to either just you, or you and other recipients, or a mailing list that you were on.\n\n<img src=\"https://raw.githubusercontent.com/nylas/nylas-mail/master/internal_packages/personal-level-indicators/examples-screencap-personal-level-icon.png\"/>\n\n#### Enable this plugin\n\n1. Download and run N1\n\n2. Navigate to Preferences > Plugins and click \"Enable\" beside the plugin.\n\n#### Who?\n\nThis package is annotated for developers who have no experience with React, Flux, Electron, or N1.\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/docs/docco.css",
    "content": "/*--------------------- Typography ----------------------------*/\n\n@font-face {\n    font-family: 'aller-light';\n    src: url('public/fonts/aller-light.eot');\n    src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'),\n         url('public/fonts/aller-light.woff') format('woff'),\n         url('public/fonts/aller-light.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n@font-face {\n    font-family: 'aller-bold';\n    src: url('public/fonts/aller-bold.eot');\n    src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'),\n         url('public/fonts/aller-bold.woff') format('woff'),\n         url('public/fonts/aller-bold.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n@font-face {\n    font-family: 'roboto-black';\n    src: url('public/fonts/roboto-black.eot');\n    src: url('public/fonts/roboto-black.eot?#iefix') format('embedded-opentype'),\n         url('public/fonts/roboto-black.woff') format('woff'),\n         url('public/fonts/roboto-black.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n/*--------------------- Layout ----------------------------*/\nhtml { height: 100%; }\nbody {\n  font-family: \"aller-light\";\n  font-size: 14px;\n  line-height: 18px;\n  color: #30404f;\n  margin: 0; padding: 0;\n  height:100%;\n}\n#container { min-height: 100%; }\n\na {\n  color: #000;\n}\n\nb, strong {\n  font-weight: normal;\n  font-family: \"aller-bold\";\n}\n\np {\n  margin: 15px 0 0px;\n}\n  .annotation ul, .annotation ol {\n    margin: 25px 0;\n  }\n    .annotation ul li, .annotation ol li {\n      font-size: 14px;\n      line-height: 18px;\n      margin: 10px 0;\n    }\n\nh1, h2, h3, h4, h5, h6 {\n  color: #112233;\n  line-height: 1em;\n  font-weight: normal;\n  font-family: \"roboto-black\";\n  text-transform: uppercase;\n  margin: 30px 0 15px 0;\n}\n\nh1 {\n  margin-top: 40px;\n}\nh2 {\n  font-size: 1.26em;\n}\n\nhr {\n  border: 0;\n  background: 1px #ddd;\n  height: 1px;\n  margin: 20px 0;\n}\n\npre, tt, code {\n  font-size: 12px; line-height: 16px;\n  font-family: Menlo, Monaco, Consolas, \"Lucida Console\", monospace;\n  margin: 0; padding: 0;\n}\n  .annotation pre {\n    display: block;\n    margin: 0;\n    padding: 7px 10px;\n    background: #fcfcfc;\n    -moz-box-shadow:    inset 0 0 10px rgba(0,0,0,0.1);\n    -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);\n    box-shadow:         inset 0 0 10px rgba(0,0,0,0.1);\n    overflow-x: auto;\n  }\n    .annotation pre code {\n      border: 0;\n      padding: 0;\n      background: transparent;\n    }\n\n\nblockquote {\n  border-left: 5px solid #ccc;\n  margin: 0;\n  padding: 1px 0 1px 1em;\n}\n  .sections blockquote p {\n    font-family: Menlo, Consolas, Monaco, monospace;\n    font-size: 12px; line-height: 16px;\n    color: #999;\n    margin: 10px 0 0;\n    white-space: pre-wrap;\n  }\n\nul.sections {\n  list-style: none;\n  padding:0 0 5px 0;;\n  margin:0;\n}\n\n/*\n  Force border-box so that % widths fit the parent\n  container without overlap because of margin/padding.\n\n  More Info : http://www.quirksmode.org/css/box.html\n*/\nul.sections > li > div {\n  -moz-box-sizing: border-box;    /* firefox */\n  -ms-box-sizing: border-box;     /* ie */\n  -webkit-box-sizing: border-box; /* webkit */\n  -khtml-box-sizing: border-box;  /* konqueror */\n  box-sizing: border-box;         /* css3 */\n}\n\n\n/*---------------------- Jump Page -----------------------------*/\n#jump_to, #jump_page {\n  margin: 0;\n  background: white;\n  -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;\n  -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;\n  font: 16px Arial;\n  cursor: pointer;\n  text-align: right;\n  list-style: none;\n}\n\n#jump_to a {\n  text-decoration: none;\n}\n\n#jump_to a.large {\n  display: none;\n}\n#jump_to a.small {\n  font-size: 22px;\n  font-weight: bold;\n  color: #676767;\n}\n\n#jump_to, #jump_wrapper {\n  position: fixed;\n  right: 0; top: 0;\n  padding: 10px 15px;\n  margin:0;\n}\n\n#jump_wrapper {\n  display: none;\n  padding:0;\n}\n\n#jump_to:hover #jump_wrapper {\n  display: block;\n}\n\n#jump_page_wrapper{\n  position: fixed;\n  right: 0;\n  top: 0;\n  bottom: 0;\n}\n\n#jump_page {\n  padding: 5px 0 3px;\n  margin: 0 0 25px 25px;\n  max-height: 100%;\n  overflow: auto;\n}\n\n#jump_page .source {\n  display: block;\n  padding: 15px;\n  text-decoration: none;\n  border-top: 1px solid #eee;\n}\n\n#jump_page .source:hover {\n  background: #f5f5ff;\n}\n\n#jump_page .source:first-child {\n}\n\n/*---------------------- Low resolutions (> 320px) ---------------------*/\n@media only screen and (min-width: 320px) {\n  .pilwrap { display: none; }\n\n  ul.sections > li > div {\n    display: block;\n    padding:5px 10px 0 10px;\n  }\n\n  ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {\n    padding-left: 30px;\n  }\n\n  ul.sections > li > div.content {\n    overflow-x:auto;\n    -webkit-box-shadow: inset 0 0 5px #e5e5ee;\n    box-shadow: inset 0 0 5px #e5e5ee;\n    border: 1px solid #dedede;\n    margin:5px 10px 5px 10px;\n    padding-bottom: 5px;\n  }\n\n  ul.sections > li > div.annotation pre {\n    margin: 7px 0 7px;\n    padding-left: 15px;\n  }\n\n  ul.sections > li > div.annotation p tt, .annotation code {\n    background: #f8f8ff;\n    border: 1px solid #dedede;\n    font-size: 12px;\n    padding: 0 0.2em;\n  }\n}\n\n/*----------------------  (> 481px) ---------------------*/\n@media only screen and (min-width: 481px) {\n  #container {\n    position: relative;\n  }\n  body {\n    background-color: #F5F5FF;\n    font-size: 15px;\n    line-height: 21px;\n  }\n  pre, tt, code {\n    line-height: 18px;\n  }\n  p, ul, ol {\n    margin: 0 0 15px;\n  }\n\n\n  #jump_to {\n    padding: 5px 10px;\n  }\n  #jump_wrapper {\n    padding: 0;\n  }\n  #jump_to, #jump_page {\n    font: 10px Arial;\n    text-transform: uppercase;\n  }\n  #jump_page .source {\n    padding: 5px 10px;\n  }\n  #jump_to a.large {\n    display: inline-block;\n  }\n  #jump_to a.small {\n    display: none;\n  }\n\n\n\n  #background {\n    position: absolute;\n    top: 0; bottom: 0;\n    width: 350px;\n    background: #fff;\n    border-right: 1px solid #e5e5ee;\n    z-index: -1;\n  }\n\n  ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {\n    padding-left: 40px;\n  }\n\n  ul.sections > li {\n    white-space: nowrap;\n  }\n\n  ul.sections > li > div {\n    display: inline-block;\n  }\n\n  ul.sections > li > div.annotation {\n    max-width: 350px;\n    min-width: 350px;\n    min-height: 5px;\n    padding: 13px;\n    overflow-x: hidden;\n    white-space: normal;\n    vertical-align: top;\n    text-align: left;\n  }\n  ul.sections > li > div.annotation pre {\n    margin: 15px 0 15px;\n    padding-left: 15px;\n  }\n\n  ul.sections > li > div.content {\n    padding: 13px;\n    vertical-align: top;\n    border: none;\n    -webkit-box-shadow: none;\n    box-shadow: none;\n  }\n\n  .pilwrap {\n    position: relative;\n    display: inline;\n  }\n\n  .pilcrow {\n    font: 12px Arial;\n    text-decoration: none;\n    color: #454545;\n    position: absolute;\n    top: 3px; left: -20px;\n    padding: 1px 2px;\n    opacity: 0;\n    -webkit-transition: opacity 0.2s linear;\n  }\n    .for-h1 .pilcrow {\n      top: 47px;\n    }\n    .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow {\n      top: 35px;\n    }\n\n  ul.sections > li > div.annotation:hover .pilcrow {\n    opacity: 1;\n  }\n}\n\n/*---------------------- (> 1025px) ---------------------*/\n@media only screen and (min-width: 1025px) {\n\n  body {\n    font-size: 16px;\n    line-height: 24px;\n  }\n\n  #background {\n    width: 525px;\n  }\n  ul.sections > li > div.annotation {\n    max-width: 525px;\n    min-width: 525px;\n    padding: 10px 25px 1px 50px;\n  }\n  ul.sections > li > div.content {\n    padding: 9px 15px 16px 25px;\n  }\n}\n\n/*---------------------- Syntax Highlighting -----------------------------*/\n\ntd.linenos { background-color: #f0f0f0; padding-right: 10px; }\nspan.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }\n/*\n\ngithub.com style (c) Vasily Polovnyov <vast@whiteants.net>\n\n*/\n\npre code {\n  display: block; padding: 0.5em;\n  color: #000;\n  background: #f8f8ff\n}\n\npre .hljs-comment,\npre .hljs-template_comment,\npre .hljs-diff .hljs-header,\npre .hljs-javadoc {\n  color: #408080;\n  font-style: italic\n}\n\npre .hljs-keyword,\npre .hljs-assignment,\npre .hljs-literal,\npre .hljs-css .hljs-rule .hljs-keyword,\npre .hljs-winutils,\npre .hljs-javascript .hljs-title,\npre .hljs-lisp .hljs-title,\npre .hljs-subst {\n  color: #954121;\n  /*font-weight: bold*/\n}\n\npre .hljs-number,\npre .hljs-hexcolor {\n  color: #40a070\n}\n\npre .hljs-string,\npre .hljs-tag .hljs-value,\npre .hljs-phpdoc,\npre .hljs-tex .hljs-formula {\n  color: #219161;\n}\n\npre .hljs-title,\npre .hljs-id {\n  color: #19469D;\n}\npre .hljs-params {\n  color: #00F;\n}\n\npre .hljs-javascript .hljs-title,\npre .hljs-lisp .hljs-title,\npre .hljs-subst {\n  font-weight: normal\n}\n\npre .hljs-class .hljs-title,\npre .hljs-haskell .hljs-label,\npre .hljs-tex .hljs-command {\n  color: #458;\n  font-weight: bold\n}\n\npre .hljs-tag,\npre .hljs-tag .hljs-title,\npre .hljs-rules .hljs-property,\npre .hljs-django .hljs-tag .hljs-keyword {\n  color: #000080;\n  font-weight: normal\n}\n\npre .hljs-attribute,\npre .hljs-variable,\npre .hljs-instancevar,\npre .hljs-lisp .hljs-body {\n  color: #008080\n}\n\npre .hljs-regexp {\n  color: #B68\n}\n\npre .hljs-class {\n  color: #458;\n  font-weight: bold\n}\n\npre .hljs-symbol,\npre .hljs-ruby .hljs-symbol .hljs-string,\npre .hljs-ruby .hljs-symbol .hljs-keyword,\npre .hljs-ruby .hljs-symbol .hljs-keymethods,\npre .hljs-lisp .hljs-keyword,\npre .hljs-tex .hljs-special,\npre .hljs-input_number {\n  color: #990073\n}\n\npre .hljs-builtin,\npre .hljs-constructor,\npre .hljs-built_in,\npre .hljs-lisp .hljs-title {\n  color: #0086b3\n}\n\npre .hljs-preprocessor,\npre .hljs-pi,\npre .hljs-doctype,\npre .hljs-shebang,\npre .hljs-cdata {\n  color: #999;\n  font-weight: bold\n}\n\npre .hljs-deletion {\n  background: #fdd\n}\n\npre .hljs-addition {\n  background: #dfd\n}\n\npre .hljs-diff .hljs-change {\n  background: #0086b3\n}\n\npre .hljs-chunk {\n  color: #aaa\n}\n\npre .hljs-tex .hljs-formula {\n  opacity: 0.5;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/docs/personal-level-icon.html",
    "content": "<!DOCTYPE html>\n\n<html>\n<head>\n  <title>Personal Level Icon</title>\n  <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;\">\n  <link rel=\"stylesheet\" media=\"all\" href=\"docco.css\" />\n</head>\n<body>\n  <div id=\"container\">\n    <div id=\"background\"></div>\n    \n    <ul class=\"sections\">\n        \n        \n        \n        <li id=\"section-1\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-1\">&#182;</a>\n              </div>\n              <h1 id=\"personal-level-icon\">Personal Level Icon</h1>\n<p>Show an icon for each thread to indicate whether you’re the only recipient,\none of many recipients, or a member of a mailing list.</p>\n\n            </div>\n            \n        </li>\n        \n        \n        <li id=\"section-2\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-2\">&#182;</a>\n              </div>\n              <p>Access core components by requiring <code>nylas-exports</code>.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>{Utils, DraftStore, React} = <span class=\"hljs-built_in\">require</span> <span class=\"hljs-string\">'nylas-exports'</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-3\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-3\">&#182;</a>\n              </div>\n              <p>Access N1 React components by requiring <code>nylas-component-kit</code>.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>{RetinaImg} = <span class=\"hljs-built_in\">require</span> <span class=\"hljs-string\">'nylas-component-kit'</span>\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title\">PersonalLevelIcon</span> <span class=\"hljs-keyword\">extends</span> <span class=\"hljs-title\">React</span>.<span class=\"hljs-title\">Component</span></span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-4\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-4\">&#182;</a>\n              </div>\n              <p>Note: You should assign a new displayName to avoid naming\nconflicts when injecting your item</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-property\">@displayName</span>: <span class=\"hljs-string\">'PersonalLevelIcon'</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-5\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-5\">&#182;</a>\n              </div>\n              <p>In the constructor, we’re setting the component’s initial state.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">constructor</span>: <span class=\"hljs-function\"><span class=\"hljs-params\">(<span class=\"hljs-property\">@props</span>)</span> -&gt;</span>\n    <span class=\"hljs-property\">@state</span> =\n      <span class=\"hljs-attribute\">level</span>: <span class=\"hljs-property\">@_calculateLevel</span>(<span class=\"hljs-property\">@props</span>.thread)</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-6\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-6\">&#182;</a>\n              </div>\n              <p>React components’ <code>render</code> methods return a virtual DOM element to render.\nThe returned DOM fragment is a result of the component’s <code>state</code> and\n<code>props</code>. In that sense, <code>render</code> methods are deterministic.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">render</span>: <span class=\"hljs-function\">=&gt;</span>\n    React.createElement(<span class=\"hljs-string\">\"div\"</span>, {<span class=\"hljs-string\">\"className\"</span>: <span class=\"hljs-string\">\"personal-level-icon\"</span>},\n      (<span class=\"hljs-property\">@_renderIcon</span>())\n    )</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-7\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-7\">&#182;</a>\n              </div>\n              <p>Some application logic which is specific to this package to decide which\ncharacter to render.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">_renderIcon</span>: <span class=\"hljs-function\">=&gt;</span>\n    <span class=\"hljs-keyword\">switch</span> <span class=\"hljs-property\">@state</span>.level\n      <span class=\"hljs-keyword\">when</span> <span class=\"hljs-number\">0</span> <span class=\"hljs-keyword\">then</span> <span class=\"hljs-string\">\"\"</span>\n      <span class=\"hljs-keyword\">when</span> <span class=\"hljs-number\">1</span> <span class=\"hljs-keyword\">then</span> <span class=\"hljs-string\">\"\\u3009\"</span>\n      <span class=\"hljs-keyword\">when</span> <span class=\"hljs-number\">2</span> <span class=\"hljs-keyword\">then</span> <span class=\"hljs-string\">\"\\u300b\"</span>\n      <span class=\"hljs-keyword\">when</span> <span class=\"hljs-number\">3</span> <span class=\"hljs-keyword\">then</span> <span class=\"hljs-string\">\"\\u21ba\"</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-8\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-8\">&#182;</a>\n              </div>\n              <p>Some more application logic which is specific to this package to decide\nwhat level of personalness is related to the <code>thread</code>.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">_calculateLevel</span>: <span class=\"hljs-function\"><span class=\"hljs-params\">(thread)</span> =&gt;</span>\n<span class=\"hljs-function\">    <span class=\"hljs-title\">hasMe</span> = <span class=\"hljs-params\">(thread.participants.filter (p) -&gt; p.isMe())</span>.<span class=\"hljs-title\">length</span> &gt; 0\n    <span class=\"hljs-title\">numOthers</span> = <span class=\"hljs-title\">thread</span>.<span class=\"hljs-title\">participants</span>.<span class=\"hljs-title\">length</span> - <span class=\"hljs-title\">hasMe</span>\n    <span class=\"hljs-title\">if</span> <span class=\"hljs-title\">not</span> <span class=\"hljs-title\">hasMe</span>\n      <span class=\"hljs-title\">return</span> 0\n    <span class=\"hljs-title\">if</span> <span class=\"hljs-title\">numOthers</span> &gt; 1\n      <span class=\"hljs-title\">return</span> 1\n    <span class=\"hljs-title\">if</span> <span class=\"hljs-title\">numOthers</span> <span class=\"hljs-title\">is</span> 1\n      <span class=\"hljs-title\">return</span> 2\n    <span class=\"hljs-title\">else</span>\n      <span class=\"hljs-title\">return</span> 3\n\n<span class=\"hljs-title\">module</span>.<span class=\"hljs-title\">exports</span> = <span class=\"hljs-title\">PersonalLevelIcon</span>\n\n</span></pre></div></div>\n            \n        </li>\n        \n    </ul>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/docs/public/stylesheets/normalize.css",
    "content": "/*! normalize.css v2.0.1 | MIT License | git.io/normalize */\n\n/* ==========================================================================\n   HTML5 display definitions\n   ========================================================================== */\n\n/*\n * Corrects `block` display not defined in IE 8/9.\n */\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nnav,\nsection,\nsummary {\n    display: block;\n}\n\n/*\n * Corrects `inline-block` display not defined in IE 8/9.\n */\n\naudio,\ncanvas,\nvideo {\n    display: inline-block;\n}\n\n/*\n * Prevents modern browsers from displaying `audio` without controls.\n * Remove excess height in iOS 5 devices.\n */\n\naudio:not([controls]) {\n    display: none;\n    height: 0;\n}\n\n/*\n * Addresses styling for `hidden` attribute not present in IE 8/9.\n */\n\n[hidden] {\n    display: none;\n}\n\n/* ==========================================================================\n   Base\n   ========================================================================== */\n\n/*\n * 1. Sets default font family to sans-serif.\n * 2. Prevents iOS text size adjust after orientation change, without disabling\n *    user zoom.\n */\n\nhtml {\n    font-family: sans-serif; /* 1 */\n    -webkit-text-size-adjust: 100%; /* 2 */\n    -ms-text-size-adjust: 100%; /* 2 */\n}\n\n/*\n * Removes default margin.\n */\n\nbody {\n    margin: 0;\n}\n\n/* ==========================================================================\n   Links\n   ========================================================================== */\n\n/*\n * Addresses `outline` inconsistency between Chrome and other browsers.\n */\n\na:focus {\n    outline: thin dotted;\n}\n\n/*\n * Improves readability when focused and also mouse hovered in all browsers.\n */\n\na:active,\na:hover {\n    outline: 0;\n}\n\n/* ==========================================================================\n   Typography\n   ========================================================================== */\n\n/*\n * Addresses `h1` font sizes within `section` and `article` in Firefox 4+,\n * Safari 5, and Chrome.\n */\n\nh1 {\n    font-size: 2em;\n}\n\n/*\n * Addresses styling not present in IE 8/9, Safari 5, and Chrome.\n */\n\nabbr[title] {\n    border-bottom: 1px dotted;\n}\n\n/*\n * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.\n */\n\nb,\nstrong {\n    font-weight: bold;\n}\n\n/*\n * Addresses styling not present in Safari 5 and Chrome.\n */\n\ndfn {\n    font-style: italic;\n}\n\n/*\n * Addresses styling not present in IE 8/9.\n */\n\nmark {\n    background: #ff0;\n    color: #000;\n}\n\n\n/*\n * Corrects font family set oddly in Safari 5 and Chrome.\n */\n\ncode,\nkbd,\npre,\nsamp {\n    font-family: monospace, serif;\n    font-size: 1em;\n}\n\n/*\n * Improves readability of pre-formatted text in all browsers.\n */\n\npre {\n    white-space: pre;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n\n/*\n * Sets consistent quote types.\n */\n\nq {\n    quotes: \"\\201C\" \"\\201D\" \"\\2018\" \"\\2019\";\n}\n\n/*\n * Addresses inconsistent and variable font size in all browsers.\n */\n\nsmall {\n    font-size: 80%;\n}\n\n/*\n * Prevents `sub` and `sup` affecting `line-height` in all browsers.\n */\n\nsub,\nsup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n}\n\nsup {\n    top: -0.5em;\n}\n\nsub {\n    bottom: -0.25em;\n}\n\n/* ==========================================================================\n   Embedded content\n   ========================================================================== */\n\n/*\n * Removes border when inside `a` element in IE 8/9.\n */\n\nimg {\n    border: 0;\n}\n\n/*\n * Corrects overflow displayed oddly in IE 9.\n */\n\nsvg:not(:root) {\n    overflow: hidden;\n}\n\n/* ==========================================================================\n   Figures\n   ========================================================================== */\n\n/*\n * Addresses margin not present in IE 8/9 and Safari 5.\n */\n\nfigure {\n    margin: 0;\n}\n\n/* ==========================================================================\n   Forms\n   ========================================================================== */\n\n/*\n * Define consistent border, margin, and padding.\n */\n\nfieldset {\n    border: 1px solid #c0c0c0;\n    margin: 0 2px;\n    padding: 0.35em 0.625em 0.75em;\n}\n\n/*\n * 1. Corrects color not being inherited in IE 8/9.\n * 2. Remove padding so people aren't caught out if they zero out fieldsets.\n */\n\nlegend {\n    border: 0; /* 1 */\n    padding: 0; /* 2 */\n}\n\n/*\n * 1. Corrects font family not being inherited in all browsers.\n * 2. Corrects font size not being inherited in all browsers.\n * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome\n */\n\nbutton,\ninput,\nselect,\ntextarea {\n    font-family: inherit; /* 1 */\n    font-size: 100%; /* 2 */\n    margin: 0; /* 3 */\n}\n\n/*\n * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in\n * the UA stylesheet.\n */\n\nbutton,\ninput {\n    line-height: normal;\n}\n\n/*\n * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n *    and `video` controls.\n * 2. Corrects inability to style clickable `input` types in iOS.\n * 3. Improves usability and consistency of cursor style between image-type\n *    `input` and others.\n */\n\nbutton,\nhtml input[type=\"button\"], /* 1 */\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n    -webkit-appearance: button; /* 2 */\n    cursor: pointer; /* 3 */\n}\n\n/*\n * Re-set default cursor for disabled elements.\n */\n\nbutton[disabled],\ninput[disabled] {\n    cursor: default;\n}\n\n/*\n * 1. Addresses box sizing set to `content-box` in IE 8/9.\n * 2. Removes excess padding in IE 8/9.\n */\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n    box-sizing: border-box; /* 1 */\n    padding: 0; /* 2 */\n}\n\n/*\n * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.\n * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome\n *    (include `-moz` to future-proof).\n */\n\ninput[type=\"search\"] {\n    -webkit-appearance: textfield; /* 1 */\n    -moz-box-sizing: content-box;\n    -webkit-box-sizing: content-box; /* 2 */\n    box-sizing: content-box;\n}\n\n/*\n * Removes inner padding and search cancel button in Safari 5 and Chrome\n * on OS X.\n */\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n    -webkit-appearance: none;\n}\n\n/*\n * Removes inner padding and border in Firefox 4+.\n */\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n    border: 0;\n    padding: 0;\n}\n\n/*\n * 1. Removes default vertical scrollbar in IE 8/9.\n * 2. Improves readability and alignment in all browsers.\n */\n\ntextarea {\n    overflow: auto; /* 1 */\n    vertical-align: top; /* 2 */\n}\n\n/* ==========================================================================\n   Tables\n   ========================================================================== */\n\n/*\n * Remove most spacing between table cells.\n */\n\ntable {\n    border-collapse: collapse;\n    border-spacing: 0;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/lib/main.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports'\nimport PersonalLevelIcon from './personal-level-icon'\n\n/*\nAll packages must export a basic object that has at least the following 3\nmethods:\n\n1. `activate` - Actions to take once the package gets turned on.\nPre-enabled packages get activated on N1 bootup. They can also be\nactivated manually by a user.\n\n2. `deactivate` - Actions to take when a package gets turned off. This can\nhappen when a user manually disables a package.\n\n3. `serialize` - A simple serializable object that gets saved to disk\nbefore N1 quits. This gets passed back into `activate` next time N1 boots\nup or your package is manually activated.\n*/\n\nexport function activate() {\n  ComponentRegistry.register(PersonalLevelIcon, {\n    role: 'ThreadListIcon',\n  });\n}\n\nexport function serialize() {\n  return {};\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(PersonalLevelIcon);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/lib/personal-level-icon.jsx",
    "content": "import {React} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\n\nconst StaticEmptyIndicator = (\n  <div className=\"personal-level-icon\" />\n);\n\nexport default class PersonalLevelIcon extends React.Component {\n  // Note: You should assign a new displayName to avoid naming\n  // conflicts when injecting your item\n  static displayName = 'PersonalLevelIcon';\n\n  static propTypes = {\n    thread: React.PropTypes.object.isRequired,\n  };\n\n  renderIndicator(level) {\n    return (\n      <div className=\"personal-level-icon\">\n        <RetinaImg\n          url={`nylas://personal-level-indicators/assets/PLI-Level${level}@2x.png`}\n          mode={RetinaImg.Mode.ContentDark}\n        />\n      </div>\n    )\n  }\n\n  // React components' `render` methods return a virtual DOM element to render.\n  // The returned DOM fragment is a result of the component's `state` and\n  // `props`. In that sense, `render` methods are deterministic.\n  render() {\n    const {thread} = this.props;\n    const me = thread.participants.find(p => p.isMe());\n\n    if (me && thread.participants.length === 2) {\n      return this.renderIndicator(2);\n    }\n    if (me && thread.participants.length > 2) {\n      return this.renderIndicator(1);\n    }\n\n    return StaticEmptyIndicator;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/package.json",
    "content": "{\n  \"name\": \"personal-level-indicators\",\n  \"main\": \"./lib/main\",\n  \"version\": \"0.1.0\",\n  \"isHiddenOnPluginsPage\": true,\n\n  \"title\": \"Personal Level Indicators\",\n  \"description\": \"Display chevrons beside threads that indicate whether you're a direct recipient or the only recipient on a thread.\",\n  \"icon\": \"./icon.png\",\n  \"isOptional\": true,\n\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"\"\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/personal-level-indicators/stylesheets/main.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\ndiv.personal-level-icon {\n  display: inline-block;\n  margin: 0 3px;\n  width: 12px;\n  img {\n    vertical-align: initial;\n  }\n}\n\n.list-item.focused, .list-item.selected {\n  div.personal-level-icon {\n    img {\n      -webkit-filter: brightness(600%) grayscale(100%);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/README.md",
    "content": "## Phishing Detection\n\nA sample package for Nylas Mail to detect simple phishing attempts. This package display a simple warning if\na message's originating address is different from its return address. The warning looks like this:\n\n![screenshot](./screenshot.png)\n\n#### Install this plugin\n\n1. Download and run N1\n\n2. From the menu, select `Developer > Install a Package Manually...`\n   The dialog will default to this examples directory. Just choose the\n   package to install it!\n\n   > When you install packages, they're moved to `~/.nylas-mail/packages`,\n   > and N1 runs `apm install` on the command line to fetch dependencies\n   > listed in the package's `package.json`\n\n#### Who is this for?\n\nThis package is our slimmest example package. It's annotated for developers who have no experience with React, Flux, Electron, or N1.\n\n#### To build documentation (the manual way)\n\n```\ncjsx-transform lib/main.cjsx > docs/main.coffee\ndocco docs/main.coffee\nrm docs/main.coffee\n```\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/docs/docco.css",
    "content": "/*--------------------- Typography ----------------------------*/\n\n@font-face {\n    font-family: 'aller-light';\n    src: url('public/fonts/aller-light.eot');\n    src: url('public/fonts/aller-light.eot?#iefix') format('embedded-opentype'),\n         url('public/fonts/aller-light.woff') format('woff'),\n         url('public/fonts/aller-light.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n@font-face {\n    font-family: 'aller-bold';\n    src: url('public/fonts/aller-bold.eot');\n    src: url('public/fonts/aller-bold.eot?#iefix') format('embedded-opentype'),\n         url('public/fonts/aller-bold.woff') format('woff'),\n         url('public/fonts/aller-bold.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n@font-face {\n    font-family: 'roboto-black';\n    src: url('public/fonts/roboto-black.eot');\n    src: url('public/fonts/roboto-black.eot?#iefix') format('embedded-opentype'),\n         url('public/fonts/roboto-black.woff') format('woff'),\n         url('public/fonts/roboto-black.ttf') format('truetype');\n    font-weight: normal;\n    font-style: normal;\n}\n\n/*--------------------- Layout ----------------------------*/\nhtml { height: 100%; }\nbody {\n  font-family: \"aller-light\";\n  font-size: 14px;\n  line-height: 18px;\n  color: #30404f;\n  margin: 0; padding: 0;\n  height:100%;\n}\n#container { min-height: 100%; }\n\na {\n  color: #000;\n}\n\nb, strong {\n  font-weight: normal;\n  font-family: \"aller-bold\";\n}\n\np {\n  margin: 15px 0 0px;\n}\n  .annotation ul, .annotation ol {\n    margin: 25px 0;\n  }\n    .annotation ul li, .annotation ol li {\n      font-size: 14px;\n      line-height: 18px;\n      margin: 10px 0;\n    }\n\nh1, h2, h3, h4, h5, h6 {\n  color: #112233;\n  line-height: 1em;\n  font-weight: normal;\n  font-family: \"roboto-black\";\n  text-transform: uppercase;\n  margin: 30px 0 15px 0;\n}\n\nh1 {\n  margin-top: 40px;\n}\nh2 {\n  font-size: 1.26em;\n}\n\nhr {\n  border: 0;\n  background: 1px #ddd;\n  height: 1px;\n  margin: 20px 0;\n}\n\npre, tt, code {\n  font-size: 12px; line-height: 16px;\n  font-family: Menlo, Monaco, Consolas, \"Lucida Console\", monospace;\n  margin: 0; padding: 0;\n}\n  .annotation pre {\n    display: block;\n    margin: 0;\n    padding: 7px 10px;\n    background: #fcfcfc;\n    -moz-box-shadow:    inset 0 0 10px rgba(0,0,0,0.1);\n    -webkit-box-shadow: inset 0 0 10px rgba(0,0,0,0.1);\n    box-shadow:         inset 0 0 10px rgba(0,0,0,0.1);\n    overflow-x: auto;\n  }\n    .annotation pre code {\n      border: 0;\n      padding: 0;\n      background: transparent;\n    }\n\n\nblockquote {\n  border-left: 5px solid #ccc;\n  margin: 0;\n  padding: 1px 0 1px 1em;\n}\n  .sections blockquote p {\n    font-family: Menlo, Consolas, Monaco, monospace;\n    font-size: 12px; line-height: 16px;\n    color: #999;\n    margin: 10px 0 0;\n    white-space: pre-wrap;\n  }\n\nul.sections {\n  list-style: none;\n  padding:0 0 5px 0;;\n  margin:0;\n}\n\n/*\n  Force border-box so that % widths fit the parent\n  container without overlap because of margin/padding.\n\n  More Info : http://www.quirksmode.org/css/box.html\n*/\nul.sections > li > div {\n  -moz-box-sizing: border-box;    /* firefox */\n  -ms-box-sizing: border-box;     /* ie */\n  -webkit-box-sizing: border-box; /* webkit */\n  -khtml-box-sizing: border-box;  /* konqueror */\n  box-sizing: border-box;         /* css3 */\n}\n\n\n/*---------------------- Jump Page -----------------------------*/\n#jump_to, #jump_page {\n  margin: 0;\n  background: white;\n  -webkit-box-shadow: 0 0 25px #777; -moz-box-shadow: 0 0 25px #777;\n  -webkit-border-bottom-left-radius: 5px; -moz-border-radius-bottomleft: 5px;\n  font: 16px Arial;\n  cursor: pointer;\n  text-align: right;\n  list-style: none;\n}\n\n#jump_to a {\n  text-decoration: none;\n}\n\n#jump_to a.large {\n  display: none;\n}\n#jump_to a.small {\n  font-size: 22px;\n  font-weight: bold;\n  color: #676767;\n}\n\n#jump_to, #jump_wrapper {\n  position: fixed;\n  right: 0; top: 0;\n  padding: 10px 15px;\n  margin:0;\n}\n\n#jump_wrapper {\n  display: none;\n  padding:0;\n}\n\n#jump_to:hover #jump_wrapper {\n  display: block;\n}\n\n#jump_page_wrapper{\n  position: fixed;\n  right: 0;\n  top: 0;\n  bottom: 0;\n}\n\n#jump_page {\n  padding: 5px 0 3px;\n  margin: 0 0 25px 25px;\n  max-height: 100%;\n  overflow: auto;\n}\n\n#jump_page .source {\n  display: block;\n  padding: 15px;\n  text-decoration: none;\n  border-top: 1px solid #eee;\n}\n\n#jump_page .source:hover {\n  background: #f5f5ff;\n}\n\n#jump_page .source:first-child {\n}\n\n/*---------------------- Low resolutions (> 320px) ---------------------*/\n@media only screen and (min-width: 320px) {\n  .pilwrap { display: none; }\n\n  ul.sections > li > div {\n    display: block;\n    padding:5px 10px 0 10px;\n  }\n\n  ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {\n    padding-left: 30px;\n  }\n\n  ul.sections > li > div.content {\n    overflow-x:auto;\n    -webkit-box-shadow: inset 0 0 5px #e5e5ee;\n    box-shadow: inset 0 0 5px #e5e5ee;\n    border: 1px solid #dedede;\n    margin:5px 10px 5px 10px;\n    padding-bottom: 5px;\n  }\n\n  ul.sections > li > div.annotation pre {\n    margin: 7px 0 7px;\n    padding-left: 15px;\n  }\n\n  ul.sections > li > div.annotation p tt, .annotation code {\n    background: #f8f8ff;\n    border: 1px solid #dedede;\n    font-size: 12px;\n    padding: 0 0.2em;\n  }\n}\n\n/*----------------------  (> 481px) ---------------------*/\n@media only screen and (min-width: 481px) {\n  #container {\n    position: relative;\n  }\n  body {\n    background-color: #F5F5FF;\n    font-size: 15px;\n    line-height: 21px;\n  }\n  pre, tt, code {\n    line-height: 18px;\n  }\n  p, ul, ol {\n    margin: 0 0 15px;\n  }\n\n\n  #jump_to {\n    padding: 5px 10px;\n  }\n  #jump_wrapper {\n    padding: 0;\n  }\n  #jump_to, #jump_page {\n    font: 10px Arial;\n    text-transform: uppercase;\n  }\n  #jump_page .source {\n    padding: 5px 10px;\n  }\n  #jump_to a.large {\n    display: inline-block;\n  }\n  #jump_to a.small {\n    display: none;\n  }\n\n\n\n  #background {\n    position: absolute;\n    top: 0; bottom: 0;\n    width: 350px;\n    background: #fff;\n    border-right: 1px solid #e5e5ee;\n    z-index: -1;\n  }\n\n  ul.sections > li > div.annotation ul, ul.sections > li > div.annotation ol {\n    padding-left: 40px;\n  }\n\n  ul.sections > li {\n    white-space: nowrap;\n  }\n\n  ul.sections > li > div {\n    display: inline-block;\n  }\n\n  ul.sections > li > div.annotation {\n    max-width: 350px;\n    min-width: 350px;\n    min-height: 5px;\n    padding: 13px;\n    overflow-x: hidden;\n    white-space: normal;\n    vertical-align: top;\n    text-align: left;\n  }\n  ul.sections > li > div.annotation pre {\n    margin: 15px 0 15px;\n    padding-left: 15px;\n  }\n\n  ul.sections > li > div.content {\n    padding: 13px;\n    vertical-align: top;\n    border: none;\n    -webkit-box-shadow: none;\n    box-shadow: none;\n  }\n\n  .pilwrap {\n    position: relative;\n    display: inline;\n  }\n\n  .pilcrow {\n    font: 12px Arial;\n    text-decoration: none;\n    color: #454545;\n    position: absolute;\n    top: 3px; left: -20px;\n    padding: 1px 2px;\n    opacity: 0;\n    -webkit-transition: opacity 0.2s linear;\n  }\n    .for-h1 .pilcrow {\n      top: 47px;\n    }\n    .for-h2 .pilcrow, .for-h3 .pilcrow, .for-h4 .pilcrow {\n      top: 35px;\n    }\n\n  ul.sections > li > div.annotation:hover .pilcrow {\n    opacity: 1;\n  }\n}\n\n/*---------------------- (> 1025px) ---------------------*/\n@media only screen and (min-width: 1025px) {\n\n  body {\n    font-size: 16px;\n    line-height: 24px;\n  }\n\n  #background {\n    width: 525px;\n  }\n  ul.sections > li > div.annotation {\n    max-width: 525px;\n    min-width: 525px;\n    padding: 10px 25px 1px 50px;\n  }\n  ul.sections > li > div.content {\n    padding: 9px 15px 16px 25px;\n  }\n}\n\n/*---------------------- Syntax Highlighting -----------------------------*/\n\ntd.linenos { background-color: #f0f0f0; padding-right: 10px; }\nspan.lineno { background-color: #f0f0f0; padding: 0 5px 0 5px; }\n/*\n\ngithub.com style (c) Vasily Polovnyov <vast@whiteants.net>\n\n*/\n\npre code {\n  display: block; padding: 0.5em;\n  color: #000;\n  background: #f8f8ff\n}\n\npre .hljs-comment,\npre .hljs-template_comment,\npre .hljs-diff .hljs-header,\npre .hljs-javadoc {\n  color: #408080;\n  font-style: italic\n}\n\npre .hljs-keyword,\npre .hljs-assignment,\npre .hljs-literal,\npre .hljs-css .hljs-rule .hljs-keyword,\npre .hljs-winutils,\npre .hljs-javascript .hljs-title,\npre .hljs-lisp .hljs-title,\npre .hljs-subst {\n  color: #954121;\n  /*font-weight: bold*/\n}\n\npre .hljs-number,\npre .hljs-hexcolor {\n  color: #40a070\n}\n\npre .hljs-string,\npre .hljs-tag .hljs-value,\npre .hljs-phpdoc,\npre .hljs-tex .hljs-formula {\n  color: #219161;\n}\n\npre .hljs-title,\npre .hljs-id {\n  color: #19469D;\n}\npre .hljs-params {\n  color: #00F;\n}\n\npre .hljs-javascript .hljs-title,\npre .hljs-lisp .hljs-title,\npre .hljs-subst {\n  font-weight: normal\n}\n\npre .hljs-class .hljs-title,\npre .hljs-haskell .hljs-label,\npre .hljs-tex .hljs-command {\n  color: #458;\n  font-weight: bold\n}\n\npre .hljs-tag,\npre .hljs-tag .hljs-title,\npre .hljs-rules .hljs-property,\npre .hljs-django .hljs-tag .hljs-keyword {\n  color: #000080;\n  font-weight: normal\n}\n\npre .hljs-attribute,\npre .hljs-variable,\npre .hljs-instancevar,\npre .hljs-lisp .hljs-body {\n  color: #008080\n}\n\npre .hljs-regexp {\n  color: #B68\n}\n\npre .hljs-class {\n  color: #458;\n  font-weight: bold\n}\n\npre .hljs-symbol,\npre .hljs-ruby .hljs-symbol .hljs-string,\npre .hljs-ruby .hljs-symbol .hljs-keyword,\npre .hljs-ruby .hljs-symbol .hljs-keymethods,\npre .hljs-lisp .hljs-keyword,\npre .hljs-tex .hljs-special,\npre .hljs-input_number {\n  color: #990073\n}\n\npre .hljs-builtin,\npre .hljs-constructor,\npre .hljs-built_in,\npre .hljs-lisp .hljs-title {\n  color: #0086b3\n}\n\npre .hljs-preprocessor,\npre .hljs-pi,\npre .hljs-doctype,\npre .hljs-shebang,\npre .hljs-cdata {\n  color: #999;\n  font-weight: bold\n}\n\npre .hljs-deletion {\n  background: #fdd\n}\n\npre .hljs-addition {\n  background: #dfd\n}\n\npre .hljs-diff .hljs-change {\n  background: #0086b3\n}\n\npre .hljs-chunk {\n  color: #aaa\n}\n\npre .hljs-tex .hljs-formula {\n  opacity: 0.5;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/docs/main.coffee",
    "content": "# # Phishing Detection\n#\n# This is a simple package to notify N1 users if an email is a potential\n# phishing scam.\n\n# You can access N1 dependencies by requiring 'nylas-exports'\n{React,\n # The ComponentRegistry manages all React components in N1.\n ComponentRegistry,\n # A `Store` is a Flux component which contains all business logic and data\n # models to be consumed by React components to render markup.\n MessageStore} = require 'nylas-exports'\n\n# Notice that this file is `main.cjsx` rather than `main.coffee`. We use the\n# `.cjsx` filetype because we use the CJSX DSL to describe markup for React to\n# render. Without the CJSX, we could just name this file `main.coffee` instead.\nclass PhishingIndicator extends React.Component\n\n  # Adding a @displayName to a React component helps for debugging.\n  @displayName: 'PhishingIndicator'\n\n  # @propTypes is an object which validates the datatypes of properties that\n  # this React component can receive.\n  @propTypes:\n    thread: React.PropTypes.object.isRequired\n\n  # A React component's `render` method returns a virtual DOM element described\n  # in CJSX. `render` is deterministic: with the same input, it will always\n  # render the same output. Here, the input is provided by @isPhishingAttempt.\n  # `@state` and `@props` are popular inputs as well.\n  render: =>\n\n    # Our inputs for the virtual DOM to render come from @isPhishingAttempt.\n    [from, reply_to] = @isPhishingAttempt()\n\n    # We add some more application logic to decide how to render.\n    if from isnt null and reply_to isnt null\n      React.createElement(\"div\", {\"className\": \"phishingIndicator\"},\n        React.createElement(\"b\", null, \"This message looks suspicious!\"),\n        React.createElement(\"p\", null, \"It originates from \", (from), \" but replies will go to \", (reply_to), \".\")\n      )\n\n    # If you don't want a React component to render anything at all, then your\n    # `render` method should return `null` or `undefined`.\n    else\n      null\n\n  isPhishingAttempt: =>\n\n    # In this package, the MessageStore is the source of our data which will be\n    # the input for the `render` function. @isPhishingAttempt is performing some\n    # domain-specific application logic to prepare the data for `render`.\n    message = MessageStore.items()[0]\n\n    # This package's strategy to ascertain whether or not the email is a\n    # phishing attempt boils down to checking the `replyTo` attributes on\n    # `Message` models from `MessageStore`.\n    if message.replyTo? and message.replyTo.length != 0\n\n      # The `from` and `replyTo` attributes on `Message` models both refer to\n      # arrays of `Contact` models, which in turn have `email` attributes.\n      from = message.from[0].email\n      reply_to = message.replyTo[0].email\n\n      # This is our core logic for our whole package! If the `from` and\n      # `replyTo` emails are different, then we want to show a phishing warning.\n      return [from, reply_to] if reply_to isnt from\n\n    return [null, null]\n\nmodule.exports =\n\n  # Activate is called when the package is loaded. If your package previously\n  # saved state using `serialize` it is provided.\n  activate: (@state) ->\n\n    # This is a good time to tell the `ComponentRegistry` to insert our\n    # React component into the `'MessageListHeaders'` part of the application.\n    ComponentRegistry.register PhishingIndicator,\n      role: 'MessageListHeaders'\n\n  # Serialize is called when your package is about to be unmounted.\n  # You can return a state object that will be passed back to your package\n  # when it is re-activated.\n  serialize: ->\n\n  # This **optional** method is called when the window is shutting down,\n  # or when your package is being updated or disabled. If your package is\n  # watching any files, holding external resources, providing commands or\n  # subscribing to events, release them here.\n  deactivate: ->\n    ComponentRegistry.unregister(PhishingIndicator)\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/docs/main.html",
    "content": "<!DOCTYPE html>\n\n<html>\n<head>\n  <title>Phishing Detection</title>\n  <meta http-equiv=\"content-type\" content=\"text/html; charset=UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, target-densitydpi=160dpi, initial-scale=1.0; maximum-scale=1.0; user-scalable=0;\">\n  <link rel=\"stylesheet\" media=\"all\" href=\"docco.css\" />\n</head>\n<body>\n  <div id=\"container\">\n    <div id=\"background\"></div>\n    \n    <ul class=\"sections\">\n        \n        \n        \n        <li id=\"section-1\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-1\">&#182;</a>\n              </div>\n              <h1 id=\"phishing-detection\">Phishing Detection</h1>\n<p>This is a simple package to notify N1 users if an email is a potential\nphishing scam.</p>\n\n            </div>\n            \n        </li>\n        \n        \n        <li id=\"section-2\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-2\">&#182;</a>\n              </div>\n              <p>You can access N1 dependencies by requiring ‘nylas-exports’</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>{React,</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-3\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-3\">&#182;</a>\n              </div>\n              <p>The ComponentRegistry manages all React components in N1.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre> ComponentRegistry,</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-4\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-4\">&#182;</a>\n              </div>\n              <p>A <code>Store</code> is a Flux component which contains all business logic and data\nmodels to be consumed by React components to render markup.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre> MessageStore} = <span class=\"hljs-built_in\">require</span> <span class=\"hljs-string\">'nylas-exports'</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-5\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-5\">&#182;</a>\n              </div>\n              <p>Notice that this file is <code>main.cjsx</code> rather than <code>main.coffee</code>. We use the\n<code>.cjsx</code> filetype because we use the CJSX DSL to describe markup for React to\nrender. Without the CJSX, we could just name this file <code>main.coffee</code> instead.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre><span class=\"hljs-class\"><span class=\"hljs-keyword\">class</span> <span class=\"hljs-title\">PhishingIndicator</span> <span class=\"hljs-keyword\">extends</span> <span class=\"hljs-title\">React</span>.<span class=\"hljs-title\">Component</span></span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-6\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-6\">&#182;</a>\n              </div>\n              <p>Adding a @displayName to a React component helps for debugging.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-property\">@displayName</span>: <span class=\"hljs-string\">'PhishingIndicator'</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-7\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-7\">&#182;</a>\n              </div>\n              <p>@propTypes is an object which validates the datatypes of properties that\nthis React component can receive.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-property\">@propTypes</span>:\n    <span class=\"hljs-attribute\">thread</span>: React.PropTypes.object.isRequired</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-8\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-8\">&#182;</a>\n              </div>\n              <p>A React component’s <code>render</code> method returns a virtual DOM element described\nin CJSX. <code>render</code> is deterministic: with the same input, it will always\nrender the same output. Here, the input is provided by @isPhishingAttempt.\n<code>@state</code> and <code>@props</code> are popular inputs as well.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">render</span>: <span class=\"hljs-function\">=&gt;</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-9\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-9\">&#182;</a>\n              </div>\n              <p>Our inputs for the virtual DOM to render come from @isPhishingAttempt.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>    [from, reply_to] = <span class=\"hljs-property\">@isPhishingAttempt</span>()</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-10\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-10\">&#182;</a>\n              </div>\n              <p>We add some more application logic to decide how to render.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>    <span class=\"hljs-keyword\">if</span> from <span class=\"hljs-keyword\">isnt</span> <span class=\"hljs-literal\">null</span> <span class=\"hljs-keyword\">and</span> reply_to <span class=\"hljs-keyword\">isnt</span> <span class=\"hljs-literal\">null</span>\n      React.createElement(<span class=\"hljs-string\">\"div\"</span>, {<span class=\"hljs-string\">\"className\"</span>: <span class=\"hljs-string\">\"phishingIndicator\"</span>},\n        React.createElement(<span class=\"hljs-string\">\"b\"</span>, <span class=\"hljs-literal\">null</span>, <span class=\"hljs-string\">\"This message looks suspicious!\"</span>),\n        React.createElement(<span class=\"hljs-string\">\"p\"</span>, <span class=\"hljs-literal\">null</span>, <span class=\"hljs-string\">\"It originates from \"</span>, (from), <span class=\"hljs-string\">\" but replies will go to \"</span>, (reply_to), <span class=\"hljs-string\">\".\"</span>)\n      )</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-11\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-11\">&#182;</a>\n              </div>\n              <p>If you don’t want a React component to render anything at all, then your\n<code>render</code> method should return <code>null</code> or <code>undefined</code>.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>    <span class=\"hljs-keyword\">else</span>\n      <span class=\"hljs-literal\">null</span>\n\n  <span class=\"hljs-attribute\">isPhishingAttempt</span>: <span class=\"hljs-function\">=&gt;</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-12\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-12\">&#182;</a>\n              </div>\n              <p>In this package, the MessageStore is the source of our data which will be\nthe input for the <code>render</code> function. @isPhishingAttempt is performing some\ndomain-specific application logic to prepare the data for <code>render</code>.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>    message = MessageStore.items()[<span class=\"hljs-number\">0</span>]</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-13\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-13\">&#182;</a>\n              </div>\n              <p>This package’s strategy to ascertain whether or not the email is a\nphishing attempt boils down to checking the <code>replyTo</code> attributes on\n<code>Message</code> models from <code>MessageStore</code>.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>    <span class=\"hljs-keyword\">if</span> message.replyTo? <span class=\"hljs-keyword\">and</span> message.replyTo.length != <span class=\"hljs-number\">0</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-14\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-14\">&#182;</a>\n              </div>\n              <p>The <code>from</code> and <code>replyTo</code> attributes on <code>Message</code> models both refer to\narrays of <code>Contact</code> models, which in turn have <code>email</code> attributes.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>      from = message.from[<span class=\"hljs-number\">0</span>].email\n      reply_to = message.replyTo[<span class=\"hljs-number\">0</span>].email</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-15\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-15\">&#182;</a>\n              </div>\n              <p>This is our core logic for our whole package! If the <code>from</code> and\n<code>replyTo</code> emails are different, then we want to show a phishing warning.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>      <span class=\"hljs-keyword\">if</span> reply_to <span class=\"hljs-keyword\">isnt</span> from\n          <span class=\"hljs-keyword\">return</span> [from, reply_to]\n\n    <span class=\"hljs-keyword\">return</span> [<span class=\"hljs-literal\">null</span>, <span class=\"hljs-literal\">null</span>];\n\n<span class=\"hljs-built_in\">module</span>.exports =</pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-16\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-16\">&#182;</a>\n              </div>\n              <p>Activate is called when the package is loaded. If your package previously\nsaved state using <code>serialize</code> it is provided.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">activate</span>: <span class=\"hljs-function\"><span class=\"hljs-params\">(<span class=\"hljs-property\">@state</span>)</span> -&gt;</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-17\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-17\">&#182;</a>\n              </div>\n              <p>This is a good time to tell the <code>ComponentRegistry</code> to insert our\nReact component into the <code>&#39;MessageListHeaders&#39;</code> part of the application.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>    ComponentRegistry.register PhishingIndicator,\n      <span class=\"hljs-attribute\">role</span>: <span class=\"hljs-string\">'MessageListHeaders'</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-18\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-18\">&#182;</a>\n              </div>\n              <p>Serialize is called when your package is about to be unmounted.\nYou can return a state object that will be passed back to your package\nwhen it is re-activated.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">serialize</span>: <span class=\"hljs-function\">-&gt;</span></pre></div></div>\n            \n        </li>\n        \n        \n        <li id=\"section-19\">\n            <div class=\"annotation\">\n              \n              <div class=\"pilwrap \">\n                <a class=\"pilcrow\" href=\"#section-19\">&#182;</a>\n              </div>\n              <p>This <strong>optional</strong> method is called when the window is shutting down,\nor when your package is being updated or disabled. If your package is\nwatching any files, holding external resources, providing commands or\nsubscribing to events, release them here.</p>\n\n            </div>\n            \n            <div class=\"content\"><div class='highlight'><pre>  <span class=\"hljs-attribute\">deactivate</span>: <span class=\"hljs-function\">-&gt;</span>\n    ComponentRegistry.unregister(PhishingIndicator)</pre></div></div>\n            \n        </li>\n        \n    </ul>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/docs/public/stylesheets/normalize.css",
    "content": "/*! normalize.css v2.0.1 | MIT License | git.io/normalize */\n\n/* ==========================================================================\n   HTML5 display definitions\n   ========================================================================== */\n\n/*\n * Corrects `block` display not defined in IE 8/9.\n */\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nnav,\nsection,\nsummary {\n    display: block;\n}\n\n/*\n * Corrects `inline-block` display not defined in IE 8/9.\n */\n\naudio,\ncanvas,\nvideo {\n    display: inline-block;\n}\n\n/*\n * Prevents modern browsers from displaying `audio` without controls.\n * Remove excess height in iOS 5 devices.\n */\n\naudio:not([controls]) {\n    display: none;\n    height: 0;\n}\n\n/*\n * Addresses styling for `hidden` attribute not present in IE 8/9.\n */\n\n[hidden] {\n    display: none;\n}\n\n/* ==========================================================================\n   Base\n   ========================================================================== */\n\n/*\n * 1. Sets default font family to sans-serif.\n * 2. Prevents iOS text size adjust after orientation change, without disabling\n *    user zoom.\n */\n\nhtml {\n    font-family: sans-serif; /* 1 */\n    -webkit-text-size-adjust: 100%; /* 2 */\n    -ms-text-size-adjust: 100%; /* 2 */\n}\n\n/*\n * Removes default margin.\n */\n\nbody {\n    margin: 0;\n}\n\n/* ==========================================================================\n   Links\n   ========================================================================== */\n\n/*\n * Addresses `outline` inconsistency between Chrome and other browsers.\n */\n\na:focus {\n    outline: thin dotted;\n}\n\n/*\n * Improves readability when focused and also mouse hovered in all browsers.\n */\n\na:active,\na:hover {\n    outline: 0;\n}\n\n/* ==========================================================================\n   Typography\n   ========================================================================== */\n\n/*\n * Addresses `h1` font sizes within `section` and `article` in Firefox 4+,\n * Safari 5, and Chrome.\n */\n\nh1 {\n    font-size: 2em;\n}\n\n/*\n * Addresses styling not present in IE 8/9, Safari 5, and Chrome.\n */\n\nabbr[title] {\n    border-bottom: 1px dotted;\n}\n\n/*\n * Addresses style set to `bolder` in Firefox 4+, Safari 5, and Chrome.\n */\n\nb,\nstrong {\n    font-weight: bold;\n}\n\n/*\n * Addresses styling not present in Safari 5 and Chrome.\n */\n\ndfn {\n    font-style: italic;\n}\n\n/*\n * Addresses styling not present in IE 8/9.\n */\n\nmark {\n    background: #ff0;\n    color: #000;\n}\n\n\n/*\n * Corrects font family set oddly in Safari 5 and Chrome.\n */\n\ncode,\nkbd,\npre,\nsamp {\n    font-family: monospace, serif;\n    font-size: 1em;\n}\n\n/*\n * Improves readability of pre-formatted text in all browsers.\n */\n\npre {\n    white-space: pre;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n\n/*\n * Sets consistent quote types.\n */\n\nq {\n    quotes: \"\\201C\" \"\\201D\" \"\\2018\" \"\\2019\";\n}\n\n/*\n * Addresses inconsistent and variable font size in all browsers.\n */\n\nsmall {\n    font-size: 80%;\n}\n\n/*\n * Prevents `sub` and `sup` affecting `line-height` in all browsers.\n */\n\nsub,\nsup {\n    font-size: 75%;\n    line-height: 0;\n    position: relative;\n    vertical-align: baseline;\n}\n\nsup {\n    top: -0.5em;\n}\n\nsub {\n    bottom: -0.25em;\n}\n\n/* ==========================================================================\n   Embedded content\n   ========================================================================== */\n\n/*\n * Removes border when inside `a` element in IE 8/9.\n */\n\nimg {\n    border: 0;\n}\n\n/*\n * Corrects overflow displayed oddly in IE 9.\n */\n\nsvg:not(:root) {\n    overflow: hidden;\n}\n\n/* ==========================================================================\n   Figures\n   ========================================================================== */\n\n/*\n * Addresses margin not present in IE 8/9 and Safari 5.\n */\n\nfigure {\n    margin: 0;\n}\n\n/* ==========================================================================\n   Forms\n   ========================================================================== */\n\n/*\n * Define consistent border, margin, and padding.\n */\n\nfieldset {\n    border: 1px solid #c0c0c0;\n    margin: 0 2px;\n    padding: 0.35em 0.625em 0.75em;\n}\n\n/*\n * 1. Corrects color not being inherited in IE 8/9.\n * 2. Remove padding so people aren't caught out if they zero out fieldsets.\n */\n\nlegend {\n    border: 0; /* 1 */\n    padding: 0; /* 2 */\n}\n\n/*\n * 1. Corrects font family not being inherited in all browsers.\n * 2. Corrects font size not being inherited in all browsers.\n * 3. Addresses margins set differently in Firefox 4+, Safari 5, and Chrome\n */\n\nbutton,\ninput,\nselect,\ntextarea {\n    font-family: inherit; /* 1 */\n    font-size: 100%; /* 2 */\n    margin: 0; /* 3 */\n}\n\n/*\n * Addresses Firefox 4+ setting `line-height` on `input` using `!important` in\n * the UA stylesheet.\n */\n\nbutton,\ninput {\n    line-height: normal;\n}\n\n/*\n * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n *    and `video` controls.\n * 2. Corrects inability to style clickable `input` types in iOS.\n * 3. Improves usability and consistency of cursor style between image-type\n *    `input` and others.\n */\n\nbutton,\nhtml input[type=\"button\"], /* 1 */\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n    -webkit-appearance: button; /* 2 */\n    cursor: pointer; /* 3 */\n}\n\n/*\n * Re-set default cursor for disabled elements.\n */\n\nbutton[disabled],\ninput[disabled] {\n    cursor: default;\n}\n\n/*\n * 1. Addresses box sizing set to `content-box` in IE 8/9.\n * 2. Removes excess padding in IE 8/9.\n */\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n    box-sizing: border-box; /* 1 */\n    padding: 0; /* 2 */\n}\n\n/*\n * 1. Addresses `appearance` set to `searchfield` in Safari 5 and Chrome.\n * 2. Addresses `box-sizing` set to `border-box` in Safari 5 and Chrome\n *    (include `-moz` to future-proof).\n */\n\ninput[type=\"search\"] {\n    -webkit-appearance: textfield; /* 1 */\n    -moz-box-sizing: content-box;\n    -webkit-box-sizing: content-box; /* 2 */\n    box-sizing: content-box;\n}\n\n/*\n * Removes inner padding and search cancel button in Safari 5 and Chrome\n * on OS X.\n */\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n    -webkit-appearance: none;\n}\n\n/*\n * Removes inner padding and border in Firefox 4+.\n */\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n    border: 0;\n    padding: 0;\n}\n\n/*\n * 1. Removes default vertical scrollbar in IE 8/9.\n * 2. Improves readability and alignment in all browsers.\n */\n\ntextarea {\n    overflow: auto; /* 1 */\n    vertical-align: top; /* 2 */\n}\n\n/* ==========================================================================\n   Tables\n   ========================================================================== */\n\n/*\n * Remove most spacing between table cells.\n */\n\ntable {\n    border-collapse: collapse;\n    border-spacing: 0;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/lib/main.jsx",
    "content": "import {\n  React,\n  // The ComponentRegistry manages all React components in N1.\n  ComponentRegistry,\n  // A `Store` is a Flux component which contains all business logic and data\n  // models to be consumed by React components to render markup.\n  MessageStore,\n} from 'nylas-exports';\n\nconst tld = require('tld');\n\n// Notice that this file is `main.cjsx` rather than `main.coffee`. We use the\n// `.cjsx` filetype because we use the CJSX DSL to describe markup for React to\n// render. Without the CJSX, we could just name this file `main.coffee` instead.\nclass PhishingIndicator extends React.Component {\n\n  // Adding a displayName to a React component helps for debugging.\n  static displayName = 'PhishingIndicator';\n\n  constructor() {\n    super();\n    this.state = {\n      message: MessageStore.items()[0],\n    };\n  }\n  componentDidMount() {\n    this._unlisten = MessageStore.listen(this._onMessagesChanged);\n  }\n\n  componentWillUnmount() {\n    if (this._unlisten) {\n      this._unlisten();\n    }\n  }\n\n  _onMessagesChanged = () => {\n    this.setState({\n      message: MessageStore.items()[0],\n    });\n  }\n\n  // A React component's `render` method returns a virtual DOM element described\n  // in CJSX. `render` is deterministic: with the same input, it will always\n  // render the same output. Here, the input is provided by @isPhishingAttempt.\n  // `@state` and `@props` are popular inputs as well.\n  render() {\n    const {message} = this.state;\n    if (!message) {\n      return (<span />);\n    }\n\n    const {replyTo, from} = message;\n    if (!replyTo || !replyTo.length || !from || !from.length) {\n      return (<span />);\n    }\n\n    // This package's strategy to ascertain whether or not the email is a\n    // phishing attempt boils down to checking the `replyTo` attributes on\n    // `Message` models from `MessageStore`.\n    const fromEmail = from[0].email.toLowerCase();\n    const replyToEmail = replyTo[0].email.toLowerCase();\n    if (!fromEmail || !replyToEmail) {\n      return (<span />);\n    }\n\n    const fromDomain = tld.registered(fromEmail.split('@')[1] || '');\n    const replyToDomain = tld.registered(replyToEmail.split('@')[1] || '');\n    if (replyToDomain !== fromDomain) {\n      return (\n        <div className=\"phishingIndicator\">\n          <b>This message looks suspicious!</b>\n          <div className=\"description\">{`It originates from ${fromEmail} but replies will go to ${replyToEmail}.`}</div>\n        </div>\n      );\n    }\n\n    return (<span />);\n  }\n}\n\nexport function activate() {\n  ComponentRegistry.register(PhishingIndicator, {\n    role: 'MessageListHeaders',\n  });\n}\n\nexport function serialize() {\n\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(PhishingIndicator);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/package.json",
    "content": "{\n  \"name\": \"phishing-detection\",\n  \"version\": \"0.2.1\",\n  \"main\": \"./lib/main\",\n  \"isHiddenOnPluginsPage\": true,\n  \"license\": \"GPL-3.0\",\n\n  \"title\": \"Phishing Detection\",\n  \"description\": \"Get warnings when an email specifies a reply-to address which is not the from address.\",\n  \"icon\": \"./icon.png\",\n  \"isOptional\": true,\n\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/spec/main-spec.jsx",
    "content": "describe(\"Phishing Detection Indicator\", () => {\n  it(\"should exhibit some behavior\", () => {\n    expect(true).toBe(true);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/stylesheets/index.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.phishing-indicator {\n    text-align: center;\n    background-color: white;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/phishing-detection/stylesheets/phishing.less",
    "content": ".phishingIndicator {\n  display: block;\n  box-sizing: border-box;\n  -webkit-print-color-adjust: exact;\n  padding: 8px 12px;\n  margin-bottom: 5px;\n  border: 1px solid rgb(235, 204, 209);\n  border-radius: 4px;\n  color: rgb(169, 68, 66);\n  background-color: rgb(242, 222, 222);\n  white-space: nowrap;\n  overflow: hidden;\n}\n\n.phishingIndicator .description {\n  overflow: hidden;\n  text-overflow: ellipsis;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/main.jsx",
    "content": "import {PreferencesUIStore} from 'nylas-exports';\nimport PluginsView from './preferences-plugins';\n\nexport function activate() {\n  this.preferencesTab = new PreferencesUIStore.TabItem({\n    tabId: \"Plugins\",\n    displayName: \"Plugins\",\n    component: PluginsView,\n  });\n\n  PreferencesUIStore.registerPreferencesTab(this.preferencesTab);\n}\n\nexport function deactivate() {\n  PreferencesUIStore.unregisterPreferencesTab(this.preferencesTab.sectionId)\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/package-set.jsx",
    "content": "import React from 'react';\n\nimport Package from './package';\n\n\nclass PackageSet extends React.Component {\n\n  static propTypes = {\n    title: React.PropTypes.string.isRequired,\n    packages: React.PropTypes.array,\n    emptyText: React.PropTypes.element,\n    showVersions: React.PropTypes.bool,\n  }\n\n  render() {\n    if (!this.props.packages) return false;\n\n    const packages = this.props.packages.map((pkg) =>\n      <Package key={pkg.name} package={pkg} showVersions={this.props.showVersions} />\n    );\n    let count = <span>({this.props.packages.length})</span>\n\n    if (packages.length === 0) {\n      count = [];\n      packages.push(\n        <div key=\"empty\" className=\"empty\">{this.props.emptyText || \"No plugins to display.\"}</div>\n      )\n    }\n\n    return (\n      <div className=\"package-set\">\n        <h6>{this.props.title} {count}</h6>\n        {packages}\n      </div>\n    );\n  }\n\n}\n\nexport default PackageSet;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/package.jsx",
    "content": "import React from 'react';\n\nimport {Flexbox, RetinaImg, Switch} from 'nylas-component-kit';\nimport PluginsActions from './plugins-actions';\n\n\nclass Package extends React.Component {\n\n  static displayName = 'Package';\n\n  static propTypes = {\n    \"package\": React.PropTypes.object.isRequired,\n    \"showVersions\": React.PropTypes.bool,\n  }\n\n  _onDisablePackage = () => {\n    PluginsActions.disablePackage(this.props.package);\n  }\n\n  _onEnablePackage = () => {\n    PluginsActions.enablePackage(this.props.package);\n  }\n\n  _onUninstallPackage = () => {\n    PluginsActions.uninstallPackage(this.props.package);\n  }\n\n  _onUpdatePackage = () => {\n    PluginsActions.updatePackage(this.props.package);\n  }\n\n  _onInstallPackage = () => {\n    PluginsActions.installPackage(this.props.package);\n  }\n\n  _onShowPackage = () => {\n    PluginsActions.showPackage(this.props.package);\n  }\n\n  render() {\n    const actions = [];\n    const extras = [];\n    let icon = (<RetinaImg name=\"plugin-icon-default.png\" mode=\"ContentPreserve\" />);\n    let uninstallButton = null;\n\n    if (this.props.package.icon) {\n      icon = (<img src={`nylas://${this.props.package.name}/${this.props.package.icon}`} role=\"presentation\" style={{width: 27, alignContent: \"center\", objectFit: \"scale-down\"}} />);\n    } else if (this.props.package.theme) {\n      icon = (<RetinaImg name=\"theme-icon-default.png\" mode=\"ContentPreserve\" />);\n    }\n\n    if (this.props.package.installed) {\n      if (['user', 'dev', 'example'].indexOf(this.props.package.category) !== -1 && !this.props.package.theme) {\n        if (this.props.package.enabled) {\n          actions.push(<Switch key=\"disable\" checked onChange={this._onDisablePackage}>Disable</Switch>);\n        } else {\n          actions.push(<Switch key=\"enable\" onChange={this._onEnablePackage}>Enable</Switch>);\n        }\n      }\n      if (this.props.package.category === 'user') {\n        uninstallButton = <div className=\"uninstall-plugin\" onClick={this._onUninstallPackage}>Uninstall</div>\n      }\n      if (this.props.package.category === 'dev') {\n        actions.push(<div key=\"show-package\" className=\"btn\" onClick={this._onShowPackage}>Show...</div>);\n      }\n    } else if (this.props.package.installing) {\n      actions.push(<div key=\"installing\" className=\"btn\">Installing...</div>);\n    } else {\n      actions.push(<div key=\"install\" className=\"btn\" onClick={this._onInstallPackage}>Install</div>);\n    }\n\n    const {name, description, title, version} = this.props.package;\n\n    if (this.props.package.newerVersionAvailable) {\n      extras.push(\n        <div className=\"padded update-info\">\n          A newer version is available: {this.props.package.newerVersion}\n          <div className=\"btn btn-emphasis\" onClick={this._onUpdatePackage}>Update</div>\n        </div>\n      )\n    }\n\n    const versionLabel = this.props.showVersions ? `v${version}` : null;\n\n    return (\n      <Flexbox className=\"package\" direction=\"row\">\n        <div className=\"icon-container\">\n          <div className=\"icon\" >{icon}</div>\n        </div>\n        <div className=\"info\">\n          <div style={{display: \"flex\", flexDirection: \"row\"}}>\n            <div className=\"title\">{title || name} <span className=\"version\">{versionLabel}</span></div>\n            {uninstallButton}\n          </div>\n          <div className=\"description\">{description}</div>\n        </div>\n        <div className=\"actions\">{actions}</div>\n        {extras}\n      </Flexbox>\n    );\n  }\n\n}\n\nexport default Package;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/packages-store.jsx",
    "content": "import _ from 'underscore';\nimport Reflux from 'reflux';\nimport path from 'path';\nimport fs from 'fs-plus';\nimport {APMWrapper} from 'nylas-exports';\nimport {ipcRenderer, shell, remote} from 'electron';\n\nimport PluginsActions from './plugins-actions';\n\nconst dialog = remote.dialog;\n\n\nconst PackagesStore = Reflux.createStore({\n  init: function init() {\n    this._apm = new APMWrapper();\n\n    this._globalSearch = \"\";\n    this._installedSearch = \"\";\n    this._installing = {};\n    this._featured = {\n      themes: [],\n      packages: [],\n    };\n    this._newerVersions = [];\n    this._searchResults = null;\n    this._refreshFeatured();\n\n    this.listenTo(PluginsActions.refreshFeaturedPackages, this._refreshFeatured);\n    this.listenTo(PluginsActions.refreshInstalledPackages, this._refreshInstalled);\n\n    NylasEnv.commands.add(document.body,\n      'application:create-package',\n      () => this._onCreatePackage()\n    );\n\n    NylasEnv.commands.add(document.body,\n      'application:install-package',\n      () => this._onInstallPackage()\n    );\n\n    this.listenTo(PluginsActions.installNewPackage, this._onInstallPackage);\n    this.listenTo(PluginsActions.createPackage, this._onCreatePackage);\n    this.listenTo(PluginsActions.updatePackage, this._onUpdatePackage);\n    this.listenTo(PluginsActions.setGlobalSearchValue, this._onGlobalSearchChange);\n    this.listenTo(PluginsActions.setInstalledSearchValue, this._onInstalledSearchChange);\n\n    this.listenTo(PluginsActions.showPackage, (pkg) => {\n      const dir = NylasEnv.packages.resolvePackagePath(pkg.name);\n      if (dir) shell.showItemInFolder(dir);\n    });\n\n    this.listenTo(PluginsActions.installPackage, (pkg) => {\n      this._installing[pkg.name] = true;\n      this.trigger(this);\n      this._apm.install(pkg, (err) => {\n        if (err) {\n          delete this._installing[pkg.name];\n          this._displayMessage(\"Sorry, an error occurred\", err.toString());\n        } else {\n          if (NylasEnv.packages.isPackageDisabled(pkg.name)) {\n            NylasEnv.packages.enablePackage(pkg.name);\n          }\n        }\n        this._onPackagesChanged();\n      });\n    });\n\n    this.listenTo(PluginsActions.uninstallPackage, (pkg) => {\n      if (NylasEnv.packages.isPackageLoaded(pkg.name)) {\n        NylasEnv.packages.disablePackage(pkg.name);\n        NylasEnv.packages.unloadPackage(pkg.name);\n      }\n      this._apm.uninstall(pkg, (err) => {\n        if (err) this._displayMessage(\"Sorry, an error occurred\", err.toString())\n        this._onPackagesChanged();\n      })\n    });\n\n    this.listenTo(PluginsActions.enablePackage, (pkg) => {\n      if (NylasEnv.packages.isPackageDisabled(pkg.name)) {\n        NylasEnv.packages.enablePackage(pkg.name);\n        this._onPackagesChanged();\n      }\n    });\n\n    this.listenTo(PluginsActions.disablePackage, (pkg) => {\n      if (!NylasEnv.packages.isPackageDisabled(pkg.name)) {\n        NylasEnv.packages.disablePackage(pkg.name);\n        this._onPackagesChanged();\n      }\n    });\n\n    this._hasPrepared = false;\n  },\n\n  // Getters\n\n  installed: function installed() {\n    this._prepareIfFresh();\n    return this._addPackageStates(this._filter(this._installed, this._installedSearch));\n  },\n\n  installedSearchValue: function installedSearchValue() {\n    return this._installedSearch;\n  },\n\n  featured: function featured() {\n    this._prepareIfFresh();\n    return this._addPackageStates(this._featured);\n  },\n\n  searchResults: function searchResults() {\n    return this._addPackageStates(this._searchResults);\n  },\n\n  globalSearchValue: function globalSearchValue() {\n    return this._globalSearch;\n  },\n\n  // Action Handlers\n\n  _prepareIfFresh: function _prepareIfFresh() {\n    if (this._hasPrepared) return;\n    NylasEnv.packages.onDidActivatePackage(() => this._onPackagesChangedDebounced());\n    NylasEnv.packages.onDidDeactivatePackage(() => this._onPackagesChangedDebounced());\n    NylasEnv.packages.onDidLoadPackage(() => this._onPackagesChangedDebounced());\n    NylasEnv.packages.onDidUnloadPackage(() => this._onPackagesChangedDebounced());\n    this._onPackagesChanged();\n    this._hasPrepared = true;\n  },\n\n  _filter: function _filter(hash, search) {\n    const result = {}\n    const query = search.toLowerCase();\n    if (hash) {\n      Object.keys(hash).forEach((key) => {\n        result[key] = _.filter(hash[key], (p) =>\n          query.length === 0 || p.name.toLowerCase().indexOf(query) !== -1\n        );\n      });\n    }\n    return result;\n  },\n\n  _refreshFeatured: function _refreshFeatured() {\n    this._apm.getFeatured({themes: false})\n    .then((results) => {\n      this._featured.packages = results;\n      this.trigger();\n    })\n    .catch(() => {\n      // We may be offline\n    });\n    this._apm.getFeatured({themes: true})\n    .then((results) => {\n      this._featured.themes = results;\n      this.trigger();\n    })\n    .catch(() => {\n      // We may be offline\n    });\n  },\n\n  _refreshInstalled: function _refreshInstalled() {\n    this._onPackagesChanged();\n  },\n\n  _refreshSearch: function _refreshSearch() {\n    if (!this._globalSearch || this._globalSearch.length <= 0) return;\n\n    this._apm.search(this._globalSearch)\n    .then((results) => {\n      this._searchResults = {\n        packages: results.filter(({theme}) => !theme),\n        themes: results.filter(({theme}) => theme),\n      }\n      this.trigger();\n    })\n    .catch(() => {\n      // We may be offline\n    });\n  },\n\n  _refreshSearchThrottled: function _refreshSearchThrottled() {\n    _.debounce(this._refreshSearch, 400)\n  },\n\n  _onPackagesChanged: function _onPackagesChanged() {\n    this._apm.getInstalled()\n    .then((packages) => {\n      for (const category of ['dev', 'user']) {\n        packages[category].forEach((pkg) => {\n          pkg.category = category;\n          delete this._installing[pkg.name];\n        });\n      }\n\n      const available = NylasEnv.packages.getAvailablePackageMetadata();\n      const examples = available.filter(({isOptional, isHiddenOnPluginsPage}) =>\n        isOptional && !isHiddenOnPluginsPage);\n      packages.example = examples.map((pkg) =>\n        _.extend({}, pkg, {installed: true, category: 'example'})\n      );\n      this._installed = packages;\n      this.trigger();\n    });\n  },\n\n  _onPackagesChangedDebounced: function _onPackagesChangedDebounced() {\n    _.debounce(this._onPackagesChanged, 200);\n  },\n\n  _onInstalledSearchChange: function _onInstalledSearchChange(val) {\n    this._installedSearch = val;\n    this.trigger();\n  },\n\n  _onUpdatePackage: function _onUpdatePackage(pkg) {\n    this._apm.update(pkg, pkg.newerVersion);\n  },\n\n  _onInstallPackage: function _onInstallPackage() {\n    NylasEnv.showOpenDialog({\n      title: \"Choose a Plugin Directory\",\n      buttonLabel: 'Choose',\n      properties: ['openDirectory'],\n    },\n    (filenames) => {\n      if (!filenames || filenames.length === 0) return;\n      NylasEnv.packages.installPackageFromPath(filenames[0], (err, packageName) => {\n        if (err) {\n          this._displayMessage(\"Could not install plugin\", err.message);\n        } else {\n          this._onPackagesChanged();\n          const msg = `${packageName} has been installed and enabled. No need to restart! If you don't see the plugin loaded, check the console for errors.`\n          this._displayMessage(\"Plugin installed! 🎉\", msg);\n        }\n      });\n    });\n  },\n\n  _onCreatePackage: function _onCreatePackage() {\n    if (!NylasEnv.inDevMode()) {\n      const btn = dialog.showMessageBox({\n        type: 'warning',\n        message: \"Run with debug flags?\",\n        detail: `To develop plugins, you should run N1 with debug flags. This gives you better error messages, the debug version of React, and more. You can disable it at any time from the Developer menu.`,\n        buttons: [\"OK\", \"Cancel\"],\n      });\n      if (btn === 0) {\n        ipcRenderer.send('command', 'application:toggle-dev');\n      }\n      return;\n    }\n\n    const packagesDir = path.join(NylasEnv.getConfigDirPath(), 'dev', 'packages');\n    fs.makeTreeSync(packagesDir);\n\n    NylasEnv.showSaveDialog({\n      title: \"Save New Package\",\n      defaultPath: packagesDir,\n      properties: ['createDirectory'],\n    }, (packageDir) => {\n      if (!packageDir) return;\n\n      const packageName = path.basename(packageDir);\n\n      if (!packageDir.startsWith(packagesDir)) {\n        this._displayMessage('Invalid plugin location',\n          'Sorry, you must create plugins in the packages folder.');\n      }\n\n      if (NylasEnv.packages.resolvePackagePath(packageName)) {\n        this._displayMessage('Invalid plugin name',\n          'Sorry, you must give your plugin a unique name.');\n      }\n\n      if (packageName.indexOf(' ') !== -1) {\n        this._displayMessage('Invalid plugin name',\n          'Sorry, plugin names cannot contain spaces.');\n      }\n\n      fs.mkdir(packageDir, (err) => {\n        if (err) {\n          this._displayMessage('Could not create plugin', err.toString());\n          return;\n        }\n        const {resourcePath} = NylasEnv.getLoadSettings();\n        const packageTemplatePath = path.join(resourcePath, 'static', 'package-template');\n        const packageJSON = {\n          name: packageName,\n          main: \"./lib/main\",\n          version: '0.1.0',\n          repository: {\n            type: 'git',\n            url: '',\n          },\n          engines: {\n            nylas: `>=${NylasEnv.getVersion().split('-')[0]}`,\n          },\n          windowTypes: {\n            'default': true,\n            'composer': true,\n          },\n          description: \"Enter a description of your package!\",\n          dependencies: {},\n          license: \"MIT\",\n        };\n\n        fs.copySync(packageTemplatePath, packageDir);\n        fs.writeFileSync(path.join(packageDir, 'package.json'), JSON.stringify(packageJSON, null, 2));\n        shell.showItemInFolder(packageDir);\n        _.defer(() => {\n          NylasEnv.packages.enablePackage(packageDir);\n          NylasEnv.packages.activatePackage(packageName);\n        });\n      });\n    });\n  },\n\n  _onGlobalSearchChange: function _onGlobalSearchChange(val) {\n    // Clear previous search results data if this is a new\n    // search beginning from \"\".\n    if (this._globalSearch.length === 0 && val.length > 0) {\n      this._searchResults = null;\n    }\n\n    this._globalSearch = val;\n    this._refreshSearchThrottled();\n    this.trigger();\n  },\n\n  _addPackageStates: function _addPackageStates(pkgs) {\n    const installedNames = _.flatten(_.values(this._installed)).map((pkg) => pkg.name);\n\n    _.flatten(_.values(pkgs)).forEach((pkg) => {\n      pkg.enabled = !NylasEnv.packages.isPackageDisabled(pkg.name);\n      pkg.installed = installedNames.indexOf(pkg.name) !== -1;\n      pkg.installing = this._installing[pkg.name];\n      pkg.newerVersionAvailable = this._newerVersions[pkg.name];\n      pkg.newerVersion = this._newerVersions[pkg.name];\n    });\n\n    return pkgs;\n  },\n\n  _displayMessage: function _displayMessage(title, message) {\n    dialog.showMessageBox({\n      type: 'warning',\n      message: title,\n      detail: message,\n      buttons: [\"OK\"],\n    });\n  },\n\n});\n\nexport default PackagesStore;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/plugins-actions.jsx",
    "content": "import Reflux from 'reflux';\n\nconst Actions = Reflux.createActions([\n  'selectTabIndex',\n  'setInstalledSearchValue',\n  'setGlobalSearchValue',\n\n  'disablePackage',\n  'enablePackage',\n  'installPackage',\n  'installNewPackage',\n  'uninstallPackage',\n  'createPackage',\n  'reloadPackage',\n  'showPackage',\n  'updatePackage',\n\n  'refreshFeaturedPackages',\n  'refreshInstalledPackages',\n]);\n\nfor (const key of Object.keys(Actions)) {\n  Actions[key].sync = true;\n}\n\nexport default Actions;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/plugins-tabs-view.jsx",
    "content": "import React from 'react';\nimport classNames from 'classnames';\n\nimport Tabs from './tabs';\nimport TabsStore from './tabs-store';\nimport PluginsActions from './plugins-actions';\n\n\nclass PluginsTabs extends React.Component {\n\n  static displayName = 'PluginsTabs';\n\n  static propTypes = {\n    onChange: React.PropTypes.Func,\n  };\n\n  static containerRequired = false;\n\n  static containerStyles = {\n    minWidth: 200,\n    maxWidth: 290,\n  };\n\n  constructor() {\n    super();\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsubscribers = [];\n    this._unsubscribers.push(TabsStore.listen(this._onChange));\n  }\n\n  componentWillUnmount() {\n    this._unsubscribers.forEach(unsubscribe => unsubscribe());\n  }\n\n  _getStateFromStores() {\n    return {\n      tabIndex: TabsStore.tabIndex(),\n    };\n  }\n\n  _onChange = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  _renderItems() {\n    return Tabs.map(({name, key, icon}, idx) => {\n      const classes = classNames({\n        tab: true,\n        active: idx === this.state.tabIndex,\n      });\n      return (<li key={key} className={classes} onClick={() => PluginsActions.selectTabIndex(idx)}>{name}</li>);\n    });\n  }\n\n  render() {\n    return (\n      <ul className=\"plugins-view-tabs\">\n        {this._renderItems()}\n      </ul>\n    );\n  }\n\n}\n\nexport default PluginsTabs;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/preferences-plugins.jsx",
    "content": "import React from 'react';\n\nimport TabsStore from './tabs-store';\nimport Tabs from './tabs';\n\n\nclass PluginsView extends React.Component {\n\n  static displayName = 'PluginsView';\n\n  static containerStyles = {\n    minWidth: 500,\n    maxWidth: 99999,\n  }\n\n  constructor() {\n    super();\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsubscribers = [];\n    this._unsubscribers.push(TabsStore.listen(this._onChange));\n  }\n\n  componentWillUnmount() {\n    this._unsubscribers.forEach(unsubscribe => unsubscribe());\n  }\n\n  _getStateFromStores() {\n    return {tabIndex: TabsStore.tabIndex()};\n  }\n\n  _onChange = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  render() {\n    const PluginsTabComponent = Tabs[this.state.tabIndex].component;\n    return (\n      <div className=\"plugins-view\">\n        <PluginsTabComponent />\n      </div>\n    );\n  }\n\n}\n\nexport default PluginsView;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/tab-explore.jsx",
    "content": "import React from 'react';\n\nimport PackageSet from './package-set';\nimport PackagesStore from './packages-store';\nimport PluginsActions from './plugins-actions';\n\n\nclass TabExplore extends React.Component {\n\n  static displayName = 'TabExplore';\n\n  constructor() {\n    super();\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsubscribers = [];\n    this._unsubscribers.push(PackagesStore.listen(this._onChange));\n\n    // Trigger a refresh of the featured packages\n    PluginsActions.refreshFeaturedPackages()\n  }\n\n  componentWillUnmount() {\n    this._unsubscribers.forEach(unsubscribe => unsubscribe());\n  }\n\n  _getStateFromStores() {\n    return {\n      featured: PackagesStore.featured(),\n      search: PackagesStore.globalSearchValue(),\n      searchResults: PackagesStore.searchResults(),\n    };\n  }\n\n  _onChange = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  _onSearchChange = (event) => {\n    PluginsActions.setGlobalSearchValue(event.target.value);\n  }\n\n  render() {\n    let collection = this.state.featured;\n    let collectionPrefix = \"Featured \";\n    let emptyText = null;\n    if (this.state.search.length > 0) {\n      collectionPrefix = \"Matching \";\n      if (this.state.searchResults) {\n        collection = this.state.searchResults;\n        emptyText = \"No results found.\";\n      } else {\n        collection = {\n          packages: [],\n          themes: [],\n        };\n        emptyText = \"Loading results...\";\n      }\n    }\n\n    return (\n      <div className=\"explore\">\n        <div className=\"inner\">\n          <input\n            type=\"text\"\n            className=\"search\"\n            value={this.state.search}\n            onChange={this._onSearchChange}\n            placeholder=\"Search Packages and Themes\"\n          />\n          <PackageSet\n            title={`${collectionPrefix} Themes`}\n            emptyText={emptyText || \"There are no featured themes yet.\"}\n            packages={collection.themes}\n          />\n          <PackageSet\n            title={`${collectionPrefix} Packages`}\n            emptyText={emptyText || \"There are no featured packages yet.\"}\n            packages={collection.packages}\n          />\n        </div>\n      </div>\n    );\n  }\n\n}\n\nexport default TabExplore;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/tab-installed.jsx",
    "content": "import React from 'react';\nimport {ipcRenderer} from 'electron';\nimport {Flexbox} from 'nylas-component-kit';\n\nimport PackageSet from './package-set';\nimport PackagesStore from './packages-store';\nimport PluginsActions from './plugins-actions';\n\n\nclass TabInstalled extends React.Component {\n\n  static displayName = 'TabInstalled';\n\n  constructor() {\n    super();\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsubscribers = [];\n    this._unsubscribers.push(PackagesStore.listen(this._onChange));\n\n    PluginsActions.refreshInstalledPackages();\n  }\n\n  componentWillUnmount() {\n    this._unsubscribers.forEach(unsubscribe => unsubscribe());\n  }\n\n  _getStateFromStores() {\n    return {\n      packages: PackagesStore.installed(),\n      search: PackagesStore.installedSearchValue(),\n    };\n  }\n\n  _onChange = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  _onInstallPackage() {\n    PluginsActions.installNewPackage();\n  }\n\n  _onCreatePackage() {\n    PluginsActions.createPackage();\n  }\n\n  _onSearchChange = (event) => {\n    PluginsActions.setInstalledSearchValue(event.target.value);\n  }\n\n  _onEnableDevMode() {\n    ipcRenderer.send('command', 'application:toggle-dev');\n  }\n\n  render() {\n    let searchEmpty = null;\n    if (this.state.search.length > 0) {\n      searchEmpty = \"No matching packages.\";\n    }\n\n    let devPackages = []\n    let devEmpty = (<span>Run with debug flags enabled to load ~/.nylas-mail/dev/packages.</span>);\n    let devCTA = (<div className=\"btn btn-small\" onClick={this._onEnableDevMode}>Enable Debug Flags</div>);\n\n    if (NylasEnv.inDevMode()) {\n      devPackages = this.state.packages.dev || [];\n      devEmpty = (<span>\n        {`You don't have any packages installed in ~/.nylas-mail/dev/packages. `}\n        These plugins are only loaded when you run the app with debug flags\n        enabled (via the Developer menu).<br /><br />Learn more about building\n        plugins with <a href=\"https://nylas.github.io/N1/docs/\">our docs</a>.\n      </span>);\n      devCTA = (<div className=\"btn btn-small\" onClick={this._onCreatePackage}>Create New Plugin...</div>);\n    }\n\n    return (\n      <div className=\"installed\">\n        <div className=\"inner\">\n          <Flexbox className=\"search-container\">\n            <div className=\"btn btn-small\" onClick={this._onInstallPackage}>Install Plugin...</div>\n            <input\n              type=\"text\"\n              className=\"search\"\n              value={this.state.search}\n              onChange={this._onSearchChange}\n              placeholder=\"Search Installed Plugins\"\n            />\n          </Flexbox>\n          <PackageSet\n            packages={this.state.packages.user}\n            showVersions\n            title=\"Installed plugins\"\n            emptyText={searchEmpty || <span>{`You don't have any plugins installed in ~/.nylas-mail/packages.`}</span>}\n          />\n          <PackageSet\n            title=\"Built-in plugins\"\n            packages={this.state.packages.example}\n          />\n          <PackageSet\n            title=\"Development plugins\"\n            packages={devPackages}\n            emptyText={searchEmpty || devEmpty}\n          />\n          <div className=\"new-package\">\n            {devCTA}\n          </div>\n        </div>\n      </div>\n    );\n  }\n\n}\n\nexport default TabInstalled;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/tabs-store.jsx",
    "content": "import Reflux from 'reflux';\n\nimport PluginsActions from './plugins-actions';\n\n\nconst TabsStore = Reflux.createStore({\n\n  init: function init() {\n    this._tabIndex = 0;\n    this.listenTo(PluginsActions.selectTabIndex, this._onTabIndexChanged);\n  },\n\n  // Getters\n\n  tabIndex: function tabIndex() {\n    return this._tabIndex;\n  },\n\n  // Action Handlers\n\n  _onTabIndexChanged: function _onTabIndexChanged(idx) {\n    this._tabIndex = idx;\n    this.trigger(this);\n  },\n\n});\n\nexport default TabsStore;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/lib/tabs.jsx",
    "content": "import TabInstalled from './tab-installed';\n\nconst Tabs = [{\n  key: 'installed',\n  name: 'Installed',\n  icon: 'tbd',\n  component: TabInstalled,\n}]\n\nexport default Tabs;\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/package.json",
    "content": "{\n  \"name\": \"plugins\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Plugins\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/plugins/stylesheets/plugins.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.plugins-view-tabs {\n  color:  @text-color-subtle;\n  list-style-type: none;\n  padding-left:0;\n  cursor: default;\n\n  li {\n    padding: @padding-large-vertical @padding-large-horizontal;\n    border-bottom: 1px solid @border-color-divider;\n\n    &.active {\n      background: @source-list-active-bg;\n      color: @source-list-active-color;\n      img.colorfill {\n        background: @source-list-active-color;\n      }\n    }\n  }\n}\n\n.plugins-view {\n  max-width: 800px;\n  margin: 0 auto;\n  .new-package {\n    margin-bottom: 50px;\n  }\n\n  .installed, .explore {\n    overflow-y: scroll;\n    padding-left: @padding-large-horizontal;\n    height: 100%;\n\n    .inner {\n      max-width: 800px;\n      .search-container {\n        margin: @padding-large-vertical 2px;\n        justify-content: space-between;\n      }\n    }\n\n    input {\n      box-sizing: border-box;\n      width: 30%;\n    }\n    .search {\n      padding-left: 0;\n      background-repeat: no-repeat;\n      background-image: url(\"../static/images/search/searchloupe@2x.png\");\n      background-size: 15px 15px;\n      background-position: 7px 4px;\n      text-indent: 31px;\n    }\n    .empty {\n      color: @text-color-very-subtle;\n      margin-bottom: @padding-large-vertical * 2;\n    }\n  }\n\n  .package-set {\n    margin-top: 35px;\n  }\n  .package {\n    align-items: center;\n    background: @background-primary;\n    border: 1px solid @border-color-divider;\n    border-radius: @border-radius-large;\n    margin-top: @padding-large-vertical;\n    margin-bottom: @padding-large-vertical;\n    padding: @padding-large-vertical @padding-large-horizontal;\n\n    .icon-container {\n      width: 52px;\n      height: 52px;\n      border-radius: 6px;\n      background: linear-gradient(to bottom, @background-primary 0%, @background-secondary 100%);\n      box-shadow: 0 0.5px 0 rgba(0,0,0,0.15), 0 -0.5px 0 rgba(0,0,0,0.15), 0.5px 0 0 rgba(0,0,0,0.15), -0.5px 0 0 rgba(0,0,0,0.15), 0 0.5px 1px rgba(0, 0, 0, 0.15);\n      flex-shrink: 0;\n      margin-right: @padding-large-horizontal;\n      text-align: center;\n      line-height: 50px;\n    }\n    .info {\n      max-width: 380px;\n      cursor: default;\n\n      .title {\n        color: @text-color-heading;\n        font-size: @font-size-h4;\n        font-weight: @font-weight-normal;\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n      .version {\n        font-size: @font-size-small;\n        font-weight: @font-weight-normal;\n        margin-left: 10px;\n        margin-top: 4px;\n      }\n      .uninstall-plugin {\n        color: @text-color-link;\n        margin-left: 10px;\n        margin-top: 4px;\n      }\n      .description {\n        padding-top:@padding-base-vertical;\n        color: @text-color-very-subtle;\n        font-size: @font-size-small;\n      }\n    }\n    .actions {\n      flex: 1;\n      text-align: right;\n      .btn {\n        margin-left:@padding-small-horizontal;\n      }\n    }\n    .update-info {\n      background: fade(@accent-primary, 10%);\n      line-height: @line-height-computed * 1.1;\n      .btn {\n        float: right;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/main.jsx",
    "content": "import {PreferencesUIStore,\n  WorkspaceStore,\n  ComponentRegistry} from 'nylas-exports';\n\nimport PreferencesRoot from './preferences-root';\nimport PreferencesGeneral from './tabs/preferences-general';\nimport PreferencesAccounts from './tabs/preferences-accounts';\nimport PreferencesAppearance from './tabs/preferences-appearance';\nimport PreferencesKeymaps from './tabs/preferences-keymaps';\n// import PreferencesMailRules from './tabs/preferences-mail-rules';\n\nexport function activate() {\n  PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({\n    tabId: 'General',\n    displayName: 'General',\n    component: PreferencesGeneral,\n    order: 1,\n  }))\n  PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({\n    tabId: 'Accounts',\n    displayName: 'Accounts',\n    component: PreferencesAccounts,\n    order: 2,\n  }))\n  PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({\n    tabId: 'Appearance',\n    displayName: 'Appearance',\n    component: PreferencesAppearance,\n    order: 4,\n  }))\n  PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({\n    tabId: 'Shortcuts',\n    displayName: 'Shortcuts',\n    component: PreferencesKeymaps,\n    order: 5,\n  }))\n  // PreferencesUIStore.registerPreferencesTab(new PreferencesUIStore.TabItem({\n  //   tabId: 'Mail Rules',\n  //   displayName: 'Mail Rules',\n  //   component: PreferencesMailRules,\n  //   order: 6,\n  // }))\n\n  WorkspaceStore.defineSheet('Preferences', {}, {\n    split: ['Preferences'],\n    list: ['Preferences'],\n  });\n\n  ComponentRegistry.register(PreferencesRoot, {\n    location: WorkspaceStore.Location.Preferences,\n  });\n}\n\nexport function deactivate() {\n}\n\nexport function serialize() {\n  return this.state;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/preferences-root.jsx",
    "content": "/* eslint jsx-a11y/tabindex-no-positive: 0 */\nimport React, {PropTypes} from 'react';\nimport ReactDOM from 'react-dom';\nimport {\n  Flexbox,\n  ScrollRegion,\n  KeyCommandsRegion,\n  ListensToFluxStore,\n  ConfigPropContainer,\n} from 'nylas-component-kit';\nimport {PreferencesUIStore} from 'nylas-exports';\nimport PreferencesTabsBar from './preferences-tabs-bar';\n\n\nclass PreferencesRoot extends React.Component {\n  static displayName = 'PreferencesRoot';\n\n  static containerRequired = false;\n\n  static propTypes = {\n    tab: PropTypes.object,\n    tabs: PropTypes.object,\n    selection: PropTypes.object,\n  }\n\n  componentDidMount() {\n    ReactDOM.findDOMNode(this).focus();\n    this._focusContent();\n  }\n\n  componentDidUpdate(oldProps) {\n    if (oldProps.tab !== this.props.tab) {\n      const scrollRegion = document.querySelector(\".preferences-content .scroll-region-content\");\n      scrollRegion.scrollTop = 0;\n      this._focusContent();\n    }\n  }\n\n  _localHandlers() {\n    const stopPropagation = (e) => {\n      e.stopPropagation();\n    }\n    // This prevents some basic commands from propagating to the threads list and\n    // producing unexpected results\n\n    // TODO This is a partial/temporary solution and should go away when we do the\n    // Keymap/Commands/Menu refactor\n    return {\n      'core:next-item': stopPropagation,\n      'core:previous-item': stopPropagation,\n      'core:select-up': stopPropagation,\n      'core:select-down': stopPropagation,\n      'core:select-item': stopPropagation,\n      'core:messages-page-up': stopPropagation,\n      'core:messages-page-down': stopPropagation,\n      'core:list-page-up': stopPropagation,\n      'core:list-page-down': stopPropagation,\n      'core:remove-from-view': stopPropagation,\n      'core:gmail-remove-from-view': stopPropagation,\n      'core:remove-and-previous': stopPropagation,\n      'core:remove-and-next': stopPropagation,\n      'core:archive-item': stopPropagation,\n      'core:delete-item': stopPropagation,\n      'core:print-thread': stopPropagation,\n    }\n  }\n\n  // Focus the first thing with a tabindex when we update.\n  // inside the content area. This makes it way easier to interact with prefs.\n  _focusContent() {\n    const node = ReactDOM.findDOMNode(this.refs.content).querySelector('[tabindex]')\n    if (node) {\n      node.focus();\n    }\n  }\n\n  render() {\n    const {tab, selection, tabs} = this.props\n\n    return (\n      <KeyCommandsRegion className=\"preferences-wrap\" tabIndex=\"1\" localHandlers={this._localHandlers()}>\n        <Flexbox direction=\"column\">\n          <PreferencesTabsBar\n            tabs={tabs}\n            selection={selection}\n          />\n          <ScrollRegion className=\"preferences-content\">\n            <ConfigPropContainer ref=\"content\">\n              {tab ?\n                <tab.component accountId={selection.get('accountId')} /> :\n                false\n              }\n            </ConfigPropContainer>\n          </ScrollRegion>\n        </Flexbox>\n      </KeyCommandsRegion>\n    );\n  }\n\n}\n\nexport default ListensToFluxStore(PreferencesRoot, {\n  stores: [PreferencesUIStore],\n  getStateFromStores() {\n    const tabs = PreferencesUIStore.tabs();\n    const selection = PreferencesUIStore.selection();\n    const tabId = selection.get('tabId');\n    const tab = tabs.find((s) => s.tabId === tabId);\n    return {tabs, selection, tab}\n  },\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/preferences-tabs-bar.jsx",
    "content": "import React from 'react';\nimport fs from 'fs'\nimport Immutable from 'immutable';\nimport classNames from 'classnames';\n\nimport {Flexbox, RetinaImg} from 'nylas-component-kit';\nimport {Actions, PreferencesUIStore, Utils} from 'nylas-exports';\n\n\nclass PreferencesTabItem extends React.Component {\n  static displayName = 'PreferencesTabItem';\n\n  static propTypes = {\n    selection: React.PropTypes.instanceOf(Immutable.Map).isRequired,\n    tabItem: React.PropTypes.instanceOf(PreferencesUIStore.TabItem).isRequired,\n  }\n\n  _onClick = () => {\n    Actions.switchPreferencesTab(this.props.tabItem.tabId);\n  }\n\n  _onClickAccount = (event, accountId) => {\n    Actions.switchPreferencesTab(this.props.tabItem.tabId, {accountId});\n    event.stopPropagation();\n  }\n\n  render() {\n    const {selection, tabItem} = this.props\n    const {tabId, displayName} = tabItem;\n    const classes = classNames({\n      item: true,\n      active: tabId === selection.get('tabId'),\n    });\n\n    let path = `icon-preferences-${displayName.toLowerCase().replace(\" \", \"-\")}.png`\n    if (!fs.existsSync(Utils.imageNamed(path))) {\n      path = \"icon-preferences-general.png\";\n    }\n    const icon = (\n      <RetinaImg\n        className=\"tab-icon\"\n        name={path}\n        mode={RetinaImg.Mode.ContentPreserve}\n      />\n    );\n\n    return (\n      <div className={classes} onClick={this._onClick}>\n        {icon}\n        <div className=\"name\">{displayName}</div>\n      </div>\n    );\n  }\n\n}\n\n\nclass PreferencesTabsBar extends React.Component {\n  static displayName = 'PreferencesTabsBar';\n\n  static propTypes = {\n    tabs: React.PropTypes.instanceOf(Immutable.List).isRequired,\n    selection: React.PropTypes.instanceOf(Immutable.Map).isRequired,\n  }\n\n  renderTabs() {\n    return this.props.tabs.map((tabItem) =>\n      <PreferencesTabItem\n        key={tabItem.tabId}\n        tabItem={tabItem}\n        selection={this.props.selection}\n      />\n    );\n  }\n\n  render() {\n    return (\n      <div className=\"container-preference-tabs\">\n        <Flexbox direction=\"row\" className=\"preferences-tabs\">\n          <div style={{flex: 1}} />\n          {this.renderTabs()}\n          <div style={{flex: 1}} />\n        </Flexbox>\n      </div>\n    );\n  }\n\n}\n\nexport default PreferencesTabsBar;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/config-schema-item.jsx",
    "content": "import React from 'react';\nimport _ from 'underscore';\nimport _str from 'underscore.string';\n\n/*\nThis component renders input controls for a subtree of the N1 config-schema\nand reads/writes current values using the `config` prop, which is expected to\nbe an instance of the config provided by `ConfigPropContainer`.\n\nThe config schema follows the JSON Schema standard: http://json-schema.org/\n*/\nclass ConfigSchemaItem extends React.Component {\n\n  static displayName = 'ConfigSchemaItem';\n\n  static propTypes = {\n    config: React.PropTypes.object,\n    configSchema: React.PropTypes.object,\n    keyName: React.PropTypes.string,\n    keyPath: React.PropTypes.string,\n  };\n\n  _appliesToPlatform() {\n    if (!this.props.configSchema.platform) {\n      return true;\n    } else if (this.props.configSchema.platforms.indexOf(process.platform) !== -1) {\n      return true;\n    }\n    return false;\n  }\n\n  _onChangeChecked = (event) => {\n    this.props.config.toggle(this.props.keyPath);\n    event.target.blur();\n  }\n\n  _onChangeValue = (event) => {\n    this.props.config.set(this.props.keyPath, event.target.value);\n    event.target.blur();\n  }\n\n  render() {\n    if (!this._appliesToPlatform()) return false;\n\n    // In the future, we may add an option to reveal \"advanced settings\"\n    if (this.props.configSchema.advanced) return false;\n\n    if (this.props.configSchema.type === 'object') {\n      return (\n        <section>\n          <h6>{_str.humanize(this.props.keyName)}</h6>\n          {_.pairs(this.props.configSchema.properties).map(([key, value]) =>\n            <ConfigSchemaItem\n              key={key}\n              keyName={key}\n              keyPath={`${this.props.keyPath}.${key}`}\n              configSchema={value}\n              config={this.props.config}\n            />\n          )}\n        </section>\n      );\n    } else if (this.props.configSchema.enum) {\n      return (\n        <div className=\"item\">\n          <label htmlFor={this.props.keyPath}>{this.props.configSchema.title}:</label>\n          <select onChange={this._onChangeValue} value={this.props.config.get(this.props.keyPath)}>\n            {_.zip(this.props.configSchema.enum, this.props.configSchema.enumLabels).map(([value, label]) =>\n              <option key={value} value={value}>{label}</option>\n            )}\n          </select>\n        </div>\n      );\n    } else if (this.props.configSchema.type === 'boolean') {\n      return (\n        <div className=\"item\">\n          <input\n            id={this.props.keyPath}\n            type=\"checkbox\"\n            onChange={this._onChangeChecked}\n            checked={this.props.config.get(this.props.keyPath)}\n          />\n          <label htmlFor={this.props.keyPath}>{this.props.configSchema.title}</label>\n        </div>\n      );\n    }\n    return (\n      <span />\n    );\n  }\n\n}\n\nexport default ConfigSchemaItem;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/keymaps/command-item.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport _ from 'underscore';\nimport { Flexbox } from 'nylas-component-kit';\nimport fs from 'fs';\n\nimport {keyAndModifiersForEvent} from './mousetrap-keybinding-helpers';\n\nexport default class CommandKeybinding extends React.Component {\n  static propTypes = {\n    bindings: React.PropTypes.array,\n    label: React.PropTypes.string,\n    command: React.PropTypes.string,\n  }\n\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      editing: false,\n    }\n  }\n  componentDidUpdate() {\n    const {modifiers, keys, editing} = this.state;\n    if (editing) {\n      const finished = (((modifiers.length > 0) && (keys.length > 0)) || (keys.length >= 2));\n      if (finished) {\n        ReactDOM.findDOMNode(this).blur();\n      }\n    }\n  }\n\n  _formatKeystrokes(original) {\n    // On Windows, display cmd-shift-c\n    if (process.platform === \"win32\") return original;\n\n    // Replace \"cmd\" => ⌘, etc.\n    const modifiers = [\n      [/\\+(?!$)/gi, ''],\n      [/command/gi, '⌘'],\n      [/meta/gi, '⌘'],\n      [/alt/gi, '⌥'],\n      [/shift/gi, '⇧'],\n      [/ctrl/gi, '^'],\n      [/mod/gi, (process.platform === 'darwin' ? '⌘' : '^')],\n    ];\n    let clean = original;\n    for (const [regexp, char] of modifiers) {\n      clean = clean.replace(regexp, char);\n    }\n\n    // ⌘⇧c => ⌘⇧C\n    if (clean !== original) {\n      clean = clean.toUpperCase();\n    }\n\n    // backspace => Backspace\n    if (original.length > 1 && clean === original) {\n      clean = clean[0].toUpperCase() + clean.slice(1);\n    }\n    return clean;\n  }\n\n  _renderKeystrokes = (keystrokes, idx) => {\n    const elements = [];\n    const splitKeystrokes = keystrokes.split(' ');\n    splitKeystrokes.forEach((keystroke, kidx) => {\n      elements.push(<span key={kidx}>{this._formatKeystrokes(keystroke)}</span>);\n      if (kidx < splitKeystrokes.length - 1) {\n        elements.push(<span className=\"then\" key={`then${kidx}`}> then </span>);\n      }\n    });\n    return (\n      <span key={`keystrokes-${idx}`} className=\"shortcut-value\">{elements}</span>\n    );\n  }\n\n  _onEdit = () => {\n    this.setState({editing: true, editingBinding: null, keys: [], modifiers: []});\n    NylasEnv.keymaps.suspendAllKeymaps();\n  }\n\n  _onFinishedEditing = () => {\n    if (this.state.editingBinding) {\n      const keymapPath = NylasEnv.keymaps.getUserKeymapPath();\n      let keymaps = {};\n\n      try {\n        const exists = fs.existsSync(keymapPath);\n        if (exists) {\n          keymaps = JSON.parse(fs.readFileSync(keymapPath));\n        }\n      } catch (err) {\n        console.error(err);\n      }\n\n      keymaps[this.props.command] = this.state.editingBinding;\n\n      try {\n        fs.writeFileSync(keymapPath, JSON.stringify(keymaps, null, 2));\n      } catch (err) {\n        NylasEnv.showErrorDialog(`Nylas was unable to modify your keymaps at ${keymapPath}. ${err.toString()}`);\n      }\n    }\n    this.setState({editing: false, editingBinding: null});\n    NylasEnv.keymaps.resumeAllKeymaps();\n  }\n\n  _onKey = (event) => {\n    if (!this.state.editing) {\n      return;\n    }\n\n    event.preventDefault();\n    event.stopPropagation();\n\n    const [eventKey, eventMods] = keyAndModifiersForEvent(event);\n    if (!eventKey || ['mod', 'meta', 'command', 'ctrl', 'alt', 'shift'].includes(eventKey)) {\n      return;\n    }\n\n    let {keys, modifiers} = this.state;\n    keys = keys.concat([eventKey]);\n    modifiers = _.uniq(modifiers.concat(eventMods));\n\n    let editingBinding = keys.join(' ');\n    if (modifiers.length > 0) {\n      editingBinding = [].concat(modifiers, keys).join('+');\n      editingBinding = editingBinding.replace(/(meta|command|ctrl)/g, 'mod');\n    }\n\n    this.setState({keys, modifiers, editingBinding});\n  }\n\n  render() {\n    const {editing, editingBinding} = this.state;\n    const bindings = editingBinding ? [editingBinding] : this.props.bindings;\n\n    let value = \"None\";\n    if (bindings.length > 0) {\n      value = _.uniq(bindings).map(this._renderKeystrokes);\n    }\n\n    let classnames = \"shortcut\";\n    if (editing) {\n      classnames += \" editing\";\n    }\n    return (\n      <Flexbox\n        className={classnames}\n        tabIndex={-1}\n        onKeyDown={this._onKey}\n        onKeyPress={this._onKey}\n        onFocus={this._onEdit}\n        onBlur={this._onFinishedEditing}\n      >\n        <div className=\"col-left shortcut-name\">\n          {this.props.label}\n        </div>\n        <div className=\"col-right\">\n          <div className=\"values\">{value}</div>\n        </div>\n      </Flexbox>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/keymaps/displayed-keybindings.js",
    "content": "module.exports = [\n  {\n    title: 'Application',\n    items: [\n      ['application:new-message', 'New Message'],\n      ['core:focus-search', 'Search'],\n    ],\n  },\n  {\n    title: 'Actions',\n    items: [\n      ['core:reply', 'Reply'],\n      ['core:reply-all', 'Reply All'],\n      ['core:forward', 'Forward'],\n      ['core:archive-item', 'Archive'],\n      ['core:delete-item', 'Trash'],\n      ['core:remove-from-view', 'Remove from view'],\n      ['core:gmail-remove-from-view', 'Gmail Remove from view'],\n      ['core:star-item', 'Star'],\n      ['core:snooze-item', 'Snooze'],\n      ['core:change-category', 'Change Folder / Labels'],\n      ['core:mark-as-read', 'Mark as read'],\n      ['core:mark-as-unread', 'Mark as unread'],\n      ['core:mark-important', 'Mark as important (Gmail)'],\n      ['core:mark-unimportant', 'Mark as unimportant (Gmail)'],\n      ['core:remove-and-previous', 'Remove from view and previous'],\n      ['core:remove-and-next', 'Remove from view and next'],\n    ],\n  },\n  {\n    title: 'Composer',\n    items: [\n      ['composer:send-message', 'Send Message'],\n      ['composer:focus-to', 'Focus the To field'],\n      ['composer:show-and-focus-cc', 'Focus the Cc field'],\n      ['composer:show-and-focus-bcc', 'Focus the Bcc field'],\n    ],\n  },\n  {\n    title: 'Navigation',\n    items: [\n      ['core:pop-sheet', 'Return to conversation list'],\n      ['core:focus-item', 'Open selected conversation'],\n      ['core:previous-item', 'Move to newer conversation'],\n      ['core:next-item', 'Move to older conversation'],\n    ],\n  },\n  {\n    title: 'Selection',\n    items: [\n      ['core:select-item', 'Select conversation'],\n      ['multiselect-list:select-all', 'Select all conversations'],\n      ['multiselect-list:deselect-all', 'Deselect all conversations'],\n      ['thread-list:select-read', 'Select all read conversations'],\n      ['thread-list:select-unread', 'Select all unread conversations'],\n      ['thread-list:select-starred', 'Select all starred conversations'],\n      ['thread-list:select-unstarred', 'Select all unstarred conversations'],\n    ],\n  },\n  {\n    title: 'Jumping',\n    items: [\n      ['navigation:go-to-inbox', 'Go to \"Inbox\"'],\n      ['navigation:go-to-starred', 'Go to \"Starred\"'],\n      ['navigation:go-to-sent', 'Go to \"Sent Mail\"'],\n      ['navigation:go-to-drafts', 'Go to \"Drafts\"'],\n      ['navigation:go-to-all', 'Go to \"All Mail\"'],\n    ],\n  },\n]\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/keymaps/mousetrap-keybinding-helpers.js",
    "content": "/* eslint-disable */\n/**\n * mapping of special keycodes to their corresponding keys\n *\n * everything in this dictionary cannot use keypress events\n * so it has to be here to map to the correct keycodes for\n * keyup/keydown events\n *\n * @type {Object}\n */\nvar _MAP = {\n    '8': 'backspace',\n    '9': 'tab',\n    '13': 'enter',\n    '16': 'shift',\n    '17': 'ctrl',\n    '18': 'alt',\n    '20': 'capslock',\n    '27': 'esc',\n    '32': 'space',\n    '33': 'pageup',\n    '34': 'pagedown',\n    '35': 'end',\n    '36': 'home',\n    '37': 'left',\n    '38': 'up',\n    '39': 'right',\n    '40': 'down',\n    '45': 'ins',\n    '46': 'del',\n    '91': 'meta',\n    '93': 'meta',\n    '224': 'meta'\n};\n\n/**\n * mapping for special characters so they can support\n *\n * this dictionary is only used incase you want to bind a\n * keyup or keydown event to one of these keys\n *\n * @type {Object}\n */\nvar _KEYCODE_MAP = {\n    '106': '*',\n    '107': '+',\n    '109': '-',\n    '110': '.',\n    '111' : '/',\n    '186': ';',\n    '187': '=',\n    '188': ',',\n    '189': '-',\n    '190': '.',\n    '191': '/',\n    '192': '`',\n    '219': '[',\n    '220': '\\\\',\n    '221': ']',\n    '222': '\\''\n};\n\n/**\n * this is a mapping of keys that require shift on a US keypad\n * back to the non shift equivelents\n *\n * this is so you can use keyup events with these keys\n *\n * note that this will only work reliably on US keyboards\n *\n * @type {Object}\n */\nvar _SHIFT_MAP = {\n    '~': '`',\n    '!': '1',\n    '@': '2',\n    '#': '3',\n    '$': '4',\n    '%': '5',\n    '^': '6',\n    '&': '7',\n    '*': '8',\n    '(': '9',\n    ')': '0',\n    '_': '-',\n    '+': '=',\n    ':': ';',\n    '\\\"': '\\'',\n    '<': ',',\n    '>': '.',\n    '?': '/',\n    '|': '\\\\'\n};\n\n/**\n * this is a list of special strings you can use to map\n * to modifier keys when you specify your keyboard shortcuts\n *\n * @type {Object}\n */\nvar _SPECIAL_ALIASES = {\n    'option': 'alt',\n    'command': 'meta',\n    'return': 'enter',\n    'escape': 'esc',\n    'plus': '+',\n    'mod': /Mac|iPod|iPhone|iPad/.test(navigator.platform) ? 'meta' : 'ctrl'\n};\n\n/**\n * variable to store the flipped version of _MAP from above\n * needed to check if we should use keypress or not when no action\n * is specified\n *\n * @type {Object|undefined}\n */\nvar _REVERSE_SHIFT_MAP = {};\nfor (var key of Object.keys(_SHIFT_MAP)) {\n  _REVERSE_SHIFT_MAP[_SHIFT_MAP[key]] = key;\n}\n\n/**\n * loop through the f keys, f1 to f19 and add them to the map\n * programatically\n */\nfor (var i = 1; i < 20; ++i) {\n    _MAP[111 + i] = 'f' + i;\n}\n\n/**\n * loop through to map numbers on the numeric keypad\n */\nfor (i = 0; i <= 9; ++i) {\n    _MAP[i + 96] = i;\n}\n\nfunction characterFromEvent(e) {\n  // for keypress events we should return the character as is\n  if (e.type == 'keypress') {\n      var character = String.fromCharCode(e.which);\n\n      // if the shift key is not pressed then it is safe to assume\n      // that we want the character to be lowercase.  this means if\n      // you accidentally have caps lock on then your key bindings\n      // will continue to work\n      //\n      // the only side effect that might not be desired is if you\n      // bind something like 'A' cause you want to trigger an\n      // event when capital A is pressed caps lock will no longer\n      // trigger the event.  shift+a will though.\n      if (!e.shiftKey) {\n          character = character.toLowerCase();\n      }\n\n      return character;\n  }\n\n  // for non keypress events the special maps are needed\n  if (_MAP[e.which]) {\n      return _MAP[e.which];\n  }\n\n  if (_KEYCODE_MAP[`${e.which}`]) {\n      return _KEYCODE_MAP[`${e.which}`];\n  }\n\n  // if it is not in the special map\n\n  // with keydown and keyup events the character seems to always\n  // come in as an uppercase character whether you are pressing shift\n  // or not.  we should make sure it is always lowercase for comparisons\n  return String.fromCharCode(e.which).toLowerCase();\n}\n\n/**\n * takes a key event and figures out what the modifiers are\n *\n * @param {Event} e\n * @returns {Array}\n */\nfunction eventModifiers(e) {\n    var modifiers = [];\n\n    if (e.shiftKey) {\n        modifiers.push('shift');\n    }\n\n    if (e.altKey) {\n        modifiers.push('alt');\n    }\n\n    if (e.ctrlKey) {\n        modifiers.push('ctrl');\n    }\n\n    if (e.metaKey) {\n        modifiers.push('meta');\n    }\n\n    return modifiers;\n}\n\nfunction keyAndModifiersForEvent(e) {\n  var eventKey = characterFromEvent(e);\n  var eventMods = eventModifiers(e);\n  if (_REVERSE_SHIFT_MAP[eventKey] && (eventMods.indexOf('shift') !== -1)) {\n    eventKey = _REVERSE_SHIFT_MAP[eventKey];\n    eventMods = eventMods.filter((k) => k !== 'shift');\n  }\n  return [eventKey, eventMods];\n}\n\nmodule.exports = {characterFromEvent, eventModifiers, keyAndModifiersForEvent};\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/preferences-account-details.jsx",
    "content": "/* eslint global-require: 0 */\nimport React, {Component, PropTypes} from 'react';\nimport {EditableList} from 'nylas-component-kit';\nimport {RegExpUtils, Account} from 'nylas-exports';\n\nclass PreferencesAccountDetails extends Component {\n\n  static propTypes = {\n    account: PropTypes.object,\n    onAccountUpdated: PropTypes.func.isRequired,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = {account: props.account.clone()};\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState({account: nextProps.account.clone()});\n  }\n\n  componentWillUnmount() {\n    this._saveChanges();\n  }\n\n\n  // Helpers\n\n  /**\n   * @private Will transform any user input into alias format.\n   * It will ignore any text after an email, if one is entered.\n   * If no email is entered, it will use the account's email.\n   * It will treat the text before the email as the name for the alias.\n   * If no name is entered, it will use the account's name value.\n   * @param {string} str - The string the user entered on the alias input\n   * @param {object} [account=this.props.account] - The account object\n   */\n  _makeAlias(str, account = this.props.account) {\n    const emailRegex = RegExpUtils.emailRegex();\n    const match = emailRegex.exec(str);\n    if (!match) {\n      return `${str || account.name} <${account.emailAddress}>`;\n    }\n    const email = match[0];\n    let name = str.slice(0, Math.max(0, match.index - 1));\n    if (!name) {\n      name = account.name || 'No name provided';\n    }\n    name = name.trim();\n    // TODO Sanitize the name string\n    return `${name} <${email}>`;\n  }\n\n  _saveChanges = () => {\n    this.props.onAccountUpdated(this.props.account, this.state.account);\n  };\n\n  _setState = (updates, callback = () => {}) => {\n    const account = Object.assign(this.state.account.clone(), updates);\n    this.setState({account}, callback);\n  };\n\n  _setStateAndSave = (updates) => {\n    this._setState(updates, () => {\n      this._saveChanges();\n    });\n  };\n\n\n  // Handlers\n\n  _onAccountLabelUpdated = (event) => {\n    this._setState({label: event.target.value});\n  };\n\n  _onAccountAliasCreated = (newAlias) => {\n    const coercedAlias = this._makeAlias(newAlias);\n    const aliases = this.state.account.aliases.concat([coercedAlias]);\n    this._setStateAndSave({aliases})\n  };\n\n  _onAccountAliasUpdated = (newAlias, alias, idx) => {\n    const coercedAlias = this._makeAlias(newAlias);\n    const aliases = this.state.account.aliases.slice();\n    let defaultAlias = this.state.account.defaultAlias;\n    if (defaultAlias === alias) {\n      defaultAlias = coercedAlias;\n    }\n    aliases[idx] = coercedAlias;\n    this._setStateAndSave({aliases, defaultAlias});\n  };\n\n  _onAccountAliasRemoved = (alias, idx) => {\n    const aliases = this.state.account.aliases.slice();\n    let defaultAlias = this.state.account.defaultAlias;\n    if (defaultAlias === alias) {\n      defaultAlias = null;\n    }\n    aliases.splice(idx, 1);\n    this._setStateAndSave({aliases, defaultAlias});\n  };\n\n  _onDefaultAliasSelected = (event) => {\n    const defaultAlias = event.target.value === 'None' ? null : event.target.value;\n    this._setStateAndSave({defaultAlias});\n  };\n\n  _onReconnect = () => {\n    const ipc = require('electron').ipcRenderer;\n    ipc.send('command', 'application:add-account', {existingAccount: this.state.account, source: 'Reconnect from preferences'});\n  }\n\n  _onContactSupport = () => {\n    const {shell} = require(\"electron\");\n    shell.openExternal(\"https://support.nylas.com/hc/en-us/requests/new\");\n  }\n\n  // Renderers\n\n  _renderDefaultAliasSelector(account) {\n    const aliases = account.aliases;\n    const defaultAlias = account.defaultAlias || 'None';\n    if (aliases.length > 0) {\n      return (\n        <div className=\"default-alias-selector\">\n          <div>Default for new messages:</div>\n          <select value={defaultAlias} onChange={this._onDefaultAliasSelected}>\n            <option value=\"None\">{`${account.name} <${account.emailAddress}>`}</option>\n            {aliases.map((alias, idx) => <option key={`alias-${idx}`} value={alias}>{alias}</option>)}\n          </select>\n        </div>\n      );\n    }\n    return null;\n  }\n\n\n  _renderErrorDetail(message, buttonText, buttonAction) {\n    return (<div className=\"account-error-detail\">\n      <div className=\"message\">{message}</div>\n      <a className=\"action\" onClick={buttonAction}>{buttonText}</a>\n    </div>)\n  }\n\n  _renderSyncErrorDetails() {\n    const {account} = this.state;\n    if (account.hasSyncStateError()) {\n      switch (account.syncState) {\n        case Account.N1_Cloud_AUTH_FAILED:\n          return this._renderErrorDetail(\n            `Nylas Mail can no longer authenticate N1 Cloud Services with\n            ${account.emailAddress}. The password or authentication may\n            have changed.`,\n            \"Reconnect\",\n            this._onReconnect);\n        case Account.SYNC_STATE_AUTH_FAILED:\n          return this._renderErrorDetail(\n            `Nylas Mail can no longer authenticate with ${account.emailAddress}. The password or\n            authentication may have changed.`,\n            \"Reconnect\",\n            this._onReconnect);\n        default:\n          return this._renderErrorDetail(\n            `Nylas encountered an error while syncing mail for ${account.emailAddress}. Contact Nylas support for details.`,\n            \"Contact support\",\n            this._onContactSupport);\n      }\n    }\n    return null;\n  }\n\n  render() {\n    const {account} = this.state;\n    const aliasPlaceholder = this._makeAlias(\n      `alias@${account.emailAddress.split('@')[1]}`\n    );\n\n    return (\n      <div className=\"account-details\">\n        {this._renderSyncErrorDetails()}\n        <h3>Account Label</h3>\n        <input\n          type=\"text\"\n          value={account.label}\n          onBlur={this._saveChanges}\n          onChange={this._onAccountLabelUpdated}\n        />\n\n        <h3>Account Settings</h3>\n\n        <div className=\"btn\" onClick={this._onReconnect}>\n          {account.provider === 'imap' ? 'Update Connection Settings...' : 'Re-authenticate...'}\n        </div>\n\n        <h3>Aliases</h3>\n\n        <div className=\"platform-note\">\n          You may need to configure aliases with your\n          mail provider (Outlook, Gmail) before using them.\n        </div>\n\n        <EditableList\n          showEditIcon\n          items={account.aliases}\n          createInputProps={{placeholder: aliasPlaceholder}}\n          onItemCreated={this._onAccountAliasCreated}\n          onItemEdited={this._onAccountAliasUpdated}\n          onDeleteItem={this._onAccountAliasRemoved}\n        />\n\n        {this._renderDefaultAliasSelector(account)}\n      </div>\n    );\n  }\n\n}\n\nexport default PreferencesAccountDetails;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/preferences-account-list.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport {RetinaImg, Flexbox, EditableList} from 'nylas-component-kit';\nimport classnames from 'classnames';\n\nclass PreferencesAccountList extends Component {\n\n  static propTypes = {\n    accounts: PropTypes.array,\n    selected: PropTypes.object,\n    onAddAccount: PropTypes.func.isRequired,\n    onReorderAccount: PropTypes.func.isRequired,\n    onSelectAccount: PropTypes.func.isRequired,\n    onRemoveAccount: PropTypes.func.isRequired,\n  };\n\n  _renderAccountStateIcon(account) {\n    if (account.syncState !== \"running\") {\n      return (\n        <div className=\"sync-error-icon\">\n          <RetinaImg\n            className=\"sync-error-icon\"\n            name=\"ic-settings-account-error.png\"\n            mode={RetinaImg.Mode.ContentIsMask}\n          />\n        </div>\n      )\n    }\n    return null;\n  }\n\n  _renderAccount = (account) => {\n    const label = account.label;\n    const accountSub = `${account.name || 'No name provided'} <${account.emailAddress}>`;\n    const syncError = account.hasSyncStateError();\n\n    return (\n      <div\n        className={classnames({\"account\": true, \"sync-error\": syncError})}\n        key={account.id}\n      >\n        <Flexbox direction=\"row\" style={{alignItems: 'middle'}}>\n          <div style={{textAlign: 'center'}}>\n            <RetinaImg\n              name={syncError ? \"ic-settings-account-error.png\" : `ic-settings-account-${account.provider}.png`}\n              fallback=\"ic-settings-account-imap.png\"\n              mode={RetinaImg.Mode.ContentPreserve}\n            />\n          </div>\n          <div style={{flex: 1, marginLeft: 10}}>\n            <div className=\"account-name\">\n              {label}\n            </div>\n            <div className=\"account-subtext\">{accountSub} ({account.displayProvider()})</div>\n          </div>\n        </Flexbox>\n      </div>\n    );\n  };\n\n  render() {\n    if (!this.props.accounts) {\n      return <div className=\"account-list\" />;\n    }\n    return (\n      <EditableList\n        className=\"account-list\"\n        items={this.props.accounts}\n        itemContent={this._renderAccount}\n        selected={this.props.selected}\n        onReorderItem={this.props.onReorderAccount}\n        onCreateItem={this.props.onAddAccount}\n        onSelectItem={this.props.onSelectAccount}\n        onDeleteItem={this.props.onRemoveAccount}\n      />\n    );\n  }\n\n}\n\nexport default PreferencesAccountList;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/preferences-accounts.jsx",
    "content": "import _ from 'underscore';\nimport React from 'react';\nimport {ipcRenderer} from 'electron';\nimport {AccountStore, Actions} from 'nylas-exports';\nimport PreferencesAccountList from './preferences-account-list';\nimport PreferencesAccountDetails from './preferences-account-details';\n\n\nclass PreferencesAccounts extends React.Component {\n  static displayName = 'PreferencesAccounts';\n\n  constructor() {\n    super();\n    this.state = this.getStateFromStores();\n  }\n\n  componentDidMount() {\n    this.unsubscribe = AccountStore.listen(this._onAccountsChanged)\n  }\n\n  componentWillUnmount() {\n    if (this.unsubscribe) {\n      this.unsubscribe();\n    }\n  }\n\n  getStateFromStores({selected} = {}) {\n    const accounts = AccountStore.accounts()\n    let selectedAccount;\n    if (selected) {\n      selectedAccount = _.findWhere(accounts, {id: selected.id})\n    }\n    // If selected was null or no longer exists in the AccountStore,\n    // just use the first account.\n    if (!selectedAccount) {\n      selectedAccount = accounts[0];\n    }\n    return {\n      accounts,\n      selected: selectedAccount,\n    };\n  }\n\n  _onAccountsChanged = () => {\n    this.setState(this.getStateFromStores(this.state));\n  }\n\n  // Update account list actions\n  _onAddAccount() {\n    ipcRenderer.send('command', 'application:add-account', {source: 'Preferences'});\n  }\n\n  _onReorderAccount(account, oldIdx, newIdx) {\n    Actions.reorderAccount(account.id, newIdx);\n  }\n\n  _onSelectAccount = (account) => {\n    this.setState({selected: account});\n  }\n\n  _onRemoveAccount(account) {\n    Actions.removeAccount(account.id);\n  }\n\n  // Update account actions\n  _onAccountUpdated(account, updates) {\n    Actions.updateAccount(account.id, updates);\n  }\n\n  render() {\n    return (\n      <div className=\"container-accounts\">\n        <div className=\"accounts-content\">\n          <PreferencesAccountList\n            accounts={this.state.accounts}\n            selected={this.state.selected}\n            onAddAccount={this._onAddAccount}\n            onReorderAccount={this._onReorderAccount}\n            onSelectAccount={this._onSelectAccount}\n            onRemoveAccount={this._onRemoveAccount}\n          />\n          <PreferencesAccountDetails\n            account={this.state.selected}\n            onAccountUpdated={this._onAccountUpdated}\n          />\n        </div>\n      </div>\n    );\n  }\n\n}\n\nexport default PreferencesAccounts;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/preferences-appearance.jsx",
    "content": "import React from 'react';\nimport {RetinaImg, Flexbox} from 'nylas-component-kit';\n\nclass AppearanceModeSwitch extends React.Component {\n\n  static displayName = 'AppearanceModeSwitch';\n\n  static propTypes = {\n    config: React.PropTypes.object.isRequired,\n  };\n\n  constructor(props) {\n    super();\n    this.state = {\n      value: props.config.get('core.workspace.mode'),\n    };\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState({\n      value: nextProps.config.get('core.workspace.mode'),\n    });\n  }\n\n  _onApplyChanges = () => {\n    NylasEnv.commands.dispatch(`application:select-${this.state.value}-mode`);\n  }\n\n  _renderModeOptions() {\n    return ['list', 'split'].map((mode) =>\n      <AppearanceModeOption\n        mode={mode}\n        key={mode}\n        active={this.state.value === mode}\n        onClick={() => this.setState({value: mode})}\n      />\n    );\n  }\n\n  render() {\n    const hasChanges = this.state.value !== this.props.config.get('core.workspace.mode');\n    let applyChangesClass = \"btn\";\n    if (!hasChanges) applyChangesClass += \" btn-disabled\";\n\n    return (\n      <div className=\"appearance-mode-switch\">\n        <Flexbox\n          direction=\"row\"\n          style={{alignItems: \"center\"}}\n          className=\"item\"\n        >\n          {this._renderModeOptions()}\n        </Flexbox>\n        <div className={applyChangesClass} onClick={this._onApplyChanges}>Apply Layout</div>\n      </div>\n    );\n  }\n\n}\n\nconst AppearanceModeOption = function AppearanceModeOption(props) {\n  let classname = \"appearance-mode\";\n  if (props.active) classname += \" active\";\n\n  const label = {\n    list: 'Single Panel',\n    split: 'Two Panel',\n  }[props.mode];\n\n  return (\n    <div className={classname} onClick={props.onClick}>\n      <RetinaImg name={`appearance-mode-${props.mode}.png`} mode={RetinaImg.Mode.ContentIsMask} />\n      <div>{label}</div>\n    </div>\n  );\n}\nAppearanceModeOption.propTypes = {\n  mode: React.PropTypes.string.isRequired,\n  active: React.PropTypes.bool,\n  onClick: React.PropTypes.func,\n}\n\nclass PreferencesAppearance extends React.Component {\n\n  static displayName = 'PreferencesAppearance';\n\n  static propTypes = {\n    config: React.PropTypes.object,\n    configSchema: React.PropTypes.object,\n  }\n\n  onClick = () => {\n    NylasEnv.commands.dispatch(\"window:launch-theme-picker\");\n  }\n\n  render() {\n    return (\n      <div className=\"container-appearance\">\n        <label htmlFor=\"change-layout\">Change layout:</label>\n        <AppearanceModeSwitch id=\"change-layout\" config={this.props.config} />\n        <button className=\"btn btn-large\" onClick={this.onClick}>Change theme...</button>\n      </div>\n    );\n  }\n}\n\nexport default PreferencesAppearance;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/preferences-general.jsx",
    "content": "/* eslint global-require: 0*/\nimport React from 'react';\n\nimport {Actions} from 'nylas-exports'\nimport ConfigSchemaItem from './config-schema-item';\nimport WorkspaceSection from './workspace-section';\nimport SendingSection from './sending-section';\nclass PreferencesGeneral extends React.Component {\n  static displayName = 'PreferencesGeneral'\n\n  static propTypes = {\n    config: React.PropTypes.object,\n    configSchema: React.PropTypes.object,\n  };\n\n  _reboot = () => {\n    const app = require('electron').remote.app;\n    app.relaunch()\n    app.quit()\n  }\n\n\n  _resetAccountsAndSettings = () => {\n    const rimraf = require('rimraf')\n    rimraf(NylasEnv.getConfigDirPath(), {disableGlob: true}, (err) => {\n      if (err) console.log(err)\n      else this._reboot()\n    })\n  }\n\n  _resetEmailCache = () => {\n    Actions.resetEmailCache()\n  }\n\n  render() {\n    return (\n      <div className=\"container-general\" style={{maxWidth: 600}}>\n\n        <WorkspaceSection config={this.props.config} configSchema={this.props.configSchema} />\n\n        <ConfigSchemaItem\n          configSchema={this.props.configSchema.properties.notifications}\n          keyName=\"Notifications\"\n          keyPath=\"core.notifications\"\n          config={this.props.config}\n        />\n\n        <div className=\"platform-note platform-linux-only\">\n          Nylas Mail desktop notifications on Linux require Zenity. You may need to install\n          it with your package manager (i.e., <code>sudo apt-get install zenity</code>).\n        </div>\n\n        <ConfigSchemaItem\n          configSchema={this.props.configSchema.properties.reading}\n          keyName=\"Reading\"\n          keyPath=\"core.reading\"\n          config={this.props.config}\n        />\n\n        <ConfigSchemaItem\n          configSchema={this.props.configSchema.properties.composing}\n          keyName=\"Composing\"\n          keyPath=\"core.composing\"\n          config={this.props.config}\n        />\n\n        <SendingSection\n          config={this.props.config}\n          configSchema={this.props.configSchema}\n        />\n\n        <ConfigSchemaItem\n          configSchema={this.props.configSchema.properties.attachments}\n          keyName=\"Attachments\"\n          keyPath=\"core.attachments\"\n          config={this.props.config}\n        />\n\n        <div className=\"local-data\">\n          <h6>Local Data</h6>\n          <div className=\"btn\" onClick={this._resetEmailCache}>Reset Email Cache</div>\n          <div className=\"btn\" onClick={this._resetAccountsAndSettings}>Reset Accounts and Settings</div>\n        </div>\n      </div>\n    )\n  }\n}\n\n\nexport default PreferencesGeneral;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/preferences-keymaps.jsx",
    "content": "import React from 'react';\nimport path from 'path';\nimport fs from 'fs';\nimport { remote } from 'electron';\nimport { Flexbox } from 'nylas-component-kit';\n\nimport displayedKeybindings from './keymaps/displayed-keybindings';\nimport CommandItem from './keymaps/command-item';\n\nclass PreferencesKeymaps extends React.Component {\n\n  static displayName = 'PreferencesKeymaps';\n\n  static propTypes = {\n    config: React.PropTypes.object,\n  };\n\n  constructor() {\n    super();\n    this.state = {\n      templates: [],\n      bindings: this._getStateFromKeymaps(),\n    };\n    this._loadTemplates();\n  }\n\n  componentDidMount() {\n    this._disposable = NylasEnv.keymaps.onDidReloadKeymap(() => {\n      this.setState({bindings: this._getStateFromKeymaps()});\n    });\n  }\n\n  componentWillUnmount() {\n    this._disposable.dispose();\n  }\n\n  _getStateFromKeymaps() {\n    const bindings = {};\n    for (const section of displayedKeybindings) {\n      for (const [command] of section.items) {\n        bindings[command] = NylasEnv.keymaps.getBindingsForCommand(command) || [];\n      }\n    }\n    return bindings;\n  }\n\n  _loadTemplates() {\n    const templatesDir = path.join(NylasEnv.getLoadSettings().resourcePath, 'keymaps', 'templates');\n    fs.readdir(templatesDir, (err, files) => {\n      if (!files || !(files instanceof Array)) return;\n      let templates = files.filter((filename) => {\n        return path.extname(filename) === '.json';\n      });\n      templates = templates.map((filename) => {\n        return path.parse(filename).name;\n      });\n      this.setState({templates: templates});\n    });\n  }\n\n  _onShowUserKeymaps() {\n    const keymapsFile = NylasEnv.keymaps.getUserKeymapPath();\n    if (!fs.existsSync(keymapsFile)) {\n      fs.writeFileSync(keymapsFile, '{}');\n    }\n    remote.shell.showItemInFolder(keymapsFile);\n  }\n\n  _onDeleteUserKeymap() {\n    const chosen = remote.dialog.showMessageBox(NylasEnv.getCurrentWindow(), {\n      type: 'info',\n      message: \"Are you sure?\",\n      detail: \"Delete your custom key bindings and reset to the template defaults?\",\n      buttons: ['Cancel', 'Reset'],\n    });\n\n    if (chosen === 1) {\n      const keymapsFile = NylasEnv.keymaps.getUserKeymapPath();\n      fs.writeFileSync(keymapsFile, '{}');\n    }\n  }\n\n  _renderBindingsSection = (section) => {\n    return (\n      <section key={`section-${section.title}`}>\n        <div className=\"shortcut-section-title\">{section.title}</div>\n        {\n          section.items.map(([command, label]) => {\n            return (\n              <CommandItem\n                key={command}\n                command={command}\n                label={label}\n                bindings={this.state.bindings[command]}\n              />\n            );\n          })\n        }\n      </section>\n    );\n  }\n\n  render() {\n    return (\n      <div className=\"container-keymaps\">\n        <section>\n          <Flexbox className=\"container-dropdown\">\n            <div>Shortcut set:</div>\n            <div className=\"dropdown\">\n              <select\n                style={{margin: 0}}\n                tabIndex={-1}\n                value={this.props.config.get('core.keymapTemplate')}\n                onChange={(event) => this.props.config.set('core.keymapTemplate', event.target.value)}\n              >\n                {this.state.templates.map((template) => {\n                  return <option key={template} value={template}>{template}</option>\n                })}\n              </select>\n            </div>\n            <div style={{flex: 1}} />\n            <button className=\"btn\" onClick={this._onDeleteUserKeymap}>Reset to Defaults</button>\n          </Flexbox>\n          <p>\n            You can choose a shortcut set to use keyboard shortcuts of familiar email clients.\n            To edit a shortcut, click it in the list below and enter a replacement on the keyboard.\n          </p>\n          {displayedKeybindings.map(this._renderBindingsSection)}\n        </section>\n        <section>\n          <h2>Customization</h2>\n          <p>You can manage your custom shortcuts directly by editing your shortcuts file.</p>\n          <button className=\"btn\" onClick={this._onShowUserKeymaps}>Edit custom shortcuts</button>\n        </section>\n      </div>\n    );\n  }\n\n}\n\nexport default PreferencesKeymaps;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/preferences-mail-rules.jsx",
    "content": "import React from 'react';\nimport _ from 'underscore';\n\nimport {Actions,\n  AccountStore,\n  MailRulesStore,\n  MailRulesTemplates,\n  TaskQueueStatusStore,\n  ReprocessMailRulesTask} from 'nylas-exports';\n\nimport {Flexbox,\n  EditableList,\n  RetinaImg,\n  ScrollRegion,\n  ScenarioEditor} from 'nylas-component-kit';\n\nconst {\n  ActionTemplatesForAccount,\n  ConditionTemplatesForAccount,\n} = MailRulesTemplates;\n\n\nclass PreferencesMailRules extends React.Component {\n  static displayName = 'PreferencesMailRules';\n\n  constructor() {\n    super();\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsubscribers = [];\n    this._unsubscribers.push(MailRulesStore.listen(this._onRulesChanged));\n    this._unsubscribers.push(TaskQueueStatusStore.listen(this._onTasksChanged));\n  }\n\n  componentWillUnmount() {\n    this._unsubscribers.forEach(unsubscribe => unsubscribe());\n  }\n\n  _getStateFromStores() {\n    const accounts = AccountStore.accounts();\n    const state = this.state || {};\n    let {currentAccount} = state;\n    if (!accounts.find(acct => acct === currentAccount)) {\n      currentAccount = accounts[0];\n    }\n    const rules = MailRulesStore.rulesForAccountId(currentAccount.id);\n    const selectedRule = this.state && this.state.selectedRule ? _.findWhere(rules, {id: this.state.selectedRule.id}) : rules[0];\n\n    return {\n      accounts: accounts,\n      currentAccount: currentAccount,\n      rules: rules,\n      selectedRule: selectedRule,\n      tasks: TaskQueueStatusStore.tasksMatching(ReprocessMailRulesTask, {}),\n      actionTemplates: ActionTemplatesForAccount(currentAccount),\n      conditionTemplates: ConditionTemplatesForAccount(currentAccount),\n    }\n  }\n\n  _onSelectAccount = (event) => {\n    const accountId = event.target.value;\n    const currentAccount = this.state.accounts.find(acct => acct.id === accountId);\n    this.setState({currentAccount: currentAccount}, () => {\n      this.setState(this._getStateFromStores())\n    });\n  }\n\n  _onReprocessRules = () => {\n    const needsMessageBodies = () => {\n      for (const rule of this.state.rules) {\n        for (const condition of rule.conditions) {\n          if (condition.templateKey === 'body') {\n            return true;\n          }\n        }\n      }\n      return false;\n    }\n\n    if (needsMessageBodies()) {\n      NylasEnv.showErrorDialog(\"One or more of your mail rules requires the bodies of messages being processed. These rules can't be run on your entire mailbox.\");\n    }\n\n    const task = new ReprocessMailRulesTask(this.state.currentAccount.id)\n    Actions.queueTask(task);\n  }\n\n  _onAddRule = () => {\n    Actions.addMailRule({accountId: this.state.currentAccount.id});\n  }\n\n  _onSelectRule = (rule) => {\n    this.setState({selectedRule: rule});\n  }\n\n  _onReorderRule = (rule, startIdx, endIdx) => {\n    Actions.reorderMailRule(rule.id, endIdx);\n  }\n\n  _onDeleteRule = (rule) => {\n    Actions.deleteMailRule(rule.id);\n  }\n\n  _onRuleNameEdited = (newName, rule) => {\n    Actions.updateMailRule(rule.id, {name: newName});\n  }\n\n  _onRuleConditionModeEdited = (event) => {\n    Actions.updateMailRule(this.state.selectedRule.id, {conditionMode: event.target.value});\n  }\n\n  _onRuleEnabled = () => {\n    Actions.updateMailRule(this.state.selectedRule.id, {disabled: false, disabledReason: null});\n  }\n\n  _onRulesChanged = () => {\n    const next = this._getStateFromStores();\n    const nextRules = next.rules;\n    const prevRules = this.state.rules ? this.state.rules : [];\n\n    const added = _.difference(nextRules, prevRules);\n    if (added.length === 1) {\n      next.selectedRule = added[0];\n    }\n\n    this.setState(next);\n  }\n\n  _onTasksChanged = () => {\n    this.setState({tasks: TaskQueueStatusStore.tasksMatching(ReprocessMailRulesTask, {})})\n  }\n\n  _renderAccountPicker() {\n    const options = this.state.accounts.map(account =>\n      <option value={account.id} key={account.id}>{account.label}</option>\n    );\n\n    return (\n      <select\n        value={this.state.currentAccount.id}\n        onChange={this._onSelectAccount}\n        style={{margin: 0, minWidth: 200}}\n      >\n        {options}\n      </select>\n    );\n  }\n\n  _renderMailRules() {\n    if (this.state.rules.length === 0) {\n      return (\n        <div className=\"empty-list\">\n          <RetinaImg\n            className=\"icon-mail-rules\"\n            name=\"rules-big.png\"\n            mode={RetinaImg.Mode.ContentDark}\n          />\n          <h2>No rules</h2>\n          <button className=\"btn btn-small\" onMouseDown={this._onAddRule}>\n            Create a new rule\n          </button>\n        </div>\n      );\n    }\n    return (\n      <Flexbox>\n        <EditableList\n          showEditIcon\n          className=\"rule-list\"\n          items={this.state.rules}\n          itemContent={this._renderListItemContent}\n          onCreateItem={this._onAddRule}\n          onReorderItem={this._onReorderRule}\n          onDeleteItem={this._onDeleteRule}\n          onItemEdited={this._onRuleNameEdited}\n          selected={this.state.selectedRule}\n          onSelectItem={this._onSelectRule}\n        />\n        {this._renderDetail()}\n      </Flexbox>\n    );\n  }\n\n  _renderListItemContent(rule) {\n    if (rule.disabled) {\n      return (<div className=\"item-rule-disabled\">{rule.name}</div>);\n    }\n    return rule.name;\n  }\n\n  _renderDetail() {\n    const rule = this.state.selectedRule;\n\n    if (rule) {\n      return (\n        <ScrollRegion className=\"rule-detail\">\n          {this._renderDetailDisabledNotice()}\n          <div className=\"inner\">\n            <span>If </span>\n            <select value={rule.conditionMode} onChange={this._onRuleConditionModeEdited}>\n              <option value=\"any\">Any</option>\n              <option value=\"all\">All</option>\n            </select>\n            <span> of the following conditions are met:</span>\n            <ScenarioEditor\n              instances={rule.conditions}\n              templates={this.state.conditionTemplates}\n              onChange={(conditions) => Actions.updateMailRule(rule.id, {conditions})}\n              className=\"well well-matchers\"\n            />\n            <span>Perform the following actions:</span>\n            <ScenarioEditor\n              instances={rule.actions}\n              templates={this.state.actionTemplates}\n              onChange={(actions) => Actions.updateMailRule(rule.id, {actions})}\n              className=\"well well-actions\"\n            />\n          </div>\n        </ScrollRegion>\n      );\n    }\n\n    return (\n      <div className=\"rule-detail\">\n        <div className=\"no-selection\">Create a rule or select one to get started</div>\n      </div>\n    );\n  }\n\n  _renderDetailDisabledNotice() {\n    if (!this.state.selectedRule.disabled) return false;\n    return (\n      <div className=\"disabled-reason\">\n        <button className=\"btn\" onClick={this._onRuleEnabled}>Enable</button>\n        This rule has been disabled. Make sure the actions below are valid\n        and re-enable the rule.\n        <div>({this.state.selectedRule.disabledReason})</div>\n      </div>\n    );\n  }\n\n  _renderTasks() {\n    if (this.state.tasks.length === 0) return false;\n    return (\n      <div style={{flex: 1, paddingLeft: 20}}>\n        {this.state.tasks.map((task) => {\n          return (\n            <Flexbox style={{alignItems: 'baseline'}}>\n              <div style={{paddingRight: \"12px\"}}>\n                <RetinaImg name=\"sending-spinner.gif\" width={18} mode={RetinaImg.Mode.ContentPreserve} />\n              </div>\n              <div>\n                <strong>{AccountStore.accountForId(task.accountId).emailAddress}</strong>\n                {` — ${Number(task.numberOfImpactedItems()).toLocaleString()} processed...`}\n              </div>\n              <div style={{flex: 1}} />\n              <button className=\"btn btn-sm\" onClick={() => Actions.dequeueTask(task.id)}>\n                Cancel\n              </button>\n            </Flexbox>\n          );\n        })}\n      </div>\n    );\n  }\n\n  render() {\n    const processDisabled = _.any(this.state.tasks, (task) => {\n      return (task.accountId === this.state.currentAccount.id);\n    });\n\n    return (\n      <div className=\"container-mail-rules\">\n        <section>\n          <Flexbox className=\"container-dropdown\">\n            <div>Account:</div>\n            <div className=\"dropdown\">{this._renderAccountPicker()}</div>\n          </Flexbox>\n          <p>Rules only apply to the selected account.</p>\n\n          {this._renderMailRules()}\n\n          <Flexbox style={{marginTop: 40, maxWidth: 600}}>\n            <div>\n              <button disabled={processDisabled} className=\"btn\" style={{'float': 'right'}} onClick={this._onReprocessRules}>\n                Process entire inbox\n              </button>\n            </div>\n            {this._renderTasks()}\n          </Flexbox>\n\n          <p style={{marginTop: 10}}>\n            By default, mail rules are only applied to new mail as it arrives.\n            Applying rules to your entire inbox may take a long time and\n            degrade performance.\n          </p>\n        </section>\n      </div>\n    );\n  }\n\n}\n\nexport default PreferencesMailRules;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/sending-section.jsx",
    "content": "import _ from 'underscore';\nimport React from 'react';\nimport {AccountStore, SendActionsStore} from 'nylas-exports';\nimport {ListensToFluxStore} from 'nylas-component-kit';\nimport ConfigSchemaItem from './config-schema-item';\n\n\nfunction getExtendedSendingSchema(configSchema) {\n  const accounts = AccountStore.accounts();\n  // const sendActions = SendActionsStore.sendActions()\n  const defaultAccountIdForSend = {\n    'type': 'string',\n    'title': 'Send new messages from',\n    'default': 'selected-mailbox',\n    'enum': ['selected-mailbox'].concat(accounts.map(acc => acc.id)),\n    'enumLabels': ['Account of selected mailbox'].concat(accounts.map(acc => acc.me().toString())),\n  }\n  // TODO re-enable sending actions at some point\n  // const defaultSendType = {\n  //   'type': 'string',\n  //   'default': 'send',\n  //   'enum': sendActions.map(({configKey}) => configKey),\n  //   'enumLabels': sendActions.map(({title}) => title),\n  //   'title': \"Default send behavior\",\n  // }\n\n  _.extend(configSchema.properties.sending.properties, {\n    defaultAccountIdForSend,\n  });\n  return configSchema.properties.sending;\n}\n\nfunction SendingSection(props) {\n  const {config, sendingConfigSchema} = props\n\n  return (\n    <ConfigSchemaItem\n      config={config}\n      configSchema={sendingConfigSchema}\n      keyName=\"Sending\"\n      keyPath=\"core.sending\"\n    />\n  );\n}\n\nSendingSection.displayName = 'SendingSection';\nSendingSection.propTypes = {\n  config: React.PropTypes.object,\n  configSchema: React.PropTypes.object,\n  sendingConfigSchema: React.PropTypes.object,\n}\n\nexport default ListensToFluxStore(SendingSection, {\n  stores: [AccountStore, SendActionsStore],\n  getStateFromStores(props) {\n    const {configSchema} = props\n    return {\n      sendingConfigSchema: getExtendedSendingSchema(configSchema),\n    }\n  },\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/lib/tabs/workspace-section.jsx",
    "content": "import React from 'react';\nimport {DefaultClientHelper, SystemStartService} from 'nylas-exports';\nimport ConfigSchemaItem from './config-schema-item';\n\nclass DefaultMailClientItem extends React.Component {\n\n  constructor() {\n    super();\n    this.state = {defaultClient: false};\n    this._helper = new DefaultClientHelper();\n    if (this._helper.available()) {\n      this._helper.isRegisteredForURLScheme('mailto', (registered) => {\n        if (this._mounted) this.setState({defaultClient: registered});\n      });\n    }\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  toggleDefaultMailClient = (event) => {\n    if (this.state.defaultClient) {\n      this.setState({defaultClient: false});\n      this._helper.resetURLScheme('mailto');\n    } else {\n      this.setState({defaultClient: true});\n      this._helper.registerForURLScheme('mailto');\n    }\n    event.target.blur();\n  }\n\n  render() {\n    return (\n      <div className=\"item\">\n        <input\n          type=\"checkbox\"\n          id=\"default-client\"\n          checked={this.state.defaultClient}\n          onChange={this.toggleDefaultMailClient}\n        />\n        <label htmlFor=\"default-client\">Use Nylas Mail as default mail client</label>\n      </div>\n    );\n  }\n\n}\n\n\nclass LaunchSystemStartItem extends React.Component {\n\n  constructor() {\n    super();\n    this.state = {\n      available: false,\n      launchOnStart: false,\n    };\n    this._service = new SystemStartService();\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    this._service.checkAvailability().then((available) => {\n      if (this._mounted) {\n        this.setState({available});\n      }\n      if (!available || !this._mounted) return;\n      this._service.doesLaunchOnSystemStart().then((launchOnStart) => {\n        if (this._mounted) {\n          this.setState({launchOnStart});\n        }\n      });\n    });\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  _toggleLaunchOnStart = (event) => {\n    if (this.state.launchOnStart) {\n      this.setState({launchOnStart: false});\n      this._service.dontLaunchOnSystemStart();\n    } else {\n      this.setState({launchOnStart: true});\n      this._service.configureToLaunchOnSystemStart();\n    }\n    event.target.blur();\n  }\n\n  render() {\n    if (!this.state.available) return false;\n    return (\n      <div className=\"item\">\n        <input\n          type=\"checkbox\"\n          id=\"launch-on-start\"\n          checked={this.state.launchOnStart}\n          onChange={this._toggleLaunchOnStart}\n        />\n        <label htmlFor=\"launch-on-start\">Launch on system start</label>\n      </div>\n    );\n  }\n\n}\n\nconst WorkspaceSection = (props) => {\n  return (\n    <section>\n      <DefaultMailClientItem />\n\n      <LaunchSystemStartItem />\n\n      <ConfigSchemaItem\n        configSchema={props.configSchema.properties.workspace.properties.systemTray}\n        keyPath=\"core.workspace.systemTray\"\n        config={props.config}\n      />\n\n      <ConfigSchemaItem\n        configSchema={props.configSchema.properties.workspace.properties.showImportant}\n        keyPath=\"core.workspace.showImportant\"\n        config={props.config}\n      />\n\n      <ConfigSchemaItem\n        configSchema={props.configSchema.properties.workspace.properties.showUnreadForAllCategories}\n        keyPath=\"core.workspace.showUnreadForAllCategories\"\n        config={props.config}\n      />\n\n      <ConfigSchemaItem\n        configSchema={props.configSchema.properties.workspace.properties.use24HourClock}\n        keyPath=\"core.workspace.use24HourClock\"\n        config={props.config}\n      />\n\n      <ConfigSchemaItem\n        configSchema={props.configSchema.properties.workspace.properties.interfaceZoom}\n        keyPath=\"core.workspace.interfaceZoom\"\n        config={props.config}\n      />\n\n      <div className=\"platform-note platform-linux-only\">\n        &quot;Launch on system start&quot; only works in XDG-compliant desktop environments.\n        To enable the Nylas Mail icon in the system tray, you may need to install libappindicator1.\n        (i.e., &lt;code&gt;sudo apt-get install libappindicator1&lt;/code&gt;)\n      </div>\n    </section>\n  );\n}\n\nWorkspaceSection.propTypes = {\n  config: React.PropTypes.object,\n  configSchema: React.PropTypes.object,\n}\n\nexport default WorkspaceSection;\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/package.json",
    "content": "{\n  \"name\": \"preferences\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Nylas Preferences Window Component\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/spec/preferences-account-details-spec.jsx",
    "content": "import React from 'react';\nimport {renderIntoDocument} from 'react-addons-test-utils';\nimport {Account} from 'nylas-exports';\n\nimport PreferencesAccountDetails from '../lib/tabs/preferences-account-details';\n\n\nconst makeComponent = (props = {}) => {\n  return renderIntoDocument(<PreferencesAccountDetails {...props} />);\n};\n\nconst account = new Account({\n  id: 1,\n  clientId: 1,\n  name: 'someone',\n  emailAddress: 'someone@nylas.com',\n  aliases: [],\n  defaultAlias: null,\n})\n\ndescribe('PreferencesAccountDetails', function preferencesAccountDetails() {\n  beforeEach(() => {\n    this.account = account\n    this.onAccountUpdated = jasmine.createSpy('onAccountUpdated')\n    this.component = makeComponent({account, onAccountUpdated: this.onAccountUpdated})\n    spyOn(this.component, 'setState')\n  })\n\n  function assertAccountState(actual, expected) {\n    for (const key of Object.keys(expected)) {\n      expect(actual.account[key]).toEqual(expected[key]);\n    }\n  }\n\n  describe('_makeAlias', () => {\n    it('returns correct alias when empty string provided', () => {\n      const alias = this.component._makeAlias('', this.account)\n      expect(alias).toEqual('someone <someone@nylas.com>')\n    });\n\n    it('returns correct alias when only the name provided', () => {\n      const alias = this.component._makeAlias('Chad', this.account)\n      expect(alias).toEqual('Chad <someone@nylas.com>')\n    });\n\n    it('returns correct alias when email provided', () => {\n      const alias = this.component._makeAlias('keith@nylas.com', this.account)\n      expect(alias).toEqual('someone <keith@nylas.com>')\n    });\n\n    it('returns correct alias if name and email provided', () => {\n      const alias = this.component._makeAlias('Donald donald@nylas.com', this.account)\n      expect(alias).toEqual('Donald <donald@nylas.com>')\n    });\n\n    it('returns correct alias if alias provided', () => {\n      const alias = this.component._makeAlias('Donald <donald@nylas.com>', this.account)\n      expect(alias).toEqual('Donald <donald@nylas.com>')\n    });\n  });\n\n  describe('_setState', () => {\n    it('sets the correct state', () => {\n      this.component._setState({aliases: ['something']})\n      assertAccountState(this.component.setState.calls[0].args[0], {aliases: ['something']})\n    });\n  });\n\n  describe('_onDefaultAliasSelected', () => {\n    it('sets the default alias correctly when set to None', () => {\n      this.component._onDefaultAliasSelected({target: {value: 'None'}})\n      assertAccountState(this.component.setState.calls[0].args[0], {defaultAlias: null})\n    });\n\n    it('sets the default alias correctly when set to any value', () => {\n      this.component._onDefaultAliasSelected({target: {value: 'my alias'}})\n      assertAccountState(this.component.setState.calls[0].args[0], {defaultAlias: 'my alias'})\n    });\n  });\n\n  describe('alias handlers', () => {\n    beforeEach(() => {\n      this.currentAlias = 'juan <blah@nylas>'\n      this.newAlias = 'some <alias@nylas.com>';\n      this.account.aliases = [\n        this.currentAlias,\n      ]\n      this.component = makeComponent({account: this.account, onAccountUpdated: this.onAccountUpdated})\n      spyOn(this.component, '_makeAlias').andCallFake((alias) => alias)\n      spyOn(this.component, 'setState')\n    })\n    describe('_onAccountAliasCreated', () => {\n      it('creates alias correctly', () => {\n        this.component._onAccountAliasCreated(this.newAlias)\n        assertAccountState(this.component.setState.calls[0].args[0],\n                           {aliases: [this.currentAlias, this.newAlias]})\n      });\n    });\n\n    describe('_onAccountAliasUpdated', () => {\n      it('updates alias correctly when no default alias present', () => {\n        this.component._onAccountAliasUpdated(this.newAlias, this.currentAlias, 0)\n        assertAccountState(this.component.setState.calls[0].args[0],\n                           {aliases: [this.newAlias]})\n      });\n\n      it('updates alias correctly when default alias present and it is being updated', () => {\n        this.account.defaultAlias = this.currentAlias\n        this.component = makeComponent({account: this.account, onAccountUpdated: this.onAccountUpdated})\n        spyOn(this.component, '_makeAlias').andCallFake((alias) => alias)\n        spyOn(this.component, 'setState')\n\n        this.component._onAccountAliasUpdated(this.newAlias, this.currentAlias, 0)\n        assertAccountState(this.component.setState.calls[0].args[0],\n                           {aliases: [this.newAlias], defaultAlias: this.newAlias})\n      });\n\n      it('updates alias correctly when default alias present and it is not being updated', () => {\n        this.account.defaultAlias = this.currentAlias\n        this.account.aliases.push('otheralias')\n        this.component = makeComponent({account: this.account, onAccountUpdated: this.onAccountUpdated})\n        spyOn(this.component, '_makeAlias').andCallFake((alias) => alias)\n        spyOn(this.component, 'setState')\n\n        this.component._onAccountAliasUpdated(this.newAlias, 'otheralias', 1)\n        assertAccountState(\n          this.component.setState.calls[0].args[0],\n          {aliases: [this.currentAlias, this.newAlias], defaultAlias: this.currentAlias}\n        )\n      });\n    });\n\n\n    describe('_onAccountAliasRemoved', () => {\n      it('removes alias correctly when no default alias present', () => {\n        this.component._onAccountAliasRemoved(this.currentAlias, 0)\n        assertAccountState(this.component.setState.calls[0].args[0], {aliases: []})\n      });\n\n      it('removes alias correctly when default alias present and it is being removed', () => {\n        this.account.defaultAlias = this.currentAlias\n        this.component = makeComponent({account: this.account, onAccountUpdated: this.onAccountUpdated})\n        spyOn(this.component, '_makeAlias').andCallFake((alias) => alias)\n        spyOn(this.component, 'setState')\n\n        this.component._onAccountAliasRemoved(this.currentAlias, 0)\n        assertAccountState(this.component.setState.calls[0].args[0],\n                           {aliases: [], defaultAlias: null})\n      });\n\n      it('removes alias correctly when default alias present and it is not being removed', () => {\n        this.account.defaultAlias = this.currentAlias\n        this.account.aliases.push('otheralias')\n        this.component = makeComponent({account: this.account, onAccountUpdated: this.onAccountUpdated})\n        spyOn(this.component, '_makeAlias').andCallFake((alias) => alias)\n        spyOn(this.component, 'setState')\n\n        this.component._onAccountAliasRemoved('otheralias', 1)\n        assertAccountState(\n          this.component.setState.calls[0].args[0],\n          {aliases: [this.currentAlias], defaultAlias: this.currentAlias}\n        )\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/stylesheets/preferences-accounts.less",
    "content": "@import \"ui-variables\";\n\n// Preferences Specific\n.preferences-wrap {\n  .container-accounts {\n    width: 70%;\n    min-width: 420px;\n    margin: 0 auto;\n    .accounts-content {\n      display: flex;\n      justify-content: center;\n\n      .account-list {\n        display: flex;\n        flex-direction: column;\n        height: auto;\n        width: 400px;\n\n        .items-wrapper {\n          flex: 1;\n        }\n\n        .account {\n          padding: 10px;\n          border-bottom: 1px solid @border-color-divider;\n        }\n\n        .list-item:not(.selected) .sync-error {\n          color: @color-error;\n        }\n\n        .account-name {\n          font-size: @font-size-large;\n          cursor: default;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          vertical-align: middle;\n        }\n\n        .account-subtext {\n          font-size: @font-size-small;\n          cursor: default;\n        }\n\n        .btn-editable-list {\n          height: 37px;\n          width: 37px;\n          line-height: 37px;\n          font-size: 1em;\n        }\n      }\n\n      .account-details {\n        width: 400px;\n        padding: 20px;\n        padding-left: @spacing-standard * 2.25;\n        padding-right: @spacing-standard * 2.25;\n        background-color: @gray-lighter;\n        border-top: 1px solid @border-color-divider;\n        border-right: 1px solid @border-color-divider;\n        border-bottom: 1px solid @border-color-divider;\n\n        .key-commands-region {\n          height: inherit;\n        }\n\n        .items-wrapper {\n          height: 140px;\n        }\n\n        .account-error-detail {\n          display: flex;\n          flex-direction: column;\n          background: linear-gradient(to top, #ca2541 0%, #d55268 100%);\n\n          .action {\n            flex-shrink: 0;\n            background-color: rgba(0,0,0,0.15);\n            text-align: center;\n            padding: 3px @padding-base-horizontal;\n            color: @text-color-inverse\n          }\n          .action:hover {\n            background-color: rgba(255,255,255,0.15);\n            text-decoration:none;\n          }\n          .message {\n            flex-grow: 1;\n            padding: 3px @padding-base-horizontal;\n            color: @text-color-inverse\n          }\n        }\n\n        .newsletter {\n          padding-top: @padding-base-vertical * 2;\n          input[type=checkbox] { margin: 0; position: relative; top: 0; }\n        }\n\n        &>h3 {\n          font-size: 1.2em;\n          &:first-child {\n            margin-top: 0;\n          }\n        }\n\n        &>input {\n          font-size: 0.9em;\n          width: 100%;\n        }\n\n        .default-alias-selector {\n          padding-top: @padding-base-vertical * 3;\n          padding-bottom: @padding-base-vertical;\n\n          &>select {\n            font-size: 0.9em;\n            margin-left:0;\n            width: 100%;\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/stylesheets/preferences-identity.less",
    "content": "@import \"ui-variables\";\n\n@keyframes spin { 100% { -webkit-transform: rotate(360deg); transform:rotate(360deg); } }\n\n.container-identity {\n  max-width: 887px;\n  min-width: 530px;\n  margin: auto;\n\n  .id-header {\n    color: @text-color-very-subtle;\n    margin-bottom: @padding-base-vertical * 2;\n  }\n\n  .refresh {\n    float: right;\n    color: @text-color-very-subtle;\n    margin-bottom: @padding-base-vertical * 2;\n    img { background-color: @text-color-very-subtle; }\n  }\n  .refresh.spinning img {\n    animation:spin 1.4s linear infinite;\n  }\n\n  .identity-content-box {\n    display: flex;\n    flex-direction: column;\n    align-items: flex-start;\n    color: @text-color-subtle;\n    border-radius: @border-radius-large;\n    border: 1px solid @border-color-primary;\n    background-color: @background-secondary;\n\n    .row {\n      display: flex;\n      align-items: center;\n      width: 100%;\n    }\n\n    .padded {\n      display: block;\n      padding: 20px;\n      padding-left: 137px;\n      border-top: 1px solid @border-color-primary;\n    }\n\n    .btn {\n      width: 180px;\n      &.minor-width {\n        width: 120px;\n      }\n      text-align: center;\n      margin-right: @padding-base-horizontal;\n      margin-bottom: @padding-base-horizontal;\n    }\n    .identity-actions {\n      margin-top: @padding-small-vertical + 1;\n    }\n    .subscription-actions {\n      margin-top: 20px;\n    }\n\n    .info-row {\n      padding: 30px;\n      .logo {\n        margin-right: 30px;\n      }\n      .identity-info {\n        flex: 1;\n        line-height: 1.9em;\n        .name {\n          font-size: 1.2em;\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/stylesheets/preferences-mail-rules.less",
    "content": "@import \"ui-variables\";\n\n.container-mail-rules {\n  max-width: 800px;\n  margin: 0 auto;\n\n  .empty-list {\n    height: 376px;\n    width: inherit;\n    background-color: @background-secondary;\n    border: 1px solid @border-color-divider;\n    text-align: center;\n\n    .icon-mail-rules {\n      margin-top: 80px;\n    }\n\n    h2 {\n      color: @text-color-very-subtle;\n    }\n\n    .btn {\n      margin-top: 10px;\n    }\n  }\n\n  .rule-list {\n    position: relative;\n    height: inherit;\n    width: inherit;\n\n    .items-wrapper {\n      min-width:200px;\n      height: 350px;\n    }\n    .item-rule-disabled {\n      color: @color-error;\n      padding: 4px 10px;\n      border-bottom: 1px solid @border-color-divider;\n    }\n    .selected .item-rule-disabled {\n      color: @component-active-bg;\n    }\n    .btn-editable-list {\n      height: 37px;\n      width: 37px;\n      line-height: 37px;\n      font-size: 1em;\n    }\n  }\n  .rule-detail {\n    flex: 1;\n    cursor: default;\n    background-color: @background-secondary;\n    border: 1px solid @border-color-divider;\n    border-left: 0;\n\n    .disabled-reason {\n      padding: @padding-base-vertical * 2 @padding-base-vertical * 2;\n      background-color: fade(@background-color-error, 30%);\n      border-bottom: 1px solid @background-color-error;\n      margin-bottom: @padding-base-vertical;\n      .btn {\n        margin-left:@padding-base-horizontal * 2;\n        float:right;\n      }\n    }\n    .inner {\n      padding: @padding-base-vertical @padding-base-horizontal;\n    }\n    .no-selection {\n      color: @text-color-very-subtle;\n      text-align: center;\n      padding:100px;\n    }\n\n    .well {\n      background-color: @background-primary;\n      border: 1px solid @border-color-divider;\n      margin: @padding-base-vertical 0;\n      font-size:0.9em;\n\n      .well-row {\n        padding: @padding-base-vertical @padding-base-horizontal;\n        border-bottom: 1px solid @border-color-divider;\n        select, input {\n          margin:@padding-base-vertical / 4 @padding-base-horizontal / 2;\n          &:first-child {\n            margin-left: 0;\n          }\n          &:last-child {\n            margin-right: 0;\n          }\n        }\n        select {\n          max-width:170px;\n        }\n        input {\n          width:200px;\n        }\n        .actions {\n          white-space: nowrap;\n          vertical-align: middle;\n          .btn {\n            padding: 0;\n            border-radius: 100%;\n            text-align: center;\n            margin-left:10px;\n            margin-top:1px;\n            width:24px;\n            line-height: 24px;\n            height: 24px;\n          }\n        }\n      }\n      .well-row:last-child {\n        border-bottom: none;\n      }\n    }\n  }\n  .footer {\n    border-top:1px solid @border-color-divider;\n    background-color: @background-secondary;\n    padding: @padding-base-vertical*3 @padding-base-horizontal;\n    .btn {\n      margin-right: @padding-base-horizontal;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/preferences/stylesheets/preferences.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n// Preferences Specific\n\n.preferences-wrap {\n  input[type=checkbox] { margin: 0 7px 0 0; position: relative; top: -1px; }\n  input[type=radio] { margin: 0 7px 0 0; position: relative; top: -1px; }\n  select { margin: 4px 0 0 8px; }\n\n  height: 100%;\n  background-color: @background-primary;\n  color: @text-color;\n\n  h6 {\n    color: @text-color-very-subtle;\n    margin-top: 20px;\n    margin-bottom: 10px;\n  }\n\n  section:first-child h6:first-child {\n    margin-top: 0;\n  }\n\n  p {\n    color: @text-color-very-subtle;\n    font-size: @font-size-smaller;\n\n    a {\n      color: @text-color-very-subtle;\n      font-weight: bold;\n      text-decoration: underline;\n    }\n  }\n  *[contenteditable] {\n    p {\n      color: @text-color;\n      font-size: @font-size-base;\n    }\n  }\n\n  section {\n    padding-bottom: @padding-base-vertical;\n    .item {\n      padding-top: @padding-small-vertical;\n      padding-bottom: @padding-small-vertical;\n    }\n  }\n\n  .container-preference-tabs {\n    width: 100%;\n    background-color: @source-list-bg;\n    border-bottom: 1px solid @border-color-divider;\n\n    .preferences-tabs {\n      padding-top: 10px;\n      background-color: @source-list-bg;\n\n      .item {\n        cursor: default;\n        margin: 0 auto;\n        flex: 1.3;\n        min-width: 54px;\n        max-width: 120px;\n        text-align: center;\n\n        &:active {\n          img {\n            -webkit-filter: brightness(40%);\n          }\n        }\n\n        &.active {\n          background: darken(@background-primary, 10%);\n          border-radius: @border-radius-large @border-radius-large 0 0;\n          box-shadow: @shadow-border;\n        }\n\n        .tab-icon {\n          padding-top: 8px;\n          padding-bottom: 8px;\n          display: block;\n          margin: auto;\n        }\n\n        .name {\n          padding: @padding-base-vertical @padding-base-horizontal * 0.3 @padding-large-vertical @padding-base-horizontal * 0.3;\n          white-space: nowrap;\n          overflow: hidden;\n          text-overflow: ellipsis;\n          font-size: @font-size-small;\n          line-height: @font-size-small;\n        }\n      }\n    }\n  }\n\n  .preferences-content {\n    flex: 8;\n    background-color: lighten(@background-secondary, 1%);\n    &>.scroll-region-content {\n      padding: @padding-large-vertical * 3 @padding-large-horizontal * 3;\n    }\n    .container-dropdown {\n      margin: 5px 0;\n      .dropdown {\n        padding-left: 10px;\n      }\n    }\n\n  }\n\n  .container-general {\n    width: 40%;\n    min-width: 400px;\n    margin: 0 auto;\n\n    .local-data {\n      .btn {\n        margin: 4px 0 0 8px;\n        border: 1px solid @dropdown-default-border-color;\n        box-shadow: none;\n        border-radius: 5px;\n      }\n    }\n  }\n\n  .container-appearance {\n    width: 400px;\n    margin: 0 auto;\n\n    .appearance-mode-switch {\n      max-width: 400px;\n      text-align: right;\n      margin: 10px 0;\n\n      .appearance-mode {\n        background-color: @background-off-primary;;\n        border-radius: 10px;\n        border: 1px solid @background-tertiary;\n        text-align: center;\n        flex: 1;\n        padding: 25px;\n        padding-bottom: 9px;\n        margin-right: 10px;\n        margin-bottom: 7px;\n        margin-top: 0;\n        img {\n          background-color: @background-tertiary;\n        }\n        div {\n          margin-top: 15px;\n          text-transform: capitalize;\n          cursor: default;\n        }\n        &:last-child {\n          margin-right: 0;\n        }\n      }\n      .appearance-mode.active {\n        border: 1px solid @component-active-color;\n        color: @component-active-color;\n        img { background-color: @component-active-color; }\n      }\n    }\n  }\n\n  .container-keymaps {\n    width: 40%;\n    min-width: 460px;\n    margin: 0 auto;\n\n    .col-left {\n      text-align: right;\n      flex: 1;\n      margin-right: 20px;\n    }\n    .col-right {\n      text-align: left;\n      flex: 1;\n      select {\n          width: 75%;\n      }\n    }\n\n    .shortcut-section-title {\n      border-bottom: 1px solid @border-color-divider;\n      margin: @padding-large-vertical * 1.5 0;\n    }\n\n    .shortcut {\n      padding: 3px 0;\n      color: @text-color-very-subtle;\n      .values {\n        font-family: monospace;\n        font-weight: 600;\n        color: @text-color;\n        display: inline-block;\n        padding-left: @padding-small-horizontal;\n        padding-right: @padding-small-horizontal;\n        cursor: text;\n\n        .shortcut-value {\n          .then {\n            font-size:0.9em;\n            color: @text-color-very-subtle;\n          }\n          &:after {\n            content: \", \"\n          }\n          &:last-child:after {\n            content: \"\";\n          }\n        }\n      }\n\n      &.editing {\n        .values {\n          background: @input-bg;\n          color: @component-active-color;\n\n          border-radius: @border-radius-base;\n          outline: 1px solid @input-border-color;\n        }\n      }\n\n    }\n  }\n\n  .platform-note {\n    padding: @padding-base-vertical @padding-base-horizontal;\n    background: fade(@black, 4%);\n    border-left: 3px solid @color-info;\n    margin: @padding-base-vertical 0;\n    font-size: 0.95em;\n    &:before {\n      color: @color-info;\n      font-weight: 600;\n      content: \"NOTE: \";\n    }\n  }\n  .platform-linux-only {\n    display: none;\n  }\n}\n\nbody.platform-win32 {\n  .preferences-wrap {\n    .well {\n      border-radius: 0;\n    }\n    .container-appearance {\n      .appearance-mode {\n        border-radius: 0;\n      }\n    }\n  }\n}\n\nbody.platform-linux {\n  .preferences-wrap {\n    .platform-linux-only {\n      display: block;\n    }\n  }\n}\n\n@media (-webkit-min-device-pixel-ratio: 2) {\n  .preferences-tabs {\n    .tab-icon {\n      padding-top: 15px !important;\n    }\n  }\n}\n\n@media (max-width: 600px) {\n  .preferences-tabs .item .name {\n    display:none;\n  }\n  .preferences-wrap .preferences-content > .scroll-region-content {\n    padding-left: @padding-large-horizontal * 1;\n    padding-right: @padding-large-horizontal * 1;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/print/lib/main.es6",
    "content": "import Printer from './printer';\n\nlet printer = null;\nexport function activate() {\n  printer = new Printer();\n}\n\nexport function deactivate() {\n  if (printer) printer.deactivate();\n}\n\nexport function serialize() {\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/print/lib/print-window.es6",
    "content": "import path from 'path';\nimport fs from 'fs';\nimport {remote} from 'electron';\n\nconst {app, BrowserWindow} = remote;\n\nexport default class PrintWindow {\n\n  constructor({subject, account, participants, styleTags, htmlContent, printMessages}) {\n    // This script will create the print prompt when loaded. We can also call\n    // print directly from this process, but inside print.js we can make sure to\n    // call window.print() after we've cleaned up the dom for printing\n    const scriptPath = path.join(__dirname, '..', 'static', 'print.js');\n    const stylesPath = path.join(__dirname, '..', 'static', 'print-styles.css');\n    const imgPath = path.join(__dirname, '..', 'assets', 'nylas-print-logo.png');\n    const participantsHtml = participants.map((part) => {\n      return (`<li class=\"participant\"><span>${part.name} &lt;${part.email}&gt;</span></li>`);\n    }).join('');\n\n    const content = (`\n      <html>\n        <head>\n          <meta charset=\"utf-8\">\n          ${styleTags}\n          <link rel=\"stylesheet\" type=\"text/css\" href=\"${stylesPath}\">\n        </head>\n        <body>\n          <div id=\"print-header\">\n            <div onClick=\"continueAndPrint()\" id=\"print-button\">\n              Print\n            </div>\n            <div class=\"logo-wrapper\">\n              <img src=\"${imgPath}\" alt=\"nylas-logo\"/>\n              <span class=\"account\">${account.name} &lt;${account.email}&gt;</span>\n            </div>\n            <h1>${subject}</h1>\n          <div class=\"participants\">\n            <ul>\n              ${participantsHtml}\n            </ul>\n          </div>\n          </div>\n          ${htmlContent}\n          <script type=\"text/javascript\">\n            window.printMessages = ${printMessages}\n          </script>\n          <script type=\"text/javascript\" src=\"${scriptPath}\"></script>\n        </body>\n      </html>\n    `);\n\n    this.tmpFile = path.join(app.getPath('temp'), 'print.html');\n    this.browserWin = new BrowserWindow({\n      width: 800,\n      height: 600,\n      title: `Print - ${subject}`,\n      webPreferences: {\n        nodeIntegration: false,\n      },\n    });\n    fs.writeFileSync(this.tmpFile, content);\n  }\n\n  /**\n   * Load our temp html file. Once the file is loaded it will run print.js, and\n   * that script will pop out the print dialog.\n   */\n  load() {\n    this.browserWin.loadURL(`file://${this.tmpFile}`);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/print/lib/printer.es6",
    "content": "import {AccountStore, Actions} from 'nylas-exports';\nimport PrintWindow from './print-window';\n\nclass Printer {\n\n  constructor() {\n    this.unsub = Actions.printThread.listen(this._printThread);\n  }\n\n  _printThread(thread, htmlContent) {\n    if (!thread) throw new Error('Printing: No thread active!');\n    const account = AccountStore.accountForId(thread.accountId)\n\n    // Get the <nylas-styles> tag present in the document\n    const styleTag = document.getElementsByTagName('nylas-styles')[0];\n    // These iframes should correspond to the message iframes when a thread is\n    // focused\n    const iframes = document.getElementsByTagName('iframe');\n    // Grab the html inside the iframes\n    const messagesHtml = [].slice.call(iframes).map((iframe) => {\n      return iframe.contentDocument.documentElement.innerHTML;\n    });\n\n    const win = new PrintWindow({\n      subject: thread.subject,\n      account: {\n        name: account.name,\n        email: account.emailAddress,\n      },\n      participants: thread.participants,\n      styleTags: styleTag.innerHTML,\n      htmlContent,\n      printMessages: JSON.stringify(messagesHtml),\n    });\n    win.load();\n  }\n\n  deactivate() {\n    this.unsub();\n  }\n}\n\nexport default Printer;\n"
  },
  {
    "path": "packages/client-app/internal_packages/print/package.json",
    "content": "{\n  \"name\": \"print\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Print\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/print/static/print-styles.css",
    "content": "body {\n  overflow: auto !important;\n}\n#message-list {\n  background: transparent;\n}\n#print-button {\n  float:right;\n  margin-left: 10px;\n\n  /* From main button styles: */\n  padding: 0 0.8em;\n  border-radius: 3px;\n  display: inline-block;\n  height: 1.9em;\n  line-height: 1.9em;\n  font-size: 13.02px;\n  cursor: default;\n  color: #231f20;\n  position: relative;\n  color: #ffffff;\n  font-weight: 500;\n  background: linear-gradient(to bottom, #6bb1f9 0%, #0a80ff 100%);\n  box-shadow: none;\n  border: 1px solid #3878fa;\n}\n#print-header {\n  padding: 15px 20px 0 20px;\n}\n#print-header img {\n  zoom: 0.5;\n}\n#print-header .logo-wrapper {\n  display: flex;\n  align-items: center;\n  font-family: \"Nylas-Pro\", \"Helvetica\", \"Lucidia Grande\", sans-serif !important;\n}\n#print-header h1 {\n  font-size: 1.5em !important;\n  font-family: \"Nylas-Pro\", \"Helvetica\", \"Lucidia Grande\", sans-serif !important;\n}\n#print-header .account {\n  margin-left: auto;\n  font-size: 0.8em !important;\n}\n#print-header .participant {\n  font-size: 0.7em;\n  font-family: \"Nylas-Pro\", \"Helvetica\", \"Lucidia Grande\", sans-serif !important;\n}\n\n/* Elements to hide */\n.message-subject-wrap {\n  display: none !important;\n}\n.minified-bundle,.headers,.scrollbar-track,.message-icons-wrap,.header-toggle-control {\n  display: none !important;\n}\n.message-actions-wrap {\n  display: none;\n}\n.collapsed.message-item-wrap,.draft.message-item-wrap {\n  display: none !important;\n}\n.message-item-area>div {\n  display: none !important;\n}\n.quoted-text-control, .footer-reply-area-wrap {\n  display: none;\n}\n\n@media only print {\n  body,#message-list,.message-item-wrap,.message-item-white-wrap,\n  .message-item-area,.inbox-html-wrapper {\n    display: block !important;\n    width: auto !important;\n    height: auto !important;\n    overflow: visible !important;\n  }\n  #message-list {\n    min-height: initial;\n  }\n  #print-header {\n    padding: 0;\n  }\n  #print-header .account {\n    font-size: 0.7em;\n  }\n  .message-item-wrap {\n    display: block;\n  }\n  .message-item-area>span {\n    page-break-before: avoid;\n  }\n  .message-item-area>header {\n    page-break-after: avoid;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/print/static/print.js",
    "content": "(function() {\n  function rebuildMessages(messageNodes, messages) {\n    // Simply insert the message html inside the appropriate node\n    for (var idx = 0; idx < messageNodes.length; idx++) {\n      var msgNode = messageNodes[idx];\n      var msgHtml = messages[idx];\n      msgNode.innerHTML = msgHtml;\n    }\n  }\n\n  function removeClassFromNodes(nodeList, className) {\n    for (var idx = 0; idx < nodeList.length; idx++) {\n      var node = nodeList[idx];\n      var re = new RegExp('\\\\b' + className + '\\\\b', 'g');\n      node.className = node.className.replace(re, '');\n    }\n  }\n\n  function removeScrollClasses() {\n    var scrollRegions = document.querySelectorAll('.scroll-region');\n    var scrollContents = document.querySelectorAll('.scroll-region-content');\n    var scrollContentInners = document.querySelectorAll('.scroll-region-content-inner');\n    removeClassFromNodes(scrollRegions, 'scroll-region');\n    removeClassFromNodes(scrollContents, 'scroll-region-content');\n    removeClassFromNodes(scrollContentInners, 'scroll-region-content-inner');\n  }\n\n  function continueAndPrint() {\n    document.getElementById('print-button').style.display = 'none';\n    window.requestAnimationFrame(function() {\n      window.print();\n      // Close this print window after selecting to print\n      // This is really hackish but appears to be the only working solution\n      setTimeout(window.close, 500);\n    });\n  }\n\n  var messageNodes = document.querySelectorAll('.message-item-area>span');\n\n  removeScrollClasses();\n  rebuildMessages(messageNodes, window.printMessages);\n\n  window.continueAndPrint = continueAndPrint;\n})();\n"
  },
  {
    "path": "packages/client-app/internal_packages/remove-tracking-pixels/lib/main.es6",
    "content": "/* eslint no-cond-assign: 0 */\n\nimport {\n  ExtensionRegistry,\n  MessageViewExtension,\n  ComposerExtension,\n  RegExpUtils,\n} from 'nylas-exports';\n\nconst TrackingBlacklist = [{\n  name: 'Sidekick',\n  pattern: 't.signaux',\n  homepage: 'http://getsidekick.com',\n}, {\n  name: 'Sidekick',\n  pattern: 't.senal',\n  homepage: 'http://getsidekick.com',\n}, {\n  name: 'Sidekick',\n  pattern: 't.sidekickopen',\n  homepage: 'http://getsidekick.com',\n}, {\n  name: 'Sidekick',\n  pattern: 't.sigopn',\n  homepage: 'http://getsidekick.com',\n}, {\n  name: 'Banana Tag',\n  pattern: 'bl-1.com',\n  homepage: 'http://bananatag.com',\n}, {\n  name: 'Boomerang',\n  pattern: 'mailstat.us/tr',\n  homepage: 'http://boomeranggmail.com',\n}, {\n  name: 'Cirrus Inisght',\n  pattern: 'tracking.cirrusinsight.com',\n  homepage: 'http://cirrusinsight.com',\n}, {\n  name: 'Yesware',\n  pattern: 'app.yesware.com',\n  homepage: 'http://yesware.com',\n}, {\n  name: 'Yesware',\n  pattern: 't.yesware.com',\n  homepage: 'http://yesware.com',\n}, {\n  name: 'Streak',\n  pattern: 'mailfoogae.appspot.com',\n  homepage: 'http://streak.com',\n}, {\n  name: 'LaunchBit',\n  pattern: 'launchbit.com/taz-pixel',\n  homepage: 'http://launchbit.com',\n}, {\n  name: 'MailChimp',\n  pattern: 'list-manage.com/track',\n  homepage: 'http://mailchimp.com',\n}, {\n  name: 'Postmark',\n  pattern: 'cmail1.com/t',\n  homepage: 'http://postmarkapp.com',\n}, {\n  name: 'iContact',\n  pattern: 'click.icptrack.com/icp/',\n  homepage: 'http://icontact.com',\n}, {\n  name: 'Infusionsoft',\n  pattern: 'infusionsoft.com/app/emailOpened',\n  homepage: 'http://infusionsoft.com',\n}, {\n  name: 'Intercom',\n  pattern: 'via.intercom.io/o',\n  homepage: 'http://intercom.io',\n}, {\n  name: 'Mandrill',\n  pattern: 'mandrillapp.com/track',\n  homepage: 'http://mandrillapp.com',\n}, {\n  name: 'Hubspot',\n  pattern: 't.hsms06.com',\n  homepage: 'http://hubspot.com',\n}, {\n  name: 'RelateIQ',\n  pattern: 'app.relateiq.com/t.png',\n  homepage: 'http://relateiq.com',\n}, {\n  name: 'RJ Metrics',\n  pattern: 'go.rjmetrics.com',\n  homepage: 'http://rjmetrics.com',\n}, {\n  name: 'Mixpanel',\n  pattern: 'api.mixpanel.com/track',\n  homepage: 'http://mixpanel.com',\n}, {\n  name: 'Front App',\n  pattern: 'web.frontapp.com/api',\n  homepage: 'http://frontapp.com',\n}, {\n  name: 'Mailtrack.io',\n  pattern: 'mailtrack.io/trace',\n  homepage: 'http://mailtrack.io',\n}, {\n  name: 'Salesloft',\n  pattern: 'sdr.salesloft.com/email_trackers',\n  homepage: 'http://salesloft.com',\n}]\n\nexport function rejectImagesInBody(body, callback) {\n  const spliceRegions = [];\n  const regex = RegExpUtils.imageTagRegex();\n\n  // Identify img tags that should be cut\n  let result = null;\n  while ((result = regex.exec(body)) !== null) {\n    if (callback(result[1])) {\n      spliceRegions.push({start: result.index, end: result.index + result[0].length})\n    }\n  }\n  // Remove them all, from the end of the string to the start\n  let updated = body;\n  spliceRegions.reverse().forEach(({start, end}) => {\n    updated = updated.substr(0, start) + updated.substr(end);\n  });\n\n  return updated;\n}\n\nexport function removeTrackingPixels(message) {\n  const isFromMe = message.isFromMe();\n\n  message.body = rejectImagesInBody(message.body, (imageURL) => {\n    if (isFromMe) {\n      // If the image is sent by the user, remove all forms of tracking pixels.\n      // They could be viewing an email they sent with Salesloft, etc.\n      for (const item of TrackingBlacklist) {\n        if (imageURL.indexOf(item.pattern) >= 0) {\n          return true;\n        }\n      }\n    }\n\n    // Remove Nylas read receipt pixels for the current account. If this is a\n    // reply, our read receipt could still be in the body and could trigger\n    // additional opens. (isFromMe is not sufficient!)\n    if (imageURL.indexOf(`nylas.com/open/${message.accountId}`) >= 0) {\n      return true;\n    }\n    return false;\n  });\n}\n\nclass TrackingPixelsMessageExtension extends MessageViewExtension {\n  static formatMessageBody = ({message}) => {\n    removeTrackingPixels(message);\n  }\n}\n\nclass TrackingPixelsComposerExtension extends ComposerExtension {\n  static prepareNewDraft = ({draft}) => {\n    removeTrackingPixels(draft);\n  }\n}\n\n\nexport function activate() {\n  ExtensionRegistry.MessageView.register(TrackingPixelsMessageExtension);\n  ExtensionRegistry.Composer.register(TrackingPixelsComposerExtension);\n}\n\nexport function deactivate() {\n  ExtensionRegistry.MessageView.unregister(TrackingPixelsMessageExtension);\n  ExtensionRegistry.Composer.unregister(TrackingPixelsComposerExtension);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/remove-tracking-pixels/package.json",
    "content": "{\n  \"name\": \"remove-tracking-pixels\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"license\": \"GPL-3.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/remove-tracking-pixels/spec/fixtures/a-after.txt",
    "content": "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><p>Hey Ben,</p><p>\nI've noticed that we don't yet have an SLA in place with&nbsp;Nylas. Are you the right\nperson to be speaking with to make sure everything is set up on that end? If not,\ncould you please put me in touch with them, so that we can get you guys set up\ncorrectly as soon as possible?</p><p>Thanks!</p><p>Gleb Polyakov</p><p>Head of\nBusiness Development and Growth</p>After Pixel\n\n<br><br><signature>Sent from <a href=\"https://nylas.com/n1?ref=n1\">Nylas Mail</a>, the extensible, open source mail client.<br/></signature><div class=\"gmail_quote\">\n  On Apr 28 2016, at 2:14 pm, Ben Gotow (Careless) &lt;careless@foundry376.com&gt; wrote:\n  <br>\n  <blockquote class=\"gmail_quote\"\n    style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n    <body>nother mailA<br /><br />Sent from <a href=\"https://link.nylas.com/link/b5djvgcuhj6i3x8nm53d0vnjm/local-a84ad76e-006b/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">Nylas Mail</a>, the extensible, open source mail client.<br /><img width=\"0\" height=\"0\" style=\"border:0; width:0; height:0;\" src=\"https://link.nylas.com/open/b5djvgcuhj6i3x8nm53d0vnjm/local-a84ad76e-006b\" /><div>\n  On Apr 28 2016, at 1:46 pm, Ben Gotow (Careless) &lt;careless@foundry376.com&gt; wrote:\n  <br />\n  <blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n    Hi Ben this is just a test.<br /><br />Sent from <a href=\"https://link.nylas.com/link/b5djvgcuhj6i3x8nm53d0vnjm/local-aa39d95b-b883/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">Nylas Mail</a>, the extensible, open source mail client.<br /><img width=\"0\" height=\"0\" style=\"border:0; width:0; height:0;\" src=\"https://link.nylas.com/open/b5djvgcuhj6i3x8nm53d0vnjm/local-aa39d95b-b883\" /><div>\n  On Apr 26 2016, at 6:03 pm, Ben Gotow &lt;bengotow@gmail.com&gt; wrote:\n  <br />\n  <blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n    <p>To test this, send https://www.google.com/search?q=test@example.com to yourself from a client that allows plaintext or html editing.</p>\n<p><br />Ben Gotow<br />-----------------------------------<br /><a href=\"http://www.foundry376.com/\">http://www.foundry376.com/</a><br />bengotow@gmail.com<br />540-250-2334</p>\n  </blockquote>\n</div>\n  </blockquote>\n</div></body>\n  </blockquote>\n</div>"
  },
  {
    "path": "packages/client-app/internal_packages/remove-tracking-pixels/spec/fixtures/a-before.txt",
    "content": "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><p>Hey Ben,</p><p>\nI've noticed that we don't yet have an SLA in place with&nbsp;Nylas. Are you the right\nperson to be speaking with to make sure everything is set up on that end? If not,\ncould you please put me in touch with them, so that we can get you guys set up\ncorrectly as soon as possible?</p><p>Thanks!</p><p>Gleb Polyakov</p><p>Head of\nBusiness Development and Growth</p><img src=\"https://sdr.salesloft.com/email_trackers/8c8bea88-af43-4f66-bf78-a97ad73d7aec/open.gif\" alt=\"\" width=\"1\" height=\"1\">After Pixel\n\n<br><br><signature>Sent from <a href=\"https://nylas.com/n1?ref=n1\">Nylas Mail</a>, the extensible, open source mail client.<br/></signature><div class=\"gmail_quote\">\n  On Apr 28 2016, at 2:14 pm, Ben Gotow (Careless) &lt;careless@foundry376.com&gt; wrote:\n  <br>\n  <blockquote class=\"gmail_quote\"\n    style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n    <body>nother mailA<br /><br />Sent from <a href=\"https://link.nylas.com/link/b5djvgcuhj6i3x8nm53d0vnjm/local-a84ad76e-006b/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">Nylas Mail</a>, the extensible, open source mail client.<br /><img width=\"0\" height=\"0\" style=\"border:0; width:0; height:0;\" src=\"https://link.nylas.com/open/b5djvgcuhj6i3x8nm53d0vnjm/local-a84ad76e-006b\" /><div>\n  On Apr 28 2016, at 1:46 pm, Ben Gotow (Careless) &lt;careless@foundry376.com&gt; wrote:\n  <br />\n  <blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n    Hi Ben this is just a test.<br /><br />Sent from <a href=\"https://link.nylas.com/link/b5djvgcuhj6i3x8nm53d0vnjm/local-aa39d95b-b883/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">Nylas Mail</a>, the extensible, open source mail client.<br /><img width=\"0\" height=\"0\" style=\"border:0; width:0; height:0;\" src=\"https://link.nylas.com/open/b5djvgcuhj6i3x8nm53d0vnjm/local-aa39d95b-b883\" /><div>\n  On Apr 26 2016, at 6:03 pm, Ben Gotow &lt;bengotow@gmail.com&gt; wrote:\n  <br />\n  <blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n    <p>To test this, send https://www.google.com/search?q=test@example.com to yourself from a client that allows plaintext or html editing.</p>\n<p><br />Ben Gotow<br />-----------------------------------<br /><a href=\"http://www.foundry376.com/\">http://www.foundry376.com/</a><br />bengotow@gmail.com<br />540-250-2334</p>\n  </blockquote>\n</div>\n  </blockquote>\n</div></body>\n  </blockquote>\n</div>\n"
  },
  {
    "path": "packages/client-app/internal_packages/remove-tracking-pixels/spec/fixtures/b-after.txt",
    "content": "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><p>Hey Ben,</p><p>\nThis is the reply! This tracking pixel should not be removed.\n<img src=\"https://nylas.com/open/abcd/zza1231231\" />\n<blockquote>\nThis is the email I sent!\n\n</blockquote>\n</div>"
  },
  {
    "path": "packages/client-app/internal_packages/remove-tracking-pixels/spec/fixtures/b-before.txt",
    "content": "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><p>Hey Ben,</p><p>\nThis is the reply! This tracking pixel should not be removed.\n<img src=\"https://nylas.com/open/abcd/zza1231231\" />\n<blockquote>\nThis is the email I sent!\n<img src=\"https://nylas.com/open/1234/12zxczxc123\" />\n</blockquote>\n</div>\n"
  },
  {
    "path": "packages/client-app/internal_packages/remove-tracking-pixels/spec/tracking-pixels-extension-spec.es6",
    "content": "/* eslint no-irregular-whitespace: 0 */\nimport fs from 'fs';\nimport {removeTrackingPixels} from '../lib/main';\n\nconst readFixture = (name) => {\n  return fs.readFileSync(`${__dirname}/fixtures/${name}`).toString().trim()\n}\n\ndescribe(\"TrackingPixelsExtension\", function trackingPixelsExtension() {\n  it(\"should splice all tracking pixels from emails I've sent\", () => {\n    const before = readFixture('a-before.txt');\n    const expected = readFixture('a-after.txt');\n\n    const message = {\n      body: before,\n      accountId: '1234',\n      isFromMe: () => true,\n    }\n    removeTrackingPixels(message);\n    expect(message.body).toEqual(expected);\n  });\n\n  it(\"should always splice Nylas read receipts for the current account id \", () => {\n    const before = readFixture('b-before.txt');\n    const expected = readFixture('b-after.txt');\n\n    const message = {\n      body: before,\n      accountId: '1234',\n      isFromMe: () => false,\n    }\n    removeTrackingPixels(message);\n    expect(message.body).toEqual(expected);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/screenshot-mode/assets/font-override.css",
    "content": "@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 200;\n  src: url('nylas://screenshot-mode/assets/BLOKKNeue-Regular.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 300;\n  src: url('nylas://screenshot-mode/assets/BLOKKNeue-Regular.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 400;\n  src: url('nylas://screenshot-mode/assets/BLOKKNeue-Regular.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 500;\n  src: url('nylas://screenshot-mode/assets/BLOKKNeue-Regular.otf');\n}\n\n@font-face {\n  font-family: 'Nylas-Pro';\n  font-style: normal;\n  font-weight: 600;\n  src: url('nylas://screenshot-mode/assets/BLOKKNeue-Regular.otf');\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/screenshot-mode/lib/main.coffee",
    "content": "fs = require 'fs'\n\nstyle = null\n\nmodule.exports =\n  activate: ->\n    NylasEnv.commands.add document.body, \"window:toggle-screenshot-mode\", ->\n      if not style\n        style = document.createElement('style')\n        style.innerText = fs.readFileSync(path.join(__dirname, '..', 'assets','font-override.css')).toString()\n\n      if style.parentElement\n        document.body.removeChild(style)\n      else\n        document.body.appendChild(style)\n\n  deactivate: ->\n    if style and style.parentElement\n      document.body.removeChild(style)\n\n  serialize: ->\n"
  },
  {
    "path": "packages/client-app/internal_packages/screenshot-mode/package.json",
    "content": "{\n  \"name\": \"screenshot-mode\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Replaces all text with blocks for taking screenshots without PII\",\n  \"license\": \"Proprietary\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"all\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/search-index/lib/contact-search-indexer.es6",
    "content": "import {\n  Contact,\n  ModelSearchIndexer,\n} from 'nylas-exports';\n\n\nconst INDEX_VERSION = 1;\n\nclass ContactSearchIndexer extends ModelSearchIndexer {\n\n  get MaxIndexSize() {\n    return 100000;\n  }\n\n  get ModelClass() {\n    return Contact;\n  }\n\n  get ConfigKey() {\n    return \"contactSearchIndexVersion\";\n  }\n\n  get IndexVersion() {\n    return INDEX_VERSION;\n  }\n\n  getIndexDataForModel(contact) {\n    return {\n      content: [\n        contact.name ? contact.name : '',\n        contact.email ? contact.email : '',\n        contact.email ? contact.email.replace('@', ' ') : '',\n      ].join(' '),\n    };\n  }\n}\n\nexport default new ContactSearchIndexer()\n"
  },
  {
    "path": "packages/client-app/internal_packages/search-index/lib/event-search-indexer.es6",
    "content": "import {Event, ModelSearchIndexer} from 'nylas-exports'\n\n\nconst INDEX_VERSION = 1\n\nclass EventSearchIndexer extends ModelSearchIndexer {\n\n  get MaxIndexSize() {\n    return 5000;\n  }\n\n  get ConfigKey() {\n    return 'eventSearchIndexVersion';\n  }\n\n  get IndexVersion() {\n    return INDEX_VERSION;\n  }\n\n  get ModelClass() {\n    return Event;\n  }\n\n  getIndexDataForModel(event) {\n    const {title, description, location, participants} = event\n    return {\n      title,\n      location,\n      description,\n      participants: participants\n        .map((c) => `${c.name || ''} ${c.email || ''}`)\n        .join(' '),\n    }\n  }\n}\n\nexport default new EventSearchIndexer()\n"
  },
  {
    "path": "packages/client-app/internal_packages/search-index/lib/main.es6",
    "content": "import ThreadSearchIndexStore from './thread-search-index-store'\nimport ContactSearchIndexer from './contact-search-indexer'\n// import EventSearchIndexer from './event-search-indexer'\n\n\nexport function activate() {\n  ThreadSearchIndexStore.activate()\n  ContactSearchIndexer.activate()\n  // TODO Calendar feature has been punted, we will disable this indexer for now\n  // EventSearchIndexer.activate(indexer)\n}\n\nexport function deactivate() {\n  ThreadSearchIndexStore.deactivate()\n  ContactSearchIndexer.deactivate()\n  // EventSearchIndexer.deactivate()\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/search-index/lib/thread-search-index-store.es6",
    "content": "import _ from 'underscore'\nimport {\n  Utils,\n  Thread,\n  AccountStore,\n  DatabaseStore,\n  SearchIndexScheduler,\n} from 'nylas-exports'\n\nconst MAX_INDEX_SIZE = 100000\nconst MESSAGE_BODY_LENGTH = 50000\nconst INDEX_VERSION = 2\n\nclass ThreadSearchIndexStore {\n\n  constructor() {\n    this.unsubscribers = []\n    this.indexer = SearchIndexScheduler;\n    this.threadsWaitingToBeIndexed = new Set();\n  }\n\n  activate() {\n    this.indexer.registerSearchableModel({\n      modelClass: Thread,\n      indexSize: MAX_INDEX_SIZE,\n      indexCallback: (model) => this.updateThreadIndex(model),\n      unindexCallback: (model) => this.unindexThread(model),\n    });\n\n    const date = Date.now();\n    console.log('Thread Search: Initializing thread search index...')\n\n    this.accountIds = _.pluck(AccountStore.accounts(), 'id')\n    this.initializeIndex()\n    .then(() => {\n      NylasEnv.config.set('threadSearchIndexVersion', INDEX_VERSION)\n      return Promise.resolve()\n    })\n    .then(() => {\n      console.log(`Thread Search: Index built successfully in ${((Date.now() - date) / 1000)}s`)\n      this.unsubscribers = [\n        AccountStore.listen(this.onAccountsChanged),\n        DatabaseStore.listen(this.onDataChanged),\n      ]\n    })\n  }\n\n  _isInvalidSize(size) {\n    return !size || size > MAX_INDEX_SIZE || size === 0;\n  }\n\n  /**\n   * We only want to build the entire index if:\n   * - It doesn't exist yet\n   * - It is too big\n   * - We bumped the index version\n   *\n   * Otherwise, we just want to index accounts that haven't been indexed yet.\n   * An account may not have been indexed if it is added and the app is closed\n   * before sync completes\n   */\n  initializeIndex() {\n    if (NylasEnv.config.get('threadSearchIndexVersion') !== INDEX_VERSION) {\n      return this.clearIndex()\n      .then(() => this.buildIndex(this.accountIds))\n    }\n\n    return this.buildIndex(this.accountIds);\n  }\n\n  /**\n   * When accounts change, we are only interested in knowing if an account has\n   * been added or removed\n   *\n   * - If an account has been added, we want to index its threads, but wait\n   *   until that account has been successfully synced\n   *\n   * - If an account has been removed, we want to remove its threads from the\n   *   index\n   *\n   * If the application is closed before sync is completed, the new account will\n   * be indexed via `initializeIndex`\n   */\n  onAccountsChanged = () => {\n    _.defer(() => {\n      const latestIds = _.pluck(AccountStore.accounts(), 'id')\n      if (_.isEqual(this.accountIds, latestIds)) {\n        return;\n      }\n      const date = Date.now()\n      console.log(`Thread Search: Updating thread search index for accounts ${latestIds}`)\n\n      const newIds = _.difference(latestIds, this.accountIds)\n      const removedIds = _.difference(this.accountIds, latestIds)\n      const promises = []\n      if (newIds.length > 0) {\n        promises.push(this.buildIndex(newIds))\n      }\n\n      if (removedIds.length > 0) {\n        promises.push(\n          Promise.all(removedIds.map(id => DatabaseStore.unindexModelsForAccount(id, Thread)))\n        )\n      }\n      this.accountIds = latestIds\n      Promise.all(promises)\n      .then(() => {\n        console.log(`Thread Search: Index updated successfully in ${((Date.now() - date) / 1000)}s`)\n      })\n    })\n  }\n\n  /**\n   * When a thread gets updated we will update the search index with the data\n   * from that thread if the account it belongs to is not being currently\n   * synced.\n   *\n   * When the account is successfully synced, its threads will be added to the\n   * index either via `onAccountsChanged` or via `initializeIndex` when the app\n   * starts\n   */\n  onDataChanged = (change) => {\n    if (change.objectClass !== Thread.name) {\n      return;\n    }\n    _.defer(async () => {\n      const {objects, type} = change\n      const threads = objects;\n\n      let promises = []\n      if (type === 'persist') {\n        const threadsToIndex = _.uniq(threads.filter(t => !this.threadsWaitingToBeIndexed.has(t.id)), false /* isSorted */, t => t.id);\n        const threadsIndexed = threads.filter(t => t.isSearchIndexed && this.threadsWaitingToBeIndexed.has(t.id));\n\n        for (const thread of threadsIndexed) {\n          this.threadsWaitingToBeIndexed.delete(thread.id);\n        }\n\n        if (threadsToIndex.length > 0) {\n          threadsToIndex.forEach(thread => {\n            // Mark already indexed threads as unindexed so that we re-index them\n            // with updates\n            thread.isSearchIndexed = false;\n            this.threadsWaitingToBeIndexed.add(thread.id);\n          })\n          await DatabaseStore.inTransaction(t => t.persistModels(threadsToIndex, {silent: true, affectsJoins: false}));\n          this.indexer.notifyHasIndexingToDo();\n        }\n      } else if (type === 'unpersist') {\n        promises = threads.map(thread => this.unindexThread(thread,\n                                                  {isBeingUnpersisted: true}))\n      }\n      Promise.all(promises)\n    })\n  }\n\n  buildIndex = (accountIds) => {\n    if (!accountIds || accountIds.length === 0) { return Promise.resolve() }\n    this.indexer.notifyHasIndexingToDo();\n    return Promise.resolve()\n  }\n\n  clearIndex() {\n    return (\n      DatabaseStore.dropSearchIndex(Thread)\n      .then(() => DatabaseStore.createSearchIndex(Thread))\n    )\n  }\n\n  indexThread = (thread) => {\n    return (\n      this.getIndexData(thread)\n      .then((indexData) => (\n        DatabaseStore.indexModel(thread, indexData)\n      ))\n    )\n  }\n\n  updateThreadIndex = (thread) => {\n    return (\n      this.getIndexData(thread)\n      .then((indexData) => (\n        DatabaseStore.updateModelIndex(thread, indexData)\n      ))\n    )\n  }\n\n  unindexThread = (thread, opts) => {\n    return DatabaseStore.unindexModel(thread, opts)\n  }\n\n  getIndexData(thread) {\n    return thread.messages().then((messages) => {\n      return {\n        bodies: messages\n           .map(({body, snippet}) => (!_.isString(body) ? {snippet} : {body}))\n           .map(({body, snippet}) => (\n             snippet || Utils.extractTextFromHtml(body, {maxLength: MESSAGE_BODY_LENGTH}).replace(/(\\s)+/g, ' ')\n           )).join(' '),\n        to: messages.map(({to, cc, bcc}) => (\n          _.uniq(to.concat(cc).concat(bcc).map(({name, email}) => `${name} ${email}`))\n        )).join(' '),\n        from: messages.map(({from}) => (\n          from.map(({name, email}) => `${name} ${email}`)\n        )).join(' '),\n      };\n    }).then(({bodies, to, from}) => {\n      const categories = (\n        thread.categories\n        .map(({displayName}) => displayName)\n        .join(' ')\n      )\n\n      return {\n        categories: categories,\n        to_: to,\n        from_: from,\n        body: bodies,\n        subject: thread.subject,\n      };\n    });\n  }\n\n  deactivate() {\n    this.unsubscribers.forEach(unsub => unsub())\n  }\n}\n\nexport default new ThreadSearchIndexStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/search-index/package.json",
    "content": "{\n  \"name\": \"search-index\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Keeps search index up to date\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"work\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-and-archive/lib/main.es6",
    "content": "import {ExtensionRegistry} from 'nylas-exports'\nimport * as SendAndArchiveExtension from './send-and-archive-extension'\n\n\nexport function activate() {\n  ExtensionRegistry.Composer.register(SendAndArchiveExtension)\n}\n\nexport function deactivate() {\n  ExtensionRegistry.Composer.unregister(SendAndArchiveExtension)\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-and-archive/lib/send-and-archive-extension.es6",
    "content": "import {\n  Actions,\n  Thread,\n  DatabaseStore,\n  TaskFactory,\n  SendDraftTask,\n} from 'nylas-exports'\n\n\nexport const name = 'SendAndArchiveExtension'\n\nexport function sendActions() {\n  return [{\n    title: 'Send and Archive',\n    iconUrl: 'nylas://send-and-archive/images/composer-archive@2x.png',\n    isAvailableForDraft({draft}) {\n      return draft.threadId != null\n    },\n    performSendAction({draft}) {\n      Actions.queueTask(new SendDraftTask(draft.clientId))\n      return DatabaseStore.modelify(Thread, [draft.threadId])\n      .then((threads) => {\n        Actions.archiveThreads({\n          source: \"Send and Archive\",\n          threads: threads,\n        })\n      })\n    },\n  }]\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-and-archive/package.json",
    "content": "{\n  \"name\": \"send-and-archive\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Adds a send and archive option to the composer.\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true,\n    \"work\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-and-archive/spec/send-and-archive-spec.coffee",
    "content": "describe \"SendAndArchive\", ->\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-and-archive/styles/send-and-archive.less",
    "content": ".send-and-archive {\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/lib/main.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports';\nimport {HasTutorialTip} from 'nylas-component-kit';\nimport SendLaterButton from './send-later-button';\nimport SendLaterStatus from './send-later-status';\n\nconst SendLaterButtonWithTip = HasTutorialTip(SendLaterButton, {\n  title: \"Send on your own schedule\",\n  instructions: \"Schedule this message to send at the ideal time. N1 makes it easy to control the fabric of spacetime!\",\n});\n\nexport function activate() {\n  ComponentRegistry.register(SendLaterButtonWithTip, {role: 'Composer:ActionButton'})\n  ComponentRegistry.register(SendLaterStatus, {role: 'DraftList:DraftStatus'})\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(SendLaterButtonWithTip)\n  ComponentRegistry.unregister(SendLaterStatus)\n}\n\nexport function serialize() {\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/lib/send-later-button.jsx",
    "content": "import fs from 'fs';\nimport React, {Component, PropTypes} from 'react'\nimport ReactDOM from 'react-dom'\nimport {Actions, DateUtils, NylasAPIHelpers, DraftHelpers, FeatureUsageStore} from 'nylas-exports'\nimport {RetinaImg, FeatureUsedUpModal} from 'nylas-component-kit'\nimport SendLaterPopover from './send-later-popover'\nimport {PLUGIN_ID, PLUGIN_NAME} from './send-later-constants'\nconst {NylasAPIRequest, NylasAPI, N1CloudAPI} = require('nylas-exports')\n\nconst OPEN_TRACKING_ID = NylasEnv.packages.pluginIdFor('open-tracking')\nconst LINK_TRACKING_ID = NylasEnv.packages.pluginIdFor('link-tracking')\n\nPromise.promisifyAll(fs);\n\nclass SendLaterButton extends Component {\n  static displayName = 'SendLaterButton';\n\n  static containerRequired = false;\n\n  static propTypes = {\n    draft: PropTypes.object.isRequired,\n    session: PropTypes.object.isRequired,\n    isValidDraft: PropTypes.func,\n  };\n\n  constructor() {\n    super();\n    this.state = {\n      saving: false,\n    };\n  }\n\n  componentDidMount() {\n    this.mounted = true;\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    if (nextState.saving !== this.state.saving) {\n      return true;\n    }\n    if (this._sendLaterDateForDraft(nextProps.draft) !== this._sendLaterDateForDraft(this.props.draft)) {\n      return true;\n    }\n    return false;\n  }\n\n  componentWillUnmount() {\n    this.mounted = false;\n  }\n\n  onAssignSendLaterDate = async (sendLaterDate, dateLabel) => {\n    if (!this.props.isValidDraft()) { return }\n    Actions.closePopover();\n\n    const currentSendLaterDate = this._sendLaterDateForDraft(this.props.draft)\n    if (currentSendLaterDate === sendLaterDate) { return }\n\n    // Only check for feature usage and record metrics if this draft is not\n    // already set to send later.\n    if (!currentSendLaterDate) {\n      const lexicon = {\n        displayName: \"Send Later\",\n        usedUpHeader: \"All delayed sends used\",\n        iconUrl: \"nylas://send-later/assets/ic-send-later-modal@2x.png\",\n      }\n\n      try {\n        await FeatureUsageStore.asyncUseFeature('send-later', {lexicon})\n      } catch (error) {\n        if (error instanceof FeatureUsageStore.NoProAccess) {\n          return\n        }\n      }\n\n      this.setState({saving: true});\n      const sendInSec = Math.round(((new Date(sendLaterDate)).valueOf() - Date.now()) / 1000)\n      Actions.recordUserEvent(\"Draft Send Later\", {\n        timeInSec: sendInSec,\n        timeInLog10Sec: Math.log10(sendInSec),\n        label: dateLabel,\n      });\n    }\n    this.onSetMetadata({expiration: sendLaterDate});\n  };\n\n  onCancelSendLater = () => {\n    Actions.closePopover();\n    this.onSetMetadata({expiration: null, cancelled: true});\n  };\n\n  onSetMetadata = async (metadatum = {}) => {\n    if (!this.mounted) { return; }\n    const {draft, session} = this.props;\n    const {expiration, ...extra} = metadatum\n    this.setState({saving: true});\n\n    try {\n      await NylasAPIHelpers.authPlugin(PLUGIN_ID, PLUGIN_NAME, draft.accountId);\n      if (!this.mounted) { return; }\n\n      if (!expiration) {\n        session.changes.addPluginMetadata(PLUGIN_ID, {\n          ...extra,\n          expiration: null,\n        });\n      } else {\n        session.changes.add({pristine: false})\n        const draftContents = await DraftHelpers.finalizeDraft(session);\n        const req = new NylasAPIRequest({\n          api: NylasAPI,\n          options: {\n            path: `/drafts/build`,\n            method: 'POST',\n            body: draftContents,\n            accountId: draft.accountId,\n            returnsModel: false,\n          },\n        });\n\n        const draftMessage = await req.run();\n        const uploads = [];\n\n        // Now, upload attachments to our blob service.\n        for (const attachment of draftContents.uploads) {\n          const uploadReq = new NylasAPIRequest({\n            api: N1CloudAPI,\n            options: {\n              path: `/blobs`,\n              method: 'PUT',\n              blob: true,\n              accountId: draft.accountId,\n              returnsModel: false,\n              formData: {\n                id: attachment.id,\n                file: fs.createReadStream(attachment.originPath),\n              },\n            },\n          });\n          await uploadReq.run();\n          attachment.serverId = `${draftContents.accountId}-${attachment.id}`;\n          uploads.push(attachment);\n        }\n        draftMessage.usesOpenTracking = draft.metadataForPluginId(OPEN_TRACKING_ID) != null;\n        draftMessage.usesLinkTracking = draft.metadataForPluginId(LINK_TRACKING_ID) != null;\n        session.changes.add({serverId: draftMessage.id})\n        session.changes.addPluginMetadata(PLUGIN_ID, {\n          ...draftMessage,\n          ...extra,\n          expiration,\n          uploads,\n        });\n      }\n\n      // TODO: This currently is only useful for syncing the draft metadata,\n      // even though we don't actually syncback drafts\n      Actions.finalizeDraftAndSyncbackMetadata(draft.clientId);\n\n      if (expiration && NylasEnv.isComposerWindow()) {\n        NylasEnv.close();\n      }\n    } catch (error) {\n      NylasEnv.reportError(error);\n      NylasEnv.showErrorDialog(`Sorry, we were unable to schedule this message. ${error.message}`);\n    }\n\n    if (!this.mounted) { return }\n    this.setState({saving: false})\n  }\n\n  onClick = () => {\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()\n    Actions.openPopover(\n      <SendLaterPopover\n        sendLaterDate={this._sendLaterDateForDraft(this.props.draft)}\n        onAssignSendLaterDate={this.onAssignSendLaterDate}\n        onCancelSendLater={this.onCancelSendLater}\n      />,\n      {originRect: buttonRect, direction: 'up'}\n    )\n  };\n\n  _sendLaterDateForDraft(draft) {\n    if (!draft) {\n      return null;\n    }\n    const messageMetadata = draft.metadataForPluginId(PLUGIN_ID) || {};\n    return messageMetadata.expiration;\n  }\n\n\n  render() {\n    let className = 'btn btn-toolbar btn-send-later';\n\n    if (this.state.saving) {\n      return (\n        <button className={className} title=\"Saving send date...\" tabIndex={-1} style={{order: -99}}>\n          <RetinaImg\n            name=\"inline-loading-spinner.gif\"\n            mode={RetinaImg.Mode.ContentDark}\n            style={{width: 14, height: 14}}\n          />\n        </button>\n      );\n    }\n\n    let sendLaterLabel = false;\n    const sendLaterDate = this._sendLaterDateForDraft(this.props.draft);\n\n    if (sendLaterDate) {\n      className += ' btn-enabled';\n      const momentDate = DateUtils.futureDateFromString(sendLaterDate);\n      if (momentDate) {\n        sendLaterLabel = <span className=\"at\">Sending in {momentDate.fromNow(true)}</span>;\n      } else {\n        sendLaterLabel = <span className=\"at\">Sending now</span>;\n      }\n    }\n    return (\n      <button className={className} title=\"Send later…\" onClick={this.onClick} tabIndex={-1} style={{order: -99}}>\n        <RetinaImg name=\"icon-composer-sendlater.png\" mode={RetinaImg.Mode.ContentIsMask} />\n        {sendLaterLabel}\n        <span>&nbsp;</span>\n        <RetinaImg name=\"icon-composer-dropdown.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n}\n\nexport default SendLaterButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/lib/send-later-constants.es6",
    "content": "import plugin from '../package.json'\n\nexport const PLUGIN_ID = plugin.name;\nexport const PLUGIN_NAME = \"Send Later\"\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/lib/send-later-popover.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport {DateUtils} from 'nylas-exports'\nimport {DatePickerPopover} from 'nylas-component-kit'\n\n\nconst SendLaterOptions = {\n  'In 1 hour': DateUtils.in1Hour,\n  'In 2 hours': DateUtils.in2Hours,\n  'Later today': DateUtils.laterToday,\n  'Tomorrow morning': DateUtils.tomorrow,\n  'Tomorrow evening': DateUtils.tomorrowEvening,\n  'This weekend': DateUtils.thisWeekend,\n  'Next week': DateUtils.nextWeek,\n}\n\nfunction SendLaterPopover(props) {\n  let footer;\n  const {onAssignSendLaterDate, onCancelSendLater, sendLaterDate} = props\n  const header = <span key=\"send-later-header\">Send later:</span>\n  if (sendLaterDate) {\n    footer = [\n      <div key=\"divider-unschedule\" className=\"divider\" />,\n      <div className=\"section\" key=\"cancel-section\">\n        <button className=\"btn btn-cancel\" onClick={onCancelSendLater}>\n          Unschedule Send\n        </button>\n      </div>,\n    ]\n  }\n\n  return (\n    <DatePickerPopover\n      className=\"send-later-popover\"\n      header={header}\n      footer={footer}\n      dateOptions={SendLaterOptions}\n      onSelectDate={onAssignSendLaterDate}\n    />\n  );\n}\nSendLaterPopover.displayName = 'SendLaterPopover';\nSendLaterPopover.propTypes = {\n  sendLaterDate: PropTypes.string,\n  onAssignSendLaterDate: PropTypes.func.isRequired,\n  onCancelSendLater: PropTypes.func.isRequired,\n};\n\nexport default SendLaterPopover\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/lib/send-later-status.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport moment from 'moment'\nimport {DateUtils, Actions} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit'\nimport {PLUGIN_ID} from './send-later-constants'\n\nconst {DATE_FORMAT_SHORT} = DateUtils\n\n\nexport default class SendLaterStatus extends Component {\n  static displayName = 'SendLaterStatus';\n\n  static propTypes = {\n    draft: PropTypes.object,\n  };\n\n  onCancelSendLater = () => {\n    Actions.setMetadata(this.props.draft, PLUGIN_ID, {expiration: null, cancelled: true});\n  };\n\n  render() {\n    const {draft} = this.props\n    const metadata = draft.metadataForPluginId(PLUGIN_ID)\n    if (metadata && metadata.expiration) {\n      const {expiration} = metadata\n      const formatted = DateUtils.format(moment(expiration), DATE_FORMAT_SHORT)\n      return (\n        <div className=\"send-later-status\">\n          <span className=\"time\">\n            {`Scheduled for ${formatted}`}\n          </span>\n          <RetinaImg\n            name=\"image-cancel-button.png\"\n            title=\"Cancel Send Later\"\n            onClick={this.onCancelSendLater}\n            mode={RetinaImg.Mode.ContentPreserve}\n          />\n        </div>\n      )\n    }\n    return <span />\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/package.json",
    "content": "{\n  \"name\": \"send-later\",\n  \"version\": \"1.0.0\",\n  \"title\": \"Send Later\",\n  \"description\": \"Choose to send emails at a specified time in the future.\",\n  \"isHiddenOnPluginsPage\": true,\n  \"icon\": \"./icon.png\",\n  \"main\": \"lib/main\",\n  \"supportedEnvs\": [\"local\", \"development\", \"staging\", \"production\"],\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true,\n    \"thread-popout\": true\n  },\n  \"isOptional\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/spec/send-later-button-spec.jsx",
    "content": "/* eslint react/no-render-return-value: 0 */\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport {findRenderedDOMComponentWithClass} from 'react-addons-test-utils';\n\nimport {DateUtils, NylasAPIHelpers, Actions} from 'nylas-exports'\nimport SendLaterButton from '../lib/send-later-button';\nimport {PLUGIN_ID, PLUGIN_NAME} from '../lib/send-later-constants'\n\nconst node = document.createElement('div');\n\nconst makeButton = (initialState, metadataValue) => {\n  const draft = {\n    accountId: 'accountId',\n    metadataForPluginId: () => metadataValue,\n  }\n  const session = {\n    changes: {\n      add: jasmine.createSpy('add'),\n      addPluginMetadata: jasmine.createSpy('addPluginMetadata'),\n    },\n  }\n  const button = ReactDOM.render(<SendLaterButton draft={draft} session={session} isValidDraft={() => true} />, node);\n  if (initialState) {\n    button.setState(initialState)\n  }\n  return button\n};\n\nxdescribe('SendLaterButton', function sendLaterButton() {\n  beforeEach(() => {\n    spyOn(DateUtils, 'format').andReturn('formatted')\n  });\n\n  describe('onSendLater', () => {\n    it('sets scheduled date to \"saving\" and adds plugin metadata to the session', () => {\n      const button = makeButton(null, {sendLaterDate: 'date'})\n      spyOn(button, 'setState')\n      spyOn(NylasAPIHelpers, 'authPlugin').andReturn(Promise.resolve());\n      spyOn(Actions, 'finalizeDraftAndSyncbackMetadata')\n\n      const sendLaterDate = {utc: () => 'utc'}\n      button.onSendLater(sendLaterDate)\n      advanceClock()\n\n      expect(button.setState).toHaveBeenCalledWith({saving: true})\n      expect(NylasAPIHelpers.authPlugin).toHaveBeenCalledWith(PLUGIN_ID, PLUGIN_NAME, button.props.draft.accountId)\n      expect(button.props.session.changes.addPluginMetadata).toHaveBeenCalledWith(PLUGIN_ID, {sendLaterDate})\n    });\n\n    it('displays dialog if an auth error occurs', () => {\n      const button = makeButton(null, {sendLaterDate: 'date'})\n      spyOn(button, 'setState')\n      spyOn(NylasEnv, 'reportError')\n      spyOn(NylasEnv, 'showErrorDialog')\n      spyOn(NylasAPIHelpers, 'authPlugin').andReturn(Promise.reject(new Error('Oh no!')))\n      spyOn(Actions, 'finalizeDraftAndSyncbackMetadata')\n      button.onSendLater({utc: () => 'utc'})\n      advanceClock()\n      expect(NylasEnv.reportError).toHaveBeenCalled()\n      expect(NylasEnv.showErrorDialog).toHaveBeenCalled()\n    });\n\n    it('closes the composer window if a sendLaterDate has been set', () => {\n      const button = makeButton(null, {sendLaterDate: 'date'})\n      spyOn(button, 'setState')\n      spyOn(NylasEnv, 'close')\n      spyOn(NylasAPIHelpers, 'authPlugin').andReturn(Promise.resolve());\n      spyOn(NylasEnv, 'isComposerWindow').andReturn(true)\n      spyOn(Actions, 'finalizeDraftAndSyncbackMetadata')\n      button.onSendLater({utc: () => 'utc'})\n      advanceClock()\n      expect(NylasEnv.close).toHaveBeenCalled()\n    });\n  });\n\n  describe('render', () => {\n    it('renders spinner if saving', () => {\n      const button = ReactDOM.findDOMNode(makeButton({saving: true}, null))\n      expect(button.title).toEqual('Saving send date...')\n    });\n\n    it('renders date if message is scheduled', () => {\n      spyOn(DateUtils, 'futureDateFromString').andReturn({fromNow: () => '5 minutes'})\n      const button = makeButton({saving: false}, {sendLaterDate: 'date'})\n      const span = ReactDOM.findDOMNode(findRenderedDOMComponentWithClass(button, 'at'))\n      expect(span.textContent).toEqual('Sending in 5 minutes')\n    });\n\n    it('does not render date if message is not scheduled', () => {\n      const button = makeButton(null, null)\n      expect(() => {\n        findRenderedDOMComponentWithClass(button, 'at')\n      }).toThrow()\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/spec/send-later-popover-spec.jsx",
    "content": "import React from 'react';\nimport {mount} from 'enzyme'\nimport SendLaterPopover from '../lib/send-later-popover';\n\n\nconst makePopover = (props = {}) => {\n  return mount(\n    <SendLaterPopover\n      sendLaterDate={null}\n      onSendLater={() => {}}\n      onCancelSendLater={() => {}}\n      {...props}\n    />\n  );\n};\n\ndescribe('SendLaterPopover', function sendLaterPopover() {\n  describe('render', () => {\n    it('renders cancel button if scheduled', () => {\n      const onCancelSendLater = jasmine.createSpy('onCancelSendLater')\n      const popover = makePopover({onCancelSendLater, sendLaterDate: 'date'})\n      const button = popover.find('.btn-cancel')\n      button.simulate('click')\n      expect(onCancelSendLater).toHaveBeenCalled()\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/stylesheets/send-later-used-modal.less",
    "content": "@import \"ui-variables\";\n\n.feature-usage-modal.send-later {\n  @send-later-color: #777ff0;\n  .feature-header {\n    @from: @send-later-color;\n    @to: lighten(@send-later-color, 10%);\n    background: linear-gradient(to top, @from, @to);\n  }\n  .feature-name {\n    color: @send-later-color;\n  }\n  .pro-description {\n    li {\n      &:before {\n        color: @send-later-color;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-later/stylesheets/send-later.less",
    "content": "@import \"ui-variables\";\n\n.send-later-popover {\n  .btn-cancel {\n    width: 100%;\n  }\n}\n\n.btn-send-later {\n  .at {\n    margin-left: 3px;\n  }\n}\n\n.send-later-status {\n  display: flex;\n  align-items: center;\n\n  .time {\n    font-size: 0.9em;\n    opacity: 0.62;\n    color: @component-active-color;\n    font-weight: @font-weight-normal;\n  }\n  img {\n    width: 38px;\n    margin-left: 15px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/main.es6",
    "content": "import {ComponentRegistry, ExtensionRegistry} from 'nylas-exports';\nimport {HasTutorialTip} from 'nylas-component-kit';\nimport SendRemindersThreadTimestamp from './send-reminders-thread-timestamp';\nimport SendRemindersComposerButton from './send-reminders-composer-button';\nimport SendRemindersToolbarButton from './send-reminders-toolbar-button';\nimport {ThreadHeader, MessageHeader} from './send-reminders-headers';\nimport SendRemindersStore from './send-reminders-store';\nimport * as ThreadListExtension from './send-reminders-thread-list-extension';\nimport * as AccountSidebarExtension from './send-reminders-account-sidebar-extension';\n\n\nconst ComposerButtonWithTip = HasTutorialTip(SendRemindersComposerButton, {\n  title: \"Get reminded!\",\n  instructions: \"Get reminded if you don't receive a reply for this message within a specified time.\",\n});\n\nexport function activate() {\n  ComponentRegistry.register(ComposerButtonWithTip, {role: 'Composer:ActionButton'})\n  ComponentRegistry.register(SendRemindersToolbarButton, {role: 'ThreadActionsToolbarButton'});\n  ComponentRegistry.register(SendRemindersThreadTimestamp, {role: 'ThreadListTimestamp'});\n  ComponentRegistry.register(MessageHeader, {role: 'MessageHeader'});\n  ComponentRegistry.register(ThreadHeader, {role: 'MessageListHeaders'});\n  ExtensionRegistry.ThreadList.register(ThreadListExtension)\n  ExtensionRegistry.AccountSidebar.register(AccountSidebarExtension)\n  SendRemindersStore.activate()\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ComposerButtonWithTip)\n  ComponentRegistry.unregister(SendRemindersToolbarButton)\n  ComponentRegistry.unregister(SendRemindersThreadTimestamp);\n  ComponentRegistry.unregister(MessageHeader);\n  ComponentRegistry.unregister(ThreadHeader);\n  ExtensionRegistry.ThreadList.unregister(ThreadListExtension)\n  ExtensionRegistry.AccountSidebar.unregister(AccountSidebarExtension)\n  SendRemindersStore.deactivate()\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-account-sidebar-extension.es6",
    "content": "import SendRemindersMailboxPerspective from './send-reminders-mailbox-perspective'\n\n\nexport const name = 'SendRemindersAccountSidebarExtension'\n\nexport function sidebarItem(accountIds) {\n  return {\n    id: 'Reminders',\n    name: 'Reminders',\n    iconName: 'reminders.png',\n    perspective: new SendRemindersMailboxPerspective(accountIds),\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-composer-button.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport ReactDOM from 'react-dom'\nimport {Actions} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit'\nimport SendRemindersPopover from './send-reminders-popover'\nimport {setDraftReminder, reminderDateForMessage, getReminderLabel} from './send-reminders-utils'\n\n\nclass SendRemindersComposerButton extends Component {\n  static displayName = 'SendRemindersComposerButton';\n\n  static containerRequired = false;\n\n  static propTypes = {\n    draft: PropTypes.object.isRequired,\n    session: PropTypes.object.isRequired,\n  };\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      saving: false,\n    }\n  }\n\n  componentWillReceiveProps() {\n    if (this.state.saving) {\n      this.setState({saving: false})\n    }\n  }\n\n  shouldComponentUpdate(nextProps) {\n    if (reminderDateForMessage(nextProps.draft) !== reminderDateForMessage(this.props.draft)) {\n      return true;\n    }\n    return false;\n  }\n\n  onSetReminder = (reminderDate, dateLabel) => {\n    const {draft, session} = this.props\n    this.setState({saving: true})\n    setDraftReminder(draft.accountId, session, reminderDate, dateLabel)\n  }\n\n  onClick = () => {\n    const {draft} = this.props\n    const buttonRect = ReactDOM.findDOMNode(this).getBoundingClientRect()\n    Actions.openPopover(\n      <SendRemindersPopover\n        onRemind={this.onSetReminder}\n        reminderDate={reminderDateForMessage(draft)}\n        onCancelReminder={() => this.onSetReminder(null)}\n      />,\n      {originRect: buttonRect, direction: 'up'}\n    )\n  };\n\n  render() {\n    const {saving} = this.state\n    let className = 'btn btn-toolbar btn-send-reminder';\n\n    if (saving) {\n      return (\n        <button className={className} title=\"Saving reminder...\" tabIndex={-1}>\n          <RetinaImg\n            name=\"inline-loading-spinner.gif\"\n            mode={RetinaImg.Mode.ContentDark}\n            style={{width: 14, height: 14}}\n          />\n        </button>\n      );\n    }\n\n    const {draft} = this.props\n    const reminderDate = reminderDateForMessage(draft);\n    let reminderLabel = 'Set reminder';\n    if (reminderDate) {\n      className += ' btn-enabled';\n      reminderLabel = getReminderLabel(reminderDate, {fromNow: true})\n    }\n\n    return (\n      <button\n        tabIndex={-1}\n        className={className}\n        title={reminderLabel}\n        onClick={this.onClick}\n      >\n        <RetinaImg name=\"icon-composer-reminders.png\" mode={RetinaImg.Mode.ContentIsMask} />\n        <span>&nbsp;</span>\n        <RetinaImg name=\"icon-composer-dropdown.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n}\n\nexport default SendRemindersComposerButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-constants.es6",
    "content": "import plugin from '../package.json'\n\nexport const PLUGIN_ID = plugin.name;\nexport const PLUGIN_NAME = \"Send Reminders\"\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-headers.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport {RetinaImg} from 'nylas-component-kit'\nimport {FocusedPerspectiveStore} from 'nylas-exports'\nimport {getReminderLabel, getLatestMessage, getLatestMessageWithReminder, setMessageReminder} from './send-reminders-utils'\nimport {PLUGIN_ID} from './send-reminders-constants'\n\n\nexport function MessageHeader(props) {\n  const {thread, messages, message} = props\n  const {shouldNotify} = thread.metadataForPluginId(PLUGIN_ID) || {}\n  if (!shouldNotify) {\n    return <span />\n  }\n  const latestMessage = getLatestMessage(thread, messages)\n  if (message.id !== latestMessage.id) {\n    return <span />\n  }\n  return (\n    <div className=\"send-reminders-header\">\n      <RetinaImg\n        name=\"ic-timestamp-reminder.png\"\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n      <span title=\"This thread was brought back to the top of your inbox as a reminder\">\n        Reminder\n      </span>\n    </div>\n  )\n}\nMessageHeader.displayName = 'MessageHeader'\nMessageHeader.containerRequired = false\nMessageHeader.propTypes = {\n  messages: PropTypes.array,\n  message: PropTypes.object,\n  thread: PropTypes.object,\n}\n\nexport function ThreadHeader(props) {\n  const {thread, messages} = props\n  const message = getLatestMessageWithReminder(thread, messages)\n  if (!message) {\n    return <span />\n  }\n  const {expiration} = message.metadataForPluginId(PLUGIN_ID) || {}\n  const clearReminder = () => {\n    setMessageReminder(message.accountId, message, null)\n  }\n  return (\n    <div className=\"send-reminders-header\">\n      <RetinaImg\n        name=\"ic-timestamp-reminder.png\"\n        mode={RetinaImg.Mode.ContentIsMask}\n      />\n      <span className=\"reminder-date\">\n        {` ${getReminderLabel(expiration)}`}\n      </span>\n      <span className=\"clear-reminder\" onClick={clearReminder}>Cancel</span>\n    </div>\n  )\n}\nThreadHeader.displayName = 'ThreadHeader'\nThreadHeader.containerRequired = false\nThreadHeader.propTypes = {\n  thread: PropTypes.object,\n  messages: PropTypes.array,\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-mailbox-perspective.es6",
    "content": "import {\n  MailboxPerspective,\n} from 'nylas-exports'\nimport SendRemindersQuerySubscription from './send-reminders-query-subscription'\n\n\nclass SendRemindersMailboxPerspective extends MailboxPerspective {\n\n  constructor(accountIds) {\n    super(accountIds)\n    this.accountIds = accountIds\n    this.name = 'Reminders'\n    this.iconName = 'reminders.png'\n  }\n\n  get isReminders() {\n    return true\n  }\n\n  emptyMessage() {\n    return \"No reminders set\"\n  }\n\n  threads() {\n    return new SendRemindersQuerySubscription(this.accountIds)\n  }\n\n  canReceiveThreadsFromAccountIds() {\n    return false\n  }\n\n  canArchiveThreads() {\n    return false\n  }\n\n  canTrashThreads() {\n    return false\n  }\n\n  canMoveThreadsTo() {\n    return false\n  }\n\n}\n\nexport default SendRemindersMailboxPerspective\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-popover-button.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Rx, Actions, Message, DatabaseStore} from 'nylas-exports';\nimport {RetinaImg, ListensToObservable} from 'nylas-component-kit';\nimport SendRemindersPopover from './send-reminders-popover';\nimport {getLatestMessage, setMessageReminder, reminderDateForMessage} from './send-reminders-utils'\n\n\nfunction getMessageObservable({thread} = {}) {\n  if (!thread) { return Rx.Observable.empty() }\n  const latestMessage = getLatestMessage(thread) || {}\n  const query = DatabaseStore.find(Message, latestMessage.id)\n  return Rx.Observable.fromQuery(query)\n}\n\nfunction getStateFromObservable(message, {props}) {\n  const {thread} = props\n  const latestMessage = message || getLatestMessage(thread)\n  return {latestMessage}\n}\n\n\nclass SendRemindersPopoverButton extends Component {\n  static displayName = 'SendRemindersPopoverButton';\n\n  static propTypes = {\n    className: PropTypes.string,\n    thread: PropTypes.object,\n    latestMessage: PropTypes.object,\n    direction: PropTypes.string,\n    getBoundingClientRect: PropTypes.func,\n  };\n\n  static defaultProps = {\n    className: 'btn btn-toolbar',\n    direction: 'down',\n    getBoundingClientRect: (inst) => ReactDOM.findDOMNode(inst).getBoundingClientRect(),\n  };\n\n  onSetReminder = (reminderDate, dateLabel) => {\n    const {latestMessage, thread} = this.props\n    setMessageReminder(latestMessage.accountId, latestMessage, reminderDate, dateLabel, thread)\n  }\n\n  onClick = (event) => {\n    event.stopPropagation()\n    const {direction, latestMessage, getBoundingClientRect} = this.props\n    const reminderDate = reminderDateForMessage(latestMessage)\n    const buttonRect = getBoundingClientRect(this)\n    Actions.openPopover(\n      <SendRemindersPopover\n        reminderDate={reminderDate}\n        onRemind={this.onSetReminder}\n        onCancelReminder={() => this.onSetReminder(null)}\n      />,\n      {originRect: buttonRect, direction}\n    )\n  };\n\n  render() {\n    const {className, latestMessage} = this.props\n    const reminderDate = reminderDateForMessage(latestMessage)\n    const title = reminderDate ? 'Edit reminder' : 'Set reminder';\n    return (\n      <button\n        title={title}\n        tabIndex={-1}\n        className={`send-reminders-toolbar-button ${className}`}\n        onClick={this.onClick}\n      >\n        <RetinaImg\n          name=\"ic-toolbar-native-reminder.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </button>\n    );\n  }\n}\n\nexport default ListensToObservable(SendRemindersPopoverButton, {\n  getObservable: getMessageObservable,\n  getStateFromObservable,\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-popover.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport {DateUtils} from 'nylas-exports'\nimport {DatePickerPopover} from 'nylas-component-kit'\nimport {getReminderLabel} from './send-reminders-utils'\n\n\nconst SendRemindersOptions = {\n  'In 1 hour': DateUtils.in1Hour,\n  'In 2 hours': DateUtils.in2Hours,\n  'In 4 hours': () => DateUtils.minutesFromNow(240),\n  'Tomorrow morning': DateUtils.tomorrow,\n  'Tomorrow evening': DateUtils.tomorrowEvening,\n  'In 2 days': () => DateUtils.hoursFromNow(48),\n  'In 4 days': () => DateUtils.hoursFromNow(96),\n  'In 1 week': () => DateUtils.weeksFromNow(1),\n  'In 2 weeks': () => DateUtils.weeksFromNow(2),\n  'In 1 month': () => DateUtils.monthsFromNow(1),\n}\n\nfunction SendRemindersPopover(props) {\n  const {reminderDate, onRemind, onCancelReminder} = props\n  const header = <span key=\"reminders-header\">Remind me if no one replies:</span>\n  const footer = [\n    reminderDate ? <div key=\"reminders-divider\" className=\"divider\" /> : null,\n    reminderDate ?\n      <div\n        key=\"send-reminders-footer\"\n        className=\"section send-reminders-footer\"\n      >\n        <div className=\"reminders-label\">\n          <span>\n            This thread will come back to the top of your inbox if nobody replies by:\n            <span className=\"reminder-date\">\n              {` ${getReminderLabel(reminderDate)}`}\n            </span>\n          </span>\n        </div>\n        <button className=\"btn btn-cancel\" onClick={onCancelReminder}>\n          Clear reminder\n        </button>\n      </div> :\n      null,\n  ]\n\n  return (\n    <DatePickerPopover\n      className=\"send-reminders-popover\"\n      header={header}\n      footer={footer}\n      onSelectDate={onRemind}\n      dateOptions={SendRemindersOptions}\n    />\n  );\n}\nSendRemindersPopover.displayName = 'SendRemindersPopover';\n\nSendRemindersPopover.propTypes = {\n  reminderDate: PropTypes.string,\n  onRemind: PropTypes.func,\n  onCancelReminder: PropTypes.func,\n};\n\n\nexport default SendRemindersPopover\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-query-subscription.es6",
    "content": "import {\n  Thread,\n  DatabaseStore,\n  MutableQuerySubscription,\n} from 'nylas-exports'\nimport {observableForThreadsWithReminders} from './send-reminders-utils'\n\n\nclass SendRemindersQuerySubscription extends MutableQuerySubscription {\n\n  constructor(accountIds) {\n    super(null, {emitResultSet: true})\n    this._disposable = null\n    this._accountIds = accountIds\n    setImmediate(() => this.fetchThreadsWithReminders())\n  }\n\n  replaceRange = () => {\n    // TODO\n  }\n\n  fetchThreadsWithReminders() {\n    this._disposable = observableForThreadsWithReminders(this._accountIds, {emitIds: true})\n    .subscribe((threadIds) => {\n      const threadQuery = (\n        DatabaseStore.findAll(Thread)\n        .where({id: threadIds})\n        .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n      )\n      this.replaceQuery(threadQuery)\n    })\n  }\n\n  onLastCallbackRemoved() {\n    if (this._disposable) {\n      this._disposable.dispose()\n    }\n  }\n}\n\nexport default SendRemindersQuerySubscription\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-store.es6",
    "content": "import {Actions, FocusedContentStore} from 'nylas-exports'\nimport {PLUGIN_ID} from './send-reminders-constants'\nimport {\n  getLatestMessage,\n  setMessageReminder,\n  getLatestMessageWithReminder,\n  asyncUpdateFromSentMessage,\n  observableForThreadsWithReminders,\n} from './send-reminders-utils'\n\n\nclass SendRemindersStore {\n\n  activate() {\n    this._lastFocusedThread = null\n    this._unsubscribers = [\n      FocusedContentStore.listen(this._onFocusedContentChanged),\n      Actions.draftDeliverySucceeded.listen(asyncUpdateFromSentMessage),\n    ]\n    this._disposables = [\n      observableForThreadsWithReminders().subscribe(this._onThreadsWithRemindersChanged),\n    ]\n  }\n\n  _onFocusedContentChanged = () => {\n    const thread = FocusedContentStore.focused('thread') || null\n    const didUnfocusLastThread = (\n      (!thread && this._lastFocusedThread) ||\n      (thread && this._lastFocusedThread && thread.id !== this._lastFocusedThread.id)\n    )\n    // When we unfocus a thread that had `shouldNotify == true`, it means that\n    // we have acknowledged the notification, or in this case, the reminder. If\n    // that's the case, set `shouldNotify` to false.\n    if (didUnfocusLastThread) {\n      const {shouldNotify} = this._lastFocusedThread.metadataForPluginId(PLUGIN_ID) || {}\n      if (shouldNotify) {\n        const nextMetadata = {shouldNotify: false}\n        Actions.setMetadata(this._lastFocusedThread.clone(), PLUGIN_ID, nextMetadata)\n      }\n    }\n    this._lastFocusedThread = thread\n  }\n\n  _onThreadsWithRemindersChanged = (threads) => {\n    // If a new message was received on the thread, clear the reminder\n    threads.forEach((thread) => {\n      const {accountId} = thread\n      thread.messages().then((messages) => {\n        const latestMessage = getLatestMessage(thread, messages)\n        const latestMessageWithReminder = getLatestMessageWithReminder(thread, messages)\n        if (!latestMessageWithReminder) { return }\n        if (latestMessage.id !== latestMessageWithReminder.id) {\n          setMessageReminder(accountId, latestMessageWithReminder, null)\n        }\n      })\n    })\n  }\n\n  deactivate() {\n    this._unsubscribers.forEach((unsub) => unsub())\n    this._disposables.forEach((disp) => disp.dispose())\n  }\n}\n\nexport default new SendRemindersStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-thread-list-extension.es6",
    "content": "import {PLUGIN_ID} from './send-reminders-constants'\nimport {getLatestMessageWithReminder} from './send-reminders-utils'\n\nexport const name = 'SendRemindersThreadListExtension'\n\nexport function cssClassNamesForThreadListItem(thread) {\n  const {shouldNotify} = thread.metadataForPluginId(PLUGIN_ID) || {}\n  if (shouldNotify) {\n    return 'thread-list-reminder-item'\n  }\n  return ''\n}\n\nexport function cssClassNamesForThreadListIcon(thread) {\n  const {shouldNotify} = thread.metadataForPluginId(PLUGIN_ID) || {}\n  if (shouldNotify) {\n    return 'thread-icon-reminder-triggered'\n  }\n  if (getLatestMessageWithReminder(thread)) {\n    return 'thread-icon-reminder-pending'\n  }\n  return ''\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-thread-timestamp.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport {RetinaImg} from 'nylas-component-kit';\nimport {Rx, Message, DatabaseStore, FocusedPerspectiveStore} from 'nylas-exports';\nimport {getReminderLabel, getLatestMessageWithReminder, setMessageReminder} from './send-reminders-utils'\nimport {PLUGIN_ID} from './send-reminders-constants';\n\n\nfunction canRenderTimestamp(message) {\n  const current = FocusedPerspectiveStore.current()\n  if (!current.isReminders) {\n    return false\n  }\n  if (!message) {\n    return false\n  }\n  return true\n}\n\nclass SendRemindersThreadTimestamp extends Component {\n  static displayName = 'SendRemindersThreadTimestamp';\n\n  static propTypes = {\n    thread: PropTypes.object,\n    messages: PropTypes.array,\n    fallback: PropTypes.func,\n  };\n\n  static containerRequired = false;\n\n  constructor(props) {\n    super(props)\n    this._disposable = null\n    this.state = {\n      message: getLatestMessageWithReminder(props.thread, props.messages),\n    }\n  }\n\n  componentDidMount() {\n    const {message} = this.state\n    this.setupMessageObservable(message)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    const {thread, messages} = nextProps\n    const message = getLatestMessageWithReminder(thread, messages)\n    this.disposeMessageObservable()\n    if (!message) {\n      this.setState({message})\n    } else {\n      this.setupMessageObservable(message)\n    }\n  }\n\n  componentWillUnmount() {\n    this.disposeMessageObservable()\n  }\n\n  onRemoveReminder(message) {\n    setMessageReminder(message.accountId, message, null)\n  }\n\n  setupMessageObservable(message) {\n    if (!canRenderTimestamp(message)) { return }\n    const message$ = Rx.Observable.fromQuery(DatabaseStore.find(Message, message.id))\n    this._disposable = message$.subscribe((msg) => {\n      const {expiration} = msg.metadataForPluginId(PLUGIN_ID) || {};\n      if (!expiration) {\n        this.setState({message: null})\n      } else {\n        this.setState({message: msg})\n      }\n    })\n  }\n\n  disposeMessageObservable() {\n    if (this._disposable) {\n      this._disposable.dispose()\n    }\n  }\n\n  render() {\n    const {message} = this.state;\n    const Fallback = this.props.fallback;\n    if (!canRenderTimestamp(message)) {\n      return <Fallback {...this.props} />\n    }\n    const {expiration} = message.metadataForPluginId(PLUGIN_ID);\n    const title = getReminderLabel(expiration, {fromNow: true})\n    const shortLabel = getReminderLabel(expiration, {shortFormat: true})\n    return (\n      <span className=\"send-reminders-thread-timestamp timestamp\" title={title}>\n        <RetinaImg\n          name=\"ic-timestamp-reminder.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        <span className=\"date-message\">\n          {shortLabel}\n        </span>\n      </span>\n    )\n  }\n}\n\nexport default SendRemindersThreadTimestamp\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-toolbar-button.jsx",
    "content": "import React, {PropTypes} from 'react';\nimport {HasTutorialTip} from 'nylas-component-kit';\nimport {getLatestMessage} from './send-reminders-utils'\nimport SendRemindersPopoverButton from './send-reminders-popover-button';\n\nconst SendRemindersPopoverButtonWithTip = HasTutorialTip(SendRemindersPopoverButton, {\n  title: \"Get reminded!\",\n  instructions: \"Get reminded if you don't receive a reply for this message within a specified time.\",\n});\n\nfunction canSetReminderOnThread(thread) {\n  const {from} = getLatestMessage(thread) || {}\n  return (\n    from && from.length > 0 && from[0].isMe()\n  )\n}\n\nexport default function SendRemindersToolbarButton(props) {\n  const threads = props.items\n  if (threads.length > 1) {\n    return <span />;\n  }\n  const thread = threads[0]\n  if (!canSetReminderOnThread(thread)) {\n    return <span />;\n  }\n  return (\n    <SendRemindersPopoverButtonWithTip thread={thread} />\n  );\n}\n\nSendRemindersToolbarButton.containerRequired = false;\nSendRemindersToolbarButton.displayName = 'SendRemindersToolbarButton';\nSendRemindersToolbarButton.propTypes = {\n  items: PropTypes.array,\n};\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/lib/send-reminders-utils.jsx",
    "content": "import moment from 'moment'\nimport {\n  Rx,\n  Thread,\n  Message,\n  Actions,\n  Category,\n  NylasAPIHelpers,\n  DateUtils,\n  DatabaseStore,\n  FeatureUsageStore,\n} from 'nylas-exports'\nimport {PLUGIN_ID, PLUGIN_NAME} from './send-reminders-constants'\n\n\nconst {DATE_FORMAT_LONG_NO_YEAR} = DateUtils\n\nexport function reminderDateForMessage(message) {\n  if (!message) {\n    return null;\n  }\n  const messageMetadata = message.metadataForPluginId(PLUGIN_ID) || {};\n  return messageMetadata.expiration;\n}\n\nasync function asyncBuildMetadata({message, thread, expiration} = {}) {\n  if (message) { // Not a draft\n    let messageIdHeaders = [message.messageIdHeader];\n    let folderImapNames = [];\n    let hasPrimary = false; // whether folderImapNames includes All Mail or Inbox\n\n    // There won't be a thread if this is a newly sent draft that wasn't a reply.\n    if (thread) {\n      // We need to include the hidden messages so the cloud-worker doesn't think\n      // that previously hidden messages are new replies to the thread.\n      const messages = await thread.messages({includeHidden: true})\n      messageIdHeaders = messages.map(msg => msg.messageIdHeader)\n\n      const folders = thread.categories.filter(c => c.object === 'folder')\n      hasPrimary = folders.some(f => ['all', 'inbox'].includes(f.name))\n      folderImapNames = folders.map(f => f.imapName).filter(name => name)\n    }\n\n    // We always want to check the main inbox folder for replies\n    if (!hasPrimary) {\n      let primary = await DatabaseStore.findBy(Category, {name: 'all', accountId: message.accountId})\n      if (!primary) {\n        primary = await DatabaseStore.findBy(Category, {name: 'inbox', accountId: message.accountId})\n      }\n      const primaryName = primary ? primary.imapName : null;\n\n      if (primaryName) {\n        folderImapNames.unshift(primaryName); // Put it at the front so we check it first\n      }\n    }\n\n    return {\n      expiration,\n      folderImapNames,\n      messageIdHeaders,\n      replyTo: message.messageIdHeader,\n      subject: message.subject,\n    }\n  }\n  // else: this is a draft, the rest of the metadata will be updated after send.\n  return {expiration}\n}\n\nexport async function asyncUpdateFromSentMessage({messageClientId}) {\n  const message = await DatabaseStore.findBy(Message, {clientId: messageClientId})\n  if (!message) {\n    throw new Error(\"SendReminders: Could not find message to update\")\n  }\n  const {expiration} = message.metadataForPluginId(PLUGIN_ID) || {}\n  if (!expiration) {\n    // This message doesn't have a reminder\n    return;\n  }\n\n  let thread;\n  if (message.threadId) {\n    thread = await DatabaseStore.find(Thread, message.threadId)\n  } // else: this message wasn't a reply and doesn't have a thread yet\n\n  const newMetadata = await asyncBuildMetadata({message, thread, expiration})\n  Actions.setMetadata(message, PLUGIN_ID, newMetadata)\n}\n\nasync function asyncSetReminder(accountId, reminderDate, dateLabel, {message, thread, isDraft, draftSession} = {}) {\n  // Only check for feature usage and record metrics if this message doesn't\n  // already have a reminder set\n  if (!reminderDateForMessage(message)) {\n    const lexicon = {\n      displayName: \"be Reminded\",\n      usedUpHeader: \"All reminders used\",\n      iconUrl: \"nylas://send-reminders/assets/ic-send-reminders-modal@2x.png\",\n    }\n\n    try {\n      await FeatureUsageStore.asyncUseFeature('send-reminders', {lexicon})\n    } catch (error) {\n      if (error instanceof FeatureUsageStore.NoProAccess) {\n        return\n      }\n    }\n\n    if (reminderDate && dateLabel) {\n      const remindInSec = Math.round(((new Date(reminderDate)).valueOf() - Date.now()) / 1000)\n      Actions.recordUserEvent(\"Set Reminder\", {\n        timeInSec: remindInSec,\n        timeInLog10Sec: Math.log10(remindInSec),\n        label: dateLabel,\n      });\n    }\n  }\n\n  let metadata = {}\n  if (reminderDate) {\n    metadata = await asyncBuildMetadata({message, thread, expiration: reminderDate})\n  } // else: we're clearing the reminder and the metadata should remain empty\n\n  await NylasAPIHelpers.authPlugin(PLUGIN_ID, PLUGIN_NAME, accountId)\n  .then(() => {\n    if (isDraft) {\n      if (!draftSession) { throw new Error('setDraftReminder: Must provide draftSession') }\n      draftSession.changes.add({pristine: false})\n      draftSession.changes.addPluginMetadata(PLUGIN_ID, metadata);\n    } else {\n      if (!message) { throw new Error('setMessageReminder: Must provide message') }\n      Actions.setMetadata(message, PLUGIN_ID, metadata)\n    }\n    Actions.closePopover()\n  })\n  .catch((error) => {\n    Actions.closePopover()\n    NylasEnv.reportError(error);\n    NylasEnv.showErrorDialog(`Sorry, we were unable to save the reminder for this message. ${error.message}`);\n  });\n}\n\nexport function setMessageReminder(accountId, message, reminderDate, dateLabel, thread) {\n  return asyncSetReminder(accountId, reminderDate, dateLabel, {isDraft: false, message, thread})\n}\n\nexport function setDraftReminder(accountId, draftSession, reminderDate, dateLabel) {\n  return asyncSetReminder(accountId, reminderDate, dateLabel, {isDraft: true, draftSession})\n}\n\n\nfunction reminderThreadIdsFromMessages(messages) {\n  return Array.from(new Set(\n    messages\n    .filter((message) => (message.metadataForPluginId(PLUGIN_ID) || {}).expiration != null)\n    .map(({threadId}) => threadId)\n    .filter((threadId) => threadId != null)\n  ))\n}\n\nexport function observableForThreadsWithReminders(accountIds = [], {emitIds = false} = {}) {\n  let messagesQuery = (\n    DatabaseStore.findAll(Message)\n    .where(Message.attributes.pluginMetadata.contains(PLUGIN_ID))\n  )\n  if (accountIds.length === 1) {\n    messagesQuery = messagesQuery.where({accountId: accountIds[0]})\n  }\n  const messages$ = Rx.Observable.fromQuery(messagesQuery)\n  if (emitIds) {\n    return messages$.map((messages) => reminderThreadIdsFromMessages(messages))\n  }\n  return messages$.flatMapLatest((messages) => {\n    const threadIds = reminderThreadIdsFromMessages(messages)\n    const threadsQuery = (\n      DatabaseStore.findAll(Thread)\n      .where({id: threadIds})\n      .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n    )\n    return Rx.Observable.fromQuery(threadsQuery)\n  })\n}\n\nexport function getLatestMessage(thread, messages) {\n  const msgs = messages || thread.__messages || [];\n  return msgs[msgs.length - 1]\n}\n\nexport function getLatestMessageWithReminder(thread, messages) {\n  const msgs = (messages || thread.__messages || []).slice().reverse();\n  return msgs.find((message) => {\n    const {expiration} = message.metadataForPluginId(PLUGIN_ID) || {}\n    return expiration != null\n  })\n}\n\nexport function getReminderLabel(reminderDate, {fromNow = false, shortFormat = false} = {}) {\n  const momentDate = DateUtils.futureDateFromString(reminderDate);\n  if (shortFormat) {\n    return momentDate ? `in ${momentDate.fromNow(true)}` : 'now'\n  }\n  if (fromNow) {\n    return momentDate ? `Reminder set for ${momentDate.fromNow(true)} from now` : `Reminder set`;\n  }\n  return moment(reminderDate).format(DATE_FORMAT_LONG_NO_YEAR)\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/package.json",
    "content": "{\n  \"name\": \"send-reminders\",\n  \"version\": \"1.0.0\",\n  \"title\": \"Send Reminders\",\n  \"description\": \"Get reminded if you don't receive a reply for a message within a specified time in the future\",\n  \"isHiddenOnPluginsPage\": true,\n  \"icon\": \"./icon.png\",\n  \"main\": \"lib/main\",\n  \"supportedEnvs\": [\"local\", \"development\", \"staging\", \"production\"],\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"composer\": true\n  },\n  \"isOptional\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/stylesheets/reminders-used-modal.less",
    "content": "@import \"ui-variables\";\n\n.feature-usage-modal.send-reminders {\n  @send-reminders-color: #517ff2;\n  .feature-header {\n    @from: @send-reminders-color;\n    @to: lighten(@send-reminders-color, 10%);\n    background: linear-gradient(to top, @from, @to);\n  }\n  .feature-name {\n    color: @send-reminders-color;\n  }\n  .pro-description {\n    li {\n      &:before {\n        color: @send-reminders-color;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/send-reminders/stylesheets/send-reminders.less",
    "content": "@import \"ui-variables\";\n@reminders-background-color: #f6f6fe;\n@reminders-color: #5a31e1;\n\n.send-reminders-popover {\n  .section.send-reminders-footer {\n    .reminders-label {\n      color: @text-color-very-subtle;\n      font-size: 0.8em;\n      min-height: 36px;\n      .reminder-date {\n        font-weight: bold;\n      }\n    }\n\n    .btn-cancel {\n      width: 100%;\n      margin-top: 5px;\n    }\n  }\n}\n\n.send-reminders-toolbar-button {\n  order: -102;\n}\n\n.thread-list {\n  .list-item.focused {\n    .timestamp.send-reminders-thread-timestamp {\n      color: #fff;\n      img {\n        background-color: #fff;\n      }\n    }\n  }\n\n  .timestamp.send-reminders-thread-timestamp {\n    opacity: 0.82;\n    color: #979797;\n    img {\n      background-color: #979797;\n      margin-top: -3px;\n      padding-right: 6px;\n    }\n  }\n}\n\n.send-reminders-header {\n  color: @reminders-color;\n  background: linear-gradient(to bottom, #fbfafe 0%, #fff 25%);\n  padding-top: 13px;\n  padding-bottom: 10px;\n  cursor: default;\n  margin-top: -19px;\n  margin-bottom: 15px;\n  border-bottom: 1px solid #e7e1fb;\n  img {\n    background-color: @reminders-color;\n    margin-top: -6px;\n    margin-right: 10px;\n  }\n}\n.message-list-headers .send-reminders-header {\n  padding-left: 15px;\n  margin-top: 0;\n  .reminder-date {\n    font-weight: bold;\n  }\n  .clear-reminder {\n    position: absolute;\n    right: 18px;\n    cursor: pointer;\n    text-decoration: underline;\n  }\n}\n\n.thread-list .list-item.thread-list-reminder-item {\n  background-color: @reminders-background-color;\n\n  &.unread {\n    background-color: @reminders-background-color;\n    &:not(.focused):not(.selected) {\n      background-color: @reminders-background-color;\n    }\n  }\n\n  &.focused,&.selected {\n    background: @list-focused-bg;\n  }\n  .thread-icon-reminder-triggered {\n    margin-top: 1px;\n    background-image:url(../static/images/thread-list/icon-reminder@2x.png);\n  }\n}\n\n.thread-list .list-item {\n  .thread-icon-reminder-pending {\n    margin-top: 1px;\n    background-image:url(../static/images/thread-list/icon-reminder-outline@2x.png);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/sync-health-checker/lib/main.es6",
    "content": "import SyncHealthChecker from './sync-health-checker'\n\nexport function activate() {\n  SyncHealthChecker.start()\n}\n\nexport function deactivate() {\n  SyncHealthChecker.stop()\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/sync-health-checker/lib/sync-health-checker.es6",
    "content": "import {ipcRenderer} from 'electron'\nimport {IdentityStore, AccountStore, Actions, NylasAPI, NylasAPIRequest} from 'nylas-exports'\n\nconst CHECK_HEALTH_INTERVAL = 5 * 60 * 1000;\n\nclass SyncHealthChecker {\n  constructor() {\n    this._lastSyncActivity = null\n    this._interval = null\n  }\n\n  start() {\n    if (this._interval) {\n      console.warn('SyncHealthChecker has already been started')\n    } else {\n      this._interval = setInterval(this._checkSyncHealth, CHECK_HEALTH_INTERVAL)\n    }\n  }\n\n  stop() {\n    clearInterval(this._interval)\n    this._interval = null\n  }\n\n  // This is a separate function so the request can be manipulated in the specs\n  _buildRequest = () => {\n    return new NylasAPIRequest({\n      api: NylasAPI,\n      options: {\n        accountId: AccountStore.accounts()[0].id,\n        path: `/health`,\n      },\n    });\n  }\n\n  _checkSyncHealth = async () => {\n    try {\n      if (!IdentityStore.identity()) {\n        return\n      }\n      const request = this._buildRequest()\n      const response = await request.run()\n      this._lastSyncActivity = response\n    } catch (err) {\n      if (/ECONNREFUSED/i.test(err.toString())) {\n        this._onWorkerWindowUnavailable()\n      } else {\n        err.message = `Error checking sync health: ${err.message}`\n        NylasEnv.reportError(err)\n      }\n    }\n  }\n\n  _onWorkerWindowUnavailable() {\n    let extraData = {};\n\n    // Extract data that we want to report. We'll report the entire\n    // _lastSyncActivity object, but it'll probably be useful if we can segment\n    // by the data in the oldest or newest entry, so we report those as\n    // individual values too.\n    const lastActivityEntries = Object.entries(this._lastSyncActivity || {})\n    if (lastActivityEntries.length > 0) {\n      const times = lastActivityEntries.map((entry) => entry[1].time)\n      const now = Date.now()\n\n      const maxTime = Math.max(...times)\n      const mostRecentEntry = lastActivityEntries.find((entry) => entry[1].time === maxTime)\n      const [mostRecentActivityAccountId, {\n        activity: mostRecentActivity,\n        time: mostRecentActivityTime,\n      }] = mostRecentEntry;\n      const mostRecentDuration = now - mostRecentActivityTime\n\n      const minTime = Math.min(...times)\n      const leastRecentEntry = lastActivityEntries.find((entry) => entry[1].time === minTime)\n      const [leastRecentActivityAccountId, {\n        activity: leastRecentActivity,\n        time: leastRecentActivityTime,\n      }] = leastRecentEntry;\n      const leastRecentDuration = now - leastRecentActivityTime\n\n      extraData = {\n        mostRecentActivity,\n        mostRecentActivityTime,\n        mostRecentActivityAccountId,\n        mostRecentDuration,\n        leastRecentActivity,\n        leastRecentActivityTime,\n        leastRecentActivityAccountId,\n        leastRecentDuration,\n      }\n    }\n\n    NylasEnv.reportError(new Error('Worker window was unavailable'), {\n      // This information isn't as useful in Sentry, but include it here until\n      // the data is actually sent to Mixpanel. (See the TODO below)\n      lastActivityPerAccount: this._lastSyncActivity,\n      ...extraData,\n    })\n\n    // TODO: This doesn't make it to Mixpanel because our analytics process\n    // lives in the worker window. We should move analytics to the main process.\n    // https://phab.nylas.com/T8029\n    Actions.recordUserEvent('Worker Window Unavailable', {\n      lastActivityPerAccount: this._lastSyncActivity,\n      ...extraData,\n    })\n\n    console.log(`Detected worker window was unavailable. Restarting it.`, this._lastSyncActivity)\n    ipcRenderer.send('ensure-worker-window')\n  }\n}\n\nexport default new SyncHealthChecker()\n"
  },
  {
    "path": "packages/client-app/internal_packages/sync-health-checker/package.json",
    "content": "{\n  \"name\": \"sync-health-checker\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Periodically ping the sync process to ensure it's running\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/sync-health-checker/spec/sync-health-checker-spec.es6",
    "content": "import {ipcRenderer} from 'electron'\nimport SyncHealthChecker from '../lib/sync-health-checker'\n\nconst requestWithErrorResponse = () => {\n  return {\n    run: async () => {\n      throw new Error('ECONNREFUSED');\n    },\n  }\n}\n\nconst activityData = {account1: {time: 1490305104619, activity: ['activity']}}\n\nconst requestWithDataResponse = () => {\n  return {\n    run: async () => {\n      return activityData\n    },\n  }\n}\n\ndescribe('SyncHealthChecker', () => {\n  describe('when the worker window is not available', () => {\n    beforeEach(() => {\n      spyOn(SyncHealthChecker, '_buildRequest').andCallFake(requestWithErrorResponse)\n      spyOn(ipcRenderer, 'send')\n      spyOn(NylasEnv, 'reportError')\n    })\n    it('attempts to restart it', async () => {\n      await SyncHealthChecker._checkSyncHealth();\n      expect(NylasEnv.reportError.calls.length).toEqual(1)\n      expect(ipcRenderer.send.calls[0].args[0]).toEqual('ensure-worker-window')\n    })\n  })\n  describe('when data is returned', () => {\n    beforeEach(() => {\n      spyOn(SyncHealthChecker, '_buildRequest').andCallFake(requestWithDataResponse)\n    })\n    it('stores the data', async () => {\n      await SyncHealthChecker._checkSyncHealth();\n      expect(SyncHealthChecker._lastSyncActivity).toEqual(activityData)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/system-tray/lib/main.es6",
    "content": "import SystemTrayIconStore from './system-tray-icon-store';\n\nexport function activate() {\n  this.store = new SystemTrayIconStore();\n  this.store.activate();\n}\n\nexport function deactivate() {\n  this.store.deactivate();\n}\n\nexport function serialize() {\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/system-tray/lib/system-tray-icon-store.es6",
    "content": "import path from 'path';\nimport {ipcRenderer} from 'electron';\nimport {BadgeStore} from 'nylas-exports';\n\n// Must be absolute real system path\n// https://github.com/atom/electron/issues/1299\nconst {platform} = process\nconst INBOX_ZERO_ICON = path.join(__dirname, '..', 'assets', platform, 'MenuItem-Inbox-Zero.png');\nconst INBOX_UNREAD_ICON = path.join(__dirname, '..', 'assets', platform, 'MenuItem-Inbox-Full.png');\nconst INBOX_UNREAD_ALT_ICON = path.join(__dirname, '..', 'assets', platform, 'MenuItem-Inbox-Full-NewItems.png');\n\n\nclass SystemTrayIconStore {\n\n  static INBOX_ZERO_ICON = INBOX_ZERO_ICON;\n\n  static INBOX_UNREAD_ICON = INBOX_UNREAD_ICON;\n\n  static INBOX_UNREAD_ALT_ICON = INBOX_UNREAD_ALT_ICON;\n\n  constructor() {\n    this._windowBlurred = false;\n    this._unsubscribers = [];\n  }\n\n  activate() {\n    this._updateIcon();\n    this._unsubscribers.push(BadgeStore.listen(this._updateIcon));\n\n    window.addEventListener('browser-window-blur', this._onWindowBlur);\n    window.addEventListener('browser-window-focus', this._onWindowFocus);\n    this._unsubscribers.push(() => window.removeEventListener('browser-window-blur', this._onWindowBlur))\n    this._unsubscribers.push(() => window.removeEventListener('browser-window-focus', this._onWindowFocus))\n  }\n\n  _getIconImageData(isInboxZero, isWindowBlurred) {\n    if (isInboxZero) {\n      return {iconPath: INBOX_ZERO_ICON, isTemplateImg: true};\n    }\n    return isWindowBlurred ?\n      {iconPath: INBOX_UNREAD_ALT_ICON, isTemplateImg: false} :\n      {iconPath: INBOX_UNREAD_ICON, isTemplateImg: true};\n  }\n\n  _onWindowBlur = () => {\n    // Set state to blurred, but don't trigger a change. The icon should only be\n    // updated when the count changes\n    this._windowBlurred = true;\n  };\n\n  _onWindowFocus = () => {\n    // Make sure that as long as the window is focused we never use the alt icon\n    this._windowBlurred = false;\n    this._updateIcon();\n  };\n\n  _updateIcon = () => {\n    const unread = BadgeStore.unread();\n    const unreadString = (+unread).toLocaleString();\n    const isInboxZero = (BadgeStore.total() === 0);\n    const {iconPath, isTemplateImg} = this._getIconImageData(isInboxZero, this._windowBlurred);\n    ipcRenderer.send('update-system-tray', iconPath, unreadString, isTemplateImg);\n  };\n\n  deactivate() {\n    this._unsubscribers.forEach(unsub => unsub())\n  }\n}\n\nexport default SystemTrayIconStore;\n"
  },
  {
    "path": "packages/client-app/internal_packages/system-tray/package.json",
    "content": "{\n  \"name\": \"system-tray\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Displays cross-platform system tray icon with unread count\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/system-tray/spec/system-tray-icon-store-spec.es6",
    "content": "import {ipcRenderer} from 'electron';\nimport {BadgeStore} from 'nylas-exports';\nimport SystemTrayIconStore from '../lib/system-tray-icon-store';\n\nconst {\n  INBOX_ZERO_ICON,\n  INBOX_UNREAD_ICON,\n  INBOX_UNREAD_ALT_ICON,\n} = SystemTrayIconStore;\n\n\ndescribe('SystemTrayIconStore', function systemTrayIconStore() {\n  beforeEach(() => {\n    spyOn(ipcRenderer, 'send')\n    this.iconStore = new SystemTrayIconStore()\n  });\n\n  function getCallData() {\n    const {args} = ipcRenderer.send.calls[0]\n    return {iconPath: args[1], isTemplateImg: args[3]}\n  }\n\n  describe('_getIconImageData', () => {\n    it('shows inbox zero icon when isInboxZero and window is focused', () => {\n      const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(true, false)\n      expect(iconPath).toBe(INBOX_ZERO_ICON)\n      expect(isTemplateImg).toBe(true)\n    });\n\n    it('shows inbox zero icon when isInboxZero and window is blurred', () => {\n      const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(true, true)\n      expect(iconPath).toBe(INBOX_ZERO_ICON)\n      expect(isTemplateImg).toBe(true)\n    });\n\n    it('shows inbox full icon when not isInboxZero and window is focused', () => {\n      const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(false, false)\n      expect(iconPath).toBe(INBOX_UNREAD_ICON)\n      expect(isTemplateImg).toBe(true)\n    });\n\n    it('shows inbox full /alt/ icon when not isInboxZero and window is blurred', () => {\n      const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(false, true)\n      expect(iconPath).toBe(INBOX_UNREAD_ALT_ICON)\n      expect(isTemplateImg).toBe(false)\n    });\n  });\n\n  describe('updating the icon based on focus and blur', () => {\n    it('always shows inbox full icon when the window gets focused', () => {\n      spyOn(BadgeStore, 'total').andReturn(1)\n      this.iconStore._onWindowFocus()\n      const {iconPath} = getCallData()\n      expect(iconPath).toBe(INBOX_UNREAD_ICON)\n    });\n\n    it('shows inbox full /alt/ icon ONLY when window is currently blurred and total count changes', () => {\n      this.iconStore._windowBlurred = false\n      this.iconStore._onWindowBlur()\n      expect(ipcRenderer.send).not.toHaveBeenCalled()\n\n      // BadgeStore triggers a change\n      spyOn(BadgeStore, 'total').andReturn(1)\n      this.iconStore._updateIcon()\n\n      const {iconPath} = getCallData()\n      expect(iconPath).toBe(INBOX_UNREAD_ALT_ICON)\n    });\n\n    it('does not show inbox full /alt/ icon when window is currently focused and total count changes', () => {\n      this.iconStore._windowBlurred = false\n\n      // BadgeStore triggers a change\n      spyOn(BadgeStore, 'total').andReturn(1)\n      this.iconStore._updateIcon()\n\n      const {iconPath} = getCallData()\n      expect(iconPath).toBe(INBOX_UNREAD_ICON)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/theme-picker/lib/main.jsx",
    "content": "import React from 'react';\nimport {Actions, WorkspaceStore} from 'nylas-exports';\n\nimport ThemePicker from './theme-picker';\n\n\nexport function activate() {\n  this.disposable = NylasEnv.commands.add(document.body, \"window:launch-theme-picker\", () => {\n    WorkspaceStore.popToRootSheet();\n    Actions.openModal({\n      component: (<ThemePicker />),\n      height: 390,\n      width: 250,\n    });\n  });\n}\n\nexport function deactivate() {\n  this.disposable.dispose();\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/theme-picker/lib/theme-option.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport fs from 'fs-plus';\nimport path from 'path';\n\nimport {EventedIFrame} from 'nylas-component-kit';\nimport LessCompileCache from '../../../src/less-compile-cache'\n\n\nclass ThemeOption extends React.Component {\n  static propTypes = {\n    theme: React.PropTypes.object.isRequired,\n    active: React.PropTypes.bool.isRequired,\n    onSelect: React.PropTypes.func.isRequired,\n  }\n\n  constructor(props) {\n    super(props);\n    this.lessCache = null;\n  }\n\n  componentDidMount() {\n    this._writeContent();\n  }\n\n  _getImportPaths() {\n    const themes = [this.props.theme];\n    // Pulls the theme package for Light as the base theme\n    for (const theme of NylasEnv.themes.getActiveThemes()) {\n      if (theme.name === NylasEnv.themes.baseThemeName()) {\n        themes.push(theme);\n      }\n    }\n    const themePaths = [];\n    for (const theme of themes) {\n      themePaths.push(theme.getStylesheetsPath());\n    }\n    return themePaths.filter((themePath) => fs.isDirectorySync(themePath));\n  }\n\n  _loadStylesheet(stylesheetPath) {\n    if (path.extname(stylesheetPath) === '.less') {\n      return this._loadLessStylesheet(stylesheetPath);\n    }\n    return fs.readFileSync(stylesheetPath, 'utf8');\n  }\n\n  _loadLessStylesheet(lessStylesheetPath) {\n    const {configDirPath, resourcePath} = NylasEnv.getLoadSettings();\n    if (this.lessCache) {\n      this.lessCache.setImportPaths(this._getImportPaths());\n    } else {\n      const importPaths = this._getImportPaths();\n      this.lessCache = new LessCompileCache({configDirPath, resourcePath, importPaths});\n    }\n    const themeVarPath = path.relative(`${resourcePath}/internal_packages/theme-picker/preview-styles`,\n                                      this.props.theme.getStylesheetsPath());\n    let varImports = `@import \"../../../static/variables/ui-variables\";`\n    if (fs.existsSync(`${this.props.theme.getStylesheetsPath()}/ui-variables.less`)) {\n      varImports += `@import \"${themeVarPath}/ui-variables\";`\n    }\n    if (fs.existsSync(`${this.props.theme.getStylesheetsPath()}/theme-colors.less`)) {\n      varImports += `@import \"${themeVarPath}/theme-colors\";`\n    }\n    const less = fs.readFileSync(lessStylesheetPath, 'utf8');\n    return this.lessCache.cssForFile(lessStylesheetPath, [varImports, less].join('\\n'));\n  }\n\n  _writeContent() {\n    const domNode = ReactDOM.findDOMNode(this.refs.iframe);\n    const doc = domNode.contentDocument;\n    if (!doc) return;\n\n    const {resourcePath} = NylasEnv.getLoadSettings();\n    const css = `<style>${this._loadStylesheet(`${resourcePath}/internal_packages/theme-picker/preview-styles/theme-option.less`)}</style>`\n    const html = `<!DOCTYPE html>\n                  ${css}\n                  <body>\n                    <div class=\"theme-option active-${this.props.active}\">\n                      <div class=\"theme-name \">${this.props.theme.displayName}</div>\n                      <div class=\"swatches\" style=\"display:flex;flex-direction:row;\">\n                        <div class=\"swatch font-color\"></div>\n                        <div class=\"swatch active-color\"></div>\n                        <div class=\"swatch toolbar-color\"></div>\n                      </div>\n                      <div class=\"divider-black\"></div>\n                      <div class=\"divider-white\"></div>\n                      <div class=\"strip\"></div>\n                    </div>\n                  </body>`\n\n    doc.open();\n    doc.write(html);\n    doc.close();\n  }\n\n  render() {\n    return (\n      <div className=\"clickable-theme-option\" onMouseDown={this.props.onSelect}>\n        <EventedIFrame\n          ref=\"iframe\"\n          className={`theme-preview-${this.props.theme.name.replace(/\\./g, '-')}`}\n          frameBorder=\"0\"\n          width=\"115px\"\n          height=\"70px\"\n        />\n      </div>\n    );\n  }\n}\n\nexport default ThemeOption;\n"
  },
  {
    "path": "packages/client-app/internal_packages/theme-picker/lib/theme-picker.jsx",
    "content": "/* eslint jsx-a11y/tabindex-no-positive: 0 */\nimport React from 'react';\n\nimport {Flexbox, ScrollRegion} from 'nylas-component-kit';\nimport ThemeOption from './theme-option';\n\n\nclass ThemePicker extends React.Component {\n  static displayName = 'ThemePicker';\n\n  constructor(props) {\n    super(props);\n    this.themes = NylasEnv.themes;\n    this.state = this._getState();\n  }\n\n  componentDidMount() {\n    this.disposable = this.themes.onDidChangeActiveThemes(() => {\n      this.setState(this._getState());\n    });\n  }\n\n  componentWillUnmount() {\n    this.disposable.dispose();\n  }\n\n  _getState() {\n    return {\n      themes: this.themes.getLoadedThemes(),\n      activeTheme: this.themes.getActiveTheme().name,\n    }\n  }\n\n  _setActiveTheme(theme) {\n    const prevActiveTheme = this.state.activeTheme;\n    this.themes.setActiveTheme(theme);\n    this._rewriteIFrame(prevActiveTheme, theme);\n  }\n\n  _rewriteIFrame(prevActiveTheme, activeTheme) {\n    const prevActiveThemeDoc = document.querySelector(`.theme-preview-${prevActiveTheme.replace(/\\./g, '-')}`).contentDocument;\n    const prevActiveElement = prevActiveThemeDoc.querySelector(\".theme-option.active-true\");\n    if (prevActiveElement) prevActiveElement.className = \"theme-option active-false\";\n    const activeThemeDoc = document.querySelector(`.theme-preview-${activeTheme.replace(/\\./g, '-')}`).contentDocument;\n    const activeElement = activeThemeDoc.querySelector(\".theme-option.active-false\");\n    if (activeElement) activeElement.className = \"theme-option active-true\";\n  }\n\n  _renderThemeOptions() {\n    const internalThemes = ['ui-less-is-more', 'ui-ubuntu', 'ui-taiga', 'ui-darkside', 'ui-dark', 'ui-light'];\n    const sortedThemes = [].concat(this.state.themes);\n    sortedThemes.sort((a, b) => {\n      return (internalThemes.indexOf(a.name) - internalThemes.indexOf(b.name)) * -1;\n    });\n    return sortedThemes.map((theme) =>\n      <ThemeOption\n        key={theme.name}\n        theme={theme}\n        active={this.state.activeTheme === theme.name}\n        onSelect={() => this._setActiveTheme(theme.name)}\n      />\n    );\n  }\n\n  render() {\n    return (\n      <div className=\"theme-picker\" tabIndex=\"1\">\n        <Flexbox direction=\"column\">\n          <h4 style={{color: \"#434648\"}}>Themes</h4>\n          <div style={{color: \"rgba(35, 31, 32, 0.5)\", fontSize: \"12px\"}}>Click any theme to apply:</div>\n          <ScrollRegion style={{margin: \"10px 5px 0 5px\", height: \"290px\"}}>\n            <Flexbox\n              direction=\"row\"\n              height=\"auto\"\n              style={{alignItems: \"flex-start\", flexWrap: \"wrap\"}}\n            >\n              {this._renderThemeOptions()}\n            </Flexbox>\n          </ScrollRegion>\n          <div className=\"create-theme\">\n            <a\n              href=\"https://github.com/nylas/N1-theme-starter\"\n              style={{color: \"#3187e1\"}}\n            >\n              Create a Theme\n            </a>\n          </div>\n        </Flexbox>\n      </div>\n    );\n  }\n}\n\nexport default ThemePicker;\n"
  },
  {
    "path": "packages/client-app/internal_packages/theme-picker/package.json",
    "content": "{\n  \"name\": \"theme-picker\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"View different themes and choose them easily\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/theme-picker/preview-styles/theme-option.less",
    "content": "@import \"ui-variables\";\n\nhtml,\nbody {\n  margin: 0;\n  height: 100%;\n  width: 100%;\n  overflow: hidden;\n  -webkit-font-smoothing: antialiased;\n}\n\n.theme-option {\n  position: absolute;\n  top: 0;\n  margin-top: 4px;\n  margin-left: 5px;\n  width: 100px;\n  height: 60px;\n  background-color: @background-secondary;\n  color: @text-color;\n  border-radius: 5px;\n  text-align: center;\n  overflow: hidden;\n\n  &.active-true {\n    border: 1px solid #3187e1;\n    box-shadow: 0 0 4px #9ecaed;\n  }\n\n  &.active-false {\n    border: 1px solid darken(#f6f6f6, 10%);\n  }\n\n  .theme-name {\n    font-family: @font-family;\n    font-size: 12px;\n    font-weight: 600;\n    margin-top: 7px;\n    height: 18px;\n    overflow: hidden;\n  }\n\n  .swatches {\n    padding-left: 27px;\n    padding-right: 27px;\n    display: flex;\n    flex-direction: row;\n\n    .swatch {\n      flex: 1;\n      height: 10px;\n      width: 10px;\n      margin: 4px 2px 4px 2px;\n      border-radius: 2px;\n      border: 1px solid rgba(0, 0, 0, 0.15);\n      background-clip: border-box;\n      background-origin: border-box;\n\n      &.font-color {\n        background-color: @text-color;\n      }\n\n      &.active-color {\n        background-color: @component-active-color;\n      }\n\n      &.toolbar-color {\n        background-color: @toolbar-background-color;\n      }\n    }\n  }\n\n  .divider-black {\n    position: absolute;\n    bottom: 12px;\n    height: 1px;\n    width: 100%;\n    background-color: black;\n    opacity: 0.15;\n  }\n\n  .divider-white {\n    position: absolute;\n    z-index: 10;\n    bottom: 11px;\n    height: 1px;\n    width: 100%;\n    background-color: white;\n    opacity: 0.15;\n  }\n\n  .strip {\n    position: absolute;\n    bottom: 0;\n    height: 12px;\n    width: 100%;\n    background-color: @panel-background-color;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/theme-picker/spec/theme-picker-spec.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport ReactTestUtils from 'react-addons-test-utils';\n\nimport ThemePackage from '../../../src/theme-package';\nimport ThemePicker from '../lib/theme-picker';\n\nconst {resourcePath} = NylasEnv.getLoadSettings();\nconst light = new ThemePackage(`${resourcePath}/internal_packages/ui-light`);\nconst dark = new ThemePackage(`${resourcePath}/internal_packages/ui-dark`);\n\ndescribe('ThemePicker', function themePicker() {\n  beforeEach(() => {\n    spyOn(NylasEnv.themes, 'getLoadedThemes').andReturn([light, dark]);\n    spyOn(NylasEnv.themes, 'getActiveTheme').andReturn(light);\n    this.component = ReactTestUtils.renderIntoDocument(<ThemePicker />);\n  });\n\n  it('changes the active theme when a theme is clicked', () => {\n    spyOn(ThemePicker.prototype, '_setActiveTheme').andCallThrough();\n    spyOn(ThemePicker.prototype, '_rewriteIFrame');\n    const themeOption = ReactDOM.findDOMNode(ReactTestUtils.scryRenderedDOMComponentsWithClass(this.component, 'clickable-theme-option')[1]);\n    ReactTestUtils.Simulate.mouseDown(themeOption);\n    expect(ThemePicker.prototype._setActiveTheme).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/theme-picker/styles/theme-picker.less",
    "content": "@import \"ui-variables\";\n\n.theme-picker {\n  text-align: center;\n  cursor: default;\n  h4 {\n    font-size: 14.5px;\n    margin-top: -10px;\n    margin-bottom: 5px;\n  }\n  .clickable-theme-option {\n    width: 115px;\n    height: 70px;\n    margin: 2px;\n    top: -20px;\n    iframe {\n      pointer-events: none;\n      position: relative;\n      z-index: 0;\n    }\n  }\n  .create-theme {\n    width: 100%;\n    text-align: center;\n    margin-top: 5px;\n    a {\n      text-decoration: none;\n      cursor: default;\n    }\n  }\n}\n\n@media (-webkit-min-device-pixel-ratio: 2) {\n  .theme-picker {\n    .theme-picker-x {\n      margin: 12px;\n    }\n    .clickable-theme-option {\n      top: -10px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/category-removal-target-rulesets.es6",
    "content": "import {AccountStore, CategoryStore} from 'nylas-exports';\n\n\n/**\n * A RemovalTargetRuleset for categories is a map that represents the\n * target/destination Category when removing threads from another given\n * category, i.e., when removing them from their current CategoryPerspective.\n * Rulesets are of the form:\n *\n *   (categoryName) => function(accountId): Category\n *\n * Keys correspond to category names, e.g.`{'inbox', 'trash',...}`, which\n * correspond to the name of the categories associated with a perspective\n * Values are functions with the following signature:\n *\n *   `function(accountId): Category`\n *\n * If a value is null instead of a function, it means that removing threads from\n * that standard category has no effect, i.e. it is a no-op\n *\n * RemovalRulesets should also contain a special key `other`, that is meant to be used\n * when a key cannot be found for a given Category name\n *\n * @typedef {Object} - RemovalTargetRuleset\n * @property {(function|null)} target - Function that returns the target category\n*/\nconst CategoryRemovalTargetRulesets = {\n\n  Default: {\n    // + Has no effect in Spam, Sent.\n    spam: null,\n    sent: null,\n\n    // + In inbox, move to [Archive or Trash]\n    inbox: (accountId) => {\n      const account = AccountStore.accountForId(accountId)\n      return account.defaultFinishedCategory()\n    },\n\n    // + In all/archive, move to trash.\n    all: (accountId) => CategoryStore.getTrashCategory(accountId),\n    archive: (accountId) => CategoryStore.getTrashCategory(accountId),\n\n    // TODO\n    // + In trash, it should delete permanently or do nothing.\n    trash: null,\n\n    // + In label or folder, move to [Archive or Trash]\n    other: (accountId) => {\n      const account = AccountStore.accountForId(accountId)\n      return account.defaultFinishedCategory()\n    },\n  },\n\n  Gmail: {\n    // + It has no effect in Spam, Sent, All Mail/Archive\n    all: null,\n    spam: null,\n    sent: null,\n    archive: null,\n\n    // + In inbox, move to [Archive or Trash].\n    inbox: (accountId) => {\n      const account = AccountStore.accountForId(accountId)\n      return account.defaultFinishedCategory()\n    },\n\n    // + In trash, move to Inbox\n    trash: (accountId) => CategoryStore.getInboxCategory(accountId),\n\n    // + In label, remove label\n    // + In folder, move to archive\n    other: (accountId) => {\n      const account = AccountStore.accountForId(accountId)\n      if (account.usesFolders()) {\n        // If we are removing threads from a folder, it means we are move the\n        // threads // somewhere. In this case, to the archive\n        return CategoryStore.getArchiveCategory(account)\n      }\n      // Otherwise, when removing a label, we don't want to move it anywhere\n      return null\n    },\n  },\n}\n\nexport default CategoryRemovalTargetRulesets\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/injects-toolbar-buttons.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {ListensToObservable, InjectedComponentSet} from 'nylas-component-kit'\nimport ThreadListStore from './thread-list-store'\n\n\nexport const ToolbarRole = 'ThreadActionsToolbarButton'\n\n\nfunction defaultObservable() {\n  return ThreadListStore.selectionObservable()\n}\n\nfunction InjectsToolbarButtons(ToolbarComponent, {getObservable, extraRoles = []}) {\n  const roles = [ToolbarRole].concat(extraRoles)\n\n  class ComposedComponent extends Component {\n    static displayName = ToolbarComponent.displayName;\n\n    static propTypes = {\n      items: PropTypes.array,\n    };\n\n    static containerRequired = false;\n\n    render() {\n      const {items} = this.props;\n      const {selection} = ThreadListStore.dataSource()\n\n      // Keep all of the exposed props from deprecated regions that now map to this one\n      const exposedProps = {\n        items,\n        selection,\n        thread: items[0],\n      }\n      const injectedButtons = (\n        <InjectedComponentSet\n          key=\"injected\"\n          matching={{roles}}\n          exposedProps={exposedProps}\n        />\n      )\n      return (\n        <ToolbarComponent\n          items={items}\n          selection={selection}\n          injectedButtons={injectedButtons}\n        />\n      )\n    }\n  }\n\n  const getStateFromObservable = (items) => {\n    if (!items) {\n      return {items: []}\n    }\n    return {items}\n  }\n  return ListensToObservable(ComposedComponent, {\n    getObservable: getObservable || defaultObservable,\n    getStateFromObservable,\n  })\n}\n\nexport default InjectsToolbarButtons\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/main.es6",
    "content": "import {ComponentRegistry, WorkspaceStore} from \"nylas-exports\";\n\nimport ThreadList from './thread-list';\nimport ThreadListToolbar from './thread-list-toolbar';\nimport MessageListToolbar from './message-list-toolbar';\nimport SelectedItemsStack from './selected-items-stack';\n\nimport {\n  UpButton,\n  DownButton,\n  TrashButton,\n  ArchiveButton,\n  MarkAsSpamButton,\n  ToggleUnreadButton,\n  ToggleStarredButton,\n} from \"./thread-toolbar-buttons\";\n\nexport function activate() {\n  ComponentRegistry.register(ThreadList, {\n    location: WorkspaceStore.Location.ThreadList,\n  });\n\n  ComponentRegistry.register(SelectedItemsStack, {\n    location: WorkspaceStore.Location.MessageList,\n    modes: ['split'],\n  });\n\n  // Toolbars\n  ComponentRegistry.register(ThreadListToolbar, {\n    location: WorkspaceStore.Location.ThreadList.Toolbar,\n    modes: ['list'],\n  });\n\n  ComponentRegistry.register(MessageListToolbar, {\n    location: WorkspaceStore.Location.MessageList.Toolbar,\n  });\n\n  ComponentRegistry.register(DownButton, {\n    location: WorkspaceStore.Location.MessageList.Toolbar,\n    modes: ['list'],\n  });\n\n  ComponentRegistry.register(UpButton, {\n    location: WorkspaceStore.Location.MessageList.Toolbar,\n    modes: ['list'],\n  });\n\n  ComponentRegistry.register(ArchiveButton, {\n    role: 'ThreadActionsToolbarButton',\n  });\n\n  ComponentRegistry.register(TrashButton, {\n    role: 'ThreadActionsToolbarButton',\n  });\n\n  ComponentRegistry.register(MarkAsSpamButton, {\n    role: 'ThreadActionsToolbarButton',\n  });\n\n  ComponentRegistry.register(ToggleStarredButton, {\n    role: 'ThreadActionsToolbarButton',\n  });\n\n  ComponentRegistry.register(ToggleUnreadButton, {\n    role: 'ThreadActionsToolbarButton',\n  });\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ThreadList);\n  ComponentRegistry.unregister(SelectedItemsStack);\n  ComponentRegistry.unregister(ThreadListToolbar);\n  ComponentRegistry.unregister(MessageListToolbar);\n  ComponentRegistry.unregister(ArchiveButton);\n  ComponentRegistry.unregister(TrashButton);\n  ComponentRegistry.unregister(MarkAsSpamButton);\n  ComponentRegistry.unregister(ToggleUnreadButton);\n  ComponentRegistry.unregister(ToggleStarredButton);\n  ComponentRegistry.unregister(UpButton);\n  ComponentRegistry.unregister(DownButton);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/message-list-toolbar.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport ReactCSSTransitionGroup from 'react-addons-css-transition-group'\nimport {Rx, FocusedContentStore} from 'nylas-exports'\nimport ThreadListStore from './thread-list-store'\nimport InjectsToolbarButtons, {ToolbarRole} from './injects-toolbar-buttons'\n\n\nfunction getObservable() {\n  return (\n    Rx.Observable.combineLatest(\n      Rx.Observable.fromStore(FocusedContentStore),\n      ThreadListStore.selectionObservable(),\n      (store, items) => ({focusedThread: store.focused('thread'), items})\n    )\n    .map(({focusedThread, items}) => {\n      if (focusedThread) {\n        return [focusedThread]\n      }\n      return items\n    })\n  )\n}\n\nconst MessageListToolbar = ({items, injectedButtons}) => {\n  const shouldRender = items.length > 0\n\n  return (\n    <ReactCSSTransitionGroup\n      className=\"message-toolbar-items\"\n      transitionLeaveTimeout={125}\n      transitionEnterTimeout={125}\n      transitionName=\"opacity-125ms\"\n    >\n      {shouldRender ? injectedButtons : undefined}\n    </ReactCSSTransitionGroup>\n  )\n}\nMessageListToolbar.displayName = 'MessageListToolbar';\nMessageListToolbar.propTypes = {\n  items: PropTypes.array,\n  injectedButtons: PropTypes.element,\n};\n\nconst toolbarProps = {\n  getObservable,\n  extraRoles: [`MessageList:${ToolbarRole}`],\n}\n\nexport default InjectsToolbarButtons(MessageListToolbar, toolbarProps)\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/selected-items-stack.jsx",
    "content": "import _ from 'underscore'\nimport React, {Component, PropTypes} from 'react'\nimport {ListensToObservable} from 'nylas-component-kit'\nimport ThreadListStore from './thread-list-store'\n\n\nfunction getObservable() {\n  return (\n    ThreadListStore.selectionObservable()\n    .map(items => items.length)\n  )\n}\n\nfunction getStateFromObservable(selectionCount) {\n  if (!selectionCount) {\n    return {selectionCount: 0}\n  }\n  return {selectionCount}\n}\n\nclass SelectedItemsStack extends Component {\n  static displayName = \"SelectedItemsStack\";\n\n  static propTypes = {\n    selectionCount: PropTypes.number,\n  };\n\n  static containerRequired = false;\n\n  onClearSelection = () => {\n    ThreadListStore.dataSource().selection.clear()\n  };\n\n  render() {\n    const {selectionCount} = this.props\n    if (selectionCount <= 1) {\n      return <span />\n    }\n    const cardCount = Math.min(5, selectionCount)\n\n    return (\n      <div className=\"selected-items-stack\">\n        <div className=\"selected-items-stack-content\">\n          <div className=\"stack\">\n            {_.times(cardCount, (idx) => {\n              let deg = idx * 0.9;\n\n              if (idx === 1) {\n                deg += 0.5\n              }\n              let transform = `rotate(${deg}deg)`\n              if (idx === cardCount - 1) {\n                transform += ' translate3d(2px, 3px, 0)'\n              }\n              const style = {\n                transform,\n                zIndex: 5 - idx,\n              }\n              return <div key={`card-${idx}`} style={style} className=\"card\" />\n            })}\n          </div>\n          <div className=\"count-info\">\n            <div className=\"count\">{selectionCount}</div>\n            <div className=\"count-message\">messages selected</div>\n            <div className=\"clear btn\" onClick={this.onClearSelection}>Clear Selection</div>\n          </div>\n        </div>\n      </div>\n    )\n  }\n}\n\nexport default ListensToObservable(SelectedItemsStack, {getObservable, getStateFromObservable})\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-columns.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nclassNames = require 'classnames'\nmoment = require 'moment'\n\n{ListTabular,\n RetinaImg,\n MailLabelSet,\n MailImportantIcon,\n InjectedComponent,\n InjectedComponentSet} = require 'nylas-component-kit'\n\n{Thread, FocusedPerspectiveStore, Utils, DateUtils} = require 'nylas-exports'\n\n{ThreadArchiveQuickAction,\n ThreadTrashQuickAction} = require './thread-list-quick-actions'\n\nThreadListParticipants = require './thread-list-participants'\nThreadListStore = require './thread-list-store'\nThreadListIcon = require './thread-list-icon'\n\n# Get and format either last sent or last received timestamp depending on thread-list being viewed\nThreadListTimestamp = ({thread}) ->\n  if FocusedPerspectiveStore.current().isSent()\n    rawTimestamp = thread.lastMessageSentTimestamp\n  else\n    rawTimestamp = thread.lastMessageReceivedTimestamp\n  timestamp = DateUtils.shortTimeString(rawTimestamp)\n  return <span className=\"timestamp\">{timestamp}</span>\nThreadListTimestamp.containerRequired = false\n\nsubject = (subj) ->\n  if (subj ? \"\").trim().length is 0\n    return <span className=\"no-subject\">(No Subject)</span>\n  else if subj.split(/([\\uD800-\\uDBFF][\\uDC00-\\uDFFF])/g).length > 1\n    subjComponents = []\n    subjParts = subj.split /([\\uD800-\\uDBFF][\\uDC00-\\uDFFF])/g\n    for part, idx in subjParts\n      if part.match /([\\uD800-\\uDBFF][\\uDC00-\\uDFFF])/g\n        subjComponents.push <span className=\"emoji\" key={idx}>{part}</span>\n      else\n        subjComponents.push <span key={idx}>{part}</span>\n    return subjComponents\n  else\n    return subj\n\ngetSnippet = (thread) ->\n  messages = thread.__messages || []\n  if (messages.length is 0)\n    return thread.snippet\n\n  return messages[messages.length - 1].snippet\n\n\nc1 = new ListTabular.Column\n  name: \"★\"\n  resolver: (thread) =>\n    [\n      <ThreadListIcon key=\"thread-list-icon\" thread={thread} />\n      <MailImportantIcon\n        key=\"mail-important-icon\"\n        thread={thread}\n        showIfAvailableForAnyAccount={true}\n      />\n      <InjectedComponentSet\n        key=\"injected-component-set\"\n        inline={true}\n        containersRequired={false}\n        matching={role: \"ThreadListIcon\"}\n        className=\"thread-injected-icons\"\n        exposedProps={thread: thread}\n      />\n    ]\n\nc2 = new ListTabular.Column\n  name: \"Participants\"\n  width: 200\n  resolver: (thread) =>\n    hasDraft = (thread.__messages || []).find((m) => m.draft)\n    if hasDraft\n      <div style={display: 'flex'}>\n        <ThreadListParticipants thread={thread} />\n        <RetinaImg name=\"icon-draft-pencil.png\"\n                   className=\"draft-icon\"\n                   mode={RetinaImg.Mode.ContentPreserve} />\n      </div>\n    else\n      <ThreadListParticipants thread={thread} />\n\nc3 = new ListTabular.Column\n  name: \"Message\"\n  flex: 4\n  resolver: (thread) =>\n    attachment = false\n    messages = thread.__messages || []\n\n    hasAttachments = thread.hasAttachments and messages.find (m) -> Utils.showIconForAttachments(m.files)\n    if hasAttachments\n      attachment = <div className=\"thread-icon thread-icon-attachment\"></div>\n\n    <span className=\"details\">\n      <MailLabelSet thread={thread} />\n      <span className=\"subject\">{subject(thread.subject)}</span>\n      <span className=\"snippet\">{getSnippet(thread)}</span>\n      {attachment}\n    </span>\n\nc4 = new ListTabular.Column\n  name: \"Date\"\n  resolver: (thread) =>\n    return (\n      <InjectedComponent\n        className=\"thread-injected-timestamp\"\n        fallback={ThreadListTimestamp}\n        exposedProps={thread: thread}\n        matching={role: \"ThreadListTimestamp\"}\n      />\n    )\n\nc5 = new ListTabular.Column\n  name: \"HoverActions\"\n  resolver: (thread) =>\n    <div className=\"inner\">\n      <InjectedComponentSet\n        key=\"injected-component-set\"\n        inline={true}\n        containersRequired={false}\n        children=\n        {[\n          <ThreadTrashQuickAction key=\"thread-trash-quick-action\" thread={thread} />\n          <ThreadArchiveQuickAction key=\"thread-archive-quick-action\" thread={thread} />\n        ]}\n        matching={role: \"ThreadListQuickAction\"}\n        className=\"thread-injected-quick-actions\"\n        exposedProps={thread: thread}\n      />\n    </div>\n\ncNarrow = new ListTabular.Column\n  name: \"Item\"\n  flex: 1\n  resolver: (thread) =>\n    pencil = false\n    attachment = false\n    messages = thread.__messages || []\n\n    hasAttachments = thread.hasAttachments and messages.find (m) -> Utils.showIconForAttachments(m.files)\n    if hasAttachments\n      attachment = <div className=\"thread-icon thread-icon-attachment\"></div>\n\n    hasDraft = messages.find((m) => m.draft)\n    if hasDraft\n      pencil = <RetinaImg name=\"icon-draft-pencil.png\" className=\"draft-icon\" mode={RetinaImg.Mode.ContentPreserve} />\n\n    # TODO We are limiting the amount on injected icons in narrow mode to 1\n    # until we revisit the UI to accommodate more icons\n    <div style={display: 'flex', alignItems: 'flex-start'}>\n      <div className=\"icons-column\">\n        <ThreadListIcon thread={thread} />\n        <InjectedComponentSet\n          inline={true}\n          matchLimit={1}\n          direction=\"column\"\n          containersRequired={false}\n          key=\"injected-component-set\"\n          exposedProps={thread: thread}\n          matching={role: \"ThreadListIcon\"}\n          className=\"thread-injected-icons\"\n        />\n        <MailImportantIcon\n          thread={thread}\n          showIfAvailableForAnyAccount={true}\n        />\n      </div>\n      <div className=\"thread-info-column\">\n        <div className=\"participants-wrapper\">\n          <ThreadListParticipants thread={thread} />\n          {pencil}\n          <span style={flex:1}></span>\n          {attachment}\n          <InjectedComponent\n            key=\"thread-injected-timestamp\"\n            className=\"thread-injected-timestamp\"\n            fallback={ThreadListTimestamp}\n            exposedProps={thread: thread}\n            matching={role: \"ThreadListTimestamp\"}\n          />\n        </div>\n        <div className=\"subject\">{subject(thread.subject)}</div>\n        <div className=\"snippet-and-labels\">\n          <div className=\"snippet\">{getSnippet(thread)}&nbsp;</div>\n          <div style={flex: 1, flexShrink: 1}></div>\n          <MailLabelSet thread={thread} />\n        </div>\n      </div>\n    </div>\n\nmodule.exports =\n  Narrow: [cNarrow]\n  Wide: [c1, c2, c3, c4, c5]\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-context-menu.es6",
    "content": "/* eslint global-require: 0*/\nimport _ from 'underscore'\nimport {\n  Thread,\n  Actions,\n  Message,\n  TaskFactory,\n  DatabaseStore,\n  FocusedPerspectiveStore,\n} from 'nylas-exports'\n\nexport default class ThreadListContextMenu {\n  constructor({threadIds = [], accountIds = []}) {\n    this.threadIds = threadIds\n    this.accountIds = accountIds\n  }\n\n  menuItemTemplate() {\n    return DatabaseStore.modelify(Thread, this.threadIds)\n    .then((threads) => {\n      this.threads = threads;\n\n      return Promise.all([\n        this.replyItem(),\n        this.replyAllItem(),\n        this.forwardItem(),\n        {type: 'separator'},\n        this.archiveItem(),\n        this.trashItem(),\n        this.markAsReadItem(),\n        this.starItem(),\n        // this.moveToOrLabelItem(),\n        // {type: 'separator'},\n        // this.extensionItems(),\n      ])\n    }).then((menuItems) => {\n      return _.filter(_.compact(menuItems), (item, index) => {\n        if ((index === 0 || index === menuItems.length - 1) && item.type === \"separator\") {\n          return false\n        }\n        return true\n      });\n    });\n  }\n\n  replyItem() {\n    if (this.threadIds.length !== 1) { return null }\n    return {\n      label: \"Reply\",\n      click: () => {\n        Actions.composeReply({\n          threadId: this.threadIds[0],\n          popout: true,\n          type: 'reply',\n          behavior: 'prefer-existing-if-pristine',\n        });\n      },\n    }\n  }\n\n  replyAllItem() {\n    if (this.threadIds.length !== 1) {\n      return null;\n    }\n\n    return DatabaseStore.findBy(Message, {threadId: this.threadIds[0]})\n    .order(Message.attributes.date.descending())\n    .limit(1)\n    .then((message) => {\n      if (message && message.canReplyAll()) {\n        return {\n          label: \"Reply All\",\n          click: () => {\n            Actions.composeReply({\n              threadId: this.threadIds[0],\n              popout: true,\n              type: 'reply-all',\n              behavior: 'prefer-existing-if-pristine',\n            });\n          },\n        }\n      }\n      return null;\n    })\n  }\n\n  forwardItem() {\n    if (this.threadIds.length !== 1) { return null }\n    return {\n      label: \"Forward\",\n      click: () => {\n        Actions.composeForward({threadId: this.threadIds[0], popout: true});\n      },\n    }\n  }\n\n  archiveItem() {\n    const perspective = FocusedPerspectiveStore.current()\n    const allowed = perspective.canArchiveThreads(this.threads)\n    if (!allowed) {\n      return null\n    }\n    return {\n      label: \"Archive\",\n      click: () => {\n        Actions.archiveThreads({\n          source: \"Context Menu: Thread List\",\n          threads: this.threads,\n        })\n      },\n    }\n  }\n\n  trashItem() {\n    const perspective = FocusedPerspectiveStore.current()\n    const allowed = perspective.canMoveThreadsTo(this.threads, 'trash')\n    if (!allowed) {\n      return null\n    }\n    return {\n      label: \"Trash\",\n      click: () => {\n        Actions.trashThreads({\n          source: \"Context Menu: Thread List\",\n          threads: this.threads,\n        })\n      },\n    }\n  }\n\n  markAsReadItem() {\n    const unread = _.every(this.threads, (t) => {\n      return _.isMatch(t, {unread: false})\n    });\n    const dir = unread ? \"Unread\" : \"Read\"\n\n    return {\n      label: `Mark as ${dir}`,\n      click: () => {\n        Actions.toggleUnreadThreads({\n          source: \"Context Menu: Thread List\",\n          threads: this.threads,\n        })\n      },\n    }\n  }\n\n  starItem() {\n    const starred = _.every(this.threads, (t) => {\n      return _.isMatch(t, {starred: false})\n    });\n\n    let dir = \"\"\n    let star = \"Star\"\n    if (!starred) {\n      dir = \"Remove \"\n      star = (this.threadIds.length > 1) ? \"Stars\" : \"Star\"\n    }\n\n\n    return {\n      label: `${dir}${star}`,\n      click: () => {\n        Actions.toggleStarredThreads({\n          source: \"Context Menu: Thread List\",\n          threads: this.threads,\n        })\n      },\n    }\n  }\n\n  displayMenu() {\n    const {remote} = require('electron')\n    this.menuItemTemplate().then((template) => {\n      remote.Menu.buildFromTemplate(template)\n        .popup(remote.getCurrentWindow());\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-data-source.es6",
    "content": "import {\n  Rx,\n  ObservableListDataSource,\n  DatabaseStore,\n  Message,\n  QueryResultSet,\n  QuerySubscription,\n} from 'nylas-exports';\n\nconst _observableForThreadMessages = (id, initialModels) => {\n  const subscription = new QuerySubscription(DatabaseStore.findAll(Message, {threadId: id}), {\n    initialModels: initialModels,\n    emitResultSet: true,\n  });\n  return Rx.Observable.fromNamedQuerySubscription(`message-${id}`, subscription);\n};\n\nconst _flatMapJoiningMessages = ($threadsResultSet) => {\n  // DatabaseView leverages `QuerySubscription` for threads /and/ for the\n  // messages on each thread, which are passed to out as `thread.__messages`.\n  let $messagesResultSets = {};\n\n  // 2. when we receive a set of threads, we check to see if we have message\n  //    observables for each thread. If threads have been added to the result set,\n  //    we make a single database query and load /all/ the message metadata for\n  //    the new threads at once. (This is a performance optimization -it's about\n  //    ~80msec faster than making 100 queries for 100 new thread ids separately.)\n  return $threadsResultSet.flatMapLatest((threadsResultSet) => {\n    const missingIds = threadsResultSet.ids().filter(id => !$messagesResultSets[id]);\n    let promise = null;\n    if (missingIds.length === 0) {\n      promise = Promise.resolve([threadsResultSet, []]);\n    } else {\n      promise = DatabaseStore.findAll(Message, {threadId: missingIds}).then((messages) => {\n        return Promise.resolve([threadsResultSet, messages]);\n      });\n    }\n    return Rx.Observable.fromPromise(promise);\n  })\n\n  // 3. when that finishes, we group the loaded messsages by threadId and create\n  //    the missing observables. Creating a query subscription would normally load\n  //    an initial result set. To avoid that, we just hand new subscriptions the\n  //    results we loaded in #2.\n  .flatMapLatest(([threadsResultSet, messagesForNewThreads]) => {\n    const messagesGrouped = {};\n    for (const message of messagesForNewThreads) {\n      if (messagesGrouped[message.threadId] == null) { messagesGrouped[message.threadId] = []; }\n      messagesGrouped[message.threadId].push(message);\n    }\n\n    const oldSets = $messagesResultSets;\n    $messagesResultSets = {};\n\n    const sets = threadsResultSet.ids().map(id => {\n      $messagesResultSets[id] = oldSets[id] || _observableForThreadMessages(id, messagesGrouped[id]);\n      return $messagesResultSets[id];\n    });\n    sets.unshift(Rx.Observable.from([threadsResultSet]));\n\n    // 4. We use `combineLatest` to merge the message observables into a single\n    //    stream (like Promise.all).  When /any/ of them emit a new result set, we\n    //    trigger.\n    return Rx.Observable.combineLatest(sets);\n  })\n\n  .flatMapLatest(([threadsResultSet, ...messagesResultSets]) => {\n    const threadsWithMessages = {};\n    threadsResultSet.models().forEach((thread, idx) => {\n      const clone = new thread.constructor(thread);\n      clone.__messages = messagesResultSets[idx] ? messagesResultSets[idx].models() : [];\n      clone.__messages = clone.__messages.filter((m) => !m.isHidden())\n      threadsWithMessages[clone.id] = clone;\n    });\n\n    return Rx.Observable.from([\n      QueryResultSet.setByApplyingModels(threadsResultSet, threadsWithMessages),\n    ]);\n  });\n};\n\n\nclass ThreadListDataSource extends ObservableListDataSource {\n  constructor(subscription) {\n    let $resultSetObservable = Rx.Observable.fromNamedQuerySubscription('thread-list', subscription);\n    $resultSetObservable = _flatMapJoiningMessages($resultSetObservable);\n    super($resultSetObservable, subscription.replaceRange.bind(subscription));\n  }\n}\n\nexport default ThreadListDataSource;\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-icon.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\n{DraftHelpers,\n Actions,\n Thread,\n ChangeStarredTask,\n ExtensionRegistry,\n AccountStore} = require 'nylas-exports'\n\nclass ThreadListIcon extends React.Component\n  @displayName: 'ThreadListIcon'\n  @propTypes:\n    thread: React.PropTypes.object\n\n  _extensionsIconClassNames: =>\n    return ExtensionRegistry.ThreadList.extensions()\n    .filter((ext) => ext.cssClassNamesForThreadListIcon?)\n    .reduce(((prev, ext) => prev + ' ' + ext.cssClassNamesForThreadListIcon(@props.thread)), '')\n    .trim()\n\n  _iconClassNames: =>\n    if !@props.thread\n      return 'thread-icon-star-on-hover'\n\n    extensionIconClassNames = @_extensionsIconClassNames()\n    if extensionIconClassNames.length > 0\n      return extensionIconClassNames\n\n    if @props.thread.starred\n      return 'thread-icon-star'\n\n    if @props.thread.unread\n      return 'thread-icon-unread thread-icon-star-on-hover'\n\n    msgs = @_nonDraftMessages()\n    last = msgs[msgs.length - 1]\n\n    if msgs.length > 1 and last.from[0]?.isMe()\n      if DraftHelpers.isForwardedMessage(last)\n        return 'thread-icon-forwarded thread-icon-star-on-hover'\n      else\n        return 'thread-icon-replied thread-icon-star-on-hover'\n\n    return 'thread-icon-none thread-icon-star-on-hover'\n\n  _nonDraftMessages: =>\n    msgs = @props.thread.__messages\n    return [] unless msgs and msgs instanceof Array\n    msgs = _.filter msgs, (m) -> m.serverId and not m.draft\n    return msgs\n\n  shouldComponentUpdate: (nextProps) =>\n    return false if nextProps.thread is @props.thread\n    true\n\n  render: =>\n    <div className=\"thread-icon #{@_iconClassNames()}\"\n         title=\"Star\"\n         onClick={@_onToggleStar}></div>\n\n  _onToggleStar: (event) =>\n    Actions.toggleStarredThreads(threads: [@props.thread], source: \"Thread List Icon\")\n    # Don't trigger the thread row click\n    event.stopPropagation()\n\nmodule.exports = ThreadListIcon\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-participants.cjsx",
    "content": "React = require 'react'\n{Utils} = require 'nylas-exports'\n_ = require 'underscore'\n\nclass ThreadListParticipants extends React.Component\n  @displayName: 'ThreadListParticipants'\n\n  @propTypes:\n    thread: React.PropTypes.object.isRequired\n\n  shouldComponentUpdate: (nextProps) =>\n    return false if nextProps.thread is @props.thread\n    true\n\n  render: =>\n    items = @getTokens()\n    <div className=\"participants\">\n      {@renderSpans(items)}\n    </div>\n\n  renderSpans: (items) =>\n    spans = []\n    accumulated = null\n    accumulatedUnread = false\n\n    flush = ->\n      if accumulated\n        spans.push <span key={spans.length} className=\"unread-#{accumulatedUnread}\">{accumulated}</span>\n      accumulated = null\n      accumulatedUnread = false\n\n    accumulate = (text, unread) ->\n      if accumulated and unread and accumulatedUnread isnt unread\n        flush()\n      if accumulated\n        accumulated += text\n      else\n        accumulated = text\n        accumulatedUnread = unread\n\n    for {spacer, contact, unread}, idx in items\n      if spacer\n        accumulate('...')\n      else\n        if contact.name.length > 0\n          if items.length > 1\n            short = contact.displayName(includeAccountLabel: false, compact: true)\n          else\n            short = contact.displayName(includeAccountLabel: false)\n        else\n          short = contact.email\n        if idx < items.length-1 and not items[idx+1].spacer\n          short += \", \"\n        accumulate(short, unread)\n\n    messages = (@props.thread.__messages ? [])\n    if messages.length > 1\n      accumulate(\" (#{messages.length})\")\n\n    flush()\n\n    return spans\n\n  getTokensFromMessages: =>\n    messages = @props.thread.__messages\n    tokens = []\n\n    field = 'from'\n    if (messages.every (message) -> message.isFromMe())\n      field = 'to'\n\n    for message, idx in messages\n      if message.draft\n        continue\n\n      for contact in message[field]\n        if tokens.length is 0\n          tokens.push({ contact: contact, unread: message.unread })\n        else\n          lastToken = tokens[tokens.length - 1]\n          lastContact = lastToken.contact\n\n          sameEmail = Utils.emailIsEquivalent(lastContact.email, contact.email)\n          sameName = lastContact.name is contact.name\n          if sameEmail and sameName\n            lastToken.unread ||= message.unread\n          else\n            tokens.push({ contact: contact, unread: message.unread })\n\n    tokens\n\n  getTokensFromParticipants: =>\n    contacts = @props.thread.participants ? []\n    contacts = contacts.filter (contact) -> not contact.isMe()\n    contacts.map (contact) -> { contact: contact, unread: false }\n\n  getTokens: =>\n    if @props.thread.__messages instanceof Array\n      list = @getTokensFromMessages()\n    else\n      list = @getTokensFromParticipants()\n\n    # If no participants, we should at least add current user as sole participant\n    if list.length is 0 and @props.thread.participants?.length > 0\n      list.push({ contact: @props.thread.participants[0], unread: false })\n\n    # We only ever want to show three. Ben...Kevin... Marty\n    # But we want the *right* three.\n    if list.length > 3\n      listTrimmed = [\n        # Always include the first item\n        list[0],\n        { spacer: true },\n\n        # Always include last two items\n        list[list.length - 2],\n        list[list.length - 1]\n      ]\n      list = listTrimmed\n\n    list\n\n\nmodule.exports = ThreadListParticipants\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-quick-actions.cjsx",
    "content": "React = require 'react'\n{Actions,\n CategoryStore,\n TaskFactory,\n AccountStore,\n FocusedPerspectiveStore} = require 'nylas-exports'\n\nclass ThreadArchiveQuickAction extends React.Component\n  @displayName: 'ThreadArchiveQuickAction'\n  @propTypes:\n    thread: React.PropTypes.object\n\n  render: =>\n    allowed = FocusedPerspectiveStore.current().canArchiveThreads([@props.thread])\n    return <span /> unless allowed\n\n    <div\n      key=\"archive\"\n      title=\"Archive\"\n      style={{ order: 100 }}\n      className=\"btn action action-archive\"\n      onClick={@_onArchive} />\n\n  shouldComponentUpdate: (newProps, newState) ->\n    newProps.thread.id isnt @props?.thread.id\n\n  _onArchive: (event) =>\n    # Don't trigger the thread row click\n    event.stopPropagation()\n    Actions.archiveThreads({\n      source: \"Quick Actions: Thread List\",\n      threads: [@props.thread],\n    })\n\nclass ThreadTrashQuickAction extends React.Component\n  @displayName: 'ThreadTrashQuickAction'\n  @propTypes:\n    thread: React.PropTypes.object\n\n  render: =>\n    allowed = FocusedPerspectiveStore.current().canMoveThreadsTo([@props.thread], 'trash')\n    return <span /> unless allowed\n\n    <div\n      key=\"remove\"\n      title=\"Trash\"\n      style={{ order: 110 }}\n      className='btn action action-trash'\n      onClick={@_onRemove} />\n\n  shouldComponentUpdate: (newProps, newState) ->\n    newProps.thread.id isnt @props?.thread.id\n\n  _onRemove: (event) =>\n    Actions.trashThreads({\n      source: \"Quick Actions: Thread List\",\n      threads: [@props.thread],\n    })\n    # Don't trigger the thread row click\n    event.stopPropagation()\n\nmodule.exports = { ThreadArchiveQuickAction, ThreadTrashQuickAction }\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-scroll-tooltip.cjsx",
    "content": "React = require 'react'\n{Utils, DateUtils} = require 'nylas-exports'\nThreadListStore = require './thread-list-store'\n\nclass ThreadListScrollTooltip extends React.Component\n  @displayName: 'ThreadListScrollTooltip'\n  @propTypes:\n    viewportCenter: React.PropTypes.number.isRequired\n    totalHeight: React.PropTypes.number.isRequired\n\n  componentWillMount: =>\n    @setupForProps(@props)\n\n  componentWillReceiveProps: (newProps) =>\n    @setupForProps(newProps)\n\n  shouldComponentUpdate: (newProps, newState) =>\n    @state?.idx isnt newState.idx\n\n  setupForProps: (props) ->\n    idx = Math.floor(ThreadListStore.dataSource().count() / @props.totalHeight * @props.viewportCenter)\n    @setState\n      idx: idx\n      item: ThreadListStore.dataSource().get(idx)\n\n  render: ->\n    if @state.item\n      content = DateUtils.shortTimeString(@state.item.lastMessageReceivedTimestamp)\n    else\n      content = \"Loading...\"\n    <div className=\"scroll-tooltip\">\n      {content}\n    </div>\n\nmodule.exports = ThreadListScrollTooltip\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-store.coffee",
    "content": "_ = require 'underscore'\nNylasStore = require 'nylas-store'\n\n{Rx,\n Thread,\n Message,\n Actions,\n DatabaseStore,\n WorkspaceStore,\n FocusedContentStore,\n TaskQueueStatusStore,\n FocusedPerspectiveStore} = require 'nylas-exports'\n{ListTabular} = require 'nylas-component-kit'\n\nThreadListDataSource = require('./thread-list-data-source').default\n\nclass ThreadListStore extends NylasStore\n  constructor: ->\n    @listenTo FocusedPerspectiveStore, @_onPerspectiveChanged\n    @createListDataSource()\n\n  dataSource: =>\n    @_dataSource\n\n  createListDataSource: =>\n    @_dataSourceUnlisten?()\n    @_dataSource = null\n\n    threadsSubscription = FocusedPerspectiveStore.current().threads()\n    if threadsSubscription\n      @_dataSource = new ThreadListDataSource(threadsSubscription)\n      @_dataSourceUnlisten = @_dataSource.listen(@_onDataChanged, @)\n\n    else\n      @_dataSource = new ListTabular.DataSource.Empty()\n\n    @trigger(@)\n    Actions.setFocus(collection: 'thread', item: null)\n\n  selectionObservable: =>\n    return Rx.Observable.fromListSelection(@)\n\n  # Inbound Events\n\n  _onPerspectiveChanged: =>\n    @createListDataSource()\n\n  _onDataChanged: ({previous, next} = {}) =>\n    # This code keeps the focus and keyboard cursor in sync with the thread list.\n    # When the thread list changes, it looks to see if the focused thread is gone,\n    # or no longer matches the query criteria and advances the focus to the next\n    # thread.\n\n    # This means that removing a thread from view in any way causes selection\n    # to advance to the adjacent thread. Nice and declarative.\n\n    if previous and next\n      focused = FocusedContentStore.focused('thread')\n      keyboard = FocusedContentStore.keyboardCursor('thread')\n      viewModeAutofocuses = WorkspaceStore.layoutMode() is 'split' or WorkspaceStore.topSheet().root is true\n      matchers = next.query()?.matchers()\n\n      focusedIndex = if focused then previous.offsetOfId(focused.id) else -1\n      keyboardIndex = if keyboard then previous.offsetOfId(keyboard.id) else -1\n\n      nextItemFromIndex = (i) =>\n        if i > 0 and (next.modelAtOffset(i - 1)?.unread or i >= next.count())\n          nextIndex = i - 1\n        else\n          nextIndex = i\n\n        # May return null if no thread is loaded at the next index\n        next.modelAtOffset(nextIndex)\n\n      notInSet = (model) ->\n        if matchers\n          return model.matches(matchers) is false\n        else\n          return next.offsetOfId(model.id) is -1\n\n      if viewModeAutofocuses and focused and notInSet(focused)\n        Actions.setFocus(collection: 'thread', item: nextItemFromIndex(focusedIndex))\n\n      if keyboard and notInSet(keyboard)\n        Actions.setCursorPosition(collection: 'thread', item: nextItemFromIndex(keyboardIndex))\n\n\nmodule.exports = new ThreadListStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list-toolbar.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {MultiselectToolbar} from 'nylas-component-kit'\nimport InjectsToolbarButtons, {ToolbarRole} from './injects-toolbar-buttons'\n\n\nclass ThreadListToolbar extends Component {\n  static displayName = 'ThreadListToolbar';\n\n  static propTypes = {\n    items: PropTypes.array,\n    selection: PropTypes.shape({\n      clear: PropTypes.func,\n    }),\n    injectedButtons: PropTypes.element,\n  };\n\n  onClearSelection = () => {\n    this.props.selection.clear()\n  };\n\n  render() {\n    const {injectedButtons, items} = this.props\n\n    return (\n      <MultiselectToolbar\n        collection=\"thread\"\n        selectionCount={items.length}\n        toolbarElement={injectedButtons}\n        onClearSelection={this.onClearSelection}\n      />\n    )\n  }\n}\n\nconst toolbarProps = {\n  extraRoles: [`ThreadList:${ToolbarRole}`],\n}\n\nexport default InjectsToolbarButtons(ThreadListToolbar, toolbarProps)\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-list.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\nclassnames = require 'classnames'\n\n{MultiselectList,\n FocusContainer,\n EmptyListState,\n FluxContainer\n SyncingListState} = require 'nylas-component-kit'\n\n{Actions,\n Utils,\n Thread,\n Category,\n CanvasUtils,\n TaskFactory,\n ChangeStarredTask,\n WorkspaceStore,\n AccountStore,\n CategoryStore,\n ExtensionRegistry,\n FocusedContentStore,\n FocusedPerspectiveStore\n FolderSyncProgressStore} = require 'nylas-exports'\n\nThreadListColumns = require './thread-list-columns'\nThreadListScrollTooltip = require './thread-list-scroll-tooltip'\nThreadListStore = require './thread-list-store'\nThreadListContextMenu = require('./thread-list-context-menu').default\nCategoryRemovalTargetRulesets = require('./category-removal-target-rulesets').default\n\n\nclass ThreadList extends React.Component\n  @displayName: 'ThreadList'\n\n  @containerRequired: false\n  @containerStyles:\n    minWidth: 300\n    maxWidth: 3000\n\n  constructor: (@props) ->\n    @state =\n      style: 'unknown'\n      syncing: false\n\n  componentDidMount: =>\n    @_reportAppBootTime()\n    @unsub = FolderSyncProgressStore.listen(@_onSyncStatusChanged)\n    window.addEventListener('resize', @_onResize, true)\n    ReactDOM.findDOMNode(@).addEventListener('contextmenu', @_onShowContextMenu)\n    @_onResize()\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    return (\n      (not Utils.isEqualReact(@props, nextProps)) or\n      (not Utils.isEqualReact(@state, nextState))\n    )\n\n  componentWillUnmount: =>\n    @unsub()\n    window.removeEventListener('resize', @_onResize, true)\n    ReactDOM.findDOMNode(@).removeEventListener('contextmenu', @_onShowContextMenu)\n\n  _reportAppBootTime: =>\n    if NylasEnv.timer.isPending('app-boot')\n      Actions.recordPerfMetric({\n        action: 'app-boot',\n        actionTimeMs: NylasEnv.timer.stop('app-boot'),\n        maxValue: 60 * 1000,\n      })\n\n  _shift: ({offset, afterRunning}) =>\n    dataSource = ThreadListStore.dataSource()\n    focusedId = FocusedContentStore.focusedId('thread')\n    focusedIdx = Math.min(dataSource.count() - 1, Math.max(0, dataSource.indexOfId(focusedId) + offset))\n    item = dataSource.get(focusedIdx)\n    afterRunning()\n    Actions.setFocus(collection: 'thread', item: item)\n\n  _keymapHandlers: ->\n    'core:remove-from-view': =>\n      @_onRemoveFromView()\n    'core:gmail-remove-from-view': =>\n      @_onRemoveFromView(CategoryRemovalTargetRulesets.Gmail)\n    'core:archive-item': @_onArchiveItem\n    'core:delete-item': @_onDeleteItem\n    'core:star-item': @_onStarItem\n    'core:snooze-item': @_onSnoozeItem\n    'core:mark-important': => @_onSetImportant(true)\n    'core:mark-unimportant': => @_onSetImportant(false)\n    'core:mark-as-unread': => @_onSetUnread(true)\n    'core:mark-as-read': => @_onSetUnread(false)\n    'core:report-as-spam': => @_onMarkAsSpam(false)\n    'core:remove-and-previous': =>\n      @_shift(offset: -1, afterRunning: @_onRemoveFromView)\n    'core:remove-and-next': =>\n      @_shift(offset: 1, afterRunning: @_onRemoveFromView)\n    'thread-list:select-read': @_onSelectRead\n    'thread-list:select-unread': @_onSelectUnread\n    'thread-list:select-starred': @_onSelectStarred\n    'thread-list:select-unstarred': @_onSelectUnstarred\n\n  _getFooter: ->\n    return null unless @state.syncing\n    return null if ThreadListStore.dataSource().count() <= 0\n    return <SyncingListState />\n\n  render: ->\n    if @state.style is 'wide'\n      columns = ThreadListColumns.Wide\n      itemHeight = 36\n    else\n      columns = ThreadListColumns.Narrow\n      itemHeight = 85\n\n    <FluxContainer\n      footer={@_getFooter()}\n      stores=[ThreadListStore]\n      getStateFromStores={ -> dataSource: ThreadListStore.dataSource() }>\n      <FocusContainer collection=\"thread\">\n        <MultiselectList\n          ref=\"list\"\n          draggable\n          columns={columns}\n          itemPropsProvider={@_threadPropsProvider}\n          itemHeight={itemHeight}\n          className=\"thread-list thread-list-#{@state.style}\"\n          scrollTooltipComponent={ThreadListScrollTooltip}\n          EmptyComponent={EmptyListState}\n          keymapHandlers={@_keymapHandlers()}\n          onDoubleClick={(thread) -> Actions.popoutThread(thread)}\n          onDragStart={@_onDragStart}\n          onDragEnd={@_onDragEnd}\n          onComponentDidUpdate={@_onThreadListDidUpdate}\n        />\n      </FocusContainer>\n    </FluxContainer>\n\n  _onThreadListDidUpdate: =>\n    dataSource = ThreadListStore.dataSource()\n    threads = dataSource.itemsCurrentlyInView()\n    Actions.threadListDidUpdate(threads)\n\n  _threadPropsProvider: (item) ->\n    classes = classnames({\n      'unread': item.unread\n    })\n    classes += ExtensionRegistry.ThreadList.extensions()\n    .filter((ext) => ext.cssClassNamesForThreadListItem?)\n    .reduce(((prev, ext) => prev + ' ' + ext.cssClassNamesForThreadListItem(item)), ' ')\n\n    props =\n      className: classes\n\n\n    # TODO this swiping logic needs some serious cleanup\n    props.shouldEnableSwipe = =>\n      perspective = FocusedPerspectiveStore.current()\n      tasks = perspective.tasksForRemovingItems([item], CategoryRemovalTargetRulesets.Default, \"Swipe\")\n      return tasks.length > 0\n\n    props.onSwipeRightClass = =>\n      perspective = FocusedPerspectiveStore.current()\n      tasks = perspective.tasksForRemovingItems([item], CategoryRemovalTargetRulesets.Default, \"Swipe\")\n      return null if tasks.length is 0\n\n      task = tasks[0]\n      name = if task instanceof ChangeStarredTask\n        'unstar'\n      else if task.categoriesToAdd().length is 1\n        task.categoriesToAdd()[0].name\n      else\n        'remove'\n\n      return \"swipe-#{name}\"\n\n    props.onSwipeRight = (callback) ->\n      perspective = FocusedPerspectiveStore.current()\n      tasks = perspective.tasksForRemovingItems([item], CategoryRemovalTargetRulesets.Default, \"Swipe\")\n      if tasks.length is 0\n        callback(false)\n        return\n      Actions.removeThreadsFromView({threads: [item], source: 'Swipe', ruleset: CategoryRemovalTargetRulesets.Default})\n      Actions.closePopover()\n      callback(true)\n\n    disabledPackages = NylasEnv.config.get('core.disabledPackages') ? []\n    if 'thread-snooze' in disabledPackages\n      return props\n\n    if FocusedPerspectiveStore.current().isInbox()\n      props.onSwipeLeftClass = 'swipe-snooze'\n      props.onSwipeCenter = =>\n        Actions.closePopover()\n      props.onSwipeLeft = (callback) =>\n        # TODO this should be grabbed from elsewhere\n        SnoozePopover = require('../../thread-snooze/lib/snooze-popover').default\n\n        element = document.querySelector(\"[data-item-id=\\\"#{item.id}\\\"]\")\n        originRect = element.getBoundingClientRect()\n        Actions.openPopover(\n          <SnoozePopover\n            threads={[item]}\n            swipeCallback={callback} />,\n          {originRect, direction: 'right', fallbackDirection: 'down'}\n        )\n\n    return props\n\n  _targetItemsForMouseEvent: (event) ->\n    itemThreadId = @refs.list.itemIdAtPoint(event.clientX, event.clientY)\n    unless itemThreadId\n      return null\n\n    dataSource = ThreadListStore.dataSource()\n    if itemThreadId in dataSource.selection.ids()\n      return {\n        threadIds: dataSource.selection.ids()\n        accountIds: _.uniq(_.pluck(dataSource.selection.items(), 'accountId'))\n      }\n    else\n      thread = dataSource.getById(itemThreadId)\n      return null unless thread\n      return {\n        threadIds: [thread.id]\n        accountIds: [thread.accountId]\n      }\n\n  _onSyncStatusChanged: =>\n    syncing = FocusedPerspectiveStore.current().hasSyncingCategories()\n    @setState({syncing})\n\n  _onShowContextMenu: (event) =>\n    data = @_targetItemsForMouseEvent(event)\n    if not data\n      event.preventDefault()\n      return\n    (new ThreadListContextMenu(data)).displayMenu()\n\n  _onDragStart: (event) =>\n    data = @_targetItemsForMouseEvent(event)\n    if not data\n      event.preventDefault()\n      return\n\n    event.dataTransfer.effectAllowed = \"move\"\n    event.dataTransfer.dragEffect = \"move\"\n\n    canvas = CanvasUtils.canvasWithThreadDragImage(data.threadIds.length)\n    event.dataTransfer.setDragImage(canvas, 10, 10)\n    event.dataTransfer.setData(\"nylas-threads-data\", JSON.stringify(data))\n    event.dataTransfer.setData(\"nylas-accounts=#{data.accountIds.join(',')}\", \"1\")\n    return\n\n  _onDragEnd: (event) =>\n\n  _onResize: (event) =>\n    current = @state.style\n    desired = if ReactDOM.findDOMNode(@).offsetWidth < 540 then 'narrow' else 'wide'\n    if current isnt desired\n      @setState(style: desired)\n\n  _threadsForKeyboardAction: ->\n    return null unless ThreadListStore.dataSource()\n    focused = FocusedContentStore.focused('thread')\n    if focused\n      return [focused]\n    else if ThreadListStore.dataSource().selection.count() > 0\n      return ThreadListStore.dataSource().selection.items()\n    else\n      return null\n\n  _onStarItem: =>\n    threads = @_threadsForKeyboardAction()\n    return unless threads\n    Actions.toggleStarredThreads({threads, source: \"Keyboard Shortcut\"})\n\n  _onSnoozeItem: =>\n    disabledPackages = NylasEnv.config.get('core.disabledPackages') ? []\n    if 'thread-snooze' in disabledPackages\n      return\n\n    threads = @_threadsForKeyboardAction()\n    return unless threads\n    # TODO this should be grabbed from elsewhere\n    SnoozePopover = require('../../thread-snooze/lib/snooze-popover').default\n\n    element = document.querySelector(\".snooze-button.btn.btn-toolbar\")\n    return unless element\n    originRect = element.getBoundingClientRect()\n    Actions.openPopover(\n      <SnoozePopover\n        threads={threads} />,\n      {originRect, direction: 'down'}\n    )\n\n  _onSetImportant: (important) =>\n    threads = @_threadsForKeyboardAction()\n    return unless threads\n    return unless NylasEnv.config.get('core.workspace.showImportant')\n\n    if important\n      tasks = TaskFactory.tasksForApplyingCategories\n        source: \"Keyboard Shortcut\"\n        threads: threads\n        categoriesToRemove: (accountId) -> []\n        categoriesToAdd: (accountId) ->\n          [CategoryStore.getStandardCategory(accountId, 'important')]\n\n    else\n      tasks = TaskFactory.tasksForApplyingCategories\n        source: \"Keyboard Shortcut\"\n        threads: threads\n        categoriesToRemove: (accountId) ->\n          important = CategoryStore.getStandardCategory(accountId, 'important')\n          return [important] if important\n          return []\n\n    Actions.queueTasks(tasks)\n\n  _onSetUnread: (unread) =>\n    threads = @_threadsForKeyboardAction()\n    return unless threads\n    Actions.setUnreadThreads({threads, unread, source: \"Keyboard Shortcut\"})\n    Actions.popSheet()\n\n  _onMarkAsSpam: =>\n    threads = @_threadsForKeyboardAction()\n    return unless threads\n    Actions.markAsSpamThreads({\n      source: \"Keyboard Shortcut\",\n      threads: threads,\n    })\n\n  _onRemoveFromView: (ruleset = CategoryRemovalTargetRulesets.Default) =>\n    threads = @_threadsForKeyboardAction()\n    if not threads\n      return\n    Actions.removeThreadsFromView({threads, ruleset, source: \"Keyboard Shortcut\"})\n    Actions.popSheet()\n\n  _onArchiveItem: =>\n    threads = @_threadsForKeyboardAction()\n    if not threads\n      return\n    Actions.archiveThreads({threads, source: \"Keyboard Shortcut\"})\n    Actions.popSheet()\n\n  _onDeleteItem: =>\n    threads = @_threadsForKeyboardAction()\n    if threads\n      Actions.trashThreads({\n        source: \"Keyboard Shortcut\",\n        threads: threads,\n      })\n    Actions.popSheet()\n\n  _onSelectRead: =>\n    dataSource = ThreadListStore.dataSource()\n    items = dataSource.itemsCurrentlyInViewMatching (item) -> not item.unread\n    @refs.list.handler().onSelect(items)\n\n  _onSelectUnread: =>\n    dataSource = ThreadListStore.dataSource()\n    items = dataSource.itemsCurrentlyInViewMatching (item) -> item.unread\n    @refs.list.handler().onSelect(items)\n\n  _onSelectStarred: =>\n    dataSource = ThreadListStore.dataSource()\n    items = dataSource.itemsCurrentlyInViewMatching (item) -> item.starred\n    @refs.list.handler().onSelect(items)\n\n  _onSelectUnstarred: =>\n    dataSource = ThreadListStore.dataSource()\n    items = dataSource.itemsCurrentlyInViewMatching (item) -> not item.starred\n    @refs.list.handler().onSelect(items)\n\nmodule.exports = ThreadList\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/lib/thread-toolbar-buttons.jsx",
    "content": "import React from \"react\";\nimport classNames from 'classnames';\nimport {RetinaImg} from 'nylas-component-kit';\nimport {\n  Actions,\n  TaskFactory,\n  AccountStore,\n  CategoryStore,\n  FocusedContentStore,\n  FocusedPerspectiveStore,\n} from \"nylas-exports\";\n\nimport ThreadListStore from './thread-list-store';\n\n\nexport class ArchiveButton extends React.Component {\n  static displayName = 'ArchiveButton';\n  static containerRequired = false;\n\n  static propTypes = {\n    items: React.PropTypes.array.isRequired,\n  }\n\n  _onArchive = (event) => {\n    Actions.archiveThreads({\n      threads: this.props.items,\n      source: \"Toolbar Button: Thread List\",\n    })\n    Actions.popSheet();\n    event.stopPropagation();\n    return;\n  }\n\n  render() {\n    const allowed = FocusedPerspectiveStore.current().canArchiveThreads(this.props.items);\n    if (!allowed) {\n      return <span />;\n    }\n\n    return (\n      <button\n        tabIndex={-1}\n        style={{order: -107}}\n        className=\"btn btn-toolbar\"\n        title=\"Archive\"\n        onClick={this._onArchive}\n      >\n        <RetinaImg name=\"toolbar-archive.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    )\n  }\n}\n\nexport class TrashButton extends React.Component {\n  static displayName = 'TrashButton'\n  static containerRequired = false;\n\n  static propTypes = {\n    items: React.PropTypes.array.isRequired,\n  }\n\n  _onRemove = (event) => {\n    Actions.trashThreads({threads: this.props.items, source: \"Toolbar Button: Thread List\"});\n    Actions.popSheet();\n    event.stopPropagation();\n    return;\n  }\n\n  render() {\n    const allowed = FocusedPerspectiveStore.current().canMoveThreadsTo(this.props.items, 'trash')\n    if (!allowed) {\n      return <span />;\n    }\n\n    return (\n      <button\n        tabIndex={-1}\n        style={{order: -106}}\n        className=\"btn btn-toolbar\"\n        title=\"Move to Trash\"\n        onClick={this._onRemove}\n      >\n        <RetinaImg name=\"toolbar-trash.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n}\n\nexport class MarkAsSpamButton extends React.Component {\n  static displayName = 'MarkAsSpamButton';\n  static containerRequired = false;\n\n  static propTypes = {\n    items: React.PropTypes.array.isRequired,\n  }\n\n  _allInSpam() {\n    return this.props.items.every(item => item.categories.map(c => c.name).includes('spam'));\n  }\n\n  _onNotSpam = (event) => {\n    const tasks = TaskFactory.tasksForApplyingCategories({\n      source: \"Toolbar Button: Thread List\",\n      threads: this.props.items,\n      categoriesToAdd: (accountId) => {\n        const account = AccountStore.accountForId(accountId)\n        return account.usesFolders() ? [CategoryStore.getInboxCategory(accountId)] : [];\n      },\n      categoriesToRemove: (accountId) => {\n        return [CategoryStore.getSpamCategory(accountId)];\n      },\n    })\n    Actions.queueTasks(tasks);\n    Actions.popSheet();\n    event.stopPropagation();\n    return;\n  }\n\n  _onMarkAsSpam = (event) => {\n    Actions.markAsSpamThreads({threads: this.props.items, source: \"Toolbar Button: Thread List\"});\n    Actions.popSheet();\n    event.stopPropagation();\n    return;\n  }\n\n  render() {\n    if (this._allInSpam()) {\n      return (\n        <button\n          tabIndex={-1}\n          style={{order: -105}}\n          className=\"btn btn-toolbar\"\n          title=\"Not Spam\"\n          onClick={this._onNotSpam}\n        >\n          <RetinaImg name=\"toolbar-not-spam.png\" mode={RetinaImg.Mode.ContentIsMask} />\n        </button>\n      )\n    }\n\n    const allowed = FocusedPerspectiveStore.current().canMoveThreadsTo(this.props.items, 'spam');\n    if (!allowed) {\n      return <span />;\n    }\n    return (\n      <button\n        tabIndex={-1}\n        style={{order: -105}}\n        className=\"btn btn-toolbar\"\n        title=\"Mark as Spam\"\n        onClick={this._onMarkAsSpam}\n      >\n        <RetinaImg name=\"toolbar-spam.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n}\n\nexport class ToggleStarredButton extends React.Component {\n  static displayName = 'ToggleStarredButton';\n  static containerRequired = false;\n\n  static propTypes = {\n    items: React.PropTypes.array.isRequired,\n  };\n\n  _onStar = (event) => {\n    Actions.toggleStarredThreads({threads: this.props.items, source: \"Toolbar Button: Thread List\"});\n    event.stopPropagation();\n    return;\n  }\n\n  render() {\n    const postClickStarredState = this.props.items.every((t) => t.starred === false);\n    const title = postClickStarredState ? \"Star\" : \"Unstar\";\n    const imageName = postClickStarredState ? \"toolbar-star.png\" : \"toolbar-star-selected.png\"\n\n    return (\n      <button\n        tabIndex={-1}\n        style={{order: -103}}\n        className=\"btn btn-toolbar\"\n        title={title}\n        onClick={this._onStar}\n      >\n        <RetinaImg name={imageName} mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n}\n\nexport class ToggleUnreadButton extends React.Component {\n  static displayName = 'ToggleUnreadButton';\n  static containerRequired = false;\n\n  static propTypes = {\n    items: React.PropTypes.array.isRequired,\n  }\n\n  _onClick = (event) => {\n    Actions.toggleUnreadThreads({threads: this.props.items, source: \"Toolbar Button: Thread List\"});\n    Actions.popSheet();\n    event.stopPropagation();\n    return;\n  }\n\n  render() {\n    const postClickUnreadState = this.props.items.every(t => t.unread === false);\n    const fragment = postClickUnreadState ? \"unread\" : \"read\";\n\n    return (\n      <button\n        tabIndex={-1}\n        style={{order: -104}}\n        className=\"btn btn-toolbar\"\n        title={`Mark as ${fragment}`}\n        onClick={this._onClick}\n      >\n        <RetinaImg\n          name={`toolbar-markas${fragment}.png`}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </button>\n    );\n  }\n}\n\nclass ThreadArrowButton extends React.Component {\n  static propTypes = {\n    getStateFromStores: React.PropTypes.func,\n    direction: React.PropTypes.string,\n    command: React.PropTypes.string,\n    title: React.PropTypes.string,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = this.props.getStateFromStores();\n  }\n\n  componentDidMount() {\n    this._unsubscribe = ThreadListStore.listen(this._onStoreChange);\n    this._unsubscribe_focus = FocusedContentStore.listen(this._onStoreChange);\n  }\n\n  componentWillUnmount() {\n    this._unsubscribe();\n    this._unsubscribe_focus();\n  }\n\n  _onClick = () => {\n    if (this.state.disabled) {\n      return;\n    }\n    NylasEnv.commands.dispatch(this.props.command);\n    return;\n  }\n\n  _onStoreChange = () => {\n    this.setState(this.props.getStateFromStores());\n  }\n\n  render() {\n    const {direction, title} = this.props;\n    const classes = classNames({\n      \"btn-icon\": true,\n      \"message-toolbar-arrow\": true,\n      \"disabled\": this.state.disabled,\n    });\n\n    return (\n      <div className={`${classes} ${direction}`} onClick={this._onClick} title={title}>\n        <RetinaImg name={`toolbar-${direction}-arrow.png`} mode={RetinaImg.Mode.ContentIsMask} />\n      </div>\n    );\n  }\n}\n\nexport const DownButton = () => {\n  const getStateFromStores = () => {\n    const selectedId = FocusedContentStore.focusedId('thread');\n    const lastIndex = ThreadListStore.dataSource().count() - 1\n    const lastItem = ThreadListStore.dataSource().get(lastIndex);\n    return {\n      disabled: (lastItem && lastItem.id === selectedId),\n    };\n  }\n\n  return (\n    <ThreadArrowButton\n      getStateFromStores={getStateFromStores}\n      direction={\"down\"}\n      title={\"Next thread\"}\n      command={'core:next-item'}\n    />\n  );\n}\nDownButton.displayName = 'DownButton';\nDownButton.containerRequired = false;\n\nexport const UpButton = () => {\n  const getStateFromStores = () => {\n    const selectedId = FocusedContentStore.focusedId('thread');\n    const item = ThreadListStore.dataSource().get(0)\n    return {\n      disabled: (item && item.id === selectedId),\n    };\n  }\n\n  return (\n    <ThreadArrowButton\n      getStateFromStores={getStateFromStores}\n      direction={\"up\"}\n      title={\"Previous thread\"}\n      command={'core:previous-item'}\n    />\n  );\n}\nUpButton.displayName = 'UpButton';\nUpButton.containerRequired = false;\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/package.json",
    "content": "{\n  \"name\": \"thread-list\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"View threads using React\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/spec/category-removal-target-rulesets-spec.es6",
    "content": "import {AccountStore, CategoryStore} from 'nylas-exports'\nimport CategoryRemovalTargetRulesets from '../lib/category-removal-target-rulesets'\nconst {Gmail} = CategoryRemovalTargetRulesets;\n\ndescribe('CategoryRemovalTargetRulesets', function categoryRemovalTargetRulesets() {\n  describe('Gmail', () => {\n    it('is a no op in archive, all, spam and sent', () => {\n      expect(Gmail.all).toBe(null)\n      expect(Gmail.sent).toBe(null)\n      expect(Gmail.spam).toBe(null)\n      expect(Gmail.archive).toBe(null)\n    });\n\n    describe('default', () => {\n      it('moves to archive if account uses folders', () => {\n        const account = {usesFolders: () => true}\n        spyOn(AccountStore, 'accountForId').andReturn(account)\n        spyOn(CategoryStore, 'getArchiveCategory').andReturn('archive')\n        expect(Gmail.other('a1')).toEqual('archive')\n      });\n\n      it('moves to nowhere if account uses labels', () => {\n        const account = {usesFolders: () => false}\n        spyOn(AccountStore, 'accountForId').andReturn(account)\n        expect(Gmail.other('a1')).toBe(null)\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/spec/thread-list-column-spec.coffee",
    "content": ""
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/spec/thread-list-participants-spec.cjsx",
    "content": "React = require \"react\"\nReactTestUtils = require('react-addons-test-utils')\n\n_ = require 'underscore'\n{AccountStore, Thread, Contact, Message} = require 'nylas-exports'\nThreadListParticipants = require '../lib/thread-list-participants'\n\ndescribe \"ThreadListParticipants\", ->\n\n  beforeEach ->\n    @account = AccountStore.accounts()[0]\n\n  it \"renders into the document\", ->\n    @participants = ReactTestUtils.renderIntoDocument(\n      <ThreadListParticipants thread={new Thread}/>\n    )\n    expect(ReactTestUtils.isCompositeComponentWithType(@participants, ThreadListParticipants)).toBe true\n\n  it \"renders unread contacts with .unread-true\", ->\n    ben = new Contact(email: 'ben@nylas.com', name: 'ben')\n    ben.unread = true\n    thread = new Thread()\n    thread.__messages = [new Message(from: [ben], unread:true)]\n\n    @participants = ReactTestUtils.renderIntoDocument(\n      <ThreadListParticipants thread={thread}/>\n    )\n    unread = ReactTestUtils.scryRenderedDOMComponentsWithClass(@participants, 'unread-true')\n    expect(unread.length).toBe(1)\n\n  describe \"getTokens\", ->\n    beforeEach ->\n      @ben = new Contact(email: 'ben@nylas.com', name: 'ben')\n      @evan = new Contact(email: 'evan@nylas.com', name: 'evan')\n      @evanAgain = new Contact(email: 'evan@nylas.com', name: 'evan')\n      @michael = new Contact(email: 'michael@nylas.com', name: 'michael')\n      @kavya = new Contact(email: 'kavya@nylas.com', name: 'kavya')\n      @phab1 = new Contact(email: 'no-reply@phab.nylas.com', name: 'Ben')\n      @phab2 = new Contact(email: 'no-reply@phab.nylas.com', name: 'MG')\n\n    describe \"when thread.messages is available\", ->\n      it \"correctly produces items for display in a wide range of scenarios\", ->\n        scenarios = [{\n          name: 'single read email'\n          in: [\n            new Message(unread: false, from: [@ben]),\n          ]\n          out: [{contact: @ben, unread: false}]\n        },{\n          name: 'single read email and draft'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(from: [@ben], draft: true),\n          ]\n          out: [{contact: @ben, unread: false}]\n        },{\n          name: 'single unread email'\n          in: [\n            new Message(unread: true, from: [@evan]),\n          ]\n          out: [{contact: @evan, unread: true}]\n        },{\n          name: 'single unread response'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: true, from: [@evan]),\n          ]\n          out: [{contact: @ben, unread: false}, {contact: @evan, unread: true}]\n        },{\n          name: 'two unread responses'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: true, from: [@evan]),\n            new Message(unread: true, from: [@kavya]),\n          ]\n          out: [{contact: @ben, unread: false},\n                {contact: @evan, unread: true},\n                {contact: @kavya, unread: true}]\n        },{\n          name: 'two unread responses (repeated participants)'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: true, from: [@evan]),\n            new Message(unread: true, from: [@evanAgain]),\n          ]\n          out: [{contact: @ben, unread: false}, {contact: @evan, unread: true}]\n        },{\n          name: 'three unread responses (repeated participants)'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: true, from: [@evan]),\n            new Message(unread: true, from: [@michael]),\n            new Message(unread: true, from: [@evanAgain]),\n          ]\n          out: [{contact: @ben, unread: false},\n                {spacer: true},\n                {contact: @michael, unread: true},\n                {contact: @evanAgain, unread: true}]\n        },{\n          name: 'three unread responses'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: true, from: [@evan]),\n            new Message(unread: true, from: [@michael]),\n            new Message(unread: true, from: [@kavya]),\n          ]\n          out: [{contact: @ben, unread: false},\n                {spacer: true},\n                {contact: @michael, unread: true},\n                {contact: @kavya, unread: true}]\n        },{\n          name: 'ends with two emails from the same person, second one is unread'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: false, from: [@evan]),\n            new Message(unread: false, from: [@kavya]),\n            new Message(unread: true, from: [@kavya]),\n          ]\n          out: [{contact: @ben, unread: false},\n                {contact: @evan, unread: false},\n                {contact: @kavya, unread: true}]\n        },{\n          name: 'three unread responses to long thread'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: false, from: [@evan]),\n            new Message(unread: false, from: [@michael]),\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: true, from: [@evanAgain]),\n            new Message(unread: true, from: [@michael]),\n            new Message(unread: true, from: [@evanAgain]),\n          ]\n          out: [{contact: @ben, unread: false},\n                {spacer: true},\n                {contact: @michael, unread: true},\n                {contact: @evanAgain, unread: true}]\n        },{\n          name: 'single unread responses to long thread'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: false, from: [@evan]),\n            new Message(unread: false, from: [@michael]),\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: true, from: [@evanAgain]),\n          ]\n          out: [{contact: @ben, unread: false},\n                {spacer: true},\n                {contact: @ben, unread: false},\n                {contact: @evanAgain, unread: true}]\n        },{\n          name: 'long read thread'\n          in: [\n            new Message(unread: false, from: [@ben]),\n            new Message(unread: false, from: [@evan]),\n            new Message(unread: false, from: [@michael]),\n            new Message(unread: false, from: [@ben]),\n          ]\n          out: [{contact: @ben, unread: false},\n                {spacer: true},\n                {contact: @michael, unread: false},\n                {contact: @ben, unread: false}]\n        },{\n          name: 'thread with different participants with the same email address'\n          in: [\n            new Message(unread: false, from: [@phab1]),\n            new Message(unread: false, from: [@phab2])\n          ]\n          out: [{contact: @phab1, unread: false},\n                {contact: @phab2, unread: false}]\n        }]\n\n        for scenario in scenarios\n          thread = new Thread()\n          thread.__messages = scenario.in\n          participants = ReactTestUtils.renderIntoDocument(\n            <ThreadListParticipants thread={thread}/>\n          )\n\n          expect(participants.getTokens()).toEqual(scenario.out)\n\n          # Slightly misuse jasmine to get the output we want to show\n          if (!_.isEqual(participants.getTokens(), scenario.out))\n            expect(scenario.name).toBe('correct')\n\n    describe \"when getTokens() called and current user is only sender\", ->\n      beforeEach ->\n        @me = @account.me()\n        @ben = new Contact(email: 'ben@nylas.com', name: 'ben')\n        @evan = new Contact(email: 'evan@nylas.com', name: 'evan')\n        @evanCapitalized = new Contact(email: 'EVAN@nylas.com', name: 'evan')\n        @michael = new Contact(email: 'michael@nylas.com', name: 'michael')\n        @kavya = new Contact(email: 'kavya@nylas.com', name: 'kavya')\n\n      getTokens = (threadMessages) ->\n        thread = new Thread()\n        thread.__messages = threadMessages\n        participants = ReactTestUtils.renderIntoDocument(\n          <ThreadListParticipants thread={thread}/>\n        )\n        participants.getTokens()\n\n      it \"shows only recipients for emails sent from me to different recipients\", ->\n        input = [new Message(unread: false, from: [@me], to: [@ben])\n                 new Message(unread: false, from: [@me], to: [@evan])\n                 new Message(unread: false, from: [@me], to: [@ben])]\n        actualOut = getTokens(input)\n        expectedOut = [{contact: @ben, unread: false}\n                       {contact: @evan, unread: false}\n                       {contact: @ben, unread: false}]\n        expect(actualOut).toEqual expectedOut\n\n      it \"is case insensitive\", ->\n        input = [new Message(unread: false, from: [@me], to: [@evan])\n                 new Message(unread: false, from: [@me], to: [@evanCapitalized])]\n        actualOut = getTokens(input)\n        expectedOut = [{contact: @evan, unread: false}]\n        expect(actualOut).toEqual expectedOut\n\n      it \"shows only first, spacer, second to last, and last recipients if recipients count > 3\", ->\n        input = [new Message(unread: false, from: [@me], to: [@ben])\n                 new Message(unread: false, from: [@me], to: [@evan])\n                 new Message(unread: false, from: [@me], to: [@michael])\n                 new Message(unread: false, from: [@me], to: [@kavya])]\n        actualOut = getTokens(input)\n        expectedOut = [{contact: @ben, unread: false}\n                       {spacer: true}\n                       {contact: @michael, unread: false}\n                       {contact: @kavya, unread: false}]\n        expect(actualOut).toEqual expectedOut\n\n      it \"shows correct recipients even if only one email\", ->\n        input = [new Message(unread: false, from: [@me], to: [@ben, @evan, @michael, @kavya])]\n        actualOut = getTokens(input)\n        expectedOut = [{contact: @ben, unread: false}\n                       {spacer: true}\n                       {contact: @michael, unread: false}\n                       {contact: @kavya, unread: false}]\n        expect(actualOut).toEqual expectedOut\n\n      it \"shows only one recipient if the sender only sent to one recipient\", ->\n        input = [new Message(unread: false, from: [@me], to: [@evan])\n                 new Message(unread: false, from: [@me], to: [@evan])\n                 new Message(unread: false, from: [@me], to: [@evan])\n                 new Message(unread: false, from: [@me], to: [@evan])]\n        actualOut = getTokens(input)\n        expectedOut = [{contact: @evan, unread: false}]\n        expect(actualOut).toEqual expectedOut\n\n      it \"shows only the recipient for one sent email\", ->\n        input = [new Message(unread: false, from: [@me], to: [@evan])]\n        actualOut = getTokens(input)\n        expectedOut = [{contact: @evan, unread: false}]\n        expect(actualOut).toEqual expectedOut\n\n      it \"shows unread email as well\", ->\n        input = [new Message(unread: false, from: [@me], to: [@evan])\n                 new Message(unread: false, from: [@me], to: [@ben])\n                 new Message(unread: true, from: [@me], to: [@kavya])\n                 new Message(unread: true, from: [@me], to: [@michael])]\n        actualOut = getTokens(input)\n        expectedOut = [{contact: @evan, unread: false},\n                       {spacer: true},\n                       {contact: @kavya, unread: true},\n                       {contact: @michael, unread: true}]\n        expect(actualOut).toEqual expectedOut\n\n    describe \"when thread.messages is not available\", ->\n      it \"correctly produces items for display in a wide range of scenarios\", ->\n        me = @account.me()\n        scenarios = [{\n          name: 'one participant'\n          in: [@ben]\n          out: [{contact: @ben, unread: false}]\n        },{\n          name: 'one participant (me)'\n          in: [me]\n          out: [{contact: me, unread: false}]\n        },{\n          name: 'two participants'\n          in: [@evan, @ben]\n          out: [{contact: @evan, unread: false}, {contact: @ben, unread: false}]\n        },{\n          name: 'two participants (me)'\n          in: [@ben, me]\n          out: [{contact: @ben, unread: false}]\n        },{\n          name: 'lots of participants'\n          in: [@ben, @evan, @michael, @kavya]\n          out: [{contact: @ben, unread: false},\n                {spacer: true},\n                {contact: @michael, unread: false},\n                {contact: @kavya, unread: false}]\n        }]\n\n        for scenario in scenarios\n          thread = new Thread()\n          thread.participants = scenario.in\n          participants = ReactTestUtils.renderIntoDocument(\n            <ThreadListParticipants thread={thread}/>\n          )\n\n          expect(participants.getTokens()).toEqual(scenario.out)\n\n          # Slightly misuse jasmine to get the output we want to show\n          if (!_.isEqual(participants.getTokens(), scenario.out))\n            expect(scenario.name).toBe('correct')\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/spec/thread-list-spec.cjsx",
    "content": "\n\n\n\n\nreturn\n\n\n\n\n\n\nmoment = require \"moment\"\n_ = require 'underscore'\nReact = require \"react\"\nReactTestUtils = require('react-addons-test-utils')\nReactTestUtils = _.extend ReactTestUtils, require \"jasmine-react-helpers\"\n\n{Thread,\n Actions,\n Account,\n DatabaseStore,\n WorkspaceStore,\n NylasTestUtils,\n AccountStore,\n ComponentRegistry} = require \"nylas-exports\"\n{ListTabular} = require 'nylas-component-kit'\n\n\nThreadStore = require \"../lib/thread-store\"\nThreadList = require \"../lib/thread-list\"\n\ntest_threads = -> [\n  (new Thread).fromJSON({\n    \"id\": \"111\",\n    \"object\": \"thread\",\n    \"created_at\": null,\n    \"updated_at\": null,\n    \"account_id\": TEST_ACCOUNT_ID,\n    \"snippet\": \"snippet 111\",\n    \"subject\": \"Subject 111\",\n    \"tags\": [\n      {\n        \"id\": \"unseen\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"unseen\"\n      },\n      {\n        \"id\": \"all\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"all\"\n      },\n      {\n        \"id\": \"inbox\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"inbox\"\n      },\n      {\n        \"id\": \"unread\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"unread\"\n      },\n      {\n        \"id\": \"attachment\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"attachment\"\n      }\n    ],\n    \"participants\": [\n      {\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"User One\",\n        \"email\": \"user1@nylas.com\"\n      },\n      {\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"User Two\",\n        \"email\": \"user2@nylas.com\"\n      }\n    ],\n    \"last_message_received_timestamp\": 1415742036\n  }),\n  (new Thread).fromJSON({\n    \"id\": \"222\",\n    \"object\": \"thread\",\n    \"created_at\": null,\n    \"updated_at\": null,\n    \"account_id\": TEST_ACCOUNT_ID,\n    \"snippet\": \"snippet 222\",\n    \"subject\": \"Subject 222\",\n    \"tags\": [\n      {\n        \"id\": \"unread\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"unread\"\n      },\n      {\n        \"id\": \"all\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"all\"\n      },\n      {\n        \"id\": \"unseen\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"unseen\"\n      },\n      {\n        \"id\": \"inbox\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"inbox\"\n      }\n    ],\n    \"participants\": [\n      {\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"User One\",\n        \"email\": \"user1@nylas.com\"\n      },\n      {\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"User Three\",\n        \"email\": \"user3@nylas.com\"\n      }\n    ],\n    \"last_message_received_timestamp\": 1415741913\n  }),\n  (new Thread).fromJSON({\n    \"id\": \"333\",\n    \"object\": \"thread\",\n    \"created_at\": null,\n    \"updated_at\": null,\n    \"account_id\": TEST_ACCOUNT_ID,\n    \"snippet\": \"snippet 333\",\n    \"subject\": \"Subject 333\",\n    \"tags\": [\n      {\n        \"id\": \"inbox\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"inbox\"\n      },\n      {\n        \"id\": \"all\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"all\"\n      },\n      {\n        \"id\": \"unseen\",\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"unseen\"\n      }\n    ],\n    \"participants\": [\n      {\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"User One\",\n        \"email\": \"user1@nylas.com\"\n      },\n      {\n        \"created_at\": null,\n        \"updated_at\": null,\n        \"name\": \"User Four\",\n        \"email\": \"user4@nylas.com\"\n      }\n    ],\n    \"last_message_received_timestamp\": 1415741837\n  })\n]\n\n\ncjsxSubjectResolver = (thread) ->\n  <div>\n    <span>Subject {thread.id}</span>\n    <span className=\"snippet\">Snippet</span>\n  </div>\n\ndescribe \"ThreadList\", ->\n\n  Foo = React.createClass({render: -> <div>{@props.children}</div>})\n  c1 = new ListTabular.Column\n    name: \"Name\"\n    flex: 1\n    resolver: (thread) -> \"#{thread.id} Test Name\"\n  c2 = new ListTabular.Column\n    name: \"Subject\"\n    flex: 3\n    resolver: cjsxSubjectResolver\n  c3 = new ListTabular.Column\n    name: \"Date\"\n    resolver: (thread) -> <Foo>{thread.id}</Foo>\n\n  columns = [c1,c2,c3]\n\n  beforeEach ->\n    NylasTestUtils.loadKeymap(\"internal_packages/thread-list/keymaps/thread-list\")\n    spyOn(ThreadStore, \"_onAccountChanged\")\n    spyOn(DatabaseStore, \"findAll\").andCallFake ->\n      new Promise (resolve, reject) -> resolve(test_threads())\n    ReactTestUtils.spyOnClass(ThreadList, \"_prepareColumns\").andCallFake ->\n      @_columns = columns\n\n    ThreadStore._resetInstanceVars()\n\n    @thread_list = ReactTestUtils.renderIntoDocument(\n      <ThreadList />\n    )\n\n  it \"renders into the document\", ->\n    expect(ReactTestUtils.isCompositeComponentWithType(@thread_list,\n                                          ThreadList)).toBe true\n\n  it \"has the expected columns\", ->\n    expect(@thread_list._columns).toEqual columns\n\n  it \"by default has zero children\", ->\n    items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, ListTabular.Item)\n    expect(items.length).toBe 0\n\n  describe \"when the workspace is in list mode\", ->\n    beforeEach ->\n      spyOn(WorkspaceStore, \"layoutMode\").andReturn \"list\"\n      @thread_list.setState focusedId: \"t111\"\n\n    it \"allows reply only when the sheet type is 'Thread'\", ->\n      spyOn(WorkspaceStore, \"sheet\").andCallFake -> {type: \"Thread\"}\n      spyOn(Actions, \"composeReply\")\n      @thread_list._onReply()\n      expect(Actions.composeReply).toHaveBeenCalled()\n      expect(@thread_list._actionInVisualScope()).toBe true\n\n    it \"doesn't reply only when the sheet type isnt 'Thread'\", ->\n      spyOn(WorkspaceStore, \"sheet\").andCallFake -> {type: \"Root\"}\n      spyOn(Actions, \"composeReply\")\n      @thread_list._onReply()\n      expect(Actions.composeReply).not.toHaveBeenCalled()\n      expect(@thread_list._actionInVisualScope()).toBe false\n\n  describe \"when the workspace is in split mode\", ->\n    beforeEach ->\n      spyOn(WorkspaceStore, \"layoutMode\").andReturn \"split\"\n      @thread_list.setState focusedId: \"t111\"\n\n    it \"allows reply and reply-all regardless of sheet type\", ->\n      spyOn(WorkspaceStore, \"sheet\").andCallFake -> {type: \"anything\"}\n      spyOn(Actions, \"composeReply\")\n      @thread_list._onReply()\n      expect(Actions.composeReply).toHaveBeenCalled()\n      expect(@thread_list._actionInVisualScope()).toBe true\n\n  describe \"Populated thread list\", ->\n    beforeEach ->\n      view =\n        loaded: -> true\n        get: (i) -> test_threads()[i]\n        count: -> test_threads().length\n        setRetainedRange: ->\n      ThreadStore._view = view\n      ThreadStore._focusedId = null\n      ThreadStore.trigger(ThreadStore)\n      @thread_list_node = ReactDOM.findDOMNode(@thread_list)\n      spyOn(@thread_list, \"setState\").andCallThrough()\n\n    it \"renders all of the thread list items\", ->\n      advanceClock(100)\n      items = ReactTestUtils.scryRenderedComponentsWithType(@thread_list, ListTabular.Item)\n      expect(items.length).toBe(test_threads().length)\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/spec/thread-toolbar-buttons-spec.cjsx",
    "content": "React = require \"react\"\nReactDOM = require \"react-dom\"\nReactTestUtils = require 'react-addons-test-utils'\n{\n  Thread,\n  FocusedContentStore,\n  Actions,\n  CategoryStore,\n  ChangeUnreadTask,\n  MailboxPerspective\n} = require \"nylas-exports\"\n{ToggleStarredButton, ToggleUnreadButton, MarkAsSpamButton} = require '../lib/thread-toolbar-buttons'\n\ntest_thread = (new Thread).fromJSON({\n  \"id\" : \"thread_12345\"\n  \"account_id\": TEST_ACCOUNT_ID\n  \"subject\" : \"Subject 12345\"\n  \"starred\": false\n})\n\ntest_thread_starred = (new Thread).fromJSON({\n  \"id\" : \"thread_starred_12345\"\n  \"account_id\": TEST_ACCOUNT_ID\n  \"subject\" : \"Subject 12345\"\n  \"starred\": true\n})\n\ndescribe \"ThreadToolbarButtons\", ->\n  beforeEach ->\n    spyOn Actions, \"queueTask\"\n    spyOn Actions, \"queueTasks\"\n    spyOn Actions, \"toggleStarredThreads\"\n    spyOn Actions, \"toggleUnreadThreads\"\n\n  describe \"Starring\", ->\n    it \"stars a thread if the star button is clicked and thread is unstarred\", ->\n      starButton = ReactTestUtils.renderIntoDocument(<ToggleStarredButton items={[test_thread]}/>)\n\n      ReactTestUtils.Simulate.click ReactDOM.findDOMNode(starButton)\n\n      expect(Actions.toggleStarredThreads.mostRecentCall.args[0].threads).toEqual([test_thread])\n\n    it \"unstars a thread if the star button is clicked and thread is starred\", ->\n      starButton = ReactTestUtils.renderIntoDocument(<ToggleStarredButton items={[test_thread_starred]}/>)\n\n      ReactTestUtils.Simulate.click ReactDOM.findDOMNode(starButton)\n\n      expect(Actions.toggleStarredThreads.mostRecentCall.args[0].threads).toEqual([test_thread_starred])\n\n  describe \"Marking as unread\", ->\n    thread = null\n    markUnreadBtn = null\n\n    beforeEach ->\n      thread = new Thread(id: \"thread-id-lol-123\", accountId: TEST_ACCOUNT_ID, unread: false)\n      markUnreadBtn = ReactTestUtils.renderIntoDocument(\n        <ToggleUnreadButton items={[thread]} />\n      )\n\n    it \"queues a task to change unread status to true\", ->\n      ReactTestUtils.Simulate.click ReactDOM.findDOMNode(markUnreadBtn).childNodes[0]\n      expect(Actions.toggleUnreadThreads.mostRecentCall.args[0].threads).toEqual([thread])\n\n    it \"returns to the thread list\", ->\n      spyOn Actions, \"popSheet\"\n      ReactTestUtils.Simulate.click ReactDOM.findDOMNode(markUnreadBtn).childNodes[0]\n\n      expect(Actions.popSheet).toHaveBeenCalled()\n\n  describe \"Marking as spam\", ->\n    thread = null\n    markSpamButton = null\n\n    describe \"when the thread is already in spam\", ->\n      beforeEach ->\n        thread = new Thread({\n          id: \"thread-id-lol-123\",\n          accountId: TEST_ACCOUNT_ID,\n          categories: [{name: 'spam'}]\n        })\n        markSpamButton = ReactTestUtils.renderIntoDocument(\n          <MarkAsSpamButton items={[thread]} />\n        )\n\n      it \"queues a task to remove spam\", ->\n        spyOn(CategoryStore, 'getSpamCategory').andReturn(thread.categories[0])\n        ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(markSpamButton))\n        {labelsToAdd, labelsToRemove} = Actions.queueTasks.mostRecentCall.args[0][0]\n        expect(labelsToAdd).toEqual([])\n        expect(labelsToRemove).toEqual([thread.categories[0]])\n\n    describe \"when the thread can be moved to spam\", ->\n      beforeEach ->\n        spyOn(MailboxPerspective.prototype, 'canMoveThreadsTo').andReturn(true)\n        thread = new Thread(id: \"thread-id-lol-123\", accountId: TEST_ACCOUNT_ID, categories: [])\n        markSpamButton = ReactTestUtils.renderIntoDocument(\n          <MarkAsSpamButton items={[thread]} />\n        )\n\n      it \"queues a task to mark as spam\", ->\n        spyOn(Actions, 'markAsSpamThreads')\n        ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(markSpamButton))\n        expect(Actions.markAsSpamThreads).toHaveBeenCalledWith({\n          threads: [thread],\n          source: 'Toolbar Button: Thread List'\n        })\n\n      it \"returns to the thread list\", ->\n        spyOn(Actions, 'popSheet')\n        ReactTestUtils.Simulate.click(ReactDOM.findDOMNode(markSpamButton))\n        expect(Actions.popSheet).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/stylesheets/selected-items-stack.less",
    "content": "@import \"ui-variables\";\n@img-path: \"../internal_packages/thread-list/assets/graphic-stackable-card-filled.svg\";\n\n.selected-items-stack {\n  display: flex;\n  align-self: center;\n  align-items: center;\n  height: 100%;\n\n  .selected-items-stack-content {\n    display: flex;\n    position: relative;\n    align-items: center;\n    justify-content: center;\n    width: 198px;\n    height: 268px;\n\n    .stack {\n      .card {\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 198px;\n        height: 268px;\n        background: url(@img-path);\n        background-size: 198px 268px;\n      }\n    }\n\n    .count-info {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n      z-index: 6;\n\n      .count {\n        font-size: 4em;\n        font-weight: 200;\n        color: @text-color-very-subtle;\n      }\n      .count-message {\n        padding-top: @padding-base-vertical;\n        color: @text-color-very-subtle;\n      }\n      .clear.btn {\n        margin-top: @padding-large-vertical * 2;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-list/stylesheets/thread-list.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n@scrollbar-margin: 8px;\n\n// MIXINS\n\n.inverseContent() {\n  // Note: these styles are also applied below\n  // subpixel antialiasing looks awful against dark background colors\n  -webkit-font-smoothing: antialiased;\n\n  color: @text-color-inverse;\n\n  .participants {\n    .unread-true {\n      font-weight: @font-weight-normal;\n    }\n  }\n  .subject {\n    font-weight: @font-weight-normal;\n  }\n\n  .thread-icon, .draft-icon, .mail-important-icon {\n    -webkit-filter: brightness(600%) grayscale(100%);\n  }\n\n  .mail-label {\n    // Note - these !important styles override values set by a style tag\n    // since the color of the label is detemined programatically.\n    background: fade(@text-color-inverse, 20%) !important;\n    box-shadow: none !important;\n    -webkit-filter: brightness(600%) grayscale(100%);\n  }\n\n}\n\n// STYLES\n\n*:focus, input:focus {\n  outline:none;\n}\n\n.thread-list, .draft-list {\n  .list-container, .scroll-region {\n    width:100%;\n    height:100%;\n    -webkit-font-smoothing: subpixel-antialiased;\n  }\n\n  .swipe-backing {\n    background-color: darken(@background-primary, 10%);\n    &::after {\n      color: fade(white, 90%);\n      padding-top: 45px;\n      text-align: center;\n      font-weight: 400;\n      font-size: @font-size-small;\n      position: absolute;\n      top: 0;\n      transform: translateX(0%);\n      width: 80px;\n      bottom: 0;\n      opacity: 0.8;\n      transition: opacity linear 150ms;\n      background-repeat: no-repeat;\n      background-position: 50% 35%;\n      background-size: 24px 24px;\n    }\n\n    &.swipe-trash {\n      transition: background-color linear 150ms;\n      background-color: mix(#ed304b, @background-primary, 75%);\n      &::after {\n        transition: left linear 150ms, transform linear 150ms;\n        content: \"Trash\";\n        left: 0;\n        background-image: url(../static/images/swipe/icon-swipe-trash@2x.png);\n      }\n      &.confirmed {\n        background-color: #ed304b;\n        &::after {\n          left: 100%;\n          transform: translateX(-100%);\n          opacity: 1;\n        }\n      }\n    }\n    &.swipe-archive,&.swipe-all {\n      transition: background-color linear 150ms;\n      background-color: mix(#6cd420, @background-primary, 75%);\n      &::after {\n        transition: left linear 150ms, transform linear 150ms;\n        content: \"Archive\";\n        left: 0;\n        background-image: url(../static/images/swipe/icon-swipe-archive@2x.png);\n      }\n      &.confirmed {\n        background-color: #6cd420;\n        &::after {\n          left: 100%;\n          transform: translateX(-100%);\n          opacity: 1;\n        }\n      }\n    }\n    &.swipe-snooze {\n      transition: background-color linear 150ms;\n      background-color: mix(#8d6be3, @background-primary, 75%);\n      &::after {\n        transition: right linear 150ms, transform linear 150ms;\n        content: \"Snooze\";\n        right: 0;\n        background-image: url(../static/images/swipe/icon-swipe-snooze@2x.png);\n      }\n      &.confirmed {\n        background-color: #8d6be3;\n        &::after {\n          right: 100%;\n          transform: translateX(100%);\n          opacity: 1;\n        }\n      }\n    }\n  }\n\n  .list-item {\n    background-color: darken(@background-primary, 2%);\n    border-bottom: 1px solid fade(@list-border, 60%);\n    line-height: 36px;\n  }\n\n  .mail-important-icon {\n    margin-left:6px;\n    padding: 12px;\n    vertical-align: initial;\n    &:not(.active) {\n      visibility: hidden;\n    }\n  }\n\n  .message-count {\n    color: @text-color-inverse;\n    background: @background-tertiary;\n    padding: 4px 6px 2px 6px;\n    margin-left: 1em;\n  }\n\n  .draft-icon {\n    margin-left:10px;\n    flex-shrink: 0;\n    object-fit: contain;\n  }\n\n  .participants {\n    font-size:   @font-size-small;\n    text-overflow: ellipsis;\n    text-align: left;\n    overflow: hidden;\n\n    &.no-recipients {\n      color: @text-color-very-subtle;\n    }\n  }\n\n  .details {\n    display:flex;\n    align-items: center;\n    overflow: hidden;\n\n    .subject {\n      font-size:   @font-size-small;\n      font-weight: @font-weight-normal;\n      padding-right: @padding-base-horizontal;\n      text-overflow: ellipsis;\n      overflow: hidden;\n\n      // Shrink, but only after snippet has shrunk\n      flex-shrink:0.1;\n    }\n    .snippet {\n      font-size:   @font-size-small;\n      font-weight: @font-weight-normal;\n      text-overflow: ellipsis;\n      overflow: hidden;\n      opacity: 0.62;\n      flex: 1;\n    }\n    .thread-icon {\n      margin-right:@padding-base-horizontal;\n      margin-left:@padding-base-horizontal;\n    }\n  }\n\n  .list-column-State {\n    display: flex;\n    align-items: center;\n  }\n  .list-column-Date {\n    text-align: right;\n  }\n\n  .timestamp {\n    font-size: @font-size-small;\n    font-weight: @font-weight-normal;\n    min-width:70px;\n    margin-right: @scrollbar-margin;\n    opacity: 0.62;\n  }\n\n  .unread:not(.focused):not(.selected) {\n    background-color: @background-primary;\n    &:hover {\n      background: darken(@background-primary, 2%);\n    }\n    .snippet {\n      color: @text-color-subtle;\n    }\n  }\n\n  .unread:not(.focused):not(.selected):not(.next-is-selected) {\n    border-bottom: 1px solid @list-border;\n  }\n\n  .unread:not(.focused) {\n    // Never show any unread styles when the thread is focused.\n    // It will be marked as read and the delay from focus=>read\n    // is noticeable.\n    .subject {\n      font-weight: @font-weight-semi-bold;\n      .emoji {\n        font-weight: @font-weight-normal;\n      }\n    }\n    .participants {\n      .unread-true {\n        font-weight: @font-weight-semi-bold;\n      }\n    }\n  }\n\n  .focused {\n    .inverseContent;\n  }\n\n  .thread-injected-icons {\n    vertical-align: top;\n  }\n  .thread-injected-mail-labels {\n    margin-right: 6px;\n  }\n  .thread-icon {\n    width:25px;\n    height:24px;\n    flex-shrink:0;\n    background-size: 15px;\n    display:inline-block;\n    background-repeat: no-repeat;\n    background-position:center;\n\n    &.thread-icon-attachment {\n      background-image:url(../static/images/thread-list/icon-attachment-@2x.png);\n      margin-right:0;\n      margin-left:0;\n    }\n    &.thread-icon-unread {\n      background-image:url(../static/images/thread-list/icon-unread-@2x.png);\n    }\n    &.thread-icon-replied {\n      background-image:url(../static/images/thread-list/icon-replied-@2x.png);\n    }\n    &.thread-icon-forwarded {\n      background-image:url(../static/images/thread-list/icon-forwarded-@2x.png);\n    }\n    &.thread-icon-star {\n      background-size: 16px;\n      background-image:url(../static/images/thread-list/icon-star-@2x.png);\n    }\n  }\n  .star-button {\n    font-size: 16px;\n    .fa-star {\n      color: rgb(239, 209, 0);\n      &:hover {\n        cursor: pointer;\n        color: rgb(220,220,220);\n      }\n    }\n    .fa-star-o {\n      color: rgb(220,220,220);\n      &:hover {\n        cursor: pointer;\n        color: rgb(239, 209, 0);\n      }\n    }\n  }\n}\n\n\n// quick actions\n@archive-img: \"../static/images/thread-list-quick-actions/ic-quick-button-archive@2x.png\";\n@trash-img: \"../static/images/thread-list-quick-actions/ic-quick-button-trash@2x.png\";\n@snooze-img: \"../static/images/thread-list-quick-actions/ic-quickaction-snooze@2x.png\";\n\n.thread-list .list-item .list-column-HoverActions {\n  display:none;\n  .action {\n    display: inline-block;\n    background-size: 100%;\n    zoom: 0.5;\n    width: 81px;\n    height: 51px;\n    margin: 9px 16px 0 16px;\n  }\n  .action.action-archive {\n    background: url(@archive-img) center no-repeat, @background-gradient;\n  }\n  .action.action-trash {\n    background: url(@trash-img) center no-repeat, @background-gradient;\n  }\n  .action.action-snooze {\n    background: url(@snooze-img) center no-repeat, @background-gradient;\n  }\n}\nbody.platform-win32 {\n  .thread-list .list-item .list-column-HoverActions {\n    .action {\n      border: 0;\n      margin: 9px 0 0 0;\n    }\n  }\n}\n.thread-list .list-item:hover .list-column-HoverActions {\n  width: 0;\n  padding: 0;\n  display:block;\n  overflow: visible;\n  height:100%;\n\n  .inner {\n    position:relative;\n    width:300px;\n    height:100%;\n    left: -300px;\n    .thread-injected-quick-actions {\n      margin-right: 10px;\n    }\n  }\n}\n\n.thread-list .list-item:hover .list-column-HoverActions .inner {\n  background-image: -webkit-linear-gradient(left, fade(darken(@list-bg, 5%), 0%) 0%, darken(@list-bg, 5%) 50%, darken(@list-bg, 5%) 100%);\n}\n\n.thread-list .list-item.selected:hover .list-column-HoverActions .inner {\n  background-image: -webkit-linear-gradient(left, fade(@list-selected-bg, 0%) 0%, @list-selected-bg 50%, @list-selected-bg 100%);\n}\n\n.thread-list .list-item.focused:hover .list-column-HoverActions .inner {\n  background-image: -webkit-linear-gradient(left, fade(@list-focused-bg, 0%) 0%, @list-focused-bg 50%, @list-focused-bg 100%);\n  .action {\n    -webkit-filter: invert(100%) brightness(300%);\n  }\n  .action.action-archive {\n    background: url(@archive-img) center no-repeat;\n  }\n  .action.action-trash {\n    background: url(@trash-img) center no-repeat;\n  }\n  .action.action-snooze {\n    background: url(@snooze-img) center no-repeat;\n  }\n}\n\n\n// stars\n\n.thread-list .thread-icon-star:hover\n{\n  background-image:url(../static/images/thread-list/icon-star-@2x.png);\n  background-size: 16px;\n  -webkit-filter: brightness(90%);\n}\n.thread-list .list-item:hover .thread-icon-none:hover {\n  background-image:url(../static/images/thread-list/icon-star-action-hover-@2x.png);\n  background-size: 16px;\n}\n.thread-list .list-item:hover .thread-icon-none {\n  background-image:url(../static/images/thread-list/icon-star-hover-@2x.png);\n  background-size: 16px;\n}\n.thread-list .list-item:hover .mail-important-icon.enabled {\n  visibility: inherit;\n}\n.thread-list .thread-icon-star-on-hover:hover {\n  background-image:url(../static/images/thread-list/icon-star-hover-@2x.png);\n  background-size: 16px;\n}\n\n.thread-list-narrow {\n  .icons-column {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    width: 25px;\n    margin-right: 5px;\n\n    .thread-injected-icons {\n      align-items: center;\n    }\n  }\n  .thread-info-column {\n    flex: 1;\n    align-self: center;\n    overflow: hidden;\n\n    .participants-wrapper {\n      display: flex;\n      align-items: center;\n      min-height: 24px;\n    }\n  }\n  .list-column {\n    display:block;\n  }\n  .list-tabular-item {\n    line-height: 21px;\n  }\n  .timestamp {\n    order: 100;\n    min-width: 0;\n  }\n  .participants {\n    font-size: @font-size-base;\n  }\n\n  .mail-important-icon {\n    margin-left:1px;\n    float:left;\n    padding: 12px;\n    vertical-align: initial;\n  }\n\n  .subject {\n    font-size: @font-size-base;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    text-align: left;\n    margin-right: @scrollbar-margin;\n  }\n  .snippet-and-labels {\n    margin-right: @scrollbar-margin;\n    display: flex;\n    align-items: baseline;\n    overflow: hidden;\n\n    .mail-label {\n      font-size: 0.8em;\n      line-height: 17px;\n    }\n\n    .snippet {\n      font-size: @font-size-small;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      opacity: 0.7;\n      text-align: left;\n      min-height: 21px;\n      margin-right:4px;\n    }\n  }\n}\n\n// selection looks like focus in split mode\n\n.thread-list.handler-split {\n  .list-item {\n    &.selected {\n      background: @list-focused-bg;\n      color: @list-focused-color;\n      .inverseContent;\n    }\n  }\n  .list-item.selected:hover .list-column-HoverActions .inner {\n    background-image: -webkit-linear-gradient(left, fade(@list-focused-bg, 0%) 0%, @list-focused-bg 50%, @list-focused-bg 100%);\n    .action {\n      -webkit-filter: invert(100%) brightness(300%);\n    }\n    .action.action-archive {\n      background: url(@archive-img) center no-repeat;\n    }\n    .action.action-trash {\n      background: url(@trash-img) center no-repeat;\n    }\n    .action.action-snooze {\n      background: url(@snooze-img) center no-repeat;\n    }\n  }\n}\nbody.is-blurred {\n  .thread-list.handler-split {\n    .list-item {\n      &.selected {\n        background: fadeout(desaturate(@list-focused-bg, 100%), 65%);\n        border-bottom: 1px solid fadeout(desaturate(@list-focused-border, 100%), 65%);\n        color: @text-color;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/README.md",
    "content": "# React version of thread list\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/lib/main.es6",
    "content": "import {ComponentRegistry, WorkspaceStore} from 'nylas-exports'\nimport ThreadSearchBar from './thread-search-bar'\n\nexport const configDefaults = {\n  showOnRightSide: false,\n}\n\nexport function activate() {\n  ComponentRegistry.register(ThreadSearchBar, {\n    location: WorkspaceStore.Location.ThreadList.Toolbar,\n  })\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ThreadSearchBar)\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/lib/search-actions.es6",
    "content": "import Reflux from 'reflux';\n\nconst SearchActions = Reflux.createActions([\n  \"querySubmitted\",\n  \"queryChanged\",\n  \"searchBlurred\",\n  \"searchCompleted\",\n]);\n\nfor (const key of Object.keys(SearchActions)) {\n  SearchActions[key].sync = true;\n}\n\nexport default SearchActions\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/lib/search-mailbox-perspective.es6",
    "content": "import _ from 'underscore'\nimport {AccountStore, CategoryStore, TaskFactory, MailboxPerspective} from 'nylas-exports'\nimport SearchQuerySubscription from './search-query-subscription'\n\nclass SearchMailboxPerspective extends MailboxPerspective {\n\n  constructor(sourcePerspective, searchQuery) {\n    super(sourcePerspective.accountIds)\n    if (!_.isString(searchQuery)) {\n      throw new Error(\"SearchMailboxPerspective: Expected a `string` search query\")\n    }\n\n    this.searchQuery = searchQuery;\n    if (sourcePerspective instanceof SearchMailboxPerspective) {\n      this.sourcePerspective = sourcePerspective.sourcePerspective;\n    } else {\n      this.sourcePerspective = sourcePerspective;\n    }\n\n    this.name = `Searching ${this.sourcePerspective.name}`\n  }\n\n  _folderScope() {\n    // When the inbox is focused we don't specify a folder scope. If the user\n    // wants to search just the inbox then they have to specify it explicitly.\n    if (this.sourcePerspective.isInbox()) {\n      return '';\n    }\n    const folderQuery = this.sourcePerspective.categories().map((c) => c.displayName).join('\" OR in:\"');\n    return `AND (in:\"${folderQuery}\")`;\n  }\n\n  emptyMessage() {\n    return \"No search results available\"\n  }\n\n  isEqual(other) {\n    return super.isEqual(other) && other.searchQuery === this.searchQuery\n  }\n\n  threads() {\n    return new SearchQuerySubscription(`(${this.searchQuery}) ${this._folderScope()}`, this.accountIds)\n  }\n\n  canReceiveThreadsFromAccountIds() {\n    return false\n  }\n\n  tasksForRemovingItems(threads) {\n    return TaskFactory.tasksForApplyingCategories({\n      source: \"Removing from Search Results\",\n      threads: threads,\n      categoriesToAdd: (accountId) => {\n        const account = AccountStore.accountForId(accountId)\n        return [account.defaultFinishedCategory()]\n      },\n      categoriesToRemove: (accountId) => {\n        return [CategoryStore.getInboxCategory(accountId)]\n      },\n    })\n  }\n}\n\nexport default SearchMailboxPerspective;\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/lib/search-query-subscription.es6",
    "content": "import _ from 'underscore'\nimport {\n  Actions,\n  NylasAPI,\n  Thread,\n  DatabaseStore,\n  SearchQueryParser,\n  ComponentRegistry,\n  NylasLongConnection,\n  FocusedContentStore,\n  MutableQuerySubscription,\n} from 'nylas-exports'\nimport SearchActions from './search-actions'\n\nconst {LongConnectionStatus} = NylasAPI\n\nclass SearchQuerySubscription extends MutableQuerySubscription {\n\n  constructor(searchQuery, accountIds) {\n    super(null, {emitResultSet: true})\n    this._searchQuery = searchQuery\n    this._accountIds = accountIds\n\n    this.resetData()\n\n    this._connections = []\n    this._unsubscribers = [\n      FocusedContentStore.listen(this.onFocusedContentChanged),\n    ]\n    this._extDisposables = []\n\n    _.defer(() => this.performSearch())\n  }\n\n  replaceRange = () => {\n    // TODO\n  }\n\n  resetData() {\n    this._searchStartedAt = null\n    this._localResultsReceivedAt = null\n    this._remoteResultsReceivedAt = null\n    this._remoteResultsCount = 0\n    this._localResultsCount = 0\n    this._firstThreadSelectedAt = null\n    this._lastFocusedThread = null\n    this._focusedThreadCount = 0\n  }\n\n  performSearch() {\n    this._searchStartedAt = Date.now()\n\n    this.performLocalSearch()\n    this.performRemoteSearch()\n    this.performExtensionSearch()\n  }\n\n  performLocalSearch() {\n    let dbQuery = DatabaseStore.findAll(Thread).distinct()\n    if (this._accountIds.length === 1) {\n      dbQuery = dbQuery.where({accountId: this._accountIds[0]})\n    }\n    try {\n      const parsedQuery = SearchQueryParser.parse(this._searchQuery);\n      console.info('Successfully parsed and codegened search query', parsedQuery);\n      dbQuery = dbQuery.structuredSearch(parsedQuery);\n    } catch (e) {\n      console.info('Failed to parse local search query, falling back to generic query', e);\n      dbQuery = dbQuery.search(this._searchQuery);\n    }\n    dbQuery = dbQuery\n      .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n      .limit(100)\n\n    console.info('dbQuery.sql() =', dbQuery.sql());\n\n    dbQuery.then((results) => {\n      if (!this._localResultsReceivedAt) {\n        this._localResultsReceivedAt = Date.now()\n      }\n      this._localResultsCount += results.length\n      // Even if we don't have any results now we might sync additional messages\n      // from the provider which will cause new results to appear later.\n      this.replaceQuery(dbQuery)\n    })\n  }\n\n  _addThreadIdsToSearch(ids = []) {\n    const currentResults = this._set && this._set.ids().length > 0;\n    let searchIds = ids;\n    if (currentResults) {\n      const currentResultIds = this._set.ids()\n      searchIds = _.uniq(currentResultIds.concat(ids))\n    }\n    const dbQuery = (\n      DatabaseStore.findAll(Thread)\n      .where({id: searchIds})\n      .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n    )\n    this.replaceQuery(dbQuery)\n  }\n\n  performRemoteSearch() {\n    const accountsSearched = new Set()\n    const allAccountsSearched = () => accountsSearched.size === this._accountIds.length\n    this._connections = this._accountIds.map((accountId) => {\n      const conn = new NylasLongConnection({\n        accountId,\n        api: NylasAPI,\n        path: `/threads/search/streaming?q=${encodeURIComponent(this._searchQuery)}`,\n        onResults: (results) => {\n          if (!this._remoteResultsReceivedAt) {\n            this._remoteResultsReceivedAt = Date.now();\n          }\n          const threads = results[0];\n          this._remoteResultsCount += threads.length;\n        },\n        onStatusChanged: (status) => {\n          const hasClosed = [\n            LongConnectionStatus.Closed,\n            LongConnectionStatus.Ended,\n          ].includes(status)\n\n          if (hasClosed) {\n            accountsSearched.add(accountId)\n            if (allAccountsSearched()) {\n              SearchActions.searchCompleted()\n            }\n          }\n        },\n      })\n\n      return conn.start()\n    })\n  }\n\n  performExtensionSearch() {\n    const searchExtensions = ComponentRegistry.findComponentsMatching({\n      role: \"SearchBarResults\",\n    })\n\n    this._extDisposables = searchExtensions.map((ext) => {\n      return ext.observeThreadIdsForQuery(this._searchQuery)\n      .subscribe((ids = []) => {\n        const allIds = _.compact(_.flatten(ids))\n        if (allIds.length === 0) return;\n        this._addThreadIdsToSearch(allIds)\n      })\n    })\n  }\n\n  // We want to keep track of how many threads from the search results were\n  // focused\n  onFocusedContentChanged = () => {\n    const thread = FocusedContentStore.focused('thread')\n    const shouldRecordChange = (\n      thread &&\n      (this._lastFocusedThread || {}).id !== thread.id\n    )\n    if (shouldRecordChange) {\n      if (this._focusedThreadCount === 0) {\n        this._firstThreadSelectedAt = Date.now()\n      }\n      this._focusedThreadCount += 1\n      this._lastFocusedThread = thread\n    }\n  }\n\n  reportSearchMetrics() {\n    if (!this._searchStartedAt) {\n      return;\n    }\n\n    let timeToLocalResultsMs = null\n    let timeToFirstRemoteResultsMs = null;\n    let timeToFirstThreadSelectedMs = null;\n    const timeInsideSearchMs = Date.now() - this._searchStartedAt\n    const numThreadsSelected = this._focusedThreadCount\n    const numLocalResults = this._localResultsCount\n    const numRemoteResults = this._remoteResultsCount\n\n    if (this._firstThreadSelectedAt) {\n      timeToFirstThreadSelectedMs = this._firstThreadSelectedAt - this._searchStartedAt\n    }\n    if (this._localResultsReceivedAt) {\n      timeToLocalResultsMs = this._localResultsReceivedAt - this._searchStartedAt\n    }\n    if (this._remoteResultsReceivedAt) {\n      timeToFirstRemoteResultsMs = this._remoteResultsReceivedAt - this._searchStartedAt\n    }\n\n    Actions.recordPerfMetric({\n      action: 'search-performed',\n      actionTimeMs: timeToLocalResultsMs,\n      numLocalResults,\n      numRemoteResults,\n      numThreadsSelected,\n      clippedData: [\n        {key: 'timeToLocalResultsMs', val: timeToLocalResultsMs},\n        {key: 'timeToFirstThreadSelectedMs', val: timeToFirstThreadSelectedMs},\n        {key: 'timeInsideSearchMs', val: timeInsideSearchMs, maxValue: 60 * 1000},\n        {key: 'timeToFirstRemoteResultsMs', val: timeToFirstRemoteResultsMs, maxValue: 10 * 1000},\n      ],\n    })\n    this.resetData()\n  }\n\n  // This function is called when the user leaves the SearchPerspective\n  onLastCallbackRemoved() {\n    this.reportSearchMetrics();\n    this._connections.forEach((conn) => conn.end())\n    this._unsubscribers.forEach((unsub) => unsub())\n    this._extDisposables.forEach((disposable) => disposable.dispose())\n  }\n}\n\nexport default SearchQuerySubscription\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/lib/search-store.es6",
    "content": "import NylasStore from 'nylas-store';\nimport {\n  Thread,\n  Actions,\n  ContactStore,\n  AccountStore,\n  DatabaseStore,\n  ComponentRegistry,\n  FocusedPerspectiveStore,\n  SearchQueryParser,\n} from 'nylas-exports';\n\nimport SearchActions from './search-actions';\nimport SearchMailboxPerspective from './search-mailbox-perspective';\n\n// Stores should closely match the needs of a particular part of the front end.\n// For example, we might create a \"MessageStore\" that observes this store\n// for changes in selectedThread, \"DatabaseStore\" for changes to the underlying database,\n// and vends up the array used for that view.\n\nclass SearchStore extends NylasStore {\n  constructor() {\n    super();\n\n    this._searchQuery = FocusedPerspectiveStore.current().searchQuery || \"\";\n    this._searchSuggestionsVersion = 1;\n    this._isSearching = false;\n    this._extensionData = []\n    this._clearResults();\n\n    this.listenTo(FocusedPerspectiveStore, this._onPerspectiveChanged);\n    this.listenTo(SearchActions.querySubmitted, this._onQuerySubmitted);\n    this.listenTo(SearchActions.queryChanged, this._onQueryChanged);\n    this.listenTo(SearchActions.searchBlurred, this._onSearchBlurred);\n    this.listenTo(SearchActions.searchCompleted, this._onSearchCompleted);\n  }\n\n  query() {\n    return this._searchQuery;\n  }\n\n  queryPopulated() {\n    return this._searchQuery && this._searchQuery.trim().length > 0;\n  }\n\n  suggestions() {\n    return this._suggestions;\n  }\n\n  isSearching() {\n    return this._isSearching;\n  }\n\n  _onSearchCompleted = () => {\n    this._isSearching = false;\n    this.trigger();\n  }\n\n  _onPerspectiveChanged = () => {\n    this._searchQuery = FocusedPerspectiveStore.current().searchQuery || \"\";\n    this.trigger();\n  }\n\n  _onQueryChanged = (query) => {\n    this._searchQuery = query;\n    if (this._searchQuery.length <= 1) {\n      this.trigger()\n      return\n    }\n    this._compileResults();\n    setTimeout(() => this._rebuildResults(), 0);\n  }\n\n  _onQuerySubmitted = (query) => {\n    this._searchQuery = query;\n    const current = FocusedPerspectiveStore.current();\n\n    if (this.queryPopulated()) {\n      this._isSearching = true;\n      if (this._perspectiveBeforeSearch == null) {\n        this._perspectiveBeforeSearch = current;\n      }\n      const next = new SearchMailboxPerspective(current, this._searchQuery.trim());\n      Actions.focusMailboxPerspective(next);\n    } else if (current instanceof SearchMailboxPerspective) {\n      if (this._perspectiveBeforeSearch) {\n        Actions.focusMailboxPerspective(this._perspectiveBeforeSearch);\n        this._perspectiveBeforeSearch = null;\n      } else {\n        Actions.focusDefaultMailboxPerspectiveForAccounts(AccountStore.accounts());\n      }\n    }\n\n    this._clearResults();\n  }\n\n  _onSearchBlurred = () => {\n    this._clearResults();\n  }\n\n  _clearResults() {\n    this._searchSuggestionsVersion = 1;\n    this._threadResults = [];\n    this._contactResults = [];\n    this._suggestions = [];\n    this.trigger();\n  }\n\n  _rebuildResults() {\n    if (!this.queryPopulated()) {\n      this._clearResults();\n      return;\n    }\n    this._searchSuggestionsVersion += 1;\n    const searchExtensions = ComponentRegistry.findComponentsMatching({\n      role: \"SearchBarResults\",\n    })\n\n    Promise.map(searchExtensions, (ext) => {\n      return Promise.props({\n        label: ext.searchLabel(),\n        suggestions: ext.fetchSearchSuggestions(this._searchQuery),\n      })\n    }).then((extensionData = []) => {\n      this._extensionData = extensionData;\n      this._compileResults();\n    })\n\n    this._fetchThreadResults();\n    this._fetchContactResults();\n  }\n\n  _fetchContactResults() {\n    const version = this._searchSuggestionsVersion;\n    ContactStore.searchContacts(this._searchQuery, {limit: 10}).then(contacts => {\n      if (version !== this._searchSuggestionsVersion) {\n        return;\n      }\n      this._contactResults = contacts;\n      this._compileResults();\n    });\n  }\n\n  _fetchThreadResults() {\n    if (this._fetchingThreadResultsVersion) { return; }\n    this._fetchingThreadResultsVersion = this._searchSuggestionsVersion;\n\n    const {accountIds} = FocusedPerspectiveStore.current();\n    let dbQuery = DatabaseStore.findAll(Thread).distinct()\n    if (Array.isArray(accountIds) && accountIds.length === 1) {\n      dbQuery = dbQuery.where({accountId: accountIds[0]})\n    }\n\n    try {\n      const parsedQuery = SearchQueryParser.parse(this._searchQuery);\n      // console.info('Successfully parsed and codegened search query', parsedQuery);\n      dbQuery = dbQuery.structuredSearch(parsedQuery);\n    } catch (e) {\n      // console.info('Failed to parse local search query, falling back to generic query', e);\n      dbQuery = dbQuery.search(this._searchQuery);\n    }\n    dbQuery = dbQuery\n      .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n\n    // console.info(dbQuery.sql());\n\n    dbQuery.background().then(results => {\n      // We've fetched the latest thread results - display them!\n      if (this._searchSuggestionsVersion === this._fetchingThreadResultsVersion) {\n        this._fetchingThreadResultsVersion = null;\n        this._threadResults = results;\n        this._compileResults();\n      // We're behind and need to re-run the search for the latest results\n      } else if (this._searchSuggestionsVersion > this._fetchingThreadResultsVersion) {\n        this._fetchingThreadResultsVersion = null;\n        this._fetchThreadResults();\n      } else {\n        this._fetchingThreadResultsVersion = null;\n      }\n    }\n    );\n  }\n\n  _compileResults() {\n    this._suggestions = [];\n\n    this._suggestions.push({\n      label: `Message Contains: ${this._searchQuery}`,\n      value: this._searchQuery,\n    });\n\n    if (this._threadResults.length) {\n      this._suggestions.push({divider: 'Threads'});\n      for (const thread of this._threadResults) {\n        this._suggestions.push({thread});\n      }\n    }\n\n    if (this._contactResults.length) {\n      this._suggestions.push({divider: 'People'});\n      for (const contact of this._contactResults) {\n        this._suggestions.push({\n          contact: contact,\n          value: contact.email,\n        });\n      }\n    }\n\n    if (this._extensionData.length) {\n      for (const {label, suggestions} of this._extensionData) {\n        this._suggestions.push({divider: label});\n        this._suggestions = this._suggestions.concat(suggestions)\n      }\n    }\n\n    this.trigger();\n  }\n}\n\nexport default new SearchStore();\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/lib/thread-search-bar.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {Menu, SearchBar, ListensToFluxStore} from 'nylas-component-kit'\nimport {FocusedPerspectiveStore} from 'nylas-exports'\nimport SearchStore from './search-store'\nimport SearchActions from './search-actions'\n\n\nclass ThreadSearchBar extends Component {\n  static displayName = 'ThreadSearchBar';\n\n  static propTypes = {\n    query: PropTypes.string,\n    isSearching: PropTypes.bool,\n    suggestions: PropTypes.array,\n    perspective: PropTypes.object,\n  }\n\n  _onSelectSuggestion = (suggestion) => {\n    if (suggestion.thread) {\n      SearchActions.querySubmitted(`\"${suggestion.thread.subject}\"`)\n    } else {\n      SearchActions.querySubmitted(suggestion.value);\n    }\n  }\n\n  _onSearchQueryChanged = (query) => {\n    SearchActions.queryChanged(query);\n    if (query === '') {\n      this._onClearSearchQuery();\n    }\n  }\n\n  _onSubmitSearchQuery = (query) => {\n    SearchActions.querySubmitted(query);\n  }\n\n  _onClearSearchQuery = () => {\n    SearchActions.querySubmitted('');\n  }\n\n  _onClearSearchSuggestions = () => {\n    SearchActions.searchBlurred()\n  }\n\n  _renderSuggestion = (suggestion) => {\n    if (suggestion.contact) {\n      return <Menu.NameEmailItem name={suggestion.contact.name} email={suggestion.contact.email} />;\n    }\n    if (suggestion.thread) {\n      return suggestion.thread.subject;\n    }\n    if (suggestion.customElement) {\n      return suggestion.customElement\n    }\n    return suggestion.label;\n  }\n\n  _placeholder = () => {\n    if (this.props.perspective.isInbox()) {\n      return 'Search all email';\n    }\n    return `Search ${this.props.perspective.name}`;\n  }\n\n  render() {\n    const {query, isSearching, suggestions} = this.props;\n\n    return (\n      <SearchBar\n        className=\"thread-search-bar\"\n        placeholder={this._placeholder()}\n        query={query}\n        suggestions={suggestions}\n        isSearching={isSearching}\n        suggestionKey={(suggestion) => suggestion.label || (suggestion.contact || {}).id || (suggestion.thread || {}).id}\n        suggestionRenderer={this._renderSuggestion}\n        onSelectSuggestion={this._onSelectSuggestion}\n        onSubmitSearchQuery={this._onSubmitSearchQuery}\n        onSearchQueryChanged={this._onSearchQueryChanged}\n        onClearSearchQuery={this._onClearSearchQuery}\n        onClearSearchSuggestions={this._onClearSearchSuggestions}\n      />\n    )\n  }\n}\n\nexport default ListensToFluxStore(ThreadSearchBar, {\n  stores: [SearchStore, FocusedPerspectiveStore],\n  getStateFromStores() {\n    return {\n      query: SearchStore.query(),\n      suggestions: SearchStore.suggestions(),\n      isSearching: SearchStore.isSearching(),\n      perspective: FocusedPerspectiveStore.current(),\n    };\n  },\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/package.json",
    "content": "{\n  \"name\": \"thread-search\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Search for threads\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/spec/search-bar-spec.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\nReactTestUtils = require('react-addons-test-utils')\n\nThreadSearchBar = require('../lib/thread-search-bar').default\nSearchActions = require('../lib/search-actions').default\n\ndescribe 'ThreadSearchBar', ->\n  beforeEach ->\n    spyOn(NylasEnv, \"isMainWindow\").andReturn true\n    @searchBar = ReactTestUtils.renderIntoDocument(<ThreadSearchBar />)\n    @input = ReactDOM.findDOMNode(@searchBar).querySelector(\"input\")\n\n  it 'supports search queries with a colon character', ->\n    spyOn(SearchActions, \"queryChanged\")\n    test = \"::Hello: World::\"\n    ReactTestUtils.Simulate.change @input, target: value: test\n    expect(SearchActions.queryChanged).toHaveBeenCalledWith(test)\n\n  it 'preserves capitalization on searches', ->\n    test = \"HeLlO wOrLd\"\n    ReactTestUtils.Simulate.change @input, target: value: test\n    waitsFor =>\n      @input.value.length > 0\n    runs =>\n      expect(@input.value).toBe(test)\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-search/stylesheets/thread-search-bar.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.nylas-search-bar.thread-search-bar {\n  position: relative;\n  order: -100;\n  overflow: visible;\n  z-index: 100;\n  width: 450px;\n  margin-top: (38px - 23px) / 2;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/lib/copy-button.jsx",
    "content": "import React from 'react'\nimport {Utils} from 'nylas-exports';\nimport {clipboard} from 'electron';\n\n\nclass CopyButton extends React.Component {\n\n  static propTypes = {\n    btnLabel: React.PropTypes.string,\n    copyValue: React.PropTypes.string,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      btnLabel: props.btnLabel,\n    }\n    this._timeout = null\n  }\n\n  componentWillReceiveProps(nextProps) {\n    clearTimeout(this._timeout)\n    this._timeout = null\n    this.setState({btnLabel: nextProps.btnLabel})\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this._timeout)\n  }\n\n  _onCopy = () => {\n    if (this._timeout) { return }\n    const {copyValue, btnLabel} = this.props\n    clipboard.writeText(copyValue)\n    this.setState({btnLabel: 'Copied!'})\n    this._timeout = setTimeout(() => {\n      this._timeout = null\n      this.setState({btnLabel: btnLabel})\n    }, 2000)\n  }\n\n  render() {\n    const {btnLabel} = this.state\n    const otherProps = Utils.fastOmit(this.props, Object.keys(CopyButton.propTypes));\n    return (\n      <button onClick={this._onCopy} {...otherProps}>\n        {btnLabel}\n      </button>\n    )\n  }\n}\nexport default CopyButton\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/lib/external-threads.es6",
    "content": "import url from 'url'\nimport querystring from 'querystring';\nimport {ipcRenderer} from 'electron';\nimport {DatabaseStore, Thread, Matcher, Actions} from \"nylas-exports\";\n\n\nconst DATE_EPSILON = 60  // Seconds\n\nconst parseOpenThreadUrl = (nylasUrlString) => {\n  const parsedUrl = url.parse(nylasUrlString)\n  const params = querystring.parse(parsedUrl.query)\n  params.lastDate = parseInt(params.lastDate, 10)\n  return params;\n}\n\nconst findCorrespondingThread = ({subject, lastDate}, dateEpsilon = DATE_EPSILON) => {\n  return DatabaseStore.findBy(Thread).where([\n    Thread.attributes.subject.equal(subject),\n    new Matcher.Or([\n      new Matcher.And([\n        Thread.attributes.lastMessageSentTimestamp.lessThan(lastDate + dateEpsilon),\n        Thread.attributes.lastMessageSentTimestamp.greaterThan(lastDate - dateEpsilon),\n      ]),\n      new Matcher.And([\n        Thread.attributes.lastMessageReceivedTimestamp.lessThan(lastDate + dateEpsilon),\n        Thread.attributes.lastMessageReceivedTimestamp.greaterThan(lastDate - dateEpsilon),\n      ]),\n    ]),\n  ])\n}\n\nconst _openExternalThread = (event, nylasUrl) => {\n  const {subject, lastDate} = parseOpenThreadUrl(nylasUrl);\n\n  findCorrespondingThread({subject, lastDate})\n  .then((thread) => {\n    if (!thread) {\n      throw new Error('Thread not found')\n    }\n    Actions.popoutThread(thread);\n  })\n  .catch((error) => {\n    NylasEnv.reportError(error)\n    NylasEnv.showErrorDialog(`The thread ${subject} does not exist in your mailbox!`)\n  })\n}\n\nconst activate = () => {\n  ipcRenderer.on('openExternalThread', _openExternalThread)\n}\n\nconst deactivate = () => {\n  ipcRenderer.removeListener('openExternalThread', _openExternalThread)\n}\n\nexport default {activate, deactivate}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/lib/main.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports';\nimport ThreadSharingButton from \"./thread-sharing-button\";\nimport ExternalThreads from \"./external-threads\"\n\nexport function activate() {\n  ComponentRegistry.register(ThreadSharingButton, {\n    role: 'ThreadActionsToolbarButton',\n  });\n  ExternalThreads.activate();\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ThreadSharingButton);\n  ExternalThreads.deactivate();\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/lib/thread-sharing-button.jsx",
    "content": "import {Actions, React, ReactDOM} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\nimport ThreadSharingPopover from './thread-sharing-popover';\n\nexport default class ThreadSharingButton extends React.Component {\n  static displayName = 'ThreadSharingButton';\n\n  static containerRequired = false;\n\n  static propTypes = {\n    items: React.PropTypes.array,\n    thread: React.PropTypes.object,\n  };\n\n  componentWillReceiveProps(nextProps) {\n    if (nextProps.thread.id !== this.props.thread.id) {\n      Actions.closePopover()\n    }\n  }\n\n  _onClick = () => {\n    const {thread} = this.props;\n\n    Actions.openPopover(\n      <ThreadSharingPopover\n        thread={thread}\n        accountId={thread.accountId}\n        closePopover={Actions.closePopover}\n      />,\n      {\n        originRect: ReactDOM.findDOMNode(this).getBoundingClientRect(),\n        direction: 'down',\n      }\n    )\n  }\n\n  render() {\n    if (this.props.items && this.props.items.length > 1) {\n      return <span />\n    }\n\n    return (\n      <button\n        className=\"btn btn-toolbar thread-sharing-button\"\n        title=\"Share\"\n        style={{marginRight: 0}}\n        onClick={this._onClick}\n      >\n        <RetinaImg\n          name=\"ic-toolbar-native-share.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </button>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/lib/thread-sharing-constants.es6",
    "content": "import plugin from '../package.json'\n\nexport const PLUGIN_NAME = plugin.title\nexport const PLUGIN_ID = plugin.name;\nexport const PLUGIN_URL = plugin.serverUrl[NylasEnv.config.get(\"env\")];\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/lib/thread-sharing-popover.jsx",
    "content": "/* eslint jsx-a11y/tabindex-no-positive: 0 */\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport classnames from 'classnames';\nimport {Rx, Actions, NylasAPIHelpers, Thread, DatabaseStore} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\n\nimport CopyButton from './copy-button';\nimport {PLUGIN_ID, PLUGIN_NAME, PLUGIN_URL} from './thread-sharing-constants';\n\n\nfunction isShared(thread) {\n  const metadata = thread.metadataForPluginId(PLUGIN_ID) || {};\n  return metadata.shared || false;\n}\n\nexport default class ThreadSharingPopover extends React.Component {\n  static propTypes = {\n    thread: React.PropTypes.object,\n    accountId: React.PropTypes.string,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      shared: isShared(props.thread),\n      saving: false,\n    }\n    this._disposable = {dispose: () => {}}\n  }\n\n  componentDidMount() {\n    const {thread} = this.props;\n    this._mounted = true;\n    this._disposable = Rx.Observable.fromQuery(DatabaseStore.find(Thread, thread.id))\n    .subscribe((t) => this.setState({shared: isShared(t)}))\n  }\n\n  componentDidUpdate() {\n    ReactDOM.findDOMNode(this).focus()\n  }\n\n  componentWillUnmount() {\n    this._disposable.dispose();\n    this._mounted = false;\n  }\n\n  _onToggleShared = () => {\n    const {thread, accountId} = this.props;\n    const {shared} = this.state;\n\n    this.setState({saving: true});\n\n    NylasAPIHelpers.authPlugin(PLUGIN_ID, PLUGIN_NAME, accountId)\n    .then(() => {\n      if (!this._mounted) { return; }\n      if (!shared === true) {\n        Actions.recordUserEvent(\"Thread Sharing Enabled\", {accountId, threadId: thread.id})\n      }\n      Actions.setMetadata(thread, PLUGIN_ID, {shared: !shared})\n    })\n    .catch((error) => {\n      NylasEnv.reportError(error);\n      NylasEnv.showErrorDialog(`Sorry, we were unable to update your sharing settings.\\n\\n${error.message}`)\n    })\n    .finally(() => {\n      if (!this._mounted) { return; }\n      this.setState({saving: false})\n    });\n  }\n\n  _onClickInput = (event) => {\n    const input = event.target\n    input.select()\n  }\n\n  render() {\n    const {thread, accountId} = this.props;\n    const {shared, saving} = this.state;\n\n    const url = `${PLUGIN_URL}/thread/${accountId}/${thread.id}`\n    const shareMessage = shared ? 'Anyone with the link can read the thread' : 'Sharing is disabled';\n    const classes = classnames({\n      'thread-sharing-popover': true,\n      'disabled': !shared,\n    })\n\n    const control = saving ? (\n      <RetinaImg\n        style={{width: 14, height: 14, marginBottom: 3, marginRight: 4}}\n        name=\"inline-loading-spinner.gif\"\n        mode={RetinaImg.Mode.ContentPreserve}\n      />\n    ) : (\n      <input\n        type=\"checkbox\"\n        id=\"shareCheckbox\"\n        checked={shared}\n        onChange={this._onToggleShared}\n      />\n  );\n\n    // tabIndex is necessary for the popover's onBlur events to work properly\n    return (\n      <div tabIndex=\"1\" className={classes}>\n        <div className=\"share-toggle\">\n          <label htmlFor=\"shareCheckbox\">\n            {control}\n            Share this thread\n          </label>\n        </div>\n        <div className=\"share-input\">\n          <input\n            ref=\"urlInput\"\n            id=\"urlInput\"\n            type=\"text\"\n            value={url}\n            readOnly\n            disabled={!shared}\n            onClick={this._onClickInput}\n          />\n        </div>\n        <div className={`share-controls`}>\n          <div className=\"share-message\">{shareMessage}</div>\n          <button href={url} className=\"btn\" disabled={!shared}>\n            Open in browser\n          </button>\n          <CopyButton\n            className=\"btn\"\n            disabled={!shared}\n            copyValue={url}\n            btnLabel=\"Copy link\"\n          />\n        </div>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/package.json",
    "content": "{\n  \"name\": \"thread-sharing\",\n  \"version\": \"0.1.0\",\n  \"serverUrl\": {\n    \"development\": \"http://localhost:5009\",\n    \"staging\": \"https://share-staging.nylas.com\",\n    \"production\": \"https://share.nylas.com\"\n  },\n\n  \"title\": \"Thread Sharing\",\n  \"description\": \"Share a thread through the web.\",\n  \"main\": \"./lib/main\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-sharing/stylesheets/main.less",
    "content": "@import \"ui-variables\";\n\n.thread-sharing-button {\n  order: -102;\n}\n\n.fixed-popover .thread-sharing-popover {\n  position: relative;\n  width: 265px;\n\n  .share-toggle {\n    padding: 7px 10px;\n    input {\n      margin-right: @spacing-half;\n    }\n  }\n\n  .share-input {\n    border-top: 2px solid rgba(0, 0, 0, 0.15);\n    padding: 10px 5px 0 5px;\n    input[type=\"text\"] {\n      cursor: default;\n      -webkit-user-select: all;\n    }\n  }\n\n  .share-controls {\n    text-align: center;\n    padding: 10px;\n\n    .share-message {\n      color: @text-color-very-subtle;\n      margin-bottom: 10px;\n      font-size: @font-size-smaller;\n    }\n\n    button.btn + button.btn {\n      margin-left: 10px;\n    }\n  }\n\n  &.disabled {\n    .share-input input[type=\"text\"] {\n      color: @text-color-very-subtle;\n      -webkit-user-select: none;\n    }\n    button.btn {\n      color: @text-color-very-subtle;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/README.md",
    "content": ""
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/main.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports';\nimport {HasTutorialTip} from 'nylas-component-kit';\n\nimport {ToolbarSnooze, QuickActionSnooze} from './snooze-buttons';\nimport SnoozeMailLabel from './snooze-mail-label'\nimport SnoozeStore from './snooze-store'\n\n\nexport function activate() {\n  this.snoozeStore = new SnoozeStore()\n\n  const ToolbarSnoozeWithTutorialTip = HasTutorialTip(ToolbarSnooze, {\n    title: \"Handle it later!\",\n    instructions: \"Snooze this email and it'll return to your inbox later. Click here or swipe across the thread in your inbox to snooze.\",\n  });\n\n  this.snoozeStore.activate()\n  ComponentRegistry.register(ToolbarSnoozeWithTutorialTip, {role: 'ThreadActionsToolbarButton'});\n  ComponentRegistry.register(QuickActionSnooze, {role: 'ThreadListQuickAction'});\n  ComponentRegistry.register(SnoozeMailLabel, {role: 'Thread:MailLabel'});\n}\n\nexport function deactivate() {\n  ComponentRegistry.unregister(ToolbarSnooze);\n  ComponentRegistry.unregister(QuickActionSnooze);\n  ComponentRegistry.unregister(SnoozeMailLabel);\n  this.snoozeStore.deactivate()\n}\n\nexport function serialize() {\n\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/snooze-actions.es6",
    "content": "import Reflux from 'reflux';\n\nconst SnoozeActions = Reflux.createActions([\n  'snoozeThreads',\n])\n\nfor (const key of Object.keys(SnoozeActions)) {\n  SnoozeActions[key].sync = true\n}\n\nexport default SnoozeActions\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/snooze-buttons.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport ReactDOM from 'react-dom';\nimport {Actions, FocusedPerspectiveStore} from 'nylas-exports';\nimport {RetinaImg} from 'nylas-component-kit';\nimport SnoozePopover from './snooze-popover';\n\n\nclass SnoozeButton extends Component {\n\n  static propTypes = {\n    className: PropTypes.string,\n    threads: PropTypes.array,\n    direction: PropTypes.string,\n    shouldRenderIconImg: PropTypes.bool,\n    getBoundingClientRect: PropTypes.func,\n  };\n\n  static defaultProps = {\n    className: 'btn btn-toolbar',\n    direction: 'down',\n    shouldRenderIconImg: true,\n    getBoundingClientRect: (inst) => ReactDOM.findDOMNode(inst).getBoundingClientRect(),\n  };\n\n  onClick = (event) => {\n    event.stopPropagation()\n    const {threads, direction, getBoundingClientRect} = this.props\n    const buttonRect = getBoundingClientRect(this)\n    Actions.openPopover(\n      <SnoozePopover\n        threads={threads}\n        closePopover={Actions.closePopover}\n      />,\n      {originRect: buttonRect, direction: direction}\n    )\n  };\n\n  render() {\n    if (!FocusedPerspectiveStore.current().isInbox()) {\n      return <span />;\n    }\n    return (\n      <button\n        title=\"Snooze\"\n        tabIndex={-1}\n        className={`snooze-button ${this.props.className}`}\n        onClick={this.onClick}\n      >\n        {this.props.shouldRenderIconImg ?\n          <RetinaImg\n            name=\"toolbar-snooze.png\"\n            mode={RetinaImg.Mode.ContentIsMask}\n          /> :\n          null\n        }\n      </button>\n    );\n  }\n}\n\n\nexport class QuickActionSnooze extends Component {\n  static displayName = 'QuickActionSnooze';\n\n  static propTypes = {\n    thread: PropTypes.object,\n  };\n\n  static containerRequired = false;\n\n  getBoundingClientRect = () => {\n    // Grab the parent node because of the zoom applied to this button. If we\n    // took this element directly, we'd have to divide everything by 2\n    const element = ReactDOM.findDOMNode(this).parentNode;\n    const {height, width, top, bottom, left, right} = element.getBoundingClientRect()\n\n    // The parent node is a bit too much to the left, lets adjust this.\n    return {height, width, top, bottom, right, left: left + 5}\n  };\n\n  render() {\n    if (!FocusedPerspectiveStore.current().isInbox()) {\n      return <span />;\n    }\n    return (\n      <SnoozeButton\n        direction=\"left\"\n        threads={[this.props.thread]}\n        className=\"btn action action-snooze\"\n        shouldRenderIconImg={false}\n        getBoundingClientRect={this.getBoundingClientRect}\n      />\n    );\n  }\n}\n\n\nexport class ToolbarSnooze extends Component {\n  static displayName = 'ToolbarSnooze';\n\n  static propTypes = {\n    items: PropTypes.array,\n  };\n\n  static containerRequired = false;\n\n  render() {\n    if (!FocusedPerspectiveStore.current().isInbox()) {\n      return <span />;\n    }\n    return (\n      <SnoozeButton threads={this.props.items} />\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/snooze-constants.es6",
    "content": "import plugin from '../package.json'\n\nexport const PLUGIN_ID = plugin.name;\nexport const PLUGIN_NAME = \"Snooze Plugin\"\nexport const SNOOZE_CATEGORY_NAME = \"N1-Snoozed\"\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/snooze-mail-label.jsx",
    "content": "import _ from 'underscore';\nimport React, {Component, PropTypes} from 'react';\nimport {FocusedPerspectiveStore} from 'nylas-exports';\nimport {RetinaImg, MailLabel} from 'nylas-component-kit';\nimport {SNOOZE_CATEGORY_NAME, PLUGIN_ID} from './snooze-constants';\nimport SnoozeUtils from './snooze-utils';\n\n\nclass SnoozeMailLabel extends Component {\n  static displayName = 'SnoozeMailLabel';\n\n  static propTypes = {\n    thread: PropTypes.object,\n  };\n\n  static containerRequired = false;\n\n  render() {\n    const current = FocusedPerspectiveStore.current()\n    const isSnoozedPerspective = (\n      current.categories().length > 0 &&\n      current.categories()[0].displayName === SNOOZE_CATEGORY_NAME\n    )\n\n    if (!isSnoozedPerspective) {\n      return false\n    }\n\n    const {thread} = this.props;\n    if (_.findWhere(thread.categories, {displayName: SNOOZE_CATEGORY_NAME})) {\n      const metadata = thread.metadataForPluginId(PLUGIN_ID);\n      if (metadata) {\n        // TODO this is such a hack\n        const {snoozeDate} = metadata;\n        const message = SnoozeUtils.snoozedUntilMessage(snoozeDate).replace('Snoozed', '')\n        const content = (\n          <span className=\"snooze-mail-label\">\n            <RetinaImg\n              name=\"icon-snoozed.png\"\n              mode={RetinaImg.Mode.ContentIsMask}\n            />\n            <span className=\"date-message\">{message}</span>\n          </span>\n        )\n        const label = {\n          displayName: content,\n          isLockedCategory: () => true,\n          hue: () => 259,\n        }\n        return <MailLabel label={label} key={`snooze-message-${thread.id}`} />;\n      }\n      return <span />\n    }\n    return <span />\n  }\n}\n\nexport default SnoozeMailLabel;\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/snooze-popover.jsx",
    "content": "import _ from 'underscore';\nimport React, {Component, PropTypes} from 'react';\nimport {DateUtils, Actions} from 'nylas-exports'\nimport {RetinaImg, DateInput} from 'nylas-component-kit';\nimport SnoozeActions from './snooze-actions'\n\nconst {DATE_FORMAT_LONG} = DateUtils\n\n\nconst SnoozeOptions = [\n  [\n    'Later today',\n    'Tonight',\n    'Tomorrow',\n  ],\n  [\n    'This weekend',\n    'Next week',\n    'Next month',\n  ],\n]\n\nconst SnoozeDatesFactory = {\n  'Later today': DateUtils.laterToday,\n  'Tonight': DateUtils.tonight,\n  'Tomorrow': DateUtils.tomorrow,\n  'This weekend': DateUtils.thisWeekend,\n  'Next week': DateUtils.nextWeek,\n  'Next month': DateUtils.nextMonth,\n}\n\nconst SnoozeIconNames = {\n  'Later today': 'later',\n  'Tonight': 'tonight',\n  'Tomorrow': 'tomorrow',\n  'This weekend': 'weekend',\n  'Next week': 'week',\n  'Next month': 'month',\n}\n\n\nclass SnoozePopover extends Component {\n  static displayName = 'SnoozePopover';\n\n  static propTypes = {\n    threads: PropTypes.array.isRequired,\n    swipeCallback: PropTypes.func,\n  };\n\n  static defaultProps = {\n    swipeCallback: () => {},\n  };\n\n  constructor() {\n    super();\n    this.didSnooze = false;\n  }\n\n  componentWillUnmount() {\n    this.props.swipeCallback(this.didSnooze);\n  }\n\n  onSnooze(date, itemLabel) {\n    const utcDate = date.utc();\n    const formatted = DateUtils.format(utcDate);\n    SnoozeActions.snoozeThreads(this.props.threads, formatted, itemLabel);\n    this.didSnooze = true;\n    Actions.closePopover();\n\n    // if we're looking at a thread, go back to the main view.\n    // has no effect otherwise.\n    Actions.popSheet();\n  }\n\n  onSelectCustomDate = (date, inputValue) => {\n    if (date) {\n      this.onSnooze(date, \"Custom\");\n    } else {\n      NylasEnv.showErrorDialog(`Sorry, we can't parse ${inputValue} as a valid date.`);\n    }\n  };\n\n  renderItem = (itemLabel) => {\n    const date = SnoozeDatesFactory[itemLabel]();\n    const iconName = SnoozeIconNames[itemLabel];\n    const iconPath = `nylas://thread-snooze/assets/ic-snoozepopover-${iconName}@2x.png`;\n    return (\n      <div\n        key={itemLabel}\n        className=\"snooze-item\"\n        onClick={() => this.onSnooze(date, itemLabel)}\n      >\n        <RetinaImg\n          url={iconPath}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        {itemLabel}\n      </div>\n    )\n  };\n\n  renderRow = (options, idx) => {\n    const items = _.map(options, this.renderItem);\n    return (\n      <div key={`snooze-popover-row-${idx}`} className=\"snooze-row\">\n        {items}\n      </div>\n    );\n  };\n\n  render() {\n    const rows = SnoozeOptions.map(this.renderRow);\n\n    return (\n      <div className=\"snooze-popover\" tabIndex=\"-1\">\n        {rows}\n        <DateInput\n          className=\"snooze-input\"\n          dateFormat={DATE_FORMAT_LONG}\n          onDateSubmitted={this.onSelectCustomDate}\n        />\n      </div>\n    );\n  }\n\n}\n\nexport default SnoozePopover;\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/snooze-store.jsx",
    "content": "import _ from 'underscore';\nimport {FeatureUsageStore, Actions, AccountStore,\n  DatabaseStore, Message, CategoryStore} from 'nylas-exports';\nimport SnoozeUtils from './snooze-utils'\nimport {PLUGIN_ID, PLUGIN_NAME} from './snooze-constants';\nimport SnoozeActions from './snooze-actions';\n\nclass SnoozeStore {\n\n  constructor(pluginId = PLUGIN_ID, pluginName = PLUGIN_NAME) {\n    this.pluginId = pluginId\n    this.pluginName = pluginName\n    this.accountIds = _.pluck(AccountStore.accounts(), 'id')\n    this.snoozeCategoriesPromise = SnoozeUtils.getSnoozeCategoriesByAccount(AccountStore.accounts())\n  }\n\n  activate() {\n    this.unsubscribers = [\n      AccountStore.listen(this.onAccountsChanged),\n      SnoozeActions.snoozeThreads.listen(this.onSnoozeThreads),\n    ]\n  }\n\n  recordSnoozeEvent(threads, snoozeDate, label) {\n    try {\n      const timeInSec = Math.round(((new Date(snoozeDate)).valueOf() - Date.now()) / 1000);\n      Actions.recordUserEvent(\"Threads Snoozed\", {\n        timeInSec: timeInSec,\n        timeInLog10Sec: Math.log10(timeInSec),\n        label: label,\n        numItems: threads.length,\n      });\n    } catch (e) {\n      // Do nothing\n    }\n  }\n\n  groupUpdatedThreads = (threads, snoozeCategoriesByAccount) => {\n    const getSnoozeCategory = (accId) => snoozeCategoriesByAccount[accId]\n    const {getInboxCategory} = CategoryStore\n    const threadsByAccountId = {}\n\n    threads.forEach((thread) => {\n      const accId = thread.accountId\n      if (!threadsByAccountId[accId]) {\n        threadsByAccountId[accId] = {\n          threads: [thread],\n          snoozeCategoryId: getSnoozeCategory(accId).serverId,\n          returnCategoryId: getInboxCategory(accId).serverId,\n        }\n      } else {\n        threadsByAccountId[accId].threads.push(thread);\n      }\n    });\n    return Promise.resolve(threadsByAccountId);\n  };\n\n  onAccountsChanged = () => {\n    const nextIds = _.pluck(AccountStore.accounts(), 'id')\n    const isSameAccountIds = (\n      this.accountIds.length === nextIds.length &&\n      this.accountIds.length === _.intersection(this.accountIds, nextIds).length\n    )\n    if (!isSameAccountIds) {\n      this.accountIds = nextIds\n      this.snoozeCategoriesPromise = SnoozeUtils.getSnoozeCategoriesByAccount(AccountStore.accounts())\n    }\n  };\n\n  onSnoozeThreads = (threads, snoozeDate, label) => {\n    const lexicon = {\n      displayName: \"Snooze\",\n      usedUpHeader: \"All Snoozes used\",\n      iconUrl: \"nylas://thread-snooze/assets/ic-snooze-modal@2x.png\",\n    }\n\n    FeatureUsageStore.asyncUseFeature('snooze', {lexicon})\n    .then(() => {\n      this.recordSnoozeEvent(threads, snoozeDate, label)\n      return SnoozeUtils.moveThreadsToSnooze(threads, this.snoozeCategoriesPromise, snoozeDate)\n    })\n    .then((updatedThreads) => {\n      return this.snoozeCategoriesPromise\n      .then(snoozeCategories => this.groupUpdatedThreads(updatedThreads, snoozeCategories))\n    })\n    .then((updatedThreadsByAccountId) => {\n      _.each(updatedThreadsByAccountId, (update) => {\n        const {snoozeCategoryId, returnCategoryId} = update;\n\n        // Get messages for those threads and metadata for those.\n        DatabaseStore.findAll(Message, {threadId: update.threads.map(t => t.id)}).then((messages) => {\n          for (const message of messages) {\n            const header = message.messageIdHeader;\n            const stableId = message.id;\n            Actions.setMetadata(message, this.pluginId,\n              {expiration: snoozeDate, header, stableId, snoozeCategoryId, returnCategoryId})\n          }\n        });\n      });\n    })\n    .catch((error) => {\n      if (error instanceof FeatureUsageStore.NoProAccess) {\n        return\n      }\n      SnoozeUtils.moveThreadsFromSnooze(threads, this.snoozeCategoriesPromise)\n      Actions.closePopover();\n      NylasEnv.reportError(error);\n      NylasEnv.showErrorDialog(`Sorry, we were unable to save your snooze settings. ${error.message}`);\n      return\n    });\n  };\n\n  deactivate() {\n    this.unsubscribers.forEach(unsub => unsub())\n  }\n}\n\nexport default SnoozeStore;\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/lib/snooze-utils.es6",
    "content": "import moment from 'moment';\nimport _ from 'underscore';\nimport {\n  Actions,\n  Thread,\n  Category,\n  DateUtils,\n  TaskFactory,\n  AccountStore,\n  CategoryStore,\n  DatabaseStore,\n  SyncbackCategoryTask,\n  TaskQueueStatusStore,\n  FolderSyncProgressStore,\n} from 'nylas-exports';\nimport {SNOOZE_CATEGORY_NAME} from './snooze-constants'\n\nconst {DATE_FORMAT_SHORT} = DateUtils\n\n\nconst SnoozeUtils = {\n\n  snoozedUntilMessage(snoozeDate, now = moment()) {\n    let message = 'Snoozed'\n    if (snoozeDate) {\n      let dateFormat = DATE_FORMAT_SHORT\n      const date = moment(snoozeDate)\n      const hourDifference = moment.duration(date.diff(now)).asHours()\n\n      if (hourDifference < 24) {\n        dateFormat = dateFormat.replace('MMM D, ', '');\n      }\n      if (date.minutes() === 0) {\n        dateFormat = dateFormat.replace(':mm', '');\n      }\n\n      message += ` until ${DateUtils.format(date, dateFormat)}`;\n    }\n    return message;\n  },\n\n  createSnoozeCategory(accountId, name = SNOOZE_CATEGORY_NAME) {\n    const category = new Category({\n      displayName: name,\n      accountId: accountId,\n    })\n    const task = new SyncbackCategoryTask({category})\n\n    Actions.queueTask(task)\n    return TaskQueueStatusStore.waitForPerformRemote(task).then(() => {\n      return DatabaseStore.findBy(Category, {clientId: category.clientId})\n      .then((updatedCat) => {\n        if (updatedCat && updatedCat.isSavedRemotely()) {\n          return Promise.resolve(updatedCat)\n        }\n        return Promise.reject(new Error('Could not create Snooze category'))\n      })\n    })\n  },\n\n  getSnoozeCategory(accountId, categoryName = SNOOZE_CATEGORY_NAME) {\n    return FolderSyncProgressStore.whenCategoryListSynced(accountId)\n    .then(() => {\n      const allCategories = CategoryStore.categories(accountId)\n      const category = _.findWhere(allCategories, {displayName: categoryName})\n      if (category) {\n        return Promise.resolve(category);\n      }\n      return SnoozeUtils.createSnoozeCategory(accountId, categoryName)\n    })\n  },\n\n  getSnoozeCategoriesByAccount(accounts = AccountStore.accounts()) {\n    const snoozeCategoriesByAccountId = {}\n    accounts.forEach(({id}) => {\n      if (snoozeCategoriesByAccountId[id] != null) return;\n      snoozeCategoriesByAccountId[id] = SnoozeUtils.getSnoozeCategory(id)\n    })\n    return Promise.props(snoozeCategoriesByAccountId)\n  },\n\n  moveThreads(threads, {snooze, getSnoozeCategory, getInboxCategory, description} = {}) {\n    const tasks = TaskFactory.tasksForApplyingCategories({\n      source: \"Snooze Move\",\n      threads,\n      categoriesToRemove: snooze ? getInboxCategory : getSnoozeCategory,\n      categoriesToAdd: snooze ? getSnoozeCategory : getInboxCategory,\n      taskDescription: description,\n    })\n\n    Actions.queueTasks(tasks)\n    const promises = tasks.map(task => TaskQueueStatusStore.waitForPerformRemote(task))\n    // Resolve with the updated threads\n    return (\n      Promise.all(promises).then(() => {\n        return DatabaseStore.modelify(Thread, _.pluck(threads, 'clientId'))\n      })\n    )\n  },\n\n  moveThreadsToSnooze(threads, snoozeCategoriesByAccountPromise, snoozeDate) {\n    return snoozeCategoriesByAccountPromise\n    .then((snoozeCategoriesByAccountId) => {\n      const getSnoozeCategory = (accId) => [snoozeCategoriesByAccountId[accId]]\n      const getInboxCategory = (accId) => [CategoryStore.getInboxCategory(accId)]\n      const description = SnoozeUtils.snoozedUntilMessage(snoozeDate)\n      return SnoozeUtils.moveThreads(\n        threads,\n        {snooze: true, getSnoozeCategory, getInboxCategory, description}\n      )\n    })\n  },\n\n  moveThreadsFromSnooze(threads, snoozeCategoriesByAccountPromise) {\n    return snoozeCategoriesByAccountPromise\n    .then((snoozeCategoriesByAccountId) => {\n      const getSnoozeCategory = (accId) => [snoozeCategoriesByAccountId[accId]]\n      const getInboxCategory = (accId) => [CategoryStore.getInboxCategory(accId)]\n      const description = 'Unsnoozed';\n      return SnoozeUtils.moveThreads(\n        threads,\n        {snooze: false, getSnoozeCategory, getInboxCategory, description}\n      )\n    })\n  },\n}\n\nexport default SnoozeUtils\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/package.json",
    "content": "{\n  \"name\": \"thread-snooze\",\n  \"version\": \"1.0.0\",\n  \"title\": \"Thread Snooze\",\n  \"description\": \"Snooze mail!\",\n  \"main\": \"lib/main\",\n  \"scripts\": {\n    \"test\": \"echo \\\"Error: no test specified\\\" && exit 1\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"github.com/nylas/nylas-mail\"\n  },\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"license\": \"GPL-3.0\"\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/spec/snooze-store-spec.es6",
    "content": "import {\n  AccountStore,\n  CategoryStore,\n  NylasAPIHelpers,\n  Thread,\n  Actions,\n  Category,\n} from 'nylas-exports'\nimport SnoozeUtils from '../lib/snooze-utils'\nimport SnoozeStore from '../lib/snooze-store'\n\n\nxdescribe('SnoozeStore', function snoozeStore() {\n  beforeEach(() => {\n    this.store = new SnoozeStore('plug-id', 'plug-name')\n    this.name = 'Snooze folder'\n    this.accounts = [{id: 123}, {id: 321}]\n\n    this.snoozeCatsByAccount = {\n      123: new Category({accountId: 123, displayName: this.name, serverId: 'sn-1'}),\n      321: new Category({accountId: 321, displayName: this.name, serverId: 'sn-2'}),\n    }\n    this.inboxCatsByAccount = {\n      123: new Category({accountId: 123, name: 'inbox', serverId: 'in-1'}),\n      321: new Category({accountId: 321, name: 'inbox', serverId: 'in-2'}),\n    }\n    this.threads = [\n      new Thread({accountId: 123, serverId: 's-1'}),\n      new Thread({accountId: 123, serverId: 's-2'}),\n      new Thread({accountId: 321, serverId: 's-3'}),\n    ]\n    this.updatedThreadsByAccountId = {\n      123: {\n        threads: [this.threads[0], this.threads[1]],\n        snoozeCategoryId: 'sn-1',\n        returnCategoryId: 'in-1',\n      },\n      321: {\n        threads: [this.threads[2]],\n        snoozeCategoryId: 'sn-2',\n        returnCategoryId: 'in-2',\n      },\n    }\n    this.store.snoozeCategoriesPromise = Promise.resolve()\n    spyOn(this.store, 'recordSnoozeEvent')\n    spyOn(this.store, 'groupUpdatedThreads').andReturn(Promise.resolve(this.updatedThreadsByAccountId))\n\n    spyOn(AccountStore, 'accountsForItems').andReturn(this.accounts)\n    spyOn(NylasAPIHelpers, 'authPlugin').andReturn(Promise.resolve())\n    spyOn(SnoozeUtils, 'moveThreadsToSnooze').andReturn(Promise.resolve(this.threads))\n    spyOn(SnoozeUtils, 'moveThreadsFromSnooze')\n    spyOn(Actions, 'setMetadata')\n    spyOn(Actions, 'closePopover')\n    spyOn(NylasEnv, 'reportError')\n    spyOn(NylasEnv, 'showErrorDialog')\n  })\n\n  describe('groupUpdatedThreads', () => {\n    it('groups the threads correctly by account id, with their snooze and inbox categories', () => {\n      spyOn(CategoryStore, 'getInboxCategory').andCallFake(accId => this.inboxCatsByAccount[accId])\n\n      waitsForPromise(() => {\n        return this.store.groupUpdatedThreads(this.threads, this.snoozeCatsByAccount)\n        .then((result) => {\n          expect(result['123']).toEqual({\n            threads: [this.threads[0], this.threads[1]],\n            snoozeCategoryId: 'sn-1',\n            returnCategoryId: 'in-1',\n          })\n          expect(result['321']).toEqual({\n            threads: [this.threads[2]],\n            snoozeCategoryId: 'sn-2',\n            returnCategoryId: 'in-2',\n          })\n        })\n      })\n    });\n  });\n\n  describe('onAccountsChanged', () => {\n    it('updates categories promise if an account has been added', () => {\n      const nextAccounts = [\n        {id: 'ac1'},\n        {id: 'ac2'},\n        {id: 'ac3'},\n      ]\n      this.store.accountIds = ['ac1', 'ac2']\n      spyOn(SnoozeUtils, 'getSnoozeCategoriesByAccount')\n      spyOn(AccountStore, 'accounts').andReturn(nextAccounts)\n      this.store.onAccountsChanged()\n      expect(SnoozeUtils.getSnoozeCategoriesByAccount).toHaveBeenCalledWith(nextAccounts)\n    });\n\n    it('updates categories promise if an account has been removed', () => {\n      const nextAccounts = [\n        {id: 'ac1'},\n        {id: 'ac3'},\n      ]\n      this.store.accountIds = ['ac1', 'ac2', 'ac3']\n      spyOn(SnoozeUtils, 'getSnoozeCategoriesByAccount')\n      spyOn(AccountStore, 'accounts').andReturn(nextAccounts)\n      this.store.onAccountsChanged()\n      expect(SnoozeUtils.getSnoozeCategoriesByAccount).toHaveBeenCalledWith(nextAccounts)\n    });\n\n    it('updates categories promise if an account is added and another removed', () => {\n      const nextAccounts = [\n        {id: 'ac1'},\n        {id: 'ac3'},\n      ]\n      this.store.accountIds = ['ac1', 'ac2']\n      spyOn(SnoozeUtils, 'getSnoozeCategoriesByAccount')\n      spyOn(AccountStore, 'accounts').andReturn(nextAccounts)\n      this.store.onAccountsChanged()\n      expect(SnoozeUtils.getSnoozeCategoriesByAccount).toHaveBeenCalledWith(nextAccounts)\n    });\n\n    it('does not update categories promise if accounts have not changed', () => {\n      const nextAccounts = [\n        {id: 'ac1'},\n        {id: 'ac2'},\n      ]\n      this.store.accountIds = ['ac1', 'ac2']\n      spyOn(SnoozeUtils, 'getSnoozeCategoriesByAccount')\n      spyOn(AccountStore, 'accounts').andReturn(nextAccounts)\n      this.store.onAccountsChanged()\n      expect(SnoozeUtils.getSnoozeCategoriesByAccount).not.toHaveBeenCalled()\n    });\n  });\n\n  describe('onSnoozeThreads', () => {\n    it('auths plugin against all present accounts', () => {\n      waitsForPromise(() => {\n        return this.store.onSnoozeThreads(this.threads, 'date', 'label')\n        .then(() => {\n          expect(NylasAPIHelpers.authPlugin).toHaveBeenCalled()\n          expect(NylasAPIHelpers.authPlugin.calls[0].args[2]).toEqual(this.accounts[0])\n          expect(NylasAPIHelpers.authPlugin.calls[1].args[2]).toEqual(this.accounts[1])\n        })\n      })\n    });\n\n    it('calls Actions.setMetadata with the correct metadata', () => {\n      waitsForPromise(() => {\n        return this.store.onSnoozeThreads(this.threads, 'date', 'label')\n        .then(() => {\n          expect(Actions.setMetadata).toHaveBeenCalled()\n          expect(Actions.setMetadata.calls[0].args).toEqual([\n            this.updatedThreadsByAccountId['123'].threads,\n            'plug-id',\n            {\n              snoozeDate: 'date',\n              snoozeCategoryId: 'sn-1',\n              returnCategoryId: 'in-1',\n            },\n          ])\n          expect(Actions.setMetadata.calls[1].args).toEqual([\n            this.updatedThreadsByAccountId['321'].threads,\n            'plug-id',\n            {\n              snoozeDate: 'date',\n              snoozeCategoryId: 'sn-2',\n              returnCategoryId: 'in-2',\n            },\n          ])\n        })\n      })\n    });\n\n    it('displays dialog on error', () => {\n      jasmine.unspy(SnoozeUtils, 'moveThreadsToSnooze')\n      spyOn(SnoozeUtils, 'moveThreadsToSnooze').andReturn(Promise.reject(new Error('Oh no!')))\n\n      waitsForPromise(() => {\n        return this.store.onSnoozeThreads(this.threads, 'date', 'label')\n        .finally(() => {\n          expect(SnoozeUtils.moveThreadsFromSnooze).toHaveBeenCalled()\n          expect(NylasEnv.reportError).toHaveBeenCalled()\n          expect(NylasEnv.showErrorDialog).toHaveBeenCalled()\n        })\n      })\n    });\n  });\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/spec/snooze-utils-spec.es6",
    "content": "import moment from 'moment'\nimport {\n  Actions,\n  TaskQueueStatusStore,\n  TaskFactory,\n  DatabaseStore,\n  Category,\n  Thread,\n  CategoryStore,\n  FolderSyncProgressStore,\n} from 'nylas-exports'\nimport SnoozeUtils from '../lib/snooze-utils'\n\nxdescribe('Snooze Utils', function snoozeUtils() {\n  beforeEach(() => {\n    this.name = 'Snoozed Folder'\n    this.accId = 123\n    spyOn(FolderSyncProgressStore, 'whenCategoryListSynced').andReturn(Promise.resolve())\n  })\n\n  describe('snoozedUntilMessage', () => {\n    it('returns correct message if no snooze date provided', () => {\n      expect(SnoozeUtils.snoozedUntilMessage()).toEqual('Snoozed')\n    });\n\n    describe('when less than 24 hours from now', () => {\n      it('returns correct message if snoozeDate is on the hour of the clock', () => {\n        const now9AM = window.testNowMoment().hour(9).minute(0)\n        const tomorrowAt8 = moment(now9AM).add(1, 'day').hour(8)\n        const result = SnoozeUtils.snoozedUntilMessage(tomorrowAt8, now9AM)\n        expect(result).toEqual('Snoozed until 8 AM')\n      });\n\n      it('returns correct message if snoozeDate otherwise', () => {\n        const now9AM = window.testNowMoment().hour(9).minute(0)\n        const snooze10AM = moment(now9AM).hour(10).minute(5)\n        const result = SnoozeUtils.snoozedUntilMessage(snooze10AM, now9AM)\n        expect(result).toEqual('Snoozed until 10:05 AM')\n      });\n    });\n\n    describe('when more than 24 hourse from now', () => {\n      it('returns correct message if snoozeDate is on the hour of the clock', () => {\n        // Jan 1\n        const now9AM = window.testNowMoment().month(0).date(1).hour(9).minute(0)\n        const tomorrowAt10 = moment(now9AM).add(1, 'day').hour(10)\n        const result = SnoozeUtils.snoozedUntilMessage(tomorrowAt10, now9AM)\n        expect(result).toEqual('Snoozed until Jan 2, 10 AM')\n      });\n\n      it('returns correct message if snoozeDate otherwise', () => {\n        // Jan 1\n        const now9AM = window.testNowMoment().month(0).date(1).hour(9).minute(0)\n        const tomorrowAt930 = moment(now9AM).add(1, 'day').minute(30)\n        const result = SnoozeUtils.snoozedUntilMessage(tomorrowAt930, now9AM)\n        expect(result).toEqual('Snoozed until Jan 2, 9:30 AM')\n      });\n    });\n  });\n\n  describe('createSnoozeCategory', () => {\n    beforeEach(() => {\n      this.category = new Category({\n        displayName: this.name,\n        accountId: this.accId,\n        clientId: 321,\n        serverId: 321,\n      })\n      spyOn(Actions, 'queueTask')\n      spyOn(TaskQueueStatusStore, 'waitForPerformRemote').andReturn(Promise.resolve())\n      spyOn(DatabaseStore, 'findBy').andReturn(Promise.resolve(this.category))\n    })\n\n    it('creates category with correct snooze name', () => {\n      SnoozeUtils.createSnoozeCategory(this.accId, this.name)\n      expect(Actions.queueTask).toHaveBeenCalled()\n      const task = Actions.queueTask.calls[0].args[0]\n      expect(task.category.displayName).toEqual(this.name)\n      expect(task.category.accountId).toEqual(this.accId)\n    });\n\n    it('resolves with the updated category that has been saved to the server', () => {\n      waitsForPromise(() => {\n        return SnoozeUtils.createSnoozeCategory(this.accId, this.name).then((result) => {\n          expect(DatabaseStore.findBy).toHaveBeenCalled()\n          expect(result).toBe(this.category)\n        })\n      })\n    });\n\n    it('rejects if the category could not be found in the database', () => {\n      this.category.serverId = null\n      jasmine.unspy(DatabaseStore, 'findBy')\n      spyOn(DatabaseStore, 'findBy').andReturn(Promise.resolve(this.category))\n      waitsForPromise(() => {\n        return SnoozeUtils.createSnoozeCategory(this.accId, this.name)\n        .then(() => {\n          throw new Error('SnoozeUtils.createSnoozeCategory should not resolve in this case!')\n        })\n        .catch((error) => {\n          expect(DatabaseStore.findBy).toHaveBeenCalled()\n          expect(error.message).toEqual('Could not create Snooze category')\n        })\n      })\n    });\n\n    it('rejects if the category could not be saved to the server', () => {\n      jasmine.unspy(DatabaseStore, 'findBy')\n      spyOn(DatabaseStore, 'findBy').andReturn(Promise.resolve(undefined))\n      waitsForPromise(() => {\n        return SnoozeUtils.createSnoozeCategory(this.accId, this.name)\n        .then(() => {\n          throw new Error('SnoozeUtils.createSnoozeCategory should not resolve in this case!')\n        })\n        .catch((error) => {\n          expect(DatabaseStore.findBy).toHaveBeenCalled()\n          expect(error.message).toEqual('Could not create Snooze category')\n        })\n      })\n    });\n  });\n\n  describe('getSnoozeCategory', () => {\n    it('resolves category if it exists in the category store', () => {\n      const categories = [\n        new Category({accountId: this.accId, name: 'inbox'}),\n        new Category({accountId: this.accId, displayName: this.name}),\n      ]\n      spyOn(CategoryStore, 'categories').andReturn(categories)\n      spyOn(SnoozeUtils, 'createSnoozeCategory')\n\n      waitsForPromise(() => {\n        return SnoozeUtils.getSnoozeCategory(this.accountId, this.name)\n        .then((result) => {\n          expect(SnoozeUtils.createSnoozeCategory).not.toHaveBeenCalled()\n          expect(result).toBe(categories[1])\n        })\n      })\n    });\n\n    it('creates category if it does not exist', () => {\n      const categories = [\n        new Category({accountId: this.accId, name: 'inbox'}),\n      ]\n      const snoozeCat = new Category({accountId: this.accId, displayName: this.name})\n      spyOn(CategoryStore, 'categories').andReturn(categories)\n      spyOn(SnoozeUtils, 'createSnoozeCategory').andReturn(Promise.resolve(snoozeCat))\n\n      waitsForPromise(() => {\n        return SnoozeUtils.getSnoozeCategory(this.accId, this.name)\n        .then((result) => {\n          expect(SnoozeUtils.createSnoozeCategory).toHaveBeenCalledWith(this.accId, this.name)\n          expect(result).toBe(snoozeCat)\n        })\n      })\n    });\n  });\n\n  describe('moveThreads', () => {\n    beforeEach(() => {\n      this.description = 'Snoozin';\n      this.snoozeCatsByAccount = {\n        123: new Category({accountId: 123, displayName: this.name, serverId: 'sr-1'}),\n        321: new Category({accountId: 321, displayName: this.name, serverId: 'sr-2'}),\n      }\n      this.inboxCatsByAccount = {\n        123: new Category({accountId: 123, name: 'inbox', serverId: 'sr-3'}),\n        321: new Category({accountId: 321, name: 'inbox', serverId: 'sr-4'}),\n      }\n      this.threads = [\n        new Thread({accountId: 123}),\n        new Thread({accountId: 123}),\n        new Thread({accountId: 321}),\n      ]\n      this.getInboxCat = (accId) => [this.inboxCatsByAccount[accId]]\n      this.getSnoozeCat = (accId) => [this.snoozeCatsByAccount[accId]]\n\n      spyOn(DatabaseStore, 'modelify').andReturn(Promise.resolve(this.threads))\n      spyOn(TaskFactory, 'tasksForApplyingCategories').andReturn([])\n      spyOn(TaskQueueStatusStore, 'waitForPerformRemote').andReturn(Promise.resolve())\n      spyOn(Actions, 'queueTasks')\n    })\n\n    it('creates the tasks to move threads correctly when snoozing', () => {\n      const snooze = true\n      const description = this.description\n\n      waitsForPromise(() => {\n        return SnoozeUtils.moveThreads(this.threads, {snooze, description, getInboxCategory: this.getInboxCat, getSnoozeCategory: this.getSnoozeCat})\n        .then(() => {\n          expect(TaskFactory.tasksForApplyingCategories).toHaveBeenCalled()\n          expect(Actions.queueTasks).toHaveBeenCalled()\n          const {threads, categoriesToAdd, categoriesToRemove, taskDescription} = TaskFactory.tasksForApplyingCategories.calls[0].args[0]\n          expect(threads).toBe(this.threads)\n          expect(categoriesToRemove('123')[0]).toBe(this.inboxCatsByAccount['123'])\n          expect(categoriesToRemove('321')[0]).toBe(this.inboxCatsByAccount['321'])\n          expect(categoriesToAdd('123')[0]).toBe(this.snoozeCatsByAccount['123'])\n          expect(categoriesToAdd('321')[0]).toBe(this.snoozeCatsByAccount['321'])\n          expect(taskDescription).toEqual(description)\n        })\n      })\n    });\n\n    it('creates the tasks to move threads correctly when unsnoozing', () => {\n      const snooze = false\n      const description = this.description\n\n      waitsForPromise(() => {\n        return SnoozeUtils.moveThreads(this.threads, {snooze, description, getInboxCategory: this.getInboxCat, getSnoozeCategory: this.getSnoozeCat})\n        .then(() => {\n          expect(TaskFactory.tasksForApplyingCategories).toHaveBeenCalled()\n          expect(Actions.queueTasks).toHaveBeenCalled()\n          const {threads, categoriesToAdd, categoriesToRemove, taskDescription} = TaskFactory.tasksForApplyingCategories.calls[0].args[0]\n          expect(threads).toBe(this.threads)\n          expect(categoriesToAdd('123')[0]).toBe(this.inboxCatsByAccount['123'])\n          expect(categoriesToAdd('321')[0]).toBe(this.inboxCatsByAccount['321'])\n          expect(categoriesToRemove('123')[0]).toBe(this.snoozeCatsByAccount['123'])\n          expect(categoriesToRemove('321')[0]).toBe(this.snoozeCatsByAccount['321'])\n          expect(taskDescription).toEqual(description)\n        })\n      })\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/stylesheets/snooze-feature-used-modal.less",
    "content": "@import \"ui-variables\";\n\n.feature-usage-modal.snooze {\n  @snooze-color: #8e6ce3;\n  .feature-header {\n    @from: @snooze-color;\n    @to: lighten(@snooze-color, 10%);\n    background: linear-gradient(to top, @from, @to);\n  }\n  .feature-name {\n    color: @snooze-color;\n  }\n  .pro-description {\n    li {\n      &:before {\n        color: @snooze-color;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/stylesheets/snooze-mail-label.less",
    "content": "@snooze-color: #472B82;\n\n.snooze-mail-label {\n  display: flex;\n  align-items: center;\n\n  img {\n    background-color: @snooze-color;\n    margin-right: 5px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/thread-snooze/stylesheets/snooze-popover.less",
    "content": "@import \"ui-variables\";\n\n@snooze-quickaction-img: \"../static/images/thread-list-quick-actions/ic-quickaction-snooze@2x.png\";\n\n.thread-list .list-item .list-column-HoverActions .action.action-snooze {\n  background: url(@snooze-quickaction-img) center no-repeat, @background-gradient;\n}\n\n.snooze-button {\n  order: -103;\n}\n\n.snooze-popover {\n  color: fadeout(@btn-default-text-color, 20%);\n  display: flex;\n  flex-direction: column;\n\n  .snooze-row {\n    display: flex;\n\n    .snooze-item {\n      display: flex;\n      flex-direction: column;\n      align-items: center;\n\n      padding: 15px 0;\n      cursor: default;\n      width: 105px;\n      line-height: initial;\n      text-align: initial;\n\n      img { background-color: fadeout(@btn-default-text-color, 20%); }\n      &:hover {\n        background-color: darken(@btn-default-bg-color, 5%);\n        color: fadeout(@btn-default-text-color, 10%);\n        img { background-color: fadeout(@btn-default-text-color, 10%); }\n      }\n      &:active {\n        background-color: darken(@btn-default-bg-color, 8%);\n        color: fadeout(@btn-default-text-color, 0%);\n        img { background-color: fadeout(@btn-default-text-color, 0%); }\n      }\n      &+.snooze-item {\n        border-left: 1px solid @border-color-divider;\n      }\n    }\n    &+.snooze-row {\n      border-top: 1px solid @border-color-divider;\n    }\n  }\n\n  .snooze-input {\n    border-top: 1px solid @border-color-divider;\n    padding: @padding-large-vertical @padding-large-horizontal;\n\n    input {\n      margin-bottom: 3px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-dark/LICENSE.md",
    "content": "Copyright (c) 2014 GitHub Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining\na copy of this software and associated documentation files (the\n\"Software\"), to deal in the Software without restriction, including\nwithout limitation the rights to use, copy, modify, merge, publish,\ndistribute, sublicense, and/or sell copies of the Software, and to\npermit persons to whom the Software is furnished to do so, subject to\nthe following conditions:\n\nThe above copyright notice and this permission notice shall be\nincluded in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\nNONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE\nLIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION\nOF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION\nWITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-dark/README.md",
    "content": "# N1 Dark UI theme\n\nDefault dark UI theme for N1.\n\nThis theme is installed by default with N1 and can be activated by going to\nthe _Themes_ section in the Settings view (`cmd-,`) and selecting it from the\n_UI Themes_ drop-down menu.\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-dark/package.json",
    "content": "{\n  \"name\": \"ui-dark\",\n  \"displayName\": \"Dark\",\n  \"theme\": \"ui\",\n  \"version\": \"0.1.0\",\n  \"description\": \"The Dark N1 Client Theme\",\n  \"license\": \"GPL-3.0\",\n  \"styleSheets\": [\"email-frame\"],\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-dark/styles/email-frame.less",
    "content": ".ignore-in-parent-frame {\n  body {\n    -webkit-filter: invert() hue-rotate(180deg);\n    color: #111;\n  }\n  img {\n    -webkit-filter: invert() hue-rotate(180deg);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-dark/styles/ui-variables.less",
    "content": "@gray-base:    #ffffff;\n@gray-darker:  darken(@gray-base, 13.5%);\n@gray-dark:    darken(@gray-base, 20%);\n@gray:         darken(@gray-base, 33.5%);\n@gray-light:   darken(@gray-base, 46.7%);\n@gray-lighter: darken(@gray-base, 92.5%);\n@white:        #0a0b0c;\n\n@accent-primary:            #5e6a77;\n@accent-primary-dark:       #5e6a77;\n\n@background-primary:     #333539;\n@background-off-primary: #282828;\n@background-secondary:   #333539;\n@background-tertiary:    #333539;\n\n@border-color-primary:   darken(@background-primary, 1%);\n@border-color-secondary: darken(@background-secondary, 1%);\n@border-color-tertiary:  darken(@background-tertiary, 1%);\n@border-color-divider:   @border-color-secondary;\n\n@text-color:                      #c0c6cb;\n@text-color-subtle:               fadeout(@text-color, 20%);\n@text-color-very-subtle:          fadeout(@text-color, 40%);\n@text-color-inverse:              white;\n@text-color-inverse-subtle:       fadeout(@text-color-inverse, 20%);\n@text-color-inverse-very-subtle:  fadeout(@text-color-inverse, 50%);\n@text-color-heading:              #FFF;\n\n@btn-default-bg-color:    lighten(@background-primary, 5%);\n@dropdown-default-bg-color: #404040;\n\n@input-bg:              #242424;\n@input-border:          @border-color-divider;\n\n@list-bg:               #333;\n@list-border:           #383838;\n@list-selected-color:   @text-color-inverse;\n@list-focused-color: @text-color;\n\n@toolbar-background-color: @background-secondary;\n@panel-background-color: #282b30;\n\n.sheet-toolbar {\n  border-bottom: none;\n  box-shadow: 0 0 0.5px @border-color-primary, 0 1px 1.5px @border-color-primary, 0 0 3px @border-color-primary;\n}\n\n.thread-icon:not(.thread-icon-unread):not(.thread-icon-star) {\n  -webkit-filter: invert(100%);\n}\nimg.content-dark {\n  -webkit-filter: invert(100%);\n}\nimg.content-light {\n  -webkit-filter: invert(100%);\n}\n\n.popover {\n  border: 1px solid @border-color-primary;\n}\n\n.mail-label {\n  -webkit-filter: contrast(110%) brightness(85%);\n}"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Jamie Wilson\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/README.md",
    "content": "# Darkside\n**An dark sidebar theme for [Nylas Mail](https://nylas.com/n1). Created by [Jamie Wilson](http://jamiewilson.io)**\n\n## Activation\nDarkside comes [pre-installed](https://github.com/nylas/nylas-mail/tree/master/internal_packages/ui-darkside) with N1. To change themes, go to `Nylas Mail > Change Theme…` in the menu bar, then select `Darkside`. Learn more at [support.nylas.com](https://support.nylas.com/hc/en-us/articles/217557858-How-do-I-change-my-theme-).\n\n## Customization\nIn order to customize Darkside, you'll need to manually install it.\n\n#### 1. Download the `ui-darkside` folder.\n\n> **Download Option 1:**  \n> [Download just the 'ui-darkside' folder](https://kinolien.github.io/gitzip/?download=https://github.com/nylas/nylas-mail/tree/master/internal_packages/ui-darkside) thanks to the service [gitzip by @kinolien](https://kinolien.github.io/gitzip/).\n  \n\n> **Download Option 2:**  \n> [Download the entire N1 repo](https://github.com/nylas/nylas-mail/archive/master.zip) or `git clone https://github.com/nylas/nylas-mail.git`. Then grab the folder from `N1/internal_packages/ui-darkside`.\n  \n#### 2. Manual Install\n\n> To manually install a theme, go to `Nylas Mail > Install Theme…` in the menu bar. Select the `ui-darkside` folder you just downloaded. This will copy the folder into your N1 packages directory so you can delete the orginal download if you want to. \n\n#### 3. Customize\n\n> **Open the theme directory**  \n> If you're on a Mac, you can find the theme files at `~/.nylas-mail/packages`. To get there quickly, use the key command <kbd>Cmd</kbd> + <kbd>Shift</kbd> + <kbd>G</kbd> and enter `~/.nylas-mail/packages`.\n\n> **Change package.json**  \n> In order to avoid conflicts between your custom theme and the pre-installed version, change `name` and `displayName` in `package.json` to:\n\n    \"name\": \"ui-darkside-custom\",\n    \"displayName\": \"Darkside Custom\",\n\n> **Edit LESS files**  \n> Open the `darkside-variables.less` file. To change colors, just comment out the default `@sidebar` and `@accent` variables and uncomment another theme or simply replace with your own colors.\n\n```sass\n// Default\n@sidebar: #313042;\n@accent: #F18260;\n\n// Luna\n// @sidebar: #202C46;\n// @accent: #39DFF8;\n\n// Zond\n// @sidebar: #333333;\n// @accent: #F6D49C;\n\n// Gemini\n// @sidebar: #00203C;\n// @accent: #F6B312;\n\n// Mercury\n// @sidebar: #555;\n// @accent: #999;\n\n// Apollo\n// @sidebar: #3A1E15;\n// @accent: #F6AA1C;\n```\n\n### Feedback\nIf you have questions or suggestions, please submit an issue. If you need to, you can email me at [jamie@jamiewilson.io](mailto:jamie@jamiewilson?subject=Re: Darkside).\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/index.less",
    "content": "@import \"styles/darkside-variables\";\n@import \"styles/darkside-sidebar\";\n@import \"styles/darkside-toolbars\";\n@import \"styles/darkside-window-controls\";\n@import \"styles/darkside-threadlist\";\n@import \"styles/darkside-inputs\";\n@import \"styles/darkside-thread-icons\";\n@import \"styles/darkside-swiping\";\n@import \"styles/darkside-labels\";\n@import \"styles/darkside-message-list\";\n@import \"styles/darkside-composer\";\n@import \"styles/darkside-preferences\";\n@import \"styles/darkside-notifications\";\n@import \"styles/darkside-drafts\";\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/package.json",
    "content": "{\n  \"name\": \"ui-darkside\",\n  \"displayName\": \"Darkside\",\n  \"theme\": \"ui\",\n  \"version\": \"1.0.0\",\n  \"description\": \"A customizable, dark sidebar theme for Nylas Mail.\",\n  \"license\": \"MIT\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-composer.less",
    "content": "@import \"darkside-variables\";\n\n.tokenizing-field .token.invalid,\n.tokenizing-field .token.invalid:hover,\n.tokenizing-field .token.invalid.selected,\n.tokenizing-field .token.invalid.dragging {\n  color: @sidebar;\n  background: none;\n  border: none;\n  box-shadow: inset 0 0 0 1px @invalid;\n}\n\n// Darken composer action bar to contrast from background\n.composer-inner-wrap .composer-action-bar-wrap,\n.composer-full-window .composer-inner-wrap .composer-action-bar-wrap {\n  background: darken(@messagelist-bg, 1%);\n  box-shadow: none;\n  border-radius: 0;\n  border-bottom-left-radius: 6px;\n  border-bottom-right-radius: 6px;\n}\n\n// Replacing focused state with theme accent\n#message-list .message-item-wrap .message-item-white-wrap.composer-outer-wrap {\n  background: white;\n  &.focused {\n    box-shadow: 0 0 0 1px @accent;\n  }\n}\n\n.message-item-white-wrap.composer-outer-wrap .composer-participant-field .dropdown-component .signature-button-dropdown .only-item {\n  background: white;\n}\n\n// make action bar at bottom of composer a bit darker than background\n#message-list .message-item-wrap .message-item-white-wrap.composer-outer-wrap {\n  & .composer-action-bar-wrap { background: transparent; }\n  &.focused .composer-action-bar-wrap { background: darken(@messagelist-bg, 1%); }\n}\n\n.composer-inner-wrap .composer-action-bar-wrap .composer-action-bar-content {\n  padding: 20px;\n  max-width: 100%;\n}\n\n// ============================\n//  Attachements\n// ============================\n\n.file-wrap.file-image-wrap .file-preview .file-name-container {\n  background: fade(@sidebar, 20%);\n  min-height: 0;\n  & .file-name {\n    left: 0;\n    right: 0;\n    bottom: 0;\n    color: white;\n    background: fade(@sidebar, 80%);\n    padding: 5px 15px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-drafts.less",
    "content": "@import \"darkside-variables\";\n\n// Make corresponding toolbar match threadlist background\n.draft-list,\n.toolbar-DraftList {\n  background: @messagelist-bg;\n}\n\n.draft-list .list-container .list-item.selected,\n.draft-list .list-tabular .list-tabular-item.keyboard-cursor {\n  background: white;\n}\n\n.draft-list .list-tabular .list-tabular-item .checkmark .inner {\n  background-color: white;\n  border-color: tint(@sidebar, 75%);\n}\n\n.list-tabular .list-tabular-item.selected .checkmark .inner {\n  background-color: @accent;\n  background-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"8\" height=\"6\" viewBox=\"0 0 8 6\"><path fill=\"#FFF\" d=\"M7 0h1v1L3 6 0 3V2h1l2 2z\"/></svg>');\n  background-size: 8px 6px;\n}\n\n// Make draft-list items slightly darker on hover\n// Using !important so multiple selection actions\n.draft-list .list-tabular .list-tabular-item:hover {\n  background: tint(@sidebar, 90%) !important;\n}\n\n// Center vertically regardless of list item height\n.draft-list .sending-progress {\n  align-self: center;\n  background-color: #f5f5f5;\n  border: none;\n  margin-top: 0;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-inputs.less",
    "content": "@import \"darkside-variables\";\n\ntextarea:focus,\ninput[type=\"text\"]:focus,\ninput[type=\"email\"]:focus,\n.search-bar .menu .header-container input:focus {\n  border-color: @accent;\n  box-shadow: 0 0 1.5px @accent;\n}\n\n.search-bar {\n  margin: 7.5px;\n  width: 100%;\n}\n\n.search-container .content-container {\n  margin-top: 5px !important;\n}\n\n.menu .item.selected, .menu .item:active,\n.search-container .content-container .item.selected {\n  background: @accent;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-labels.less",
    "content": "@import \"darkside-variables\";\n\n// Make labels white on accent color when message is selected\n.thread-list .focused .mail-label, .draft-list .focused .mail-label,\n.thread-list.handler-split .list-item.selected .mail-label {\n  background: @accent !important;\n  color: white !important;\n  box-shadow: none !important;\n  -webkit-filter: none !important;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-message-list.less",
    "content": "@import \"darkside-variables\";\n\n#message-list {\n  background: @messagelist-bg;\n}\n\n// Make toolbars match panels\n.column-MessageList,\n.toolbar-MessageList,\n.column-MessageListSidebar,\n.toolbar-MessageListSidebar {\n  height: 100%;\n  background: @messagelist-bg;\n  border-left: 1px solid @border-color;\n}\n\n// Message List top and bottom spacing\n#message-list .messages-wrap .scroll-region-content-inner {\n  padding: 20px;\n  padding-bottom: 40vh;\n}\n\n// Reset padding\n#message-list .message-header,\n#message-list .message-item-wrap.collapsed .message-item-white-wrap,\n#message-list .message-item-wrap.collapsed .message-item-area {\n  padding: 0;\n}\n\n// Make padding uniform\n#message-list .message-item-area,\n#message-list .footer-reply-area-wrap .footer-reply-area {\n  padding: 20px !important;\n}\n\n#message-list .message-item-wrap.collapsed .message-item-area .collapsed-attachment {\n  padding: 10px;\n}\n\n// Adjusting position of thread participants toggle\n#message-list .header-toggle-control {\n  top: 6px !important;\n  left: -11px !important;\n  display: flex !important;\n  justify-content: center;\n  align-items: center;\n}\n\n// Reducing size and overriding invalid -webkit-mask-repeat- property\n#message-list .header-toggle-control img {\n  zoom: 0.35 !important;\n  -webkit-mask-repeat: no-repeat !important;\n}\n\n.message-participants.to-participants .collapsed-participants,\n.message-participants .expanded-participants .participant-type {\n  margin-top: 0;\n}\n\n.message-participants .from-label,\n.message-participants .to-label,\n.message-participants .cc-label,\n.message-participants .bcc-label {\n  margin-right: 6px;\n}\n\n#message-list .message-item-wrap .message-item-white-wrap,\n#message-list .minified-bundle .msg-line,\n#message-list .minified-bundle .num-messages,\n#message-list .footer-reply-area-wrap, {\n  box-shadow: inset 0 0 0 1px @border-color;\n  border: none;\n}\n\n#message-list .minified-bundle + .message-item-wrap,\n#message-list .message-item-wrap.collapsed + .minified-bundle,\n#message-list .minified-bundle .num-messages,\n#message-list .minified-bundle .msg-lines {\n  margin-top: 0;\n}\n\n// Collapsed Messages Pill Label\n#message-list .minified-bundle .num-messages {\n  padding: 3px;\n}\n\n// remove margin for last message before reply\n#message-list .message-item-wrap.before-reply-area {\n  margin-bottom: 0;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-notifications.less",
    "content": "@import \"darkside-variables\";\n\n.notifications {\n  background-color: @sidebar;\n}\n\n.notifications .notification{\n  background-color: @accent;\n  border: none;\n}\n\n.sidebar-activity {\n  background: darken(@sidebar, 5%);\n  color: @active-sidebar-text;\n  box-shadow: none;\n}\n\n.sidebar-activity .item {\n  border: none;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-preferences.less",
    "content": "@import \"darkside-variables\";\n\n.preferences-sidebar,\n.preferences-content {\n  background: @messagelist-bg;\n}\n\n.preferences-wrap .preferences-content > .scroll-region-content {\n  padding-bottom: 100px;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-sidebar.less",
    "content": "@import \"darkside-variables\";\n\n// ============================\n//  Sidebar Base\n// ============================\n\n// Make sidebar and corresponding toolbar match\n.column-RootSidebar,\n.account-sidebar,\n.toolbar-RootSidebar {\n  height: 100%;\n  background-color: @sidebar;\n  // If NOT Retina display, subpixel-antialias fonts instead\n  @media\n    not screen and (-webkit-min-device-pixel-ratio: 1.3),\n    not screen and (-o-min-device-pixel-ratio: 13/10),\n    not screen and (min-resolution: 120dpi) {\n      -webkit-font-smoothing: subpixel-antialiased !important;\n  }\n}\n\n.notifications {\n  box-shadow: none;\n}\n\n\n// Refactored this to make sure all items\n// in sidebar always align left with each other\n.account-sidebar {\n  // make absolute elements (like compose button)\n  // relate to the sidear, not the column\n  position: relative;\n  margin: @sidebar-margin;\n}\n\n.nylas-outline-view {\n  margin-bottom: @sidebar-margin;\n}\n\n// Section headers\n.account-sidebar .heading {\n  font-size: 10px;\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  color: fade(@sidebar-text, 50%);\n  margin-bottom: 10px;\n  padding: 0;\n}\n\n// Down arrow icon\n.account-switcher {\n  height: 14px;\n  width: 16px;\n  top: 0;\n  right: 0;\n  display: flex;\n  justify-content: center;\n  align-items: center;\n  opacity: 0.5;\n  transition: opacity 200ms;\n  &:hover {\n    opacity: 1;\n  }\n}\n\n// Down arrow icon\n.account-switcher img {\n  zoom: 1 !important;\n  max-width: 10px;\n  max-height: 6px;\n  transform: none;\n  background-image: none;\n  background-color: @sidebar-text;\n  -webkit-mask-repeat: no-repeat;\n  -webkit-mask-image: url('data:image/svg+xml;utf8,<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"10\" height=\"6\" viewBox=\"0 0 10 6\"><path fill=\"#FFF\" d=\"M0 0h1l4 4 4-4h1v1L5 6 0 1z\"/></svg>');\n}\n\n.account-sidebar .item,\n.account-sidebar .item.selected {\n  color: @sidebar-text;\n  font-size: 13px;\n  font-weight: 400;\n  padding-right: 0;\n}\n\n.account-sidebar .item.selected {\n  background: transparent;\n  color: @active-sidebar-text;\n}\n\n// Item expansion icon wrapper\n.disclosure-triangle {\n  display: flex;\n  align-items: center;\n  padding: 0;\n  width: 15px;\n}\n\n// Item expansion icon\n.disclosure-triangle div {\n  border-left-color: fade(@sidebar-text, 50%);\n  border-top-width: 3px;\n  border-bottom-width: 3px;\n  border-left-width: 5px;\n  transform-origin: 2px;\n}\n\n//====================================================\n//  Sidebar Icons\n//====================================================\n\n.account-sidebar .item img.content-mask,\n.account-sidebar .add-item-button img, {\n  background-color: @sidebar-text;\n}\n\n.account-sidebar .item.selected img.content-mask {\n  background-color: @active-sidebar-text;\n}\n\n.nylas-outline-view .item-container.dropping {\n  background: transparent;\n}\n\n.nylas-outline-view .item-container.dropping .item {\n  color: @accent;\n}\n\n.nylas-outline-view .item-container.dropping .item img.content-mask {\n  background-color: @accent;\n}\n\n.nylas-outline-view .heading .add-item-button img {\n  background: fade(@sidebar-text, 50%);\n}\n\n//====================================================\n//  Sidebar Count Badges\n//====================================================\n\n.nylas-outline-view .item .item-count-box.alt-count {\n  background: @accent;\n  color: @sidebar;\n}\n\n.nylas-outline-view .item .item-count-box {\n  color: @accent;\n  box-shadow: inset 0 0 0 1px fade(@accent, 50%);\n}\n\n//====================================================\n//  Scrollbar Base & Sidebar Scrollbar\n//====================================================\n\n.scrollbar-track {\n  background: transparent;\n  border-left: none;\n  width: 10px;\n}\n\n// transitioning background instead of opacity\n// so the location tooltip isn't affected\n.scrollbar-track .scrollbar-handle {\n  background: fade(@sidebar, 20%);\n  border: none !important;\n  cursor: -webkit-grab;\n  transition: background 300ms;\n}\n\n.scrollbar-track.dragging .scrollbar-handle {\n  background: fade(@sidebar, 50%);\n  cursor: -webkit-grabbing;\n}\n\n@keyframes slideInRight {\n  from { opacity: 0; transform: translateX(-50px); }\n  to   { opacity: 1; transform: translateX(-15px); }\n}\n\n.scrollbar-track .scrollbar-handle .tooltip .scroll-tooltip {\n  transform-origin: center right;\n  animation: slideInRight 300ms;\n}\n\n.flexbox-handle-horizontal div {\n  box-shadow: none;\n}\n\n// Removing overlap of scrollbar and handle\n.column-ThreadList .flexbox-handle-horizontal.flexbox-handle-right {\n  right: -8px;\n  padding: 0;\n}\n\n// we now offset the margin on scrollbar\n// in sidebar since it's position: relative\n.account-sidebar .scrollbar-track {\n  margin-right: -@sidebar-margin;\n}\n\n// and lighten the handle background\n.account-sidebar .scrollbar-track .scrollbar-handle {\n  background: fade(lighten(@sidebar, 50%), 40%);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-swiping.less",
    "content": "@import \"darkside-variables\";\n\n.thread-list .swipe-backing.swipe-all,\n.thread-list .swipe-backing.swipe-archive,\n.draft-list .swipe-backing.swipe-all,\n.draft-list .swipe-backing.swipe-archive {\n  background: @swipe-archive;\n  &.confirmed {\n    background: saturate(@swipe-archive, 10%);\n  }\n}\n\n.thread-list .swipe-backing.swipe-snooze,\n.draft-list .swipe-backing.swipe-snooze {\n  background: @swipe-snooze;\n  &.confirmed {\n    background: saturate(@swipe-snooze, 10%);\n  }\n}\n\n.thread-list .swipe-backing.swipe-trash,\n.draft-list .swipe-backing.swipe-trash {\n  background: @swipe-trash;\n  &.confirmed {\n    background: saturate(@swipe-trash, 10%);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-thread-icons.less",
    "content": "@import \"darkside-variables\";\n\n// Remove inverted color effects\n.thread-list .focused .thread-icon,\n.draft-list .focused .thread-icon,\n.thread-list .focused .draft-icon,\n.draft-list .focused .draft-icon,\n.thread-list .focused .mail-important-icon,\n.draft-list .focused .mail-important-icon,\n.thread-list.handler-split .list-item.selected .thread-icon,\n.thread-list.handler-split .list-item.selected .draft-icon,\n.thread-list.handler-split .list-item.selected .mail-important-icon {\n  -webkit-filter: none !important;\n}\n\n// Base settings for replacing backgrounds with -webkit-filters for easier color changes\n.thread-list .thread-icon {\n  -webkit-mask-repeat: no-repeat;\n  -webkit-mask-size: 12px;\n  -webkit-mask-position: center;\n}\n\n// Change color of unread dot icon\n.thread-list .thread-icon.thread-icon-unread {\n  background-image: none;\n  background-color: @accent;\n  -webkit-mask-image: url(../static/images/thread-list/icon-unread-@2x.png);\n}\n\n// replace undread icon with star icon on thread item hover\n.thread-list .list-item:hover .thread-icon.thread-icon-unread {\n  background-color: tint(@sidebar);\n  -webkit-mask-image: url(../static/images/thread-list/icon-star-@2x.png);\n  &:hover { background-color: tint(@sidebar, 20%); }\n}\n\n// Replace outlined star icon with solid one\n.thread-list .thread-icon.thread-icon-star,\n.thread-list .thread-icon-star-on-hover:hover {\n  background-color: tint(@sidebar);\n  -webkit-mask-image: url(../static/images/thread-list/icon-star-@2x.png);\n}\n\n// for Read messages, use the solid star on item hover as well\n.thread-list .list-item:hover .thread-icon-none {\n  background-image: none;\n  background-color: tint(@sidebar);\n  -webkit-mask-image: url(../static/images/thread-list/icon-star-@2x.png);\n}\n\n// Make the star a bit darker on direct hover\n.thread-list .list-item:hover .thread-icon-none:hover {\n  background-image: none;\n  background-color: tint(@sidebar, 20%);\n  -webkit-mask-image: url(../static/images/thread-list/icon-star-@2x.png);\n}\n\n.thread-icon.thread-icon-attachment {\n  opacity: 0.5;\n  background-size: 12px;\n}\n\n// The gradient behind threadlist hover icons (Snooze, Arvhive Delete)\n.thread-list .list-item:hover .list-column-HoverActions .inner,\n.thread-list .list-item.focused:hover .list-column-HoverActions .inner,\n.thread-list .list-item.selected:hover .list-column-HoverActions .inner,\n.thread-list.handler-split .list-item.selected:hover .list-column-HoverActions .inner {\n  background-image: -webkit-linear-gradient(left, fade(@messagelist-bg, 0%) 0%, @messagelist-bg 40%, @messagelist-bg 100%);\n}\n\n.thread-list .list-item.focused:hover .list-column-HoverActions .inner .action,\n.thread-list.handler-split .list-item.selected:hover .list-column-HoverActions .inner .action {\n  -webkit-filter: none;\n}\n\n.thread-list .list-item.focused:hover .list-column-HoverActions .inner .action.action-trash {\n  background: url(\"../static/images/thread-list-quick-actions/ic-quick-button-trash@2x.png\") center no-repeat, linear-gradient(to top, rgba(241, 241, 241, 0.75) 0%, rgba(253, 253, 253, 0.75) 100%);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-threadlist.less",
    "content": "@import \"darkside-variables\";\n\n// Make corresponding toolbar match threadlist background\n.column-ThreadList,\n.toolbar-ThreadList {\n  height: 100%;\n  background: @threadlist-bg;\n  border-bottom: 1px solid @border-color;\n}\n\n// jackiehluo -> Hide search bar when buttons appear in list mode\n.toolbar-ThreadList .selection-bar .inner {\n  background: @threadlist-bg;\n}\n\n.list-tabular .list-tabular-item {\n  background-color: @threadlist-bg;\n  border-bottom: 1px solid @border-color !important;\n}\n\n// Using !important so multiple selection actions\n.list-tabular .list-tabular-item:hover {\n  background: tint(@sidebar, 95%) !important;\n}\n\n.list-container .list-item.focused,\n.list-container .list-item.selected,\n.thread-list.handler-split .list-item.selected {\n  background: tint(@accent, 90%);\n  color: @active-thread-text;\n}\n\nbody.is-blurred .list-container .list-item.focused,\nbody.is-blurred .list-container .list-item.selected,\nbody.is-blurred .thread-list.handler-split .list-item.selected {\n  background: tint(@sidebar, 90%);\n  color: @active-thread-text;\n}\n\n.list-tabular .list-tabular-item.keyboard-cursor {\n  border-left-color: @accent;\n  background: tint(@accent, 90%);\n}\n\nbody.is-blurred .list-tabular .list-tabular-item.keyboard-cursor {\n  border-left-color: tint(@sidebar, 70%);\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-toolbars.less",
    "content": "@import \"darkside-variables\";\n\n.sheet-toolbar {\n  border: none;\n}\n\n.sheet-toolbar-container {\n  background: transparent;\n  box-shadow: none;\n  border: none;\n}\n\n.sheet-toolbar .selection-bar .absolute {\n  left: 0;\n  right: 0;\n  border-left: none;\n  border-right: none;\n  background: none;\n}\n\n// Match left and right alignment across all toolbars\n.toolbar-RootSidebar,\n.toolbar-MessageList,\n.toolbar-MessageListSidebar,\n.toolbar-Center,\n.toolbar-Preferences {\n  height: 100%;\n  padding-left: @sidebar-margin;\n  padding-right: @sidebar-margin;\n}\n\n// Slightly darker toolbar for Prefs, Single Panel Messages, and Popout\n.toolbar-Preferences,\n.layout-mode-list .toolbar-MessageList,\n.sheet-toolbar-container.mode-popout {\n  background: transparent;\n  background-color: tint(@sidebar, 90%);\n  border: none;\n}\n\n// jackiehluo -> (themes): Fixes Windows button UI issues in #1649\nbody.platform-win32 .sheet-toolbar-container .btn-toolbar:hover {\n  background: none;\n}\n\n// Centering vertially without magic numbers\n.layout-mode-popout .toolbar-window-controls {\n  margin-top: 0;\n}\n\n.sheet-toolbar .item-container .window-title {\n  position: static;\n  // compensate for width of .toolbar-window-controls\n  transform: translateX(-25px);\n}\n\n.sheet-toolbar .btn-toolbar {\n  box-shadow: 0 0 0 1px @border-color;\n}\n\n// Let toolbar define outer padding/margin\n.sheet-toolbar .btn-toolbar:only-of-type {\n  margin-right: 0;\n}\n\n.btn-toolbar.mode-toggle.mode-false img.content-mask {\n  background-color: @accent;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-variables.less",
    "content": "// Default\n@sidebar: #313042;\n@accent: #F18260;\n\n// Luna\n// @sidebar: #202C46;\n// @accent: #39DFF8;\n\n// Zond\n// @sidebar: #333333;\n// @accent: #F6D49C;\n\n// Gemini\n// @sidebar: #00203C;\n// @accent: #F6B312;\n\n// Mercury\n// @sidebar: #555;\n// @accent: #999;\n\n// Apollo\n// @sidebar: #3A1E15;\n// @accent: #F6AA1C;\n\n@threadlist-bg: #FFFFFF;\n@messagelist-bg: tint(@sidebar, 95%);\n@active-thread-text: @sidebar;\n@sidebar-text: desaturate(lighten(@sidebar, 40%), 75%);\n@active-sidebar-text: #FFFFFF;\n@border-color: fade(@sidebar, 10%);\n@danger: #FF5F56;\n@minimize: #FBD852;\n@maximize: #8DD07D;\n@swipe-archive: #8DD07D;\n@swipe-snooze: #FBD852;\n@swipe-trash: @danger;\n@invalid: @danger;\n\n@sidebar-margin: 15px;\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/darkside-window-controls.less",
    "content": "@import \"darkside-variables\";\n\n.toolbar-window-controls {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin: 0;\n  min-width: 50px;\n  width: 50px;\n}\n\n.toolbar-window-controls button {\n  background-color: @sidebar-text;\n  background-image: none !important;\n  float: none;\n  opacity: 0.5;\n  margin: 0;\n  transform: scaleY(0.5);\n  border-radius: 2px;\n  transition-duration: 150ms;\n  transition-property: border-radius, opacity, transform;\n}\n\n.toolbar-window-controls:hover button {\n  opacity: 1;\n  border-radius: 50%;\n  transform: scaleY(1);\n}\n\n.toolbar-window-controls .close {\n  background-color: @danger;\n}\n\n.toolbar-window-controls .minimize {\n  background-color: @minimize;\n}\n\n.toolbar-window-controls .maximize {\n  background-color: @maximize;\n}\n\n.is-blurred {\n  .toolbar-window-controls .close,\n  .toolbar-window-controls .minimize,\n  .toolbar-window-controls .maximize {\n    background-color: fade(@sidebar-text, 50%);\n  }\n}\n\n// Compose Button Overrides\n.sheet-toolbar .btn.btn-toolbar.item-compose {\n  background: transparent;\n  box-shadow: none;\n  opacity: 0.5;\n  padding: 0;\n  margin: 0;\n  height: 100%;\n  transition: opacity 200ms;\n  &:hover {\n    opacity: 1;\n  }\n}\n\n// Compose button icon color\n.sheet-toolbar .btn.btn-toolbar.item-compose img.content-mask {\n  background-color: @sidebar-text;\n}\n\n// Activity List\n.toolbar-activity {\n  margin-right: 8px;\n}\n\n.activity-list-container {\n  .disclosure-triangle div {\n    margin-left: 4px;\n    margin-top: -2px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-darkside/styles/theme-colors.less",
    "content": "@import \"darkside-variables\";\n\n@component-active-color: @accent;\n@panel-background-color: @sidebar;\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-less-is-more/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2016 Alexander Adkins\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-less-is-more/README.md",
    "content": "# N1 Less Is More UI theme\n\nLess Is More UI theme for N1.\n\nThis theme is installed by default with N1 and can be activated by going to\nthe _Themes_ section in the Settings view (`cmd-,`) and selecting it from the\n_UI Themes_ drop-down menu.\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-less-is-more/index.less",
    "content": ""
  },
  {
    "path": "packages/client-app/internal_packages/ui-less-is-more/package.json",
    "content": "{\n  \"name\": \"less-is-more\",\n  \"displayName\": \"Less Is More\",\n  \"theme\": \"ui-less-is-more\",\n  \"version\": \"1.0.7\",\n  \"description\": \"A minimal approach to email in Nylas Mail\",\n  \"license\": \"MIT\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"styleSheets\": [\"less-is-more\"],\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-less-is-more/styles/less-is-more.less",
    "content": "//====================================================\n//  Less Is More Index\n//====================================================\n//  Theme Variables\n//  Window Controls\n//  Sheet Toolbars\n//  Sidebar & Account Switcher\n//  Sidebar Count Badges\n//  Scrollbars & Resize Handles\n//  Thread List\n//  Message List\n//  Message List Sidebar\n//  Swiping\n//  Preferences\n//  Form Inputs & Search Bar\n//  Menu Dropdowns\n//  Notifications\n//  Drafts\n//  Composer\n\n\n//====================================================\n//  Theme Variables\n//====================================================\n\n@less-background:       #FFFFFF; //white\n@less-text:             #566C75; //gray\n@less-highlight:        #FAFAFA; //lightest-gray\n@less-divider:          #DDDDDD; //lighter-gray\n@minimize:              #FBD852; //yellow\n@maximize:              #8DD07D; //green\n@close:                 #FF5F56; //red\n@swipe-archive:         @maximize; //green\n@swipe-snooze:          @minimize; //yellow\n@swipe-trash:           @close; //red\n@invalid:               @close; //red\n@sidebar-text:          lighten(@less-text, 20%); //light-gray\n\n\n//====================================================\n//  Window Controls\n//====================================================\n\n// Padding and Color for Account Sidebar, Message List, Message Sidebar,\n// Preference Sidebar and Draft List.\n.column-RootSidebar,\n.column-MessageListSidebar,\n.preferences-sidebar,\n.column-DraftList {\n  padding: 5em 0 2em 2em;\n  background: @less-background;\n}\n\n// Message List padding\n.column-MessageList {\n  padding: 3em 2em;\n}\n\n\n// Window Control Button transforms\n.toolbar-window-controls button {\n  background-color: @sidebar-text;\n  background-image: none !important;\n  width: 12px;\n  height: 12px;\n  float: none;\n  opacity: 0.5;\n  transform: scaleY(0.5);\n  border-radius: 0;\n  transition-duration: 150ms;\n  transition-property: border-radius, opacity, transform;\n}\n\n// Window Control Button transforms on hover\n.toolbar-window-controls:hover button {\n  opacity: 1;\n  border-radius: 50%;\n  transform: scaleY(1);\n}\n\n// Window Control close Button color\n.toolbar-window-controls .close {\n  background-color: @close;\n}\n\n// Window Control minimize Button color\n.toolbar-window-controls .minimize {\n  background-color: @minimize;\n}\n\n// Window Control maximize Button color\n.toolbar-window-controls .maximize {\n  background-color: @maximize;\n}\n\n// Remove underline and dropshadow on compose button\n.sheet-toolbar .btn-toolbar {\n  background: transparent;\n  border: none;\n  box-shadow: none;\n}\n\n\n//====================================================\n//  Sheet Toolbars\n//====================================================\n\n// Create white background mask on message list sidebar toolbar\n.toolbar-MessageListSidebar,\n.sheet-toolbar-container {\n  background-color: @less-background;\n}\n\n// Create divider line for message list sidebar toolbar\n.toolbar-MessageListSidebar {\n  border-left: 1px solid @less-divider;\n  height: 40px;\n  margin-left: -0.5px;\n}\n\n// Make top toolbar mask our searchbar with white background\n.sheet-toolbar-container [data-column='0'] .item-container {\n  background-color: @less-background;\n}\n\n// Correctly position and remove border on the top toolbar\n.sheet-toolbar {\n  border: none;\n  height: 0;\n  min-height: 0;\n  .selection-bar .absolute {\n    position: absolute;\n    left: 0;\n    right: 0;\n    border-left: none;\n    border-right: none;\n  }\n}\n\n\n//====================================================\n//  Sidebar & Account Switcher\n//====================================================\n\n// Change default account sidebar color from default gray to white\n.column-RootSidebar,\n.account-sidebar {\n  background-color: @less-background;\n}\n\n// Account sidebar label controls\n.account-sidebar .item {\n  color: @sidebar-text;\n  font-size: 14px;\n  font-weight: 400;\n  padding-left: 20px;\n  margin-bottom: 6px;\n}\n\n// Account sidebar headings overrides\n.account-sidebar .heading {\n  font-size: 10px;\n  text-transform: uppercase;\n  letter-spacing: 2px;\n  color: @sidebar-text;\n  margin-bottom: 12px;\n}\n\n// Keep account sidebar icons from flashing on click\n.account-sidebar .item img.content-mask,\n.account-sidebar .add-item-button img {\n  background-color: transparent;\n  -webkit-mask-image: none;\n}\n\n// Account sidebar selected label overrides\n.account-sidebar {\n  // Fix padding jump\n  .item .name {\n    padding-left: 0;\n    margin-left: -10px;\n  }\n  // Change label color and font weight\n  .item.selected {\n    background: transparent;\n    color: @less-text;\n    font-weight: 600;\n  }\n}\n\n// Account sidebar label triangle bullet overrides\n.disclosure-triangle {\n  padding-top: 7px;\n  & div {\n    border-left-color: @sidebar-text;\n    border-top-width: 3px;\n    border-bottom-width: 3px;\n    border-left-width: 5px;\n    transform-origin: 2px;\n  }\n}\n\n// Remove default nylas icon images\n.nylas-outline-view .item .icon img {\n  display: none;\n}\n\n// Account sidebar add folder icon color overrides\n.nylas-outline-view .heading .add-item-button img {\n  background: @sidebar-text;\n}\n\n\n//====================================================\n//  Sidebar Count Badges\n//====================================================\n\n// Sidebar unread email count color overrides\n.nylas-outline-view .item .item-count-box.alt-count {\n  background: @less-text;\n  color: @less-background;\n}\n\n\n//====================================================\n//  Scrollbars & Resize Handles\n//====================================================\n\n.scrollbar-track {\n  background-color: transparent;\n  width: 10px;\n  border-left: none;\n}\n\n.flexbox-handle-horizontal div {\n  border-right: none;\n  box-shadow: none;\n}\n\n// Position scrollbar on message list on divider\n#message-list .scrollbar-track {\n  margin-right: -2em;\n}\n\n\n//====================================================\n//  Thread List\n//====================================================\n\n// Thread list overrides\n.column-ThreadList,\n.list-container .list-item,\n.list-container .list-item:hover {\n  cursor: pointer !important;\n  box-sizing: border-box;\n  border: 0 !important;\n  background-color: @less-background;\n  color: @less-text;\n}\n\n// Thread list padding overrides\n.column-ThreadList {\n  padding: 5em 2em 1em;\n}\n\n// Selected thread list items overrides\nbody.is-blurred .list-container .list-item.focused,\nbody.is-blurred .list-container .list-item.selected,\nbody.is-blurred .thread-list.handler-split .list-item.selected {\n  background-color: @less-highlight;\n  color: darken(@less-text, 50%);\n  font-weight: bold;\n}\n\n// Thread list turns gray on hover\n.list-container .list-item.selected,\n.list-container .list-item:hover {\n  background: @less-highlight;\n  color: @less-text;\n}\n\n// Remove gradient on thread list during quick action hover\n.thread-list .list-item.selected:hover .list-column-HoverActions .inner,\n.thread-list .list-item:hover .list-column-HoverActions .inner {\n  background-image: -webkit-linear-gradient(left, fade(@less-highlight, 0%) 0%, @less-highlight 40%, @less-highlight 100%);\n}\n\n// Remove box-shadow on thread list quick action buttons\n.thread-injected-quick-actions .btn {\n  box-shadow: none;\n}\n\n// Remove gradients quick action buttons\n.thread-list .list-item .list-column-HoverActions .action.action-trash {\n  background: url(\"../static/images/thread-list-quick-actions/ic-quick-button-trash@2x.png\")\n  center no-repeat, @less-highlight;\n}\n\n// Remove gradients quick action buttons\n.thread-list .list-item .list-column-HoverActions .action.action-archive {\n  background: url(\"../static/images/thread-list-quick-actions/ic-quick-button-archive@2x.png\")\n  center no-repeat, @less-highlight;\n}\n\n// Remove gradients quick action buttons\n.thread-list .list-item .list-column-HoverActions .action.action-snooze {\n  background: url(\"../static/images/thread-list-quick-actions/ic-quickaction-snooze@2x.png\")\n  center no-repeat, @less-highlight;\n}\n\n// Change default color of star to gray\n.thread-list .thread-icon.thread-icon-star, .draft-list .thread-icon.thread-icon-star {\n  -webkit-filter: grayscale(100%);\n}\n\n\n//====================================================\n//  Message List\n//====================================================\n\n// Theme message list\n#message-list {\n  background-color: @less-background;\n}\n\n// Theme collapsed message item\n#message-list .message-item-wrap .message-item-white-wrap {\n  box-shadow: none;\n  border-radius: 0;\n}\n\n// Theme message list composer footer\n#message-list .footer-reply-area-wrap {\n  box-shadow: none;\n  border-radius: 0;\n  border-top: none;\n  background: @less-highlight;\n}\n\n// Draft message background color\n#message-list .message-item-wrap .message-item-white-wrap.composer-outer-wrap,\n#message-list .message-item-wrap .message-item-white-wrap.composer-outer-wrap .composer-action-bar-wrap {\n  background-color: @less-highlight;\n  border-top: none;\n  box-shadow: none;\n  border-radius: 0;\n}\n\n// Draft message background color on focus\n#message-list .message-item-wrap .message-item-white-wrap.composer-outer-wrap.focused {\n  background-color: @less-background;\n  box-shadow: none;\n  border: 1px solid @less-divider;\n  border-radius: 0;\n}\n\n// Draft message background action bar theme on focus\n#message-list .message-item-wrap .message-item-white-wrap.composer-outer-wrap.focused .composer-action-bar-wrap {\n  background-color: @less-background;\n}\n\n\n//====================================================\n//  Message List Sidebar\n//====================================================\n\n// Re-center message list in sidebar with padding\n.column-MessageListSidebar {\n  padding: 5em 1em;\n}\n\n.sidebar-participant-picker {\n  padding-bottom: 50px;\n}\n\n// Remove border line surrounding on message list sidebar\n.sidebar-section {\n  border: none;\n  border-radius: 0;\n}\n\n// Message list sidebar headings to match account sidebar headings\n.sidebar-section h2 {\n  font-size: 12px;\n  text-transform: uppercase;\n  letter-spacing: 3px;\n  color: @sidebar-text;\n  border-bottom: none;\n}\n\n// Theme related threads tabs\n.related-threads {\n  background: transparent;\n  border-top: none;\n  border-radius: 0;\n  overflow: visible;\n}\n\n// Theme related threads tabs items\n.related-threads .related-thread {\n  border-top: none;\n  background-color: @less-highlight;\n  color: @less-text;\n  margin-bottom: 8px;\n  padding: 15px 10px;\n}\n\n// Theme related threads \"Show More\" label overrides\n.related-threads .toggle {\n  border-top: none;\n  color: @less-text;\n}\n\n\n//====================================================\n//  Swiping\n//====================================================\n\n// Adjust color of archive swipe to green\n.thread-list .swipe-backing.swipe-all,\n.thread-list .swipe-backing.swipe-archive,\n.draft-list .swipe-backing.swipe-all,\n.draft-list .swipe-backing.swipe-archive {\n  background: @swipe-archive;\n  &.confirmed {\n    background: saturate(@swipe-archive, 10%);\n  }\n}\n\n// Adjust color of snooze swipe to yellow\n.thread-list .swipe-backing.swipe-snooze,\n.draft-list .swipe-backing.swipe-snooze {\n  background: @swipe-snooze;\n  &.confirmed {\n    background: saturate(@swipe-snooze, 10%);\n  }\n}\n\n// Adjust color of trash swipe to red\n.thread-list .swipe-backing.swipe-trash,\n.draft-list .swipe-backing.swipe-trash {\n  background: @swipe-trash;\n  &.confirmed {\n    background: saturate(@swipe-trash, 10%);\n  }\n}\n\n\n//====================================================\n//  Preferences\n//====================================================\n\n// Extra padding and color adjust needed for preferences top panel\n.preferences-wrap .container-preference-tabs .preferences-tabs {\n  padding-top: 40px;\n  background-color: @less-background;\n}\n\n// Padding for bottom of preferences panel\n.preferences-wrap .preferences-content > .scroll-region-content {\n  padding-bottom: 100px;\n}\n\n\n//====================================================\n//  Form Inputs & Search Bar\n//====================================================\n\n// Input style overrides\ninput[type=\"text\"],\ninput[type=\"email\"],\ninput[type=\"date\"],\ninput[type=\"datetime\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"],\ninput[type=\"number\"],\ninput[type=\"password\"],\ninput[type=\"range\"],\ninput[type=\"search\"],\ninput[type=\"tel\"],\ninput[type=\"time\"],\ninput[type=\"url\"] {\n    border-radius: 0;\n    border: none !important;\n}\n\n// Input style overrides on hover\ntextarea:focus,\ninput[type=\"text\"]:focus,\ninput[type=\"email\"]:focus,\n.search-bar .menu .header-container input:focus {\n  border: none !important;\n  border-radius: 0;\n  border-bottom: 2px solid @less-text !important;\n  box-shadow: none;\n}\n\n// Search bar overrides\n.search-bar {\n  background-color: transparent;\n  width: 400px;\n  margin-right: 7.5px;\n}\n\n// Remove box-shadow on search bar\nbody.is-blurred .search-bar .menu .header-container input,\nbody.is-blurred .sheet-toolbar-container .btn.btn-toolbar,\n.search-bar .menu .header-container input {\n  box-shadow: none;\n}\n\n\n//====================================================\n//  Notifications\n//====================================================\n\n.notifications-sticky .notifications-sticky-item {\n  background-color: @close;\n  line-height: 50px;\n  border: none;\n}\n\n.sidebar-activity {\n  background: @less-background;\n  color: @less-text;\n  box-shadow: none;\n}\n\n.sidebar-activity .item {\n  border: none;\n}\n\n\n//====================================================\n//  Composer\n//====================================================\n\n// make top of composer window uniform in color\n.sheet-toolbar-container,\nbody.is-blurred .sheet-toolbar-container {\n  background-color: @less-background;\n  background-image: none;\n  box-shadow: none;\n}\n\n// make bottom of composer window uniform in color\n.composer-full-window .composer-inner-wrap .composer-action-bar-wrap {\n  background: @less-background;\n  border-top: none;\n  box-shadow: none;\n  padding-bottom: .8em;\n}\n\n// Border at bottom of composer subject field\n.composer-inner-wrap .compose-subject-wrap {\n  border-bottom: 1px solid @sidebar-text;\n}\n\n.tokenizing-field .token.invalid {\n  border: 1px solid lighten(@close,25%);\n}\n\n.tokenizing-field .token.selected,\n.tokenizing-field .token.dragging {\n  background: @less-text;\n  box-shadow: none;\n  border: none;\n}\n\n.tokenizing-field .token.invalid.selected,\n.tokenizing-field .token.invalid.dragging {\n  background: lighten(@close,25%);\n}\n\n.tokenizing-field .tokenizing-field-input input[type=\"text\"],\n.tokenizing-field .tokenizing-field-input input[type=\"text\"]:focus {\n  border-bottom: none !important;\n}\n\ntextarea:focus,\ninput[type=\"text\"]:focus,\ninput[type=\"email\"]:focus {\n  border: 1px solid @less-text;\n  box-shadow: none;\n  padding-left: 0;\n  padding-right: 0;\n  min-width: 30px;\n}\n\n.button-dropdown .primary-item, .button-dropdown .only-item {\n  box-shadow: none;\n}\n\n.button-dropdown.btn-emphasis .primary-item,\n.button-dropdown.btn-emphasis .secondary-picker,\n.button-dropdown.btn-emphasis .only-item\n.button-dropdown.btn-emphasis .primary-item:active,\n.button-dropdown.btn-emphasis .secondary-picker:active,\n.button-dropdown.btn-emphasis .only-item:active,\n.button-dropdown.bordered .primary-item,\n.button-dropdown:hover .primary-item,\n.button-dropdown.bordered .only-item,\n.button-dropdown:hover .only-item,\n.button-dropdown .secondary-picker,\n.btn.btn-emphasis {\n  background-color: lighten(@less-text, 30%);\n  background: lighten(@less-text, 30%);\n  box-shadow: none;\n  border: 1px solid lighten(@less-text, 25%);\n}\n\n.button-dropdown .primary-item img.content-mask,\n.button-dropdown .only-item img.content-mask,\n.button-dropdown .secondary-picker img.content-mask {\n  background-color: @less-background;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-less-is-more/styles/theme-colors.less",
    "content": "@import \"less-is-more\";\n\n@background-secondary: @less-background;\n@text-color: @less-text;\n@component-active-color: @less-background;\n@toolbar-background-color: @less-background;\n@panel-background-color: @less-background;\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-light/package.json",
    "content": "{\n  \"name\": \"ui-light\",\n  \"displayName\": \"Light\",\n  \"theme\": \"ui\",\n  \"version\": \"0.1.0\",\n  \"description\": \"The N1 Client Theme\",\n  \"license\": \"GPL-3.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-light/styles/ui-variables.less",
    "content": "@background-primary:     #ffffff;\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2015 Noah Buscher\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/README.md",
    "content": "# Taiga\n\nTaiga is a clean, simple, Mailbox-inspired theme for N1 that allows you to focus on what matters most: your emails.\n\n![](./preview.jpg)\n\n## Installing\n\n1. [Download](https://nylas.com/n1) Nylas Mail email client if you have not yet\n2. [Grab](https://github.com/noahbuscher/N1-Taiga/releases) the latest release of Taiga\n3. Open `N1>Preferences>General>Select theme` and select `Install new theme...` from the dropdown\n\nProfit! :money_with_wings:\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/package.json",
    "content": "{\n  \"name\": \"ui-taiga\",\n  \"displayName\": \"Taiga\",\n  \"theme\": \"ui\",\n  \"version\": \"0.2.8\",\n  \"description\": \"A clean, Mailbox-inspired theme for Nylas Mail.\",\n  \"license\": \"GPL-3.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"styleSheets\": [\"controls\", \"email-frame\", \"sidebar\", \"threads\", \"notifications\"],\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/controls.less",
    "content": "@import \"variables\";\n\n.header-container {\n  margin-right: 10px;\n}\n\n/**\n * Buttons\n */\n.btn-toolbar, .token, .actions>.btn, .new-package>.btn, .appearance-mode-switch>.btn {\n  box-shadow: none !important;\n  background: @white !important;\n  border: 0;\n  border-radius: @base-border-radius !important;\n  &.item-compose {\n    border: 1px solid @taiga-light;\n  }\n}\n\n.composer-action-bar-content {\n  .btn-toolbar {\n    border: 0 !important;\n    background: transparent !important;\n  }\n}\n\nbody.platform-win32 {\n  .sheet-toolbar-container {\n    .btn-toolbar {\n      border: 0 !important;\n    }\n  }\n}\n\n.btn.btn-emphasis {\n  background-color: @taiga-accent !important;\n  border-color: @taiga-accent !important;\n  color: @white !important;\n\n  img.content-mask {\n    background-color: @white !important;\n  }\n}\n\n.button-dropdown.bordered {\n  .primary-item {\n    box-shadow: none !important;\n    background: @white !important;\n    border: 1px solid @taiga-light !important;\n    border-top-left-radius: @base-border-radius !important;\n    border-bottom-left-radius: @base-border-radius !important;\n    img {\n      position: relative;\n      top: -2px;\n    }\n  }\n\n  .secondary-picker {\n    box-shadow: none !important;\n    background: @white !important;\n    border: 1px solid @taiga-light !important;\n    border-top-right-radius: @base-border-radius !important;\n    border-bottom-right-radius: @base-border-radius !important;\n    margin-left: -1px !important;\n  }\n\n  .secondary-items {\n    .item  {\n      color: @taiga-light !important;\n      .search-match {\n        background: @white !important;\n      }\n      .button-dropdown {\n        img {\n          background: @taiga-light  !important;\n        }\n      }\n    }\n  }\n}\n\n.sheet-toolbar .btn-toolbar {\n  height: 2em !important;\n  line-height: 1 !important;\n  margin-top: 6px !important;\n}\n\n/**\n * Feedback button\n */\n.btn-feedback {\n  background: @taiga-accent !important;\n  border: none !important;\n}\n\n\n/**\n * Dropdown\n */\n.menu {\n  .item.selected {\n    .primary {\n      color: @white !important;\n    }\n    .secondary {\n      color: @taiga-lighter !important;\n    }\n  }\n  &.search-container {\n    .item {\n      background: @white !important;\n    }\n    .item.selected {\n      background: @taiga-light !important;\n      color: @white !important;\n    }\n  }\n}\n\n/**\n * Plugin page\n */\n.package {\n  border-radius: @base-border-radius;\n}\n\n/**\n * Prefs page\n */\n.appearance-mode {\n  &.active {\n    background-color: lighten(@taiga-light, 30%) !important;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/email-frame.less",
    "content": "@import \"variables\";\n\n.ignore-in-parent-frame {\n  body {\n    color: @taiga-dark;\n  }\n  img {\n    color: @taiga-dark;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/notifications.less",
    "content": "@import \"variables\";\n\n.notification {\n  color: @white !important;\n  background: @taiga-accent !important;\n\n  .action {\n    color: @white !important;\n    background: darken(@taiga-accent, 20%);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/sidebar.less",
    "content": "@import \"../../../static/variables/ui-variables\";\n@import \"variables\";\n\n#account-switcher .primary-item .name {\n  color: @taiga-dark;\n}\n\n.account-sidebar-sections {\n  background-color: @white !important;\n\n  section {\n    &:first-child .heading {\n      padding-right: 40px;\n    }\n\n    .heading {\n      padding-bottom: 5px;\n      .text {\n        flex: 1;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n    }\n    .item-container {\n      margin: 0 10px 0 0 !important;\n\n      .disclosure-triangle {\n        display: flex;\n        align-items: center;\n        width: 15px;\n        div {\n          border-left-color: @border-color-primary;\n          border-top-width: 3px;\n          border-bottom-width: 3px;\n          border-left-width: 5px;\n          transform-origin: 2px;\n        }\n      }\n\n      .item {\n        padding: 0 10px !important;\n        color: @taiga-light !important;\n        cursor: pointer !important;\n\n        .item-count-box {\n          background: transparent !important;\n          color: @taiga-light !important;\n          box-shadow: 0 0.5px 0 @taiga-light, 0 -0.5px 0 @taiga-light, 0.5px 0 0 @taiga-light, -0.5px 0 0 @taiga-light !important;\n        }\n\n        &.selected {\n          background: @taiga-accent !important;\n          border-radius: @base-border-radius;\n          color: @white !important;\n\n          .item-count-box {\n            background: transparent !important;\n            color: @white !important;\n            box-shadow: 0 0.5px 0 @white, 0 -0.5px 0 @white, 0.5px 0 0 @white, -0.5px 0 0 @white !important;\n          }\n\n          .icon {\n            img {\n              background: @white !important;\n            }\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/theme-colors.less",
    "content": "@component-active-color: #5dade1;\n@toolbar-background-color: #ddedf4;"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/threads.less",
    "content": "@import \"variables\";\n\n.list-tabular .list-column.list-column-Item {\n  margin-left: -20px;\n  padding-left: 30px;\n}\n\n.thread-list {\n  .list-container {\n    .list-item {\n      &.focused:hover .list-column-HoverActions .inner {\n        color: @taiga-dark !important;\n        background-image: linear-gradient(90deg, fadeout(@taiga-lighter, 100%) 0%, darken(@taiga-lighter, 10%) 100%);\n        .action {\n          -webkit-filter: none;\n        }\n        .thread-icon {\n          &:not(.thread-icon-star) {\n            opacity: 0.7;\n          }\n        }\n      }\n      &.focused {\n        border-bottom: 0;\n        .thread-icon, .mail-important-icon, .draft-icon {\n          -webkit-filter: none;\n        }\n      }\n      &:hover {\n        .thread-icon {\n          visibility: inherit;\n        }\n      }\n      .list-column {\n        border-bottom: 0 !important;\n      }\n    }\n    .scroll-region-content .scroll-region-content-inner .list-rows {\n      .list-item {\n        cursor: pointer !important;\n        box-sizing: border-box;\n        background-color: @white !important;\n        color: @taiga-dark !important;\n        &.focused {\n          color: @taiga-dark !important;\n          background-color: darken(@taiga-lighter, 5%) !important;\n        }\n        &.selected {\n          color: @taiga-dark !important;\n          background-color: darken(@taiga-lighter, 5%) !important;\n        }\n      }\n    }\n  }\n  .thread-icon {\n    background-image: url(../static/images/thread-list/icon-star-hover-@2x.png);\n    &:not(.thread-icon-star) {\n      visibility: hidden;\n    }\n  }\n}\n\n.is-blurred {\n  .thread-list .list-container .list-item.focused {\n    border-bottom: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/ui-variables.less",
    "content": "@import \"variables\";\n\n@accent-primary:         @taiga-light;\n@accent-primary-dark:    darken(@taiga-light, 20%);\n\n@background-secondary: @white;\n@text-color: @taiga-dark;\n@text-color-subtle: lighten(@taiga-dark, 20%);\n@text-color-very-subtle: @taiga-light;\n@text-color-inverse: @taiga-light;\n@text-color-inverse-subtle: darken(@taiga-light, 30%);\n@text-color-inverse-very-subtle: darken(@taiga-light, 20%);\n\n@panel-background-color: @white;\n@toolbar-background-color: @white;\n\n@btn-default-bg-color: @white;\n@btn-default-text-color: @taiga-light;\n@background-gradient: none;\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-taiga/styles/variables.less",
    "content": "/**\n * Colors\n */\n@taiga-light: darken(#A3ACB1, 10%);\n@taiga-lighter: #F0F7FA;\n@taiga-dark: darken(#727C83, 4%);\n@taiga-accent: #5DADE1;\n@white: #ffffff;\n\n/**\n * Borders\n */\n@base-border-radius: 4px;\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-ubuntu/README.md",
    "content": "# Ubuntu Theme for Nylas Mail #\n\n![img](https://raw.githubusercontent.com/ahmedlhanafy/Ubuntu-Ui-Theme-for-Nylas-N1/master/Screenshot.png)\n\n## Installation: ##\n\n* Download the zip folder and extract it.\n* Update N1 to the latest version go to Preferences -> General -> Select theme -> Install a theme and then select the extracted folder.\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-ubuntu/index.less",
    "content": ""
  },
  {
    "path": "packages/client-app/internal_packages/ui-ubuntu/package.json",
    "content": "{\n  \"name\": \"ui-ubuntu\",\n  \"displayName\": \"Ubuntu\",\n  \"theme\": \"ui\",\n  \"version\": \"0.1.0\",\n  \"description\": \"The Ubuntu theme for N1.\",\n  \"license\": \"Proprietary\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"private\": true\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-ubuntu/styles/theme-colors.less",
    "content": "@component-active-color: #f07746;\n@toolbar-background-color: #41403b;\n"
  },
  {
    "path": "packages/client-app/internal_packages/ui-ubuntu/styles/ui-variables.less",
    "content": "@import \"../../../static/variables/ui-variables\";\n\n@accent-primary:            #f07746;\n@accent-primary-dark:       darken(#f07746, 1%);\n\n@border-color-secondary:   lighten(@background-secondary, 10%);\n\n@toolbar-background-color: #41403b;\n@light:  rgb(246, 246, 246);\n\n.sheet-toolbar .btn-toolbar img.content-mask {\n  background-color: @light;\n}\n\n.sheet-toolbar-container {\n  background-image: -webkit-linear-gradient(top,@toolbar-background-color, darken(@toolbar-background-color,5%));\n  box-shadow: none;\n  .btn {\n    background: lighten(@toolbar-background-color,4%);\n  }\n  .btn.btn-toolbar {\n    color: @light;\n  }\n  .toolbar-activity .activity-toolbar-icon {\n    background: @light;\n  }\n}\n\n.sheet-toolbar .item-back img.content-mask{\n  background-color: @light;\n}\n\n.sheet-toolbar .item-back .item-back-title{\n  color: @light;\n}\n\n.sheet-toolbar .selection-bar {\n  .absolute {\n    border-color: lighten(@toolbar-background-color, 5%);\n    background-color: darken(@toolbar-background-color, 8%);\n\n    .inner {\n      .centered {\n        color: @light;\n      }\n    }\n  }\n}\n\n.btn-icon img.content-mask {\n  background-color:@light;\n  color: @light;\n}\n\n.btn-icon img.content-mask:hover {\n  background-color:@accent-primary;\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/undo-redo/lib/main.es6",
    "content": "import {ComponentRegistry, WorkspaceStore} from 'nylas-exports'\nimport UndoRedoThreadListToast from './undo-redo-thread-list-toast'\nimport UndoSendStore from './undo-send-store';\nimport UndoSendToast from './undo-send-toast';\n\n\nexport function activate() {\n  UndoSendStore.activate()\n  ComponentRegistry.register(UndoSendToast, {\n    location: WorkspaceStore.Sheet.Global.Footer,\n  });\n  if (NylasEnv.isMainWindow()) {\n    ComponentRegistry.register(UndoRedoThreadListToast, {\n      location: WorkspaceStore.Location.ThreadList,\n    })\n  }\n}\n\nexport function deactivate() {\n  UndoSendStore.deactivate()\n  ComponentRegistry.unregister(UndoSendToast);\n  if (NylasEnv.isMainWindow()) {\n    ComponentRegistry.unregister(UndoRedoThreadListToast)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/undo-redo/lib/undo-redo-thread-list-toast.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport {UndoRedoStore} from 'nylas-exports'\nimport {UndoToast, ListensToFluxStore} from 'nylas-component-kit'\n\n\nfunction onUndo() {\n  NylasEnv.commands.dispatch('core:undo')\n}\n\nfunction UndoRedoThreadListToast(props) {\n  const {tasks} = props\n  return (\n    <UndoToast\n      {...props}\n      onUndo={onUndo}\n      visibleDuration={3000}\n      className=\"undo-redo-thread-list-toast\"\n      undoMessage={tasks.map((t) => t.description()).join(', ')}\n    />\n  )\n}\n\nUndoRedoThreadListToast.displayName = 'UndoRedoThreadListToast'\nUndoRedoThreadListToast.containerRequired = false\nUndoRedoThreadListToast.propTypes = {\n  tasks: PropTypes.array,\n}\n\nexport default ListensToFluxStore(UndoRedoThreadListToast, {\n  stores: [UndoRedoStore],\n  getStateFromStores() {\n    const tasks = UndoRedoStore.getMostRecent()\n    return {\n      tasks,\n      visible: tasks && tasks.length > 0,\n    }\n  },\n})\n"
  },
  {
    "path": "packages/client-app/internal_packages/undo-redo/lib/undo-send-store.es6",
    "content": "import NylasStore from 'nylas-store'\nimport {Actions} from 'nylas-exports'\n\n\nclass UndoSendStore extends NylasStore {\n\n  activate() {\n    this._showUndoSend = false\n    this._sendActionTaskId = null\n    this._unlisteners = [\n      Actions.willPerformSendAction.listen(this._onWillPerformSendAction),\n      Actions.didPerformSendAction.listen(this._onDidPerformSendAction),\n      Actions.didCancelSendAction.listen(this._onDidCancelSendAction),\n    ]\n  }\n\n  shouldShowUndoSend() {\n    return this._showUndoSend\n  }\n\n  sendActionTaskId() {\n    return this._sendActionTaskId\n  }\n\n  _onWillPerformSendAction = ({taskId}) => {\n    this._showUndoSend = true\n    this._sendActionTaskId = taskId\n    this.trigger()\n  }\n\n  _onDidPerformSendAction = () => {\n    this._showUndoSend = false\n    this._sendActionTaskId = null\n    this.trigger()\n  }\n\n  _onDidCancelSendAction = () => {\n    this._showUndoSend = false\n    this._sendActionTaskId = null\n    this.trigger()\n  }\n\n  deactivate() {\n    this._unlisteners.forEach((unsub) => unsub())\n  }\n}\n\nexport default new UndoSendStore()\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/undo-redo/lib/undo-send-toast.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport {Actions} from 'nylas-exports'\nimport {KeyCommandsRegion, UndoToast, ListensToFluxStore} from 'nylas-component-kit'\nimport UndoSendStore from './undo-send-store'\n\n\nfunction UndoSendToast(props) {\n  const {visible, sendActionTaskId} = props\n  return (\n    <KeyCommandsRegion\n      globalHandlers={{\n        'core:undo': (event) => {\n          if (!visible) { return }\n          event.preventDefault();\n          event.stopPropagation();\n          Actions.dequeueTask(sendActionTaskId)\n        },\n      }}\n    >\n      <UndoToast\n        {...props}\n        className=\"undo-send-toast\"\n        undoMessage=\"Sending draft\"\n        visibleDuration={null}\n        onUndo={() => Actions.dequeueTask(sendActionTaskId)}\n      />\n    </KeyCommandsRegion>\n  )\n}\nUndoSendToast.displayName = 'UndoSendToast'\nUndoSendToast.propTypes = {\n  visible: PropTypes.bool,\n  sendActionTaskId: PropTypes.string,\n}\n\nexport default ListensToFluxStore(UndoSendToast, {\n  stores: [UndoSendStore],\n  getStateFromStores() {\n    return {\n      visible: UndoSendStore.shouldShowUndoSend(),\n      sendActionTaskId: UndoSendStore.sendActionTaskId(),\n    }\n  },\n})\n\n"
  },
  {
    "path": "packages/client-app/internal_packages/undo-redo/package.json",
    "content": "{\n  \"name\": \"undo-redo\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Undo modal button\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true,\n    \"thread-popout\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/unread-notifications/lib/main.es6",
    "content": "import _ from 'underscore'\nimport {\n  Thread,\n  Actions,\n  SoundRegistry,\n  NativeNotifications,\n  DatabaseStore,\n} from 'nylas-exports';\n\nexport class Notifier {\n  constructor() {\n    this.unlisteners = [];\n    this.unlisteners.push(Actions.onNewMailDeltas.listen(this._onNewMailReceived, this));\n    this.activationTime = Date.now();\n    this.unnotifiedQueue = [];\n    this.hasScheduledNotify = false;\n\n    this.activeNotifications = {};\n    this.unlisteners.push(DatabaseStore.listen(this._onDatabaseUpdated, this));\n  }\n\n  unlisten() {\n    for (const unlisten of this.unlisteners) {\n      unlisten();\n    }\n  }\n\n  _onDatabaseUpdated({objectClass, objects}) {\n    if (objectClass === 'Thread') {\n      objects\n        .filter((thread) => !thread.unread)\n        .forEach((thread) => this._onThreadIsRead(thread));\n    }\n  }\n\n  _onThreadIsRead({id: threadId}) {\n    if (threadId in this.activeNotifications) {\n      this.activeNotifications[threadId].forEach((n) => n.close());\n      delete this.activeNotifications[threadId];\n    }\n  }\n\n  _notifyAll() {\n    NativeNotifications.displayNotification({\n      title: `${this.unnotifiedQueue.length} Unread Messages`,\n      tag: 'unread-update',\n    });\n    this.unnotifiedQueue = [];\n  }\n\n  _notifyOne({message, thread}) {\n    const from = (message.from[0]) ? message.from[0].displayName() : \"Unknown\";\n    const title = from;\n    let subtitle = null;\n    let body = null;\n    if (message.subject && message.subject.length > 0) {\n      subtitle = message.subject;\n      body = message.snippet;\n    } else {\n      subtitle = message.snippet\n      body = null\n    }\n\n    const notification = NativeNotifications.displayNotification({\n      title: title,\n      subtitle: subtitle,\n      body: body,\n      canReply: true,\n      tag: 'unread-update',\n      onActivate: ({response, activationType}) => {\n        if ((activationType === 'replied') && response && _.isString(response)) {\n          Actions.sendQuickReply({thread, message}, response);\n        } else {\n          NylasEnv.displayWindow()\n        }\n\n        if (!thread) {\n          NylasEnv.showErrorDialog(`Can't find that thread`)\n          return\n        }\n        Actions.ensureCategoryIsFocused('inbox', thread.accountId);\n        Actions.setFocus({collection: 'thread', item: thread});\n      },\n    });\n\n    if (!this.activeNotifications[thread.id]) {\n      this.activeNotifications[thread.id] = [notification];\n    } else {\n      this.activeNotifications[thread.id].push(notification);\n    }\n  }\n\n  _notifyMessages() {\n    if (this.unnotifiedQueue.length >= 5) {\n      this._notifyAll()\n    } else if (this.unnotifiedQueue.length > 0) {\n      this._notifyOne(this.unnotifiedQueue.shift());\n    }\n\n    this.hasScheduledNotify = false;\n    if (this.unnotifiedQueue.length > 0) {\n      setTimeout(() => this._notifyMessages(), 2000);\n      this.hasScheduledNotify = true;\n    }\n  }\n\n  // https://phab.nylas.com/D2188\n  _onNewMessagesMissingThreads(messages) {\n    setTimeout(() => {\n      const threads = {}\n      for (const {threadId} of messages) {\n        threads[threadId] = threads[threadId] || DatabaseStore.find(Thread, threadId);\n      }\n      Promise.props(threads).then((resolvedThreads) => {\n        const resolved = messages.filter((msg) => resolvedThreads[msg.threadId]);\n        if (resolved.length > 0) {\n          this._onNewMailReceived({message: resolved, thread: _.values(resolvedThreads)});\n        }\n      });\n    }, 10000);\n  }\n\n  _onNewMailReceived(incoming) {\n    return new Promise((resolve) => {\n      if (NylasEnv.config.get('core.notifications.enabled') === false) {\n        resolve();\n        return;\n      }\n\n      const incomingMessages = incoming.message || [];\n      const incomingThreads = incoming.thread || [];\n\n      // Filter for new messages that are not sent by the current user\n      const newUnread = incomingMessages.filter((msg) => {\n        const isUnread = msg.unread === true;\n        const isNew = msg.date && msg.date.valueOf() >= this.activationTime;\n        const isFromMe = msg.isFromMe();\n        return isUnread && isNew && !isFromMe;\n      });\n\n      if (newUnread.length === 0) {\n        resolve();\n        return;\n      }\n\n      // For each message, find it's corresponding thread. First, look to see\n      // if it's already in the `incoming` payload (sent via delta sync\n      // at the same time as the message.) If it's not, try loading it from\n      // the local cache.\n\n      // Note we may receive multiple unread msgs for the same thread.\n      // Using a map and ?= to avoid repeating work.\n      const threads = {}\n      for (const {threadId} of newUnread) {\n        threads[threadId] = threads[threadId] || _.findWhere(incomingThreads, {id: threadId})\n        threads[threadId] = threads[threadId] || DatabaseStore.find(Thread, threadId);\n      }\n\n      Promise.props(threads).then((resolvedThreads) => {\n        // Filter new unread messages to just the ones in the inbox\n        const newUnreadInInbox = newUnread.filter((msg) =>\n          resolvedThreads[msg.threadId] && resolvedThreads[msg.threadId].categoryNamed('inbox')\n        )\n\n        // Filter messages that we can't decide whether to display or not\n        // since no associated Thread object has arrived yet.\n        const newUnreadMissingThreads = newUnread.filter((msg) => !resolvedThreads[msg.threadId])\n\n        if (newUnreadMissingThreads.length > 0) {\n          this._onNewMessagesMissingThreads(newUnreadMissingThreads);\n        }\n\n        if (newUnreadInInbox.length === 0) {\n          resolve();\n          return;\n        }\n\n        for (const msg of newUnreadInInbox) {\n          this.unnotifiedQueue.push({message: msg, thread: resolvedThreads[msg.threadId]});\n        }\n        if (!this.hasScheduledNotify) {\n          if (NylasEnv.config.get(\"core.notifications.sounds\")) {\n            this._playNewMailSound = this._playNewMailSound || _.debounce(() => SoundRegistry.playSound('new-mail'), 5000, true);\n            this._playNewMailSound();\n          }\n          this._notifyMessages();\n        }\n\n        resolve();\n      });\n    });\n  }\n}\n\nexport const config = {\n  enabled: {\n    'type': 'boolean',\n    'default': true,\n  },\n};\n\nexport function activate() {\n  this.notifier = new Notifier();\n}\n\nexport function deactivate() {\n  this.notifier.unlisten();\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/unread-notifications/package.json",
    "content": "{\n  \"name\": \"unread-notifications\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Fires notifications when new mail is received\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/unread-notifications/spec/main-spec.es6",
    "content": "import Contact from '../../../src/flux/models/contact'\nimport Message from '../../../src/flux/models/message'\nimport Thread from '../../../src/flux/models/thread'\nimport Category from '../../../src/flux/models/category'\nimport CategoryStore from '../../../src/flux/stores/category-store'\nimport DatabaseStore from '../../../src/flux/stores/database-store'\nimport AccountStore from '../../../src/flux/stores/account-store'\nimport SoundRegistry from '../../../src/registries/sound-registry'\nimport NativeNotifications from '../../../src/native-notifications'\nimport {Notifier} from '../lib/main'\n\nxdescribe(\"UnreadNotifications\", function UnreadNotifications() {\n  beforeEach(() => {\n    this.notifier = new Notifier();\n\n    const inbox = new Category({id: \"l1\", name: \"inbox\", displayName: \"Inbox\"})\n    const archive = new Category({id: \"l2\", name: \"archive\", displayName: \"Archive\"})\n\n    spyOn(CategoryStore, \"getStandardCategory\").andReturn(inbox);\n\n    const account = AccountStore.accounts()[0];\n\n    this.threadA = new Thread({\n      id: 'A',\n      categories: [inbox],\n    });\n    this.threadB = new Thread({\n      id: 'B',\n      categories: [archive],\n    });\n\n    this.msg1 = new Message({\n      unread: true,\n      date: new Date(),\n      from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],\n      subject: \"Hello World\",\n      threadId: \"A\",\n    });\n    this.msgNoSender = new Message({\n      unread: true,\n      date: new Date(),\n      from: [],\n      subject: \"Hello World\",\n      threadId: \"A\",\n    });\n    this.msg2 = new Message({\n      unread: true,\n      date: new Date(),\n      from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],\n      subject: \"Hello World 2\",\n      threadId: \"A\",\n    });\n    this.msg3 = new Message({\n      unread: true,\n      date: new Date(),\n      from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],\n      subject: \"Hello World 3\",\n      threadId: \"A\",\n    });\n    this.msg4 = new Message({\n      unread: true,\n      date: new Date(),\n      from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],\n      subject: \"Hello World 4\",\n      threadId: \"A\",\n    });\n    this.msg5 = new Message({\n      unread: true,\n      date: new Date(),\n      from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],\n      subject: \"Hello World 5\",\n      threadId: \"A\",\n    });\n    this.msgUnreadButArchived = new Message({\n      unread: true,\n      date: new Date(),\n      from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],\n      subject: \"Hello World 2\",\n      threadId: \"B\",\n    });\n    this.msgRead = new Message({\n      unread: false,\n      date: new Date(),\n      from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],\n      subject: \"Hello World Read Already\",\n      threadId: \"A\",\n    });\n    this.msgOld = new Message({\n      unread: true,\n      date: new Date(2000, 1, 1),\n      from: [new Contact({name: 'Mark', email: 'markthis.example.com'})],\n      subject: \"Hello World Old\",\n      threadId: \"A\",\n    });\n    this.msgFromMe = new Message({\n      unread: true,\n      date: new Date(),\n      from: [account.me()],\n      subject: \"A Sent Mail!\",\n      threadId: \"A\",\n    });\n\n    spyOn(DatabaseStore, 'find').andCallFake((klass, id) => {\n      if (id === 'A') {\n        return Promise.resolve(this.threadA);\n      }\n      if (id === 'B') {\n        return Promise.resolve(this.threadB);\n      }\n      return Promise.resolve(null);\n    });\n\n    this.notification = jasmine.createSpyObj('notification', ['close']);\n    spyOn(NativeNotifications, 'displayNotification').andReturn(this.notification);\n\n    spyOn(Promise, 'props').andCallFake((dict) => {\n      const dictOut = {};\n      for (const key of Object.keys(dict)) {\n        const val = dict[key];\n        if (val.value !== undefined) {\n          dictOut[key] = val.value();\n        } else {\n          dictOut[key] = val;\n        }\n      }\n      return Promise.resolve(dictOut);\n    });\n  });\n\n  afterEach(() => {\n    this.notifier.unlisten();\n  })\n\n  it(\"should create a Notification if there is one unread message\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msgRead, this.msg1]})\n      .then(() => {\n        advanceClock(2000)\n        expect(NativeNotifications.displayNotification).toHaveBeenCalled()\n        const options = NativeNotifications.displayNotification.mostRecentCall.args[0]\n        delete options.onActivate;\n        expect(options).toEqual({\n          title: 'Ben',\n          subtitle: 'Hello World',\n          body: undefined,\n          canReply: true,\n          tag: 'unread-update',\n        });\n      });\n    });\n  });\n\n  it(\"should create multiple Notifications if there is more than one but less than five unread messages\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msg1, this.msg2, this.msg3]})\n      .then(() => {\n        // Need to call advance clock twice because we call setTimeout twice\n        advanceClock(2000)\n        advanceClock(2000)\n        expect(NativeNotifications.displayNotification.callCount).toEqual(3)\n      });\n    });\n  });\n\n  it(\"should create Notifications in the order of messages received\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msg1, this.msg2]})\n      .then(() => {\n        advanceClock(2000);\n        return this.notifier._onNewMailReceived({message: [this.msg3, this.msg4]});\n      })\n      .then(() => {\n        advanceClock(2000);\n        advanceClock(2000);\n        expect(NativeNotifications.displayNotification.callCount).toEqual(4);\n        const subjects = NativeNotifications.displayNotification.calls.map((call) => {\n          return call.args[0].subtitle;\n        });\n        const expected = [this.msg1, this.msg2, this.msg3, this.msg4]\n          .map((msg) => msg.subject);\n        expect(subjects).toEqual(expected);\n      });\n    });\n  });\n\n  it(\"should create a Notification if there are five or more unread messages\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({\n        message: [this.msg1, this.msg2, this.msg3, this.msg4, this.msg5]})\n      .then(() => {\n        advanceClock(2000)\n        expect(NativeNotifications.displayNotification).toHaveBeenCalled()\n        expect(NativeNotifications.displayNotification.mostRecentCall.args).toEqual([{\n          title: '5 Unread Messages',\n          tag: 'unread-update',\n        }])\n      });\n    });\n  });\n\n  it(\"should create a Notification correctly, even if new mail has no sender\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msgNoSender]})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).toHaveBeenCalled()\n\n        const options = NativeNotifications.displayNotification.mostRecentCall.args[0]\n        delete options.onActivate;\n        expect(options).toEqual({\n          title: 'Unknown',\n          subtitle: 'Hello World',\n          body: undefined,\n          canReply: true,\n          tag: 'unread-update',\n        })\n      });\n    });\n  });\n\n  it(\"should not create a Notification if there are no new messages\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: []})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()\n      });\n    });\n\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()\n      });\n    });\n  });\n\n  it(\"should not notify about unread messages that are outside the inbox\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msgUnreadButArchived, this.msg1]})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).toHaveBeenCalled()\n        const options = NativeNotifications.displayNotification.mostRecentCall.args[0]\n        delete options.onActivate;\n        expect(options).toEqual({\n          title: 'Ben',\n          subtitle: 'Hello World',\n          body: undefined,\n          canReply: true,\n          tag: 'unread-update',\n        })\n      });\n    });\n  });\n\n  it(\"should not create a Notification if the new messages are read\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msgRead]})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()\n      });\n    });\n  });\n\n  it(\"should not create a Notification if the new messages are actually old ones\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msgOld]})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()\n      });\n    });\n  });\n\n  it(\"should not create a Notification if the new message is one I sent\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msgFromMe]})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()\n      });\n    });\n  });\n\n  it(\"clears notifications when a thread is read\", () => {\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msg1]})\n      .then(() => {\n        expect(NativeNotifications.displayNotification).toHaveBeenCalled();\n        expect(this.notification.close).not.toHaveBeenCalled();\n        this.notifier._onThreadIsRead(this.threadA);\n        expect(this.notification.close).toHaveBeenCalled();\n      });\n    });\n  });\n\n  it(\"detects changes that may be a thread being read\", () => {\n    const unreadThread = { unread: true };\n    const readThread = { unread: false };\n    spyOn(this.notifier, '_onThreadIsRead');\n    this.notifier._onDatabaseUpdated({ objectClass: 'Thread', objects: [unreadThread, readThread]});\n    expect(this.notifier._onThreadIsRead.calls.length).toEqual(1);\n    expect(this.notifier._onThreadIsRead).toHaveBeenCalledWith(readThread);\n  });\n\n  it(\"should play a sound when it gets new mail\", () => {\n    spyOn(NylasEnv.config, \"get\").andCallFake((config) => {\n      if (config === \"core.notifications.enabled\") return true\n      if (config === \"core.notifications.sounds\") return true\n      return undefined;\n    });\n\n    spyOn(SoundRegistry, \"playSound\");\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msg1]})\n      .then(() => {\n        expect(NylasEnv.config.get.calls[1].args[0]).toBe(\"core.notifications.sounds\");\n        expect(SoundRegistry.playSound).toHaveBeenCalledWith(\"new-mail\");\n      });\n    });\n  });\n\n  it(\"should not play a sound if the config is off\", () => {\n    spyOn(NylasEnv.config, \"get\").andCallFake((config) => {\n      if (config === \"core.notifications.enabled\") return true;\n      if (config === \"core.notifications.sounds\") return false;\n      return undefined;\n    });\n    spyOn(SoundRegistry, \"playSound\")\n    waitsForPromise(() => {\n      return this.notifier._onNewMailReceived({message: [this.msg1]})\n      .then(() => {\n        expect(NylasEnv.config.get.calls[1].args[0]).toBe(\"core.notifications.sounds\");\n        expect(SoundRegistry.playSound).not.toHaveBeenCalled()\n      });\n    });\n  });\n\n  it(\"should not play a sound if other notiications are still in flight\", () => {\n    spyOn(NylasEnv.config, \"get\").andCallFake((config) => {\n      if (config === \"core.notifications.enabled\") return true;\n      if (config === \"core.notifications.sounds\") return true;\n      return undefined;\n    });\n    waitsForPromise(() => {\n      spyOn(SoundRegistry, \"playSound\")\n      return this.notifier._onNewMailReceived({message: [this.msg1, this.msg2]}).then(() => {\n        expect(SoundRegistry.playSound).toHaveBeenCalled();\n        SoundRegistry.playSound.reset();\n        return this.notifier._onNewMailReceived({message: [this.msg3]}).then(() => {\n          expect(SoundRegistry.playSound).not.toHaveBeenCalled();\n        });\n      });\n    });\n  });\n\n  describe(\"when the message has no matching thread\", () => {\n    beforeEach(() => {\n      this.msgNoThread = new Message({\n        unread: true,\n        date: new Date(),\n        from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],\n        subject: \"Hello World\",\n        threadId: \"missing\",\n      });\n    });\n\n    it(\"should not create a Notification, since it cannot be determined whether the message is in the Inbox\", () => {\n      waitsForPromise(() => {\n        return this.notifier._onNewMailReceived({message: [this.msgNoThread]})\n        .then(() => {\n          advanceClock(2000)\n          expect(NativeNotifications.displayNotification).not.toHaveBeenCalled()\n        });\n      });\n    });\n\n    it(\"should call _onNewMessagesMissingThreads to try displaying a notification again in 10 seconds\", () => {\n      waitsForPromise(() => {\n        spyOn(this.notifier, '_onNewMessagesMissingThreads')\n        return this.notifier._onNewMailReceived({message: [this.msgNoThread]})\n        .then(() => {\n          advanceClock(2000)\n          expect(this.notifier._onNewMessagesMissingThreads).toHaveBeenCalledWith([this.msgNoThread])\n        });\n      });\n    });\n  });\n\n  describe(\"_onNewMessagesMissingThreads\", () => {\n    beforeEach(() => {\n      this.msgNoThread = new Message({\n        unread: true,\n        date: new Date(),\n        from: [new Contact({name: 'Ben', email: 'benthis.example.com'})],\n        subject: \"Hello World\",\n        threadId: \"missing\",\n      });\n      spyOn(this.notifier, '_onNewMailReceived')\n      this.notifier._onNewMessagesMissingThreads([this.msgNoThread])\n      advanceClock(2000)\n    });\n\n    it(\"should wait 10 seconds and then re-query for threads\", () => {\n      expect(DatabaseStore.find).not.toHaveBeenCalled()\n      this.msgNoThread.threadId = \"A\"\n      advanceClock(10000)\n      expect(DatabaseStore.find).toHaveBeenCalled()\n      advanceClock()\n      expect(this.notifier._onNewMailReceived).toHaveBeenCalledWith({message: [this.msgNoThread], thread: [this.threadA]})\n    });\n\n    it(\"should do nothing if the threads still can't be found\", () => {\n      expect(DatabaseStore.find).not.toHaveBeenCalled()\n      advanceClock(10000)\n      expect(DatabaseStore.find).toHaveBeenCalled()\n      advanceClock()\n      expect(this.notifier._onNewMailReceived).not.toHaveBeenCalled()\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/internal_packages/verify-install-location/lib/main.es6",
    "content": "import {ipcRenderer, remote} from 'electron'\n\n/**\n * We want to make sure that people have installed the app in a\n * reasonable location.\n *\n * On the Mac, you can accidentally run the app from the DMG. If you do\n * this, it will no longer auto-update. It's also common for Mac users to\n * leave their app in the /Downloads folder (which frequently gets\n * erased!).\n */\n\nfunction onDialogActionTaken(numAsks) {\n  return (buttonIndex) => {\n    if (numAsks >= 1) {\n      if (buttonIndex === 1) {\n        NylasEnv.config.set(\"asksAboutAppMove\", 5)\n      } else {\n        NylasEnv.config.set(\"asksAboutAppMove\", numAsks + 1)\n      }\n    } else {\n      NylasEnv.config.set(\"asksAboutAppMove\", numAsks + 1)\n    }\n  }\n}\n\nexport function activate() {\n  if (NylasEnv.inDevMode() || NylasEnv.inSpecMode()) { return; }\n\n  if (process.platform !== \"darwin\") { return; }\n\n  const appRe = /Applications/gi;\n  if (appRe.test(process.argv[0])) { return; }\n\n  // If we're in Volumes, that means we've launched from the DMG. This\n  // is unsupported. We should optimistically move.\n  const volTest = /Volumes/gi;\n  if (volTest.test(process.argv[0])) {\n    ipcRenderer.send(\"move-to-applications\");\n    return;\n  }\n\n  const numAsks = NylasEnv.config.get(\"asksAboutAppMove\") || 0\n  if (numAsks <= 0) {\n    NylasEnv.config.set(\"asksAboutAppMove\", 1)\n    return;\n  }\n\n  NylasEnv.config.set(\"asksAboutAppMove\", numAsks + 1)\n  if (numAsks >= 5) return;\n\n  let buttons;\n  if (numAsks >= 1) {\n    buttons = [\n      \"Okay\",\n      \"Don't ask again\",\n    ]\n  } else {\n    buttons = [\n      \"Okay\",\n    ]\n  }\n\n  const msg = `We recommend that you move Nylas Mail to your Applications folder to get updates correctly and keep this folder uncluttered.`\n\n  const CANCEL_ID = 0;\n\n  remote.dialog.showMessageBox({\n    type: \"warning\",\n    buttons: buttons,\n    title: \"A Better Place to Install Nylas Mail\",\n    message: \"Please move Nylas Mail to your Applications folder\",\n    detail: msg,\n    defaultId: 0,\n    cancelId: CANCEL_ID,\n  }, onDialogActionTaken(numAsks))\n}\n\nexport function deactivate() {\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/verify-install-location/package.json",
    "content": "{\n  \"name\": \"verify-install-location\",\n  \"main\": \"./lib/main\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Verifies the install location for N1\",\n  \"license\": \"GPL-3.0\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"default\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/lib/developer-bar-curl-item.cjsx",
    "content": "classNames = require 'classnames'\nReact = require 'react'\n\nclass DeveloperBarCurlItem extends React.Component\n  @displayName: 'DeveloperBarCurlItem'\n\n  render: =>\n    classes = classNames\n      \"item\": true\n      \"error-code\": @_isError()\n    <div className={classes}>\n      <div className=\"code\">{@props.item.statusCode}{@_errorMessage()}</div>\n      <span className=\"timestamp\">{@props.item.startMoment.format(\"HH:mm:ss\")}&nbsp;&nbsp;</span>\n      <a onClick={@_onRunCommand}>Run</a>\n      <a onClick={@_onCopyCommand}>Copy</a>\n      {@props.item.command}\n    </div>\n\n  shouldComponentUpdate: (nextProps) =>\n    return @props.item isnt nextProps.item\n\n  _onCopyCommand: =>\n    clipboard = require('electron').clipboard\n    clipboard.writeText(@props.item.commandWithAuth)\n\n  _isError: ->\n    return false if @props.item.statusCode is \"pending\"\n    return not (parseInt(@props.item.statusCode) <= 399)\n\n  _errorMessage: ->\n    if (@props.item.errorMessage ? \"\").length > 0\n      return \" | #{@props.item.errorMessage}\"\n    else\n      return \"\"\n\n  _onRunCommand: =>\n    curlFile = \"#{NylasEnv.getConfigDirPath()}/curl.command\"\n    fs = require 'fs-plus'\n    if fs.existsSync(curlFile)\n      fs.unlinkSync(curlFile)\n    fs.writeFileSync(curlFile, @props.item.commandWithAuth)\n    fs.chmodSync(curlFile, '777')\n    {shell} = require 'electron'\n    shell.openItem(curlFile)\n\n\nmodule.exports = DeveloperBarCurlItem\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/lib/developer-bar-long-poll-item.cjsx",
    "content": "React = require 'react'\nmoment = require 'moment'\n{DateUtils, Utils} = require 'nylas-exports'\n\nclass DeveloperBarLongPollItem extends React.Component\n  @displayName: 'DeveloperBarLongPollItem'\n\n  constructor: (@props) ->\n    @state = expanded: false\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    return not Utils.isEqualReact(nextProps, @props) or not Utils.isEqualReact(nextState, @state)\n\n  render: =>\n    if @state.expanded\n      payload = JSON.stringify(@props.item, null, 2)\n    else\n      payload = []\n\n    itemId = @props.item.id\n    itemVersion = @props.item.version || @props.item.attributes?.version\n    itemId += \" (version #{itemVersion})\" if itemVersion\n\n    timeFormat = DateUtils.getTimeFormat { seconds: true }\n    timestamp = moment(@props.item.timestamp).format(timeFormat)\n\n    classname = \"item\"\n    right = @props.item.cursor\n\n    if @props.ignoredBecause\n      classname += \" ignored\"\n      right = @props.ignoredBecause + \" - \" + right\n\n    <div className={classname} onClick={ => @setState expanded: not @state?.expanded}>\n      <div className=\"cursor\">{right}</div>\n      {\" #{timestamp}: #{@props.item.event} #{@props.item.object} #{itemId}\"}\n      <div className=\"payload\" onClick={ (e) -> e.stopPropagation() }>\n        {payload}\n      </div>\n    </div>\n\n\n\nmodule.exports = DeveloperBarLongPollItem\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/lib/developer-bar-store.coffee",
    "content": "NylasStore = require 'nylas-store'\n{Rx, Actions, DatabaseStore, ProviderSyncbackRequest, DeltaConnectionStore} = require 'nylas-exports'\nqs = require 'querystring'\n_ = require 'underscore'\nmoment = require 'moment'\n\nclass DeveloperBarCurlRequest\n  constructor: ({@id, request, statusCode, error}) ->\n    url = request.url\n    urlWithAuth = url\n    if request.auth and (request.auth.user || request.auth.pass)\n      urlWithAuth = url.replace('://', \"://#{request.auth.user ? \"\"}:#{request.auth.pass ? \"\"}@\")\n\n    if request.qs\n      url += \"?#{qs.stringify(request.qs)}\"\n      urlWithAuth += \"?#{qs.stringify(request.qs)}\"\n\n    postBody = \"\"\n    postBody = JSON.stringify(request.body).replace(/'/g, '\\\\u0027') if request.body\n\n    data = \"\"\n    data = \"-d '#{postBody}'\" unless request.method == 'GET'\n\n    headers = \"\"\n    if request.headers\n      for k,v of request.headers\n        headers += \"-H \\\"#{k}: #{v}\\\" \"\n\n    # When constructed during _onWillMakeAPIRequest(), `request` has not been\n    # processed by node-request yet. Therefore, it will not have Content-Type\n    # set in the request headers.\n    if (request.json and not request._json and\n        request.headers and\n        'content-type' not in request.headers and\n        'Content-Type' not in request.headers)\n      headers += '-H \"Content-Type: application\\/json\" '\n\n    if request.auth?.bearer\n      tok = request.auth.bearer.replace(\"!\", \"\\\\!\")\n      headers += \"-H \\\"Authorization: Bearer #{tok}\\\" \"\n\n    baseCommand = \"curl -X #{request.method} #{headers}#{data}\"\n    @command = baseCommand + \" \\\"#{url}\\\"\"\n    @commandWithAuth = baseCommand + \" \\\"#{urlWithAuth}\\\"\"\n    @statusCode = statusCode ? error?.code ? \"pending\"\n    @errorMessage = error?.message ? error\n    @startMoment = moment(request.startTime)\n    @\n\nclass DeveloperBarStore extends NylasStore\n  constructor: ->\n    @_setStoreDefaults()\n    @_registerListeners()\n\n  ########### PUBLIC #####################################################\n\n  curlHistory: -> @_curlHistory\n\n  longPollStates: -> @_longPollStates\n\n  longPollHistory: -> @_longPollHistory\n\n  providerSyncbackRequests: -> @_providerSyncbackRequests\n\n  ########### PRIVATE ####################################################\n\n  triggerThrottled: ->\n    @_triggerThrottled ?= _.throttle(@trigger, 150)\n    @_triggerThrottled()\n\n  _setStoreDefaults: ->\n    @_curlHistoryIds = []\n    @_curlHistory = []\n    @_longPollHistory = []\n    @_longPollStates = {}\n    @_providerSyncbackRequests = []\n\n  _registerListeners: ->\n    query = DatabaseStore.findAll(ProviderSyncbackRequest)\n      .order(ProviderSyncbackRequest.attributes.id.descending())\n      .limit(100)\n    Rx.Observable.fromQuery(query).subscribe(@_onSyncbackRequestChange)\n    @listenTo DeltaConnectionStore, @_onDeltaConnectionStatusChanged\n    @listenTo Actions.willMakeAPIRequest, @_onWillMakeAPIRequest\n    @listenTo Actions.didMakeAPIRequest, @_onDidMakeAPIRequest\n    @listenTo Actions.longPollReceivedRawDeltas, @_onLongPollDeltas\n    @listenTo Actions.longPollProcessedDeltas, @_onLongPollProcessedDeltas\n    @listenTo Actions.clearDeveloperConsole, @_onClear\n\n  _onClear: ->\n    @_curlHistoryIds = []\n    @_curlHistory = []\n    @_longPollHistory = []\n    @trigger(@)\n\n  _onSyncbackRequestChange: (reqs = []) =>\n    @_providerSyncbackRequests = reqs\n    @trigger()\n\n  _onDeltaConnectionStatusChanged: ->\n    @_longPollStates = {}\n    _.forEach DeltaConnectionStore.getDeltaConnectionStates(), (state, accountId) =>\n      @_longPollStates[accountId] = state.status\n    @trigger()\n\n  _onLongPollDeltas: (deltas) ->\n    # Add a local timestamp to deltas so we can display it\n    now = new Date()\n    delta.timestamp = now for delta in deltas\n\n    # Incoming deltas are [oldest...newest]. Append them to the beginning\n    # of our internal history which is [newest...oldest]\n    @_longPollHistory.unshift([].concat(deltas).reverse()...)\n    if @_longPollHistory.length > 200\n      @_longPollHistory.length = 200\n    @triggerThrottled(@)\n\n  _onLongPollProcessedDeltas: ->\n    @triggerThrottled(@)\n\n  _onWillMakeAPIRequest: ({requestId, request}) =>\n    item = new DeveloperBarCurlRequest({id: requestId, request})\n\n    @_curlHistory.unshift(item)\n    @_curlHistoryIds.unshift(requestId)\n    if @_curlHistory.length > 200\n      @_curlHistory.pop()\n      @_curlHistoryIds.pop()\n\n    @triggerThrottled(@)\n\n  _onDidMakeAPIRequest: ({requestId, request, statusCode, error}) =>\n    idx = @_curlHistoryIds.indexOf(requestId)\n    return if idx is -1 # Could be more than 200 requests ago\n\n    item = new DeveloperBarCurlRequest({id: requestId, request, statusCode, error})\n    @_curlHistory[idx] = item\n    @triggerThrottled(@)\n\nmodule.exports = new DeveloperBarStore()\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/lib/developer-bar-task.cjsx",
    "content": "React = require 'react'\nclassNames = require 'classnames'\n_ = require 'underscore'\n{Utils} = require 'nylas-exports'\n\nclass DeveloperBarTask extends React.Component\n  @displayName: 'DeveloperBarTask'\n\n  constructor: (@props) ->\n    @state =\n      expanded: false\n\n  render: =>\n    details = false\n    if @state.expanded\n      # This could be a potentially large amount of JSON.\n      # Do not render unless it's actually being displayed!\n      details = <div className=\"task-details\">{JSON.stringify(@props.task.toJSON(), null, 2)}</div>\n\n    <div className={@_classNames()} onClick={=> @setState(expanded: not @state.expanded)}>\n      <div className=\"task-summary\">\n        {@_taskSummary()}\n      </div>\n      {details}\n    </div>\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    return not Utils.isEqualReact(nextProps, @props) or not Utils.isEqualReact(nextState, @state)\n\n  _taskSummary: =>\n    qs = @props.task.queueState\n    errType = \"\"\n    errCode = \"\"\n    errMessage = \"\"\n    if qs.localError?\n      localError = qs.localError\n      errType = localError.constructor.name\n      errMessage = localError.message ? JSON.stringify(localError)\n    else if qs.remoteError?\n      remoteError = qs.remoteError\n      errType = remoteError.constructor.name\n      errCode = remoteError.statusCode ? \"\"\n      errMessage = remoteError.body?.message ? remoteError?.message ? JSON.stringify(remoteError)\n\n    id = @props.task.id[-4..-1]\n\n    if qs.status\n      status = \"#{qs.status} (#{qs.debugStatus})\"\n    else\n      status = \"#{qs.debugStatus}\"\n\n    return \"#{@props.task.constructor.name} (ID: #{id}) #{status} #{errType} #{errCode} #{errMessage}\"\n\n  _classNames: =>\n    qs = @props.task.queueState ? {}\n    classNames\n      \"task\": true\n      \"task-queued\": @props.type is \"queued\"\n      \"task-completed\": @props.type is \"completed\"\n      \"task-expanded\": @state.expanded\n      \"task-local-error\": qs.localError\n      \"task-remote-error\": qs.remoteError\n      \"task-is-processing\": qs.isProcessing\n      \"task-success\": qs.localComplete and qs.remoteComplete\n\n\nmodule.exports = DeveloperBarTask\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/lib/developer-bar.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\n{DatabaseStore,\n AccountStore,\n TaskQueue,\n Actions,\n Contact,\n Utils,\n Message} = require 'nylas-exports'\n{InjectedComponentSet} = require 'nylas-component-kit'\n\nDeveloperBarStore = require './developer-bar-store'\nDeveloperBarTask = require './developer-bar-task'\nDeveloperBarCurlItem = require './developer-bar-curl-item'\nDeveloperBarLongPollItem = require './developer-bar-long-poll-item'\n\n\nclass DeveloperBar extends React.Component\n  @displayName: \"DeveloperBar\"\n\n  @containerRequired: false\n\n  constructor: (@props) ->\n    @state = _.extend @_getStateFromStores(),\n      section: 'curl'\n      filter: ''\n\n  componentDidMount: =>\n    @taskQueueUnsubscribe = TaskQueue.listen @_onChange\n    @activityStoreUnsubscribe = DeveloperBarStore.listen @_onChange\n\n  componentWillUnmount: =>\n    @taskQueueUnsubscribe() if @taskQueueUnsubscribe\n    @activityStoreUnsubscribe() if @activityStoreUnsubscribe\n\n  render: =>\n    <div className=\"developer-bar\">\n      <div className=\"controls\">\n        <div className=\"btn-container pull-left\">\n          <div className=\"btn\" onClick={ => @_onExpandSection('queue')}>\n            <span>Client Tasks ({@state.queue?.length})</span>\n          </div>\n        </div>\n        <div className=\"btn-container pull-left\">\n          <div className=\"btn\" onClick={ => @_onExpandSection('providerSyncbackRequests')}>\n            <span>Provider Syncback Requests</span>\n          </div>\n        </div>\n        <div className=\"btn-container pull-left\">\n          <div className=\"btn\" onClick={ => @_onExpandSection('long-polling')}>\n            {@_renderDeltaStates()}\n            <span>Cloud Deltas</span>\n          </div>\n        </div>\n        <div className=\"btn-container pull-left\">\n          <div className=\"btn\" onClick={ => @_onExpandSection('curl')}>\n            <span>Requests: {@state.curlHistory.length}</span>\n          </div>\n        </div>\n        <div className=\"btn-container pull-left\">\n          <div className=\"btn\" onClick={ => @_onExpandSection('local-sync')}>\n            <span>Local Sync Engine</span>\n          </div>\n        </div>\n      </div>\n      {@_sectionContent()}\n      <div className=\"footer\">\n        <div className=\"btn\" onClick={@_onClear}>Clear</div>\n        <input className=\"filter\" placeholder=\"Filter...\" value={@state.filter} onChange={@_onFilter} />\n      </div>\n    </div>\n\n  _renderDeltaStates: =>\n    _.map @state.longPollStates, (status, accountId) =>\n      <div className=\"delta-state-wrap\" key={accountId} >\n        <div title={\"Account #{accountId} - Cloud State: #{status}\"} key={\"#{accountId}-n1Cloud\"} className={\"activity-status-bubble state-\" + status}></div>\n      </div>\n\n  _sectionContent: =>\n    expandedDiv = <div></div>\n\n    matchingFilter = (item) =>\n      return true if @state.filter is ''\n      return JSON.stringify(item).indexOf(@state.filter) >= 0\n\n    if @state.section == 'curl'\n      itemDivs = @state.curlHistory.filter(matchingFilter).map (item) ->\n        <DeveloperBarCurlItem item={item} key={item.id}/>\n      expandedDiv = <div className=\"expanded-section curl-history\">{itemDivs}</div>\n\n    else if @state.section == 'long-polling'\n      itemDivs = @state.longPollHistory.filter(matchingFilter).map (item) ->\n        <DeveloperBarLongPollItem item={item} ignoredBecause={item.ignoredBecause} key={\"#{item.cursor}-#{item.timestamp}\"}/>\n      expandedDiv = <div className=\"expanded-section long-polling\">{itemDivs}</div>\n\n    else if @state.section == 'local-sync'\n      expandedDiv = <div className=\"expanded-section local-sync\">\n        <InjectedComponentSet matching={{role: \"Developer:LocalSyncUI\"}} />\n      </div>\n\n    else if @state.section == 'providerSyncbackRequests'\n      reqs = @state.providerSyncbackRequests.map (req) =>\n        <div key={req.id}>&nbsp;{req.type}: {req.status} - {JSON.stringify(req.props)}</div>\n      expandedDiv = <div className=\"expanded-section provider-syncback-requests\">{reqs}</div>\n\n    else if @state.section == 'queue'\n      queue = @state.queue.filter(matchingFilter)\n      queueDivs = for i in [@state.queue.length - 1..0] by -1\n        task = @state.queue[i]\n        # We need to pass the task separately because we want to update\n        # when just that variable changes. Otherwise, since the `task`\n        # pointer doesn't change, the `DeveloperBarTask` doesn't know to\n        # update.\n        status = @state.queue[i].queueState.status\n        <DeveloperBarTask task={task}\n                         key={task.id}\n                         status={status}\n                         type=\"queued\" />\n\n      queueCompleted = @state.completed.filter(matchingFilter)\n      queueCompletedDivs = for i in [@state.completed.length - 1..0] by -1\n        task = @state.completed[i]\n        <DeveloperBarTask task={task}\n                         key={task.id}\n                         type=\"completed\" />\n\n      expandedDiv =\n        <div className=\"expanded-section queue\">\n          <div className=\"btn queue-buttons\"\n               onClick={@_onDequeueAll}>Remove Queued Tasks</div>\n          <div className=\"section-content\">\n            {queueDivs}\n            <hr />\n            {queueCompletedDivs}\n          </div>\n        </div>\n\n      expandedDiv\n\n  _onChange: =>\n    @setState(@_getStateFromStores())\n\n  _onClear: =>\n    Actions.clearDeveloperConsole()\n\n  _onFilter: (ev) =>\n    @setState(filter: ev.target.value)\n\n  _onDequeueAll: =>\n    Actions.dequeueAllTasks()\n\n  _onExpandSection: (section) =>\n    @setState(@_getStateFromStores())\n    @setState(section: section)\n\n  _getStateFromStores: =>\n    queue: Utils.deepClone(TaskQueue._queue)\n    completed: TaskQueue._completed\n    curlHistory: DeveloperBarStore.curlHistory()\n    longPollHistory: DeveloperBarStore.longPollHistory()\n    longPollStates: DeveloperBarStore.longPollStates()\n    providerSyncbackRequests: DeveloperBarStore.providerSyncbackRequests()\n\n\nmodule.exports = DeveloperBar\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/lib/main.cjsx",
    "content": "React = require 'react'\n{ComponentRegistry, WorkspaceStore} = require 'nylas-exports'\nDeveloperBar = require './developer-bar'\n\nmodule.exports =\n  item: null\n\n  activate: (@state={}) ->\n    WorkspaceStore.defineSheet 'Main', {root: true},\n      popout: ['Center']\n\n    ComponentRegistry.register DeveloperBar,\n      location: WorkspaceStore.Location.Center\n\n  deactivate: ->\n    ComponentRegistry.unregister DeveloperBar\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/package.json",
    "content": "{\n  \"name\": \"worker-ui\",\n  \"version\": \"0.1.0\",\n  \"main\": \"./lib/main\",\n  \"description\": \"Interface for the worker window\",\n  \"license\": \"GPL-3.0\",\n  \"private\": true,\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"work\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-app/internal_packages/worker-ui/stylesheets/worker-ui.less",
    "content": "@import \"ui-variables\";\n\n.developer-bar {\n  -webkit-font-smoothing: auto;\n  background-color: rgba(80,80,80,1);\n  border-top:1px solid rgba(0,0,0,0.7);\n  color:white;\n  font-size:12px;\n  display:flex;\n  flex-direction:column;\n  height:100%;\n\n  .controls {\n    z-index:2;\n    background-color: rgba(80,80,80,1);\n    position: relative;\n    min-height:30px;\n    -webkit-app-region: drag;\n    .btn-container {\n      -webkit-app-region: no-drag;\n    }\n  }\n\n  .footer {\n    padding:2px;\n    input.filter {\n      margin-left: 4px;\n      padding: 2px;\n      color:black;\n      vertical-align: middle;\n      width: 400px;\n    }\n  }\n  .section-content {\n    position: relative;\n    z-index: 1;\n  }\n  .queue-buttons {\n    position: relative;\n    z-index: 1;\n  }\n  .btn {\n    padding: 5px;\n    font-size: 13px;\n    line-height: 15px;\n    height: 25px;\n    background: rgba(60,60,60,1);\n    color: white;\n  }\n\n  .btn:hover {\n    background: rgba(40,40,40,1);\n  }\n\n  .fa-caret-square-o-down,\n  .fa-caret-square-o-up {\n    display:inline-block;\n    width:20px;\n    height:20px;\n    float:left;\n    margin:7px;\n    margin-bottom:0;\n    font-size:18px;\n  }\n\n  .btn-container {\n    padding:3px;\n  }\n\n  .delta-state-wrap {\n    display: inline-block;\n  }\n\n  .activity-status-bubble {\n    border-radius:6px;\n    display:inline-block;\n    margin-right:5px;\n    margin-top:-2px;\n    width:11px;\n    height:11px;\n    vertical-align: middle;\n\n    &.state-connecting {\n      background-color:#aff2a7;\n    }\n    &.state-connected {\n      background-color:#94E864;\n    }\n    &.state-none,\n    &.state-closed,\n    &.state-ended, {\n      background-color:gray;\n    }\n  }\n\n  .expanded-section {\n    clear:both;\n    flex: 1;\n    border-top:1px solid black;\n    padding-top:8px;\n    padding-bottom:8px;\n    overflow-y: scroll;\n    background-color: rgba(0,0,0,0.5);\n    font-family: monospace;\n    -webkit-user-select:auto;\n\n    &.queue {\n      padding: 0;\n      .btn { float:right; z-index: 10; }\n      hr {\n        margin: 1em 0;\n      }\n    }\n    .item {\n      overflow-x: hidden;\n      text-overflow: ellipsis;\n    }\n\n    &.curl-history {\n      .item {\n        padding-left:8px;\n        padding-right:8px;\n        padding-bottom:3px;\n      }\n      .timestamp {\n        color: rgba(255,255,255,0.5);\n      }\n      .error-code {\n        background-color:#740000;\n      }\n      .item.status-code-500,\n      .item.status-code-501,\n      .item.status-code-502,\n      .item.status-code-503,\n      .item.status-code-504,\n      .item.status-code-400,\n      .item.status-code-404,\n      .item.status-code-409 {\n        background-color:#740000;\n      }\n      .code {\n        float:right;\n        clear:right;\n        opacity: 0.5;\n      }\n      a {\n        padding-right:4px;\n        border-bottom: 0;\n      }\n      a:hover {\n        border-bottom: 0;\n        text-decoration: none;\n        background-color: #003845;\n        color: white;\n      }\n    }\n\n\n    &.long-polling {\n      .item {\n        padding-left:8px;\n        padding-right:8px;\n        padding-bottom:3px;\n\n        .cursor {\n          float:right;\n          clear:right;\n          opacity: 0.5;\n        }\n\n        &:hover {\n          cursor: pointer;\n          background-color: rgba(255,255,255,0.2);\n        }\n\n        .payload {\n          white-space: pre;\n          color: burlywood;\n        }\n      }\n      .item.ignored {\n        opacity: 0.5;\n      }\n    }\n  }\n\n  .task {\n    padding: 0.5em 1em 0.5em 1.5em;\n    margin: 2px 0;\n\n    &:hover {\n      cursor: pointer;\n      background-color: rgba(255,255,255,0.2);\n    }\n\n    position: relative;\n    &:before {\n      content: \" \";\n      position: absolute;\n      top: 0;\n      left: 0;\n      width: 10px;\n      height: 100%;\n      background: @background-color-pending;\n    }\n\n    &.task-queued{\n      &.task-is-processing:before {\n        background: @background-color-info;\n      }\n    }\n\n    &.task-completed{\n      &.task-local-error:before, &.task-remote-error:before {\n        background: @background-color-error;\n      }\n      &.task-completed.task-success:before {\n        background: @background-color-success;\n      }\n    }\n\n    .task-details { display: none; }\n    &.task-expanded{\n      .task-details { display: block; white-space: pre; }\n    }\n\n  }\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/README.m",
    "content": "# This is the core set of universal, cross-platform keymaps. This is\n# extended in the following places:\n#\n# 1. keymaps/base.cson - (This file) Core, universal keymaps across all platforms\n# 2. keymaps/base-darwin.cson - Any universal mac-only keymaps\n# 3. keymaps/base-win32.cson - Any universal windows-only keymaps\n# 4. keymaps/base-darwin.cson - Any universal linux-only keymaps\n# 5. keymaps/templates/Gmail.cson - Gmail key bindings for all platforms\n# 6. keymaps/templates/Outlook.cson - Outlook key bindings for all platforms\n# 7. keymaps/templates/Apple Mail.cson - Mac Mail key bindings for all platforms\n# 8. some/package/keymaps/package.cson - Keymaps for a specific package\n# 9. ~/.nylas/keymap.cson - Custom user-specific overrides\n#\n# NOTE: We have a special N1 extension called `mod` that automatically\n# uses `cmd` on mac and `ctrl` on windows and linux. This covers most\n# cross-platform cases. For truely platform-specific features, use the\n# platform keymap extensions.\n"
  },
  {
    "path": "packages/client-app/keymaps/base-darwin.json",
    "content": "{\n  \"application:minimize\": \"command+m\",\n  \"application:hide\": \"command+h\",\n  \"application:hide-other-applications\": \"command+alt+h\",\n  \"application:zoom\": \"alt+command+ctrl+m\",\n\n  \"window:toggle-full-screen\": \"command+ctrl+f\",\n  \"window:reload\": \"mod+alt+l\",\n  \"window:toggle-dev-tools\": \"meta+alt+i\"\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/base-linux.json",
    "content": "{\n  \"core:copy\": \"ctrl+insert\",\n  \"core:paste\": \"shift+insert\",\n  \"window:toggle-full-screen\": \"f11\",\n  \"window:reload\": \"mod+alt+l\",\n  \"window:toggle-dev-tools\": \"mod+alt+i\"\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/base-win32.json",
    "content": "{\n  \"window:toggle-full-screen\": \"f11\",\n  \"window:reload\": \"ctrl+shift+r\",\n  \"window:toggle-dev-tools\": \"ctrl+shift+i\"\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/base.json",
    "content": "{\n  \"core:undo\": \"mod+z\",\n  \"core:redo\": [\"mod+shift+z\", \"mod+y\"],\n  \"core:cut\": \"mod+x\",\n  \"core:copy\": \"mod+c\",\n  \"core:paste\": \"mod+v\",\n  \"core:paste-and-match-style\": \"mod+alt+shift+v\",\n  \"core:select-all\": \"mod+a\",\n  \"core:previous-item\": \"up\",\n  \"core:next-item\": \"down\",\n  \"core:move-left\": \"left\",\n  \"core:move-right\": \"right\",\n  \"core:select-up\": \"shift+up\",\n  \"core:select-down\": \"shift+down\",\n  \"core:select-left\": \"shift+left\",\n  \"core:select-right\": \"shift+right\",\n\n  \"application:open-preferences\": \"mod+,\",\n  \"application:quit\": \"mod+q\",\n\n  \"window:close\": \"mod+w\",\n\n  \"core:snooze-item\": \"z\",\n  \"core:print-thread\": \"mod+p\",\n  \"core:focus-item\": \"enter\",\n  \"core:remove-from-view\": [\"backspace\", \"del\"],\n  \"core:pop-sheet\": \"escape\",\n  \"core:show-keybindings\": \"?\",\n\n  \"core:messages-page-up\": \"pageup\",\n  \"core:messages-page-down\": \"pagedown\",\n  \"core:list-page-up\": \"shift+pageup\",\n  \"core:list-page-down\": \"shift+pagedown\",\n\n  \"window:select-account-0\": \"mod+1\",\n  \"window:select-account-1\": \"mod+2\",\n  \"window:select-account-2\": \"mod+3\",\n  \"window:select-account-3\": \"mod+4\",\n  \"window:select-account-4\": \"mod+5\",\n  \"window:select-account-5\": \"mod+6\",\n  \"window:select-account-6\": \"mod+7\",\n  \"window:select-account-7\": \"mod+8\",\n  \"window:select-account-8\": \"mod+9\",\n\n  \"core:find-in-thread\": \"mod+f\",\n  \"core:find-in-thread-next\": \"mod+g\",\n  \"core:find-in-thread-previous\": \"mod+shift+g\",\n\n  \"contenteditable:set-right-to-left\": \"mod+,\",\n  \"contenteditable:underline\": \"mod+u\",\n  \"contenteditable:bold\": \"mod+b\",\n  \"contenteditable:italic\": \"mod+i\",\n  \"contenteditable:insert-link\": \"mod+k\",\n  \"contenteditable:numbered-list\": \"mod+shift+7\",\n  \"contenteditable:bulleted-list\": \"mod+shift+8\",\n  \"contenteditable:quote\": \"mod+shift+9\",\n  \"contenteditable:outdent\": \"mod+[\",\n  \"contenteditable:indent\": \"mod+]\",\n  \"contenteditable:next-selection\": \"mod+\\\"\",\n  \"contenteditable:open-spelling-suggestions\": \"mod+m\"\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/templates/Apple Mail.json",
    "content": "{\n  \"application:new-message\": \"mod+n\",\n\n  \"navigation:go-to-inbox\": \"command+ctrl+1\",\n  \"navigation:go-to-starred\": \"command+ctrl+2\",\n  \"navigation:go-to-sent\": \"command+ctrl+3\",\n  \"navigation:go-to-drafts\": \"command+ctrl+4\",\n  \"navigation:go-to-all\": \"command+ctrl+5\",\n  \"navigation:go-to-contacts\": \"command+ctrl+6\",\n  \"navigation:go-to-tasks\": \"command+ctrl+7\",\n  \"navigation:go-to-label\": \"command+ctrl+8\",\n\n  \"multiselect-list:select-all\": \"command+a\",\n\n  \"core:previous-item\": \"command+[\",\n  \"core:select-up\": \"shift+command+[\",\n  \"core:next-item\": \"command+]\",\n  \"core:select-down\": \"shift+command+]\",\n\n  \"core:reply\": \"mod+r\",\n  \"core:reply-all\": \"mod+shift+r\",\n  \"core:forward\": \"mod+shift+f\",\n  \"core:report-as-spam\": \"mod+shift+j\",\n  \"core:mark-as-unread\": \"mod+shift+u\",\n  \"core:star-item\": \"mod+shift+l\",\n  \"core:focus-search\": \"mod+alt+f\",\n  \"core:archive-item\": \"command+ctrl+a\",\n\n  \"composer:send-message\": \"mod+shift+d\"\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/templates/Gmail.json",
    "content": "{\n  \"navigation:go-to-inbox\": \"g i\",\n  \"navigation:go-to-starred\": \"g s\",\n  \"navigation:go-to-sent\": \"g t\",\n  \"navigation:go-to-drafts\": \"g d\",\n  \"navigation:go-to-all\": \"g a\",\n  \"navigation:go-to-contacts\": \"g c\",\n  \"navigation:go-to-tasks\": \"g k\",\n  \"navigation:go-to-label\": \"g l\",\n\n  \"multiselect-list:select-all\": \"* a\",\n  \"multiselect-list:deselect-all\": \"* n\",\n\n  \"thread-list:select-read\": \"* r\",\n  \"thread-list:select-unread\": \"* u\",\n  \"thread-list:select-starred\": \"* s\",\n  \"thread-list:select-unstarred\": \"* t\",\n\n  \"core:pop-sheet\": \"u\",\n  \"core:previous-item\": \"k\",\n  \"core:select-up\": \"shift+k\",\n  \"core:next-item\": \"j\",\n  \"core:select-down\": \"shift+j\",\n  \"core:focus-item\": \"o\",\n  \"core:select-item\": \"x\",\n  \"core:undo\": \"mod+z\",\n\n  \"message-list:previous-message\": \"p\",\n  \"message-list:next-message\": \"n\",\n  \"message-list:expand-all\": \";\",\n  \"message-list:collapse-all\": \":\",\n\n  \"application:new-message\": [\"c\", \"d\", \"mod+n\"],\n  \"application:more-actions\": \".\",\n  \"application:open-help\": \"?\",\n\n  \"core:mute-conversation\": \"m\",\n  \"core:focus-search\": \"/\",\n  \"core:change-category\": [\"l\", \"v\"],\n  \"core:focus-toolbar\": \",\",\n  \"core:star-item\": \"s\",\n  \"core:gmail-remove-from-view\": \"y\",\n  \"core:archive-item\": \"e\",\n  \"core:report-as-spam\": \"!\",\n  \"core:delete-item\": \"#\",\n\n  \"core:reply\": [\"r\", \"mod+r\"],\n  \"core:reply-new-window\": \"shift+r\",\n  \"core:reply-all\": [\"a\", \"mod+shift+r\"],\n  \"core:reply-all-new-window\": \"shift+a\",\n  \"core:forward\": [\"f\", \"mod+shift+f\"],\n  \"core:forward-new-window\": \"shift+f\",\n\n  \"core:remove-and-previous\": [\"}\", \"]\"],\n  \"core:remove-and-next\": [\"{\", \"[\"],\n\n  \"core:mark-as-read\": \"shift+i\",\n  \"core:mark-as-unread\": [\"shift+u\", \"_\"],\n  \"core:mark-important\": [\"+\", \"=\"],\n  \"core:mark-unimportant\": \"-\"\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/templates/Inbox by Gmail.json",
    "content": "{\n  \"application:new-message\": [\"c\", \"mod+n\"],\n\n  \"navigation:go-to-inbox\": \"i\",\n  \"multiselect-list:select-all\": \"shift+x\",\n\n  \"core:pop-sheet\": \"u\",\n  \"core:previous-item\": \"k\",\n  \"core:select-up\": \"shift+k\",\n  \"core:next-item\": \"j\",\n  \"core:select-down\": \"shift+j\",\n  \"core:focus-item\": \"o\",\n\n  \"message-list:previous-message\": \"p\",\n  \"message-list:next-message\": \"n\",\n\n  \"core:mute-conversation\": \"m\",\n  \"core:focus-search\": \"/\",\n  \"core:change-category\": \".\",\n  \"core:select-item\": \"x\",\n  \"core:gmail-remove-from-view\": \"y\",\n  \"core:archive-item\": \"e\",\n  \"core:report-as-spam\": \"!\",\n  \"core:delete-item\": \"#\",\n\n  \"core:reply\": [\"r\", \"mod+r\"],\n  \"core:reply-new-window\": \"shift+r\",\n  \"core:reply-all\": [\"a\", \"mod+shift+r\"],\n  \"core:reply-all-new-window\": \"shift+a\",\n  \"core:forward\": [\"f\", \"mod+shift+f\"],\n  \"core:forward-new-window\": \"shift+f\",\n\n  \"core:remove-and-previous\": [\"}\", \"]\"],\n  \"core:remove-and-next\": [\"{\", \"[\"],\n  \"core:undo\": \"mod+z\"\n}\n"
  },
  {
    "path": "packages/client-app/keymaps/templates/Outlook.json",
    "content": "{\n  \"core:change-category\": \"mod+shift+v\",\n  \"core:focus-search\": [\"f3\", \"mod+e\"],\n  \"core:forward\": \"mod+f\",\n  \"core:delete-item\": \"mod+d\",\n  \"core:undo\": \"alt+backspace\",\n  \"composer:send-message\": \"alt+s\",\n  \"core:reply\": \"mod+r\",\n  \"core:reply-all\": \"mod+shift+r\",\n  \"application:new-message\": [\"mod+n\", \"mod+shift+m\"],\n  \"send\": \"mod+enter\",\n  \"core:find-in-thread\": \"f4\",\n  \"core:find-in-thread-next\": \"shift+f4\",\n  \"core:find-in-thread-previous\": \"ctrl+shift+f4\",\n\n  \"multiselect-list:select-all\": \"ctrl+a\"\n}\n"
  },
  {
    "path": "packages/client-app/menus/darwin.json",
    "content": "{\n  \"menu\": [\n  {\n    \"label\": \"Nylas Mail\",\n    \"submenu\": [\n      { \"label\": \"About Nylas Mail\", \"command\": \"application:about\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Preferences\", \"command\": \"application:open-preferences\" },\n      { \"label\": \"Change Theme...\", \"command\": \"window:launch-theme-picker\" },\n      { \"label\": \"Install Theme...\", \"command\": \"application:install-package\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Add Account...\", \"command\": \"application:add-account\", \"args\": {\"source\": \"Menu\"}},\n      { \"label\": \"VERSION\", \"enabled\": false },\n      { \"type\": \"separator\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Services\", \"submenu\": [] },\n      { \"type\": \"separator\" },\n      { \"label\": \"Hide Nylas Mail\", \"command\": \"application:hide\" },\n      { \"label\": \"Hide Others\", \"command\": \"application:hide-other-applications\" },\n      { \"label\": \"Show All\", \"command\": \"application:unhide-all-applications\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Quit\", \"command\": \"application:quit\" }\n    ]\n  },\n  {\n    \"label\": \"File\",\n    \"submenu\": [\n      { \"label\": \"New Message\", \"command\": \"application:new-message\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Close Window\", \"command\": \"window:close\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Print Current Thread\", \"command\": \"core:print-thread\" }\n    ]\n  },\n\n  {\n    \"label\": \"Edit\",\n    \"submenu\": [\n      { \"label\": \"Undo\", \"command\": \"core:undo\" },\n      { \"label\": \"Redo\", \"command\": \"core:redo\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Cut\", \"command\": \"core:cut\" },\n      { \"label\": \"Copy\", \"command\": \"core:copy\" },\n      { \"label\": \"Paste\", \"command\": \"core:paste\" },\n      { \"label\": \"Paste and Match Style\", \"command\": \"core:paste-and-match-style\" },\n      { \"label\": \"Select All\", \"command\": \"core:select-all\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Find\", \"submenu\": [\n        { \"label\": \"Find in Thread...\", \"command\": \"core:find-in-thread\" },\n        { \"label\": \"Find Next\", \"command\": \"core:find-in-thread-next\" },\n        { \"label\": \"Find Previous\", \"command\": \"core:find-in-thread-previous\" }\n      ] }\n    ]\n  },\n\n  {\n    \"label\": \"View\",\n    \"submenu\": [\n      { \"type\": \"separator\", \"id\": \"mailbox-navigation\"},\n      { \"label\": \"Go to Inbox\", \"command\": \"navigation:go-to-inbox\" },\n      { \"label\": \"Go to Starred\", \"command\": \"navigation:go-to-starred\" },\n      { \"label\": \"Go to Sent\", \"command\": \"navigation:go-to-sent\" },\n      { \"label\": \"Go to Drafts\", \"command\": \"navigation:go-to-drafts\" },\n      { \"label\": \"Go to All mail\", \"command\": \"navigation:go-to-all\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Enter Full Screen\", \"command\": \"window:toggle-full-screen\" },\n      { \"label\": \"Exit Full Screen\", \"command\": \"window:toggle-full-screen\", \"visible\": false }\n    ]\n  },\n\n  {\n    \"label\": \"Thread\",\n    \"submenu\": [\n      { \"label\": \"Reply\", \"command\": \"core:reply\" },\n      { \"label\": \"Reply All\", \"command\": \"core:reply-all\" },\n      { \"label\": \"Forward\", \"command\": \"core:forward\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Star\", \"command\": \"core:star-item\" },\n      { \"type\": \"separator\", \"id\": \"thread-actions\" },\n      { \"label\": \"Remove from view\", \"command\": \"core:remove-from-view\" },\n      { \"type\": \"separator\", \"id\": \"view-actions\" }\n    ]\n  },\n\n  {\n    \"label\": \"Developer\",\n    \"submenu\": [\n      { \"label\": \"Run with Debug Flags\", \"type\": \"checkbox\", \"command\": \"application:toggle-dev\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Reload\", \"command\": \"window:reload\" },\n      { \"label\": \"Toggle Developer Tools\", \"command\": \"window:toggle-dev-tools\" },\n      { \"label\": \"Toggle Component Regions\", \"command\": \"window:toggle-component-regions\" },\n      { \"label\": \"Toggle Screenshot Mode\", \"command\": \"window:toggle-screenshot-mode\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Create a Plugin...\", \"command\": \"application:create-package\" },\n      { \"label\": \"Install a Plugin...\", \"command\": \"application:install-package\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Open Detailed Logs\", \"command\": \"window:open-errorlogger-logs\" }\n    ]\n  },\n  {\n    \"label\": \"Window\",\n    \"submenu\": [\n      { \"label\": \"Minimize\", \"command\": \"application:minimize\" },\n      { \"label\": \"Zoom\", \"command\": \"application:zoom\" },\n      { \"type\": \"separator\", \"id\": \"window-list-separator\" },\n      { \"type\": \"separator\" },\n      { \"label\": \"Bring All to Front\", \"command\": \"application:bring-all-windows-to-front\" }\n    ]\n  },\n\n  {\n    \"label\": \"Help\",\n    \"submenu\": [\n      { \"label\": \"Nylas Mail Help\", \"command\": \"application:view-help\" }\n    ]\n  }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/menus/linux.json",
    "content": "{\n  \"menu\": [\n    {\n      \"label\": \"&File\",\n      \"submenu\": [\n        { \"label\": \"&New Message...\", \"command\": \"application:new-message\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Add Account...\", \"command\": \"application:add-account\", \"args\": {\"source\": \"Menu\"}},\n        { \"label\": \"Clos&e Window\", \"command\": \"window:close\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Print Current Thread...\", \"command\": \"core:print-thread\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Quit\", \"command\": \"application:quit\" }\n      ]\n    },\n\n    {\n      \"label\": \"&Edit\",\n      \"submenu\": [\n        { \"label\": \"&Undo\", \"command\": \"core:undo\" },\n        { \"label\": \"&Redo\", \"command\": \"core:redo\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"&Cut\", \"command\": \"core:cut\" },\n        { \"label\": \"C&opy\", \"command\": \"core:copy\" },\n        { \"label\": \"&Paste\", \"command\": \"core:paste\" },\n        { \"label\": \"Paste and Match Style\", \"command\": \"core:paste-and-match-style\" },\n        { \"label\": \"Select &All\", \"command\": \"core:select-all\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Find\", \"submenu\": [\n          { \"label\": \"Find in Thread...\", \"command\": \"core:find-in-thread\" },\n          { \"label\": \"Find Next\", \"command\": \"core:find-in-thread-next\" },\n          { \"label\": \"Find Previous\", \"command\": \"core:find-in-thread-previous\" }\n        ]},\n        { \"type\": \"separator\" },\n        { \"label\": \"Preferences\", \"command\": \"application:open-preferences\" },\n        { \"label\": \"Change Theme...\", \"command\": \"window:launch-theme-picker\" },\n        { \"label\": \"Install Theme...\", \"command\": \"application:install-package\" }\n      ]\n    },\n\n    {\n      \"label\": \"&View\",\n      \"submenu\": [\n        { \"type\": \"separator\", \"id\": \"mailbox-navigation\"},\n        { \"label\": \"Go to Inbox\", \"command\": \"navigation:go-to-inbox\" },\n        { \"label\": \"Go to Starred\", \"command\": \"navigation:go-to-starred\" },\n        { \"label\": \"Go to Sent\", \"command\": \"navigation:go-to-sent\" },\n        { \"label\": \"Go to Drafts\", \"command\": \"navigation:go-to-drafts\" },\n        { \"label\": \"Go to All mail\", \"command\": \"navigation:go-to-all\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Enter Full Screen\", \"command\": \"window:toggle-full-screen\" },\n        { \"label\": \"Exit Full Screen\", \"command\": \"window:toggle-full-screen\", \"visible\": false }\n      ]\n    },\n\n    {\n      \"label\": \"Thread\",\n      \"submenu\": [\n        { \"label\": \"Reply\", \"command\": \"core:reply\" },\n        { \"label\": \"Reply All\", \"command\": \"core:reply-all\" },\n        { \"label\": \"Forward\", \"command\": \"core:forward\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Star\", \"command\": \"core:star-item\" },\n        { \"type\": \"separator\", \"id\": \"thread-actions\" },\n        { \"label\": \"Remove from view\", \"command\": \"core:remove-from-view\" },\n        { \"type\": \"separator\", \"id\": \"view-actions\" }\n      ]\n    },\n    {\n      \"label\": \"Developer\",\n      \"submenu\": [\n        { \"label\": \"Run with &Debug Flags\", \"type\": \"checkbox\", \"command\": \"application:toggle-dev\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Reload\", \"command\": \"window:reload\" },\n        { \"label\": \"Toggle Developer &Tools\", \"command\": \"window:toggle-dev-tools\" },\n        { \"label\": \"Toggle Component Regions\", \"command\": \"window:toggle-component-regions\" },\n        { \"label\": \"Toggle Screenshot Mode\", \"command\": \"window:toggle-screenshot-mode\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Create a Plugin...\", \"command\": \"application:create-package\" },\n        { \"label\": \"Install a Plugin...\", \"command\": \"application:install-package\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Open Detailed Logs\", \"command\": \"window:open-errorlogger-logs\" }\n      ]\n    },\n    {\n      \"label\": \"Window\",\n      \"submenu\": [\n        { \"label\": \"Minimize\", \"command\": \"application:minimize\" },\n        { \"label\": \"Zoom\", \"command\": \"application:zoom\" },\n        { \"type\": \"separator\", \"id\": \"window-list-separator\" }\n      ]\n    },\n    {\n      \"label\": \"&Help\",\n      \"submenu\": [\n        { \"label\": \"VERSION\", \"enabled\": false },\n        { \"type\": \"separator\" },\n        { \"label\": \"Nylas Mail Help\", \"command\": \"application:view-help\" }\n      ]\n    }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/menus/win32.json",
    "content": "{\n  \"menu\": [\n    {\n      \"label\": \"&Edit\",\n      \"submenu\": [\n        { \"label\": \"&Undo\", \"command\": \"core:undo\" },\n        { \"label\": \"&Redo\", \"command\": \"core:redo\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Cu&t\", \"command\": \"core:cut\" },\n        { \"label\": \"&Copy\", \"command\": \"core:copy\" },\n        { \"label\": \"&Paste\", \"command\": \"core:paste\" },\n        { \"label\": \"Paste and Match Style\", \"command\": \"core:paste-and-match-style\" },\n        { \"label\": \"Select &All\", \"command\": \"core:select-all\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Find\", \"submenu\": [\n          { \"label\": \"Find in Thread...\", \"command\": \"core:find-in-thread\" },\n          { \"label\": \"Find Next\", \"command\": \"core:find-in-thread-next\" },\n          { \"label\": \"Find Previous\", \"command\": \"core:find-in-thread-previous\" }\n        ] }\n      ]\n    },\n\n    {\n      \"label\": \"&View\",\n      \"submenu\": [\n        { \"type\": \"separator\", \"id\": \"mailbox-navigation\"},\n        { \"label\": \"Go to Inbox\", \"command\": \"navigation:go-to-inbox\" },\n        { \"label\": \"Go to Starred\", \"command\": \"navigation:go-to-starred\" },\n        { \"label\": \"Go to Sent\", \"command\": \"navigation:go-to-sent\" },\n        { \"label\": \"Go to Drafts\", \"command\": \"navigation:go-to-drafts\" },\n        { \"label\": \"Go to All mail\", \"command\": \"navigation:go-to-all\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Enter Full Screen\", \"command\": \"window:toggle-full-screen\" },\n        { \"label\": \"Exit Full Screen\", \"command\": \"window:toggle-full-screen\", \"visible\": false }\n      ]\n    },\n\n    {\n      \"label\": \"Thread\",\n      \"submenu\": [\n        { \"label\": \"Reply\", \"command\": \"core:reply\" },\n        { \"label\": \"Reply All\", \"command\": \"core:reply-all\" },\n        { \"label\": \"Forward\", \"command\": \"core:forward\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Star\", \"command\": \"core:star-item\" },\n        { \"type\": \"separator\", \"id\": \"thread-actions\" },\n        { \"label\": \"Remove from view\", \"command\": \"core:remove-from-view\" },\n        { \"type\": \"separator\", \"id\": \"view-actions\" }\n      ]\n    },\n    {\n      \"label\": \"Developer\",\n      \"submenu\": [\n        { \"label\": \"Run with &Debug Flags\", \"type\": \"checkbox\", \"command\": \"application:toggle-dev\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"&Reload\", \"command\": \"window:reload\" },\n        { \"label\": \"Toggle Developer &Tools\", \"command\": \"window:toggle-dev-tools\" },\n        { \"label\": \"Toggle Component Regions\", \"command\": \"window:toggle-component-regions\" },\n        { \"label\": \"Toggle Screenshot Mode\", \"command\": \"window:toggle-screenshot-mode\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Create a Plugin...\", \"command\": \"application:create-package\" },\n        { \"label\": \"Install a Plugin...\", \"command\": \"application:install-package\" },\n        { \"type\": \"separator\" },\n        { \"label\": \"Open Detailed Logs\", \"command\": \"window:open-errorlogger-logs\" }\n      ]\n    },\n    {\n      \"label\": \"Window\",\n      \"submenu\": [\n        { \"label\": \"Minimize\", \"command\": \"application:minimize\" },\n        { \"label\": \"Zoom\", \"command\": \"application:zoom\" },\n        { \"type\": \"separator\", \"id\": \"window-list-separator\" }\n      ]\n    },\n    { \"type\": \"separator\" },\n    {\n      \"label\": \"&Help...\",\n      \"command\": \"application:view-help\"\n    },\n    { \"type\": \"separator\" },\n    { \"label\": \"Preferences\", \"command\": \"application:open-preferences\" },\n    { \"label\": \"Add Account...\", \"command\": \"application:add-account\", \"args\": {\"source\": \"Menu\"}},\n    { \"label\": \"Change Theme...\", \"command\": \"window:launch-theme-picker\" },\n    { \"label\": \"Install Theme...\", \"command\": \"application:install-package\" },\n    { \"type\": \"separator\" },\n    { \"label\": \"VERSION\", \"enabled\": false },\n    { \"type\": \"separator\" },\n    { \"label\": \"Print Current Thread\", \"command\": \"core:print-thread\" },\n    { \"type\": \"separator\" },\n    { \"label\": \"E&xit\", \"command\": \"application:quit\" }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/package.json",
    "content": "{\n  \"name\": \"nylas-mail\",\n  \"productName\": \"Nylas Mail\",\n  \"version\": \"2.0.32\",\n  \"description\": \"The best email app for people and teams at work\",\n  \"license\": \"GPL-3.0\",\n  \"main\": \"./src/browser/main.js\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/nylas/nylas-mail.git\"\n  },\n  \"bugs\": {\n    \"url\": \"https://github.com/nylas/nylas-mail/issues\"\n  },\n  \"dependencies\": {\n    \"async\": \"^0.9\",\n    \"babel-core\": \"6.22.0\",\n    \"babel-preset-electron\": \"1.4.15\",\n    \"babel-preset-react\": \"6.22.0\",\n    \"babel-regenerator-runtime\": \"6.5.0\",\n    \"base64-stream\": \"0.1.3\",\n    \"better-sqlite3\": \"bengotow/better-sqlite3#a888061ad334c76d2db4c06554c90785cc6e7cce\",\n    \"bluebird\": \"3.4.x\",\n    \"chromium-net-errors\": \"1.0.3\",\n    \"chrono-node\": \"^1.1.2\",\n    \"classnames\": \"1.2.1\",\n    \"clearbit\": \"^1.2\",\n    \"coffee-react\": \"^5.0.0\",\n    \"coffee-script\": \"1.10.0\",\n    \"coffeestack\": \"^1.1\",\n    \"color\": \"^0.7.3\",\n    \"debug\": \"github:emorikawa/debug#nylas\",\n    \"electron\": \"1.4.15\",\n    \"electron-spellchecker\": \"1.0.1\",\n    \"emissary\": \"^1.3.1\",\n    \"emoji-data\": \"^0.2.0\",\n    \"encoding\": \"0.1.12\",\n    \"enzyme\": \"2.7.1\",\n    \"esdoc\": \"^0.5.2\",\n    \"esdoc-es7-plugin\": \"0.0.3\",\n    \"event-kit\": \"^1.0.2\",\n    \"fs-plus\": \"^2.3.2\",\n    \"getmac\": \"1.x.x\",\n    \"googleapis\": \"9.0.0\",\n    \"guid\": \"0.0.10\",\n    \"hapi\": \"16.1.0\",\n    \"hapi-auth-basic\": \"^4.2.0\",\n    \"hapi-boom-decorators\": \"2.2.2\",\n    \"hapi-plugin-websocket\": \"^0.9.2\",\n    \"hapi-swagger\": \"7.6.0\",\n    \"he\": \"1.1.0\",\n    \"iconv\": \"2.2.1\",\n    \"immutable\": \"3.7.5\",\n    \"inert\": \"4.0.0\",\n    \"is-online\": \"7.0.0\",\n    \"isomorphic-core\": \"0.x.x\",\n    \"jasmine-json\": \"~0.0\",\n    \"jasmine-react-helpers\": \"^0.2\",\n    \"jasmine-reporters\": \"1.x.x\",\n    \"jasmine-tagged\": \"^1.1.2\",\n    \"joi\": \"8.4.2\",\n    \"jsx-transform\": \"^2.3.0\",\n    \"juice\": \"^1.4\",\n    \"kbpgp\": \"^2.0.52\",\n    \"keytar\": \"3.0.0\",\n    \"less-cache\": \"0.21\",\n    \"lru-cache\": \"^4.0.1\",\n    \"marked\": \"^0.3\",\n    \"mimelib\": \"0.2.19\",\n    \"mkdirp\": \"^0.5\",\n    \"moment\": \"2.12.0\",\n    \"moment-round\": \"^1.0.1\",\n    \"moment-timezone\": \"0.5.4\",\n    \"mousetrap\": \"^1.5.3\",\n    \"nock\": \"^2\",\n    \"node-emoji\": \"^1.2.1\",\n    \"node-uuid\": \"^1.4\",\n    \"nslog\": \"^3\",\n    \"optimist\": \"0.4.0\",\n    \"papaparse\": \"^4.1.2\",\n    \"pathwatcher\": \"~6.2\",\n    \"pick-react-known-prop\": \"0.x.x\",\n    \"promise-queue\": \"2.1.1\",\n    \"property-accessors\": \"^1\",\n    \"proxyquire\": \"1.3.1\",\n    \"q\": \"^1.0.1\",\n    \"raven\": \"1.1.4\",\n    \"react\": \"15.4.2\",\n    \"react-addons-css-transition-group\": \"15.4.2\",\n    \"react-addons-perf\": \"15.4.2\",\n    \"react-addons-test-utils\": \"15.4.2\",\n    \"react-dom\": \"15.4.2\",\n    \"reflux\": \"0.1.13\",\n    \"request\": \"2.79.x\",\n    \"request-progress\": \"^0.3\",\n    \"rimraf\": \"2.5.2\",\n    \"runas\": \"^3.1\",\n    \"rx-lite\": \"4.0.8\",\n    \"rx-lite-testing\": \"^4.0.7\",\n    \"sanitize-html\": \"1.9.0\",\n    \"season\": \"^5.1\",\n    \"semver\": \"^4.2\",\n    \"sequelize\": \"nylas/sequelize#nylas-3.40.0\",\n    \"simplemde\": \"jstejada/simplemde-markdown-editor#input-style-support\",\n    \"source-map-support\": \"^0.3.2\",\n    \"sqlite3\": \"https://github.com/bengotow/node-sqlite3/archive/bengotow/usleep-v3.1.4.tar.gz\",\n    \"temp\": \"^0.8\",\n    \"tld\": \"^0.0.2\",\n    \"underscore\": \"1.8.x\",\n    \"underscore.string\": \"^3.0\",\n    \"vision\": \"4.1.0\",\n    \"windows-shortcuts\": \"emorikawa/windows-shortcuts#b0a0fc7\"\n  },\n  \"devDependencies\": {\n    \"donna\": \"^1.0.15\",\n    \"gitbook\": \"^3.2.2\",\n    \"gitbook-cli\": \"^2.3.0\",\n    \"gitbook-plugin-anchors\": \"^0.7.1\",\n    \"gitbook-plugin-editlink\": \"^1.0.2\",\n    \"gitbook-plugin-favicon\": \"0.0.2\",\n    \"gitbook-plugin-github\": \"^2.0.0\",\n    \"gitbook-plugin-theme-api\": \"^1.1.2\",\n    \"handlebars\": \"4.0.6\",\n    \"joanna\": \"0.0.8\",\n    \"meta-marked\": \"0.4.2\",\n    \"tello\": \"1.0.6\"\n  },\n  \"optionalDependencies\": {\n    \"node-mac-notifier\": \"0.0.13\"\n  },\n  \"packageDependencies\": {},\n  \"scripts\": {\n    \"test\": \"electron . --test --enable-logging\",\n    \"test-window\": \"electron . --test=window --enable-logging\",\n    \"test-junit\": \"electron . --test --enable-logging --junit-xml=junitxml\",\n    \"start\": \"electron . --dev --enable-logging\",\n    \"lint\": \"script/grunt lint\",\n    \"build\": \"script/grunt build\"\n  }\n}\n"
  },
  {
    "path": "packages/client-app/script/grunt",
    "content": "#!/usr/bin/env node\nvar cp = require('./utils/child-process-wrapper.js');\nvar fs = require('fs');\nvar path = require('path');\n\n// node build/node_modules/.bin/grunt \"$@\"\nvar gruntPath = path.resolve(__dirname, '..', 'build', 'node_modules', '.bin', 'grunt') + (process.platform === 'win32' ? '.cmd' : '');\n\nif (!fs.existsSync(gruntPath)) {\n  console.error('Grunt command does not exist at: ' + gruntPath);\n  console.error('Run script/bootstrap to install Grunt');\n  process.exit(1);\n}\n\nvar args = ['--gruntfile', path.resolve('build', 'Gruntfile.js')];\nargs = args.concat(process.argv.slice(2));\ncp.safeSpawn(gruntPath, args, process.exit);\n"
  },
  {
    "path": "packages/client-app/script/grunt.cmd",
    "content": "@IF EXIST \"%~dp0\\node.exe\" (\n  \"%~dp0\\node.exe\"  \"%~dp0\\grunt\" %*\n) ELSE (\n  node  \"%~dp0\\grunt\" %*\n)\n"
  },
  {
    "path": "packages/client-app/script/mkdeb",
    "content": "#!/bin/bash\n# mkdeb version arch control-file-path desktop-file-path icon-file-path sources-file-path deb-file-path\n\nset -e\n\nSCRIPT=`readlink -f \"$0\"`\nROOT=`readlink -f $(dirname $SCRIPT)/..`\ncd $ROOT\n\nVERSION=\"$1\"\nARCH=\"$2\"\nICON_FILE=\"$3\"\nLINUX_ASSETS_DIRECTORY=\"$4\"\nAPP_CONTENTS_DIRECTORY=\"$5\"\nOUTPUT_PATH=\"$6\"\n\nFILE_MODE=755\n\nTARGET_ROOT=\"`mktemp -d`\"\nchmod $FILE_MODE \"$TARGET_ROOT\"\nTARGET=\"$TARGET_ROOT/nylas-$VERSION-$ARCH\"\n\nmkdir -m $FILE_MODE -p \"$TARGET/usr\"\nmkdir -m $FILE_MODE -p \"$TARGET/usr/share\"\ncp -r \"$APP_CONTENTS_DIRECTORY\" \"$TARGET/usr/share/nylas-mail\"\n\nmkdir -m $FILE_MODE -p \"$TARGET/DEBIAN\"\ncp \"$OUTPUT_PATH/control\" \"$TARGET/DEBIAN/control\"\n\ncp \"$LINUX_ASSETS_DIRECTORY/debian/postinst\" \"$TARGET/DEBIAN/postinst\"\ncp \"$LINUX_ASSETS_DIRECTORY/debian/postrm\" \"$TARGET/DEBIAN/postrm\"\n\nmkdir -m $FILE_MODE -p \"$TARGET/usr/bin\"\nln -s \"../share/nylas-mail/nylas\" \"$TARGET/usr/bin/nylas-mail\"\nchmod +x \"$TARGET/usr/bin/nylas-mail\"\n\nmkdir -m $FILE_MODE -p \"$TARGET/usr/share/applications\"\ncp \"$OUTPUT_PATH/nylas-mail.desktop\" \"$TARGET/usr/share/applications\"\n\nmkdir -m $FILE_MODE -p \"$TARGET/usr/share/pixmaps\"\ncp \"$ICON_FILE\" \"$TARGET/usr/share/pixmaps/nylas-mail.png\"\n\nmkdir -m $FILE_MODE -p \"$TARGET/usr/share/icons/hicolor\"\nfor i in 256 128 64 32 16; do\n  mkdir -p \"$TARGET/usr/share/icons/hicolor/${i}x${i}/apps\"\n  cp \"$LINUX_ASSETS_DIRECTORY/icons/${i}.png\" \"$TARGET/usr/share/icons/hicolor/${i}x${i}/apps/nylas-mail.png\"\ndone\n\n# Copy generated LICENSE.md to /usr/share/doc/nylas-mail/copyright\nmkdir -m $FILE_MODE -p \"$TARGET/usr/share/doc/nylas-mail\"\ncp \"$TARGET/usr/share/nylas-mail/LICENSE\" \"$TARGET/usr/share/doc/nylas-mail/copyright\"\n\n# Add lintian overrides\nmkdir -m $FILE_MODE -p \"$TARGET/usr/share/lintian/overrides\"\ncp \"$ROOT/build/resources/linux/debian/lintian-overrides\" \"$TARGET/usr/share/lintian/overrides/nylas-mail\"\n\n# Remove group write from all files\nchmod -R g-w \"$TARGET\";\n\n# Remove executable bit from .node files\nfind \"$TARGET\" -type f -name \"*.node\" -exec chmod a-x {} \\;\n\nfakeroot dpkg-deb -b \"$TARGET\"\nmv \"$TARGET_ROOT/nylas-$VERSION-$ARCH.deb\" \"$OUTPUT_PATH\"\nrm -rf \"$TARGET_ROOT\"\n"
  },
  {
    "path": "packages/client-app/script/mkrpm",
    "content": "#!/bin/bash\n\nset -e\n\nBUILD_DIRECTORY=\"$1\"\nAPP_CONTENTS_DIRECTORY=\"$2\"\nLINUX_ASSETS_DIRECTORY=\"$3\"\n\nRPM_BUILD_ROOT=~/rpmbuild\nARCH=`uname -m`\n\n# Work around for `uname -m` returning i686 when rpmbuild uses i386 instead\nif [ \"$ARCH\" = \"i686\" ]; then\n\tARCH=\"i386\"\nfi\n\n# rpmdev-setuptree\nmkdir -p $RPM_BUILD_ROOT/BUILD\nmkdir -p $RPM_BUILD_ROOT/SPECS\nmkdir -p $RPM_BUILD_ROOT/RPMS\n\ncp -r \"$APP_CONTENTS_DIRECTORY/\"* \"$RPM_BUILD_ROOT/BUILD\"\ncp -r \"$LINUX_ASSETS_DIRECTORY/icons\" \"$RPM_BUILD_ROOT/BUILD\"\ncp \"$BUILD_DIRECTORY/nylas.spec\" \"$RPM_BUILD_ROOT/SPECS\"\ncp \"$BUILD_DIRECTORY/nylas-mail.desktop\" \"$RPM_BUILD_ROOT/BUILD\"\n\nrpmbuild -ba \"$BUILD_DIRECTORY/nylas.spec\"\ncp $RPM_BUILD_ROOT/RPMS/$ARCH/nylas-*.rpm \"$BUILD_DIRECTORY\"\n\nrm -rf \"$RPM_BUILD_ROOT\"\n"
  },
  {
    "path": "packages/client-app/script/publish-docs",
    "content": "#!/bin/bash\n\nset -e\n\n# Builds docs and moves the output to gh-pages branch (overwrites)\nmkdir -p _docs_output\nscript/grunt docs\n./node_modules/.bin/gitbook --gitbook=latest build . ./_docs_output --log=debug --debug\nrm -r docs_src/classes\ngit checkout gh-pages --quiet\ncp -rf _docs_output/* .\n# rm -r _docs_output\n\ngit add .\ngit status -s\nprintf \"\\nDocs updated! \\n\\n\"\ngit commit -m 'Update Docs'\ngit push origin gh-pages\ngit checkout master\n"
  },
  {
    "path": "packages/client-app/script/utils/child-process-wrapper.js",
    "content": "var childProcess = require('child_process');\n\n// Exit the process if the command failed and only call the callback if the\n// command succeed, output of the command would also be piped.\nexports.safeExec = function(command, options, callback) {\n  if (!callback) {\n    callback = options;\n    options = {};\n  }\n  if (!options)\n    options = {};\n\n  // This needed to be increased for `apm test` runs that generate many failures\n  // The default is 200KB.\n  options.maxBuffer = 1024 * 1024;\n\n  options.stdio = \"inherit\"\n  var child = childProcess.exec(command, options, function(error, stdout, stderr) {\n    if (error && !options.ignoreStderr)\n      process.exit(error.code || 1);\n    else\n      callback(null);\n  });\n  child.stderr.pipe(process.stderr);\n  if (!options.ignoreStdout)\n    child.stdout.pipe(process.stdout);\n}\n\n// Same with safeExec but call child_process.spawn instead.\nexports.safeSpawn = function(command, args, options, callback) {\n  if (!callback) {\n    callback = options;\n    options = {};\n  }\n  options.stdio = \"inherit\"\n  var child = childProcess.spawn(command, args, options);\n  child.on('error', function(error) {\n    console.error('Command \\'' + command + '\\' failed: ' + error.message);\n  });\n  child.on('exit', function(code) {\n    if (code != 0)\n      process.exit(code);\n    else\n      callback(null);\n  });\n}\n"
  },
  {
    "path": "packages/client-app/spec/action-bridge-spec.coffee",
    "content": "Reflux = require 'reflux'\nActions = require('../src/flux/actions').default\nMessage = require('../src/flux/models/message').default\nDatabaseStore = require('../src/flux/stores/database-store').default\nAccountStore = require('../src/flux/stores/account-store').default\nActionBridge = require('../src/flux/action-bridge').default\n_ = require 'underscore'\n\nipc =\n    on: ->\n    send: ->\n\ndescribe \"ActionBridge\", ->\n\n  describe \"in the work window\", ->\n    beforeEach ->\n      spyOn(NylasEnv, \"getWindowType\").andReturn \"default\"\n      spyOn(NylasEnv, \"isWorkWindow\").andReturn true\n      @bridge = new ActionBridge(ipc)\n\n    it \"should have the role Role.WORK\", ->\n      expect(@bridge.role).toBe(ActionBridge.Role.WORK)\n\n    it \"should rebroadcast global actions\", ->\n      spyOn(@bridge, 'onRebroadcast')\n      testAction = Actions[Actions.globalActions[0]]\n      testAction('bla')\n      expect(@bridge.onRebroadcast).toHaveBeenCalled()\n\n    it \"should rebroadcast when the DatabaseStore triggers\", ->\n      spyOn(@bridge, 'onRebroadcast')\n      DatabaseStore.trigger({})\n      expect(@bridge.onRebroadcast).toHaveBeenCalled()\n\n    it \"should not rebroadcast mainWindow actions since it is the main window\", ->\n      spyOn(@bridge, 'onRebroadcast')\n      testAction = Actions.didMakeAPIRequest\n      testAction('bla')\n      expect(@bridge.onRebroadcast).not.toHaveBeenCalled()\n\n    it \"should not rebroadcast window actions\", ->\n      spyOn(@bridge, 'onRebroadcast')\n      testAction = Actions[Actions.windowActions[0]]\n      testAction('bla')\n      expect(@bridge.onRebroadcast).not.toHaveBeenCalled()\n\n  describe \"in another window\", ->\n    beforeEach ->\n      spyOn(NylasEnv, \"getWindowType\").andReturn \"popout\"\n      spyOn(NylasEnv, \"isWorkWindow\").andReturn false\n      @bridge = new ActionBridge(ipc)\n      @message = new Message\n        id: 'test-id'\n        accountId: TEST_ACCOUNT_ID\n\n    it \"should have the role Role.SECONDARY\", ->\n      expect(@bridge.role).toBe(ActionBridge.Role.SECONDARY)\n\n    it \"should rebroadcast global actions\", ->\n      spyOn(@bridge, 'onRebroadcast')\n      testAction = Actions[Actions.globalActions[0]]\n      testAction('bla')\n      expect(@bridge.onRebroadcast).toHaveBeenCalled()\n\n    it \"should rebroadcast mainWindow actions\", ->\n      spyOn(@bridge, 'onRebroadcast')\n      testAction = Actions.didMakeAPIRequest\n      testAction('bla')\n      expect(@bridge.onRebroadcast).toHaveBeenCalled()\n\n    it \"should not rebroadcast window actions\", ->\n      spyOn(@bridge, 'onRebroadcast')\n      testAction = Actions[Actions.windowActions[0]]\n      testAction('bla')\n      expect(@bridge.onRebroadcast).not.toHaveBeenCalled()\n\n  describe \"onRebroadcast\", ->\n    beforeEach ->\n      spyOn(NylasEnv, \"getWindowType\").andReturn \"popout\"\n      spyOn(NylasEnv, \"isMainWindow\").andReturn false\n      @bridge = new ActionBridge(ipc)\n\n    describe \"when called with TargetWindows.ALL\", ->\n      it \"should broadcast the action over IPC to all windows\", ->\n        spyOn(ipc, 'send')\n        Actions.onNewMailDeltas.firing = false\n        @bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}])\n        expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-all', 'popout', 'onNewMailDeltas', '[{\"oldModel\":\"1\",\"newModel\":2}]')\n\n    describe \"when called with TargetWindows.WORK\", ->\n      it \"should broadcast the action over IPC to the main window only\", ->\n        spyOn(ipc, 'send')\n        Actions.onNewMailDeltas.firing = false\n        @bridge.onRebroadcast(ActionBridge.TargetWindows.WORK, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}])\n        expect(ipc.send).toHaveBeenCalledWith('action-bridge-rebroadcast-to-work', 'popout', 'onNewMailDeltas', '[{\"oldModel\":\"1\",\"newModel\":2}]')\n\n    it \"should not do anything if the current invocation of the Action was triggered by itself\", ->\n      spyOn(ipc, 'send')\n      Actions.onNewMailDeltas.firing = true\n      @bridge.onRebroadcast(ActionBridge.TargetWindows.ALL, 'onNewMailDeltas', [{oldModel: '1', newModel: 2}])\n      expect(ipc.send).not.toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/async-test-spec.es6",
    "content": "const foo = () => {\n  return new Promise((resolve) => {\n    setTimeout(() => {\n      console.log(\"---------------------------------- RESOLVING\")\n      resolve()\n    }, 100)\n  })\n}\n\nxdescribe(\"test spec\", function testSpec() {\n  // it(\"has 1 failure\", () => {\n  //   expect(false).toBe(true)\n  // });\n\n  it(\"is async\", () => {\n    const p = foo().then(() => {\n      console.log(\"THEN\")\n      expect(true).toBe(true)\n    })\n    advanceClock(200);\n    return p\n  });\n\n  // it(\"has another failure\", () => {\n  //   expect(false).toBe(true)\n  // });\n});\n"
  },
  {
    "path": "packages/client-app/spec/buffered-process-spec.coffee",
    "content": "ChildProcess = require 'child_process'\npath = require 'path'\nBufferedProcess  = require '../src/buffered-process'\n\ndescribe \"BufferedProcess\", ->\n  describe \"when a bad command is specified\", ->\n    [oldOnError] = []\n    beforeEach ->\n      oldOnError = window.onerror\n      window.onerror = jasmine.createSpy()\n\n    afterEach ->\n      window.onerror = oldOnError\n\n    describe \"when there is an error handler specified\", ->\n      it \"calls the error handler and does not throw an exception\", ->\n        p = new BufferedProcess\n          command: 'bad-command-nope'\n          args: ['nothing']\n          options: {}\n\n        errorSpy = jasmine.createSpy().andCallFake (error) -> error.handle()\n        p.onWillThrowError(errorSpy)\n\n        waitsFor -> errorSpy.callCount > 0\n\n        runs ->\n          expect(window.onerror).not.toHaveBeenCalled()\n          expect(errorSpy).toHaveBeenCalled()\n          expect(errorSpy.mostRecentCall.args[0].error.message).toContain 'spawn bad-command-nope ENOENT'\n\n    # describe \"when there is not an error handler specified\", ->\n    #   it \"calls the error handler and does not throw an exception\", ->\n    #     spyOn(process, \"nextTick\").andCallFake (fn) -> fn()\n    #\n    #     try\n    #       p = new BufferedProcess\n    #         command: 'bad-command-nope'\n    #         args: ['nothing']\n    #         options: {stdout: 'ignore'}\n    #\n    #     catch error\n    #       expect(error.message).toContain 'Failed to spawn command `bad-command-nope`'\n    #       expect(error.name).toBe 'BufferedProcessError'\n\n  describe \"on Windows\", ->\n    originalPlatform = null\n\n    beforeEach ->\n      # Prevent any commands from actually running and affecting the host\n      originalSpawn = ChildProcess.spawn\n      spyOn(ChildProcess, 'spawn').andCallFake ->\n        # Just spawn something that won't actually modify the host\n        if originalPlatform is 'win32'\n          originalSpawn('dir')\n        else\n          originalSpawn('ls')\n\n      originalPlatform = process.platform\n      Object.defineProperty process, 'platform', value: 'win32'\n\n    afterEach ->\n      Object.defineProperty process, 'platform', value: originalPlatform\n\n    describe \"when the explorer command is spawned on Windows\", ->\n      it \"doesn't quote arguments of the form /root,C...\", ->\n        new BufferedProcess({command: 'explorer.exe', args: ['/root,C:\\\\foo']})\n        expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '\"explorer.exe /root,C:\\\\foo\"'\n\n    it \"spawns the command using a cmd.exe wrapper\", ->\n      new BufferedProcess({command: 'dir'})\n      expect(path.basename(ChildProcess.spawn.argsForCall[0][0])).toBe 'cmd.exe'\n      expect(ChildProcess.spawn.argsForCall[0][1][0]).toBe '/s'\n      expect(ChildProcess.spawn.argsForCall[0][1][1]).toBe '/c'\n      expect(ChildProcess.spawn.argsForCall[0][1][2]).toBe '\"dir\"'\n"
  },
  {
    "path": "packages/client-app/spec/components/blockquote-manager-spec.es6",
    "content": "import { DOMUtils } from 'nylas-exports';\nimport BlockquoteManager from '../../src/components/contenteditable/blockquote-manager';\n\ndescribe(\"BlockquoteManager\", function BlockquoteManagerSpecs() {\n  const outdentCases = [`\n  <div>|</div>\n  `,\n    `\n  <div>\n    <span>|</span>\n  </div>\n  `,\n    `\n  <p></p>\n  <span>\\n</span>\n  <span>|</span>\n  `,\n    `\n  <span></span>\n  <p></p>\n  <span></span>\n  <span>|</span>\n  `,\n    `\n  <div>\n    <div>\n      <div>|</div>\n    </div>\n  </div>\n  `,\n    `\n  <div>\n    <span></span>\n    <span>|</span>\n  </div>\n  `,\n    `\n  <span></span>\n  <p><span>yo</span></p>\n  <span></span>\n  <span>\n    <span></span>\n    <span></span>\n    <span>|test</span>\n  </span>\n  `,\n  ]\n\n  const backspaceCases = [`\n  <div>yo|</div>\n  `,\n    `\n  <div>\n    yo\n    <span>|</span>\n  </div>\n  `,\n    `\n  <p></p>\n  <span>&nbsp;</span>\n  <span>|</span>\n  `,\n    `\n  <span></span>\n  <p></p>\n  <span>yo</span>\n  <span>|</span>\n  `,\n    `\n  <div>\n    <div>\n      <div>yo|</div>\n    </div>\n  </div>\n  `,\n    `\n  <div>\n    <span>yo</span>\n    <span>|</span>\n  </div>\n  `,\n    `\n  <span></span>\n  <p><span>yo</span></p>\n  <span></span>\n  <span>\n    <span>yo</span>\n    <span></span>\n    <span>|test</span>\n  </span>\n  `,\n  ]\n\n  const setupContext = (testCase) => {\n    const context = document.createElement(\"blockquote\");\n    context.innerHTML = testCase;\n    const {node, index} = DOMUtils.findCharacter(context, \"|\");\n    if (!node) {\n      throw new Error(\"Couldn't find where to set Selection\");\n    }\n    const mockSelection = {\n      isCollapsed: true,\n      anchorNode: node,\n      anchorOffset: index,\n    };\n    return mockSelection;\n  };\n\n  outdentCases.forEach(testCase =>\n    it(`outdents\\n${testCase}`, () => {\n      const mockSelection = setupContext(testCase);\n      const editor = {currentSelection() { return mockSelection; }};\n      expect(BlockquoteManager._isInBlockquote(editor)).toBe(true);\n      return expect(BlockquoteManager._isAtStartOfLine(editor)).toBe(true);\n    })\n  );\n\n  return backspaceCases.forEach(testCase =>\n    it(`backspaces (does NOT outdent)\\n${testCase}`, () => {\n      const mockSelection = setupContext(testCase);\n      const editor = {currentSelection() { return mockSelection; }};\n      expect(BlockquoteManager._isInBlockquote(editor)).toBe(true);\n      return expect(BlockquoteManager._isAtStartOfLine(editor)).toBe(false);\n    })\n  );\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/clipboard-service-spec.coffee",
    "content": "ClipboardService = require('../../src/components/contenteditable/clipboard-service').default\n{InlineStyleTransformer, SanitizeTransformer} = require 'nylas-exports'\nfs = require 'fs'\n\ndescribe \"ClipboardService\", ->\n  beforeEach ->\n    @onFilePaste = jasmine.createSpy('onFilePaste')\n    @setInnerState = jasmine.createSpy('setInnerState')\n    @clipboardService = new ClipboardService\n      data: {props: {@onFilePaste}}\n      methods: {@setInnerState}\n\n    spyOn(document, 'execCommand')\n\n  describe \"when both html and plain text parts are present\", ->\n    beforeEach ->\n      @mockEvent =\n        preventDefault: jasmine.createSpy('preventDefault')\n        clipboardData:\n          getData: (mimetype) ->\n            return '<strong>This is text</strong>' if mimetype is 'text/html'\n            return 'This is plain text' if mimetype is 'text/plain'\n            return null\n          items: [{\n            kind: 'string'\n            type: 'text/html'\n            getAsString: -> '<strong>This is text</strong>'\n          },{\n            kind: 'string'\n            type: 'text/plain'\n            getAsString: -> 'This is plain text'\n          }]\n\n    it \"should choose to insert the HTML representation\", ->\n      spyOn(@clipboardService, '_sanitizeHTMLInput').andCallFake (input) =>\n        Promise.resolve(input)\n\n      runs ->\n        @clipboardService.onPaste(@mockEvent)\n      waitsFor ->\n        document.execCommand.callCount > 0\n      runs ->\n        [command, a, html] = document.execCommand.mostRecentCall.args\n        expect(command).toEqual('insertHTML')\n        expect(html).toEqual('<strong>This is text</strong>')\n\n  describe \"when only plain text is present\", ->\n    beforeEach ->\n      @mockEvent =\n        preventDefault: jasmine.createSpy('preventDefault')\n        clipboardData:\n          getData: (mimetype) ->\n            return 'This is plain text\\nAnother line  Hello  World' if mimetype is 'text/plain'\n            return null\n          items: [{\n            kind: 'string'\n            type: 'text/plain'\n            getAsString: -> 'This is plain text\\nAnother line  Hello  World'\n          }]\n\n    it \"should convert the plain text to HTML and call insertHTML\", ->\n      runs ->\n        @clipboardService.onPaste(@mockEvent)\n      waitsFor ->\n        document.execCommand.callCount > 0\n      runs ->\n        [command, a, html] = document.execCommand.mostRecentCall.args\n        expect(command).toEqual('insertHTML')\n        expect(html).toEqual('This is plain text<br/>Another line &nbsp;Hello &nbsp;World')\n\n  describe \"HTML sanitization\", ->\n    beforeEach ->\n      spyOn(InlineStyleTransformer, 'run').andCallThrough()\n      spyOn(SanitizeTransformer, 'run').andCallThrough()\n\n    it \"should inline CSS styles and run the standard permissive HTML sanitizer\", ->\n      input = \"HTML HERE\"\n      waitsForPromise =>\n        @clipboardService._sanitizeHTMLInput(input)\n        .then =>\n          expect(InlineStyleTransformer.run).toHaveBeenCalledWith(input)\n          expect(SanitizeTransformer.run).toHaveBeenCalledWith(input, SanitizeTransformer.Preset.Permissive)\n\n    it \"should replace two or more <br/>s in a row\", ->\n      tests = [{\n        in: \"Hello\\n\\n\\nWorld\"\n        out: \"Hello<br/><br/>World\"\n      },{\n        in: \"Hello<br/><br/><br/><br/>World\"\n        out: \"Hello<br/><br/>World\"\n      }]\n      for test in tests\n        waitsForPromise =>\n          @clipboardService._sanitizeHTMLInput(test.in).then (out) ->\n            expect(out).toBe(test.out)\n\n\n    it \"should remove all leading and trailing <br/>s from the text\", ->\n      tests = [{\n        in: \"<br/><br/>Hello<br/>World\"\n        out: \"Hello<br/>World\"\n      },{\n        in: \"<br/><br/>Hello<br/><br/><br/><br/>\"\n        out: \"Hello\"\n      }]\n      for test in tests\n        waitsForPromise =>\n          @clipboardService._sanitizeHTMLInput(test.in).then (out) ->\n            expect(out).toBe(test.out)\n\n  # Unfortunately, it doesn't seem we can do real IPC (to `juice` in the main process)\n  # so these tests are non-functional.\n  xdescribe \"real-world examples\", ->\n    it \"should produce the correct output\", ->\n      scenarios = []\n      fixtures = path.resolve('./spec/fixtures/paste')\n      for filename in fs.readdirSync(fixtures)\n        if filename[-8..-1] is '-in.html'\n          scenarios.push\n            in: fs.readFileSync(path.join(fixtures, filename)).toString()\n            out: fs.readFileSync(path.join(fixtures, \"#{filename[0..-9]}-out.html\")).toString()\n\n      scenarios.forEach (scenario) =>\n        @clipboardService._sanitizeHTMLInput(scenario.in).then (out) ->\n          expect(out).toBe(scenario.out)\n"
  },
  {
    "path": "packages/client-app/spec/components/contenteditable-component-spec.cjsx",
    "content": "# This tests the basic Contenteditable component. For various modules of\n# the contenteditable (such as selection, tooltip, quoting, etc) see the\n# related test files.\n#\n_ = require \"underscore\"\nfs = require 'fs'\nReact = require \"react\"\nReactDOM = require 'react-dom'\nReactTestUtils = require('react-addons-test-utils')\nContenteditable = require \"../../src/components/contenteditable/contenteditable\",\n\ndescribe \"Contenteditable\", ->\n  beforeEach ->\n    @onChange = jasmine.createSpy('onChange')\n    html = 'Test <strong>HTML</strong>'\n    @component = ReactTestUtils.renderIntoDocument(\n      <Contenteditable html={html} onChange={@onChange}/>\n    )\n    @editableNode = ReactDOM.findDOMNode(@component).querySelector('[contenteditable]')\n\n  describe \"render\", ->\n    it 'should render into the document', ->\n      expect(ReactTestUtils.isCompositeComponentWithType @component, Contenteditable).toBe true\n\n    it \"should include a content-editable div\", ->\n      expect(@editableNode).toBeDefined()\n\n  describe \"when the html is changed\", ->\n    beforeEach ->\n      @changedHtmlWithoutQuote = 'Changed <strong>NEW 1 HTML</strong><br>'\n\n      @performEdit = (newHTML, component = @component) =>\n        @editableNode.innerHTML = newHTML\n\n    it \"should fire `props.onChange`\", ->\n      runs =>\n        @performEdit('Test <strong>New HTML</strong>')\n      waitsFor =>\n        @onChange.calls.length > 0\n      runs =>\n        expect(@onChange).toHaveBeenCalled()\n\n    # One day we may make this more efficient. For now we aggressively\n    # re-render because of the manual cursor positioning.\n    it \"should fire if the html is the same\", ->\n      expect(@onChange.callCount).toBe(0)\n      runs =>\n        @performEdit(@changedHtmlWithoutQuote)\n        @performEdit(@changedHtmlWithoutQuote)\n      waitsFor =>\n        @onChange.callCount > 0\n      runs =>\n        expect(@onChange).toHaveBeenCalled()\n\n  describe \"pasting\", ->\n    beforeEach ->\n\n    describe \"when a file item is present\", ->\n      beforeEach ->\n        @mockEvent =\n          preventDefault: jasmine.createSpy('preventDefault')\n          clipboardData:\n            items: [{\n              kind: 'file'\n              type: 'image/png'\n              getAsFile: -> new Blob(['12341352312411'], {type : 'image/png'})\n            }]\n\n      it \"should save the image to a temporary file and call `onFilePaste`\", ->\n        onPaste = jasmine.createSpy('onPaste')\n        @component = ReactTestUtils.renderIntoDocument(\n          <Contenteditable html={''} onChange={@onChange} onFilePaste={onPaste} />\n        )\n        @editableNode = ReactDOM.findDOMNode(@component).querySelector('[contenteditable]')\n        runs ->\n          ReactTestUtils.Simulate.paste(@editableNode, @mockEvent)\n        waitsFor ->\n          onPaste.callCount > 0\n        runs ->\n          path = require('path')\n          file = onPaste.mostRecentCall.args[0]\n          expect(path.basename(file)).toEqual('Pasted File.png')\n          contents = fs.readFileSync(file)\n          expect(contents.toString()).toEqual('12341352312411')\n"
  },
  {
    "path": "packages/client-app/spec/components/date-input-spec.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport {\n  Simulate,\n  findRenderedDOMComponentWithClass,\n} from 'react-addons-test-utils';\n\nimport {DateUtils} from 'nylas-exports'\nimport DateInput from '../../src/components/date-input';\nimport {renderIntoDocument} from '../nylas-test-utils'\n\nconst {findDOMNode} = ReactDOM;\n\nconst makeInput = (props = {}) => {\n  const input = renderIntoDocument(<DateInput {...props} dateFormat=\"blah\" />);\n  if (props.initialState) {\n    input.setState(props.initialState)\n  }\n  return input\n};\n\ndescribe('DateInput', function dateInput() {\n  describe('onInputKeyDown', () => {\n    it('should submit the input if Enter or Escape pressed', () => {\n      const onDateSubmitted = jasmine.createSpy('onDateSubmitted')\n      const component = makeInput({onDateSubmitted: onDateSubmitted})\n      const inputNode = ReactDOM.findDOMNode(component).querySelector('input')\n      const stopPropagation = jasmine.createSpy('stopPropagation')\n      const keys = ['Enter', 'Return']\n      inputNode.value = 'tomorrow'\n      spyOn(DateUtils, 'futureDateFromString').andReturn('someday')\n\n      keys.forEach((key) => {\n        Simulate.keyDown(inputNode, {key, stopPropagation})\n        expect(stopPropagation).toHaveBeenCalled()\n        expect(onDateSubmitted).toHaveBeenCalledWith('someday', 'tomorrow')\n        stopPropagation.reset()\n        onDateSubmitted.reset()\n      })\n    });\n  });\n\n  describe('render', () => {\n    beforeEach(() => {\n      spyOn(DateUtils, 'format').andReturn('formatted')\n    });\n\n    it('should render a date interpretation if a date has been inputted', () => {\n      const component = makeInput({initialState: {inputDate: 'something!'}})\n      spyOn(component, 'setState')\n      const dateInterpretation = findDOMNode(findRenderedDOMComponentWithClass(component, 'date-interpretation'))\n\n      expect(dateInterpretation.textContent).toEqual('formatted')\n    });\n\n    it('should not render a date interpretation if no input date available', () => {\n      const component = makeInput({initialState: {inputDate: null}})\n      spyOn(component, 'setState')\n      expect(() => {\n        findRenderedDOMComponentWithClass(component, 'date-interpretation')\n      }).toThrow()\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/date-picker-popover-spec.jsx",
    "content": "import React from 'react';\nimport {mount} from 'enzyme'\nimport {DateUtils} from 'nylas-exports'\nimport {DatePickerPopover} from 'nylas-component-kit'\n\n\nconst makePopover = (props = {}) => {\n  return mount(\n    <DatePickerPopover\n      dateOptions={{}}\n      header={<span className=\"header\">my header</span>}\n      onSelectDate={() => {}}\n      {...props}\n    />\n  );\n};\n\ndescribe('DatePickerPopover', function sendLaterPopover() {\n  beforeEach(() => {\n    spyOn(DateUtils, 'format').andReturn('formatted')\n  });\n\n  describe('selectDate', () => {\n    it('calls props.onSelectDate', () => {\n      const onSelectDate = jasmine.createSpy('onSelectDate')\n      const popover = makePopover({onSelectDate})\n      popover.instance().selectDate({utc: () => 'utc'}, 'Custom')\n\n      expect(onSelectDate).toHaveBeenCalledWith('formatted', 'Custom')\n    });\n  });\n\n  describe('onSelectMenuOption', () => {\n\n  });\n\n  describe('onCustomDateSelected', () => {\n    it('selects date', () => {\n      const popover = makePopover()\n      const instance = popover.instance()\n      spyOn(instance, 'selectDate')\n      instance.onCustomDateSelected('date', 'abc')\n      expect(instance.selectDate).toHaveBeenCalledWith('date', 'Custom')\n    });\n\n    it('throws error if date is invalid', () => {\n      spyOn(NylasEnv, 'showErrorDialog')\n      const popover = makePopover()\n      popover.instance().onCustomDateSelected(null, 'abc')\n      expect(NylasEnv.showErrorDialog).toHaveBeenCalled()\n    });\n  });\n\n  describe('render', () => {\n    it('renders the provided dateOptions', () => {\n      const popover = makePopover({\n        dateOptions: {\n          'label 1-': () => {},\n          'label 2-': () => {},\n        },\n      })\n      const items = popover.find('.item')\n      expect(items.at(0).text()).toEqual('label 1-formatted')\n      expect(items.at(1).text()).toEqual('label 2-formatted')\n    });\n\n    it('renders header components', () => {\n      const popover = makePopover()\n      expect(popover.find('.header').text()).toEqual('my header')\n    })\n\n    it('renders footer components', () => {\n      const popover = makePopover({\n        footer: <span key=\"footer\" className=\"footer\">footer</span>,\n      })\n      expect(popover.find('.footer').text()).toEqual('footer')\n      expect(popover.find('.date-input-section').exists()).toBe(true)\n    });\n  });\n});\n\n"
  },
  {
    "path": "packages/client-app/spec/components/editable-list-spec.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport {\n  findRenderedDOMComponentWithClass,\n  scryRenderedDOMComponentsWithClass,\n  Simulate,\n} from 'react-addons-test-utils';\n\nimport EditableList from '../../src/components/editable-list';\nimport {renderIntoDocument, simulateCommand} from '../nylas-test-utils'\n\nconst {findDOMNode} = ReactDOM;\n\nconst makeList = (items = [], props = {}) => {\n  const list = renderIntoDocument(<EditableList {...props} items={items} />);\n  if (props.initialState) {\n    list.setState(props.initialState)\n  }\n  return list\n};\n\ndescribe('EditableList', function editableList() {\n  describe('_onItemClick', () => {\n    it('calls onSelectItem', () => {\n      const onSelectItem = jasmine.createSpy('onSelectItem');\n      const list = makeList(['1', '2'], {onSelectItem});\n      const item = scryRenderedDOMComponentsWithClass(list, 'editable-item')[0];\n\n      Simulate.click(item);\n\n      expect(onSelectItem).toHaveBeenCalledWith('1', 0);\n    });\n  });\n\n  describe('_onItemEdit', () => {\n    it('enters editing mode when double click', () => {\n      const list = makeList(['1', '2']);\n      spyOn(list, 'setState');\n      const item = scryRenderedDOMComponentsWithClass(list, 'editable-item')[0];\n\n      Simulate.doubleClick(item);\n\n      expect(list.setState).toHaveBeenCalledWith({editingIndex: 0});\n    });\n\n    it('enters editing mode when edit icon clicked', () => {\n      const list = makeList(['1', '2']);\n      spyOn(list, 'setState');\n      const editIcon = scryRenderedDOMComponentsWithClass(list, 'edit-icon')[0];\n\n      Simulate.click(editIcon);\n\n      expect(list.setState).toHaveBeenCalledWith({editingIndex: 0});\n    });\n  });\n\n  describe('core:previous-item / core:next-item', () => {\n    it('calls onSelectItem', () => {\n      const onSelectItem = jasmine.createSpy('onSelectItem');\n      const list = makeList(['1', '2'], {selected: '1', onSelectItem});\n      const innerList = findRenderedDOMComponentWithClass(list, 'items-wrapper');\n\n      simulateCommand(innerList, 'core:next-item')\n\n      expect(onSelectItem).toHaveBeenCalledWith('2', 1);\n    });\n\n    it('does not select an item when at the bottom of the list and moves down', () => {\n      const onSelectItem = jasmine.createSpy('onSelectItem');\n      const list = makeList(['1', '2'], {selected: '2', onSelectItem});\n      const innerList = findRenderedDOMComponentWithClass(list, 'items-wrapper');\n\n      simulateCommand(innerList, 'core:next-item')\n\n      expect(onSelectItem).not.toHaveBeenCalled();\n    });\n\n    it('does not select an item when at the top of the list and moves up', () => {\n      const onSelectItem = jasmine.createSpy('onSelectItem');\n      const list = makeList(['1', '2'], {selected: '1', onSelectItem});\n      const innerList = findRenderedDOMComponentWithClass(list, 'items-wrapper');\n\n      simulateCommand(innerList, 'core:previous-item')\n\n      expect(onSelectItem).not.toHaveBeenCalled();\n    });\n\n    it('does not clear the selection when esc pressed but prop does not allow it', () => {\n      const onSelectItem = jasmine.createSpy('onSelectItem');\n      const list = makeList(['1', '2'], {selected: '1', allowEmptySelection: false, onSelectItem});\n      const innerList = findRenderedDOMComponentWithClass(list, 'items-wrapper');\n\n      Simulate.keyDown(innerList, {key: 'Escape'});\n\n      expect(onSelectItem).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('_onCreateInputKeyDown', () => {\n    it('calls onItemCreated', () => {\n      const onItemCreated = jasmine.createSpy('onItemCreated');\n      const list = makeList(['1', '2'], {initialState: {creatingItem: true}, onItemCreated});\n      const createItem = findRenderedDOMComponentWithClass(list, 'create-item-input');\n      const input = createItem.querySelector('input');\n      findDOMNode(input).value = 'New Item';\n\n      Simulate.keyDown(input, {key: 'Enter'});\n\n      expect(onItemCreated).toHaveBeenCalledWith('New Item');\n    });\n\n    it('does not call onItemCreated when no value entered', () => {\n      const onItemCreated = jasmine.createSpy('onItemCreated');\n      const list = makeList(['1', '2'], {initialState: {creatingItem: true}, onItemCreated});\n      const createItem = findRenderedDOMComponentWithClass(list, 'create-item-input');\n      const input = createItem.querySelector('input');\n      findDOMNode(input).value = '';\n\n      Simulate.keyDown(input, {key: 'Enter'});\n\n      expect(onItemCreated).not.toHaveBeenCalled();\n    });\n  });\n\n  describe('_onCreateItem', () => {\n    it('should call prop callback when provided', () => {\n      const onCreateItem = jasmine.createSpy('onCreateItem');\n      const list = makeList(['1', '2'], {onCreateItem});\n\n      list._onCreateItem();\n      expect(onCreateItem).toHaveBeenCalled();\n    });\n\n    it('should set state for creating item when no callback provided', () => {\n      const list = makeList(['1', '2']);\n      spyOn(list, 'setState');\n      list._onCreateItem();\n      expect(list.setState).toHaveBeenCalledWith({creatingItem: true});\n    });\n  });\n\n  describe('_onDeleteItem', () => {\n    let onSelectItem;\n    let onDeleteItem;\n    beforeEach(() => {\n      onSelectItem = jasmine.createSpy('onSelectItem');\n      onDeleteItem = jasmine.createSpy('onDeleteItem');\n    })\n    it('deletes the item from the list', () => {\n      const list = makeList(['1', '2'], {selected: '2', onDeleteItem, onSelectItem});\n      const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[1];\n\n      Simulate.click(button);\n      expect(onDeleteItem).toHaveBeenCalledWith('2', 1);\n    })\n    it('sets the selected item to the one above if it exists', () => {\n      const list = makeList(['1', '2'], {selected: '2', onDeleteItem, onSelectItem});\n      const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[1];\n\n      Simulate.click(button);\n      expect(onSelectItem).toHaveBeenCalledWith('1', 0)\n    })\n    it('sets the selected item to the one below if it is at the top', () => {\n      const list = makeList(['1', '2'], {selected: '1', onDeleteItem, onSelectItem});\n      const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[1];\n\n      Simulate.click(button);\n      expect(onSelectItem).toHaveBeenCalledWith('2', 1)\n    })\n    it('sets the selected item to nothing when you delete the last item', () => {\n      const list = makeList(['1'], {selected: '1', onDeleteItem, onSelectItem});\n      const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[1];\n\n      Simulate.click(button);\n      expect(onSelectItem).not.toHaveBeenCalled()\n    })\n  })\n  describe('_renderItem', () => {\n    const makeItem = (item, idx, state = {}, handlers = {}) => {\n      const list = makeList([], {initialState: state});\n      return renderIntoDocument(\n        list._renderItem(item, idx, state, handlers)\n      );\n    };\n\n    it('binds correct click callbacks', () => {\n      const onClick = jasmine.createSpy('onClick');\n      const onEdit = jasmine.createSpy('onEdit');\n      const item = makeItem('item 1', 0, {}, {onClick, onEdit});\n\n      Simulate.click(item);\n      expect(onClick.calls[0].args[1]).toEqual('item 1');\n      expect(onClick.calls[0].args[2]).toEqual(0);\n\n      Simulate.doubleClick(item);\n      expect(onEdit.calls[0].args[1]).toEqual('item 1');\n      expect(onEdit.calls[0].args[2]).toEqual(0);\n    });\n\n    it('renders correctly when item is selected', () => {\n      const item = findDOMNode(makeItem('item 1', 0, {selected: 'item 1'}));\n      expect(item.className.indexOf('selected')).not.toEqual(-1);\n    });\n\n    it('renders correctly when item is string', () => {\n      const item = findDOMNode(makeItem('item 1', 0));\n      expect(item.className.indexOf('selected')).toEqual(-1);\n      expect(item.className.indexOf('editable-item')).not.toEqual(-1);\n      expect(item.innerText).toEqual('item 1');\n    });\n\n    it('renders correctly when item is component', () => {\n      const item = findDOMNode(makeItem(<div />, 0));\n      expect(item.className.indexOf('selected')).toEqual(-1);\n      expect(item.className.indexOf('editable-item')).toEqual(-1);\n      expect(item.childNodes[0].tagName).toEqual('DIV');\n    });\n\n    it('renders correctly when item is in editing state', () => {\n      const onInputBlur = jasmine.createSpy('onInputBlur');\n      const onInputFocus = jasmine.createSpy('onInputFocus');\n      const onInputKeyDown = jasmine.createSpy('onInputKeyDown');\n\n      const item = makeItem('item 1', 0, {editingIndex: 0}, {onInputBlur, onInputFocus, onInputKeyDown});\n      const input = item.querySelector('input')\n\n      Simulate.focus(input);\n      Simulate.keyDown(input);\n      Simulate.blur(input);\n\n      expect(onInputFocus).toHaveBeenCalled();\n      expect(onInputBlur).toHaveBeenCalled();\n      expect(onInputKeyDown.calls[0].args[1]).toEqual('item 1');\n      expect(onInputKeyDown.calls[0].args[2]).toEqual(0);\n\n      expect(findDOMNode(input).tagName).toEqual('INPUT');\n    });\n  });\n\n  describe('render', () => {\n    it('renders list of items', () => {\n      const items = ['1', '2', '3'];\n      const list = makeList(items);\n      const innerList = findDOMNode(\n        findRenderedDOMComponentWithClass(list, 'scroll-region-content-inner')\n      );\n      expect(() => {\n        findRenderedDOMComponentWithClass(list, 'create-item-input');\n      }).toThrow();\n\n      expect(innerList.childNodes.length).toEqual(3);\n      items.forEach((item, idx) => expect(innerList.childNodes[idx].textContent).toEqual(item));\n    });\n\n    it('renders create input as an item when creating', () => {\n      const items = ['1', '2', '3'];\n      const list = makeList(items, {initialState: {creatingItem: true}});\n      const createItem = findRenderedDOMComponentWithClass(list, 'create-item-input');\n      expect(createItem).toBeDefined();\n    });\n\n    it('renders add button', () => {\n      const list = makeList();\n      const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[0];\n\n      expect(findDOMNode(button).textContent).toEqual('+');\n    });\n\n    it('renders delete button', () => {\n      const list = makeList(['1', '2'], {selected: '2'});\n      const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[1];\n\n      expect(findDOMNode(button).textContent).toEqual('—');\n    });\n\n    it('disables the delete button when no item is selected', () => {\n      const onSelectItem = jasmine.createSpy('onSelectItem');\n      const onDeleteItem = jasmine.createSpy('onDeleteItem');\n      const list = makeList(['1', '2'], {selected: null, onDeleteItem, onSelectItem});\n      const button = scryRenderedDOMComponentsWithClass(list, 'btn-editable-list')[1];\n\n      Simulate.click(button);\n\n      expect(onDeleteItem).not.toHaveBeenCalledWith('2', 1);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/editable-table-spec.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport {mount, shallow} from 'enzyme'\nimport {SelectableTable, EditableTableCell, EditableTable} from 'nylas-component-kit'\nimport {selection, cellProps, tableProps, testDataSource} from '../fixtures/table-data'\n\n\ndescribe('EditableTable Components', function describeBlock() {\n  describe('EditableTableCell', () => {\n    function renderCell(props) {\n      // This node is used so that React does not issue DOM tree warnings when running\n      // the tests\n      const table = document.createElement('table')\n      table.innerHTML = '<tbody><tr></tr></tbody>'\n      const cellRootNode = table.querySelector('tr')\n      return mount(\n        <EditableTableCell\n          {...cellProps}\n          {...props}\n        />,\n        {attachTo: cellRootNode}\n      )\n    }\n\n    describe('onInputBlur', () => {\n      it('should call onCellEdited if value is different from current value', () => {\n        const onCellEdited = jasmine.createSpy('onCellEdited')\n        const event = {\n          target: {value: 'new-val'},\n        }\n        const cell = renderCell({onCellEdited, isHeader: false}).instance()\n        cell.onInputBlur(event)\n        expect(onCellEdited).toHaveBeenCalledWith({\n          rowIdx: 0, colIdx: 0, value: 'new-val', isHeader: false,\n        })\n      });\n\n      it('should not call onCellEdited otherwise', () => {\n        const onCellEdited = jasmine.createSpy('onCellEdited')\n        const event = {\n          target: {value: 1},\n        }\n        const cell = renderCell({onCellEdited}).instance()\n        cell.onInputBlur(event)\n        expect(onCellEdited).not.toHaveBeenCalled()\n      });\n    });\n\n    describe('onInputKeyDown', () => {\n      it('calls onAddRow if Enter pressed and cell is in last row', () => {\n        const onAddRow = jasmine.createSpy('onAddRow')\n        const event = {\n          key: 'Enter',\n          stopPropagation: jasmine.createSpy('stopPropagation'),\n        }\n        const cell = renderCell({rowIdx: 2, onAddRow}).instance()\n        cell.onInputKeyDown(event)\n        expect(event.stopPropagation).toHaveBeenCalled()\n        expect(onAddRow).toHaveBeenCalled()\n      });\n\n      it('stops event propagation and blurs input if Escape pressed', () => {\n        const focusSpy = jasmine.createSpy('focusSpy')\n        spyOn(ReactDOM, 'findDOMNode').andReturn({\n          focus: focusSpy,\n        })\n        const event = {\n          key: 'Escape',\n          stopPropagation: jasmine.createSpy('stopPropagation'),\n        }\n        const cell = renderCell().instance()\n        cell.onInputKeyDown(event)\n        expect(event.stopPropagation).toHaveBeenCalled()\n        expect(focusSpy).toHaveBeenCalled()\n      });\n    });\n\n    it('renders a SelectableTableCell with the correct props', () => {\n      const cell = renderCell()\n      expect(cell.prop('tableDataSource')).toBe(testDataSource)\n      expect(cell.prop('selection')).toBe(selection)\n      expect(cell.prop('rowIdx')).toBe(0)\n      expect(cell.prop('colIdx')).toBe(0)\n    });\n\n    it('renders the InputRenderer as the child of the SelectableTableCell with the correct props', () => {\n      const InputRenderer = () => <input />\n      const inputProps = {p1: 'p1'}\n      const input = renderCell({\n        rowIdx: 2,\n        colIdx: 2,\n        inputProps,\n        InputRenderer,\n      }).childAt(0).childAt(0)\n      expect(input.type()).toBe(InputRenderer)\n      expect(input.prop('rowIdx')).toBe(2)\n      expect(input.prop('colIdx')).toBe(2)\n      expect(input.prop('p1')).toBe('p1')\n      expect(input.prop('defaultValue')).toBe(9)\n      expect(input.prop('tableDataSource')).toBe(testDataSource)\n    });\n  });\n\n  describe('EditableTable', () => {\n    function renderTable(props) {\n      return shallow(\n        <EditableTable\n          {...tableProps}\n          {...props}\n        />\n      )\n    }\n\n    it('renders column buttons if onAddColumn and onRemoveColumn are provided', () => {\n      const onAddColumn = () => {}\n      const onRemoveColumn = () => {}\n      const table = renderTable({onAddColumn, onRemoveColumn})\n      expect(table.hasClass('editable-table-container')).toBe(true)\n      expect(table.find('.btn').length).toBe(2)\n    });\n\n    it('renders only a SelectableTable if column callbacks are not provided', () => {\n      const table = renderTable()\n      expect(table.find('.btn').length).toBe(0)\n    });\n\n    it('renders with the correct props', () => {\n      const onAddRow = () => {}\n      const onCellEdited = () => {}\n      const inputProps = {}\n      const InputRenderer = () => <input />\n      const other = 'other'\n      const table = renderTable({\n        onAddRow,\n        onCellEdited,\n        inputProps,\n        InputRenderer,\n        other,\n      }).find(SelectableTable)\n      expect(table.prop('extraProps').onAddRow).toBe(onAddRow)\n      expect(table.prop('extraProps').onCellEdited).toBe(onCellEdited)\n      expect(table.prop('extraProps').inputProps).toBe(inputProps)\n      expect(table.prop('extraProps').InputRenderer).toBe(InputRenderer)\n      expect(table.prop('other')).toEqual('other')\n      expect(table.prop('CellRenderer')).toBe(EditableTableCell)\n      expect(table.hasClass('editable-table')).toBe(true)\n    });\n  });\n});\n\n"
  },
  {
    "path": "packages/client-app/spec/components/evented-iframe-spec.cjsx",
    "content": "React = require \"react\"\nReactTestUtils = require('react-addons-test-utils')\nEventedIFrame = require '../../src/components/evented-iframe'\n\ndescribe 'EventedIFrame', ->\n  describe 'link clicking behavior', ->\n\n    beforeEach ->\n      @frame = ReactTestUtils.renderIntoDocument(\n        <EventedIFrame src=\"about:blank\" />\n      )\n\n      @setAttributeSpy = jasmine.createSpy('setAttribute')\n      @preventDefaultSpy = jasmine.createSpy('preventDefault')\n      @openLinkSpy = jasmine.createSpy(\"openLink\")\n\n      @oldOpenLink = NylasEnv.windowEventHandler.openLink\n      NylasEnv.windowEventHandler.openLink = @openLinkSpy\n\n      @fakeEvent = (href) =>\n        stopPropagation: ->\n        preventDefault: @preventDefaultSpy\n        target:\n          getAttribute: (attr) -> return href\n          setAttribute: @setAttributeSpy\n\n    afterEach ->\n      NylasEnv.windowEventHandler.openLink = @oldOpenLink\n\n    it 'works for acceptable link types', ->\n      hrefs = [\n        \"http://nylas.com\"\n        \"https://www.nylas.com\"\n        \"mailto:evan@nylas.com\"\n        \"tel:8585311718\"\n        \"custom:www.nylas.com\"\n      ]\n      for href, i in hrefs\n        @frame._onIFrameClick(@fakeEvent(href))\n        expect(@setAttributeSpy).not.toHaveBeenCalled()\n        expect(@openLinkSpy).toHaveBeenCalled()\n        target = @openLinkSpy.calls[i].args[0].target\n        targetHref = @openLinkSpy.calls[i].args[0].href\n        expect(target).not.toBeDefined()\n        expect(targetHref).toBe href\n\n    it 'corrects relative uris', ->\n      hrefs = [\n        \"nylas.com\"\n        \"www.nylas.com\"\n      ]\n      for href, i in hrefs\n        @frame._onIFrameClick(@fakeEvent(href))\n        expect(@setAttributeSpy).toHaveBeenCalled()\n        modifiedHref = @setAttributeSpy.calls[i].args[1]\n        expect(modifiedHref).toBe \"http://#{href}\"\n\n    it 'corrects protocol-relative uris', ->\n      hrefs = [\n        \"//nylas.com\"\n        \"//www.nylas.com\"\n      ]\n      for href, i in hrefs\n        @frame._onIFrameClick(@fakeEvent(href))\n        expect(@setAttributeSpy).toHaveBeenCalled()\n        modifiedHref = @setAttributeSpy.calls[i].args[1]\n        expect(modifiedHref).toBe \"https:#{href}\"\n\n    it 'disallows malicious uris', ->\n      hrefs = [\n        \"file://usr/bin/bad\"\n      ]\n      for href in hrefs\n        @frame._onIFrameClick(@fakeEvent(href))\n        expect(@preventDefaultSpy).toHaveBeenCalled()\n        expect(@openLinkSpy).not.toHaveBeenCalled()\n\n"
  },
  {
    "path": "packages/client-app/spec/components/fixed-popover-spec.jsx",
    "content": "import React from 'react';\nimport FixedPopover from '../../src/components/fixed-popover';\nimport {renderIntoDocument} from '../nylas-test-utils'\n\n\nconst {Directions: {Up, Down, Left, Right}} = FixedPopover\n\nconst makePopover = (props = {}) => {\n  const originRect = props.originRect ? props.originRect : {};\n  const popover = renderIntoDocument(\n    <FixedPopover\n      {...props}\n      originRect={originRect}\n    />\n  );\n  if (props.initialState) {\n    popover.setState(props.initialState)\n  }\n  return popover\n};\n\ndescribe('FixedPopover', function fixedPopover() {\n  describe('computeAdjustedOffsetAndDirection', () => {\n    beforeEach(() => {\n      this.popover = makePopover()\n      this.PADDING = 10\n      this.windowDimensions = {\n        height: 500,\n        width: 500,\n      }\n    });\n\n    const compute = (direction, {fallback, top, left, bottom, right}) => {\n      return this.popover.computeAdjustedOffsetAndDirection({\n        direction,\n        windowDimensions: this.windowDimensions,\n        currentRect: {\n          top,\n          left,\n          bottom,\n          right,\n        },\n        fallback,\n        offsetPadding: this.PADDING,\n      })\n    }\n\n    it('returns null when no overflows present', () => {\n      const res = compute(Up, {top: 10, left: 10, right: 20, bottom: 20})\n      expect(res).toBe(null)\n    });\n\n    describe('when overflowing on 1 side of the window', () => {\n      it('returns fallback direction when it is specified', () => {\n        const {offset, direction} = compute(Up, {fallback: Left, top: -10, left: 10, right: 20, bottom: 10})\n        expect(offset).toEqual({})\n        expect(direction).toEqual(Left)\n      });\n\n      it('inverts direction if is Up and overflows on the top', () => {\n        const {offset, direction} = compute(Up, {top: -10, left: 10, right: 20, bottom: 10})\n        expect(offset).toEqual({})\n        expect(direction).toEqual(Down)\n      });\n\n      it('inverts direction if is Down and overflows on the bottom', () => {\n        const {offset, direction} = compute(Down, {top: 490, left: 10, right: 20, bottom: 510})\n        expect(offset).toEqual({})\n        expect(direction).toEqual(Up)\n      });\n\n      it('inverts direction if is Right and overflows on the right', () => {\n        const {offset, direction} = compute(Right, {top: 10, left: 490, right: 510, bottom: 20})\n        expect(offset).toEqual({})\n        expect(direction).toEqual(Left)\n      });\n\n      it('inverts direction if is Left and overflows on the left', () => {\n        const {offset, direction} = compute(Left, {top: 10, left: -10, right: 10, bottom: 20})\n        expect(offset).toEqual({})\n        expect(direction).toEqual(Right)\n      });\n\n      [Up, Down, Left, Right].forEach((dir) => {\n        if (dir === Up || dir === Down) {\n          it('moves left if its overflowing on the right', () => {\n            const {offset, direction} = compute(dir, {top: 10, left: 490, right: 510, bottom: 20})\n            expect(offset).toEqual({x: -20})\n            expect(direction).toEqual(dir)\n          });\n\n          it('moves right if overflows on the left', () => {\n            const {offset, direction} = compute(dir, {top: 10, left: -10, right: 10, bottom: 20})\n            expect(offset).toEqual({x: 20})\n            expect(direction).toEqual(dir)\n          });\n        }\n\n        if (dir === Left || dir === Right) {\n          it('moves up if its overflowing on the bottom', () => {\n            const {offset, direction} = compute(dir, {top: 490, left: 10, right: 20, bottom: 510})\n            expect(offset).toEqual({y: -20})\n            expect(direction).toEqual(dir)\n          });\n\n          it('moves down if overflows on the top', () => {\n            const {offset, direction} = compute(dir, {top: -10, left: 10, right: 20, bottom: 10})\n            expect(offset).toEqual({y: 20})\n            expect(direction).toEqual(dir)\n          });\n        }\n      })\n    })\n\n    describe('when overflowing on 2 sides of the window', () => {\n      describe('when direction is up', () => {\n        it('computes correctly when it overflows up and right', () => {\n          const {offset, direction} = compute(Up, {top: -10, left: 10, right: 510, bottom: 10})\n          expect(offset).toEqual({x: -20})\n          expect(direction).toEqual(Down)\n        });\n\n        it('computes correctly when it overflows up and left', () => {\n          const {offset, direction} = compute(Up, {top: -10, left: -10, right: 10, bottom: 10})\n          expect(offset).toEqual({x: 20})\n          expect(direction).toEqual(Down)\n        });\n      });\n\n      describe('when direction is right', () => {\n        it('computes correctly when it overflows right and up', () => {\n          const {offset, direction} = compute(Right, {top: -10, left: 490, right: 510, bottom: 10})\n          expect(offset).toEqual({y: 20})\n          expect(direction).toEqual(Left)\n        });\n\n        it('computes correctly when it overflows right and down', () => {\n          const {offset, direction} = compute(Right, {top: 490, left: 490, right: 510, bottom: 510})\n          expect(offset).toEqual({y: -20})\n          expect(direction).toEqual(Left)\n        });\n      });\n\n      describe('when direction is left', () => {\n        it('computes correctly when it overflows left and up', () => {\n          const {offset, direction} = compute(Left, {top: -10, left: -10, right: 10, bottom: 10})\n          expect(offset).toEqual({y: 20})\n          expect(direction).toEqual(Right)\n        });\n\n        it('computes correctly when it overflows left and down', () => {\n          const {offset, direction} = compute(Left, {top: 490, left: -10, right: 10, bottom: 510})\n          expect(offset).toEqual({y: -20})\n          expect(direction).toEqual(Right)\n        });\n      });\n\n      describe('when direction is down', () => {\n        it('computes correctly when it overflows down and left', () => {\n          const {offset, direction} = compute(Down, {top: 490, left: -10, right: 10, bottom: 510})\n          expect(offset).toEqual({x: 20})\n          expect(direction).toEqual(Up)\n        });\n\n        it('computes correctly when it overflows down and right', () => {\n          const {offset, direction} = compute(Down, {top: 490, left: 490, right: 510, bottom: 510})\n          expect(offset).toEqual({x: -20})\n          expect(direction).toEqual(Up)\n        });\n      });\n    });\n  });\n\n  describe('computePopoverStyles', () => {\n    // TODO\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/injected-component-set-spec.jsx",
    "content": "/* eslint react/prefer-es6-class: \"off\" */\n/* eslint react/prefer-stateless-function: \"off\" */\n\nimport {React, ComponentRegistry, NylasTestUtils} from 'nylas-exports';\nimport {InjectedComponentSet} from 'nylas-component-kit';\n\nconst {renderIntoDocument} = NylasTestUtils;\n\nconst reactStub = (displayName) => {\n  return React.createClass({\n    displayName,\n    render() { return <div className={displayName} /> },\n  });\n};\n\n\ndescribe('InjectedComponentSet', function injectedComponentSet() {\n  describe('render', () => {\n    beforeEach(() => {\n      const components = [reactStub('comp1'), reactStub('comp2')];\n      spyOn(ComponentRegistry, 'findComponentsMatching').andReturn(components);\n    });\n\n    it('calls `onComponentsDidRender` when all child comps have actually been rendered to the dom', () => {\n      let rendered;\n      const onComponentsDidRender = () => {\n        rendered = true;\n      };\n      runs(() => {\n        renderIntoDocument(\n          <InjectedComponentSet\n            matching={{}}\n            onComponentsDidRender={onComponentsDidRender}\n          />\n        );\n      });\n\n      waitsFor(\n        () => { return rendered; },\n        '`onComponentsDidMount` should be called',\n        100\n      );\n\n      runs(() => {\n        expect(rendered).toBe(true);\n        expect(document.querySelectorAll('.comp1').length).toEqual(1);\n        expect(document.querySelectorAll('.comp2').length).toEqual(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/multiselect-dropdown-spec.jsx",
    "content": "import React from 'react'\nimport {\n  scryRenderedDOMComponentsWithClass,\n  Simulate,\n} from 'react-addons-test-utils';\n\nimport MultiselectDropdown from '../../src/components/multiselect-dropdown'\nimport {renderIntoDocument} from '../nylas-test-utils'\n\nconst makeDropdown = (items = [], props = {}) => {\n  return renderIntoDocument(<MultiselectDropdown {...props} items={items} />)\n}\ndescribe('MultiselectDropdown', function multiSelectedDropdown() {\n  describe('_onItemClick', () => {\n    it('calls onToggleItem function', () => {\n      const onToggleItem = jasmine.createSpy('onToggleItem')\n      const itemChecked = jasmine.createSpy('itemChecked')\n      const itemKey = (i) => i\n      const dropdown = makeDropdown([\"annie@nylas.com\", \"anniecook@ostby.com\"], {onToggleItem, itemChecked, itemKey})\n      dropdown.setState({selectingItems: true})\n      const item = scryRenderedDOMComponentsWithClass(dropdown, 'item')[0]\n      Simulate.mouseDown(item)\n      expect(onToggleItem).toHaveBeenCalled()\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-app/spec/components/multiselect-list-interaction-handler-spec.coffee",
    "content": "MultiselectListInteractionHandler = require '../../src/components/multiselect-list-interaction-handler'\nWorkspaceStore = require '../../src/flux/stores/workspace-store'\nFocusedContentStore = require '../../src/flux/stores/focused-content-store'\nThread = require('../../src/flux/models/thread').default\nActions = require('../../src/flux/actions').default\n_ = require 'underscore'\n\ndescribe \"MultiselectListInteractionHandler\", ->\n  beforeEach ->\n    @item = new Thread(id:'123')\n    @itemFocus = new Thread({id: 'focus'})\n    @itemKeyboardFocus = new Thread({id: 'keyboard-focus'})\n    @itemAfterFocus = new Thread(id:'after-focus')\n    @itemAfterKeyboardFocus = new Thread(id:'after-keyboard-focus')\n\n    data = [@item, @itemFocus, @itemAfterFocus, @itemKeyboardFocus, @itemAfterKeyboardFocus]\n\n    @onFocusItem = jasmine.createSpy('onFocusItem')\n    @onSetCursorPosition = jasmine.createSpy('onSetCursorPosition')\n    @dataSource =\n      selection:\n        toggle: jasmine.createSpy('toggle')\n        expandTo: jasmine.createSpy('expandTo')\n        walk: jasmine.createSpy('walk')\n      get: (idx) ->\n        data[idx]\n      getById: (id) ->\n        _.find data, (item) -> item.id is id\n      indexOfId: (id) ->\n        _.findIndex data, (item) -> item.id is id\n      count: -> data.length\n\n    @props =\n      dataSource: @dataSource\n      keyboardCursorId: 'keyboard-focus'\n      focusedId: 'focus'\n      onFocusItem: @onFocusItem\n      onSetCursorPosition: @onSetCursorPosition\n\n    @collection = 'threads'\n    @isRootSheet = true\n    @handler = new MultiselectListInteractionHandler(@props)\n\n    spyOn(WorkspaceStore, 'topSheet').andCallFake => {root: @isRootSheet}\n\n  it \"should never show focus\", ->\n    expect(@handler.shouldShowFocus()).toEqual(false)\n\n  it \"should always show the keyboard cursor\", ->\n    expect(@handler.shouldShowKeyboardCursor()).toEqual(true)\n\n  it \"should always show checkmarks\", ->\n    expect(@handler.shouldShowCheckmarks()).toEqual(true)\n\n  describe \"onClick\", ->\n    it \"should focus list items\", ->\n      @handler.onClick(@item)\n      expect(@onFocusItem).toHaveBeenCalledWith(@item)\n\n  describe \"onMetaClick\", ->\n    it \"shoud toggle selection\", ->\n      @handler.onMetaClick(@item)\n      expect(@dataSource.selection.toggle).toHaveBeenCalledWith(@item)\n\n    it \"should focus the keyboard on the clicked item\", ->\n      @handler.onMetaClick(@item)\n      expect(@onSetCursorPosition).toHaveBeenCalledWith(@item)\n\n  describe \"onShiftClick\", ->\n    it \"should expand selection\", ->\n      @handler.onShiftClick(@item)\n      expect(@dataSource.selection.expandTo).toHaveBeenCalledWith(@item)\n\n    it \"should focus the keyboard on the clicked item\", ->\n      @handler.onShiftClick(@item)\n      expect(@onSetCursorPosition).toHaveBeenCalledWith(@item)\n\n  describe \"onEnter\", ->\n    it \"should focus the item with the current keyboard selection\", ->\n      @handler.onEnter()\n      expect(@onFocusItem).toHaveBeenCalledWith(@itemKeyboardFocus)\n\n  describe \"onSelectKeyboardItem (x key on keyboard)\", ->\n    describe \"on the root view\", ->\n      it \"should toggle the selection of the keyboard item\", ->\n        @isRootSheet = true\n        @handler.onSelectKeyboardItem()\n        expect(@dataSource.selection.toggle).toHaveBeenCalledWith(@itemKeyboardFocus)\n\n    describe \"on the thread view\", ->\n      it \"should toggle the selection of the focused item\", ->\n        @isRootSheet = false\n        @handler.onSelectKeyboardItem()\n        expect(@dataSource.selection.toggle).toHaveBeenCalledWith(@itemFocus)\n\n  describe \"onShift\", ->\n    describe \"on the root view\", ->\n      beforeEach ->\n        @isRootSheet = true\n\n      it \"should shift the keyboard item\", ->\n        @handler.onShift(1, {})\n        expect(@onSetCursorPosition).toHaveBeenCalledWith(@itemAfterKeyboardFocus)\n\n      it \"should walk selection if the select option is passed\", ->\n        @handler.onShift(1, select: true)\n        expect(@dataSource.selection.walk).toHaveBeenCalledWith({current: @itemKeyboardFocus, next: @itemAfterKeyboardFocus})\n\n    describe \"on the thread view\", ->\n      beforeEach ->\n        @isRootSheet = false\n\n      it \"should shift the focused item\", ->\n        @handler.onShift(1, {})\n        expect(@onFocusItem).toHaveBeenCalledWith(@itemAfterFocus)\n"
  },
  {
    "path": "packages/client-app/spec/components/multiselect-split-interaction-handler-spec.coffee",
    "content": "MultiselectSplitInteractionHandler = require '../../src/components/multiselect-split-interaction-handler'\nWorkspaceStore = require '../../src/flux/stores/workspace-store'\nFocusedContentStore = require '../../src/flux/stores/focused-content-store'\nThread = require('../../src/flux/models/thread').default\nActions = require('../../src/flux/actions').default\n_ = require 'underscore'\n\ndescribe \"MultiselectSplitInteractionHandler\", ->\n  beforeEach ->\n    @item = new Thread(id:'123')\n    @itemFocus = new Thread({id: 'focus'})\n    @itemKeyboardFocus = new Thread({id: 'keyboard-focus'})\n    @itemAfterFocus = new Thread(id:'after-focus')\n    @itemAfterKeyboardFocus = new Thread(id:'after-keyboard-focus')\n\n    data = [@item, @itemFocus, @itemAfterFocus, @itemKeyboardFocus, @itemAfterKeyboardFocus]\n\n    @onFocusItem = jasmine.createSpy('onFocusItem')\n    @onSetCursorPosition = jasmine.createSpy('onSetCursorPosition')\n    @selection = []\n    @dataSource =\n      selection:\n        toggle: jasmine.createSpy('toggle')\n        expandTo: jasmine.createSpy('expandTo')\n        add: jasmine.createSpy('add')\n        walk: jasmine.createSpy('walk')\n        clear: jasmine.createSpy('clear')\n        count: => @selection.length\n        items: => @selection\n        top: => @selection[-1]\n\n      get: (idx) ->\n        data[idx]\n      getById: (id) ->\n        _.find data, (item) -> item.id is id\n      indexOfId: (id) ->\n        _.findIndex data, (item) -> item.id is id\n      count: -> data.length\n\n    @props =\n      dataSource: @dataSource\n      keyboardCursorId: 'keyboard-focus'\n      focused: @itemFocus\n      focusedId: 'focus'\n      onFocusItem: @onFocusItem\n      onSetCursorPosition: @onSetCursorPosition\n\n    @collection = 'threads'\n    @isRootSheet = true\n    @handler = new MultiselectSplitInteractionHandler(@props)\n\n    spyOn(WorkspaceStore, 'topSheet').andCallFake => {root: @isRootSheet}\n\n  it \"should always show focus\", ->\n    expect(@handler.shouldShowFocus()).toEqual(true)\n\n  it \"should show the keyboard cursor when multiple items are selected\", ->\n    @selection = []\n    expect(@handler.shouldShowKeyboardCursor()).toEqual(false)\n    @selection = [@item]\n    expect(@handler.shouldShowKeyboardCursor()).toEqual(false)\n    @selection = [@item, @itemFocus]\n    expect(@handler.shouldShowKeyboardCursor()).toEqual(true)\n\n  describe \"onClick\", ->\n    it \"should focus the list item and indicate it was focused via click\", ->\n      @handler.onClick(@item)\n      expect(@onFocusItem).toHaveBeenCalledWith(@item)\n\n  describe \"onMetaClick\", ->\n    describe \"when there is currently a focused item\", ->\n      it \"should turn the focused item into the first selected item\", ->\n        @handler.onMetaClick(@item)\n        expect(@dataSource.selection.add).toHaveBeenCalledWith(@itemFocus)\n\n      it \"should clear the focus\", ->\n        @handler.onMetaClick(@item)\n        expect(@onFocusItem).toHaveBeenCalledWith(null)\n\n    it \"should toggle selection\", ->\n      @handler.onMetaClick(@item)\n      expect(@dataSource.selection.toggle).toHaveBeenCalledWith(@item)\n\n    it \"should call _checkSelectionAndFocusConsistency\", ->\n      spyOn(@handler, '_checkSelectionAndFocusConsistency')\n      @handler.onMetaClick(@item)\n      expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()\n\n  describe \"onShiftClick\", ->\n    describe \"when there is currently a focused item\", ->\n\n      it \"should turn the focused item into the first selected item\", ->\n        @handler.onMetaClick(@item)\n        expect(@dataSource.selection.add).toHaveBeenCalledWith(@itemFocus)\n\n      it \"should clear the focus\", ->\n        @handler.onMetaClick(@item)\n        expect(@onFocusItem).toHaveBeenCalledWith(null)\n\n    it \"should expand selection\", ->\n      @handler.onShiftClick(@item)\n      expect(@dataSource.selection.expandTo).toHaveBeenCalledWith(@item)\n\n    it \"should call _checkSelectionAndFocusConsistency\", ->\n      spyOn(@handler, '_checkSelectionAndFocusConsistency')\n      @handler.onMetaClick(@item)\n      expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()\n\n  describe \"onEnter\", ->\n\n  describe \"onSelect (x key on keyboard)\", ->\n    it \"should call _checkSelectionAndFocusConsistency\", ->\n      spyOn(@handler, '_checkSelectionAndFocusConsistency')\n      @handler.onMetaClick(@item)\n      expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()\n\n  describe \"onShift\", ->\n    it \"should call _checkSelectionAndFocusConsistency\", ->\n      spyOn(@handler, '_checkSelectionAndFocusConsistency')\n      @handler.onMetaClick(@item)\n      expect(@handler._checkSelectionAndFocusConsistency).toHaveBeenCalled()\n\n    describe \"when the select option is passed\", ->\n      it \"should turn the existing focused item into a selected item\", ->\n        @handler.onShift(1, {select: true})\n        expect(@dataSource.selection.add).toHaveBeenCalledWith(@itemFocus)\n\n      it \"should walk the selection to the shift target\", ->\n        @handler.onShift(1, {select: true})\n        expect(@dataSource.selection.walk).toHaveBeenCalledWith({current: @itemFocus, next: @itemAfterFocus})\n\n    describe \"when one or more items is selected\", ->\n      it \"should move the keyboard cursor\", ->\n        @selection = [@itemFocus, @itemAfterFocus, @itemKeyboardFocus]\n        @handler.onShift(1, {})\n        expect(@onSetCursorPosition).toHaveBeenCalledWith(@itemAfterKeyboardFocus)\n\n    describe \"when no items are selected\", ->\n      it \"should move the focus\", ->\n        @handler.onShift(1, {})\n        expect(@onFocusItem).toHaveBeenCalledWith(@itemAfterFocus)\n\n\n  describe \"_checkSelectionAndFocusConsistency\", ->\n    describe \"when only one item is selected\", ->\n      beforeEach ->\n        @selection = [@item]\n        @props.focused = null\n        @handler = new MultiselectSplitInteractionHandler(@props)\n\n      it \"should clear the selection and make the item focused\", ->\n        @handler._checkSelectionAndFocusConsistency()\n        expect(@dataSource.selection.clear).toHaveBeenCalled()\n        expect(@onFocusItem).toHaveBeenCalledWith(@item)\n"
  },
  {
    "path": "packages/client-app/spec/components/nylas-calendar/calendar-toggles-spec.jsx",
    "content": "import React from 'react'\nimport ReactTestUtils from 'react-addons-test-utils'\nimport {NylasCalendar} from 'nylas-component-kit'\n\nimport { now } from './test-utils'\nimport TestDataSource from './test-data-source'\nimport CalendarToggles from '../../../src/components/nylas-calendar/calendar-toggles'\n\ndescribe(\"Nylas Calendar Toggles\", function calendarPickerSpec() {\n  beforeEach(() => {\n    this.dataSource = new TestDataSource();\n    this.calendar = ReactTestUtils.renderIntoDocument(\n      <NylasCalendar\n        currentMoment={now()}\n        onCalendarMouseDown={this.onCalendarMouseDown}\n        dataSource={this.dataSource}\n      />\n    );\n    this.toggles = ReactTestUtils.findRenderedComponentWithType(this.calendar, CalendarToggles);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/nylas-calendar/fixtures/events.es6",
    "content": "import moment from 'moment-timezone'\nimport {Event} from 'nylas-exports'\nimport {TZ, TEST_CALENDAR} from '../test-utils'\n\n// All day\n// All day overlap\n//\n// Simple single event\n// Event that spans a day\n// Overlapping events\n\nlet gen = 0\n\nconst genEvent = ({start, end, object = \"timespan\"}) => {\n  gen += 1;\n\n  let when = {}\n  if (object === \"timespan\") {\n    when = {\n      object: \"timespan\",\n      end_time: moment.tz(end, TZ).unix(),\n      start_time: moment.tz(start, TZ).unix(),\n    }\n  }\n  if (object === \"datespan\") {\n    when = {\n      object: \"datespan\",\n      end_date: end,\n      start_date: start,\n    }\n  }\n\n  return new Event().fromJSON({\n    id: `server-${gen}`,\n    calendar_id: TEST_CALENDAR,\n    account_id: window.TEST_ACCOUNT_ID,\n    description: `description ${gen}`,\n    location: `location ${gen}`,\n    owner: `${window._TEST_ACCOUNT_NAME} <${window.TEST_ACCOUNT_EMAIL}>`,\n    participants: [{\n      email: window.TEST_ACCOUNT_EMAIL,\n      name: window.TEST_ACCOUNT_NAME,\n      status: \"yes\",\n    }],\n    read_only: \"false\",\n    title: `Title ${gen}`,\n    busy: true,\n    when,\n    status: \"confirmed\",\n  })\n}\n\n// NOTE:\n// DST Started 2016-03-13 01:59 and immediately jumps to 03:00.\n// DST Ended 2016-11-06 01:59 and immediately jumps to 01:00 again!\n//\n// See: http://momentjs.com/timezone/docs/#/using-timezones/parsing-ambiguous-inputs/\n\n// All times are in \"America/Los_Angeles\"\nexport const numAllDayEvents = 6\nexport const numStandardEvents = 9\nexport const numByDay = {\n  1457769600: 2,\n  1457856000: 7,\n}\nexport const eventOverlapForSunday = {\n  \"server-2\": {\n    concurrentEvents: 2,\n    order: 1,\n  },\n  \"server-3\": {\n    concurrentEvents: 2,\n    order: 2,\n  },\n  \"server-6\": {\n    concurrentEvents: 1,\n    order: 1,\n  },\n  \"server-7\": {\n    concurrentEvents: 1,\n    order: 1,\n  },\n  \"server-8\": {\n    concurrentEvents: 2,\n    order: 1,\n  },\n  \"server-9\": {\n    concurrentEvents: 2,\n    order: 2,\n  },\n  \"server-10\": {\n    concurrentEvents: 2,\n    order: 1,\n  },\n}\nexport const events = [\n  // Single event\n  genEvent({start: \"2016-03-12 12:00\", end: \"2016-03-12 13:00\"}),\n\n  // DST start spanning event. 6 hours when it should be 7!\n  genEvent({start: \"2016-03-12 23:00\", end: \"2016-03-13 06:00\"}),\n\n  // DST start invalid event. Does not exist!\n  genEvent({start: \"2016-03-13 02:15\", end: \"2016-03-13 02:45\"}),\n\n  // DST end spanning event. 8 hours when it shoudl be 7!\n  genEvent({start: \"2016-11-05 23:00\", end: \"2016-11-06 06:00\"}),\n\n  // DST end ambiguous event. This timespan happens twice!\n  genEvent({start: \"2016-11-06 01:15\", end: \"2016-11-06 01:45\"}),\n\n  // Adjacent events\n  genEvent({start: \"2016-03-13 12:00\", end: \"2016-03-13 13:00\"}),\n  genEvent({start: \"2016-03-13 13:00\", end: \"2016-03-13 14:00\"}),\n\n  // Overlapping events\n  genEvent({start: \"2016-03-13 14:30\", end: \"2016-03-13 15:30\"}),\n  genEvent({start: \"2016-03-13 15:00\", end: \"2016-03-13 16:00\"}),\n  genEvent({start: \"2016-03-13 15:30\", end: \"2016-03-13 16:30\"}),\n\n  // All day timespan event\n  genEvent({start: \"2016-03-15 00:00\", end: \"2016-03-16 00:00\"}),\n\n  // All day datespan\n  genEvent({start: \"2016-03-17\", end: \"2016-03-18\", object: \"datespan\"}),\n\n  // Overlapping all day\n  genEvent({start: \"2016-03-19\", end: \"2016-03-20\", object: \"datespan\"}),\n  genEvent({start: \"2016-03-19 00:00\", end: \"2016-03-20 00:00\"}),\n  genEvent({start: \"2016-03-19 12:00\", end: \"2016-03-20 12:00\"}),\n  genEvent({start: \"2016-03-20 00:00\", end: \"2016-03-21 00:00\"}),\n]\n"
  },
  {
    "path": "packages/client-app/spec/components/nylas-calendar/test-data-source.es6",
    "content": "// import Rx from 'rx-lite-testing'\nimport {CalendarDataSource} from 'nylas-exports'\nimport {events} from './fixtures/events'\n\nexport default class TestDataSource extends CalendarDataSource {\n  buildObservable({startTime, endTime}) {\n    this.endTime = endTime;\n    this.startTime = startTime;\n    return this\n  }\n\n  subscribe(onNext) {\n    onNext({events})\n    this.unsubscribe = jasmine.createSpy(\"unusbscribe\");\n    return {dispose: this.unsubscribe}\n  }\n}\n"
  },
  {
    "path": "packages/client-app/spec/components/nylas-calendar/test-utils.es6",
    "content": "import moment from 'moment-timezone'\n\nexport const TZ = window.TEST_TIME_ZONE;\nexport const TEST_CALENDAR = \"TEST_CALENDAR\";\n\nexport const now = () => window.testNowMoment();\n\nexport const NOW_WEEK_START = moment.tz(\"2016-03-13 00:00\", TZ);\nexport const NOW_BUFFER_START = moment.tz(\"2016-03-06 00:00\", TZ);\nexport const NOW_BUFFER_END = moment.tz(\"2016-03-26 23:59:59\", TZ);\n\n// Makes test failure output easier to read.\nexport const u2h = (unixTime) => moment.unix(unixTime).format(\"LLL z\")\nexport const m2h = (m) => m.format(\"LLL z\")\n"
  },
  {
    "path": "packages/client-app/spec/components/nylas-calendar/week-view-extended-spec.jsx",
    "content": "// import {events} from './fixtures/events'\n// import {NylasCalendar} from 'nylas-component-kit'\n//\n// describe('Extended Nylas Calendar Week View', function extendedNylasCalendarWeekView() {\n// });\n"
  },
  {
    "path": "packages/client-app/spec/components/nylas-calendar/week-view-spec.jsx",
    "content": "import _ from 'underscore'\nimport moment from 'moment'\nimport React from 'react'\nimport ReactTestUtils from 'react-addons-test-utils'\nimport {NylasCalendar} from 'nylas-component-kit'\n\nimport {\n  now,\n  NOW_WEEK_START,\n  NOW_BUFFER_START,\n  NOW_BUFFER_END,\n} from './test-utils'\n\nimport TestDataSource from './test-data-source'\nimport {\n  numByDay,\n  numAllDayEvents,\n  numStandardEvents,\n  eventOverlapForSunday,\n} from './fixtures/events'\n\nimport WeekView from '../../../src/components/nylas-calendar/week-view'\n\ndescribe(\"Nylas Calendar Week View\", function weekViewSpec() {\n  beforeEach(() => {\n    spyOn(WeekView.prototype, \"_now\").andReturn(now());\n\n    this.onCalendarMouseDown = jasmine.createSpy(\"onCalendarMouseDown\")\n    this.dataSource = new TestDataSource();\n    this.calendar = ReactTestUtils.renderIntoDocument(\n      <NylasCalendar\n        currentMoment={now()}\n        onCalendarMouseDown={this.onCalendarMouseDown}\n        dataSource={this.dataSource}\n      />\n    );\n    this.weekView = ReactTestUtils.findRenderedComponentWithType(this.calendar, WeekView);\n  });\n\n  it(\"renders a calendar\", () => {\n    const cal = ReactTestUtils.findRenderedComponentWithType(this.calendar, NylasCalendar)\n    expect(cal instanceof NylasCalendar).toBe(true)\n  });\n\n  it(\"sets the correct moment\", () => {\n    expect(this.calendar.state.currentMoment.valueOf()).toBe(now().valueOf())\n  });\n\n  it(\"defaulted to WeekView\", () => {\n    expect(this.calendar.state.currentView).toBe(\"week\");\n    expect(this.weekView instanceof WeekView).toBe(true);\n  });\n\n  it(\"initializes the component\", () => {\n    expect(this.weekView.todayYear).toBe(now().year());\n    expect(this.weekView.todayDayOfYear).toBe(now().dayOfYear());\n  });\n\n  it(\"initializes the data source & state with the correct times\", () => {\n    expect(this.dataSource.startTime).toBe(NOW_BUFFER_START.unix());\n    expect(this.dataSource.endTime).toBe(NOW_BUFFER_END.unix());\n    expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());\n    expect(this.weekView.state.endMoment.unix()).toBe(NOW_BUFFER_END.unix());\n    expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())\n  });\n\n  it(\"has the correct days in buffer\", () => {\n    const days = this.weekView._daysInView();\n    expect(days.length).toBe(21);\n    expect(days[0].dayOfYear()).toBe(66)\n    expect(days[days.length - 1].dayOfYear()).toBe(86)\n  });\n\n  it(\"shows the correct current week\", () => {\n    expect(this.weekView._currentWeekText()).toBe(\"March 13 - March 19 2016\")\n  });\n\n  it(\"goes to next week on click\", () => {\n    const nextBtn = this.weekView.refs.headerControls.refs.onNextAction\n    expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());\n    expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())\n\n    ReactTestUtils.Simulate.click(nextBtn);\n\n    expect((this.weekView.state.startMoment).unix())\n      .toBe(moment(NOW_BUFFER_START).add(1, 'week').unix());\n\n    expect(this.weekView._scrollTime)\n      .toBe(moment(NOW_WEEK_START).add(1, 'week').unix());\n  });\n\n  it(\"goes to the previous week on click\", () => {\n    const prevBtn = this.weekView.refs.headerControls.refs.onPreviousAction\n    expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());\n    expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())\n\n    ReactTestUtils.Simulate.click(prevBtn);\n\n    expect((this.weekView.state.startMoment).unix())\n      .toBe(moment(NOW_BUFFER_START).subtract(1, 'week').unix());\n\n    expect(this.weekView._scrollTime)\n      .toBe(moment(NOW_WEEK_START).subtract(1, 'week').unix());\n  });\n\n  it(\"goes to 'today' when the 'today' btn is pressed\", () => {\n    const todayBtn = this.weekView.refs.todayBtn;\n    const nextBtn = this.weekView.refs.headerControls.refs.onNextAction\n    ReactTestUtils.Simulate.click(nextBtn);\n    ReactTestUtils.Simulate.click(todayBtn)\n\n    expect(this.weekView.state.startMoment.unix()).toBe(NOW_BUFFER_START.unix());\n    expect(this.weekView._scrollTime).toBe(NOW_WEEK_START.unix())\n  });\n\n  it(\"sets the interval height properly\", () => {\n    expect(this.weekView.state.intervalHeight).toBe(21)\n  });\n\n  it(\"properly segments the events by day\", () => {\n    const days = this.weekView._daysInView();\n    const eventsByDay = this.weekView._eventsByDay(days);\n\n    // See fixtures/events\n    expect(eventsByDay.allDay.length).toBe(numAllDayEvents);\n    for (const day of Object.keys(numByDay)) {\n      expect(eventsByDay[day].length).toBe(numByDay[day])\n    }\n  });\n\n  it(\"correctly stacks all day events\", () => {\n    const height = this.weekView.refs.weekViewAllDayEvents.props.height;\n    // This means it's 3-high\n    expect(height).toBe(64);\n  });\n\n  it(\"correctly sets up the event overlap for a day\", () => {\n    const days = this.weekView._daysInView();\n    const eventsByDay = this.weekView._eventsByDay(days);\n    const eventOverlap = this.weekView._eventOverlap(eventsByDay['1457856000']);\n    expect(eventOverlap).toEqual(eventOverlapForSunday)\n  });\n\n  it(\"renders the events onto the grid\", () => {\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.weekView);\n\n    const events = $(\"calendar-event\");\n    const standardEvents = $(\"calendar-event vertical\");\n    const allDayEvents = $(\"calendar-event horizontal\");\n\n    expect(events.length).toBe(numStandardEvents + numAllDayEvents)\n    expect(standardEvents.length).toBe(numStandardEvents)\n    expect(allDayEvents.length).toBe(numAllDayEvents)\n  });\n\n  it(\"finds the correct data from mouse events\", () => {\n    const $ = _.partial(ReactTestUtils.scryRenderedDOMComponentsWithClass, this.weekView);\n\n    const eventContainer = this.weekView.refs.calendarEventContainer;\n\n    // Unfortunately, _dataFromMouseEvent requires the component to both\n    // be mounted and have size. To truly test this we'd have to load the\n    // integratino test environment. For now, we test that the event makes\n    // its way back to passed in callback handlers\n    const mouseData = {\n      x: 100,\n      y: 100,\n      width: 100,\n      height: 100,\n      time: now(),\n    }\n    spyOn(eventContainer, \"_dataFromMouseEvent\").andReturn(mouseData)\n\n    const eventEl = $(\"calendar-event vertical\")[0];\n    ReactTestUtils.Simulate.mouseDown(eventEl, {x: 100, y: 100});\n\n    const mouseEvent = eventContainer._dataFromMouseEvent.calls[0].args[0];\n    expect(mouseEvent.x).toBe(100)\n    expect(mouseEvent.y).toBe(100)\n\n    const mouseDataOut = this.onCalendarMouseDown.calls[0].args[0]\n    expect(mouseDataOut.x).toEqual(mouseData.x)\n    expect(mouseDataOut.y).toEqual(mouseData.y)\n    expect(mouseDataOut.width).toEqual(mouseData.width)\n    expect(mouseDataOut.height).toEqual(mouseData.height)\n    expect(mouseDataOut.time.unix()).toEqual(mouseData.time.unix())\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/participants-text-field-spec.jsx",
    "content": "import React from 'react';\nimport { mount } from 'enzyme';\nimport { ContactStore, Contact } from 'nylas-exports';\n\nimport { ParticipantsTextField } from 'nylas-component-kit';\n\nconst participant1 = new Contact({\n  id: 'local-1',\n  email: 'ben@nylas.com',\n});\nconst participant2 = new Contact({\n  id: 'local-2',\n  email: 'ben@example.com',\n  name: 'Ben Gotow',\n});\nconst participant3 = new Contact({\n  id: 'local-3',\n  email: 'evan@nylas.com',\n  name: 'Evan Morikawa',\n});\n\nxdescribe('ParticipantsTextField', function ParticipantsTextFieldSpecs() {\n  beforeEach(() => {\n    spyOn(NylasEnv, \"isMainWindow\").andReturn(true)\n    this.propChange = jasmine.createSpy('change')\n\n    this.fieldName = 'to';\n    this.participants = {\n      to: [participant1, participant2],\n      cc: [participant3],\n      bcc: [],\n    };\n\n    this.renderedField = mount(\n      <ParticipantsTextField\n        field={this.fieldName}\n        visible\n        participants={this.participants}\n        draft={{clientId: 'draft-1'}}\n        session={{}}\n        change={this.propChange}\n      />\n    )\n    this.renderedInput = this.renderedField.find('input')\n\n    this.expectInputToYield = (input, expected) => {\n      const reviver = function reviver(k, v) {\n        if (k === \"id\" || k === \"client_id\" || k === \"server_id\" || k === \"object\") { return undefined; }\n        return v;\n      };\n      runs(() => {\n        this.renderedInput.simulate('change', {target: {value: input}});\n        advanceClock(100);\n        return this.renderedInput.simulate('keyDown', {key: 'Enter', keyCode: 9});\n      });\n      waitsFor(() => {\n        return this.propChange.calls.length > 0;\n      });\n      runs(() => {\n        let found = this.propChange.mostRecentCall.args[0];\n        found = JSON.parse(JSON.stringify(found), reviver);\n        expect(found).toEqual(JSON.parse(JSON.stringify(expected), reviver));\n\n        // This advance clock needs to be here because our waitsFor latch\n        // catches the first time that propChange gets called. More stuff\n        // may happen after this and we need to advance the clock to\n        // \"clear\" all of that. If we don't do this it throws errors about\n        // `setState` being called on unmounted components :(\n        return advanceClock(100);\n      });\n    };\n  });\n\n  it('renders into the document', () => {\n    expect(this.renderedField.find(ParticipantsTextField).length).toBe(1)\n  });\n\n  describe(\"inserting participant text\", () => {\n    it(\"should fire onChange with an updated participants hash\", () => {\n      this.expectInputToYield('abc@abc.com', {\n        to: [participant1, participant2, new Contact({name: 'abc@abc.com', email: 'abc@abc.com'})],\n        cc: [participant3],\n        bcc: [],\n      });\n    });\n\n    it(\"should remove added participants from other fields\", () => {\n      this.expectInputToYield(participant3.email, {\n        to: [participant1, participant2, new Contact({name: participant3.email, email: participant3.email})],\n        cc: [],\n        bcc: [],\n      });\n    });\n\n    it(\"should use the name of an existing contact in the ContactStore if possible\", () => {\n      spyOn(ContactStore, 'searchContacts').andCallFake((val) => {\n        if (val === participant3.name) {\n          return Promise.resolve([participant3]);\n        }\n        return Promise.resolve([]);\n      });\n\n      this.expectInputToYield(participant3.name, {\n        to: [participant1, participant2, participant3],\n        cc: [],\n        bcc: [],\n      });\n    });\n\n    it(\"should use the plain email if that's what's entered\", () => {\n      spyOn(ContactStore, 'searchContacts').andCallFake((val) => {\n        if (val === participant3.name) {\n          return Promise.resolve([participant3]);\n        }\n        return Promise.resolve([]);\n      });\n\n      this.expectInputToYield(participant3.email, {\n        to: [participant1, participant2, new Contact({email: \"evan@nylas.com\"})],\n        cc: [],\n        bcc: [],\n      });\n    });\n\n    it(\"should not have the same contact auto-picked multiple times\", () => {\n      spyOn(ContactStore, 'searchContacts').andCallFake((val) => {\n        if (val === participant2.name) {\n          return Promise.resolve([participant2]);\n        }\n        return Promise.resolve([])\n      });\n\n      this.expectInputToYield(participant2.name, {\n        to: [participant1, participant2, new Contact({email: participant2.name, name: participant2.name})],\n        cc: [participant3],\n        bcc: [],\n      });\n    });\n\n    describe(\"when text contains Name (Email) formatted data\", () => {\n      it(\"should correctly parse it into named Contact objects\", () => {\n        const newContact1 = new Contact({id: \"b1\", name: 'Ben Imposter', email: 'imposter@nylas.com'});\n        const newContact2 = new Contact({name: 'Nylas Team', email: 'feedback@nylas.com'});\n\n        const inputs = [\n          \"Ben Imposter <imposter@nylas.com>, Nylas Team <feedback@nylas.com>\",\n          \"\\n\\nbla\\nBen Imposter (imposter@nylas.com), Nylas Team (feedback@nylas.com)\",\n          \"Hello world! I like cheese. \\rBen Imposter (imposter@nylas.com)\\nNylas Team (feedback@nylas.com)\",\n          \"Ben Imposter<imposter@nylas.com>Nylas Team (feedback@nylas.com)\",\n        ];\n\n        for (const input of inputs) {\n          this.expectInputToYield(input, {\n            to: [participant1, participant2, newContact1, newContact2],\n            cc: [participant3],\n            bcc: [],\n          });\n        }\n      });\n    });\n\n    describe(\"when text contains emails mixed with garbage text\", () => {\n      it(\"should still parse out emails into Contact objects\", () => {\n        const newContact1 = new Contact({id: 'gm', name: 'garbage-man@nylas.com', email: 'garbage-man@nylas.com'});\n        const newContact2 = new Contact({id: 'rm', name: 'recycling-guy@nylas.com', email: 'recycling-guy@nylas.com'});\n\n        const inputs = [\n          \"Hello world I real. \\n asd. garbage-man@nylas.com—he's cool Also 'recycling-guy@nylas.com'!\",\n          \"garbage-man@nylas.com1WHOA I REALLY HATE DATA,recycling-guy@nylas.com\",\n          \"nils.com garbage-man@nylas.com @nylas.com nope@.com nope! recycling-guy@nylas.com HOLLA AT recycling-guy@nylas.\",\n        ];\n\n        for (const input of inputs) {\n          this.expectInputToYield(input, {\n            to: [participant1, participant2, newContact1, newContact2],\n            cc: [participant3],\n            bcc: [],\n          });\n        }\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/selectable-table-spec.jsx",
    "content": "import React from 'react'\nimport {mount, shallow} from 'enzyme'\nimport {Table, SelectableTableCell, SelectableTableRow, SelectableTable} from 'nylas-component-kit'\nimport {selection, cellProps, rowProps, tableProps, testDataSource} from '../fixtures/table-data'\n\n\ndescribe('SelectableTable Components', function describeBlock() {\n  describe('SelectableTableCell', () => {\n    function renderCell(props) {\n      return shallow(\n        <SelectableTableCell\n          {...cellProps}\n          {...props}\n        />\n      )\n    }\n\n    describe('shouldComponentUpdate', () => {\n      it('should update if selection status for cell has changed', () => {\n        const nextSelection = {colIdx: 0, rowIdx: 2}\n        const cell = renderCell()\n        const nextProps = {...cellProps, selection: nextSelection}\n        const shouldUpdate = cell.instance().shouldComponentUpdate(nextProps)\n        expect(shouldUpdate).toBe(true)\n      });\n\n      it('should update if data for cell has changed', () => {\n        const nextRows = testDataSource.rows().slice()\n        nextRows[0] = ['something else', 2]\n        const nextDataSource = testDataSource.setRows(nextRows)\n        const cell = renderCell()\n        const nextProps = {...cellProps, tableDataSource: nextDataSource}\n        const shouldUpdate = cell.instance().shouldComponentUpdate(nextProps)\n        expect(shouldUpdate).toBe(true)\n      });\n\n      it('should not update otherwise', () => {\n        const nextRows = testDataSource.rows().slice()\n        nextRows[0] = nextRows[0].slice()\n        const nextDataSource = testDataSource.setRows(nextRows)\n        const nextSelection = {...selection}\n        const cell = renderCell()\n        const nextProps = {...cellProps, selection: nextSelection, tableDataSource: nextDataSource}\n        const shouldUpdate = cell.instance().shouldComponentUpdate(nextProps)\n        expect(shouldUpdate).toBe(false)\n      });\n    });\n\n    describe('isSelected', () => {\n      it('returns true if selection matches props', () => {\n        const cell = renderCell()\n        expect(cell.instance().isSelected(cellProps)).toBe(true)\n      });\n\n      it('returns false otherwise', () => {\n        const cell = renderCell()\n        expect(cell.instance().isSelected({\n          ...cellProps,\n          selection: {rowIdx: 1, colIdx: 2},\n        })).toBe(false)\n      });\n    });\n\n    describe('isSelectedUsingKey', () => {\n      it('returns true if cell was selected using the provided key', () => {\n        const cell = renderCell({selection: {...selection, key: 'Enter'}})\n        expect(cell.instance().isSelectedUsingKey('Enter')).toBe(true)\n      });\n\n      it('returns false if cell was not selected using the provided key', () => {\n        const cell = renderCell()\n        expect(cell.instance().isSelectedUsingKey('Enter')).toBe(false)\n      });\n    });\n\n    describe('isInLastRow', () => {\n      it('returns true if cell is in last row', () => {\n        const cell = renderCell({rowIdx: 2})\n        expect(cell.instance().isInLastRow()).toBe(true)\n      });\n\n      it('returns true if cell is not in last row', () => {\n        const cell = renderCell()\n        expect(cell.instance().isInLastRow()).toBe(false)\n      });\n    });\n\n    it('renders with the appropriate className when selected', () => {\n      const cell = renderCell()\n      expect(cell.hasClass('selected')).toBe(true)\n    });\n\n    it('renders with the appropriate className when not selected', () => {\n      const cell = renderCell({rowIdx: 2, colIdx: 1})\n      expect(cell.hasClass('selected')).toBe(false)\n    });\n\n    it('renders any extra classNames', () => {\n      const cell = renderCell({className: 'my-cell'})\n      expect(cell.hasClass('my-cell')).toBe(true)\n    });\n  });\n\n  describe('SelectableTableRow', () => {\n    function renderRow(props) {\n      return shallow(\n        <SelectableTableRow\n          {...rowProps}\n          {...props}\n        />\n      )\n    }\n\n    describe('shouldComponentUpdate', () => {\n      it('should update if the row data has changed', () => {\n        const nextRows = testDataSource.rows().slice()\n        nextRows[0] = ['new', 'row']\n        const nextDataSource = testDataSource.setRows(nextRows)\n        const row = renderRow()\n        const shouldUpdate = row.instance().shouldComponentUpdate({...rowProps, tableDataSource: nextDataSource})\n        expect(shouldUpdate).toBe(true)\n      });\n\n      it('should update if selection status for row has changed', () => {\n        const nextSelection = {rowIdx: 2, colIdx: 0}\n        const row = renderRow()\n        const shouldUpdate = row.instance().shouldComponentUpdate({...rowProps, selection: nextSelection})\n        expect(shouldUpdate).toBe(true)\n      });\n\n      it('should update even if row is still selected but selected cell has changed', () => {\n        const nextSelection = {rowIdx: 1, colIdx: 1}\n        const row = renderRow()\n        const shouldUpdate = row.instance().shouldComponentUpdate({...rowProps, selection: nextSelection})\n        expect(shouldUpdate).toBe(true)\n      });\n\n      it('should not update otherwise', () => {\n        const nextRows = testDataSource.rows().slice()\n        const nextDataSource = testDataSource.setRows(nextRows)\n        const nextSelection = {...selection}\n        const row = renderRow()\n        const nextProps = {...rowProps, selection: nextSelection, tableDataSource: nextDataSource}\n        const shouldUpdate = row.instance().shouldComponentUpdate(nextProps)\n        expect(shouldUpdate).toBe(false)\n      });\n    });\n\n    describe('isSelected', () => {\n      it('returns true when selection matches props', () => {\n        const row = renderRow()\n        expect(row.instance().isSelected({\n          selection: {rowIdx: 1},\n          rowIdx: 1,\n        })).toBe(true)\n      });\n\n      it('returns false otherwise', () => {\n        const row = renderRow()\n        expect(row.instance().isSelected({\n          selection: {rowIdx: 2},\n          rowIdx: 1,\n        })).toBe(false)\n      });\n    });\n\n    it('renders with the appropriate className when selected', () => {\n      const row = renderRow()\n      expect(row.hasClass('selected')).toBe(true)\n    });\n\n    it('renders with the appropriate className when not selected', () => {\n      const row = renderRow({selection: {rowIdx: 2, colIdx: 0}})\n      expect(row.hasClass('selected')).toBe(false)\n    });\n\n    it('renders any extra classNames', () => {\n      const row = renderRow({className: 'my-row'})\n      expect(row.hasClass('my-row')).toBe(true)\n    });\n  });\n\n  describe('SelectableTable', () => {\n    function renderTable(props) {\n      return mount(\n        <SelectableTable\n          {...tableProps}\n          {...props}\n        />\n      )\n    }\n\n    describe('onTab', () => {\n      it('shifts selection to the next row if last column is selected', () => {\n        const onShiftSelection = jasmine.createSpy('onShiftSelection')\n        const table = renderTable({selection: {colIdx: 2, rowIdx: 1}, onShiftSelection})\n        table.instance().onTab({key: 'Tab'})\n        expect(onShiftSelection).toHaveBeenCalledWith({\n          row: 1, col: -2, key: 'Tab',\n        })\n      });\n\n      it('shifts selection to the next col otherwise', () => {\n        const onShiftSelection = jasmine.createSpy('onShiftSelection')\n        const table = renderTable({selection: {colIdx: 0, rowIdx: 1}, onShiftSelection})\n        table.instance().onTab({key: 'Tab'})\n        expect(onShiftSelection).toHaveBeenCalledWith({\n          col: 1, key: 'Tab',\n        })\n      });\n    });\n\n    describe('onShiftTab', () => {\n      it('shifts selection to the previous row if first column is selected', () => {\n        const onShiftSelection = jasmine.createSpy('onShiftSelection')\n        const table = renderTable({selection: {colIdx: 0, rowIdx: 2}, onShiftSelection})\n        table.instance().onShiftTab({key: 'Tab'})\n        expect(onShiftSelection).toHaveBeenCalledWith({\n          row: -1, col: 2, key: 'Tab',\n        })\n      });\n\n      it('shifts selection to the previous col otherwise', () => {\n        const onShiftSelection = jasmine.createSpy('onShiftSelection')\n        const table = renderTable({selection: {colIdx: 1, rowIdx: 1}, onShiftSelection})\n        table.instance().onShiftTab({key: 'Tab'})\n        expect(onShiftSelection).toHaveBeenCalledWith({\n          col: -1, key: 'Tab',\n        })\n      });\n    });\n\n    it('renders with the correct props', () => {\n      const RowRenderer = () => <tr />\n      const CellRenderer = () => <td />\n      const onSetSelection = () => {}\n      const onShiftSelection = () => {}\n      const extraProps = {p1: 'p1'}\n      const table = renderTable({\n        extraProps,\n        onSetSelection,\n        onShiftSelection,\n        RowRenderer,\n        CellRenderer,\n      }).find(Table)\n      expect(table.prop('extraProps')).toEqual({\n        p1: 'p1',\n        selection,\n        onSetSelection,\n        onShiftSelection,\n      })\n      expect(table.prop('tableDataSource')).toBe(testDataSource)\n      expect(table.prop('RowRenderer')).toBe(RowRenderer)\n      expect(table.prop('CellRenderer')).toBe(CellRenderer)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/table/table-data-source-spec.jsx",
    "content": "import {\n  testData,\n  testDataSource,\n  testDataSourceEmpty,\n  testDataSourceUneven,\n} from '../../fixtures/table-data'\n\n\ndescribe('TableDataSource', function describeBlock() {\n  describe('colAt', () => {\n    it('returns the correct value for column', () => {\n      expect(testDataSource.colAt(1)).toEqual('col2')\n    });\n\n    it('returns null if col does not exist', () => {\n      expect(testDataSource.colAt(3)).toBe(null)\n    });\n  });\n\n  describe('rowAt', () => {\n    it('returns correct row', () => {\n      expect(testDataSource.rowAt(1)).toEqual([4, 5, 6])\n    });\n\n    it('returns columns if rowIdx is null', () => {\n      expect(testDataSource.rowAt(null)).toEqual(['col1', 'col2', 'col3'])\n    });\n\n    it('returns null if row does not exist', () => {\n      expect(testDataSource.rowAt(3)).toBe(null)\n    });\n  });\n\n  describe('cellAt', () => {\n    it('returns correct cell', () => {\n      expect(testDataSource.cellAt({rowIdx: 1, colIdx: 1})).toEqual(5)\n    });\n\n    it('returns correct col if rowIdx is null', () => {\n      expect(testDataSource.cellAt({rowIdx: null, colIdx: 1})).toEqual('col2')\n    });\n\n    it('returns null if cell does not exist', () => {\n      expect(testDataSource.cellAt({rowIdx: 3, colIdx: 1})).toBe(null)\n      expect(testDataSource.cellAt({rowIdx: 1, colIdx: 3})).toBe(null)\n    });\n  });\n\n  describe('isEmpty', () => {\n    it('throws if no args passed', () => {\n      expect(() => testDataSource.isEmpty()).toThrow()\n    });\n\n    it('throws if row does not exist', () => {\n      expect(() => testDataSource.isEmpty({rowIdx: 100})).toThrow()\n    });\n\n    it('throws if col does not exist', () => {\n      expect(() => testDataSource.isEmpty({colIdx: 100})).toThrow()\n    });\n\n    it('returns correct value when checking cell', () => {\n      expect(testDataSourceEmpty.isEmpty({rowIdx: 2, colIdx: 1})).toBe(true)\n      expect(testDataSourceEmpty.isEmpty({rowIdx: 3, colIdx: 1})).toBe(true)\n      expect(testDataSourceEmpty.isEmpty({rowIdx: 0, colIdx: 0})).toBe(false)\n    });\n\n    it('returns correct value when checking col', () => {\n      expect(testDataSourceEmpty.isEmpty({colIdx: 2})).toBe(true)\n      expect(testDataSourceEmpty.isEmpty({colIdx: 0})).toBe(false)\n    });\n\n    it('returns correct value when checking row', () => {\n      expect(testDataSourceEmpty.isEmpty({rowIdx: 2})).toBe(true)\n      expect(testDataSourceEmpty.isEmpty({rowIdx: 3})).toBe(true)\n      expect(testDataSourceEmpty.isEmpty({rowIdx: 1})).toBe(false)\n    });\n  });\n\n  describe('rows', () => {\n    it('returns all rows', () => {\n      expect(testDataSource.rows()).toBe(testData.rows)\n    });\n  });\n\n  describe('columns', () => {\n    it('returns all columns', () => {\n      expect(testDataSource.columns()).toBe(testData.columns)\n    });\n  });\n\n  describe('addColumn', () => {\n    it('pushes a new column to the data source\\'s columns', () => {\n      const res = testDataSource.addColumn()\n      expect(res.columns()).toEqual(['col1', 'col2', 'col3', null])\n    });\n\n    it('pushes a new column to every row', () => {\n      const res = testDataSource.addColumn()\n      expect(res.rows()).toEqual([\n        [1, 2, 3, null],\n        [4, 5, 6, null],\n        [7, 8, 9, null],\n      ])\n    });\n  });\n\n  describe('removeLastColumn', () => {\n    it('removes last column from the data source\\'s columns', () => {\n      const res = testDataSource.removeLastColumn()\n      expect(res.columns()).toEqual(['col1', 'col2'])\n    });\n\n    it('removes last column from every row', () => {\n      const res = testDataSource.removeLastColumn()\n      expect(res.rows()).toEqual([\n        [1, 2],\n        [4, 5],\n        [7, 8],\n      ])\n    });\n\n    it('removes the last column only from every row with that column', () => {\n      const res = testDataSourceUneven.removeLastColumn()\n      expect(res.rows()).toEqual([\n        [1, 2],\n        [4, 5],\n        [7, 8],\n      ])\n    })\n  });\n\n  describe('addRow', () => {\n    it('pushes an empty row with correct number of columns', () => {\n      const res = testDataSource.addRow()\n      expect(res.rows()).toEqual([\n        [1, 2, 3],\n        [4, 5, 6],\n        [7, 8, 9],\n        [null, null, null],\n      ])\n    });\n  });\n\n  describe('removeRow', () => {\n    it('removes last row', () => {\n      const res = testDataSource.removeRow()\n      expect(res.rows()).toEqual([\n        [1, 2, 3],\n        [4, 5, 6],\n      ])\n    });\n  });\n\n  describe('updateCell', () => {\n    it('updates cell value correctly when updating a cell that is /not/ a header', () => {\n      const res = testDataSource.updateCell({\n        rowIdx: 0, colIdx: 0, isHeader: false, value: 'new-val',\n      })\n      expect(res.columns()).toBe(testDataSource.columns())\n      expect(res.rows()).toEqual([\n        ['new-val', 2, 3],\n        [4, 5, 6],\n        [7, 8, 9],\n      ])\n\n      // If a row doesn't change, it should be the same row\n      testDataSource.rows().slice(1).forEach((row, rowIdx) => expect(row).toBe(testDataSource.rowAt(rowIdx + 1)))\n    });\n\n    it('updates cell value correctly when updating a cell that /is/ a header', () => {\n      const res = testDataSource.updateCell({\n        rowIdx: null, colIdx: 0, isHeader: true, value: 'new-val',\n      })\n      expect(res.columns()).toEqual(['new-val', 'col2', 'col3'])\n      expect(res.rows()).toBe(testDataSource.rows())\n\n      // If a row doesn't change, it should be the same row\n      testDataSource.rows().forEach((row, rowIdx) => expect(row).toBe(testDataSource.rowAt(rowIdx)))\n    });\n  });\n\n  describe('clear', () => {\n    it('clears all data correcltly', () => {\n      const res = testDataSource.clear()\n      expect(res.toJSON()).toEqual({\n        columns: [],\n        rows: [[]],\n      })\n    });\n  });\n\n  describe('toJSON', () => {\n    it('returns correct json object from data source', () => {\n      const res = testDataSource.toJSON()\n      expect(res).toEqual(testData)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/table/table-spec.jsx",
    "content": "import React from 'react'\nimport {shallow} from 'enzyme'\nimport {Table, TableRow, TableCell, LazyRenderedList} from 'nylas-component-kit'\nimport {testDataSource} from '../../fixtures/table-data'\n\n\ndescribe('Table Components', function describeBlock() {\n  describe('TableCell', () => {\n    it('renders children correctly', () => {\n      const element = shallow(<TableCell>Cell</TableCell>)\n      expect(element.text()).toEqual('Cell')\n    });\n\n    it('renders a th when is header', () => {\n      const element = shallow(<TableCell isHeader />)\n      expect(element.type()).toEqual('th')\n    });\n\n    it('renders a td when is not header', () => {\n      const element = shallow(<TableCell isHeader={false} />)\n      expect(element.type()).toEqual('td')\n    });\n\n    it('renders extra classNames', () => {\n      const element = shallow(<TableCell className=\"my-cell\" />)\n      expect(element.hasClass('my-cell')).toBe(true)\n    });\n\n    it('passes additional props to cell', () => {\n      const handler = () => {}\n      const element = shallow(<TableCell className=\"my-cell\" onClick={handler} />)\n      expect(element.prop('onClick')).toBe(handler)\n    });\n  });\n\n  describe('TableRow', () => {\n    function renderRow(props = {}) {\n      return shallow(\n        <TableRow\n          rowIdx={0}\n          tableDataSource={testDataSource}\n          {...props}\n        />\n      )\n    }\n\n    it('renders extra classNames', () => {\n      const row = renderRow({className: 'my-row'})\n      expect(row.hasClass('my-row')).toBe(true)\n    });\n\n    it('renders correct className when row is header', () => {\n      const row = renderRow({isHeader: true})\n      expect(row.hasClass('table-row-header')).toBe(true)\n    });\n\n    it('renders cells correctly given the tableDataSource', () => {\n      const row = renderRow()\n      expect(row.children().length).toBe(3)\n      row.children().forEach((cell, idx) => {\n        expect(cell.type()).toBe(TableCell)\n        expect(cell.childAt(0).text()).toEqual(`${idx + 1}`)\n      })\n    });\n\n    it('renders cells correctly if row is header', () => {\n      const row = renderRow({isHeader: true, rowIdx: null})\n      expect(row.children().length).toBe(3)\n      row.children().forEach((cell, idx) => {\n        expect(cell.type()).toBe(TableCell)\n        expect(cell.childAt(0).text()).toEqual(`col${idx + 1}`)\n      })\n    });\n\n    it('renders an empty first cell if displayNumbers is specified and is header', () => {\n      const row = renderRow({displayNumbers: true, isHeader: true, rowIdx: null})\n      const cell = row.childAt(0)\n      expect(row.children().length).toBe(4)\n      expect(cell.type()).toBe(TableCell)\n      expect(cell.hasClass('numbered-cell')).toBe(true)\n      expect(cell.childAt(0).text()).toEqual('')\n    });\n\n    it('renders first cell with row number if displayNumbers specified', () => {\n      const row = renderRow({displayNumbers: true})\n      expect(row.children().length).toBe(4)\n\n      const cell = row.childAt(0)\n      expect(cell.type()).toBe(TableCell)\n      expect(cell.hasClass('numbered-cell')).toBe(true)\n      expect(cell.childAt(0).text()).toEqual('1')\n    });\n\n    it('renders cell correctly given the CellRenderer', () => {\n      const CellRenderer = (props) => <div {...props} />\n      const row = renderRow({CellRenderer})\n      expect(row.children().length).toBe(3)\n      row.children().forEach((cell) => {\n        expect(cell.type()).toBe(CellRenderer)\n      })\n    });\n\n    it('passes correct props to children cells', () => {\n      const extraProps = {prop1: 'prop1'}\n      const row = renderRow({extraProps})\n      expect(row.children().length).toBe(3)\n      row.children().forEach((cell, idx) => {\n        expect(cell.type()).toBe(TableCell)\n        expect(cell.prop('rowIdx')).toEqual(0)\n        expect(cell.prop('colIdx')).toEqual(idx)\n        expect(cell.prop('prop1')).toEqual('prop1')\n        expect(cell.prop('tableDataSource')).toBe(testDataSource)\n      })\n    });\n  });\n\n  describe('Table', () => {\n    function renderTable(props = {}) {\n      return shallow(<Table {...props} tableDataSource={testDataSource} />)\n    }\n\n    it('renders extra classNames', () => {\n      const table = renderTable({className: 'my-table'})\n      expect(table.hasClass('nylas-table')).toBe(true)\n      expect(table.hasClass('my-table')).toBe(true)\n    });\n\n    describe('renderHeader', () => {\n      it('renders nothing if displayHeader is not specified', () => {\n        const table = renderTable({displayHeader: false})\n        expect(table.find('thead').length).toBe(0)\n      });\n\n      it('renders header row with the given RowRenderer', () => {\n        const RowRenderer = (props) => <div {...props} />\n        const table = renderTable({displayHeader: true, RowRenderer})\n        const header = table.find('thead').childAt(0)\n        expect(header.type()).toBe(RowRenderer)\n      });\n\n      it('passes correct props to header row', () => {\n        const table = renderTable({displayHeader: true, displayNumbers: true, extraProps: {p1: 'p1'}})\n        const header = table.find('thead').childAt(0)\n        expect(header.type()).toBe(TableRow)\n        expect(header.prop('rowIdx')).toBe(null)\n        expect(header.prop('tableDataSource')).toBe(testDataSource)\n        expect(header.prop('displayNumbers')).toBe(true)\n        expect(header.prop('isHeader')).toBe(true)\n        expect(header.prop('p1')).toEqual('p1')\n        expect(header.prop('extraProps')).toEqual({isHeader: true, p1: 'p1'})\n      });\n    });\n\n    describe('renderBody', () => {\n      it('renders a lazy list with correct rows when header should not be displayed', () => {\n        const table = renderTable()\n        const body = table.find(LazyRenderedList)\n        expect(body.prop('items')).toEqual(testDataSource.rows())\n        expect(body.prop('BufferTag')).toEqual('tr')\n        expect(body.prop('RootRenderer')).toEqual('tbody')\n      });\n    });\n\n    describe('renderRow', () => {\n      it('renders row with the given RowRenderer', () => {\n        const RowRenderer = (props) => <div {...props} />\n        const table = renderTable({RowRenderer})\n        const Renderer = table.instance().renderRow\n        const row = shallow(<Renderer idx={5} />)\n        expect(row.type()).toBe(RowRenderer)\n      });\n\n      it('passes the correct props to the row when displayHeader is true', () => {\n        const CellRenderer = (props) => <div {...props} />\n        const extraProps = {p1: 'p1'}\n        const table = renderTable({displayHeader: true, displayNumbers: true, extraProps, CellRenderer})\n        const Renderer = table.instance().renderRow\n        const row = shallow(<Renderer idx={5} />)\n        expect(row.prop('p1')).toEqual('p1')\n        expect(row.prop('rowIdx')).toBe(5)\n        expect(row.prop('displayNumbers')).toBe(true)\n        expect(row.prop('tableDataSource')).toBe(testDataSource)\n        expect(row.prop('extraProps')).toBe(extraProps)\n        expect(row.prop('CellRenderer')).toBe(CellRenderer)\n      });\n\n      it('passes the correct props to the row when displayHeader is false', () => {\n        const table = renderTable({displayHeader: false})\n        const Renderer = table.instance().renderRow\n        const row = shallow(<Renderer idx={5} />)\n        expect(row.prop('rowIdx')).toBe(5)\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/components/tokenizing-text-field-spec.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\n{mount} = require 'enzyme'\n\n\n{NylasTestUtils,\n Account,\n AccountStore,\n Contact,\n} = require 'nylas-exports'\n{TokenizingTextField, Menu} = require 'nylas-component-kit'\n\nCustomToken = React.createClass\n  render: ->\n    <span>{@props.token.email}</span>\n\nCustomSuggestion = React.createClass\n  render: ->\n    <span>{@props.item.email}</span>\n\nparticipant1 = new Contact\n  id: '1'\n  email: 'ben@nylas.com'\n  isSearchIndexed: false\nparticipant2 = new Contact\n  id: '2'\n  email: 'burgers@nylas.com'\n  name: 'Nylas Burger Basket'\n  isSearchIndexed: false\nparticipant3 = new Contact\n  id: '3'\n  email: 'evan@nylas.com'\n  name: 'Evan'\n  isSearchIndexed: false\nparticipant4 = new Contact\n  id: '4'\n  email: 'tester@elsewhere.com',\n  name: 'Tester'\n  isSearchIndexed: false\nparticipant5 = new Contact\n  id: '5'\n  email: 'michael@elsewhere.com',\n  name: 'Michael'\n  isSearchIndexed: false\n\ndescribe 'TokenizingTextField', ->\n  beforeEach ->\n    @completions = []\n    @propAdd = jasmine.createSpy 'add'\n    @propEdit = jasmine.createSpy 'edit'\n    @propRemove = jasmine.createSpy 'remove'\n    @propEmptied = jasmine.createSpy 'emptied'\n    @propTokenKey = jasmine.createSpy(\"tokenKey\").andCallFake (p) -> p.email\n    @propTokenIsValid = jasmine.createSpy(\"tokenIsValid\").andReturn(true)\n    @propTokenRenderer = CustomToken\n    @propOnTokenAction = jasmine.createSpy 'tokenAction'\n    @propCompletionNode = (p) -> <CustomSuggestion item={p} />\n    @propCompletionsForInput = (input) => @completions\n\n    spyOn(@, 'propCompletionNode').andCallThrough()\n    spyOn(@, 'propCompletionsForInput').andCallThrough()\n\n    @tokens = [participant1, participant2, participant3]\n\n    @rebuildRenderedField = (tokens) =>\n      tokens ?= @tokens\n      @renderedField = mount(\n        <TokenizingTextField\n          tokens={@tokens}\n          tokenKey={@propTokenKey}\n          tokenRenderer={@propTokenRenderer}\n          tokenIsValid={@propTokenIsValid}\n          onRequestCompletions={@propCompletionsForInput}\n          completionNode={@propCompletionNode}\n          onAdd={@propAdd}\n          onEdit={@propEdit}\n          onRemove={@propRemove}\n          onEmptied={@propEmptied}\n          onTokenAction={@propOnTokenAction}\n          tabIndex={@tabIndex}\n          />\n      )\n      @renderedInput = @renderedField.find('input')\n      return @renderedField\n\n    @rebuildRenderedField()\n\n  it 'renders into the document', ->\n    expect(@renderedField.find(TokenizingTextField).length).toBe(1)\n\n  it 'should render an input field', ->\n    expect(@renderedInput).toBeDefined()\n\n  it 'shows the tokens provided by the tokenRenderer', ->\n    expect(@renderedField.find(CustomToken).length).toBe(@tokens.length)\n\n  it 'shows the tokens in the correct order', ->\n    @renderedTokens = @renderedField.find(CustomToken)\n    for i in [0..@tokens.length-1]\n      expect(@renderedTokens.at(i).props().token).toBe(@tokens[i])\n\n  describe \"prop: tokenIsValid\", ->\n    it \"should be evaluated for each token when it's provided\", ->\n      @propTokenIsValid = jasmine.createSpy(\"tokenIsValid\").andCallFake (p) =>\n        if p is participant2 then true else false\n\n      @rebuildRenderedField()\n      @tokens = @renderedField.find(TokenizingTextField.Token)\n      expect(@tokens.at(0).props().valid).toBe(false)\n      expect(@tokens.at(1).props().valid).toBe(true)\n      expect(@tokens.at(2).props().valid).toBe(false)\n\n    it \"should default to true when not provided\", ->\n      @propTokenIsValid = null\n      @rebuildRenderedField()\n      @tokens = @renderedField.find(TokenizingTextField.Token)\n      expect(@tokens.at(0).props().valid).toBe(true)\n      expect(@tokens.at(1).props().valid).toBe(true)\n      expect(@tokens.at(2).props().valid).toBe(true)\n\n  describe \"when the user drags and drops a token between two fields\", ->\n    it \"should work properly\", ->\n      participant2.clientId = '123'\n\n      tokensA = [participant1, participant2, participant3]\n      fieldA = @rebuildRenderedField(tokensA)\n\n      tokensB = []\n      fieldB = @rebuildRenderedField(tokensB)\n\n      tokenIndexToDrag = 1\n      token = fieldA.find('.token').at(tokenIndexToDrag)\n\n      dragStartEventData = {}\n      dragStartEvent =\n        dataTransfer:\n          setData: (type, val) ->\n            dragStartEventData[type] = val\n      token.simulate('dragStart', dragStartEvent)\n\n      expect(dragStartEventData).toEqual({\n        'nylas-token-items': '[{\"client_id\":\"123\",\"server_id\":\"2\",\"name\":\"Nylas Burger Basket\",\"email\":\"burgers@nylas.com\",\"thirdPartyData\":{},\"is_search_indexed\":false,\"id\":\"2\",\"__constructorName\":\"Contact\"}]'\n        'text/plain': 'Nylas Burger Basket <burgers@nylas.com>'\n      })\n\n      dropEvent =\n        dataTransfer:\n          types: Object.keys(dragStartEventData)\n          getData: (type) -> dragStartEventData[type]\n\n      fieldB.ref('field-drop-target').simulate('drop', dropEvent)\n\n      expect(@propAdd).toHaveBeenCalledWith([tokensA[tokenIndexToDrag]])\n\n  describe \"When the user selects a token\", ->\n    beforeEach ->\n      token = @renderedField.find('.token').first()\n      token.simulate('click')\n\n    it \"should set the selectedKeys state\", ->\n      expect(@renderedField.state().selectedKeys).toEqual([participant1.email])\n\n    it \"should return the appropriate token object\", ->\n      expect(@propTokenKey).toHaveBeenCalledWith(participant1)\n      expect(@renderedField.find('.token.selected').length).toEqual(1)\n\n  describe \"when focused\", ->\n    it 'should receive the `focused` class', ->\n      expect(@renderedField.find('.focused').length).toBe(0)\n      @renderedInput.simulate('focus')\n      expect(@renderedField.find('.focused').length).toBe(1)\n\n  describe \"when the user types in the input\", ->\n    it 'should fetch completions for the text', ->\n      @renderedInput.simulate('change', {target: {value: 'abc'}})\n      advanceClock(1000)\n      expect(@propCompletionsForInput.calls[0].args[0]).toBe('abc')\n\n    it 'should fetch completions on focus', ->\n      @renderedField.setState({inputValue: \"abc\"})\n      @renderedInput.simulate('focus')\n      advanceClock(1000)\n      expect(@propCompletionsForInput.calls[0].args[0]).toBe('abc')\n\n    it 'should display the completions', ->\n      @completions = [participant4, participant5]\n      @renderedInput.simulate('change', {target: {value: 'abc'}})\n\n      components = @renderedField.find(CustomSuggestion)\n      expect(components.length).toBe(2)\n      expect(components.at(0).props().item).toBe(participant4)\n      expect(components.at(1).props().item).toBe(participant5)\n\n    it 'should not display items with keys matching items already in the token field', ->\n      @completions = [participant2, participant4, participant1]\n      @renderedInput.simulate('change', {target: {value: 'abc'}})\n\n      components = @renderedField.find(CustomSuggestion)\n      expect(components.length).toBe(1)\n      expect(components.at(0).props().item).toBe(participant4)\n\n    it 'should highlight the first completion', ->\n      @completions = [participant4, participant5]\n      @renderedInput.simulate('change', {target: {value: 'abc'}})\n      components = @renderedField.find(Menu.Item)\n      menuItem = components.first()\n      expect(menuItem.props().selected).toBe true\n\n    it 'select the clicked element', ->\n      @completions = [participant4, participant5]\n      @renderedInput.simulate('change', {target: {value: 'abc'}})\n      components = @renderedField.find(Menu.Item)\n      menuItem = components.first()\n      menuItem.simulate('mouseDown')\n      expect(@propAdd).toHaveBeenCalledWith([participant4])\n\n    it \"doesn't sumbmit if it looks like an email but has no space at the end\", ->\n      @renderedInput.simulate('change', {target: {value: 'abc@foo.com'}})\n      advanceClock(10)\n      expect(@propCompletionsForInput.calls[0].args[0]).toBe('abc@foo.com')\n      expect(@propAdd).not.toHaveBeenCalled()\n\n    it \"allows spaces if what's currently being entered doesn't look like an email\", ->\n      @renderedInput.simulate('change', {target: {value: 'ab'}})\n      advanceClock(10)\n      @renderedInput.simulate('change', {target: {value: 'ab '}})\n      advanceClock(10)\n      @renderedInput.simulate('change', {target: {value: 'ab c'}})\n      advanceClock(10)\n      expect(@propCompletionsForInput.calls[2].args[0]).toBe('ab c')\n      expect(@propAdd).not.toHaveBeenCalled()\n\n  [{key:'Enter', keyCode:13}, {key:',', keyCode: 188}].forEach ({key, keyCode}) ->\n    describe \"when the user presses #{key}\", ->\n      describe \"and there is an completion available\", ->\n        it \"should call add with the first completion\", ->\n          @completions = [participant4]\n          @renderedInput.simulate('change', {target: {value: 'abc'}})\n          @renderedInput.simulate('keyDown', {key: key, keyCode: keyCode})\n          expect(@propAdd).toHaveBeenCalledWith([participant4])\n\n      describe \"and there is NO completion available\", ->\n        it 'should call add, allowing the parent to (optionally) turn the text into a token', ->\n          @completions = []\n          @renderedInput.simulate('change', {target: {value: 'abc'}})\n          @renderedInput.simulate('keyDown', {key: key, keyCode: keyCode})\n          expect(@propAdd).toHaveBeenCalledWith('abc', {})\n\n  describe \"when the user presses tab\", ->\n    beforeEach ->\n      @tabDownEvent =\n        key: \"Tab\"\n        keyCode: 9\n        preventDefault: jasmine.createSpy('preventDefault')\n        stopPropagation: jasmine.createSpy('stopPropagation')\n\n    describe \"and there is an completion available\", ->\n      it \"should call add with the first completion\", ->\n        @completions = [participant4]\n        @renderedInput.simulate('change', {target: {value: 'abc'}})\n        @renderedInput.simulate('keyDown', @tabDownEvent)\n        expect(@propAdd).toHaveBeenCalledWith([participant4])\n        expect(@tabDownEvent.preventDefault).toHaveBeenCalled()\n        expect(@tabDownEvent.stopPropagation).toHaveBeenCalled()\n\n    it \"shouldn't handle the event in the input is empty\", ->\n      # We ignore on empty input values\n      @renderedInput.simulate('change', {target: {value: ' '}})\n      @renderedInput.simulate('keyDown', @tabDownEvent)\n      expect(@propAdd).not.toHaveBeenCalled()\n\n    it \"should NOT stop the propagation if the input is empty.\", ->\n      # This is to allow tabs to propagate up to controls that might want\n      # to change the focus later.\n      @renderedInput.simulate('change', {target: {value: ' '}})\n      @renderedInput.simulate('keyDown', @tabDownEvent)\n      expect(@propAdd).not.toHaveBeenCalled()\n      expect(@tabDownEvent.stopPropagation).not.toHaveBeenCalled()\n\n    it \"should add the raw input value if there are no completions\", ->\n      @completions = []\n      @renderedInput.simulate('change', {target: {value: 'abc'}})\n      @renderedInput.simulate('keyDown', @tabDownEvent)\n      expect(@propAdd).toHaveBeenCalledWith('abc', {})\n      expect(@tabDownEvent.preventDefault).toHaveBeenCalled()\n      expect(@tabDownEvent.stopPropagation).toHaveBeenCalled()\n\n  describe \"when blurred\", ->\n    it 'should do nothing if the relatedTarget is null meaning the app has been blurred', ->\n      @renderedInput.simulate('focus')\n      @renderedInput.simulate('change', {target: {value: 'text'}})\n      @renderedInput.simulate('blur', {relatedTarget: null})\n      expect(@propAdd).not.toHaveBeenCalled()\n      expect(@renderedField.find('.focused').length).toBe(1)\n\n    it 'should call add, allowing the parent component to (optionally) turn the entered text into a token', ->\n      @renderedInput.simulate('focus')\n      @renderedInput.simulate('change', {target: {value: 'text'}})\n      @renderedInput.simulate('blur', {relatedTarget: document.body})\n      expect(@propAdd).toHaveBeenCalledWith('text', {})\n\n    it 'should clear the entered text', ->\n      @renderedInput.simulate('focus')\n      @renderedInput.simulate('change', {target: {value: 'text'}})\n      @renderedInput.simulate('blur', {relatedTarget: document.body})\n      expect(@renderedInput.props().value).toBe('')\n\n    it 'should no longer have the `focused` class', ->\n      @renderedInput.simulate('focus')\n      expect(@renderedField.find('.focused').length).toBe(1)\n      @renderedInput.simulate('blur', {relatedTarget: document.body})\n      expect(@renderedField.find('.focused').length).toBe(0)\n\n  describe \"cut\", ->\n    it \"removes the selected tokens\", ->\n      @renderedField.setState({selectedKeys: [participant1.email]})\n      @renderedInput.simulate('cut')\n      expect(@propRemove).toHaveBeenCalledWith([participant1])\n      expect(@renderedField.find('.token.selected').length).toEqual(0)\n      expect(@propEmptied).not.toHaveBeenCalled()\n\n  describe \"backspace\", ->\n    describe \"when no token is selected\", ->\n      it \"selects the last token first and doesn't remove\", ->\n        @renderedInput.simulate('keyDown', {key: 'Backspace', keyCode: 8})\n        expect(@renderedField.find('.token.selected').length).toEqual(1)\n        expect(@propRemove).not.toHaveBeenCalled()\n        expect(@propEmptied).not.toHaveBeenCalled()\n\n    describe \"when a token is selected\", ->\n      it \"removes that token and deselects\", ->\n        @renderedField.setState({selectedKeys: [participant1.email]})\n        expect(@renderedField.find('.token.selected').length).toEqual(1)\n        @renderedInput.simulate('keyDown', {key: 'Backspace', keyCode: 8})\n        expect(@propRemove).toHaveBeenCalledWith([participant1])\n        expect(@renderedField.find('.token.selected').length).toEqual(0)\n        expect(@propEmptied).not.toHaveBeenCalled()\n\n    describe \"when there are no tokens left\", ->\n      it \"fires onEmptied\", ->\n        @renderedField.setProps({tokens: []})\n        expect(@renderedField.find('.token').length).toEqual(0)\n        @renderedInput.simulate('keyDown', {key: 'Backspace', keyCode: 8})\n        expect(@propEmptied).toHaveBeenCalled()\n\ndescribe \"TokenizingTextField.Token\", ->\n  describe \"when an onEdit prop has been provided\", ->\n    beforeEach ->\n      @propEdit = jasmine.createSpy('onEdit')\n      @propClick = jasmine.createSpy('onClick')\n      @token = mount(React.createElement(TokenizingTextField.Token, {\n        selected: false,\n        valid: true,\n        item: participant1,\n        onClick: @propClick,\n        onEdited: @propEdit,\n        onDragStart: jasmine.createSpy('onDragStart'),\n      }))\n\n    it \"should enter editing mode\", ->\n      expect(@token.state().editing).toBe(false)\n      @token.simulate('doubleClick', {})\n      expect(@token.state().editing).toBe(true)\n\n    it \"should call onEdit to commit the new token value when the edit field is blurred\", ->\n      expect(@token.state().editing).toBe(false)\n      @token.simulate('doubleClick', {})\n      tokenEditInput = @token.find('input')\n      tokenEditInput.simulate('change', {target: {value: 'new tag content'}})\n      tokenEditInput.simulate('blur')\n      expect(@propEdit).toHaveBeenCalledWith(participant1, 'new tag content')\n\n  describe \"when no onEdit prop has been provided\", ->\n    it \"should not enter editing mode\", ->\n      @token = mount(React.createElement(TokenizingTextField.Token, {\n        selected: false,\n        valid: true,\n        item: participant1,\n        onClick: jasmine.createSpy('onClick'),\n        onDragStart: jasmine.createSpy('onDragStart'),\n        onEdited: null,\n      }))\n      expect(@token.state().editing).toBe(false)\n      @token.simulate('doubleClick', {})\n      expect(@token.state().editing).toBe(false)\n"
  },
  {
    "path": "packages/client-app/spec/database-object-registry-spec.es6",
    "content": "/* eslint quote-props: 0 */\nimport _ from 'underscore';\nimport Model from '../src/flux/models/model';\nimport Attributes from '../src/flux/attributes';\nimport DatabaseObjectRegistry from '../src/registries/database-object-registry';\n\nclass GoodTest extends Model {\n  static attributes = _.extend({}, Model.attributes, {\n    \"foo\": Attributes.String({\n      modelKey: 'foo',\n      jsonKey: 'foo',\n    }),\n  });\n}\n\ndescribe('DatabaseObjectRegistry', function DatabaseObjectRegistrySpecs() {\n  beforeEach(() => DatabaseObjectRegistry.unregister(\"GoodTest\"));\n\n  it(\"can register constructors\", () => {\n    const testFn = () => GoodTest;\n    expect(() => DatabaseObjectRegistry.register(\"GoodTest\", testFn)).not.toThrow();\n    expect(DatabaseObjectRegistry.get(\"GoodTest\")).toBe(GoodTest);\n  });\n\n  it(\"Tests if a constructor is in the registry\", () => {\n    DatabaseObjectRegistry.register(\"GoodTest\", () => GoodTest);\n    expect(DatabaseObjectRegistry.isInRegistry(\"GoodTest\")).toBe(true);\n  });\n\n  it(\"deserializes the objects for a constructor\", () => {\n    DatabaseObjectRegistry.register(\"GoodTest\", () => GoodTest);\n    const obj = DatabaseObjectRegistry.deserialize(\"GoodTest\", {foo: \"bar\"});\n    expect(obj instanceof GoodTest).toBe(true);\n    expect(obj.foo).toBe(\"bar\");\n  });\n\n  it(\"throws an error if the object can't be deserialized\", () =>\n    expect(() => DatabaseObjectRegistry.deserialize(\"GoodTest\", {foo: \"bar\"})).toThrow()\n  );\n});\n"
  },
  {
    "path": "packages/client-app/spec/default-client-helper-spec.coffee",
    "content": "_ = require 'underscore'\nproxyquire = require 'proxyquire'\n\nstubDefaultsJSON = null\nexecHitory = []\n\nChildProcess =\n  exec: (command, callback) ->\n    execHitory.push(arguments)\n    callback(null, '', null)\n\nfs =\n  exists: (path, callback) ->\n    callback(true)\n  readFile: (path, callback) ->\n    callback(null, JSON.stringify(stubDefaultsJSON))\n  readFileSync: (path) ->\n    JSON.stringify(stubDefaultsJSON)\n  writeFileSync: (path) ->\n    null\n  unlink: (path, callback) ->\n    callback(null) if callback\n\nDefaultClientHelper = proxyquire \"../src/default-client-helper\",\n  \"child_process\": ChildProcess\n  \"fs\": fs\n\ndescribe \"DefaultClientHelper\", ->\n  beforeEach ->\n    stubDefaultsJSON = [\n      {\n          LSHandlerRoleAll: 'com.apple.dt.xcode',\n          LSHandlerURLScheme: 'xcdoc'\n      },\n      {\n          LSHandlerRoleAll: 'com.fournova.tower',\n          LSHandlerURLScheme: 'github-mac'\n      },\n      {\n          LSHandlerRoleAll: 'com.fournova.tower',\n          LSHandlerURLScheme: 'sourcetree'\n      },\n      {\n          LSHandlerRoleAll: 'com.google.chrome',\n          LSHandlerURLScheme: 'http'\n      },\n      {\n          LSHandlerRoleAll: 'com.google.chrome',\n          LSHandlerURLScheme: 'https'\n      },\n      {\n          LSHandlerContentType: 'public.html',\n          LSHandlerRoleViewer: 'com.google.chrome'\n      },\n      {\n          LSHandlerContentType: 'public.url',\n          LSHandlerRoleViewer: 'com.google.chrome'\n      },\n      {\n          LSHandlerContentType: 'com.apple.ical.backup',\n          LSHandlerRoleAll: 'com.apple.ical'\n      },\n      {\n          LSHandlerContentTag: 'icalevent',\n          LSHandlerContentTagClass: 'public.filename-extension',\n          LSHandlerRoleAll: 'com.apple.ical'\n      },\n      {\n          LSHandlerContentTag: 'icaltodo',\n          LSHandlerContentTagClass: 'public.filename-extension',\n          LSHandlerRoleAll: 'com.apple.reminders'\n      },\n      {\n          LSHandlerRoleAll: 'com.apple.ical',\n          LSHandlerURLScheme: 'webcal'\n      },\n      {\n          LSHandlerContentTag: 'coffee',\n          LSHandlerContentTagClass: 'public.filename-extension',\n          LSHandlerRoleAll: 'com.sublimetext.2'\n      },\n      {\n          LSHandlerRoleAll: 'com.apple.facetime',\n          LSHandlerURLScheme: 'facetime'\n      },\n      {\n          LSHandlerRoleAll: 'com.apple.dt.xcode',\n          LSHandlerURLScheme: 'xcdevice'\n      },\n      {\n          LSHandlerContentType: 'public.png',\n          LSHandlerRoleAll: 'com.macromedia.fireworks'\n      },\n      {\n          LSHandlerRoleAll: 'com.apple.dt.xcode',\n          LSHandlerURLScheme: 'xcbot'\n      },\n      {\n          LSHandlerRoleAll: 'com.microsoft.rdc.mac',\n          LSHandlerURLScheme: 'rdp'\n      },\n      {\n          LSHandlerContentTag: 'rdp',\n          LSHandlerContentTagClass: 'public.filename-extension',\n          LSHandlerRoleAll: 'com.microsoft.rdc.mac'\n      },\n      {\n          LSHandlerContentType: 'public.json',\n          LSHandlerRoleAll: 'com.sublimetext.2'\n      },\n      {\n          LSHandlerContentTag: 'cson',\n          LSHandlerContentTagClass: 'public.filename-extension',\n          LSHandlerRoleAll: 'com.sublimetext.2'\n      },\n      {\n          LSHandlerRoleAll: 'com.apple.mail',\n          LSHandlerURLScheme: 'mailto'\n      }\n    ]\n\n\n  describe \"DefaultClientHelperMac\", ->\n    beforeEach ->\n      execHitory = []\n      @helper = new DefaultClientHelper.Mac()\n\n    describe \"available\", ->\n      it \"should return true\", ->\n        expect(@helper.available()).toEqual(true)\n\n    describe \"readDefaults\", ->\n\n    describe \"writeDefaults\", ->\n      it \"should `lsregister` to reload defaults after saving them\", ->\n        callback = jasmine.createSpy('callback')\n        @helper.writeDefaults(stubDefaultsJSON, callback)\n        callback.callCount is 1\n        command = execHitory[2][0]\n        expect(command).toBe(\"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user\")\n\n    describe \"isRegisteredForURLScheme\", ->\n      it \"should require a callback is provided\", ->\n        expect( -> @helper.isRegisteredForURLScheme('mailto')).toThrow()\n\n      it \"should return true if a matching `LSHandlerURLScheme` record exists for the bundle identifier\", ->\n        spyOn(@helper, 'readDefaults').andCallFake (callback) ->\n          callback([{\n            \"LSHandlerRoleAll\": \"com.apple.dt.xcode\",\n            \"LSHandlerURLScheme\": \"xcdoc\"\n          }, {\n            \"LSHandlerContentTag\": \"cson\",\n            \"LSHandlerContentTagClass\": \"public.filename-extension\",\n            \"LSHandlerRoleAll\": \"com.sublimetext.2\"\n          }, {\n            \"LSHandlerRoleAll\": \"com.nylas.nylas-mail\",\n            \"LSHandlerURLScheme\": \"mailto\"\n          }])\n        @helper.isRegisteredForURLScheme 'mailto', (registered) ->\n          expect(registered).toBe(true)\n\n      it \"should return false when other records exist for the bundle identifier but do not match\", ->\n        spyOn(@helper, 'readDefaults').andCallFake (callback) ->\n          callback([{\n            LSHandlerRoleAll: \"com.apple.dt.xcode\",\n            LSHandlerURLScheme: \"xcdoc\"\n          },{\n            LSHandlerContentTag: \"cson\",\n            LSHandlerContentTagClass: \"public.filename-extension\",\n            LSHandlerRoleAll: \"com.sublimetext.2\"\n          },{\n            LSHandlerRoleAll: \"com.nylas.nylas-mail\",\n            LSHandlerURLScheme: \"atom\"\n          }])\n        @helper.isRegisteredForURLScheme 'mailto', (registered) ->\n          expect(registered).toBe(false)\n\n      it \"should return false if another bundle identifier is registered for the `LSHandlerURLScheme`\", ->\n        spyOn(@helper, 'readDefaults').andCallFake (callback) ->\n          callback([{\n            LSHandlerRoleAll: \"com.apple.dt.xcode\",\n            LSHandlerURLScheme: \"xcdoc\"\n          },{\n            LSHandlerContentTag: \"cson\",\n            LSHandlerContentTagClass: \"public.filename-extension\",\n            LSHandlerRoleAll: \"com.sublimetext.2\"\n          },{\n            LSHandlerRoleAll: \"com.apple.mail\",\n            LSHandlerURLScheme: \"mailto\"\n          }])\n        @helper.isRegisteredForURLScheme 'mailto', (registered) ->\n          expect(registered).toBe(false)\n\n    describe \"registerForURLScheme\", ->\n      it \"should remove any existing records for the `LSHandlerURLScheme`\", ->\n        @helper.registerForURLScheme 'mailto', =>\n          @helper.readDefaults (values) ->\n            expect(JSON.stringify(values).indexOf('com.apple.mail')).toBe(-1)\n\n      it \"should add a record for the `LSHandlerURLScheme` and the app's bundle identifier\", ->\n        @helper.registerForURLScheme 'mailto', =>\n          @helper.readDefaults (defaults) ->\n            match = _.find defaults, (d) ->\n              d.LSHandlerURLScheme is 'mailto' and d.LSHandlerRoleAll is 'com.nylas.nylas-mail'\n            expect(match).not.toBe(null)\n\n      it \"should write the new defaults\", ->\n        spyOn(@helper, 'readDefaults').andCallFake (callback) ->\n          callback([{\n            LSHandlerRoleAll: \"com.apple.dt.xcode\",\n            LSHandlerURLScheme: \"xcdoc\"\n          }])\n        spyOn(@helper, 'writeDefaults')\n        @helper.registerForURLScheme('mailto')\n        expect(@helper.writeDefaults).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/css.css",
    "content": "body {\n  font-size: 1234px;\n  width: 110%;\n  font-weight: bold !important;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/db-test-model.coffee",
    "content": "Model = require '../../src/flux/models/model'\nCategory = require('../../src/flux/models/category').default\nAttributes = require('../../src/flux/attributes').default\n\nclass TestModel extends Model\n  @attributes =\n    'id': Attributes.String\n      queryable: true\n      modelKey: 'id'\n\n    'clientId': Attributes.String\n      queryable: true\n      modelKey: 'clientId'\n      jsonKey: 'client_id'\n\n    'serverId': Attributes.ServerId\n      queryable: true\n      modelKey: 'serverId'\n      jsonKey: 'server_id'\n\nTestModel.configureBasic = ->\n  TestModel.additionalSQLiteConfig = undefined\n  TestModel.attributes =\n    'id': Attributes.String\n      queryable: true\n      modelKey: 'id'\n    'clientId': Attributes.String\n      queryable: true\n      modelKey: 'clientId'\n      jsonKey: 'client_id'\n    'serverId': Attributes.ServerId\n      queryable: true\n      modelKey: 'serverId'\n      jsonKey: 'server_id'\n\nTestModel.configureWithAllAttributes = ->\n  TestModel.additionalSQLiteConfig = undefined\n  TestModel.attributes =\n    'datetime': Attributes.DateTime\n      queryable: true\n      modelKey: 'datetime'\n    'string': Attributes.String\n      queryable: true\n      modelKey: 'string'\n      jsonKey: 'string-json-key'\n    'boolean': Attributes.Boolean\n      queryable: true\n      modelKey: 'boolean'\n    'number': Attributes.Number\n      queryable: true\n      modelKey: 'number'\n    'other': Attributes.String\n      modelKey: 'other'\n\nTestModel.configureWithCollectionAttribute = ->\n  TestModel.additionalSQLiteConfig = undefined\n  TestModel.attributes =\n    'id': Attributes.String\n      queryable: true\n      modelKey: 'id'\n    'clientId': Attributes.String\n      queryable: true\n      modelKey: 'clientId'\n      jsonKey: 'client_id'\n    'serverId': Attributes.ServerId\n      queryable: true\n      modelKey: 'serverId'\n      jsonKey: 'server_id'\n    'other': Attributes.String\n      queryable: true,\n      modelKey: 'other'\n    'categories': Attributes.Collection\n      queryable: true,\n      modelKey: 'categories'\n      itemClass: Category,\n      joinOnField: 'id',\n      joinQueryableBy: ['other'],\n\nTestModel.configureWithJoinedDataAttribute = ->\n  TestModel.additionalSQLiteConfig = undefined\n  TestModel.attributes =\n    'id': Attributes.String\n      queryable: true\n      modelKey: 'id'\n    'clientId': Attributes.String\n      queryable: true\n      modelKey: 'clientId'\n      jsonKey: 'client_id'\n    'serverId': Attributes.ServerId\n      queryable: true\n      modelKey: 'serverId'\n      jsonKey: 'server_id'\n    'body': Attributes.JoinedData\n      modelTable: 'TestModelBody'\n      modelKey: 'body'\n\n\nTestModel.configureWithAdditionalSQLiteConfig = ->\n  TestModel.attributes =\n    'id': Attributes.String\n      queryable: true\n      modelKey: 'id'\n    'clientId': Attributes.String\n      modelKey: 'clientId'\n      jsonKey: 'client_id'\n    'serverId': Attributes.ServerId\n      modelKey: 'serverId'\n      jsonKey: 'server_id'\n    'body': Attributes.JoinedData\n      modelTable: 'TestModelBody'\n      modelKey: 'body'\n  TestModel.additionalSQLiteConfig =\n    setup: ->\n      ['CREATE INDEX IF NOT EXISTS ThreadListIndex ON Thread(last_message_received_timestamp DESC, account_id, id)']\n\nmodule.exports = TestModel\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/correct_sig.txt",
    "content": "this is an email with a correct -- signature.\n\n-- \nrick\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n</head>\n<body style=\"word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\">\n<div><span class=\"message_content\">\n<pre class=\"special_formatting\"><font face=\"Calibri\"><span style=\"font-size: 15px;\">Hi Jeff,\n\nQuick update on the event bugs:\n- I fixed the bug where events would be incorrectly marked as read-only.\n- We expose RRULEs as valid JSON now. \n\nWe're currently testing the fixes, they should ship early next week.\n\nConcerning timezones, an event should always be associated with a timezone. Having a NULL value instead is a bug on our end. I will be working on fixing this on Monday and will let you know when it's fixed.\n\nThanks for your detailed bug reports,</span></font></pre>\n<pre class=\"special_formatting\"><font face=\"Calibri\"><span style=\"font-size: 15px;\">\nKarim</span></font></pre>\n<pre class=\"special_formatting\" style=\"color: rgb(0, 0, 0); font-family: Calibri, sans-serif; font-size: 14px;\"><span style=\"font-family: Calibri; font-size: 11pt; font-weight: bold;\">From: </span><span style=\"font-family: Calibri; font-size: 11pt;\"> Kavya Joshi &lt;</span><a href=\"mailto:kavya@nylas.com\" style=\"font-family: Calibri; font-size: 11pt;\">kavya@nylas.com</a><span style=\"font-family: Calibri; font-size: 11pt;\">&gt;</span></pre>\n</span></div>\n<span id=\"OLK_SRC_BODY_SECTION\" style=\"color: rgb(0, 0, 0); font-family: Calibri, sans-serif; font-size: 14px;\">\n<div style=\"font-family:Calibri; font-size:11pt; text-align:left; color:black; BORDER-BOTTOM: medium none; BORDER-LEFT: medium none; PADDING-BOTTOM: 0in; PADDING-LEFT: 0in; PADDING-RIGHT: 0in; BORDER-TOP: #b5c4df 1pt solid; BORDER-RIGHT: medium none; PADDING-TOP: 3pt\">\n<span style=\"font-weight:bold\">Date: </span>jeudi 28 mai 2015 20:21<br>\n<span style=\"font-weight:bold\">To: </span>Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\">jeff@esper.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Cc: </span>Jennie Lees &lt;<a href=\"mailto:jennie@nylas.com\">jennie@nylas.com</a>&gt;, Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\">andrew@esper.com</a>&gt;, Mackenzie Dallas &lt;<a href=\"mailto:mackenzie@esper.com\">mackenzie@esper.com</a>&gt;,\n support &lt;<a href=\"mailto:support@nylas.com\">support@nylas.com</a>&gt;, Karim Hamidou &lt;<a href=\"mailto:karim@nylas.com\">karim@nylas.com</a>&gt;, Christine Spang &lt;<a href=\"mailto:spang@nylas.com\">spang@nylas.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Subject: </span>Re: Esper &lt;-&gt; Nilas<br>\n</div>\n<div><br>\n</div>\n<div><style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n<div dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:12pt; color:#000000; background-color:#FFFFFF; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<p>Hi Jeff,<br>\n</p>\n<p><br>\n</p>\n<p>The events are incorrectly marked as&nbsp;read&nbsp;only because of a&nbsp;bug in how we determine the organizer of an event; read-writable permissions are only granted to the organizer as per the&nbsp;<span style=\"font-size:12pt\">Exchange ActiveSync protocol. Karim's working\n on fixing the bug,&nbsp;it will be rolled out&nbsp;shortly and we'll keep you posted.</span></p>\n<p><span style=\"font-size:12pt\"><br>\n</span></p>\n<p><span style=\"font-size:12pt\">With respect to the rrule returned by the API - absolutely;&nbsp;we will change the representation and let you know when that's done too.</span></p>\n<p><span style=\"font-size:12pt\"><br>\n</span></p>\n<p><span style=\"font-size:12pt\">With respect to your question about when the&nbsp;timezone would be null for calendars&nbsp;- we're looking into it and will get back to you.</span></p>\n<p><span style=\"font-size:12pt\"><br>\n</span></p>\n<p><span style=\"font-size:12pt\">Thanks!</span></p>\n<p><span style=\"font-size:12pt\">Kavya</span></p>\n<p><br>\n</p>\n<p><br>\n</p>\n<div style=\"color:rgb(0,0,0)\">\n<hr tabindex=\"-1\" style=\"display:inline-block; width:98%\">\n<div id=\"divRplyFwdMsg\" dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\">jeff@esper.com</a>&gt;<br>\n<b>Sent:</b> Wednesday, May 27, 2015 3:30 PM<br>\n<b>To:</b> Kavya Joshi<br>\n<b>Cc:</b> Jennie Lees; Andrew Lee; Mackenzie Dallas; support; Karim Hamidou; Christine Spang<br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>Hi Kavya,<br>\n<br>\n</div>\nI just did some more testing with the Meg account, and everything related to recurring events worked correctly for me. I was about to try with one of our real Formation 8 accounts, but I ran into an issue syncing data back to O365 through Nylas: even non-recurring\n events that I create or modify on the O365 side become read only in Nylas, so I'm unable to update them through your API. I remember we had this problem before and you guys fixed it, so maybe this was reintroduced during your recurring event changes, since\n those events should be read only? Hopefully this isn't a complicated fix, let me know if I can provide more info.<br>\n<br>\n</div>\n<div>The stuff below is not as important, just wanted to mention it:<br>\n</div>\n<div><br>\n</div>\nI noticed a couple things about the recurrence data in Nylas. First, the rrule is given as a single-quote-delimited string array packed inside a JSON string. We're currently dealing with this by taking the rrule string, replacing ' with &quot;, then parsing the\n now-valid JSON array. This could fail if there are other quote characters in the rule... could your API return the rrule simply as a JSON array containing double-quoted JSON strings, or would that break existing things? Second, I see that a recurrence entry\n comes with a timezone, which is great because Google needs one, but my Meg calendars are always showing null for the timezone. Christine explained in the past that there are difficulties in getting timezones for Exchange calendars... do you know in what cases\n this field will be non-null? For now, we require our users to specify their calendar timezone during onboarding, and we just use that zone every time.<br>\n<br>\n</div>\nThanks again,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Wed, May 27, 2015 at 2:03 PM, Kavya Joshi <span dir=\"ltr\">\n&lt;<a href=\"mailto:kavya@nylas.com\" target=\"_blank\">kavya@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-size:12pt; color:#000000; background-color:#ffffff; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<p>Hi Jeff,<br>\n</p>\n<p><br>\n</p>\n<p>We ran the script to back-fix recurring events created in the past (before last Friday) for the following accounts:<br>\n</p>\n<p><br>\n</p>\n<p><a href=\"mailto:meg@espertech.onmicrosoft.com\" target=\"_blank\">meg@espertech.onmicrosoft.com</a><br>\n</p>\n<p><a href=\"mailto:stewiegriffin@espertech.onmicrosoft.com\" target=\"_blank\">stewiegriffin@espertech.onmicrosoft.com</a><br>\n</p>\n<p><br>\n</p>\n<p>Please note that as a result, the event and&nbsp;calendar IDs returned by the API&nbsp;will be different for these accounts.<br>\n</p>\n<p><br>\n</p>\n<p>Let us know if any of the<span style=\"font-size:12pt\">&nbsp;</span><span style=\"font-size:12pt\">recurring events look incorrect, thanks!</span></p>\n<p>Kavya<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<div style=\"color:rgb(0,0,0)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Christine Spang<br>\n<b>Sent:</b> Tuesday, May 26, 2015 4:32 PM<br>\n<b>To:</b> Jeff Meister<br>\n<b>Cc:</b> Kavya Joshi; Jennie Lees; Andrew Lee; Mackenzie Dallas; support; Karim Hamidou\n<div>\n<div class=\"h5\"><br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</div>\n</div>\n</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div class=\"h5\">\n<div>Jeff, that's correct—read-only support for now.<br>\n<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\nOn May 26 2015, at 4:16 pm, Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt; wrote:\n<br>\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>Hi Karim,<br>\n<br>\n</div>\nI just ran my tests from the previous email again, and it looks like they're all working correctly now! I see the overrides working too; I tried deleting a single instance of an event and got an EXDATE as expected. I'll hook things up to GCal on our end and\n let you know if I see any more recurrence issues.<br>\n<br>\n</div>\nBTW, this is read-only support, correct? We shouldn't be attempting to make changes to recurring events through Nylas? (That's OK if so, just want to check and be sure.)<br>\n<br>\n</div>\nThanks,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Tue, May 26, 2015 at 12:49 PM, Karim Hamidou <span dir=\"ltr\">\n&lt;<a href=\"mailto:karim@nylas.com\" target=\"_blank\">karim@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div style=\"word-wrap:break-word; color:rgb(0,0,0); font-size:14px; font-family:Calibri,sans-serif\">\n<div><span>Hi Andrew, Jeff</span></div>\n<div><span><br>\n</span></div>\n<div><span>Sorry for the delay in getting back to you.&nbsp;</span><span>We just deployed the changes to production and the code now supports all documented Exchange recurrence rules and event overrides.<br>\nWe will be running a script today to back-fix recurring events created before last Friday too.</span></div>\n<div><span><br>\n</span></div>\n<div><span>We’ve tested the code extensively but as always, do let us know if you run into problems.</span></div>\n<div><span><br>\n</span></div>\n<div><span>Karim &nbsp; &nbsp; &nbsp;&nbsp;</span></div>\n<div><br>\n</div>\n<span>\n<div style=\"font-family:Calibri; font-size:11pt; text-align:left; color:black; border-bottom:medium none; border-left:medium none; padding-bottom:0in; padding-left:0in; padding-right:0in; border-top:#b5c4df 1pt solid; border-right:medium none; padding-top:3pt\">\n<span style=\"font-weight:bold\">From: </span>Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Date: </span>mardi 26 mai 2015 20:24\n<div>\n<div><br>\n<span style=\"font-weight:bold\">To: </span>Karim Hamidou &lt;<a href=\"mailto:karim@nylas.com\" target=\"_blank\">karim@nylas.com</a>&gt;, Christine Spang &lt;<a href=\"mailto:spang@nylas.com\" target=\"_blank\">spang@nylas.com</a>&gt;, Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Cc: </span>Kavya Joshi &lt;<a href=\"mailto:kavya@nylas.com\" target=\"_blank\">kavya@nylas.com</a>&gt;, support &lt;<a href=\"mailto:support@nylas.com\" target=\"_blank\">support@nylas.com</a>&gt;, Mackenzie Dallas &lt;<a href=\"mailto:mackenzie@esper.com\" target=\"_blank\">mackenzie@esper.com</a>&gt;,\n Jennie Lees &lt;<a href=\"mailto:jennie@nylas.com\" target=\"_blank\">jennie@nylas.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Subject: </span>Re: Esper &lt;-&gt; Nilas<br>\n</div>\n</div>\n</div>\n<div>\n<div>\n<div><br>\n</div>\n<div>\n<div>\n<div dir=\"ltr\">Hey Karim, just checking on this. Did you guys release to production?<br>\n</div>\n<br>\n<div class=\"gmail_quote\">On Tue, May 19, 2015 at 11:59 AM Karim Hamidou &lt;<a href=\"mailto:karim@nylas.com\" target=\"_blank\">karim@nylas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div style=\"word-wrap:break-word; color:rgb(0,0,0); font-size:14px; font-family:Calibri,sans-serif\">\n<div>\n<div style=\"font-family:Calibri\">Hi Andrew &#43; Esper team,</div>\n<div style=\"font-family:Calibri\"><br>\n</div>\n<div style=\"font-family:Calibri\">Just a quick heads up: we're currently testing the fixes on our staging system. We're planning to ship them to production by the end of the week.</div>\n<div style=\"font-family:Calibri\"><br>\n</div>\n<div style=\"font-family:Calibri\">regards,</div>\n<div style=\"font-family:Calibri\"><br>\n</div>\n<div style=\"font-family:Calibri\">Karim</div>\n</div>\n<div><br>\n</div>\n<span>\n<div style=\"font-family:Calibri; font-size:11pt; text-align:left; color:black; border-bottom:medium none; border-left:medium none; padding-bottom:0in; padding-left:0in; padding-right:0in; border-top:#b5c4df 1pt solid; border-right:medium none; padding-top:3pt\">\n<span style=\"font-weight:bold\">From: </span>Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Date: </span>lundi 18 mai 2015 19:20<br>\n<span style=\"font-weight:bold\">To: </span>Karim Hamidou &lt;<a href=\"mailto:karim@nylas.com\" target=\"_blank\">karim@nylas.com</a>&gt;, Christine Spang &lt;<a href=\"mailto:spang@nylas.com\" target=\"_blank\">spang@nylas.com</a>&gt;, Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Cc: </span>Kavya Joshi &lt;<a href=\"mailto:kavya@nylas.com\" target=\"_blank\">kavya@nylas.com</a>&gt;, support &lt;<a href=\"mailto:support@nylas.com\" target=\"_blank\">support@nylas.com</a>&gt;, Mackenzie Dallas &lt;<a href=\"mailto:mackenzie@esper.com\" target=\"_blank\">mackenzie@esper.com</a>&gt;,\n Jennie Lees &lt;<a href=\"mailto:jennie@nylas.com\" target=\"_blank\">jennie@nylas.com</a>&gt;</div>\n</span></div>\n<div style=\"word-wrap:break-word; color:rgb(0,0,0); font-size:14px; font-family:Calibri,sans-serif\">\n<span>\n<div style=\"font-family:Calibri; font-size:11pt; text-align:left; color:black; border-bottom:medium none; border-left:medium none; padding-bottom:0in; padding-left:0in; padding-right:0in; border-top:#b5c4df 1pt solid; border-right:medium none; padding-top:3pt\">\n<br>\n<span style=\"font-weight:bold\">Subject: </span>Re: Esper &lt;-&gt; Nilas<br>\n</div>\n</span></div>\n<div style=\"word-wrap:break-word; color:rgb(0,0,0); font-size:14px; font-family:Calibri,sans-serif\">\n<span>\n<div><br>\n</div>\n<div>\n<div>\n<div dir=\"ltr\">Thanks for the e-mail Karim! Just let us know when you guys ship to production. -A<br>\n</div>\n<br>\n<div class=\"gmail_quote\">On Wed, May 13, 2015 at 12:08 PM Karim Hamidou &lt;<a href=\"mailto:karim@nylas.com\" target=\"_blank\">karim@nylas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div style=\"word-wrap:break-word; color:rgb(0,0,0); font-size:14px; font-family:Calibri,sans-serif\">\n<div>\n<div>Hi Andrew,</div>\n<div><br>\n</div>\n<div><span>Sorry for the delay in getting back to you. I was actually drafting a reply to Jeff.&nbsp;</span></div>\n<div>We’ve taken an iterative approach to this, and the recurring event code we shipped to production only supported the basic cases. That’s why Jeff found so many bugs -- sorry for not making this clearer.</div>\n<div><br>\n</div>\n<div><span>However, I’ve been working on an updated version of the code which should support almost every recurrence rule. I also went through all the bugs you reported to make sure the new code fixes them.</span></div>\n<div>It’s currently under review, so it should be shipped to prod early next week.</div>\n<div><br>\n</div>\n<div><br>\n</div>\n<div><span>Sorry for the misunderstanding,</span></div>\n<div><br>\n</div>\n<div><span>regards</span></div>\n<div><br>\n</div>\n<div><span>ps @Jeff&nbsp; —here’s a short list of what the new code supports:</span></div>\n<div><span>- UNTIL and COUNT rules</span></div>\n<div><span>- events recurring on the nth day of every month (e.g: a meeting occurring on the third wednesday of the month, of the year, etc.)</span></div>\n<div><span>- complex recurrences (e.g: events occurring every three days but only on Wednesday and Fridays, etc.)</span></div>\n<div><br>\n</div>\n<div><span>HOWEVER —we’re not yet supporting EXRULES. This is coming soon.</span></div>\n<div><br>\n</div>\n</div>\n<div><br>\n</div>\n<span>\n<div style=\"font-family:Calibri; font-size:11pt; text-align:left; color:black; border-bottom:medium none; border-left:medium none; padding-bottom:0in; padding-left:0in; padding-right:0in; border-top:#b5c4df 1pt solid; border-right:medium none; padding-top:3pt\">\n<span style=\"font-weight:bold\">From: </span>Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Date: </span>mercredi 13 mai 2015 20:08<br>\n<span style=\"font-weight:bold\">To: </span>Christine Spang &lt;<a href=\"mailto:spang@nylas.com\" target=\"_blank\">spang@nylas.com</a>&gt;, Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;<br>\n<span style=\"font-weight:bold\">Cc: </span>Kavya Joshi &lt;<a href=\"mailto:kavya@nylas.com\" target=\"_blank\">kavya@nylas.com</a>&gt;, support &lt;<a href=\"mailto:support@nylas.com\" target=\"_blank\">support@nylas.com</a>&gt;, Mackenzie Dallas &lt;<a href=\"mailto:mackenzie@esper.com\" target=\"_blank\">mackenzie@esper.com</a>&gt;,\n Karim Hamidou &lt;<a href=\"mailto:karim@nylas.com\" target=\"_blank\">karim@nylas.com</a>&gt;, Jennie Lees &lt;<a href=\"mailto:jennie@nylas.com\" target=\"_blank\">jennie@nylas.com</a>&gt;</div>\n</span></div>\n<div style=\"word-wrap:break-word; color:rgb(0,0,0); font-size:14px; font-family:Calibri,sans-serif\">\n<span>\n<div style=\"font-family:Calibri; font-size:11pt; text-align:left; color:black; border-bottom:medium none; border-left:medium none; padding-bottom:0in; padding-left:0in; padding-right:0in; border-top:#b5c4df 1pt solid; border-right:medium none; padding-top:3pt\">\n<br>\n<span style=\"font-weight:bold\">Subject: </span>Re: Esper &lt;-&gt; Nilas<br>\n</div>\n</span></div>\n<div style=\"word-wrap:break-word; color:rgb(0,0,0); font-size:14px; font-family:Calibri,sans-serif\">\n<span>\n<div><br>\n</div>\n<div>\n<div>\n<div dir=\"ltr\">Hey guys, just checking in and want to make sure we're giving everything you need. Let us know what the status is!&nbsp;<br>\n</div>\n<br>\n<div class=\"gmail_quote\">On Fri, May 8, 2015 at 10:16 AM Christine Spang &lt;<a href=\"mailto:spang@nylas.com\" target=\"_blank\">spang@nylas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-size:10pt; color:#000000; background-color:#ffffff; font-family:Arial,Helvetica,sans-serif\">\n<p>Thanks for the info, Jeff! Karim's out today, but we'll take a look on Monday.<br>\n</p>\n<div style=\"color:rgb(0,0,0)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;<br>\n<b>Sent:</b> Thursday, May 7, 2015 9:03 PM<br>\n<b>To:</b> Christine Spang<br>\n<b>Cc:</b> Andrew Lee; Kavya Joshi; support; Mackenzie Dallas; Karim Hamidou; Jennie Lees</font></div>\n</div>\n</div>\n</div>\n<div dir=\"ltr\">\n<div style=\"font-size:10pt; color:#000000; background-color:#ffffff; font-family:Arial,Helvetica,sans-serif\">\n<div style=\"color:rgb(0,0,0)\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</font></div>\n</div>\n</div>\n</div>\n<div dir=\"ltr\">\n<div style=\"font-size:10pt; color:#000000; background-color:#ffffff; font-family:Arial,Helvetica,sans-serif\">\n<div style=\"color:rgb(0,0,0)\">\n<div>\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>Hi Christine,<br>\n<br>\n</div>\nI got delayed working on other things, but I finally found time to test this today. I went back to my O365 test accounts, meg@ and\n<a href=\"mailto:stewiegriffin@espertech.onmicrosoft.com\" target=\"_blank\">stewiegriffin@espertech.onmicrosoft.com</a>, so I could mess around with the calendars without affecting customers. I started with Meg and tested some cases successfully, creating events\n with the following recurrence patterns through the Office 365 Web interface:<br>\n<br>\n</div>\nPattern: Every day, no end<br>\n</div>\nRule from Nylas: &quot;['RRULE:FREQ=DAILY']&quot;<br>\n</div>\nOK, as expected<br>\n<br>\n</div>\nPattern: Every week, on the same day as the first occurrence, no end<br>\n</div>\nRule from Nylas: &quot;['RRULE:FREQ=WEEKLY']&quot;<br>\n</div>\nOK, as expected<br>\n<br>\n</div>\nPattern: Every other week, on the same day as the first occurrence, no end<br>\n</div>\nRule from Nylas: &quot;['RRULE:FREQ=WEEKLY;INTERVAL=2']&quot;<br>\n</div>\nOK, as expected<br>\n<br>\n</div>\nThose were all good. Next, I tried some more complicated recurrences:<br>\n<br>\n</div>\nPattern: Every week on Wednesday and Friday, no end<br>\n</div>\nRule from Nylas: &quot;['RRULE:FREQ=WEEKLY']&quot;<br>\n</div>\nExpected BYDAY=TU,TH in RRULE<br>\n<br>\n</div>\n<div>Pattern: Every day for 4 days<br>\n</div>\n<div>Rule from Nylas: &quot;['RRULE:FREQ=DAILY']&quot;<br>\n</div>\n<div>Expected COUNT=4 or UNTIL in RRULE<br>\n</div>\n<div><br>\n</div>\nThose didn't give the rules I expected based on my reading of RFC 2445, but they're pretty close, so hopefully they are not so difficult to fix. I'm more worried about the next pattern I tried:<br>\n<br>\n</div>\nPattern: Second Wednesday of every month, no end<br>\n</div>\nRule from Nylas: None, event never synced<br>\n</div>\nExpected RRULE:FREQ=MONTHLY;BYDAY=2WE<br>\n<br>\n</div>\nIn this case, the event never showed up in my Nylas delta sync. Additionally, no events on that calendar have shown up in the delta sync since then, including the ones that worked fine before (even non-recurring events).<br>\n<br>\nI wasn't sure about the source of the problem, so I switched to my other testing account, Stewie, which has three calendars. I was able to reproduce the same pattern: everything works fine, even some basic recurring events, but when I try the every-second-Wednesday\n pattern, it stops sending me data for that calendar. I repeated the pattern for Stewie's second calendar and got the same result. Finally, on his third calendar, I tried a slight variation (every third Thursday) with the same result.<br>\n<br>\nSince this seems consistent to me, I suspect I've run into a corner case in the recurrence code. Maybe you can see more in your logs? Let me know if I can provide any further info to help debug this.<br>\n<br>\n</div>\nThank you,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Thu, May 7, 2015 at 3:11 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nylas.com\" target=\"_blank\">spang@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-size:10pt; color:#000000; background-color:#ffffff; font-family:Arial,Helvetica,sans-serif\">\n<p>Hi Andrew &amp; co,<br>\n</p>\n<p><br>\n</p>\n<p>Just checking in here. Were you able to successfully test the new recurring event support for Exchange?<br>\n</p>\n<p><br>\n</p>\n<p>regards,<br>\n</p>\n<p>Christine</p>\n<div style=\"color:rgb(0,0,0)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;<br>\n<b>Sent:</b> Sunday, May 3, 2015 5:05 PM<br>\n<b>To:</b> Kavya Joshi; Jeff Meister<br>\n<b>Cc:</b> support; Mackenzie Dallas; Karim Hamidou; Jennie Lees\n<div>\n<div><br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</div>\n</div>\n</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div>\n<div>\n<div dir=\"ltr\">Thanks so much for working round the clock on this Kavya &#43; Nylas team! This is a very big milestone for us and we're really excited to finally put this all into action. We'll get back to you as soon as possible.&nbsp;<br>\n</div>\n<br>\n<div class=\"gmail_quote\">On Fri, May 1, 2015 at 11:45 AM Kavya Joshi &lt;<a href=\"mailto:kavya@nylas.com\" target=\"_blank\">kavya@nylas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-size:12pt; color:#000000; background-color:#ffffff; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<p></p>\n<p>Hi Jeff,</p>\n<p><em></em><br>\n</p>\n<p>We just shipped basic support for recurring events for Exchange.</p>\n<p><em></em><br>\n</p>\n<p>Specifically, we now sync recurring events from the Exchange server and the API returns these events with an additional &quot;recurrence&quot; object that contains the RRULE and EXDATE strings.</p>\n<p><em></em><br>\n</p>\n<p>For example, a recurring event would be returned as:</p>\n<p>{</p>\n<p>&nbsp;&nbsp;&nbsp;&nbsp; &nbsp; …</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp;&quot;recurrence&quot;: {</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&quot;rrule&quot;: &quot;['RRULE:FREQ=WEEKLY', 'EXDATE:20150611T210000Z,20150618T210000Z']&quot;,</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&quot;timezone&quot;: null</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp;},</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp;&quot;when&quot;: {</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&quot;end_time&quot;: 1431034200,</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&quot;object&quot;: &quot;timespan&quot;,</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&quot;start_time&quot;: 1431032400</p>\n<p>&nbsp; &nbsp; &nbsp; &nbsp;}</p>\n<p>}</p>\n<p><em></em><br>\n</p>\n<p>You can also expand recurring events using:</p>\n<p><span>GET <a href=\"https://api.nylas.com/n/&amp;lt;namespace_id&amp;gt;/events?expand_recurring=true\" target=\"_blank\">\n<span>https://api.nylas.com/n/&lt;namespace_id&gt;/events?expand_recurring=true</span></a></span></p>\n<p><em></em><br>\n</p>\n<p>We don't support updating recurring events via the API yet, we'll be working on it in next week,&nbsp;but we wanted to get this out to you so you can get started!</p>\n<p><em></em><br>\n</p>\n<p>As always, let us know if you have any questions!<br>\n</p>\n<p><br>\n</p>\n<p>Cheers,</p>\n<p>Kavya<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<div style=\"color:rgb(0,0,0)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;<br>\n<b>Sent:</b> Tuesday, April 28, 2015 3:34 PM<br>\n<b>To:</b> Jennie Lees<br>\n<b>Cc:</b> Andrew Lee; support; Mackenzie Dallas; Karim Hamidou</font></div>\n</div>\n</div>\n</div>\n<div dir=\"ltr\">\n<div style=\"font-size:12pt; color:#000000; background-color:#ffffff; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<div style=\"color:rgb(0,0,0)\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</font></div>\n</div>\n</div>\n</div>\n<div dir=\"ltr\">\n<div style=\"font-size:12pt; color:#000000; background-color:#ffffff; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<div style=\"color:rgb(0,0,0)\">\n<div>\n<div dir=\"ltr\">\n<div>\n<div>\n<div>Hi Jennie,<br>\n<br>\n</div>\nThank you for the update, it all sounds good to me! Having the recurrence info in standard RRULE format would be best for us, since that's what Google Calendar supports too. It would allow us to simply copy that data over to a Google event (unless you know\n of recurrence features that we may get from Exchange events through Nylas that Google won't support). We may use the expansion feature as well, but I think an RRULE is what we'll want in most cases.<br>\n<br>\n</div>\nThanks,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Tue, Apr 28, 2015 at 11:00 AM, Jennie Lees <span dir=\"ltr\">\n&lt;<a href=\"mailto:jennie@nylas.com\" target=\"_blank\">jennie@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div dir=\"ltr\" style=\"font-size:12pt; color:rgb(0,0,0); font-family:Calibri,Arial,Helvetica,sans-serif,'Apple Color Emoji','Segoe UI Emoji',NotoColorEmoji,'Segoe UI Symbol','Android Emoji',EmojiSymbols; background-color:rgb(255,255,255)\">\n<p><br>\n</p>\nHey Jeff,\n<div><br>\n</div>\n<div>I wanted to give you a quick update on recurring events for Exchange while Christine's out.&nbsp;</div>\n<div><br>\n</div>\n<div>We're implementing them using the same approach we took for Google Calendar, including converting the Exchange recurrence info into a standard RRULE, which will be accessible on the event object via our API. However, we've found some nuances in the way\n Exchange treats exceptions to the rule, so we're currently testing that thoroughly and making sure it lines up with our overall handling of events. You'll be able to access all individual instances of a recurring event without having to do the expansion yourselves,\n and/or get the recurrence info from the parent.</div>\n<div><br>\n</div>\n<div>If this doesn't sound like it's going in the right direction for you, let me know!</div>\n<div><br>\n</div>\n<div>Jennie<br>\n<br>\n<div style=\"color:rgb(0,0,0)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Christine Spang<br>\n<b>Sent:</b> Friday, April 24, 2015 3:52 PM\n<div>\n<div><br>\n<b>To:</b> Jeff Meister<br>\n<b>Cc:</b> Andrew Lee; Kavya Joshi; support; Mackenzie Dallas<br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</div>\n</div>\n</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div>\n<div>\n<div style=\"font-size:10pt; color:#000000; background-color:#ffffff; font-family:Arial,Helvetica,sans-serif\">\n<p>Good to hear things are working for you! :)<br>\n</p>\n<p><br>\n</p>\n<p>The &quot;cancelled events&quot; change was indeed intentional—we want the behaviour to match. This was part of some cleanups and bugfixing we did surrounding cancelled events.<br>\n</p>\n<p><br>\n</p>\n<p>We're going to&nbsp;work on implementing&nbsp;read-only&nbsp;Exchange support for recurring events next week. I think your use case is pretty straightforward, but we'll let you know if any questions come up that you folks could help us out with.<br>\n</p>\n<p><br>\n</p>\n<p>I'll be out of town next week&nbsp;but as long as you keep support@ in the loop someone from our side will keep you posted.<br>\n</p>\n<p><br>\n</p>\n<p>cheers,<br>\n</p>\n<p>Christine<br>\n</p>\n<p><br>\n</p>\n<div style=\"color:rgb(33,33,33)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;<br>\n<b>Sent:</b> Friday, April 24, 2015 3:30 PM<br>\n<b>To:</b> Christine Spang<br>\n<b>Cc:</b> Andrew Lee; Kavya Joshi; support; Mackenzie Dallas<br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>\n<div>Hi Christine,<br>\n<br>\n</div>\nI reset the account on our end too and started the Nylas syncing again. Everything looks good so far! I haven't seen the duplicate issue today. Since it happened unpredictably before, I'll continue testing with this customer's account as well as others, just\n to make sure it doesn't come back.<br>\n<br>\n</div>\nI noticed a difference in my Nylas delta that isn't a bug but I thought was worth pointing out. When I delete an event on the O365 calendar, I used to see a delete in the Nylas delta. But in today's testing, instead of deletes, I saw updates with the status\n of the event set to &quot;cancelled&quot;. This is actually the same behavior as in Google's calendar API, and it's not a problem for us, just wanted to let you know about it and see if it was an intentional change on your end (or maybe O365 changed what they send you).<br>\n<br>\n</div>\nLet me know when you have some thoughts on recurring events. I'm happy to discuss our use case over the phone if that's better for you.<br>\n<br>\n</div>\nThanks again,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Thu, Apr 23, 2015 at 2:38 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nylas.com\" target=\"_blank\">spang@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-size:12pt; color:#000000; background-color:#ffffff; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<p>Hi again,<br>\n</p>\n<p><br>\n</p>\n<p>We just shipped this fix to production and reset the affected account. This particular bug only occurs when the Exchange server encounters a particular condition, which is why it was both different from the earlier issue and doesn't affect all accounts.<br>\n</p>\n<p><br>\n</p>\n<p>Can you confirm that events look good for this account now?<br>\n</p>\n<p><br>\n</p>\n<p>Hold tight on recurring events for Exchange. We're still figuring out our gameplan there.<br>\n</p>\n<p>--Christine<br>\n</p>\n<div dir=\"ltr\" style=\"color:rgb(33,33,33)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Christine Spang<br>\n<b>Sent:</b> Wednesday, April 22, 2015 4:17 PM<br>\n<b>To:</b> Jeff Meister\n<div>\n<div><br>\n<b>Cc:</b> Andrew Lee; Kavya Joshi; support; Mackenzie Dallas<br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</div>\n</div>\n</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div>\n<div>\n<div style=\"font-size:12pt; color:#000000; background-color:#ffffff; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<p>Hi Jeff,<br>\n</p>\n<p><br>\n</p>\n<p>Sorry for the delay here.<br>\n</p>\n<p><br>\n</p>\n<p>We have a fix for this duplicate events issue on our staging environment right now. We're planning to ship it to production tomorrow morning. Will let you know when this is done.<br>\n</p>\n<p><br>\n</p>\n<p>We're figuring out the best gameplan for Exchange recurring events. I'll let you know when we have more details.<br>\n</p>\n<p>--Christine<br>\n</p>\n<div style=\"color:rgb(33,33,33)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;<br>\n<b>Sent:</b> Monday, April 20, 2015 6:19 PM<br>\n<b>To:</b> Christine Spang<br>\n<b>Cc:</b> Andrew Lee; Kavya Joshi; support; Mackenzie Dallas<br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div dir=\"ltr\">\n<div>\n<div>Hi Christine,<br>\n<br>\n</div>\n<div>Glad you guys are back, hope you had a good retreat!<br>\n</div>\n<div><br>\nRegarding recurring events, we can try expanding but I'm not sure if that will work in our case, unless I can make some changes to avoid us syncing these expanded copies back to the Exchange calendar. What would be better is if we could see the recurrence rules\n on Exchange events. I think we could deal with that being read-only to start with, we just need an RRULE (or something we can translate into one) for our mirrored calendar on Google. I wrote some more details in my April 6 reply to this thread, explaining\n what we're doing here. Let me know if you have other questions about this.<br>\n<br>\n</div>\nThanks,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Mon, Apr 20, 2015 at 5:48 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nylas.com\" target=\"_blank\">spang@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-size:12pt; color:#000000; background-color:#ffffff; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<p>Hi Andrew,<br>\n</p>\n<p><br>\n</p>\n<p>We just got back from retreat today and looking into this was on hold last week while we did some longer-term planning.<br>\n</p>\n<p><br>\n</p>\n<p>Karim is going to dive back into resolving the duplicate events issue now. We have a hunch as to what is up and will keep you posted.<br>\n</p>\n<p><br>\n</p>\n<p>To be clear, the recurring events issue is that recurring events expansion isn't supported on Exchange yet?<br>\n</p>\n<p><br>\nThanks a ton for your patience.<br>\n</p>\n<p>--Christine<br>\n</p>\n<div style=\"color:rgb(33,33,33)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri,sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;<br>\n<b>Sent:</b> Monday, April 20, 2015 5:29 PM<br>\n<b>To:</b> Kavya Joshi; support<br>\n<b>Cc:</b> Jeff Meister; Mackenzie Dallas; Christine Spang<span><br>\n<b>Subject:</b> Re: Esper &lt;-&gt; Nilas</span></font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div>\n<div>\n<div dir=\"ltr\">Just realized that this isn't exactly the last issue, it's this duplicate events issue and the recurring events issue. But once we have those two - we have users chomping at the bit!\n<div><br>\n</div>\n<div>Please let us know if there is anything we can help with. -Andrew<br>\n<br>\n<div class=\"gmail_quote\">On Mon, Apr 20, 2015 at 9:39 AM Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hey Kavya, any update on this. This is the LAST issue for us and we're really excited to do a launch so please let us know where we are here.<br>\n</div>\n<br>\n<div class=\"gmail_quote\">On Tue, Apr 14, 2015 at 9:35 PM Kavya Joshi &lt;<a href=\"mailto:kavya@nilas.com\" target=\"_blank\">kavya@nilas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hi Jeff, Andrew\n<div><br>\n</div>\n<div>We've identified the cause of the duplicate events - the sync engine uses&nbsp;</div>\n<div>Exchange server-assigned identifiers to identify events that were previously synced versus new events that need to be synced.</div>\n<div>In the case of the duplicated events, we received the same event with a different server-assigned ID. As a result, the sync engine created a new event object with a different Nilas API event ID.</div>\n<div>\n<p>We're currently looking into what causes the Exchange server to return different IDs for the same event so we can implement a fix - we'll keep you posted.</p>\n</div>\n<div>Thanks!</div>\n</div>\n<div dir=\"ltr\">\n<div>Kavya</div>\n<div><br>\n</div>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Tue, Apr 14, 2015 at 11:30 AM, Andrew Lee <span dir=\"ltr\">\n&lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hope you guys had a great retreat! Just wanted to check up on the status of this. Happy to jump on a call too! -Andrew\n<div>\n<div><br>\n<br>\n<div class=\"gmail_quote\">On Tue, Apr 7, 2015 at 4:44 PM Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>Hi again,<br>\n<br>\n</div>\nIn testing today with a real customer's calendar, I ran into the duplicate events issue again. Not all events are affected, but a significant amount of them are. I'm not sure what, if anything, they all have in common... maybe they were modified at some point\n after their creation on the Exchange side?<br>\n<br>\n</div>\n<div>In more detail: I noticed this when doing an initial copy of events from Nilas for this user. At this stage, we aren't using the delta sync API; we start out by making a request for the next 10 weeks' worth of events on the chosen calendar. This request\n returned 69 events, and I noticed that some were very similar, so I ran a little script that checked for pairs of events that are identical except for their event ID. It found 16 such pairs.<br>\n<br>\n</div>\n<div>Since I hadn't yet hooked up the code to write changes on our end back to Nilas for this user, and I don't see any of these duplicates on his actual Exchange calendar, I suspect a Nilas issue... in the past you fixed a similar bug in the delta sync caused\n by changing IDs in Exchange, maybe that's where the problem is?<br>\n<br>\n</div>\n<div>The Nilas namespace ID of this user is 2ncnab6pkwjmckn6i2niknkr4<br>\n</div>\n<div>The calendar ID for all these events is d6hg8h7c57bx926toavj6tc9q<br>\n<br>\n</div>\n<div>Here are the pairs of events that have identical data but different event IDs:<br>\n5rc0a29m3j8aw39uln228uyv and enn1w0w1x862t39upeqnhde1e <br>\n8e3axp1rtqon780xq3f2x242t and 6ov6kvsqswqrthru9ktnnvnlo <br>\nfw39ttmbnqfttppvh9au9m35 and dvhdce880pbbpw5e0xmrp7iai<br>\nd7pekrqot18ksbv76zjru0yzo and 2wqkb0ebzzzvmm5hl2dxuiymc <br>\n2wqkb0ebzzzvmm5hl2dxuiymc and d7pekrqot18ksbv76zjru0yzo <br>\n6jmiyxn5px913zw1havpfer7s and cft5si4ac6gsujouv87275qn0 <br>\n790x1c6omswe018o0om5uvtlh and 4apbs7rhfz7xjwkc5l0f2qxtn <br>\nev25fsxy2mm6wvdyn8ool0fs2 and 6f92dcpaeh4aheygi0px7e259 <br>\nc1ktorpee2wtjowfn9ushwtci and 8uy25k4r7ug6qwmn29upx06qf <br>\nbf836ol0p8dbmq66pd342m3ij and co9aehxq3c8g1ugidfit76up4 <br>\n249ifo6pxjhdeqeqlknylfnkk and lcf6f4pjbhzrr11awkrjw6dn <br>\n6rys9s4ag4sanho5lazglcjw9 and 9j7i0t664rkp5lcwofcs9g55m <br>\n2u2sltush4ij0d4x502gz0zw8 and 6qw8hjuq2bxhxsv34w477p3r8 <br>\n47g1o8rpoejolplbqj9pyug9t and abpf9p2eeifrbksgy1432x82s <br>\n87dks93wfcnsdoha8c6kpykf4 and 5bjnykg45fajv6x1f75wuvhay <br>\n2oj9t3gz0wyz4ir4rjpm7ba2k and 67ruyrgk9iq9kiqshboh43j3h<br>\n<br>\n</div>\n<div>Let me know if I can provide anything else that would help.<br>\n<br>\n</div>\n<div>Thank you!<br>\n</div>\n</div>\n<div dir=\"ltr\">\n<div>Jeff<br>\n</div>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Mon, Apr 6, 2015 at 2:42 PM, Jeff Meister <span dir=\"ltr\">\n&lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>\n<div>Hi Christine,<br>\n<br>\n</div>\nI've been waiting for accounts to sync before using them with Esper due to the way we implemented things, which involves an initial sync of some calendar data and then uses your delta sync API thereafter. If we change it to use the delta for everything, and\n Nilas sends us info as it comes in, that would probably work too. So I don't think this one is an issue on your end really, except that if I don't wait for the sync, there is significant delay before my actions on Nilas show up on the Exchange calendar. They\n eventually do, so I figured the delay was due to the sync still running. But if I wait a while before using Nilas, then everything is fast.<br>\n<br>\n</div>\nI spoke with our assistants, and it sounds like recurring events are pretty common on our customers' calendars. The reason I brought up Google, even though we're talking about Exchange calendars here, is because of how our assistant software works. Basically,\n our assistants schedule in Gmail and Google Calendar, with our software providing extra capabilities inside those interfaces. To manage an executive's Google calendar, he can simply share it with us directly. But for Exchange, the executive logs in to Nilas\n which syncs his calendar, then our backend fetches the data from Nilas and populates a corresponding Google calendar for the assistant to interact with. Then we watch the Google and Nilas calendars for new data to sync between them. So, if our assistant creates\n a recurring event, the RRULE will be specified in Google's format, and we must translate it to a format that Nilas can handle for an Exchange calendar. That's why I was wondering about compatibility. We're taking your sync and doing another sync with it!<br>\n<br>\n</div>\n<div>I think it's rare for an assistant to need to create a recurring event, and we could do that directly on Exchange for the time being... the main issue is how to present existing recurring Exchange events on Google. Without the recurrence info, we'd only\n get a single event on our Google calendar, and the assistant wouldn't know that some open time on the calendar is actually taken up by the recurrences. I see in your API docs that there is an option to expand recurring events, which might work, but if our\n assistant edits a recurring event, we will make an improper change to the Exchange calendar, probably creating a duplicate. (Our system assumes that anything on the Google calendar belongs on the corresponding Exchange calendar, and vice versa, so it will\n try to propagate the expanded event back to Nilas, although perhaps we can special case this.)<br>\n<br>\n</div>\n<div>Let me know how long you think it would take to get recurrence support for Exchange, if possible in the RRULE format that Google and others use, or something translatable to it. If it helps, I think we'd be OK with read-only support for a while, but without\n that I think this may be a blocker for us, unless I can figure something out with expanded events.<br>\n<br>\n</div>\n<div>As for timezone to fix all-day events, we do collect the timezone of our customers already, so we can supply that. For specifying the timezone, we use the IANA zone names (like America/Los_Angeles) as in the tz database. Users prefer the abbreviations,\n but we've found this easier for machines, since abbreviations can be ambiguous and they require knowing about daylight savings (PST vs. PDT).<br>\n<br>\n</div>\n<div>Thanks again,<br>\n</div>\n<div>Jeff<br>\n</div>\n</div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Thu, Apr 2, 2015 at 6:41 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\nFor (2), we're also considering making a best guess on the user's timezone based on the timezone of the last event on the user's calendar, or a couple previous events. It requires a bit more thought. Let me know if you have any thoughts. (In general, we'd like\n to avoid complicating the API with provider-specific parameters that make it so you can't specify the same event the same way across providers.)\n<div>\n<div><br>\n<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\nOn Apr 2 2015, at 6:03 pm, Christine Spang &lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt; wrote:\n<br>\nHi Jeff,\n<div><br>\n</div>\n<div>The state machine for our Exchange syncs isn't being interpreted correctly by our developer console dashboard—we've created a ticket in our bug tracker to make sure we fix this.</div>\n<div><br>\n</div>\n<div>Do you need an account to be fully synced before the user can start using Esper? Were you seeing problems before a day or so had passed? We've designed our sync algorithm to be usable from the time an account is connected, even if all data is not yet synced,\n so I'm curious to hear if you were running into specific issues.</div>\n<div><br>\n</div>\n<div>Here are answers to your other questions:</div>\n<div>1. We have complete read-only support on Google calendars, but not for Exchange yet. Exchange uses a different format from RRULEs, and we haven't implemented the expansion yet. This is exposed via the `recurrence` property on events, but we haven't done\n much testing so we're not sure if it's useful to end-users in its current state. (If you take a look at it at all, let us know what you find out.)</div>\n<div><br>\n</div>\n<div>Do you need this? If so, we can add it to our roadmap. We've planned to do it, but it hasn't been a blocker for anyone yet so we've been prioritizing other features.</div>\n<div><br>\n</div>\n<div>2. As Kavya explained previously, this is a fundamental limitation of how Exchange implements calendars that can only be solved by manually specifying the user's timezone correctly when creating an all-day event. We can't reliably determine this from a\n user's account, so to fix this properly you'd have to be able to send our API the user's timezone. (On Exchange, &quot;all day&quot; events are treated as a 24-hour timespan, so if we send the event in UTC, and the user's calendar is set to display in PST... bam, display\n issue.)</div>\n<div><br>\n</div>\n<div>If you can get this timezone information, would this API modification to how &quot;when&quot; blocks work for Exchange event creation solve your problem?</div>\n<div><span style=\"font-size:15.9px; line-height:1.4; background-color:inherit\"><br>\n</span></div>\n<div><span style=\"font-size:15.9px; line-height:1.4; background-color:inherit\">&quot;when&quot; : {</span><br>\n</div>\n<div>&nbsp; &nbsp; &quot;date&quot;: &quot;2015-04-15&quot;,</div>\n<div>&nbsp; &nbsp; &quot;timezone&quot;: &quot;PST&quot;<br>\n},</div>\n<div><br>\n</div>\n<div>(Still thinking about the best way to specify timezones, but you get the gist of it.)</div>\n<div><br>\n</div>\n<div>If so, this is not too difficult to implement on our end and we can have it to you by next week sometime. Let us know.</div>\n<div><br>\n</div>\n--Christine<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\nOn Apr 2 2015, at 3:46 pm, Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt; wrote:\n<br>\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>Hi everyone,<br>\n<br>\n</div>\nWe decided to try one more testing account and have an assistant play with it before adding the real customer. This account sees daily use, so it's a more realistic test case than my fake accounts. The initial sync to Nilas took a while, but it seems to have\n stabilized now, and events are syncing in both directions... looking good! We're going to have our real customer sign into Nilas and give his account some time to sync up (a day or so?) before using it. I noticed on my Nilas dashboard that our latest test\n account sometimes says &quot;Sync Issue&quot; or &quot;Downloading&quot;, but eventually it goes back to &quot;Running&quot;.<br>\n<br>\n</div>\nA couple minor questions for you:<br>\n<br>\n</div>\n1. One thing I forgot to test was recurring events. It looks like you and Google use the same strategy for specifying recurring events: just embed the RRULE as a string in RFC format. I'm not too familiar with this part of iCalendar, so I'll have to read some\n documentation, but I'm wondering if you think it will work to just copy RRULEs between Google and Nilas. Are you aware of any incompatibilities in how this spec is implemented? I'll keep looking into it myself, just wanted to know if there are any gotchas\n I should be prepared for here. Recurrences get pretty complicated.<br>\n<br>\n</div>\n2. I tried specifying an all day event with just a single date field as Kavya explained, and Nilas accepted this, but I'm still seeing the event span two days in O365: my all day 4/10 event covers both 4/9 and 4/10. It's consistently going back one day for\n me... maybe this is related to the timezone issue Kavya also mentioned? My Nilas API responses just have dates, but if 4/10 is being interpreted in PST, that might be subtracting some hours and getting us to 4/9. Not sure though.<br>\n<br>\n</div>\nI'll let you know if we run into problems with our customer's account, which is rather large. I'm optimistic, though!<br>\n<br>\n</div>\nThanks,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Tue, Mar 31, 2015 at 2:35 PM, Andrew Lee <span dir=\"ltr\">\n&lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Yep. By the end of the week (barring crazy things happening), we should be testing with some current O365 executives and we'll let you know if we experience any problems.&nbsp;<br>\n</div>\n<div>\n<div><br>\n<div class=\"gmail_quote\">On Tue, Mar 31, 2015 at 2:07 PM Christine Spang &lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Per our conversation today:\n<div><br>\n</div>\n<div>You folks are going to start onboarding Exchange users, having them directly auth to Nilas instead of using Exchange shared calendars.</div>\n<div><br>\n</div>\n<div>We're going to scope&#43;spec application scopes (specifically for calendars vs mail) and work implementation into our roadmap. We have a big conference&#43;company offsite coming up in the next two weeks, so are not sure about the timing for the project right\n now. In the meantime, you can get started without anything specific from us.</div>\n<div><br>\n</div>\n<div>As mentioned on the call, we're right in the middle of an improvement project for our Exchange auth flow, which includes providing better error messages when things go wrong and creating some better documentation about what each screen of the auth flow\n looks like on our end. Let us know if you have any questions before that's live, and if you have any trouble with Exchange sign-ins, we're available to help figure out what's going wrong.</div>\n<div><br>\n</div>\n<div>cheers,</div>\n<div>Christine</div>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Mon, Mar 30, 2015 at 6:00 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<p dir=\"ltr\">Great, talk to you tomorrow. :)</p>\n<div>\n<div>\n<div class=\"gmail_quote\">On Mar 30, 2015 5:48 PM, &quot;Andrew Lee&quot; &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt; wrote:<br type=\"attribution\">\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Yeah, that's fine. Let's jump on the phone then. Blake will follow up with calendar invites etc.<br>\n<br>\n<div class=\"gmail_quote\">On Mon, Mar 30, 2015 at 5:46 PM Christine Spang &lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Does 1pm tomorrow work for a call?</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Mon, Mar 30, 2015 at 5:28 PM, Andrew Lee <span dir=\"ltr\">\n&lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Thanks so much for the e-mail! Christin, on Shared Calendars, we're happy to jump on the phone today or tomorrow to come up with a solution. This is very important to us and we'd like to get a resolution soon. -A<br>\n</div>\n<div>\n<div><br>\n<div class=\"gmail_quote\">On Mon, Mar 30, 2015 at 12:48 PM Kavya Joshi &lt;<a href=\"mailto:kavya@nilas.com\" target=\"_blank\">kavya@nilas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hi Jeff,\n<div><br>\n</div>\n<div>Thanks for the detailed response!</div>\n<div><br>\n</div>\n<div>With respect to all-day event creation, there are a couple of issues:</div>\n<div><br>\n</div>\n1. The display of all-day events depends on the timezone the calendar is set to in O365 (Calendar settings &lt; Options &lt; General &lt; Region and timezone):\n<div><a href=\"http://support.microsoft.com/en-us/kb/262451\" target=\"_blank\">http://support.microsoft.com/en-us/kb/262451</a><br>\n</div>\n<div><br>\n<div>Since the `when` field for event creation using the Nilas API is specified in the UTC timezone and is sent to the Exchange server in this timezone too, if your O365 calendar is set to the UTC timezone, it will display as expected. However, if your calendar\n is set to a different timezone (for e.g. UTC-8), the event will display incorrectly. The same behavior is observed if you create an all-day event in the O365 UI and then change the timezone in the options.</div>\n<div>\n<div><br>\n</div>\n<div>To overcome the timezone-dependency of the display, we plan to allow users to specify timezone information as well while creating events - we'll keep you updated on it.</div>\n<div>\n<div><br>\n</div>\n<div>2. There was a small error in the end-dates we were sending the server for all-day events, which caused the incorrect event durations. The fix for this is now live, so this should not be an issue anymore - please let us know if it is!</div>\n<div><br>\n</div>\n<div><br>\n</div>\n<div>To answer your question about date ranges for events -</div>\n<div>* to create an all-day event on a single day using the API, provide a date object for the `when` field.&nbsp;</div>\n<div>For example, to create an all-day event on 04/11/2015:</div>\n<div>\n<div>when = {</div>\n<div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;date&quot;: &quot;2015-04-11&quot;</div>\n<div>}</div>\n</div>\n<div><br>\n</div>\n<div>* to create an all-day event spanning multiple days, provide a datespan object for the `when` field. In this case, the end-date is /inclusive/.</div>\n<div>For example, to create an event that runs all-day on the 11th, 12th, 13th and 14th (inclusive):&nbsp;</div>\n<div>\n<div>when = {</div>\n<div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;start_date&quot;: &quot;2015-04-11&quot;,</div>\n<div>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &quot;end_date&quot;: &quot;2015-04-14&quot;</div>\n<div>}</div>\n</div>\n<div><br>\n</div>\n<div>Does that help?</div>\n<div><br>\n</div>\n<div>With respect to shared calendars, Christine will be in touch.</div>\n<div><br>\n</div>\n<div>Thanks!</div>\n</div>\n</div>\n</div>\n</div>\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>Kavya</div>\n<div>\n<div><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Thu, Mar 26, 2015 at 3:07 PM, Jeff Meister <span dir=\"ltr\">\n&lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>On the topic of shared calendars, let me explain our use case. Esper employs some in-house assistants to handle scheduling for our customers, who are mostly busy executives. Our assistants use custom software that is built on top of Gmail and Google Calendar,\n both frontend (browser extension to enhance the interface) and backend (server to automate various workflows using Google APIs). Each assistant logs into a Google account, and our customers share their calendars from their own private Google account to ours.\n This lets our assistants see free/busy times for the executives and create appropriate events for them, without requiring the executive to give us his private login information.<br>\n<br>\n</div>\nWe would like to support executives who use Exchange calendars, but we're trying to avoid replicating our frontend work on top of O365 or desktop Outlook, and we don't want to deal with the Exchange APIs either. Our plan was to have Exchange executives share\n their calendars with our assistant's own Exchange account (often created by the executive's organization for our use). Then, we would authenticate our assistant's account with Nilas, and our backend would use your API instead of Microsoft's. For the frontend,\n we have some custom code that mirrors the Nilas data over to a Google calendar where our assistant can use our software.<br>\n<br>\n</div>\nI tried checking my test O365 account that had some shared calendars with my iPhone like you suggested, and indeed I did not see the shared calendars... only the owned calendars showed up. Unless there's an undocumented part of EAS that Apple isn't aware of,\n I guess we may not be able to access shared calendars through that protocol, as you said. The shared calendars do show up on the O365 interface, and everything works there as you'd expect, but since that's a cloud service I have no idea what (likely private)\n API they are using. If I can get another non-O365 Exchange account, I'll try some sharing tests there too. However, the customer we would like to onboard first does use O365.<br>\n<br>\n</div>\nIf we can't get shared calendars through EAS or another protocol, we may have to rethink how we're doing things. I thought of sharing the calendar in the other direction, where our assistant creates it on her account and shares it with the executive, but then\n we still wouldn't be able to see free/busy times on the executive's other calendars, and they probably won't want to use our calendar exclusively. The only other thought I'm left with is to have the executive sign in to Nilas with his own account. But that\n just pushes the privacy issue up the chain; while you could restrict our access through your API, our customers probably don't want a third party having access to all their email etc. Does Exchange have some access control capabilities that could help here?\n Maybe there is a way for our customers to sign in to Nilas with their own accounts, but then choose only to share their calendars with Nilas, kind of like how Google's OAuth domains work?<br>\n<br>\n</div>\nSorry for the long email, I'm just trying to be clear. I should have noticed this issue before, but I had been testing with owned calendars figuring that shared calendars would work the same, as they do in Google. That's what I get for assuming EAS would be\n cooperative! If you can think of an alternative approach, we'd be very happy to hear it.<br>\n<br>\n</div>\nThanks again, and let me know if you need any more info or clarification.<span><font color=\"#888888\"><br>\n</font></span></div>\n<span><font color=\"#888888\">- Jeff<br>\n</font></span></div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Thu, Mar 26, 2015 at 12:40 PM, Jeff Meister <span dir=\"ltr\">\n&lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>Hi Kavya and others,<br>\n<br>\n</div>\nI just tested the all-day events again, and the problem of showing up at 4 or 5pm is gone, but I'm still having some issues that I think are related to whether the end date is exclusive or inclusive. When I request an all-day event through the Google Calendar\n API, say on 4/2, I get back an event with a start date of 4/2 and end date of 4/3. I was assuming that Nilas worked the same way, with exclusive end times/dates (i.e., just beyond the end of the time range).<br>\n<br>\n</div>\nWhen I create an event on Nilas with a start date of 4/2 and end date of 4/3, it appears on the O365 calendar across two days: 4/1 and 4/2. Opening its details confirms that it has those start and end dates, even though my Nilas API response said 4/2 to 4/3.\n Strangely, the sidebar of O365 says &quot;1 day&quot; for the event duration. To make it appear only across 4/2, I had to go into the event on O365 and set both start and end dates to 4/2.<br>\n<br>\n</div>\nSo, I tried creating a Nilas event with start and end dates both set to 4/2. This did result in a single-day event in O365, but it was on 4/1 instead of 4/2. The sidebar also says &quot;0 minutes&quot;, even though everything looks fine on the calendar. I noticed that\n my Nilas API response contained a when field with a single date 4/2 instead of a datespan, so I also tried creating my event this way, but the result on O365 was the same as the 4/2 to 4/2 datespan.<br>\n<br>\n</div>\nIt seems like there is some confusion between me, Nilas, and O365 about whether an end time or date is part of the range. How should I be specifying my ranges to Nilas? And could this account for the off-by-one behavior I'm seeing on my O365 calendar?<br>\n<div>\n<div>\n<div>\n<div><br>\n</div>\n<div>I'll write a different response regarding shared calendars, because this email is already a bit long.<br>\n<br>\n</div>\n<div>Thank you,<br>\n</div>\n<div>Jeff<br>\n</div>\n</div>\n</div>\n</div>\n</div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Wed, Mar 25, 2015 at 9:49 PM, Kavya Joshi <span dir=\"ltr\">\n&lt;<a href=\"mailto:kavya@nilas.com\" target=\"_blank\">kavya@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hi Andrew, Jeff\n<div><br>\n</div>\n<div>At first glance, it seems like the ActiveSync protocol does not support syncing shared calendars. We're going to double check this with a mobile phone, since sometimes ActiveSync features are undocumented.&nbsp;</div>\n<div>In the meantime, could you please tell us how you plan to use synced shared calendars? We don't want this to be a blocker for you and might be able to suggest an alternative approach.</div>\n<div><br>\n</div>\n<div><span style=\"font-size:12.8000001907349px\">W.r.t&nbsp;</span><span style=\"font-size:12.8000001907349px\">creating all-day events through Nilas, d</span><span style=\"font-size:12.8000001907349px\">id you have the chance to test it again?</span></div>\n<div><span style=\"font-size:12.8000001907349px\">From the logs, it looks like your prior testing for the following accounts&nbsp;</span><span style=\"font-size:12.8000001907349px\">might've occurred before the fix was deployed:</span></div>\n<div><span style=\"font-size:12.8000001907349px\">meg@</span><span style=\"font-size:12.8000001907349px\"><a href=\"http://espertech.onmicrosoft.com\" target=\"_blank\">espertech.onmicrosoft.com</a>,</span><span style=\"font-size:12.8000001907349px\">&nbsp;</span></div>\n<div><span style=\"font-size:12.8000001907349px\">stewiegriffin@</span><span style=\"font-size:12.8000001907349px\"><a href=\"http://espertech.onmicrosoft.com\" target=\"_blank\">espertech.onmicrosoft.com</a></span></div>\n<div><span style=\"font-size:12.8000001907349px\"><br>\n</span></div>\n<div><span style=\"font-size:12.8000001907349px\">Thanks!</span></div>\n<span><font color=\"#888888\">\n<div>Kavya</div>\n<div><br>\n</div>\n</font></span></div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Wed, Mar 25, 2015 at 4:26 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hi Andrew,\n<div><br>\n</div>\n<div>Kavya's taking a look at this today. We'll get back as soon as we have more info.</div>\n<span><font color=\"#888888\">\n<div><br>\n</div>\n<div>--Christine</div>\n</font></span></div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Wed, Mar 25, 2015 at 9:35 AM, Andrew Lee <span dir=\"ltr\">\n&lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hey guys, just wanted to check up on what you think about this issue. I'm really hoping it's a small issue because once this is fixed, we're gonna start on-boarding people.<br>\n</div>\n<div>\n<div><br>\n<div class=\"gmail_quote\">On Mon, Mar 23, 2015 at 5:01 PM Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>\n<div>Hello again,<br>\n<br>\n</div>\nAndrew brought another issue to my attention: some of the calendars we use are not actually owned by the account we log into. They are owned by a different account and shared with us. On the O365 interface, these calendars appear under &quot;Other calendars&quot; instead\n of &quot;My calendars&quot;, similar to Google's behavior. I tried sharing a couple calendars between test accounts, and they show up in O365, but I haven't seen them appear in Nilas. I don't think it's a sync delay issue, because I created another owned calendar and\n that one showed up in Nilas immediately. Are you folks able to access and sync shared O365 (or other Exchange) calendars? I'm hoping this is simply a matter of checking an extra data field.<br>\n<br>\n</div>\nThanks,<br>\n</div>\nJeff<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Mon, Mar 23, 2015 at 2:53 PM, Jeff Meister <span dir=\"ltr\">\n&lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>Hi Christine,<br>\n<br>\n</div>\nI was just about to send an email with our latest results. From my testing this morning, I see that deletes and guests are working, so the issues (1) and (2) from my previous email are fixed! Things are looking pretty good today, I'm seeing creates/updates/deletes\n sync in both directions.<br>\n<br>\nOne thing maybe worth mentioning: when I began testing this morning, at first I thought Nilas wasn't syncing to O365, because my actions weren't going all the way through to the O365 side even though my Nilas API calls were successful. However, when I checked\n back around 30min later, everything had showed up. I presume you have to queue up actions to send to the real Exchange server, but usually my Nilas interactions sync back within seconds (except deletes sometimes take a bit longer). Perhaps your reset of my\n test accounts made them require a bit more background syncing to catch up to the point where my actions would go through? Anyway, it's working more quickly now, I'm just curious what we should expect when dealing with real customer calendars (which will be\n larger than my testing ones). Should we give a new customer's Nilas account some time to sync up with their Exchange server before trying to use it further?<br>\n<br>\nAs for all-day events, the ones I created through O365 are showing up properly on the Nilas side, but I'm still seeing all-day events show up at improper times on my O365 calendar when I add them through Nilas. Maybe your deploy happened after I tested that,\n so I'll keep trying this afternoon and let you know how it goes.<br>\n<br>\n</div>\nStill no luck with Andrew's Exchange account, same error as before. I'm not sure what's up with it. If I can get another standalone Exchange account to test with, I'll report my findings there.<br>\n<br>\n</div>\nThank you,<br>\n</div>\nJeff<br>\n</div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Mon, Mar 23, 2015 at 2:34 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Heya folks,\n<div><br>\n</div>\n<div>We hadn't implemented support for the &quot;All Day&quot; bit for Exchange events. We just shipped a patch this morning that implements this support. Does this fix your issue?</div>\n<div><br>\n</div>\n<div>Has anything else come up?</div>\n<div><br>\n</div>\n<div>Let us know.</div>\n<span><font color=\"#888888\">\n<div>--Christine</div>\n</font></span></div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Fri, Mar 20, 2015 at 6:58 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>Eben scrutinized our logs about the Exchange issue, and as far as we can tell, the Exchange server may have repeatedly encountered an internal server error trying to service the auth request. We haven't seen this before, but as best we can tell there's\n not much we can do in this case, other than have you try again to auth later. Does it work for you now?</div>\n<div><br>\n</div>\n<div>We'll keep in mind that this can happen as we're working on improving the feedback in the Exchange auth flow.</div>\n<div><br>\n</div>\n<div>Karim's worked up a fix for all day events as well, which we'll ship to production as soon as it's not Friday night. :)</div>\n<div><br>\n</div>\n<div>Have a great weekend,</div>\n<div>Christine</div>\n</div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Fri, Mar 20, 2015 at 1:41 PM, Christine Spang <span dir=\"ltr\">\n&lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\nHey Andrew,\n<div><br>\n</div>\n<div>He sure did. Shipped with a regression test this time, and I reset the calendar syncs for your Exchange accounts so this should be back-applied to existing events as well.<br>\n<br>\n</div>\n<div>Eben's going to trawl the logs to look at what happened with your attempt to reauth your college account today as well, and Karim reports that it's not too difficult to fix the issue with all day events and Exchange either, so hopefully we'll have that\n fixed on production for you folks next week.</div>\n<div><br>\n</div>\n<div>Excited that you can start testing with a customer! Let us know what we can do to support.</div>\n<span><font color=\"#888888\">\n<div>--Christine</div>\n</font></span>\n<div>\n<div>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\nOn Mar 20 2015, at 11:54 am, Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\" target=\"_blank\">andrew@esper.com</a>&gt; wrote:\n<br>\n<div dir=\"ltr\">Hi Christine,<br>\n<br>\n<div>Just wanted to check on whether Karim was able to come up with a fix for the second issue. We can begin testing if you guys have a sense as to what's happening there.</div>\n<div><br>\n</div>\n<div>-Andrew</div>\n</div>\n<br>\n<div class=\"gmail_quote\">On Tue, Mar 17, 2015 at 2:06 PM Christine Spang &lt;<a href=\"mailto:spang@nilas.com\" target=\"_blank\">spang@nilas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hi Jeff,\n<div><br>\n</div>\n<div>1. The deletes issue was due to a bad code deploy yesterday, which we have already fixed. Sounds like you were testing right around the time of the deploy!</div>\n<div><br>\n</div>\n<div>2. Karim's going to take a look at this later today.</div>\n<div><br>\n</div>\n<div>re: Andrew's school Exchange account, we'll take a look at the logs. We also have a project planned to make the Exchange sign-in process less arcane, which will make whatever is going wrong in cases like this more apparent to the user signing in.</div>\n<div><br>\n</div>\n<div>I'll see if we can take a look at the all day events issue this week as well.</div>\n</div>\n<div dir=\"ltr\">\n<div><br>\n</div>\n<div>--Christine</div>\n<div><br>\n</div>\n<div><br>\n</div>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Tue, Mar 17, 2015 at 1:01 PM, Jeff Meister <span dir=\"ltr\">\n&lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>\n<div>Hello Michael and other Nilas folks,<br>\n<br>\n</div>\nI've done some testing of your latest fixes, and I'm happy to report that the sync anomalies from before are no longer occurring! With two Office 365 test accounts, Nilas has been syncing consistently both ways, although I did run into a few bugs, listed below.\n These things were all working in the past, so hopefully they are not huge issues to fix. These two are the most important for us:<br>\n<br>\n</div>\n1. Event deletions aren't propagating from Nilas to my O365 calendar anymore. The Nilas API call to delete the event succeeds (with a response of null), and I see my delete in the delta sync, but the event remains in O365. I waited a while to see if the deletion\n was simply queued up, but it never went through.<br>\n<br>\n</div>\n2. When I add a guest on my O365 event that only has an email address (no name), the Nilas event I get has the email address in the participant name field and the email field set to null. This didn't happen in the past; I remember that guests were working both\n ways in previous tests. A guest added to Nilas shows up fine on the O365 side.<br>\n<br>\n</div>\n<div>This one is less important, since the initial customer we'll be trying out is on O365:<br>\n</div>\n<div><br>\n</div>\n- I can't sign in with Andrew's school Exchange account anymore. Maybe I'm doing something wrong, but I think I used the same login procedure that worked last time... it just tells me &quot;Sorry, an error occurred.&quot; Can you see anything in your logs for account\n<a href=\"mailto:alee07@cmc.edu\" target=\"_blank\">alee07@cmc.edu</a> that might explain more?<br>\n<br>\n</div>\nAnd finally, this one is not a regression, it's an issue I've seen before that I just wanted to bring to your attention again:<br>\n<br>\n- All-day events show up on my O365 calendars at the wrong time. Here is what I said before:<br>\n&quot;If I create an <span>all</span>-<span>day</span> <span>event</span> through Nilas, say on Thursday, then I get the expected API response with a datespan of 2015-02-12 to 2015-02-13, but the\n<span>event</span> I see on O365 is actually from 2015-02-11 4:00pm to 2015-02-12 4:00pm. My calendar is in Pacific Time, if that helps. I've only seen this happen for\n<span>all</span>-<span>day</span> <span>events</span>.&quot;<br>\n</div>\nI'm repeating this because now I'm noticing that the event is from 5pm to 5pm, and we're in DST now but weren't when I reported 4pm to 4pm before. So, I suspect the bug is related to timezone handling for all-day events.<br>\n<br>\n</div>\nLet me know if you need any more details. Thanks again!<span><font color=\"#888888\"><br>\n</font></span></div>\n<span><font color=\"#888888\">Jeff<br>\n</font></span></div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Sun, Mar 15, 2015 at 9:30 PM, Michael Grinich <span dir=\"ltr\">\n&lt;<a href=\"mailto:mg@nilas.com\" target=\"_blank\">mg@nilas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hi Jeff,\n<div><br>\n</div>\n<div>Regarding the ActiveSync protocol, I usually invoke&nbsp;Hanlon's razor. :(</div>\n<div><br>\n</div>\n<div>We *definitely* want you to have a big success with this first account! Did you find any bugs or issues this weekend? What can we do to help you roll-it out?</div>\n<div><br>\n</div>\n<div>Exciting stuff!&nbsp;</div>\n<span><font color=\"#888888\">\n<div><br>\n</div>\n<div>--Michael</div>\n<div><br>\n</div>\n<div><br>\n</div>\n<div><br>\n</div>\n</font></span></div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Fri, Mar 13, 2015 at 2:28 PM, Jeff Meister <span dir=\"ltr\">\n&lt;<a href=\"mailto:jeff@esper.com\" target=\"_blank\">jeff@esper.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div>\n<div>\n<div>Hi Christine, So the Exchange server sometimes decides to just change event IDs on you, and you have to detect this situation by comparing the data fields? Wow, that almost sounds like Microsoft is breaking compatibility for third parties intentionally.\n I'm really glad you were able to figure it out!<br>\n<br>\n</div>\nI'll be doing some more testing this afternoon and over the weekend, so I'll let you know if I run into other issues, but I think this was the last major thing blocking us. A few weeks ago, I successfully demonstrated creation, modification, and deletion of\n events being propagated both ways between Nilas and an O365 account. For whatever reason, the event ID changing didn't occur that time. And I suspect that my previous issues with occasional floods of sync data, which included duplicates of old events, were\n due to the same ID-changing issue you just fixed.<br>\n<br>\nSo, if everything looks fine with my test accounts over the weekend, I think our next step will be to try hooking up a real customer. We currently have an executive who we're serving separately with regular MS Exchange software, and our assistants would really\n like to use our own software instead, which Nilas integration will enable. But I've been worried about the possibility of bugs either messing up the executive's live calendar or confusing the assistants on our end, which is why I've been using fake test accounts.\n If the anomalies don't appear again, I'll feel more confident about hooking up this customer.<br>\n<br>\n</div>\nThank you,<br>\n</div>\nJeff<br>\n</div>\n<div>\n<div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Thu, Mar 12, 2015 at 5:40 PM, Christine Spang &lt;<a href=\"mailto:spang@nilas.com\">spang@nilas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\nHi Andrew and Jeff, I wanted to let you know that we shipped this fix to production this week as expected. If you see any more anomalies with event changes in delta sync responses, let us know. It's still possible that if you change every single part of an\n event (title, location, start time, end time), the ID could change in the API response---but modifications to a subset of attributes should retain the same ID. What are your next testing steps? I'm curious how far along you folks are, and what else may be\n blocking you. --Christine On Fri, Mar 6, 2015 at 8:30 PM, Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\">andrew@esper.com</a>&gt; wrote: Thanks so much for the update Christine! On Fri, Mar 6, 2015 at 7:51 PM Christine Spang &lt;<a href=\"mailto:spang@nilas.com\">spang@nilas.com</a>&gt;\n wrote: Hi Andrew, We shipped this fix to our staging environment today, and expect to ship it to production next week. Basically, there are some cases in Exchange ActiveSync where event IDs provided by the server may change, and we needed to implement extra\n heuristics to match events in order to prevent our event IDs from changing as well. As you can imagine, this is pretty tricky to get right, so we've been taking some extra time to test it. I'll let you know when this is live. --Christine On Mar 5, 2015 3:12\n PM, &quot;Christine Spang&quot; &lt;<a href=\"mailto:spang@nilas.com\">spang@nilas.com</a>&gt; wrote: Hi Andrew, Apologies for the long delay here. Kavya has figured out what's going with the changing IDs for Exchange calendar, and has been working on a fix for a few days now.\n We don't need any more information from you folks right now. You've been super helpful testing the calendar support. Thanks a ton. --Christine On Thu, Mar 5, 2015 at 1:27 PM, Andrew Lee &lt;<a href=\"mailto:andrew@esper.com\">andrew@esper.com</a>&gt; wrote: Hey guys,&nbsp;\n Just wanted to check up on the status of your changes related to Nilas/Esper. I took the liberty of creating a separate e-mail thread, since the last one was 61 messages long, kept just the latest two messages that mattered, and removed Tikhon, nilas support,\n and removed all of your old inboxapp e-mails. Let us know how we can help! -Andrew On Tue, Feb 24, 2015 at 11:18 AM, Christine Spang &lt;<a href=\"mailto:spang@nilas.com\">spang@nilas.com</a>&gt; wrote: Thanks for this report, Jeff. We're looking into it and should\n have some more info for you soon. On Mon, Feb 23, 2015 at 2:42 PM, Jeff Meister &lt;<a href=\"mailto:jeff@esper.com\">jeff@esper.com</a>&gt; wrote: Hi Christine, I was able to sign in with Andrew's Exchange account the way you explained, so I tried some testing with\n it today. Event creation and update was working fine in both directions, but then a sync issue popped up again, and I think it might have been triggered by me doing a delete (not sure though). Some old events appeared in my Nilas delta sync, including one\n duplicate. I went in our logs to investigate, and by searching for the details of the duplicate event, I found that it had been identified in previous syncs by ID &quot;1d2ex1lni3t89w3rxh8d0in9c&quot;... but after a certain point, I stopped seeing that ID, and instead\n saw ID &quot;1zeaati82ubv706f0sraii380&quot; with the same event details. This ID first appeared in the abnormal sync I received, and I've attached the JSON data that I saw, but here's a summary with (C)reate/(M)odify/(D)elete, ID, and title: D, 4oerzc51uazhwsnb52jbwlrgn\n C, dd438y3gd0xelvukq3lj6vhax, Lunch time!!! M, ah3q993oiyo3rwtieot25e5v, Wednesday event M, 7nrro21ez0cjsopbcftl0aggo, Friday morning M, 4pqygmkpyylqqvjffqx6gwbec, Friday night M, 2x130rv0h6ujkphj4s5xwwduk, Call with some dude M, 1zeaati82ubv706f0sraii380,\n Short meeting M, 11kzbfgr3bes717kgv1bjwc02, Look out the window Some of these events are from my testing today, but &quot;Wednesday event&quot;, &quot;Friday morning&quot;, and &quot;Friday night&quot; are from a week or two ago, and I haven't touched them since then. This is what I mean\n by old events suddenly appearing. Our current implementation actually ignores the data in the delta sync besides the ID, then it goes and looks up the event details using the endpoints based on ID. Looking at the logs, that appears to have worked fine at the\n time, and the existence of two IDs with the &quot;Short meeting&quot; details explains why I saw a duplicate (our system assumes that new ID = new event). Now, some minutes later, after I tried looking up the details of ID &quot;1d2ex1lni3t89w3rxh8d0in9c&quot; and got &quot;Short\n meeting&quot; as expected. But when I tried looking up &quot;1zeaati82ubv706f0sraii380&quot;, expecting the same &quot;Short meeting&quot; as I saw in the delta sync... instead I got a different event, &quot;Wednesday event&quot;! Confused, I went to look up the rest of the IDs above, and here's\n what I got (ID, title): 4oerzc51uazhwsnb52jbwlrgn, N/A (event was deleted) dd438y3gd0xelvukq3lj6vhax, Look out the window ah3q993oiyo3rwtieot25e5v, Friday morning 7nrro21ez0cjsopbcftl0aggo, Friday night 4pqygmkpyylqqvjffqx6gwbec, Call with some dude 2x130rv0h6ujkphj4s5xwwduk,\n Short meeting 1zeaati82ubv706f0sraii380, Wednesday event 11kzbfgr3bes717kgv1bjwc02, Look out the window It almost looks like an off-by-one pattern, but there are weird swaps too that don't seem to follow any rule. I have no idea what to make of this. I can't\n find anything in our logs to suggest our side is causing this... the last appearance of e.g. &quot;1zeaati82ubv706f0sraii380&quot; in my log file associates it with &quot;Short meeting&quot;, and I see no API calls that could have put &quot;Wednesday event&quot; data in there. I wish I\n could reproduce this better and more consistently... hopefully this detail is of some use. I've attached a pretty-printed JSON file containing the sync whose IDs I'm referring to above. Thanks again, Jeff\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</blockquote>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</blockquote>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</span></div>\n</blockquote>\n</div>\n</div>\n</div>\n</span></div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</div>\n</span></div>\n</blockquote>\n</div>\n<br>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</span>\n</body>\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_10.html",
    "content": "<html> <head> <meta http-equiv=\\\"Content-Type\\\" content=\\\"text/html;\ncharset=us-ascii\\\"> </head> <body> I saw it this weekend and will totally go\nagain. :) One of the best movies I've seen recently!&nbsp;<br> <br> <blockquote\nclass=\\\"gmail_quote\\\" style=\\\"margin:0 0 0 .8ex;border-left:1px #ccc\nsolid;padding-left:1ex;\\\"> On Jul 6 2015, at 3:58 pm, Makala Keys\n&lt;makala@nylas.com&gt; wrote: <br> Hey Team, <div> <div><br> </div>\n<div>Let's go see a movie! The latest Pixar movie, Inside Out, is supposed to\nbe excellent (link to the trailor below).&nbsp;</div> <div><span\nstyle=\\\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\\\">I'm\ntaking a headcount for the AMC Van Ness 14 show on <b>WED @ 7:45pm. </b>&#43;1s\nare welcome and tickets will be covered by the company. Please let me know by\nWednesday at noon, so I can purchase tickets in advance!</span></div>\n<div><span style=\\\"font-size: 15.9px; line-height: 1.4; background-color:\ninherit;\\\"><br> </span></div> <div><span style=\\\"font-size: 15.9px;\nline-height: 1.4; background-color: inherit;\\\">Thanks,&nbsp;<br>\nMakala&nbsp;</span></div> <div><br> </div> <div><span style=\\\"font-size:\n15.9px; line-height: 1.4; background-color: inherit;\\\"><br> </span></div>\n<div><span style=\\\"font-size: 15.9px; line-height: 1.4; background-color:\ninherit;\\\">https://www.youtube.com/watch?v=seMwpP0yeu4</span><br> </div> <div>\n<div> <div><br> </div>\n<div>http://www.rottentomatoes.com/m/inside_out_2015/<br> </div> <div><br>\n</div> </div> </div> </div> </blockquote> </body> </html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_10_stripped.html",
    "content": "<head> <meta http-equiv=\"\\&quot;Content-Type\\&quot;\" content=\"\\&quot;text/html;\" charset=\"us-ascii\\&quot;\"> </head> <body> I saw it this weekend and will totally go\nagain. :) One of the best movies I've seen recently!&nbsp;<br> <br>   \n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_11.html",
    "content": "<html> <body> <p>Hi folks</p> <p> What is the best way to clear a Riak bucket of all key, values after running a test?<br/>I am currently using the Java HTTP API.  </p> <p> -Abhishek Kona </p> <br/> <p> _______________________________________________ riak-users mailing list riak-users@lists.basho.com http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com </p> </body> </html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_11_stripped.html",
    "content": "<head></head><body> <p>Hi folks</p> <p> What is the best way to clear a Riak bucket of all key, values after running a test?<br>I am currently using the Java HTTP API.  </p> <p> -Abhishek Kona </p> <br> <p> _______________________________________________ riak-users mailing list riak-users@lists.basho.com http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com </p>  \n</body>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_12.html",
    "content": "<html><body>\n<div>\n  Hi,<br/>\n  <blockquote class=\"foo\">\nOn Tue, 2011-03-01 at 18:02 +0530, Abhishek Kona wrote:\n<div>\n  > Hi folks <br/>\n  >  <br/>\n> What is the best way to clear a Riak bucket of all key, values after\n<br/>\n> running a test? <br/>\n> I am currently using the Java HTTP API. <br/>\n</div>\n  </blockquote>\n\n  <p>\nYou can list the keys for the bucket and call delete for each. Or if you\nput the keys (and kept track of them in your test) you can delete them\none at a time (without incurring the cost of calling list first.)\n  </p>\n  <p>\nSomething like:\n<br/>\n<pre>\n\n        String bucket = \"my_bucket\";\n        BucketResponse bucketResponse = riakClient.listBucket(bucket);\n        RiakBucketInfo bucketInfo = bucketResponse.getBucketInfo();\n        \n        for(String key : bucketInfo.getKeys()) {\n            riakClient.delete(bucket, key);\n        }\n</pre>\n  </p>\n\n  <p>\nwould do it.\n<br/>\nSee also \n<br/>\nhttp://wiki.basho.com/REST-API.html#Bucket-operations\n<br/>\nwhich says \n<br/>\n\"At the moment there is no straightforward way to delete an entire\nBucket. There is, however, an open ticket for the feature. To delete all\nthe keys in a bucket, you’ll need to delete them all individually.\"\n  </p>\n\n<blockquote>\n  <div>\n    > <br/>\n    > -Abhishek Kona <br/>\n    >  <br/>\n    >  <br/>\n    > _______________________________________________ <br/>\n    > riak-users mailing list <br/>\n    > riak-users@lists.basho.com <br/>\n    > http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com <br/>\n  </div>\n</blockquote>\n<br/>\n<br/>\n<br/>\n<br/>\n_______________________________________________\nriak-users mailing list\nriak-users@lists.basho.com\nhttp://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n</body></html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_12_stripped.html",
    "content": "<head></head><body>\n<div>\n  Hi,<br>\n  <blockquote class=\"foo\">\nOn Tue, 2011-03-01 at 18:02 +0530, Abhishek Kona wrote:\n<div>\n  &gt; Hi folks <br>\n  &gt;  <br>\n&gt; What is the best way to clear a Riak bucket of all key, values after\n<br>\n&gt; running a test? <br>\n&gt; I am currently using the Java HTTP API. <br>\n</div>\n  </blockquote>\n\n  <p>\nYou can list the keys for the bucket and call delete for each. Or if you\nput the keys (and kept track of them in your test) you can delete them\none at a time (without incurring the cost of calling list first.)\n  </p>\n  <p>\nSomething like:\n<br>\n</p><pre>\n        String bucket = \"my_bucket\";\n        BucketResponse bucketResponse = riakClient.listBucket(bucket);\n        RiakBucketInfo bucketInfo = bucketResponse.getBucketInfo();\n        \n        for(String key : bucketInfo.getKeys()) {\n            riakClient.delete(bucket, key);\n        }\n</pre>\n  <p></p>\n\n  <p>\nwould do it.\n<br>\nSee also \n<br>\nhttp://wiki.basho.com/REST-API.html#Bucket-operations\n<br>\nwhich says \n<br>\n\"At the moment there is no straightforward way to delete an entire\nBucket. There is, however, an open ticket for the feature. To delete all\nthe keys in a bucket, you’ll need to delete them all individually.\"\n  </p>\n\n<blockquote>\n  <div>\n    &gt; <br>\n    &gt; -Abhishek Kona <br>\n    &gt;  <br>\n    &gt;  <br>\n    &gt; _______________________________________________ <br>\n    &gt; riak-users mailing list <br>\n    &gt; riak-users@lists.basho.com <br>\n    &gt; http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com <br>\n  </div>\n</blockquote>\n<br>\n<br>\n<br>\n<br>\n_______________________________________________\nriak-users mailing list\nriak-users@lists.basho.com\nhttp://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n\n</div></body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_13.html",
    "content": "<html><body>\n    <div>\n      Oh thanks. <br/>\n\n      Having the function would be great. <br/>\n\n      -Abhishek Kona <br/>\n    </div>\n\n    <blockquote>\nOn 01/03/11 7:07 PM, Russell Brown wrote:\n> Hi,\n> On Tue, 2011-03-01 at 18:02 +0530, Abhishek Kona wrote:\n>> Hi folks\n>>\n>> What is the best way to clear a Riak bucket of all key, values after\n>> running a test?\n>> I am currently using the Java HTTP API.\n> You can list the keys for the bucket and call delete for each. Or if you\n> put the keys (and kept track of them in your test) you can delete them\n> one at a time (without incurring the cost of calling list first.)\n>\n> Something like:\n>\n>          String bucket = \"my_bucket\";\n>          BucketResponse bucketResponse = riakClient.listBucket(bucket);\n>          RiakBucketInfo bucketInfo = bucketResponse.getBucketInfo();\n>\n>          for(String key : bucketInfo.getKeys()) {\n>              riakClient.delete(bucket, key);\n>          }\n>\n>\n> would do it.\n>\n> See also\n>\n> http://wiki.basho.com/REST-API.html#Bucket-operations\n>\n> which says\n>\n> \"At the moment there is no straightforward way to delete an entire\n> Bucket. There is, however, an open ticket for the feature. To delete all\n> the keys in a bucket, you’ll need to delete them all individually.\"\n>\n>> -Abhishek Kona\n>>\n>>\n>> _______________________________________________\n>> riak-users mailing list\n>> riak-users@lists.basho.com\n>> http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n>\n\n    </blockquote>\n</body></html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_13_stripped.html",
    "content": "<head></head><body>\n    <div>\n      Oh thanks. <br>\n\n      Having the function would be great. <br>\n\n      -Abhishek Kona <br>\n    </div>\n\n    \n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_14.html",
    "content": "<div style=\"\"><table cellspacing=\"0\" cellpadding=\"8\" border=\"0\" summary=\"\" style=\"width:100%;font-family:Arial,Sans-serif;border:1px Solid #ccc;border-width:1px 2px 2px 1px;background-color:#fff;\" itemscope itemtype=\"http://schema.org/Event\"><tr><td><meta itemprop=\"eventStatus\" content=\"http://schema.org/EventScheduled\"/><div style=\"padding:2px\"><span itemprop=\"publisher\" itemscope itemtype=\"http://schema.org/Organization\"><meta itemprop=\"name\" content=\"Google Calendar\"/></span><meta itemprop=\"eventId/googleCalendar\" content=\"l4qksjqm6v89mqfisq8ddhdvh4\"/><div style=\"float:right;font-weight:bold;font-size:13px\"> <a href=\"https://www.google.com/calendar/event?action=VIEW&amp;eid=bDRxa3NqcW02djg5b…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">more details &raquo;</a><br></div><h3 style=\"padding:0 0 6px 0;margin:0;font-family:Arial,Sans-serif;font-size:16px;font-weight:bold;color:#222\"><span itemprop=\"name\">Recruiting Email Weekly Blastoff</span></h3><div style=\"padding-bottom:15px;font-size:13px;color:#222;white-space:pre-wrap!important;white-space:-moz-pre-wrap!important;white-space:-pre-wrap!important;white-space:-o-pre-wrap!important;white-space:pre;word-wrap:break-word\">Turn those cold leads into phone screens! You can make this go super fast by queueing up your drafts before hand and just sending them out during this time. </div><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" summary=\"Event details\"><tr><td style=\"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap\" valign=\"top\"><div><i style=\"font-style:normal\">When</i></div></td><td style=\"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\" valign=\"top\"><time itemprop=\"startDate\" datetime=\"20150228T010000Z\"></time><time itemprop=\"endDate\" datetime=\"20150228T013000Z\"></time>Fri Feb 27, 2015 5pm &ndash; 5:30pm <span style=\"color:#888\">Pacific Time</span></td></tr><tr><td style=\"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap\" valign=\"top\"><div><i style=\"font-style:normal\">Calendar</i></div></td><td style=\"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\" valign=\"top\">Ben Gotow</td></tr><tr><td style=\"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap\" valign=\"top\"><div><i style=\"font-style:normal\">Who</i></div></td><td style=\"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\" valign=\"top\"><table cellspacing=\"0\" cellpadding=\"0\"><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Michael Grinich</span><meta itemprop=\"email\" content=\"mg@nylas.com\"/></span><span style=\"font-size:11px;color:#888\"> - organizer</span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Kartik Talwar</span><meta itemprop=\"email\" content=\"kartik@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">team</span><meta itemprop=\"email\" content=\"team@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Rob McQueen</span><meta itemprop=\"email\" content=\"rob@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Evan Morikawa</span><meta itemprop=\"email\" content=\"evan@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Christine Spang</span><meta itemprop=\"email\" content=\"spang@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Karim Hamidou</span><meta itemprop=\"email\" content=\"karim@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">nylas.com@allusers.d.calendar.google.com</span><meta itemprop=\"email\" content=\"nylas.com@allusers.d.calendar.google.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Makala Keys</span><meta itemprop=\"email\" content=\"makala@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Eben Freeman</span><meta itemprop=\"email\" content=\"eben@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Jennie Lees</span><meta itemprop=\"email\" content=\"jennie@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Ben Gotow</span><meta itemprop=\"email\" content=\"ben@nylas.com\"/></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">&#x2022;</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Kavya Joshi</span><meta itemprop=\"email\" content=\"kavya@nylas.com\"/></span></div></div></td></tr></table></td></tr></table></div><p style=\"color:#222;font-size:13px;margin:0\"><span style=\"color:#888\">Going?&nbsp;&nbsp;&nbsp;</span><wbr><strong><span itemprop=\"action\" itemscope itemtype=\"http://schema.org/RsvpAction\"><meta itemprop=\"attendance\" content=\"http://schema.org/RsvpAttendance/Yes\"/><span itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\"><link itemprop=\"method\" href=\"http://schema.org/HttpRequestMethod/GET\"/><a href=\"https://www.google.com/calendar/event?action=RESPOND&amp;eid=bDRxa3NqcW02dj…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">Yes</a></span></span><span style=\"margin:0 0.4em;font-weight:normal\"> - </span><span itemprop=\"action\" itemscope itemtype=\"http://schema.org/RsvpAction\"><meta itemprop=\"attendance\" content=\"http://schema.org/RsvpAttendance/Maybe\"/><span itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\"><link itemprop=\"method\" href=\"http://schema.org/HttpRequestMethod/GET\"/><a href=\"https://www.google.com/calendar/event?action=RESPOND&amp;eid=bDRxa3NqcW02dj…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">Maybe</a></span></span><span style=\"margin:0 0.4em;font-weight:normal\"> - </span><span itemprop=\"action\" itemscope itemtype=\"http://schema.org/RsvpAction\"><meta itemprop=\"attendance\" content=\"http://schema.org/RsvpAttendance/No\"/><span itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\"><link itemprop=\"method\" href=\"http://schema.org/HttpRequestMethod/GET\"/><a href=\"https://www.google.com/calendar/event?action=RESPOND&amp;eid=bDRxa3NqcW02dj…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">No</a></span></span></strong>&nbsp;&nbsp;&nbsp;&nbsp;<wbr><a href=\"https://www.google.com/calendar/event?action=VIEW&amp;eid=bDRxa3NqcW02djg5b…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">more options &raquo;</a></p></td></tr><tr><td style=\"background-color:#f6f6f6;color:#888;border-top:1px Solid #ccc;font-family:Arial,Sans-serif;font-size:11px\"><p>Invitation from <a href=\"https://www.google.com/calendar/\" target=\"_blank\" style=\"\">Google Calendar</a></p><p>You are receiving this email at the account ben@nylas.com because you are subscribed for invitations on calendar Ben Gotow.</p><p>To stop receiving these emails, please log in to https://www.google.com/calendar/ and change your notification settings for this calendar.</p></td></tr></table></div>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_14_stripped.html",
    "content": "<div style=\"\"><table cellspacing=\"0\" cellpadding=\"8\" border=\"0\" summary=\"\" style=\"width:100%;font-family:Arial,Sans-serif;border:1px Solid #ccc;border-width:1px 2px 2px 1px;background-color:#fff;\" itemscope=\"\" itemtype=\"http://schema.org/Event\"><tbody><tr><td><meta itemprop=\"eventStatus\" content=\"http://schema.org/EventScheduled\"><div style=\"padding:2px\"><span itemprop=\"publisher\" itemscope=\"\" itemtype=\"http://schema.org/Organization\"><meta itemprop=\"name\" content=\"Google Calendar\"></span><meta itemprop=\"eventId/googleCalendar\" content=\"l4qksjqm6v89mqfisq8ddhdvh4\"><div style=\"float:right;font-weight:bold;font-size:13px\"> <a href=\"https://www.google.com/calendar/event?action=VIEW&amp;eid=bDRxa3NqcW02djg5b…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">more details »</a><br></div><h3 style=\"padding:0 0 6px 0;margin:0;font-family:Arial,Sans-serif;font-size:16px;font-weight:bold;color:#222\"><span itemprop=\"name\">Recruiting Email Weekly Blastoff</span></h3><div style=\"padding-bottom:15px;font-size:13px;color:#222;white-space:pre-wrap!important;white-space:-moz-pre-wrap!important;white-space:-pre-wrap!important;white-space:-o-pre-wrap!important;white-space:pre;word-wrap:break-word\">Turn those cold leads into phone screens! You can make this go super fast by queueing up your drafts before hand and just sending them out during this time. </div><table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" summary=\"Event details\"><tbody><tr><td style=\"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap\" valign=\"top\"><div><i style=\"font-style:normal\">When</i></div></td><td style=\"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\" valign=\"top\"><time itemprop=\"startDate\" datetime=\"20150228T010000Z\"></time><time itemprop=\"endDate\" datetime=\"20150228T013000Z\"></time>Fri Feb 27, 2015 5pm – 5:30pm <span style=\"color:#888\">Pacific Time</span></td></tr><tr><td style=\"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap\" valign=\"top\"><div><i style=\"font-style:normal\">Calendar</i></div></td><td style=\"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\" valign=\"top\">Ben Gotow</td></tr><tr><td style=\"padding:0 1em 10px 0;font-family:Arial,Sans-serif;font-size:13px;color:#888;white-space:nowrap\" valign=\"top\"><div><i style=\"font-style:normal\">Who</i></div></td><td style=\"padding-bottom:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\" valign=\"top\"><table cellspacing=\"0\" cellpadding=\"0\"><tbody><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Michael Grinich</span><meta itemprop=\"email\" content=\"mg@nylas.com\"></span><span style=\"font-size:11px;color:#888\"> - organizer</span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Kartik Talwar</span><meta itemprop=\"email\" content=\"kartik@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">team</span><meta itemprop=\"email\" content=\"team@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Rob McQueen</span><meta itemprop=\"email\" content=\"rob@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Evan Morikawa</span><meta itemprop=\"email\" content=\"evan@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Christine Spang</span><meta itemprop=\"email\" content=\"spang@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Karim Hamidou</span><meta itemprop=\"email\" content=\"karim@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">nylas.com@allusers.d.calendar.google.com</span><meta itemprop=\"email\" content=\"nylas.com@allusers.d.calendar.google.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Makala Keys</span><meta itemprop=\"email\" content=\"makala@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Eben Freeman</span><meta itemprop=\"email\" content=\"eben@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Jennie Lees</span><meta itemprop=\"email\" content=\"jennie@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Ben Gotow</span><meta itemprop=\"email\" content=\"ben@nylas.com\"></span></div></div></td></tr><tr><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><span style=\"font-family:Courier New,monospace\">•</span></td><td style=\"padding-right:10px;font-family:Arial,Sans-serif;font-size:13px;color:#222\"><div><div style=\"margin:0 0 0.3em 0\"><span itemprop=\"attendee\" itemscope=\"\" itemtype=\"http://schema.org/Person\"><span itemprop=\"name\">Kavya Joshi</span><meta itemprop=\"email\" content=\"kavya@nylas.com\"></span></div></div></td></tr></tbody></table></td></tr></tbody></table></div><p style=\"color:#222;font-size:13px;margin:0\"><span style=\"color:#888\">Going?&nbsp;&nbsp;&nbsp;</span><wbr><strong><span itemprop=\"action\" itemscope=\"\" itemtype=\"http://schema.org/RsvpAction\"><meta itemprop=\"attendance\" content=\"http://schema.org/RsvpAttendance/Yes\"><span itemprop=\"handler\" itemscope=\"\" itemtype=\"http://schema.org/HttpActionHandler\"><link itemprop=\"method\" href=\"http://schema.org/HttpRequestMethod/GET\"><a href=\"https://www.google.com/calendar/event?action=RESPOND&amp;eid=bDRxa3NqcW02dj…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">Yes</a></span></span><span style=\"margin:0 0.4em;font-weight:normal\"> - </span><span itemprop=\"action\" itemscope=\"\" itemtype=\"http://schema.org/RsvpAction\"><meta itemprop=\"attendance\" content=\"http://schema.org/RsvpAttendance/Maybe\"><span itemprop=\"handler\" itemscope=\"\" itemtype=\"http://schema.org/HttpActionHandler\"><link itemprop=\"method\" href=\"http://schema.org/HttpRequestMethod/GET\"><a href=\"https://www.google.com/calendar/event?action=RESPOND&amp;eid=bDRxa3NqcW02dj…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">Maybe</a></span></span><span style=\"margin:0 0.4em;font-weight:normal\"> - </span><span itemprop=\"action\" itemscope=\"\" itemtype=\"http://schema.org/RsvpAction\"><meta itemprop=\"attendance\" content=\"http://schema.org/RsvpAttendance/No\"><span itemprop=\"handler\" itemscope=\"\" itemtype=\"http://schema.org/HttpActionHandler\"><link itemprop=\"method\" href=\"http://schema.org/HttpRequestMethod/GET\"><a href=\"https://www.google.com/calendar/event?action=RESPOND&amp;eid=bDRxa3NqcW02dj…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">No</a></span></span></strong>&nbsp;&nbsp;&nbsp;&nbsp;<wbr><a href=\"https://www.google.com/calendar/event?action=VIEW&amp;eid=bDRxa3NqcW02djg5b…c4ZTEwZDM2NzRjY2NkYzkyZDI0ZTQ3OWY5OA&amp;ctz=America/Los_Angeles&amp;hl=en\" style=\"color:#20c;white-space:nowrap\" itemprop=\"url\">more options »</a></p></td></tr><tr><td style=\"background-color:#f6f6f6;color:#888;border-top:1px Solid #ccc;font-family:Arial,Sans-serif;font-size:11px\"><p>Invitation from <a href=\"https://www.google.com/calendar/\" target=\"_blank\" style=\"\">Google Calendar</a></p><p>You are receiving this email at the account ben@nylas.com because you are subscribed for invitations on calendar Ben Gotow.</p><p>To stop receiving these emails, please log in to https://www.google.com/calendar/ and change your notification settings for this calendar.</p></td></tr></tbody></table></div>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_15.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\nTest 123<br/><br><br >\n\n<blockquote ><blockquote>Quote level 2</blockquote></blockquote>\n\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\nOn Jul 6 2015, at 12:34 pm, spang@nylas.com &lt;spang@nylas.com&gt; wrote: <br>\nKarim, Michael and I just discussed this. I'll send you a redwood docs diff later today to speed this along. Here's the summary for now.\n<div><br>\n</div>\n<div>We agreed on:<br>\n- add a param to the create/update APIs for events instead of creating a new API<br>\n- no body customization for now<br>\n- sending notifications defaults to `false`<br>\n</div>\n<div><br>\n</div>\n<div>Differences from what we discussed:</div>\n<div>- call the parameter `notify_participants` instead of `send_notifications` (it's more explicit about who is getting notified)</div>\n<div>- only fail if <i>event creation fails,</i>&nbsp;not if the notifications fail to send*</div>\n<div>- to make failure happen less often, we should validate event participants' email addresses and reject requests with bad email addresses as invalid</div>\n<div>- on Google, use the `sendNotifications` parameter in the calendar API instead of sending out event notifications ourself, for consistency with expectations and increased reliability</div>\n<div><br>\n</div>\n<div>* We can document that we make a best-effort to deliver notifications, but they may not always succeed. This is a tiny edge case and it shouldn't come up very often, so we shouldn't cause clients to complicate their logic because of it.</div>\n<div><br>\n</div>\n<div>Please include the code for both create and update in your updated diff. It doesn't make sense to ship create only without update, and most of the code is there already.</div>\n<div><br>\n</div>\n<div>Samples:</div>\n<div><br>\n</div>\n<div>POST /n/&lt;ns-id&gt;/events?notify_participants=true -d '{ ... }'</div>\n<div>PUT /n/&lt;ns-id&gt;/events?notify_participants=true -d '{ ... }'</div>\n<div><br>\n</div>\n<div>Let's ship this and see what Lever thinks.<br>\n</div>\n<div><br>\n</div>\n<br>\n<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\nOn Jul 6 2015, at 9:55 am, Karim Hamidou &lt;karim@nylas.com&gt; wrote: <br>\n<div>Christine,</div>\n<div><br>\n</div>\n<div>Here are my notes about the quick chat we had.</div>\n<div><br>\n</div>\n<div>1. We need an invite sending API. We could either:</div>\n<div>- add a parameter to the event creation/update API to send emails to the participants</div>\n<div>- or have a separate invite sending endpoint.</div>\n<div><br>\n</div>\n<div>We chose to go with the former, because it's simpler.</div>\n<div><br>\n</div>\n<div>2. <b>How would this work?</b>&nbsp;When creating/updating/deleting an event, an API user can set the `send_notifications` parameter to `true`. In this case, the API will generate an event invite email and will send it to the participants.&nbsp;<br>\n</div>\n<div><br>\n</div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\">Example: curl -X POST http://localhost:5555/n/namespace_id/events?send_notifications=true &nbsp;&quot;{ title: 'test event', start: ... }&quot;</span><br>\n</div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\"><br>\n</span></div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\">3.\n<b>Error handling. &nbsp;</b>Invite sending can fail in a variety of ways because we're sending emails. Because API users need to know if an message went through or not, the API behaves a bit like the synchronous sending API.&nbsp;</span></div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\"><br>\n</span></div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\">Here's what happens when a user creates an event with send_notifications=true:</span><br>\n</div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\">1. We create the event in the db</span></div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\">2. We try sending emails</span></div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\">3. If it failed, we delete the event from the db and return the SMTP error.</span></div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\"><br>\n</span></div>\n<div>Of course API users can try re-sending the same invite as often as they wish.</div>\n<div><br>\n</div>\n<div>4. <b>Limitations:</b></div>\n<div><b>- </b>it's not possible to define a custom body (though we could have define an ad-hoc `invite_body` field in the event JSON that could be used only for invite sending)</div>\n<div>- no support for attached files&nbsp;</div>\n<div><br>\n</div>\n<div>Did I forget anything?</div>\n<div><br>\n</div>\n<div>k</div>\n<div><span style=\"font-size: 15.9px; line-height: 1.4; background-color: inherit;\"><b><br>\n</b></span></div>\n<div><br>\n</div>\n</blockquote>\n</blockquote>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_15_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\nTest 123<br><br><br>\n\n\n\n\n\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_16.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n<div>Hi all,</div>\n<div><br>\n</div>\n<div>Below is the sign up link for on-site massages tomorrow. The moussuse will arrive after lunch. Please sign up for your time if you wish to participate :).&nbsp;</div>\n<div><br>\n</div>\n<div>Makala&nbsp;</div>\n<br>\nFrom: makala@nylas.com<br>\nSubject: Fwd: Refresh Corporate Confirmation<br>\nDate: Aug 28 2015, at 3:51 pm<br>\nTo: Michael Grinich &lt;mg@nylas.com&gt;, Christine Spang &lt;spang@nylas.com&gt; <br>\n<br>\n<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n<div class=\"body\">\n<table style=\"background-color:rgb(239, 239, 239);margin-left:auto;margin-right:auto\" bgcolor=\"#F4F4F4\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td rowspan=\"1\" colspan=\"1\"><img height=\"5\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"1\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n<td style=\"width:610px\" valign=\"top\" width=\"610\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div style=\"max-width:610px;margin-left:auto;margin-right:auto\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 5px 15px 5px\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 8px 0px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<span class=\"im\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 9px 0px;color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"100\" cellspacing=\"0\" cellpadding=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:5px;color:#4d4d4d\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img height=\"100\" vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.81\" hspace=\"0\" width=\"-3\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/refresh_logo.png\" class=\"CToWUd\"></div>\n</td>\n</tr>\n</tbody>\n</table>\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<table width=\"156\" cellpadding=\"0\" cellspacing=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:5px;color:#4d4d4d\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img height=\"21\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.94\" border=\"0\" hspace=\"0\" width=\"156\" alt=\"Refresh Logo\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/refresh_name.png\" class=\"CToWUd\"></div>\n</td>\n</tr>\n</tbody>\n</table>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</span>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 9px 0px;color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<br>\n</div>\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<span style=\"font-size:16pt\"><strong>Nylas is Bringing Massage to the Office</strong></span><br>\n</div>\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<br>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"background-color:#ffffff\" bgcolor=\"#FFFFFF\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:1px 1px 1px 1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"background-color:#ffffff\" bgcolor=\"#FFFFFF\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 15px 0px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 0px 0px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"598\" cellspacing=\"0\" cellpadding=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:5px;color:#333333\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.100\" hspace=\"0\" width=\"598\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/corporate_header.jpg\" class=\"CToWUd a6T\" tabindex=\"0\">\n<div class=\"a6S\" dir=\"ltr\" style=\"opacity: 0.01;\">\n<div id=\":12v\" class=\"T-I J-J5-Ji aQv T-I-ax7 L3 a5q\" title=\"Download\" role=\"button\" tabindex=\"0\" aria-label=\"Download attachment \" data-tooltip-class=\"a1V\">\n<div class=\"aSK J-J5-Ji aYr\"></div>\n</div>\n</div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"left\">\n<div>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i>Nylas and Refresh&nbsp;</i></span></p>\n<span class=\"im\">\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i>&nbsp;invite you to sign-up for</i></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><strong><i>&nbsp;</i></strong></span></p>\n</span>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"color:#333333;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif\" color=\"#333333\" face=\"Century Gothic, ITC Avant Garde, Arial, Helvetica, sans-serif\"><b><i>A\n Massage</i></b></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i>on&nbsp;</i></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i><strong>Tuesday September 1st</strong> at\n<strong>1 PM</strong></i></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\">&nbsp;</p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\">2030 Harrison Street Floor 2 San Francisco, CA 94110</p>\n<span class=\"im\">\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i><b></b></i></span></p>\n<div><i><b><br>\n</b></i></div>\n<i><b>&nbsp;</b></i></span>\n<p></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i><b>Click below to reserve your spot&nbsp;<br>\n&nbsp;</b>&nbsp;</i></span></p>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n<span class=\"im\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"background-color:#fb7f34;width:auto!important;border-radius:5px\" bgcolor=\"#FB7F34\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:9px 15px 10px 15px;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div>\n<div style=\"font-size:14px;color:#ffffff;text-decoration:none;font-weight:bold\">&nbsp;<a style=\"font-weight:bold;color:rgb(255,255,255);text-decoration:none\" shape=\"rect\" href=\"http://www.refreshbody.com/group/employeebooking?id=229274438\" alt=\"http://www.refreshbody.com/group/employeebooking?id=229274438\" target=\"_blank\">RESERVE\n NOW</a></div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<table style=\"display:table\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"left\">\n<div style=\"text-align:center\" align=\"center\">\n<div style=\"font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif\">\n<span style=\"font-size:10pt\">Please contact Refresh Body with any issues </span><a style=\"font-size:10pt;color:rgb(153,78,190);text-decoration:none\" href=\"mailto:partners@refreshbody.com\" shape=\"rect\" target=\"_blank\">support@refreshbody.com</a></div>\n<div style=\"font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif\">\n<a style=\"font-size:10pt;color:rgb(153,78,190);text-decoration:none\" href=\"mailto:partners@refreshbody.com\" shape=\"rect\" target=\"_blank\"><span style=\"color:#4c4c4c;font-size:10pt\">Or call 800-616-9271</span></a></div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 20px 15px 20px;height:1px;line-height:1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"height:1px\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:0px;background-color:#cccccc;height:1px;line-height:1px\" height=\"1\" bgcolor=\"#CCCCCC\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"display:table\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"106\" cellpadding=\"0\" cellspacing=\"0\" align=\"left\">\n<tbody>\n<tr>\n<td style=\"color:#000000;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div style=\"margin-left:120px\" align=\"center\"><a href=\"https://play.google.com/store/apps/developer?id=Refresh&#43;Body&amp;hl=en\" shape=\"rect\" alt=\"https://play.google.com/store/apps/developer?id=Refresh&#43;Body&amp;hl=en\" target=\"_blank\"><img height=\"34\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.103\" border=\"0\" hspace=\"0\" width=\"106\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/google_play.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"text-align:right\" width=\"106\" cellpadding=\"0\" cellspacing=\"0\" align=\"left\">\n<tbody>\n<tr>\n<td style=\"color:#000000;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div style=\"margin-left:120px\" align=\"center\"><a href=\"https://itunes.apple.com/us/app/refresh-massage-yoga-pilates/id874317687?mt=8\" shape=\"rect\" alt=\"https://itunes.apple.com/us/app/refresh-massage-yoga-pilates/id874317687?mt=8\" target=\"_blank\"><img height=\"34\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.102\" border=\"0\" hspace=\"0\" width=\"106\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/app_store.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 20px 15px 20px;height:1px;line-height:1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"height:1px\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:0px;background-color:#cccccc;height:1px;line-height:1px\" height=\"1\" bgcolor=\"#CCCCCC\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</span>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 0px 0px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"540\" cellspacing=\"0\" cellpadding=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:0px;color:#333333\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img style=\"display:block;min-height:auto!important;max-width:100%!important\" height=\"91\" vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.51\" hspace=\"0\" width=\"540\" alt=\"Critical Acclaim\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/partners1.jpg\" class=\"CToWUd\"></div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<span class=\"im\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 20px 15px 20px;height:1px;line-height:1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"height:1px\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:0px;background-color:#cccccc;height:1px;line-height:1px\" height=\"1\" bgcolor=\"#CCCCCC\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"display:table\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 0px 0px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"540\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"color:#333333;padding:0px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img style=\"display:block;min-height:auto!important;max-width:100%!important\" height=\"329\" vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.49\" hspace=\"0\" width=\"540\" alt=\"Our Partners\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/partners2.jpg\" class=\"CToWUd a6T\" tabindex=\"0\">\n<div class=\"a6S\" dir=\"ltr\" style=\"opacity: 0.01;\">\n<div id=\":12w\" class=\"T-I J-J5-Ji aQv T-I-ax7 L3 a5q\" title=\"Download\" role=\"button\" tabindex=\"0\" aria-label=\"Download attachment \" data-tooltip-class=\"a1V\">\n<div class=\"aSK J-J5-Ji aYr\"></div>\n</div>\n</div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</span></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:9px 0px 8px 0px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 9px 0px;color:rgb(0,0,0)\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div><br>\n</div>\n<span class=\"im\">\n<table style=\"margin:0px 0px 0px 0px\" border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 10px;color:#000000\" valign=\"center\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n</td>\n</tr>\n<tr>\n<td style=\"font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#666666\" valign=\"center\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div>Refresh Body</div>\n<div><span style=\"font-size:13.3333330154419px\"><a style=\"color:blue;text-decoration:underline\" shape=\"rect\" href=\"http://refreshbody.com\" alt=\"http://refreshbody.com\" target=\"_blank\">refreshbody.com<br>\n</a><br>\n</span></div>\n<div><span style=\"font-size:13.3333330154419px\">\n<table style=\"width:100%;border-style:none;border-width:0\" cellspacing=\"0\" cellpadding=\"3\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"vertical-align:top;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#666666;font-size:13.3333330154419px;font-style:normal;font-weight:normal;width:290px;height:23px\" rowspan=\"1\" colspan=\"1\">\n<table style=\"text-align:center\" width=\"30\" cellpadding=\"0\" cellspacing=\"0\" align=\"right\">\n<tbody>\n<tr>\n<td style=\"color:#666666;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><a href=\"https://twitter.com/refresh_body\" shape=\"rect\" alt=\"https://twitter.com/refresh_body\" target=\"_blank\"><img height=\"28\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.98\" border=\"0\" hspace=\"0\" width=\"30\" alt=\"Twitter\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/twitter.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n<br>\n</td>\n<td style=\"vertical-align:top;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#666666;font-size:13.3333330154419px;font-style:normal;font-weight:normal;width:290px;height:23px\" rowspan=\"1\" colspan=\"1\">\n<table style=\"text-align:center\" width=\"30\" cellpadding=\"0\" cellspacing=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"color:#666666;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><a href=\"https://www.facebook.com/GetRefreshd\" shape=\"rect\" alt=\"https://www.facebook.com/GetRefreshd\" target=\"_blank\"><img height=\"29\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.96\" border=\"0\" hspace=\"0\" width=\"30\" alt=\"Facebook\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/facebook.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n<br>\n</td>\n</tr>\n</tbody>\n</table>\n</span></div>\n<br>\n</td>\n</tr>\n</tbody>\n</table>\n</span></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</div>\n</td>\n<td rowspan=\"1\" colspan=\"1\"><img height=\"5\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"1\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<div class=\"yj6qo ajU\">\n<div id=\":pl\" class=\"ajR\" role=\"button\" tabindex=\"0\" data-tooltip=\"Show trimmed content\" aria-label=\"Show trimmed content\">\n<img class=\"ajT\" src=\"//ssl.gstatic.com/ui/v1/icons/mail/images/cleardot.gif\"></div>\n</div>\n<div class=\"adL\"></div>\n</div>\n<div>\n<table width=\"600\" border=\"0\" bordercolor=\"#000000\" style=\"background-color:#FFFFFF; margin: 0 auto;\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td height=\"40\" colspan=\"2\" style=\"padding-left: 7%; padding-right: 7%;\">\n<p>Sincerely,</p>\n<p>The Refresh Body Team<br>\n<em>On-Demand, On-Location Wellness Services. Get Refresh'd</em><br>\n<span class=\"mobile_link\">800-616-9271</span> </p>\n</td>\n</tr>\n<tr>\n<td height=\"47\" colspan=\"2\" style=\"padding-left: 7%; padding-right: 7%; font-size: 12px;\">\n<p><a href=\"http://www.refreshbody.com/privacy\" target=\"_blank\">Privacy Policy</a> |\n<a href=\"http://www.refreshbody.com/terms\" target=\"_blank\">Terms of Use</a> <br>\nNote: This is an auto-generated email.&nbsp; Please do not reply.</p>\n</td>\n</tr>\n<tr>\n<td height=\"47\" colspan=\"2\" style=\"padding-left: 7%; padding-right: 7%;\"></td>\n</tr>\n</tbody>\n</table>\n<table width=\"600\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0 auto;\">\n<tbody>\n<tr>\n<td><img class=\"image_fix\" src=\"http://www.refreshbody.com/images/email/shadow.png\" style=\"width: 100%; height: auto; margin: -3px 0 0 0; padding: 0; display: block\"></td>\n</tr>\n</tbody>\n</table>\n</div>\n</blockquote>\n<style type=\"text/css\">\n            /* Based on The MailChimp Reset INLINE: Yes. */\n            /* Client-specific Styles */\n        #outlook a {\n            padding: 0;\n        }\n\n            /* Force Outlook to provide a \"view in browser\" menu link. */\n        body {\n            width: 100% !important;\n            -webkit-text-size-adjust: 100%;\n            -ms-text-size-adjust: 100%;\n            margin: 0;\n            padding: 0;\n        }\n\n            /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/\n        .ExternalClass {\n            width: 100%;\n        }\n\n            /* Force Hotmail to display emails at full width */\n        .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {\n            line-height: 100%;\n        }\n\n            /* Forces Hotmail to display normal line spacing.  More on that: http://www.emailonacid.com/forum/viewthread/43/ */\n        #backgroundTable {\n            margin: 0;\n            padding: 0;\n            width: 100% !important;\n            line-height: 100% !important;\n        }\n\n            /* End reset */\n\n            /* Some sensible defaults for images\n            Bring inline: Yes. */\n        img {\n            outline: none;\n            text-decoration: none;\n            -ms-interpolation-mode: bicubic;\n        }\n\n        a img {\n            border: none;\n        }\n\n        .image_fix {\n            display: block;\n        }\n\n            /* Yahoo paragraph fix\n            Bring inline: Yes. */\n        p {\n            margin: 1em 0;\n        }\n\n            /* Hotmail header color reset\n            Bring inline: Yes. */\n        h1, h2, h3, h4, h5, h6 {\n            color: #8c8c8c !important;\n        }\n\n        h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {\n            color: blue !important;\n        }\n\n        h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {\n            color: red !important; /* Preferably not the same color as the normal header link color.  There is limited support for psuedo classes in email clients, this was added just for good measure. */\n        }\n\n        h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {\n            color: purple !important; /* Preferably not the same color as the normal header link color. There is limited support for psuedo classes in email clients, this was added just for good measure. */\n        }\n\n            /* Outlook 07, 10 Padding issue fix\n            Bring inline: No.*/\n        table td {\n            border-collapse: collapse;\n        }\n\n            /* Remove spacing around Outlook 07, 10 tables\n            Bring inline: Yes */\n        table {\n            border-collapse: collapse;\n            mso-table-lspace: 0pt;\n            mso-table-rspace: 0pt;\n        }\n\n            /* Styling your links has become much simpler with the new Yahoo.  In fact, it falls in line with the main credo of styling in email and make sure to bring your styles inline.  Your link colors will be uniform across clients when brought inline.\n            Bring inline: Yes. */\n        a {\n            color: blue;\n        }\n\n            /***************************************************\n            ****************************************************\n            MOBILE TARGETING\n            ****************************************************\n            ***************************************************/\n        @media only screen and (max-device-width: 480px) {\n            /* Part one of controlling phone number linking for mobile. */\n            a[href^=\"tel\"], a[href^=\"sms\"] {\n                text-decoration: none;\n                color: blue; /* or whatever your want */\n                pointer-events: none;\n                cursor: default;\n            }\n\n            .mobile_link a[href^=\"tel\"], .mobile_link a[href^=\"sms\"] {\n                text-decoration: default;\n                color: blue !important;\n                pointer-events: auto;\n                cursor: default;\n            }\n\n        }\n\n            /* More Specific Targeting */\n\n        @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {\n            /* You guessed it, ipad (tablets, smaller screens, etc) */\n            /* repeating for the ipad */\n            a[href^=\"tel\"], a[href^=\"sms\"] {\n                text-decoration: none;\n                color: blue; /* or whatever your want */\n                pointer-events: none;\n                cursor: default;\n            }\n\n            .mobile_link a[href^=\"tel\"], .mobile_link a[href^=\"sms\"] {\n                text-decoration: default;\n                color: blue !important;\n                pointer-events: auto;\n                cursor: default;\n            }\n        }\n\n        @media only screen and (-webkit-min-device-pixel-ratio: 2) {\n            /* Put your iPhone 4g styles in here */\n        }\n\n            /* Android targeting */\n        @media only screen and (-webkit-device-pixel-ratio:.75) {\n            /* Put CSS for low density (ldpi) Android layouts in here */\n        }\n\n        @media only screen and (-webkit-device-pixel-ratio:1) {\n            /* Put CSS for medium density (mdpi) Android layouts in here */\n        }\n\n        @media only screen and (-webkit-device-pixel-ratio:1.5) {\n            /* Put CSS for high density (hdpi) Android layouts in here */\n        }\n\n            /* end Android targeting */\n\n    </style></blockquote>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_16_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n<div>Hi all,</div>\n<div><br>\n</div>\n<div>Below is the sign up link for on-site massages tomorrow. The moussuse will arrive after lunch. Please sign up for your time if you wish to participate :).&nbsp;</div>\n<div><br>\n</div>\n<div>Makala&nbsp;</div>\n<br>\nFrom: makala@nylas.com<br>\nSubject: Fwd: Refresh Corporate Confirmation<br>\nDate: Aug 28 2015, at 3:51 pm<br>\nTo: Michael Grinich &lt;mg@nylas.com&gt;, Christine Spang &lt;spang@nylas.com&gt; <br>\n<br>\n<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n<div class=\"body\">\n<table style=\"background-color:rgb(239, 239, 239);margin-left:auto;margin-right:auto\" bgcolor=\"#F4F4F4\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td rowspan=\"1\" colspan=\"1\"><img height=\"5\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"1\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n<td style=\"width:610px\" valign=\"top\" width=\"610\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div style=\"max-width:610px;margin-left:auto;margin-right:auto\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 5px 15px 5px\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 8px 0px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<span class=\"im\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 9px 0px;color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"100\" cellspacing=\"0\" cellpadding=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:5px;color:#4d4d4d\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img height=\"100\" vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.81\" hspace=\"0\" width=\"-3\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/refresh_logo.png\" class=\"CToWUd\"></div>\n</td>\n</tr>\n</tbody>\n</table>\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<table width=\"156\" cellpadding=\"0\" cellspacing=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:5px;color:#4d4d4d\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img height=\"21\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.94\" border=\"0\" hspace=\"0\" width=\"156\" alt=\"Refresh Logo\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/refresh_name.png\" class=\"CToWUd\"></div>\n</td>\n</tr>\n</tbody>\n</table>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</span>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 9px 0px;color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<br>\n</div>\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<span style=\"font-size:16pt\"><strong>Nylas is Bringing Massage to the Office</strong></span><br>\n</div>\n<div style=\"color:#4d4d4d;font-family:Century Gothic,ITC Avant Garde,Arial,Helvetica,sans-serif;font-size:9pt\">\n<br>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"background-color:#ffffff\" bgcolor=\"#FFFFFF\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:1px 1px 1px 1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"background-color:#ffffff\" bgcolor=\"#FFFFFF\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 15px 0px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 0px 0px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"598\" cellspacing=\"0\" cellpadding=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:5px;color:#333333\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.100\" hspace=\"0\" width=\"598\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/corporate_header.jpg\" class=\"CToWUd a6T\" tabindex=\"0\">\n<div class=\"a6S\" dir=\"ltr\" style=\"opacity: 0.01;\">\n<div id=\":12v\" class=\"T-I J-J5-Ji aQv T-I-ax7 L3 a5q\" title=\"Download\" role=\"button\" tabindex=\"0\" aria-label=\"Download attachment \" data-tooltip-class=\"a1V\">\n<div class=\"aSK J-J5-Ji aYr\"></div>\n</div>\n</div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"left\">\n<div>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i>Nylas and Refresh&nbsp;</i></span></p>\n<span class=\"im\">\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i>&nbsp;invite you to sign-up for</i></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><strong><i>&nbsp;</i></strong></span></p>\n</span>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"color:#333333;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif\" color=\"#333333\" face=\"Century Gothic, ITC Avant Garde, Arial, Helvetica, sans-serif\"><b><i>A\n Massage</i></b></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i>on&nbsp;</i></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i><strong>Tuesday September 1st</strong> at\n<strong>1 PM</strong></i></span></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\">&nbsp;</p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\">2030 Harrison Street Floor 2 San Francisco, CA 94110</p>\n<span class=\"im\">\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i><b></b></i></span></p>\n<div><i><b><br>\n</b></i></div>\n<i><b>&nbsp;</b></i></span>\n<p></p>\n<p style=\"text-align:center;margin-top:0px;margin-bottom:0px\" align=\"center\"><span style=\"font-size:12pt;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#333333\"><i><b>Click below to reserve your spot&nbsp;<br>\n&nbsp;</b>&nbsp;</i></span></p>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n<span class=\"im\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"background-color:#fb7f34;width:auto!important;border-radius:5px\" bgcolor=\"#FB7F34\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:9px 15px 10px 15px;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div>\n<div style=\"font-size:14px;color:#ffffff;text-decoration:none;font-weight:bold\">&nbsp;<a style=\"font-weight:bold;color:rgb(255,255,255);text-decoration:none\" shape=\"rect\" href=\"http://www.refreshbody.com/group/employeebooking?id=229274438\" alt=\"http://www.refreshbody.com/group/employeebooking?id=229274438\" target=\"_blank\">RESERVE\n NOW</a></div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<table style=\"display:table\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"left\">\n<div style=\"text-align:center\" align=\"center\">\n<div style=\"font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif\">\n<span style=\"font-size:10pt\">Please contact Refresh Body with any issues </span><a style=\"font-size:10pt;color:rgb(153,78,190);text-decoration:none\" href=\"mailto:partners@refreshbody.com\" shape=\"rect\" target=\"_blank\">support@refreshbody.com</a></div>\n<div style=\"font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif\">\n<a style=\"font-size:10pt;color:rgb(153,78,190);text-decoration:none\" href=\"mailto:partners@refreshbody.com\" shape=\"rect\" target=\"_blank\"><span style=\"color:#4c4c4c;font-size:10pt\">Or call 800-616-9271</span></a></div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 20px 15px 20px;height:1px;line-height:1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"height:1px\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:0px;background-color:#cccccc;height:1px;line-height:1px\" height=\"1\" bgcolor=\"#CCCCCC\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"display:table\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 20px 9px 20px;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:rgb(0,0,0)\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"106\" cellpadding=\"0\" cellspacing=\"0\" align=\"left\">\n<tbody>\n<tr>\n<td style=\"color:#000000;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div style=\"margin-left:120px\" align=\"center\"><a href=\"https://play.google.com/store/apps/developer?id=Refresh+Body&amp;hl=en\" shape=\"rect\" alt=\"https://play.google.com/store/apps/developer?id=Refresh+Body&amp;hl=en\" target=\"_blank\"><img height=\"34\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.103\" border=\"0\" hspace=\"0\" width=\"106\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/google_play.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"text-align:right\" width=\"106\" cellpadding=\"0\" cellspacing=\"0\" align=\"left\">\n<tbody>\n<tr>\n<td style=\"color:#000000;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div style=\"margin-left:120px\" align=\"center\"><a href=\"https://itunes.apple.com/us/app/refresh-massage-yoga-pilates/id874317687?mt=8\" shape=\"rect\" alt=\"https://itunes.apple.com/us/app/refresh-massage-yoga-pilates/id874317687?mt=8\" target=\"_blank\"><img height=\"34\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.102\" border=\"0\" hspace=\"0\" width=\"106\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/app_store.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 20px 15px 20px;height:1px;line-height:1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"height:1px\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:0px;background-color:#cccccc;height:1px;line-height:1px\" height=\"1\" bgcolor=\"#CCCCCC\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</span>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 0px 0px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"540\" cellspacing=\"0\" cellpadding=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"padding:0px;color:#333333\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img style=\"display:block;min-height:auto!important;max-width:100%!important\" height=\"91\" vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.51\" hspace=\"0\" width=\"540\" alt=\"Critical Acclaim\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/partners1.jpg\" class=\"CToWUd\"></div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<span class=\"im\">\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:15px 20px 15px 20px;height:1px;line-height:1px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table style=\"height:1px\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:0px;background-color:#cccccc;height:1px;line-height:1px\" height=\"1\" bgcolor=\"#CCCCCC\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table style=\"display:table\" border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 0px 0px;color:#333333;font-family:'Times New Roman',Times,serif;font-size:12pt\" valign=\"top\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table width=\"540\" cellspacing=\"0\" cellpadding=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"color:#333333;padding:0px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><img style=\"display:block;min-height:auto!important;max-width:100%!important\" height=\"329\" vspace=\"0\" border=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.49\" hspace=\"0\" width=\"540\" alt=\"Our Partners\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/partners2.jpg\" class=\"CToWUd a6T\" tabindex=\"0\">\n<div class=\"a6S\" dir=\"ltr\" style=\"opacity: 0.01;\">\n<div id=\":12w\" class=\"T-I J-J5-Ji aQv T-I-ax7 L3 a5q\" title=\"Download\" role=\"button\" tabindex=\"0\" aria-label=\"Download attachment \" data-tooltip-class=\"a1V\">\n<div class=\"aSK J-J5-Ji aYr\"></div>\n</div>\n</div>\n</div>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding-bottom:9px;height:1px;line-height:1px\" height=\"1\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<img height=\"1\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"5\" style=\"display:block;min-height:1px;width:5px\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n</span></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n<table border=\"0\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:9px 0px 8px 0px\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<table border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td style=\"padding:8px 0px 9px 0px;color:rgb(0,0,0)\" valign=\"top\" width=\"100%\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div><br>\n</div>\n<span class=\"im\">\n<table style=\"margin:0px 0px 0px 0px\" border=\"0\" width=\"100%\" cellpadding=\"0\" cellspacing=\"0\" align=\"center\">\n<tbody>\n<tr>\n<td style=\"padding:0px 0px 10px;color:#000000\" valign=\"center\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n</td>\n</tr>\n<tr>\n<td style=\"font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#666666\" valign=\"center\" rowspan=\"1\" colspan=\"1\" align=\"center\">\n<div>Refresh Body</div>\n<div><span style=\"font-size:13.3333330154419px\"><a style=\"color:blue;text-decoration:underline\" shape=\"rect\" href=\"http://refreshbody.com\" alt=\"http://refreshbody.com\" target=\"_blank\">refreshbody.com<br>\n</a><br>\n</span></div>\n<div><span style=\"font-size:13.3333330154419px\">\n<table style=\"width:100%;border-style:none;border-width:0\" cellspacing=\"0\" cellpadding=\"3\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"vertical-align:top;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#666666;font-size:13.3333330154419px;font-style:normal;font-weight:normal;width:290px;height:23px\" rowspan=\"1\" colspan=\"1\">\n<table style=\"text-align:center\" width=\"30\" cellpadding=\"0\" cellspacing=\"0\" align=\"right\">\n<tbody>\n<tr>\n<td style=\"color:#666666;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><a href=\"https://twitter.com/refresh_body\" shape=\"rect\" alt=\"https://twitter.com/refresh_body\" target=\"_blank\"><img height=\"28\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.98\" border=\"0\" hspace=\"0\" width=\"30\" alt=\"Twitter\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/twitter.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n<br>\n</td>\n<td style=\"vertical-align:top;font-family:'Century Gothic','ITC Avant Garde',Arial,Helvetica,sans-serif;color:#666666;font-size:13.3333330154419px;font-style:normal;font-weight:normal;width:290px;height:23px\" rowspan=\"1\" colspan=\"1\">\n<table style=\"text-align:center\" width=\"30\" cellpadding=\"0\" cellspacing=\"0\" align=\"none\">\n<tbody>\n<tr>\n<td style=\"color:#666666;padding:5px\" width=\"1%\" rowspan=\"1\" colspan=\"1\">\n<div align=\"center\"><a href=\"https://www.facebook.com/GetRefreshd\" shape=\"rect\" alt=\"https://www.facebook.com/GetRefreshd\" target=\"_blank\"><img height=\"29\" vspace=\"0\" name=\"14e311105676e2ba_ACCOUNT.IMAGE.96\" border=\"0\" hspace=\"0\" width=\"30\" alt=\"Facebook\" src=\"http://www.refreshbody.com/images/email/corporate_confirmation_email/facebook.png\" class=\"CToWUd\"></a></div>\n</td>\n</tr>\n</tbody>\n</table>\n<br>\n</td>\n</tr>\n</tbody>\n</table>\n</span></div>\n<br>\n</td>\n</tr>\n</tbody>\n</table>\n</span></td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</td>\n</tr>\n</tbody>\n</table>\n</div>\n</td>\n<td rowspan=\"1\" colspan=\"1\"><img height=\"5\" vspace=\"0\" border=\"0\" hspace=\"0\" width=\"1\" alt=\"\" src=\"https://ci5.googleusercontent.com/proxy/3wITCEd9rLrI-u_I5o-E5vPgx508Va9Ji76n_8r8_4Ohq3My8RdXtcOtE11glHbUzZyUV7dgc7jLQs0Ym-fpHfJiVFs8dC9bY9nY32XDJbcxZzI=s0-d-e1-ft#https://static.ctctcdn.com/letters/images/1101116784221/S.gif\" class=\"CToWUd\"></td>\n</tr>\n</tbody>\n</table>\n<div class=\"yj6qo ajU\">\n<div id=\":pl\" class=\"ajR\" role=\"button\" tabindex=\"0\" data-tooltip=\"Show trimmed content\" aria-label=\"Show trimmed content\">\n<img class=\"ajT\" src=\"//ssl.gstatic.com/ui/v1/icons/mail/images/cleardot.gif\"></div>\n</div>\n<div class=\"adL\"></div>\n</div>\n<div>\n<table width=\"600\" border=\"0\" bordercolor=\"#000000\" style=\"background-color:#FFFFFF; margin: 0 auto;\" cellpadding=\"0\" cellspacing=\"0\">\n<tbody>\n<tr>\n<td height=\"40\" colspan=\"2\" style=\"padding-left: 7%; padding-right: 7%;\">\n<p>Sincerely,</p>\n<p>The Refresh Body Team<br>\n<em>On-Demand, On-Location Wellness Services. Get Refresh'd</em><br>\n<span class=\"mobile_link\">800-616-9271</span> </p>\n</td>\n</tr>\n<tr>\n<td height=\"47\" colspan=\"2\" style=\"padding-left: 7%; padding-right: 7%; font-size: 12px;\">\n<p><a href=\"http://www.refreshbody.com/privacy\" target=\"_blank\">Privacy Policy</a> |\n<a href=\"http://www.refreshbody.com/terms\" target=\"_blank\">Terms of Use</a> <br>\nNote: This is an auto-generated email.&nbsp; Please do not reply.</p>\n</td>\n</tr>\n<tr>\n<td height=\"47\" colspan=\"2\" style=\"padding-left: 7%; padding-right: 7%;\"></td>\n</tr>\n</tbody>\n</table>\n<table width=\"600\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" style=\"margin: 0 auto;\">\n<tbody>\n<tr>\n<td><img class=\"image_fix\" src=\"http://www.refreshbody.com/images/email/shadow.png\" style=\"width: 100%; height: auto; margin: -3px 0 0 0; padding: 0; display: block\"></td>\n</tr>\n</tbody>\n</table>\n</div>\n</blockquote>\n<style type=\"text/css\">\n            /* Based on The MailChimp Reset INLINE: Yes. */\n            /* Client-specific Styles */\n        #outlook a {\n            padding: 0;\n        }\n\n            /* Force Outlook to provide a \"view in browser\" menu link. */\n        body {\n            width: 100% !important;\n            -webkit-text-size-adjust: 100%;\n            -ms-text-size-adjust: 100%;\n            margin: 0;\n            padding: 0;\n        }\n\n            /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/\n        .ExternalClass {\n            width: 100%;\n        }\n\n            /* Force Hotmail to display emails at full width */\n        .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {\n            line-height: 100%;\n        }\n\n            /* Forces Hotmail to display normal line spacing.  More on that: http://www.emailonacid.com/forum/viewthread/43/ */\n        #backgroundTable {\n            margin: 0;\n            padding: 0;\n            width: 100% !important;\n            line-height: 100% !important;\n        }\n\n            /* End reset */\n\n            /* Some sensible defaults for images\n            Bring inline: Yes. */\n        img {\n            outline: none;\n            text-decoration: none;\n            -ms-interpolation-mode: bicubic;\n        }\n\n        a img {\n            border: none;\n        }\n\n        .image_fix {\n            display: block;\n        }\n\n            /* Yahoo paragraph fix\n            Bring inline: Yes. */\n        p {\n            margin: 1em 0;\n        }\n\n            /* Hotmail header color reset\n            Bring inline: Yes. */\n        h1, h2, h3, h4, h5, h6 {\n            color: #8c8c8c !important;\n        }\n\n        h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {\n            color: blue !important;\n        }\n\n        h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {\n            color: red !important; /* Preferably not the same color as the normal header link color.  There is limited support for psuedo classes in email clients, this was added just for good measure. */\n        }\n\n        h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {\n            color: purple !important; /* Preferably not the same color as the normal header link color. There is limited support for psuedo classes in email clients, this was added just for good measure. */\n        }\n\n            /* Outlook 07, 10 Padding issue fix\n            Bring inline: No.*/\n        table td {\n            border-collapse: collapse;\n        }\n\n            /* Remove spacing around Outlook 07, 10 tables\n            Bring inline: Yes */\n        table {\n            border-collapse: collapse;\n            mso-table-lspace: 0pt;\n            mso-table-rspace: 0pt;\n        }\n\n            /* Styling your links has become much simpler with the new Yahoo.  In fact, it falls in line with the main credo of styling in email and make sure to bring your styles inline.  Your link colors will be uniform across clients when brought inline.\n            Bring inline: Yes. */\n        a {\n            color: blue;\n        }\n\n            /***************************************************\n            ****************************************************\n            MOBILE TARGETING\n            ****************************************************\n            ***************************************************/\n        @media only screen and (max-device-width: 480px) {\n            /* Part one of controlling phone number linking for mobile. */\n            a[href^=\"tel\"], a[href^=\"sms\"] {\n                text-decoration: none;\n                color: blue; /* or whatever your want */\n                pointer-events: none;\n                cursor: default;\n            }\n\n            .mobile_link a[href^=\"tel\"], .mobile_link a[href^=\"sms\"] {\n                text-decoration: default;\n                color: blue !important;\n                pointer-events: auto;\n                cursor: default;\n            }\n\n        }\n\n            /* More Specific Targeting */\n\n        @media only screen and (min-device-width: 768px) and (max-device-width: 1024px) {\n            /* You guessed it, ipad (tablets, smaller screens, etc) */\n            /* repeating for the ipad */\n            a[href^=\"tel\"], a[href^=\"sms\"] {\n                text-decoration: none;\n                color: blue; /* or whatever your want */\n                pointer-events: none;\n                cursor: default;\n            }\n\n            .mobile_link a[href^=\"tel\"], .mobile_link a[href^=\"sms\"] {\n                text-decoration: default;\n                color: blue !important;\n                pointer-events: auto;\n                cursor: default;\n            }\n        }\n\n        @media only screen and (-webkit-min-device-pixel-ratio: 2) {\n            /* Put your iPhone 4g styles in here */\n        }\n\n            /* Android targeting */\n        @media only screen and (-webkit-device-pixel-ratio:.75) {\n            /* Put CSS for low density (ldpi) Android layouts in here */\n        }\n\n        @media only screen and (-webkit-device-pixel-ratio:1) {\n            /* Put CSS for medium density (mdpi) Android layouts in here */\n        }\n\n        @media only screen and (-webkit-device-pixel-ratio:1.5) {\n            /* Put CSS for high density (hdpi) Android layouts in here */\n        }\n\n            /* end Android targeting */\n\n    </style></blockquote>\n\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_17.html",
    "content": "<table width=3D\"600\" border=3D\"0\" align=3D\"center\" cellpadding=3D\"0\" cellsp=\nacing=3D\"0\" bgcolor=3D\"#ffffff\"><tbody><tr><td colspan=3D\"3\"><table border=\n=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" width=3D\"100%\"></table></td></tr=\n><tr><td valign=3D\"top\" width=3D\"1\"><img src=3D\"http://gs.place.edu/file=\ns/gs/place-gs-lockup.gif\"></td></tr></tbody>\n  <tbody>\n    <tr>\n<td width=3D\"583\" align=3D\"left\">&nbsp;</td>\n    </tr>\n    <tr></tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:15pt 15px;font-fami=\nly:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:le=\nft\">Dear FOOBAR,</td>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:5pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\">It is my sincere pleasure to inform you that you have been selected for =\nmembership in the Honor Society of the School of General Studies. The Socie=\nty was created in 1997 to celebrate the academic achievement of exceptional=\n GS scholars. Only juniors or seniors with a grade point average of 3.8 or =\nabove who have completed at least 30 points at place are eligible for me=\nmbership. The chief aim of the Honor Society is to cultivate interaction am=\nong students committed to intellectual discovery and the faculty who enjoy =\nteaching them. </td>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:5pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\">Please join us for the Induction Ceremony, with a reception to follow.</=\ntd>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:5pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\"><blockquote>\n        <p><strong>Induction Ceremony <br>\n          Honor Society</strong><br>\n          <br>\n          Reception to follow.</p>\n      </blockquote></td>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:5pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\">The <a href=3D\"http://place.us6.list-manage.com/track/click?u=3D257ce=\ne2ddd47afbb8be32c6ce&amp;id=3D639cf43a42&amp;e=3D223ebffa5a\" style=3D\"color=\n:#5d82de\" target=3D\"_blank\">favor of a reply</a> is  requested by Friday, J=\nanuary 29 at 5 p.m. You are invited to bring one guest; business attire is =\nrequested.</td>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:5pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\">I look forward to celebrating with you soon.</td>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:5pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\"><p>Best wishes,<br>\n        <img src=3D\"http://gs.place.edu/files/gs/BAR_signature.jpg\" alt=\n=3D\"Dean FOO J. BAR\" width=3D\"148\" height=3D\"44\" border=3D\"0\" style=3D\"pa=\ndding-top:15px;padding-left:0px\" title=3D\"place University School of Gen=\neral Studies\"><br>\n        FOO J. BAR<br>\n        Dean <br>\n        University<br>\n        <br>\n        <br>\n        N.B. A printed letter concerning your selection has been mailed to =\nyour local address.</p></td>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:0pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\">&nbsp;</td>\n    </tr>\n    <tr>\n      <td colspan=3D\"2\" valign=3D\"top\" style=3D\"padding:0pt 15px;font-famil=\ny:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\nt\"></td>\n    </tr>\n  </tbody>\n  <tbody>\n    <tr>\n      <td valign=3D\"top\" width=3D\"598\"><table border=3D\"0\" cellpadding=3D\"0=\n\" cellspacing=3D\"0\" width=3D\"594\"></table></td><td align=3D\"left\" valign=3D=\n\"top\" width=3D\"1\"><img src=3D\"http://www.place.edu/cu/gs/images/yrp_spac=\ner.jpg\"></td>\n    </tr>\n  </tbody>\n</table>\n            <center>\n                <br>\n                <br>\n                <br>\n                <br>\n                <br>\n                <br>\n                <table border=3D\"0\" cellpadding=3D\"0\" cellspacing=3D\"0\" wid=\nth=3D\"100%\" style=3D\"background-color:#ffffff;border-top:1px solid #e5e5e5\"=\n>\n                    <tbody><tr>\n                        <td align=3D\"center\" valign=3D\"top\" style=3D\"paddin=\ng-top:20px;padding-bottom:20px\">\n                            <table border=3D\"0\" cellpadding=3D\"0\" cellspaci=\nng=3D\"0\">\n                                <tbody><tr>\n                                    <td align=3D\"center\" valign=3D\"top\" sty=\nle=3D\"color:#606060;font-family:Helvetica,Arial,sans-serif;font-size:11px;l=\nine-height:150%;padding-right:20px;padding-bottom:5px;padding-left:20px;tex=\nt-align:center\">\n                                        This email was sent to <a href=3D\"m=\nailto:FOOBAR.BAZ@place.edu\" style=3D\"color:#404040!important\" target=3D\"_b=\nlank\">FOOBAR.BAZ@place.edu</a>\n                                        <br>\n                                        <a href=3D\"http://place.us6.list=\n-manage1.com/about?u=xxxxxxxxxxxxxxxxxxxxxxxxxxx&amp;id=xxxxxxxxxxxx&amp;e=\n=3D223ebffa5a&amp;c=3Df2c0d84500\" style=3D\"color:#404040!important\" target=\n=3D\"_blank\"><em>why did I get this?</em></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\n=3D\"http://place.us6.list-manage1.com/unsubscribe?u=xxxxxxxxxxxxxxxxxxxx=\ne32c6ce&amp;id=3Dcebd346d3d&amp;e=3D223ebffa5a&amp;c=3Df2c0d84500\" style=3D=\n\"color:#404040!important\" target=3D\"_blank\">unsubscribe from this list</a>&=\nnbsp;&nbsp;&nbsp;&nbsp;<a href=3D\"http://place.us6.list-manage2.com/prof=\nile?u=3D257cee2ddd47afbb8be32c6ce&amp;id=3Dcebd346d3d&amp;e=3D223ebffa5a\" s=\ntyle=3D\"color:#404040!important\" target=3D\"_blank\">update subscription pref=\nerences</a>\n                                        <br>\n                                       place \n                                        <br>\n                                        <br>\n                                       =20\n                                    </td>\n                                </tr>\n                            </tbody></table>\n                        </td>\n                    </tr>\n                </tbody></table>\n            </center>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_17_stripped.html",
    "content": "<table width=\"3D&quot;600&quot;\" border=\"3D&quot;0&quot;\" align=\"3D&quot;center&quot;\" cellpadding=\"3D&quot;0&quot;\" cellsp=\"acing=3D&quot;0&quot;\" bgcolor=\"3D&quot;#ffffff&quot;\"><tbody><tr><td colspan=\"3D&quot;3&quot;\"><table border=\"=3D&quot;0&quot;\" cellpadding=\"3D&quot;0&quot;\" cellspacing=\"3D&quot;0&quot;\" width=\"3D&quot;100%&quot;\"></table></td></tr><tr><td valign=\"3D&quot;top&quot;\" width=\"3D&quot;1&quot;\"><img src=\"3D&quot;http://gs.place.edu/file=\" s=\"\" gs=\"\" place-gs-lockup.gif\"=\"\"></td></tr></tbody>\n  <tbody>\n    <tr>\n<td width=\"3D&quot;583&quot;\" align=\"3D&quot;left&quot;\">&nbsp;</td>\n    </tr>\n    <tr></tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:15pt\" 15px;font-fami=\"ly:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:le=\" ft\"=\"\">Dear FOOBAR,</td>\n    </tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:5pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\">It is my sincere pleasure to inform you that you have been selected for =\nmembership in the Honor Society of the School of General Studies. The Socie=\nty was created in 1997 to celebrate the academic achievement of exceptional=\n GS scholars. Only juniors or seniors with a grade point average of 3.8 or =\nabove who have completed at least 30 points at place are eligible for me=\nmbership. The chief aim of the Honor Society is to cultivate interaction am=\nong students committed to intellectual discovery and the faculty who enjoy =\nteaching them. </td>\n    </tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:5pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\">Please join us for the Induction Ceremony, with a reception to follow.<!--=\ntd-->\n    </td></tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:5pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\"><blockquote>\n        <p><strong>Induction Ceremony <br>\n          Honor Society</strong><br>\n          <br>\n          Reception to follow.</p>\n      </blockquote></td>\n    </tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:5pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\">The <a href=\"3D&quot;http://place.us6.list-manage.com/track/click?u=3D257ce=\" e2ddd47afbb8be32c6ce&amp;id=\"3D639cf43a42&amp;e=3D223ebffa5a&quot;\" style=\"3D&quot;color=\" :#5d82de\"=\"\" target=\"3D&quot;_blank&quot;\">favor of a reply</a> is  requested by Friday, J=\nanuary 29 at 5 p.m. You are invited to bring one guest; business attire is =\nrequested.</td>\n    </tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:5pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\">I look forward to celebrating with you soon.</td>\n    </tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:5pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\"><p>Best wishes,<br>\n        <img src=\"3D&quot;http://gs.place.edu/files/gs/BAR_signature.jpg&quot;\" alt=\"=3D&quot;Dean\" foo=\"\" j.=\"\" bar\"=\"\" width=\"3D&quot;148&quot;\" height=\"3D&quot;44&quot;\" border=\"3D&quot;0&quot;\" style=\"3D&quot;pa=\" dding-top:15px;padding-left:0px\"=\"\" title=\"3D&quot;place\" university=\"\" school=\"\" of=\"\" gen=\"eral\" studies\"=\"\"><br>\n        FOO J. BAR<br>\n        Dean <br>\n        University<br>\n        <br>\n        <br>\n        N.B. A printed letter concerning your selection has been mailed to =\nyour local address.</p></td>\n    </tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:0pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\">&nbsp;</td>\n    </tr>\n    <tr>\n      <td colspan=\"3D&quot;2&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;padding:0pt\" 15px;font-famil=\"y:Arial,Helvetica,sans-serif;font-size:12px;color:rgb(0,0,0);text-align:lef=\" t\"=\"\"></td>\n    </tr>\n  </tbody>\n  <tbody>\n    <tr>\n      <td valign=\"3D&quot;top&quot;\" width=\"3D&quot;598&quot;\"><table border=\"3D&quot;0&quot;\" cellpadding=\"3D&quot;0=\" \"=\"\" cellspacing=\"3D&quot;0&quot;\" width=\"3D&quot;594&quot;\"></table></td><td align=\"3D&quot;left&quot;\" valign=\"3D=\" \"top\"=\"\" width=\"3D&quot;1&quot;\"><img src=\"3D&quot;http://www.place.edu/cu/gs/images/yrp_spac=\" er.jpg\"=\"\"></td>\n    </tr>\n  </tbody>\n</table>\n            <center>\n                <br>\n                <br>\n                <br>\n                <br>\n                <br>\n                <br>\n                <table border=\"3D&quot;0&quot;\" cellpadding=\"3D&quot;0&quot;\" cellspacing=\"3D&quot;0&quot;\" wid=\"th=3D&quot;100%&quot;\" style=\"3D&quot;background-color:#ffffff;border-top:1px\" solid=\"\" #e5e5e5\"=\"\">\n                    <tbody><tr>\n                        <td align=\"3D&quot;center&quot;\" valign=\"3D&quot;top&quot;\" style=\"3D&quot;paddin=\" g-top:20px;padding-bottom:20px\"=\"\">\n                            <table border=\"3D&quot;0&quot;\" cellpadding=\"3D&quot;0&quot;\" cellspaci=\"ng=3D&quot;0&quot;\">\n                                <tbody><tr>\n                                    <td align=\"3D&quot;center&quot;\" valign=\"3D&quot;top&quot;\" sty=\"le=3D&quot;color:#606060;font-family:Helvetica,Arial,sans-serif;font-size:11px;l=\" ine-height:150%;padding-right:20px;padding-bottom:5px;padding-left:20px;tex=\"t-align:center&quot;\">\n                                        This email was sent to <a href=\"3D&quot;m=\" ailto:foobar.baz@place.edu\"=\"\" style=\"3D&quot;color:#404040!important&quot;\" target=\"3D&quot;_b=\" lank\"=\"\">FOOBAR.BAZ@place.edu</a>\n                                        <br>\n                                        <a href=\"3D&quot;http://place.us6.list=\" -manage1.com=\"\" about?u=\"xxxxxxxxxxxxxxxxxxxxxxxxxxx&amp;id=xxxxxxxxxxxx&amp;e=\" =3d223ebffa5a&amp;c=\"3Df2c0d84500&quot;\" style=\"3D&quot;color:#404040!important&quot;\" target=\"=3D&quot;_blank&quot;\"><em>why did I get this?</em></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"=3D&quot;http://place.us6.list-manage1.com/unsubscribe?u=xxxxxxxxxxxxxxxxxxxx=\" e32c6ce&amp;id=\"3Dcebd346d3d&amp;e=3D223ebffa5a&amp;c=3Df2c0d84500&quot;\" style=\"3D=\" \"color:#404040!important\"=\"\" target=\"3D&quot;_blank&quot;\">unsubscribe from this list</a>&amp;=\nnbsp;&nbsp;&nbsp;&nbsp;<a href=\"3D&quot;http://place.us6.list-manage2.com/prof=\" ile?u=\"3D257cee2ddd47afbb8be32c6ce&amp;id=3Dcebd346d3d&amp;e=3D223ebffa5a&quot;\" s=\"tyle=3D&quot;color:#404040!important&quot;\" target=\"3D&quot;_blank&quot;\">update subscription pref=\nerences</a>\n                                        <br>\n                                       place \n                                        <br>\n                                        <br>\n                                       =20\n                                    </td>\n                                </tr>\n                            </tbody></table>\n                        </td>\n                    </tr>\n                </tbody></table>\n            </center>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_18.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<h1>README:</h1>\n<strong>So this is an interesting test case. The email below looks like it failed quoted text detection. However, you can see that there is some plain text (the signature) at the bottom of the email after the blockquote. Unfortunately this looks arguably identical to someone who inline-replied to a message after a piece of quoted text. As such there's not a lot we can do about this until we can come up with an efficient way to inspect the bodies of previous messages. This is likely something that will have to happen server-side.</strong>\n<div dir=\"ltr\">Hi,\n<div><br>\n</div>\n<div>TEXT</div>\n<div><br>\n</div>\n<div>Regards,</div>\n<div>FROM</div>\n</div>\n<div>\n<div><br>\n</div>\n<div>On Thu, Mar 3, 2016 at 3:19 AM, Nylas <span dir=\"ltr\">&lt;<a href=\"mailto:test@nylas.com\" target=\"_blank\">test@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Hey Recipient,\n<div>\n<div>\n<div><br>\n</div>\n</div>\n<div>Checking in -- will you guys be needing to test with 10&#43; accounts soon?</div>\n<span>\n<div>\n<div><br>\n</div>\n</div>\n<div>Best,</div>\n<div>Nylas</div>\n<br>\n<div>--&nbsp;<br style=\"font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:16px;line-height:24px\">\n<div style=\"max-width:100%;font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:15px;line-height:24px\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div style=\"max-width:100%\"><span style=\"font-weight:500\">Test Sender</span></div>\n<div style=\"max-width:100%;font-size:12.8px\">\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Head of Business Development and Growth</span></div>\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Nylas Inc.</span></div>\n<div style=\"max-width:100%;font-size:12.8px\"><a title=\"tel:123\" style=\"color:rgb(16,129,247);font-size:12.8px\"></a><br>\n</div>\n<div style=\"max-width:100%;font-size:12.8px\"><a href=\"https://link.nylas.com/link/8gsph0digeh6kyasmamhe4xr/1faaa7209b204b2fb0ecb16751007ec8/0?redirect=http%3A%2F%2Fwww.nylas.com%2F\" title=\"http://www.nylas.com/\" style=\"font-size:12.8px;color:rgb(16,129,247);background-color:rgb(255,255,255)\" target=\"_blank\">nylas.com</a></div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</span></div>\n<br>\n<img width=\"0\" height=\"0\" style=\"border:0;width:0;min-height:0\" src=\"https://link.nylas.com/open/8gsph0digeh6kyasmamhe4xr/3816aed3237c4a7abdda4c962b0e3b65\">\n<div>\n<div>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\nOn Feb 10 2016, at 3:28 am, Recipient Name &lt;<a href=\"mailto:email.name@nylas.com\" target=\"_blank\">email.name@nylas.com</a>&gt; wrote:\n<br>\n<div dir=\"ltr\">Fantastic! Thank you, Nylas.\n<div>\n<div><br>\n</div>\n</div>\n<div>Have a good day,</div>\n<div>Recipient</div>\n</div>\n<div>\n<div><br>\n</div>\n<div>On Wed, Feb 10, 2016 at 1:27 AM, Test Sender <span dir=\"ltr\">&lt;<a href=\"mailto:test@nylas.com\" target=\"_blank\">test@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Hi Recipient,\n<div>\n<div>\n<div><br>\n</div>\n</div>\n<div>CONTENT 4</div>\n<div>\n<div><br>\n</div>\n</div>\n<div>CONTENT 5</div>\n<span>\n<div>\n<div><br>\n</div>\n</div>\n<div>Best,</div>\n<div>Nylas</div>\n<div>\n<div><br>\n</div>\n</div>\n<div>--&nbsp;<br style=\"font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:16px;line-height:24px\">\n<div style=\"max-width:100%;font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:15px;line-height:24px\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div style=\"max-width:100%\"><span style=\"font-weight:500\">Test Sender</span></div>\n<div style=\"max-width:100%;font-size:12.8px\">\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Head of Business Development and Growth</span></div>\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Nylas Inc.</span></div>\n<div style=\"max-width:100%;font-size:12.8px\"><a title=\"tel:123\" style=\"color:rgb(16,129,247);font-size:12.8px\">num</a><br>\n</div>\n<div style=\"max-width:100%;font-size:12.8px\"><a href=\"http://www.nylas.com/\" title=\"http://www.nylas.com/\" style=\"font-size:12.8px;color:rgb(16,129,247);background-color:rgb(255,255,255)\" target=\"_blank\">nylas.com</a></div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</span></div>\n<div>\n<div>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\nOn Feb 9 2016, at 12:49 am, Recipient Name &lt;<a href=\"mailto:email.name@nylas.com\" target=\"_blank\">email.name@nylas.com</a>&gt; wrote:\n<br>\n<div dir=\"ltr\">Hi Nylas,\n<div>\n<div><br>\n</div>\n</div>\n<div>Content 1<br>\n<br>\nContent 2<br>\n<br>\nRegards,<br>\nRecipient</div>\n</div>\n<div>\n<div><br>\n</div>\n<div>On Tue, Feb 9, 2016 at 1:37 AM, Test Sender <span dir=\"ltr\">&lt;<a href=\"mailto:test@nylas.com\" target=\"_blank\">test@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Thanks APerson!\n<div>\n<div>\n<div><br>\n</div>\n</div>\n<div>Content 3\n</div>\n<span>\n<div>\n<div><br>\n</div>\n</div>\n<div>Best,</div>\n<div>Nylas</div>\n<br>\n<div>--&nbsp;<br style=\"font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:16px;line-height:24px\">\n<div style=\"max-width:100%;font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:15px;line-height:24px\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div style=\"max-width:100%\"><span style=\"font-weight:500\">Test Sender</span></div>\n<div style=\"max-width:100%;font-size:12.8px\">\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Head of Business Development and Growth</span></div>\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Nylas Inc.</span></div>\n<div style=\"max-width:100%;font-size:12.8px\"><a title=\"tel:123\" style=\"color:rgb(16,129,247);font-size:12.8px\">num</a><br>\n</div>\n<div style=\"max-width:100%;font-size:12.8px\"><a href=\"http://www.nylas.com/\" title=\"http://www.nylas.com/\" style=\"font-size:12.8px;color:rgb(16,129,247);background-color:rgb(255,255,255)\" target=\"_blank\">nylas.com</a></div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</span></div>\n<div>\n<div>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\nOn Feb 8 2016, at 3:33 pm, Another Person &lt;<a href=\"mailto:another.email@nylas.com\" target=\"_blank\">another.email@nylas.com</a>&gt; wrote:\n<br>\n<div dir=\"ltr\">CONTENT 8\n<div>\n<div><br>\n</div>\n</div>\n<div>Regards,</div>\n<div>APerson</div>\n<div>\n<div><br>\n</div>\n<div>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\"><span style=\"border-collapse:collapse;color:rgb(136,136,136);font-family:arial,sans-serif;font-size:13px\">\n<div>\n<div>\n<div>Another Person</div>\n<div>Co-founder &amp; President of Pipedrive</div>\n<div><a value=\"&#43;14153200885\">(415) 320-0885</a></div>\n<div><a href=\"http://linkedin.com/in/timorein\" style=\"color:rgb(0,0,204)\" target=\"_blank\">linkedin.com/in/timorein</a></div>\n<div><a href=\"http://www.pipedrive.com/\" style=\"color:rgb(0,0,204)\" target=\"_blank\">www.pipedrive.com</a></div>\n</div>\n</div>\n</span></div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n<br>\n<div>On Mon, Feb 8, 2016 at 2:27 PM, Test Sender <span dir=\"ltr\">&lt;<a href=\"mailto:test@nylas.com\" target=\"_blank\">test@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Hi APerson,\n<div>\n<div>\n<div><br>\n</div>\n</div>\n<div>CONTENT 9</div>\n<div>\n<div><br>\n</div>\n</div>\n<div>CONTENT 10</div>\n<div>\n<div><br>\n</div>\n</div>\n<div>Best,</div>\n<div>\n<div><br>\n</div>\n</div>\n<div>Nylas<span><span></span></span></div>\n<span>\n<div>\n<div><br>\n</div>\n</div>\n<br>\n<div>--&nbsp;<br style=\"font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:16px;line-height:24px\">\n<div style=\"max-width:100%;font-family:Nylas-Pro,Helvetica,'Lucidia Grande',sans-serif;font-size:15px;line-height:24px\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div dir=\"ltr\" style=\"max-width:100%\">\n<div style=\"max-width:100%\">\n<div style=\"max-width:100%\"><span style=\"font-weight:500\">Test Sender</span></div>\n<div style=\"max-width:100%;font-size:12.8px\">\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Head of Business Development and Growth</span></div>\n<div style=\"max-width:100%\"><span style=\"font-size:12.8px\">Nylas Inc.</span></div>\n<div style=\"max-width:100%;font-size:12.8px\"><a title=\"tel:123\" style=\"color:rgb(16,129,247);font-size:12.8px\">num</a><br>\n</div>\n<div style=\"max-width:100%;font-size:12.8px\"><a href=\"http://www.nylas.com/\" title=\"http://www.nylas.com/\" style=\"font-size:12.8px;color:rgb(16,129,247);background-color:rgb(255,255,255)\" target=\"_blank\">nylas.com</a></div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</span></div>\n<div>\n<div>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\nOn Feb 8 2016, at 1:40 pm, Another Person &lt;<a href=\"mailto:another.email@nylas.com\" target=\"_blank\">another.email@nylas.com</a>&gt; wrote:\n<br>\n<div dir=\"ltr\">Hey Nylas,\n<div>\n<div><br>\n</div>\n</div>\n<div>CONTENT 11</div>\n<div>\n<div><br>\n</div>\n</div>\n<div>CONTENT 12<br>\n<br>\n<blockquote style=\"margin:0px 0px 0px 0.8ex;border-left-width:1px;border-left-color:rgb(204,204,204);border-left-style:solid;padding-left:1ex\">\n<div style=\"font-size:12.8px\">CONTENT 13</div>\n<div style=\"font-size:12.8px\">\n<div><br>\n</div>\nCONTENT 14</div>\n</blockquote>\n<div>\n<div><br>\n</div>\n</div>\n<div>CONTENT 15</div>\n<div>\n<div><br>\n</div>\n</div>\n<div>Regards,</div>\n<div>APerson&nbsp;</div>\n</div>\n<div>\n<div><br>\n</div>\n<div>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\"><span style=\"border-collapse:collapse;color:rgb(136,136,136);font-family:arial,sans-serif;font-size:13px\">\n<div>\n<div>\n<div>Another Person</div>\n<div>Co-founder &amp; President of Place</div>\n<div><a value=\"&#43;14153200885\">num</a></div>\n<div><a href=\"http://linkedin.com/in/test\" style=\"color:rgb(0,0,204)\" target=\"_blank\">linkedin.com/in/test</a></div>\n<div><a href=\"http://www.nylas.com/\" style=\"color:rgb(0,0,204)\" target=\"_blank\">www.nylas.com</a></div>\n</div>\n</div>\n</span></div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div>\n<div><br>\n</div>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Test Person</div>\n<div>Product Manager | Pipedrive</div>\n<div>@testtwitter</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div>\n<div><br>\n</div>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Test Person</div>\n<div>Product Manager | Pipedrive</div>\n<div>@testtwitter</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Test Person</div>\n<div>Product Manager | Pipedrive</div>\n<div>@testtwitter</div>\n</div>\n</div>\n</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_18_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<h1>README:</h1>\n<strong>So this is an interesting test case. The email below looks like it failed quoted text detection. However, you can see that there is some plain text (the signature) at the bottom of the email after the blockquote. Unfortunately this looks arguably identical to someone who inline-replied to a message after a piece of quoted text. As such there's not a lot we can do about this until we can come up with an efficient way to inspect the bodies of previous messages. This is likely something that will have to happen server-side.</strong>\n<div dir=\"ltr\">Hi,\n<div><br>\n</div>\n<div>TEXT</div>\n<div><br>\n</div>\n<div>Regards,</div>\n<div>FROM</div>\n</div>\n<div>\n<div><br>\n</div>\n<div><br>\n</div></div></body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_19.html",
    "content": "<html>\n<head>\n<meta http-equiv=3D\"Content-Type\" content=3D\"text/html; charset=3Diso-8859-=\n1\">\n</head>\n<body>\nand again<br>\n<br>\n<signature>Sent from <a href=3D\"https://link.nylas.com/link/aoi4q8bqixxd59z=\nvuog74b15d/local-1b7450ec-1444/0?redirect=3Dhttps%3A%2F%2Fnylas.com%2Fn1%3F=\nref%3Dn1\">\nNylas Mail</a>, the extensible, open source mail client.</signature><img clas=\ns=3D\"n1-open\" width=3D\"0\" height=3D\"0\" style=3D\"border:0; width:0; height:0=\n;\" src=3D\"https://link.nylas.com/open/aoi4q8bqixxd59zvuog74b15d/local-1b745=\n0ec-1444\">\n<div class=3D\"gmail_quote nylas-quote nylas-quote-id-12zz9ff2coj10pe2e6g23i=\nib4\"><br>\nOn Nov 4 2016, at 2:28 pm, Juan Tejada &lt;juan@nylas.com&gt; wrote: <br>\n<blockquote class=3D\"gmail_quote\" style=3D\"margin:0 0 0 .8ex;border-left:1p=\nx #ccc solid;padding-left:1ex;\">\nhey evan sorry to spam you so much<br>\n<br>\nSent from <a href=3D\"https://link.nylas.com/link/aoi4q8bqixxd59zvuog74b15d/=\nlocal-038a4c1f-8bd7/0?redirect=3Dhttps%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">\nNylas Mail</a>, the extensible, open source mail client. <img width=3D\"0\" hei=\nght=3D\"0\" style=3D\"border:0; width:0; height:0;\" src=3D\"https://link.nylas.=\ncom/open/aoi4q8bqixxd59zvuog74b15d/local-038a4c1f-8bd7\">\n<div><br>\nOn Nov 4 2016, at 2:20 pm, Juan Tejada &lt;juan@nylas.com&gt; wrote: <br>\n<blockquote style=3D\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-l=\neft:1ex;\">\nwat<br>\n<br>\nSent from <a href=3D\"https://link.nylas.com/link/aoi4q8bqixxd59zvuog74b15d/=\nlocal-fa431492-4362/0?redirect=3Dhttps%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">\nNylas Mail</a>, the extensible, open source mail client.<img width=3D\"0\" heig=\nht=3D\"0\" style=3D\"border:0; width:0; height:0;\" src=3D\"https://link.nylas.c=\nom/open/aoi4q8bqixxd59zvuog74b15d/local-fa431492-4362\">\n<div><br>\nOn Nov 4 2016, at 1:19 pm, Juan Tejada &lt;juan@nylas.com&gt; wrote: <br>\n<blockquote style=3D\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-l=\neft:1ex;\">\nthis should only happen once<br>\n<br>\nSent from <a href=3D\"https://link.nylas.com/link/aoi4q8bqixxd59zvuog74b15d/=\nlocal-7e7f9d5f-73ac/0?redirect=3Dhttps%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">\nNylas Mail</a>, the extensible, open source mail client.<img width=3D\"0\" heig=\nht=3D\"0\" style=3D\"border:0; width:0; height:0;\" src=3D\"https://link.nylas.c=\nom/open/aoi4q8bqixxd59zvuog74b15d/local-7e7f9d5f-73ac\">\n<div><br>\nOn Nov 4 2016, at 1:13 pm, Juan Tejada &lt;juan@nylas.com&gt; wrote: <br>\n<blockquote style=3D\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-l=\neft:1ex;\">\nand again<br>\n<br>\nSent from <a href=3D\"https://link.nylas.com/link/aoi4q8bqixxd59zvuog74b15d/=\nlocal-da59d244-e3f8/0?redirect=3Dhttps%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\">\nNylas Mail</a>, the extensible, open source mail client.<img width=3D\"0\" heig=\nht=3D\"0\" style=3D\"border:0; width:0; height:0;\" src=3D\"https://link.nylas.c=\nom/open/aoi4q8bqixxd59zvuog74b15d/local-da59d244-e3f8\">\n<div><br>\nOn Nov 4 2016, at 1:13 pm, Juan Tejada &lt;juan@nylas.com&gt; wrote: <br>\n<blockquote style=3D\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-l=\neft:1ex;\">\nand some other stuff<br>\n<br>\nSent from <a href=3D\"https://nylas.com/n1?ref=3Dn1\">Nylas Mail</a>, the exten=\nsible, open source mail client.\n</blockquote>\n</div>\n</blockquote>\n</div>\n</blockquote>\n</div>\n</blockquote>\n</div>\n</blockquote>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_19_stripped.html",
    "content": "<head>\n<meta http-equiv=\"3D&quot;Content-Type&quot;\" content=\"3D&quot;text/html;\" charset=\"3Diso-8859-=\" 1\"=\"\">\n</head>\n<body>\nand again<br>\n<br>\n<signature>Sent from <a href=\"3D&quot;https://link.nylas.com/link/aoi4q8bqixxd59z=\" vuog74b15d=\"\" local-1b7450ec-1444=\"\" 0?redirect=\"3Dhttps%3A%2F%2Fnylas.com%2Fn1%3F=\" ref%3dn1\"=\"\">\nNylas Mail</a>, the extensible, open source mail client.</signature><img clas=\"s=3D&quot;n1-open&quot;\" width=\"3D&quot;0&quot;\" height=\"3D&quot;0&quot;\" style=\"3D&quot;border:0;\" width:0;=\"\" height:0=\";&quot;\" src=\"3D&quot;https://link.nylas.com/open/aoi4q8bqixxd59zvuog74b15d/local-1b745=\" 0ec-1444\"=\"\">\n<div class=\"3D&quot;gmail_quote\" nylas-quote=\"\" nylas-quote-id-12zz9ff2coj10pe2e6g23i=\"ib4&quot;\"><br><br>\n\n</div>\n\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_1.txt",
    "content": "Hi folks\n\nWhat is the best way to clear a Riak bucket of all key, values after \nrunning a test?\nI am currently using the Java HTTP API.\n\n-Abhishek Kona\n\n\n_______________________________________________\nriak-users mailing list\nriak-users@lists.basho.com\nhttp://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_2.txt",
    "content": "Hi,\nOn Tue, 2011-03-01 at 18:02 +0530, Abhishek Kona wrote:\n> Hi folks\n> \n> What is the best way to clear a Riak bucket of all key, values after \n> running a test?\n> I am currently using the Java HTTP API.\n\nYou can list the keys for the bucket and call delete for each. Or if you\nput the keys (and kept track of them in your test) you can delete them\none at a time (without incurring the cost of calling list first.)\n\nSomething like:\n\n        String bucket = \"my_bucket\";\n        BucketResponse bucketResponse = riakClient.listBucket(bucket);\n        RiakBucketInfo bucketInfo = bucketResponse.getBucketInfo();\n        \n        for(String key : bucketInfo.getKeys()) {\n            riakClient.delete(bucket, key);\n        }\n\n\nwould do it.\n\nSee also \n\nhttp://wiki.basho.com/REST-API.html#Bucket-operations\n\nwhich says \n\n\"At the moment there is no straightforward way to delete an entire\nBucket. There is, however, an open ticket for the feature. To delete all\nthe keys in a bucket, you’ll need to delete them all individually.\"\n\n> \n> -Abhishek Kona\n> \n> \n> _______________________________________________\n> riak-users mailing list\n> riak-users@lists.basho.com\n> http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n\n\n\n\n_______________________________________________\nriak-users mailing list\nriak-users@lists.basho.com\nhttp://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_3.txt",
    "content": "Oh thanks.\n\nHaving the function would be great.\n\n-Abhishek Kona\n\nOn 01/03/11 7:07 PM, Russell Brown wrote:\n> Hi,\n> On Tue, 2011-03-01 at 18:02 +0530, Abhishek Kona wrote:\n>> Hi folks\n>>\n>> What is the best way to clear a Riak bucket of all key, values after\n>> running a test?\n>> I am currently using the Java HTTP API.\n> You can list the keys for the bucket and call delete for each. Or if you\n> put the keys (and kept track of them in your test) you can delete them\n> one at a time (without incurring the cost of calling list first.)\n>\n> Something like:\n>\n>          String bucket = \"my_bucket\";\n>          BucketResponse bucketResponse = riakClient.listBucket(bucket);\n>          RiakBucketInfo bucketInfo = bucketResponse.getBucketInfo();\n>\n>          for(String key : bucketInfo.getKeys()) {\n>              riakClient.delete(bucket, key);\n>          }\n>\n>\n> would do it.\n>\n> See also\n>\n> http://wiki.basho.com/REST-API.html#Bucket-operations\n>\n> which says\n>\n> \"At the moment there is no straightforward way to delete an entire\n> Bucket. There is, however, an open ticket for the feature. To delete all\n> the keys in a bucket, you’ll need to delete them all individually.\"\n>\n>> -Abhishek Kona\n>>\n>>\n>> _______________________________________________\n>> riak-users mailing list\n>> riak-users@lists.basho.com\n>> http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n>\n\n\n_______________________________________________\nriak-users mailing list\nriak-users@lists.basho.com\nhttp://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_4.txt",
    "content": "Awesome! I haven't had another problem with it.\n\nOn Aug 22, 2011, at 7:37 PM, defunkt<reply@reply.github.com> wrote:\n\n> Loader seems to be working well.\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_5.txt",
    "content": "One: Here's what I've got.\n\n- This would be the first bullet point that wraps to the second line\nto the next\n- This is the second bullet point and it doesn't wrap\n- This is the third bullet point and I'm having trouble coming up with enough\nto say\n- This is the fourth bullet point\n\nTwo:\n- Here is another bullet point\n- And another one\n\nThis is a paragraph that talks about a bunch of stuff. It goes on and on\nfor a while.\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_6.txt",
    "content": "I get proper rendering as well.\n\nSent from a magnificent torch of pixels\n\nOn Dec 16, 2011, at 12:47 PM, Corey Donohoe\n<reply@reply.github.com>\nwrote:\n\n> Was this caching related or fixed already?  I get proper rendering here.\n>\n> ![](https://img.skitch.com/20111216-m9munqjsy112yqap5cjee5wr6c.jpg)\n>\n> ---\n> Reply to this email directly or view it on GitHub:\n> https://github.com/github/github/issues/2278#issuecomment-3182418\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_7.txt",
    "content": "Oh thanks.\n\nOn the topic of having text, this should show too.\n\n-Abhishek Kona\n\nOn 01/03/11 7:07 PM, Russell Brown wrote:\n> Hi,\n> On Tue, 2011-03-01 at 18:02 +0530, Abhishek Kona wrote:\n>> Hi folks\n>>\n>> What is the best way to clear a Riak bucket of all key, values after\n>> running a test?\n>> I am currently using the Java HTTP API.\n> You can list the keys for the bucket and call delete for each. Or if you\n> put the keys (and kept track of them in your test) you can delete them\n> one at a time (without incurring the cost of calling list first.)\n>\n> Something like:\n>\n>          String bucket = \"my_bucket\";\n>          BucketResponse bucketResponse = riakClient.listBucket(bucket);\n>          RiakBucketInfo bucketInfo = bucketResponse.getBucketInfo();\n>\n>          for(String key : bucketInfo.getKeys()) {\n>              riakClient.delete(bucket, key);\n>          }\n>\n>\n> would do it.\n>\n> See also\n>\n> http://wiki.basho.com/REST-API.html#Bucket-operations\n>\n> which says\n>\n> \"At the moment there is no straightforward way to delete an entire\n> Bucket. There is, however, an open ticket for the feature. To delete all\n> the keys in a bucket, you’ll need to delete them all individually.\"\n>\n>> -Abhishek Kona\n>>\n>>\n>> _______________________________________________\n>> riak-users mailing list\n>> riak-users@lists.basho.com\n>> http://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n>\n\n\n_______________________________________________\nriak-users mailing list\nriak-users@lists.basho.com\nhttp://lists.basho.com/mailman/listinfo/riak-users_lists.basho.com\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_8.txt",
    "content": "On Tue, Apr 29, 2014 at 4:22 PM, Example Dev <sugar@example.com>wrote:\n\n> okay.  Well, here's some stuff I can write.\n>\n> And if I write a 2 second line you and maybe reply under this?\n>\n> Or if you didn't really feel like it, you could reply under this line.Or\n> if you didn't really feel like it, you could reply under this line. Or if\n> you didn't really feel like it, you could reply under this line. Or if you\n> didn't really feel like it, you could reply under this line.\n>\n\nI will reply under this one\n\n>\n> okay?\n>\n\nand under this.\n\n>\n> -- Tim\n>\n> On Tue, April 29, 2014 at 4:21 PM, Tim Haines <tmhaines@example.com> wrote:\n> > hi there\n> >\n> > After you reply to this I'm going to send you some inline responses.\n> >\n> > --\n> > Hey there, this is my signature\n>\n>\n>\n\n\n--\nHey there, this is my signature\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_1_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n</head>\n<body style=\"word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\">\n<div><span class=\"message_content\">\n<pre class=\"special_formatting\"><font face=\"Calibri\"><span style=\"font-size: 15px;\">Hi Jeff,\n\nQuick update on the event bugs:\n- I fixed the bug where events would be incorrectly marked as read-only.\n- We expose RRULEs as valid JSON now. \n\nWe're currently testing the fixes, they should ship early next week.\n\nConcerning timezones, an event should always be associated with a timezone. Having a NULL value instead is a bug on our end. I will be working on fixing this on Monday and will let you know when it's fixed.\n\nThanks for your detailed bug reports,</span></font></pre>\n<pre class=\"special_formatting\"><font face=\"Calibri\"><span style=\"font-size: 15px;\">\nKarim</span></font></pre>\n<pre class=\"special_formatting\" style=\"color: rgb(0, 0, 0); font-family: Calibri, sans-serif; font-size: 14px;\"><span style=\"font-family: Calibri; font-size: 11pt; font-weight: bold;\">From: </span><span style=\"font-family: Calibri; font-size: 11pt;\"> Kavya Joshi &lt;</span><a href=\"mailto:kavya@nylas.com\" style=\"font-family: Calibri; font-size: 11pt;\">kavya@nylas.com</a><span style=\"font-family: Calibri; font-size: 11pt;\">&gt;</span></pre>\n</span></div>\n\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_2.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:10pt;color:#000000;background-color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;\">\n<p>It's be great to talk with Jonathan-- feel free to connect us. Thanks.<br>\n</p>\n<p><br>\n</p>\n<p>The bug I mentioned manifested itself like this:<br>\n</p>\n<p><br>\n</p>\n<div class=\"remarkup-code-block\" data-code-lang=\"text\" data-sigil=\"remarkup-code-block\" style=\"margin: 0px 0px 12px; padding: 0px; border: 0px; white-space: pre; font-family: 'Segoe UI', 'Segoe UI Web Regular', 'Segoe UI Symbol', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; line-height: 18.8500003814697px; background-color: rgb(255, 255, 255);\">\n<pre class=\"remarkup-code\" style=\"margin-top: 0px; margin-bottom: 0px; padding: 8px; border: 1px solid rgb(233, 219, 205); overflow: auto; font-family: Menlo, Consolas, Monaco, monospace; font-stretch: normal; font-size: 10px; line-height: normal; background: rgb(253, 243, 218);\">Traceback (most recent call last):\n  File &quot;/usr/local/lib/python2.7/dist-packages/gevent/greenlet.py&quot;, line 327, in run\n    result = self._run(*self.args, **self.kwargs)\n  File &quot;/vagrant/inbox/mailsync/backends/imap/generic.py&quot;, line 190, in _run\n    fail_classes=self.retry_fail_classes)\n  File &quot;/vagrant/inbox/util/concurrency.py&quot;, line 120, in retry_and_report_killed\n    **reset_params)()\n  File &quot;/vagrant/inbox/util/concurrency.py&quot;, line 73, in wrapped\n    return func(*args, **kwargs)\n  File &quot;/vagrant/inbox/mailsync/backends/imap/generic.py&quot;, line 217, in _run_impl\n    self.state = self.state_handlers[old_state]()\n  File &quot;/vagrant/inbox/util/concurrency.py&quot;, line 73, in wrapped\n    return func(*args, **kwargs)\n  File &quot;/vagrant/inbox/mailsync/backends/imap/generic.py&quot;, line 270, in initial_sync\n    self.initial_sync_impl(crispin_client)\n  File &quot;/vagrant/inbox/mailsync/backends/imap/generic.py&quot;, line 293, in initial_sync_impl\n    remote_uids = crispin_client.all_uids()\n  File &quot;/vagrant/inbox/crispin.py&quot;, line 489, in all_uids\n    fetch_result = self.conn.search(['ALL', 'UID'])\n  File &quot;/usr/local/lib/python2.7/dist-packages/imapclient/imapclient.py&quot;, line 588, in search\n    return self._search(normalise_search_criteria(criteria), charset)\n  File &quot;/usr/local/lib/python2.7/dist-packages/imapclient/imapclient.py&quot;, line 621, in _search\n    for item in parse_response(data):\n  File &quot;/usr/local/lib/python2.7/dist-packages/imapclient/response_parser.py&quot;, line 46, in parse_response\n    return tuple(gen_parsed_response(data))\n  File &quot;/usr/local/lib/python2.7/dist-packages/imapclient/response_parser.py&quot;, line 56, in gen_parsed_response\n    for token in src:\n  File &quot;/usr/local/lib/python2.7/dist-packages/imapclient/response_lexer.py&quot;, line 118, in __iter__\n    for tok in self.read_token_stream(iter(source)):\n  File &quot;/usr/local/lib/python2.7/dist-packages/imapclient/response_lexer.py&quot;, line 149, in __iter__\n    return PushableIterator(six.iterbytes(self.src_text))\n  File &quot;/usr/local/lib/python2.7/dist-packages/imapclient/six.py&quot;, line 597, in iterbytes\n    return (ord(byte) for byte in buf)\nTypeError: 'NoneType' object is not iterable\n&lt;FolderSyncEngine at 0x5e4e550&gt; failed with TypeError</pre>\n<div><br>\n</div>\n</div>\n<p><br>\n</p>\n<p>But turns out Tom fixed it <a href=\"https://bitbucket.org/mjs0/imapclient/commits/1de7a0b63367d49587b0454804d4e29079f84f0b?at=default\">\nhere</a>.&nbsp;&nbsp;I don't think it's yet on PyPI.<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<div style=\"color: rgb(0, 0, 0);\">\n<hr tabindex=\"-1\" style=\"display:inline-block; width:98%\">\n<div id=\"divRplyFwdMsg\" dir=\"ltr\"><font face=\"Calibri, sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Menno Smits &lt;menno@freshfoo.com&gt;<br>\n<b>Sent:</b> Wednesday, May 27, 2015 2:41 AM<br>\n<b>To:</b> Michael Grinich<br>\n<b>Cc:</b> Christine Spang<br>\n<b>Subject:</b> Re: Thoughts</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div>Hi Michael,<br>\n</div>\n<div>&nbsp;</div>\n<div>No problems about the delay. I know what it's like.<br>\n</div>\n<div>&nbsp;</div>\n<div>Another interesting development: after our call I put out my feelers to see if any of the developers that I know and trust would be interested in tackling this work, and I got a bite from a former colleague and good friend, Jonathan Hartley (<a href=\"http://tartley.com/\">http://tartley.com/</a>).\n He's one of the smartest developers I know - with huge amounts of experience with Python - and is highly disciplined about testing and code quality. On top of that, he's in the process of arranging a move to the US (his wife his American) so that could work\n well too. He has no experience with IMAP or Go, but these are things I'm confident he could quickly pick up. Due to the pending move to the US, he currently on a short contract so could be available on fairly short notice.<br>\n</div>\n<div>&nbsp;</div>\n<div>Are you interested in talking to him? He could give you a full 5 days a week working on IMAPClient and related bits.<br>\n</div>\n<div>&nbsp;</div>\n<div>- Menno<br>\n</div>\n<div>&nbsp;</div>\n<div>p.s. Can you give me details on the bug that you found today?<br>\n</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>On Wed, 27 May 2015, at 06:59, Michael Grinich wrote:<br>\n</div>\n<blockquote type=\"cite\">\n<div>Hi Menno,<br>\n</div>\n<div>&nbsp;</div>\n<div>Sorry for the delay.&nbsp;<br>\n</div>\n<div>&nbsp;</div>\n<div>Having you work dedicated 1 day a week for Nylas would be fantastic. We already have several low-hanging IMAPclient projects we need help with, and there are many places in our sync engine codebase that I think you could make huge contributions to.<br>\n</div>\n<div>&nbsp;</div>\n<div>So in short, yes. We'd be ready to get started immediately. (Already had a bug come up today where IMAPclient fails when folders have no items in them...)<br>\n</div>\n<div>&nbsp;</div>\n<div>--Michael<br>\n</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<div>&nbsp;</div>\n<blockquote style=\"margin-top:0px; margin-right:0px; margin-bottom:0px; margin-left:0.8ex; border-left-width:1px; border-left-style:solid; border-left-color:rgb(204,204,204); padding-left:1ex\">\n<div>On May 21 2015, at 7:44 pm, Menno Smits &lt;menno@freshfoo.com&gt; wrote: <br>\n</div>\n<p>Hi Michael,<br>\n</p>\n<p>It was great to talk to you and Christine earlier this week.<br>\n</p>\n<p>I've been thinking about ways that we could make this work. I'm really<br>\nnot ready to leave my current position at Canonical but I'd be prepared<br>\nto consider dropping my hours to work for Nylas part-time. Would you<br>\nconsider having me work for Nylas one full day a week if I could<br>\nnegotiate my hours down to 4 days a week at Canonical? I think they<br>\nmight be open to that (any less could be a struggle).</p>\n<p>I realise this is probably less of a commitment from me than you'd<br>\nprobably like but one day a week would give me much more time to work on<br>\nIMAPClient than I have now. You'll get you the features you need much<br>\nsooner. If this seems workable to you I can start the conversation with<br>\nmy managers.</p>\n<p>I'm also writing to a couple top notch developers that I trust about the<br>\npossibility of working with you on this (they're both in London). I<br>\nbelieve that one in particular could be thinking about leaving his<br>\ncurrent role.</p>\n<p>Cheers,<br>\nMenno</p>\n</blockquote>\n</blockquote>\n<div>&nbsp;</div>\n</div>\n</div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_20.html",
    "content": "<div dir=\"ltr\">Yaaay!&nbsp; So excited :) &nbsp;And no worries, see you in PR, if not before</div><div class=\"gmail_extra\"><br><div class=\"gmail_quote\">On Fri, Nov 4, 2016 at 2:07 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@evanmorikawa.com\" target=\"_blank\">evan@evanmorikawa.com</a>&gt;</span> wrote:<br><blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">Y,<div><div><br></div><div>YES! We&#39;d love to go to Puerto Rico. I just signed up on the site for Nora and I. I&#39;m so sorry for the LONG delay on getting back to you. We had a lot of other commitments up in the air around then. So excited to see you guys in Puerto Rico!</div></div><div><br></div><div>Also, I&#39;m unfortunately in NYC the week of Nov 7th and back in SF week of the 14th, otherwise I&#39;d love to see you here too.</div><div><br></div><div>Evan</div><img class=\"m_2465269450974321714n1-open\" width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/1ocrhlu1fap8935xrnic0cmnm/local-139b0028-d812?r=eWlmYW56aGFuZzJAZ21haWwuY29t\"><div class=\"HOEnZb\"><div class=\"h5\">\n        <div class=\"gmail_quote m_2465269450974321714nylas-quote m_2465269450974321714nylas-quote-id-92my6rmekrk94aws2clzwhwgy\">\n          <br>\n          On Nov 3 2016, at 6:56 pm, Y J &lt;<a href=\"mailto:YJ2@gmail.com\" target=\"_blank\">YJ2@gmail.com</a>&gt; wrote:\n          <br>\n          <blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n            <div dir=\"ltr\">Hi Evan &amp; Nora,<div>We&#39;re getting down to the wire and need to send final counts to our vendors <b>tomorrow</b>.&nbsp; If you could please let us know your RSVP for the Dec 10 engagement party and/or the Jan 15 wedding via our website, that would be amazing!&nbsp; Hope to see you soon :)</div><div><br></div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Ftheknot.com%2Fus%2FY-and-geoff&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">theknot.com/us/Y-and-geoff</a><br></div><div><br></div><div>Y &amp; Geoff</div>\n<div><div><br></div>-- <br><div><div dir=\"ltr\"><div>Ms. Y J<br><br><div> College 2010<br>AB in Economics</div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Fwww.twitter.com%2FYz&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">@Yz</a></div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Fwww.app.com&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">www.app.com</a></div><div><br></div></div></div></div>\n</div>\n</div>\n<img src=\"https://YJ2-dot-yamm-track.appspot.com/FireBase?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177\" width=\"1\" height=\"1\" alt=\"beacon\" style=\"display:none;display:none!important\">\n          </blockquote>\n        </div></div></div></blockquote></div><br><br clear=\"all\"><div><br></div>-- <br><div class=\"gmail_signature\" data-smartmail=\"gmail_signature\"><div dir=\"ltr\"><div>Ms. Y J<br><br><div> College 2010<br>AB in Economics</div><div><a href=\"http://www.twitter.com/Yz\" target=\"_blank\">@Yz</a></div><div><a href=\"http://www.app.com\" target=\"_blank\">www.app.com</a></div><div><br></div></div></div></div>\n</div>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_20_stripped.html",
    "content": "<div dir=\"ltr\">Yaaay!&nbsp; So excited :) &nbsp;And no worries, see you in PR, if not before</div><div class=\"gmail_extra\"><br></div>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_21.html",
    "content": "<div dir=\"ltr\">Yaaay!&nbsp; So excited :) &nbsp;And no worries, see you in PR, if not before</div><div class=\"gmail_extra\"><br><div class=\"gmail_quote\">On Fri, Nov 4, 2016 at 2:07 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@evanmorikawa.com\" target=\"_blank\">evan@evanmorikawa.com</a>&gt;</span> wrote:<br><blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">Y,<div><div><br></div><div>YES! We&#39;d love to go to Puerto Rico. I just signed up on the site for Nora and I. I&#39;m so sorry for the LONG delay on getting back to you. We had a lot of other commitments up in the air around then. So excited to see you guys in Puerto Rico!</div></div><div><br></div><div>Also, I&#39;m unfortunately in NYC the week of Nov 7th and back in SF week of the 14th, otherwise I&#39;d love to see you here too.</div><div><br></div><div>Evan</div><img class=\"m_2465269450974321714n1-open\" width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/1ocrhlu1fap8935xrnic0cmnm/local-139b0028-d812?r=eWlmYW56aGFuZzJAZ21haWwuY29t\"><div class=\"HOEnZb\"><div class=\"h5\">\n        <div class=\"gmail_quote m_2465269450974321714nylas-quote m_2465269450974321714nylas-quote-id-92my6rmekrk94aws2clzwhwgy\">\n          <br>\n          On Nov 3 2016, at 6:56 pm, Y J &lt;<a href=\"mailto:YJ2@gmail.com\" target=\"_blank\">YJ2@gmail.com</a>&gt; wrote:\n          <br>\n          <blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n            <div dir=\"ltr\">Hi Evan &amp; Nora,<div>We&#39;re getting down to the wire and need to send final counts to our vendors <b>tomorrow</b>.&nbsp; If you could please let us know your RSVP for the Dec 10 engagement party and/or the Jan 15 wedding via our website, that would be amazing!&nbsp; Hope to see you soon :)</div><div><br></div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Ftheknot.com%2Fus%2FY-and-geoff&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">theknot.com/us/Y-and-geoff</a><br></div><div><br></div><div>Y &amp; Geoff</div>\n<div><div><br></div>-- <br><div><div dir=\"ltr\"><div>Ms. Y J<br><br><div> College 2010<br>AB in Economics</div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Fwww.twitter.com%2FYz&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">@Yz</a></div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Fwww.app.com&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">www.app.com</a></div><div><br></div></div></div></div>\n</div>\n</div>\n<img src=\"https://YJ2-dot-yamm-track.appspot.com/FireBase?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177\" width=\"1\" height=\"1\" alt=\"beacon\" style=\"display:none;display:none!important\">\n          </blockquote>\n        </div></div></div></blockquote></div><br><br clear=\"all\"><div><br></div>-- <br><div class=\"gmail_signature\" data-smartmail=\"gmail_signature\"><div dir=\"ltr\"><div>Ms. Y J<br><br><div> College 2010<br>AB in Economics</div><div><a href=\"http://www.twitter.com/Yz\" target=\"_blank\">@Yz</a></div><div><a href=\"http://www.app.com\" target=\"_blank\">www.app.com</a></div><div><br></div></div></div></div>\n        <div>This is some unique text after the signature. It's as if I'm\n        typing inline. We should NOT collapse this area</div>\n</div>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_21_stripped.html",
    "content": "<div dir=\"ltr\">Yaaay!&nbsp; So excited :) &nbsp;And no worries, see you in PR, if not before</div><div class=\"gmail_extra\"><br><div class=\"gmail_quote\">On Fri, Nov 4, 2016 at 2:07 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@evanmorikawa.com\" target=\"_blank\">evan@evanmorikawa.com</a>&gt;</span> wrote:<br><blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">Y,<div><div><br></div><div>YES! We'd love to go to Puerto Rico. I just signed up on the site for Nora and I. I'm so sorry for the LONG delay on getting back to you. We had a lot of other commitments up in the air around then. So excited to see you guys in Puerto Rico!</div></div><div><br></div><div>Also, I'm unfortunately in NYC the week of Nov 7th and back in SF week of the 14th, otherwise I'd love to see you here too.</div><div><br></div><div>Evan</div><img class=\"m_2465269450974321714n1-open\" width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/1ocrhlu1fap8935xrnic0cmnm/local-139b0028-d812?r=eWlmYW56aGFuZzJAZ21haWwuY29t\"><div class=\"HOEnZb\"><div class=\"h5\">\n        <div class=\"gmail_quote m_2465269450974321714nylas-quote m_2465269450974321714nylas-quote-id-92my6rmekrk94aws2clzwhwgy\">\n          <br>\n          On Nov 3 2016, at 6:56 pm, Y J &lt;<a href=\"mailto:YJ2@gmail.com\" target=\"_blank\">YJ2@gmail.com</a>&gt; wrote:\n          <br>\n          <blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n            <div dir=\"ltr\">Hi Evan &amp; Nora,<div>We're getting down to the wire and need to send final counts to our vendors <b>tomorrow</b>.&nbsp; If you could please let us know your RSVP for the Dec 10 engagement party and/or the Jan 15 wedding via our website, that would be amazing!&nbsp; Hope to see you soon :)</div><div><br></div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Ftheknot.com%2Fus%2FY-and-geoff&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">theknot.com/us/Y-and-geoff</a><br></div><div><br></div><div>Y &amp; Geoff</div>\n<div><div><br></div>-- <br><div><div dir=\"ltr\"><div>Ms. Y J<br><br><div> College 2010<br>AB in Economics</div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Fwww.twitter.com%2FYz&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">@Yz</a></div><div><a href=\"https://YJ2-dot-yamm-track.appspot.com/Redirect?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177&amp;link=http%3A%2F%2Fwww.app.com&amp;r=eWlmYW56aGFuZzJAZ21haWwuY29t\" target=\"_blank\">www.app.com</a></div><div><br></div></div></div></div>\n</div>\n</div>\n<img src=\"https://YJ2-dot-yamm-track.appspot.com/FireBase?ukey=1KbnLIl4_8tooUTgmQ_uMogd5HthhVhfP6x6UFR8wq28-0&amp;key=YAMMID-24566177\" width=\"1\" height=\"1\" alt=\"beacon\" style=\"display:none;display:none!important\">\n          </blockquote>\n        </div></div></div></blockquote></div><br><br clear=\"all\"><div><br></div>-- <br><div class=\"gmail_signature\" data-smartmail=\"gmail_signature\"><div dir=\"ltr\"><div>Ms. Y J<br><br><div> College 2010<br>AB in Economics</div><div><a href=\"http://www.twitter.com/Yz\" target=\"_blank\">@Yz</a></div><div><a href=\"http://www.app.com\" target=\"_blank\">www.app.com</a></div><div><br></div></div></div></div>\n        <div>This is some unique text after the signature. It's as if I'm\n        typing inline. We should NOT collapse this area</div>\n</div>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_22.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<meta content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<div>And a reply!<br>\n<br>\n<div class=\"acompli_signature\"></div>\n<br>\n</div>\n<hr tabindex=\"-1\" style=\"display:inline-block; width:98%\">\n<div id=\"divRplyFwdMsg\" dir=\"ltr\"><font face=\"Calibri, sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Michael Grinich<br>\n<b>Sent:</b> Tuesday, November 8, 2016 12:02:48 PM<br>\n<b>To:</b> Evan Morikawa<br>\n<b>Cc:</b> Michael Grinich<br>\n<b>Subject:</b> Test folded email </font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div>Bla bla&nbsp;</div>\n<div><br>\n</div>\n<div>--Michael&nbsp;<br>\n<br>\n<div class=\"acompli_signature\"></div>\n<br>\n</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_22_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<meta content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<div>And a reply!<br>\n<br>\n<div class=\"acompli_signature\"></div>\n<br>\n</div>\n\n\n\n\n\n</body>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_23.html",
    "content": "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"><div dir=\"ltr\"><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Hi,</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\"><br></div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Thank you</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\"><br></div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Text</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Text</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\"><br></div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Thank you again,</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Name</div></div><div class=\"gmail_extra\"><br><div class=\"gmail_quote\">On Thu, Nov 3, 2016 at 3:34 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br><blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n\n\n\n<div>\nName,\n<div style=\"max-width:100%;font-family:Nylas-Pro,Helvetica,&quot;Lucidia Grande&quot;,sans-serif;font-size:14.5px\">\n<div style=\"max-width:100%\"><br>\n</div>\n<div style=\"max-width:100%\">Text A</div>\n<div style=\"max-width:100%\"><br>\n</div>\n<div style=\"max-width:100%\">Text B</div>\n<div style=\"max-width:100%\"><br>\n</div>\n<div style=\"max-width:100%\">Text C</div>\n<div style=\"max-width:100%\"><br>\n</div>\n<div style=\"max-width:100%\">Text D</div>\n<div style=\"max-width:100%\"><br>\n</div>\n<div style=\"max-width:100%\">Best,</div>\n<div style=\"max-width:100%\">Evan</div>\n<div style=\"max-width:100%\"><a href=\"tel:%28858%29%20531-1718\" value=\"&#43;18585311718\" target=\"_blank\">(858) 531-1718</a></div>\n</div>\n<img class=\"m_4916245491697320432n1-open\" width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-2ac86b20-5e8a\"><div><div class=\"h5\">\n<div class=\"gmail_quote m_4916245491697320432nylas-quote m_4916245491697320432nylas-quote-id-9j8cw553wjdknnnrrypxr794v\"><br>\nOn Nov 1 2016, at 11:21 am, Halla Moore &lt;<a href=\"mailto:Halla@nylas.com\" target=\"_blank\">Halla@nylas.com</a>&gt; wrote: <br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n\nHi Name,\n<div>\n<div><br>\n</div>\n<div>Nice to e-meet you too! :)</div>\n</div>\n<div><br>\n</div>\n<div>Speak to you soon,</div>\n<div>Halla</div>\n<div><br>\nOn Nov 1 2016, at 11:18 am, Last, Name &lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt; wrote: <br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n\n<div dir=\"ltr\">\n<div style=\"font-family:tahoma,sans-serif\">Hi Evan and Halla,</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Halla -- nice to e-meet you! I'm looking forward to speaking. :)</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Thank you,</div>\n<div style=\"font-family:tahoma,sans-serif\">Name</div>\n</div>\n<div><br>\n<div>On Tue, Nov 1, 2016 at 1:42 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Name,\n<div><br>\n</div>\n<div>Text J</div>\n<div><br>\n</div>\n<div>You should also get a calendar invite shortly.</div>\n<div><br>\n</div>\n<div>Evan</div>\n<img width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-2a3d3a3d-a321\">\n<div>\n<div>\n<div><br>\nOn Oct 31 2016, at 5:22 pm, Last, Name &lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt; wrote:\n<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-family:tahoma,sans-serif\">Hi Evan,</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Yes, that works! Looking forward to it.</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Name</div>\n</div>\n<div><br>\n<div>On Mon, Oct 31, 2016 at 2:49 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Name, how about tomorrow, Tuesday 11/1 at 7:00pm EDT (4:00pm PDT)?<br>\n<br>\nEvan<img width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-42953eac-36c9\">\n<div>\n<div>\n<div><br>\nOn Oct 28 2016, at 10:16 am, Last, Name &lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt; wrote:\n<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-family:tahoma,sans-serif\">Hi Evan,</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Thank you for your patience; I've been recovering from a cold this week.</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Text H</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Thank you very much,</div>\n<div style=\"font-family:tahoma,sans-serif\">Name</div>\n<div><br>\n<div>On Tue, Oct 25, 2016 at 11:51 AM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Name,\n<div><br>\n</div>\n<div>Text I</div>\n<div><br>\n</div>\n<div>Text J</div>\n<div><br>\n</div>\n<div>Text K</div>\n<div><br>\n</div>\n<div>Really looking forward to chatting more soon.</div>\n<div><br>\n</div>\n<div>Evan</div>\n<img width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-d5253a11-8753\">\n<div>\n<div>\n<div><br>\nOn Oct 24 2016, at 11:54 pm, Last, Name &lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt; wrote:\n<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-family:tahoma,sans-serif\">Hello Evan,</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Text L</div>\n<div style=\"font-family:tahoma,sans-serif\"><br>\n</div>\n<div style=\"font-family:tahoma,sans-serif\">Cheers,</div>\n<div style=\"font-family:tahoma,sans-serif\">Name</div>\n</div>\n<div><br>\n<div>On Wed, Oct 19, 2016 at 5:59 PM, Last, Name <span dir=\"ltr\">&lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\">Hi Evan,\n<div><br>\n</div>\n<div>Text M</div>\n<span>\n<div><br>\n</div>\n<div>Name</div>\n</span></div>\n<div>\n<div>\n<div><br>\n<div>On Wed, Oct 19, 2016 at 12:40 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Great!\n<div>\n<div><br>\n</div>\n<div>I just sent over a calendar invite. Join me on this Google Hangout:&nbsp;<a href=\"https://plus.google.com/hangouts/_/nylas.com/evan-name\" target=\"_blank\">https://plus.google.c<wbr>om/hangouts/_/nylas.com/evan-j<wbr>ulia</a>&nbsp;this\n Friday.</div>\n<div><br>\n</div>\n<div>Talk soon</div>\n<span>\n<div>Evan</div>\n<div><a value=\"&#43;18585311718\">(858) 531-1718</a></div>\n<br>\n<u></u>Sent from <a href=\"https://link.nylas.com/link/e7pdeb1ninvdy61tg6liermf8/local-d0981f11-53c7/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\" target=\"_blank\">\nNylas Mail</a>, the extensible, open source mail client.<u></u></span></div>\n<img width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-d0981f11-53c7\">\n<div>\n<div>\n<div><br>\nOn Oct 18 2016, at 7:21 pm, Last, Name &lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt; wrote:\n<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\">Hi Evan,\n<div><br>\n</div>\n<div>Sounds great! My number is <a value=\"&#43;555555\">555-5555</a>.</div>\n<div><br>\n</div>\n<div>Looking forward to speaking,</div>\n<div>Name</div>\n</div>\n<div><br>\n<div>On Tue, Oct 18, 2016 at 4:12 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Name, (Michael to bcc),\n<div>\n<div><br>\n</div>\n<div>Friday works. How about 30 min this Friday 10/21 at 11:30am PDT (2:30pm EDT)?</div>\n<div><br>\n</div>\n<div>Evan</div>\n<span><br>\n<u></u>Sent from <a href=\"https://link.nylas.com/link/e7pdeb1ninvdy61tg6liermf8/local-aee34b8e-af74/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\" target=\"_blank\">\nNylas Mail</a>, the extensible, open source mail client.<u></u></span></div>\n<img width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-aee34b8e-af74\">\n<div>\n<div>\n<div><br>\nOn Oct 18 2016, at 2:03 pm, Last, Name &lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt; wrote:\n<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\">Hi Evan,\n<div><br>\n</div>\n<div>Text N</div>\n<div>Text O</div>\n<div><br>\n</div>\n<div>Thanks a lot,</div>\n<div>Name</div>\n<div><br>\n<div>On Mon, Oct 17, 2016 at 1:24 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div>Juila,\n<div>\n<div><br>\n</div>\n<div>Text P</div>\n<div><br>\n</div>\n<div>Have time early this week?</div>\n<div><br>\n</div>\n<div>Evan</div>\n<span>\n<div><a value=\"&#43;18585311718\">(858) 531-1718</a></div>\n<br>\n<u></u>Sent from <a href=\"https://link.nylas.com/link/e7pdeb1ninvdy61tg6liermf8/local-8427e727-d342/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\" target=\"_blank\">\nNylas Mail</a>, the extensible, open source mail client.<u></u></span></div>\n<img width=\"0\" height=\"0\" style=\"border:0;width:0;height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-8427e727-d342\">\n<div>\n<div>\n<div><br>\nOn Oct 7 2016, at 9:17 am, Last, Name &lt;<a href=\"mailto:name_wu@brown.edu\" target=\"_blank\">name_wu@brown.edu</a>&gt; wrote:\n<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\">Hi Evan,\n<div><br>\n</div>\n<div>Text Q</div>\n<div><br>\n</div>\n<div>Text R</div>\n<div><br>\n</div>\n<div>I look forward to staying in touch. :)</div>\n<div><br>\n</div>\n<div>Thank you very much,</div>\n<div>Name</div>\n<div><br>\n</div>\n</div>\n<div><br>\n<div>On Mon, Oct 3, 2016 at 5:17 PM, Evan Morikawa <span dir=\"ltr\">&lt;<a href=\"mailto:evan@nylas.com\" target=\"_blank\">evan@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div><span>Name</span>,\n<div>\n<div><br>\n</div>\n<div>Text S</div>\n<div><br>\n</div>\n<div>Evan</div>\n<div>Nylas</div>\n<div><a value=\"&#43;18585311718\">(858) 531-1718</a></div>\n<br>\n<u></u>Sent from <a href=\"https://link.nylas.com/link/e7pdeb1ninvdy61tg6liermf8/local-87ed2e6b-3158/0?redirect=https%3A%2F%2Fnylas.com%2Fn1%3Fref%3Dn1\" target=\"_blank\">\nNylas Mail</a>, the extensible, open source mail client.<u></u></div>\n<img width=\"0\" height=\"0\" style=\"border:0;width:0;min-height:0\" src=\"https://link.nylas.com/open/e7pdeb1ninvdy61tg6liermf8/local-87ed2e6b-3158\">\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">\n<div>\n<div dir=\"ltr\">Name Last\n<div>Sc.B., Some Major</div>\n<div>Some University | Class of 2028</div>\n<div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</blockquote>\n</div>\n</div></div></div>\n\n</blockquote></div><br><br clear=\"all\"><div><br></div>-- <br><div class=\"gmail_signature\" data-smartmail=\"gmail_signature\"><div dir=\"ltr\"><div><div dir=\"ltr\">Name Last<div>Sc.B., Some Major</div><div>Some University | Class of 2028</div><div><a href=\"http://www.linkedin.com/pub/name-wu/83/528/b67\" target=\"_blank\"><img src=\"https://static.licdn.com/scds/common/u/img/webpromo/btn_profile_bluetxt_80x15.png\"></a><br></div></div></div></div></div>\n</div>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_23_stripped.html",
    "content": "<div dir=\"ltr\"><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Hi,</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\"><br></div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Thank you</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\"><br></div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Text</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Text</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\"><br></div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Thank you again,</div><div class=\"gmail_default\" style=\"font-family:tahoma,sans-serif\">Name</div></div><div class=\"gmail_extra\"><br></div>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_2_1.txt",
    "content": "Outlook with a reply\n\n\n ------------------------------\n\n*From:* Google Apps Sync Team [mailto:mail-noreply@google.com]\n*Sent:* Thursday, February 09, 2012 1:36 PM\n*To:* jow@xxxx.com\n*Subject:* Google Apps Sync was updated!\n\n\n\nDear Google Apps Sync user,\n\nGoogle Apps Sync for Microsoft Outlook® was recently updated. Your computer\nnow has the latest version (version 2.5). This release includes bug fixes\nto improve product reliability. For more information about these and other\nchanges, please see the help article here:\n\nhttp://www.google.com/support/a/bin/answer.py?answer=153463\n\nSincerely,\n\nThe Google Apps Sync Team.\n\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_2_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:10pt;color:#000000;background-color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;\">\n<p>It's be great to talk with Jonathan-- feel free to connect us. Thanks.<br>\n</p>\n<p><br>\n</p>\n<p>The bug I mentioned manifested itself like this:<br>\n</p>\n<p><br>\n</p>\n<div class=\"remarkup-code-block\" data-code-lang=\"text\" data-sigil=\"remarkup-code-block\" style=\"margin: 0px 0px 12px; padding: 0px; border: 0px; white-space: pre; font-family: 'Segoe UI', 'Segoe UI Web Regular', 'Segoe UI Symbol', 'Helvetica Neue', Helvetica, Arial, sans-serif; font-size: 13px; line-height: 18.8500003814697px; background-color: rgb(255, 255, 255);\">\n<pre class=\"remarkup-code\" style=\"margin-top: 0px; margin-bottom: 0px; padding: 8px; border: 1px solid rgb(233, 219, 205); overflow: auto; font-family: Menlo, Consolas, Monaco, monospace; font-stretch: normal; font-size: 10px; line-height: normal; background: rgb(253, 243, 218);\">Traceback (most recent call last):\n  File \"/usr/local/lib/python2.7/dist-packages/gevent/greenlet.py\", line 327, in run\n    result = self._run(*self.args, **self.kwargs)\n  File \"/vagrant/inbox/mailsync/backends/imap/generic.py\", line 190, in _run\n    fail_classes=self.retry_fail_classes)\n  File \"/vagrant/inbox/util/concurrency.py\", line 120, in retry_and_report_killed\n    **reset_params)()\n  File \"/vagrant/inbox/util/concurrency.py\", line 73, in wrapped\n    return func(*args, **kwargs)\n  File \"/vagrant/inbox/mailsync/backends/imap/generic.py\", line 217, in _run_impl\n    self.state = self.state_handlers[old_state]()\n  File \"/vagrant/inbox/util/concurrency.py\", line 73, in wrapped\n    return func(*args, **kwargs)\n  File \"/vagrant/inbox/mailsync/backends/imap/generic.py\", line 270, in initial_sync\n    self.initial_sync_impl(crispin_client)\n  File \"/vagrant/inbox/mailsync/backends/imap/generic.py\", line 293, in initial_sync_impl\n    remote_uids = crispin_client.all_uids()\n  File \"/vagrant/inbox/crispin.py\", line 489, in all_uids\n    fetch_result = self.conn.search(['ALL', 'UID'])\n  File \"/usr/local/lib/python2.7/dist-packages/imapclient/imapclient.py\", line 588, in search\n    return self._search(normalise_search_criteria(criteria), charset)\n  File \"/usr/local/lib/python2.7/dist-packages/imapclient/imapclient.py\", line 621, in _search\n    for item in parse_response(data):\n  File \"/usr/local/lib/python2.7/dist-packages/imapclient/response_parser.py\", line 46, in parse_response\n    return tuple(gen_parsed_response(data))\n  File \"/usr/local/lib/python2.7/dist-packages/imapclient/response_parser.py\", line 56, in gen_parsed_response\n    for token in src:\n  File \"/usr/local/lib/python2.7/dist-packages/imapclient/response_lexer.py\", line 118, in __iter__\n    for tok in self.read_token_stream(iter(source)):\n  File \"/usr/local/lib/python2.7/dist-packages/imapclient/response_lexer.py\", line 149, in __iter__\n    return PushableIterator(six.iterbytes(self.src_text))\n  File \"/usr/local/lib/python2.7/dist-packages/imapclient/six.py\", line 597, in iterbytes\n    return (ord(byte) for byte in buf)\nTypeError: 'NoneType' object is not iterable\n&lt;FolderSyncEngine at 0x5e4e550&gt; failed with TypeError</pre>\n<div><br>\n</div>\n</div>\n<p><br>\n</p>\n<p>But turns out Tom fixed it <a href=\"https://bitbucket.org/mjs0/imapclient/commits/1de7a0b63367d49587b0454804d4e29079f84f0b?at=default\">\nhere</a>.&nbsp;&nbsp;I don't think it's yet on PyPI.<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n\n</div>\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_3.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:12pt;color:#000000;background-color:#FFFFFF;font-family:Calibri,Arial,Helvetica,sans-serif;\">\n<p>You should send these to team@&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p>hope you feel better!&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<div dir=\"ltr\" style=\"color: rgb(33, 33, 33);\">\n<hr tabindex=\"-1\" style=\"display:inline-block; width:98%\">\n<div id=\"divRplyFwdMsg\" dir=\"ltr\"><font face=\"Calibri, sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Andrea Whiting<br>\n<b>Sent:</b> Monday, April 20, 2015 9:48 AM<br>\n<b>To:</b> Christine Spang; Michael Grinich<br>\n<b>Subject:</b> WFH this morn</font>\n<div>&nbsp;</div>\n</div>\n<div>\n<div id=\"divtagdefaultwrapper\" style=\"font-size:12pt; color:#000000; background-color:#FFFFFF; font-family:Calibri,Arial,Helvetica,sans-serif\">\n<p>Morning!<br>\n</p>\n<p><br>\n</p>\n<p>I'm coughing up a post-pneumonia storm this morning, going to see if I can get a doctor's appt. in the AM and then potentially come in after lunch. My body might just need a day of rest after all that travel.<br>\n</p>\n<p><br>\n</p>\n<p>Not sure if WFH is a message to email team@ or post on slack in 'general' - lmk what works best.<br>\n</p>\n<p><br>\n</p>\n<p>Andrea<br>\n</p>\n</div>\n</div>\n</div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_3_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:12pt;color:#000000;background-color:#FFFFFF;font-family:Calibri,Arial,Helvetica,sans-serif;\">\n<p>You should send these to team@&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p>hope you feel better!&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n\n</div>\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_4.html",
    "content": "<html>\n<head>\n<meta http-equiv=3D\"Content-Type\" content=3D\"text/html; charset=3Diso-8859-=\n1\">\n<style type=3D\"text/css\" style=3D\"display:none;\"><!-- P {margin-top:0;margi=\nn-bottom:0;} --></style>\n</head>\n<body dir=3D\"ltr\">\n<div id=3D\"divtagdefaultwrapper\" style=3D\"font-size:12pt;color:#000000;back=\nground-color:#FFFFFF;font-family:Calibri,Arial,Helvetica,sans-serif;\">\n<p>Hey All,</p>\n<p><br>\n</p>\n<p>This was a long email but wanted to do a quick followup on the&nbsp;<a h=\nref=3D\"http://www.onemedical.com/sf/doctors/?gclid=3DCJi_-9bP9sUCFUiGfgodMA=\nUAxQ\">One Medical</a> portion. I've only received a few responses so far.&n=\nbsp;</p>\n<p><br>\n</p>\n<p>Signing up would not require you to change your current primary care doc=\ntor or pay additional insurance, rather it is a primary care center (think =\nKaiser) where you can make all of your appointments. Super convenient right=\n!?</p>\n<p><br>\n</p>\n<p>Get back to me if this is something you want to participate in. Nylas wo=\nuld cover the cost.&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p>Thanks,<br>\n</p>\n<p>Makala&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<br>\n<br>\n<div style=3D\"color: rgb(0, 0, 0);\">\n<hr tabindex=3D\"-1\" style=3D\"display:inline-block; width:98%\" customtabinde=\nx=3D\"-1\" disabled=3D\"true\">\n<div id=3D\"divRplyFwdMsg\" dir=3D\"ltr\"><font face=3D\"Calibri, sans-serif\" co=\nlor=3D\"#000000\" style=3D\"font-size:11pt\"><b>From:</b> Makala Keys<br>\n<b>Sent:</b> Friday, May 29, 2015 4:59 PM<br>\n<b>To:</b> team<br>\n<b>Subject:</b> Revisiting Health Insurance </font>\n<div>&nbsp;</div>\n</div>\n<div>Hi All!\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\"><br>\n</span></div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\">A number of health insurance questions have come up recently. As we g=\nrow I want to make sure that everyone is well-informed about insurance and =\nthat you are getting the most out\n of your health benefits. &nbsp;</span>\n<div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\"><br>\n</span></div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\">My original insurance presentation\n</span><a href=3D\"https://docs.google.com/presentation/d/1VcKjcKNfvPS5BSfgJ=\nlie9lYhFAb5U7_3ujfLHONRw6E/edit#slide=3Did.p\" style=3D\"font-size:15.9px; li=\nne-height:1.4\" tabindex=3D\"-1\" disabled=3D\"true\">here&nbsp;</a>, explains w=\nhat plans Nylas currently offers.</div>\n<div>\n<div><br>\n</div>\n<div>This week I also looked into how to set up a <a href=3D\"https://docs.g=\noogle.com/document/d/1rnX8wGGkKvSEBV3kr25W9lKYnvEP_CPvOpl7R0BPK5U/edit#\" ta=\nbindex=3D\"-1\" disabled=3D\"true\">\nhealth savings account</a>. A health savings account is a<span style=3D\"fon=\nt-size:15.9px; line-height:1.4; background-color:inherit\">&nbsp;special tax=\n advantaged account that is used with a high-deductible health plan (HDHP),=\n and it allows you to pay for various qualified\n medical expenses tax-free. Nylas offers two high-deductible health plans. =\nYou can see exactly which plan you are enrolled in, and if you qualify to s=\net up an HSA, through your\n<a href=3D\"https://secure.zenefits.com/accounts/login/\" tabindex=3D\"-1\" dis=\nabled=3D\"true\">\nZenefits </a>account.&nbsp;</span></div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\"><br>\n</span></div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\">Finally, I researched info about &nbsp;<a href=3D\"http://www.onemedic=\nal.com/enterprise/\" tabindex=3D\"-1\" disabled=3D\"true\">One Medical Group.&nb=\nsp;</a>&nbsp;which is an innovate\n<a href=3D\"http://techcrunch.com/2014/04/17/one-medical-group-raises-40m-to=\n-help-reinvent-the-doctors-office/\" tabindex=3D\"-1\" disabled=3D\"true\">\nprimary care health organization.&nbsp;</a>&nbsp;One Medical group has seve=\nral locations in San Francisco and a location in Berkeley. It offers an abu=\nndance of services listed\n<a href=3D\"http://www.onemedical.com/sf/help/\" tabindex=3D\"-1\" disabled=3D\"=\ntrue\">here</a>, making it easier for you to make doctors appointments.&nbsp=\n;</span></div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\"><br>\n</span></div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\">Nylas is going to start offering One Medical Group membership on an o=\npt-in basis. Take some time to look it over this weekend and see if this is=\n something you are interested in.\n Our One Medical membership plan &nbsp;will also cover spouses and children=\n.&nbsp;</span></div>\n<div><br>\n</div>\n<div>As always, feel free to reach out with questions!&nbsp;</div>\n<div><br>\n</div>\n<div>Makala&nbsp;</div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\"><br>\n</span></div>\n<div><span style=3D\"font-size:15.9px; line-height:1.4; background-color:inh=\nerit\"><br>\n</span></div>\n<div><br>\n</div>\n<div><br>\n</div>\n<div>\n<div><br>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_4_stripped.html",
    "content": "<head>\n<meta http-equiv=\"3D&quot;Content-Type&quot;\" content=\"3D&quot;text/html;\" charset=\"3Diso-8859-=\" 1\"=\"\">\n<style type=\"3D&quot;text/css&quot;\" style=\"3D&quot;display:none;&quot;\"><!-- P {margin-top:0;margi=\nn-bottom:0;} --></style>\n</head>\n<body dir=\"3D&quot;ltr&quot;\">\n<div id=\"3D&quot;divtagdefaultwrapper&quot;\" style=\"3D&quot;font-size:12pt;color:#000000;back=\" ground-color:#ffffff;font-family:calibri,arial,helvetica,sans-serif;\"=\"\">\n<p>Hey All,</p>\n<p><br>\n</p>\n<p>This was a long email but wanted to do a quick followup on the&nbsp;<a h=\"ref=3D&quot;http://www.onemedical.com/sf/doctors/?gclid=3DCJi_-9bP9sUCFUiGfgodMA=\" uaxq\"=\"\">One Medical</a> portion. I've only received a few responses so far.&amp;n=\nbsp;</p>\n<p><br>\n</p>\n<p>Signing up would not require you to change your current primary care doc=\ntor or pay additional insurance, rather it is a primary care center (think =\nKaiser) where you can make all of your appointments. Super convenient right=\n!?</p>\n<p><br>\n</p>\n<p>Get back to me if this is something you want to participate in. Nylas wo=\nuld cover the cost.&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p>Thanks,<br>\n</p>\n<p>Makala&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<br>\n<br>\n\n</div>\n\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_5.html",
    "content": "<html><head></head><body><div id=\"inbox-html-wrapper\"\nclass=\"show-quoted-text\"><meta http-equiv=\"Content-Type\" content=\"text/html;\ncharset=utf-8\"><div dir=\"ltr\"><div class=\"gmail_quote\"><div>Just an update on\nthis guys, we had three other customers login this morning and it seems that\ntheir accounts are also listed as: \"Sync not running\" but some of their\nmessages have been synced. Emails are below:<br><br><a\nhref=\"mailto:drew@a.com\">drew@a.com</a> (which is somehow\nlisted twice)<br><a\nhref=\"mailto:s.k@g.com\">s.k@g.com</a><br><a\nhref=\"mailto:oprokhorenko@splunk.com\">oprokhorenko@splunk.com</a></div><div><br></div><div>-Andrew</div><div>&nbsp;</div><blockquote\nclass=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc\nsolid;padding-left:1ex\"><div dir=\"ltr\"><div><div class=\"gmail_quote\">From: <b\nclass=\"gmail_sendername\">Jeff Meister</b> <span dir=\"ltr\">&lt;<a\nhref=\"mailto:jeff@esper.com\"\ntarget=\"_blank\">jeff@esper.com</a>&gt;</span><br>Date: Mon, Jun 22, 2015 at\n4:00 PM<br>Subject: Nylas bug reports<br>To: support &lt;<a\nhref=\"mailto:support@nylas.com\"\ntarget=\"_blank\">support@nylas.com</a>&gt;<br>Cc: Michael Grinich &lt;<a\nhref=\"mailto:mg@nilas.com\" target=\"_blank\">mg@nilas.com</a>&gt;, Christine\nSpang &lt;<a href=\"mailto:spang@nylas.com\"\ntarget=\"_blank\">spang@nylas.com</a>&gt;, Kavya Joshi &lt;<a\nhref=\"mailto:kavya@nylas.com\" target=\"_blank\">kavya@nylas.com</a>&gt;, Karim\nHamidou &lt;<a href=\"mailto:karim@nylas.com\"\ntarget=\"_blank\">karim@nylas.com</a>&gt;<br><br><br><div dir=\"ltr\">Hi Nylas\nfolks,<br><div><br>I made a new thread because the old one was getting really\nlong. We've noticed a few things when onboarding our first few Exchange\ncustomers:<br><br></div><div>1. One of our users signed in to Nylas\nsuccessfully with email <a href=\"mailto:oprokhorenko@splunk.com\"\ntarget=\"_blank\">oprokhorenko@splunk.com</a>, but I see in the dashboard that\nhis status is \"Sync not running\", with a white circle rather than red or green.\nWe haven't seen this one before. Clicking on his entry does show 84 messages\nsynced, but that hasn't changed in a while. What should we\ndo?<br><br></div><div>2. We may be experiencing event duplication issues again,\nbut I'm still looking into this to see if we're causing the problem from our\nside. The affected account in this case is <a href=\"mailto:han@a.com\"\ntarget=\"_blank\">han@a.com</a>. I'll send an update once I have\nsomething concrete, but I wanted to bring it up in case there are any quick\nchecks you can run on his account in the\nmeantime.<br></div><div><br></div><div>3. I noticed that drafts have started\nappearing in my delta sync. Maybe it was always this way and I didn't notice\nbecause my testing accounts didn't have a user actively emailing, but we're\ngetting a lot of data that we don't need. We use exclude_types in our delta\nsync requests, with every type except \"event\", but if I add \"draft\" to this\nlist, I get a bad request error from Nylas. Can you enable filtering of drafts\nhere?<br><br></div><div>Thanks again, and see you on Wednesday!<span><font\ncolor=\"#888888\"><br></font></span></div><span><font\ncolor=\"#888888\"><div>Jeff<br></div></font></span></div>\n</div><br></div></div>\n</blockquote></div></div></div></body></html>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_5_stripped.html",
    "content": "<head></head><body><div id=\"inbox-html-wrapper\" class=\"show-quoted-text\"><meta http-equiv=\"Content-Type\" content=\"text/html;\ncharset=utf-8\"><div dir=\"ltr\"></div></div>\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_6.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<p>Hello!</p>\n<p>Here is a summary of the alerts in the past 24 hours:</p>\n<table>\n<tbody>\n<tr>\n<td><b>Alert name</b></td>\n<td style=\"text-align:right\"><b># Critical</b></td>\n<td style=\"text-align:right\"><b># Warning</b></td>\n</tr>\n<tr>\n<td>mt-st-helena.account-dead-check</td>\n<td style=\"text-align:right\">199.0</td>\n<td style=\"text-align:right\">0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-redwoodstaging-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">2.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-redwoodproduction-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">11.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-edgehillproduction-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">1.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-edgehillstaging-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">9.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-mailsyncstaging-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">9.0</td>\n</tr>\n</tbody>\n</table>\n<p>Have a good day!</p>\n</body>\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_6_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<p>Hello!</p>\n<p>Here is a summary of the alerts in the past 24 hours:</p>\n<table>\n<tbody>\n<tr>\n<td><b>Alert name</b></td>\n<td style=\"text-align:right\"><b># Critical</b></td>\n<td style=\"text-align:right\"><b># Warning</b></td>\n</tr>\n<tr>\n<td>mt-st-helena.account-dead-check</td>\n<td style=\"text-align:right\">199.0</td>\n<td style=\"text-align:right\">0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-redwoodstaging-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">2.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-redwoodproduction-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">11.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-edgehillproduction-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">1.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-edgehillstaging-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">9.0</td>\n</tr>\n<tr>\n<td>mt-st-helena.mysql-mailsyncstaging-check</td>\n<td style=\"text-align:right\">0</td>\n<td style=\"text-align:right\">9.0</td>\n</tr>\n</tbody>\n</table>\n<p>Have a good day!</p>\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_7.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:10pt;color:#000000;background-color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;\">\n<p>Thanks for coming by today. :) Let me know next time you're around and settled and I'll show you a wicked awesome demo of the stuff we've been building!&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p>And congrats on YC!&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<div style=\"color: rgb(33, 33, 33);\">\n<hr tabindex=\"-1\" style=\"display:inline-block; width:98%\">\n<div id=\"divRplyFwdMsg\" dir=\"ltr\"><font face=\"Calibri, sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Ted Benson &lt;eob@csail.mit.edu&gt;<br>\n<b>Sent:</b> Monday, April 27, 2015 10:51 AM<br>\n<b>To:</b> Michael Grinich<br>\n<b>Subject:</b> Re: James Tamplin intro?</font>\n<div>&nbsp;</div>\n</div>\n<div>Hey Michael,<br>\n<br>\nWe're in you're neck of the woods and can meet whenever you're free -- or happy to swing by at 12 like planned if that fits your schedule better.\n<br>\n<br>\nLooking forward to it!<br>\nTed<br>\n<div class=\"gmail_quote\">On Fri, Apr 24, 2015 at 11:55 AM Michael Grinich &lt;<a href=\"mailto:mg@nylas.com\">mg@nylas.com</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">\n<div style=\"font-size:10pt; color:#000000; background-color:#ffffff; font-family:Arial,Helvetica,sans-serif\">\n<p>Sure-- bring him along too. See you then. :)<br>\n</p>\n<p><br>\n</p>\n<p>--mg<br>\n</p>\n<p><br>\n</p>\n<div style=\"color:rgb(33,33,33)\">\n<hr style=\"display:inline-block; width:98%\">\n<div dir=\"ltr\"><font face=\"Calibri, sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b>\n<a href=\"mailto:edward.benson@gmail.com\" target=\"_blank\">edward.benson@gmail.com</a> &lt;<a href=\"mailto:edward.benson@gmail.com\" target=\"_blank\">edward.benson@gmail.com</a>&gt; on behalf of Ted Benson &lt;<a href=\"mailto:eob@csail.mit.edu\" target=\"_blank\">eob@csail.mit.edu</a>&gt;<br>\n<b>Sent:</b> Friday, April 24, 2015 11:39 AM<br>\n<b>To:</b> Michael Grinich<br>\n<b>Subject:</b> Re: James Tamplin intro?</font>\n<div>&nbsp;</div>\n</div>\n</div>\n</div>\n</div>\n<div dir=\"ltr\">\n<div style=\"font-size:10pt; color:#000000; background-color:#ffffff; font-family:Arial,Helvetica,sans-serif\">\n<div style=\"color:rgb(33,33,33)\">\n<div>\n<div dir=\"ltr\">\n<div>w/ James: Thanks a bunch!</div>\n<div><br>\n</div>\n<div>Lunch: Great! Just sent you an invite. Noon OK?</div>\n<div><br>\n</div>\n<div>If YC rejection: That doesn't worry me. We're doing this either way. And if we truly hit a brick wall then that's valuable data for us as well.</div>\n<div><br>\n</div>\n<div>Looking forward to seeing you! Mind if Jake (cofounder) comes along for lunch?</div>\n<div><br>\n</div>\n<br>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Fri, Apr 24, 2015 at 12:31 AM, Michael Grinich <span dir=\"ltr\">\n&lt;<a href=\"mailto:mg@nylas.com\" target=\"_blank\">mg@nylas.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div>Cool, I'll see what I can do wrt James. He might be in Google land these days.&nbsp;<br>\n<div><br>\nSome VCs see YC rejection as a red flag. You likely need to have clearly demonstrated conviction and/or traction to raise if that happens.</div>\n<div><br>\n</div>\n<div>Want to stop by for lunch on Monday? We're at <a>2030 Harrison St. in SF</a>.&nbsp;</div>\n<div>\n<div><br>\n<br>\n<br>\n<br>\n<div class=\"gmail_quote\">On Wed, Apr 22, 2015 at 4:21 PM -0700, &quot;Ted Benson&quot; <span dir=\"ltr\">\n&lt;<a href=\"mailto:eob@csail.mit.edu\" target=\"_blank\">eob@csail.mit.edu</a>&gt;</span> wrote:<br>\n<br>\n</div>\n<div>\n<div dir=\"ltr\">Hi Michael,\n<div><br>\n</div>\n<div>No worries about the delay! I've never been to PyCon but heard it's a good time.</div>\n<div><br>\n</div>\n<div>Thanks -- I know it's just the beginning, but it feels like such a long road to have gotten this far already. Psyched for what's to come.</div>\n<div><br>\n</div>\n<div>Connections:</div>\n<div>Right now we're looking for advice and angel investment.&nbsp;</div>\n<div>- If we don't get YC, we're going to start a seed round</div>\n<div>- If we do get YC, we're still really interested in meeting the kinds of people who can offer advice having already grown successful platform companies</div>\n<div><br>\n</div>\n<div>In your judgement is that a reasonable way to plan out meetings for the post-YC days we're there.&nbsp;</div>\n<div><br>\n</div>\n<div>Would love an intro to James, his tech counterpart, or others you think could help us out along those lines! (Agree about their model, btw).</div>\n<div><br>\n</div>\n<div>I'd love to drop by Monday or Tuesday and see what you all are cooking up!</div>\n<div><br>\n</div>\n<div>Thanks again!</div>\n<div>Ted</div>\n<div><br>\n</div>\n<div>&nbsp; &nbsp;</div>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Wed, Apr 22, 2015 at 5:03 AM, Michael Grinich <span dir=\"ltr\">\n&lt;<a href=\"mailto:mgrinich@gmail.com\" target=\"_blank\">mgrinich@gmail.com</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<br>\nHey Ted-- sorry for the delay. I was at PyCon in Montreal (we sponsored this year) and then our company retreat in Tahoe. Finally catching up back in SF.\n<br>\n<br>\nCongrats on making it to the interview circuit!<br>\nregarding networking, are you looking for partnerships or YC advise or investment or something else?<br>\n<br>\nI've actually been coaching a couple startups, one of whichever is pitching YC this wkd. Anything specific you need there?<br>\n<br>\nOur first engineer came from Firebase (and MIT/PDOS before that), so I could get an intro to James. FYI he's more on the biz side since he's not a programmer. Also I'm not sure they're the best model for success honestly.\n<br>\n<br>\nIn any case, want to grab a coffee and catch up? I'd love to show you some of the stuff we're building too. (The good stuff we haven't announced yet.) Happy to help on any of the mentioned points above.\n<br>\n<span><font color=\"#888888\"><br>\n--mg</font></span>\n<div>\n<div><br>\n<div class=\"gmail_quote\">On Sun, Apr 19, 2015 at 8:45 AM Ted Benson &lt;<a href=\"mailto:eob@csail.mit.edu\" target=\"_blank\">eob@csail.mit.edu</a>&gt; wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hah - I just realized I might have read Facebook wrong. It is WE who are friends, not you and James.\n<div><br>\n</div>\n<div>So I'll spin that ask around: do you know any good advisors or angels that you'd be comfortable putting us in touch with? Nearest neighbors to us are companies like IFTTT, Firebase, Ionic, Parse, Automattic.</div>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\">On Sun, Apr 19, 2015 at 11:39 AM, Ted Benson <span dir=\"ltr\">\n&lt;<a href=\"mailto:eob@csail.mit.edu\" target=\"_blank\">eob@csail.mit.edu</a>&gt;</span> wrote:<br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex; border-left:1px #ccc solid; padding-left:1ex\">\n<div dir=\"ltr\">Hey Michael,\n<div><br>\n</div>\n<div>Hope all is well! FB says your friends with James Tamplin. Do you know him well enough to introduce me to him so I could ask for some advice?</div>\n<div><br>\n</div>\n<div>My cofounder and I are headed out to YC to interview next weekend, and we're trying to make the most of the trip. Firebase and James' other involvements are right up the alley of what we're doing, so I think he would be a really good person to meet.&nbsp;</div>\n<div><br>\n</div>\n<div>Company quickie, for context:</div>\n<div><br>\n<span style=\"color:rgb(0,0,0); font-family:Arial; font-size:12.8000001907349px; white-space:pre-wrap\">Cloudstitch.io is a beginner-friendly reactive platform for making web apps that run off of everyday objects. Objects like Google Spreadsheets and Docs, all\n the way to physical objects like sensors and Amazon Dash-style buttons. Think IFTTT but for building apps.</span><br>\n<div>\n<div><br>\n</div>\n<div>Let me know -- and thanks either way!&nbsp;</div>\n<div><br>\n</div>\n<div>Cheers,</div>\n<div>Ted</div>\n<span><font color=\"#888888\">\n<div><br>\n</div>\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">:: Ted Benson<br>\n::&nbsp;<a href=\"http://people.csail.mit.edu/eob/\" target=\"_blank\">http://people.csail.mit.edu/eob/</a><br>\n<div>:: @edwardbenson</div>\n</div>\n</div>\n</font></span></div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br clear=\"all\">\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">:: Ted Benson<br>\n::&nbsp;<a href=\"http://people.csail.mit.edu/eob/\" target=\"_blank\">http://people.csail.mit.edu/eob/</a><br>\n<div>:: @edwardbenson</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br clear=\"all\">\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">:: Ted Benson<br>\n::&nbsp;<a href=\"http://people.csail.mit.edu/eob/\" target=\"_blank\">http://people.csail.mit.edu/eob/</a><br>\n<div>:: @edwardbenson</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n<br>\n<br clear=\"all\">\n<div><br>\n</div>\n-- <br>\n<div>\n<div dir=\"ltr\">:: Ted Benson<br>\n::&nbsp;<a href=\"http://people.csail.mit.edu/eob/\" target=\"_blank\">http://people.csail.mit.edu/eob/</a><br>\n<div>:: @edwardbenson</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</div>\n</blockquote>\n</div>\n</div>\n</div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_7_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:10pt;color:#000000;background-color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;\">\n<p>Thanks for coming by today. :) Let me know next time you're around and settled and I'll show you a wicked awesome demo of the stuff we've been building!&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p>And congrats on YC!&nbsp;<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n\n</div>\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_8.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:10pt;color:#000000;background-color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;\">\n<p>What about Haystack?&nbsp;https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Beaver.pdf<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n<div style=\"color: rgb(0, 0, 0);\">\n<div style=\"text-align: left;\"><br>\n</div>\n<hr tabindex=\"-1\" style=\"display:inline-block; width:98%\">\n<div id=\"divRplyFwdMsg\" dir=\"ltr\"><font face=\"Calibri, sans-serif\" color=\"#000000\" style=\"font-size:11pt\"><b>From:</b> Christine Spang<br>\n<b>Sent:</b> Thursday, May 21, 2015 6:52 PM<br>\n<b>To:</b> Nylas Study Group<br>\n<b>Subject:</b> Intros &amp; paper suggestions</font>\n<div>&nbsp;</div>\n</div>\n<div>hey folks,\n<div><br>\n</div>\n<div>Thought I'd let y'all know who's on this list. Current list members are:</div>\n<div><br>\n</div>\n<div><b>Nylas team</b></div>\n<div>Michael Grinich</div>\n<div>me</div>\n<div>Kavya Joshi</div>\n<div>Eben Freeman</div>\n<div>Jennie Lees</div>\n<div>Karim Hamidou</div>\n<div>Evan Morikawa</div>\n<div>Ben Gotow</div>\n<div>Rob McQueen</div>\n<div>Kartik Talwar</div>\n<div>Andrea Whiting</div>\n<div>Makala Keys</div>\n<div><br>\n</div>\n<div><b>Friends</b></div>\n<div>Nelson Elhage</div>\n<div>Deborah Hanus</div>\n<div>Owen Derby</div>\n<div>Marco Munizaga</div>\n<div><br>\n</div>\n<div>Anyone have a paper they're really hankering to read or present in two weeks? :)</div>\n<div>--spang</div>\n</div>\n</div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_8_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\n</head>\n<body dir=\"ltr\">\n<div id=\"divtagdefaultwrapper\" style=\"font-size:10pt;color:#000000;background-color:#FFFFFF;font-family:Arial,Helvetica,sans-serif;\">\n<p>What about Haystack?&nbsp;https://www.usenix.org/legacy/event/osdi10/tech/full_papers/Beaver.pdf<br>\n</p>\n<p><br>\n</p>\n<p><br>\n</p>\n\n</div>\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_9.html",
    "content": "<html>\n<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<div dir=\"ltr\">Hi Christine,&nbsp;\n<div><br>\n</div>\n<div>My apologies, please use the below referral code when taking the Insights evaluation:&nbsp;</div>\n<div><br>\n</div>\n<div>\n<p style=\"font-size:13px;margin:0px;font-family:Times;color:rgb(18,85,204)\"><span style=\"font-family:Arial;letter-spacing:0px;color:rgb(35,35,35)\"><b>Go To</b>:&nbsp;&nbsp;<a href=\"https://online.insights.com/evaluator/SNP/discovery\" target=\"_blank\"><span style=\"font-family:Times;letter-spacing:0px;color:rgb(18,85,204)\">https://online.insights.com/evaluator/SNP/discovery</span></a>&nbsp;\n &nbsp;&nbsp;</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\"><b>Referral Code</b>: Nylas2015</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\"><br>\n</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\">Please let us know if you have any questions!</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\"><br>\n</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\">Thank you,</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\">Eva</span></p>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\"><br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\"><span class=\"HOEnZb\"><font color=\"#888888\"></font></span></div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n</body>\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_9_stripped.html",
    "content": "<head>\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=us-ascii\">\n</head>\n<body>\n<div dir=\"ltr\">Hi Christine,&nbsp;\n<div><br>\n</div>\n<div>My apologies, please use the below referral code when taking the Insights evaluation:&nbsp;</div>\n<div><br>\n</div>\n<div>\n<p style=\"font-size:13px;margin:0px;font-family:Times;color:rgb(18,85,204)\"><span style=\"font-family:Arial;letter-spacing:0px;color:rgb(35,35,35)\"><b>Go To</b>:&nbsp;&nbsp;<a href=\"https://online.insights.com/evaluator/SNP/discovery\" target=\"_blank\"><span style=\"font-family:Times;letter-spacing:0px;color:rgb(18,85,204)\">https://online.insights.com/evaluator/SNP/discovery</span></a>&nbsp;\n &nbsp;&nbsp;</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\"><b>Referral Code</b>: Nylas2015</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\"><br>\n</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\">Please let us know if you have any questions!</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\"><br>\n</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\">Thank you,</span></p>\n<p style=\"font-size:13px;margin:0px;font-family:Arial;color:rgb(35,35,35)\"><span style=\"letter-spacing:0px\">Eva</span></p>\n</div>\n<div class=\"gmail_extra\"><br>\n<div class=\"gmail_quote\"><br>\n<blockquote class=\"gmail_quote\" style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex\">\n<div dir=\"ltr\"><span class=\"HOEnZb\"><font color=\"#888888\"></font></span></div>\n</blockquote>\n</div>\n<br>\n</div>\n</div>\n\n</body>"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_BlackBerry.txt",
    "content": "Here is another email\n\nSent from my BlackBerry\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_bullets.txt",
    "content": "test 2 this should list second\n\nand have spaces\n\nand retain this formatting\n\n\n   - how about bullets\n   - and another\n\n\nOn Fri, Feb 24, 2012 at 10:19 AM, <examples@email.goalengine.com> wrote:\n\n> Give us an example of how you applied what they learned to achieve\n> something in your organization\n\n\n\n\n-- \n\n*Joe Smith | Director, Product Management*\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_iPhone.txt",
    "content": "Here is another email\n\nSent from my iPhone\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_multi_word_sent_from_my_mobile_device.txt",
    "content": "Here is another email\n\nSent from my Verizon Wireless BlackBerry\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/emails/email_sent_from_my_not_signature.txt",
    "content": "Here is another email\n\nSent from my desk, is much easier then my mobile phone.\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/module-cache/file.json",
    "content": "{\n  \"foo\": \"bar\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-that-throws-an-exception/index.coffee",
    "content": "throw new Error(\"This package throws an exception\")\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-that-throws-on-activate/index.coffee",
    "content": "module.exports =\n  activate: -> throw new Error('Top that')\n  deactivate: ->\n  serialize: ->\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-that-throws-on-deactivate/index.coffee",
    "content": "module.exports =\n  activate: ->\n  deactivate: -> throw new Error('Top that')\n  serialize: ->\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-broken-keymap/keymaps/broken.json",
    "content": "INVALID\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-broken-package-json/package.json",
    "content": "INVALID\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-config-defaults/index.coffee",
    "content": "module.exports =\n  configDefaults:\n    numbers: { one: 1, two: 2 }\n\n  activate: -> # no-op\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-config-schema/index.coffee",
    "content": "module.exports =\n  config:\n    numbers:\n      type: 'object'\n      properties:\n        one:\n          type: 'integer'\n          default: 1\n        two:\n          type: 'integer'\n          default: 2\n\n  activate: -> # no-op\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-deactivate/index.coffee",
    "content": "module.exports =\n  activate: ->\n  deactivate: ->\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-deprecated-pane-item-method/index.coffee",
    "content": "class TestItem\n  getUri: -> \"test\"\n\nexports.activate = ->\n  NylasEnv.workspace.addOpener -> new TestItem\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-empty-activation-commands/index.coffee",
    "content": "module.exports = activate: ->\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-empty-activation-commands/package.json",
    "content": "{\n  \"name\": \"no events\",\n  \"version\": \"0.1.0\",\n  \"activationCommands\": {\"nylas-workspace\": []}\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-empty-keymap/keymaps/keymap.cson",
    "content": ""
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-empty-keymap/package.json",
    "content": "{\n  \"name\": \"package-with-empty-keymap\",\n  \"version\": \"1.0.0\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-empty-menu/menus/menu.cson",
    "content": ""
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-empty-menu/package.json",
    "content": "{\n  \"name\": \"package-with-empty-menu\",\n  \"version\": \"1.0.0\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-incompatible-native-module/main.js",
    "content": ""
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-incompatible-native-module/node_modules/native-module/build/Release/native.node",
    "content": ""
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-incompatible-native-module/node_modules/native-module/main.js",
    "content": "throw new Error(\"this simulates a native module's failure to load\")\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-incompatible-native-module/node_modules/native-module/package.json",
    "content": "{\n  \"name\": \"native-module\",\n  \"main\": \"./main.js\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-incompatible-native-module/package.json",
    "content": "{\n  \"name\": \"package-with-incompatible-native-module\",\n  \"version\": \"1.0\",\n  \"main\": \"./main.js\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-index/index.coffee",
    "content": "module.exports =\nactivate: ->\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-invalid-styles/package.json",
    "content": "{\n  \"name\": \"package-with-invalid-styles\",\n  \"version\": \"1.0.0\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-invalid-styles/styles/index.less",
    "content": "{\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-keymaps/keymaps/keymap-1.json",
    "content": "{\n  \"my-package:command-a\": \"ctrl+x\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-keymaps/keymaps/keymap-2.json",
    "content": "{\n  \"my-package:command-b\": \"ctrl+y\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-keymaps-manifest/keymaps/keymap-1.json",
    "content": "{\n  \"my-package:command-a\": \"ctrl+x\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-keymaps-manifest/keymaps/keymap-2.json",
    "content": "{\n  \"my-package:command-b\": \"ctrl+y\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-keymaps-manifest/package.json",
    "content": "{\n  \"keymaps\": [\"keymap-2\", \"keymap-1\"]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-main/main-module.coffee",
    "content": "module.exports =\nactivate: ->\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-main/package.json",
    "content": "{\n  \"main\": \"main-module.coffee\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-menus/menus/menu-1.json",
    "content": "{\n  \"menu\": [\n    { \"label\": \"Last\" }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-menus/menus/menu-2.json",
    "content": "{\n  \"menu\": [\n    { \"label\": \"Second to Last\" }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-menus-manifest/menus/menu-1.json",
    "content": "{\n  \"menu\": [\n    { \"label\": \"Last\" }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-menus-manifest/menus/menu-2.json",
    "content": "{\n  \"menu\": [\n    { \"label\": \"Second to Last\" }\n  ]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-menus-manifest/package.json",
    "content": "{\n  \"menus\": [\"menu-2\", \"menu-1\"]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-models/main-module.coffee",
    "content": "Model = require '../../../../src/flux/models/model'\nTask = require '../../../../src/task'\n{TaskSubclassA} = require '../../../stores/task-subclass'\n\nclass ModelA extends Model\nclass ModelB extends Model\n\nmodule.exports =\n  modelConstructors: [ModelA, ModelB]\n  taskConstructors: [TaskSubclassA]\n  activate: ->\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-models/package.json",
    "content": "{\n  \"main\": \"main-module.coffee\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-serialization/index.coffee",
    "content": "module.exports =\n  activate: ({@someNumber}) ->\n    @someNumber ?= 1\n\n  serialize: ->\n    {@someNumber}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-serialize-error/index.coffee",
    "content": "module.exports =\n  activate: ->\n\n  deactivate: ->\n\n  serialize: ->\n    throw new Error(\"I'm no good at this.\")\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-serialize-error/package.json",
    "content": "{\n  \"main\": \"index.coffee\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-style-sheets-manifest/package.json",
    "content": "{\n  \"styleSheets\": [\"2\", \"1\"]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-style-sheets-manifest/styles/1.css",
    "content": "#jasmine-content {\n  font-size: 1px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-style-sheets-manifest/styles/2.less",
    "content": "@size: 2px;\n\n#jasmine-content {\n  font-size: @size;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-style-sheets-manifest/styles/3.css",
    "content": "#jasmine-content {\n  font-size: 3px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-styles/styles/1.css",
    "content": "#jasmine-content {\n  font-size: 1px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-styles/styles/2.less",
    "content": "@size: 2px;\n\n#jasmine-content {\n  font-size: @size;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-styles/styles/3.test-context.css",
    "content": "#jasmine-content {\n  font-size: 3px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-styles/styles/4.css",
    "content": "a { color: red }\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-with-stylesheets-manifest/package.json",
    "content": "{\n  \"styleSheets\": [\"2\", \"1\"]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/package-without-module/package.json",
    "content": "{\n  \"name\": \"perfect\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-incomplete-ui-variables/package.json",
    "content": "{\n  \"theme\": \"ui\",\n  \"styleSheets\": [\"editor.less\"]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-incomplete-ui-variables/styles/editor.less",
    "content": "@import \"ui-variables\";\n\nnylas-theme-wrap {\n  padding-top: @component-padding;\n  padding-right: @component-padding;\n  padding-bottom: @component-padding;\n\n  color: @input-bg;\n  background-color: @spec-test-variable; // From the fallback variables, not overridden\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-incomplete-ui-variables/styles/ui-variables.less",
    "content": "// This does not contain all of the ui-variables available.\n@background-primary: #00f; // Changed\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-index-css/index.css",
    "content": "nylas-theme-wrap {\n  padding-top: 1234px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-index-css/package.json",
    "content": "{\n  \"theme\": \"ui\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-index-less/index.less",
    "content": "@padding: 4321px;\n\nnylas-theme-wrap {\n  padding-top: @padding;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-index-less/package.json",
    "content": "{\n  \"theme\": \"ui\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-invalid-styles/index.less",
    "content": "<>\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-invalid-styles/package.json",
    "content": "{\n  \"name\": \"theme-with-invalid-styles\",\n  \"theme\": \"ui\"\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-package-file/package.json",
    "content": "{\n  \"theme\": \"ui\",\n  \"styleSheets\": [\"first.css\", \"second.less\", \"last.css\"]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-package-file/styles/first.css",
    "content": "nylas-theme-wrap {\n  padding-top: 101px;\n  padding-right: 101px;\n  padding-bottom: 101px;\n\n  color: red;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-package-file/styles/last.css",
    "content": "nylas-theme-wrap {\n/*  padding-top: 103px;\n  padding-right: 103px;*/\n  padding-bottom: 103px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-package-file/styles/second.less",
    "content": "@number: 102px;\n\nnylas-theme-wrap {\n/*  padding-top: 102px;*/\n  padding-right: @number;\n  padding-bottom: @number;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-ui-variables/package.json",
    "content": "{\n  \"theme\": \"ui\",\n  \"styleSheets\": [\"editor.less\"]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-ui-variables/styles/editor.less",
    "content": "@import \"ui-variables\";\n\nnylas-theme-wrap {\n  padding-top: @component-padding;\n  padding-right: @component-padding;\n  padding-bottom: @component-padding;\n\n  color: @input-background-color;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-with-ui-variables/styles/ui-variables.less",
    "content": "// Variables different from the original are marked 'Changed'\n\n@text-color: #333;\n@text-color-subtle: #777;\n@text-color-highlight: #111;\n@text-color-selected: @text-color-highlight;\n\n@text-color-info: #5293d8;\n@text-color-success: #1fe977;\n@text-color-warning: #f78a46;\n@text-color-error: #c00;\n\n@background-color-info: #0098ff;\n@background-color-success: #17ca65;\n@background-color-warning: #ff4800;\n@background-color-error: #c00;\n@background-color-highlight: rgba(255, 255, 255, 0.10);\n@background-color-selected: @background-color-highlight;\n\n@background-primary: #00f; // Changed\n\n@base-background-color: #fff;\n@base-border-color: #eee;\n\n@pane-item-background-color: @base-background-color;\n@pane-item-border-color: @base-border-color;\n\n@input-background-color: #f00; // Changed\n@input-border-color: @base-border-color;\n\n@tool-panel-background-color: #f4f4f4;\n@tool-panel-border-color: @base-border-color;\n\n@inset-panel-background-color: #eee;\n@inset-panel-border-color: @base-border-color;\n\n@panel-heading-background-color: #ddd;\n@panel-heading-border-color: transparent;\n\n@overlay-background-color: #f4f4f4;\n@overlay-border-color: @base-border-color;\n\n@button-background-color: #ccc;\n@button-background-color-hover: lighten(@button-background-color, 5%);\n@button-background-color-selected: @button-background-color-hover;\n@button-border-color: #aaa;\n\n@tab-bar-background-color: #fff;\n@tab-bar-border-color: darken(@tab-background-color-active, 10%);\n@tab-background-color: #f4f4f4;\n@tab-background-color-active: #fff;\n@tab-border-color: @base-border-color;\n\n@tree-view-background-color: @tool-panel-background-color;\n@tree-view-border-color: @tool-panel-border-color;\n\n@ui-site-color-1: @background-color-success; // green\n@ui-site-color-2: @background-color-info; // blue\n@ui-site-color-3: @background-color-warning; // orange\n@ui-site-color-4: #db2ff4; // purple\n@ui-site-color-5: #f5e11d; // yellow\n\n@font-size: 12px;\n\n@disclosure-arrow-size: 12px;\n\n@component-padding: 150px;\n@component-icon-padding: 5px;\n@component-icon-size: 16px;\n@component-line-height: 25px;\n@component-border-radius: 2px;\n\n@tab-height: 30px;\n\n@font-family: Arial;\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-without-package-file/styles/a.css",
    "content": "nylas-theme-wrap {\n  padding-top: 10px;\n  padding-right: 10px;\n  padding-bottom: 10px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-without-package-file/styles/b.css",
    "content": "nylas-theme-wrap {\n  padding-right: 20px;\n  padding-bottom: 20px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-without-package-file/styles/c.less",
    "content": "@number: 30px;\n\nnylas-theme-wrap {\n  padding-bottom: @number;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/packages/theme-without-package-file/styles/d.csv",
    "content": "nylas-theme-wrap {\n  padding-top: 100px;\n  padding-right: 100px;\n  padding-bottom: 100px;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/paste/excel-paste-in.html",
    "content": "\"<html xmlns:v=\"urn:schemas-microsoft-com:vml\"\nxmlns:o=\"urn:schemas-microsoft-com:office:office\"\nxmlns:x=\"urn:schemas-microsoft-com:office:excel\"\nxmlns=\"http://www.w3.org/TR/REC-html40\">\n\n<head>\n<meta http-equiv=Content-Type content=\"text/html; charset=utf-8\">\n<meta name=ProgId content=Excel.Sheet>\n<meta name=Generator content=\"Microsoft Excel 14\">\n<link id=Main-File rel=Main-File\nhref=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip.htm\">\n<link rel=File-List\nhref=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_filelist.xml\">\n<!--[if !mso]>\n<style>\nv\\:* {behavior:url(#default#VML);}\no\\:* {behavior:url(#default#VML);}\nx\\:* {behavior:url(#default#VML);}\n.shape {behavior:url(#default#VML);}\n</style>\n<![endif]-->\n<style>\n<!--table\n\t{mso-displayed-decimal-separator:\"\\.\";\n\tmso-displayed-thousand-separator:\"\\,\";}\n@page\n\t{margin:1.0in .75in 1.0in .75in;\n\tmso-header-margin:.5in;\n\tmso-footer-margin:.5in;}\n.font5\n\t{color:black;\n\tfont-size:10.0pt;\n\tfont-weight:400;\n\tfont-style:normal;\n\ttext-decoration:none;\n\tfont-family:Geneva;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;}\n.font6\n\t{color:black;\n\tfont-size:10.0pt;\n\tfont-weight:700;\n\tfont-style:normal;\n\ttext-decoration:none;\n\tfont-family:Geneva;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;}\ntd\n\t{padding:0px;\n\tmso-ignore:padding;\n\tcolor:black;\n\tfont-size:12.0pt;\n\tfont-weight:400;\n\tfont-style:normal;\n\ttext-decoration:none;\n\tfont-family:Calibri, sans-serif;\n\tmso-font-charset:0;\n\tmso-number-format:General;\n\ttext-align:general;\n\tvertical-align:bottom;\n\tborder:none;\n\tmso-background-source:auto;\n\tmso-pattern:auto;\n\tmso-protection:locked visible;\n\twhite-space:nowrap;\n\tmso-rotate:0;}\n.xl63\n\t{vertical-align:middle;}\n.xl64\n\t{font-size:24.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;}\n.xl65\n\t{border-top:none;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl66\n\t{border-top:none;\n\tborder-right:1.0pt solid windowtext;\n\tborder-bottom:none;\n\tborder-left:none;}\n.xl67\n\t{font-size:22.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;}\n.xl68\n\t{font-size:36.0pt;\n\tfont-weight:700;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;}\n.xl69\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\tvertical-align:middle;}\n.xl70\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\tvertical-align:middle;\n\tborder-top:none;\n\tborder-right:1.0pt solid windowtext;\n\tborder-bottom:none;\n\tborder-left:none;}\n.xl71\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:left;\n\tvertical-align:middle;\n\tborder-top:none;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl72\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\tvertical-align:middle;\n\tborder-top:none;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl73\n\t{font-size:26.0pt;\n\tfont-style:italic;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;\n\tborder-top:1.0pt solid windowtext;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl74\n\t{font-size:26.0pt;\n\tfont-style:italic;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;\n\tborder-top:1.0pt solid windowtext;\n\tborder-right:1.0pt solid windowtext;\n\tborder-bottom:none;\n\tborder-left:none;}\n-->\n</style>\n</head>\n\n<body link=blue vlink=purple>\n\n<table border=0 cellpadding=0 cellspacing=0 width=471 style='border-collapse:\n collapse;width:471pt'>\n<!--StartFragment-->\n <col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>\n <col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>\n <col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>\n <col width=67 style='mso-width-source:userset;mso-width-alt:2858;width:67pt'>\n <col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>\n <col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>\n <col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>\n <tr height=33 style='height:33.0pt'>\n  <td colspan=2 height=33 class=xl73 width=190 style='border-right:1.0pt solid black;\n  height:33.0pt;width:190pt'>+ Pros +</td>\n  <td class=xl64 width=12 style='width:12pt'></td>\n  <td class=xl67 width=67 style='width:67pt'>vs.</td>\n  <td width=12 style='width:12pt'></td>\n  <td colspan=2 class=xl73 width=190 style='border-right:1.0pt solid black;\n  width:190pt'>- Cons -</td>\n </tr>\n <tr height=15 style='height:15.0pt'>\n  <td height=15 class=xl65 style='height:15.0pt;font-size:12.0pt;color:white;\n  font-weight:700;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  1.0pt solid white;border-left:1.0pt solid windowtext;background:black;\n  mso-pattern:black none'>Item</td>\n  <td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:\n  black none'>Importance</td>\n  <td></td>\n  <td></td>\n  <td></td>\n  <td class=xl65 style='font-size:12.0pt;color:white;font-weight:700;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:1.0pt solid white;\n  border-left:1.0pt solid windowtext;background:black;mso-pattern:black none'>Item</td>\n  <td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:\n  black none'>Importance</td>\n </tr>\n <tr height=28 style='mso-height-source:userset;height:28.0pt'>\n  <td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;\n  font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Good</td>\n  <td class=xl69 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;background:#76933C;mso-pattern:#76933C none'>2</td>\n  <td class=xl63></td>\n  <td class=xl68></td>\n  <td class=xl63></td>\n  <td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:none;\n  border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Bad</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>2</td>\n </tr>\n <tr height=28 style='mso-height-source:userset;height:28.0pt'>\n  <td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;\n  font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  none;border-left:1.0pt solid windowtext;background:#9BBB59;mso-pattern:#9BBB59 none'>Cheap</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#9BBB59;mso-pattern:#9BBB59 none'>4</td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:none;\n  border-left:1.0pt solid windowtext;background:#C0504D;mso-pattern:#C0504D none'>Expensive</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#C0504D;mso-pattern:#C0504D none'>3</td>\n </tr>\n <tr height=28 style='mso-height-source:userset;height:28.0pt'>\n  <td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;\n  font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Fast</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#76933C;mso-pattern:#76933C none'>1</td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:none;\n  border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Slow</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>1</td>\n </tr>\n<!--EndFragment-->\n</table>\n\n</body>\n\n</html>\n\""
  },
  {
    "path": "packages/client-app/spec/fixtures/paste/excel-paste-out.html",
    "content": "\"<html xmlns:v=\"urn:schemas-microsoft-com:vml\"\nxmlns:o=\"urn:schemas-microsoft-com:office:office\"\nxmlns:x=\"urn:schemas-microsoft-com:office:excel\"\nxmlns=\"http://www.w3.org/TR/REC-html40\">\n\n<head>\n<meta http-equiv=Content-Type content=\"text/html; charset=utf-8\">\n<meta name=ProgId content=Excel.Sheet>\n<meta name=Generator content=\"Microsoft Excel 14\">\n<link id=Main-File rel=Main-File\nhref=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip.htm\">\n<link rel=File-List\nhref=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_filelist.xml\">\n<!--[if !mso]>\n<style>\nv\\:* {behavior:url(#default#VML);}\no\\:* {behavior:url(#default#VML);}\nx\\:* {behavior:url(#default#VML);}\n.shape {behavior:url(#default#VML);}\n</style>\n<![endif]-->\n<style>\n<!--table\n\t{mso-displayed-decimal-separator:\"\\.\";\n\tmso-displayed-thousand-separator:\"\\,\";}\n@page\n\t{margin:1.0in .75in 1.0in .75in;\n\tmso-header-margin:.5in;\n\tmso-footer-margin:.5in;}\n.font5\n\t{color:black;\n\tfont-size:10.0pt;\n\tfont-weight:400;\n\tfont-style:normal;\n\ttext-decoration:none;\n\tfont-family:Geneva;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;}\n.font6\n\t{color:black;\n\tfont-size:10.0pt;\n\tfont-weight:700;\n\tfont-style:normal;\n\ttext-decoration:none;\n\tfont-family:Geneva;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;}\ntd\n\t{padding:0px;\n\tmso-ignore:padding;\n\tcolor:black;\n\tfont-size:12.0pt;\n\tfont-weight:400;\n\tfont-style:normal;\n\ttext-decoration:none;\n\tfont-family:Calibri, sans-serif;\n\tmso-font-charset:0;\n\tmso-number-format:General;\n\ttext-align:general;\n\tvertical-align:bottom;\n\tborder:none;\n\tmso-background-source:auto;\n\tmso-pattern:auto;\n\tmso-protection:locked visible;\n\twhite-space:nowrap;\n\tmso-rotate:0;}\n.xl63\n\t{vertical-align:middle;}\n.xl64\n\t{font-size:24.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;}\n.xl65\n\t{border-top:none;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl66\n\t{border-top:none;\n\tborder-right:1.0pt solid windowtext;\n\tborder-bottom:none;\n\tborder-left:none;}\n.xl67\n\t{font-size:22.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;}\n.xl68\n\t{font-size:36.0pt;\n\tfont-weight:700;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;}\n.xl69\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\tvertical-align:middle;}\n.xl70\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\tvertical-align:middle;\n\tborder-top:none;\n\tborder-right:1.0pt solid windowtext;\n\tborder-bottom:none;\n\tborder-left:none;}\n.xl71\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:left;\n\tvertical-align:middle;\n\tborder-top:none;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl72\n\t{font-size:14.0pt;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\tvertical-align:middle;\n\tborder-top:none;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl73\n\t{font-size:26.0pt;\n\tfont-style:italic;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;\n\tborder-top:1.0pt solid windowtext;\n\tborder-right:none;\n\tborder-bottom:none;\n\tborder-left:1.0pt solid windowtext;}\n.xl74\n\t{font-size:26.0pt;\n\tfont-style:italic;\n\tfont-family:Calibri;\n\tmso-generic-font-family:auto;\n\tmso-font-charset:0;\n\ttext-align:center;\n\tvertical-align:middle;\n\tborder-top:1.0pt solid windowtext;\n\tborder-right:1.0pt solid windowtext;\n\tborder-bottom:none;\n\tborder-left:none;}\n-->\n</style>\n</head>\n\n<body link=blue vlink=purple>\n\n<table border=0 cellpadding=0 cellspacing=0 width=471 style='border-collapse:\n collapse;width:471pt'>\n<!--StartFragment-->\n <col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>\n <col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>\n <col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>\n <col width=67 style='mso-width-source:userset;mso-width-alt:2858;width:67pt'>\n <col width=12 style='mso-width-source:userset;mso-width-alt:512;width:12pt'>\n <col width=110 style='mso-width-source:userset;mso-width-alt:4693;width:110pt'>\n <col width=80 style='mso-width-source:userset;mso-width-alt:3413;width:80pt'>\n <tr height=33 style='height:33.0pt'>\n  <td colspan=2 height=33 class=xl73 width=190 style='border-right:1.0pt solid black;\n  height:33.0pt;width:190pt'>+ Pros +</td>\n  <td class=xl64 width=12 style='width:12pt'></td>\n  <td class=xl67 width=67 style='width:67pt'>vs.</td>\n  <td width=12 style='width:12pt'></td>\n  <td colspan=2 class=xl73 width=190 style='border-right:1.0pt solid black;\n  width:190pt'>- Cons -</td>\n </tr>\n <tr height=15 style='height:15.0pt'>\n  <td height=15 class=xl65 style='height:15.0pt;font-size:12.0pt;color:white;\n  font-weight:700;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  1.0pt solid white;border-left:1.0pt solid windowtext;background:black;\n  mso-pattern:black none'>Item</td>\n  <td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:\n  black none'>Importance</td>\n  <td></td>\n  <td></td>\n  <td></td>\n  <td class=xl65 style='font-size:12.0pt;color:white;font-weight:700;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:1.0pt solid white;\n  border-left:1.0pt solid windowtext;background:black;mso-pattern:black none'>Item</td>\n  <td class=xl66 style='font-size:12.0pt;color:white;font-weight:700;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:1.0pt solid white;border-left:none;background:black;mso-pattern:\n  black none'>Importance</td>\n </tr>\n <tr height=28 style='mso-height-source:userset;height:28.0pt'>\n  <td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;\n  font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Good</td>\n  <td class=xl69 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;background:#76933C;mso-pattern:#76933C none'>2</td>\n  <td class=xl63></td>\n  <td class=xl68></td>\n  <td class=xl63></td>\n  <td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:none;\n  border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Bad</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>2</td>\n </tr>\n <tr height=28 style='mso-height-source:userset;height:28.0pt'>\n  <td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;\n  font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  none;border-left:1.0pt solid windowtext;background:#9BBB59;mso-pattern:#9BBB59 none'>Cheap</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#9BBB59;mso-pattern:#9BBB59 none'>4</td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:none;\n  border-left:1.0pt solid windowtext;background:#C0504D;mso-pattern:#C0504D none'>Expensive</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#C0504D;mso-pattern:#C0504D none'>3</td>\n </tr>\n <tr height=28 style='mso-height-source:userset;height:28.0pt'>\n  <td height=28 class=xl71 style='height:28.0pt;font-size:14.0pt;color:white;\n  font-weight:400;text-decoration:none;text-underline-style:none;text-line-through:\n  none;font-family:Calibri;border-top:none;border-right:none;border-bottom:\n  none;border-left:1.0pt solid windowtext;background:#76933C;mso-pattern:#76933C none'>Fast</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#76933C;mso-pattern:#76933C none'>1</td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl63></td>\n  <td class=xl72 style='font-size:14.0pt;color:white;font-weight:400;\n  text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:none;border-bottom:none;\n  border-left:1.0pt solid windowtext;background:#963634;mso-pattern:#963634 none'>Slow</td>\n  <td class=xl70 align=right style='font-size:14.0pt;color:white;font-weight:\n  400;text-decoration:none;text-underline-style:none;text-line-through:none;\n  font-family:Calibri;border-top:none;border-right:1.0pt solid windowtext;\n  border-bottom:none;border-left:none;background:#963634;mso-pattern:#963634 none'>1</td>\n </tr>\n<!--EndFragment-->\n</table>\n\n</body>\n\n</html>\n\""
  },
  {
    "path": "packages/client-app/spec/fixtures/paste/word-paste-in.html",
    "content": "<html xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" xmlns:m=\"http://schemas.microsoft.com/office/2004/12/omml\" xmlns:mv=\"http://macVmlSchemaUri\" xmlns=\"http://www.w3.org/TR/REC-html40\">\n\n<head>\n<meta name=\"Title\" content=\"\">\n<meta name=\"Keywords\" content=\"\">\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n<meta name=\"ProgId\" content=\"Word.Document\">\n<meta name=\"Generator\" content=\"Microsoft Word 14\">\n<meta name=\"Originator\" content=\"Microsoft Word 14\">\n<link rel=\"File-List\" href=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_filelist.xml\">\n<link rel=\"Edit-Time-Data\" href=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_editdata.mso\">\n<!--[if !mso]>\n<style>@namespace \"http://www.w3.org/1999/xhtml\";\nhtml {\n    display: block\n}\nhead {\n    display: none\n}\nmeta {\n    display: none\n}\ntitle {\n    display: none\n}\nlink {\n    display: none\n}\nstyle {\n    display: none\n}\nscript {\n    display: none\n}\nbody {\n    display: block;\n    margin: 8px\n}\nbody:-webkit-full-page-media {\n    background-color: rgb(0, 0, 0)\n}\np {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1__qem;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n}\ndiv {\n    display: block\n}\nlayer {\n    display: block\n}\narticle, aside, footer, header, hgroup, main, nav, section {\n    display: block\n}\nmarquee {\n    display: inline-block;\n}\naddress {\n    display: block\n}\nblockquote {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 40px;\n    -webkit-margin-end: 40px;\n}\nfigcaption {\n    display: block\n}\nfigure {\n    display: block;\n    -webkit-margin-before: 1em;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 40px;\n    -webkit-margin-end: 40px;\n}\nq {\n    display: inline\n}\nq:before {\n    content: open-quote;\n}\nq:after {\n    content: close-quote;\n}\ncenter {\n    display: block;\n    text-align: -webkit-center\n}\nhr {\n    display: block;\n    -webkit-margin-before: 0.5em;\n    -webkit-margin-after: 0.5em;\n    -webkit-margin-start: auto;\n    -webkit-margin-end: auto;\n    border-style: inset;\n    border-width: 1px\n}\nmap {\n    display: inline\n}\nvideo {\n    object-fit: contain;\n}\nh1 {\n    display: block;\n    font-size: 2em;\n    -webkit-margin-before: 0.67__qem;\n    -webkit-margin-after: 0.67em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\n:-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.5em;\n    -webkit-margin-before: 0.83__qem;\n    -webkit-margin-after: 0.83em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.17em;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.00em;\n    -webkit-margin-before: 1.33__qem;\n    -webkit-margin-after: 1.33em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: .83em;\n    -webkit-margin-before: 1.67__qem;\n    -webkit-margin-after: 1.67em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: .67em;\n    -webkit-margin-before: 2.33__qem;\n    -webkit-margin-after: 2.33em;\n}\nh2 {\n    display: block;\n    font-size: 1.5em;\n    -webkit-margin-before: 0.83__qem;\n    -webkit-margin-after: 0.83em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh3 {\n    display: block;\n    font-size: 1.17em;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh4 {\n    display: block;\n    -webkit-margin-before: 1.33__qem;\n    -webkit-margin-after: 1.33em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh5 {\n    display: block;\n    font-size: .83em;\n    -webkit-margin-before: 1.67__qem;\n    -webkit-margin-after: 1.67em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh6 {\n    display: block;\n    font-size: .67em;\n    -webkit-margin-before: 2.33__qem;\n    -webkit-margin-after: 2.33em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\ntable {\n    display: table;\n    border-collapse: separate;\n    border-spacing: 2px;\n    border-color: gray\n}\nthead {\n    display: table-header-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntbody {\n    display: table-row-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntfoot {\n    display: table-footer-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntable > tr {\n    vertical-align: middle;\n}\ncol {\n    display: table-column\n}\ncolgroup {\n    display: table-column-group\n}\ntr {\n    display: table-row;\n    vertical-align: inherit;\n    border-color: inherit\n}\ntd, th {\n    display: table-cell;\n    vertical-align: inherit\n}\nth {\n    font-weight: bold\n}\ncaption {\n    display: table-caption;\n    text-align: -webkit-center\n}\nul, menu, dir {\n    display: block;\n    list-style-type: disc;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    -webkit-padding-start: 40px\n}\nol {\n    display: block;\n    list-style-type: decimal;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    -webkit-padding-start: 40px\n}\nli {\n    display: list-item;\n    text-align: -webkit-match-parent;\n}\nul ul, ol ul {\n    list-style-type: circle\n}\nol ol ul, ol ul ul, ul ol ul, ul ul ul {\n    list-style-type: square\n}\ndd {\n    display: block;\n    -webkit-margin-start: 40px\n}\ndl {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n}\ndt {\n    display: block\n}\nol ul, ul ol, ul ul, ol ol {\n    -webkit-margin-before: 0;\n    -webkit-margin-after: 0\n}\nform {\n    display: block;\n    margin-top: 0__qem;\n}\nlabel {\n    cursor: default;\n}\nlegend {\n    display: block;\n    -webkit-padding-start: 2px;\n    -webkit-padding-end: 2px;\n    border: none\n}\nfieldset {\n    display: block;\n    -webkit-margin-start: 2px;\n    -webkit-margin-end: 2px;\n    -webkit-padding-before: 0.35em;\n    -webkit-padding-start: 0.75em;\n    -webkit-padding-end: 0.75em;\n    -webkit-padding-after: 0.625em;\n    border: 2px groove ThreeDFace;\n    min-width: -webkit-min-content;\n}\nbutton {\n    -webkit-appearance: button;\n}\ninput, textarea, keygen, select, button, meter, progress {\n    -webkit-writing-mode: horizontal-tb !important;\n}\ninput, textarea, keygen, select, button {\n    margin: 0__qem;\n    font: -webkit-small-control;\n    text-rendering: auto;\n    color: initial;\n    letter-spacing: normal;\n    word-spacing: normal;\n    line-height: normal;\n    text-transform: none;\n    text-indent: 0;\n    text-shadow: none;\n    display: inline-block;\n    text-align: start;\n}\ninput[type=\"hidden\" i] {\n    display: none\n}\ninput {\n    -webkit-appearance: textfield;\n    padding: 1px;\n    background-color: white;\n    border: 2px inset;\n    -webkit-rtl-ordering: logical;\n    -webkit-user-select: text;\n    cursor: auto;\n}\ninput[type=\"search\" i] {\n    -webkit-appearance: searchfield;\n    box-sizing: border-box;\n}\ninput::-webkit-textfield-decoration-container {\n    display: flex;\n    align-items: center;\n    -webkit-user-modify: read-only !important;\n    content: none !important;\n}\ninput[type=\"search\" i]::-webkit-textfield-decoration-container {\n    direction: ltr;\n}\ninput::-webkit-clear-button {\n    -webkit-appearance: searchfield-cancel-button;\n    display: inline-block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-margin-start: 2px;\n    opacity: 0;\n    pointer-events: none;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-clear-button {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"search\" i]::-webkit-search-cancel-button {\n    -webkit-appearance: searchfield-cancel-button;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-margin-start: 1px;\n    opacity: 0;\n    pointer-events: none;\n}\ninput[type=\"search\" i]:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-search-cancel-button {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"search\" i]::-webkit-search-decoration {\n    -webkit-appearance: searchfield-decoration;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-align-self: flex-start;\n    margin: auto 0;\n}\ninput[type=\"search\" i]::-webkit-search-results-decoration {\n    -webkit-appearance: searchfield-results-decoration;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-align-self: flex-start;\n    margin: auto 0;\n}\ninput::-webkit-inner-spin-button {\n    -webkit-appearance: inner-spin-button;\n    display: inline-block;\n    cursor: default;\n    flex: none;\n    align-self: stretch;\n    -webkit-user-select: none;\n    -webkit-user-modify: read-only !important;\n    opacity: 0;\n    pointer-events: none;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-inner-spin-button {\n    opacity: 1;\n    pointer-events: auto;\n}\nkeygen, select {\n    border-radius: 5px;\n}\nkeygen::-webkit-keygen-select {\n    margin: 0px;\n}\ntextarea {\n    -webkit-appearance: textarea;\n    background-color: white;\n    border: 1px solid;\n    -webkit-rtl-ordering: logical;\n    -webkit-user-select: text;\n    flex-direction: column;\n    resize: auto;\n    cursor: auto;\n    padding: 2px;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n::-webkit-input-placeholder {\n    -webkit-text-security: none;\n    color: darkGray;\n    pointer-events: none !important;\n}\ninput::-webkit-input-placeholder {\n    white-space: pre;\n    word-wrap: normal;\n    overflow: hidden;\n    -webkit-user-modify: read-only !important;\n}\ninput[type=\"password\" i] {\n    -webkit-text-security: disc !important;\n}\ninput[type=\"hidden\" i], input[type=\"image\" i], input[type=\"file\" i] {\n    -webkit-appearance: initial;\n    padding: initial;\n    background-color: initial;\n    border: initial;\n}\ninput[type=\"file\" i] {\n    align-items: baseline;\n    color: inherit;\n    text-align: start !important;\n}\ninput:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {\n    background-color: #FAFFBD !important;\n    background-image:none !important;\n    color: #000000 !important;\n}\ninput[type=\"radio\" i], input[type=\"checkbox\" i] {\n    margin: 3px 0.5ex;\n    padding: initial;\n    background-color: initial;\n    border: initial;\n}\ninput[type=\"button\" i], input[type=\"submit\" i], input[type=\"reset\" i] {\n    -webkit-appearance: push-button;\n    -webkit-user-select: none;\n    white-space: pre\n}\ninput[type=\"file\" i]::-webkit-file-upload-button {\n    -webkit-appearance: push-button;\n    -webkit-user-modify: read-only !important;\n    white-space: nowrap;\n    margin: 0;\n    font-size: inherit;\n}\ninput[type=\"button\" i], input[type=\"submit\" i], input[type=\"reset\" i], input[type=\"file\" i]::-webkit-file-upload-button, button {\n    align-items: flex-start;\n    text-align: center;\n    cursor: default;\n    color: ButtonText;\n    padding: 2px 6px 3px 6px;\n    border: 2px outset ButtonFace;\n    background-color: ButtonFace;\n    box-sizing: border-box\n}\ninput[type=\"range\" i] {\n    -webkit-appearance: slider-horizontal;\n    padding: initial;\n    border: initial;\n    margin: 2px;\n    color: #909090;\n}\ninput[type=\"range\" i]::-webkit-slider-container, input[type=\"range\" i]::-webkit-media-slider-container {\n    flex: 1;\n    min-width: 0;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: flex;\n}\ninput[type=\"range\" i]::-webkit-slider-runnable-track {\n    flex: 1;\n    min-width: 0;\n    -webkit-align-self: center;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: block;\n}\ninput[type=\"range\" i]::-webkit-slider-thumb, input[type=\"range\" i]::-webkit-media-slider-thumb {\n    -webkit-appearance: sliderthumb-horizontal;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: block;\n}\ninput[type=\"button\" i]:disabled, input[type=\"submit\" i]:disabled, input[type=\"reset\" i]:disabled,\ninput[type=\"file\" i]:disabled::-webkit-file-upload-button, button:disabled,\nselect:disabled, keygen:disabled, optgroup:disabled, option:disabled,\nselect[disabled]>option {\n    color: GrayText\n}\ninput[type=\"button\" i]:active, input[type=\"submit\" i]:active, input[type=\"reset\" i]:active, input[type=\"file\" i]:active::-webkit-file-upload-button, button:active {\n    border-style: inset\n}\ninput[type=\"button\" i]:active:disabled, input[type=\"submit\" i]:active:disabled, input[type=\"reset\" i]:active:disabled, input[type=\"file\" i]:active:disabled::-webkit-file-upload-button, button:active:disabled {\n    border-style: outset\n}\noption:-internal-spatial-navigation-focus {\n    outline: black dashed 1px;\n    outline-offset: -1px;\n}\ndatalist {\n    display: none\n}\narea {\n    display: inline;\n    cursor: pointer;\n}\nparam {\n    display: none\n}\ninput[type=\"checkbox\" i] {\n    -webkit-appearance: checkbox;\n    box-sizing: border-box;\n}\ninput[type=\"radio\" i] {\n    -webkit-appearance: radio;\n    box-sizing: border-box;\n}\ninput[type=\"color\" i] {\n    -webkit-appearance: square-button;\n    width: 44px;\n    height: 23px;\n    background-color: ButtonFace;\n    border: 1px #a9a9a9 solid;\n    padding: 1px 2px;\n}\ninput[type=\"color\" i]::-webkit-color-swatch-wrapper {\n    display:flex;\n    padding: 4px 2px;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    width: 100%;\n    height: 100%\n}\ninput[type=\"color\" i]::-webkit-color-swatch {\n    background-color: #000000;\n    border: 1px solid #777777;\n    flex: 1;\n    min-width: 0;\n    -webkit-user-modify: read-only !important;\n}\ninput[type=\"color\" i][list] {\n    -webkit-appearance: menulist;\n    width: 88px;\n    height: 23px\n}\ninput[type=\"color\" i][list]::-webkit-color-swatch-wrapper {\n    padding-left: 8px;\n    padding-right: 24px;\n}\ninput[type=\"color\" i][list]::-webkit-color-swatch {\n    border-color: #000000;\n}\ninput::-webkit-calendar-picker-indicator {\n    display: inline-block;\n    width: 0.66em;\n    height: 0.66em;\n    padding: 0.17em 0.34em;\n    -webkit-user-modify: read-only !important;\n    opacity: 0;\n    pointer-events: none;\n}\ninput::-webkit-calendar-picker-indicator:hover {\n    background-color: #eee;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-calendar-picker-indicator,\ninput::-webkit-calendar-picker-indicator:focus {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"date\" i]:disabled::-webkit-clear-button,\ninput[type=\"date\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"datetime-local\" i]:disabled::-webkit-clear-button,\ninput[type=\"datetime-local\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"month\" i]:disabled::-webkit-clear-button,\ninput[type=\"month\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"week\" i]:disabled::-webkit-clear-button,\ninput[type=\"week\" i]:disabled::-webkit-inner-spin-button,\ninput:disabled::-webkit-calendar-picker-indicator,\ninput[type=\"date\" i][readonly]::-webkit-clear-button,\ninput[type=\"date\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"datetime-local\" i][readonly]::-webkit-clear-button,\ninput[type=\"datetime-local\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"month\" i][readonly]::-webkit-clear-button,\ninput[type=\"month\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"week\" i][readonly]::-webkit-clear-button,\ninput[type=\"week\" i][readonly]::-webkit-inner-spin-button,\ninput[readonly]::-webkit-calendar-picker-indicator {\n    visibility: hidden;\n}\nselect {\n    -webkit-appearance: menulist;\n    box-sizing: border-box;\n    align-items: center;\n    border: 1px solid;\n    white-space: pre;\n    -webkit-rtl-ordering: logical;\n    color: black;\n    background-color: white;\n    cursor: default;\n}\nselect:not(:-internal-list-box) {\n    overflow: visible !important;\n}\nselect:-internal-list-box {\n    -webkit-appearance: listbox;\n    align-items: flex-start;\n    border: 1px inset gray;\n    border-radius: initial;\n    overflow-x: hidden;\n    overflow-y: scroll;\n    vertical-align: text-bottom;\n    -webkit-user-select: none;\n    white-space: nowrap;\n}\noptgroup {\n    font-weight: bolder;\n    display: block;\n}\noption {\n    font-weight: normal;\n    display: block;\n    padding: 0 2px 1px 2px;\n    white-space: pre;\n    min-height: 1.2em;\n}\nselect:-internal-list-box option,\nselect:-internal-list-box optgroup {\n    line-height: initial !important;\n}\nselect:-internal-list-box:focus option:checked {\n    background-color: -internal-active-list-box-selection !important;\n    color: -internal-active-list-box-selection-text !important;\n}\nselect:-internal-list-box option:checked {\n    background-color: -internal-inactive-list-box-selection !important;\n    color: -internal-inactive-list-box-selection-text !important;\n}\nselect:-internal-list-box:disabled option:checked,\nselect:-internal-list-box option:checked:disabled {\n    color: gray !important;\n}\nselect:-internal-list-box hr {\n    border-style: none;\n}\noutput {\n    display: inline;\n}\nmeter {\n    -webkit-appearance: meter;\n    box-sizing: border-box;\n    display: inline-block;\n    height: 1em;\n    width: 5em;\n    vertical-align: -0.2em;\n}\nmeter::-webkit-meter-inner-element {\n    -webkit-appearance: inherit;\n    box-sizing: inherit;\n    -webkit-user-modify: read-only !important;\n    height: 100%;\n    width: 100%;\n}\nmeter::-webkit-meter-bar {\n    background: linear-gradient(to bottom, #ddd, #eee 20%, #ccc 45%, #ccc 55%, #ddd);\n    height: 100%;\n    width: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-optimum-value {\n    background: linear-gradient(to bottom, #ad7, #cea 20%, #7a3 45%, #7a3 55%, #ad7);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-suboptimum-value {\n    background: linear-gradient(to bottom, #fe7, #ffc 20%, #db3 45%, #db3 55%, #fe7);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-even-less-good-value {\n    background: linear-gradient(to bottom, #f77, #fcc 20%, #d44 45%, #d44 55%, #f77);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nprogress {\n    -webkit-appearance: progress-bar;\n    box-sizing: border-box;\n    display: inline-block;\n    height: 1em;\n    width: 10em;\n    vertical-align: -0.2em;\n}\nprogress::-webkit-progress-inner-element {\n    -webkit-appearance: inherit;\n    box-sizing: inherit;\n    -webkit-user-modify: read-only;\n    height: 100%;\n    width: 100%;\n}\nprogress::-webkit-progress-bar {\n    background-color: gray;\n    height: 100%;\n    width: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nprogress::-webkit-progress-value {\n    background-color: green;\n    height: 100%;\n    width: 50%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nu, ins {\n    text-decoration: underline\n}\nstrong, b {\n    font-weight: bold\n}\ni, cite, em, var, address, dfn {\n    font-style: italic\n}\ntt, code, kbd, samp {\n    font-family: monospace\n}\npre, xmp, plaintext, listing {\n    display: block;\n    font-family: monospace;\n    white-space: pre;\n    margin: 1__qem 0\n}\nmark {\n    background-color: yellow;\n    color: black\n}\nbig {\n    font-size: larger\n}\nsmall {\n    font-size: smaller\n}\ns, strike, del {\n    text-decoration: line-through\n}\nsub {\n    vertical-align: sub;\n    font-size: smaller\n}\nsup {\n    vertical-align: super;\n    font-size: smaller\n}\nnobr {\n    white-space: nowrap\n}\n:focus {\n    outline: auto 5px -webkit-focus-ring-color\n}\nhtml:focus, body:focus, input[readonly]:focus {\n    outline: none\n}\napplet:focus, embed:focus, iframe:focus, object:focus {\n    outline: none\n}\n\ninput:focus, textarea:focus, keygen:focus, select:focus {\n    outline-offset: -2px\n}\ninput[type=\"button\" i]:focus,\ninput[type=\"checkbox\" i]:focus,\ninput[type=\"file\" i]:focus,\ninput[type=\"hidden\" i]:focus,\ninput[type=\"image\" i]:focus,\ninput[type=\"radio\" i]:focus,\ninput[type=\"reset\" i]:focus,\ninput[type=\"search\" i]:focus,\ninput[type=\"submit\" i]:focus,\ninput[type=\"file\" i]:focus::-webkit-file-upload-button {\n    outline-offset: 0\n}\n    \na:-webkit-any-link {\n    color: -webkit-link;\n    text-decoration: underline;\n    cursor: auto;\n}\na:-webkit-any-link:active {\n    color: -webkit-activelink\n}\nruby, rt {\n    text-indent: 0;\n}\nrt {\n    line-height: normal;\n    -webkit-text-emphasis: none;\n}\nruby > rt {\n    display: block;\n    font-size: 50%;\n    text-align: start;\n}\nruby > rp {\n    display: none;\n}\nnoframes {\n    display: none\n}\nframeset, frame {\n    display: block\n}\nframeset {\n    border-color: inherit\n}\niframe {\n    border: 2px inset\n}\ndetails {\n    display: block\n}\nsummary {\n    display: block\n}\nsummary::-webkit-details-marker {\n    display: inline-block;\n    width: 0.66em;\n    height: 0.66em;\n    -webkit-margin-end: 0.4em;\n}\ntemplate {\n    display: none\n}\nbdi, output {\n    unicode-bidi: -webkit-isolate;\n}\nbdo {\n    unicode-bidi: bidi-override;\n}\ntextarea[dir=auto i] {\n    unicode-bidi: -webkit-plaintext;\n}\ndialog:not([open]) {\n    display: none\n}\ndialog {\n    position: absolute;\n    left: 0;\n    right: 0;\n    width: -webkit-fit-content;\n    height: -webkit-fit-content;\n    margin: auto;\n    border: solid;\n    padding: 1em;\n    background: white;\n    color: black\n}\ndialog::backdrop {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    background: rgba(0,0,0,0.1)\n}\n@page {\n    size: auto;\n    margin: auto;\n    padding: 0px;\n    border-width: 0px;\n}\n@media print {\n    * { -webkit-columns: auto !important; }\n}</style><style>\nv\\:* {behavior:url(#default#VML);}\no\\:* {behavior:url(#default#VML);}\nw\\:* {behavior:url(#default#VML);}\n.shape {behavior:url(#default#VML);}\n</style>\n<![endif]--><!--[if gte mso 9]><xml>\n <o:OfficeDocumentSettings>\n  <o:AllowPNG/>\n </o:OfficeDocumentSettings>\n</xml><![endif]-->\n<link rel=\"themeData\" href=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_themedata.xml\">\n<!--[if gte mso 9]><xml>\n <w:WordDocument>\n  <w:View>Normal</w:View>\n  <w:Zoom>0</w:Zoom>\n  <w:TrackMoves>false</w:TrackMoves>\n  <w:TrackFormatting/>\n  <w:PunctuationKerning/>\n  <w:ValidateAgainstSchemas/>\n  <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>\n  <w:IgnoreMixedContent>false</w:IgnoreMixedContent>\n  <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>\n  <w:DoNotPromoteQF/>\n  <w:LidThemeOther>EN-US</w:LidThemeOther>\n  <w:LidThemeAsian>JA</w:LidThemeAsian>\n  <w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript>\n  <w:Compatibility>\n   <w:BreakWrappedTables/>\n   <w:SnapToGridInCell/>\n   <w:WrapTextWithPunct/>\n   <w:UseAsianBreakRules/>\n   <w:DontGrowAutofit/>\n   <w:SplitPgBreakAndParaMark/>\n   <w:EnableOpenTypeKerning/>\n   <w:DontFlipMirrorIndents/>\n   <w:OverrideTableStyleHps/>\n   <w:UseFELayout/>\n  </w:Compatibility>\n  <m:mathPr>\n   <m:mathFont m:val=\"Cambria Math\"/>\n   <m:brkBin m:val=\"before\"/>\n   <m:brkBinSub m:val=\"&#45;-\"/>\n   <m:smallFrac m:val=\"off\"/>\n   <m:dispDef/>\n   <m:lMargin m:val=\"0\"/>\n   <m:rMargin m:val=\"0\"/>\n   <m:defJc m:val=\"centerGroup\"/>\n   <m:wrapIndent m:val=\"1440\"/>\n   <m:intLim m:val=\"subSup\"/>\n   <m:naryLim m:val=\"undOvr\"/>\n  </m:mathPr></w:WordDocument>\n</xml><![endif]--><!--[if gte mso 9]><xml>\n <w:LatentStyles DefLockedState=\"false\" DefUnhideWhenUsed=\"true\"\n  DefSemiHidden=\"true\" DefQFormat=\"false\" DefPriority=\"99\"\n  LatentStyleCount=\"276\">\n  <w:LsdException Locked=\"false\" Priority=\"0\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Normal\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"heading 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 7\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 8\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 9\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 7\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 8\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 9\"/>\n  <w:LsdException Locked=\"false\" Priority=\"35\" QFormat=\"true\" Name=\"caption\"/>\n  <w:LsdException Locked=\"false\" Priority=\"10\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Title\"/>\n  <w:LsdException Locked=\"false\" Priority=\"1\" Name=\"Default Paragraph Font\"/>\n  <w:LsdException Locked=\"false\" Priority=\"11\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Subtitle\"/>\n  <w:LsdException Locked=\"false\" Priority=\"22\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Strong\"/>\n  <w:LsdException Locked=\"false\" Priority=\"20\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Emphasis\"/>\n  <w:LsdException Locked=\"false\" Priority=\"59\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Table Grid\"/>\n  <w:LsdException Locked=\"false\" UnhideWhenUsed=\"false\" Name=\"Placeholder Text\"/>\n  <w:LsdException Locked=\"false\" Priority=\"1\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"No Spacing\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 1\"/>\n  <w:LsdException Locked=\"false\" UnhideWhenUsed=\"false\" Name=\"Revision\"/>\n  <w:LsdException Locked=\"false\" Priority=\"34\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"List Paragraph\"/>\n  <w:LsdException Locked=\"false\" Priority=\"29\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Quote\"/>\n  <w:LsdException Locked=\"false\" Priority=\"30\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Intense Quote\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"19\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Subtle Emphasis\"/>\n  <w:LsdException Locked=\"false\" Priority=\"21\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Intense Emphasis\"/>\n  <w:LsdException Locked=\"false\" Priority=\"31\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Subtle Reference\"/>\n  <w:LsdException Locked=\"false\" Priority=\"32\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Intense Reference\"/>\n  <w:LsdException Locked=\"false\" Priority=\"33\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Book Title\"/>\n  <w:LsdException Locked=\"false\" Priority=\"37\" Name=\"Bibliography\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" QFormat=\"true\" Name=\"TOC Heading\"/>\n </w:LatentStyles>\n</xml><![endif]-->\n\n<!--[if gte mso 10]>\n<style>\n /* Style Definitions */\r\ntable.MsoNormalTable\n\t{mso-style-name:\"Table Normal\";\n\tmso-tstyle-rowband-size:0;\n\tmso-tstyle-colband-size:0;\n\tmso-style-noshow:yes;\n\tmso-style-priority:99;\n\tmso-style-parent:\"\";\n\tmso-padding-alt:0in 5.4pt 0in 5.4pt;\n\tmso-para-margin:0in;\n\tmso-para-margin-bottom:.0001pt;\n\tmso-pagination:widow-orphan;\n\tfont-size:12.0pt;\n\tfont-family:Cambria;\n\tmso-ascii-font-family:Cambria;\n\tmso-ascii-theme-font:minor-latin;\n\tmso-hansi-font-family:Cambria;\n\tmso-hansi-theme-font:minor-latin;}\n</style>\n<![endif]-->\n</head>\n\n<body bgcolor=\"white\" lang=\"EN-US\" link=\"blue\" vlink=\"purple\" style=\"tab-interval:.5in\">\n<!--StartFragment-->\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><span style=\"mso-no-proof:yes\"><!--[if gte vml 1]><v:shapetype\n id=\"_x0000_t75\" coordsize=\"21600,21600\" o:spt=\"75\" o:preferrelative=\"t\"\n path=\"m@4@5l@4@11@9@11@9@5xe\" filled=\"f\" stroked=\"f\">\n <v:stroke joinstyle=\"miter\"/>\n <v:formulas>\n  <v:f eqn=\"if lineDrawn pixelLineWidth 0\"/>\n  <v:f eqn=\"sum @0 1 0\"/>\n  <v:f eqn=\"sum 0 0 @1\"/>\n  <v:f eqn=\"prod @2 1 2\"/>\n  <v:f eqn=\"prod @3 21600 pixelWidth\"/>\n  <v:f eqn=\"prod @3 21600 pixelHeight\"/>\n  <v:f eqn=\"sum @0 0 1\"/>\n  <v:f eqn=\"prod @6 1 2\"/>\n  <v:f eqn=\"prod @7 21600 pixelWidth\"/>\n  <v:f eqn=\"sum @8 21600 0\"/>\n  <v:f eqn=\"prod @7 21600 pixelHeight\"/>\n  <v:f eqn=\"sum @10 21600 0\"/>\n </v:formulas>\n <v:path o:extrusionok=\"f\" gradientshapeok=\"t\" o:connecttype=\"rect\"/>\n <o:lock v:ext=\"edit\" aspectratio=\"t\"/>\n</v:shapetype><v:shape id=\"Picture_x0020_2\" o:spid=\"_x0000_i1025\" type=\"#_x0000_t75\"\n style='width:207pt;height:126pt;visibility:visible;mso-wrap-style:square'>\n <v:imagedata src=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_image001.png\"\n  o:title=\"\"/>\n</v:shape><![endif]--><![if !vml]><img width=\"209\" height=\"128\" src=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_image002.png\" v:shapes=\"Picture_x0020_2\"><![endif]></span><o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">For immediate release<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">Media contact:<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">Christine Dunn<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">ArcPoint Strategic Communications<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">617.484.1660, x101<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><a href=\"mailto:cdunn@arcpointstrategy.com\">cdunn@arcpointstrategy.com</a><o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" align=\"center\" style=\"font-family: Cambria; mso-bidi-theme-font: minor-bidi; mso-style-qformat: yes; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; mso-style-unhide: no; mso-ascii-font-family: Cambria; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; text-align: center;\"><b style=\"mso-bidi-font-weight:\nnormal\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:Cambria\">Optio\nLabs Announces the Acquisition of Oculis Labs, and Names Oculis Founder, Dr.\nBill Anderson, as Chief Product Officer <o:p></o:p></span></b></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><b style=\"mso-bidi-font-weight:normal\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:Cambria\"><o:p>&nbsp;</o:p></span></b></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:\nCambria\"><o:p>&nbsp;</o:p></span></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:\nCambria\">Baltimore (April 8, 2015) – Optio Labs, </span>which creates\ntechnology products that make mobile devices more secure, <span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:Cambria\">announced\nthat it has purchased Maryland-based security company Oculis Labs, and its CEO,\nDr. Bill Anderson, will be joining the company as Chief Product Officer. Oculis\nis developer of the award-winning products PrivateEye and Chameleon. <o:p></o:p></span></p>\n\n<!--EndFragment-->\n</body>\n\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/paste/word-paste-out.html",
    "content": "<html xmlns:v=\"urn:schemas-microsoft-com:vml\" xmlns:o=\"urn:schemas-microsoft-com:office:office\" xmlns:w=\"urn:schemas-microsoft-com:office:word\" xmlns:m=\"http://schemas.microsoft.com/office/2004/12/omml\" xmlns:mv=\"http://macVmlSchemaUri\" xmlns=\"http://www.w3.org/TR/REC-html40\">\n\n<head>\n<meta name=\"Title\" content=\"\">\n<meta name=\"Keywords\" content=\"\">\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\n<meta name=\"ProgId\" content=\"Word.Document\">\n<meta name=\"Generator\" content=\"Microsoft Word 14\">\n<meta name=\"Originator\" content=\"Microsoft Word 14\">\n<link rel=\"File-List\" href=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_filelist.xml\">\n<link rel=\"Edit-Time-Data\" href=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_editdata.mso\">\n<!--[if !mso]>\n<style>@namespace \"http://www.w3.org/1999/xhtml\";\nhtml {\n    display: block\n}\nhead {\n    display: none\n}\nmeta {\n    display: none\n}\ntitle {\n    display: none\n}\nlink {\n    display: none\n}\nstyle {\n    display: none\n}\nscript {\n    display: none\n}\nbody {\n    display: block;\n    margin: 8px\n}\nbody:-webkit-full-page-media {\n    background-color: rgb(0, 0, 0)\n}\np {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1__qem;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n}\ndiv {\n    display: block\n}\nlayer {\n    display: block\n}\narticle, aside, footer, header, hgroup, main, nav, section {\n    display: block\n}\nmarquee {\n    display: inline-block;\n}\naddress {\n    display: block\n}\nblockquote {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 40px;\n    -webkit-margin-end: 40px;\n}\nfigcaption {\n    display: block\n}\nfigure {\n    display: block;\n    -webkit-margin-before: 1em;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 40px;\n    -webkit-margin-end: 40px;\n}\nq {\n    display: inline\n}\nq:before {\n    content: open-quote;\n}\nq:after {\n    content: close-quote;\n}\ncenter {\n    display: block;\n    text-align: -webkit-center\n}\nhr {\n    display: block;\n    -webkit-margin-before: 0.5em;\n    -webkit-margin-after: 0.5em;\n    -webkit-margin-start: auto;\n    -webkit-margin-end: auto;\n    border-style: inset;\n    border-width: 1px\n}\nmap {\n    display: inline\n}\nvideo {\n    object-fit: contain;\n}\nh1 {\n    display: block;\n    font-size: 2em;\n    -webkit-margin-before: 0.67__qem;\n    -webkit-margin-after: 0.67em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\n:-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.5em;\n    -webkit-margin-before: 0.83__qem;\n    -webkit-margin-after: 0.83em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.17em;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.00em;\n    -webkit-margin-before: 1.33__qem;\n    -webkit-margin-after: 1.33em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: .83em;\n    -webkit-margin-before: 1.67__qem;\n    -webkit-margin-after: 1.67em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: .67em;\n    -webkit-margin-before: 2.33__qem;\n    -webkit-margin-after: 2.33em;\n}\nh2 {\n    display: block;\n    font-size: 1.5em;\n    -webkit-margin-before: 0.83__qem;\n    -webkit-margin-after: 0.83em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh3 {\n    display: block;\n    font-size: 1.17em;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh4 {\n    display: block;\n    -webkit-margin-before: 1.33__qem;\n    -webkit-margin-after: 1.33em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh5 {\n    display: block;\n    font-size: .83em;\n    -webkit-margin-before: 1.67__qem;\n    -webkit-margin-after: 1.67em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh6 {\n    display: block;\n    font-size: .67em;\n    -webkit-margin-before: 2.33__qem;\n    -webkit-margin-after: 2.33em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\ntable {\n    display: table;\n    border-collapse: separate;\n    border-spacing: 2px;\n    border-color: gray\n}\nthead {\n    display: table-header-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntbody {\n    display: table-row-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntfoot {\n    display: table-footer-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntable > tr {\n    vertical-align: middle;\n}\ncol {\n    display: table-column\n}\ncolgroup {\n    display: table-column-group\n}\ntr {\n    display: table-row;\n    vertical-align: inherit;\n    border-color: inherit\n}\ntd, th {\n    display: table-cell;\n    vertical-align: inherit\n}\nth {\n    font-weight: bold\n}\ncaption {\n    display: table-caption;\n    text-align: -webkit-center\n}\nul, menu, dir {\n    display: block;\n    list-style-type: disc;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    -webkit-padding-start: 40px\n}\nol {\n    display: block;\n    list-style-type: decimal;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    -webkit-padding-start: 40px\n}\nli {\n    display: list-item;\n    text-align: -webkit-match-parent;\n}\nul ul, ol ul {\n    list-style-type: circle\n}\nol ol ul, ol ul ul, ul ol ul, ul ul ul {\n    list-style-type: square\n}\ndd {\n    display: block;\n    -webkit-margin-start: 40px\n}\ndl {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n}\ndt {\n    display: block\n}\nol ul, ul ol, ul ul, ol ol {\n    -webkit-margin-before: 0;\n    -webkit-margin-after: 0\n}\nform {\n    display: block;\n    margin-top: 0__qem;\n}\nlabel {\n    cursor: default;\n}\nlegend {\n    display: block;\n    -webkit-padding-start: 2px;\n    -webkit-padding-end: 2px;\n    border: none\n}\nfieldset {\n    display: block;\n    -webkit-margin-start: 2px;\n    -webkit-margin-end: 2px;\n    -webkit-padding-before: 0.35em;\n    -webkit-padding-start: 0.75em;\n    -webkit-padding-end: 0.75em;\n    -webkit-padding-after: 0.625em;\n    border: 2px groove ThreeDFace;\n    min-width: -webkit-min-content;\n}\nbutton {\n    -webkit-appearance: button;\n}\ninput, textarea, keygen, select, button, meter, progress {\n    -webkit-writing-mode: horizontal-tb !important;\n}\ninput, textarea, keygen, select, button {\n    margin: 0__qem;\n    font: -webkit-small-control;\n    text-rendering: auto;\n    color: initial;\n    letter-spacing: normal;\n    word-spacing: normal;\n    line-height: normal;\n    text-transform: none;\n    text-indent: 0;\n    text-shadow: none;\n    display: inline-block;\n    text-align: start;\n}\ninput[type=\"hidden\" i] {\n    display: none\n}\ninput {\n    -webkit-appearance: textfield;\n    padding: 1px;\n    background-color: white;\n    border: 2px inset;\n    -webkit-rtl-ordering: logical;\n    -webkit-user-select: text;\n    cursor: auto;\n}\ninput[type=\"search\" i] {\n    -webkit-appearance: searchfield;\n    box-sizing: border-box;\n}\ninput::-webkit-textfield-decoration-container {\n    display: flex;\n    align-items: center;\n    -webkit-user-modify: read-only !important;\n    content: none !important;\n}\ninput[type=\"search\" i]::-webkit-textfield-decoration-container {\n    direction: ltr;\n}\ninput::-webkit-clear-button {\n    -webkit-appearance: searchfield-cancel-button;\n    display: inline-block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-margin-start: 2px;\n    opacity: 0;\n    pointer-events: none;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-clear-button {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"search\" i]::-webkit-search-cancel-button {\n    -webkit-appearance: searchfield-cancel-button;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-margin-start: 1px;\n    opacity: 0;\n    pointer-events: none;\n}\ninput[type=\"search\" i]:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-search-cancel-button {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"search\" i]::-webkit-search-decoration {\n    -webkit-appearance: searchfield-decoration;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-align-self: flex-start;\n    margin: auto 0;\n}\ninput[type=\"search\" i]::-webkit-search-results-decoration {\n    -webkit-appearance: searchfield-results-decoration;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-align-self: flex-start;\n    margin: auto 0;\n}\ninput::-webkit-inner-spin-button {\n    -webkit-appearance: inner-spin-button;\n    display: inline-block;\n    cursor: default;\n    flex: none;\n    align-self: stretch;\n    -webkit-user-select: none;\n    -webkit-user-modify: read-only !important;\n    opacity: 0;\n    pointer-events: none;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-inner-spin-button {\n    opacity: 1;\n    pointer-events: auto;\n}\nkeygen, select {\n    border-radius: 5px;\n}\nkeygen::-webkit-keygen-select {\n    margin: 0px;\n}\ntextarea {\n    -webkit-appearance: textarea;\n    background-color: white;\n    border: 1px solid;\n    -webkit-rtl-ordering: logical;\n    -webkit-user-select: text;\n    flex-direction: column;\n    resize: auto;\n    cursor: auto;\n    padding: 2px;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n::-webkit-input-placeholder {\n    -webkit-text-security: none;\n    color: darkGray;\n    pointer-events: none !important;\n}\ninput::-webkit-input-placeholder {\n    white-space: pre;\n    word-wrap: normal;\n    overflow: hidden;\n    -webkit-user-modify: read-only !important;\n}\ninput[type=\"password\" i] {\n    -webkit-text-security: disc !important;\n}\ninput[type=\"hidden\" i], input[type=\"image\" i], input[type=\"file\" i] {\n    -webkit-appearance: initial;\n    padding: initial;\n    background-color: initial;\n    border: initial;\n}\ninput[type=\"file\" i] {\n    align-items: baseline;\n    color: inherit;\n    text-align: start !important;\n}\ninput:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {\n    background-color: #FAFFBD !important;\n    background-image:none !important;\n    color: #000000 !important;\n}\ninput[type=\"radio\" i], input[type=\"checkbox\" i] {\n    margin: 3px 0.5ex;\n    padding: initial;\n    background-color: initial;\n    border: initial;\n}\ninput[type=\"button\" i], input[type=\"submit\" i], input[type=\"reset\" i] {\n    -webkit-appearance: push-button;\n    -webkit-user-select: none;\n    white-space: pre\n}\ninput[type=\"file\" i]::-webkit-file-upload-button {\n    -webkit-appearance: push-button;\n    -webkit-user-modify: read-only !important;\n    white-space: nowrap;\n    margin: 0;\n    font-size: inherit;\n}\ninput[type=\"button\" i], input[type=\"submit\" i], input[type=\"reset\" i], input[type=\"file\" i]::-webkit-file-upload-button, button {\n    align-items: flex-start;\n    text-align: center;\n    cursor: default;\n    color: ButtonText;\n    padding: 2px 6px 3px 6px;\n    border: 2px outset ButtonFace;\n    background-color: ButtonFace;\n    box-sizing: border-box\n}\ninput[type=\"range\" i] {\n    -webkit-appearance: slider-horizontal;\n    padding: initial;\n    border: initial;\n    margin: 2px;\n    color: #909090;\n}\ninput[type=\"range\" i]::-webkit-slider-container, input[type=\"range\" i]::-webkit-media-slider-container {\n    flex: 1;\n    min-width: 0;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: flex;\n}\ninput[type=\"range\" i]::-webkit-slider-runnable-track {\n    flex: 1;\n    min-width: 0;\n    -webkit-align-self: center;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: block;\n}\ninput[type=\"range\" i]::-webkit-slider-thumb, input[type=\"range\" i]::-webkit-media-slider-thumb {\n    -webkit-appearance: sliderthumb-horizontal;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: block;\n}\ninput[type=\"button\" i]:disabled, input[type=\"submit\" i]:disabled, input[type=\"reset\" i]:disabled,\ninput[type=\"file\" i]:disabled::-webkit-file-upload-button, button:disabled,\nselect:disabled, keygen:disabled, optgroup:disabled, option:disabled,\nselect[disabled]>option {\n    color: GrayText\n}\ninput[type=\"button\" i]:active, input[type=\"submit\" i]:active, input[type=\"reset\" i]:active, input[type=\"file\" i]:active::-webkit-file-upload-button, button:active {\n    border-style: inset\n}\ninput[type=\"button\" i]:active:disabled, input[type=\"submit\" i]:active:disabled, input[type=\"reset\" i]:active:disabled, input[type=\"file\" i]:active:disabled::-webkit-file-upload-button, button:active:disabled {\n    border-style: outset\n}\noption:-internal-spatial-navigation-focus {\n    outline: black dashed 1px;\n    outline-offset: -1px;\n}\ndatalist {\n    display: none\n}\narea {\n    display: inline;\n    cursor: pointer;\n}\nparam {\n    display: none\n}\ninput[type=\"checkbox\" i] {\n    -webkit-appearance: checkbox;\n    box-sizing: border-box;\n}\ninput[type=\"radio\" i] {\n    -webkit-appearance: radio;\n    box-sizing: border-box;\n}\ninput[type=\"color\" i] {\n    -webkit-appearance: square-button;\n    width: 44px;\n    height: 23px;\n    background-color: ButtonFace;\n    border: 1px #a9a9a9 solid;\n    padding: 1px 2px;\n}\ninput[type=\"color\" i]::-webkit-color-swatch-wrapper {\n    display:flex;\n    padding: 4px 2px;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    width: 100%;\n    height: 100%\n}\ninput[type=\"color\" i]::-webkit-color-swatch {\n    background-color: #000000;\n    border: 1px solid #777777;\n    flex: 1;\n    min-width: 0;\n    -webkit-user-modify: read-only !important;\n}\ninput[type=\"color\" i][list] {\n    -webkit-appearance: menulist;\n    width: 88px;\n    height: 23px\n}\ninput[type=\"color\" i][list]::-webkit-color-swatch-wrapper {\n    padding-left: 8px;\n    padding-right: 24px;\n}\ninput[type=\"color\" i][list]::-webkit-color-swatch {\n    border-color: #000000;\n}\ninput::-webkit-calendar-picker-indicator {\n    display: inline-block;\n    width: 0.66em;\n    height: 0.66em;\n    padding: 0.17em 0.34em;\n    -webkit-user-modify: read-only !important;\n    opacity: 0;\n    pointer-events: none;\n}\ninput::-webkit-calendar-picker-indicator:hover {\n    background-color: #eee;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-calendar-picker-indicator,\ninput::-webkit-calendar-picker-indicator:focus {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"date\" i]:disabled::-webkit-clear-button,\ninput[type=\"date\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"datetime-local\" i]:disabled::-webkit-clear-button,\ninput[type=\"datetime-local\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"month\" i]:disabled::-webkit-clear-button,\ninput[type=\"month\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"week\" i]:disabled::-webkit-clear-button,\ninput[type=\"week\" i]:disabled::-webkit-inner-spin-button,\ninput:disabled::-webkit-calendar-picker-indicator,\ninput[type=\"date\" i][readonly]::-webkit-clear-button,\ninput[type=\"date\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"datetime-local\" i][readonly]::-webkit-clear-button,\ninput[type=\"datetime-local\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"month\" i][readonly]::-webkit-clear-button,\ninput[type=\"month\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"week\" i][readonly]::-webkit-clear-button,\ninput[type=\"week\" i][readonly]::-webkit-inner-spin-button,\ninput[readonly]::-webkit-calendar-picker-indicator {\n    visibility: hidden;\n}\nselect {\n    -webkit-appearance: menulist;\n    box-sizing: border-box;\n    align-items: center;\n    border: 1px solid;\n    white-space: pre;\n    -webkit-rtl-ordering: logical;\n    color: black;\n    background-color: white;\n    cursor: default;\n}\nselect:not(:-internal-list-box) {\n    overflow: visible !important;\n}\nselect:-internal-list-box {\n    -webkit-appearance: listbox;\n    align-items: flex-start;\n    border: 1px inset gray;\n    border-radius: initial;\n    overflow-x: hidden;\n    overflow-y: scroll;\n    vertical-align: text-bottom;\n    -webkit-user-select: none;\n    white-space: nowrap;\n}\noptgroup {\n    font-weight: bolder;\n    display: block;\n}\noption {\n    font-weight: normal;\n    display: block;\n    padding: 0 2px 1px 2px;\n    white-space: pre;\n    min-height: 1.2em;\n}\nselect:-internal-list-box option,\nselect:-internal-list-box optgroup {\n    line-height: initial !important;\n}\nselect:-internal-list-box:focus option:checked {\n    background-color: -internal-active-list-box-selection !important;\n    color: -internal-active-list-box-selection-text !important;\n}\nselect:-internal-list-box option:checked {\n    background-color: -internal-inactive-list-box-selection !important;\n    color: -internal-inactive-list-box-selection-text !important;\n}\nselect:-internal-list-box:disabled option:checked,\nselect:-internal-list-box option:checked:disabled {\n    color: gray !important;\n}\nselect:-internal-list-box hr {\n    border-style: none;\n}\noutput {\n    display: inline;\n}\nmeter {\n    -webkit-appearance: meter;\n    box-sizing: border-box;\n    display: inline-block;\n    height: 1em;\n    width: 5em;\n    vertical-align: -0.2em;\n}\nmeter::-webkit-meter-inner-element {\n    -webkit-appearance: inherit;\n    box-sizing: inherit;\n    -webkit-user-modify: read-only !important;\n    height: 100%;\n    width: 100%;\n}\nmeter::-webkit-meter-bar {\n    background: linear-gradient(to bottom, #ddd, #eee 20%, #ccc 45%, #ccc 55%, #ddd);\n    height: 100%;\n    width: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-optimum-value {\n    background: linear-gradient(to bottom, #ad7, #cea 20%, #7a3 45%, #7a3 55%, #ad7);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-suboptimum-value {\n    background: linear-gradient(to bottom, #fe7, #ffc 20%, #db3 45%, #db3 55%, #fe7);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-even-less-good-value {\n    background: linear-gradient(to bottom, #f77, #fcc 20%, #d44 45%, #d44 55%, #f77);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nprogress {\n    -webkit-appearance: progress-bar;\n    box-sizing: border-box;\n    display: inline-block;\n    height: 1em;\n    width: 10em;\n    vertical-align: -0.2em;\n}\nprogress::-webkit-progress-inner-element {\n    -webkit-appearance: inherit;\n    box-sizing: inherit;\n    -webkit-user-modify: read-only;\n    height: 100%;\n    width: 100%;\n}\nprogress::-webkit-progress-bar {\n    background-color: gray;\n    height: 100%;\n    width: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nprogress::-webkit-progress-value {\n    background-color: green;\n    height: 100%;\n    width: 50%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nu, ins {\n    text-decoration: underline\n}\nstrong, b {\n    font-weight: bold\n}\ni, cite, em, var, address, dfn {\n    font-style: italic\n}\ntt, code, kbd, samp {\n    font-family: monospace\n}\npre, xmp, plaintext, listing {\n    display: block;\n    font-family: monospace;\n    white-space: pre;\n    margin: 1__qem 0\n}\nmark {\n    background-color: yellow;\n    color: black\n}\nbig {\n    font-size: larger\n}\nsmall {\n    font-size: smaller\n}\ns, strike, del {\n    text-decoration: line-through\n}\nsub {\n    vertical-align: sub;\n    font-size: smaller\n}\nsup {\n    vertical-align: super;\n    font-size: smaller\n}\nnobr {\n    white-space: nowrap\n}\n:focus {\n    outline: auto 5px -webkit-focus-ring-color\n}\nhtml:focus, body:focus, input[readonly]:focus {\n    outline: none\n}\napplet:focus, embed:focus, iframe:focus, object:focus {\n    outline: none\n}\n\ninput:focus, textarea:focus, keygen:focus, select:focus {\n    outline-offset: -2px\n}\ninput[type=\"button\" i]:focus,\ninput[type=\"checkbox\" i]:focus,\ninput[type=\"file\" i]:focus,\ninput[type=\"hidden\" i]:focus,\ninput[type=\"image\" i]:focus,\ninput[type=\"radio\" i]:focus,\ninput[type=\"reset\" i]:focus,\ninput[type=\"search\" i]:focus,\ninput[type=\"submit\" i]:focus,\ninput[type=\"file\" i]:focus::-webkit-file-upload-button {\n    outline-offset: 0\n}\n    \na:-webkit-any-link {\n    color: -webkit-link;\n    text-decoration: underline;\n    cursor: auto;\n}\na:-webkit-any-link:active {\n    color: -webkit-activelink\n}\nruby, rt {\n    text-indent: 0;\n}\nrt {\n    line-height: normal;\n    -webkit-text-emphasis: none;\n}\nruby > rt {\n    display: block;\n    font-size: 50%;\n    text-align: start;\n}\nruby > rp {\n    display: none;\n}\nnoframes {\n    display: none\n}\nframeset, frame {\n    display: block\n}\nframeset {\n    border-color: inherit\n}\niframe {\n    border: 2px inset\n}\ndetails {\n    display: block\n}\nsummary {\n    display: block\n}\nsummary::-webkit-details-marker {\n    display: inline-block;\n    width: 0.66em;\n    height: 0.66em;\n    -webkit-margin-end: 0.4em;\n}\ntemplate {\n    display: none\n}\nbdi, output {\n    unicode-bidi: -webkit-isolate;\n}\nbdo {\n    unicode-bidi: bidi-override;\n}\ntextarea[dir=auto i] {\n    unicode-bidi: -webkit-plaintext;\n}\ndialog:not([open]) {\n    display: none\n}\ndialog {\n    position: absolute;\n    left: 0;\n    right: 0;\n    width: -webkit-fit-content;\n    height: -webkit-fit-content;\n    margin: auto;\n    border: solid;\n    padding: 1em;\n    background: white;\n    color: black\n}\ndialog::backdrop {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    background: rgba(0,0,0,0.1)\n}\n@page {\n    size: auto;\n    margin: auto;\n    padding: 0px;\n    border-width: 0px;\n}\n@media print {\n    * { -webkit-columns: auto !important; }\n}</style><style>\nv\\:* {behavior:url(#default#VML);}\no\\:* {behavior:url(#default#VML);}\nw\\:* {behavior:url(#default#VML);}\n.shape {behavior:url(#default#VML);}\n</style>\n<![endif]--><!--[if gte mso 9]><xml>\n <o:OfficeDocumentSettings>\n  <o:AllowPNG/>\n </o:OfficeDocumentSettings>\n</xml><![endif]-->\n<link rel=\"themeData\" href=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_themedata.xml\">\n<!--[if gte mso 9]><xml>\n <w:WordDocument>\n  <w:View>Normal</w:View>\n  <w:Zoom>0</w:Zoom>\n  <w:TrackMoves>false</w:TrackMoves>\n  <w:TrackFormatting/>\n  <w:PunctuationKerning/>\n  <w:ValidateAgainstSchemas/>\n  <w:SaveIfXMLInvalid>false</w:SaveIfXMLInvalid>\n  <w:IgnoreMixedContent>false</w:IgnoreMixedContent>\n  <w:AlwaysShowPlaceholderText>false</w:AlwaysShowPlaceholderText>\n  <w:DoNotPromoteQF/>\n  <w:LidThemeOther>EN-US</w:LidThemeOther>\n  <w:LidThemeAsian>JA</w:LidThemeAsian>\n  <w:LidThemeComplexScript>X-NONE</w:LidThemeComplexScript>\n  <w:Compatibility>\n   <w:BreakWrappedTables/>\n   <w:SnapToGridInCell/>\n   <w:WrapTextWithPunct/>\n   <w:UseAsianBreakRules/>\n   <w:DontGrowAutofit/>\n   <w:SplitPgBreakAndParaMark/>\n   <w:EnableOpenTypeKerning/>\n   <w:DontFlipMirrorIndents/>\n   <w:OverrideTableStyleHps/>\n   <w:UseFELayout/>\n  </w:Compatibility>\n  <m:mathPr>\n   <m:mathFont m:val=\"Cambria Math\"/>\n   <m:brkBin m:val=\"before\"/>\n   <m:brkBinSub m:val=\"&#45;-\"/>\n   <m:smallFrac m:val=\"off\"/>\n   <m:dispDef/>\n   <m:lMargin m:val=\"0\"/>\n   <m:rMargin m:val=\"0\"/>\n   <m:defJc m:val=\"centerGroup\"/>\n   <m:wrapIndent m:val=\"1440\"/>\n   <m:intLim m:val=\"subSup\"/>\n   <m:naryLim m:val=\"undOvr\"/>\n  </m:mathPr></w:WordDocument>\n</xml><![endif]--><!--[if gte mso 9]><xml>\n <w:LatentStyles DefLockedState=\"false\" DefUnhideWhenUsed=\"true\"\n  DefSemiHidden=\"true\" DefQFormat=\"false\" DefPriority=\"99\"\n  LatentStyleCount=\"276\">\n  <w:LsdException Locked=\"false\" Priority=\"0\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Normal\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"heading 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 7\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 8\"/>\n  <w:LsdException Locked=\"false\" Priority=\"9\" QFormat=\"true\" Name=\"heading 9\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 7\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 8\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" Name=\"toc 9\"/>\n  <w:LsdException Locked=\"false\" Priority=\"35\" QFormat=\"true\" Name=\"caption\"/>\n  <w:LsdException Locked=\"false\" Priority=\"10\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Title\"/>\n  <w:LsdException Locked=\"false\" Priority=\"1\" Name=\"Default Paragraph Font\"/>\n  <w:LsdException Locked=\"false\" Priority=\"11\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Subtitle\"/>\n  <w:LsdException Locked=\"false\" Priority=\"22\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Strong\"/>\n  <w:LsdException Locked=\"false\" Priority=\"20\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Emphasis\"/>\n  <w:LsdException Locked=\"false\" Priority=\"59\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Table Grid\"/>\n  <w:LsdException Locked=\"false\" UnhideWhenUsed=\"false\" Name=\"Placeholder Text\"/>\n  <w:LsdException Locked=\"false\" Priority=\"1\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"No Spacing\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 1\"/>\n  <w:LsdException Locked=\"false\" UnhideWhenUsed=\"false\" Name=\"Revision\"/>\n  <w:LsdException Locked=\"false\" Priority=\"34\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"List Paragraph\"/>\n  <w:LsdException Locked=\"false\" Priority=\"29\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Quote\"/>\n  <w:LsdException Locked=\"false\" Priority=\"30\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Intense Quote\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 1\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 2\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 3\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 4\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 5\"/>\n  <w:LsdException Locked=\"false\" Priority=\"60\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Shading Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"61\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light List Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"62\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Light Grid Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"63\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 1 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"64\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Shading 2 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"65\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 1 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"66\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium List 2 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"67\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 1 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"68\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 2 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"69\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Medium Grid 3 Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"70\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Dark List Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"71\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Shading Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"72\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful List Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"73\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" Name=\"Colorful Grid Accent 6\"/>\n  <w:LsdException Locked=\"false\" Priority=\"19\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Subtle Emphasis\"/>\n  <w:LsdException Locked=\"false\" Priority=\"21\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Intense Emphasis\"/>\n  <w:LsdException Locked=\"false\" Priority=\"31\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Subtle Reference\"/>\n  <w:LsdException Locked=\"false\" Priority=\"32\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Intense Reference\"/>\n  <w:LsdException Locked=\"false\" Priority=\"33\" SemiHidden=\"false\"\n   UnhideWhenUsed=\"false\" QFormat=\"true\" Name=\"Book Title\"/>\n  <w:LsdException Locked=\"false\" Priority=\"37\" Name=\"Bibliography\"/>\n  <w:LsdException Locked=\"false\" Priority=\"39\" QFormat=\"true\" Name=\"TOC Heading\"/>\n </w:LatentStyles>\n</xml><![endif]-->\n\n<!--[if gte mso 10]>\n<style>\n /* Style Definitions */\r\ntable.MsoNormalTable\n\t{mso-style-name:\"Table Normal\";\n\tmso-tstyle-rowband-size:0;\n\tmso-tstyle-colband-size:0;\n\tmso-style-noshow:yes;\n\tmso-style-priority:99;\n\tmso-style-parent:\"\";\n\tmso-padding-alt:0in 5.4pt 0in 5.4pt;\n\tmso-para-margin:0in;\n\tmso-para-margin-bottom:.0001pt;\n\tmso-pagination:widow-orphan;\n\tfont-size:12.0pt;\n\tfont-family:Cambria;\n\tmso-ascii-font-family:Cambria;\n\tmso-ascii-theme-font:minor-latin;\n\tmso-hansi-font-family:Cambria;\n\tmso-hansi-theme-font:minor-latin;}\n</style>\n<![endif]-->\n</head>\n\n<body bgcolor=\"white\" lang=\"EN-US\" link=\"blue\" vlink=\"purple\" style=\"tab-interval:.5in\">\n<!--StartFragment-->\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><span style=\"mso-no-proof:yes\"><!--[if gte vml 1]><v:shapetype\n id=\"_x0000_t75\" coordsize=\"21600,21600\" o:spt=\"75\" o:preferrelative=\"t\"\n path=\"m@4@5l@4@11@9@11@9@5xe\" filled=\"f\" stroked=\"f\">\n <v:stroke joinstyle=\"miter\"/>\n <v:formulas>\n  <v:f eqn=\"if lineDrawn pixelLineWidth 0\"/>\n  <v:f eqn=\"sum @0 1 0\"/>\n  <v:f eqn=\"sum 0 0 @1\"/>\n  <v:f eqn=\"prod @2 1 2\"/>\n  <v:f eqn=\"prod @3 21600 pixelWidth\"/>\n  <v:f eqn=\"prod @3 21600 pixelHeight\"/>\n  <v:f eqn=\"sum @0 0 1\"/>\n  <v:f eqn=\"prod @6 1 2\"/>\n  <v:f eqn=\"prod @7 21600 pixelWidth\"/>\n  <v:f eqn=\"sum @8 21600 0\"/>\n  <v:f eqn=\"prod @7 21600 pixelHeight\"/>\n  <v:f eqn=\"sum @10 21600 0\"/>\n </v:formulas>\n <v:path o:extrusionok=\"f\" gradientshapeok=\"t\" o:connecttype=\"rect\"/>\n <o:lock v:ext=\"edit\" aspectratio=\"t\"/>\n</v:shapetype><v:shape id=\"Picture_x0020_2\" o:spid=\"_x0000_i1025\" type=\"#_x0000_t75\"\n style='width:207pt;height:126pt;visibility:visible;mso-wrap-style:square'>\n <v:imagedata src=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_image001.png\"\n  o:title=\"\"/>\n</v:shape><![endif]--><![if !vml]><img width=\"209\" height=\"128\" src=\"file://localhost/Users/bengotow/Library/Caches/TemporaryItems/msoclip/0/clip_image002.png\" v:shapes=\"Picture_x0020_2\"><![endif]></span><o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">For immediate release<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">Media contact:<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">Christine Dunn<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">ArcPoint Strategic Communications<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\">617.484.1660, x101<o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><a href=\"mailto:cdunn@arcpointstrategy.com\">cdunn@arcpointstrategy.com</a><o:p></o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><o:p>&nbsp;</o:p></p>\n\n<p class=\"MsoNormal\" align=\"center\" style=\"font-family: Cambria; mso-bidi-theme-font: minor-bidi; mso-style-qformat: yes; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; mso-style-unhide: no; mso-ascii-font-family: Cambria; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; text-align: center;\"><b style=\"mso-bidi-font-weight:\nnormal\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:Cambria\">Optio\nLabs Announces the Acquisition of Oculis Labs, and Names Oculis Founder, Dr.\nBill Anderson, as Chief Product Officer <o:p></o:p></span></b></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><b style=\"mso-bidi-font-weight:normal\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:Cambria\"><o:p>&nbsp;</o:p></span></b></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:\nCambria\"><o:p>&nbsp;</o:p></span></p>\n\n<p class=\"MsoNormal\" style=\"mso-ascii-font-family: Cambria; mso-style-unhide: no; mso-style-parent: ''; margin: 0in; margin-bottom: .0001pt; mso-pagination: widow-orphan; font-size: 12.0pt; font-family: Cambria; mso-style-qformat: yes; mso-ascii-theme-font: minor-latin; mso-fareast-font-family: 'ＭＳ 明朝'; mso-fareast-theme-font: minor-fareast; mso-hansi-font-family: Cambria; mso-hansi-theme-font: minor-latin; mso-bidi-font-family: 'Times New Roman'; mso-bidi-theme-font: minor-bidi;\"><span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:\nCambria\">Baltimore (April 8, 2015) – Optio Labs, </span>which creates\ntechnology products that make mobile devices more secure, <span style=\"mso-ascii-font-family:Cambria;mso-hansi-font-family:Cambria\">announced\nthat it has purchased Maryland-based security company Oculis Labs, and its CEO,\nDr. Bill Anderson, will be joining the company as Chief Product Officer. Oculis\nis developer of the award-winning products PrivateEye and Chameleon. <o:p></o:p></span></p>\n\n<!--EndFragment-->\n</body>\n\n</html>"
  },
  {
    "path": "packages/client-app/spec/fixtures/sample-deltas-clustered.json",
    "content": "{\n    \"create\": {\n        \"message\": {\n            \"9p571g0ie63rg0ekec699ol5e\": {\n                \"body\": \"<<BODY>>\",\n                \"files\": [],\n                \"from\": [\n                    {\n                        \"name\": \"Karen Rustad Tölva\",\n                        \"email\": \"karen.rustad@gmail.com\"\n                    }\n                ],\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"thread_id\": \"62ehtebypazlgokcjvo8o9yak\",\n                \"cc\": [],\n                \"object\": \"message\",\n                \"bcc\": [],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"to\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"ben@nylas.com\"\n                    }\n                ],\n                \"folder\": {\n                    \"display_name\": \"Inbox\",\n                    \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                    \"name\": \"inbox\"\n                },\n                \"date\": 1440610192,\n                \"reply_to\": [],\n                \"events\": [],\n                \"starred\": false,\n                \"unread\": true,\n                \"id\": \"9p571g0ie63rg0ekec699ol5e\",\n                \"subject\": \"This is an email following up on the Electron meetup.\"\n            },\n            \"4rv7upa3gzuf1hal48b15lxrx\": {\n                \"body\": \"<<BODY>>\",\n                \"files\": [],\n                \"from\": [\n                    {\n                        \"name\": \"evan (Evan Morikawa)\",\n                        \"email\": \"noreply+phabricator@nilas.com\"\n                    }\n                ],\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"thread_id\": \"cungn0trv89l19mf08a2blyuh\",\n                \"cc\": [],\n                \"object\": \"message\",\n                \"bcc\": [],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"to\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"ben@inboxapp.com\"\n                    }\n                ],\n                \"folder\": {\n                    \"display_name\": \"Inbox\",\n                    \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                    \"name\": \"inbox\"\n                },\n                \"date\": 1440609916,\n                \"reply_to\": [],\n                \"events\": [],\n                \"starred\": false,\n                \"unread\": true,\n                \"id\": \"4rv7upa3gzuf1hal48b15lxrx\",\n                \"subject\": \"[Differential] [Accepted] D1936: feat(work): Create the \\\"Work\\\" window, move TaskQueue, Nylas sync workers\"\n            },\n            \"tt4i92q8rtpgrbxaq6viqlf4\": {\n                \"body\": \"<<BODY>>\",\n                \"files\": [],\n                \"from\": [\n                    {\n                        \"name\": \"Ismail Pelaseyed\",\n                        \"email\": \"ismail@sendcase.com\"\n                    }\n                ],\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"thread_id\": \"8qesqxoftrd3nqiig3cz6gu49\",\n                \"cc\": [],\n                \"object\": \"message\",\n                \"bcc\": [],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"to\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"support@nylas.com\"\n                    }\n                ],\n                \"folder\": {\n                    \"display_name\": \"Inbox\",\n                    \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                    \"name\": \"inbox\"\n                },\n                \"date\": 1440609839,\n                \"reply_to\": [],\n                \"events\": [],\n                \"starred\": false,\n                \"unread\": true,\n                \"id\": \"tt4i92q8rtpgrbxaq6viqlf4\",\n                \"subject\": \"Re: Incoming webhook POST body empty\"\n            },\n            \"7q4wtqzj3nxb42y6ysbl5l73t\": {\n                \"body\": \"<<BODY>>\",\n                \"files\": [],\n                \"from\": [\n                    {\n                        \"name\": \"Ismail Pelaseyed\",\n                        \"email\": \"ismail@sendcase.com\"\n                    }\n                ],\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"thread_id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n                \"cc\": [],\n                \"object\": \"message\",\n                \"bcc\": [],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"to\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"support@nylas.com\"\n                    }\n                ],\n                \"folder\": {\n                    \"display_name\": \"Deleted Items\",\n                    \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                    \"name\": \"trash\"\n                },\n                \"date\": 1440609839,\n                \"reply_to\": [],\n                \"events\": [],\n                \"starred\": false,\n                \"unread\": true,\n                \"id\": \"7q4wtqzj3nxb42y6ysbl5l73t\",\n                \"subject\": \"Re: Incoming webhook POST body empty\"\n            },\n            \"4oesh7fhsbd45t7dx30p03vz1\": {\n                \"body\": \"<<BODY>>\",\n                \"files\": [],\n                \"from\": [\n                    {\n                        \"name\": \"Ismail Pelaseyed\",\n                        \"email\": \"ismail@sendcase.com\"\n                    }\n                ],\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"thread_id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n                \"cc\": [],\n                \"object\": \"message\",\n                \"bcc\": [],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"to\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"support@nylas.com\"\n                    }\n                ],\n                \"folder\": {\n                    \"display_name\": \"Deleted Items\",\n                    \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                    \"name\": \"trash\"\n                },\n                \"date\": 1440594160,\n                \"reply_to\": [],\n                \"events\": [],\n                \"starred\": false,\n                \"unread\": true,\n                \"id\": \"4oesh7fhsbd45t7dx30p03vz1\",\n                \"subject\": \"Incoming webhook POST body empty\"\n            }\n        },\n        \"thread\": {\n            \"62ehtebypazlgokcjvo8o9yak\": {\n                \"folders\": [\n                    {\n                        \"display_name\": \"Inbox\",\n                        \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                        \"name\": \"inbox\"\n                    }\n                ],\n                \"object\": \"thread\",\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"tags\": [\n                    {\n                        \"name\": \"Inbox\",\n                        \"id\": \"inbox\"\n                    },\n                    {\n                        \"name\": \"unread\",\n                        \"id\": \"unread\"\n                    }\n                ],\n                \"last_message_timestamp\": 1440610192,\n                \"has_attachments\": false,\n                \"first_message_timestamp\": 1440610192,\n                \"id\": \"62ehtebypazlgokcjvo8o9yak\",\n                \"subject\": \"This is an email following up on the Electron meetup.\",\n                \"last_message_received_timestamp\": 1440610192,\n                \"message_ids\": [\n                    \"9p571g0ie63rg0ekec699ol5e\"\n                ],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"participants\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"ben@nylas.com\"\n                    },\n                    {\n                        \"name\": \"Karen Rustad Tölva\",\n                        \"email\": \"karen.rustad@gmail.com\"\n                    }\n                ],\n                \"version\": 0,\n                \"starred\": false,\n                \"unread\": true,\n                \"draft_ids\": []\n            },\n            \"auro0q0gn0eawfrmqgzbi4f53\": {\n                \"folders\": [\n                    {\n                        \"display_name\": \"Deleted Items\",\n                        \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                        \"name\": \"trash\"\n                    }\n                ],\n                \"object\": \"thread\",\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"tags\": [\n                    {\n                        \"name\": \"Deleted Items\",\n                        \"id\": \"trash\"\n                    },\n                    {\n                        \"name\": \"unread\",\n                        \"id\": \"unread\"\n                    }\n                ],\n                \"last_message_timestamp\": 1440609839,\n                \"has_attachments\": false,\n                \"first_message_timestamp\": 1440594160,\n                \"id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n                \"subject\": \"Incoming webhook POST body empty\",\n                \"last_message_received_timestamp\": 1440609839,\n                \"message_ids\": [\n                    \"4oesh7fhsbd45t7dx30p03vz1\",\n                    \"7q4wtqzj3nxb42y6ysbl5l73t\"\n                ],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"participants\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"support@nylas.com\"\n                    },\n                    {\n                        \"name\": \"Ismail Pelaseyed\",\n                        \"email\": \"ismail@sendcase.com\"\n                    }\n                ],\n                \"version\": 1,\n                \"starred\": false,\n                \"unread\": true,\n                \"draft_ids\": []\n            }\n        },\n        \"contact\": {\n            \"1faoqpnn2rhf6mstmyc0ve71u\": {\n                \"email\": \"karen.rustad@gmail.com\",\n                \"object\": \"contact\",\n                \"id\": \"1faoqpnn2rhf6mstmyc0ve71u\",\n                \"name\": \"Karen Rustad Tölva\",\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\"\n            }\n        }\n    },\n    \"modify\": {\n        \"thread\": {\n            \"cungn0trv89l19mf08a2blyuh\": {\n                \"folders\": [\n                    {\n                        \"display_name\": \"Inbox\",\n                        \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                        \"name\": \"inbox\"\n                    }\n                ],\n                \"object\": \"thread\",\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"tags\": [\n                    {\n                        \"name\": \"Inbox\",\n                        \"id\": \"inbox\"\n                    },\n                    {\n                        \"name\": \"unread\",\n                        \"id\": \"unread\"\n                    }\n                ],\n                \"last_message_timestamp\": 1440609916,\n                \"has_attachments\": false,\n                \"first_message_timestamp\": 1440543552,\n                \"id\": \"cungn0trv89l19mf08a2blyuh\",\n                \"subject\": \"[Differential] [Request, 1,621 lines] D1936: feat(work): Create the \\\"Work\\\" window, move TaskQueue, Nylas sync workers\",\n                \"last_message_received_timestamp\": 1440609916,\n                \"message_ids\": [\n                    \"9uznoal93shahahlkesxkmfkr\",\n                    \"4rv7upa3gzuf1hal48b15lxrx\"\n                ],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"participants\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"ben@inboxapp.com\"\n                    },\n                    {\n                        \"name\": \"evan (Evan Morikawa)\",\n                        \"email\": \"noreply+phabricator@nilas.com\"\n                    },\n                    {\n                        \"name\": \"bengotow (Ben Gotow)\",\n                        \"email\": \"noreply+phabricator@nilas.com\"\n                    }\n                ],\n                \"version\": 1,\n                \"starred\": false,\n                \"unread\": true,\n                \"draft_ids\": []\n            },\n            \"8qesqxoftrd3nqiig3cz6gu49\": {\n                \"folders\": [\n                    {\n                        \"display_name\": \"Inbox\",\n                        \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                        \"name\": \"inbox\"\n                    }\n                ],\n                \"object\": \"thread\",\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"tags\": [\n                    {\n                        \"name\": \"Inbox\",\n                        \"id\": \"inbox\"\n                    },\n                    {\n                        \"name\": \"unread\",\n                        \"id\": \"unread\"\n                    }\n                ],\n                \"last_message_timestamp\": 1440609839,\n                \"has_attachments\": false,\n                \"first_message_timestamp\": 1440594160,\n                \"id\": \"8qesqxoftrd3nqiig3cz6gu49\",\n                \"subject\": \"Incoming webhook POST body empty\",\n                \"last_message_received_timestamp\": 1440609839,\n                \"message_ids\": [\n                    \"b2dq8u1kbambwor2vy5nz619n\",\n                    \"tt4i92q8rtpgrbxaq6viqlf4\"\n                ],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"participants\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"support@nylas.com\"\n                    },\n                    {\n                        \"name\": \"Ismail Pelaseyed\",\n                        \"email\": \"ismail@sendcase.com\"\n                    }\n                ],\n                \"version\": 1,\n                \"starred\": false,\n                \"unread\": true,\n                \"draft_ids\": []\n            },\n            \"auro0q0gn0eawfrmqgzbi4f53\": {\n                \"folders\": [\n                    {\n                        \"display_name\": \"Deleted Items\",\n                        \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                        \"name\": \"trash\"\n                    }\n                ],\n                \"object\": \"thread\",\n                \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n                \"tags\": [\n                    {\n                        \"name\": \"Deleted Items\",\n                        \"id\": \"trash\"\n                    },\n                    {\n                        \"name\": \"unread\",\n                        \"id\": \"unread\"\n                    }\n                ],\n                \"last_message_timestamp\": 1440609839,\n                \"has_attachments\": false,\n                \"first_message_timestamp\": 1440594160,\n                \"id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n                \"subject\": \"Incoming webhook POST body empty\",\n                \"last_message_received_timestamp\": 1440609839,\n                \"message_ids\": [\n                    \"4oesh7fhsbd45t7dx30p03vz1\",\n                    \"7q4wtqzj3nxb42y6ysbl5l73t\"\n                ],\n                \"snippet\": \"<<SNIPPET>>\",\n                \"participants\": [\n                    {\n                        \"name\": \"\",\n                        \"email\": \"support@nylas.com\"\n                    },\n                    {\n                        \"name\": \"Ismail Pelaseyed\",\n                        \"email\": \"ismail@sendcase.com\"\n                    }\n                ],\n                \"version\": 1,\n                \"starred\": false,\n                \"unread\": true,\n                \"draft_ids\": []\n            }\n        }\n    },\n    \"destroy\": [\n        {\n            \"cursor\": \"bb95ddzqtr2gpmvgrng73t6ih\",\n            \"object\": \"thread\",\n            \"event\": \"delete\",\n            \"id\": \"8qesqxoftrd3nqiig3cz6gu49\",\n            \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n        },\n        {\n            \"cursor\": \"f1pw0buzv336n5xbuod7579cv\",\n            \"object\": \"message\",\n            \"event\": \"delete\",\n            \"id\": \"tt4i92q8rtpgrbxaq6viqlf4\",\n            \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n        },\n        {\n            \"cursor\": \"du99szyqlrujornwr5fgrzxeg\",\n            \"object\": \"message\",\n            \"event\": \"delete\",\n            \"id\": \"b2dq8u1kbambwor2vy5nz619n\",\n            \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n        }\n    ]\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/sample-deltas.json",
    "content": "[\n    {\n        \"cursor\": \"9w3el67207f8vvbkv89os5ay6\",\n        \"attributes\": {\n            \"body\": \"<<BODY>>\",\n            \"files\": [],\n            \"from\": [\n                {\n                    \"name\": \"Karen Rustad Tölva\",\n                    \"email\": \"karen.rustad@gmail.com\"\n                }\n            ],\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"thread_id\": \"62ehtebypazlgokcjvo8o9yak\",\n            \"cc\": [],\n            \"object\": \"message\",\n            \"bcc\": [],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"to\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"ben@nylas.com\"\n                }\n            ],\n            \"folder\": {\n                \"display_name\": \"Inbox\",\n                \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                \"name\": \"inbox\"\n            },\n            \"date\": 1440610192,\n            \"reply_to\": [],\n            \"events\": [],\n            \"starred\": false,\n            \"unread\": true,\n            \"id\": \"9p571g0ie63rg0ekec699ol5e\",\n            \"subject\": \"This is an email following up on the Electron meetup.\"\n        },\n        \"object\": \"message\",\n        \"event\": \"create\",\n        \"id\": \"9p571g0ie63rg0ekec699ol5e\",\n        \"timestamp\": \"2015-08-26T17:35:16.506Z\"\n    },\n    {\n        \"cursor\": \"bqw8x58pghty4fw828tezb0va\",\n        \"attributes\": {\n            \"folders\": [\n                {\n                    \"display_name\": \"Inbox\",\n                    \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                    \"name\": \"inbox\"\n                }\n            ],\n            \"object\": \"thread\",\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"tags\": [\n                {\n                    \"name\": \"Inbox\",\n                    \"id\": \"inbox\"\n                },\n                {\n                    \"name\": \"unread\",\n                    \"id\": \"unread\"\n                }\n            ],\n            \"last_message_timestamp\": 1440610192,\n            \"has_attachments\": false,\n            \"first_message_timestamp\": 1440610192,\n            \"id\": \"62ehtebypazlgokcjvo8o9yak\",\n            \"subject\": \"This is an email following up on the Electron meetup.\",\n            \"last_message_received_timestamp\": 1440610192,\n            \"message_ids\": [\n                \"9p571g0ie63rg0ekec699ol5e\"\n            ],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"participants\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"ben@nylas.com\"\n                },\n                {\n                    \"name\": \"Karen Rustad Tölva\",\n                    \"email\": \"karen.rustad@gmail.com\"\n                }\n            ],\n            \"version\": 0,\n            \"starred\": false,\n            \"unread\": true,\n            \"draft_ids\": []\n        },\n        \"object\": \"thread\",\n        \"event\": \"create\",\n        \"id\": \"62ehtebypazlgokcjvo8o9yak\",\n        \"timestamp\": \"2015-08-26T17:35:16.506Z\"\n    },\n    {\n        \"cursor\": \"ewa1rr5nczdvrwpu11d3454ee\",\n        \"attributes\": {\n            \"email\": \"karen.rustad@gmail.com\",\n            \"object\": \"contact\",\n            \"id\": \"1faoqpnn2rhf6mstmyc0ve71u\",\n            \"name\": \"Karen Rustad Tölva\",\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\"\n        },\n        \"object\": \"contact\",\n        \"event\": \"create\",\n        \"id\": \"1faoqpnn2rhf6mstmyc0ve71u\",\n        \"timestamp\": \"2015-08-26T17:35:16.506Z\"\n    },\n    {\n        \"cursor\": \"756iwtaiufb6vxbtp4lt57cx0\",\n        \"attributes\": {\n            \"folders\": [\n                {\n                    \"display_name\": \"Inbox\",\n                    \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                    \"name\": \"inbox\"\n                }\n            ],\n            \"object\": \"thread\",\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"tags\": [\n                {\n                    \"name\": \"Inbox\",\n                    \"id\": \"inbox\"\n                },\n                {\n                    \"name\": \"unread\",\n                    \"id\": \"unread\"\n                }\n            ],\n            \"last_message_timestamp\": 1440609916,\n            \"has_attachments\": false,\n            \"first_message_timestamp\": 1440543552,\n            \"id\": \"cungn0trv89l19mf08a2blyuh\",\n            \"subject\": \"[Differential] [Request, 1,621 lines] D1936: feat(work): Create the \\\"Work\\\" window, move TaskQueue, Nylas sync workers\",\n            \"last_message_received_timestamp\": 1440609916,\n            \"message_ids\": [\n                \"9uznoal93shahahlkesxkmfkr\",\n                \"4rv7upa3gzuf1hal48b15lxrx\"\n            ],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"participants\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"ben@inboxapp.com\"\n                },\n                {\n                    \"name\": \"evan (Evan Morikawa)\",\n                    \"email\": \"noreply+phabricator@nilas.com\"\n                },\n                {\n                    \"name\": \"bengotow (Ben Gotow)\",\n                    \"email\": \"noreply+phabricator@nilas.com\"\n                }\n            ],\n            \"version\": 1,\n            \"starred\": false,\n            \"unread\": true,\n            \"draft_ids\": []\n        },\n        \"object\": \"thread\",\n        \"event\": \"modify\",\n        \"id\": \"cungn0trv89l19mf08a2blyuh\",\n        \"timestamp\": \"2015-08-26T17:35:16.506Z\"\n    },\n    {\n        \"cursor\": \"ev9ewky31ps7deaf0dm90f4qa\",\n        \"attributes\": {\n            \"body\": \"<<BODY>>\",\n            \"files\": [],\n            \"from\": [\n                {\n                    \"name\": \"evan (Evan Morikawa)\",\n                    \"email\": \"noreply+phabricator@nilas.com\"\n                }\n            ],\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"thread_id\": \"cungn0trv89l19mf08a2blyuh\",\n            \"cc\": [],\n            \"object\": \"message\",\n            \"bcc\": [],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"to\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"ben@inboxapp.com\"\n                }\n            ],\n            \"folder\": {\n                \"display_name\": \"Inbox\",\n                \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                \"name\": \"inbox\"\n            },\n            \"date\": 1440609916,\n            \"reply_to\": [],\n            \"events\": [],\n            \"starred\": false,\n            \"unread\": true,\n            \"id\": \"4rv7upa3gzuf1hal48b15lxrx\",\n            \"subject\": \"[Differential] [Accepted] D1936: feat(work): Create the \\\"Work\\\" window, move TaskQueue, Nylas sync workers\"\n        },\n        \"object\": \"message\",\n        \"event\": \"create\",\n        \"id\": \"4rv7upa3gzuf1hal48b15lxrx\",\n        \"timestamp\": \"2015-08-26T17:35:16.506Z\"\n    },\n    {\n        \"cursor\": \"3gynk12o31m2199ppja76nkve\",\n        \"attributes\": {\n            \"folders\": [\n                {\n                    \"display_name\": \"Inbox\",\n                    \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                    \"name\": \"inbox\"\n                }\n            ],\n            \"object\": \"thread\",\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"tags\": [\n                {\n                    \"name\": \"Inbox\",\n                    \"id\": \"inbox\"\n                },\n                {\n                    \"name\": \"unread\",\n                    \"id\": \"unread\"\n                }\n            ],\n            \"last_message_timestamp\": 1440609839,\n            \"has_attachments\": false,\n            \"first_message_timestamp\": 1440594160,\n            \"id\": \"8qesqxoftrd3nqiig3cz6gu49\",\n            \"subject\": \"Incoming webhook POST body empty\",\n            \"last_message_received_timestamp\": 1440609839,\n            \"message_ids\": [\n                \"b2dq8u1kbambwor2vy5nz619n\",\n                \"tt4i92q8rtpgrbxaq6viqlf4\"\n            ],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"participants\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"support@nylas.com\"\n                },\n                {\n                    \"name\": \"Ismail Pelaseyed\",\n                    \"email\": \"ismail@sendcase.com\"\n                }\n            ],\n            \"version\": 1,\n            \"starred\": false,\n            \"unread\": true,\n            \"draft_ids\": []\n        },\n        \"object\": \"thread\",\n        \"event\": \"modify\",\n        \"id\": \"8qesqxoftrd3nqiig3cz6gu49\",\n        \"timestamp\": \"2015-08-26T17:35:16.506Z\"\n    },\n    {\n        \"cursor\": \"e8wnx217l8v1qx205d4hxvlk1\",\n        \"attributes\": {\n            \"body\": \"<<BODY>>\",\n            \"files\": [],\n            \"from\": [\n                {\n                    \"name\": \"Ismail Pelaseyed\",\n                    \"email\": \"ismail@sendcase.com\"\n                }\n            ],\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"thread_id\": \"8qesqxoftrd3nqiig3cz6gu49\",\n            \"cc\": [],\n            \"object\": \"message\",\n            \"bcc\": [],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"to\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"support@nylas.com\"\n                }\n            ],\n            \"folder\": {\n                \"display_name\": \"Inbox\",\n                \"id\": \"bn7z083ho0mqfhqd68tio5a70\",\n                \"name\": \"inbox\"\n            },\n            \"date\": 1440609839,\n            \"reply_to\": [],\n            \"events\": [],\n            \"starred\": false,\n            \"unread\": true,\n            \"id\": \"tt4i92q8rtpgrbxaq6viqlf4\",\n            \"subject\": \"Re: Incoming webhook POST body empty\"\n        },\n        \"object\": \"message\",\n        \"event\": \"create\",\n        \"id\": \"tt4i92q8rtpgrbxaq6viqlf4\",\n        \"timestamp\": \"2015-08-26T17:35:16.506Z\"\n    },\n    {\n        \"cursor\": \"asyt17hyf9shing0tbdc1s5n9\",\n        \"attributes\": {\n            \"folders\": [\n                {\n                    \"display_name\": \"Deleted Items\",\n                    \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                    \"name\": \"trash\"\n                }\n            ],\n            \"object\": \"thread\",\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"tags\": [\n                {\n                    \"name\": \"Deleted Items\",\n                    \"id\": \"trash\"\n                },\n                {\n                    \"name\": \"unread\",\n                    \"id\": \"unread\"\n                }\n            ],\n            \"last_message_timestamp\": 1440609839,\n            \"has_attachments\": false,\n            \"first_message_timestamp\": 1440594160,\n            \"id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n            \"subject\": \"Incoming webhook POST body empty\",\n            \"last_message_received_timestamp\": 1440609839,\n            \"message_ids\": [\n                \"4oesh7fhsbd45t7dx30p03vz1\",\n                \"7q4wtqzj3nxb42y6ysbl5l73t\"\n            ],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"participants\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"support@nylas.com\"\n                },\n                {\n                    \"name\": \"Ismail Pelaseyed\",\n                    \"email\": \"ismail@sendcase.com\"\n                }\n            ],\n            \"version\": 1,\n            \"starred\": false,\n            \"unread\": true,\n            \"draft_ids\": []\n        },\n        \"object\": \"thread\",\n        \"event\": \"modify\",\n        \"id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n    },\n    {\n        \"cursor\": \"2r2f2indqv6fc9zptw9y49h97\",\n        \"attributes\": {\n            \"body\": \"<<BODY>>\",\n            \"files\": [],\n            \"from\": [\n                {\n                    \"name\": \"Ismail Pelaseyed\",\n                    \"email\": \"ismail@sendcase.com\"\n                }\n            ],\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"thread_id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n            \"cc\": [],\n            \"object\": \"message\",\n            \"bcc\": [],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"to\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"support@nylas.com\"\n                }\n            ],\n            \"folder\": {\n                \"display_name\": \"Deleted Items\",\n                \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                \"name\": \"trash\"\n            },\n            \"date\": 1440609839,\n            \"reply_to\": [],\n            \"events\": [],\n            \"starred\": false,\n            \"unread\": true,\n            \"id\": \"7q4wtqzj3nxb42y6ysbl5l73t\",\n            \"subject\": \"Re: Incoming webhook POST body empty\"\n        },\n        \"object\": \"message\",\n        \"event\": \"create\",\n        \"id\": \"7q4wtqzj3nxb42y6ysbl5l73t\",\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n    },\n    {\n        \"cursor\": \"ahvjhhsorjc0qe0snhwqydvui\",\n        \"attributes\": {\n            \"folders\": [\n                {\n                    \"display_name\": \"Deleted Items\",\n                    \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                    \"name\": \"trash\"\n                }\n            ],\n            \"object\": \"thread\",\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"tags\": [\n                {\n                    \"name\": \"Deleted Items\",\n                    \"id\": \"trash\"\n                },\n                {\n                    \"name\": \"unread\",\n                    \"id\": \"unread\"\n                }\n            ],\n            \"last_message_timestamp\": 1440609839,\n            \"has_attachments\": false,\n            \"first_message_timestamp\": 1440594160,\n            \"id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n            \"subject\": \"Incoming webhook POST body empty\",\n            \"last_message_received_timestamp\": 1440609839,\n            \"message_ids\": [\n                \"4oesh7fhsbd45t7dx30p03vz1\",\n                \"7q4wtqzj3nxb42y6ysbl5l73t\"\n            ],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"participants\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"support@nylas.com\"\n                },\n                {\n                    \"name\": \"Ismail Pelaseyed\",\n                    \"email\": \"ismail@sendcase.com\"\n                }\n            ],\n            \"version\": 1,\n            \"starred\": false,\n            \"unread\": true,\n            \"draft_ids\": []\n        },\n        \"object\": \"thread\",\n        \"event\": \"create\",\n        \"id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n    },\n    {\n        \"cursor\": \"2e35c6lcfvsx165kuu5534zng\",\n        \"attributes\": {\n            \"body\": \"<<BODY>>\",\n            \"files\": [],\n            \"from\": [\n                {\n                    \"name\": \"Ismail Pelaseyed\",\n                    \"email\": \"ismail@sendcase.com\"\n                }\n            ],\n            \"account_id\": \"9jx3zd30wqx04p26vh09ptox1\",\n            \"thread_id\": \"auro0q0gn0eawfrmqgzbi4f53\",\n            \"cc\": [],\n            \"object\": \"message\",\n            \"bcc\": [],\n            \"snippet\": \"<<SNIPPET>>\",\n            \"to\": [\n                {\n                    \"name\": \"\",\n                    \"email\": \"support@nylas.com\"\n                }\n            ],\n            \"folder\": {\n                \"display_name\": \"Deleted Items\",\n                \"id\": \"b5t18ldx25xibwq5ctuh10u8h\",\n                \"name\": \"trash\"\n            },\n            \"date\": 1440594160,\n            \"reply_to\": [],\n            \"events\": [],\n            \"starred\": false,\n            \"unread\": true,\n            \"id\": \"4oesh7fhsbd45t7dx30p03vz1\",\n            \"subject\": \"Incoming webhook POST body empty\"\n        },\n        \"object\": \"message\",\n        \"event\": \"create\",\n        \"id\": \"4oesh7fhsbd45t7dx30p03vz1\",\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n    },\n    {\n        \"cursor\": \"bb95ddzqtr2gpmvgrng73t6ih\",\n        \"object\": \"thread\",\n        \"event\": \"delete\",\n        \"id\": \"8qesqxoftrd3nqiig3cz6gu49\",\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n    },\n    {\n        \"cursor\": \"f1pw0buzv336n5xbuod7579cv\",\n        \"object\": \"message\",\n        \"event\": \"delete\",\n        \"id\": \"tt4i92q8rtpgrbxaq6viqlf4\",\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n    },\n    {\n        \"cursor\": \"du99szyqlrujornwr5fgrzxeg\",\n        \"object\": \"message\",\n        \"event\": \"delete\",\n        \"id\": \"b2dq8u1kbambwor2vy5nz619n\",\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n    }\n]\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/sample.less",
    "content": "@color: #4D926F;\n\n#header {\n  color: @color;\n}\nh2 {\n  color: @color;\n}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/table-data.es6",
    "content": "import {Table} from 'nylas-component-kit'\n\nexport const testData = {\n  columns: ['col1', 'col2', 'col3'],\n  rows: [\n    [1, 2, 3],\n    [4, 5, 6],\n    [7, 8, 9],\n  ],\n}\n\nexport const testDataUneven = {\n  columns: ['col1', 'col2', 'col3'],\n  rows: [\n    [1, 2],\n    [4, 5, 6],\n    [7, 8],\n  ],\n}\n\nexport const testDataEmpty = {\n  columns: ['col1', 'col2', ''],\n  rows: [\n    [1, 2],\n    [4, 5, 6],\n    ['', ''],\n    [],\n  ],\n}\n\nclass TestSource extends Table.TableDataSource {\n  setRows(rows) {\n    const data = {\n      rows: [...rows],\n      columns: this.columns(),\n    }\n    return new TestSource(data)\n  }\n}\n\nexport const testDataSource = new TestSource(testData)\n\nexport const testDataSourceUneven = new TestSource(testDataUneven)\n\nexport const testDataSourceEmpty = new TestSource(testDataEmpty)\n\nexport const selection = {colIdx: 0, rowIdx: 0, key: null}\n\nexport const cellProps = {tableDataSource: testDataSource, selection, colIdx: 0, rowIdx: 0, onSetSelection: () => {}, onCellEdited: () => {}}\n\nexport const rowProps = {tableDataSource: testDataSource, selection, rowIdx: 0}\n\nexport const tableProps = {tableDataSource: testDataSource, selection, onSetSelection: () => {}, onShiftSelection: () => {}, onCellEdited: () => {}}\n"
  },
  {
    "path": "packages/client-app/spec/fixtures/task-spec-handler.coffee",
    "content": "module.exports = ->\n  emit(\"some-event\", 1, 2, 3)\n  'hello'\n"
  },
  {
    "path": "packages/client-app/spec/list-selection-spec.coffee",
    "content": "_ = require 'underscore'\n\n{Thread} = require 'nylas-exports'\n{ListTabular} = require 'nylas-component-kit'\n\nListDataSource = ListTabular.DataSource\nListSelection = ListTabular.Selection\n\ndescribe \"ListSelection\", ->\n  beforeEach ->\n    @trigger = jasmine.createSpy('trigger')\n\n    @items = []\n    @items.push(new Thread(id: \"#{ii}\", clientId: \"#{ii}\")) for ii in [0..99]\n\n    @view = new ListDataSource()\n    @view.indexOfId = jasmine.createSpy('indexOfId').andCallFake (id) =>\n      _.findIndex(@items, _.matcher({id}))\n    @view.get = jasmine.createSpy('get').andCallFake (idx) =>\n      @items[idx]\n\n    @selection = new ListSelection(@view, @trigger)\n\n  it \"should initialize with an empty set\", ->\n    expect(@selection.items()).toEqual([])\n    expect(@selection.ids()).toEqual([])\n\n  it \"should throw an exception if a view is not provided\", ->\n    expect( => new ListSelection(null, @trigger)).toThrow()\n\n  describe \"set\", ->\n    it \"should replace the current selection with the provided models\", ->\n      @selection.set([@items[2], @items[4], @items[7]])\n      expect(@selection.ids()).toEqual(['2', '4', '7'])\n      @selection.set([@items[2], @items[5], @items[6]])\n      expect(@selection.ids()).toEqual(['2', '5', '6'])\n\n    it \"should throw an exception if the items passed are not models\", ->\n      expect( => @selection.set(['hi'])).toThrow()\n\n    it \"should trigger\", ->\n      @selection.set([@items[2], @items[4], @items[7]])\n      expect(@trigger).toHaveBeenCalled()\n\n  describe \"clear\", ->\n    beforeEach ->\n      @selection.set([@items[2]])\n\n    it \"should empty the selection set\", ->\n      @selection.clear()\n      expect(@selection.ids()).toEqual([])\n\n    it \"should trigger\", ->\n      @selection.clear()\n      expect(@trigger).toHaveBeenCalled()\n\n  describe \"remove\", ->\n    beforeEach ->\n      @selection.set([@items[2], @items[4], @items[7]])\n\n    it \"should do nothing if called without a valid item\", ->\n      @selection.remove(null)\n      @selection.remove(undefined)\n      @selection.remove(false)\n      expect(@selection.ids()).toEqual(['2', '4', '7'])\n\n    it \"should remove the item from the set\", ->\n      @selection.remove(@items[2])\n      expect(@selection.ids()).toEqual(['4', '7'])\n\n    it \"should throw an exception if any item passed is not a model\", ->\n      expect( => @selection.remove('hi')).toThrow()\n\n    it \"should accept an array of models as well as a single item\", ->\n      @selection.remove([@items[2], @items[4]])\n      expect(@selection.ids()).toEqual(['7'])\n\n    it \"should trigger\", ->\n      @selection.remove()\n      expect(@trigger).toHaveBeenCalled()\n\n  describe \"_applyChangeRecord\", ->\n    it \"should replace items in the selection with the matching provided items, if present\", ->\n      @selection.set([@items[2], @items[4], @items[7]])\n      expect(@selection.items()[0]).toBe(@items[2])\n      expect(@selection.items()[0].subject).toBe(undefined)\n      newItem2 = new Thread(id: '2', clientId: '2', subject:'Hello world!')\n      @selection._applyChangeRecord({objectClass: 'Thread', objects: [newItem2], type: 'persist'})\n      expect(@selection.items()[0].subject).toBe('Hello world!')\n\n    it \"should rremove items in the selection if type is unpersist\", ->\n      @selection.set([@items[2], @items[4], @items[7]])\n      newItem2 = new Thread(id: '2', clientId: '2', subject:'Hello world!')\n      @selection._applyChangeRecord({objectClass: 'Thread', objects: [newItem2], type: 'unpersist'})\n      expect(@selection.ids()).toEqual(['4', '7'])\n\n  describe \"toggle\", ->\n    beforeEach ->\n      @selection.set([@items[2]])\n\n    it \"should do nothing if called without a valid item\", ->\n      @selection.toggle(null)\n      @selection.toggle(undefined)\n      @selection.toggle(false)\n      expect(@selection.ids()).toEqual(['2'])\n\n    it \"should throw an exception if the item passed is not a model\", ->\n      expect( => @selection.toggle('hi')).toThrow()\n\n    it \"should select the item if it is not selected\", ->\n      @selection.toggle(@items[3])\n      expect(@selection.ids()).toEqual(['2', '3'])\n\n    it \"should de-select the item if it is selected\", ->\n      @selection.toggle(@items[2])\n      expect(@selection.ids()).toEqual([])\n\n    it \"should trigger\", ->\n      @selection.toggle(@items[2])\n      expect(@trigger).toHaveBeenCalled()\n\n  describe \"expandTo\", ->\n    it \"should select the item, if no other items are selected\", ->\n      @selection.clear()\n      @selection.expandTo(@items[2])\n      expect(@selection.ids()).toEqual(['2'])\n\n    it \"should do nothing if called without a valid item\", ->\n      @selection.expandTo(null)\n      @selection.expandTo(undefined)\n      @selection.expandTo(false)\n      expect(@selection.ids()).toEqual([])\n\n    it \"should throw an exception if the item passed is not a model\", ->\n      expect( => @selection.expandTo('hi')).toThrow()\n\n    it \"should select all items from the last selected item to the provided item when the provided item is below the current selection\", ->\n      @selection.set([@items[2], @items[5]])\n      @selection.expandTo(@items[8])\n      expect(@selection.ids()).toEqual(['2','5','6','7','8'])\n\n    it \"should select all items from the last selected item to the provided item when the provided item is above the current selection\", ->\n      @selection.set([@items[7], @items[5]])\n      @selection.expandTo(@items[2])\n      expect(@selection.ids()).toEqual(['7', '5', '4', '3', '2'])\n\n    it \"should not do anything if the provided item is not in the view set\", ->\n      @selection.set([@items[2]])\n      @selection.expandTo(new Thread(id:'not-in-view!'))\n      expect(@selection.ids()).toEqual(['2'])\n\n    it \"should re-order items so that the order still reflects the order selection actions were taken\", ->\n      @selection.set([@items[10], @items[4], @items[1]])\n      @selection.expandTo(@items[8])\n      expect(@selection.ids()).toEqual(['10','1','2','3','4','5','6','7','8'])\n\n    it \"should trigger\", ->\n      @selection.set([@items[5], @items[4], @items[1]])\n      @selection.expandTo(@items[8])\n      expect(@trigger).toHaveBeenCalled()\n\n  describe \"walk\", ->\n    beforeEach ->\n      @selection.set([@items[2]])\n\n    it \"should trigger\", ->\n      current = @items[4]\n      next = @items[5]\n      @selection.walk({current, next})\n      expect(@trigger).toHaveBeenCalled()\n\n    it \"should select both items if neither the start row or the end row are selected\", ->\n      current = @items[4]\n      next = @items[5]\n      @selection.walk({current, next})\n      expect(@selection.ids()).toEqual(['2', '4', '5'])\n\n    it \"should select only one item if either current or next is null or undefined\", ->\n      current = null\n      next = @items[5]\n      @selection.walk({current, next})\n      expect(@selection.ids()).toEqual(['2', '5'])\n\n      next = null\n      current = @items[7]\n      @selection.walk({current, next})\n      expect(@selection.ids()).toEqual(['2', '5', '7'])\n\n    describe \"when the `next` item is a step backwards in the selection history\", ->\n      it \"should deselect the current item\", ->\n        @selection.set([@items[2], @items[3], @items[4], @items[5]])\n        current = @items[5]\n        next = @items[4]\n        @selection.walk({current, next})\n        expect(@selection.ids()).toEqual(['2', '3', '4'])\n\n    describe \"otherwise\", ->\n      it \"should select the next item\", ->\n        @selection.set([@items[2], @items[3], @items[4], @items[5]])\n        current = @items[5]\n        next = @items[6]\n        @selection.walk({current, next})\n        expect(@selection.ids()).toEqual(['2', '3', '4', '5', '6'])\n\n      describe \"if the item was already selected\", ->\n        it \"should re-order the selection array so the selection still represents selection history\", ->\n          @selection.set([@items[5], @items[8], @items[7], @items[6]])\n          expect(@selection.ids()).toEqual(['5', '8', '7', '6'])\n\n          current = @items[6]\n          next = @items[5]\n          @selection.walk({current, next})\n          expect(@selection.ids()).toEqual(['8', '7', '6', '5'])\n"
  },
  {
    "path": "packages/client-app/spec/mail-rules-processor-spec.coffee",
    "content": "_ = require 'underscore'\n{Message,\n Contact,\n Thread,\n File,\n DatabaseStore,\n TaskQueueStatusStore,\n Actions} = require 'nylas-exports'\n\nMailRulesProcessor = require '../src/mail-rules-processor'\n\nTests = [{\n  rule: {\n    id: \"local-ac7f1671-ba03\",\n    name: \"conditionMode Any, contains, equals\",\n    conditions: [\n      {\n        templateKey: \"from\"\n        comparatorKey: \"contains\"\n        value: \"@nylas.com\"\n      },\n      {\n        templateKey: \"from\"\n        comparatorKey: \"equals\"\n        value: \"oldschool@nilas.com\"\n      }\n    ],\n    conditionMode: \"any\",\n    actions: [\n      {\n        templateKey: \"markAsRead\"\n      }\n    ],\n    accountId: \"b5djvgcuhj6i3x8nm53d0vnjm\"\n  },\n  good: [\n    new Message(from: [new Contact(email:'ben@nylas.com')])\n    new Message(from: [new Contact(email:'ben@nylas.com.jp')])\n    new Message(from: [new Contact(email:'oldschool@nilas.com')])\n  ]\n  bad: [\n    new Message(from: [new Contact(email:'ben@other.com')])\n    new Message(from: [new Contact(email:'ben@nilas.com')])\n    new Message(from: [new Contact(email:'twooldschool@nilas.com')])\n  ]\n},{\n  rule: {\n    id: \"local-ac7f1671-ba03\",\n    name: \"conditionMode all, ends with, begins with\",\n    conditions: [\n      {\n        templateKey: \"cc\"\n        comparatorKey: \"endsWith\"\n        value: \".com\"\n      },\n      {\n        templateKey: \"subject\"\n        comparatorKey: \"beginsWith\"\n        value: \"[TEST] \"\n      }\n    ],\n    conditionMode: \"any\",\n    actions: [\n      {\n        templateKey: \"applyLabel\"\n        value: \"51a0hb8d6l78mmhy19ffx4txs\"\n      }\n    ],\n    accountId: \"b5djvgcuhj6i3x8nm53d0vnjm\"\n  },\n  good: [\n    new Message(cc: [new Contact(email:'ben@nylas.org')], subject: '[TEST] ABCD')\n    new Message(cc: [new Contact(email:'ben@nylas.org')], subject: '[test] ABCD')\n    new Message(cc: [new Contact(email:'ben@nylas.com')], subject: 'Whatever')\n    new Message(cc: [new Contact(email:'a@test.com')], subject: 'Whatever')\n    new Message(cc: [new Contact(email:'a@hasacom.com')], subject: '[test] Whatever')\n    new Message(cc: [new Contact(email:'a@hasacom.org'), new Contact(email:'b@nylas.com')], subject: 'Whatever')\n  ]\n  bad: [\n    new Message(cc: [new Contact(email:'a@hasacom.org')], subject: 'Whatever')\n    new Message(cc: [new Contact(email:'a@hasacom.org')], subject: '[test]Whatever')\n    new Message(cc: [new Contact(email:'a.com@hasacom.org')], subject: 'Whatever [test] ')\n  ]\n},{\n  rule: {\n    id: \"local-ac7f1671-ba03\",\n    name: \"Any attachment name endsWith, anyRecipient equals\",\n    conditions: [\n      {\n        templateKey: \"anyAttachmentName\"\n        comparatorKey: \"endsWith\"\n        value: \".pdf\"\n      },\n      {\n        templateKey: \"anyRecipient\"\n        comparatorKey: \"equals\"\n        value: \"files@nylas.com\"\n      }\n    ],\n    conditionMode: \"any\",\n    actions: [\n      {\n        templateKey: \"changeFolder\"\n        value: \"51a0hb8d6l78mmhy19ffx4txs\"\n      }\n    ],\n    accountId: \"b5djvgcuhj6i3x8nm53d0vnjm\"\n  },\n  good: [\n    new Message(files: [new File(filename: 'bengotow.pdf')], to: [new Contact(email:'ben@nylas.org')])\n    new Message(to: [new Contact(email:'files@nylas.com')])\n    new Message(to: [new Contact(email:'ben@nylas.com')], cc: [new Contact(email:'ben@test.com'), new Contact(email:'files@nylas.com')])\n  ],\n  bad: [\n    new Message(to: [new Contact(email:'ben@nylas.org')])\n    new Message(files: [new File(filename: 'bengotow.pdfz')], to: [new Contact(email:'ben@nylas.org')])\n    new Message(files: [new File(filename: 'bengotowpdf')], to: [new Contact(email:'ben@nylas.org')])\n    new Message(to: [new Contact(email:'afiles@nylas.com')])\n    new Message(to: [new Contact(email:'files@nylas.coma')])\n  ]\n}]\n\nxdescribe \"MailRulesProcessor\", ->\n\n  describe \"_checkRuleForMessage\", ->\n    it \"should correctly filter sample messages\", ->\n      Tests.forEach ({rule, good, bad}) =>\n        for message, idx in good\n          message.accountId = rule.accountId\n          if MailRulesProcessor._checkRuleForMessage(rule, message) isnt true\n            expect(\"#{idx} (#{rule.name})\").toBe(true)\n        for message, idx in bad\n          message.accountId = rule.accountId\n          if MailRulesProcessor._checkRuleForMessage(rule, message) isnt false\n            expect(\"#{idx} (#{rule.name})\").toBe(false)\n\n    it \"should check the account id\", ->\n      {rule, good, bad} = Tests[0]\n      message = good[0]\n      message.accountId = 'not the same!'\n      expect(MailRulesProcessor._checkRuleForMessage(rule, message)).toBe(false)\n\n  describe \"_applyRuleToMessage\", ->\n    it \"should queue tasks for messages\", ->\n      spyOn(TaskQueueStatusStore, 'waitForPerformLocal')\n      spyOn(Actions, 'queueTask')\n      spyOn(DatabaseStore, 'findBy').andReturn(Promise.resolve({}))\n      Tests.forEach ({rule}) =>\n        TaskQueueStatusStore.waitForPerformLocal.reset()\n        Actions.queueTask.reset()\n\n        message = new Message({accountId: rule.accountId})\n        thread = new Thread({accountId: rule.accountId})\n        response = MailRulesProcessor._applyRuleToMessage(rule, message, thread)\n        expect(response instanceof Promise).toBe(true)\n\n        waitsForPromise =>\n          response.then =>\n            expect(TaskQueueStatusStore.waitForPerformLocal).toHaveBeenCalled()\n            expect(Actions.queueTask).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/mailbox-perspective-spec.es6",
    "content": "import {\n  AccountStore,\n  MailboxPerspective,\n  TaskFactory,\n  Category,\n  CategoryStore,\n} from 'nylas-exports'\n\nimport CategoryRemovalTargetRulesets from '../internal_packages/thread-list/lib/category-removal-target-rulesets'\n\nconst {Default} = CategoryRemovalTargetRulesets;\n\n\ndescribe('MailboxPerspective', function mailboxPerspective() {\n  beforeEach(() => {\n    this.accountIds = ['a1', 'a2']\n    this.accounts = {\n      a1: {\n        id: 'a1',\n        defaultFinishedCategory: () => ({displayName: 'archive'}),\n        categoryIcon: () => null,\n      },\n      a2: {\n        id: 'a2',\n        defaultFinishedCategory: () => ({displayName: 'trash2'}),\n        categoryIcon: () => null,\n      },\n    }\n    this.perspective = new MailboxPerspective(this.accountIds)\n    spyOn(AccountStore, 'accountForId').andCallFake((accId) => this.accounts[accId])\n  });\n\n  describe('isEqual', () => {\n    // TODO\n  });\n\n  describe('canArchiveThreads', () => {\n    it('returns false if the perspective is archive', () => {\n      const accounts = [\n        {canArchiveThreads: () => true},\n        {canArchiveThreads: () => true},\n      ]\n      spyOn(AccountStore, 'accountsForItems').andReturn(accounts)\n      spyOn(this.perspective, 'isArchive').andReturn(true)\n      expect(this.perspective.canArchiveThreads()).toBe(false)\n    });\n\n    it('returns false if one of the accounts associated with the threads cannot archive', () => {\n      const accounts = [\n        {canArchiveThreads: () => true},\n        {canArchiveThreads: () => false},\n      ]\n      spyOn(AccountStore, 'accountsForItems').andReturn(accounts)\n      spyOn(this.perspective, 'isArchive').andReturn(false)\n      expect(this.perspective.canArchiveThreads()).toBe(false)\n    });\n\n    it('returns true otherwise', () => {\n      const accounts = [\n        {canArchiveThreads: () => true},\n        {canArchiveThreads: () => true},\n      ]\n      spyOn(AccountStore, 'accountsForItems').andReturn(accounts)\n      spyOn(this.perspective, 'isArchive').andReturn(false)\n      expect(this.perspective.canArchiveThreads()).toBe(true)\n    });\n  });\n\n  describe('canMoveThreadsTo', () => {\n    it('returns false if the perspective is the target folder', () => {\n      const accounts = [\n        {id: 'a'},\n        {id: 'b'},\n      ]\n      spyOn(AccountStore, 'accountsForItems').andReturn(accounts)\n      spyOn(this.perspective, 'categoriesSharedName').andReturn('trash')\n      expect(this.perspective.canMoveThreadsTo([], 'trash')).toBe(false)\n    });\n\n    it('returns false if one of the accounts associated with the threads does not have the folder', () => {\n      const accounts = [\n        {id: 'a'},\n        {id: 'b'},\n      ]\n      spyOn(CategoryStore, 'getStandardCategory').andReturn(null)\n      spyOn(AccountStore, 'accountsForItems').andReturn(accounts)\n      spyOn(this.perspective, 'categoriesSharedName').andReturn('inbox')\n      expect(this.perspective.canMoveThreadsTo([], 'trash')).toBe(false)\n    });\n\n    it('returns true otherwise', () => {\n      const accounts = [\n        {id: 'a'},\n        {id: 'b'},\n      ]\n      const category = {id: 'cat'};\n      spyOn(CategoryStore, 'getStandardCategory').andReturn(category)\n      spyOn(AccountStore, 'accountsForItems').andReturn(accounts)\n      spyOn(this.perspective, 'categoriesSharedName').andReturn('inbox')\n      expect(this.perspective.canMoveThreadsTo([], 'trash')).toBe(true)\n    });\n  });\n\n  describe('canReceiveThreadsFromAccountIds', () => {\n    it('returns true if the thread account ids are included in the current account ids', () => {\n      expect(this.perspective.canReceiveThreadsFromAccountIds(['a1'])).toBe(true)\n    });\n\n    it('returns false otherwise', () => {\n      expect(this.perspective.canReceiveThreadsFromAccountIds(['a4'])).toBe(false)\n      expect(this.perspective.canReceiveThreadsFromAccountIds([])).toBe(false)\n      expect(this.perspective.canReceiveThreadsFromAccountIds()).toBe(false)\n    });\n  });\n\n  describe('tasksForRemovingItems', () => {\n    beforeEach(() => {\n      this.categories = {\n        a1: {\n          archive: new Category({name: 'archive', displayName: 'archive', accountId: 'a1'}),\n          inbox: new Category({name: 'inbox', displayName: 'inbox1', accountId: 'a1'}),\n          trash: new Category({name: 'trash', displayName: 'trash1', accountId: 'a1'}),\n          category: new Category({name: null, displayName: 'folder1', accountId: 'a1'}),\n        },\n        a2: {\n          archive: new Category({name: 'all', displayName: 'all', accountId: 'a2'}),\n          inbox: new Category({name: 'inbox', displayName: 'inbox2', accountId: 'a2'}),\n          trash: new Category({name: 'trash', displayName: 'trash2', accountId: 'a2'}),\n          category: new Category({name: null, displayName: 'label2', accountId: 'a2'}),\n        },\n      }\n      this.threads = [\n        {accountId: 'a1'},\n        {accountId: 'a2'},\n      ]\n      spyOn(TaskFactory, 'tasksForApplyingCategories')\n      spyOn(CategoryStore, 'getTrashCategory').andCallFake((accId) => {\n        return this.categories[accId].trash\n      })\n    });\n\n    function assertMoved(accId) {\n      expect(TaskFactory.tasksForApplyingCategories).toHaveBeenCalled()\n      const {args} = TaskFactory.tasksForApplyingCategories.calls[0]\n      const {categoriesToRemove, categoriesToAdd} = args[0]\n\n      const assertor = {\n        from(originName) {\n          expect(categoriesToRemove(accId)[0].displayName).toEqual(originName)\n          return assertor\n        },\n        to(destinationName) {\n          expect(categoriesToAdd(accId)[0].displayName).toEqual(destinationName)\n          return assertor\n        },\n      }\n      return assertor\n    }\n\n    it('moves to finished category if viewing inbox', () => {\n      const perspective = MailboxPerspective.forCategories([\n        this.categories.a1.inbox,\n        this.categories.a2.inbox,\n      ])\n      perspective.tasksForRemovingItems(this.threads, Default)\n      assertMoved('a1').from('inbox1').to('archive')\n      assertMoved('a2').from('inbox2').to('trash2')\n    });\n\n    it('moves to trash if viewing archive', () => {\n      const perspective = MailboxPerspective.forCategories([\n        this.categories.a1.archive,\n        this.categories.a2.archive,\n      ])\n      perspective.tasksForRemovingItems(this.threads, Default)\n      assertMoved('a1').from('archive').to('trash1')\n      assertMoved('a2').from('all').to('trash2')\n    })\n\n    it('deletes permanently if viewing trash', () => {\n      // TODO\n      // Not currently possible\n    });\n\n    it('moves to default finished category if viewing category', () => {\n      const perspective = MailboxPerspective.forCategories([\n        this.categories.a1.category,\n        this.categories.a2.category,\n      ])\n      perspective.tasksForRemovingItems(this.threads, Default)\n      assertMoved('a1').from('folder1').to('archive')\n      assertMoved('a2').from('label2').to('trash2')\n    })\n\n    it('unstars if viewing starred', () => {\n      spyOn(TaskFactory, 'taskForInvertingStarred').andReturn({some: 'task'})\n      const perspective = MailboxPerspective.forStarred(this.accountIds)\n      const tasks = perspective.tasksForRemovingItems(this.threads, Default)\n      expect(tasks).toEqual([{some: 'task'}])\n    });\n\n    it('does nothing when viewing spam or sent', () => {\n      ['spam', 'sent'].forEach((invalid) => {\n        const perspective = MailboxPerspective.forCategories([\n          new Category({name: invalid, accountId: 'a1'}),\n          new Category({name: invalid, accountId: 'a2'}),\n        ])\n        const tasks = perspective.tasksForRemovingItems(this.threads, Default)\n        expect(TaskFactory.tasksForApplyingCategories).not.toHaveBeenCalled()\n        expect(tasks).toEqual([])\n      })\n    });\n\n    describe('when perspective is category perspective', () => {\n      it('overrides default ruleset', () => {\n        const customRuleset = {\n          all: () => ({displayName: 'my category'}),\n        }\n        const perspective = MailboxPerspective.forCategories([\n          this.categories.a1.category,\n        ])\n        spyOn(perspective, 'categoriesSharedName').andReturn('all')\n        perspective.tasksForRemovingItems(this.threads, customRuleset)\n        assertMoved('a1').to('my category')\n      });\n\n      it('does not create tasks if any name in the ruleset is null', () => {\n        const customRuleset = {\n          all: null,\n        }\n        const perspective = MailboxPerspective.forCategories([\n          this.categories.a1.category,\n        ])\n        spyOn(perspective, 'categoriesSharedName').andReturn('all')\n        const tasks = perspective.tasksForRemovingItems(this.threads, customRuleset)\n        expect(tasks).toEqual([])\n      });\n    });\n  });\n\n  describe('CategoryMailboxPerspective', () => {\n    beforeEach(() => {\n      this.categories = [\n        new Category({displayName: 'c1', accountId: 'a1'}),\n        new Category({displayName: 'c2', accountId: 'a2'}),\n        new Category({displayName: 'c3', accountId: 'a2'}),\n      ]\n      this.perspective = MailboxPerspective.forCategories(this.categories)\n    });\n\n    describe('canReceiveThreadsFromAccountIds', () => {\n      it('returns true if the thread account ids are included in the current account ids', () => {\n        expect(this.perspective.canReceiveThreadsFromAccountIds(['a1'])).toBe(true)\n      });\n\n      it('returns false otherwise', () => {\n        expect(this.perspective.canReceiveThreadsFromAccountIds(['a4'])).toBe(false)\n        expect(this.perspective.canReceiveThreadsFromAccountIds([])).toBe(false)\n        expect(this.perspective.canReceiveThreadsFromAccountIds()).toBe(false)\n      });\n\n      it('returns false if it is a locked category', () => {\n        this.perspective._categories.push(\n          new Category({name: 'sent', displayName: 'c4', accountId: 'a1'})\n        )\n        expect(this.perspective.canReceiveThreadsFromAccountIds(['a2'])).toBe(false)\n      });\n    });\n\n    describe('receiveThreads', () => {\n      // TODO\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/menu-manager-spec.coffee",
    "content": "MenuManager = require('../src/menu-manager').default\n\ndescribe \"MenuManager\", ->\n  menu = null\n\n  beforeEach ->\n    menu = new MenuManager(resourcePath: NylasEnv.getLoadSettings().resourcePath)\n    menu.template = []\n\n  describe \"::add(items)\", ->\n    it \"can add new menus that can be removed with the returned disposable\", ->\n      disposable = menu.add [{label: \"A\", submenu: [{label: \"B\", command: \"b\"}]}]\n      expect(menu.template).toEqual [{label: \"A\", submenu: [{label: \"B\", command: \"b\"}]}]\n      disposable.dispose()\n      expect(menu.template).toEqual []\n\n    it \"can add submenu items to existing menus that can be removed with the returned disposable\", ->\n      disposable1 = menu.add [{label: \"A\", submenu: [{label: \"B\", command: \"b\"}]}]\n      disposable2 = menu.add [{label: \"A\", submenu: [{label: \"C\", submenu: [{label: \"D\", command: 'd'}]}]}]\n      disposable3 = menu.add [{label: \"A\", submenu: [{label: \"C\", submenu: [{label: \"E\", command: 'e'}]}]}]\n\n      expect(menu.template).toEqual [{\n        label: \"A\",\n        submenu: [\n          {label: \"B\", command: \"b\"},\n          {label: \"C\", submenu: [{label: 'D', command: 'd'}, {label: 'E', command: 'e'}]}\n        ]\n      }]\n\n      disposable3.dispose()\n      expect(menu.template).toEqual [{\n        label: \"A\",\n        submenu: [\n          {label: \"B\", command: \"b\"},\n          {label: \"C\", submenu: [{label: 'D', command: 'd'}]}\n        ]\n      }]\n\n      disposable2.dispose()\n      expect(menu.template).toEqual [{label: \"A\", submenu: [{label: \"B\", command: \"b\"}]}]\n\n      disposable1.dispose()\n      expect(menu.template).toEqual []\n\n    it \"does not add duplicate labels to the same menu\", ->\n      originalItemCount = menu.template.length\n      menu.add [{label: \"A\", submenu: [{label: \"B\", command: \"b\"}]}]\n      menu.add [{label: \"A\", submenu: [{label: \"B\", command: \"b\"}]}]\n      expect(menu.template[originalItemCount]).toEqual {label: \"A\", submenu: [{label: \"B\", command: \"b\"}]}\n"
  },
  {
    "path": "packages/client-app/spec/models/category-spec.coffee",
    "content": "{Category, Label} = require 'nylas-exports'\n\ndescribe 'Category', ->\n\n  describe '.categoriesSharedName', ->\n\n    it 'returns the name if all the categories on the perspective have the same name', ->\n      expect(Category.categoriesSharedName([\n        new Category({name: 'c1', accountId: 'a1'}),\n        new Category({name: 'c1', accountId: 'a2'}),\n      ])).toEqual('c1')\n\n    it 'returns null if there are no categories', ->\n      expect(Category.categoriesSharedName([])).toEqual(null)\n\n    it 'returns null if the categories have different names', ->\n      expect(Category.categoriesSharedName([\n        new Category({name: 'c1', accountId: 'a1'}),\n        new Category({name: 'c2', accountId: 'a2'}),\n      ])).toEqual(null)\n\n  describe 'fromJSON', ->\n    it \"should strip the INBOX. prefix from FastMail folders\", ->\n      foo = (new Category()).fromJSON({display_name: 'INBOX.Foo'})\n      expect(foo.displayName).toEqual('Foo')\n      foo = (new Category()).fromJSON({display_name: 'INBOX'})\n      expect(foo.displayName).toEqual('Inbox')\n\n  describe 'category types', ->\n    it 'assigns type correctly when it is a user category', ->\n      cat = new Label\n      cat.name = undefined\n      expect(cat.isUserCategory()).toBe true\n      expect(cat.isStandardCategory()).toBe false\n      expect(cat.isHiddenCategory()).toBe false\n      expect(cat.isLockedCategory()).toBe false\n\n    it 'assigns type correctly when it is a standard category', ->\n      cat = new Label\n      cat.name = 'inbox'\n      expect(cat.isUserCategory()).toBe false\n      expect(cat.isStandardCategory()).toBe true\n      expect(cat.isHiddenCategory()).toBe false\n      expect(cat.isLockedCategory()).toBe false\n\n    it 'assigns type for `important` category when should not show important', ->\n      cat = new Label\n      cat.name = 'important'\n      expect(cat.isUserCategory()).toBe false\n      expect(cat.isStandardCategory(false)).toBe false\n      expect(cat.isHiddenCategory()).toBe true\n      expect(cat.isLockedCategory()).toBe false\n\n    it 'assigns type correctly when it is a hidden category', ->\n      cat = new Label\n      cat.name = 'archive'\n      expect(cat.isUserCategory()).toBe false\n      expect(cat.isStandardCategory()).toBe true\n      expect(cat.isHiddenCategory()).toBe true\n      expect(cat.isLockedCategory()).toBe false\n\n    it 'assigns type correctly when it is a locked category', ->\n      cat = new Label\n      cat.name = 'sent'\n      expect(cat.isUserCategory()).toBe false\n      expect(cat.isStandardCategory()).toBe true\n      expect(cat.isHiddenCategory()).toBe true\n      expect(cat.isLockedCategory()).toBe true\n"
  },
  {
    "path": "packages/client-app/spec/models/contact-spec.coffee",
    "content": "Contact = require(\"../../src/flux/models/contact\").default\nAccountStore = require(\"../../src/flux/stores/account-store\").default\nAccount = require(\"../../src/flux/models/account\").default\n\ncontact_1 =\n  name: \"Evan Morikawa\"\n  email: \"evan@nylas.com\"\n\ndescribe \"Contact\", ->\n\n  beforeEach ->\n    @account = AccountStore.accounts()[0]\n\n  it \"can be built via the constructor\", ->\n    c1 = new Contact contact_1\n    expect(c1.name).toBe \"Evan Morikawa\"\n    expect(c1.email).toBe \"evan@nylas.com\"\n\n  it \"accepts a JSON response\", ->\n    c1 = (new Contact).fromJSON(contact_1)\n    expect(c1.name).toBe \"Evan Morikawa\"\n    expect(c1.email).toBe \"evan@nylas.com\"\n\n  it \"correctly parses first and last names\", ->\n    c1 = new Contact {name: \"Evan Morikawa\"}\n    expect(c1.firstName()).toBe \"Evan\"\n    expect(c1.lastName()).toBe \"Morikawa\"\n\n    c2 = new Contact {name: \"Evan Takashi Morikawa\"}\n    expect(c2.firstName()).toBe \"Evan\"\n    expect(c2.lastName()).toBe \"Takashi Morikawa\"\n\n    c3 = new Contact {name: \"evan foo last-name\"}\n    expect(c3.firstName()).toBe \"evan\"\n    expect(c3.lastName()).toBe \"foo last-name\"\n\n    c4 = new Contact {name: \"Prince\"}\n    expect(c4.firstName()).toBe \"Prince\"\n    expect(c4.lastName()).toBe \"\"\n\n    c5 = new Contact {name: \"Mr. Evan Morikawa\"}\n    expect(c5.firstName()).toBe \"Evan\"\n    expect(c5.lastName()).toBe \"Morikawa\"\n\n    c6 = new Contact {name: \"Mr Evan morikawa\"}\n    expect(c6.firstName()).toBe \"Evan\"\n    expect(c6.lastName()).toBe \"morikawa\"\n\n    c7 = new Contact {name: \"Dr. No\"}\n    expect(c7.firstName()).toBe \"No\"\n    expect(c7.lastName()).toBe \"\"\n\n    c8 = new Contact {name: \"Mr\"}\n    expect(c8.firstName()).toBe \"Mr\"\n    expect(c8.lastName()).toBe \"\"\n\n  it \"properly parses Mike Kaylor via LinkedIn\", ->\n    c8 = new Contact {name: \"Mike Kaylor via LinkedIn\"}\n    expect(c8.firstName()).toBe \"Mike\"\n    expect(c8.lastName()).toBe \"Kaylor\"\n    c8 = new Contact {name: \"Mike Kaylor VIA LinkedIn\"}\n    expect(c8.firstName()).toBe \"Mike\"\n    expect(c8.lastName()).toBe \"Kaylor\"\n    c8 = new Contact {name: \"Mike Viator\"}\n    expect(c8.firstName()).toBe \"Mike\"\n    expect(c8.lastName()).toBe \"Viator\"\n    c8 = new Contact {name: \"Olivia Pope\"}\n    expect(c8.firstName()).toBe \"Olivia\"\n    expect(c8.lastName()).toBe \"Pope\"\n\n  it \"should not by fancy about the contents of parenthesis (Evan Morikawa)\", ->\n    c8 = new Contact {name: \"Evan (Evan Morikawa)\"}\n    expect(c8.firstName()).toBe \"Evan\"\n    expect(c8.lastName()).toBe \"(Evan Morikawa)\"\n\n  it \"falls back to the first component of the email if name isn't present\", ->\n    c1 = new Contact {name: \" Evan Morikawa \", email: \"evan@nylas.com\"}\n    expect(c1.displayName()).toBe \"Evan Morikawa\"\n    expect(c1.displayName(compact: true)).toBe \"Evan\"\n\n    c2 = new Contact {name: \"\", email: \"evan@nylas.com\"}\n    expect(c2.displayName()).toBe \"evan\"\n    expect(c2.displayName(compact: true)).toBe \"evan\"\n\n    c3 = new Contact {name: \"\", email: \"\"}\n    expect(c3.displayName()).toBe \"\"\n    expect(c3.displayName(compact: true)).toBe \"\"\n\n\n  it \"properly parses names with @\", ->\n    c1 = new Contact {name: \"nyl@s\"}\n    expect(c1.firstName()).toBe \"nyl@s\"\n    expect(c1.lastName()).toBe \"\"\n\n    c1 = new Contact {name: \"nyl@s@n1\"}\n    expect(c1.firstName()).toBe \"nyl@s@n1\"\n    expect(c1.lastName()).toBe \"\"\n\n    c2 = new Contact {name: \"nyl@s nyl@s\"}\n    expect(c2.firstName()).toBe \"nyl@s\"\n    expect(c2.lastName()).toBe \"nyl@s\"\n\n    c3 = new Contact {name: \"nyl@s 2000\"}\n    expect(c3.firstName()).toBe \"nyl@s\"\n    expect(c3.lastName()).toBe \"2000\"\n\n    c6 = new Contact {name: \"ev@nylas.com\", email: \"ev@nylas.com\"}\n    expect(c6.firstName()).toBe \"ev@nylas.com\"\n    expect(c6.lastName()).toBe \"\"\n\n    c7 = new Contact {name: \"evan@nylas.com\"}\n    expect(c7.firstName()).toBe \"evan@nylas.com\"\n    expect(c7.lastName()).toBe \"\"\n\n    c8 = new Contact {name: \"Mike K@ylor via L@nkedIn\"}\n    expect(c8.firstName()).toBe \"Mike\"\n    expect(c8.lastName()).toBe \"K@ylor\"\n\n  it \"properly parses names with last, first (description)\", ->\n    c1 = new Contact {name: \"Smith, Bob\"}\n    expect(c1.firstName()).toBe \"Bob\"\n    expect(c1.lastName()).toBe \"Smith\"\n    expect(c1.fullName()).toBe \"Bob Smith\"\n\n    c2 = new Contact {name: \"von Smith, Ricky Bobby\"}\n    expect(c2.firstName()).toBe \"Ricky Bobby\"\n    expect(c2.lastName()).toBe \"von Smith\"\n    expect(c2.fullName()).toBe \"Ricky Bobby von Smith\"\n\n    c3 = new Contact {name: \"von Smith, Ricky Bobby (Awesome Employee)\"}\n    expect(c3.firstName()).toBe \"Ricky Bobby\"\n    expect(c3.lastName()).toBe \"von Smith (Awesome Employee)\"\n    expect(c3.fullName()).toBe \"Ricky Bobby von Smith (Awesome Employee)\"\n\n  it \"should properly return `You` as the display name for the current user\", ->\n    c1 = new Contact {name: \" Test Monkey\", email: @account.emailAddress}\n    expect(c1.displayName()).toBe \"You\"\n    expect(c1.displayName(compact: true)).toBe \"You\"\n\n  describe \"isMe\", ->\n    it \"returns true if the contact name matches the account email address\", ->\n      c1 = new Contact {email: @account.emailAddress}\n      expect(c1.isMe()).toBe(true)\n      c1 = new Contact {email: 'ben@nylas.com'}\n      expect(c1.isMe()).toBe(false)\n\n    it \"is case insensitive\", ->\n      c1 = new Contact {email: @account.emailAddress.toUpperCase()}\n      expect(c1.isMe()).toBe(true)\n\n    it \"it calls through to accountForEmail\", ->\n      c1 = new Contact {email: @account.emailAddress}\n      acct = new Account()\n      spyOn(AccountStore, 'accountForEmail').andReturn(acct)\n      expect(c1.isMe()).toBe(true)\n      expect(AccountStore.accountForEmail).toHaveBeenCalled()\n\n  describe 'isValid', ->\n    it \"should return true for a variety of valid contacts\", ->\n      expect((new Contact(name: 'Ben', email: 'ben@nylas.com')).isValid()).toBe(true)\n      expect((new Contact(email: 'ben@nylas.com')).isValid()).toBe(true)\n      expect((new Contact(email: 'ben+123@nylas.com')).isValid()).toBe(true)\n\n    it \"should support contacts with unicode characters in domains\", ->\n      expect((new Contact(name: 'Ben', email: 'ben@arıman.com')).isValid()).toBe(true)\n\n    it \"should return false if the contact has no email\", ->\n      expect((new Contact(name: 'Ben')).isValid()).toBe(false)\n\n    it \"should return false if the contact has an email that is not valid\", ->\n      expect((new Contact(name: 'Ben', email:'Ben <ben@nylas.com>')).isValid()).toBe(false)\n      expect((new Contact(name: 'Ben', email:'<ben@nylas.com>')).isValid()).toBe(false)\n      expect((new Contact(name: 'Ben', email:'\"ben@nylas.com\"')).isValid()).toBe(false)\n\n    it \"returns false if the email doesn't satisfy the regex\", ->\n      expect((new Contact(name: \"test\", email: \"foo\")).isValid()).toBe false\n\n    it \"returns false if the email doesn't match\", ->\n      expect((new Contact(name: \"test\", email: \"foo@\")).isValid()).toBe false\n"
  },
  {
    "path": "packages/client-app/spec/models/event-spec.coffee",
    "content": "Event = require(\"../../src/flux/models/event\").default\nAccountStore = require(\"../../src/flux/stores/account-store\").default\n\njson_event =\n  {\n    \"object\": \"event\",\n    \"id\": \"4ee4xbnx7pxdb9g7c2f8ncyto\",\n    \"calendar_id\": \"ci0k1wfyv533ccgox4t7uri4h\",\n    \"account_id\": \"14e5bn96uizyuhidhcw5rfrb0\",\n    \"description\": null,\n    \"location\": null,\n    \"participants\": [\n      {\n        \"email\": \"example@gmail.com\",\n        \"name\": \"Ben Bitdiddle\",\n        \"status\": \"yes\"\n      }\n    ],\n    \"read_only\": false,\n    \"title\": \"Meeting with Ben Bitdiddle\",\n    \"when\": {\n      \"object\": \"timespan\",\n      \"end_time\": 1408123800,\n      \"start_time\": 1408120200\n    },\n    \"busy\": true,\n    \"status\": \"confirmed\",\n  }\n\nwhen_1 =\n  end_time: 1408123800\n  start_time: 1408120200\n\nparticipant_1 =\n  name: \"Ethan Blackburn\"\n  status: \"yes\"\n  email: \"ethan@nylas.com\"\n\nparticipant_2 =\n  name: \"Other Person\"\n  status: \"maybe\"\n  email: \"other@person.com\"\n\nparticipant_3 =\n  name: \"Another Person\"\n  status: \"no\"\n  email: \"another@person.com\"\n\nevent_1 =\n  title: \"Dolores\"\n  description: \"Hanging at the park\"\n  location: \"Dolores Park\"\n  when: when_1\n  start: 1408120200\n  end: 1408123800\n  participants: [participant_1, participant_2, participant_3]\n\ndescribe \"Event\", ->\n\n  it \"can be built via the constructor\", ->\n    e1 = new Event event_1\n    expect(e1.title).toBe \"Dolores\"\n    expect(e1.description).toBe \"Hanging at the park\"\n    expect(e1.location).toBe \"Dolores Park\"\n    expect(e1.when.start_time).toBe 1408120200\n    expect(e1.when.end_time).toBe 1408123800\n    expect(e1.start).toBe 1408120200\n    expect(e1.end).toBe 1408123800\n    expect(e1.participants[0].name).toBe \"Ethan Blackburn\"\n    expect(e1.participants[0].email).toBe \"ethan@nylas.com\"\n    expect(e1.participants[0].status).toBe \"yes\"\n    expect(e1.participants[1].name).toBe \"Other Person\"\n    expect(e1.participants[1].email).toBe \"other@person.com\"\n    expect(e1.participants[1].status).toBe \"maybe\"\n    expect(e1.participants[2].name).toBe \"Another Person\"\n    expect(e1.participants[2].email).toBe \"another@person.com\"\n    expect(e1.participants[2].status).toBe \"no\"\n\n  it \"accepts a JSON response\", ->\n    e1 = (new Event).fromJSON(json_event)\n    expect(e1.title).toBe \"Meeting with Ben Bitdiddle\"\n    expect(e1.description).toBe null\n    expect(e1.location).toBe null\n    expect(e1.start).toBe 1408120200\n    expect(e1.end).toBe 1408123800\n    expect(e1.participants[0].name).toBe \"Ben Bitdiddle\"\n    expect(e1.participants[0].email).toBe \"example@gmail.com\"\n    expect(e1.participants[0].status).toBe \"yes\"\n"
  },
  {
    "path": "packages/client-app/spec/models/file-spec.coffee",
    "content": "File = require('../../src/flux/models/file').default\n\ntest_file_path = \"/path/to/file.jpg\"\n\ndescribe \"File\", ->\n  it \"attempts to generate a new file upload task on creation\", ->\n    # File.create(test_file_path)\n\n  describe \"displayName\", ->\n    it \"should return the filename if populated\", ->\n      f = new File(filename: 'Hello world.jpg', contentType: 'image/jpg')\n      expect(f.displayName()).toBe('Hello world.jpg')\n      f = new File(filename: 'a', contentType: 'image/jpg')\n      expect(f.displayName()).toBe('a')\n\n    it \"should return a good default name if a content type is populated\", ->\n      f = new File(filename: '', contentType: 'image/jpg')\n      expect(f.displayName()).toBe('Unnamed Image.jpg')\n      f = new File(filename: null, contentType: 'image/jpg')\n      expect(f.displayName()).toBe('Unnamed Image.jpg')\n      f = new File(filename: null, contentType: 'text/calendar')\n      expect(f.displayName()).toBe('Event.ics')\n\n    it \"should return Unnamed Attachment otherwise\", ->\n      f = new File(filename: '', contentType: null)\n      expect(f.displayName()).toBe('Unnamed Attachment')\n      f = new File(filename: null, contentType: '')\n      expect(f.displayName()).toBe('Unnamed Attachment')\n      f = new File(filename: null, contentType: null)\n      expect(f.displayName()).toBe('Unnamed Attachment')\n\n  describe \"displayExtension\", ->\n    it \"should return an extension based on the filename when populated\", ->\n      f = new File(filename: 'Hello world.jpg', contentType: 'image/jpg')\n      expect(f.displayExtension()).toBe('jpg')\n      f = new File(filename: 'a', contentType: 'image/jpg')\n      expect(f.displayExtension()).toBe('')\n\n    it \"should ignore the case of the extension i nthe filename\", ->\n      f = new File(filename: 'Hello world.JPG', contentType: 'image/jpg')\n      expect(f.displayExtension()).toBe('jpg')\n      f = new File(filename: 'Hello world.Jpg', contentType: 'image/jpg')\n      expect(f.displayExtension()).toBe('jpg')\n      f = new File(filename: 'Hello world.jpg', contentType: 'image/jpg')\n      expect(f.displayExtension()).toBe('jpg')\n\n    it \"should return an extension based on the default filename otherwise\", ->\n      f = new File(filename: '', contentType: 'image/jpg')\n      expect(f.displayExtension()).toBe('jpg')\n      f = new File(filename: null, contentType: 'text/calendar')\n      expect(f.displayExtension()).toBe('ics')\n"
  },
  {
    "path": "packages/client-app/spec/models/message-spec.coffee",
    "content": "Utils = require \"../../src/flux/models/utils\"\nMessage = require(\"../../src/flux/models/message\").default\nContact = require(\"../../src/flux/models/contact\").default\n\nevan = new Contact\n  name: \"Evan Morikawa\"\n  email: \"evan@nylas.com\"\nben = new Contact\n  name: \"Ben Gotow\"\n  email: \"ben@nylas.com\"\nteam = new Contact\n  name: \"Nylas Team\"\n  email: \"team@nylas.com\"\nedgehill = new Contact\n  name: \"Edgehill\"\n  email: \"edgehill@nylas.com\"\nnoEmail = new Contact\n  name: \"Edgehill\"\n  email: null\nme = new Contact\n  name: TEST_ACCOUNT_NAME\n  email: TEST_ACCOUNT_EMAIL\nalmost_me = new Contact\n  name: TEST_ACCOUNT_NAME\n  email: \"tester+12345@nylas.com\"\n\ndescribe \"Message\", ->\n\n  describe \"detecting empty bodies\", ->\n    cases = [\n      {\n        itMsg: \"has plain br's and a signature\"\n        body: \"\"\"\n        <div class=\"contenteditable no-open-link-events\" contenteditable=\"true\" spellcheck=\"false\"><br><br><signature>Sent from <a href=\"https://nylas.com/n1?ref=n1\">Nylas Mail</a>, the extensible, open source mail client.</signature></div>\n      \"\"\"\n        isEmpty: true\n      },\n      {\n        itMsg: \"is an empty string\"\n        body: \"\"\n        isEmpty: true\n      },\n      {\n        itMsg: \"has plain text\"\n        body: \"Hi\"\n        isEmpty: false\n      },\n      {\n        itMsg: \"is null\"\n        body: null\n        isEmpty: true\n      },\n      {\n        itMsg: \"has empty tags\"\n        body: \"\"\"\n        <div class=\"contenteditable no-open-link-events\" contenteditable=\"true\" spellcheck=\"false\"><br><div><p>  </p></div>\\n\\n\\n\\n<br><signature>Sent from <a href=\"https://nylas.com/n1?ref=n1\">Nylas Mail</a>, the extensible, open source mail client.</signature></div>\n      \"\"\"\n        isEmpty: true\n      },\n      {\n        itMsg: \"has nested characters\"\n        body: \"\"\"\n        <div class=\"contenteditable no-open-link-events\" contenteditable=\"true\" spellcheck=\"false\"><br><div><p> 1</p></div>\\n\\n\\n\\n<br><signature>Sent from <a href=\"https://nylas.com/n1?ref=n1\">Nylas Mail</a>, the extensible, open source mail client.</signature></div>\n      \"\"\"\n        isEmpty: false\n      },\n      {\n        itMsg: \"has just a signature\"\n        body: \"<signature>Yo</signature>\"\n        isEmpty: true\n      },\n      {\n        itMsg: \"has content after a signature\"\n        body: \"<signature>Yo</signature>Yo\"\n        isEmpty: false\n      },\n    ]\n    cases.forEach ({itMsg, body, isEmpty}) ->\n      it itMsg, ->\n        msg = new Message(body: body, pristine: false, draft: true)\n        expect(msg.hasEmptyBody()).toBe(isEmpty)\n\n  it \"correctly aggregates participants\", ->\n    m1 = new Message\n      to: []\n      cc: null\n      from: []\n    expect(m1.participants().length).toBe 0\n\n    m2 = new Message\n      to: [evan]\n      cc: []\n      bcc: []\n      from: [ben]\n    expect(m2.participants().length).toBe 2\n\n    m3 = new Message\n      to: [evan]\n      cc: [evan]\n      bcc: [evan]\n      from: [evan]\n    expect(m3.participants().length).toBe 1\n\n    m4 = new Message\n      to: [evan]\n      cc: [ben, team, noEmail]\n      bcc: [team]\n      from: [team]\n    # because contact 4 has no email\n    expect(m4.participants().length).toBe 3\n\n    m5 = new Message\n      to: [evan]\n      cc: []\n      bcc: [team]\n      from: [ben]\n    # because we exclude bccs\n    expect(m5.participants().length).toBe 2\n\n  describe \"participant replies\", ->\n    cases = [\n      # Basic cases\n      {\n        msg: new Message\n          from: [evan]\n          to: [me]\n          cc: []\n          bcc: []\n        expected:\n          to: [evan]\n          cc: []\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [me]\n          cc: [ben]\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [ben]\n          cc: [me]\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [me]\n          cc: [ben, team, evan]\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [ben, team]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [me, ben, evan, ben, ben, evan]\n          cc: []\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [me, ben]\n          cc: [team, edgehill]\n          bcc: [evan, me, ben]\n        expected:\n          to: [evan]\n          cc: [ben, team, edgehill]\n      }\n\n      # From me (replying to a message I just sent)\n      {\n        msg: new Message\n          from: [me]\n          to: [me]\n          cc: []\n          bcc: []\n        expected:\n          to: [me]\n          cc: []\n      }\n      {\n        msg: new Message\n          from: [me]\n          to: [ben]\n          cc: []\n          bcc: []\n        expected:\n          to: [ben]\n          cc: []\n      }\n      {\n        msg: new Message\n          from: [me]\n          to: [ben, team, ben]\n          cc: [edgehill]\n          bcc: []\n        expected:\n          to: [ben, team]\n          cc: [edgehill]\n      }\n      {\n        msg: new Message\n          from: [me]\n          to: [ben, team, ben]\n          cc: [edgehill]\n          bcc: []\n        expected:\n          to: [ben, team]\n          cc: [edgehill]\n      }\n      # From me in cases my similar alias is used\n      {\n        msg: new Message\n          from: [me]\n          to: [almost_me]\n          cc: [ben]\n          bcc: []\n        expected:\n          to: [almost_me]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [me]\n          to: [me, almost_me, me]\n          cc: [ben, almost_me, me, me, ben, ben]\n          bcc: []\n        expected:\n          to: [me]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [almost_me]\n          to: [me]\n          cc: [ben]\n          bcc: []\n        expected:\n          to: [me]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [almost_me]\n          to: [almost_me]\n          cc: [ben]\n          bcc: []\n        expected:\n          to: [almost_me]\n          cc: [ben]\n      }\n\n      # Cases when I'm on email lists\n      {\n        msg: new Message\n          from: [evan]\n          to: [team]\n          cc: []\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [team]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [team]\n          cc: [ben, edgehill]\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [team, ben, edgehill]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [team]\n          cc: [me]\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [team]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [team, me]\n          cc: [ben]\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [team, ben]\n      }\n\n      # Cases when I'm bcc'd\n      {\n        msg: new Message\n          from: [evan]\n          to: []\n          cc: []\n          bcc: [me]\n        expected:\n          to: [evan]\n          cc: []\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [ben]\n          cc: []\n          bcc: [me]\n        expected:\n          to: [evan]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [ben]\n          cc: [team, edgehill]\n          bcc: [me]\n        expected:\n          to: [evan]\n          cc: [ben, team, edgehill]\n      }\n\n      # Cases my similar alias is used\n      {\n        msg: new Message\n          from: [evan]\n          to: [almost_me]\n          cc: []\n          bcc: []\n        expected:\n          to: [evan]\n          cc: []\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [ben]\n          cc: [almost_me]\n          bcc: []\n        expected:\n          to: [evan]\n          cc: [ben]\n      }\n      {\n        msg: new Message\n          from: [evan]\n          to: [ben]\n          cc: []\n          bcc: [almost_me]\n        expected:\n          to: [evan]\n          cc: [ben]\n      }\n    ]\n\n    itString = (prefix, msg) ->\n      return \"#{prefix} from: #{msg.from.map( (c) -> c.email).join(', ')} | to: #{msg.to.map( (c) -> c.email).join(', ')} | cc: #{msg.cc.map( (c) -> c.email).join(', ')} | bcc: #{msg.bcc.map( (c) -> c.email).join(', ')}\"\n\n    it \"thinks me and almost_me are equivalent\", ->\n      expect(Utils.emailIsEquivalent(me.email, almost_me.email)).toBe true\n      expect(Utils.emailIsEquivalent(ben.email, me.email)).toBe false\n\n    cases.forEach ({msg, expected}) ->\n      it itString(\"Reply All:\", msg), ->\n        expect(msg.participantsForReplyAll()).toEqual expected\n\n      it itString(\"Reply:\", msg), ->\n        {to, cc} = msg.participantsForReply()\n        expect(to).toEqual expected.to\n        expect(cc).toEqual []\n\n  describe \"participantsForReplyAll\", ->\n"
  },
  {
    "path": "packages/client-app/spec/models/model-spec.es6",
    "content": "/* eslint quote-props: 0 */\nimport _ from 'underscore';\n\nimport Model from '../../src/flux/models/model';\nimport Utils from '../../src/flux/models/utils';\nimport Attributes from '../../src/flux/attributes';\n\ndescribe(\"Model\", function modelSpecs() {\n  describe(\"constructor\", () => {\n    it(\"should accept a hash of attributes and assign them to the new Model\", () => {\n      const attrs = {\n        id: \"A\",\n        accountId: \"B\",\n      };\n      const m = new Model(attrs);\n      expect(m.id).toBe(attrs.id);\n      return expect(m.accountId).toBe(attrs.accountId);\n    });\n\n    it(\"by default assigns things passed into the id constructor to the serverId\", () => {\n      const attrs = {id: \"A\"};\n      const m = new Model(attrs);\n      return expect(m.serverId).toBe(attrs.id);\n    });\n\n    it(\"by default assigns values passed into the id constructor that look like localIds to be a localID\", () => {\n      const attrs = {id: \"A\"};\n      const m = new Model(attrs);\n      return expect(m.serverId).toBe(attrs.id);\n    });\n\n    it(\"assigns serverIds and clientIds\", () => {\n      const attrs = {\n        clientId: \"local-A\",\n        serverId: \"A\",\n      };\n      const m = new Model(attrs);\n      expect(m.serverId).toBe(attrs.serverId);\n      expect(m.clientId).toBe(attrs.clientId);\n      return expect(m.id).toBe(attrs.serverId);\n    });\n\n    it(\"throws an error if you attempt to manually assign the id\", () => {\n      const m = new Model({id: \"foo\"});\n      return expect(() => { m.id = \"bar\" }).toThrow();\n    });\n\n    return it(\"automatically assigns a clientId (and id) to the model if no id is provided\", () => {\n      const m = new Model();\n      expect(Utils.isTempId(m.id)).toBe(true);\n      expect(Utils.isTempId(m.clientId)).toBe(true);\n      return expect(m.serverId).toBeUndefined();\n    });\n  });\n\n  describe(\"attributes\", () =>\n    it(\"should return the attributes of the class EXCEPT the id field\", () => {\n      const m = new Model();\n      const retAttrs = _.clone(m.constructor.attributes);\n      delete retAttrs.id;\n      return expect(m.attributes()).toEqual(retAttrs);\n    })\n\n  );\n\n  describe(\"clone\", () =>\n    it(\"should return a deep copy of the object\", () => {\n      class SubSubmodel extends Model {\n        static attributes = Object.assign({}, Model.attributes, {\n          'value': Attributes.Number({\n            modelKey: 'value',\n            jsonKey: 'value',\n          }),\n        });\n      }\n\n      class Submodel extends Model {\n        static attributes = Object.assign({}, Model.attributes, {\n          'testNumber': Attributes.Number({\n            modelKey: 'testNumber',\n            jsonKey: 'test_number',\n          }),\n          'testArray': Attributes.Collection({\n            itemClass: SubSubmodel,\n            modelKey: 'testArray',\n            jsonKey: 'test_array',\n          }),\n        });\n      }\n\n      const old = new Submodel({testNumber: 4, testArray: [new SubSubmodel({value: 2}), new SubSubmodel({value: 6})]});\n      const clone = old.clone();\n\n      // Check entire trees are equivalent\n      expect(old.toJSON()).toEqual(clone.toJSON());\n      // Check object identity has changed\n      expect(old.constructor.name).toEqual(clone.constructor.name);\n      expect(old.testArray).not.toBe(clone.testArray);\n      // Check classes\n      expect(old.testArray[0]).not.toBe(clone.testArray[0]);\n      return expect(old.testArray[0].constructor.name).toEqual(clone.testArray[0].constructor.name);\n    })\n\n  );\n\n  describe(\"fromJSON\", () => {\n    beforeEach(() => {\n      class SubmodelItem extends Model {}\n\n      class Submodel extends Model {\n        static attributes = Object.assign({}, Model.attributes, {\n          'testNumber': Attributes.Number({\n            modelKey: 'testNumber',\n            jsonKey: 'test_number',\n          }),\n          'testBoolean': Attributes.Boolean({\n            modelKey: 'testBoolean',\n            jsonKey: 'test_boolean',\n          }),\n          'testCollection': Attributes.Collection({\n            modelKey: 'testCollection',\n            jsonKey: 'test_collection',\n            itemClass: SubmodelItem,\n          }),\n          'testJoinedData': Attributes.JoinedData({\n            modelKey: 'testJoinedData',\n            jsonKey: 'test_joined_data',\n          }),\n        });\n      }\n\n      this.json = {\n        'id': '1234',\n        'test_number': 4,\n        'test_boolean': true,\n        'daysOld': 4,\n        'account_id': 'bla',\n      };\n      this.m = new Submodel();\n    });\n\n    it(\"should assign attribute values by calling through to attribute fromJSON functions\", () => {\n      spyOn(Model.attributes.accountId, 'fromJSON').andCallFake(() => 'inflated value!');\n      this.m.fromJSON(this.json);\n      expect(Model.attributes.accountId.fromJSON.callCount).toBe(1);\n      return expect(this.m.accountId).toBe('inflated value!');\n    });\n\n    it(\"should not touch attributes that are missing in the json\", () => {\n      this.m.fromJSON(this.json);\n      expect(this.m.object).toBe(undefined);\n\n      this.m.object = 'abc';\n      this.m.fromJSON(this.json);\n      return expect(this.m.object).toBe('abc');\n    });\n\n    it(\"should not do anything with extra JSON keys\", () => {\n      this.m.fromJSON(this.json);\n      return expect(this.m.daysOld).toBe(undefined);\n    });\n\n    it(\"should maintain empty string as empty strings\", () => {\n      expect(this.m.accountId).toBe(undefined);\n      this.m.fromJSON({account_id: ''});\n      return expect(this.m.accountId).toBe('');\n    });\n\n    describe(\"Attributes.Number\", () =>\n      it(\"should read number attributes and coerce them to numeric values\", () => {\n        this.m.fromJSON({'test_number': 4});\n        expect(this.m.testNumber).toBe(4);\n\n        this.m.fromJSON({'test_number': '4'});\n        expect(this.m.testNumber).toBe(4);\n\n        this.m.fromJSON({'test_number': 'lolz'});\n        expect(this.m.testNumber).toBe(null);\n\n        this.m.fromJSON({'test_number': 0});\n        return expect(this.m.testNumber).toBe(0);\n      })\n\n    );\n\n    describe(\"Attributes.JoinedData\", () =>\n      it(\"should read joined data attributes and coerce them to string values\", () => {\n        this.m.fromJSON({'test_joined_data': null});\n        expect(this.m.testJoinedData).toBe(null);\n\n        this.m.fromJSON({'test_joined_data': ''});\n        expect(this.m.testJoinedData).toBe('');\n\n        this.m.fromJSON({'test_joined_data': 'lolz'});\n        return expect(this.m.testJoinedData).toBe('lolz');\n      })\n\n    );\n\n    describe(\"Attributes.Collection\", () => {\n      it(\"should parse and inflate items\", () => {\n        this.m.fromJSON({'test_collection': [{id: '123'}]});\n        expect(this.m.testCollection.length).toBe(1);\n        expect(this.m.testCollection[0].id).toBe('123');\n        return expect(this.m.testCollection[0].constructor.name).toBe('SubmodelItem');\n      });\n\n      return it(\"should be fine with malformed arrays\", () => {\n        this.m.fromJSON({'test_collection': [null]});\n        expect(this.m.testCollection.length).toBe(0);\n        this.m.fromJSON({'test_collection': []});\n        expect(this.m.testCollection.length).toBe(0);\n        this.m.fromJSON({'test_collection': null});\n        return expect(this.m.testCollection.length).toBe(0);\n      });\n    });\n\n    return describe(\"Attributes.Boolean\", () =>\n      it(\"should read `true` or true and coerce everything else to false\", () => {\n        this.m.fromJSON({'test_boolean': true});\n        expect(this.m.testBoolean).toBe(true);\n\n        this.m.fromJSON({'test_boolean': 'true'});\n        expect(this.m.testBoolean).toBe(true);\n\n        this.m.fromJSON({'test_boolean': 4});\n        expect(this.m.testBoolean).toBe(false);\n\n        this.m.fromJSON({'test_boolean': '4'});\n        expect(this.m.testBoolean).toBe(false);\n\n        this.m.fromJSON({'test_boolean': false});\n        expect(this.m.testBoolean).toBe(false);\n\n        this.m.fromJSON({'test_boolean': 0});\n        expect(this.m.testBoolean).toBe(false);\n\n        this.m.fromJSON({'test_boolean': null});\n        return expect(this.m.testBoolean).toBe(false);\n      })\n\n    );\n  });\n\n  describe(\"toJSON\", () => {\n    beforeEach(() => {\n      this.model = new Model({\n        id: \"1234\",\n        accountId: \"ACD\",\n      });\n      return;\n    });\n\n    it(\"should return a JSON object and call attribute toJSON functions to map values\", () => {\n      spyOn(Model.attributes.accountId, 'toJSON').andCallFake(() => 'inflated value!');\n\n      const json = this.model.toJSON();\n      expect(json instanceof Object).toBe(true);\n      expect(json.id).toBe('1234');\n      return expect(json.account_id).toBe('inflated value!');\n    });\n\n    return it(\"should surface any exception one of the attribute toJSON functions raises\", () => {\n      spyOn(Model.attributes.accountId, 'toJSON').andCallFake(() => {\n        throw new Error(\"Can't convert value into JSON format\");\n      });\n      return expect(() => { return this.model.toJSON(); }).toThrow();\n    });\n  });\n\n  return describe(\"matches\", () => {\n    beforeEach(() => {\n      this.model = new Model({\n        id: \"1234\",\n        accountId: \"ACD\",\n      });\n\n      this.truthyMatcher = {evaluate() { return true; }};\n      this.falsyMatcher = {evaluate() { return false; }};\n    });\n\n    it(\"should run the matchers and return true iff all matchers pass\", () => {\n      expect(this.model.matches([this.truthyMatcher, this.truthyMatcher])).toBe(true);\n      expect(this.model.matches([this.truthyMatcher, this.falsyMatcher])).toBe(false);\n      return expect(this.model.matches([this.falsyMatcher, this.truthyMatcher])).toBe(false);\n    });\n\n    return it(\"should pass itself as an argument to the matchers\", () => {\n      spyOn(this.truthyMatcher, 'evaluate').andCallFake(param => {\n        return expect(param).toBe(this.model);\n      });\n      return this.model.matches([this.truthyMatcher]);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/models/model-with-metadata-spec.es6",
    "content": "import ModelWithMetadata from '../../src/flux/models/model-with-metadata'\n\nclass TestModel extends ModelWithMetadata {\n\n}\n\ndescribe(\"ModelWithMetadata\", function modelWithMetadata() {\n  it(\"should initialize pluginMetadata to an empty array\", () => {\n    const model = new TestModel();\n    expect(model.pluginMetadata).toEqual([]);\n  });\n\n  describe(\"metadataForPluginId\", () => {\n    beforeEach(() => {\n      this.model = new TestModel();\n      this.model.applyPluginMetadata('plugin-id-a', {a: true});\n      this.model.applyPluginMetadata('plugin-id-b', {b: false});\n    })\n    it(\"returns the metadata value for the provided pluginId\", () => {\n      expect(this.model.metadataForPluginId('plugin-id-b')).toEqual({b: false});\n    });\n    it(\"returns null if no value is found\", () => {\n      expect(this.model.metadataForPluginId('plugin-id-c')).toEqual(null);\n    });\n  });\n\n  describe(\"metadataObjectForPluginId\", () => {\n    it(\"returns the metadata object for the provided pluginId\", () => {\n      const model = new TestModel();\n      model.applyPluginMetadata('plugin-id-a', {a: true});\n      model.applyPluginMetadata('plugin-id-b', {b: false});\n      expect(model.metadataObjectForPluginId('plugin-id-a')).toEqual(model.pluginMetadata[0]);\n      expect(model.metadataObjectForPluginId('plugin-id-b')).toEqual(model.pluginMetadata[1]);\n      expect(model.metadataObjectForPluginId('plugin-id-c')).toEqual(undefined);\n    });\n  });\n\n  describe(\"applyPluginMetadata\", () => {\n    it(\"creates or updates the appropriate metadata object\", () => {\n      const model = new TestModel();\n      expect(model.pluginMetadata.length).toEqual(0);\n\n      // create new metadata object with correct value\n      model.applyPluginMetadata('plugin-id-a', {a: true});\n      const obj = model.metadataObjectForPluginId('plugin-id-a');\n      expect(model.pluginMetadata.length).toEqual(1);\n      expect(obj.pluginId).toBe('plugin-id-a');\n      expect(obj.id).toBe('plugin-id-a');\n      expect(obj.version).toBe(0);\n      expect(obj.value.a).toBe(true);\n\n      // update existing metadata object\n      model.applyPluginMetadata('plugin-id-a', {a: false});\n      expect(obj.value.a).toBe(false);\n    });\n  });\n\n  describe(\"clonePluginMetadataFrom\", () => {\n    it(`applies the pluginMetadata from the other model, copying values \\\nbut resetting versions`, () => {\n      const model = new TestModel();\n      model.applyPluginMetadata('plugin-id-a', {a: true});\n      model.applyPluginMetadata('plugin-id-b', {b: false});\n      model.metadataObjectForPluginId('plugin-id-a').version = 2;\n      model.metadataObjectForPluginId('plugin-id-b').version = 3;\n\n      const created = new TestModel();\n      created.clonePluginMetadataFrom(model);\n      const aMetadatum = created.metadataObjectForPluginId('plugin-id-a');\n      const bMetadatum = created.metadataObjectForPluginId('plugin-id-b');\n      expect(aMetadatum.version).toEqual(0);\n      expect(aMetadatum.value).toEqual({a: true});\n      expect(bMetadatum.version).toEqual(0);\n      expect(bMetadatum.value).toEqual({b: false});\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/models/mutable-query-result-set-spec.es6",
    "content": "/* eslint quote-props: 0 */\nimport MutableQueryResultSet from '../../src/flux/models/mutable-query-result-set';\nimport QueryRange from '../../src/flux/models/query-range';\n\ndescribe(\"MutableQueryResultSet\", function MutableQueryResultSetSpecs() {\n  describe(\"clipToRange\", () => {\n    it(\"should do nothing if the clipping range is infinite\", () => {\n      const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});\n      const beforeRange = set.range();\n      set.clipToRange(QueryRange.infinite());\n      const afterRange = set.range();\n\n      expect(beforeRange.isEqual(afterRange)).toBe(true);\n    });\n\n    it(\"should correctly trim the result set 5-10 to the clipping range 2-9\", () => {\n      const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});\n      expect(set.range().isEqual(new QueryRange({offset: 5, limit: 5}))).toBe(true);\n      set.clipToRange(new QueryRange({offset: 2, limit: 7}));\n      expect(set.range().isEqual(new QueryRange({offset: 5, limit: 4}))).toBe(true);\n      expect(set.ids()).toEqual(['A', 'B', 'C', 'D']);\n    });\n\n    it(\"should correctly trim the result set 5-10 to the clipping range 5-10\", () => {\n      const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});\n      set.clipToRange(new QueryRange({start: 5, end: 10}));\n      expect(set.range().isEqual(new QueryRange({start: 5, end: 10}))).toBe(true);\n      expect(set.ids()).toEqual(['A', 'B', 'C', 'D', 'E']);\n    });\n\n    it(\"should correctly trim the result set 5-10 to the clipping range 6\", () => {\n      const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});\n      set.clipToRange(new QueryRange({offset: 6, limit: 1}));\n      expect(set.range().isEqual(new QueryRange({offset: 6, limit: 1}))).toBe(true);\n      expect(set.ids()).toEqual(['B']);\n    });\n\n    it(\"should correctly trim the result set 5-10 to the clipping range 100-200\", () => {\n      const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});\n      set.clipToRange(new QueryRange({start: 100, end: 200}));\n      expect(set.range().isEqual(new QueryRange({start: 100, end: 100}))).toBe(true);\n      expect(set.ids()).toEqual([]);\n    });\n\n    it(\"should correctly trim the result set 5-10 to the clipping range 0-2\", () => {\n      const set = new MutableQueryResultSet({_ids: ['A', 'B', 'C', 'D', 'E'], _offset: 5});\n      set.clipToRange(new QueryRange({offset: 0, limit: 2}));\n      expect(set.range().isEqual(new QueryRange({offset: 5, limit: 0}))).toBe(true);\n      expect(set.ids()).toEqual([]);\n    });\n\n    it(\"should trim the models cache to remove models no longer needed\", () => {\n      const set = new MutableQueryResultSet({\n        _ids: ['A', 'B', 'C', 'D', 'E'],\n        _offset: 5,\n        _modelsHash: {\n          'A-local': {id: 'A', clientId: 'A-local'},\n          'A': {id: 'A', clientId: 'A-local'},\n          'B-local': {id: 'B', clientId: 'B-local'},\n          'B': {id: 'B', clientId: 'B-local'},\n          'C-local': {id: 'C', clientId: 'C-local'},\n          'C': {id: 'C', clientId: 'C-local'},\n          'D-local': {id: 'D', clientId: 'D-local'},\n          'D': {id: 'D', clientId: 'D-local'},\n          'E-local': {id: 'E', clientId: 'E-local'},\n          'E': {id: 'E', clientId: 'E-local'},\n        }});\n\n      set.clipToRange(new QueryRange({start: 5, end: 8}));\n      expect(set._modelsHash).toEqual({\n        'A-local': {id: 'A', clientId: 'A-local'},\n        'A': {id: 'A', clientId: 'A-local'},\n        'B-local': {id: 'B', clientId: 'B-local'},\n        'B': {id: 'B', clientId: 'B-local'},\n        'C-local': {id: 'C', clientId: 'C-local'},\n        'C': {id: 'C', clientId: 'C-local'},\n      });\n    });\n  });\n\n  describe(\"addIdsInRange\", () => {\n    describe(\"when the set is currently empty\", () =>\n      it(\"should set the result set to the provided one\", () => {\n        this.set = new MutableQueryResultSet();\n        this.set.addIdsInRange(['B', 'C', 'D'], new QueryRange({start: 1, end: 4}));\n        expect(this.set.ids()).toEqual(['B', 'C', 'D']);\n        expect(this.set.range().isEqual(new QueryRange({start: 1, end: 4}))).toBe(true);\n      })\n\n    );\n\n    describe(\"when the set has existing values\", () => {\n      beforeEach(() => {\n        this.set = new MutableQueryResultSet({\n          _ids: ['A', 'B', 'C', 'D', 'E'],\n          _offset: 5,\n          _modelsHash: {'A': {id: 'A'}, 'B': {id: 'B'}, 'C': {id: 'C'}, 'D': {id: 'D'}, 'E': {id: 'E'}},\n        });\n      });\n\n      it(\"should throw an exception if the range provided doesn't intersect (trailing)\", () => {\n        expect(() => {\n          this.set.addIdsInRange(['G', 'H', 'I'], new QueryRange({offset: 11, limit: 3}));\n        }).toThrow();\n\n        expect(() => {\n          this.set.addIdsInRange(['F', 'G', 'H'], new QueryRange({offset: 10, limit: 3}));\n        }).not.toThrow();\n      });\n\n      it(\"should throw an exception if the range provided doesn't intersect (leading)\", () => {\n        expect(() => {\n          this.set.addIdsInRange(['0', '1', '2'], new QueryRange({offset: 1, limit: 3}));\n        }).toThrow();\n\n        expect(() => {\n          this.set.addIdsInRange(['0', '1', '2'], new QueryRange({offset: 2, limit: 3}));\n        }).not.toThrow();\n      });\n\n      it(\"should work if the IDs array is shorter than the result range they represent (addition)\", () => {\n        this.set.addIdsInRange(['F', 'G', 'H'], new QueryRange({offset: 10, limit: 5}));\n        expect(this.set.ids()).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']);\n      });\n\n      it(\"should work if the IDs array is shorter than the result range they represent (replacement)\", () => {\n        this.set.addIdsInRange(['A', 'B', 'C'], new QueryRange({offset: 5, limit: 5}));\n        expect(this.set.ids()).toEqual(['A', 'B', 'C']);\n      });\n\n      it(\"should correctly add ids (trailing) and update the offset\", () => {\n        this.set.addIdsInRange(['F', 'G', 'H'], new QueryRange({offset: 10, limit: 3}));\n        expect(this.set.ids()).toEqual(['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H']);\n        expect(this.set.range().offset).toEqual(5);\n      });\n\n      it(\"should correctly add ids (leading) and update the offset\", () => {\n        this.set.addIdsInRange(['0', '1', '2'], new QueryRange({offset: 2, limit: 3}));\n        expect(this.set.ids()).toEqual(['0', '1', '2', 'A', 'B', 'C', 'D', 'E']);\n        expect(this.set.range().offset).toEqual(2);\n      });\n\n      it(\"should correctly add ids (middle) and update the offset\", () => {\n        this.set.addIdsInRange(['B-new', 'C-new', 'D-new'], new QueryRange({offset: 6, limit: 3}));\n        expect(this.set.ids()).toEqual(['A', 'B-new', 'C-new', 'D-new', 'E']);\n        expect(this.set.range().offset).toEqual(5);\n      });\n\n      it(\"should correctly add ids (middle+trailing) and update the offset\", () => {\n        this.set.addIdsInRange(['D-new', 'E-new', 'F-new'], new QueryRange({offset: 8, limit: 3}));\n        expect(this.set.ids()).toEqual(['A', 'B', 'C', 'D-new', 'E-new', 'F-new']);\n        expect(this.set.range().offset).toEqual(5);\n      });\n    });\n  });\n\n  describe('updateModel', () => {\n    beforeEach(() => {\n      this.mockModel = (clientId, serverId) => {\n        return {\n          id: serverId || clientId,\n          clientId: clientId,\n          serverId: serverId,\n          constructor: {\n            attributes: [],\n          },\n        }\n      }\n    })\n\n    /*\n      Previously, after creating a new folder, the UI would indicate that the new\n      folder had children, even though it didn't. This was caused by duplicate models\n      in our MutableQueryResultSet for the user's categories. Basically, we would\n      sync the server version of the folder before the SyncbackTask for the new\n      folder returned its serverId. Without the serverId, the synced version of\n      the folder couldn't yet be tied to the optimistic folder, so a second row was\n      created in the database. This second row is removed when the syncbackTask\n      does return the serverId, because we persist the optimistic folder with a\n      'REPLACE INTO' query. (This deletes other rows with the same id.) However,\n      since this was done inside a 'persist' change with the serverId and no\n      'unpersist' was ever recorded for the clientId, our MutableQueryResultSet\n      never removed the clientId model. We now detect this case within\n      updateModel and remove the clientId when necessary.\n    */\n    describe('when the model is an optimistic copy receiving its serverId', () => {\n      it('removes the clientId if both the clientId and serverId are listed', () => {\n        const set = new MutableQueryResultSet({\n          _ids: ['clientId1', 'serverId1', 'serverId2', 'serverId3'],\n          _modelsHash: {\n            'clientId1': this.mockModel('clientId1'),\n            'serverId2': this.mockModel('clientId2', 'serverId2'),\n            'serverId3': this.mockModel('clientId3', 'serverId3'),\n            'serverId1': this.mockModel('clientId4', 'serverId1'),\n          },\n        });\n\n        set.updateModel(this.mockModel('clientId1', 'serverId1'))\n        expect(set.ids().includes('clientId1')).toEqual(false)\n      })\n\n      it('does not remove the clientId if the serverId is not listed', () => {\n        const set = new MutableQueryResultSet({\n          _ids: ['clientId1', 'serverId2', 'serverId3'],\n          _modelsHash: {\n            'clientId1': this.mockModel('clientId1'),\n            'serverId2': this.mockModel('clientId2', 'serverId2'),\n            'serverId3': this.mockModel('clientId3', 'serverId3'),\n          },\n        });\n\n        set.updateModel(this.mockModel('clientId1', 'serverId1'))\n        expect(set.ids().includes('clientId1')).toEqual(true)\n      })\n    })\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/models/query-range-spec.es6",
    "content": "import QueryRange from '../../src/flux/models/query-range';\n\ndescribe(\"QueryRange\", function QueryRangeSpecs() {\n  describe(\"@infinite\", () =>\n    it(\"should return a query range with a null limit and offset\", () => {\n      const infinite = QueryRange.infinite();\n      expect(infinite.limit).toBe(null);\n      expect(infinite.offset).toBe(null);\n    })\n\n  );\n\n  describe(\"@rangesBySubtracting\", () => {\n    it(\"should throw an exception if either range is infinite\", () => {\n      const infinite = QueryRange.infinite();\n\n      expect(() =>\n        QueryRange.rangesBySubtracting(infinite, new QueryRange({offset: 0, limit: 10}))\n      ).toThrow();\n\n      expect(() =>\n        QueryRange.rangesBySubtracting(new QueryRange({offset: 0, limit: 10}), infinite)\n      ).toThrow();\n    });\n\n    it(\"should return one or more ranges created by punching the provided range\", () => {\n      const test = ({a, b, result}) => expect(QueryRange.rangesBySubtracting(a, b)).toEqual(result);\n      test({\n        a: new QueryRange({offset: 0, limit: 10}),\n        b: new QueryRange({offset: 3, limit: 3}),\n        result: [new QueryRange({offset: 0, limit: 3}), new QueryRange({offset: 6, limit: 4})]});\n\n      test({\n        a: new QueryRange({offset: 0, limit: 10}),\n        b: new QueryRange({offset: 3, limit: 10}),\n        result: [new QueryRange({offset: 0, limit: 3})]});\n\n      test({\n        a: new QueryRange({offset: 0, limit: 10}),\n        b: new QueryRange({offset: 0, limit: 10}),\n        result: []});\n\n      test({\n        a: new QueryRange({offset: 5, limit: 10}),\n        b: new QueryRange({offset: 0, limit: 4}),\n        result: [new QueryRange({offset: 5, limit: 10})]});\n\n      test({\n        a: new QueryRange({offset: 5, limit: 10}),\n        b: new QueryRange({offset: 0, limit: 8}),\n        result: [new QueryRange({offset: 8, limit: 7})]});\n    });\n  });\n\n  describe(\"isInfinite\", () =>\n    it(\"should return true for an infinite range, false otherwise\", () => {\n      const infinite = QueryRange.infinite();\n      expect(infinite.isInfinite()).toBe(true);\n      expect(new QueryRange({offset: 0, limit: 4}).isInfinite()).toBe(false);\n    })\n  );\n\n  describe(\"start\", () =>\n    it(\"should be an alias for offset\", () =>\n      expect((new QueryRange({offset: 3, limit: 4})).start).toBe(3)\n    )\n  );\n\n  describe(\"end\", () =>\n    it(\"should be offset + limit\", () =>\n      expect((new QueryRange({offset: 3, limit: 4})).end).toBe(7)\n    )\n  );\n\n  describe(\"isContiguousWith\", () => {\n    it(\"should return true if either range is infinite\", () => {\n      const a = new QueryRange({offset: 3, limit: 4});\n      expect(a.isContiguousWith(QueryRange.infinite())).toBe(true);\n      expect(QueryRange.infinite().isContiguousWith(a)).toBe(true);\n    });\n\n    it(\"should return true if the ranges intersect or touch, false otherwise\", () => {\n      const a = new QueryRange({offset: 3, limit: 4});\n      const b = new QueryRange({offset: 0, limit: 2});\n      const c = new QueryRange({offset: 0, limit: 3});\n      const d = new QueryRange({offset: 7, limit: 10});\n      const e = new QueryRange({offset: 8, limit: 10});\n\n      // True\n\n      expect(a.isContiguousWith(d)).toBe(true);\n      expect(d.isContiguousWith(a)).toBe(true);\n\n      expect(a.isContiguousWith(c)).toBe(true);\n      expect(c.isContiguousWith(a)).toBe(true);\n\n      // False\n\n      expect(a.isContiguousWith(b)).toBe(false);\n      expect(b.isContiguousWith(a)).toBe(false);\n\n      expect(a.isContiguousWith(e)).toBe(false);\n      expect(e.isContiguousWith(a)).toBe(false);\n\n      expect(b.isContiguousWith(e)).toBe(false);\n      expect(e.isContiguousWith(b)).toBe(false);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/models/query-spec.es6",
    "content": "/* eslint quote-props: 0 */\nimport ModelQuery from '../../src/flux/models/query';\nimport Attributes from '../../src/flux/attributes';\nimport Message from '../../src/flux/models/message';\nimport Thread from '../../src/flux/models/thread';\nimport Account from '../../src/flux/models/account';\n\ndescribe(\"ModelQuery\", function ModelQuerySpecs() {\n  beforeEach(() => {\n    this.db = {};\n  });\n\n  describe(\"where\", () => {\n    beforeEach(() => {\n      this.q = new ModelQuery(Thread, this.db);\n      this.m1 = Thread.attributes.id.equal(4);\n      this.m2 = Thread.attributes.categories.contains('category-id');\n    });\n\n    it(\"should accept an array of Matcher objects\", () => {\n      this.q.where([this.m1, this.m2]);\n      expect(this.q._matchers.length).toBe(2);\n      expect(this.q._matchers[0]).toBe(this.m1);\n      expect(this.q._matchers[1]).toBe(this.m2);\n    });\n\n    it(\"should accept a single Matcher object\", () => {\n      this.q.where(this.m1);\n      expect(this.q._matchers.length).toBe(1);\n      expect(this.q._matchers[0]).toBe(this.m1);\n    });\n\n    it(\"should append to any existing where clauses\", () => {\n      this.q.where(this.m1);\n      this.q.where(this.m2);\n      expect(this.q._matchers.length).toBe(2);\n      expect(this.q._matchers[0]).toBe(this.m1);\n      expect(this.q._matchers[1]).toBe(this.m2);\n    });\n\n    it(\"should accept a shorthand format\", () => {\n      this.q.where({id: 4, lastMessageReceivedTimestamp: 1234});\n      expect(this.q._matchers.length).toBe(2);\n      expect(this.q._matchers[0].attr.modelKey).toBe('id');\n      expect(this.q._matchers[0].comparator).toBe('=');\n      expect(this.q._matchers[0].val).toBe(4);\n    });\n\n    it(\"should return the query so it can be chained\", () => {\n      expect(this.q.where({id: 4})).toBe(this.q);\n    });\n\n    it(\"should immediately raise an exception if an un-queryable attribute is specified\", () =>\n      expect(() => {\n        this.q.where({snippet: 'My Snippet'});\n      }).toThrow()\n    );\n\n    it(\"should immediately raise an exception if a non-existent attribute is specified\", () =>\n      expect(() => {\n        this.q.where({looksLikeADuck: 'of course'});\n      }).toThrow()\n    );\n  });\n\n  describe(\"order\", () => {\n    beforeEach(() => {\n      this.q = new ModelQuery(Thread, this.db);\n      this.o1 = Thread.attributes.lastMessageReceivedTimestamp.descending();\n      this.o2 = Thread.attributes.subject.descending();\n    });\n\n    it(\"should accept an array of SortOrders\", () => {\n      this.q.order([this.o1, this.o2]);\n      expect(this.q._orders.length).toBe(2);\n    });\n\n    it(\"should accept a single SortOrder object\", () => {\n      this.q.order(this.o2);\n      expect(this.q._orders.length).toBe(1);\n    });\n\n    it(\"should extend any existing ordering\", () => {\n      this.q.order(this.o1);\n      this.q.order(this.o2);\n      expect(this.q._orders.length).toBe(2);\n      expect(this.q._orders[0]).toBe(this.o1);\n      expect(this.q._orders[1]).toBe(this.o2);\n    });\n\n    it(\"should return the query so it can be chained\", () => {\n      expect(this.q.order(this.o2)).toBe(this.q);\n    });\n  });\n\n  describe(\"include\", () => {\n    beforeEach(() => {\n      this.q = new ModelQuery(Message, this.db);\n    });\n\n    it(\"should throw an exception if the attribute is not a joined data attribute\", () =>\n      expect(() => {\n        this.q.include(Message.attributes.unread);\n      }).toThrow()\n\n    );\n\n    it(\"should add the provided property to the list of joined properties\", () => {\n      expect(this.q._includeJoinedData).toEqual([]);\n      this.q.include(Message.attributes.body);\n      expect(this.q._includeJoinedData).toEqual([Message.attributes.body]);\n    });\n  });\n\n  describe(\"includeAll\", () => {\n    beforeEach(() => {\n      this.q = new ModelQuery(Message, this.db);\n    });\n\n    it(\"should add all the JoinedData attributes of the class\", () => {\n      expect(this.q._includeJoinedData).toEqual([]);\n      this.q.includeAll();\n      expect(this.q._includeJoinedData).toEqual([Message.attributes.body]);\n    });\n  });\n\n  describe(\"response formatting\", () =>\n    it(\"should always return a Number for counts\", () => {\n      const q = new ModelQuery(Message, this.db);\n      q.where({accountId: 'abcd'}).count();\n\n      const raw = [{count: \"12\"}];\n      expect(q.formatResult(q.inflateResult(raw))).toBe(12);\n    })\n\n  );\n\n  describe(\"sql\", () => {\n    beforeEach(() => {\n      this.runScenario = (klass, scenario) => {\n        const q = new ModelQuery(klass, this.db);\n        Attributes.Matcher.muid = 1;\n        scenario.builder(q);\n        expect(q.sql().trim()).toBe(scenario.sql.trim());\n      };\n    });\n\n    it(\"should finalize the query so no further changes can be made\", () => {\n      const q = new ModelQuery(Account, this.db);\n      spyOn(q, 'finalize');\n      q.sql();\n      expect(q.finalize).toHaveBeenCalled();\n    });\n\n    it(\"should correctly generate queries with multiple where clauses\", () => {\n      this.runScenario(Account, {\n        builder: (q) =>\n          q.where({emailAddress: 'ben@nylas.com'}).where({id: 2}),\n        sql: \"SELECT `Account`.`data` FROM `Account`  \" +\n             \"WHERE `Account`.`email_address` = 'ben@nylas.com' AND `Account`.`id` = 2\",\n      });\n    });\n\n    it(\"should correctly escape single quotes with more double single quotes (LIKE)\", () => {\n      this.runScenario(Account, {\n        builder: (q) =>\n          q.where(Account.attributes.emailAddress.like(\"you're\")),\n        sql: \"SELECT `Account`.`data` FROM `Account`  WHERE `Account`.`email_address` like '%you''re%'\",\n      });\n    });\n\n    it(\"should correctly escape single quotes with more double single quotes (equal)\", () => {\n      this.runScenario(Account, {\n        builder: (q) =>\n          q.where(Account.attributes.emailAddress.equal(\"you're\")),\n        sql: \"SELECT `Account`.`data` FROM `Account`  WHERE `Account`.`email_address` = 'you''re'\",\n      });\n    });\n\n    it(\"should correctly generate COUNT queries\", () => {\n      this.runScenario(Thread, {\n        builder: (q) =>\n          q.where({accountId: 'abcd'}).count(),\n        sql: \"SELECT COUNT(*) as count FROM `Thread`  \" +\n             \"WHERE `Thread`.`account_id` = 'abcd'  \",\n      });\n    });\n\n    it(\"should correctly generate LIMIT 1 queries for single items\", () => {\n      this.runScenario(Thread, {\n        builder: (q) =>\n          q.where({accountId: 'abcd'}).one(),\n        sql: \"SELECT `Thread`.`data`, is_search_indexed  FROM `Thread`  \" +\n             \"WHERE `Thread`.`account_id` = 'abcd'  \" +\n             \"ORDER BY `Thread`.`last_message_received_timestamp` DESC LIMIT 1\",\n      });\n    });\n\n    it(\"should correctly generate `contains` queries using JOINS\", () => {\n      this.runScenario(Thread, {\n        builder: (q) =>\n          q.where(Thread.attributes.categories.contains('category-id')).where({id: '1234'}),\n        sql: \"SELECT `Thread`.`data`, is_search_indexed  FROM `Thread` \" +\n             \"INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` \" +\n             \"WHERE `M1`.`value` = 'category-id' AND `Thread`.`id` = '1234'  \" +\n             \"ORDER BY `Thread`.`last_message_received_timestamp` DESC\",\n      });\n\n      this.runScenario(Thread, {\n        builder: (q) =>\n          q.where([Thread.attributes.categories.contains('l-1'), Thread.attributes.categories.contains('l-2')]),\n        sql: \"SELECT `Thread`.`data`, is_search_indexed  FROM `Thread` \" +\n             \"INNER JOIN `ThreadCategory` AS `M1` ON `M1`.`id` = `Thread`.`id` \" +\n             \"INNER JOIN `ThreadCategory` AS `M2` ON `M2`.`id` = `Thread`.`id` \" +\n             \"WHERE `M1`.`value` = 'l-1' AND `M2`.`value` = 'l-2'  \" +\n             \"ORDER BY `Thread`.`last_message_received_timestamp` DESC\",\n      });\n    });\n\n    it(\"should correctly generate queries with the class's naturalSortOrder when one is available and no other orders are provided\", () => {\n      this.runScenario(Thread, {\n        builder: (q) =>\n          q.where({accountId: 'abcd'}),\n        sql: \"SELECT `Thread`.`data`, is_search_indexed  FROM `Thread`  \" +\n             \"WHERE `Thread`.`account_id` = 'abcd'  \" +\n             \"ORDER BY `Thread`.`last_message_received_timestamp` DESC\",\n      });\n\n      this.runScenario(Thread, {\n        builder: (q) =>\n          q.where({accountId: 'abcd'}).order(Thread.attributes.lastMessageReceivedTimestamp.ascending()),\n        sql: \"SELECT `Thread`.`data`, is_search_indexed  FROM `Thread`  \" +\n             \"WHERE `Thread`.`account_id` = 'abcd'  \" +\n             \"ORDER BY `Thread`.`last_message_received_timestamp` ASC\",\n      });\n\n      this.runScenario(Account, {\n        builder: (q) =>\n          q.where({id: 'abcd'}),\n        sql: \"SELECT `Account`.`data` FROM `Account`  \" +\n             \"WHERE `Account`.`id` = 'abcd'  \",\n      });\n    });\n\n    it(\"should correctly generate queries requesting joined data attributes\", () => {\n      this.runScenario(Message, {\n        builder: (q) =>\n          q.where({id: '1234'}).include(Message.attributes.body),\n        sql: \"SELECT `Message`.`data`, IFNULL(`MessageBody`.`value`, '!NULLVALUE!') AS `body`  \" +\n             \"FROM `Message` LEFT OUTER JOIN `MessageBody` ON `MessageBody`.`id` = `Message`.`id` \" +\n             \"WHERE `Message`.`id` = '1234'  ORDER BY `Message`.`date` ASC\",\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/models/query-subscription-pool-spec.es6",
    "content": "import QuerySubscriptionPool from '../../src/flux/models/query-subscription-pool';\nimport DatabaseStore from '../../src/flux/stores/database-store';\nimport Label from '../../src/flux/models/label';\n\ndescribe(\"QuerySubscriptionPool\", function QuerySubscriptionPoolSpecs() {\n  beforeEach(() => {\n    this.query = DatabaseStore.findAll(Label);\n    this.queryKey = this.query.sql();\n    QuerySubscriptionPool._subscriptions = {};\n    QuerySubscriptionPool._cleanupChecks = [];\n  });\n\n  describe(\"add\", () => {\n    it(\"should add a new subscription with the callback\", () => {\n      const callback = jasmine.createSpy('callback');\n      QuerySubscriptionPool.add(this.query, callback);\n      expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeDefined();\n\n      const subscription = QuerySubscriptionPool._subscriptions[this.queryKey];\n      expect(subscription.hasCallback(callback)).toBe(true);\n    });\n\n    it(\"should yield database changes to the subscription\", () => {\n      const callback = jasmine.createSpy('callback');\n      QuerySubscriptionPool.add(this.query, callback);\n      const subscription = QuerySubscriptionPool._subscriptions[this.queryKey];\n      spyOn(subscription, 'applyChangeRecord');\n\n      const record = {objectType: 'whateves'};\n      QuerySubscriptionPool._onChange(record);\n      expect(subscription.applyChangeRecord).toHaveBeenCalledWith(record);\n    });\n\n    describe(\"unsubscribe\", () => {\n      it(\"should return an unsubscribe method\", () => {\n        expect(QuerySubscriptionPool.add(this.query, () => {}) instanceof Function).toBe(true);\n      });\n\n      it(\"should remove the callback from the subscription\", () => {\n        const cb = () => {};\n\n        const unsub = QuerySubscriptionPool.add(this.query, cb);\n        const subscription = QuerySubscriptionPool._subscriptions[this.queryKey];\n\n        expect(subscription.hasCallback(cb)).toBe(true);\n        unsub();\n        expect(subscription.hasCallback(cb)).toBe(false);\n      });\n\n      it(\"should wait before removing th subscription to make sure it's not reused\", () => {\n        const unsub = QuerySubscriptionPool.add(this.query, () => {});\n        expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeDefined();\n        unsub();\n        expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeDefined();\n        advanceClock();\n        expect(QuerySubscriptionPool._subscriptions[this.queryKey]).toBeUndefined();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/models/query-subscription-spec.es6",
    "content": "import DatabaseStore from '../../src/flux/stores/database-store';\n\nimport QueryRange from '../../src/flux/models/query-range';\nimport MutableQueryResultSet from '../../src/flux/models/mutable-query-result-set';\nimport QuerySubscription from '../../src/flux/models/query-subscription';\nimport Thread from '../../src/flux/models/thread';\nimport Utils from '../../src/flux/models/utils';\n\ndescribe(\"QuerySubscription\", function QuerySubscriptionSpecs() {\n  describe(\"constructor\", () =>\n    describe(\"when a query is provided\", () => {\n      it(\"should finalize the query\", () => {\n        const query = DatabaseStore.findAll(Thread);\n        const subscription = new QuerySubscription(query);\n        expect(subscription).toBeDefined();\n        expect(query._finalized).toBe(true);\n      });\n\n      it(\"should throw an exception if the query is a count query, which cannot be observed\", () => {\n        const query = DatabaseStore.count(Thread);\n        expect(() => {\n          const subscription = new QuerySubscription(query);\n          return subscription;\n        })\n        .toThrow();\n      });\n\n      it(\"should call `update` to initialize the result set\", () => {\n        const query = DatabaseStore.findAll(Thread);\n        spyOn(QuerySubscription.prototype, 'update');\n        const subscription = new QuerySubscription(query);\n        expect(subscription).toBeDefined();\n        expect(QuerySubscription.prototype.update).toHaveBeenCalled();\n      });\n\n      describe(\"when initialModels are provided\", () =>\n        it(\"should apply the models and trigger\", () => {\n          const query = DatabaseStore.findAll(Thread);\n          const threads = [1, 2, 3, 4, 5].map(i => new Thread({id: i}));\n          const subscription = new QuerySubscription(query, {initialModels: threads});\n          expect(subscription._set).not.toBe(null);\n        })\n\n      );\n    })\n\n  );\n\n  describe(\"query\", () =>\n    it(\"should return the query\", () => {\n      const query = DatabaseStore.findAll(Thread);\n      const subscription = new QuerySubscription(query);\n      expect(subscription.query()).toBe(query);\n    })\n\n  );\n\n  describe(\"addCallback\", () =>\n    it(\"should emit the last result to the new callback if one is available\", () => {\n      const cb = jasmine.createSpy('callback');\n      spyOn(QuerySubscription.prototype, 'update').andReturn();\n      const subscription = new QuerySubscription(DatabaseStore.findAll(Thread));\n      subscription._lastResult = 'something';\n      runs(() => {\n        subscription.addCallback(cb);\n        advanceClock();\n      });\n      waitsFor(() => cb.calls.length > 0);\n      runs(() => expect(cb).toHaveBeenCalledWith('something'));\n    })\n  );\n\n  describe(\"applyChangeRecord\", () => {\n    spyOn(Utils, 'generateTempId').andCallFake(() => undefined);\n\n    const scenarios = [{\n      name: \"query with full set of objects (4)\",\n      query: DatabaseStore.findAll(Thread).where(Thread.attributes.accountId.equal('a')).limit(4).offset(2),\n      lastModels: [\n        new Thread({accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 4}),\n        new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),\n        new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),\n        new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),\n      ],\n      tests: [{\n        name: 'Item in set saved - new serverId, same sort value',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'a', serverId: 's-4', clientId: '4', lastMessageReceivedTimestamp: 4, subject: 'hello'})],\n          type: 'persist',\n        },\n        nextModels: [\n          new Thread({accountId: 'a', serverId: 's-4', clientId: '4', lastMessageReceivedTimestamp: 4, subject: 'hello'}),\n          new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),\n          new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),\n          new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),\n        ],\n        mustUpdate: false,\n        mustTrigger: true,\n      }, {\n        name: 'Item in set saved - new sort value',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: 3.5})],\n          type: 'persist',\n        },\n        nextModels: [\n          new Thread({accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 4}),\n          new Thread({accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: 3.5}),\n          new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),\n          new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),\n        ],\n        mustUpdate: true,\n        mustTrigger: true,\n      }, {\n        name: 'Item saved - does not match query clauses, offset > 0',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'b', clientId: '5', lastMessageReceivedTimestamp: 5})],\n          type: 'persist',\n        },\n        nextModels: 'unchanged',\n        mustUpdate: true,\n      }, {\n        name: 'Item saved - matches query clauses',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'a', clientId: '5', lastMessageReceivedTimestamp: -2})],\n          type: 'persist',\n        },\n        mustUpdate: true,\n      }, {\n        name: 'Item in set saved - no longer matches query clauses',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'b', clientId: '4', lastMessageReceivedTimestamp: 4})],\n          type: 'persist',\n        },\n        nextModels: [\n          new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),\n          new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),\n          new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),\n        ],\n        mustUpdate: true,\n      }, {\n        name: 'Item in set deleted',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'a', clientId: '4'})],\n          type: 'unpersist',\n        },\n        nextModels: [\n          new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 3}),\n          new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 2}),\n          new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1}),\n        ],\n        mustUpdate: true,\n      }, {\n        name: 'Item not in set deleted',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'a', clientId: '5'})],\n          type: 'unpersist',\n        },\n        nextModels: 'unchanged',\n        mustUpdate: false,\n      }],\n    }, {\n      name: \"query with multiple sort orders\",\n      query: DatabaseStore.findAll(Thread).where(Thread.attributes.accountId.equal('a')).limit(4).offset(2).order([\n        Thread.attributes.lastMessageReceivedTimestamp.ascending(),\n        Thread.attributes.unread.descending(),\n      ]),\n      lastModels: [\n        new Thread({accountId: 'a', clientId: '1', lastMessageReceivedTimestamp: 1, unread: true}),\n        new Thread({accountId: 'a', clientId: '2', lastMessageReceivedTimestamp: 1, unread: false}),\n        new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 1, unread: false}),\n        new Thread({accountId: 'a', clientId: '4', lastMessageReceivedTimestamp: 2, unread: true}),\n      ],\n      tests: [{\n        name: 'Item in set saved, secondary sort order changed',\n        change: {\n          objectClass: Thread.name,\n          objects: [new Thread({accountId: 'a', clientId: '3', lastMessageReceivedTimestamp: 1, unread: true})],\n          type: 'persist',\n        },\n        mustUpdate: true,\n      }],\n    }];\n\n    jasmine.unspy(Utils, 'generateTempId');\n\n    describe(\"scenarios\", () =>\n      scenarios.forEach(scenario => {\n        scenario.tests.forEach(test => {\n          it(`with ${scenario.name}, should correctly apply ${test.name}`, () => {\n            const subscription = new QuerySubscription(scenario.query);\n            subscription._set = new MutableQueryResultSet();\n            subscription._set.addModelsInRange(scenario.lastModels, new QueryRange({start: 0, end: scenario.lastModels.length}));\n\n            spyOn(subscription, 'update');\n            spyOn(subscription, '_createResultAndTrigger');\n            subscription._updateInFlight = false;\n            subscription.applyChangeRecord(test.change);\n\n            if (test.mustUpdate) {\n              expect(subscription.update).toHaveBeenCalledWith({mustRefetchEntireRange: true});\n            } else if (test.nextModels === 'unchanged') {\n              expect(subscription._set.models()).toEqual(scenario.lastModels);\n            } else {\n              expect(subscription._set.models()).toEqual(test.nextModels);\n            }\n\n            if (test.mustTriger) {\n              expect(subscription._createResultAndTrigger).toHaveBeenCalled();\n            }\n          });\n        });\n      })\n\n    );\n  });\n\n  describe(\"update\", () => {\n    beforeEach(() =>\n      spyOn(QuerySubscription.prototype, '_fetchRange').andCallFake(() => {\n        if (this._set == null) { this._set = new MutableQueryResultSet(); }\n        return Promise.resolve();\n      })\n    );\n\n    describe(\"when the query has an infinite range\", () => {\n      it(\"should call _fetchRange for the entire range\", () => {\n        const subscription = new QuerySubscription(DatabaseStore.findAll(Thread));\n        subscription.update();\n        advanceClock();\n        expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: true, version: 1});\n      });\n\n      it(\"should fetch full full models only when the previous set is empty\", () => {\n        const subscription = new QuerySubscription(DatabaseStore.findAll(Thread));\n        subscription._set = new MutableQueryResultSet();\n        subscription._set.addModelsInRange([new Thread()], new QueryRange({start: 0, end: 1}));\n        subscription.update();\n        advanceClock();\n        expect(subscription._fetchRange).toHaveBeenCalledWith(QueryRange.infinite(), {fetchEntireModels: false, version: 1});\n      });\n    });\n\n    describe(\"when the query has a range\", () => {\n      beforeEach(() => {\n        this.query = DatabaseStore.findAll(Thread).limit(10);\n      });\n\n      describe(\"when we have no current range\", () =>\n        it(\"should call _fetchRange for the entire range and fetch full models\", () => {\n          const subscription = new QuerySubscription(this.query);\n          subscription._set = null;\n          subscription.update();\n          advanceClock();\n          expect(subscription._fetchRange).toHaveBeenCalledWith(this.query.range(), {fetchEntireModels: true, version: 1});\n        })\n      );\n\n      describe(\"when we have a previous range\", () => {\n        it(\"should call _fetchRange with the missingRange\", () => {\n          const customRange = jasmine.createSpy('customRange1');\n          spyOn(QueryRange, 'rangesBySubtracting').andReturn([customRange]);\n          const subscription = new QuerySubscription(this.query);\n          subscription._set = new MutableQueryResultSet();\n          subscription._set.addModelsInRange([new Thread()], new QueryRange({start: 0, end: 1}));\n\n          advanceClock();\n          subscription._fetchRange.reset();\n          subscription._updateInFlight = false;\n          subscription.update();\n          advanceClock();\n          expect(subscription._fetchRange.callCount).toBe(1);\n          expect(subscription._fetchRange.calls[0].args).toEqual([customRange, {fetchEntireModels: true, version: 1}]);\n        });\n\n        it(\"should call _fetchRange for the entire query range when the missing range encompasses more than one range\", () => {\n          const customRange1 = jasmine.createSpy('customRange1');\n          const customRange2 = jasmine.createSpy('customRange2');\n          spyOn(QueryRange, 'rangesBySubtracting').andReturn([customRange1, customRange2]);\n\n          const range = new QueryRange({start: 0, end: 1});\n          const subscription = new QuerySubscription(this.query);\n          subscription._set = new MutableQueryResultSet();\n          subscription._set.addModelsInRange([new Thread()], range);\n\n          advanceClock();\n          subscription._fetchRange.reset();\n          subscription._updateInFlight = false;\n          subscription.update();\n          advanceClock();\n          expect(subscription._fetchRange.callCount).toBe(1);\n          expect(subscription._fetchRange.calls[0].args).toEqual([this.query.range(), {fetchEntireModels: true, version: 1}]);\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/models/thread-spec.coffee",
    "content": "Message = require('../../src/flux/models/message').default\nThread = require('../../src/flux/models/thread').default\nCategory = require('../../src/flux/models/category').default\n{Utils} = require 'nylas-exports'\n_ = require 'underscore'\n\ndescribe 'Thread', ->\n\n  describe 'serialization performance', ->\n    xit '1,000,000 iterations', ->\n      iterations = 0\n      json = '[{\"client_id\":\"local-76c370af-65de\",\"server_id\":\"f0vkowp7zxt7djue7ifylb940\",\"object\":\"thread\",\"account_id\":\"1r6w6qiq3sb0o9fiwin6v87dd\",\"snippet\":\"http://itunestandc.tumblr.com/tagged/itunes-terms-and-conditions/chrono _______________________________________________ http://www.macgroup.com/mailman/listinfo/smartfriends-chat\",\"subject\":\"iTunes Terms And Conditions as you\\'ve never seen them before\",\"unread\":true,\"starred\":false,\"version\":1,\"folders\":[],\"labels\":[{\"server_id\":\"8cf4fn20k9pjjhjawrv3xrxo0\",\"name\":\"all\",\"display_name\":\"All Mail\",\"id\":\"8cf4fn20k9pjjhjawrv3xrxo0\"},{\"server_id\":\"f1lq8faw8vv06m67y8f3xdf84\",\"name\":\"inbox\",\"display_name\":\"Inbox\",\"id\":\"f1lq8faw8vv06m67y8f3xdf84\"}],\"participants\":[{\"name\":\"Andrew Stadler\",\"email\":\"stadler@gmail.com\",\"thirdPartyData\":{}},{\"name\":\"Smart Friends™ Chat\",\"email\":\"smartfriends-chat@macgroup.com\",\"thirdPartyData\":{}}],\"has_attachments\":false,\"last_message_received_timestamp\":1446600615,\"id\":\"f0vkowp7zxt7djue7ifylb940\"}]'\n      start = Date.now()\n      while iterations < 1000000\n        if _.isString(json)\n          data = JSON.parse(json)\n        object = new Thread()\n        object.fromJSON(data)\n        object\n        iterations += 1\n      console.log((Date.now() - start) / 1000.0 + \"ms per 1000\")\n\n  describe \"inAllMail\", ->\n    describe \"when the thread categoriesType is 'folders'\", ->\n      it \"should return true\", ->\n        thread = new Thread(categoriesType: 'folders', categories: [new Category(name: 'inbox')])\n        expect(thread.inAllMail).toBe(true)\n\n        # Unlike Gmail, this means half the thread is in trash and half is in sent.\n        # It should still appear in results for \"Sent\"\n        thread = new Thread(categoriesType: 'folders', categories: [new Category(name: 'sent'), new Category(name: 'trash')])\n        expect(thread.inAllMail).toBe(true)\n\n    describe \"when the thread categoriesType is 'labels'\", ->\n      it \"should return true if the thread has an all category\", ->\n        thread = new Thread(categoriesType: 'labels', categories: [new Category(name: 'all')])\n        expect(thread.inAllMail).toBe(true)\n\n        # thread is half in spam\n        thread = new Thread(categoriesType: 'labels', categories: [new Category(name: 'all'), new Category(name: 'inbox'), new Category(name: 'spam')])\n        expect(thread.inAllMail).toBe(true)\n\n      it \"should return false if the thread has the spam category and no all mail\", ->\n        thread = new Thread(categoriesType: 'labels', categories: [new Category(name: 'sent'), new Category(name: 'spam')])\n        expect(thread.inAllMail).toBe(false)\n\n      it \"should return false if the thread has the trash category and no all mail\", ->\n        thread = new Thread(categoriesType: 'labels', categories: [new Category(name: 'sent'), new Category(name: 'trash')])\n        expect(thread.inAllMail).toBe(false)\n\n      it \"should return true if the thread has none of the above (assume all mail)\", ->\n        thread = new Thread(categoriesType: 'labels', categories: [new Category(name: 'inbox')])\n        expect(thread.inAllMail).toBe(true)\n\n  describe 'sortedCategories', ->\n    sortedForCategoryNames = (inputs) ->\n      categories = _.map inputs, (i) ->\n        new Category(name: i, displayName: i)\n      thread = new Thread(categories: categories)\n      return thread.sortedCategories()\n\n    it \"puts 'important' label first, if it's present\", ->\n      inputs = ['alphabetically before important', 'important']\n      actualOut = sortedForCategoryNames inputs\n      expect(actualOut[0].displayName).toBe 'important'\n\n    it \"ignores 'important' label if not present\", ->\n      inputs = ['not important']\n      actualOut = sortedForCategoryNames inputs\n      expect(actualOut.length).toBe 1\n      expect(actualOut[0].displayName).toBe 'not important'\n\n    it \"doesn't display 'all', 'archive', or 'drafts'\", ->\n      inputs = ['all', 'archive', 'drafts']\n      actualOut = sortedForCategoryNames inputs\n      expect(actualOut.length).toBe 0\n\n    it \"displays standard category names which aren't hidden next, if they're present\", ->\n      inputs = ['inbox', 'important', 'social']\n      actualOut = _.pluck sortedForCategoryNames(inputs), 'displayName'\n      expectedOut = ['important', 'inbox', 'social']\n      expect(actualOut).toEqual expectedOut\n\n    it \"ignores standard category names if they aren't present\", ->\n      inputs = ['social', 'work', 'important']\n      actualOut = _.pluck sortedForCategoryNames(inputs), 'displayName'\n      expectedOut = ['important', 'social', 'work']\n      expect(actualOut).toEqual expectedOut\n\n    it \"puts user-added categories at the end\", ->\n      inputs = ['food', 'inbox']\n      actualOut = _.pluck sortedForCategoryNames(inputs), 'displayName'\n      expectedOut = ['inbox', 'food']\n      expect(actualOut).toEqual expectedOut\n\n    it \"sorts user-added categories by displayName\", ->\n      inputs = ['work', 'social', 'receipts', 'important', 'inbox']\n      actualOut = _.pluck sortedForCategoryNames(inputs), 'displayName'\n      expectedOut = ['important', 'inbox', 'receipts', 'social', 'work']\n      expect(actualOut).toEqual expectedOut\n"
  },
  {
    "path": "packages/client-app/spec/module-cache-spec.coffee",
    "content": "path = require 'path'\nModule = require 'module'\nfs = require 'fs-plus'\ntemp = require 'temp'\nModuleCache = require '../src/module-cache'\n\ndescribe 'ModuleCache', ->\n  beforeEach ->\n    spyOn(Module, '_findPath').andCallThrough()\n\n  it 'resolves Electron module paths without hitting the filesystem', ->\n    builtins = ModuleCache.cache.builtins\n    expect(Object.keys(builtins).length).toBeGreaterThan 0\n\n    for builtinName, builtinPath of builtins\n      expect(require.resolve(builtinName)).toBe builtinPath\n      expect(fs.isFileSync(require.resolve(builtinName)))\n\n    expect(Module._findPath.callCount).toBe 0\n\n  it 'resolves relative core paths without hitting the filesystem', ->\n    ModuleCache.add NylasEnv.getLoadSettings().resourcePath, {\n      _nylasModuleCache:\n        extensions:\n          '.json': [\n            path.join('spec', 'fixtures', 'module-cache', 'file.json')\n          ]\n    }\n    expect(require('./fixtures/module-cache/file.json').foo).toBe 'bar'\n    expect(Module._findPath.callCount).toBe 0\n\n  it 'resolves module paths when a compatible version is provided by core', ->\n    packagePath = fs.realpathSync(temp.mkdirSync('n1-package'))\n    ModuleCache.add packagePath, {\n      _nylasModuleCache:\n        folders: [{\n          paths: [\n            ''\n          ]\n          dependencies:\n            'underscore': '*'\n        }]\n    }\n    ModuleCache.add NylasEnv.getLoadSettings().resourcePath, {\n      _nylasModuleCache:\n        dependencies: [{\n          name: 'underscore'\n          version: require('underscore/package.json').version\n          path: path.join('node_modules', 'underscore', 'underscore.js')\n        }]\n    }\n\n    indexPath = path.join(packagePath, 'index.js')\n    fs.writeFileSync indexPath, \"\"\"\n      exports.load = function() { require('underscore'); };\n    \"\"\"\n\n    packageMain = require(indexPath)\n    Module._findPath.reset()\n    packageMain.load()\n    expect(Module._findPath.callCount).toBe 0\n\n  it 'does not resolve module paths when no compatible version is provided by core', ->\n    packagePath = fs.realpathSync(temp.mkdirSync('n1-package'))\n    ModuleCache.add packagePath, {\n      _nylasModuleCache:\n        folders: [{\n          paths: [\n            ''\n          ]\n          dependencies:\n            'underscore': '0.0.1'\n        }]\n    }\n    ModuleCache.add NylasEnv.getLoadSettings().resourcePath, {\n      _nylasModuleCache:\n        dependencies: [{\n          name: 'unknown-lib'\n          version: require('underscore/package.json').version\n          path: path.join('node_modules', 'underscore', 'underscore.js')\n        }]\n    }\n\n    indexPath = path.join(packagePath, 'index.js')\n    fs.writeFileSync indexPath, \"\"\"\n      exports.load = function() { require('unknown-lib'); };\n    \"\"\"\n\n    spyOn(process, 'cwd').andReturn('/') # Required when running this test from CLI\n    packageMain = require(indexPath)\n    Module._findPath.reset()\n    expect(-> packageMain.load()).toThrow()\n    expect(Module._findPath.callCount).toBe 1\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/console-reporter.es6",
    "content": "const originalLog = console.log;\nconst originalWarn = console.warn;\nconst originalError = console.error;\n\nexport default class ConsoleReporter {\n  reportSpecStarting(spec) {\n    const withContext = (log) => {\n      return (...args) => {\n        if (args[0] === '.') {\n          return log(...args);\n        }\n        return log(`[${spec.getFullName()}] ${args[0]}`, ...args.slice(1));\n      }\n    }\n    console.log = withContext(originalLog);\n    console.warn = withContext(originalWarn);\n    console.error = withContext(originalError);\n  }\n\n  reportSpecResults() {\n    if (console.log !== originalLog) {\n      console.log = originalLog;\n    }\n    if (console.warn !== originalWarn) {\n      console.warn = originalWarn;\n    }\n    if (console.error !== originalError) {\n      console.error = originalError;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/jasmine-extensions.es6",
    "content": "/* eslint no-prototype-builtins: 0 */\nimport _ from 'underscore';\nimport moment from 'moment-timezone';\n// On import this will extend the `moment` object\nimport 'moment-round';\n\nimport nylasTestConstants from './nylas-test-constants'\n\nexport function waitsForPromise(...args) {\n  let shouldReject;\n  let timeout;\n  if (args.length > 1) {\n    shouldReject = args[0].shouldReject;\n    timeout = args[0].timeout\n  } else {\n    shouldReject = false;\n  }\n  const fn = _.last(args);\n\n  return window.waitsFor(timeout, (moveOn) => {\n    const promise = fn();\n    // Keep in mind we can't check `promise instanceof Promise` because parts of\n    // the app still use other Promise libraries Just see if it looks\n    // promise-like.\n    if (!promise || !promise.then) {\n      jasmine.getEnv().currentSpec.fail(`Expected callback to return a promise-like object, but it returned ${promise}`);\n      return moveOn();\n    } else if (shouldReject) {\n      promise.catch(moveOn);\n      return promise.then(() => {\n        jasmine.getEnv().currentSpec.fail(\"Expected promise to be rejected, but it was resolved\");\n        return moveOn();\n      });\n    }\n\n    promise.then(moveOn);\n    return promise.catch((error) => {\n      // I don't know what `pp` does, but for standard `new Error` objects,\n      // it sometimes returns \"{  }\". Catch this case and fall through to toString()\n      let msg = jasmine.pp(error);\n      if (msg === \"{  }\") { msg = error.toString(); }\n      jasmine.getEnv().currentSpec.fail(`Expected promise to be resolved, but it was rejected with ${msg}`);\n      return moveOn();\n    });\n  });\n}\n\nexport function toHaveLength(expected) {\n  if (this.actual == null) {\n    this.message = () => `Expected object ${this.actual} has no length method`;\n    return false;\n  }\n  const notText = this.isNot ? \" not\" : \"\";\n  this.message = () => `Expected object with length ${this.actual.length} to${notText} have length ${expected}`;\n  return this.actual.length === expected;\n}\n\nexport function unspy(object, methodName) {\n  if (!object[methodName].hasOwnProperty('originalValue')) { throw new Error(\"Not a spy\"); }\n  object[methodName] = object[methodName].originalValue;\n}\n\nexport function attachToDOM(element) {\n  const jasmineContent = document.querySelector('#jasmine-content');\n  if (!jasmineContent.contains(element)) { jasmineContent.appendChild(element); }\n}\n\n// This date was chosen because it's close to a DST boundary\nexport function testNowMoment() {\n  return moment.tz(\"2016-03-15 12:00\", nylasTestConstants.TEST_TIME_ZONE);\n}\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/jasmine.js",
    "content": "// Modified line\n// - var isCommonJS = typeof window == \"undefined\" && typeof exports == \"object\";\n// + var isCommonJS = typeof exports == \"object\";\n//\n// Modified method jasmine.WaitsForBlock.prototype.execute\n\nvar isCommonJS = typeof exports == \"object\";\n\n/**\n * Top level namespace for Jasmine, a lightweight JavaScript BDD/spec/testing framework.\n *\n * @namespace\n */\nvar jasmine = {};\nif (isCommonJS) exports.jasmine = jasmine;\n/**\n * @private\n */\njasmine.unimplementedMethod_ = function() {\n  throw new Error(\"unimplemented method\");\n};\n\n/**\n * Use <code>jasmine.undefined</code> instead of <code>undefined</code>, since <code>undefined</code> is just\n * a plain old variable and may be redefined by somebody else.\n *\n * @private\n */\njasmine.undefined = jasmine.___undefined___;\n\n/**\n * Show diagnostic messages in the console if set to true\n *\n */\njasmine.VERBOSE = false;\n\n/**\n * Default interval in milliseconds for event loop yields (e.g. to allow network activity or to refresh the screen with the HTML-based runner). Small values here may result in slow test running. Zero means no updates until all tests have completed.\n *\n */\njasmine.DEFAULT_UPDATE_INTERVAL = 250;\n\n/**\n * Maximum levels of nesting that will be included when an object is pretty-printed\n */\njasmine.MAX_PRETTY_PRINT_DEPTH = 40;\n\n/**\n * Default timeout interval in milliseconds for waitsFor() blocks.\n */\njasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;\n\n/**\n * By default exceptions thrown in the context of a test are caught by jasmine so that it can run the remaining tests in the suite.\n * Set to false to let the exception bubble up in the browser.\n *\n */\njasmine.CATCH_EXCEPTIONS = true;\n\njasmine.getGlobal = function() {\n  function getGlobal() {\n    return window;\n  }\n\n  return getGlobal();\n};\n\n/**\n * Allows for bound functions to be compared.  Internal use only.\n *\n * @ignore\n * @private\n * @param base {Object} bound 'this' for the function\n * @param name {Function} function to find\n */\njasmine.bindOriginal_ = function(base, name) {\n  var original = base[name];\n  if (original.apply) {\n    return function() {\n      return original.apply(base, arguments);\n    };\n  } else {\n    // IE support\n    return jasmine.getGlobal()[name];\n  }\n};\n\njasmine.setTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'setTimeout');\njasmine.clearTimeout = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearTimeout');\njasmine.setInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'setInterval');\njasmine.clearInterval = jasmine.bindOriginal_(jasmine.getGlobal(), 'clearInterval');\n\njasmine.MessageResult = function(values) {\n  this.type = 'log';\n  this.values = values;\n  this.trace = new Error(); // todo: test better\n};\n\njasmine.MessageResult.prototype.toString = function() {\n  var text = \"\";\n  for (var i = 0; i < this.values.length; i++) {\n    if (i > 0) text += \" \";\n    if (jasmine.isString_(this.values[i])) {\n      text += this.values[i];\n    } else {\n      text += jasmine.pp(this.values[i]);\n    }\n  }\n  return text;\n};\n\njasmine.ExpectationResult = function(params) {\n  this.type = 'expect';\n  this.matcherName = params.matcherName;\n  this.passed_ = params.passed;\n  this.expected = params.expected;\n  this.actual = params.actual;\n  this.message = this.passed_ ? 'Passed.' : params.message;\n\n  var trace = (params.trace || new Error(this.message));\n  this.trace = this.passed_ ? '' : trace;\n};\n\njasmine.ExpectationResult.prototype.toString = function () {\n  return this.message;\n};\n\njasmine.ExpectationResult.prototype.passed = function () {\n  return this.passed_;\n};\n\n/**\n * Getter for the Jasmine environment. Ensures one gets created\n */\njasmine.getEnv = function() {\n  var env = jasmine.currentEnv_ = jasmine.currentEnv_ || new jasmine.Env();\n  return env;\n};\n\n/**\n * @ignore\n * @private\n * @param value\n * @returns {Boolean}\n */\njasmine.isArray_ = function(value) {\n  return jasmine.isA_(\"Array\", value);\n};\n\n/**\n * @ignore\n * @private\n * @param value\n * @returns {Boolean}\n */\njasmine.isString_ = function(value) {\n  return jasmine.isA_(\"String\", value);\n};\n\n/**\n * @ignore\n * @private\n * @param value\n * @returns {Boolean}\n */\njasmine.isNumber_ = function(value) {\n  return jasmine.isA_(\"Number\", value);\n};\n\n/**\n * @ignore\n * @private\n * @param {String} typeName\n * @param value\n * @returns {Boolean}\n */\njasmine.isA_ = function(typeName, value) {\n  return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';\n};\n\n/**\n * Pretty printer for expecations.  Takes any object and turns it into a human-readable string.\n *\n * @param value {Object} an object to be outputted\n * @returns {String}\n */\njasmine.pp = function(value) {\n  var stringPrettyPrinter = new jasmine.StringPrettyPrinter();\n  stringPrettyPrinter.format(value);\n  return stringPrettyPrinter.string;\n};\n\n/**\n * Returns true if the object is a DOM Node.\n *\n * @param {Object} obj object to check\n * @returns {Boolean}\n */\njasmine.isDomNode = function(obj) {\n  return obj.nodeType > 0;\n};\n\n/**\n * Returns a matchable 'generic' object of the class type.  For use in expecations of type when values don't matter.\n *\n * @example\n * // don't care about which function is passed in, as long as it's a function\n * expect(mySpy).toHaveBeenCalledWith(jasmine.any(Function));\n *\n * @param {Class} clazz\n * @returns matchable object of the type clazz\n */\njasmine.any = function(clazz) {\n  return new jasmine.Matchers.Any(clazz);\n};\n\n/**\n * Returns a matchable subset of a JSON object. For use in expectations when you don't care about all of the\n * attributes on the object.\n *\n * @example\n * // don't care about any other attributes than foo.\n * expect(mySpy).toHaveBeenCalledWith(jasmine.objectContaining({foo: \"bar\"});\n *\n * @param sample {Object} sample\n * @returns matchable object for the sample\n */\njasmine.objectContaining = function (sample) {\n    return new jasmine.Matchers.ObjectContaining(sample);\n};\n\n/**\n * Jasmine Spies are test doubles that can act as stubs, spies, fakes or when used in an expecation, mocks.\n *\n * Spies should be created in test setup, before expectations.  They can then be checked, using the standard Jasmine\n * expectation syntax. Spies can be checked if they were called or not and what the calling params were.\n *\n * A Spy has the following fields: wasCalled, callCount, mostRecentCall, and argsForCall (see docs).\n *\n * Spies are torn down at the end of every spec.\n *\n * Note: Do <b>not</b> call new jasmine.Spy() directly - a spy must be created using spyOn, jasmine.createSpy or jasmine.createSpyObj.\n *\n * @example\n * // a stub\n * var myStub = jasmine.createSpy('myStub');  // can be used anywhere\n *\n * // spy example\n * var foo = {\n *   not: function(bool) { return !bool; }\n * }\n *\n * // actual foo.not will not be called, execution stops\n * spyOn(foo, 'not');\n\n // foo.not spied upon, execution will continue to implementation\n * spyOn(foo, 'not').andCallThrough();\n *\n * // fake example\n * var foo = {\n *   not: function(bool) { return !bool; }\n * }\n *\n * // foo.not(val) will return val\n * spyOn(foo, 'not').andCallFake(function(value) {return value;});\n *\n * // mock example\n * foo.not(7 == 7);\n * expect(foo.not).toHaveBeenCalled();\n * expect(foo.not).toHaveBeenCalledWith(true);\n *\n * @constructor\n * @see spyOn, jasmine.createSpy, jasmine.createSpyObj\n * @param {String} name\n */\njasmine.Spy = function(name) {\n  /**\n   * The name of the spy, if provided.\n   */\n  this.identity = name || 'unknown';\n  /**\n   *  Is this Object a spy?\n   */\n  this.isSpy = true;\n  /**\n   * The actual function this spy stubs.\n   */\n  this.plan = function() {\n  };\n  /**\n   * Tracking of the most recent call to the spy.\n   * @example\n   * var mySpy = jasmine.createSpy('foo');\n   * mySpy(1, 2);\n   * mySpy.mostRecentCall.args = [1, 2];\n   */\n  this.mostRecentCall = {};\n\n  /**\n   * Holds arguments for each call to the spy, indexed by call count\n   * @example\n   * var mySpy = jasmine.createSpy('foo');\n   * mySpy(1, 2);\n   * mySpy(7, 8);\n   * mySpy.mostRecentCall.args = [7, 8];\n   * mySpy.argsForCall[0] = [1, 2];\n   * mySpy.argsForCall[1] = [7, 8];\n   */\n  this.argsForCall = [];\n  this.calls = [];\n};\n\n/**\n * Tells a spy to call through to the actual implemenatation.\n *\n * @example\n * var foo = {\n *   bar: function() { // do some stuff }\n * }\n *\n * // defining a spy on an existing property: foo.bar\n * spyOn(foo, 'bar').andCallThrough();\n */\njasmine.Spy.prototype.andCallThrough = function() {\n  this.plan = this.originalValue;\n  return this;\n};\n\n/**\n * For setting the return value of a spy.\n *\n * @example\n * // defining a spy from scratch: foo() returns 'baz'\n * var foo = jasmine.createSpy('spy on foo').andReturn('baz');\n *\n * // defining a spy on an existing property: foo.bar() returns 'baz'\n * spyOn(foo, 'bar').andReturn('baz');\n *\n * @param {Object} value\n */\njasmine.Spy.prototype.andReturn = function(value) {\n  this.plan = function() {\n    return value;\n  };\n  return this;\n};\n\n/**\n * For throwing an exception when a spy is called.\n *\n * @example\n * // defining a spy from scratch: foo() throws an exception w/ message 'ouch'\n * var foo = jasmine.createSpy('spy on foo').andThrow('baz');\n *\n * // defining a spy on an existing property: foo.bar() throws an exception w/ message 'ouch'\n * spyOn(foo, 'bar').andThrow('baz');\n *\n * @param {String} exceptionMsg\n */\njasmine.Spy.prototype.andThrow = function(exceptionMsg) {\n  this.plan = function() {\n    throw exceptionMsg;\n  };\n  return this;\n};\n\n/**\n * Calls an alternate implementation when a spy is called.\n *\n * @example\n * var baz = function() {\n *   // do some stuff, return something\n * }\n * // defining a spy from scratch: foo() calls the function baz\n * var foo = jasmine.createSpy('spy on foo').andCall(baz);\n *\n * // defining a spy on an existing property: foo.bar() calls an anonymnous function\n * spyOn(foo, 'bar').andCall(function() { return 'baz';} );\n *\n * @param {Function} fakeFunc\n */\njasmine.Spy.prototype.andCallFake = function(fakeFunc) {\n  this.plan = fakeFunc;\n  return this;\n};\n\n/**\n * Resets all of a spy's the tracking variables so that it can be used again.\n *\n * @example\n * spyOn(foo, 'bar');\n *\n * foo.bar();\n *\n * expect(foo.bar.callCount).toEqual(1);\n *\n * foo.bar.reset();\n *\n * expect(foo.bar.callCount).toEqual(0);\n */\njasmine.Spy.prototype.reset = function() {\n  this.wasCalled = false;\n  this.callCount = 0;\n  this.argsForCall = [];\n  this.calls = [];\n  this.mostRecentCall = {};\n};\n\njasmine.createSpy = function(name) {\n\n  var spyObj = function() {\n    spyObj.wasCalled = true;\n    spyObj.callCount++;\n    var args = jasmine.util.argsToArray(arguments);\n    spyObj.mostRecentCall.object = this;\n    spyObj.mostRecentCall.args = args;\n    spyObj.argsForCall.push(args);\n    spyObj.calls.push({object: this, args: args});\n    return spyObj.plan.apply(this, arguments);\n  };\n\n  var spy = new jasmine.Spy(name);\n\n  for (var prop in spy) {\n    spyObj[prop] = spy[prop];\n  }\n\n  spyObj.reset();\n\n  return spyObj;\n};\n\n/**\n * Determines whether an object is a spy.\n *\n * @param {jasmine.Spy|Object} putativeSpy\n * @returns {Boolean}\n */\njasmine.isSpy = function(putativeSpy) {\n  return putativeSpy && putativeSpy.isSpy;\n};\n\n/**\n * Creates a more complicated spy: an Object that has every property a function that is a spy.  Used for stubbing something\n * large in one call.\n *\n * @param {String} baseName name of spy class\n * @param {Array} methodNames array of names of methods to make spies\n */\njasmine.createSpyObj = function(baseName, methodNames) {\n  if (!jasmine.isArray_(methodNames) || methodNames.length === 0) {\n    throw new Error('createSpyObj requires a non-empty array of method names to create spies for');\n  }\n  var obj = {};\n  for (var i = 0; i < methodNames.length; i++) {\n    obj[methodNames[i]] = jasmine.createSpy(baseName + '.' + methodNames[i]);\n  }\n  return obj;\n};\n\n/**\n * All parameters are pretty-printed and concatenated together, then written to the current spec's output.\n *\n * Be careful not to leave calls to <code>jasmine.log</code> in production code.\n */\njasmine.log = function() {\n  var spec = jasmine.getEnv().currentSpec;\n  spec.log.apply(spec, arguments);\n};\n\n/**\n * Function that installs a spy on an existing object's method name.  Used within a Spec to create a spy.\n *\n * @example\n * // spy example\n * var foo = {\n *   not: function(bool) { return !bool; }\n * }\n * spyOn(foo, 'not'); // actual foo.not will not be called, execution stops\n *\n * @see jasmine.createSpy\n * @param obj\n * @param methodName\n * @return {jasmine.Spy} a Jasmine spy that can be chained with all spy methods\n */\nvar spyOn = function(obj, methodName) {\n  return jasmine.getEnv().currentSpec.spyOn(obj, methodName);\n};\nif (isCommonJS) exports.spyOn = spyOn;\n\n/**\n * Creates a Jasmine spec that will be added to the current suite.\n *\n * // TODO: pending tests\n *\n * @example\n * it('should be true', function() {\n *   expect(true).toEqual(true);\n * });\n *\n * @param {String} desc description of this specification\n * @param {Function} func defines the preconditions and expectations of the spec\n */\nvar it = function(desc, func) {\n  return jasmine.getEnv().it(desc, func);\n};\nif (isCommonJS) exports.it = it;\n\n/**\n * Creates a <em>disabled</em> Jasmine spec.\n *\n * A convenience method that allows existing specs to be disabled temporarily during development.\n *\n * @param {String} desc description of this specification\n * @param {Function} func defines the preconditions and expectations of the spec\n */\nvar xit = function(desc, func) {\n  return jasmine.getEnv().xit(desc, func);\n};\nif (isCommonJS) exports.xit = xit;\n\n/**\n * Starts a chain for a Jasmine expectation.\n *\n * It is passed an Object that is the actual value and should chain to one of the many\n * jasmine.Matchers functions.\n *\n * @param {Object} actual Actual value to test against and expected value\n * @return {jasmine.Matchers}\n */\nvar expect = function(actual) {\n  return jasmine.getEnv().currentSpec.expect(actual);\n};\nif (isCommonJS) exports.expect = expect;\n\n/**\n * Defines part of a jasmine spec.  Used in cominbination with waits or waitsFor in asynchrnous specs.\n *\n * @param {Function} func Function that defines part of a jasmine spec.\n */\nvar runs = function(func) {\n  jasmine.getEnv().currentSpec.runs(func);\n};\nif (isCommonJS) exports.runs = runs;\n\n/**\n * Waits a fixed time period before moving to the next block.\n *\n * @deprecated Use waitsFor() instead\n * @param {Number} timeout milliseconds to wait\n */\nvar waits = function(timeout) {\n  jasmine.getEnv().currentSpec.waits(timeout);\n};\nif (isCommonJS) exports.waits = waits;\n\n/**\n * Waits for the latchFunction to return true before proceeding to the next block.\n *\n * @param {Function} latchFunction\n * @param {String} optional_timeoutMessage\n * @param {Number} optional_timeout\n */\nvar waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {\n  jasmine.getEnv().currentSpec.waitsFor.apply(jasmine.getEnv().currentSpec, arguments);\n};\nif (isCommonJS) exports.waitsFor = waitsFor;\n\n/**\n * A function that is called before each spec in a suite.\n *\n * Used for spec setup, including validating assumptions.\n *\n * @param {Function} beforeEachFunction\n */\nvar beforeEach = function(beforeEachFunction) {\n  jasmine.getEnv().beforeEach(beforeEachFunction);\n};\nif (isCommonJS) exports.beforeEach = beforeEach;\n\n/**\n * A function that is called after each spec in a suite.\n *\n * Used for restoring any state that is hijacked during spec execution.\n *\n * @param {Function} afterEachFunction\n */\nvar afterEach = function(afterEachFunction) {\n  jasmine.getEnv().afterEach(afterEachFunction);\n};\nif (isCommonJS) exports.afterEach = afterEach;\n\n/**\n * Defines a suite of specifications.\n *\n * Stores the description and all defined specs in the Jasmine environment as one suite of specs. Variables declared\n * are accessible by calls to beforeEach, it, and afterEach. Describe blocks can be nested, allowing for specialization\n * of setup in some tests.\n *\n * @example\n * // TODO: a simple suite\n *\n * // TODO: a simple suite with a nested describe block\n *\n * @param {String} description A string, usually the class under test.\n * @param {Function} specDefinitions function that defines several specs.\n */\nvar describe = function(description, specDefinitions) {\n  return jasmine.getEnv().describe(description, specDefinitions);\n};\nif (isCommonJS) exports.describe = describe;\n\n/**\n * Disables a suite of specifications.  Used to disable some suites in a file, or files, temporarily during development.\n *\n * @param {String} description A string, usually the class under test.\n * @param {Function} specDefinitions function that defines several specs.\n */\nvar xdescribe = function(description, specDefinitions) {\n  return jasmine.getEnv().xdescribe(description, specDefinitions);\n};\nif (isCommonJS) exports.xdescribe = xdescribe;\n\n\n// Provide the XMLHttpRequest class for IE 5.x-6.x:\njasmine.XmlHttpRequest = (typeof XMLHttpRequest == \"undefined\") ? function() {\n  function tryIt(f) {\n    try {\n      return f();\n    } catch(e) {\n    }\n    return null;\n  }\n\n  var xhr = tryIt(function() {\n    return new ActiveXObject(\"Msxml2.XMLHTTP.6.0\");\n  }) ||\n    tryIt(function() {\n      return new ActiveXObject(\"Msxml2.XMLHTTP.3.0\");\n    }) ||\n    tryIt(function() {\n      return new ActiveXObject(\"Msxml2.XMLHTTP\");\n    }) ||\n    tryIt(function() {\n      return new ActiveXObject(\"Microsoft.XMLHTTP\");\n    });\n\n  if (!xhr) throw new Error(\"This browser does not support XMLHttpRequest.\");\n\n  return xhr;\n} : XMLHttpRequest;\n/**\n * @namespace\n */\njasmine.util = {};\n\n/**\n * Declare that a child class inherit it's prototype from the parent class.\n *\n * @private\n * @param {Function} childClass\n * @param {Function} parentClass\n */\njasmine.util.inherit = function(childClass, parentClass) {\n  /**\n   * @private\n   */\n  var subclass = function() {\n  };\n  subclass.prototype = parentClass.prototype;\n  childClass.prototype = new subclass();\n};\n\njasmine.util.formatException = function(e) {\n  var lineNumber;\n  if (e.line) {\n    lineNumber = e.line;\n  }\n  else if (e.lineNumber) {\n    lineNumber = e.lineNumber;\n  }\n\n  var file;\n\n  if (e.sourceURL) {\n    file = e.sourceURL;\n  }\n  else if (e.fileName) {\n    file = e.fileName;\n  }\n\n  var message = (e.name && e.message) ? (e.name + ': ' + e.message) : e.toString();\n\n  if (file && lineNumber) {\n    message += ' in ' + file + ' (line ' + lineNumber + ')';\n  }\n\n  return message;\n};\n\njasmine.util.htmlEscape = function(str) {\n  if (!str) return str;\n  return str.replace(/&/g, '&amp;')\n    .replace(/</g, '&lt;')\n    .replace(/>/g, '&gt;');\n};\n\njasmine.util.argsToArray = function(args) {\n  var arrayOfArgs = [];\n  for (var i = 0; i < args.length; i++) arrayOfArgs.push(args[i]);\n  return arrayOfArgs;\n};\n\njasmine.util.extend = function(destination, source) {\n  for (var property in source) destination[property] = source[property];\n  return destination;\n};\n\n/**\n * Environment for Jasmine\n *\n * @constructor\n */\njasmine.Env = function() {\n  this.currentSpec = null;\n  this.currentSuite = null;\n  this.currentRunner_ = new jasmine.Runner(this);\n\n  this.reporter = new jasmine.MultiReporter();\n\n  this.updateInterval = jasmine.DEFAULT_UPDATE_INTERVAL;\n  this.defaultTimeoutInterval = jasmine.DEFAULT_TIMEOUT_INTERVAL;\n  this.lastUpdate = 0;\n  this.specFilter = function() {\n    return true;\n  };\n\n  this.nextSpecId_ = 0;\n  this.nextSuiteId_ = 0;\n  this.equalityTesters_ = [];\n\n  // wrap matchers\n  this.matchersClass = function() {\n    jasmine.Matchers.apply(this, arguments);\n  };\n  jasmine.util.inherit(this.matchersClass, jasmine.Matchers);\n\n  jasmine.Matchers.wrapInto_(jasmine.Matchers.prototype, this.matchersClass);\n};\n\n\njasmine.Env.prototype.setTimeout = jasmine.setTimeout;\njasmine.Env.prototype.clearTimeout = jasmine.clearTimeout;\njasmine.Env.prototype.setInterval = jasmine.setInterval;\njasmine.Env.prototype.clearInterval = jasmine.clearInterval;\n\n/**\n * @returns an object containing jasmine version build info, if set.\n */\njasmine.Env.prototype.version = function () {\n  if (jasmine.version_) {\n    return jasmine.version_;\n  } else {\n    throw new Error('Version not set');\n  }\n};\n\n/**\n * @returns string containing jasmine version build info, if set.\n */\njasmine.Env.prototype.versionString = function() {\n  if (!jasmine.version_) {\n    return \"version unknown\";\n  }\n\n  var version = this.version();\n  var versionString = version.major + \".\" + version.minor + \".\" + version.build;\n  if (version.release_candidate) {\n    versionString += \".rc\" + version.release_candidate;\n  }\n  versionString += \" revision \" + version.revision;\n  return versionString;\n};\n\n/**\n * @returns a sequential integer starting at 0\n */\njasmine.Env.prototype.nextSpecId = function () {\n  return this.nextSpecId_++;\n};\n\n/**\n * @returns a sequential integer starting at 0\n */\njasmine.Env.prototype.nextSuiteId = function () {\n  return this.nextSuiteId_++;\n};\n\n/**\n * Register a reporter to receive status updates from Jasmine.\n * @param {jasmine.Reporter} reporter An object which will receive status updates.\n */\njasmine.Env.prototype.addReporter = function(reporter) {\n  this.reporter.addReporter(reporter);\n};\n\njasmine.Env.prototype.execute = function() {\n  this.currentRunner_.execute();\n};\n\njasmine.Env.prototype.describe = function(description, specDefinitions) {\n  var suite = new jasmine.Suite(this, description, specDefinitions, this.currentSuite);\n\n  var parentSuite = this.currentSuite;\n  if (parentSuite) {\n    parentSuite.add(suite);\n  } else {\n    this.currentRunner_.add(suite);\n  }\n\n  this.currentSuite = suite;\n\n  var declarationError = null;\n  try {\n    specDefinitions.call(suite);\n  } catch(e) {\n    declarationError = e;\n  }\n\n  if (declarationError) {\n    this.it(\"encountered a declaration exception\", function() {\n      throw declarationError;\n    });\n  }\n\n  this.currentSuite = parentSuite;\n\n  return suite;\n};\n\njasmine.Env.prototype.beforeEach = function(beforeEachFunction) {\n  if (this.currentSuite) {\n    this.currentSuite.beforeEach(beforeEachFunction);\n  } else {\n    this.currentRunner_.beforeEach(beforeEachFunction);\n  }\n};\n\njasmine.Env.prototype.currentRunner = function () {\n  return this.currentRunner_;\n};\n\njasmine.Env.prototype.afterEach = function(afterEachFunction) {\n  if (this.currentSuite) {\n    this.currentSuite.afterEach(afterEachFunction);\n  } else {\n    this.currentRunner_.afterEach(afterEachFunction);\n  }\n\n};\n\njasmine.Env.prototype.xdescribe = function(desc, specDefinitions) {\n  return {\n    execute: function() {\n    }\n  };\n};\n\njasmine.Env.prototype.it = function(description, func) {\n  var spec = new jasmine.Spec(this, this.currentSuite, description);\n  this.currentSuite.add(spec);\n  this.currentSpec = spec;\n\n  if (func) {\n    spec.runs(func);\n  }\n\n  return spec;\n};\n\njasmine.Env.prototype.xit = function(desc, func) {\n  return {\n    id: this.nextSpecId(),\n    runs: function() {\n    }\n  };\n};\n\njasmine.Env.prototype.compareRegExps_ = function(a, b, mismatchKeys, mismatchValues) {\n  if (a.source != b.source)\n    mismatchValues.push(\"expected pattern /\" + b.source + \"/ is not equal to the pattern /\" + a.source + \"/\");\n\n  if (a.ignoreCase != b.ignoreCase)\n    mismatchValues.push(\"expected modifier i was\" + (b.ignoreCase ? \" \" : \" not \") + \"set and does not equal the origin modifier\");\n\n  if (a.global != b.global)\n    mismatchValues.push(\"expected modifier g was\" + (b.global ? \" \" : \" not \") + \"set and does not equal the origin modifier\");\n\n  if (a.multiline != b.multiline)\n    mismatchValues.push(\"expected modifier m was\" + (b.multiline ? \" \" : \" not \") + \"set and does not equal the origin modifier\");\n\n  if (a.sticky != b.sticky)\n    mismatchValues.push(\"expected modifier y was\" + (b.sticky ? \" \" : \" not \") + \"set and does not equal the origin modifier\");\n\n  return (mismatchValues.length === 0);\n};\n\njasmine.Env.prototype.compareObjects_ = function(a, b, mismatchKeys, mismatchValues) {\n  if (a.__Jasmine_been_here_before__ === b && b.__Jasmine_been_here_before__ === a) {\n    return true;\n  }\n\n  a.__Jasmine_been_here_before__ = b;\n  b.__Jasmine_been_here_before__ = a;\n\n  var hasKey = function(obj, keyName) {\n    return obj !== null && obj[keyName] !== jasmine.undefined;\n  };\n\n  for (var property in b) {\n    if (!hasKey(a, property) && hasKey(b, property)) {\n      mismatchKeys.push(\"expected has key '\" + property + \"', but missing from actual.\");\n    }\n  }\n  for (property in a) {\n    if (!hasKey(b, property) && hasKey(a, property)) {\n      mismatchKeys.push(\"expected missing key '\" + property + \"', but present in actual.\");\n    }\n  }\n  for (property in b) {\n    if (property == '__Jasmine_been_here_before__') continue;\n    if (!this.equals_(a[property], b[property], mismatchKeys, mismatchValues)) {\n      mismatchValues.push(\"'\" + property + \"' was '\" + (b[property] ? jasmine.util.htmlEscape(b[property].toString()) : b[property]) + \"' in expected, but was '\" + (a[property] ? jasmine.util.htmlEscape(a[property].toString()) : a[property]) + \"' in actual.\");\n    }\n  }\n\n  if (jasmine.isArray_(a) && jasmine.isArray_(b) && a.length != b.length) {\n    mismatchValues.push(\"arrays were not the same length\");\n  }\n\n  delete a.__Jasmine_been_here_before__;\n  delete b.__Jasmine_been_here_before__;\n  return (mismatchKeys.length === 0 && mismatchValues.length === 0);\n};\n\njasmine.Env.prototype.equals_ = function(a, b, mismatchKeys, mismatchValues) {\n  mismatchKeys = mismatchKeys || [];\n  mismatchValues = mismatchValues || [];\n\n  for (var i = 0; i < this.equalityTesters_.length; i++) {\n    var equalityTester = this.equalityTesters_[i];\n    var result = equalityTester(a, b, this, mismatchKeys, mismatchValues);\n    if (result !== jasmine.undefined) return result;\n  }\n\n  if (a === b) return true;\n\n  if (a === jasmine.undefined || a === null || b === jasmine.undefined || b === null) {\n    return (a == jasmine.undefined && b == jasmine.undefined);\n  }\n\n  if (jasmine.isDomNode(a) && jasmine.isDomNode(b)) {\n    return a === b;\n  }\n\n  if (a instanceof Date && b instanceof Date) {\n    return a.getTime() == b.getTime();\n  }\n\n  if (a.jasmineMatches) {\n    return a.jasmineMatches(b);\n  }\n\n  if (b.jasmineMatches) {\n    return b.jasmineMatches(a);\n  }\n\n  if (a instanceof jasmine.Matchers.ObjectContaining) {\n    return a.matches(b);\n  }\n\n  if (b instanceof jasmine.Matchers.ObjectContaining) {\n    return b.matches(a);\n  }\n\n  if (jasmine.isString_(a) && jasmine.isString_(b)) {\n    return (a == b);\n  }\n\n  if (jasmine.isNumber_(a) && jasmine.isNumber_(b)) {\n    return (a == b);\n  }\n\n  if (a instanceof RegExp && b instanceof RegExp) {\n    return this.compareRegExps_(a, b, mismatchKeys, mismatchValues);\n  }\n\n  if (typeof a === \"object\" && typeof b === \"object\") {\n    return this.compareObjects_(a, b, mismatchKeys, mismatchValues);\n  }\n\n  //Straight check\n  return (a === b);\n};\n\njasmine.Env.prototype.contains_ = function(haystack, needle) {\n  if (jasmine.isArray_(haystack)) {\n    for (var i = 0; i < haystack.length; i++) {\n      if (this.equals_(haystack[i], needle)) return true;\n    }\n    return false;\n  }\n  return haystack.indexOf(needle) >= 0;\n};\n\njasmine.Env.prototype.addEqualityTester = function(equalityTester) {\n  this.equalityTesters_.push(equalityTester);\n};\n/** No-op base class for Jasmine reporters.\n *\n * @constructor\n */\njasmine.Reporter = function() {\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.Reporter.prototype.reportRunnerStarting = function(runner) {\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.Reporter.prototype.reportRunnerResults = function(runner) {\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.Reporter.prototype.reportSuiteResults = function(suite) {\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.Reporter.prototype.reportSpecStarting = function(spec) {\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.Reporter.prototype.reportSpecResults = function(spec) {\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.Reporter.prototype.log = function(str) {\n};\n\n/**\n * Blocks are functions with executable code that make up a spec.\n *\n * @constructor\n * @param {jasmine.Env} env\n * @param {Function} func\n * @param {jasmine.Spec} spec\n */\njasmine.Block = function(env, func, spec) {\n  this.env = env;\n  this.func = func;\n  this.spec = spec;\n};\n\njasmine.Block.prototype.execute = function(onComplete) {\n  if (!jasmine.CATCH_EXCEPTIONS) {\n    this.func.apply(this.spec);\n  }\n  else {\n    try {\n      this.func.apply(this.spec);\n    } catch (e) {\n      this.spec.fail(e);\n    }\n  }\n  onComplete();\n};\n/** JavaScript API reporter.\n *\n * @constructor\n */\njasmine.JsApiReporter = function() {\n  this.started = false;\n  this.finished = false;\n  this.suites_ = [];\n  this.results_ = {};\n};\n\njasmine.JsApiReporter.prototype.reportRunnerStarting = function(runner) {\n  this.started = true;\n  var suites = runner.topLevelSuites();\n  for (var i = 0; i < suites.length; i++) {\n    var suite = suites[i];\n    this.suites_.push(this.summarize_(suite));\n  }\n};\n\njasmine.JsApiReporter.prototype.suites = function() {\n  return this.suites_;\n};\n\njasmine.JsApiReporter.prototype.summarize_ = function(suiteOrSpec) {\n  var isSuite = suiteOrSpec instanceof jasmine.Suite;\n  var summary = {\n    id: suiteOrSpec.id,\n    name: suiteOrSpec.description,\n    type: isSuite ? 'suite' : 'spec',\n    children: []\n  };\n\n  if (isSuite) {\n    var children = suiteOrSpec.children();\n    for (var i = 0; i < children.length; i++) {\n      summary.children.push(this.summarize_(children[i]));\n    }\n  }\n  return summary;\n};\n\njasmine.JsApiReporter.prototype.results = function() {\n  return this.results_;\n};\n\njasmine.JsApiReporter.prototype.resultsForSpec = function(specId) {\n  return this.results_[specId];\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.JsApiReporter.prototype.reportRunnerResults = function(runner) {\n  this.finished = true;\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.JsApiReporter.prototype.reportSuiteResults = function(suite) {\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.JsApiReporter.prototype.reportSpecResults = function(spec) {\n  this.results_[spec.id] = {\n    messages: spec.results().getItems(),\n    result: spec.results().failedCount > 0 ? \"failed\" : \"passed\"\n  };\n};\n\n//noinspection JSUnusedLocalSymbols\njasmine.JsApiReporter.prototype.log = function(str) {\n};\n\njasmine.JsApiReporter.prototype.resultsForSpecs = function(specIds){\n  var results = {};\n  for (var i = 0; i < specIds.length; i++) {\n    var specId = specIds[i];\n    results[specId] = this.summarizeResult_(this.results_[specId]);\n  }\n  return results;\n};\n\njasmine.JsApiReporter.prototype.summarizeResult_ = function(result){\n  var summaryMessages = [];\n  var messagesLength = result.messages.length;\n  for (var messageIndex = 0; messageIndex < messagesLength; messageIndex++) {\n    var resultMessage = result.messages[messageIndex];\n    summaryMessages.push({\n      text: resultMessage.type == 'log' ? resultMessage.toString() : jasmine.undefined,\n      passed: resultMessage.passed ? resultMessage.passed() : true,\n      type: resultMessage.type,\n      message: resultMessage.message,\n      trace: {\n        stack: resultMessage.passed && !resultMessage.passed() ? resultMessage.trace.stack : jasmine.undefined\n      }\n    });\n  }\n\n  return {\n    result : result.result,\n    messages : summaryMessages\n  };\n};\n\n/**\n * @constructor\n * @param {jasmine.Env} env\n * @param actual\n * @param {jasmine.Spec} spec\n */\njasmine.Matchers = function(env, actual, spec, opt_isNot) {\n  this.env = env;\n  this.actual = actual;\n  this.spec = spec;\n  this.isNot = opt_isNot || false;\n  this.reportWasCalled_ = false;\n};\n\n// todo: @deprecated as of Jasmine 0.11, remove soon [xw]\njasmine.Matchers.pp = function(str) {\n  throw new Error(\"jasmine.Matchers.pp() is no longer supported, please use jasmine.pp() instead!\");\n};\n\n// todo: @deprecated Deprecated as of Jasmine 0.10. Rewrite your custom matchers to return true or false. [xw]\njasmine.Matchers.prototype.report = function(result, failing_message, details) {\n  throw new Error(\"As of jasmine 0.11, custom matchers must be implemented differently -- please see jasmine docs\");\n};\n\njasmine.Matchers.wrapInto_ = function(prototype, matchersClass) {\n  for (var methodName in prototype) {\n    if (methodName == 'report') continue;\n    var orig = prototype[methodName];\n    matchersClass.prototype[methodName] = jasmine.Matchers.matcherFn_(methodName, orig);\n  }\n};\n\njasmine.Matchers.matcherFn_ = function(matcherName, matcherFunction) {\n  return function() {\n    var matcherArgs = jasmine.util.argsToArray(arguments);\n    var result = matcherFunction.apply(this, arguments);\n\n    if (this.isNot) {\n      result = !result;\n    }\n\n    if (this.reportWasCalled_) return result;\n\n    var message;\n    if (!result) {\n      if (this.message) {\n        message = this.message.apply(this, arguments);\n        if (jasmine.isArray_(message)) {\n          message = message[this.isNot ? 1 : 0];\n        }\n      } else {\n        var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });\n        message = \"Expected \" + jasmine.pp(this.actual) + (this.isNot ? \" not \" : \" \") + englishyPredicate;\n        if (matcherArgs.length > 0) {\n          for (var i = 0; i < matcherArgs.length; i++) {\n            if (i > 0) message += \",\";\n            message += \" \" + jasmine.pp(matcherArgs[i]);\n          }\n        }\n        message += \".\";\n      }\n    }\n    var expectationResult = new jasmine.ExpectationResult({\n      matcherName: matcherName,\n      passed: result,\n      expected: matcherArgs.length > 1 ? matcherArgs : matcherArgs[0],\n      actual: this.actual,\n      message: message\n    });\n    this.spec.addMatcherResult(expectationResult);\n    return jasmine.undefined;\n  };\n};\n\n\n\n\n/**\n * toBe: compares the actual to the expected using ===\n * @param expected\n */\njasmine.Matchers.prototype.toBe = function(expected) {\n  return this.actual === expected;\n};\n\n/**\n * toNotBe: compares the actual to the expected using !==\n * @param expected\n * @deprecated as of 1.0. Use not.toBe() instead.\n */\njasmine.Matchers.prototype.toNotBe = function(expected) {\n  return this.actual !== expected;\n};\n\n/**\n * toEqual: compares the actual to the expected using common sense equality. Handles Objects, Arrays, etc.\n *\n * @param expected\n */\njasmine.Matchers.prototype.toEqual = function(expected) {\n  return this.env.equals_(this.actual, expected);\n};\n\n/**\n * toNotEqual: compares the actual to the expected using the ! of jasmine.Matchers.toEqual\n * @param expected\n * @deprecated as of 1.0. Use not.toEqual() instead.\n */\njasmine.Matchers.prototype.toNotEqual = function(expected) {\n  return !this.env.equals_(this.actual, expected);\n};\n\n/**\n * Matcher that compares the actual to the expected using a regular expression.  Constructs a RegExp, so takes\n * a pattern or a String.\n *\n * @param expected\n */\njasmine.Matchers.prototype.toMatch = function(expected) {\n  return new RegExp(expected).test(this.actual);\n};\n\n/**\n * Matcher that compares the actual to the expected using the boolean inverse of jasmine.Matchers.toMatch\n * @param expected\n * @deprecated as of 1.0. Use not.toMatch() instead.\n */\njasmine.Matchers.prototype.toNotMatch = function(expected) {\n  return !(new RegExp(expected).test(this.actual));\n};\n\n/**\n * Matcher that compares the actual to jasmine.undefined.\n */\njasmine.Matchers.prototype.toBeDefined = function() {\n  return (this.actual !== jasmine.undefined);\n};\n\n/**\n * Matcher that compares the actual to jasmine.undefined.\n */\njasmine.Matchers.prototype.toBeUndefined = function() {\n  return (this.actual === jasmine.undefined);\n};\n\n/**\n * Matcher that compares the actual to null.\n */\njasmine.Matchers.prototype.toBeNull = function() {\n  return (this.actual === null);\n};\n\n/**\n * Matcher that compares the actual to NaN.\n */\njasmine.Matchers.prototype.toBeNaN = function() {\n\tthis.message = function() {\n\t\treturn [ \"Expected \" + jasmine.pp(this.actual) + \" to be NaN.\" ];\n\t};\n\n\treturn (this.actual !== this.actual);\n};\n\n/**\n * Matcher that boolean not-nots the actual.\n */\njasmine.Matchers.prototype.toBeTruthy = function() {\n  return !!this.actual;\n};\n\n\n/**\n * Matcher that boolean nots the actual.\n */\njasmine.Matchers.prototype.toBeFalsy = function() {\n  return !this.actual;\n};\n\n\n/**\n * Matcher that checks to see if the actual, a Jasmine spy, was called.\n */\njasmine.Matchers.prototype.toHaveBeenCalled = function() {\n  if (arguments.length > 0) {\n    throw new Error('toHaveBeenCalled does not take arguments, use toHaveBeenCalledWith');\n  }\n\n  if (!jasmine.isSpy(this.actual)) {\n    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');\n  }\n\n  this.message = function() {\n    return [\n      \"Expected spy \" + this.actual.identity + \" to have been called.\",\n      \"Expected spy \" + this.actual.identity + \" not to have been called.\"\n    ];\n  };\n\n  return this.actual.wasCalled;\n};\n\n/** @deprecated Use expect(xxx).toHaveBeenCalled() instead */\njasmine.Matchers.prototype.wasCalled = jasmine.Matchers.prototype.toHaveBeenCalled;\n\n/**\n * Matcher that checks to see if the actual, a Jasmine spy, was not called.\n *\n * @deprecated Use expect(xxx).not.toHaveBeenCalled() instead\n */\njasmine.Matchers.prototype.wasNotCalled = function() {\n  if (arguments.length > 0) {\n    throw new Error('wasNotCalled does not take arguments');\n  }\n\n  if (!jasmine.isSpy(this.actual)) {\n    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');\n  }\n\n  this.message = function() {\n    return [\n      \"Expected spy \" + this.actual.identity + \" to not have been called.\",\n      \"Expected spy \" + this.actual.identity + \" to have been called.\"\n    ];\n  };\n\n  return !this.actual.wasCalled;\n};\n\n/**\n * Matcher that checks to see if the actual, a Jasmine spy, was called with a set of parameters.\n *\n * @example\n *\n */\njasmine.Matchers.prototype.toHaveBeenCalledWith = function() {\n  var expectedArgs = jasmine.util.argsToArray(arguments);\n  if (!jasmine.isSpy(this.actual)) {\n    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');\n  }\n  this.message = function() {\n    var invertedMessage = \"Expected spy \" + this.actual.identity + \" not to have been called with \" + jasmine.pp(expectedArgs) + \" but it was.\";\n    var positiveMessage = \"\";\n    if (this.actual.callCount === 0) {\n      positiveMessage = \"Expected spy \" + this.actual.identity + \" to have been called with \" + jasmine.pp(expectedArgs) + \" but it was never called.\";\n    } else {\n      positiveMessage = \"Expected spy \" + this.actual.identity + \" to have been called with \" + jasmine.pp(expectedArgs) + \" but actual calls were \" + jasmine.pp(this.actual.argsForCall).replace(/^\\[ | \\]$/g, '')\n    }\n    return [positiveMessage, invertedMessage];\n  };\n\n  return this.env.contains_(this.actual.argsForCall, expectedArgs);\n};\n\n/** @deprecated Use expect(xxx).toHaveBeenCalledWith() instead */\njasmine.Matchers.prototype.wasCalledWith = jasmine.Matchers.prototype.toHaveBeenCalledWith;\n\n/** @deprecated Use expect(xxx).not.toHaveBeenCalledWith() instead */\njasmine.Matchers.prototype.wasNotCalledWith = function() {\n  var expectedArgs = jasmine.util.argsToArray(arguments);\n  if (!jasmine.isSpy(this.actual)) {\n    throw new Error('Expected a spy, but got ' + jasmine.pp(this.actual) + '.');\n  }\n\n  this.message = function() {\n    return [\n      \"Expected spy not to have been called with \" + jasmine.pp(expectedArgs) + \" but it was\",\n      \"Expected spy to have been called with \" + jasmine.pp(expectedArgs) + \" but it was\"\n    ];\n  };\n\n  return !this.env.contains_(this.actual.argsForCall, expectedArgs);\n};\n\n/**\n * Matcher that checks that the expected item is an element in the actual Array.\n *\n * @param {Object} expected\n */\njasmine.Matchers.prototype.toContain = function(expected) {\n  return this.env.contains_(this.actual, expected);\n};\n\n/**\n * Matcher that checks that the expected item is NOT an element in the actual Array.\n *\n * @param {Object} expected\n * @deprecated as of 1.0. Use not.toContain() instead.\n */\njasmine.Matchers.prototype.toNotContain = function(expected) {\n  return !this.env.contains_(this.actual, expected);\n};\n\njasmine.Matchers.prototype.toBeLessThan = function(expected) {\n  return this.actual < expected;\n};\n\njasmine.Matchers.prototype.toBeGreaterThan = function(expected) {\n  return this.actual > expected;\n};\n\n/**\n * Matcher that checks that the expected item is equal to the actual item\n * up to a given level of decimal precision (default 2).\n *\n * @param {Number} expected\n * @param {Number} precision, as number of decimal places\n */\njasmine.Matchers.prototype.toBeCloseTo = function(expected, precision) {\n  if (!(precision === 0)) {\n    precision = precision || 2;\n  }\n  return Math.abs(expected - this.actual) < (Math.pow(10, -precision) / 2);\n};\n\n/**\n * Matcher that checks that the expected exception was thrown by the actual.\n *\n * @param {String} [expected]\n */\njasmine.Matchers.prototype.toThrow = function(expected) {\n  var result = false;\n  var exception;\n  if (typeof this.actual != 'function') {\n    throw new Error('Actual is not a function');\n  }\n  try {\n    this.actual();\n  } catch (e) {\n    exception = e;\n  }\n  if (exception) {\n    result = (expected === jasmine.undefined || this.env.equals_(exception.message || exception, expected.message || expected));\n  }\n\n  var not = this.isNot ? \"not \" : \"\";\n\n  this.message = function() {\n    if (exception && (expected === jasmine.undefined || !this.env.equals_(exception.message || exception, expected.message || expected))) {\n      return [\"Expected function \" + not + \"to throw\", expected ? expected.message || expected : \"an exception\", \", but it threw\", exception.message || exception].join(' ');\n    } else {\n      return \"Expected function to throw an exception.\";\n    }\n  };\n\n  return result;\n};\n\njasmine.Matchers.Any = function(expectedClass) {\n  this.expectedClass = expectedClass;\n};\n\njasmine.Matchers.Any.prototype.jasmineMatches = function(other) {\n  if (this.expectedClass == String) {\n    return typeof other == 'string' || other instanceof String;\n  }\n\n  if (this.expectedClass == Number) {\n    return typeof other == 'number' || other instanceof Number;\n  }\n\n  if (this.expectedClass == Function) {\n    return typeof other == 'function' || other instanceof Function;\n  }\n\n  if (this.expectedClass == Object) {\n    return typeof other == 'object';\n  }\n\n  return other instanceof this.expectedClass;\n};\n\njasmine.Matchers.Any.prototype.jasmineToString = function() {\n  return '<jasmine.any(' + this.expectedClass + ')>';\n};\n\njasmine.Matchers.ObjectContaining = function (sample) {\n  this.sample = sample;\n};\n\njasmine.Matchers.ObjectContaining.prototype.jasmineMatches = function(other, mismatchKeys, mismatchValues) {\n  mismatchKeys = mismatchKeys || [];\n  mismatchValues = mismatchValues || [];\n\n  var env = jasmine.getEnv();\n\n  var hasKey = function(obj, keyName) {\n    return obj != null && obj[keyName] !== jasmine.undefined;\n  };\n\n  for (var property in this.sample) {\n    if (!hasKey(other, property) && hasKey(this.sample, property)) {\n      mismatchKeys.push(\"expected has key '\" + property + \"', but missing from actual.\");\n    }\n    else if (!env.equals_(this.sample[property], other[property], mismatchKeys, mismatchValues)) {\n      mismatchValues.push(\"'\" + property + \"' was '\" + (other[property] ? jasmine.util.htmlEscape(other[property].toString()) : other[property]) + \"' in expected, but was '\" + (this.sample[property] ? jasmine.util.htmlEscape(this.sample[property].toString()) : this.sample[property]) + \"' in actual.\");\n    }\n  }\n\n  return (mismatchKeys.length === 0 && mismatchValues.length === 0);\n};\n\njasmine.Matchers.ObjectContaining.prototype.jasmineToString = function () {\n  return \"<jasmine.objectContaining(\" + jasmine.pp(this.sample) + \")>\";\n};\n// Mock setTimeout, clearTimeout\n// Contributed by Pivotal Computer Systems, www.pivotalsf.com\n\njasmine.FakeTimer = function() {\n  this.reset();\n\n  var self = this;\n  self.setTimeout = function(funcToCall, millis) {\n    self.timeoutsMade++;\n    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, false);\n    return self.timeoutsMade;\n  };\n\n  self.setInterval = function(funcToCall, millis) {\n    self.timeoutsMade++;\n    self.scheduleFunction(self.timeoutsMade, funcToCall, millis, true);\n    return self.timeoutsMade;\n  };\n\n  self.clearTimeout = function(timeoutKey) {\n    self.scheduledFunctions[timeoutKey] = jasmine.undefined;\n  };\n\n  self.clearInterval = function(timeoutKey) {\n    self.scheduledFunctions[timeoutKey] = jasmine.undefined;\n  };\n\n};\n\njasmine.FakeTimer.prototype.reset = function() {\n  this.timeoutsMade = 0;\n  this.scheduledFunctions = {};\n  this.nowMillis = 0;\n};\n\njasmine.FakeTimer.prototype.tick = function(millis) {\n  var oldMillis = this.nowMillis;\n  var newMillis = oldMillis + millis;\n  this.runFunctionsWithinRange(oldMillis, newMillis);\n  this.nowMillis = newMillis;\n};\n\njasmine.FakeTimer.prototype.runFunctionsWithinRange = function(oldMillis, nowMillis) {\n  var scheduledFunc;\n  var funcsToRun = [];\n  for (var timeoutKey in this.scheduledFunctions) {\n    scheduledFunc = this.scheduledFunctions[timeoutKey];\n    if (scheduledFunc != jasmine.undefined &&\n        scheduledFunc.runAtMillis >= oldMillis &&\n        scheduledFunc.runAtMillis <= nowMillis) {\n      funcsToRun.push(scheduledFunc);\n      this.scheduledFunctions[timeoutKey] = jasmine.undefined;\n    }\n  }\n\n  if (funcsToRun.length > 0) {\n    funcsToRun.sort(function(a, b) {\n      return a.runAtMillis - b.runAtMillis;\n    });\n    for (var i = 0; i < funcsToRun.length; ++i) {\n      try {\n        var funcToRun = funcsToRun[i];\n        this.nowMillis = funcToRun.runAtMillis;\n        funcToRun.funcToCall();\n        if (funcToRun.recurring) {\n          this.scheduleFunction(funcToRun.timeoutKey,\n              funcToRun.funcToCall,\n              funcToRun.millis,\n              true);\n        }\n      } catch(e) {\n      }\n    }\n    this.runFunctionsWithinRange(oldMillis, nowMillis);\n  }\n};\n\njasmine.FakeTimer.prototype.scheduleFunction = function(timeoutKey, funcToCall, millis, recurring) {\n  this.scheduledFunctions[timeoutKey] = {\n    runAtMillis: this.nowMillis + millis,\n    funcToCall: funcToCall,\n    recurring: recurring,\n    timeoutKey: timeoutKey,\n    millis: millis\n  };\n};\n\n/**\n * @namespace\n */\njasmine.Clock = {\n  defaultFakeTimer: new jasmine.FakeTimer(),\n\n  reset: function() {\n    jasmine.Clock.assertInstalled();\n    jasmine.Clock.defaultFakeTimer.reset();\n  },\n\n  tick: function(millis) {\n    jasmine.Clock.assertInstalled();\n    jasmine.Clock.defaultFakeTimer.tick(millis);\n  },\n\n  runFunctionsWithinRange: function(oldMillis, nowMillis) {\n    jasmine.Clock.defaultFakeTimer.runFunctionsWithinRange(oldMillis, nowMillis);\n  },\n\n  scheduleFunction: function(timeoutKey, funcToCall, millis, recurring) {\n    jasmine.Clock.defaultFakeTimer.scheduleFunction(timeoutKey, funcToCall, millis, recurring);\n  },\n\n  useMock: function() {\n    if (!jasmine.Clock.isInstalled()) {\n      var spec = jasmine.getEnv().currentSpec;\n      spec.after(jasmine.Clock.uninstallMock);\n\n      jasmine.Clock.installMock();\n    }\n  },\n\n  installMock: function() {\n    jasmine.Clock.installed = jasmine.Clock.defaultFakeTimer;\n  },\n\n  uninstallMock: function() {\n    jasmine.Clock.assertInstalled();\n    jasmine.Clock.installed = jasmine.Clock.real;\n  },\n\n  real: {\n    setTimeout: jasmine.getGlobal().setTimeout,\n    clearTimeout: jasmine.getGlobal().clearTimeout,\n    setInterval: jasmine.getGlobal().setInterval,\n    clearInterval: jasmine.getGlobal().clearInterval\n  },\n\n  assertInstalled: function() {\n    if (!jasmine.Clock.isInstalled()) {\n      throw new Error(\"Mock clock is not installed, use jasmine.Clock.useMock()\");\n    }\n  },\n\n  isInstalled: function() {\n    return jasmine.Clock.installed == jasmine.Clock.defaultFakeTimer;\n  },\n\n  installed: null\n};\njasmine.Clock.installed = jasmine.Clock.real;\n\n//else for IE support\njasmine.getGlobal().setTimeout = function(funcToCall, millis) {\n  if (jasmine.Clock.installed.setTimeout.apply) {\n    return jasmine.Clock.installed.setTimeout.apply(this, arguments);\n  } else {\n    return jasmine.Clock.installed.setTimeout(funcToCall, millis);\n  }\n};\n\njasmine.getGlobal().setInterval = function(funcToCall, millis) {\n  if (jasmine.Clock.installed.setInterval.apply) {\n    return jasmine.Clock.installed.setInterval.apply(this, arguments);\n  } else {\n    return jasmine.Clock.installed.setInterval(funcToCall, millis);\n  }\n};\n\njasmine.getGlobal().clearTimeout = function(timeoutKey) {\n  if (jasmine.Clock.installed.clearTimeout.apply) {\n    return jasmine.Clock.installed.clearTimeout.apply(this, arguments);\n  } else {\n    return jasmine.Clock.installed.clearTimeout(timeoutKey);\n  }\n};\n\njasmine.getGlobal().clearInterval = function(timeoutKey) {\n  if (jasmine.Clock.installed.clearTimeout.apply) {\n    return jasmine.Clock.installed.clearInterval.apply(this, arguments);\n  } else {\n    return jasmine.Clock.installed.clearInterval(timeoutKey);\n  }\n};\n\n/**\n * @constructor\n */\njasmine.MultiReporter = function() {\n  this.subReporters_ = [];\n};\njasmine.util.inherit(jasmine.MultiReporter, jasmine.Reporter);\n\njasmine.MultiReporter.prototype.addReporter = function(reporter) {\n  this.subReporters_.push(reporter);\n};\n\n(function() {\n  var functionNames = [\n    \"reportRunnerStarting\",\n    \"reportRunnerResults\",\n    \"reportSuiteResults\",\n    \"reportSpecStarting\",\n    \"reportSpecResults\",\n    \"log\"\n  ];\n  for (var i = 0; i < functionNames.length; i++) {\n    var functionName = functionNames[i];\n    jasmine.MultiReporter.prototype[functionName] = (function(functionName) {\n      return function() {\n        for (var j = 0; j < this.subReporters_.length; j++) {\n          var subReporter = this.subReporters_[j];\n          if (subReporter[functionName]) {\n            subReporter[functionName].apply(subReporter, arguments);\n          }\n        }\n      };\n    })(functionName);\n  }\n})();\n/**\n * Holds results for a set of Jasmine spec. Allows for the results array to hold another jasmine.NestedResults\n *\n * @constructor\n */\njasmine.NestedResults = function() {\n  /**\n   * The total count of results\n   */\n  this.totalCount = 0;\n  /**\n   * Number of passed results\n   */\n  this.passedCount = 0;\n  /**\n   * Number of failed results\n   */\n  this.failedCount = 0;\n  /**\n   * Was this suite/spec skipped?\n   */\n  this.skipped = false;\n  /**\n   * @ignore\n   */\n  this.items_ = [];\n};\n\n/**\n * Roll up the result counts.\n *\n * @param result\n */\njasmine.NestedResults.prototype.rollupCounts = function(result) {\n  this.totalCount += result.totalCount;\n  this.passedCount += result.passedCount;\n  this.failedCount += result.failedCount;\n};\n\n/**\n * Adds a log message.\n * @param values Array of message parts which will be concatenated later.\n */\njasmine.NestedResults.prototype.log = function(values) {\n  this.items_.push(new jasmine.MessageResult(values));\n};\n\n/**\n * Getter for the results: message & results.\n */\njasmine.NestedResults.prototype.getItems = function() {\n  return this.items_;\n};\n\n/**\n * Adds a result, tracking counts (total, passed, & failed)\n * @param {jasmine.ExpectationResult|jasmine.NestedResults} result\n */\njasmine.NestedResults.prototype.addResult = function(result) {\n  if (result.type != 'log') {\n    if (result.items_) {\n      this.rollupCounts(result);\n    } else {\n      this.totalCount++;\n      if (result.passed()) {\n        this.passedCount++;\n      } else {\n        this.failedCount++;\n      }\n    }\n  }\n  this.items_.push(result);\n};\n\n/**\n * @returns {Boolean} True if <b>everything</b> below passed\n */\njasmine.NestedResults.prototype.passed = function() {\n  return this.passedCount === this.totalCount;\n};\n/**\n * Base class for pretty printing for expectation results.\n */\njasmine.PrettyPrinter = function() {\n  this.ppNestLevel_ = 0;\n};\n\n/**\n * Formats a value in a nice, human-readable string.\n *\n * @param value\n */\njasmine.PrettyPrinter.prototype.format = function(value) {\n  this.ppNestLevel_++;\n  try {\n    if (value === jasmine.undefined) {\n      this.emitScalar('undefined');\n    } else if (value === null) {\n      this.emitScalar('null');\n    } else if (value === jasmine.getGlobal()) {\n      this.emitScalar('<global>');\n    } else if (value.jasmineToString) {\n      this.emitScalar(value.jasmineToString());\n    } else if (typeof value === 'string') {\n      this.emitString(value);\n    } else if (jasmine.isSpy(value)) {\n      this.emitScalar(\"spy on \" + value.identity);\n    } else if (value instanceof RegExp) {\n      this.emitScalar(value.toString());\n    } else if (typeof value === 'function') {\n      this.emitScalar('Function');\n    } else if (typeof value.nodeType === 'number') {\n      this.emitScalar('HTMLNode');\n    } else if (value instanceof Date) {\n      this.emitScalar('Date(' + value + ')');\n    } else if (value.__Jasmine_been_here_before__) {\n      this.emitScalar('<circular reference: ' + (jasmine.isArray_(value) ? 'Array' : 'Object') + '>');\n    } else if (jasmine.isArray_(value) || typeof value == 'object') {\n      value.__Jasmine_been_here_before__ = true;\n      if (jasmine.isArray_(value)) {\n        this.emitArray(value);\n      } else {\n        this.emitObject(value);\n      }\n      delete value.__Jasmine_been_here_before__;\n    } else {\n      this.emitScalar(value.toString());\n    }\n  } finally {\n    this.ppNestLevel_--;\n  }\n};\n\njasmine.PrettyPrinter.prototype.iterateObject = function(obj, fn) {\n  for (var property in obj) {\n    if (!Object.prototype.hasOwnProperty.call(obj, property)) continue;\n    if (property == '__Jasmine_been_here_before__') continue;\n    fn(property, obj.__lookupGetter__ ? (obj.__lookupGetter__(property) !== jasmine.undefined &&\n                                         obj.__lookupGetter__(property) !== null) : false);\n  }\n};\n\njasmine.PrettyPrinter.prototype.emitArray = jasmine.unimplementedMethod_;\njasmine.PrettyPrinter.prototype.emitObject = jasmine.unimplementedMethod_;\njasmine.PrettyPrinter.prototype.emitScalar = jasmine.unimplementedMethod_;\njasmine.PrettyPrinter.prototype.emitString = jasmine.unimplementedMethod_;\n\njasmine.StringPrettyPrinter = function() {\n  jasmine.PrettyPrinter.call(this);\n\n  this.string = '';\n};\njasmine.util.inherit(jasmine.StringPrettyPrinter, jasmine.PrettyPrinter);\n\njasmine.StringPrettyPrinter.prototype.emitScalar = function(value) {\n  this.append(value);\n};\n\njasmine.StringPrettyPrinter.prototype.emitString = function(value) {\n  this.append(\"'\" + value + \"'\");\n};\n\njasmine.StringPrettyPrinter.prototype.emitArray = function(array) {\n  if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) {\n    this.append(\"Array\");\n    return;\n  }\n\n  this.append('[ ');\n  for (var i = 0; i < array.length; i++) {\n    if (i > 0) {\n      this.append(', ');\n    }\n    this.format(array[i]);\n  }\n  this.append(' ]');\n};\n\njasmine.StringPrettyPrinter.prototype.emitObject = function(obj) {\n  if (this.ppNestLevel_ > jasmine.MAX_PRETTY_PRINT_DEPTH) {\n    this.append(\"Object\");\n    return;\n  }\n\n  var self = this;\n  this.append('{ ');\n  var first = true;\n\n  this.iterateObject(obj, function(property, isGetter) {\n    if (first) {\n      first = false;\n    } else {\n      self.append(', ');\n    }\n\n    self.append(property);\n    self.append(' : ');\n    if (isGetter) {\n      self.append('<getter>');\n    } else {\n      self.format(obj[property]);\n    }\n  });\n\n  this.append(' }');\n};\n\njasmine.StringPrettyPrinter.prototype.append = function(value) {\n  this.string += value;\n};\njasmine.Queue = function(env) {\n  this.env = env;\n\n  // parallel to blocks. each true value in this array means the block will\n  // get executed even if we abort\n  this.ensured = [];\n  this.blocks = [];\n  this.running = false;\n  this.index = 0;\n  this.offset = 0;\n  this.abort = false;\n};\n\njasmine.Queue.prototype.addBefore = function(block, ensure) {\n  if (ensure === jasmine.undefined) {\n    ensure = false;\n  }\n\n  this.blocks.unshift(block);\n  this.ensured.unshift(ensure);\n};\n\njasmine.Queue.prototype.add = function(block, ensure) {\n  if (ensure === jasmine.undefined) {\n    ensure = false;\n  }\n\n  this.blocks.push(block);\n  this.ensured.push(ensure);\n};\n\njasmine.Queue.prototype.insertNext = function(block, ensure) {\n  if (ensure === jasmine.undefined) {\n    ensure = false;\n  }\n\n  this.ensured.splice((this.index + this.offset + 1), 0, ensure);\n  this.blocks.splice((this.index + this.offset + 1), 0, block);\n  this.offset++;\n};\n\njasmine.Queue.prototype.start = function(onComplete) {\n  this.running = true;\n  this.onComplete = onComplete;\n  this.next_();\n};\n\njasmine.Queue.prototype.isRunning = function() {\n  return this.running;\n};\n\njasmine.Queue.LOOP_DONT_RECURSE = true;\n\njasmine.Queue.prototype.next_ = function() {\n  var self = this;\n  var goAgain = true;\n\n  while (goAgain) {\n    goAgain = false;\n\n    if (self.index < self.blocks.length && !(this.abort && !this.ensured[self.index])) {\n      var calledSynchronously = true;\n      var completedSynchronously = false;\n\n      var onComplete = function () {\n        if (jasmine.Queue.LOOP_DONT_RECURSE && calledSynchronously) {\n          completedSynchronously = true;\n          return;\n        }\n\n        if (self.blocks[self.index] && self.blocks[self.index].abort) {\n          self.abort = true;\n        }\n\n        self.offset = 0;\n        self.index++;\n\n        var now = new Date().getTime();\n        if (self.env.updateInterval && now - self.env.lastUpdate > self.env.updateInterval) {\n          self.env.lastUpdate = now;\n          self.env.setTimeout(function() {\n            self.next_();\n          }, 0);\n        } else {\n          if (jasmine.Queue.LOOP_DONT_RECURSE && completedSynchronously) {\n            goAgain = true;\n          } else {\n            self.next_();\n          }\n        }\n      };\n      self.blocks[self.index].execute(onComplete);\n\n      calledSynchronously = false;\n      if (completedSynchronously) {\n        onComplete();\n      }\n\n    } else {\n      self.running = false;\n      if (self.onComplete) {\n        self.onComplete();\n      }\n    }\n  }\n};\n\njasmine.Queue.prototype.results = function() {\n  var results = new jasmine.NestedResults();\n  for (var i = 0; i < this.blocks.length; i++) {\n    if (this.blocks[i].results) {\n      results.addResult(this.blocks[i].results());\n    }\n  }\n  return results;\n};\n\n\n/**\n * Runner\n *\n * @constructor\n * @param {jasmine.Env} env\n */\njasmine.Runner = function(env) {\n  var self = this;\n  self.env = env;\n  self.queue = new jasmine.Queue(env);\n  self.before_ = [];\n  self.after_ = [];\n  self.suites_ = [];\n};\n\njasmine.Runner.prototype.execute = function() {\n  var self = this;\n  if (self.env.reporter.reportRunnerStarting) {\n    self.env.reporter.reportRunnerStarting(this);\n  }\n  self.queue.start(function () {\n    self.finishCallback();\n  });\n};\n\njasmine.Runner.prototype.beforeEach = function(beforeEachFunction) {\n  beforeEachFunction.typeName = 'beforeEach';\n  this.before_.splice(0,0,beforeEachFunction);\n};\n\njasmine.Runner.prototype.afterEach = function(afterEachFunction) {\n  afterEachFunction.typeName = 'afterEach';\n  this.after_.splice(0,0,afterEachFunction);\n};\n\n\njasmine.Runner.prototype.finishCallback = function() {\n  this.env.reporter.reportRunnerResults(this);\n};\n\njasmine.Runner.prototype.addSuite = function(suite) {\n  this.suites_.push(suite);\n};\n\njasmine.Runner.prototype.add = function(block) {\n  if (block instanceof jasmine.Suite) {\n    this.addSuite(block);\n  }\n  this.queue.add(block);\n};\n\njasmine.Runner.prototype.specs = function () {\n  var suites = this.suites();\n  var specs = [];\n  for (var i = 0; i < suites.length; i++) {\n    specs = specs.concat(suites[i].specs());\n  }\n  return specs;\n};\n\njasmine.Runner.prototype.suites = function() {\n  return this.suites_;\n};\n\njasmine.Runner.prototype.topLevelSuites = function() {\n  var topLevelSuites = [];\n  for (var i = 0; i < this.suites_.length; i++) {\n    if (!this.suites_[i].parentSuite) {\n      topLevelSuites.push(this.suites_[i]);\n    }\n  }\n  return topLevelSuites;\n};\n\njasmine.Runner.prototype.results = function() {\n  return this.queue.results();\n};\n/**\n * Internal representation of a Jasmine specification, or test.\n *\n * @constructor\n * @param {jasmine.Env} env\n * @param {jasmine.Suite} suite\n * @param {String} description\n */\njasmine.Spec = function(env, suite, description) {\n  if (!env) {\n    throw new Error('jasmine.Env() required');\n  }\n  if (!suite) {\n    throw new Error('jasmine.Suite() required');\n  }\n  var spec = this;\n  spec.id = env.nextSpecId ? env.nextSpecId() : null;\n  spec.env = env;\n  spec.suite = suite;\n  spec.description = description;\n  spec.queue = new jasmine.Queue(env);\n\n  spec.afterCallbacks = [];\n  spec.spies_ = [];\n\n  spec.results_ = new jasmine.NestedResults();\n  spec.results_.description = description;\n  spec.matchersClass = null;\n};\n\njasmine.Spec.prototype.getFullName = function() {\n  return this.suite.getFullName() + ' ' + this.description + '.';\n};\n\n\njasmine.Spec.prototype.results = function() {\n  return this.results_;\n};\n\n/**\n * All parameters are pretty-printed and concatenated together, then written to the spec's output.\n *\n * Be careful not to leave calls to <code>jasmine.log</code> in production code.\n */\njasmine.Spec.prototype.log = function() {\n  return this.results_.log(arguments);\n};\n\njasmine.Spec.prototype.runs = function (func) {\n  var block = new jasmine.Block(this.env, func, this);\n  this.addToQueue(block);\n  return this;\n};\n\njasmine.Spec.prototype.addToQueue = function (block) {\n  if (this.queue.isRunning()) {\n    this.queue.insertNext(block);\n  } else {\n    this.queue.add(block);\n  }\n};\n\n/**\n * @param {jasmine.ExpectationResult} result\n */\njasmine.Spec.prototype.addMatcherResult = function(result) {\n  this.results_.addResult(result);\n};\n\njasmine.Spec.prototype.expect = function(actual) {\n  var positive = new (this.getMatchersClass_())(this.env, actual, this);\n  positive.not = new (this.getMatchersClass_())(this.env, actual, this, true);\n  return positive;\n};\n\n/**\n * Waits a fixed time period before moving to the next block.\n *\n * @deprecated Use waitsFor() instead\n * @param {Number} timeout milliseconds to wait\n */\njasmine.Spec.prototype.waits = function(timeout) {\n  var waitsFunc = new jasmine.WaitsBlock(this.env, timeout, this);\n  this.addToQueue(waitsFunc);\n  return this;\n};\n\n/**\n * Waits for the latchFunction to return true before proceeding to the next block.\n *\n * @param {Function} latchFunction\n * @param {String} optional_timeoutMessage\n * @param {Number} optional_timeout\n */\njasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {\n  var latchFunction_ = null;\n  var optional_timeoutMessage_ = null;\n  var optional_timeout_ = null;\n\n  for (var i = 0; i < arguments.length; i++) {\n    var arg = arguments[i];\n    switch (typeof arg) {\n      case 'function':\n        latchFunction_ = arg;\n        break;\n      case 'string':\n        optional_timeoutMessage_ = arg;\n        break;\n      case 'number':\n        optional_timeout_ = arg;\n        break;\n    }\n  }\n\n  var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);\n  this.addToQueue(waitsForFunc);\n  return this;\n};\n\njasmine.Spec.prototype.fail = function (e) {\n  var expectationResult = new jasmine.ExpectationResult({\n    passed: false,\n    message: e ? jasmine.util.formatException(e) : 'Exception',\n    trace: { stack: e.stack }\n  });\n  this.results_.addResult(expectationResult);\n};\n\njasmine.Spec.prototype.getMatchersClass_ = function() {\n  return this.matchersClass || this.env.matchersClass;\n};\n\njasmine.Spec.prototype.addMatchers = function(matchersPrototype) {\n  var parent = this.getMatchersClass_();\n  var newMatchersClass = function() {\n    parent.apply(this, arguments);\n  };\n  jasmine.util.inherit(newMatchersClass, parent);\n  jasmine.Matchers.wrapInto_(matchersPrototype, newMatchersClass);\n  this.matchersClass = newMatchersClass;\n};\n\njasmine.Spec.prototype.finishCallback = function() {\n  this.env.reporter.reportSpecResults(this);\n};\n\njasmine.Spec.prototype.finish = function(onComplete) {\n  this.removeAllSpies();\n  this.finishCallback();\n  if (onComplete) {\n    onComplete();\n  }\n};\n\njasmine.Spec.prototype.after = function(doAfter) {\n  if (this.queue.isRunning()) {\n    this.queue.add(new jasmine.Block(this.env, doAfter, this), true);\n  } else {\n    this.afterCallbacks.unshift(doAfter);\n  }\n};\n\njasmine.Spec.prototype.execute = function(onComplete) {\n  var spec = this;\n  if (!spec.env.specFilter(spec)) {\n    spec.results_.skipped = true;\n    spec.finish(onComplete);\n    return;\n  }\n\n  this.env.reporter.reportSpecStarting(this);\n\n  spec.env.currentSpec = spec;\n\n  spec.addBeforesAndAftersToQueue();\n\n  spec.queue.start(function () {\n    spec.finish(onComplete);\n  });\n};\n\njasmine.Spec.prototype.addBeforesAndAftersToQueue = function() {\n  var runner = this.env.currentRunner();\n  var i;\n\n  for (var suite = this.suite; suite; suite = suite.parentSuite) {\n    for (i = 0; i < suite.before_.length; i++) {\n      this.queue.addBefore(new jasmine.Block(this.env, suite.before_[i], this));\n    }\n  }\n  for (i = 0; i < runner.before_.length; i++) {\n    this.queue.addBefore(new jasmine.Block(this.env, runner.before_[i], this));\n  }\n  for (i = 0; i < this.afterCallbacks.length; i++) {\n    this.queue.add(new jasmine.Block(this.env, this.afterCallbacks[i], this), true);\n  }\n  for (suite = this.suite; suite; suite = suite.parentSuite) {\n    for (i = 0; i < suite.after_.length; i++) {\n      this.queue.add(new jasmine.Block(this.env, suite.after_[i], this), true);\n    }\n  }\n  for (i = 0; i < runner.after_.length; i++) {\n    this.queue.add(new jasmine.Block(this.env, runner.after_[i], this), true);\n  }\n};\n\njasmine.Spec.prototype.explodes = function() {\n  throw 'explodes function should not have been called';\n};\n\njasmine.Spec.prototype.spyOn = function(obj, methodName, ignoreMethodDoesntExist) {\n  if (obj == jasmine.undefined) {\n    throw \"spyOn could not find an object to spy upon for \" + methodName + \"()\";\n  }\n\n  if (!ignoreMethodDoesntExist && obj[methodName] === jasmine.undefined) {\n    throw methodName + '() method does not exist';\n  }\n\n  if (!ignoreMethodDoesntExist && obj[methodName] && obj[methodName].isSpy) {\n    throw new Error(methodName + ' has already been spied upon');\n  }\n\n  var spyObj = jasmine.createSpy(methodName);\n\n  this.spies_.push(spyObj);\n  spyObj.baseObj = obj;\n  spyObj.methodName = methodName;\n  spyObj.originalValue = obj[methodName];\n\n  obj[methodName] = spyObj;\n\n  return spyObj;\n};\n\njasmine.Spec.prototype.removeAllSpies = function() {\n  for (var i = 0; i < this.spies_.length; i++) {\n    var spy = this.spies_[i];\n    spy.baseObj[spy.methodName] = spy.originalValue;\n  }\n  this.spies_ = [];\n};\n\n/**\n * Internal representation of a Jasmine suite.\n *\n * @constructor\n * @param {jasmine.Env} env\n * @param {String} description\n * @param {Function} specDefinitions\n * @param {jasmine.Suite} parentSuite\n */\njasmine.Suite = function(env, description, specDefinitions, parentSuite) {\n  var self = this;\n  self.id = env.nextSuiteId ? env.nextSuiteId() : null;\n  self.description = description;\n  self.queue = new jasmine.Queue(env);\n  self.parentSuite = parentSuite;\n  self.env = env;\n  self.before_ = [];\n  self.after_ = [];\n  self.children_ = [];\n  self.suites_ = [];\n  self.specs_ = [];\n};\n\njasmine.Suite.prototype.getFullName = function() {\n  var fullName = this.description;\n  for (var parentSuite = this.parentSuite; parentSuite; parentSuite = parentSuite.parentSuite) {\n    fullName = parentSuite.description + ' ' + fullName;\n  }\n  return fullName;\n};\n\njasmine.Suite.prototype.finish = function(onComplete) {\n  this.env.reporter.reportSuiteResults(this);\n  this.finished = true;\n  if (typeof(onComplete) == 'function') {\n    onComplete();\n  }\n};\n\njasmine.Suite.prototype.beforeEach = function(beforeEachFunction) {\n  beforeEachFunction.typeName = 'beforeEach';\n  this.before_.unshift(beforeEachFunction);\n};\n\njasmine.Suite.prototype.afterEach = function(afterEachFunction) {\n  afterEachFunction.typeName = 'afterEach';\n  this.after_.unshift(afterEachFunction);\n};\n\njasmine.Suite.prototype.results = function() {\n  return this.queue.results();\n};\n\njasmine.Suite.prototype.add = function(suiteOrSpec) {\n  this.children_.push(suiteOrSpec);\n  if (suiteOrSpec instanceof jasmine.Suite) {\n    this.suites_.push(suiteOrSpec);\n    this.env.currentRunner().addSuite(suiteOrSpec);\n  } else {\n    this.specs_.push(suiteOrSpec);\n  }\n  this.queue.add(suiteOrSpec);\n};\n\njasmine.Suite.prototype.specs = function() {\n  return this.specs_;\n};\n\njasmine.Suite.prototype.suites = function() {\n  return this.suites_;\n};\n\njasmine.Suite.prototype.children = function() {\n  return this.children_;\n};\n\njasmine.Suite.prototype.execute = function(onComplete) {\n  var self = this;\n  this.queue.start(function () {\n    self.finish(onComplete);\n  });\n};\njasmine.WaitsBlock = function(env, timeout, spec) {\n  this.timeout = timeout;\n  jasmine.Block.call(this, env, null, spec);\n};\n\njasmine.util.inherit(jasmine.WaitsBlock, jasmine.Block);\n\njasmine.WaitsBlock.prototype.execute = function (onComplete) {\n  if (jasmine.VERBOSE) {\n    this.env.reporter.log('>> Jasmine waiting for ' + this.timeout + ' ms...');\n  }\n  this.env.setTimeout(function () {\n    onComplete();\n  }, this.timeout);\n};\n/**\n * A block which waits for some condition to become true, with timeout.\n *\n * @constructor\n * @extends jasmine.Block\n * @param {jasmine.Env} env The Jasmine environment.\n * @param {Number} timeout The maximum time in milliseconds to wait for the condition to become true.\n * @param {Function} latchFunction A function which returns true when the desired condition has been met.\n * @param {String} message The message to display if the desired condition hasn't been met within the given time period.\n * @param {jasmine.Spec} spec The Jasmine spec.\n */\njasmine.WaitsForBlock = function(env, timeout, latchFunction, message, spec) {\n  this.timeout = timeout || env.defaultTimeoutInterval;\n  this.latchFunction = latchFunction;\n  this.message = message;\n  this.totalTimeSpentWaitingForLatch = 0;\n  jasmine.Block.call(this, env, null, spec);\n};\njasmine.util.inherit(jasmine.WaitsForBlock, jasmine.Block);\n\njasmine.WaitsForBlock.TIMEOUT_INCREMENT = 10;\n\njasmine.WaitsForBlock.prototype.execute = function(onComplete) {\n  if (jasmine.VERBOSE) {\n    this.env.reporter.log('>> Jasmine waiting for ' + (this.message || 'something to happen'));\n  }\n\n  if (this.latchFunction.length > 0) { this.waitForExplicitCompletion(onComplete); return; }\n\n  var latchFunctionResult;\n  try {\n    latchFunctionResult = this.latchFunction.apply(this.spec);\n  } catch (e) {\n    this.spec.fail(e);\n    onComplete();\n    return;\n  }\n\n  if (latchFunctionResult) {\n    onComplete();\n  } else if (this.totalTimeSpentWaitingForLatch >= this.timeout) {\n    var message = 'timed out after ' + this.timeout + ' msec waiting for ' + (this.message || 'something to happen');\n    this.spec.fail({\n      name: 'timeout',\n      message: message\n    });\n\n    this.abort = true;\n    onComplete();\n  } else {\n    this.totalTimeSpentWaitingForLatch += jasmine.WaitsForBlock.TIMEOUT_INCREMENT;\n    var self = this;\n    this.env.setTimeout(function() {\n      self.execute(onComplete);\n    }, jasmine.WaitsForBlock.TIMEOUT_INCREMENT);\n  }\n};\n\njasmine.WaitsForBlock.prototype.waitForExplicitCompletion = function(onComplete) {\n  var self = this;\n  var timeoutHandle = this.env.setTimeout(function() {\n    var message = 'timed out after ' + self.timeout + ' msec waiting for ' + (self.message || 'something to happen');\n    self.spec.fail({\n      name: 'timeout',\n      message: message\n    });\n    multiCompletion.canceled = true;\n    self.abort = true;\n    onComplete();\n  }, this.timeout);\n\n  var multiCompletion = new jasmine.WaitsForBlock.MultiCompletion(this.latchFunction.length, this.env, onComplete, timeoutHandle);\n\n  try {\n    this.latchFunction.apply(this.spec, multiCompletion.completionFunctions);\n  } catch (e) {\n    this.spec.fail(e);\n    onComplete();\n    return;\n  }\n};\n\njasmine.WaitsForBlock.MultiCompletion = function(count, env, onComplete, timeoutHandle) {\n  this.count = count;\n  this.env = env;\n  this.onComplete = onComplete;\n  this.timeoutHandle = timeoutHandle;\n  this.completionStatuses = [];\n  this.completionFunctions = [];\n\n  for (var i = 0; i < count; i++) {\n    this.completionStatuses.push(false);\n    this.completionFunctions.push(this.buildCompletionFunction(i));\n  }\n};\n\njasmine.WaitsForBlock.MultiCompletion.prototype.attemptCompletion = function() {\n  if (this.canceled) return;\n  for (var j = 0; j < this.count; j++) {\n    if (!this.completionStatuses[j]) return;\n  }\n  this.env.clearTimeout(this.timeoutHandle);\n  this.onComplete();\n};\n\njasmine.WaitsForBlock.MultiCompletion.prototype.buildCompletionFunction = function(i) {\n  var self = this;\n  var spent = false;\n  return function() {\n    if (spent) return;\n    spent = true;\n    self.completionStatuses[i] = true;\n    self.attemptCompletion();\n  };\n};\n\njasmine.version_= {\n  \"major\": 1,\n  \"minor\": 3,\n  \"build\": 1,\n  \"revision\": 1354556913\n};\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/master-after-each.es6",
    "content": "import pathwatcher from 'pathwatcher';\nimport ReactTestUtils from 'react-addons-test-utils';\nimport {TaskQueue} from 'nylas-exports'\nimport {destroyTestDatabase} from '../../internal_packages/client-sync/spec/helpers'\n\nclass MasterAfterEach {\n  setup(loadSettings, afterEach) {\n    const styleElementsToRestore = NylasEnv.styles.getSnapshot();\n\n    const self = this\n    afterEach(async function masterAfterEach() {\n      await destroyTestDatabase()\n      NylasEnv.packages.deactivatePackages();\n      NylasEnv.menu.template = [];\n\n      if (NylasEnv.state) {\n        delete NylasEnv.state.packageStates;\n      }\n\n      if (!window.debugContent) {\n        document.getElementById('jasmine-content').innerHTML = '';\n      }\n      ReactTestUtils.unmountAll();\n\n      jasmine.unspy(NylasEnv, 'saveSync');\n      self.ensureNoPathSubscriptions();\n\n      NylasEnv.styles.restoreSnapshot(styleElementsToRestore);\n\n      this.removeAllSpies();\n      if (TaskQueue._queue.length > 0) {\n        console.inspect(TaskQueue._queue)\n        TaskQueue._queue = []\n        throw new Error(\"Your test forgot to clean up the TaskQueue\")\n      }\n      waits(0);\n    }); // yield to ui thread to make screen update more frequently\n  }\n\n  ensureNoPathSubscriptions() {\n    const watchedPaths = pathwatcher.getWatchedPaths();\n    pathwatcher.closeAllWatchers();\n    if (watchedPaths.length > 0) {\n      throw new Error(`Leaking subscriptions for paths: ${watchedPaths.join(\", \")}`);\n    }\n  }\n}\n\nexport default new MasterAfterEach()\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/master-before-each.es6",
    "content": "import {\n  Account,\n  TaskQueue,\n  AccountStore,\n  DatabaseStore,\n  ComponentRegistry,\n  MailboxPerspective,\n  FocusedPerspectiveStore,\n} from 'nylas-exports';\nimport {clipboard} from 'electron';\nimport pathwatcher from 'pathwatcher';\n\nimport Config from '../../src/config';\nimport configUtils from '../../src/config-utils'\nimport TimeOverride from './time-override';\nimport nylasTestConstants from './nylas-test-constants'\nimport * as jasmineExtensions from './jasmine-extensions'\n\n\nclass MasterBeforeEach {\n  setup(loadSettings, beforeEach) {\n    this.loadSettings = loadSettings;\n    const self = this;\n\n    beforeEach(function jasmineBeforeEach() {\n      const currentSpec = this;\n      currentSpec.addMatchers({\n        toHaveLength: jasmineExtensions.toHaveLength,\n      })\n\n      self._resetNylasEnv()\n      self._resetDatabase()\n      self._resetPackageManager()\n      self._resetTaskQueue()\n      self._resetTimeOverride()\n      self._resetAccountStore()\n      self._resetConfig()\n      self._resetClipboard()\n      ComponentRegistry._clear();\n\n      advanceClock(1000);\n      TimeOverride.resetSpyData();\n    });\n  }\n\n  _resetNylasEnv() {\n    NylasEnv.testOrganizationUnit = null;\n\n    NylasEnv.workspaceViewParentSelector = '#jasmine-content';\n\n    // Don't actually write to disk\n    spyOn(NylasEnv, 'saveSync');\n\n    // prevent specs from modifying N1's menus\n    spyOn(NylasEnv.menu, 'sendToBrowserProcess');\n\n    FocusedPerspectiveStore._current = MailboxPerspective.forNothing();\n\n    spyOn(pathwatcher.File.prototype, \"detectResurrectionAfterDelay\").andCallFake(function detectResurrection() {\n      return this.detectResurrection();\n    });\n  }\n\n  _resetPackageManager = () => {\n    NylasEnv.packages.packageStates = {};\n  }\n\n  _resetDatabase() {\n    global.localStorage.clear();\n    DatabaseStore._transactionQueue = undefined;\n\n    // If we don't spy on DatabaseStore._query, then\n    // `DatabaseStore.inTransaction` will never complete and cause all\n    // tests that depend on transactions to hang.\n    //\n    // @_query(\"BEGIN IMMEDIATE TRANSACTION\") never resolves because\n    // DatabaseStore._query never runs because the @_open flag is always\n    // false because we never setup the DB when `NylasEnv.inSpecMode` is\n    // true.\n    spyOn(DatabaseStore, '_query')\n    .andCallFake(() => Promise.resolve([]));\n  }\n\n  _resetTaskQueue() {\n    TaskQueue._queue = [];\n    TaskQueue._completed = [];\n    TaskQueue._onlineStatus = true;\n  }\n\n  _resetTimeOverride() {\n    TimeOverride.resetTime();\n    TimeOverride.enableSpies();\n  }\n\n  _resetAccountStore() {\n    // Log in a fake user, and ensure that accountForId, etc. work\n    AccountStore._accounts = [\n      new Account({\n        provider: \"gmail\",\n        name: nylasTestConstants.TEST_ACCOUNT_NAME,\n        emailAddress: nylasTestConstants.TEST_ACCOUNT_EMAIL,\n        organizationUnit: NylasEnv.testOrganizationUnit || 'label',\n        clientId: nylasTestConstants.TEST_ACCOUNT_CLIENT_ID,\n        serverId: nylasTestConstants.TEST_ACCOUNT_ID,\n        aliases: [\n          `${nylasTestConstants.TEST_ACCOUNT_NAME} Alternate <${nylasTestConstants.TEST_ACCOUNT_ALIAS_EMAIL}>`,\n        ],\n      }),\n\n      new Account({\n        provider: \"gmail\",\n        name: 'Second',\n        emailAddress: 'second@gmail.com',\n        organizationUnit: NylasEnv.testOrganizationUnit || 'label',\n        clientId: 'second-test-account-id',\n        serverId: 'second-test-account-id',\n        aliases: [\n          'Second Support <second@gmail.com>',\n          'Second Alternate <second+alternate@gmail.com>',\n          'Second <second+third@gmail.com>',\n        ],\n      }),\n    ];\n  }\n\n  _resetConfig() {\n    // reset config before each spec; don't load or save from/to `config.json`\n    let fakePersistedConfig = {env: 'production'};\n    spyOn(Config.prototype, 'getRawValues').andCallFake(() => {\n      return fakePersistedConfig;\n    }\n    );\n    spyOn(Config.prototype, 'setRawValue')\n    .andCallFake(function setRawValue(keyPath, value) {\n      if (keyPath) {\n        configUtils.setValueForKeyPath(fakePersistedConfig, keyPath, value);\n      } else {\n        fakePersistedConfig = value;\n      }\n      return this.load();\n    });\n    NylasEnv.config = new Config();\n    NylasEnv.loadConfig();\n  }\n\n  _resetClipboard() {\n    let clipboardContent = 'initial clipboard content';\n    spyOn(clipboard, 'writeText').andCallFake(text => {\n      clipboardContent = text;\n    });\n    spyOn(clipboard, 'readText').andCallFake(() => clipboardContent);\n  }\n}\nexport default new MasterBeforeEach()\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/n1-gui-reporter.cjsx",
    "content": "path = require 'path'\n_ = require 'underscore'\n_str = require 'underscore.string'\n{convertStackTrace} = require 'coffeestack'\nReact = require 'react'\nReactDOM = require 'react-dom'\nmarked = require 'marked'\n\nsourceMaps = {}\nformatStackTrace = (spec, message='', stackTrace, indent=\"\") ->\n  return stackTrace unless stackTrace\n\n  jasminePattern = /^\\s*at\\s+.*\\(?.*[/\\\\]jasmine(-[^/\\\\]*)?\\.js:\\d+:\\d+\\)?\\s*$/\n  firstJasmineLinePattern = /^\\s*at [/\\\\].*[/\\\\]jasmine(-[^/\\\\]*)?\\.js:\\d+:\\d+\\)?\\s*$/\n  convertedLines = []\n  for line in stackTrace.split('\\n')\n    convertedLines.push(line) unless jasminePattern.test(line)\n    break if firstJasmineLinePattern.test(line)\n\n  stackTrace = convertStackTrace(convertedLines.join('\\n'), sourceMaps)\n  lines = stackTrace.split('\\n')\n\n  # Remove first line of stack when it is the same as the error message\n  errorMatch = lines[0]?.match(/^Error: (.*)/)\n  lines.shift() if message.trim() is errorMatch?[1]?.trim()\n\n  for line, index in lines\n    # Remove prefix of lines matching: at [object Object].<anonymous> (path:1:2)\n    prefixMatch = line.match(/at \\[object Object\\]\\.<anonymous> \\(([^)]+)\\)/)\n    line = \"at #{prefixMatch[1]}\" if prefixMatch\n\n    # Relativize locations to spec directory\n    lines[index] = line.replace(\"at #{spec.specDirectory}#{path.sep}\", 'at ')\n\n  lines = lines.map (line) -> indent + line.trim()\n  lines.join('\\n')\n\n\nindentationString: (suite, plus=0) ->\n  rootSuite = suite\n  indentLevel = 0 + plus\n  while rootSuite.parentSuite\n    rootSuite = rootSuite.parentSuite\n    indentLevel += 1\n  return [0...indentLevel].map(-> \"  \").join(\"\")\n\n\nsuiteString: (spec) ->\n  descriptions = [spec.suite.description]\n\n  rootSuite = spec.suite\n  while rootSuite.parentSuite\n    indent = indentationString(rootSuite)\n    descriptions.unshift(indent + rootSuite.description)\n    rootSuite = rootSuite.parentSuite\n\n  descriptions.join(\"\\n\")\n\n\nclass N1GuiReporter extends React.Component\n  constructor: (@props) ->\n\n  render: ->\n    <div className=\"spec-reporter\">\n      <div className=\"padded pull-right\">\n        <button className=\"btn reload-button\" onClick={@onReloadSpecs}>Reload Specs</button>\n      </div>\n      <div className=\"symbol-area\">\n        <div className=\"symbol-header\">Core</div>\n        <div className=\"symbol-summary list-unstyled\">{@_renderSpecsOfType('core')}</div>\n      </div>\n      <div className=\"symbol-area\">\n        <div className=\"symbol-header\">Bundled</div>\n        <div className=\"symbol-summary list-unstyled\">{@_renderSpecsOfType('bundled')}</div>\n      </div>\n      <div className=\"symbol-area\">\n        <div className=\"symbol-header\">User</div>\n        <div className=\"symbol-summary list-unstyled\">{@_renderSpecsOfType('user')}</div>\n      </div>\n      {@_renderStatus()}\n      <div className=\"results\">\n        {@_renderFailures()}\n      </div>\n      <div className=\"plain-text-output\">\n        {@props.plainTextOutput}\n      </div>\n    </div>\n\n  _renderSpecsOfType: (type) =>\n    items = []\n    @props.specs.forEach (spec, idx) =>\n      return unless spec.specType is type\n      statusClass = \"pending\"\n      title = undefined\n      results = spec.results()\n      if results\n        if results.skipped\n          statusClass = \"skipped\"\n        else if results.failedCount > 0\n          statusClass = \"failed\"\n          title = spec.getFullName()\n        else if spec.endedAt\n          statusClass = \"passed\"\n\n      items.push <li key={idx} title={title} className=\"spec-summary #{statusClass}\"/>\n\n    items\n\n  _renderFailures: =>\n    # We have an array of specs with `suite` and potentially N `parentSuite` from there.\n    # Create a tree instead.\n    topLevelSuites = []\n\n    failedSpecs = @props.specs.filter (spec) ->\n      spec.endedAt and spec.results().failedCount > 0\n\n    for spec in failedSpecs\n      suite = spec.suite\n      suite = suite.parentSuite while suite.parentSuite\n      if topLevelSuites.indexOf(suite) is -1\n        topLevelSuites.push(suite)\n\n    topLevelSuites.map (suite, idx) =>\n      <SuiteResultView suite={suite} key={idx} allSpecs={failedSpecs} />\n\n  _renderStatus: =>\n    failedCount = 0\n    skippedCount = 0\n    completeCount = 0\n    for spec in @props.specs\n      results = spec.results()\n      continue unless spec.endedAt\n      failedCount += 1 if results.failedCount > 0\n      skippedCount += 1 if results.skipped\n      completeCount += 1 if results.passedCount > 0 and results.failedCount is 0\n\n    if failedCount is 1\n      message = \"#{failedCount} failure\"\n    else\n      message = \"#{failedCount} failures\"\n\n    if skippedCount\n      specCount = \"#{completeCount - skippedCount}/#{@props.specs.length - skippedCount} (#{skippedCount} skipped)\"\n    else\n      specCount = \"#{completeCount}/#{@props.specs.length}\"\n\n    <div className=\"status alert alert-info\">\n      <div className=\"time\"></div>\n      <div className=\"spec-count\">{specCount}</div>\n      <div className=\"message\">{message}</div>\n    </div>\n\n  onReloadSpecs: =>\n    require('electron').remote.getCurrentWindow().reload()\n\n\nclass SuiteResultView extends React.Component\n  @propTypes: ->\n    suite: React.PropTypes.object\n    allSpecs: React.PropTypes.array\n\n  render: ->\n    items = []\n    subsuites = []\n\n    @props.allSpecs.forEach (spec) =>\n      if spec.suite is @props.suite\n        items.push(spec)\n      else\n        suite = spec.suite\n        while suite.parentSuite\n          if suite.parentSuite is @props.suite\n            subsuites.push(suite)\n            return\n          suite = suite.parentSuite\n\n    items = items.map (spec, idx) =>\n      <SpecResultView key={idx} spec={spec} />\n\n    subsuites = subsuites.map (suite, idx) =>\n      <SuiteResultView key={idx} suite={suite} allSpecs={@props.allSpecs} />\n\n    <div className=\"suite\">\n      <div className=\"description\">{@props.suite.description}</div>\n      <div className=\"results\">\n        {items}\n        {subsuites}\n      </div>\n    </div>\n\nclass SpecResultView extends React.Component\n  @propTypes: ->\n    spec: React.PropTypes.object\n\n  render: ->\n    description = @props.spec.description\n    resultItems = @props.spec.results().getItems()\n    description = \"it #{description}\" if description.indexOf('it ') isnt 0\n\n    failures = []\n    for result, idx in resultItems\n      continue if result.passed()\n      stackTrace = formatStackTrace(@props.spec, result.message, result.trace.stack)\n      failures.push(\n        <div key={idx}>\n          <div className=\"result-message fail\">{result.message}</div>\n          <div className=\"stack-trace padded\">{stackTrace}</div>\n        </div>\n      )\n\n    <div className=\"spec\">\n      <div className=\"description\">{description}</div>\n      <div className=\"spec-failures\">{failures}</div>\n    </div>\n\n\n\nel = document.createElement('div')\ndocument.body.appendChild(el)\n\nstartedAt = null\nspecs = []\nplainTextOutput = \"\"\n\nupdate = =>\n  component = <N1GuiReporter\n    startedAt={startedAt}\n    specs={specs}\n  />\n  ReactDOM.render(component, el)\n\nmodule.exports =\n  reportRunnerStarting: (runner) ->\n    specs = runner.specs()\n    startedAt = Date.now()\n    update()\n\n  reportRunnerResults: (runner) ->\n    update()\n\n  reportSuiteResults: (suite) ->\n\n  reportSpecResults: (spec) ->\n    spec.endedAt = Date.now()\n    update()\n\n  reportPlainTextSpecResult: (spec) ->\n    str = \"\"\n    if spec.results().failedCount > 0\n      str += suiteString(spec) + \"\\n\"\n      indent = indentationString(spec.suite, 1)\n      stackIndent = indentationString(spec.suite, 2)\n\n      description = spec.description\n      description = \"it #{description}\" if description.indexOf('it ') isnt 0\n      str += indent + description + \"\\n\"\n\n      for result in spec.results().getItems()\n        continue if result.passed()\n        str += indent + result.message + \"\\n\"\n        stackTrace = formatStackTrace(spec, result.message, result.trace.stack, stackIndent)\n        str += stackTrace + \"\\n\"\n      str += \"\\n\\n\"\n\n    plainTextOutput = plainTextOutput + str\n    update()\n\n  reportSpecStarting: (spec) ->\n    update()\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/n1-spec-loader.es6",
    "content": "/* eslint global-require: 0 */\n/* eslint import/no-dynamic-require: 0 */\nimport _ from 'underscore';\nimport fs from 'fs-plus';\nimport path from 'path';\n\nclass N1SpecLoader {\n  loadSpecs(loadSettings, jasmineEnv) {\n    this.jasmineEnv = jasmineEnv\n    this.loadSettings = loadSettings;\n    if (this.loadSettings.specDirectory) {\n      this._loadSpecsInDir(this.loadSettings.specDirectory);\n      this._setSpecType('user');\n    } else {\n      this._loadAllSpecs()\n    }\n  }\n\n  _loadAllSpecs() {\n    const {resourcePath} = this.loadSettings;\n\n    this._loadSpecsInDir(path.join(resourcePath, 'spec'));\n\n    this._setSpecType('core');\n\n    const fixturesPackagesPath = path.join(__dirname, 'fixtures', 'packages');\n\n    // EDGEHILL_CORE: Look in internal_packages instead of node_modules\n    let packagePaths = [];\n    const iterable = fs.listSync(path.join(resourcePath, \"internal_packages\"));\n    for (let i = 0; i < iterable.length; i++) {\n      const packagePath = iterable[i];\n      if (fs.isDirectorySync(packagePath)) {\n        packagePaths.push(packagePath);\n      }\n    }\n\n    packagePaths = _.uniq(packagePaths);\n\n    packagePaths = _.groupBy(packagePaths, (packagePath) => {\n      if (packagePath.indexOf(`${fixturesPackagesPath}${path.sep}`) === 0) {\n        return 'fixtures';\n      } else if (packagePath.indexOf(`${resourcePath}${path.sep}`) === 0) {\n        return 'bundled';\n      }\n      return 'user';\n    });\n\n    // Run bundled package specs\n    const iterable1 = packagePaths.bundled != null ? packagePaths.bundled : [];\n\n    for (let j = 0; j < iterable1.length; j++) {\n      const packagePath = iterable1[j];\n      this._loadSpecsInDir(path.join(packagePath, 'spec'));\n    }\n    this._setSpecType('bundled');\n\n    // Run user package specs\n    const iterable2 = packagePaths.user != null ? packagePaths.user : [];\n    for (let k = 0; k < iterable2.length; k++) {\n      const packagePath = iterable2[k];\n      this._loadSpecsInDir(path.join(packagePath, 'spec'));\n    }\n    return this._setSpecType('user');\n  }\n\n  _loadSpecsInDir(specDirectory) {\n    const { specFilePattern } = this.loadSettings;\n\n    let regex = /-spec\\.(coffee|js|jsx|cjsx|es6|es)$/;\n    if (_.isString(specFilePattern) && specFilePattern.length > 0) {\n      regex = new RegExp(specFilePattern);\n    }\n\n    for (const specFilePath of fs.listTreeSync(specDirectory)) {\n      if (regex.test(specFilePath)) {\n        require(specFilePath)\n      }\n    }\n\n    this._setSpecDirectory(specDirectory)\n  }\n\n  _setSpecDirectory(specDirectory) {\n    this._setSpecField('specDirectory', specDirectory);\n  }\n\n  _setSpecField(name, value) {\n    const specs = this.jasmineEnv.currentRunner().specs();\n    if (specs.length === 0) { return; }\n\n    for (let i = 0; i < specs.length; i++) {\n      if (specs[i][name]) break;\n      specs[i][name] = value\n    }\n  }\n\n  _setSpecType(specType) {\n    this._setSpecField('specType', specType);\n  }\n\n}\nexport default new N1SpecLoader()\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/n1-spec-runner.es6",
    "content": "/* eslint global-require:0 */\nimport _ from 'underscore';\n\nimport ReactTestUtils from 'react-addons-test-utils';\nimport Config from '../../src/config'\nimport N1SpecLoader from './n1-spec-loader'\nimport TimeReporter from './time-reporter'\nimport N1GuiReporter from './n1-gui-reporter';\nimport jasmineExports from './jasmine';\nimport ConsoleReporter from './console-reporter'\nimport MasterAfterEach from './master-after-each'\nimport MasterBeforeEach from './master-before-each'\nimport nylasTestConstants from './nylas-test-constants'\nimport * as jasmineExtensions from './jasmine-extensions'\nimport * as reactTestUtilsExtensions from './react-test-utils-extensions'\n\nclass N1SpecRunner {\n  runSpecs(loadSettings) {\n    this.loadSettings = loadSettings\n    this._extendGlobalWindow();\n    this._setupJasmine();\n    this._setupNylasEnv();\n    this._setupWindow();\n    Object.assign(ReactTestUtils, reactTestUtilsExtensions)\n    MasterBeforeEach.setup(this.loadSettings, window.beforeEach)\n    MasterAfterEach.setup(this.loadSettings, window.afterEach)\n    N1SpecLoader.loadSpecs(this.loadSettings, this.jasmineEnv);\n    this.jasmineEnv.execute();\n  }\n\n  /**\n   * Put jasmine methods on the global scope so they can be used anywhere\n   * without importing jasmine.\n   */\n  _extendGlobalWindow() {\n    Object.assign(window, {\n      jasmine: jasmineExports.jasmine,\n\n      it: this._makeItAsync(jasmineExports.it),\n      // it: jasmineExports.it,\n      fit: this._makeItAsync(jasmineExports.fit),\n      xit: jasmineExports.xit,\n      runs: jasmineExports.runs,\n      waits: jasmineExports.waits,\n      spyOn: jasmineExports.spyOn,\n      expect: jasmineExports.expect,\n      waitsFor: jasmineExports.waitsFor,\n      describe: jasmineExports.describe,\n      xdescribe: jasmineExports.xdescribe,\n      afterEach: this._makeSurroundAsync(jasmineExports.afterEach),\n      beforeEach: this._makeSurroundAsync(jasmineExports.beforeEach),\n      testNowMoment: jasmineExtensions.testNowMoment,\n      waitsForPromise: jasmineExtensions.waitsForPromise,\n    }, nylasTestConstants)\n\n    this.jasmineEnv = jasmineExports.jasmine.getEnv();\n  }\n\n\n  _runAsync(userFn) {\n    if (!userFn) return true\n    const resp = userFn.apply(this);\n    if (resp && resp.then) {\n      return jasmineExtensions.waitsForPromise(() => {\n        return resp\n      })\n    }\n    return resp\n  }\n\n  _makeItAsync(jasmineIt) {\n    const self = this;\n    return (desc, userFn) => {\n      return jasmineIt(desc, function asyncIt() {\n        self._runAsync.call(this, userFn)\n      })\n    }\n  }\n\n  _makeSurroundAsync(jasmineBeforeAfter) {\n    const self = this;\n    return (userFn) => {\n      return jasmineBeforeAfter(function asyncBeforeAfter() {\n        self._runAsync.call(this, userFn)\n      })\n    }\n  }\n\n  _setupJasmine() {\n    this._addReporters()\n    this._initializeDOM()\n    this._extendJasmineMethods();\n\n    // On load, this will require \"jasmine-focused\" which looks up the\n    // global `jasmine` object and extends onto it:\n    // fdescribe, ffdescribe, fffdescribe, fit, ffit, fffit\n    require('jasmine-tagged');\n\n    // On load this will extend jasmine's `beforeEach`\n    require('jasmine-json');\n  }\n\n  _setupNylasEnv() {\n    // We need to mock the config even before `beforeEach` runs because it\n    // gets accessed on module definitions\n    const fakePersistedConfig = {env: 'production'};\n    NylasEnv.config = new Config();\n    NylasEnv.config.settings = fakePersistedConfig;\n\n    NylasEnv.restoreWindowDimensions();\n    NylasEnv.themes.loadBaseStylesheets();\n    NylasEnv.themes.requireStylesheet('../../static/jasmine');\n    NylasEnv.themes.initialLoadComplete = true;\n    NylasEnv.keymaps.loadKeymaps();\n  }\n\n  _setupWindow() {\n    window.addEventListener('core:close', () => window.close());\n    window.addEventListener('beforeunload', () => {\n      NylasEnv.storeWindowDimensions();\n      return NylasEnv.saveSync();\n    });\n\n    // On load this will extend the window object\n    require('../../src/window');\n  }\n\n  _addReporters() {\n    const timeReporter = new TimeReporter();\n    const consoleReporter = new ConsoleReporter();\n\n    const loadSettings = NylasEnv.getLoadSettings();\n\n    if (loadSettings.jUnitXmlPath) {\n      // jasmine-reporters extends the jasmine global with methods, so needs to\n      // be `required` at runtime. The `jasmine` object has to be attached to the\n      // global scope before it gets extended. This is done in\n      // `_extendGlobalWindow`\n      require('jasmine-reporters');\n      const jUnitXmlReporter = new jasmine.JUnitXmlReporter(loadSettings.jUnitXmlPath, true, true);\n      this.jasmineEnv.addReporter(jUnitXmlReporter);\n    }\n    this.jasmineEnv.addReporter(timeReporter);\n    this.jasmineEnv.addReporter(consoleReporter);\n\n    if (loadSettings.showSpecsInWindow) {\n      this.jasmineEnv.addReporter(N1GuiReporter);\n      NylasEnv.show();\n    } else {\n      // this package's dep `jasmine-focused` also adds methods to the\n      // `jasmine` global\n      // NOTE: this reporter MUST be added last as it exits the test process\n      // when complete, which may result in e.g. your XML output not getting\n      // written to disk if that reporter is added afterward.\n      const N1TerminalReporter = require('./terminal-reporter').default\n\n      const terminalReporter = new N1TerminalReporter();\n      this.jasmineEnv.addReporter(terminalReporter);\n    }\n  }\n\n  _initializeDOM() {\n    const div = document.createElement('div');\n    div.id = 'jasmine-content';\n    document.body.appendChild(div);\n    document.querySelector('html').style.overflow = 'initial';\n    document.querySelector('body').style.overflow = 'initial';\n    document.getElementById(\"application-loading-cover\").remove();\n  }\n\n  _extendJasmineMethods() {\n    const jasmine = jasmineExports.jasmine;\n\n    // Use underscore's definition of equality for toEqual assertions\n    jasmine.getEnv().addEqualityTester(_.isEqual);\n\n    jasmine.unspy = jasmineExtensions.unspy\n    jasmine.attachToDOM = jasmineExtensions.attachToDOM\n\n    const origEmitObject = jasmine.StringPrettyPrinter.prototype.emitObject;\n    jasmine.StringPrettyPrinter.prototype.emitObject = function emitObject(obj) {\n      if (obj.inspect) {\n        return this.append(obj.inspect());\n      }\n      return origEmitObject.call(this, obj);\n    };\n  }\n}\nexport default new N1SpecRunner()\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/nylas-test-constants.es6",
    "content": "export default {\n  TEST_TIME_ZONE: \"America/Los_Angeles\",\n  TEST_PLUGIN_ID: \"test-plugin-id-123\",\n  TEST_ACCOUNT_ID: \"test-account-server-id\",\n  TEST_ACCOUNT_NAME: \"Nylas Test\",\n  TEST_ACCOUNT_EMAIL: \"tester@nylas.com\",\n  TEST_ACCOUNT_CLIENT_ID: \"local-test-account-client-id\",\n  TEST_ACCOUNT_ALIAS_EMAIL: \"tester+alternative@nylas.com\",\n}\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/react-test-utils-extensions.es6",
    "content": "/* eslint react/no-render-return-value: 0 */\nimport _ from 'underscore';\nimport ReactDOM from \"react-dom\";\nimport ReactTestUtils from 'react-addons-test-utils';\n\nexport function scryRenderedComponentsWithTypeAndProps(root, type, props) {\n  if (!root) { throw new Error(\"Must supply a root to scryRenderedComponentsWithTypeAndProps\"); }\n  return _.compact(_.map(ReactTestUtils.scryRenderedComponentsWithType(root, type), (el) => {\n    if (_.isEqual(_.pick(el.props, Object.keys(props)), props)) {\n      return el;\n    }\n    return false;\n  }));\n}\n\nlet ReactElementContainers = [];\n// Override ReactTestUtils.renderIntoDocument so that\n// we can remove all the created elements after the test completes.\nexport function renderIntoDocument(element) {\n  const container = document.createElement('div');\n  ReactElementContainers.push(container);\n  return ReactDOM.render(element, container);\n}\n\nexport function unmountAll() {\n  for (let i = 0; i < ReactElementContainers.length; i++) {\n    const container = ReactElementContainers[i];\n    ReactDOM.unmountComponentAtNode(container);\n  }\n  ReactElementContainers = [];\n}\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/spec-bootstrap.es6",
    "content": "/* eslint import/first: 0 */\n\n// Swap out Node's native Promise for Bluebird, which allows us to\n// do fancy things like handle exceptions inside promise blocks\nglobal.Promise = require('bluebird');\nconst timeout = global.setTimeout;\nPromise.setScheduler((fn) => timeout(fn, 0));\nPromise.config({longStackTraces: true});\n\nimport NylasEnvConstructor from '../../src/nylas-env';\nwindow.NylasEnv = NylasEnvConstructor.loadOrCreate();\n\nNylasEnv.initialize();\nconst loadSettings = NylasEnv.getLoadSettings();\n\n// This must be `required` instead of imported because\n// NylasEnv.initialize() must complete before `nylas-exports` and other\n// globals are available for import via es6 modules.\nrequire('./n1-spec-runner').default.runSpecs(loadSettings)\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/terminal-reporter.es6",
    "content": "import { remote } from 'electron';\nimport {TerminalReporter} from 'jasmine-tagged';\n\nexport default class N1TerminalReporter extends TerminalReporter {\n  constructor(opts = {}) {\n    const options = Object.assign(opts, {\n      color: true,\n      print(str) {\n        return remote.process.stdout.write(str);\n      },\n      onComplete(runner) {\n        if (runner.results().failedCount > 0) {\n          return NylasEnv.exit(1);\n        }\n        return NylasEnv.exit(0);\n      },\n    });\n    super(options)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/time-override.coffee",
    "content": "_ = require 'underscore'\n\n# Public: To make specs easier to test, we make all asynchronous behavior\n# actually synchronous. We do this by overriding all global timeout and\n# Promise functions.\n#\n# You must now manually call `advanceClock()` in order to move the \"clock\"\n# forward.\nclass TimeOverride\n\n  @advanceClock = (delta=1) =>\n    @now += delta\n    callbacks = []\n\n    @timeouts ?= []\n    @timeouts = @timeouts.filter ([id, strikeTime, callback]) =>\n      if strikeTime <= @now\n        callbacks.push(callback)\n        false\n      else\n        true\n\n    callback() for callback in callbacks\n\n  @resetTime = =>\n    @now = 0\n    @timeoutCount = 0\n    @intervalCount = 0\n    @timeouts = []\n    @intervalTimeouts = {}\n    @originalPromiseScheduler = null\n\n  @enableSpies = =>\n    window.advanceClock = @advanceClock\n\n    window.originalSetTimeout = window.setTimeout\n    window.originalSetInterval = window.setInterval\n    spyOn(window, \"setTimeout\").andCallFake @_fakeSetTimeout\n    spyOn(window, \"clearTimeout\").andCallFake @_fakeClearTimeout\n    spyOn(window, \"setInterval\").andCallFake @_fakeSetInterval\n    spyOn(window, \"clearInterval\").andCallFake @_fakeClearInterval\n    spyOn(_._, \"now\").andCallFake => @now\n\n    # spyOn(Date, \"now\").andCallFake => @now\n    # spyOn(Date.prototype, \"getTime\").andCallFake => @now\n\n    @_setPromiseScheduler()\n\n  @_setPromiseScheduler: =>\n    @originalPromiseScheduler ?= Promise.setScheduler (fn) =>\n      window.originalSetTimeout(fn, 0)\n\n  @disableSpies = =>\n    window.advanceClock = null\n\n    jasmine.unspy(window, 'setTimeout')\n    jasmine.unspy(window, 'clearTimeout')\n    jasmine.unspy(window, 'setInterval')\n    jasmine.unspy(window, 'clearInterval')\n\n    jasmine.unspy(_._, \"now\")\n\n    Promise.setScheduler(@originalPromiseScheduler) if @originalPromiseScheduler\n    @originalPromiseScheduler = null\n\n  @resetSpyData = ->\n    window.setTimeout.reset?()\n    window.clearTimeout.reset?()\n    window.setInterval.reset?()\n    window.clearInterval.reset?()\n    Date.now.reset?()\n    Date.prototype.getTime.reset?()\n\n  @_fakeSetTimeout = (callback, ms) =>\n    id = ++@timeoutCount\n    @timeouts.push([id, @now + ms, callback])\n    id\n\n  @_fakeClearTimeout = (idToClear) =>\n    @timeouts ?= []\n    @timeouts = @timeouts.filter ([id]) -> id != idToClear\n\n  @_fakeSetInterval = (callback, ms) =>\n    id = ++@intervalCount\n    action = =>\n      callback()\n      @intervalTimeouts[id] = @_fakeSetTimeout(action, ms)\n    @intervalTimeouts[id] = @_fakeSetTimeout(action, ms)\n    id\n\n  @_fakeClearInterval = (idToClear) =>\n    @_fakeClearTimeout(@intervalTimeouts[idToClear])\n\nmodule.exports = TimeOverride\n"
  },
  {
    "path": "packages/client-app/spec/n1-spec-runner/time-reporter.coffee",
    "content": "_ = require 'underscore'\n_str = require 'underscore.string'\n{jasmine} = require('./jasmine')\n\nmodule.exports =\nclass TimeReporter extends jasmine.Reporter\n\n  constructor: ->\n    window.timedSpecs = []\n    window.timedSuites = {}\n\n    window.logLongestSpec = => @logLongestSpecs(1)\n    window.logLongestSpecs = (number) => @logLongestSpecs(number)\n    window.logLongestSuite = => @logLongestSuites(1)\n    window.logLongestSuites = (number) => @logLongestSuites(number)\n\n  logLongestSuites: (number=10, log) ->\n    return unless window.timedSuites.length > 0\n\n    log ?= (line) -> console.log(line)\n    log \"Longest running suites:\"\n    suites = _.map(window.timedSuites, (key, value) -> [value, key])\n    for suite in _.sortBy(suites, (suite) -> -suite[1])[0...number]\n      time = Math.round(suite[1] / 100) / 10\n      log \"  #{suite[0]} (#{time}s)\"\n    undefined\n\n  logLongestSpecs: (number=10, log) ->\n    return unless window.timedSpecs.length > 0\n\n    log ?= (line) -> console.log(line)\n    log \"Longest running specs:\"\n    for spec in _.sortBy(window.timedSpecs, (spec) -> -spec.time)[0...number]\n      time = Math.round(spec.time / 100) / 10\n      log \"#{spec.description} (#{time}s)\"\n    undefined\n\n  reportSpecStarting: (spec) ->\n    stack = [spec.description]\n    suite = spec.suite\n    while suite\n      stack.unshift suite.description\n      @suite = suite.description\n      suite = suite.parentSuite\n\n    reducer = (memo, description, index) ->\n      if index is 0\n        \"#{description}\"\n      else\n        \"#{memo}\\n#{_str.repeat('  ', index)}#{description}\"\n    @description = _.reduce(stack, reducer, '')\n    @time = Date.now()\n\n  reportSpecResults: (spec) ->\n    return unless @time? and @description?\n\n    duration = Date.now() - @time\n\n    if duration > 0\n      window.timedSpecs.push\n        description: @description\n        time: duration\n        fullName: spec.getFullName()\n\n      if timedSuites[@suite]\n        window.timedSuites[@suite] += duration\n      else\n        window.timedSuites[@suite] = duration\n\n    @time = null\n    @description = null\n"
  },
  {
    "path": "packages/client-app/spec/nylas-api-spec.coffee",
    "content": "_ = require 'underscore'\nfs = require 'fs'\nActions = require('../src/flux/actions').default\nNylasAPI = require('../src/flux/nylas-api').default\nNylasAPIHelpers = require '../src/flux/nylas-api-helpers'\nNylasAPIRequest = require('../src/flux/nylas-api-request').default\nThread = require('../src/flux/models/thread').default\nMessage = require('../src/flux/models/message').default\nAccountStore = require('../src/flux/stores/account-store').default\nDatabaseStore = require('../src/flux/stores/database-store').default\nDatabaseWriter = require('../src/flux/stores/database-writer').default\n\ndescribe \"NylasAPI\", ->\n\n  describe \"handleModel404\", ->\n    it \"should unpersist the model from the cache that was requested\", ->\n      model = new Thread(id: 'threadidhere')\n      spyOn(DatabaseWriter.prototype, 'unpersistModel').andCallFake =>\n        return Promise.resolve()\n      spyOn(DatabaseWriter.prototype, 'find').andCallFake (klass, id) =>\n        return Promise.resolve(model)\n      waitsForPromise ->\n        NylasAPIHelpers.handleModel404(\"/threads/#{model.id}\")\n      runs ->\n        expect(DatabaseWriter.prototype.find).toHaveBeenCalledWith(Thread, model.id)\n        expect(DatabaseWriter.prototype.unpersistModel).toHaveBeenCalledWith(model)\n\n    it \"should not do anything if the model is not in the cache\", ->\n      spyOn(DatabaseWriter.prototype, 'unpersistModel')\n      spyOn(DatabaseWriter.prototype, 'find').andCallFake (klass, id) =>\n        return Promise.resolve(null)\n      waitsForPromise ->\n        NylasAPIHelpers.handleModel404(\"/threads/1234\")\n      runs ->\n        expect(DatabaseWriter.prototype.find).toHaveBeenCalledWith(Thread, '1234')\n        expect(DatabaseWriter.prototype.unpersistModel).not.toHaveBeenCalledWith()\n\n    it \"should not do anything bad if it doesn't recognize the class\", ->\n      spyOn(DatabaseStore, 'find')\n      spyOn(DatabaseWriter.prototype, 'unpersistModel')\n      waitsForPromise ->\n        NylasAPIHelpers.handleModel404(\"/asdasdasd/1234\")\n      runs ->\n        expect(DatabaseStore.find).not.toHaveBeenCalled()\n        expect(DatabaseWriter.prototype.unpersistModel).not.toHaveBeenCalled()\n\n    it \"should not do anything bad if the endpoint only has a single segment\", ->\n      spyOn(DatabaseStore, 'find')\n      spyOn(DatabaseWriter.prototype, 'unpersistModel')\n      waitsForPromise ->\n        NylasAPIHelpers.handleModel404(\"/account\")\n      runs ->\n        expect(DatabaseStore.find).not.toHaveBeenCalled()\n        expect(DatabaseWriter.prototype.unpersistModel).not.toHaveBeenCalled()\n\n  describe \"handleModelResponse\", ->\n    beforeEach ->\n      @stubDB = {}\n      @stubDB.upsertModel = (model) =>\n        @stubDB[model.id] = model\n      spyOn(DatabaseWriter.prototype, \"persistModels\").andCallFake (models) =>\n        models.forEach(@stubDB.upsertModel)\n        Promise.resolve(models)\n      spyOn(DatabaseStore, \"findAll\").andCallFake (klass) =>\n        @testClass?(klass)\n        where: (matcher) =>\n          @testMatcher?(matcher)\n          key = matcher.attr.modelKey\n          val = matcher.val\n          models = Object.values(@stubDB).filter((model) =>\n            if matcher.comparator == '='\n              return model[key] == val\n            else if matcher.comparator == 'in'\n              return val.find((item) -> model[key] == item)\n            throw new Error(\"stubDB doesn't handle comparator: #{matcher.comparator}\")\n          )\n          return Promise.resolve(models)\n\n    # stubDB = ({models, testClass, testMatcher}) ->\n    #   spyOn(DatabaseStore, \"findAll\").andCallFake (klass)  ->\n    #     testClass?(klass)\n    #     where: (matcher) ->\n    #       testMatcher?(matcher)\n    #       Promise.resolve(models)\n\n    it \"should reject if no JSON is provided\", ->\n      waitsForPromise ->\n        NylasAPIHelpers.handleModelResponse()\n        .then -> throw new Error(\"Should reject!\")\n        .catch (err) ->\n          expect(err.message).toEqual \"handleModelResponse with no JSON provided\"\n\n    it \"should resolve if an empty JSON array is provided\", ->\n      waitsForPromise ->\n        NylasAPIHelpers.handleModelResponse([])\n        .then (resp) ->\n          expect(resp).toEqual []\n\n    describe \"if JSON contains objects which are of unknown types\", ->\n      it \"should warn and resolve\", ->\n        spyOn(console, \"warn\")\n        waitsForPromise ->\n          NylasAPIHelpers.handleModelResponse([{id: 'a', object: 'unknown'}])\n          .then (resp) ->\n            expect(resp).toEqual []\n            expect(console.warn).toHaveBeenCalled()\n            expect(console.warn.calls.length).toBe 1\n\n    describe \"if JSON contains the same object more than once\", ->\n      beforeEach ->\n        spyOn(console, \"warn\")\n        @dupes = [\n          {id: 't:a', object: 'thread', message_ids: ['a']}\n          {id: 't:a', object: 'thread', message_ids: ['a']}\n          {id: 't:b', object: 'thread', message_ids: ['b']}\n        ]\n\n      it \"should warn\", ->\n        waitsForPromise =>\n          NylasAPIHelpers.handleModelResponse(@dupes)\n          .then ->\n            expect(console.warn).toHaveBeenCalled()\n            expect(console.warn.calls.length).toBe 1\n\n      it \"should omit duplicates\", ->\n        waitsForPromise =>\n          NylasAPIHelpers.handleModelResponse(@dupes)\n          .then ->\n            models = DatabaseWriter.prototype.persistModels.calls[0].args[0]\n            expect(models.length).toBe 2\n            expect(models[0].id).toBe 't:a'\n            expect(models[1].id).toBe 't:b'\n\n    describe \"when items in the JSON are locked and we are not accepting changes to them\", ->\n      it \"should remove locked models from the set\", ->\n        json = [\n          {id: 't:a', object: 'thread', message_ids: ['a', 'c']}\n          {id: 't:b', object: 'thread', message_ids: ['b']}\n        ]\n        spyOn(NylasAPI.lockTracker, \"acceptRemoteChangesTo\").andCallFake (klass, id) ->\n          if id is \"t:a\" then return false\n\n        @stubDB.upsertModel(new Thread(json[1]))\n        @testMatcher = (whereMatcher) ->\n          expect(whereMatcher.val).toEqual 't:b'\n\n        waitsForPromise =>\n          NylasAPIHelpers.handleModelResponse(json)\n          .then (models) ->\n            expect(models.length).toBe 1\n            models = DatabaseWriter.prototype.persistModels.calls[0].args[0]\n            expect(models.length).toBe 1\n            expect(models[0].id).toBe 't:b'\n\n    describe \"when updating models\", ->\n      Message = require('../src/flux/models/message').default\n      beforeEach ->\n        @json = [\n          {id: 'a', object: 'draft', unread: true}\n          {id: 'b', object: 'draft', starred: true}\n        ]\n        @existing = new Message(id: 'b', unread: true)\n        @stubDB.upsertModel(@existing)\n\n      verifyUpdateHappened = (responseModels) ->\n        changedModels = DatabaseWriter.prototype.persistModels.calls[0].args[0]\n        expect(changedModels.length).toBe 2\n        expect(responseModels.length).toBe 2\n        expect(responseModels[0].id).toBe 'a'\n        expect(responseModels[1].id).toBe 'b'\n\n        threadA = @stubDB['a']\n        threadB = @stubDB['b']\n\n        # New values were updated\n        expect(threadB.starred).toBe true\n        expect(threadA.unread).toBe true\n\n        # Existing values without new values weren't overwritten\n        expect(threadB.unread).toBe true\n\n      it \"updates found models with new data\", ->\n        waitsForPromise =>\n          NylasAPIHelpers.handleModelResponse(@json).then (responseModels) =>\n            verifyUpdateHappened.call(@, responseModels)\n\n      it \"updates if the json version is newer\", ->\n        @existing.version = 9\n        @stubDB.upsertModel(@existing)\n        @json[1].version = 10\n        waitsForPromise =>\n          NylasAPIHelpers.handleModelResponse(@json).then (responseModels) =>\n            verifyUpdateHappened.call(@, responseModels)\n\n      verifyUpdateStopped = (responseModels) ->\n        changedModels = DatabaseWriter.prototype.persistModels.calls[0].args[0]\n        expect(changedModels.length).toBe 1\n        expect(changedModels[0].id).toBe 'a'\n        expect(changedModels[0].unread).toBe true\n        expect(responseModels.length).toBe 2\n        expect(responseModels[1].id).toBe 'b'\n        expect(responseModels[1].starred).toBeUndefined()\n\n      it \"doesn't update if the json version is older\", ->\n        @existing.version = 10\n        @stubDB.upsertModel(@existing)\n        @json[1].version = 9\n        waitsForPromise =>\n          NylasAPIHelpers.handleModelResponse(@json).then (responseModels) =>\n            verifyUpdateStopped.call(@, responseModels)\n\n      it \"doesn't update if it's already sent\", ->\n        @existing.draft = false\n        @stubDB.upsertModel(@existing)\n        @json[1].draft = true\n        waitsForPromise =>\n          NylasAPIHelpers.handleModelResponse(@json).then (responseModels) =>\n            verifyUpdateStopped.call(@, responseModels)\n\n    describe \"handling all types of objects\", ->\n      apiObjectToClassMap =\n        \"file\": require('../src/flux/models/file').default\n        \"event\": require('../src/flux/models/event').default\n        \"label\": require('../src/flux/models/label').default\n        \"folder\": require('../src/flux/models/folder').default\n        \"thread\": require('../src/flux/models/thread').default\n        \"draft\": require('../src/flux/models/message').default\n        \"account\": require('../src/flux/models/account').default\n        \"message\": require('../src/flux/models/message').default\n        \"contact\": require('../src/flux/models/contact').default\n        \"calendar\": require('../src/flux/models/calendar').default\n\n      verifyUpdateHappened = (klass, responseModels) ->\n        changedModels = DatabaseWriter.prototype.persistModels.calls[0].args[0]\n        expect(changedModels.length).toBe 2\n        expect(changedModels[0].id).toBe 'a'\n        expect(changedModels[1].id).toBe 'b'\n        expect(changedModels[0] instanceof klass).toBe true\n        expect(changedModels[1] instanceof klass).toBe true\n        expect(responseModels.length).toBe 2\n        expect(responseModels[0].id).toBe 'a'\n        expect(responseModels[1].id).toBe 'b'\n        expect(responseModels[0] instanceof klass).toBe true\n        expect(responseModels[1] instanceof klass).toBe true\n\n      _.forEach apiObjectToClassMap, (klass, type) ->\n        it \"properly handle the '#{type}' type\", ->\n          json = [\n            {id: 'a', object: type, message_ids: ['1']}\n            {id: 'b', object: type, message_ids: ['2']}\n          ]\n          @stubDB.upsertModel(new klass(id: 'b'))\n\n          verifyUpdate = _.partial(verifyUpdateHappened, klass)\n          waitsForPromise =>\n            NylasAPIHelpers.handleModelResponse(json).then verifyUpdate\n\n    it \"properly reconciles threads\", ->\n      @stubDB.upsertModel(new Thread(serverId: 't:4', unread: true, starred: true))\n      @stubDB.upsertModel(new Message(serverId: '7', threadId: 't:4'))\n      @stubDB.upsertModel(new Message(serverId: '4', threadId: 't:4'))\n\n      json = [{id: 't:7', object: 'thread', message_ids: ['4', '7'], unread: false}]\n      updatedThread = null\n\n      waitsForPromise =>\n        NylasAPIHelpers.handleModelResponse(json).then( =>\n          DatabaseStore.findAll(Thread).where(Thread.attributes.id.in(['t:7']))\n            .then (threads) -> updatedThread = threads[0]\n        )\n      runs ->\n        expect(updatedThread).toBeDefined()\n        expect(updatedThread.unread).toEqual(false)\n        expect(updatedThread.starred).toEqual(true)\n\n\n  describe \"makeDraftDeletionRequest\", ->\n    it \"should make an API request to delete the draft\", ->\n      draft = new Message(accountId: TEST_ACCOUNT_ID, draft: true, clientId: 'asd', serverId: 'asd')\n      spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n        expect(this.options.path).toBe \"/drafts/#{draft.serverId}\"\n        expect(this.options.accountId).toBe TEST_ACCOUNT_ID\n        expect(this.options.method).toBe \"DELETE\"\n      NylasAPIHelpers.makeDraftDeletionRequest(draft)\n\n    it \"should increment the change tracker, preventing any further deltas about the draft\", ->\n      draft = new Message(accountId: TEST_ACCOUNT_ID, draft: true, clientId: 'asd', serverId: 'asd')\n      spyOn(NylasAPI, 'incrementRemoteChangeLock')\n      NylasAPIHelpers.makeDraftDeletionRequest(draft)\n      expect(NylasAPI.incrementRemoteChangeLock).toHaveBeenCalledWith(Message, draft.serverId)\n\n    it \"should not return a promise or anything else, to avoid accidentally making things dependent on the request\", ->\n      draft = new Message(accountId: TEST_ACCOUNT_ID, draft: true, clientId: 'asd', serverId: 'asd')\n      a = NylasAPIHelpers.makeDraftDeletionRequest(draft)\n      expect(a).toBe(undefined)\n\n    it \"should not do anything if the draft is missing a serverId\", ->\n      draft = new Message(accountId: TEST_ACCOUNT_ID, draft: true, clientId: 'asd', serverId: null)\n      spyOn(NylasAPIRequest.prototype, 'run')\n      NylasAPIHelpers.makeDraftDeletionRequest(draft)\n      expect(NylasAPIRequest.prototype.run).not.toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/nylas-env-spec.es6",
    "content": "import { remote } from 'electron';\n\ndescribe(\"the `NylasEnv` global\", function nylasEnvSpec() {\n  describe('window sizing methods', () => {\n    describe('::getPosition and ::setPosition', () =>\n      it('sets the position of the window, and can retrieve the position just set', () => {\n        NylasEnv.setPosition(22, 45);\n        expect(NylasEnv.getPosition()).toEqual({x: 22, y: 45});\n      })\n    );\n\n    describe('::getSize and ::setSize', () => {\n      beforeEach(() => {\n        this.originalSize = NylasEnv.getSize()\n      });\n      afterEach(() => NylasEnv.setSize(this.originalSize.width, this.originalSize.height));\n\n      it('sets the size of the window, and can retrieve the size just set', () => {\n        NylasEnv.setSize(100, 400);\n        expect(NylasEnv.getSize()).toEqual({width: 100, height: 400});\n      });\n    });\n\n    describe('::setMinimumWidth', () => {\n      const win = NylasEnv.getCurrentWindow();\n\n      it(\"sets the minimum width\", () => {\n        const inputMinWidth = 500;\n        win.setMinimumSize(1000, 1000);\n\n        NylasEnv.setMinimumWidth(inputMinWidth);\n\n        const [actualMinWidth] = win.getMinimumSize();\n        expect(actualMinWidth).toBe(inputMinWidth);\n      });\n\n      it(\"sets the current size if minWidth > current width\", () => {\n        const inputMinWidth = 1000;\n        win.setSize(500, 500);\n\n        NylasEnv.setMinimumWidth(inputMinWidth);\n\n        const [actualWidth] = win.getMinimumSize();\n        expect(actualWidth).toBe(inputMinWidth);\n      });\n    });\n\n    describe('::getDefaultWindowDimensions', () => {\n      it(\"returns primary display's work area size if it's small enough\", () => {\n        spyOn(remote.screen, 'getPrimaryDisplay').andReturn({workAreaSize: { width: 1440, height: 900}});\n\n        const out = NylasEnv.getDefaultWindowDimensions();\n        expect(out).toEqual({x: 0, y: 0, width: 1440, height: 900});\n      });\n\n      it(\"caps width at 1440 and centers it, if wider\", () => {\n        spyOn(remote.screen, 'getPrimaryDisplay').andReturn({workAreaSize: { width: 1840, height: 900}});\n\n        const out = NylasEnv.getDefaultWindowDimensions();\n        expect(out).toEqual({x: 200, y: 0, width: 1440, height: 900});\n      });\n\n      it(\"caps height at 900 and centers it, if taller\", () => {\n        spyOn(remote.screen, 'getPrimaryDisplay').andReturn({workAreaSize: { width: 1440, height: 1100}});\n\n        const out = NylasEnv.getDefaultWindowDimensions();\n        expect(out).toEqual({x: 0, y: 100, width: 1440, height: 900});\n      });\n\n      it(\"returns only the max viewport size if it's smaller than the defaults\", () => {\n        spyOn(remote.screen, 'getPrimaryDisplay').andReturn({workAreaSize: { width: 1000, height: 800}});\n\n        const out = NylasEnv.getDefaultWindowDimensions();\n        expect(out).toEqual({x: 0, y: 0, width: 1000, height: 800});\n      });\n\n      it(\"always rounds X and Y\", () => {\n        spyOn(remote.screen, 'getPrimaryDisplay').andReturn({workAreaSize: { width: 1845, height: 955}});\n\n        const out = NylasEnv.getDefaultWindowDimensions();\n        expect(out).toEqual({x: 202, y: 27, width: 1440, height: 900});\n      });\n    });\n  });\n\n\n  describe(\".isReleasedVersion()\", () =>\n    it(\"returns false if the version is a SHA and true otherwise\", () => {\n      let version = '0.1.0';\n      spyOn(NylasEnv, 'getVersion').andCallFake(() => version);\n      expect(NylasEnv.isReleasedVersion()).toBe(true);\n      version = '36b5518';\n      expect(NylasEnv.isReleasedVersion()).toBe(false);\n    })\n  );\n\n  describe(\"error handling\", () => {\n    beforeEach(() => {\n      spyOn(NylasEnv, \"inSpecMode\").andReturn(false)\n      spyOn(NylasEnv, \"inDevMode\").andReturn(false);\n      spyOn(NylasEnv, \"openDevTools\")\n      spyOn(NylasEnv, \"executeJavaScriptInDevTools\")\n      spyOn(NylasEnv.errorLogger, \"reportError\");\n    });\n\n    it(\"Catches errors that make it to window.onerror\", () => {\n      spyOn(NylasEnv, \"reportError\");\n      const e = new Error(\"Test Error\")\n      window.onerror.call(window, e.toString(), 'abc', 2, 3, e);\n      expect(NylasEnv.reportError).toHaveBeenCalled();\n      expect(NylasEnv.reportError.calls[0].args[0]).toBe(e);\n      const extra = NylasEnv.reportError.calls[0].args[1]\n      expect(extra.url).toBe(\"abc\")\n      expect(extra.line).toBe(2)\n      expect(extra.column).toBe(3)\n    });\n\n    it(\"Catches unhandled rejections\", async () => {\n      spyOn(NylasEnv, \"reportError\");\n      const err = new Error(\"TEST\");\n\n      const p = new Promise((resolve, reject) => {\n        reject(err);\n      })\n      p.then(() => {\n        throw new Error(\"Shouldn't resolve\")\n      })\n\n      /**\n       * This test was started from within the `setTimeout` block of the\n       * Node event loop. The unhandled rejection will not get caught\n       * until the \"pending callbacks\" block (which happens next). Since\n       * that happens immediately next it's important that we don't use:\n       *\n       * await new Promise(setImmediate)\n       *\n       * Because of setImmediate's position in the Node event loop\n       * relative to this test and process.on('unhandledRejection'), using\n       * setImmediate would require us to await for it twice.\n       *\n       * We can use the original, no-stubbed-out `setTimeout` to put our\n       * test in the correct spot in the Node event loop relative to\n       * unhandledRejection.\n       */\n      await new Promise((resolve) => {\n        window.originalSetTimeout(resolve, 0)\n      })\n\n      expect(NylasEnv.reportError.callCount).toBe(1);\n      expect(NylasEnv.reportError.calls[0].args[0]).toBe(err);\n    });\n\n    describe(\"reportError\", () => {\n      beforeEach(() => {\n        this.testErr = new Error(\"Test\");\n        spyOn(console, \"error\")\n      });\n\n      it(\"emits will-throw-error\", () => {\n        spyOn(NylasEnv.emitter, \"emit\")\n        NylasEnv.reportError(this.testErr);\n        expect(NylasEnv.emitter.emit).toHaveBeenCalled();\n        expect(NylasEnv.emitter.emit.callCount).toBe(2);\n        expect(NylasEnv.emitter.emit.calls[0].args[0]).toBe(\"will-throw-error\")\n        expect(NylasEnv.emitter.emit.calls[1].args[0]).toBe(\"did-throw-error\")\n      });\n\n      it(\"returns if the event has its default prevented\", () => {\n        spyOn(NylasEnv.emitter, \"emit\").andCallFake((name, event) => {\n          event.preventDefault()\n        })\n        NylasEnv.reportError(this.testErr);\n        expect(NylasEnv.emitter.emit).toHaveBeenCalled();\n        expect(NylasEnv.emitter.emit.callCount).toBe(1);\n        expect(NylasEnv.emitter.emit.calls[0].args[0]).toBe(\"will-throw-error\")\n      });\n\n      it(\"opens dev tools in dev mode\", () => {\n        jasmine.unspy(NylasEnv, \"inDevMode\")\n        spyOn(NylasEnv, \"inDevMode\").andReturn(true);\n        NylasEnv.reportError(this.testErr);\n        expect(NylasEnv.openDevTools).toHaveBeenCalled();\n        expect(NylasEnv.executeJavaScriptInDevTools).toHaveBeenCalled();\n      });\n\n      it(\"sends the error report to the error logger\", () => {\n        NylasEnv.reportError(this.testErr);\n        expect(NylasEnv.errorLogger.reportError).toHaveBeenCalled();\n        expect(NylasEnv.errorLogger.reportError.callCount).toBe(1);\n        expect(NylasEnv.errorLogger.reportError.calls[0].args[0]).toBe(this.testErr);\n      });\n\n      it(\"emits did-throw-error\", () => {\n        spyOn(NylasEnv.emitter, \"emit\")\n        NylasEnv.reportError(this.testErr);\n        expect(NylasEnv.openDevTools).not.toHaveBeenCalled();\n        expect(NylasEnv.executeJavaScriptInDevTools).not.toHaveBeenCalled();\n        expect(NylasEnv.emitter.emit.callCount).toBe(2);\n        expect(NylasEnv.emitter.emit.calls[0].args[0]).toBe(\"will-throw-error\")\n        expect(NylasEnv.emitter.emit.calls[1].args[0]).toBe(\"did-throw-error\")\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/nylas-protocol-handler-spec.es6",
    "content": "describe('\"nylas\" protocol URL', () => {\n  it('sends the file relative in the package as response', () => {\n    let called = false;\n    const request = new XMLHttpRequest();\n    request.addEventListener('load', () => { called = true; return });\n    request.open('GET', 'nylas://account-sidebar/package.json', true);\n    request.send();\n\n    waitsFor('request to be done', () => called === true);\n  })\n});\n"
  },
  {
    "path": "packages/client-app/spec/nylas-test-utils.coffee",
    "content": "# Utils for testing.\nReact = require 'react'\nReactDOM = require 'react-dom'\nReactTestUtils = require('react-addons-test-utils')\n\nNylasTestUtils =\n\n  loadKeymap: (path) =>\n    NylasEnv.keymaps.loadKeymap(path)\n\n  simulateCommand: (target, command) =>\n    target.dispatchEvent(new CustomEvent(command, {bubbles: true}))\n\n  # React's \"renderIntoDocument\" does not /actually/ attach the component\n  # to the document. It's a sham: http://dragon.ak.fbcdn.net/hphotos-ak-xpf1/t39.3284-6/10956909_1423563877937976_838415501_n.js\n  # The Atom keymap manager doesn't work correctly on elements outside of the\n  # DOM tree, so we need to attach it.\n  renderIntoDocument: (component) ->\n    node = ReactTestUtils.renderIntoDocument(component)\n    $node = ReactDOM.findDOMNode(node)\n    unless document.body.contains($node)\n      parent = $node\n      while parent.parentNode?\n        parent = parent.parentNode\n      document.body.appendChild(parent)\n    return node\n\n  removeFromDocument: (reactElement) ->\n    $el = ReactDOM.findDOMNode(reactElement)\n    if document.body.contains($el)\n      for child in Array::slice.call(document.body.childNodes)\n        if child.contains($el)\n          document.body.removeChild(child)\n          return\n\n  # Returns mock observable that triggers immediately and provides helper\n  # function to trigger later\n  mockObservable: (data, {dispose} = {}) ->\n    dispose ?= ->\n    func = ->\n    return {\n      subscribe: (fn) ->\n        func = fn\n        func(data)\n        return {dispose}\n      triggerNext: (nextData = data) ->\n        func(nextData)\n    }\n\nmodule.exports = NylasTestUtils\n"
  },
  {
    "path": "packages/client-app/spec/package-manager-spec.coffee",
    "content": "path = require 'path'\nPackage = require '../src/package'\nDatabaseStore = require('../src/flux/stores/database-store').default\n{Disposable} = require 'event-kit'\n\ndescribe \"PackageManager\", ->\n  workspaceElement = null\n\n  beforeEach ->\n    workspaceElement = document.createElement('nylas-workspace')\n    jasmine.attachToDOM(workspaceElement)\n\n  describe \"::loadPackage(name)\", ->\n    beforeEach ->\n      NylasEnv.config.set(\"core.disabledPackages\", [])\n\n    it \"returns the package\", ->\n      pack = NylasEnv.packages.loadPackage(\"package-with-index\")\n      expect(pack instanceof Package).toBe true\n      expect(pack.metadata.name).toBe \"package-with-index\"\n\n    it \"returns the package if it has an invalid keymap\", ->\n      spyOn(console, 'warn')\n      spyOn(console, 'error')\n      pack = NylasEnv.packages.loadPackage(\"package-with-broken-keymap\")\n      expect(pack instanceof Package).toBe true\n      expect(pack.metadata.name).toBe \"package-with-broken-keymap\"\n\n    it \"returns the package if it has an invalid stylesheet\", ->\n      spyOn(console, 'warn')\n      spyOn(console, 'error')\n      pack = NylasEnv.packages.loadPackage(\"package-with-invalid-styles\")\n      expect(pack instanceof Package).toBe true\n      expect(pack.metadata.name).toBe \"package-with-invalid-styles\"\n      expect(pack.stylesheets.length).toBe 0\n\n    it \"returns null if the package has an invalid package.json\", ->\n      spyOn(console, 'warn')\n      spyOn(console, 'error')\n      expect(NylasEnv.packages.loadPackage(\"package-with-broken-package-json\")).toBeNull()\n      expect(console.warn.callCount).toBe(2)\n      expect(console.warn.argsForCall[0][0]).toContain(\"Failed to load package.json\")\n\n    it \"returns null if the package is not found in any package directory\", ->\n      spyOn(console, 'warn')\n      spyOn(console, 'error')\n      expect(NylasEnv.packages.loadPackage(\"this-package-cannot-be-found\")).toBeNull()\n      expect(console.warn.callCount).toBe(1)\n      expect(console.warn.argsForCall[0][0]).toContain(\"Could not resolve\")\n\n    it \"invokes ::onDidLoadPackage listeners with the loaded package\", ->\n      loadedPackage = null\n      NylasEnv.packages.onDidLoadPackage (pack) -> loadedPackage = pack\n\n      NylasEnv.packages.loadPackage(\"package-with-main\")\n\n      expect(loadedPackage.name).toBe \"package-with-main\"\n\n  describe \"::unloadPackage(name)\", ->\n    describe \"when the package is active\", ->\n      it \"throws an error\", ->\n        pack = null\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage('package-with-main').then (p) -> pack = p\n\n        runs ->\n          expect(NylasEnv.packages.isPackageLoaded(pack.name)).toBeTruthy()\n          expect(NylasEnv.packages.isPackageActive(pack.name)).toBeTruthy()\n          expect( -> NylasEnv.packages.unloadPackage(pack.name)).toThrow()\n          expect(NylasEnv.packages.isPackageLoaded(pack.name)).toBeTruthy()\n          expect(NylasEnv.packages.isPackageActive(pack.name)).toBeTruthy()\n\n    describe \"when the package is not loaded\", ->\n      it \"throws an error\", ->\n        expect(NylasEnv.packages.isPackageLoaded('unloaded')).toBeFalsy()\n        expect( -> NylasEnv.packages.unloadPackage('unloaded')).toThrow()\n        expect(NylasEnv.packages.isPackageLoaded('unloaded')).toBeFalsy()\n\n    describe \"when the package is loaded\", ->\n      it \"no longers reports it as being loaded\", ->\n        pack = NylasEnv.packages.loadPackage('package-with-main')\n        expect(NylasEnv.packages.isPackageLoaded(pack.name)).toBeTruthy()\n        NylasEnv.packages.unloadPackage(pack.name)\n        expect(NylasEnv.packages.isPackageLoaded(pack.name)).toBeFalsy()\n\n    it \"invokes ::onDidUnloadPackage listeners with the unloaded package\", ->\n      NylasEnv.packages.loadPackage('package-with-main')\n      unloadedPackage = null\n      NylasEnv.packages.onDidUnloadPackage (pack) -> unloadedPackage = pack\n      NylasEnv.packages.unloadPackage('package-with-main')\n      expect(unloadedPackage.name).toBe 'package-with-main'\n\n  describe \"::activatePackage(id)\", ->\n    describe \"when called multiple times\", ->\n      it \"it only calls activate on the package once\", ->\n        spyOn(Package.prototype, 'activateNow').andCallThrough()\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage('package-with-index')\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage('package-with-index')\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage('package-with-index')\n\n        runs ->\n          expect(Package.prototype.activateNow.callCount).toBe 1\n\n    describe \"when the package has a main module\", ->\n      describe \"when the metadata specifies a main module path˜\", ->\n        it \"requires the module at the specified path\", ->\n          mainModule = require('./fixtures/packages/package-with-main/main-module')\n          spyOn(mainModule, 'activate')\n          pack = null\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage('package-with-main').then (p) -> pack = p\n\n          runs ->\n            expect(mainModule.activate).toHaveBeenCalled()\n            expect(pack.mainModule).toBe mainModule\n\n      describe \"when the metadata does not specify a main module\", ->\n        it \"requires index.coffee\", ->\n          indexModule = require('./fixtures/packages/package-with-index/index')\n          spyOn(indexModule, 'activate')\n          pack = null\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage('package-with-index').then (p) -> pack = p\n\n          runs ->\n            expect(indexModule.activate).toHaveBeenCalled()\n            expect(pack.mainModule).toBe indexModule\n\n      it \"assigns config schema, including defaults when package contains a schema\", ->\n        spyOn(NylasEnv.config, \"_logError\")\n        expect(NylasEnv.config.get('package-with-config-schema.numbers.one')).toBeUndefined()\n\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage('package-with-config-schema')\n\n        runs ->\n          expect(NylasEnv.config.get('package-with-config-schema.numbers.one')).toBe 1\n          expect(NylasEnv.config.get('package-with-config-schema.numbers.two')).toBe 2\n\n          expect(NylasEnv.config.set('package-with-config-schema.numbers.one', 'nope')).toBe false\n          expect(NylasEnv.config._logError).toHaveBeenCalled()\n          expect(NylasEnv.config._logError.callCount).toBe(1)\n          expect(NylasEnv.config.set('package-with-config-schema.numbers.one', '10')).toBe true\n          expect(NylasEnv.config.get('package-with-config-schema.numbers.one')).toBe 10\n\n    describe \"when the package has no main module\", ->\n      it \"des not compalain if it doesn't have a package.json\", ->\n        spyOn(console, \"error\")\n        spyOn(console, \"warn\")\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage('package-without-module')\n          .then ->\n            expect(-> NylasEnv.packages.activatePackage('package-without-module')).not.toThrow()\n            expect(console.error).not.toHaveBeenCalled()\n            expect(console.warn).not.toHaveBeenCalled()\n\n    it \"passes the activate method the package's previously serialized state if it exists\", ->\n      pack = null\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage(\"package-with-serialization\").then (p) -> pack = p\n\n      runs ->\n        expect(pack.mainModule.someNumber).not.toBe 77\n        pack.mainModule.someNumber = 77\n        NylasEnv.packages.deactivatePackage(\"package-with-serialization\")\n        spyOn(pack.mainModule, 'activate').andCallThrough()\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage(\"package-with-serialization\")\n        runs ->\n          expect(pack.mainModule.activate.calls[0].args[0]).toEqual({someNumber: 77})\n\n    it \"invokes ::onDidActivatePackage listeners with the activated package\", ->\n      activatedPackage = null\n      NylasEnv.packages.onDidActivatePackage (pack) ->\n        activatedPackage = pack\n\n      NylasEnv.packages.activatePackage('package-with-main')\n\n      waitsFor -> activatedPackage?\n      runs -> expect(activatedPackage.name).toBe 'package-with-main'\n\n    describe \"when the package throws an error while loading\", ->\n      it \"logs a warning instead of throwing an exception\", ->\n        NylasEnv.config.set(\"core.disabledPackages\", [])\n        spyOn(console, \"log\")\n        spyOn(console, \"warn\")\n        spyOn(console, \"error\")\n        expect(-> NylasEnv.packages.activatePackage(\"package-that-throws-an-exception\")).not.toThrow()\n        expect(console.warn).toHaveBeenCalled()\n\n    describe \"when the package is not found\", ->\n      it \"rejects the promise\", ->\n        NylasEnv.config.set(\"core.disabledPackages\", [])\n\n        onSuccess = jasmine.createSpy('onSuccess')\n        onFailure = jasmine.createSpy('onFailure')\n        spyOn(console, 'warn')\n        spyOn(console, \"error\")\n\n        NylasEnv.packages.activatePackage(\"this-doesnt-exist\").then(onSuccess, onFailure)\n\n        waitsFor \"promise to be rejected\", ->\n          onFailure.callCount > 0\n\n        runs ->\n          expect(console.warn.callCount).toBe 1\n          expect(onFailure.mostRecentCall.args[0] instanceof Error).toBe true\n          expect(onFailure.mostRecentCall.args[0].message).toContain \"Failed to load package 'this-doesnt-exist'\"\n\n    describe \"keymap loading\", ->\n      describe \"when the metadata does not contain a 'keymaps' manifest\", ->\n        it \"loads all the .json files in the keymaps directory\", ->\n          expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-a')).toHaveLength 0\n          expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-b')).toHaveLength 0\n\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-keymaps\")\n\n          runs ->\n            expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-a')).toHaveLength 1\n            expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-b')).toHaveLength 1\n\n      describe \"when the metadata contains a 'keymaps' manifest\", ->\n        it \"loads only the keymaps specified by the manifest, in the specified order\", ->\n          expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-a')).toHaveLength 0\n          expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-b')).toHaveLength 0\n\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-keymaps-manifest\")\n\n          runs ->\n            expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-a')).toHaveLength 1\n            expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-b')).toHaveLength 1\n\n      describe \"when the keymap file is empty\", ->\n        it \"does not throw an error on activation\", ->\n          spyOn(console, 'error')\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-empty-keymap\")\n\n          runs ->\n            expect(NylasEnv.packages.isPackageActive(\"package-with-empty-keymap\")).toBe true\n\n    describe \"menu loading\", ->\n      beforeEach ->\n        NylasEnv.menu.template = []\n\n      describe \"when the metadata does not contain a 'menus' manifest\", ->\n        it \"loads all the .json files in the menus directory\", ->\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-menus\")\n\n          runs ->\n            expect(NylasEnv.menu.template.length).toBe 2\n            expect(NylasEnv.menu.template[1].label).toBe \"Second to Last\"\n            expect(NylasEnv.menu.template[0].label).toBe \"Last\"\n\n      describe \"when the metadata contains a 'menus' manifest\", ->\n        it \"loads only the menus specified by the manifest, in the specified order\", ->\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-menus-manifest\")\n\n          runs ->\n            expect(NylasEnv.menu.template[0].label).toBe \"Second to Last\"\n            expect(NylasEnv.menu.template[1].label).toBe \"Last\"\n\n      describe \"when the menu file is empty\", ->\n        it \"does not throw an error on activation\", ->\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-empty-menu\")\n\n          runs ->\n            expect(NylasEnv.packages.isPackageActive(\"package-with-empty-menu\")).toBe true\n\n    describe \"stylesheet loading\", ->\n      describe \"when the metadata contains a 'styleSheets' manifest\", ->\n        it \"loads style sheets from the styles directory as specified by the manifest\", ->\n          one = require.resolve(\"./fixtures/packages/package-with-style-sheets-manifest/styles/1.css\")\n          two = require.resolve(\"./fixtures/packages/package-with-style-sheets-manifest/styles/2.less\")\n          three = require.resolve(\"./fixtures/packages/package-with-style-sheets-manifest/styles/3.css\")\n\n          one = NylasEnv.themes.stringToId(one)\n          two = NylasEnv.themes.stringToId(two)\n          three = NylasEnv.themes.stringToId(three)\n\n          expect(NylasEnv.themes.stylesheetElementForId(one)).toBeNull()\n          expect(NylasEnv.themes.stylesheetElementForId(two)).toBeNull()\n          expect(NylasEnv.themes.stylesheetElementForId(three)).toBeNull()\n\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-style-sheets-manifest\")\n\n          runs ->\n            expect(NylasEnv.themes.stylesheetElementForId(one)).not.toBeNull()\n            expect(NylasEnv.themes.stylesheetElementForId(two)).not.toBeNull()\n            expect(NylasEnv.themes.stylesheetElementForId(three)).toBeNull()\n            expect(window.getComputedStyle(document.getElementById('jasmine-content')).fontSize).toBe '1px'\n\n      describe \"when the metadata does not contain a 'styleSheets' manifest\", ->\n        it \"loads all style sheets from the styles directory\", ->\n          one = require.resolve(\"./fixtures/packages/package-with-styles/styles/1.css\")\n          two = require.resolve(\"./fixtures/packages/package-with-styles/styles/2.less\")\n          three = require.resolve(\"./fixtures/packages/package-with-styles/styles/3.test-context.css\")\n          four = require.resolve(\"./fixtures/packages/package-with-styles/styles/4.css\")\n\n          one = NylasEnv.themes.stringToId(one)\n          two = NylasEnv.themes.stringToId(two)\n          three = NylasEnv.themes.stringToId(three)\n          four = NylasEnv.themes.stringToId(four)\n\n          expect(NylasEnv.themes.stylesheetElementForId(one)).toBeNull()\n          expect(NylasEnv.themes.stylesheetElementForId(two)).toBeNull()\n          expect(NylasEnv.themes.stylesheetElementForId(three)).toBeNull()\n          expect(NylasEnv.themes.stylesheetElementForId(four)).toBeNull()\n\n          waitsForPromise ->\n            NylasEnv.packages.activatePackage(\"package-with-styles\")\n\n          runs ->\n            expect(NylasEnv.themes.stylesheetElementForId(one)).not.toBeNull()\n            expect(NylasEnv.themes.stylesheetElementForId(two)).not.toBeNull()\n            expect(NylasEnv.themes.stylesheetElementForId(three)).not.toBeNull()\n            expect(NylasEnv.themes.stylesheetElementForId(four)).not.toBeNull()\n            expect(window.getComputedStyle(document.getElementById('jasmine-content')).fontSize).toBe '3px'\n\n      it \"assigns the stylesheet's context based on the filename\", ->\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage(\"package-with-styles\")\n\n        runs ->\n          count = 0\n\n          for styleElement in NylasEnv.styles.getStyleElements()\n            if styleElement.sourcePath.match /1.css/\n              expect(styleElement.context).toBe undefined\n              count++\n\n            if styleElement.sourcePath.match /2.less/\n              expect(styleElement.context).toBe undefined\n              count++\n\n            if styleElement.sourcePath.match /3.test-context.css/\n              expect(styleElement.context).toBe 'test-context'\n              count++\n\n            if styleElement.sourcePath.match /4.css/\n              expect(styleElement.context).toBe undefined\n              count++\n\n          expect(count).toBe 4\n\n  describe \"::deactivatePackage(id)\", ->\n    afterEach ->\n      NylasEnv.packages.unloadPackages()\n\n    it \"calls `deactivate` on the package's main module if activate was successful\", ->\n      pack = null\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage(\"package-with-deactivate\").then (p) -> pack = p\n\n      runs ->\n        expect(NylasEnv.packages.isPackageActive(\"package-with-deactivate\")).toBeTruthy()\n        spyOn(pack.mainModule, 'deactivate').andCallThrough()\n\n        NylasEnv.packages.deactivatePackage(\"package-with-deactivate\")\n        expect(pack.mainModule.deactivate).toHaveBeenCalled()\n        expect(NylasEnv.packages.isPackageActive(\"package-with-module\")).toBeFalsy()\n\n        spyOn(console, 'log')\n        spyOn(console, 'warn')\n        spyOn(console, \"error\")\n\n      badPack = null\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage(\"package-that-throws-on-activate\").then (p) -> badPack = p\n\n      runs ->\n        expect(NylasEnv.packages.isPackageActive(\"package-that-throws-on-activate\")).toBeTruthy()\n        spyOn(badPack.mainModule, 'deactivate').andCallThrough()\n\n        NylasEnv.packages.deactivatePackage(\"package-that-throws-on-activate\")\n        expect(badPack.mainModule.deactivate).not.toHaveBeenCalled()\n        expect(NylasEnv.packages.isPackageActive(\"package-that-throws-on-activate\")).toBeFalsy()\n\n    it \"does not serialize packages that have not been activated called on their main module\", ->\n      spyOn(console, 'log')\n      spyOn(console, 'warn')\n      spyOn(console, \"error\")\n      badPack = null\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage(\"package-that-throws-on-activate\").then (p) -> badPack = p\n\n      runs ->\n        spyOn(badPack.mainModule, 'serialize').andCallThrough()\n\n        NylasEnv.packages.deactivatePackage(\"package-that-throws-on-activate\")\n        expect(badPack.mainModule.serialize).not.toHaveBeenCalled()\n\n    it \"absorbs exceptions that are thrown by the package module's serialize method\", ->\n      spyOn(console, 'error')\n\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage('package-with-serialize-error')\n\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage('package-with-serialization')\n\n      runs ->\n        NylasEnv.packages.deactivatePackages()\n        expect(NylasEnv.packages.packageStates['package-with-serialize-error']).toBeUndefined()\n        expect(NylasEnv.packages.packageStates['package-with-serialization']).toEqual someNumber: 1\n        expect(console.error).toHaveBeenCalled()\n\n    it \"absorbs exceptions that are thrown by the package module's deactivate method\", ->\n      spyOn(console, 'error')\n\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage(\"package-that-throws-on-deactivate\")\n\n      runs ->\n        expect(-> NylasEnv.packages.deactivatePackage(\"package-that-throws-on-deactivate\")).not.toThrow()\n        expect(console.error).toHaveBeenCalled()\n\n    it \"removes the package's keymaps\", ->\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage('package-with-keymaps')\n\n      runs ->\n        expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-a')).toHaveLength 1\n        expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-b')).toHaveLength 1\n        NylasEnv.packages.deactivatePackage('package-with-keymaps')\n        expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-a')).toHaveLength 0\n        expect(NylasEnv.keymaps.getBindingsForCommand('my-package:command-b')).toHaveLength 0\n\n    it \"removes the package's stylesheets\", ->\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage('package-with-styles')\n\n      runs ->\n        NylasEnv.packages.deactivatePackage('package-with-styles')\n        one = require.resolve(\"./fixtures/packages/package-with-style-sheets-manifest/styles/1.css\")\n        two = require.resolve(\"./fixtures/packages/package-with-style-sheets-manifest/styles/2.less\")\n        three = require.resolve(\"./fixtures/packages/package-with-style-sheets-manifest/styles/3.css\")\n        expect(NylasEnv.themes.stylesheetElementForId(one)).toBe(null)\n        expect(NylasEnv.themes.stylesheetElementForId(two)).toBe(null)\n        expect(NylasEnv.themes.stylesheetElementForId(three)).toBe(null)\n\n    it \"invokes ::onDidDeactivatePackage listeners with the deactivated package\", ->\n      waitsForPromise ->\n        NylasEnv.packages.activatePackage(\"package-with-main\")\n\n      runs ->\n        deactivatedPackage = null\n        NylasEnv.packages.onDidDeactivatePackage (pack) -> deactivatedPackage = pack\n        NylasEnv.packages.deactivatePackage(\"package-with-main\")\n        expect(deactivatedPackage.name).toBe \"package-with-main\"\n\n  describe \"::activate()\", ->\n    beforeEach ->\n      spyOn(console, 'warn')\n      spyOn(console, \"error\")\n      NylasEnv.packages.loadPackages()\n\n      loadedPackages = NylasEnv.packages.getLoadedPackages()\n      expect(loadedPackages.length).toBeGreaterThan 0\n\n    afterEach ->\n      NylasEnv.packages.deactivatePackages()\n      NylasEnv.packages.unloadPackages()\n\n    it \"activates all the packages, and none of the themes\", ->\n      packageActivator = spyOn(NylasEnv.packages, 'activatePackages')\n      themeActivator = spyOn(NylasEnv.themes, 'activatePackages')\n\n      NylasEnv.packages.activate()\n\n      expect(packageActivator).toHaveBeenCalled()\n      expect(themeActivator).toHaveBeenCalled()\n\n      packages = packageActivator.mostRecentCall.args[0]\n      expect(['nylas']).toContain(pack.getType()) for pack in packages\n\n      themes = themeActivator.mostRecentCall.args[0]\n      expect(['theme']).toContain(theme.getType()) for theme in themes\n\n    it \"refreshes the database after activating packages with models\", ->\n      spyOn(DatabaseStore, \"refreshDatabaseSchema\").andReturn(Promise.resolve())\n      package2 = NylasEnv.packages.loadPackage('package-with-models')\n      NylasEnv.packages.activatePackages([package2])\n      expect(DatabaseStore.refreshDatabaseSchema).toHaveBeenCalled()\n      expect(DatabaseStore.refreshDatabaseSchema.calls.length).toBe 1\n\n    it \"calls callbacks registered with ::onDidActivateInitialPackages\", ->\n      package1 = NylasEnv.packages.loadPackage('package-with-main')\n      package2 = NylasEnv.packages.loadPackage('package-with-index')\n      package3 = NylasEnv.packages.loadPackage('package-with-activation-commands')\n      spyOn(NylasEnv.packages, 'getLoadedPackages').andReturn([package1, package2])\n\n      activateSpy = jasmine.createSpy('activateSpy')\n      NylasEnv.packages.onDidActivateInitialPackages(activateSpy)\n\n      NylasEnv.packages.activate()\n      waitsFor -> activateSpy.callCount > 0\n      runs ->\n        jasmine.unspy(NylasEnv.packages, 'getLoadedPackages')\n        expect(package1 in NylasEnv.packages.getActivePackages()).toBe true\n        expect(package2 in NylasEnv.packages.getActivePackages()).toBe true\n        expect(package3 in NylasEnv.packages.getActivePackages()).toBe false\n\n  describe \"::enablePackage(id) and ::disablePackage(id)\", ->\n    describe \"with packages\", ->\n      it \"enables a disabled package\", ->\n        spyOn(DatabaseStore, \"refreshDatabaseSchema\")\n        packageName = 'package-with-main'\n        NylasEnv.config.pushAtKeyPath('core.disabledPackages', packageName)\n        NylasEnv.packages.observeDisabledPackages()\n        expect(NylasEnv.config.get('core.disabledPackages')).toContain packageName\n\n        pack = NylasEnv.packages.enablePackage(packageName)\n        loadedPackages = NylasEnv.packages.getLoadedPackages()\n        activatedPackages = null\n        waitsFor ->\n          activatedPackages = NylasEnv.packages.getActivePackages()\n          activatedPackages.length > 0\n\n        runs ->\n          expect(loadedPackages).toContain(pack)\n          expect(activatedPackages).toContain(pack)\n          expect(DatabaseStore.refreshDatabaseSchema).not.toHaveBeenCalled()\n          expect(NylasEnv.config.get('core.disabledPackages')).not.toContain packageName\n\n      it 'refreshes the DB when loading a package with models', ->\n        spyOn(DatabaseStore, \"refreshDatabaseSchema\").andReturn(Promise.resolve())\n        packageName = \"package-with-models\"\n        NylasEnv.config.pushAtKeyPath('core.disabledPackages', packageName)\n        NylasEnv.packages.observeDisabledPackages()\n        NylasEnv.config.removeAtKeyPath(\"core.disabledPackages\", packageName)\n        expect(DatabaseStore.refreshDatabaseSchema).toHaveBeenCalled()\n        expect(DatabaseStore.refreshDatabaseSchema.calls.length).toBe 1\n\n      it \"disables an enabled package\", ->\n        packageName = 'package-with-main'\n        waitsForPromise ->\n          NylasEnv.packages.activatePackage(packageName)\n\n        runs ->\n          NylasEnv.packages.observeDisabledPackages()\n          expect(NylasEnv.config.get('core.disabledPackages')).not.toContain packageName\n\n          pack = NylasEnv.packages.disablePackage(packageName)\n\n          activatedPackages = NylasEnv.packages.getActivePackages()\n          expect(activatedPackages).not.toContain(pack)\n          expect(NylasEnv.config.get('core.disabledPackages')).toContain packageName\n\n      it \"returns null if the package cannot be loaded\", ->\n        spyOn(console, 'warn')\n        spyOn(console, \"error\")\n        expect(NylasEnv.packages.enablePackage(\"this-doesnt-exist\")).toBeNull()\n        expect(console.warn.callCount).toBe 1\n\n    describe \"with themes\", ->\n      didChangeActiveThemesHandler = null\n\n      beforeEach ->\n        theme_dir = path.resolve(__dirname, '../internal_packages')\n        NylasEnv.packages.packageDirPaths.unshift(theme_dir)\n        waitsForPromise ->\n          NylasEnv.themes.activateThemes()\n\n      afterEach ->\n        NylasEnv.themes.deactivateThemes()\n\n      it \"enables and disables a theme\", ->\n        packageName = 'theme-with-package-file'\n\n        expect(NylasEnv.config.get('core.themes')).not.toContain packageName\n        expect(NylasEnv.config.get('core.disabledPackages')).not.toContain packageName\n\n        # enabling of theme\n        pack = NylasEnv.packages.enablePackage(packageName)\n\n        waitsFor ->\n          pack in NylasEnv.packages.getActivePackages()\n\n        runs ->\n          expect(NylasEnv.config.get('core.themes')).toContain packageName\n          expect(NylasEnv.config.get('core.disabledPackages')).not.toContain packageName\n\n          didChangeActiveThemesHandler = jasmine.createSpy('didChangeActiveThemesHandler')\n          didChangeActiveThemesHandler.reset()\n          NylasEnv.themes.onDidChangeActiveThemes didChangeActiveThemesHandler\n\n          pack = NylasEnv.packages.disablePackage(packageName)\n\n        waitsFor ->\n          didChangeActiveThemesHandler.callCount is 1\n\n        runs ->\n          expect(NylasEnv.packages.getActivePackages()).not.toContain pack\n          expect(NylasEnv.config.get('core.themes')).not.toContain packageName\n          expect(NylasEnv.config.get('core.themes')).not.toContain packageName\n          expect(NylasEnv.config.get('core.disabledPackages')).not.toContain packageName\n\n  describe 'packages with models and tasks', ->\n    beforeEach ->\n      NylasEnv.packages.deactivatePackages()\n      NylasEnv.packages.unloadPackages()\n\n    it 'registers objects on load', ->\n      withModels = NylasEnv.packages.loadPackage(\"package-with-models\")\n      withoutModels = NylasEnv.packages.loadPackage(\"package-with-main\")\n      expect(withModels.declaresNewDatabaseObjects).toBe true\n      expect(withoutModels.declaresNewDatabaseObjects).toBe false\n      expect(NylasEnv.packages.packagesWithDatabaseObjects.length).toBe 1\n      expect(NylasEnv.packages.packagesWithDatabaseObjects[0]).toBe withModels\n"
  },
  {
    "path": "packages/client-app/spec/package-spec.coffee",
    "content": "path = require 'path'\nPackage = require '../src/package'\nThemePackage = require '../src/theme-package'\n\nresolveFixturePath = (packagename) ->\n  path.join(__dirname, 'fixtures', 'packages', packagename)\n\ndescribe \"Package\", ->\n  describe \"when the package contains incompatible native modules\", ->\n    beforeEach ->\n      spyOn(NylasEnv, 'inDevMode').andReturn(false)\n\n    it \"does not activate it\", ->\n      packagePath = resolveFixturePath('package-with-incompatible-native-module')\n      pack = new Package(packagePath)\n      expect(pack.isCompatible()).toBe false\n      expect(pack.incompatibleModules[0].name).toBe 'native-module'\n      expect(pack.incompatibleModules[0].path).toBe path.join(packagePath, 'node_modules', 'native-module')\n\n    it \"caches the incompatible native modules in local storage\", ->\n      packagePath = resolveFixturePath('package-with-incompatible-native-module')\n      cacheKey = null\n      cacheItem = null\n\n      spyOn(global.localStorage, 'setItem').andCallFake (key, item) ->\n        cacheKey = key\n        cacheItem = item\n      spyOn(global.localStorage, 'getItem').andCallFake (key) ->\n        return cacheItem if cacheKey is key\n\n      expect(new Package(packagePath).isCompatible()).toBe false\n      expect(global.localStorage.getItem.callCount).toBe 1\n      expect(global.localStorage.setItem.callCount).toBe 1\n\n      expect(new Package(packagePath).isCompatible()).toBe false\n      expect(global.localStorage.getItem.callCount).toBe 2\n      expect(global.localStorage.setItem.callCount).toBe 1\n\n  describe \"theme\", ->\n    theme = null\n\n    beforeEach ->\n      @wrap = document.createElement('nylas-theme-wrap')\n      document.getElementById(\"jasmine-content\").appendChild(@wrap)\n\n    afterEach ->\n      theme.deactivate() if theme?\n\n    describe \"when the theme contains a single style file\", ->\n      it \"loads and applies css\", ->\n\n        expect(window.getComputedStyle(@wrap)['padding-bottom']).not.toBe \"1234px\"\n        themePath = resolveFixturePath('theme-with-index-css')\n        theme = new ThemePackage(themePath)\n        theme.activate()\n        expect(window.getComputedStyle(@wrap)['padding-top']).toBe \"1234px\"\n\n      it \"parses, loads and applies less\", ->\n        expect(window.getComputedStyle(@wrap)['padding-bottom']).not.toBe \"1234px\"\n        themePath = resolveFixturePath('theme-with-index-less')\n        theme = new ThemePackage(themePath)\n        theme.activate()\n        expect(window.getComputedStyle(@wrap)['padding-top']).toBe \"4321px\"\n\n    describe \"when the theme contains a package.json file\", ->\n      it \"loads and applies stylesheets from package.json in the correct order\", ->\n        styles = window.getComputedStyle(@wrap)\n        expect(styles[\"padding-top\"]).not.toBe(\"101px\")\n        expect(styles[\"padding-right\"]).not.toBe(\"102px\")\n        expect(styles[\"padding-bottom\"]).not.toBe(\"103px\")\n\n        themePath = resolveFixturePath('theme-with-package-file')\n        theme = new ThemePackage(themePath)\n        theme.activate()\n        styles = window.getComputedStyle(@wrap)\n        expect(styles[\"padding-top\"]).toBe(\"101px\")\n        expect(styles[\"padding-right\"]).toBe(\"102px\")\n        expect(styles[\"padding-bottom\"]).toBe(\"103px\")\n\n    describe \"when the theme does not contain a package.json file and is a directory\", ->\n      it \"loads all stylesheet files in the directory\", ->\n        styles = window.getComputedStyle(@wrap)\n        expect(styles[\"padding-top\"]).not.toBe(\"10px\")\n        expect(styles[\"padding-right\"]).not.toBe(\"20px\")\n        expect(styles[\"padding-bottom\"]).not.toBe(\"30px\")\n\n        themePath = resolveFixturePath('theme-without-package-file')\n        theme = new ThemePackage(themePath)\n        theme.activate()\n        styles = window.getComputedStyle(@wrap)\n        expect(styles[\"padding-top\"]).toBe(\"10px\")\n        expect(styles[\"padding-right\"]).toBe(\"20px\")\n        expect(styles[\"padding-bottom\"]).toBe(\"30px\")\n\n    describe \"reloading a theme\", ->\n      beforeEach ->\n        themePath = resolveFixturePath('theme-with-package-file')\n        theme = new ThemePackage(themePath)\n        theme.activate()\n\n      it \"reloads without readding to the stylesheets list\", ->\n        expect(theme.getStylesheetPaths().length).toBe 3\n        theme.reloadStylesheets()\n        expect(theme.getStylesheetPaths().length).toBe 3\n\n    describe \"events\", ->\n      beforeEach ->\n        themePath = resolveFixturePath('theme-with-package-file')\n        theme = new ThemePackage(themePath)\n        theme.activate()\n\n      it \"deactivated event fires on .deactivate()\", ->\n        theme.onDidDeactivate spy = jasmine.createSpy()\n        theme.deactivate()\n        expect(spy).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/registries/component-registry-spec.coffee",
    "content": "React = require 'react'\nComponentRegistry = require '../../src/registries/component-registry'\n\nclass TestComponent extends React.Component\n  @displayName: 'TestComponent'\n  constructor: ->\n\nclass TestComponentNotSameIdentity extends React.Component\n  @displayName: 'TestComponent'\n  constructor: ->\n\nclass TestComponentNoDisplayName extends React.Component\n  constructor: ->\n\nclass AComponent extends React.Component\n  @displayName: 'A'\n\nclass BComponent extends React.Component\n  @displayName: 'B'\n\nclass CComponent extends React.Component\n  @displayName: 'C'\n\nclass DComponent extends React.Component\n  @displayName: 'D'\n\nclass EComponent extends React.Component\n  @displayName: 'E'\n\nclass FComponent extends React.Component\n  @displayName: 'F'\n\ndescribe 'ComponentRegistry', ->\n  beforeEach ->\n    ComponentRegistry._clear()\n\n  describe 'register', ->\n    it 'throws an exception if passed a non-component', ->\n      expect(-> ComponentRegistry.register(null)).toThrow()\n      expect(-> ComponentRegistry.register(\"cheese\")).toThrow()\n\n    it 'returns itself', ->\n      expect(ComponentRegistry.register(TestComponent, {role: \"bla\"})).toBe(ComponentRegistry)\n\n    it 'does allow the exact same component to be redefined with different role/locations', ->\n      ComponentRegistry.register(TestComponent, {role: \"bla\"})\n      expect(-> ComponentRegistry.register(TestComponent, {role: \"other-role\"})).not.toThrow()\n\n    it 'does not allow components to be overridden by other components with the same displayName', ->\n      ComponentRegistry.register(TestComponent, {role: \"bla\"})\n      expect(-> ComponentRegistry.register(TestComponentNotSameIdentity, {role: \"bla\"})).toThrow()\n\n    it 'does not allow components to be registered without a displayName', ->\n      expect(-> ComponentRegistry.register(TestComponentNoDisplayName, {role: \"bla\"})).toThrow()\n\n  describe 'findComponentByName', ->\n    it 'should return a component', ->\n      ComponentRegistry.register(TestComponent, {role: \"bla\"})\n      expect(ComponentRegistry.findComponentByName('TestComponent')).toEqual(TestComponent)\n\n    it 'should return undefined if there is no component', ->\n      expect(ComponentRegistry.findComponentByName(\"not actually a name\")).toBeUndefined()\n\n  describe 'findComponentsMatching', ->\n    it 'should throw if a descriptor is not provided', ->\n      expect( -> ComponentRegistry.findComponentsMatching()).toThrow()\n\n    it 'should return the correct results in a wide range of test cases', ->\n      StubLocation1 =\n        id: 'StubLocation1'\n      StubLocation2 =\n        id: 'StubLocation2'\n      ComponentRegistry.register(AComponent, { role: 'ThreadAction' })\n      ComponentRegistry.register(BComponent, { role: 'ThreadAction', modes: ['list'] })\n      ComponentRegistry.register(CComponent, { location: StubLocation1, modes: ['split'] })\n      ComponentRegistry.register(DComponent, { locations: [StubLocation1, StubLocation2] })\n      ComponentRegistry.register(EComponent, { roles: ['ThreadAction', 'MessageAction'] })\n      ComponentRegistry.register(FComponent, { roles: ['MessageAction'], mode: 'list' })\n\n      scenarios = [\n        {descriptor: {role: 'ThreadAction'}, results: [AComponent, BComponent, EComponent]}\n        {descriptor: {role: 'ThreadAction', mode: 'list'}, results: [AComponent, BComponent, EComponent]}\n        {descriptor: {role: 'ThreadAction', mode: 'split'}, results: [AComponent, EComponent]}\n        {descriptor: {location: StubLocation1}, results: [CComponent, DComponent]}\n        {descriptor: {location: StubLocation1, mode: 'list'}, results: [DComponent]}\n        {descriptor: {locations: [StubLocation1, StubLocation2]}, results: [CComponent, DComponent]}\n        {descriptor: {roles: ['ThreadAction', 'MessageAction']}, results: [AComponent, BComponent, EComponent, FComponent]}\n      ]\n\n      scenarios.forEach ({descriptor, results}) ->\n        expect(ComponentRegistry.findComponentsMatching(descriptor)).toEqual(results)\n\n\n  describe 'unregister', ->\n    it 'unregisters the component if it exists', ->\n      ComponentRegistry.register(TestComponent, { role: 'bla' })\n      ComponentRegistry.unregister(TestComponent)\n      expect(ComponentRegistry.findComponentByName('TestComponent')).toBeUndefined()\n\n    it 'throws if a string is passed instead of a component', ->\n      expect( -> ComponentRegistry.unregister('TestComponent')).toThrow()\n"
  },
  {
    "path": "packages/client-app/spec/registries/extension-registry-spec.coffee",
    "content": "ExtensionRegistry = require('../../src/registries/extension-registry')\n\nclass TestExtension\n  @name: 'TestExtension'\n\ndescribe 'ExtensionRegistry', ->\n  beforeEach ->\n    @originalAdapters = ExtensionRegistry._deprecationAdapters\n    @registry = new ExtensionRegistry.Registry('Test')\n    spyOn @registry, 'triggerDebounced'\n\n  describe 'Registry', ->\n    it 'has trigger and listen to defined', ->\n      expect(@registry.trigger).toBeDefined()\n      expect(@registry.listen).toBeDefined()\n      expect(@registry.listenTo).toBeDefined()\n\n    describe 'register', ->\n      it 'throws an exception if extension not passed', ->\n        expect(=> @registry.register(null)).toThrow()\n\n      it 'throws an exception if extension does not have a name', ->\n        expect(=> @registry.register({})).toThrow()\n\n      it 'throws an exception if extension is array', ->\n        expect(=> @registry.register([])).toThrow()\n\n      it 'throws an exception if extension is string', ->\n        expect(=> @registry.register('')).toThrow()\n\n      it 'returns itself', ->\n        expect(@registry.register(TestExtension)).toBe(@registry)\n\n      it 'registers extension and triggers', ->\n        @registry.register(TestExtension)\n        expect(@registry.extensions().length).toEqual 1\n        expect(@registry.triggerDebounced).toHaveBeenCalled()\n\n      it 'does not add extensions with the same name', ->\n        expect(@registry.extensions().length).toEqual 0\n        @registry.register(TestExtension)\n        expect(@registry.extensions().length).toEqual 1\n        @registry.register({name: 'TestExtension'})\n        expect(@registry.extensions().length).toEqual 1\n\n      it 'calls deprecationAdapters if present for a role', ->\n        adapterSpy = jasmine.createSpy('adapterSpy').andCallFake (ext) -> ext\n        @registry = new ExtensionRegistry.Registry('Test', adapterSpy)\n        spyOn @registry, 'triggerDebounced'\n        @registry.register(TestExtension)\n        expect(adapterSpy.calls.length).toEqual 1\n\n    describe 'unregister', ->\n      it 'unregisters the extension if it exists', ->\n        @registry.register(TestExtension)\n        @registry.unregister(TestExtension)\n        expect(@registry.extensions().length).toEqual 0\n\n      it 'throws if invalid extension passed', ->\n        expect( => @registry.unregister('Test')).toThrow()\n        expect( => @registry.unregister(null)).toThrow()\n        expect( => @registry.unregister([])).toThrow()\n        expect( => @registry.unregister({})).toThrow()\n"
  },
  {
    "path": "packages/client-app/spec/services/delta-processor-spec.coffee",
    "content": "_ = require 'underscore'\nfs = require 'fs'\npath = require 'path'\n{NylasAPIHelpers,\n Thread,\n DatabaseWriter,\n Actions,\n Message,\n Thread} = require 'nylas-exports'\n\nDeltaProcessor = require('../../src/services/delta-processor').default\n\nfixturesPath = path.resolve(__dirname, '..', 'fixtures')\n\ndescribe \"DeltaProcessor\", ->\n\n  describe \"handleDeltas\", ->\n    beforeEach ->\n      @sampleDeltas = JSON.parse(fs.readFileSync(\"#{fixturesPath}/sample-deltas.json\"))\n      @sampleClustered = JSON.parse(fs.readFileSync(\"#{fixturesPath}/sample-deltas-clustered.json\"))\n\n    it \"should immediately fire the received raw deltas event\", ->\n      spyOn(Actions, 'longPollReceivedRawDeltas')\n      spyOn(DeltaProcessor, '_clusterDeltas').andReturn({create: {}, modify: {}, destroy: []})\n      waitsForPromise ->\n        DeltaProcessor.process(@sampleDeltas, {source: 'n1Cloud'})\n      runs ->\n        expect(Actions.longPollReceivedRawDeltas).toHaveBeenCalled()\n\n    xit \"should call helper methods for all creates first, then modifications, then destroys\", ->\n      spyOn(Actions, 'longPollProcessedDeltas')\n\n      handleDeltaDeletionPromises = []\n      resolveDeltaDeletionPromises = ->\n        fn() for fn in handleDeltaDeletionPromises\n        handleDeltaDeletionPromises = []\n\n      spyOn(DeltaProcessor, '_handleDestroyDelta').andCallFake ->\n        new Promise (resolve, reject) ->\n          handleDeltaDeletionPromises.push(resolve)\n\n      handleModelResponsePromises = []\n      resolveModelResponsePromises = ->\n        fn() for fn in handleModelResponsePromises\n        handleModelResponsePromises = []\n\n      spyOn(NylasAPIHelpers, 'handleModelResponse').andCallFake ->\n        new Promise (resolve, reject) ->\n          handleModelResponsePromises.push(resolve)\n\n      spyOn(DeltaProcessor, '_clusterDeltas').andReturn(JSON.parse(JSON.stringify(@sampleClustered)))\n      DeltaProcessor.process(@sampleDeltas)\n\n      createTypes = Object.keys(@sampleClustered['create'])\n      expect(NylasAPIHelpers.handleModelResponse.calls.length).toEqual(createTypes.length)\n      expect(NylasAPIHelpers.handleModelResponse.calls[0].args[0]).toEqual(_.values(@sampleClustered['create'][createTypes[0]]))\n      expect(DeltaProcessor._handleDestroyDelta.calls.length).toEqual(0)\n\n      NylasAPIHelpers.handleModelResponse.reset()\n      resolveModelResponsePromises()\n      advanceClock()\n\n      modifyTypes = Object.keys(@sampleClustered['modify'])\n      expect(NylasAPIHelpers.handleModelResponse.calls.length).toEqual(modifyTypes.length)\n      expect(NylasAPIHelpers.handleModelResponse.calls[0].args[0]).toEqual(_.values(@sampleClustered['modify'][modifyTypes[0]]))\n      expect(DeltaProcessor._handleDestroyDelta.calls.length).toEqual(0)\n\n      NylasAPIHelpers.handleModelResponse.reset()\n      resolveModelResponsePromises()\n      advanceClock()\n\n      destroyCount = @sampleClustered['destroy'].length\n      expect(DeltaProcessor._handleDestroyDelta.calls.length).toEqual(destroyCount)\n      expect(DeltaProcessor._handleDestroyDelta.calls[0].args[0]).toEqual(@sampleClustered['destroy'][0])\n\n      expect(Actions.longPollProcessedDeltas).not.toHaveBeenCalled()\n\n      resolveDeltaDeletionPromises()\n      advanceClock()\n\n      expect(Actions.longPollProcessedDeltas).toHaveBeenCalled()\n\n  describe \"clusterDeltas\", ->\n    beforeEach ->\n      @sampleDeltas = JSON.parse(fs.readFileSync(\"#{fixturesPath}/sample-deltas.json\"))\n      @expectedClustered = JSON.parse(fs.readFileSync(\"#{fixturesPath}/sample-deltas-clustered.json\"))\n\n    it \"should collect create/modify events into a hash by model type\", ->\n      {create, modify} = DeltaProcessor._clusterDeltas(@sampleDeltas)\n      expect(create).toEqual(@expectedClustered.create)\n      expect(modify).toEqual(@expectedClustered.modify)\n\n    it \"should collect destroys into an array\", ->\n      {destroy} = DeltaProcessor._clusterDeltas(@sampleDeltas)\n      expect(destroy).toEqual(@expectedClustered.destroy)\n\n  describe \"handleDeltaDeletion\", ->\n    beforeEach ->\n      @thread = new Thread(id: 'idhere')\n      @delta =\n        \"cursor\": \"bb95ddzqtr2gpmvgrng73t6ih\",\n        \"object\": \"thread\",\n        \"event\": \"delete\",\n        \"objectId\": @thread.id,\n        \"timestamp\": \"2015-08-26T17:36:45.297Z\"\n\n      spyOn(DatabaseWriter.prototype, 'unpersistModel')\n\n    it \"should resolve if the object cannot be found\", ->\n      spyOn(DatabaseWriter.prototype, 'find').andCallFake (klass, id) =>\n        return Promise.resolve(null)\n      waitsForPromise =>\n        DeltaProcessor._handleDestroyDelta(@delta)\n      runs =>\n        expect(DatabaseWriter.prototype.find).toHaveBeenCalledWith(Thread, 'idhere')\n        expect(DatabaseWriter.prototype.unpersistModel).not.toHaveBeenCalled()\n\n    it \"should call unpersistModel if the object exists\", ->\n      spyOn(DatabaseWriter.prototype, 'find').andCallFake (klass, id) =>\n        return Promise.resolve(@thread)\n      waitsForPromise =>\n        DeltaProcessor._handleDestroyDelta(@delta)\n      runs =>\n        expect(DatabaseWriter.prototype.find).toHaveBeenCalledWith(Thread, 'idhere')\n        expect(DatabaseWriter.prototype.unpersistModel).toHaveBeenCalledWith(@thread)\n\n  describe \"handleModelResponse\", ->\n    # SEE spec/nylas-api-spec.coffee\n\n  describe \"receives metadata deltas\", ->\n    beforeEach ->\n      @stubDB = {}\n      spyOn(DatabaseWriter.prototype, 'find').andCallFake (klass, id) =>\n        return @stubDB[id]\n      spyOn(DatabaseWriter.prototype, 'findAll').andCallFake (klass, where) =>\n        ids = where.id\n        models = []\n        ids.forEach (id) =>\n          model = @stubDB[id]\n          if model\n            models.push(model)\n        return models\n      spyOn(DatabaseWriter.prototype, 'persistModels').andCallFake (models) =>\n        models.forEach (model) =>\n          @stubDB[model.id] = model\n        return Promise.resolve()\n\n      @messageMetadataDelta =\n        id: 519,\n        event: \"create\",\n        object: \"metadata\",\n        objectId: 8876,\n        changedFields: [\"version\", \"object\"],\n        attributes:\n          id: 8876,\n          value: {link_clicks: 1},\n          object: \"metadata\",\n          version: 2,\n          plugin_id: \"link-tracking\",\n          object_id: '2887',\n          object_type: \"message\",\n          account_id: 2\n\n      @threadMetadataDelta =\n        id: 392,\n        event: \"create\",\n        object: \"metadata\",\n        objectId: 3845,\n        changedFields: [\"version\", \"object\"],\n        attributes:\n          id: 3845,\n          value: {shouldNotify: true},\n          object: \"metadata\",\n          version: 2,\n          plugin_id: \"send-reminders\",\n          object_id: 't:3984',\n          object_type: \"thread\"\n          account_id: 2,\n\n    it \"saves metadata to existing Messages\", ->\n      message = new Message({serverId: @messageMetadataDelta.attributes.object_id})\n      @stubDB[message.id] = message\n      waitsForPromise =>\n        DeltaProcessor.process([@messageMetadataDelta])\n      runs ->\n        message = @stubDB[message.id] # refresh reference\n        expect(message.pluginMetadata.length).toEqual(1)\n        expect(message.metadataForPluginId('link-tracking')).toEqual({link_clicks: 1})\n\n    it \"saves metadata to existing Threads\", ->\n      thread = new Thread({serverId: @threadMetadataDelta.attributes.object_id})\n      @stubDB[thread.id] = thread\n      waitsForPromise =>\n        DeltaProcessor.process([@threadMetadataDelta])\n      runs ->\n        thread = @stubDB[thread.id] # refresh reference\n        expect(thread.pluginMetadata.length).toEqual(1)\n        expect(thread.metadataForPluginId('send-reminders')).toEqual({shouldNotify: true})\n\n    it \"knows how to reconcile different thread ids\", ->\n      thread = new Thread({serverId: 't:1948'})\n      @stubDB[thread.id] = thread\n      message = new Message({\n        serverId: @threadMetadataDelta.attributes.object_id.substring(2),\n        threadId: thread.id\n      })\n      @stubDB[message.id] = message\n      waitsForPromise =>\n        DeltaProcessor.process([@threadMetadataDelta])\n      runs ->\n        thread = @stubDB[thread.id] # refresh reference\n        expect(thread.pluginMetadata.length).toEqual(1)\n        expect(thread.metadataForPluginId('send-reminders')).toEqual({shouldNotify: true})\n\n    it \"creates ghost Messages if necessary\", ->\n      waitsForPromise =>\n        DeltaProcessor.process([@messageMetadataDelta])\n      runs ->\n        message = @stubDB[@messageMetadataDelta.attributes.object_id]\n        expect(message).toBeDefined()\n        expect(message.pluginMetadata.length).toEqual(1)\n        expect(message.metadataForPluginId('link-tracking')).toEqual({link_clicks: 1})\n\n    it \"creates ghost Threads if necessary\", ->\n      waitsForPromise =>\n        DeltaProcessor.process([@threadMetadataDelta])\n      runs ->\n        thread = @stubDB[@threadMetadataDelta.attributes.object_id]\n        expect(thread.pluginMetadata.length).toEqual(1)\n        expect(thread.metadataForPluginId('send-reminders')).toEqual({shouldNotify: true})\n\n"
  },
  {
    "path": "packages/client-app/spec/services/delta-streaming-connection-spec.coffee",
    "content": "_ = require 'underscore'\n{NylasAPI, NylasAPIHelpers, NylasAPIRequest, Actions, DatabaseStore, DatabaseWriter, Account, Thread} = require 'nylas-exports'\nDeltaStreamingConnection = require('../../src/services/delta-streaming-connection').default\n\n# TODO these are badly out of date, we need to rewrite them\nxdescribe \"DeltaStreamingConnection\", ->\n  beforeEach ->\n    @apiRequests = []\n    spyOn(NylasAPIRequest.prototype, \"run\").andCallFake ->\n      @apiRequests.push({requestOptions: this.options})\n    @localSyncCursorStub = undefined\n    @n1CloudCursorStub = undefined\n    # spyOn(DeltaStreamingConnection.prototype, '_fetchMetadata').andReturn(Promise.resolve())\n    spyOn(DatabaseWriter.prototype, 'persistJSONBlob').andReturn(Promise.resolve())\n    spyOn(DatabaseStore, 'findJSONBlob').andCallFake (key) =>\n      if key is \"NylasSyncWorker:#{TEST_ACCOUNT_ID}\"\n        return Promise.resolve _.extend {}, {\n          \"deltaCursors\": {\n            \"localSync\": @localSyncCursorStub,\n            \"n1Cloud\": @n1CloudCursorStub,\n          }\n          \"initialized\": true,\n          \"contacts\":\n            busy: true\n            complete: false\n          \"calendars\":\n            busy:false\n            complete: true\n        }\n      else if key.indexOf('ContactRankings') is 0\n        return Promise.resolve([])\n      else\n        return throw new Error(\"Not stubbed! #{key}\")\n\n\n    spyOn(DeltaStreamingConnection.prototype, 'start')\n    @account = new Account(clientId: TEST_ACCOUNT_CLIENT_ID, serverId: TEST_ACCOUNT_ID, organizationUnit: 'label')\n    @worker = new DeltaStreamingConnection(@account)\n    @worker.loadStateFromDatabase()\n    advanceClock()\n    @worker.start()\n    @worker._metadata = {\"a\": [{\"id\":\"b\"}]}\n    @deltaStreams = @worker._deltaStreams\n    advanceClock()\n\n  it \"should reset `busy` to false when reading state from disk\", ->\n    @worker = new DeltaStreamingConnection(@account)\n    spyOn(@worker, '_resume')\n    @worker.loadStateFromDatabase()\n    advanceClock()\n    expect(@worker._state.contacts.busy).toEqual(false)\n\n  describe \"start\", ->\n    it \"should open the delta connection\", ->\n      @worker.start()\n      advanceClock()\n      expect(@deltaStreams.localSync.start).toHaveBeenCalled()\n      expect(@deltaStreams.n1Cloud.start).toHaveBeenCalled()\n\n    it \"should start querying for model collections that haven't been fully cached\", ->\n      waitsForPromise => @worker.start().then =>\n        expect(@apiRequests.length).toBe(7)\n        modelsRequested = _.compact _.map @apiRequests, ({model}) -> model\n        expect(modelsRequested).toEqual(['threads', 'messages', 'folders', 'labels', 'drafts', 'contacts', 'events'])\n\n        expect(modelsRequested).toEqual(['threads', 'messages', 'folders', 'labels', 'drafts', 'contacts', 'events'])\n\n    it \"should fetch 1000 labels and folders, to prevent issues where Inbox is not in the first page\", ->\n      labelsRequest = _.find @apiRequests, (r) -> r.model is 'labels'\n      expect(labelsRequest.params.limit).toBe(1000)\n\n    it \"should mark incomplete collections as `busy`\", ->\n      @worker.start()\n      advanceClock()\n      nextState = @worker._state\n\n      for collection in ['contacts','threads','drafts', 'labels']\n        expect(nextState[collection].busy).toEqual(true)\n\n    it \"should initialize count and fetched to 0\", ->\n      @worker.start()\n      advanceClock()\n      nextState = @worker._state\n\n      for collection in ['contacts','threads','drafts', 'labels']\n        expect(nextState[collection].fetched).toEqual(0)\n        expect(nextState[collection].count).toEqual(0)\n\n    it \"after failures, it should attempt to resume periodically but back off as failures continue\", ->\n      simulateNetworkFailure = =>\n        @apiRequests[0].requestOptions.error({statusCode: 400})\n        @apiRequests = []\n\n      spyOn(@worker, '_resume').andCallThrough()\n      spyOn(Math, 'random').andReturn(1.0)\n      @worker.start()\n\n      expectThings = (resumeCallCount, randomCallCount) =>\n        expect(@worker._resume.callCount).toBe(resumeCallCount)\n        expect(Math.random.callCount).toBe(randomCallCount)\n\n      expect(@worker._resume.callCount).toBe(1, 1)\n      simulateNetworkFailure(); expectThings(1, 1)\n      advanceClock(4000); advanceClock();       expectThings(2, 1)\n      simulateNetworkFailure(); expectThings(2, 2)\n      advanceClock(4000); advanceClock();       expectThings(2, 2)\n      advanceClock(4000); advanceClock();       expectThings(3, 2)\n      simulateNetworkFailure(); expectThings(3, 3)\n      advanceClock(4000); advanceClock();       expectThings(3, 3)\n      advanceClock(4000); advanceClock();       expectThings(3, 3)\n      advanceClock(4000); advanceClock();       expectThings(4, 3)\n      simulateNetworkFailure(); expectThings(4, 4)\n      advanceClock(4000); advanceClock();       expectThings(4, 4)\n      advanceClock(4000); advanceClock();       expectThings(4, 4)\n      advanceClock(4000); advanceClock();       expectThings(4, 4)\n      advanceClock(4000); advanceClock();       expectThings(4, 4)\n      advanceClock(4000); advanceClock();       expectThings(5, 4)\n\n    it \"handles the request as a failure if we try and grab labels or folders without an 'inbox'\", ->\n      spyOn(@worker, '_resume').andCallThrough()\n      @worker.start()\n      expect(@worker._resume.callCount).toBe(1)\n      request = _.findWhere(@apiRequests, model: 'labels')\n      request.requestOptions.success([])\n      expect(@worker._resume.callCount).toBe(1)\n      advanceClock(30000); advanceClock()\n      expect(@worker._resume.callCount).toBe(2)\n\n    it \"handles the request as a success if we try and grab labels or folders and it includes the 'inbox'\", ->\n      spyOn(@worker, '_resume').andCallThrough()\n      @worker.start()\n      expect(@worker._resume.callCount).toBe(1)\n      request = _.findWhere(@apiRequests, model: 'labels')\n      request.requestOptions.success([{name: \"inbox\"}, {name: \"archive\"}])\n      expect(@worker._resume.callCount).toBe(1)\n      advanceClock(30000); advanceClock()\n      expect(@worker._resume.callCount).toBe(1)\n\n  describe \"delta streaming cursor\", ->\n    it \"should read the cursor from the database\", ->\n      spyOn(DeltaStreamingConnection.prototype, 'latestCursor').andReturn Promise.resolve()\n\n      @localSyncCursorStub = undefined\n      @n1CloudCursorStub = undefined\n\n      # no cursor present\n      worker = new DeltaStreamingConnection(@account)\n      deltaStreams = worker._deltaStreams\n      expect(deltaStreams.localSync.hasCursor()).toBe(false)\n      expect(deltaStreams.n1Cloud.hasCursor()).toBe(false)\n      worker.loadStateFromDatabase()\n      advanceClock()\n      expect(deltaStreams.localSync.hasCursor()).toBe(false)\n      expect(deltaStreams.n1Cloud.hasCursor()).toBe(false)\n\n      # cursor present in database\n      @localSyncCursorStub = \"new-school\"\n      @n1CloudCursorStub = 123\n\n      worker = new DeltaStreamingConnection(@account)\n      deltaStreams = worker._deltaStreams\n      expect(deltaStreams.localSync.hasCursor()).toBe(false)\n      expect(deltaStreams.n1Cloud.hasCursor()).toBe(false)\n      worker.loadStateFromDatabase()\n      advanceClock()\n      expect(deltaStreams.localSync.hasCursor()).toBe(true)\n      expect(deltaStreams.n1Cloud.hasCursor()).toBe(true)\n      expect(deltaStreams.localSync._getCursor()).toEqual('new-school')\n      expect(deltaStreams.n1Cloud._getCursor()).toEqual(123)\n\n    it \"should set the cursor to the last cursor after receiving deltas\", ->\n      spyOn(DeltaStreamingConnection.prototype, 'latestCursor').andReturn Promise.resolve()\n      worker = new DeltaStreamingConnection(@account)\n      advanceClock()\n      deltaStreams = worker._deltaStreams\n      deltas = [{cursor: '1'}, {cursor: '2'}]\n      deltaStreams.localSync._emitter.emit('results-stopped-arriving', deltas)\n      deltaStreams.n1Cloud._emitter.emit('results-stopped-arriving', deltas)\n      advanceClock()\n      expect(deltaStreams.localSync._getCursor()).toEqual('2')\n      expect(deltaStreams.n1Cloud._getCursor()).toEqual('2')\n\n  describe \"_resume\", ->\n    it \"should fetch metadata first and fetch other collections when metadata is ready\", ->\n      fetchAllMetadataCallback = null\n      spyOn(@worker, '_fetchCollectionPage')\n      @worker._state = {}\n      @worker._resume()\n      expect(@worker._fetchMetadata).toHaveBeenCalled()\n      expect(@worker._fetchCollectionPage.calls.length).toBe(0)\n      advanceClock()\n      expect(@worker._fetchCollectionPage.calls.length).not.toBe(0)\n\n    it \"should fetch collections for which `_shouldFetchCollection` returns true\", ->\n      spyOn(@worker, '_fetchCollectionPage')\n      spyOn(@worker, '_shouldFetchCollection').andCallFake (collection) =>\n        return collection.model in ['threads', 'labels', 'drafts']\n      @worker._resume()\n      advanceClock()\n      advanceClock()\n      expect(@worker._fetchCollectionPage.calls.map (call) -> call.args[0]).toEqual(['threads', 'labels', 'drafts'])\n\n    it \"should be called when Actions.retryDeltaConnection is received\", ->\n      spyOn(DeltaStreamingConnection.prototype, 'latestCursor').andReturn Promise.resolve()\n\n      # TODO why do we need to call through?\n      spyOn(@worker, '_resume').andCallThrough()\n      Actions.retryDeltaConnection()\n      expect(@worker._resume).toHaveBeenCalled()\n\n  describe \"_shouldFetchCollection\", ->\n    it \"should return false if the collection sync is already in progress\", ->\n      @worker._state.threads = {\n        'busy': true\n        'complete': false\n      }\n      expect(@worker._shouldFetchCollection({model: 'threads'})).toBe(false)\n\n    it \"should return false if the collection sync is already complete\", ->\n      @worker._state.threads = {\n        'busy': false\n        'complete': true\n      }\n      expect(@worker._shouldFetchCollection({model: 'threads'})).toBe(false)\n\n    it \"should return true otherwise\", ->\n      @worker._state.threads = {\n        'busy': false\n        'complete': false\n      }\n      expect(@worker._shouldFetchCollection({model: 'threads'})).toBe(true)\n      @worker._state.threads = undefined\n      expect(@worker._shouldFetchCollection({model: 'threads'})).toBe(true)\n\n  describe \"_fetchCollection\", ->\n    beforeEach ->\n      @apiRequests = []\n\n    it \"should pass any metadata it preloaded\", ->\n      @worker._state.threads = {\n        'busy': false\n        'complete': false\n      }\n      @worker._fetchCollection({model: 'threads'})\n      expect(@apiRequests[0].model).toBe('threads')\n      expect(@apiRequests[0].requestOptions.metadataToAttach).toBe(@worker._metadata)\n\n    describe \"when there is no request history (`lastRequestRange`)\", ->\n      it \"should start the first request for models\", ->\n        @worker._state.threads = {\n          'busy': false\n          'complete': false\n        }\n        @worker._fetchCollection({model: 'threads'})\n        expect(@apiRequests[0].model).toBe('threads')\n        expect(@apiRequests[0].params.offset).toBe(0)\n\n    describe \"when it was previously trying to fetch a page (`lastRequestRange`)\", ->\n      beforeEach ->\n        @worker._state.threads =\n          'count': 1200\n          'fetched': 100\n          'busy': false\n          'complete': false\n          'error': new Error(\"Something bad\")\n          'lastRequestRange':\n            offset: 100\n            limit: 50\n\n      it \"should start paginating from the request that was interrupted\", ->\n        @worker._fetchCollection({model: 'threads'})\n        expect(@apiRequests[0].model).toBe('threads')\n        expect(@apiRequests[0].params.offset).toBe(100)\n        expect(@apiRequests[0].params.limit).toBe(50)\n\n      it \"should not reset the `count`, `fetched` or start fetching the count\", ->\n        @worker._fetchCollection({model: 'threads'})\n        expect(@worker._state.threads.fetched).toBe(100)\n        expect(@worker._state.threads.count).toBe(1200)\n        expect(@apiRequests.length).toBe(1)\n\n    describe 'when maxFetchCount option is specified', ->\n      it \"should only fetch maxFetch count on the first request if it is less than initialPageSize\", ->\n        @worker._state.messages =\n          count: 1000\n          fetched: 0\n        @worker._fetchCollection({model: 'messages', initialPageSize: 30, maxFetchCount: 25})\n        expect(@apiRequests[0].params.offset).toBe 0\n        expect(@apiRequests[0].params.limit).toBe 25\n\n      it \"sould only fetch the maxFetchCount when restoring from saved state\", ->\n        @worker._state.messages =\n          count: 1000\n          fetched: 470\n          lastRequestRange: {\n            limit: 50,\n            offset: 470,\n          }\n        @worker._fetchCollection({model: 'messages', maxFetchCount: 500})\n        expect(@apiRequests[0].params.offset).toBe 470\n        expect(@apiRequests[0].params.limit).toBe 30\n\n  describe \"_fetchCollectionPage\", ->\n    beforeEach ->\n      @apiRequests = []\n\n    describe 'when maxFetchCount option is specified', ->\n      it 'should not fetch next page if maxFetchCount has been reached', ->\n        @worker._state.messages =\n          count: 1000\n          fetched: 470\n        @worker._fetchCollectionPage('messages', {limit: 30, offset: 470}, {maxFetchCount: 500})\n        {success} = @apiRequests[0].requestOptions\n        success({length: 30})\n        expect(@worker._state.messages.fetched).toBe 500\n        advanceClock(2000); advanceClock()\n        expect(@apiRequests.length).toBe 1\n\n      it 'should limit by maxFetchCount when requesting the next page', ->\n        @worker._state.messages =\n          count: 1000\n          fetched: 450\n        @worker._fetchCollectionPage('messages', {limit: 30, offset: 450 }, {maxFetchCount: 500})\n        {success} = @apiRequests[0].requestOptions\n        success({length: 30})\n        expect(@worker._state.messages.fetched).toBe 480\n        advanceClock(2000); advanceClock()\n        expect(@apiRequests[1].params.offset).toBe 480\n        expect(@apiRequests[1].params.limit).toBe 20\n\n  describe \"when an API request completes\", ->\n    beforeEach ->\n      @worker.start()\n      advanceClock()\n      @request = @apiRequests[0]\n      @apiRequests = []\n\n    describe \"successfully, with models\", ->\n      it \"should start out by requesting a small number of items\", ->\n        expect(@request.params.limit).toBe DeltaStreamingConnection.INITIAL_PAGE_SIZE\n\n      it \"should request the next page\", ->\n        pageSize = @request.params.limit\n        models = []\n        models.push(new Thread) for i in [0..(pageSize-1)]\n        @request.requestOptions.success(models)\n        advanceClock(2000); advanceClock()\n        expect(@apiRequests.length).toBe(1)\n        expect(@apiRequests[0].params.offset).toEqual @request.params.offset + pageSize\n\n      it \"increase the limit on the next page load by 50%\", ->\n        pageSize = @request.params.limit\n        models = []\n        models.push(new Thread) for i in [0..(pageSize-1)]\n        @request.requestOptions.success(models)\n        advanceClock(2000); advanceClock()\n        expect(@apiRequests.length).toBe(1)\n        expect(@apiRequests[0].params.limit).toEqual pageSize * 1.5,\n\n      it \"never requests more then MAX_PAGE_SIZE\", ->\n        pageSize = @request.params.limit = DeltaStreamingConnection.MAX_PAGE_SIZE\n        models = []\n        models.push(new Thread) for i in [0..(pageSize-1)]\n        @request.requestOptions.success(models)\n        advanceClock(2000); advanceClock()\n        expect(@apiRequests.length).toBe(1)\n        expect(@apiRequests[0].params.limit).toEqual DeltaStreamingConnection.MAX_PAGE_SIZE\n\n      it \"should update the fetched count on the collection\", ->\n        expect(@worker._state.threads.fetched).toEqual(0)\n        pageSize = @request.params.limit\n        models = []\n        models.push(new Thread) for i in [0..(pageSize-1)]\n        @request.requestOptions.success(models)\n        expect(@worker._state.threads.fetched).toEqual(pageSize)\n\n    describe \"successfully, with fewer models than requested\", ->\n      beforeEach ->\n        models = []\n        models.push(new Thread) for i in [0..100]\n        @request.requestOptions.success(models)\n\n      it \"should not request another page\", ->\n        expect(@apiRequests.length).toBe(0)\n\n      it \"should update the state to complete\", ->\n        expect(@worker._state.threads.busy).toEqual(false)\n        expect(@worker._state.threads.complete).toEqual(true)\n\n      it \"should update the fetched count on the collection\", ->\n        expect(@worker._state.threads.fetched).toEqual(101)\n\n    describe \"successfully, with no models\", ->\n      it \"should not request another page\", ->\n        @request.requestOptions.success([])\n        expect(@apiRequests.length).toBe(0)\n\n      it \"should update the state to complete\", ->\n        @request.requestOptions.success([])\n        expect(@worker._state.threads.busy).toEqual(false)\n        expect(@worker._state.threads.complete).toEqual(true)\n\n    describe \"with an error\", ->\n      it \"should log the error to the state, along with the range that failed\", ->\n        err = new Error(\"Oh no a network error\")\n        @request.requestOptions.error(err)\n        expect(@worker._state.threads.busy).toEqual(false)\n        expect(@worker._state.threads.complete).toEqual(false)\n        expect(@worker._state.threads.error).toEqual(err.toString())\n        expect(@worker._state.threads.lastRequestRange).toEqual({offset: 0, limit: 30})\n\n      it \"should not request another page\", ->\n        @request.requestOptions.error(new Error(\"Oh no a network error\"))\n        expect(@apiRequests.length).toBe(0)\n\n    describe \"succeeds after a previous error\", ->\n      beforeEach ->\n        @worker._state.threads.error = new Error(\"Something bad happened\")\n        @worker._state.threads.lastRequestRange = {limit: 10, offset: 10}\n        @request.requestOptions.success([])\n        advanceClock(1)\n\n      it \"should clear any previous error and updates lastRequestRange\", ->\n        expect(@worker._state.threads.error).toEqual(null)\n        expect(@worker._state.threads.lastRequestRange).toEqual({offset: 0, limit: 30})\n\n  describe \"cleanup\", ->\n    it \"should termiate the delta connection\", ->\n      spyOn(@deltaStreams.localSync, 'end')\n      spyOn(@deltaStreams.n1Cloud, 'end')\n      @worker.cleanup()\n      expect(@deltaStreams.localSync.end).toHaveBeenCalled()\n      expect(@deltaStreams.n1Cloud.end).toHaveBeenCalled()\n\n    it \"should stop trying to restart failed collection syncs\", ->\n      spyOn(console, 'log')\n      spyOn(@worker, '_resume').andCallThrough()\n      @worker.cleanup()\n      advanceClock(50000); advanceClock()\n      expect(@worker._resume.callCount).toBe(0)\n\n"
  },
  {
    "path": "packages/client-app/spec/services/inline-style-transformer-spec.coffee",
    "content": "InlineStyleTransformer = require('../../src/services/inline-style-transformer').default\n{ipcRenderer} = require 'electron'\n\ndescribe \"InlineStyleTransformer\", ->\n  describe \"run\", ->\n    beforeEach ->\n      spyOn(ipcRenderer, 'send')\n      spyOn(InlineStyleTransformer, '_injectUserAgentStyles').andCallFake (input) =>\n        return input\n      InlineStyleTransformer._inlineStylePromises = {}\n\n    it \"should return a Promise\", ->\n      expect(InlineStyleTransformer.run(\"asd\") instanceof Promise).toBe(true)\n\n    it \"should resolve immediately if the html is empty\", ->\n      result = InlineStyleTransformer.run(\"\")\n      expect(result.isResolved()).toBe(true)\n\n    it \"should resolve immediately if there is no <style> tag in the source\", ->\n      result = InlineStyleTransformer.run(\"\"\"\n      This is some tricky HTML but there's no style tag here!\n      <I wonder if it'll get into trouble < style >. <Ohmgerd.>\n      \"\"\")\n      expect(result.isResolved()).toBe(true)\n\n    it \"should properly remove comment tags used to prevent style tags from being displayed when they're not understood\", ->\n      result = InlineStyleTransformer.run(\"\"\"\n      <style>\n      <!--table\n      {mso-displayed-decimal-separator:\"\\.\";\n      mso-displayed-thousand-separator:\"\\,\";}\n      -->\n      </style>\n      <style><!--table\n      {mso-displayed-decimal-separator:\"\\.\";\n      mso-displayed-thousand-separator:\"\\,\";}\n      --></style>\n      \"\"\")\n      expect(ipcRenderer.send.mostRecentCall.args[1].html).toEqual(\"\"\"\n      <style>table\n      {mso-displayed-decimal-separator:\".\";\n      mso-displayed-thousand-separator:\",\";}\n      </style>\n      <style>table\n      {mso-displayed-decimal-separator:\".\";\n      mso-displayed-thousand-separator:\",\";}\n      </style>\n      \"\"\")\n\n    it \"should add user agent styles\", ->\n      InlineStyleTransformer.run(\"\"\"<style>\n      <!--table\n      \t{mso-displayed-decimal-separator:\"\\.\";\n      \tmso-displayed-thousand-separator:\"\\,\";}\n      -->\n      </style>Other content goes here\"\"\")\n      expect(InlineStyleTransformer._injectUserAgentStyles).toHaveBeenCalled()\n\n    it \"should fire inline-style-parse to the main process\", ->\n      InlineStyleTransformer.run(\"\"\"<style>\n      <!--table\n      \t{mso-displayed-decimal-separator:\"\\.\";\n      \tmso-displayed-thousand-separator:\"\\,\";}\n      -->\n      </style>Other content goes here\"\"\")\n      expect(ipcRenderer.send).toHaveBeenCalled()\n      expect(ipcRenderer.send.mostRecentCall.args[0]).toEqual('inline-style-parse')\n"
  },
  {
    "path": "packages/client-app/spec/services/quoted-html-transformer-spec.coffee",
    "content": "_ = require('underscore')\nfs = require('fs')\npath = require 'path'\nQuotedHTMLTransformer = require('../../src/services/quoted-html-transformer').default\n\ndescribe \"QuotedHTMLTransformer\", ->\n\n  readFile = (fname) ->\n    emailPath = path.resolve(__dirname, '..', 'fixtures', 'emails', fname)\n    return fs.readFileSync(emailPath, 'utf8')\n\n  hideQuotedHTML = (fname) ->\n    return QuotedHTMLTransformer.hideQuotedHTML(readFile(fname))\n\n  removeQuotedHTML = (fname, opts={}) ->\n    return QuotedHTMLTransformer.removeQuotedHTML(readFile(fname), opts)\n\n  numQuotes = (html) ->\n    re = new RegExp(QuotedHTMLTransformer.annotationClass, 'g')\n    html.match(re)?.length ? 0\n\n  [1..23].forEach (n) ->\n    it \"properly parses email_#{n}\", ->\n      opts = keepIfWholeBodyIsQuote: true\n      expect(removeQuotedHTML(\"email_#{n}.html\", opts).trim()).toEqual(readFile(\"email_#{n}_stripped.html\").trim())\n\n  describe 'manual quote detection tests', ->\n\n    clean = (str) ->\n      str.replace(/[\\n\\r]/g, \"\").replace(/\\s{2,}/g, \" \")\n\n    # The key is the inHTML. The value is the outHTML\n    tests = []\n\n    # Test 1\n    tests.push\n      before: \"\"\"\n        <div>\n          Some text\n\n          <p>More text</p>\n\n          <blockquote id=\"inline-parent-quote\">\n            Parent\n            <blockquote id=\"inline-sub-quote\">\n              Sub\n              <blockquote id=\"inline-sub-sub-quote\">Sub Sub</blockquote>\n              Sub\n            </blockquote>\n          </blockquote>\n\n          <div>Text at end</div>\n\n          <blockquote id=\"last-quote\">\n            <blockquote>\n              The last quote!\n            </blockquote>\n          </blockquote>\n\n\n        </div>\n        \"\"\"\n      after: \"\"\"\n        <div>\n          Some text\n\n          <p>More text</p>\n\n          <blockquote id=\"inline-parent-quote\">\n            Parent\n            <blockquote id=\"inline-sub-quote\">\n              Sub\n              <blockquote id=\"inline-sub-sub-quote\">Sub Sub</blockquote>\n              Sub\n            </blockquote>\n          </blockquote>\n\n          <div>Text at end</div>\n         </div>\n        \"\"\"\n\n    # Test 2: Basic quote removal\n    tests.push\n      before: \"\"\"\n        <br>\n        Yo\n        <blockquote>Nothing but quotes</blockquote>\n        <br>\n        <br>\n        \"\"\"\n      after: \"\"\"\n        <br>\n        Yo\n        <br>\n        <br>\n        \"\"\"\n\n    # Test 3: It found the blockquote in another div\n    tests.push\n      before: \"\"\"\n        <div>Hello World</div>\n        <br>\n        <div>\n          <blockquote>Nothing but quotes</blockquote>\n        </div>\n        <br>\n        <br>\n        \"\"\"\n      after: \"\"\"\n        <div>Hello World</div>\n        <br>\n        <div>\n         </div>\n        <br>\n        <br>\n        \"\"\"\n\n      # Test 4: It works inside of a wrapped div\n    tests.push\n      before: \"\"\"\n        <div>\n          <br>\n          <blockquote>Nothing but quotes</blockquote>\n          <br>\n          <br>\n        </div>\n        \"\"\"\n      after: \"\"\"\n        <div>\n          <br>\n          <br>\n          <br>\n        </div>\n        \"\"\"\n\n    # Test 5: Inline quotes and text\n    tests.push\n      before: \"\"\"\n        Hello\n        <blockquote>Inline quote</blockquote>\n        World\n        \"\"\"\n      after: \"\"\"\n        Hello\n        <blockquote>Inline quote</blockquote>\n        World\n        \"\"\"\n\n    # Test 6: No quoted elements at all\n    tests.push\n      before: \"\"\"\n        Hello World\n        \"\"\"\n      after: \"\"\"\n        Hello World\n        \"\"\"\n\n    # Test 7: Common ancestor is a quoted node\n    tests.push\n      before: \"\"\"\n        <div>Content</div>\n        <blockquote>\n          Some content\n          <blockquote>More content</blockquote>\n          Other content\n        </blockquote>\n        \"\"\"\n      after: \"\"\"\n        <div>Content</div>\n        \"\"\"\n\n    # Test 8: All of our quote blocks we want to remove are at the end…\n    # sortof… but nested in a bunch of stuff\n    #\n    # Note that \"content\" is burried deep in the middle of a div\n    tests.push\n      before: \"\"\"\n        <div>Content</div>\n        <blockquote>\n          Some content\n          <blockquote>More content</blockquote>\n          Other content\n        </blockquote>\n        <div>\n          <blockquote>Some text quote</blockquote>\n          Some text\n          <div>\n            More text\n            <blockquote>A quote</blockquote>\n            <br>\n          </div>\n          <br>\n          <blockquote>Another quote</blockquote>\n          <br>\n        </div>\n        <br>\n        <blockquote>More quotes!</blockquote>\n        \"\"\"\n      after: \"\"\"\n        <div>Content</div>\n        <blockquote>\n          Some content\n          <blockquote>More content</blockquote>\n          Other content\n        </blockquote>\n        <div>\n          <blockquote>Some text quote</blockquote>\n          Some text\n          <div>\n            More text\n            <br>\n          </div>\n          <br>\n          <br>\n        </div>\n        <br>\n\n        \"\"\"\n\n    # Test 9: Last several tags are blockquotes. Note the 3 blockquote\n    # at the end, the interstital div, and the blockquote inside of the\n    # first div\n    tests.push\n      before: \"\"\"\n        <div>\n          <blockquote>I'm inline</blockquote>\n          Content\n          <blockquote>Remove me</blockquote>\n        </div>\n        <blockquote>Foo</blockquote>\n        <div></div>\n        <blockquote>Bar</blockquote>\n        <blockquote>Baz</blockquote>\n        \"\"\"\n      after: \"\"\"\n        <div>\n          <blockquote>I'm inline</blockquote>\n          Content\n         </div>\n        <div></div>\n        \"\"\"\n\n    # Test 10: If it's only a quote and no other text, then just show the\n    # quote\n    tests.push\n      before: \"\"\"\n        <br>\n        <blockquote>Nothing but quotes</blockquote>\n        <br>\n        <br>\n        \"\"\"\n      after: \"\"\"\n        <br>\n        <blockquote>Nothing but quotes</blockquote>\n        <br>\n        <br>\n        \"\"\"\n\n\n    # Test 11: The <body> tag itself is just a quoted text block.\n    # I believe this is https://sentry.nylas.com/sentry/edgehill/group/8323/\n    tests.push\n      before: \"\"\"\n        <body id=\"OLK_SRC_BODY_SECTION\">\n          This entire thing is quoted text!\n        </body>\n        \"\"\"\n      after: \"<head></head><body></body>\"\n      options: { keepIfWholeBodyIsQuote: false }\n\n    # Test 12: Make sure that a single quote inside of a bunch of other\n    # content is detected. We used to have a bug where we were only\n    # looking at the common ancestor of blockquotes (and if there's 1 then\n    # the ancestor is itself). We now look at the root document for\n    # trailing text.\n    tests.push\n      before: \"\"\"\n        <br>\n        Yo\n        <table><tbody>\n          <tr><td>A</td><td>B</td></tr>\n          <tr><td>C</td><td><blockquote>SAVE ME</blockquote></td></tr>\n          <tr><td>E</td><td>F</td></tr>\n        </tbody></table>\n        Yo\n        <br>\n        \"\"\"\n      after: \"\"\"\n        <br>\n        Yo\n        <table><tbody>\n          <tr><td>A</td><td>B</td></tr>\n          <tr><td>C</td><td><blockquote>SAVE ME</blockquote></td></tr>\n          <tr><td>E</td><td>F</td></tr>\n        </tbody></table>\n        Yo\n        <br>\n        \"\"\"\n\n    # Test 13: If there's an \"On date…\" string immediatley before a blockquote,\n    # then remove it.\n    tests.push\n      before: \"\"\"\n        Hey\n        <div>\n          On FOOBAR\n          <br>\n          On Thu, Mar 3, 2016\n          at 3:19 AM,\n          First Middle Last-Last\n          <span dir=\"ltr\">\n            &lt;\n            <a href=\"mailto:test@nylas.com\" target=\"_blank\">\n              test@nylas.com\n            </a>\n            &gt;\n          </span>\n          wrote:\n          <br>\n          <blockquote>\n            QUOTED TEXT\n          </blockquote>\n        </div>\n        <br>\n      \"\"\"\n      after: \"\"\"\n        Hey\n        <div>\n          On FOOBAR\n          <br><br>\n          </div><br>\n      \"\"\"\n\n    # Test 14: Don't pick up false positives on the string precursors to block\n    # quotes.\n    tests.push\n      before: \"\"\"\n        Hey\n        <div>\n        On FOOBAR\n        <br>\n        On Thu, Mar 3, 2016 I went to my writing club and wrote:\n        <strong>A little song</strong>\n        <blockquote>\n          QUOTED TEXT\n        </blockquote>\n        </div>\n      \"\"\"\n      after: \"\"\"\n        Hey\n        <div>\n        On FOOBAR\n        <br>\n        On Thu, Mar 3, 2016 I went to my writing club and wrote:\n        <strong>A little song</strong>\n        </div>\n      \"\"\"\n\n    # Test 15: Make sure inline quote in plaintext converted to HTML with <pre>\n    # is not completely stripped.\n    tests.push\n      before: \"\"\"\n        <pre class=\"nylas-plaintext\">On Wed, Dec 14, 2016 at 02:05:44PM +0100, Bálint Réczey wrote:\n        &gt; I have uploaded a dpkg NMU with bindnow enabled to DELAYED/10\n        &gt; according to current NMU rules. If the Release Team increases the\n        &gt; severity of #835146 it can reach unstable earlier.\n        Thanks!\n\n        --\n        WBR, wRAR\n        </pre>\n      \"\"\"\n      after: \"\"\"\n        <pre class=\"nylas-plaintext\">On Wed, Dec 14, 2016 at 02:05:44PM +0100, Bálint Réczey wrote:\n        &gt; I have uploaded a dpkg NMU with bindnow enabled to DELAYED/10\n        &gt; according to current NMU rules. If the Release Team increases the\n        &gt; severity of #835146 it can reach unstable earlier.\n        Thanks!\n\n        --\n        WBR, wRAR\n        </pre>\n      \"\"\"\n\n    it 'works with these manual test cases', ->\n      for {before, after, options} in tests\n        if not options\n          options = {keepIfWholeBodyIsQuote: true}\n        test = clean(QuotedHTMLTransformer.removeQuotedHTML(before, options))\n        expect(test).toEqual clean(after)\n\n    it 'removes all trailing <br> tags except one', ->\n      input0 = \"hello world<br><br><blockquote>foolololol</blockquote>\"\n      expect0 = \"hello world<br>\"\n      expect(QuotedHTMLTransformer.removeQuotedHTML(input0)).toEqual expect0\n\n    it 'preserves <br> tags in the middle and only chops off tail', ->\n      input0 = \"hello<br><br>world<br><br><blockquote>foolololol</blockquote>\"\n      expect0 = \"hello<br><br>world<br>\"\n      expect(QuotedHTMLTransformer.removeQuotedHTML(input0)).toEqual expect0\n\n    it 'works as expected when body tag inside the html', ->\n      input0 = \"\"\"\n      <br><br><blockquote class=\"gmail_quote\"\n        style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n        On Dec 16 2015, at 7:08 pm, Juan Tejada &lt;juan@nylas.com&gt; wrote:\n        <br>\n\n\n      <meta content=\"text/html; charset=us-ascii\" />\n\n      <body>\n      <h1 id=\"h2\">h2</h1>\n      <p>he he hehehehehehe</p>\n      <p>dufjcasc</p>\n\n\n      </blockquote>\n      \"\"\"\n      expect0 = \"<head></head><body><br></body>\"\n      expect(QuotedHTMLTransformer.removeQuotedHTML(input0, {keepIfWholeBodyIsQuote: false})).toEqual expect0\n\n\n  # We have a little utility method that you can manually uncomment to\n  # generate what the current iteration of the QuotedHTMLTransformer things the\n  # `removeQuotedHTML` should look like. These can be manually inspected in\n  # a browser before getting their filename changed to\n  # `email_#{n}_stripped.html\". The actually tests will run the current\n  # iteration of the `removeQuotedHTML` against these files to catch if\n  # anything has changed in the parser.\n  #\n  # It's inside of the specs here instaed of its own script because the\n  # `QuotedHTMLTransformer` needs Electron booted up in order to work because\n  # of the DOMParser.\n  xit \"Run this simple function to generate output files\", ->\n    [18, 20].forEach (n) ->\n      newHTML = QuotedHTMLTransformer.removeQuotedHTML(readFile(\"email_#{n}.html\"))\n      outPath = path.resolve(__dirname, '..', 'fixtures', 'emails', \"email_#{n}_raw_stripped.html\")\n      fs.writeFileSync(outPath, newHTML)\n"
  },
  {
    "path": "packages/client-app/spec/services/quoted-plain-text-transformer-spec.coffee",
    "content": "# This is modied from https://github.com/mko/emailreplyparser, which is a\n# JS port of GitHub's Ruby https://github.com/github/email_reply_parser\n\nfs = require('fs')\npath = require 'path'\n_ = require('underscore')\nQuotedPlainTextParser = require('../../src/services/quoted-plain-text-transformer')\n\ngetParsedEmail = (name, format=\"plain\") ->\n  data = getRawEmail(name, format)\n  reply = QuotedPlainTextParser.parse data, format\n  reply._setHiddenState()\n  return reply\n\ngetRawEmail = (name, format=\"plain\") ->\n  emailPath = path.resolve(__dirname, '..', 'fixtures', 'emails', \"#{name}.txt\")\n  return fs.readFileSync(emailPath, \"utf8\")\n\ndeepEqual = (expected=[], test=[]) ->\n  for toExpect, i in expected\n    expect(test[i]).toBe toExpect\n\ndescribe \"QuotedPlainTextParser\", ->\n  it \"reads_simple_body\", ->\n    reply = getParsedEmail('email_1_1')\n    expect(reply.fragments.length).toBe 3\n    deepEqual [\n      false\n      false\n      false\n    ], _.map(reply.fragments, (f) ->\n      f.quoted\n    )\n    deepEqual [\n      false\n      true\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.signature\n    )\n    deepEqual [\n      false\n      true\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.hidden\n    )\n    expect(reply.fragments[0].to_s()).toEqual 'Hi folks\\n\\nWhat is the best way to clear a Riak bucket of all key, values after\\nrunning a test?\\nI am currently using the Java HTTP API.'\n    expect(reply.fragments[1].to_s()).toEqual '-Abhishek Kona'\n\n  it \"reads_top_post\", ->\n    reply = getParsedEmail('email_1_3')\n    expect(reply.fragments.length).toEqual 5\n\n    deepEqual [\n      false\n      false\n      true\n      false\n      false\n    ], _.map(reply.fragments, (f) ->\n      f.quoted\n    )\n    deepEqual [\n      false\n      true\n      true\n      true\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.hidden\n    )\n    deepEqual [\n      false\n      true\n      false\n      false\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.signature\n    )\n    expect(new RegExp('^Oh thanks.\\n\\nHaving').test(reply.fragments[0].to_s())).toBe true\n    expect(new RegExp('^-A').test(reply.fragments[1].to_s())).toBe true\n    expect(/^On [^\\:]+\\:/m.test(reply.fragments[2].to_s())).toBe true\n    expect(new RegExp('^_').test(reply.fragments[4].to_s())).toBe true\n\n  it \"reads_bottom_post\", ->\n    reply = getParsedEmail('email_1_2')\n    expect(reply.fragments.length).toEqual 6\n    deepEqual [\n      false\n      true\n      false\n      true\n      false\n      false\n    ], _.map(reply.fragments, (f) ->\n      f.quoted\n    )\n    deepEqual [\n      false\n      false\n      false\n      false\n      false\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.signature\n    )\n    deepEqual [\n      false\n      false\n      false\n      true\n      true\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.hidden\n    )\n    expect(reply.fragments[0].to_s()).toEqual 'Hi,'\n    expect(new RegExp('^On [^:]+:').test(reply.fragments[1].to_s())).toBe true\n    expect(/^You can list/m.test(reply.fragments[2].to_s())).toBe true\n    expect(/^> /m.test(reply.fragments[3].to_s())).toBe true\n    expect(new RegExp('^_').test(reply.fragments[5].to_s())).toBe true\n\n  it \"reads_inline_replies\", ->\n    reply = getParsedEmail('email_1_8')\n    expect(reply.fragments.length).toEqual 7\n    deepEqual [\n      true\n      false\n      true\n      false\n      true\n      false\n      false\n    ], _.map(reply.fragments, (f) ->\n      f.quoted\n    )\n    deepEqual [\n      false\n      false\n      false\n      false\n      false\n      false\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.signature\n    )\n    deepEqual [\n      false\n      false\n      false\n      false\n      true\n      true\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.hidden\n    )\n    expect(new RegExp('^On [^:]+:').test(reply.fragments[0].to_s())).toBe true\n    expect(/^I will reply/m.test(reply.fragments[1].to_s())).toBe true\n    expect(/^> /m.test(reply.fragments[2].to_s())).toBe true\n    expect(/^and under this./m.test(reply.fragments[3].to_s())).toBe true\n    expect(/^> /m.test(reply.fragments[4].to_s())).toBe true\n    expect(reply.fragments[5].to_s().trim()).toEqual ''\n    expect(new RegExp('^-').test(reply.fragments[6].to_s())).toBe true\n\n  it \"recognizes_date_string_above_quote\", ->\n    reply = getParsedEmail('email_1_4')\n    expect(/^Awesome/.test(reply.fragments[0].to_s())).toBe true\n    expect(/^On/m.test(reply.fragments[1].to_s())).toBe true\n    expect(/Loader/m.test(reply.fragments[1].to_s())).toBe true\n\n  it \"a_complex_body_with_only_one_fragment\", ->\n    reply = getParsedEmail('email_1_5')\n    expect(reply.fragments.length).toEqual 1\n\n  it \"reads_email_with_correct_signature\", ->\n    reply = getParsedEmail('correct_sig')\n    expect(reply.fragments.length).toEqual 2\n    deepEqual [\n      false\n      false\n    ], _.map(reply.fragments, (f) ->\n      f.quoted\n    )\n    deepEqual [\n      false\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.signature\n    )\n    deepEqual [\n      false\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.hidden\n    )\n    expect(new RegExp('^-- \\nrick').test(reply.fragments[1].to_s())).toBe true\n\n  it \"deals_with_multiline_reply_headers\", ->\n    reply = getParsedEmail('email_1_6')\n    expect(new RegExp('^I get').test(reply.fragments[0].to_s())).toBe true\n    expect(/^On/m.test(reply.fragments[1].to_s())).toBe true\n    expect(new RegExp('Was this').test(reply.fragments[1].to_s())).toBe true\n\n  it \"does_not_modify_input_string\", ->\n    original = 'The Quick Brown Fox Jumps Over The Lazy Dog'\n    QuotedPlainTextParser.parse original\n    expect(original).toEqual 'The Quick Brown Fox Jumps Over The Lazy Dog'\n\n  it \"returns_only_the_visible_fragments_as_a_string\", ->\n    reply = getParsedEmail('email_2_1')\n\n    String::rtrim = ->\n      @replace /\\s*$/g, ''\n\n    fragments = _.select(reply.fragments, (f) ->\n      !f.hidden\n    )\n    fragments = _.map(fragments, (f) ->\n      f.to_s()\n    )\n    expect(reply.visibleText(format: \"plain\")).toEqual fragments.join('\\n').rtrim()\n\n  it \"parse_out_just_top_for_outlook_reply\", ->\n    body = getRawEmail('email_2_1')\n    expect(QuotedPlainTextParser.visibleText(body, format: \"plain\")).toEqual 'Outlook with a reply'\n\n  it \"parse_out_sent_from_iPhone\", ->\n    body = getRawEmail('email_iPhone')\n    expect(QuotedPlainTextParser.visibleText(body, format: \"plain\")).toEqual 'Here is another email'\n\n  it \"parse_out_sent_from_BlackBerry\", ->\n    body = getRawEmail('email_BlackBerry')\n    expect(QuotedPlainTextParser.visibleText(body, format: \"plain\")).toEqual 'Here is another email'\n\n  it \"parse_out_send_from_multiword_mobile_device\", ->\n    body = getRawEmail('email_multi_word_sent_from_my_mobile_device')\n    expect(QuotedPlainTextParser.visibleText(body, format: \"plain\")).toEqual 'Here is another email'\n\n  it \"do_not_parse_out_send_from_in_regular_sentence\", ->\n    body = getRawEmail('email_sent_from_my_not_signature')\n    expect(QuotedPlainTextParser.visibleText(body, format: \"plain\")).toEqual 'Here is another email\\n\\nSent from my desk, is much easier then my mobile phone.'\n\n  it \"retains_bullets\", ->\n    body = getRawEmail('email_bullets')\n    expect(QuotedPlainTextParser.visibleText(body, format: \"plain\")).toEqual 'test 2 this should list second\\n\\nand have spaces\\n\\nand retain this formatting\\n\\n\\n   - how about bullets\\n   - and another'\n\n  it \"visibleText\", ->\n    body = getRawEmail('email_1_2')\n    expect(QuotedPlainTextParser.visibleText(body, format: \"plain\")).toEqual QuotedPlainTextParser.parse(body).visibleText(format: \"plain\")\n\n  it \"correctly_reads_top_post_when_line_starts_with_On\", ->\n    reply = getParsedEmail('email_1_7')\n    expect(reply.fragments.length).toEqual 5\n    deepEqual [\n      false\n      false\n      true\n      false\n      false\n    ], _.map(reply.fragments, (f) ->\n      f.quoted\n    )\n    deepEqual [\n      false\n      true\n      true\n      true\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.hidden\n    )\n    deepEqual [\n      false\n      true\n      false\n      false\n      true\n    ], _.map(reply.fragments, (f) ->\n      f.signature\n    )\n    expect(new RegExp('^Oh thanks.\\n\\nOn the').test(reply.fragments[0].to_s())).toBe true\n    expect(new RegExp('^-A').test(reply.fragments[1].to_s())).toBe true\n    expect(/^On [^\\:]+\\:/m.test(reply.fragments[2].to_s())).toBe true\n    expect(new RegExp('^_').test(reply.fragments[4].to_s())).toBe true\n"
  },
  {
    "path": "packages/client-app/spec/services/search/search-query-backend-imap-spec.es6",
    "content": "import SearchQueryParser from '../../../src/services/search/search-query-parser'\nimport IMAPSearchQueryBackend from '../../../src/services/search/search-query-backend-imap'\n\ndescribe('IMAPSearchQueryBackend', () => {\n  it('correctly codegens TEXT', () => {\n    const ast = SearchQueryParser.parse('foo');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual([['TEXT', 'foo']]);\n  });\n  it('correctly codegens FROM', () => {\n    const ast = SearchQueryParser.parse('from:mark');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual([['FROM', 'mark']]);\n  });\n  it('correctly codegens TO', () => {\n    const ast = SearchQueryParser.parse('to:mark');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual([['TO', 'mark']]);\n  });\n  it('correctly codegens SUBJECT', () => {\n    const ast = SearchQueryParser.parse('subject:foobar');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual([['SUBJECT', 'foobar']]);\n  });\n  it('correctly codegens UNREAD', () => {\n    const ast = SearchQueryParser.parse('is:unread');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual(['UNSEEN']);\n  });\n  it('correctly codegens SEEN', () => {\n    const ast = SearchQueryParser.parse('is:read');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual(['SEEN']);\n  });\n  it('correctly codegens FLAGGED', () => {\n    const ast = SearchQueryParser.parse('is:starred');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual(['FLAGGED']);\n  });\n  it('correctly codegens UNFLAGGED', () => {\n    const ast = SearchQueryParser.parse('is:unstarred');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual(['UNFLAGGED']);\n  });\n  it('correctly codegens AND', () => {\n    const ast1 = SearchQueryParser.parse('is:starred AND is:unread');\n    const result1 = IMAPSearchQueryBackend.compile(ast1, {name: 'INBOX'});\n    expect(result1).toEqual(['FLAGGED', 'UNSEEN']);\n\n    const ast2 = SearchQueryParser.parse('is:starred is:unread');\n    const result2 = IMAPSearchQueryBackend.compile(ast2, {name: 'INBOX'});\n    expect(result2).toEqual(['FLAGGED', 'UNSEEN']);\n  });\n  it('correctly codegens OR', () => {\n    const ast = SearchQueryParser.parse('is:starred OR is:unread');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual([['OR', 'FLAGGED', 'UNSEEN']]);\n  });\n  it('correctly codegens \"in:foo\"', () => {\n    const ast1 = SearchQueryParser.parse('is:starred OR in:foo');\n    const result1 = IMAPSearchQueryBackend.compile(ast1, {name: 'INBOX'});\n    expect(result1).toEqual([['OR', 'FLAGGED', '!ALL']]);\n    const result2 = IMAPSearchQueryBackend.compile(ast1, {name: 'foo'});\n    expect(result2).toEqual([['OR', 'FLAGGED', 'ALL']]);\n    const ast2 = SearchQueryParser.parse('in:foo');\n    const result3 = IMAPSearchQueryBackend.compile(ast2, {name: 'foo'});\n    expect(result3).toEqual(['ALL']);\n    const result4 = IMAPSearchQueryBackend.compile(ast2, {name: 'INBOX'});\n    expect(result4).toEqual(['!ALL']);\n  });\n  it('correctly codegens has:attachment', () => {\n    const ast = SearchQueryParser.parse('has:attachment');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual([['OR', ['HEADER', 'Content-Type', 'multipart/mixed'],\n                                   ['HEADER', 'Content-Type', 'multipart/related']]]);\n  });\n  it('correctly joins adjacent AND queries', () => {\n    const ast = SearchQueryParser.parse('is:starred AND is:unread AND foo');\n    const result = IMAPSearchQueryBackend.compile(ast, {name: 'INBOX'});\n    expect(result).toEqual(['FLAGGED', 'UNSEEN', ['TEXT', 'foo']]);\n  });\n  it('correctly deduces the set of folders', () => {\n    const ast1 = SearchQueryParser.parse('is:starred');\n    const result1 = IMAPSearchQueryBackend.folderNamesForQuery(ast1);\n    expect(result1).toEqual(IMAPSearchQueryBackend.ALL_FOLDERS());\n\n    const ast2 = SearchQueryParser.parse('in:foo');\n    const result2 = IMAPSearchQueryBackend.folderNamesForQuery(ast2);\n    expect(result2).toEqual(['foo']);\n\n    const ast3 = SearchQueryParser.parse('in:foo AND in:bar');\n    const result3 = IMAPSearchQueryBackend.folderNamesForQuery(ast3);\n    expect(result3).toEqual([]);\n\n    const ast4 = SearchQueryParser.parse('in:foo OR bar');\n    const result4 = IMAPSearchQueryBackend.folderNamesForQuery(ast4);\n    expect(result4).toEqual(IMAPSearchQueryBackend.ALL_FOLDERS());\n\n    const ast5 = SearchQueryParser.parse('in:foo OR in:bar');\n    const result5 = IMAPSearchQueryBackend.folderNamesForQuery(ast5);\n    expect(result5).toEqual(['foo', 'bar']);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/services/search/search-query-parser-spec.es6",
    "content": "import {\n  SearchQueryAST,\n  SearchQueryParser,\n} from 'nylas-exports';\n\nconst {\n  SearchQueryToken,\n  OrQueryExpression,\n  AndQueryExpression,\n  FromQueryExpression,\n  ToQueryExpression,\n  SubjectQueryExpression,\n  GenericQueryExpression,\n  TextQueryExpression,\n  UnreadStatusQueryExpression,\n  StarredStatusQueryExpression,\n  InQueryExpression,\n  HasAttachmentQueryExpression,\n} = SearchQueryAST;\n\nconst token = (text) => { return new SearchQueryToken(text); }\nconst and = (e1, e2) => { return new AndQueryExpression(e1, e2); }\nconst or = (e1, e2) => { return new OrQueryExpression(e1, e2); }\nconst from = (text) => { return new FromQueryExpression(text); }\nconst to = (text) => { return new ToQueryExpression(text); }\nconst subject = (text) => { return new SubjectQueryExpression(text); }\nconst generic = (text) => { return new GenericQueryExpression(text); }\nconst in_ = (text) => { return new InQueryExpression(text); }\nconst text = (tok) => { return new TextQueryExpression(tok); }\nconst unread = (status) => { return new UnreadStatusQueryExpression(status); }\nconst starred = (status) => { return new StarredStatusQueryExpression(status); }\nconst has = () => { return new HasAttachmentQueryExpression(); }\n\n\ndescribe('SearchQueryParser.parse', () => {\n  it('correctly parses simple queries', () => {\n    expect(SearchQueryParser.parse('blah').equals(\n      generic(text(token('blah')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('\"foo bar\"').equals(\n      generic(text(token('foo bar')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:blah').equals(\n      to(text(token('blah')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('from:blah').equals(\n      from(text(token('blah')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('subject:blah').equals(\n      subject(text(token('blah')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:mhahnenb@gmail.com').equals(\n      to(text(token('mhahnenb@gmail.com')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:\"mhahnenb@gmail.com\"').equals(\n      to(text(token('mhahnenb@gmail.com')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:\"Mark mhahnenb@gmail.com\"').equals(\n      to(text(token('Mark mhahnenb@gmail.com')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('is:unread').equals(\n      unread(true)\n    )).toBe(true)\n    expect(SearchQueryParser.parse('is:read').equals(\n      unread(false)\n    )).toBe(true)\n    expect(SearchQueryParser.parse('is:starred').equals(\n      starred(true)\n    )).toBe(true)\n    expect(SearchQueryParser.parse('is:unstarred').equals(\n      starred(false)\n    )).toBe(true)\n    expect(SearchQueryParser.parse('in:foo').equals(\n      in_(text(token('foo')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('has:attachment').equals(has())).toBe(true)\n  });\n\n  it('correctly parses reserved words as normal text in certain places', () => {\n    expect(SearchQueryParser.parse('to:blah').equals(\n      to(text(token('blah')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:to').equals(\n      to(text(token('to')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:subject').equals(\n      to(text(token('subject')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:from').equals(\n      to(text(token('from')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:unread').equals(\n      to(text(token('unread')))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:starred').equals(\n      to(text(token('starred')))\n    )).toBe(true)\n  });\n\n  it('correctly parses compound queries', () => {\n    expect(SearchQueryParser.parse('foo bar').equals(\n      and(generic(text(token('foo'))), generic(text(token('bar'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo AND bar').equals(\n      and(generic(text(token('foo'))), generic(text(token('bar'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo OR bar').equals(\n      or(generic(text(token('foo'))), generic(text(token('bar'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('to:foo OR bar').equals(\n      or(to(text(token('foo'))), generic(text(token('bar'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo OR to:bar').equals(\n      or(generic(text(token('foo'))), to(text(token('bar'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo bar baz').equals(\n      and(generic(text(token('foo'))),\n        and(generic(text(token('bar'))), generic(text(token('baz')))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo AND bar AND baz').equals(\n      and(generic(text(token('foo'))),\n        and(generic(text(token('bar'))), generic(text(token('baz')))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo OR bar AND baz').equals(\n      and(\n        or(generic(text(token('foo'))), generic(text(token('bar')))),\n        generic(text(token('baz'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo OR bar OR baz').equals(\n      or(generic(text(token('foo'))),\n        or(generic(text(token('bar'))), generic(text(token('baz')))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('foo is:unread').equals(\n      and(generic(text(token('foo'))), unread(true)),\n    )).toBe(true)\n    expect(SearchQueryParser.parse('is:unread foo').equals(\n      and(unread(true), generic(text(token('foo'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('(from:mg to:mark) AND (in:\"Sent Items\")').equals(\n      and(and(from(text(token('mg'))), to(text(token('mark')))), in_(text(token('Sent Items'))))\n    )).toBe(true)\n    expect(SearchQueryParser.parse('(from:mg (to:mark subject:blah)) AND (in:\"Sent Items\" to:foo)').equals(\n      and(\n        and(from(text(token('mg'))), and(to(text(token('mark'))), subject(text(token('blah'))))),\n        and(in_(text(token('Sent Items'))), to(text(token('foo')))))\n    )).toBe(true)\n  });\n  it('correctly parses queries with whitespace at the end', () => {\n    expect(SearchQueryParser.parse('from:blah ').equals(\n      from(text(token('blah')))\n    )).toBe(true)\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/spellchecker-spec.es6",
    "content": "/* eslint global-require: 0 */\nimport fs from 'fs'\nimport {Spellchecker} from 'nylas-exports';\n\ndescribe(\"Spellchecker\", function spellcheckerTests() {\n  beforeEach(() => {\n    // electron-spellchecker is under heavy development, make sure we can still\n    // rely on this method\n    expect(Spellchecker.handler.handleElectronSpellCheck).toBeDefined()\n    this.customDict = '{}'\n    spyOn(fs, 'writeFile').andCallFake((path, customDict, cb) => {\n      this.customDict = customDict\n      cb()\n    })\n    spyOn(fs, 'readFile').andCallFake((path, cb) => {\n      cb(null, this.customDict)\n    })\n    // Apparently handleElectronSpellCheck returns !misspelled\n    spyOn(Spellchecker.handler, 'handleElectronSpellCheck').andReturn(false)\n    Spellchecker.isMisspelledCache = {}\n  });\n\n  it('does not call spellchecker when word has already been learned', () => {\n    Spellchecker.isMisspelledCache = {mispelled: true}\n    const misspelled = Spellchecker.isMisspelled('mispelled')\n    expect(misspelled).toBe(true)\n    expect(Spellchecker.handler.handleElectronSpellCheck).not.toHaveBeenCalled()\n  });\n\n  describe(\"when a custom word is added\", () => {\n    this.customWord = \"becaause\"\n\n    beforeEach(() => {\n      expect(Spellchecker.isMisspelled(this.customWord)).toEqual(true)\n      Spellchecker.learnWord(this.customWord);\n    })\n\n    afterEach(() => {\n      Spellchecker.unlearnWord(this.customWord);\n      expect(Spellchecker.isMisspelled(this.customWord)).toEqual(true)\n    })\n\n    it(\"doesn't think it's misspelled\", () => {\n      expect(Spellchecker.isMisspelled(this.customWord)).toEqual(false)\n    })\n\n    it(\"maintains it across instances\", () => {\n      const Spellchecker2 = require(\"../src/spellchecker\").default;\n      expect(Spellchecker2.isMisspelled(this.customWord)).toEqual(false);\n    })\n  })\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/account-store-spec.coffee",
    "content": "_ = require 'underscore'\nKeyManager = require('../../src/key-manager').default\nNylasAPI = require('../../src/flux/nylas-api').default\nNylasAPIRequest = require('../../src/flux/nylas-api-request').default\nAccountStore = require('../../src/flux/stores/account-store').default\nAccount = require('../../src/flux/models/account').default\nActions = require('../../src/flux/actions').default\n\ndescribe \"AccountStore\", ->\n  beforeEach ->\n    @instance = null\n    @constructor = AccountStore.constructor\n    @keys = {}\n    spyOn(KeyManager, 'getPassword').andCallFake (account) =>\n      @keys[account]\n    spyOn(KeyManager, 'deletePassword').andCallFake (account) =>\n      delete @keys[account]\n    spyOn(KeyManager, 'replacePassword').andCallFake (account, pass) =>\n      @keys[account] = pass\n\n    @spyOnConfig = =>\n      @configVersion = 1\n      @configAccounts =\n        [{\n          \"id\": \"A\",\n          \"client_id\" : 'local-4f9d476a-c173',\n          \"server_id\" : 'A',\n          \"email_address\":\"bengotow@gmail.com\",\n          \"object\":\"account\"\n          \"organization_unit\": \"label\"\n          \"aliases\": [\"Alias <alias@nylas.com>\"]\n        },{\n          \"id\": \"B\",\n          \"client_id\" : 'local-4f9d476a-c175',\n          \"server_id\" : 'B',\n          \"email_address\":\"ben@nylas.com\",\n          \"object\":\"account\"\n          \"organization_unit\": \"label\"\n        }]\n\n      spyOn(NylasEnv.config, 'get').andCallFake (key) =>\n        return 'production' if key is 'env'\n        return @configAccounts if key is 'nylas.accounts'\n        return @configVersion if key is 'nylas.accountsVersion'\n        return null\n\n  afterEach ->\n    @instance.stopListeningToAll()\n\n  describe \"initialization\", ->\n    beforeEach ->\n      spyOn(NylasEnv.config, 'set')\n      @spyOnConfig()\n\n    it \"should initialize the accounts and version from config\", ->\n      @instance = new @constructor\n      expect(@instance._version).toEqual(@configVersion)\n      expect(@instance.accounts()).toEqual([\n        (new Account).fromJSON(@configAccounts[0]),\n        (new Account).fromJSON(@configAccounts[1])\n      ])\n\n    it \"should initialize tokens from KeyManager\", ->\n      jasmine.unspy(KeyManager, 'getPassword')\n      spyOn(KeyManager, 'getPassword').andCallFake (account) =>\n        return 'AL-TOKEN' if account is 'bengotow@gmail.com.localSync'\n        return 'AC-TOKEN' if account is 'bengotow@gmail.com.n1Cloud'\n        return 'BL-TOKEN' if account is 'ben@nylas.com.localSync'\n        return 'BC-TOKEN' if account is 'ben@nylas.com.n1Cloud'\n        return null\n      @instance = new @constructor\n      expect(@instance.tokensForAccountId('A')).toEqual({localSync: 'AL-TOKEN', n1Cloud: 'AC-TOKEN'})\n      expect(@instance.tokensForAccountId('B')).toEqual({localSync: 'BL-TOKEN', n1Cloud: 'BC-TOKEN'})\n\n  describe \"accountForEmail\", ->\n    beforeEach ->\n      @instance = new @constructor\n      @ac1 = new Account emailAddress: 'juan@nylas.com', aliases: []\n      @ac2 = new Account emailAddress: 'juan@gmail.com', aliases: ['Juan <juanchis@gmail.com>']\n      @ac3 = new Account emailAddress: 'jackie@columbia.edu', aliases: ['Jackie Luo <jacqueline.luo@columbia.edu>']\n      @instance._accounts = [@ac1, @ac2, @ac3]\n\n    it 'returns correct account when no alises present', ->\n      expect(@instance.accountForEmail('juan@nylas.com')).toEqual @ac1\n\n    it 'returns correct account when alias is used', ->\n      expect(@instance.accountForEmail('juanchis@gmail.com')).toEqual @ac2\n      expect(@instance.accountForEmail('jacqueline.luo@columbia.edu')).toEqual @ac3\n\n  describe \"adding account from json\", ->\n    beforeEach ->\n      @json =\n        \"id\": \"B\",\n        \"client_id\" : 'local-4f9d476a-c175',\n        \"server_id\" : 'B',\n        \"email_address\":\"ben@nylas.com\",\n        \"provider\":\"gmail\",\n        \"object\":\"account\",\n        \"organization_unit\": \"label\",\n      @instance = new @constructor\n      spyOn(NylasEnv.config, \"set\")\n      spyOn(@instance, \"trigger\")\n      @instance.addAccountFromJSON(@json, \"LOCAL_TOKEN\", \"CLOUD_TOKEN\")\n\n    it \"saves the token to KeyManager and to the loaded tokens cache\", ->\n      expect(@instance._tokens[\"B\"]).toEqual({n1Cloud: \"CLOUD_TOKEN\", localSync: \"LOCAL_TOKEN\"})\n      expect(KeyManager.replacePassword).toHaveBeenCalledWith(\"ben@nylas.com.localSync\", \"LOCAL_TOKEN\")\n      expect(KeyManager.replacePassword).toHaveBeenCalledWith(\"ben@nylas.com.n1Cloud\", \"CLOUD_TOKEN\")\n\n    it \"saves the account to the accounts cache and saves\", ->\n      account = (new Account).fromJSON(@json)\n      expect(@instance._accounts.length).toBe 1\n      expect(@instance._accounts[0]).toEqual account\n      expect(NylasEnv.config.set.calls.length).toBe 2\n      expect(NylasEnv.config.set.calls[0].args).toEqual(['nylas.accounts', [account.toJSON()]])\n      # Version must be updated last since it will trigger other windows to load nylas.accounts\n      expect(NylasEnv.config.set.calls[1].args).toEqual(['nylas.accountsVersion', 1])\n\n    it \"triggers\", ->\n      expect(@instance.trigger).toHaveBeenCalled()\n\n    describe \"when an account with the same ID is already present\", ->\n      it \"should update it\", ->\n        @json =\n          \"id\": \"B\",\n          \"client_id\" : 'local-4f9d476a-c175',\n          \"server_id\" : 'B',\n          \"email_address\":\"ben@nylas.com\",\n          \"provider\":\"gmail\",\n          \"object\":\"account\"\n          \"organization_unit\": \"label\"\n        @spyOnConfig()\n        @instance = new @constructor\n        spyOn(@instance, \"trigger\")\n        expect(@instance._accounts.length).toBe 2\n        @instance.addAccountFromJSON(@json, \"B-NEW-LOCAL-TOKEN\", \"B-NEW-CLOUD-TOKEN\")\n        expect(@instance._accounts.length).toBe 2\n\n    describe \"when an account with the same email, but different ID, is already present\", ->\n      it \"should update it\", ->\n        @json =\n          \"id\": \"NEVER SEEN BEFORE\",\n          \"client_id\" : 'local-4f9d476a-c175',\n          \"server_id\" : 'NEVER SEEN BEFORE',\n          \"email_address\":\"ben@nylas.com\",\n          \"provider\":\"gmail\",\n          \"object\":\"account\"\n          \"organization_unit\": \"label\"\n        @spyOnConfig()\n        @instance = new @constructor\n        spyOn(@instance, \"trigger\")\n        expect(@instance._accounts.length).toBe 2\n        @instance.addAccountFromJSON(@json, \"B-NEW-LOCAL-TOKEN\", \"B-NEW-CLOUD-TOKEN\")\n        expect(@instance._accounts.length).toBe 2\n        expect(@instance.accountForId('B')).toBe(undefined)\n        expect(@instance.accountForId('NEVER SEEN BEFORE')).not.toBe(undefined)\n\n  describe \"handleAuthenticationFailure\", ->\n    beforeEach ->\n      spyOn(NylasEnv.config, 'set')\n      @spyOnConfig()\n      @instance = new @constructor\n      spyOn(@instance, \"trigger\")\n      @instance._tokens =\n        \"A\":\n          localSync: 'token'\n          n1Cloud: 'token'\n        \"B\":\n          localSync: 'token'\n          n1Cloud: 'token'\n\n    it \"should put the account in an `invalid` state\", ->\n      spyOn(@instance, \"_onUpdateAccount\")\n      spyOn(AccountStore, 'tokensForAccountId').andReturn({localSync: 'token'})\n      @instance._onAPIAuthError(new Error(), auth: user: 'token')\n      expect(@instance._onUpdateAccount).toHaveBeenCalled()\n      expect(@instance._onUpdateAccount.callCount).toBe(1)\n      expect(@instance._onUpdateAccount.mostRecentCall.args).toEqual(['A', {syncState: 'invalid'}])\n\n    it \"should put the N1 Cloud account in an `invalid` state\", ->\n      spyOn(@instance, \"_onUpdateAccount\")\n      spyOn(AccountStore, 'tokensForAccountId').andReturn({n1Cloud: 'token'})\n      @instance._onAPIAuthError(new Error(), auth: user: 'token', 'N1CloudAPI')\n      expect(@instance._onUpdateAccount).toHaveBeenCalled()\n      expect(@instance._onUpdateAccount.mostRecentCall.args).toEqual(['A', {n1CloudState: 'n1_cloud_auth_failed'}])\n\n    it \"should not throw an exception if the account cannot be found\", ->\n      spyOn(@instance, \"_onUpdateAccount\")\n      @instance._onAPIAuthError(new Error(), auth: user: 'not found')\n      expect(@instance._onUpdateAccount).not.toHaveBeenCalled()\n\n  describe \"isMyEmail\", ->\n    beforeEach ->\n      spyOn(NylasEnv.config, 'set')\n      @spyOnConfig()\n      @instance = new @constructor\n\n    it \"works with account emails\", ->\n      expect(@instance.isMyEmail(\"bengotow@gmail.com\")).toBe(true)\n      expect(@instance.isMyEmail(\"ben@nylas.com\")).toBe(true)\n      expect(@instance.isMyEmail(\"foo@bar.com\")).toBe(false)\n      expect(@instance.isMyEmail(\"ben@gmail.com\")).toBe(false)\n\n    it \"works with multiple emails\", ->\n      expect(@instance.isMyEmail([\"bengotow@gmail.com\", \"ben@nylas.com\"])).toBe(true)\n      expect(@instance.isMyEmail([\"bengotow@gmail.com\", \"foo@bar.com\"])).toBe(true)\n      expect(@instance.isMyEmail([\"blah@gmail.com\", \"foo@bar.com\"])).toBe(false)\n\n    it \"works with aliases\", ->\n      expect(@instance.isMyEmail(\"alias@nylas.com\")).toBe(true)\n      expect(@instance.isMyEmail(\"foo@bar.com\")).toBe(false)\n\n    it \"works with miscased emails\", ->\n      expect(@instance.isMyEmail(\"Bengotow@Gmail.com\")).toBe(true)\n      expect(@instance.isMyEmail(\"Ben@Nylas.com  \")).toBe(true)\n\n    it \"works with plus aliases\", ->\n      expect(@instance.isMyEmail(\"bengotow+stuff@gmail.com\")).toBe(true)\n      expect(@instance.isMyEmail(\"ben+bar+baz@nylas.com\")).toBe(true)\n      expect(@instance.isMyEmail(\"ben=stuff@nylas.com\")).toBe(false)\n"
  },
  {
    "path": "packages/client-app/spec/stores/badge-store-spec.coffee",
    "content": "Label = require('../../src/flux/models/label').default\nBadgeStore = require('../../src/flux/stores/badge-store').default\n\ndescribe \"BadgeStore\", ->\n  describe \"_setBadgeForCount\", ->\n    it \"should set the badge correctly\", ->\n      spyOn(BadgeStore, '_setBadge')\n      BadgeStore._unread = 0\n      BadgeStore._setBadgeForCount()\n      expect(BadgeStore._setBadge).toHaveBeenCalledWith(\"\")\n      BadgeStore._unread = 1\n      BadgeStore._setBadgeForCount()\n      expect(BadgeStore._setBadge).toHaveBeenCalledWith(\"1\")\n      BadgeStore._unread = 100\n      BadgeStore._setBadgeForCount()\n      expect(BadgeStore._setBadge).toHaveBeenCalledWith(\"100\")\n      BadgeStore._unread = 1000\n      BadgeStore._setBadgeForCount()\n      expect(BadgeStore._setBadge).toHaveBeenCalledWith(\"999+\")\n"
  },
  {
    "path": "packages/client-app/spec/stores/contact-store-spec.coffee",
    "content": "_ = require 'underscore'\nRx = require 'rx-lite'\n{NylasTestUtils} = require 'nylas-exports'\nContact = require('../../src/flux/models/contact').default\nNylasAPI = require('../../src/flux/nylas-api').default\nNylasAPIRequest = require('../../src/flux/nylas-api-request').default\nContactStore = require '../../src/flux/stores/contact-store'\nContactRankingStore = require '../../src/flux/stores/contact-ranking-store'\nDatabaseStore = require('../../src/flux/stores/database-store').default\nAccountStore = require('../../src/flux/stores/account-store').default\n\n{mockObservable} = NylasTestUtils\n\nxdescribe \"ContactStore\", ->\n  beforeEach ->\n    spyOn(NylasEnv, \"isMainWindow\").andReturn true\n\n    @rankings = [\n      [\"evanA@nylas.com\", 10]\n      [\"evanB@nylas.com\", 1]\n      [\"evanC@nylas.com\", 0.1]\n    ]\n\n    spyOn(NylasAPIRequest.prototype, \"run\").andCallFake (options) =>\n      if options.path is \"/contacts/rankings\"\n        return Promise.resolve(@rankings)\n      else\n        throw new Error(\"Invalid request path!\")\n\n    NylasEnv.testOrganizationUnit = \"folder\"\n    ContactStore._contactCache = []\n    ContactStore._fetchOffset = 0\n    ContactStore._accountId = null\n    ContactRankingStore.reset()\n\n  afterEach ->\n    NylasEnv.testOrganizationUnit = null\n\n  describe \"ranking contacts\", ->\n    beforeEach ->\n      @accountId = TEST_ACCOUNT_ID\n      @c1 = new Contact({name: \"Evan A\", email: \"evanA@nylas.com\", @accountId})\n      @c2 = new Contact({name: \"Evan B\", email: \"evanB@nylas.com\", @accountId})\n      @c3 = new Contact({name: \"Evan C\", email: \"evanC@nylas.com\", @accountId})\n      @c4 = new Contact({name: \"Ben\", email: \"ben@nylas.com\"})\n      @contacts = [@c3, @c1, @c2, @c4]\n\n    it \"queries for, and sorts, contacts present in the rankings\", ->\n      spyOn(ContactRankingStore, 'valuesForAllAccounts').andReturn\n        \"evana@nylas.com\": 10\n        \"evanb@nylas.com\": 1\n        \"evanc@nylas.com\": 0.1\n\n      spyOn(DatabaseStore, 'findAll').andCallFake =>\n        return {background: => Promise.resolve([@c3, @c1, @c2, @c4])}\n\n      waitsForPromise =>\n        ContactStore._updateRankedContactCache().then =>\n          expect(ContactStore._rankedContacts).toEqual [@c1, @c2, @c3, @c4]\n\n  describe \"when ContactRankings change\", ->\n    it \"re-generates the ranked contact cache\", ->\n      spyOn(ContactStore, \"_updateRankedContactCache\")\n      ContactRankingStore.trigger()\n      expect(ContactStore._updateRankedContactCache).toHaveBeenCalled()\n\n  describe \"when searching for a contact\", ->\n    beforeEach ->\n      @c1 = new Contact(name: \"\", email: \"1test@nylas.com\")\n      @c2 = new Contact(name: \"First\", email: \"2test@nylas.com\")\n      @c3 = new Contact(name: \"First Last\", email: \"3test@nylas.com\")\n      @c4 = new Contact(name: \"Fit\", email: \"fit@nylas.com\")\n      @c5 = new Contact(name: \"Fins\", email: \"fins@nylas.com\")\n      @c6 = new Contact(name: \"Fill\", email: \"fill@nylas.com\")\n      @c7 = new Contact(name: \"Fin\", email: \"fin@nylas.com\")\n      ContactStore._rankedContacts = [@c1,@c2,@c3,@c4,@c5,@c6,@c7]\n\n    it \"can find by first name\", ->\n      waitsForPromise =>\n        ContactStore.searchContacts(\"First\").then (results) =>\n          expect(results.length).toBe 2\n          expect(results[0]).toBe @c2\n          expect(results[1]).toBe @c3\n\n    it \"can find by last name\", ->\n      waitsForPromise =>\n        ContactStore.searchContacts(\"Last\").then (results) =>\n          expect(results.length).toBe 1\n          expect(results[0]).toBe @c3\n\n    it \"can find by email\", ->\n      waitsForPromise =>\n        ContactStore.searchContacts(\"1test\").then (results) =>\n          expect(results.length).toBe 1\n          expect(results[0]).toBe @c1\n\n    it \"is case insensitive\", ->\n      waitsForPromise =>\n        ContactStore.searchContacts(\"FIrsT\").then (results) =>\n          expect(results.length).toBe 2\n          expect(results[0]).toBe @c2\n          expect(results[1]).toBe @c3\n\n    it \"only returns the number requested\", ->\n      waitsForPromise =>\n        ContactStore.searchContacts(\"FIrsT\", limit: 1).then (results) =>\n          expect(results.length).toBe 1\n          expect(results[0]).toBe @c2\n\n    it \"returns no more than 5 by default\", ->\n      waitsForPromise =>\n        ContactStore.searchContacts(\"fi\").then (results) =>\n          expect(results.length).toBe 5\n\n    it \"can return more than 5 if requested\", ->\n      waitsForPromise =>\n        ContactStore.searchContacts(\"fi\", limit: 6).then (results) =>\n          expect(results.length).toBe 6\n\n  describe 'isValidContact', ->\n    it \"should call contact.isValid\", ->\n      contact = new Contact()\n      spyOn(contact, 'isValid').andReturn(true)\n      expect(ContactStore.isValidContact(contact)).toBe(true)\n\n    it \"should return false for non-Contact objects\", ->\n      expect(ContactStore.isValidContact({name: 'Ben', email: 'ben@nylas.com'})).toBe(false)\n\n    it \"returns false if we're not passed a contact\", ->\n      expect(ContactStore.isValidContact()).toBe false\n\n  describe 'parseContactsInString', ->\n    testCases =\n      # Single contact test cases\n      \"evan@nylas.com\": [new Contact(name: \"evan@nylas.com\", email: \"evan@nylas.com\")]\n      \"Evan Morikawa\": []\n      \"'evan@nylas.com'\": [new Contact(name: \"evan@nylas.com\", email: \"evan@nylas.com\")]\n      \"\\\"evan@nylas.com\\\"\": [new Contact(name: \"evan@nylas.com\", email: \"evan@nylas.com\")]\n      \"'evan@nylas.com\": [new Contact(name: \"'evan@nylas.com\", email: \"'evan@nylas.com\")]\n      \"Evan Morikawa <evan@nylas.com>\": [new Contact(name: \"Evan Morikawa\", email: \"evan@nylas.com\")]\n      \"Evan Morikawa (evan@nylas.com)\": [new Contact(name: \"Evan Morikawa\", email: \"evan@nylas.com\")]\n      \"spang (Christine Spang) <noreply+phabricator@nilas.com>\": [new Contact(name: \"spang (Christine Spang)\", email: \"noreply+phabricator@nilas.com\")]\n      \"spang 'Christine Spang' <noreply+phabricator@nilas.com>\": [new Contact(name: \"spang 'Christine Spang'\", email: \"noreply+phabricator@nilas.com\")]\n      \"spang \\\"Christine Spang\\\" <noreply+phabricator@nilas.com>\": [new Contact(name: \"spang \\\"Christine Spang\\\"\", email: \"noreply+phabricator@nilas.com\")]\n      \"Evan (evan@nylas.com)\": [new Contact(name: \"Evan\", email: \"evan@nylas.com\")]\n      \"\\\"Michael\\\" (mg@nylas.com)\": [new Contact(name: \"Michael\", email: \"mg@nylas.com\")]\n      \"announce-uc.1440659566.kankcagcmaacemjlnoma-security=nylas.com@lists.openwall.com\": [new Contact(name: \"announce-uc.1440659566.kankcagcmaacemjlnoma-security=nylas.com@lists.openwall.com\", email: \"announce-uc.1440659566.kankcagcmaacemjlnoma-security=nylas.com@lists.openwall.com\")]\n\n      # Multiple contact test cases\n      \"Evan Morikawa <evan@nylas.com>, Ben <ben@nylas.com>\": [\n        new Contact(name: \"Evan Morikawa\", email: \"evan@nylas.com\")\n        new Contact(name: \"Ben\", email: \"ben@nylas.com\")\n      ]\n      \"mark@nylas.com\\nGleb (gleb@nylas.com)\\rEvan Morikawa <evan@nylas.com>, spang (Christine Spang) <noreply+phabricator@nilas.com>\": [\n        new Contact(name: \"\", email: \"mark@nylas.com\")\n        new Contact(name: \"Gleb\", email: \"gleb@nylas.com\")\n        new Contact(name: \"Evan Morikawa\", email: \"evan@nylas.com\")\n        new Contact(name: \"spang (Christine Spang)\", email: \"noreply+phabricator@nilas.com\")\n      ]\n\n    _.forEach testCases, (value, key) ->\n      it \"works for #{key}\", ->\n        waitsForPromise ->\n          ContactStore.parseContactsInString(key).then (contacts) ->\n            contacts = contacts.map (c) -> c.toString()\n            expectedContacts = value.map (c) -> c.toString()\n            expect(contacts).toEqual expectedContacts\n"
  },
  {
    "path": "packages/client-app/spec/stores/database-setup-query-builder-spec.es6",
    "content": "/* eslint quote-props: 0 */\nimport TestModel from '../fixtures/db-test-model';\nimport Attributes from '../../src/flux/attributes';\nimport DatabaseSetupQueryBuilder from '../../src/flux/stores/database-setup-query-builder';\n\nxdescribe(\"DatabaseSetupQueryBuilder\", function DatabaseSetupQueryBuilderSpecs() {\n  beforeEach(() => {\n    this.builder = new DatabaseSetupQueryBuilder();\n  });\n\n  describe(\"setupQueriesForTable\", () => {\n    it(\"should return the queries for creating the table and the primary unique index\", () => {\n      TestModel.attributes = {\n        'attrQueryable': Attributes.DateTime({\n          queryable: true,\n          modelKey: 'attrQueryable',\n          jsonKey: 'attr_queryable',\n        }),\n\n        'attrNonQueryable': Attributes.Collection({\n          modelKey: 'attrNonQueryable',\n          jsonKey: 'attr_non_queryable',\n        }),\n      };\n      const queries = this.builder.setupQueriesForTable(TestModel);\n      const expected = [\n        'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,attr_queryable INTEGER)',\n        'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',\n      ];\n      queries.map((query, i) =>\n        expect(query).toBe(expected[i])\n      );\n    });\n\n    it(\"should correctly create join tables for models that have queryable collections\", () => {\n      TestModel.configureWithCollectionAttribute();\n      const queries = this.builder.setupQueriesForTable(TestModel);\n      const expected = [\n        'CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,client_id TEXT,server_id TEXT,other TEXT)',\n        'CREATE UNIQUE INDEX IF NOT EXISTS `TestModel_id` ON `TestModel` (`id`)',\n        'CREATE TABLE IF NOT EXISTS `TestModelCategory` (id TEXT KEY,`value` TEXT,other TEXT)',\n        'CREATE INDEX IF NOT EXISTS `TestModelCategory_id` ON `TestModelCategory` (`id` ASC)',\n        'CREATE UNIQUE INDEX IF NOT EXISTS `TestModelCategory_val_id` ON `TestModelCategory` (`value` ASC, `id` ASC)',\n      ];\n      queries.map((query, i) =>\n        expect(query).toBe(expected[i])\n      );\n    });\n\n    it(\"should use the correct column type for each attribute\", () => {\n      TestModel.configureWithAllAttributes();\n      const queries = this.builder.setupQueriesForTable(TestModel);\n      expect(queries[0]).toBe('CREATE TABLE IF NOT EXISTS `TestModel` (id TEXT PRIMARY KEY,data BLOB,datetime INTEGER,string-json-key TEXT,boolean INTEGER,number INTEGER)');\n    });\n\n    describe(\"when the model provides additional sqlite config\", () => {\n      it(\"the setup method should return these queries\", () => {\n        TestModel.configureWithAdditionalSQLiteConfig();\n        spyOn(TestModel.additionalSQLiteConfig, 'setup').andCallThrough();\n        const queries = this.builder.setupQueriesForTable(TestModel);\n        expect(TestModel.additionalSQLiteConfig.setup).toHaveBeenCalledWith();\n        expect(queries.pop()).toBe('CREATE INDEX IF NOT EXISTS ThreadListIndex ON Thread(last_message_received_timestamp DESC, account_id, id)');\n      });\n\n      it(\"should not fail if additional config is present, but setup is undefined\", () => {\n        delete TestModel.additionalSQLiteConfig.setup;\n        this.m = new TestModel({id: 'local-6806434c-b0cd', body: 'hello world'});\n        expect(() => this.builder.setupQueriesForTable(TestModel)).not.toThrow();\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/database-store-spec.es6",
    "content": "/* eslint quote-props: 0 */\nimport Thread from '../../src/flux/models/thread';\nimport TestModel from '../fixtures/db-test-model';\nimport ModelQuery from '../../src/flux/models/query';\nimport DatabaseStore from '../../src/flux/stores/database-store';\n\nconst testMatchers = {'id': 'b'};\n\ndescribe(\"DatabaseStore\", function DatabaseStoreSpecs() {\n  beforeEach(() => {\n    TestModel.configureBasic();\n\n    DatabaseStore._atomicallyQueue = undefined;\n    DatabaseStore._mutationQueue = undefined;\n    DatabaseStore._inTransaction = false;\n\n    spyOn(ModelQuery.prototype, 'where').andCallThrough();\n    spyOn(DatabaseStore, 'accumulateAndTrigger').andCallFake(() => Promise.resolve());\n\n    this.performed = [];\n\n    // Note: We spy on _query and test all of the convenience methods that sit above\n    // it. None of these tests evaluate whether _query works!\n    jasmine.unspy(DatabaseStore, \"_query\");\n    spyOn(DatabaseStore, \"_query\").andCallFake((query, values = []) => {\n      this.performed.push({query, values});\n      return Promise.resolve([]);\n    });\n  });\n\n  describe(\"find\", () =>\n    it(\"should return a ModelQuery for retrieving a single item by Id\", () => {\n      const q = DatabaseStore.find(TestModel, \"4\");\n      expect(q.sql()).toBe(\"SELECT `TestModel`.`data` FROM `TestModel`  WHERE `TestModel`.`id` = '4'  LIMIT 1\");\n    })\n\n  );\n\n  describe(\"findBy\", () => {\n    it(\"should pass the provided predicates on to the ModelQuery\", () => {\n      DatabaseStore.findBy(TestModel, testMatchers);\n      expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers);\n    });\n\n    it(\"should return a ModelQuery ready to be executed\", () => {\n      const q = DatabaseStore.findBy(TestModel, testMatchers);\n      expect(q.sql()).toBe(\"SELECT `TestModel`.`data` FROM `TestModel`  WHERE `TestModel`.`id` = 'b'  LIMIT 1\");\n    });\n  });\n\n  describe(\"findAll\", () => {\n    it(\"should pass the provided predicates on to the ModelQuery\", () => {\n      DatabaseStore.findAll(TestModel, testMatchers);\n      expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers);\n    });\n\n    it(\"should return a ModelQuery ready to be executed\", () => {\n      const q = DatabaseStore.findAll(TestModel, testMatchers);\n      expect(q.sql()).toBe(\"SELECT `TestModel`.`data` FROM `TestModel`  WHERE `TestModel`.`id` = 'b'  \");\n    });\n  });\n\n  describe(\"modelify\", () => {\n    beforeEach(() => {\n      this.models = [\n        new Thread({clientId: 'local-A'}),\n        new Thread({clientId: 'local-B'}),\n        new Thread({clientId: 'local-C'}),\n        new Thread({clientId: 'local-D', serverId: 'SERVER:D'}),\n        new Thread({clientId: 'local-E', serverId: 'SERVER:E'}),\n        new Thread({clientId: 'local-F', serverId: 'SERVER:F'}),\n        new Thread({clientId: 'local-G', serverId: 'SERVER:G'}),\n      ];\n      // Actually returns correct sets for queries, since matchers can evaluate\n      // themselves against models in memory\n      spyOn(DatabaseStore, 'run').andCallFake(query => {\n        const results = this.models.filter(model =>\n          query._matchers.every(matcher => matcher.evaluate(model))\n        );\n        return Promise.resolve(results);\n      });\n    });\n\n    describe(\"when given an array or input that is not an array\", () =>\n      it(\"resolves immediately with an empty array\", () =>\n        waitsForPromise(() => {\n          return DatabaseStore.modelify(Thread, null).then(output => {\n            expect(output).toEqual([]);\n          });\n        })\n      )\n    );\n\n    describe(\"when given an array of mixed IDs, clientIDs, and models\", () =>\n      it(\"resolves with an array of models\", () => {\n        const input = ['SERVER:F', 'local-B', 'local-C', 'SERVER:D', this.models[6]];\n        const expectedOutput = [this.models[5], this.models[1], this.models[2], this.models[3], this.models[6]];\n        return waitsForPromise(() => {\n          return DatabaseStore.modelify(Thread, input).then(output => {\n            expect(output).toEqual(expectedOutput);\n          });\n        });\n      })\n\n    );\n\n    describe(\"when the input is only IDs\", () =>\n      it(\"resolves with an array of models\", () => {\n        const input = ['SERVER:D', 'SERVER:F', 'SERVER:G'];\n        const expectedOutput = [this.models[3], this.models[5], this.models[6]];\n        return waitsForPromise(() => {\n          return DatabaseStore.modelify(Thread, input).then(output => {\n            expect(output).toEqual(expectedOutput);\n          });\n        });\n      })\n\n    );\n\n    describe(\"when the input is only clientIDs\", () =>\n      it(\"resolves with an array of models\", () => {\n        const input = ['local-A', 'local-B', 'local-C', 'local-D'];\n        const expectedOutput = [this.models[0], this.models[1], this.models[2], this.models[3]];\n        return waitsForPromise(() => {\n          return DatabaseStore.modelify(Thread, input).then(output => {\n            expect(output).toEqual(expectedOutput);\n          });\n        });\n      })\n\n    );\n\n    describe(\"when the input is all models\", () =>\n      it(\"resolves with an array of models\", () => {\n        const input = [this.models[0], this.models[1], this.models[2], this.models[3]];\n        const expectedOutput = [this.models[0], this.models[1], this.models[2], this.models[3]];\n        return waitsForPromise(() => {\n          return DatabaseStore.modelify(Thread, input).then(output => {\n            expect(output).toEqual(expectedOutput);\n          });\n        });\n      })\n\n    );\n  });\n\n  describe(\"count\", () => {\n    it(\"should pass the provided predicates on to the ModelQuery\", () => {\n      DatabaseStore.findAll(TestModel, testMatchers);\n      expect(ModelQuery.prototype.where).toHaveBeenCalledWith(testMatchers);\n    });\n\n    it(\"should return a ModelQuery configured for COUNT ready to be executed\", () => {\n      const q = DatabaseStore.findAll(TestModel, testMatchers);\n      expect(q.sql()).toBe(\"SELECT `TestModel`.`data` FROM `TestModel`  WHERE `TestModel`.`id` = 'b'  \");\n    });\n  });\n\n  describe(\"inTransaction\", () => {\n    it(\"calls the provided function inside an exclusive transaction\", () =>\n      waitsForPromise(() => {\n        return DatabaseStore.inTransaction(() => {\n          return DatabaseStore._query(\"TEST\");\n        }).then(() => {\n          expect(this.performed.length).toBe(3);\n          expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[1].query).toBe(\"TEST\");\n          expect(this.performed[2].query).toBe(\"COMMIT\");\n        });\n      })\n\n    );\n\n    it(\"preserves resolved values\", () =>\n      waitsForPromise(() => {\n        return DatabaseStore.inTransaction(() => {\n          DatabaseStore._query(\"TEST\");\n          return Promise.resolve(\"myValue\");\n        }).then(myValue => {\n          expect(myValue).toBe(\"myValue\");\n        });\n      })\n\n    );\n\n    it(\"always fires a COMMIT, even if the body function fails\", () =>\n      waitsForPromise(() => {\n        return DatabaseStore.inTransaction(() => {\n          throw new Error(\"BOOO\");\n        }).catch(() => {\n          expect(this.performed.length).toBe(2);\n          expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[1].query).toBe(\"COMMIT\");\n        });\n      })\n\n    );\n\n    it(\"can be called multiple times and get queued\", () =>\n      waitsForPromise(() => {\n        return Promise.all([\n          DatabaseStore.inTransaction(() => Promise.resolve()),\n          DatabaseStore.inTransaction(() => Promise.resolve()),\n          DatabaseStore.inTransaction(() => Promise.resolve()),\n        ]).then(() => {\n          expect(this.performed.length).toBe(6);\n          expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[1].query).toBe(\"COMMIT\");\n          expect(this.performed[2].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[3].query).toBe(\"COMMIT\");\n          expect(this.performed[4].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[5].query).toBe(\"COMMIT\");\n        });\n      })\n\n    );\n\n    it(\"carries on if one of them fails, but still calls the COMMIT for the failed block\", async () => {\n      let caughtError = false;\n      const p1 = DatabaseStore.inTransaction(() => DatabaseStore._query(\"ONE\"));\n      const p2 = DatabaseStore.inTransaction(() => { throw new Error(\"fail\"); }).catch(() => { caughtError = true });\n      const p3 = DatabaseStore.inTransaction(() => DatabaseStore._query(\"THREE\"));\n      await Promise.all([p1, p2, p3])\n      expect(this.performed.length).toBe(8);\n      expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n      expect(this.performed[1].query).toBe(\"ONE\");\n      expect(this.performed[2].query).toBe(\"COMMIT\");\n      expect(this.performed[3].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n      expect(this.performed[4].query).toBe(\"COMMIT\");\n      expect(this.performed[5].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n      expect(this.performed[6].query).toBe(\"THREE\");\n      expect(this.performed[7].query).toBe(\"COMMIT\");\n      expect(caughtError).toBe(true);\n    });\n\n    it(\"is actually running in series and blocks on never-finishing specs\", async () => {\n      let resolver = null;\n      await DatabaseStore.inTransaction(() => Promise.resolve());\n      expect(this.performed.length).toBe(2);\n      expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n      expect(this.performed[1].query).toBe(\"COMMIT\");\n      DatabaseStore.inTransaction(() => new Promise((resolve) => { resolver = resolve }));\n      let blockedPromiseDone = false;\n      DatabaseStore.inTransaction(() => Promise.resolve()).then(() => {\n        blockedPromiseDone = true;\n      });\n      await new Promise(setImmediate)\n      expect(this.performed.length).toBe(3);\n      expect(this.performed[2].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n      expect(blockedPromiseDone).toBe(false);\n      resolver();\n      await new Promise(setImmediate)\n      expect(blockedPromiseDone).toBe(true);\n    });\n\n    it(\"can be called multiple times and preserve return values\", () =>\n      waitsForPromise(() => {\n        let v1 = null;\n        let v2 = null;\n        let v3 = null;\n        return Promise.all([\n          DatabaseStore.inTransaction(() => Promise.resolve(\"a\")).then(val => { v1 = val }),\n          DatabaseStore.inTransaction(() => Promise.resolve(\"b\")).then(val => { v2 = val }),\n          DatabaseStore.inTransaction(() => Promise.resolve(\"c\")).then(val => { v3 = val }),\n        ]).then(() => {\n          expect(v1).toBe(\"a\");\n          expect(v2).toBe(\"b\");\n          expect(v3).toBe(\"c\");\n        });\n      })\n\n    );\n\n    it(\"can be called multiple times and get queued\", () =>\n      waitsForPromise(() => {\n        return DatabaseStore.inTransaction(() => Promise.resolve())\n        .then(() => DatabaseStore.inTransaction(() => Promise.resolve()))\n        .then(() => DatabaseStore.inTransaction(() => Promise.resolve()))\n        .then(() => {\n          expect(this.performed.length).toBe(6);\n          expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[1].query).toBe(\"COMMIT\");\n          expect(this.performed[2].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[3].query).toBe(\"COMMIT\");\n          expect(this.performed[4].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[5].query).toBe(\"COMMIT\");\n        });\n      })\n\n    );\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/database-transaction-spec.es6",
    "content": "/* eslint dot-notation:0 */\nimport Category from '../../src/flux/models/category';\nimport TestModel from '../fixtures/db-test-model';\nimport DatabaseWriter from '../../src/flux/stores/database-writer';\n\nconst testModelInstance = new TestModel({id: \"1234\"});\nconst testModelInstanceA = new TestModel({id: \"AAA\"});\nconst testModelInstanceB = new TestModel({id: \"BBB\"});\n\nfunction __range__(left, right, inclusive) {\n  const range = [];\n  const ascending = left < right;\n  const incr = ascending ? right + 1 : right - 1;\n  const end = !inclusive ? right : incr;\n  for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {\n    range.push(i);\n  }\n  return range;\n}\n\nxdescribe(\"DatabaseWriter\", function DatabaseWriterSpecs() {\n  beforeEach(() => {\n    this.databaseMutationHooks = [];\n    this.performed = [];\n    this.database = {\n      _query: jasmine.createSpy('database._query').andCallFake((query, values = []) => {\n        this.performed.push({query, values});\n        return Promise.resolve([]);\n      }),\n      accumulateAndTrigger: jasmine.createSpy('database.accumulateAndTrigger'),\n      mutationHooks: () => this.databaseMutationHooks,\n    };\n\n    this.transaction = new DatabaseWriter(this.database);\n  });\n\n  describe(\"execute\", () => {});\n\n  describe(\"persistModel\", () => {\n    it(\"should throw an exception if the model is not a subclass of Model\", () => expect(() => this.transaction.persistModel({id: 'asd', subject: 'bla'})).toThrow()\n    );\n\n    it(\"should call through to persistModels\", () => {\n      spyOn(this.transaction, 'persistModels').andReturn(Promise.resolve());\n      this.transaction.persistModel(testModelInstance);\n      advanceClock();\n      expect(this.transaction.persistModels.callCount).toBe(1);\n    });\n  });\n\n  describe(\"persistModels\", () => {\n    it(\"should call accumulateAndTrigger with a change that contains the models\", () => {\n      runs(() => {\n        return this.transaction.execute(t => {\n          return t.persistModels([testModelInstanceA, testModelInstanceB]);\n        });\n      });\n      waitsFor(() => {\n        return this.database.accumulateAndTrigger.callCount > 0;\n      });\n      runs(() => {\n        const change = this.database.accumulateAndTrigger.mostRecentCall.args[0];\n        expect(change).toEqual({\n          objectClass: TestModel.name,\n          objectIds: [testModelInstanceA.id, testModelInstanceB.id],\n          objects: [testModelInstanceA, testModelInstanceB],\n          type: 'persist',\n        });\n      });\n    });\n\n    it(\"should call through to _writeModels after checking them\", () => {\n      spyOn(this.transaction, '_writeModels').andReturn(Promise.resolve());\n      this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);\n      advanceClock();\n      expect(this.transaction._writeModels.callCount).toBe(1);\n    });\n\n    it(\"should throw an exception if the models are not the same class, since it cannot be specified by the trigger payload\", () =>\n      expect(() => this.transaction.persistModels([testModelInstanceA, new Category()])).toThrow()\n    );\n\n    it(\"should throw an exception if the models are not a subclass of Model\", () =>\n      expect(() => this.transaction.persistModels([{id: 'asd', subject: 'bla'}])).toThrow()\n    );\n\n    describe(\"mutationHooks\", () => {\n      beforeEach(() => {\n        this.beforeShouldThrow = false;\n        this.beforeShouldReject = false;\n\n        this.hook = {\n          beforeDatabaseChange: jasmine.createSpy('beforeDatabaseChange').andCallFake(() => {\n            if (this.beforeShouldThrow) { throw new Error(\"beforeShouldThrow\"); }\n            return new Promise((resolve) => {\n              setTimeout(() => {\n                if (this.beforeShouldReject) { resolve(new Error(\"beforeShouldReject\")); }\n                resolve(\"value\");\n              }\n              , 1000);\n            });\n          }),\n          afterDatabaseChange: jasmine.createSpy('afterDatabaseChange').andCallFake(() => {\n            return new Promise((resolve) => setTimeout(() => resolve(), 1000));\n          }),\n        };\n\n        this.databaseMutationHooks.push(this.hook);\n\n        this.writeModelsResolve = null;\n        spyOn(this.transaction, '_writeModels').andCallFake(() => {\n          return new Promise((resolve) => {\n            this.writeModelsResolve = resolve;\n          });\n        });\n      });\n\n      it(\"should run pre-mutation hooks, wait to write models, and then run post-mutation hooks\", () => {\n        this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);\n        advanceClock();\n        expect(this.hook.beforeDatabaseChange).toHaveBeenCalledWith(\n          this.transaction._query,\n          {\n            objects: [testModelInstanceA, testModelInstanceB],\n            objectIds: [testModelInstanceA.id, testModelInstanceB.id],\n            objectClass: testModelInstanceA.constructor.name,\n            type: 'persist',\n          },\n          undefined\n        );\n        expect(this.transaction._writeModels).not.toHaveBeenCalled();\n        advanceClock(1100);\n        advanceClock();\n        expect(this.transaction._writeModels).toHaveBeenCalled();\n        expect(this.hook.afterDatabaseChange).not.toHaveBeenCalled();\n        this.writeModelsResolve();\n        advanceClock();\n        advanceClock();\n        expect(this.hook.afterDatabaseChange).toHaveBeenCalledWith(\n          this.transaction._query,\n          {\n            objects: [testModelInstanceA, testModelInstanceB],\n            objectIds: [testModelInstanceA.id, testModelInstanceB.id],\n            objectClass: testModelInstanceA.constructor.name,\n            type: 'persist',\n          },\n          \"value\"\n        );\n      });\n\n      it(\"should carry on if a pre-mutation hook throws\", () => {\n        this.beforeShouldThrow = true;\n        this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);\n        advanceClock(1000);\n        expect(this.hook.beforeDatabaseChange).toHaveBeenCalled();\n        advanceClock();\n        advanceClock();\n        expect(this.transaction._writeModels).toHaveBeenCalled();\n      });\n\n      it(\"should carry on if a pre-mutation hook rejects\", () => {\n        this.beforeShouldReject = true;\n        this.transaction.persistModels([testModelInstanceA, testModelInstanceB]);\n        advanceClock(1000);\n        expect(this.hook.beforeDatabaseChange).toHaveBeenCalled();\n        advanceClock();\n        advanceClock();\n        expect(this.transaction._writeModels).toHaveBeenCalled();\n      });\n    });\n  });\n\n  describe(\"unpersistModel\", () => {\n    it(\"should delete the model by id\", () =>\n      waitsForPromise(() => {\n        return this.transaction.execute(() => {\n          return this.transaction.unpersistModel(testModelInstance);\n        })\n        .then(() => {\n          expect(this.performed.length).toBe(3);\n          expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n          expect(this.performed[1].query).toBe(\"DELETE FROM `TestModel` WHERE `id` = ?\");\n          expect(this.performed[1].values[0]).toBe('1234');\n          expect(this.performed[2].query).toBe(\"COMMIT\");\n        });\n      })\n\n    );\n\n    it(\"should call accumulateAndTrigger with a change that contains the model\", () => {\n      runs(() => {\n        return this.transaction.execute(() => {\n          return this.transaction.unpersistModel(testModelInstance);\n        });\n      });\n      waitsFor(() => {\n        return this.database.accumulateAndTrigger.callCount > 0;\n      });\n      runs(() => {\n        const change = this.database.accumulateAndTrigger.mostRecentCall.args[0];\n        expect(change).toEqual({\n          objectClass: TestModel.name,\n          objectIds: [testModelInstance.id],\n          objects: [testModelInstance],\n          type: 'unpersist',\n        });\n      });\n    });\n\n    describe(\"when the model has collection attributes\", () =>\n      it(\"should delete all of the elements in the join tables\", () => {\n        TestModel.configureWithCollectionAttribute();\n        waitsForPromise(() => {\n          return this.transaction.execute(t => {\n            return t.unpersistModel(testModelInstance);\n          })\n          .then(() => {\n            expect(this.performed.length).toBe(4);\n            expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n            expect(this.performed[2].query).toBe(\"DELETE FROM `TestModelCategory` WHERE `id` = ?\");\n            expect(this.performed[2].values[0]).toBe('1234');\n            expect(this.performed[3].query).toBe(\"COMMIT\");\n          });\n        });\n      })\n\n    );\n\n    describe(\"when the model has joined data attributes\", () =>\n      it(\"should delete the element in the joined data table\", () => {\n        TestModel.configureWithJoinedDataAttribute();\n        waitsForPromise(() => {\n          return this.transaction.execute(t => {\n            return t.unpersistModel(testModelInstance);\n          })\n          .then(() => {\n            expect(this.performed.length).toBe(4);\n            expect(this.performed[0].query).toBe(\"BEGIN IMMEDIATE TRANSACTION\");\n            expect(this.performed[2].query).toBe(\"DELETE FROM `TestModelBody` WHERE `id` = ?\");\n            expect(this.performed[2].values[0]).toBe('1234');\n            expect(this.performed[3].query).toBe(\"COMMIT\");\n          });\n        });\n      })\n\n    );\n  });\n\n  describe(\"_writeModels\", () => {\n    it(\"should compose a REPLACE INTO query to save the model\", () => {\n      TestModel.configureWithCollectionAttribute();\n      this.transaction._writeModels([testModelInstance]);\n      expect(this.performed[0].query).toBe(\"REPLACE INTO `TestModel` (id,data,client_id,server_id,other) VALUES (?,?,?,?,?)\");\n    });\n\n    it(\"should save the model JSON into the data column\", () => {\n      this.transaction._writeModels([testModelInstance]);\n      expect(this.performed[0].values[1]).toEqual(JSON.stringify(testModelInstance));\n    });\n\n    describe(\"when the model defines additional queryable attributes\", () => {\n      beforeEach(() => {\n        TestModel.configureWithAllAttributes();\n        this.m = new TestModel({\n          'id': 'local-6806434c-b0cd',\n          'datetime': new Date(),\n          'string': 'hello world',\n          'boolean': true,\n          'number': 15,\n        });\n      });\n\n      it(\"should populate additional columns defined by the attributes\", () => {\n        this.transaction._writeModels([this.m]);\n        expect(this.performed[0].query).toBe(\"REPLACE INTO `TestModel` (id,data,datetime,string-json-key,boolean,number) VALUES (?,?,?,?,?,?)\");\n      });\n\n      it(\"should use the JSON-form values of the queryable attributes\", () => {\n        const json = this.m.toJSON();\n        this.transaction._writeModels([this.m]);\n\n        const { values } = this.performed[0];\n        expect(values[2]).toEqual(json['datetime']);\n        expect(values[3]).toEqual(json['string-json-key']);\n        expect(values[4]).toEqual(json['boolean']);\n        expect(values[5]).toEqual(json['number']);\n      });\n    });\n\n    describe(\"when the model has collection attributes\", () => {\n      beforeEach(() => {\n        TestModel.configureWithCollectionAttribute();\n        this.m = new TestModel({id: 'local-6806434c-b0cd', other: 'other'});\n        this.m.categories = [new Category({id: 'a'}), new Category({id: 'b'})];\n        this.transaction._writeModels([this.m]);\n      });\n\n      it(\"should delete all association records for the model from join tables\", () => {\n        expect(this.performed[1].query).toBe('DELETE FROM `TestModelCategory` WHERE `id` IN (\\'local-6806434c-b0cd\\')');\n      });\n\n      it(\"should insert new association records into join tables in a single query, and include queryableBy columns\", () => {\n        expect(this.performed[2].query).toBe('INSERT OR IGNORE INTO `TestModelCategory` (`id`,`value`,`other`) VALUES (?,?,?),(?,?,?)');\n        expect(this.performed[2].values).toEqual(['local-6806434c-b0cd', 'a', 'other', 'local-6806434c-b0cd', 'b', 'other']);\n      });\n    });\n\n    describe(\"model collection attributes query building\", () => {\n      beforeEach(() => {\n        TestModel.configureWithCollectionAttribute();\n        this.m = new TestModel({id: 'local-6806434c-b0cd', other: 'other'});\n        this.m.categories = [];\n      });\n\n      it(\"should page association records into multiple queries correctly\", () => {\n        const iterable = __range__(0, 199, true);\n        for (let j = 0; j < iterable.length; j++) {\n          const i = iterable[j];\n          this.m.categories.push(new Category({id: `id-${i}`}));\n        }\n        this.transaction._writeModels([this.m]);\n\n        const collectionAttributeQueries = this.performed.filter(i => i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') === 0\n        );\n\n        expect(collectionAttributeQueries.length).toBe(1);\n        expect(collectionAttributeQueries[0].values[(200 * 3) - 2]).toEqual('id-199');\n      });\n\n      it(\"should page association records into multiple queries correctly\", () => {\n        const iterable = __range__(0, 200, true);\n        for (let j = 0; j < iterable.length; j++) {\n          const i = iterable[j];\n          this.m.categories.push(new Category({id: `id-${i}`}));\n        }\n        this.transaction._writeModels([this.m]);\n\n        const collectionAttributeQueries = this.performed.filter(i => i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') === 0\n        );\n\n        expect(collectionAttributeQueries.length).toBe(2);\n        expect(collectionAttributeQueries[0].values[(200 * 3) - 2]).toEqual('id-199');\n        expect(collectionAttributeQueries[1].values[1]).toEqual('id-200');\n      });\n\n      it(\"should page association records into multiple queries correctly\", () => {\n        const iterable = __range__(0, 201, true);\n        for (let j = 0; j < iterable.length; j++) {\n          const i = iterable[j];\n          this.m.categories.push(new Category({id: `id-${i}`}));\n        }\n        this.transaction._writeModels([this.m]);\n\n        const collectionAttributeQueries = this.performed.filter(i => i.query.indexOf('INSERT OR IGNORE INTO `TestModelCategory`') === 0\n        );\n\n        expect(collectionAttributeQueries.length).toBe(2);\n        expect(collectionAttributeQueries[0].values[(200 * 3) - 2]).toEqual('id-199');\n        expect(collectionAttributeQueries[1].values[1]).toEqual('id-200');\n        expect(collectionAttributeQueries[1].values[4]).toEqual('id-201');\n      });\n    });\n\n    describe(\"when the model has joined data attributes\", () => {\n      beforeEach(() => TestModel.configureWithJoinedDataAttribute());\n\n      it(\"should not include the value to the joined attribute in the JSON written to the main model table\", () => {\n        this.m = new TestModel({clientId: 'local-6806434c-b0cd', serverId: 'server-1', body: 'hello world'});\n        this.transaction._writeModels([this.m]);\n        expect(this.performed[0].values).toEqual(['server-1', '{\"client_id\":\"local-6806434c-b0cd\",\"server_id\":\"server-1\",\"id\":\"server-1\"}', 'local-6806434c-b0cd', 'server-1']);\n      });\n\n      it(\"should write the value to the joined table if it is defined\", () => {\n        this.m = new TestModel({id: 'local-6806434c-b0cd', body: 'hello world'});\n        this.transaction._writeModels([this.m]);\n        expect(this.performed[1].query).toBe('REPLACE INTO `TestModelBody` (`id`, `value`) VALUES (?, ?)');\n        expect(this.performed[1].values).toEqual([this.m.id, this.m.body]);\n      });\n\n      it(\"should not write the value to the joined table if it undefined\", () => {\n        this.m = new TestModel({id: 'local-6806434c-b0cd'});\n        this.transaction._writeModels([this.m]);\n        expect(this.performed.length).toBe(1);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/draft-editing-session-spec.coffee",
    "content": "Message = require('../../src/flux/models/message').default\nActions = require('../../src/flux/actions').default\nDatabaseStore = require('../../src/flux/stores/database-store').default\nDatabaseWriter = require('../../src/flux/stores/database-writer').default\nDraftEditingSession = require '../../src/flux/stores/draft-editing-session'\nDraftChangeSet = DraftEditingSession.DraftChangeSet\n_ = require 'underscore'\n\n\nxdescribe \"DraftEditingSession Specs\", ->\n  describe \"DraftChangeSet\", ->\n    beforeEach ->\n      @onDidAddChanges = jasmine.createSpy('onDidAddChanges')\n      @onWillAddChanges = jasmine.createSpy('onWillAddChanges')\n      @commitResolve = null\n      @commitResolves = []\n      @onCommit = jasmine.createSpy('commit').andCallFake =>\n        new Promise (resolve, reject) =>\n          @commitResolves.push(resolve)\n          @commitResolve = resolve\n\n      @changeSet = new DraftChangeSet({\n        onDidAddChanges: @onDidAddChanges,\n        onWillAddChanges: @onWillAddChanges,\n        onCommit: @onCommit,\n      })\n      @changeSet._pending =\n        subject: 'Change to subject line'\n\n    describe \"teardown\", ->\n      it \"should remove all of the pending and saving changes\", ->\n        @changeSet.teardown()\n        expect(@changeSet._saving).toEqual({})\n        expect(@changeSet._pending).toEqual({})\n\n    describe \"add\", ->\n      it \"should mark that the draft is not pristine\", ->\n        @changeSet.add(body: 'Hello World!')\n        expect(@changeSet._pending.pristine).toEqual(false)\n\n      it \"should add the changes to the _pending set\", ->\n        @changeSet.add(body: 'Hello World!')\n        expect(@changeSet._pending.body).toEqual('Hello World!')\n\n      describe \"otherwise\", ->\n        it \"should commit after thirty seconds\", ->\n          spyOn(@changeSet, 'commit')\n          @changeSet.add({body: 'Hello World!'})\n          expect(@changeSet.commit).not.toHaveBeenCalled()\n          advanceClock(31000)\n          expect(@changeSet.commit).toHaveBeenCalled()\n\n    describe \"commit\", ->\n      it \"should resolve immediately if the pending set is empty\", ->\n        @changeSet._pending = {}\n        waitsForPromise =>\n          @changeSet.commit().then =>\n            expect(@onCommit).not.toHaveBeenCalled()\n\n      it \"should move changes to the saving set\", ->\n        pendingBefore = _.extend({}, @changeSet._pending)\n        expect(@changeSet._saving).toEqual({})\n        @changeSet.commit()\n        advanceClock()\n        expect(@changeSet._pending).toEqual({})\n        expect(@changeSet._saving).toEqual(pendingBefore)\n\n      it \"should call the commit handler and then clear the saving set\", ->\n        @changeSet.commit()\n        advanceClock()\n        expect(@changeSet._saving).not.toEqual({})\n        @commitResolve()\n        advanceClock()\n        expect(@changeSet._saving).toEqual({})\n\n      describe \"concurrency\", ->\n        it \"the commit function should always run serially\", ->\n          firstFulfilled = false\n          secondFulfilled = false\n\n          @changeSet._pending = {subject: 'A'}\n          @changeSet.commit().then =>\n            @changeSet._pending = {subject: 'B'}\n            firstFulfilled = true\n          @changeSet.commit().then =>\n            secondFulfilled = true\n\n          advanceClock()\n          expect(firstFulfilled).toBe(false)\n          expect(secondFulfilled).toBe(false)\n          @commitResolves[0]()\n          advanceClock()\n          expect(firstFulfilled).toBe(true)\n          expect(secondFulfilled).toBe(false)\n          @commitResolves[1]()\n          advanceClock()\n          expect(firstFulfilled).toBe(true)\n          expect(secondFulfilled).toBe(true)\n\n    describe \"applyToModel\", ->\n      it \"should apply the saving and then the pending change sets, in that order\", ->\n        @changeSet._saving =  {subject: 'A', body: 'Basketb'}\n        @changeSet._pending = {body: 'Basketball'}\n        m = new Message()\n        @changeSet.applyToModel(m)\n        expect(m.subject).toEqual('A')\n        expect(m.body).toEqual('Basketball')\n\n  describe \"DraftEditingSession\", ->\n    describe \"constructor\", ->\n      it \"should make a query to fetch the draft\", ->\n        spyOn(DatabaseStore, 'run').andCallFake =>\n          new Promise (resolve, reject) =>\n        session = new DraftEditingSession('client-id')\n        expect(DatabaseStore.run).toHaveBeenCalled()\n\n      describe \"when given a draft object\", ->\n        beforeEach ->\n          spyOn(DatabaseStore, 'run')\n          @draft = new Message(draft: true, body: '123')\n          @session = new DraftEditingSession('client-id', @draft)\n\n        it \"should not make a query for the draft\", ->\n          expect(DatabaseStore.run).not.toHaveBeenCalled()\n\n        it \"prepare should resolve without querying for the draft\", ->\n          waitsForPromise => @session.prepare().then =>\n            expect(@session.draft()).toBeDefined()\n            expect(DatabaseStore.run).not.toHaveBeenCalled()\n\n    describe \"teardown\", ->\n      it \"should mark the session as destroyed\", ->\n        spyOn(DraftEditingSession.prototype, \"prepare\")\n        session = new DraftEditingSession('client-id')\n        session.teardown()\n        expect(session._destroyed).toEqual(true)\n\n    describe \"prepare\", ->\n      beforeEach ->\n        @draft = new Message(draft: true, body: '123', clientId: 'client-id')\n        spyOn(DraftEditingSession.prototype, \"prepare\")\n        @session = new DraftEditingSession('client-id')\n        spyOn(@session, '_setDraft').andCallThrough()\n        spyOn(DatabaseStore, 'run').andCallFake (modelQuery) =>\n          Promise.resolve(@draft)\n        jasmine.unspy(DraftEditingSession.prototype, \"prepare\")\n\n      it \"should call setDraft with the retrieved draft\", ->\n        waitsForPromise =>\n          @session.prepare().then =>\n            expect(@session._setDraft).toHaveBeenCalledWith(@draft)\n\n      it \"should resolve with the DraftEditingSession\", ->\n        waitsForPromise =>\n          @session.prepare().then (val) =>\n            expect(val).toBe(@session)\n\n      describe \"error handling\", ->\n        it \"should reject if the draft session has already been destroyed\", ->\n          @session._destroyed = true\n          waitsForPromise =>\n            @session.prepare().then =>\n              expect(false).toBe(true)\n            .catch (val) =>\n              expect(val instanceof Error).toBe(true)\n\n        it \"should reject if the draft cannot be found\", ->\n          @draft = null\n          waitsForPromise =>\n            @session.prepare().then =>\n              expect(false).toBe(true)\n            .catch (val) =>\n              expect(val instanceof Error).toBe(true)\n\n    describe \"when a draft changes\", ->\n      beforeEach ->\n        @draft = new Message(draft: true, clientId: 'client-id', body: 'A', subject: 'initial')\n        @session = new DraftEditingSession('client-id', @draft)\n        advanceClock()\n\n        spyOn(DatabaseWriter.prototype, \"persistModel\").andReturn Promise.resolve()\n        spyOn(Actions, \"queueTask\").andReturn Promise.resolve()\n\n      it \"should ignore the update unless it applies to the current draft\", ->\n        spyOn(@session, 'trigger')\n        @session._onDraftChanged(objectClass: 'message', objects: [new Message()])\n        expect(@session.trigger).not.toHaveBeenCalled()\n        @session._onDraftChanged(objectClass: 'message', objects: [@draft])\n        expect(@session.trigger).toHaveBeenCalled()\n\n      it \"should apply the update to the current draft\", ->\n        updatedDraft = @draft.clone()\n        updatedDraft.subject = 'This is the new subject'\n        spyOn(@session, '_setDraft')\n\n        @session._onDraftChanged(objectClass: 'message', objects: [updatedDraft])\n        expect(@session._setDraft).toHaveBeenCalled()\n        draft = @session._setDraft.calls[0].args[0]\n        expect(draft.subject).toEqual(updatedDraft.subject)\n\n      it \"atomically commits changes\", ->\n        spyOn(DatabaseStore, \"run\").andReturn(Promise.resolve(@draft))\n        spyOn(DatabaseStore, 'inTransaction').andCallThrough()\n        @session.changes.add({body: \"123\"})\n        waitsForPromise =>\n          @session.changes.commit().then =>\n            expect(DatabaseStore.inTransaction).toHaveBeenCalled()\n            expect(DatabaseStore.inTransaction.calls.length).toBe 1\n\n      it \"persist the applied changes\", ->\n        spyOn(DatabaseStore, \"run\").andReturn(Promise.resolve(@draft))\n        @session.changes.add({body: \"123\"})\n        waitsForPromise =>\n          @session.changes.commit().then =>\n            expect(DatabaseWriter.prototype.persistModel).toHaveBeenCalled()\n            updated = DatabaseWriter.prototype.persistModel.calls[0].args[0]\n            expect(updated.body).toBe \"123\"\n\n      describe \"when findBy does not return a draft\", ->\n        it \"continues and persists it's local draft reference, so it is resaved and draft editing can continue\", ->\n          spyOn(DatabaseStore, \"run\").andReturn(Promise.resolve(null))\n          @session.changes.add({body: \"123\"})\n          waitsForPromise =>\n            @session.changes.commit().then =>\n              expect(DatabaseWriter.prototype.persistModel).toHaveBeenCalled()\n              updated = DatabaseWriter.prototype.persistModel.calls[0].args[0]\n              expect(updated.body).toBe \"123\"\n\n      it \"does nothing if the draft is marked as destroyed\", ->\n        spyOn(DatabaseStore, \"run\").andReturn(Promise.resolve(@draft))\n        spyOn(DatabaseStore, 'inTransaction').andCallThrough()\n        waitsForPromise =>\n          @session._destroyed = true\n          @session.changes.add({body: \"123\"})\n          @session.changes.commit().then =>\n            expect(DatabaseStore.inTransaction).not.toHaveBeenCalled()\n\n    describe \"draft pristine body\", ->\n      describe \"when the draft given to the session is pristine\", ->\n        it \"should return the initial body\", ->\n          pristineDraft = new Message(draft: true, body: 'Hiya', pristine: true, clientId: 'client-id')\n          updatedDraft = pristineDraft.clone()\n          updatedDraft.body = '123444'\n          updatedDraft.pristine = false\n\n          @session = new DraftEditingSession('client-id', pristineDraft)\n          waitsForPromise =>\n            @session._draftPromise.then =>\n              @session._onDraftChanged(objectClass: 'message', objects: [updatedDraft])\n              expect(@session.draftPristineBody()).toBe('Hiya')\n\n      describe \"when the draft given to the session is not pristine\", ->\n        it \"should return null\", ->\n          dirtyDraft = new Message(draft: true, body: 'Hiya', pristine: false)\n          @session = new DraftEditingSession('client-id', dirtyDraft)\n          expect(@session.draftPristineBody()).toBe(null)\n"
  },
  {
    "path": "packages/client-app/spec/stores/draft-factory-spec.es6",
    "content": "import _ from 'underscore';\n\nimport {\n  File,\n  Actions,\n  Thread,\n  Contact,\n  Message,\n  DraftStore,\n  AccountStore,\n  DatabaseStore,\n  FileDownloadStore,\n  DatabaseWriter,\n  SanitizeTransformer,\n  InlineStyleTransformer,\n} from 'nylas-exports';\n\nimport DraftFactory from '../../src/flux/stores/draft-factory';\n\nlet msgFromMe = null;\nlet fakeThread = null;\nlet fakeMessage1 = null;\nlet msgWithReplyTo = null;\nlet fakeMessageWithFiles = null;\nlet msgWithReplyToDuplicates = null;\nlet msgWithReplyToFromMe = null;\nlet account = null;\nconst downloadData = {}\n\ndescribe('DraftFactory', function draftFactory() {\n  beforeEach(() => {\n    // Out of the scope of these specs\n    spyOn(InlineStyleTransformer, 'run').andCallFake((input) => Promise.resolve(input));\n    spyOn(SanitizeTransformer, 'run').andCallFake((input) => Promise.resolve(input));\n    spyOn(FileDownloadStore, 'getDownloadDataForFile').andCallFake((fid) => {\n      return downloadData[fid]\n    });\n\n    account = AccountStore.accounts()[0];\n    const files = [\n      new File({filename: \"test.jpg\", accountId: account.id}),\n      new File({filename: \"test.pdj\", accountId: account.id}),\n    ];\n    files.forEach((file) => {\n      downloadData[file.id] = {\n        fileId: file.id,\n        filename: file.filename,\n        targetPath: file.filename,\n      }\n    })\n\n\n    fakeThread = new Thread({\n      id: 'fake-thread-id',\n      accountId: account.id,\n      subject: 'Fake Subject',\n    });\n\n    fakeMessage1 = new Message({\n      id: 'fake-message-1',\n      accountId: account.id,\n      to: [new Contact({email: 'ben@nylas.com'}), new Contact({email: 'evan@nylas.com'})],\n      cc: [new Contact({email: 'mg@nylas.com'}), account.me()],\n      bcc: [new Contact({email: 'recruiting@nylas.com'})],\n      from: [new Contact({email: 'customer@example.com', name: 'Customer'})],\n      threadId: 'fake-thread-id',\n      body: 'Fake Message 1',\n      subject: 'Fake Subject',\n      date: new Date(1415814587),\n    });\n\n    fakeMessageWithFiles = new Message({\n      id: 'fake-message-with-files',\n      accountId: account.id,\n      to: [new Contact({email: 'ben@nylas.com'}), new Contact({email: 'evan@nylas.com'})],\n      cc: [new Contact({email: 'mg@nylas.com'}), account.me()],\n      bcc: [new Contact({email: 'recruiting@nylas.com'})],\n      from: [new Contact({email: 'customer@example.com', name: 'Customer'})],\n      files: files,\n      threadId: 'fake-thread-id',\n      body: 'Fake Message 1',\n      subject: 'Fake Subject',\n      date: new Date(1415814587),\n    });\n\n    msgFromMe = new Message({\n      id: 'fake-message-3',\n      accountId: account.id,\n      to: [new Contact({email: '1@1.com'}), new Contact({email: '2@2.com'})],\n      cc: [new Contact({email: '3@3.com'}), new Contact({email: '4@4.com'})],\n      bcc: [new Contact({email: '5@5.com'}), new Contact({email: '6@6.com'})],\n      from: [account.me()],\n      threadId: 'fake-thread-id',\n      body: 'Fake Message 2',\n      subject: 'Re: Fake Subject',\n      date: new Date(1415814587),\n    });\n\n    msgWithReplyTo = new Message({\n      id: 'fake-message-reply-to',\n      accountId: account.id,\n      to: [new Contact({email: '1@1.com'}), new Contact({email: '2@2.com'})],\n      cc: [new Contact({email: '3@3.com'}), new Contact({email: '4@4.com'})],\n      bcc: [new Contact({email: '5@5.com'}), new Contact({email: '6@6.com'})],\n      replyTo: [new Contact({email: 'reply-to@5.com'}), new Contact({email: 'reply-to@6.com'})],\n      from: [new Contact({email: 'from@5.com'})],\n      threadId: 'fake-thread-id',\n      body: 'Fake Message 2',\n      subject: 'Re: Fake Subject',\n      date: new Date(1415814587),\n    });\n\n    msgWithReplyToFromMe = new Message({\n      accountId: account.id,\n      threadId: 'fake-thread-id',\n      from: [account.me()],\n      to: [new Contact({email: 'tiffany@popular.com', name: 'Tiffany'})],\n      replyTo: [new Contact({email: 'danco@gmail.com', name: 'danco@gmail.com'})],\n    })\n\n    msgWithReplyToDuplicates = new Message({\n      id: 'fake-message-reply-to-duplicates',\n      accountId: account.id,\n      to: [new Contact({email: '1@1.com'}), new Contact({email: '2@2.com'})],\n      cc: [new Contact({email: '1@1.com'}), new Contact({email: '4@4.com'})],\n      from: [new Contact({email: 'reply-to@5.com'})],\n      replyTo: [new Contact({email: 'reply-to@5.com'})],\n      threadId: 'fake-thread-id',\n      body: 'Fake Message Duplicates',\n      subject: 'Re: Fake Subject',\n      date: new Date(1415814587),\n    });\n  });\n\n  describe(\"creating drafts\", () => {\n    describe(\"createDraftForReply\", () => {\n      it(\"should be empty string\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n            expect(draft.body).toEqual(\"\");\n          });\n        });\n      });\n\n      it(\"should address the message to the previous message's sender\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n            expect(draft.to).toEqual(fakeMessage1.from);\n          });\n        });\n      });\n\n      it(\"should set the replyToMessageId to the previous message's ids\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n            expect(draft.replyToMessageId).toEqual(fakeMessage1.id);\n          });\n        });\n      });\n\n      it(\"should set the accountId and from address based on the message\", () => {\n        waitsForPromise(() => {\n          const secondAccount = AccountStore.accounts()[1];\n          fakeMessage1.to = [\n            new Contact({email: secondAccount.emailAddress}),\n            new Contact({email: 'evan@nylas.com'}),\n          ]\n          fakeMessage1.accountId = secondAccount.id\n          fakeThread.accountId = secondAccount.id\n\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n            expect(draft.accountId).toEqual(secondAccount.id);\n            expect(draft.from[0].email).toEqual(secondAccount.defaultMe().email);\n          });\n        });\n      });\n\n      describe(\"when the email is TO an alias\", () => {\n        it(\"should use the alias as the from address\", () => {\n          waitsForPromise(() => {\n            fakeMessage1.to = [\n              new Contact({email: TEST_ACCOUNT_ALIAS_EMAIL}),\n              new Contact({email: 'evan@nylas.com'}),\n            ]\n\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n              expect(draft.accountId).toEqual(TEST_ACCOUNT_ID);\n              expect(draft.from[0].email).toEqual(TEST_ACCOUNT_ALIAS_EMAIL);\n            });\n          });\n        });\n      });\n\n      describe(\"when the email is CC'd to an alias\", () => {\n        it(\"should use the alias as the from address\", () => {\n          waitsForPromise(() => {\n            fakeMessage1.to = [\n              new Contact({email: 'juan@nylas.com'}),\n            ]\n            fakeMessage1.cc = [\n              new Contact({email: TEST_ACCOUNT_ALIAS_EMAIL}),\n              new Contact({email: 'evan@nylas.com'}),\n            ]\n\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n              expect(draft.accountId).toEqual(TEST_ACCOUNT_ID);\n              expect(draft.from[0].email).toEqual(TEST_ACCOUNT_ALIAS_EMAIL);\n            });\n          });\n        });\n      });\n\n      it(\"should make the subject the subject of the message, not the thread\", () => {\n        fakeMessage1.subject = \"OLD SUBJECT\";\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n            expect(draft.subject).toEqual(\"Re: OLD SUBJECT\");\n          });\n        });\n      });\n\n      it(\"should change the subject from Fwd: back to Re: if necessary\", () => {\n        fakeMessage1.subject = \"Fwd: This is my DRAFT\";\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'}).then((draft) => {\n            expect(draft.subject).toEqual(\"Re: This is my DRAFT\");\n          });\n        });\n      });\n    });\n\n    describe(\"type: reply\", () => {\n      describe(\"when the message provided as context has one or more 'ReplyTo' recipients\", () => {\n        it(\"addresses the draft to all of the message's 'ReplyTo' recipients\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgWithReplyTo, type: 'reply'}).then((draft) => {\n              expect(draft.to).toEqual(msgWithReplyTo.replyTo);\n              expect(draft.cc.length).toBe(0);\n              expect(draft.bcc.length).toBe(0);\n            });\n          });\n        });\n\n        it(\"addresses the draft to all of the message's 'ReplyTo' recipients, even if the message is 'From' you\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgWithReplyToFromMe, type: 'reply'}).then((draft) => {\n              expect(draft.to).toEqual(msgWithReplyToFromMe.replyTo);\n              expect(draft.cc.length).toBe(0);\n              expect(draft.bcc.length).toBe(0);\n            });\n          });\n        });\n      });\n\n      describe(\"when the message provided as context was sent by the current user\", () => {\n        it(\"addresses the draft to all of the last messages's 'To' recipients\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgFromMe, type: 'reply'}).then((draft) => {\n              expect(draft.to).toEqual(msgFromMe.to);\n              expect(draft.cc.length).toBe(0);\n              expect(draft.bcc.length).toBe(0);\n            });\n          });\n        });\n      });\n    });\n\n    describe(\"type: reply-all\", () => {\n      it(\"should include people in the cc field\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply-all'}).then((draft) => {\n            const ccEmails = draft.cc.map(cc => cc.email);\n            expect(ccEmails.sort()).toEqual(['ben@nylas.com', 'evan@nylas.com', 'mg@nylas.com']);\n          });\n        });\n      });\n\n      it(\"should not include people who were bcc'd on the previous message\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply-all'}).then((draft) => {\n            expect(draft.bcc).toEqual([]);\n            expect(draft.cc.indexOf(fakeMessage1.bcc[0])).toEqual(-1);\n          });\n        });\n      });\n\n      it(\"should not include you when you were cc'd on the previous message\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply-all'}).then((draft) => {\n            const ccEmails = draft.cc.map(cc => cc.email);\n            expect(ccEmails.indexOf(account.me().email)).toEqual(-1);\n          });\n        });\n      });\n\n      describe(\"when the message provided as context has one or more 'ReplyTo' recipients\", () => {\n        it(\"addresses the draft to all of the message's 'ReplyTo' recipients\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgWithReplyTo, type: 'reply-all'}).then((draft) => {\n              expect(draft.to).toEqual(msgWithReplyTo.replyTo);\n            });\n          });\n        });\n\n        it(\"addresses the draft to all of the message's 'ReplyTo' recipients, even if the message is 'From' you\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgWithReplyToFromMe, type: 'reply-all'}).then((draft) => {\n              expect(draft.to).toEqual(msgWithReplyToFromMe.replyTo);\n            });\n          });\n        });\n\n        it(\"should not include the message's 'From' recipient in any field\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgWithReplyTo, type: 'reply-all'}).then((draft) => {\n              const all = [].concat(draft.to, draft.cc, draft.bcc);\n              const match = _.find(all, (c) => c.email === msgWithReplyTo.from[0].email);\n              expect(match).toEqual(undefined);\n            });\n          });\n        });\n      });\n\n      describe(\"when the message provided has one or more 'ReplyTo' recipients and duplicates in the To/Cc fields\", () => {\n        it(\"should unique the to/cc fields\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgWithReplyToDuplicates, type: 'reply-all'}).then((draft) => {\n              const ccEmails = draft.cc.map(cc => cc.email);\n              expect(ccEmails.sort()).toEqual(['1@1.com', '2@2.com', '4@4.com']);\n              const toEmails = draft.to.map(to => to.email);\n              expect(toEmails.sort()).toEqual(['reply-to@5.com']);\n            });\n          });\n        });\n      });\n\n      describe(\"when the message provided as context was sent by the current user\", () => {\n        it(\"addresses the draft to all of the last messages's recipients\", () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForReply({thread: fakeThread, message: msgFromMe, type: 'reply-all'}).then((draft) => {\n              expect(draft.to).toEqual(msgFromMe.to);\n              expect(draft.cc).toEqual(msgFromMe.cc);\n              expect(draft.bcc.length).toBe(0);\n            });\n          });\n        });\n      });\n    });\n\n    describe(\"onComposeForward\", () => {\n      beforeEach(() => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForForward({thread: fakeThread, message: fakeMessage1}).then((draft) => {\n            this.model = draft;\n          });\n        });\n      });\n\n      it(\"should include forwarded message text, in a div rather than a blockquote\", () => {\n        expect(this.model.body.indexOf('gmail_quote') > 0).toBe(true);\n        expect(this.model.body.indexOf('blockquote') > 0).toBe(false);\n        expect(this.model.body.indexOf(fakeMessage1.body) > 0).toBe(true);\n        expect(this.model.body.indexOf('---------- Forwarded message ---------') > 0).toBe(true);\n        expect(this.model.body.indexOf('From: Customer &lt;customer@example.com&gt;') > 0).toBe(true);\n        expect(this.model.body.indexOf('Subject: Fake Subject') > 0).toBe(true);\n        expect(this.model.body.indexOf('To: ben@nylas.com, evan@nylas.com') > 0).toBe(true);\n        expect(this.model.body.indexOf('Cc: mg@nylas.com, Nylas Test &lt;tester@nylas.com&gt;') > 0).toBe(true);\n      });\n\n      it(\"should not mention BCC'd recipients in the forwarded message header\", () => {\n        expect(this.model.body.indexOf('recruiting@nylas.com') > 0).toBe(false);\n      });\n      it(\"should not address the message to anyone\", () => {\n        expect(this.model.to).toEqual([]);\n        expect(this.model.cc).toEqual([]);\n        expect(this.model.bcc).toEqual([]);\n      });\n\n      it(\"should not set the replyToMessageId\", () => {\n        expect(this.model.replyToMessageId).toEqual(undefined);\n      });\n\n      it(\"should sanitize the HTML\", () => {\n        expect(InlineStyleTransformer.run).toHaveBeenCalled();\n        expect(SanitizeTransformer.run).toHaveBeenCalled();\n      });\n\n      it(\"should include the attached files as uploads\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForForward({thread: fakeThread, message: fakeMessageWithFiles}).then((draft) => {\n            expect(draft.uploads.length).toBe(2);\n            expect(draft.uploads[0].filename).toBe(\"test.jpg\");\n            expect(draft.uploads[1].filename).toBe(\"test.pdj\");\n          });\n        });\n      });\n\n      it(\"should make the subject the subject of the message, not the thread\", () => {\n        fakeMessage1.subject = \"OLD SUBJECT\";\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForForward({thread: fakeThread, message: fakeMessage1}).then((draft) => {\n            expect(draft.subject).toEqual(\"Fwd: OLD SUBJECT\");\n          });\n        });\n      });\n\n      it(\"should change the subject from Re: back to Fwd: if necessary\", () => {\n        fakeMessage1.subject = \"Re: This is my DRAFT\";\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForForward({thread: fakeThread, message: fakeMessage1}).then((draft) => {\n            expect(draft.subject).toEqual(\"Fwd: This is my DRAFT\");\n          });\n        });\n      });\n    });\n  });\n\n  describe(\"createOrUpdateDraftForReply\", () => {\n    it(\"should throw an exception unless you provide `reply` or `reply-all`\", () => {\n      expect(() =>\n        DraftFactory.createOrUpdateDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'wrong'})\n      ).toThrow();\n    });\n\n    describe(\"when there is already a draft in reply to the same message the thread\", () => {\n      beforeEach(() => {\n        this.existingDraft = new Message({\n          clientId: 'asd',\n          accountId: TEST_ACCOUNT_ID,\n          replyToMessageId: fakeMessage1.id,\n          threadId: fakeMessage1.threadId,\n          draft: true,\n        });\n        this.sessionStub = {\n          changes: {\n            add: jasmine.createSpy('add'),\n          },\n        };\n        spyOn(Actions, 'focusDraft')\n        spyOn(DatabaseStore, 'run').andReturn(Promise.resolve([fakeMessage1, this.existingDraft]));\n        spyOn(DraftStore, 'sessionForClientId').andReturn(Promise.resolve(this.sessionStub));\n        spyOn(DatabaseWriter.prototype, 'persistModel').andReturn(Promise.resolve());\n\n        this.expectContactsEqual = (a, b) => {\n          expect(a.map(c => c.email).sort()).toEqual(b.map(c => c.email).sort());\n        }\n      });\n\n      describe(\"when reply-all is passed\", () => {\n        it(\"should add missing participants\", async () => {\n          this.existingDraft.to = fakeMessage1.participantsForReply().to;\n          this.existingDraft.cc = fakeMessage1.participantsForReply().cc;\n          const {to, cc} = await DraftFactory.createOrUpdateDraftForReply({\n            thread: fakeThread,\n            message: fakeMessage1,\n            type: 'reply-all',\n            behavior: 'prefer-existing',\n          })\n          this.expectContactsEqual(to, fakeMessage1.participantsForReplyAll().to);\n          this.expectContactsEqual(cc, fakeMessage1.participantsForReplyAll().cc);\n        });\n\n        it(\"should not blow away other participants who have been added to the draft\", async () => {\n          const randomA = new Contact({email: 'other-guy-a@gmail.com'});\n          const randomB = new Contact({email: 'other-guy-b@gmail.com'});\n          this.existingDraft.to = fakeMessage1.participantsForReply().to.concat([randomA]);\n          this.existingDraft.cc = fakeMessage1.participantsForReply().cc.concat([randomB]);\n          const {to, cc} = await DraftFactory.createOrUpdateDraftForReply({\n            thread: fakeThread,\n            message: fakeMessage1,\n            type: 'reply-all',\n            behavior: 'prefer-existing',\n          })\n          this.expectContactsEqual(to, fakeMessage1.participantsForReplyAll().to.concat([randomA]));\n          this.expectContactsEqual(cc, fakeMessage1.participantsForReplyAll().cc.concat([randomB]));\n        });\n      });\n\n      describe(\"when reply is passed\", () => {\n        it(\"should remove participants present in the reply-all participant set and not in the reply set\", async () => {\n          this.existingDraft.to = fakeMessage1.participantsForReplyAll().to;\n          this.existingDraft.cc = fakeMessage1.participantsForReplyAll().cc;\n          const {to, cc} = await DraftFactory.createOrUpdateDraftForReply({\n            thread: fakeThread,\n            message: fakeMessage1,\n            type: 'reply',\n            behavior: 'prefer-existing',\n          })\n          this.expectContactsEqual(to, fakeMessage1.participantsForReply().to);\n          this.expectContactsEqual(cc, fakeMessage1.participantsForReply().cc);\n        });\n\n        it(\"should not blow away other participants who have been added to the draft\", async () => {\n          const randomA = new Contact({email: 'other-guy-a@gmail.com'});\n          const randomB = new Contact({email: 'other-guy-b@gmail.com'});\n          this.existingDraft.to = fakeMessage1.participantsForReplyAll().to.concat([randomA]);\n          this.existingDraft.cc = fakeMessage1.participantsForReplyAll().cc.concat([randomB]);\n          const {to, cc} = await DraftFactory.createOrUpdateDraftForReply({\n            thread: fakeThread,\n            message: fakeMessage1,\n            type: 'reply',\n            behavior: 'prefer-existing',\n          })\n          this.expectContactsEqual(to, fakeMessage1.participantsForReply().to.concat([randomA]));\n          this.expectContactsEqual(cc, fakeMessage1.participantsForReply().cc.concat([randomB]));\n        });\n      });\n    });\n\n    describe(\"when there is not an existing draft at the bottom of the thread\", () => {\n      beforeEach(() => {\n        spyOn(Actions, 'focusDraft');\n        spyOn(DatabaseStore, 'run').andCallFake(() => [fakeMessage1]);\n        spyOn(DraftFactory, 'createDraftForReply');\n      });\n\n      it(\"should call through to createDraftForReply\", async () => {\n        await DraftFactory.createOrUpdateDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply-all'})\n        expect(DraftFactory.createDraftForReply).toHaveBeenCalledWith({thread: fakeThread, message: fakeMessage1, type: 'reply-all'})\n\n        await DraftFactory.createOrUpdateDraftForReply({thread: fakeThread, message: fakeMessage1, type: 'reply'})\n        expect(DraftFactory.createDraftForReply).toHaveBeenCalledWith({thread: fakeThread, message: fakeMessage1, type: 'reply'})\n      });\n    });\n  });\n\n  describe(\"_fromContactForReply\", () => {\n    it(\"should work correctly in a range of test cases\", () => {\n      // Note: These specs are based on the second account hard-coded in SpecHelper\n      account = AccountStore.accounts()[1];\n      const cases = [\n        {\n          to: [new Contact({name: 'Ben', email: 'ben@nylas.com'})], // user is not present, must have been BCC'd\n          cc: [],\n          expected: account.defaultMe(),\n        },\n        {\n          to: [new Contact({name: 'Second Support', email: 'second@gmail.com'})], // only name identifies alias\n          cc: [],\n          expected: new Contact({name: 'Second Support', email: 'second@gmail.com'}),\n        },\n        {\n          to: [new Contact({name: 'Second Wrong!', email: 'second+alternate@gmail.com'})], // only email identifies alias, name wrong\n          cc: [],\n          expected: new Contact({name: 'Second Alternate', email: 'second+alternate@gmail.com'}),\n        },\n        {\n          to: [new Contact({name: 'Second Alternate', email: 'second+alternate@gmail.com'})], // exact alias match\n          cc: [],\n          expected: new Contact({name: 'Second Alternate', email: 'second+alternate@gmail.com'}),\n        },\n        {\n          to: [new Contact({email: 'second+third@gmail.com'})], // exact alias match, name not present\n          cc: [],\n          expected: new Contact({name: 'Second', email: 'second+third@gmail.com'}),\n        },\n        {\n          to: [new Contact({email: 'ben@nylas.com'})],\n          cc: [new Contact({email: 'second+third@gmail.com'})], // exact alias match, but in CC\n          expected: new Contact({name: 'Second', email: 'second+third@gmail.com'}),\n        },\n      ]\n      cases.forEach(({to, cc, expected}) => {\n        const contact = DraftFactory._fromContactForReply(new Message({\n          accountId: account.id,\n          to: to,\n          cc: cc,\n        }));\n        expect(contact.name).toEqual(expected.name);\n        expect(contact.email).toEqual(expected.email);\n      });\n    });\n  });\n\n  describe(\"createDraftForMailto\", () => {\n    describe(\"parameters in the URL\", () => {\n      beforeEach(() => {\n        this.expected = \"EmailSubjectLOLOL\";\n      });\n\n      it(\"works for lowercase\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForMailto(`mailto:asdf@asdf.com?subject=${this.expected}`).then((draft) => {\n            expect(draft.subject).toBe(this.expected);\n          });\n        });\n      });\n\n      it(\"works for title case\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForMailto(`mailto:asdf@asdf.com?Subject=${this.expected}`).then((draft) => {\n            expect(draft.subject).toBe(this.expected);\n          });\n        });\n      });\n\n      it(\"works for uppercase\", () => {\n        waitsForPromise(() => {\n          return DraftFactory.createDraftForMailto(`mailto:asdf@asdf.com?SUBJECT=${this.expected}`).then((draft) => {\n            expect(draft.subject).toBe(this.expected);\n          });\n        });\n      });\n      ['mailto', 'mail', ''].forEach((url) => {\n        it(`rejects gracefully on super mangled mailto link: ${url}`, () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForMailto(url).then(() => {\n              expect('resolved').toBe(false);\n            }).catch(() => {\n            });\n          });\n        });\n      });\n    });\n\n    describe(\"should correctly instantiate drafts for a wide range of mailto URLs\", () => {\n      const links = [\n        'mailto:',\n        'mailto://bengotow@gmail.com',\n        'mailto:bengotow@gmail.com',\n        'mailto:mg%40nylas.com',\n        'mailto:?subject=%1z2a', // fails uriDecode\n        'mailto:?subject=%52z2a', // passes uriDecode\n        'mailto:?subject=Martha Stewart',\n        'mailto:?subject=Martha Stewart&cc=cc@nylas.com',\n        'mailto:bengotow@gmail.com&subject=Martha Stewart&cc=cc@nylas.com',\n        'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=bcc@nylas.com',\n        'mailto:bengotow@gmail.com?subject=Martha%20Stewart&cc=cc@nylas.com&bcc=Ben <bcc@nylas.com>',\n        'mailto:Ben Gotow <bengotow@gmail.com>,Shawn <shawn@nylas.com>?subject=Yes this is really valid',\n        'mailto:Ben%20Gotow%20<bengotow@gmail.com>,Shawn%20<shawn@nylas.com>?subject=Yes%20this%20is%20really%20valid',\n        'mailto:Reply <d+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com>?subject=Nilas%20Message%20to%20Customers',\n        'mailto:email@address.com?&subject=test&body=type%20your%0Amessage%20here',\n        'mailto:?body=type%20your%0D%0Amessage%0D%0Ahere',\n        'mailto:?subject=Issues%20%C2%B7%20atom/electron%20%C2%B7%20GitHub&body=https://github.com/atom/electron/issues?utf8=&q=is%253Aissue+is%253Aopen+123%0A%0A',\n      ];\n      const expected = [\n        new Message(),\n        new Message(\n          {to: [new Contact({name: 'bengotow@gmail.com', email: 'bengotow@gmail.com'})]}\n        ),\n        new Message(\n          {to: [new Contact({name: 'bengotow@gmail.com', email: 'bengotow@gmail.com'})]}\n        ),\n        new Message(\n          {to: [new Contact({name: 'mg@nylas.com', email: 'mg@nylas.com'})]}\n        ),\n        new Message(\n          {subject: '%1z2a'}\n        ),\n        new Message(\n          {subject: 'Rz2a'}\n        ),\n        new Message(\n          {subject: 'Martha Stewart'}\n        ),\n        new Message(\n          {cc: [new Contact({name: 'cc@nylas.com', email: 'cc@nylas.com'})],\n            subject: 'Martha Stewart'}\n        ),\n        new Message(\n          {to: [new Contact({name: 'bengotow@gmail.com', email: 'bengotow@gmail.com'})],\n            cc: [new Contact({name: 'cc@nylas.com', email: 'cc@nylas.com'})],\n            subject: 'Martha Stewart'}\n        ),\n        new Message(\n          {to: [new Contact({name: 'bengotow@gmail.com', email: 'bengotow@gmail.com'})],\n            cc: [new Contact({name: 'cc@nylas.com', email: 'cc@nylas.com'})],\n            bcc: [new Contact({name: 'bcc@nylas.com', email: 'bcc@nylas.com'})],\n            subject: 'Martha Stewart'}\n        ),\n        new Message(\n          {to: [new Contact({name: 'bengotow@gmail.com', email: 'bengotow@gmail.com'})],\n            cc: [new Contact({name: 'cc@nylas.com', email: 'cc@nylas.com'})],\n            bcc: [new Contact({name: 'Ben', email: 'bcc@nylas.com'})],\n            subject: 'Martha Stewart'}\n        ),\n        new Message(\n          {to: [new Contact({name: 'Ben Gotow', email: 'bengotow@gmail.com'}), new Contact({name: 'Shawn', email: 'shawn@nylas.com'})],\n            subject: 'Yes this is really valid'}\n        ),\n        new Message(\n          {to: [new Contact({name: 'Ben Gotow', email: 'bengotow@gmail.com'}), new Contact({name: 'Shawn', email: 'shawn@nylas.com'})],\n            subject: 'Yes this is really valid'}\n        ),\n        new Message(\n          {to: [new Contact({name: 'Reply', email: 'd+AORGpRdj0KXKUPBE1LoI0a30F10Ahj3wu3olS-aDk5_7K5Wu6WqqqG8t1HxxhlZ4KEEw3WmrSdtobgUq57SkwsYAH6tG57IrNqcQR0K6XaqLM2nGNZ22D2k@docs.google.com'})],\n            subject: 'Nilas Message to Customers'}\n        ),\n        new Message(\n          {to: [new Contact({name: 'email@address.com', email: 'email@address.com'})],\n            subject: 'test',\n            body: 'type your<br/>message here'}\n        ),\n        new Message(\n          {to: [],\n            body: 'type your<br/><br/>message<br/><br/>here'}\n        ),\n        new Message(\n          {to: [],\n            subject: 'Issues · atom/electron · GitHub',\n            body: 'https://github.com/atom/electron/issues?utf8=&q=is%3Aissue+is%3Aopen+123<br/><br/>'},\n        ),\n      ];\n\n      links.forEach((link, idx) => {\n        it(`works for ${link}`, () => {\n          waitsForPromise(() => {\n            return DraftFactory.createDraftForMailto(link).then((draft) => {\n              const expectedDraft = expected[idx];\n              expect(draft.subject).toEqual(expectedDraft.subject);\n              if (expectedDraft.body) { expect(draft.body).toEqual(expectedDraft.body); }\n              ['to', 'cc', 'bcc'].forEach((attr) => {\n                expectedDraft[attr].forEach((expectedContact, jdx) => {\n                  const actual = draft[attr][jdx];\n                  expect(actual instanceof Contact).toBe(true);\n                  expect(actual.email).toEqual(expectedContact.email);\n                  expect(actual.name).toEqual(expectedContact.name);\n                });\n              });\n            });\n          });\n        });\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/draft-helpers-spec.es6",
    "content": "import {\n  Actions,\n  Message,\n  DraftHelpers,\n  DatabaseStore,\n} from 'nylas-exports';\n\nimport InlineStyleTransformer from '../../src/services/inline-style-transformer'\nimport SanitizeTransformer from '../../src/services/sanitize-transformer';\n\n\nxdescribe('DraftHelpers', function describeBlock() {\n  describe('finalizeDraft', () => {\n    beforeEach(() => {\n      spyOn(Actions, 'queueTask')\n    });\n\n    it('calls the proper functions', () => {\n      const draft = new Message({\n        clientId: \"local-123\",\n        threadId: \"thread-123\",\n        uploads: [{inline: true, id: 1}],\n        body: \"\",\n      });\n      const session = {\n        ensureCorrectAccount() { return Promise.resolve() },\n        draft() { return draft },\n      }\n      spyOn(session, 'ensureCorrectAccount')\n      spyOn(DraftHelpers, 'applyExtensionTransforms').andCallFake(async (d) => d)\n      spyOn(DatabaseStore, 'inTransaction').andCallFake((f) => {\n        f({persistModel: m => m})\n      });\n      spyOn(DraftHelpers, 'removeStaleUploads');\n\n      waitsForPromise(async () => {\n        await DraftHelpers.finalizeDraft(session);\n        expect(session.ensureCorrectAccount).toHaveBeenCalled();\n        expect(DraftHelpers.applyExtensionTransforms).toHaveBeenCalled();\n        expect(DraftHelpers.removeStaleUploads).toHaveBeenCalled();\n      })\n    })\n  });\n\n  describe(\"prepareBodyForQuoting\", () => {\n    it(\"should transform inline styles and sanitize unsafe html\", () => {\n      const input = \"test 123\";\n      spyOn(InlineStyleTransformer, 'run').andCallThrough();\n      spyOn(SanitizeTransformer, 'run').andCallThrough();\n      DraftHelpers.prepareBodyForQuoting(input);\n      expect(InlineStyleTransformer.run).toHaveBeenCalledWith(input);\n      advanceClock();\n      expect(SanitizeTransformer.run).toHaveBeenCalledWith(input, SanitizeTransformer.Preset.UnsafeOnly);\n    });\n  });\n\n\n  describe('shouldAppendQuotedText', () => {\n    it('returns true if message is reply and has no marker', () => {\n      const draft = {\n        replyToMessageId: 1,\n        body: `<div>hello!</div>`,\n      }\n      expect(DraftHelpers.shouldAppendQuotedText(draft)).toBe(true)\n    })\n\n    it('returns false if message is reply and has marker', () => {\n      const draft = {\n        replyToMessageId: 1,\n        body: `<div>hello!</div><div id=\"n1-quoted-text-marker\"></div>Quoted Text`,\n      }\n      expect(DraftHelpers.shouldAppendQuotedText(draft)).toBe(false)\n    })\n\n    it('returns false if message is not reply', () => {\n      const draft = {\n        body: `<div>hello!</div>`,\n      }\n      expect(DraftHelpers.shouldAppendQuotedText(draft)).toBe(false)\n    })\n  })\n\n  describe('removeStaleUploads', () => {\n    describe('returns immediately when', () => {\n      beforeEach(() => {\n        spyOn(DatabaseStore, 'inTransaction').andReturn(Promise.resolve(null));\n      })\n\n      it('has 0 uploads', () => {\n        const draft = new Message({uploads: []});\n        waitsForPromise(async () => {\n          await DraftHelpers.removeStaleUploads(draft)\n          expect(DatabaseStore.inTransaction).not.toHaveBeenCalled();\n        })\n      })\n\n      it('has an invalid uploads field', () => {\n        const draft = new Message({uploads: \"uploads\"});\n        waitsForPromise(async () => {\n          await DraftHelpers.removeStaleUploads(draft)\n          expect(DatabaseStore.inTransaction).not.toHaveBeenCalled();\n        })\n      })\n    })\n\n    it('removes the proper uploads', () => {\n      const draft = new Message({\n        uploads: [\n          {inline: true, id: 1},\n          {inline: true, id: 2},\n          {inline: false, id: 3},\n        ],\n        body: 'aldkfjoe cid:2 adlfkobieejlkd',\n      })\n      waitsForPromise(async () => {\n        const {uploads} = await DraftHelpers.removeStaleUploads(draft)\n        expect(uploads.length).toEqual(2);\n        expect(uploads.find(u => u.id === 1)).not.toBeDefined();\n        expect(uploads.find(u => u.id === 2)).toBeDefined();\n        expect(uploads.find(u => u.id === 3)).toBeDefined();\n      })\n    })\n  })\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/draft-store-spec.es6",
    "content": "import {\n  Thread,\n  Actions,\n  Contact,\n  Message,\n  Account,\n  DraftStore,\n  DraftHelpers,\n  DatabaseStore,\n  SoundRegistry,\n  DestroyDraftTask,\n  ComposerExtension,\n  ExtensionRegistry,\n  FocusedContentStore,\n  DatabaseWriter,\n} from 'nylas-exports';\n\nimport {remote} from 'electron';\nimport DraftFactory from '../../src/flux/stores/draft-factory';\n\nclass TestExtension extends ComposerExtension {\n  static prepareNewDraft({draft}) {\n    draft.body = `Edited by TestExtension! ${draft.body}`;\n  }\n}\n\nxdescribe('DraftStore', function draftStore() {\n  beforeEach(() => {\n    this.fakeThread = new Thread({id: 'fake-thread', clientId: 'fake-thread'});\n    this.fakeMessage = new Message({id: 'fake-message', clientId: 'fake-message'});\n\n    spyOn(NylasEnv, 'newWindow').andCallFake(() => {});\n    spyOn(DatabaseWriter.prototype, \"persistModel\").andReturn(Promise.resolve());\n    spyOn(DatabaseStore, 'run').andCallFake((query) => {\n      if (query._klass === Thread) { return Promise.resolve(this.fakeThread); }\n      if (query._klass === Message) { return Promise.resolve(this.fakeMessage); }\n      if (query._klass === Contact) { return Promise.resolve(null); }\n      return Promise.reject(new Error(`Not Stubbed for class ${query._klass.name}`));\n    });\n\n    for (const draftClientId of Object.keys(DraftStore._draftSessions)) {\n      const sess = DraftStore._draftSessions[draftClientId];\n      if (sess.teardown) {\n        DraftStore._doneWithSession(sess);\n      }\n    }\n    DraftStore._draftSessions = {};\n  });\n\n  describe(\"creating and opening drafts\", () => {\n    beforeEach(() => {\n      const draft = new Message({id: \"A\", subject: \"B\", clientId: \"A\", body: \"123\"});\n      this.newDraft = draft;\n      spyOn(DraftFactory, \"createDraftForReply\").andReturn(Promise.resolve(draft));\n      spyOn(DraftFactory, \"createOrUpdateDraftForReply\").andReturn(Promise.resolve(draft));\n      spyOn(DraftFactory, \"createDraftForForward\").andReturn(Promise.resolve(draft));\n      spyOn(DraftFactory, \"createDraft\").andReturn(Promise.resolve(draft));\n    });\n\n    it(\"should always attempt to focus the new draft\", () => {\n      spyOn(Actions, 'focusDraft')\n      DraftStore._onComposeReply({\n        threadId: this.fakeThread.id,\n        messageId: this.fakeMessage.id,\n        type: 'reply',\n        behavior: 'prefer-existing',\n      });\n      advanceClock();\n      advanceClock();\n      expect(Actions.focusDraft).toHaveBeenCalled();\n    });\n\n    describe(\"context\", () => {\n      it(\"can accept IDs for thread and message arguments\", () => {\n        DraftStore._onComposeReply({\n          threadId: this.fakeThread.id,\n          messageId: this.fakeMessage.id,\n          type: 'reply',\n          behavior: 'prefer-existing',\n        });\n        advanceClock();\n        expect(DraftFactory.createOrUpdateDraftForReply).toHaveBeenCalledWith({\n          thread: this.fakeThread,\n          message: this.fakeMessage,\n          type: 'reply',\n          behavior: 'prefer-existing',\n        });\n      });\n\n      it(\"can accept models for thread and message arguments\", () => {\n        DraftStore._onComposeReply({\n          thread: this.fakeThread,\n          message: this.fakeMessage,\n          type: 'reply',\n          behavior: 'prefer-existing',\n        });\n        advanceClock();\n        expect(DraftFactory.createOrUpdateDraftForReply).toHaveBeenCalledWith({\n          thread: this.fakeThread,\n          message: this.fakeMessage,\n          type: 'reply',\n          behavior: 'prefer-existing',\n        });\n      });\n\n      it(\"can accept only a thread / threadId, and use the last message on the thread\", () => {\n        DraftStore._onComposeReply({\n          thread: this.fakeThread,\n          type: 'reply',\n          behavior: 'prefer-existing',\n        });\n        advanceClock();\n        expect(DraftFactory.createOrUpdateDraftForReply).toHaveBeenCalledWith({\n          thread: this.fakeThread,\n          message: this.fakeMessage,\n          type: 'reply',\n          behavior: 'prefer-existing',\n        });\n      });\n    });\n\n    describe(\"popout behavior\", () => {\n      it(\"can popout a reply\", () => {\n        runs(() => {\n          DraftStore._onComposeReply({\n            threadId: this.fakeThread.id,\n            messageId: this.fakeMessage.id,\n            type: 'reply',\n            popout: true}\n          );\n        });\n        waitsFor(() => {\n          return DatabaseWriter.prototype.persistModel.callCount > 0;\n        });\n        runs(() => {\n          expect(NylasEnv.newWindow).toHaveBeenCalledWith({\n            title: 'Message',\n            hidden: true,\n            windowKey: `composer-A`,\n            windowType: \"composer-preload\",\n            windowProps: { draftClientId: \"A\", draftJSON: this.newDraft.toJSON() },\n          });\n        });\n      });\n\n      it(\"can popout a forward\", () => {\n        runs(() => {\n          DraftStore._onComposeForward({\n            threadId: this.fakeThread.id,\n            messageId: this.fakeMessage.id,\n            popout: true,\n          });\n        });\n        waitsFor(() => {\n          return DatabaseWriter.prototype.persistModel.callCount > 0;\n        });\n        runs(() => {\n          expect(NylasEnv.newWindow).toHaveBeenCalledWith({\n            title: 'Message',\n            hidden: true,\n            windowKey: `composer-A`,\n            windowType: \"composer-preload\",\n            windowProps: { draftClientId: \"A\", draftJSON: this.newDraft.toJSON() },\n          });\n        });\n      });\n    });\n  });\n\n  describe(\"onDestroyDraft\", () => {\n    beforeEach(() => {\n      this.draftSessionTeardown = jasmine.createSpy('draft teardown');\n      this.session =\n      {draft() {\n        return {pristine: false};\n      },\n        changes:\n        {commit() { return Promise.resolve(); },\n          teardown() {},\n        },\n        teardown: this.draftSessionTeardown,\n      };\n      DraftStore._draftSessions = {abc: this.session};\n      spyOn(Actions, 'queueTask');\n    });\n\n    it(\"should teardown the draft session, ensuring no more saves are made\", () => {\n      DraftStore._onDestroyDraft('abc');\n      expect(this.draftSessionTeardown).toHaveBeenCalled();\n    });\n\n    it(\"should not throw if the draft session is not in the window\", () => {\n      expect(() => DraftStore._onDestroyDraft('other')).not.toThrow();\n    });\n\n    it(\"should queue a destroy draft task\", () => {\n      DraftStore._onDestroyDraft('abc');\n      expect(Actions.queueTask).toHaveBeenCalled();\n      expect(Actions.queueTask.mostRecentCall.args[0] instanceof DestroyDraftTask).toBe(true);\n    });\n\n    it(\"should clean up the draft session\", () => {\n      spyOn(DraftStore, '_doneWithSession');\n      DraftStore._onDestroyDraft('abc');\n      expect(DraftStore._doneWithSession).toHaveBeenCalledWith(this.session);\n    });\n\n    it(\"should close the window if it's a popout\", () => {\n      spyOn(NylasEnv, \"close\");\n      spyOn(NylasEnv, \"isComposerWindow\").andReturn(true);\n      DraftStore._onDestroyDraft('abc');\n      expect(NylasEnv.close).toHaveBeenCalled();\n    });\n\n    it(\"should NOT close the window if isn't a popout\", () => {\n      spyOn(NylasEnv, \"close\");\n      spyOn(NylasEnv, \"isComposerWindow\").andReturn(false);\n      DraftStore._onDestroyDraft('abc');\n      expect(NylasEnv.close).not.toHaveBeenCalled();\n    });\n  });\n\n  describe(\"before unloading\", () => {\n    it(\"should destroy pristine drafts\", () => {\n      DraftStore._draftSessions = {\n        abc: {\n          changes: {},\n          draft() {\n            return {pristine: true};\n          },\n        },\n      };\n\n      spyOn(Actions, 'queueTask');\n      DraftStore._onBeforeUnload();\n      expect(Actions.queueTask).toHaveBeenCalled();\n      expect(Actions.queueTask.mostRecentCall.args[0] instanceof DestroyDraftTask).toBe(true);\n    });\n\n    describe(\"when drafts return unresolved commit promises\", () => {\n      beforeEach(() => {\n        this.resolve = null;\n        DraftStore._draftSessions = {\n          abc: {\n            changes: {\n              commit: () => new Promise((resolve) => { this.resolve = resolve }),\n            },\n            draft() {\n              return {pristine: false};\n            },\n          },\n        };\n      });\n\n      it(\"should return false and call window.close itself\", () => {\n        const callback = jasmine.createSpy('callback');\n        expect(DraftStore._onBeforeUnload(callback)).toBe(false);\n        expect(callback).not.toHaveBeenCalled();\n        this.resolve();\n        advanceClock(1000);\n        advanceClock(1000);\n        expect(callback).toHaveBeenCalled();\n      });\n    });\n\n    describe(\"when drafts return immediately fulfilled commit promises\", () => {\n      beforeEach(() => {\n        DraftStore._draftSessions = {\n          abc: {\n            changes:\n              {commit: () => Promise.resolve()},\n            draft() {\n              return {pristine: false};\n            },\n          },\n        };\n      });\n\n      it(\"should still wait one tick before firing NylasEnv.close again\", () => {\n        const callback = jasmine.createSpy('callback');\n        expect(DraftStore._onBeforeUnload(callback)).toBe(false);\n        expect(callback).not.toHaveBeenCalled();\n        advanceClock();\n        advanceClock(1000);\n        expect(callback).toHaveBeenCalled();\n      });\n    });\n\n    describe(\"when there are no drafts\", () => {\n      beforeEach(() => {\n        DraftStore._draftSessions = {};\n      });\n\n      it(\"should return true and allow the window to close\", () => {\n        expect(DraftStore._onBeforeUnload()).toBe(true);\n      });\n    });\n  });\n\n  describe(\"sending a draft\", () => {\n    beforeEach(() => {\n      this.draft = new Message({\n        clientId: \"local-123\",\n        threadId: \"thread-123\",\n        replyToMessageId: \"message-123\",\n        uploads: ['stub'],\n      });\n      DraftStore._draftSessions = {};\n      DraftStore._draftsSending = {};\n      this.forceCommit = false;\n      const session = {\n        prepare() {\n          return Promise.resolve(session);\n        },\n        teardown() {},\n        draft: () => this.draft,\n        changes: {\n          commit: ({force} = {}) => {\n            this.forceCommit = force;\n            return Promise.resolve();\n          },\n        },\n      };\n\n      DraftStore._draftSessions[this.draft.clientId] = session;\n      spyOn(DraftStore, \"_doneWithSession\").andCallThrough();\n      spyOn(DraftHelpers, \"finalizeDraft\").andReturn(Promise.resolve());\n      spyOn(DraftStore, \"trigger\");\n      spyOn(SoundRegistry, \"playSound\");\n      spyOn(Actions, \"queueTask\");\n    });\n\n    it(\"plays a sound immediately when sending draft\", () => {\n      spyOn(NylasEnv.config, \"get\").andReturn(true);\n      DraftStore._onSendDraft(this.draft.clientId);\n      advanceClock();\n      expect(NylasEnv.config.get).toHaveBeenCalledWith(\"core.sending.sounds\");\n      expect(SoundRegistry.playSound).toHaveBeenCalledWith(\"hit-send\");\n    });\n\n    it(\"doesn't plays a sound if the setting is off\", () => {\n      spyOn(NylasEnv.config, \"get\").andReturn(false);\n      DraftStore._onSendDraft(this.draft.clientId);\n      advanceClock();\n      expect(NylasEnv.config.get).toHaveBeenCalledWith(\"core.sending.sounds\");\n      expect(SoundRegistry.playSound).not.toHaveBeenCalled();\n    });\n\n    it(\"sets the sending state when sending\", () => {\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(true);\n      DraftStore._onSendDraft(this.draft.clientId);\n      advanceClock();\n      expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(true);\n    });\n\n    // Since all changes haven't been applied yet, we want to ensure that\n    // no view of the draft renders the draft as if its sending, but with\n    // the wrong text.\n    it(\"does NOT trigger until the latest changes have been applied\", () => {\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(true);\n      runs(() => {\n        DraftStore._onSendDraft(this.draft.clientId);\n        expect(DraftStore.trigger).not.toHaveBeenCalled();\n      });\n      waitsFor(() => {\n        return Actions.queueTask.calls.length > 0;\n      });\n      runs(() => {\n        // Normally, the session.changes.commit will persist to the\n        // Database. Since that's stubbed out, we need to manually invoke\n        // to database update event to get the trigger (which we want to\n        // test) to fire\n        DraftStore._onDataChanged({\n          objectClass: \"Message\",\n          objects: [{draft: true}],\n        });\n        expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(true);\n        expect(DraftStore.trigger).toHaveBeenCalled();\n        expect(DraftStore.trigger.calls.length).toBe(1);\n      });\n    });\n\n    it(\"returns false if the draft hasn't been seen\", () => {\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(true);\n      expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(false);\n    });\n\n    it(\"closes the window if it's a popout\", () => {\n      spyOn(NylasEnv, \"getWindowType\").andReturn(\"composer\");\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(false);\n      spyOn(NylasEnv, \"close\");\n      runs(() => {\n        return DraftStore._onSendDraft(this.draft.clientId);\n      });\n      waitsFor(\"N1 to close\", () => NylasEnv.close.calls.length > 0);\n    });\n\n    it(\"doesn't close the window if it's inline\", () => {\n      spyOn(NylasEnv, \"getWindowType\").andReturn(\"other\");\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(false);\n      spyOn(NylasEnv, \"close\");\n      spyOn(NylasEnv, \"isComposerWindow\").andCallThrough();\n      runs(() => {\n        DraftStore._onSendDraft(this.draft.clientId);\n      });\n      waitsFor(() => NylasEnv.isComposerWindow.calls.length > 0);\n      runs(() => {\n        expect(NylasEnv.close).not.toHaveBeenCalled();\n      });\n    });\n\n    it(\"resets the sending state if there's an error\", () => {\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(false);\n      DraftStore._draftsSending[this.draft.clientId] = true;\n      Actions.draftDeliveryFailed({errorMessage: \"boohoo\", draftClientId: this.draft.clientId});\n      expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(false);\n      expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.clientId);\n    });\n\n    it(\"displays a popup in the main window if there's an error\", () => {\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(true);\n      spyOn(FocusedContentStore, \"focused\").andReturn({id: \"t1\"});\n      spyOn(remote.dialog, \"showMessageBox\");\n      spyOn(Actions, \"composePopoutDraft\");\n      DraftStore._draftsSending[this.draft.clientId] = true;\n      Actions.draftDeliveryFailed({threadId: 't1', errorMessage: \"boohoo\", draftClientId: this.draft.clientId});\n      advanceClock(400);\n      expect(DraftStore.isSendingDraft(this.draft.clientId)).toBe(false);\n      expect(DraftStore.trigger).toHaveBeenCalledWith(this.draft.clientId);\n      expect(remote.dialog.showMessageBox).toHaveBeenCalled();\n      const dialogArgs = remote.dialog.showMessageBox.mostRecentCall.args[1];\n      expect(dialogArgs.detail).toEqual(\"boohoo\");\n      expect(Actions.composePopoutDraft).not.toHaveBeenCalled();\n    });\n\n    it(\"re-opens the draft if you're not looking at the thread\", () => {\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(true);\n      spyOn(FocusedContentStore, \"focused\").andReturn({id: \"t1\"});\n      spyOn(Actions, \"composePopoutDraft\");\n      DraftStore._draftsSending[this.draft.clientId] = true;\n      Actions.draftDeliveryFailed({threadId: 't2', errorMessage: \"boohoo\", draftClientId: this.draft.clientId});\n      advanceClock(400);\n      expect(Actions.composePopoutDraft).toHaveBeenCalled();\n      const call = Actions.composePopoutDraft.calls[0];\n      expect(call.args[0]).toBe(this.draft.clientId);\n      expect(call.args[1]).toEqual({errorMessage: \"boohoo\"});\n    });\n\n    it(\"re-opens the draft if there is no thread id\", () => {\n      spyOn(NylasEnv, \"isMainWindow\").andReturn(true);\n      spyOn(Actions, \"composePopoutDraft\");\n      DraftStore._draftsSending[this.draft.clientId] = true;\n      spyOn(FocusedContentStore, \"focused\").andReturn(null);\n      Actions.draftDeliveryFailed({errorMessage: \"boohoo\", draftClientId: this.draft.clientId});\n      advanceClock(400);\n      expect(Actions.composePopoutDraft).toHaveBeenCalled();\n      const call = Actions.composePopoutDraft.calls[0];\n      expect(call.args[0]).toBe(this.draft.clientId);\n      expect(call.args[1]).toEqual({errorMessage: \"boohoo\"});\n    });\n  });\n\n  describe(\"session teardown\", () => {\n    beforeEach(() => {\n      spyOn(NylasEnv, 'isMainWindow').andReturn(true);\n      this.draftTeardown = jasmine.createSpy('draft teardown');\n      this.session = {\n        draftClientId: \"abc\",\n        draft() {\n          return {pristine: false};\n        },\n        changes: {\n          commit() { return Promise.resolve(); },\n          reset() {},\n        },\n        teardown: this.draftTeardown,\n      };\n      DraftStore._draftSessions = {abc: this.session};\n      DraftStore._doneWithSession(this.session);\n    });\n\n    it(\"removes from the list of draftSessions\", () => {\n      expect(DraftStore._draftSessions.abc).toBeUndefined();\n    });\n\n    it(\"Calls teardown on the session\", () => {\n      expect(this.draftTeardown).toHaveBeenCalled();\n    });\n  });\n\n  describe(\"mailto handling\", () => {\n    beforeEach(() => {\n      spyOn(NylasEnv, 'isMainWindow').andReturn(true);\n    });\n\n    describe(\"extensions\", () => {\n      beforeEach(() => {\n        ExtensionRegistry.Composer.register(TestExtension);\n      });\n      afterEach(() => {\n        ExtensionRegistry.Composer.unregister(TestExtension);\n      });\n\n      it(\"should give extensions a chance to customize the draft via ext.prepareNewDraft\", () => {\n        waitsForPromise(() => {\n          return DraftStore._onHandleMailtoLink({}, 'mailto:bengotow@gmail.com').then(() => {\n            const received = DatabaseWriter.prototype.persistModel.mostRecentCall.args[0];\n            expect(received.body.indexOf(\"Edited by TestExtension!\")).toBe(0);\n          });\n        });\n      });\n    });\n\n    it(\"should call through to DraftFactory and popout a new draft\", () => {\n      const draft = new Message({clientId: \"A\", body: '123'});\n      spyOn(DraftFactory, 'createDraftForMailto').andReturn(Promise.resolve(draft));\n      spyOn(DraftStore, '_onPopoutDraftClientId');\n      waitsForPromise(() => {\n        return DraftStore._onHandleMailtoLink({}, 'mailto:bengotow@gmail.com').then(() => {\n          const received = DatabaseWriter.prototype.persistModel.mostRecentCall.args[0];\n          expect(received).toEqual(draft);\n          expect(DraftStore._onPopoutDraftClientId).toHaveBeenCalled();\n        });\n      });\n    });\n  });\n\n  describe(\"mailfiles handling\", () => {\n    it(\"should popout a new draft\", () => {\n      const defaultMe = new Contact();\n      spyOn(DraftStore, '_onPopoutDraftClientId');\n      spyOn(Account.prototype, 'defaultMe').andReturn(defaultMe);\n      spyOn(Actions, 'addAttachment').andCallFake(({onUploadCreated}) => onUploadCreated());\n      DraftStore._onHandleMailFiles({}, ['/Users/ben/file1.png', '/Users/ben/file2.png']);\n      waitsFor(() => DatabaseWriter.prototype.persistModel.callCount > 0);\n      runs(() => {\n        const {body, subject, from} = DatabaseWriter.prototype.persistModel.calls[0].args[0];\n        expect({body, subject, from}).toEqual({body: '', subject: '', from: [defaultMe]});\n        expect(DraftStore._onPopoutDraftClientId).toHaveBeenCalled();\n      });\n    });\n\n    it(\"should call addAttachment for each provided file path\", () => {\n      spyOn(Actions, 'addAttachment');\n      DraftStore._onHandleMailFiles({}, ['/Users/ben/file1.png', '/Users/ben/file2.png']);\n      waitsFor(() => Actions.addAttachment.callCount === 2);\n      runs(() => {\n        expect(Actions.addAttachment.calls[0].args[0].filePath).toEqual('/Users/ben/file1.png');\n        expect(Actions.addAttachment.calls[1].args[0].filePath).toEqual('/Users/ben/file2.png');\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/feature-usage-store-spec.es6",
    "content": "import {Actions, TaskQueue, TaskQueueStatusStore} from 'nylas-exports'\nimport FeatureUsageStore from '../../src/flux/stores/feature-usage-store'\nimport Task from '../../src/flux/tasks/task'\nimport SendFeatureUsageEventTask from '../../src/flux/tasks/send-feature-usage-event-task'\nimport IdentityStore from '../../src/flux/stores/identity-store'\n\ndescribe(\"FeatureUsageStore\", function featureUsageStoreSpec() {\n  beforeEach(() => {\n    this.oldIdent = IdentityStore._identity;\n    IdentityStore._identity = {id: 'foo'}\n    IdentityStore._identity.feature_usage = {\n      \"is-usable\": {\n        quota: 10,\n        period: 'monthly',\n        used_in_period: 8,\n        feature_limit_name: 'Usable Group A',\n      },\n      \"not-usable\": {\n        quota: 10,\n        period: 'monthly',\n        used_in_period: 10,\n        feature_limit_name: 'Unusable Group A',\n      },\n    }\n  });\n\n  afterEach(() => {\n    IdentityStore._identity = this.oldIdent\n  });\n\n  describe(\"_isUsable\", () => {\n    it(\"returns true if a feature hasn't met it's quota\", () => {\n      expect(FeatureUsageStore._isUsable(\"is-usable\")).toBe(true)\n    });\n\n    it(\"returns false if a feature is at its quota\", () => {\n      expect(FeatureUsageStore._isUsable(\"not-usable\")).toBe(false)\n    });\n\n    it(\"warns if asking for an unsupported feature\", () => {\n      spyOn(NylasEnv, \"reportError\")\n      expect(FeatureUsageStore._isUsable(\"unsupported\")).toBe(false)\n      expect(NylasEnv.reportError).toHaveBeenCalled()\n    });\n  });\n\n  describe(\"_markFeatureUsed\", () => {\n    beforeEach(() => {\n      spyOn(SendFeatureUsageEventTask.prototype, \"performRemote\").andReturn(Promise.resolve(Task.Status.Success));\n      spyOn(IdentityStore, \"saveIdentity\").andCallFake((ident) => {\n        IdentityStore._identity = ident\n      })\n      spyOn(TaskQueueStatusStore, \"waitForPerformLocal\").andReturn(Promise.resolve())\n      spyOn(Actions, 'queueTask').andCallFake((task) => {\n        task.performLocal()\n      })\n    });\n\n    afterEach(() => {\n      TaskQueue._queue = []\n    })\n\n    it(\"returns the num remaining if successful\", async () => {\n      let numLeft = await FeatureUsageStore._markFeatureUsed('is-usable');\n      expect(numLeft).toBe(1)\n      numLeft = await FeatureUsageStore._markFeatureUsed('is-usable');\n      expect(numLeft).toBe(0)\n    });\n  });\n\n  describe(\"use feature\", () => {\n    beforeEach(() => {\n      spyOn(FeatureUsageStore, \"_markFeatureUsed\").andReturn(Promise.resolve());\n      spyOn(Actions, \"openModal\")\n    });\n\n    it(\"marks the feature used if you have pro access\", async () => {\n      spyOn(IdentityStore, \"hasProAccess\").andReturn(true);\n      await FeatureUsageStore.asyncUseFeature('not-usable')\n      expect(FeatureUsageStore._markFeatureUsed).toHaveBeenCalled();\n      expect(FeatureUsageStore._markFeatureUsed.callCount).toBe(1);\n    });\n\n    it(\"marks the feature used if it's usable\", async () => {\n      spyOn(IdentityStore, \"hasProAccess\").andReturn(false);\n      await FeatureUsageStore.asyncUseFeature('is-usable')\n      expect(FeatureUsageStore._markFeatureUsed).toHaveBeenCalled();\n      expect(FeatureUsageStore._markFeatureUsed.callCount).toBe(1);\n    });\n\n    describe(\"showing modal\", () => {\n      beforeEach(() => {\n        this.hasProAccess = false;\n        spyOn(IdentityStore, \"hasProAccess\").andCallFake(() => {\n          return this.hasProAccess;\n        })\n        this.lexicon = {\n          displayName: \"Test Name\",\n          rechargeCTA: \"recharge me\",\n          usedUpHeader: \"all test used\",\n          iconUrl: \"icon url\",\n        }\n      });\n\n      it(\"resolves the modal if you upgrade\", async () => {\n        setImmediate(() => {\n          this.hasProAccess = true;\n          FeatureUsageStore._onModalClose()\n        })\n        await FeatureUsageStore.asyncUseFeature('not-usable', {lexicon: this.lexicon});\n        expect(Actions.openModal).toHaveBeenCalled();\n        expect(Actions.openModal.calls.length).toBe(1)\n      });\n\n      it(\"pops open a modal with the correct text\", async () => {\n        setImmediate(() => {\n          this.hasProAccess = true;\n          FeatureUsageStore._onModalClose()\n        })\n        await FeatureUsageStore.asyncUseFeature('not-usable', {lexicon: this.lexicon});\n        expect(Actions.openModal).toHaveBeenCalled();\n        expect(Actions.openModal.calls.length).toBe(1)\n        const component = Actions.openModal.calls[0].args[0].component;\n        expect(component.props).toEqual({\n          modalClass: \"not-usable\",\n          featureName: \"Test Name\",\n          headerText: \"all test used\",\n          iconUrl: \"icon url\",\n          rechargeText: \"You’ll have 10 more next month\",\n        })\n      });\n\n      it(\"rejects if you don't upgrade\", async () => {\n        let caughtError = false;\n        setImmediate(() => {\n          this.hasProAccess = false;\n          FeatureUsageStore._onModalClose()\n        })\n        try {\n          await FeatureUsageStore.asyncUseFeature('not-usable', {lexicon: this.lexicon});\n        } catch (err) {\n          expect(err instanceof FeatureUsageStore.NoProAccess).toBe(true)\n          caughtError = true;\n        }\n        expect(caughtError).toBe(true)\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/file-download-store-spec.coffee",
    "content": "fs = require 'fs'\npath = require 'path'\n{shell} = require 'electron'\nNylasAPI = require('../../src/flux/nylas-api').default\nNylasAPIRequest = require('../../src/flux/nylas-api-request').default\nFile = require('../../src/flux/models/file').default\nMessage = require('../../src/flux/models/message').default\nFileDownloadStore = require('../../src/flux/stores/file-download-store').default\n{Download} = require('../../src/flux/stores/file-download-store')\nAccountStore = require('../../src/flux/stores/account-store').default\n\n\nxdescribe 'FileDownloadStoreSpecs', ->\n\n  describe \"Download\", ->\n    beforeEach ->\n      spyOn(fs, 'createWriteStream')\n      spyOn(NylasAPIRequest.prototype, 'run')\n\n    describe \"constructor\", ->\n      it \"should require a non-empty filename\", ->\n        expect(-> new Download(fileId: '123', targetPath: 'test.png')).toThrow()\n        expect(-> new Download(filename: null, fileId: '123', targetPath: 'test.png')).toThrow()\n        expect(-> new Download(filename: '', fileId: '123', targetPath: 'test.png')).toThrow()\n\n      it \"should require a non-empty fileId\", ->\n        expect(-> new Download(filename: 'test.png', fileId: null, targetPath: 'test.png')).toThrow()\n        expect(-> new Download(filename: 'test.png', fileId: '', targetPath: 'test.png')).toThrow()\n\n      it \"should require a download path\", ->\n        expect(-> new Download(filename: 'test.png', fileId: '123')).toThrow()\n        expect(-> new Download(filename: 'test.png', fileId: '123', targetPath: '')).toThrow()\n\n    describe \"run\", ->\n      beforeEach ->\n        account = AccountStore.accounts()[0]\n        @download = new Download(fileId: '123', targetPath: 'test.png', filename: 'test.png', accountId: account.id)\n        @download.run()\n        expect(NylasAPIRequest.prototype.run).toHaveBeenCalled()\n\n      it \"should create a request with a null encoding to prevent the request library\" +\n         \" from attempting to parse the (potentially very large) response\", ->\n        expect(NylasAPIRequest.prototype.run.mostRecentCall.object.options.json).toBe(false)\n        expect(NylasAPIRequest.prototype.run.mostRecentCall.object.options.encoding).toBe(null)\n\n      it \"should create a request for /files/123/download\", ->\n        expect(NylasAPIRequest.prototype.run.mostRecentCall.object.options.path).toBe(\"/files/123/download\")\n\n  describe \"FileDownloadStore\", ->\n    beforeEach ->\n      account = AccountStore.accounts()[0]\n\n      spyOn(shell, 'showItemInFolder')\n      spyOn(shell, 'openItem')\n      @testfile = new File({\n        accountId: account.id,\n        filename: '123.png',\n        contentType: 'image/png',\n        id: \"id\",\n        size: 100\n      })\n      @testdownload = new Download({\n        accountId: account.id,\n        state : 'unknown',\n        fileId : 'id',\n        percent : 0,\n        filename : '123.png',\n        filesize : 100,\n        targetPath : '/Users/testuser/.nylas-mail/downloads/id/123.png'\n      })\n\n      FileDownloadStore._downloads = {}\n      FileDownloadStore._downloadDirectory = \"/Users/testuser/.nylas-mail/downloads\"\n      spyOn(FileDownloadStore, '_generatePreview').andReturn(Promise.resolve())\n\n    describe \"pathForFile\", ->\n      it \"should return path within the download directory with the file id and displayName\", ->\n        f = new File(filename: '123.png', contentType: 'image/png', id: 'id')\n        spyOn(f, 'displayName').andCallThrough()\n        expect(FileDownloadStore.pathForFile(f)).toBe(\"/Users/testuser/.nylas-mail/downloads/id/123.png\")\n        expect(f.displayName).toHaveBeenCalled()\n\n      it \"should return unique paths for identical filenames with different IDs\", ->\n        f1 = new File(filename: '123.png', contentType: 'image/png', id: 'id1')\n        f2 = new File(filename: '123.png', contentType: 'image/png', id: 'id2')\n        expect(FileDownloadStore.pathForFile(f1)).toBe(\"/Users/testuser/.nylas-mail/downloads/id1/123.png\")\n        expect(FileDownloadStore.pathForFile(f2)).toBe(\"/Users/testuser/.nylas-mail/downloads/id2/123.png\")\n\n    it \"should escape the displayName if it contains path separator characters\", ->\n      f1 = new File(filename: \"static#{path.sep}b#{path.sep}a.jpg\", contentType: 'image/png', id: 'id1')\n      expect(FileDownloadStore.pathForFile(f1)).toBe(\"/Users/testuser/.nylas-mail/downloads/id1/static-b-a.jpg\")\n\n      f1 = new File(filename: \"my:file ? Windows /hates/ me :->.jpg\", contentType: 'image/png', id: 'id1')\n      expect(FileDownloadStore.pathForFile(f1)).toBe(\"/Users/testuser/.nylas-mail/downloads/id1/my-file - Windows -hates- me ---.jpg\")\n\n    describe \"_checkForDownloadedFile\", ->\n      it \"should return true if the file exists at the path and is the right size\", ->\n        f = new File(filename: '123.png', contentType: 'image/png', id: \"id\", size: 100)\n        spyOn(fs, 'statAsync').andCallFake (path) ->\n          Promise.resolve({size: 100})\n        waitsForPromise ->\n          FileDownloadStore._checkForDownloadedFile(f).then (downloaded) ->\n            expect(downloaded).toBe(true)\n\n      it \"should return false if the file does not exist\", ->\n        f = new File(filename: '123.png', contentType: 'image/png', id: \"id\", size: 100)\n        spyOn(fs, 'statAsync').andCallFake (path) ->\n          Promise.reject(new Error(\"File does not exist\"))\n        waitsForPromise ->\n          FileDownloadStore._checkForDownloadedFile(f).then (downloaded) ->\n            expect(downloaded).toBe(false)\n\n      it \"should return false if the file is too small\", ->\n        f = new File(filename: '123.png', contentType: 'image/png', id: \"id\", size: 100)\n        spyOn(fs, 'statAsync').andCallFake (path) ->\n          Promise.resolve({size: 50})\n        waitsForPromise ->\n          FileDownloadStore._checkForDownloadedFile(f).then (downloaded) ->\n            expect(downloaded).toBe(false)\n\n    describe \"_runDownload\", ->\n      beforeEach ->\n        spyOn(Download.prototype, 'run').andCallFake -> Promise.resolve(@)\n        spyOn(FileDownloadStore, '_prepareFolder').andCallFake -> Promise.resolve(true)\n\n      it \"should make sure that the download file path exists\", ->\n        waitsForPromise =>\n          FileDownloadStore._runDownload(@testfile).then ->\n            expect(FileDownloadStore._prepareFolder).toHaveBeenCalled()\n\n      it \"should return the promise returned by download.run if the download already exists\", ->\n        existing =\n          fileId: @testfile.id\n          run: jasmine.createSpy('existing.run').andCallFake ->\n            Promise.resolve(existing)\n        FileDownloadStore._downloads[@testfile.id] = existing\n\n        promise = FileDownloadStore._runDownload(@testfile)\n        expect(promise instanceof Promise).toBe(true)\n        waitsForPromise ->\n          promise.then ->\n            expect(existing.run).toHaveBeenCalled()\n\n      describe \"when the downloaded file exists\", ->\n        beforeEach ->\n          spyOn(FileDownloadStore, '_checkForDownloadedFile').andCallFake ->\n            Promise.resolve(true)\n\n        it \"should resolve with a Download without calling download.run\", ->\n          waitsForPromise =>\n            FileDownloadStore._runDownload(@testfile).then (download) ->\n              expect(Download.prototype.run).not.toHaveBeenCalled()\n              expect(download instanceof Download).toBe(true)\n              expect(download.data()).toEqual({\n                state : 'finished',\n                fileId : 'id',\n                percent : 0,\n                filename : '123.png',\n                filesize : 100,\n                targetPath : '/Users/testuser/.nylas-mail/downloads/id/123.png'\n              })\n\n      describe \"when the downloaded file does not exist\", ->\n        beforeEach ->\n          spyOn(FileDownloadStore, '_checkForDownloadedFile').andCallFake ->\n            Promise.resolve(false)\n\n        it \"should register the download with the right attributes\", ->\n          FileDownloadStore._runDownload(@testfile)\n          advanceClock(0)\n          expect(FileDownloadStore.getDownloadDataForFile(@testfile.id)).toEqual({\n            state : 'unstarted',fileId : 'id',\n            percent : 0,\n            filename : '123.png',\n            filesize : 100,\n            targetPath : '/Users/testuser/.nylas-mail/downloads/id/123.png'\n          })\n\n        it \"should call download.run\", ->\n          waitsForPromise =>\n            FileDownloadStore._runDownload(@testfile)\n          runs ->\n            expect(Download.prototype.run).toHaveBeenCalled()\n\n        it \"should resolve with a Download\", ->\n          waitsForPromise =>\n            FileDownloadStore._runDownload(@testfile).then (download) ->\n              expect(download instanceof Download).toBe(true)\n              expect(download.data()).toEqual({\n                state : 'unstarted',\n                fileId : 'id',\n                percent : 0,\n                filename : '123.png',\n                filesize : 100,\n                targetPath : '/Users/testuser/.nylas-mail/downloads/id/123.png'\n              })\n\n    describe \"_fetch\", ->\n      it \"should call through to startDownload\", ->\n        spyOn(FileDownloadStore, '_runDownload').andCallFake ->\n          Promise.resolve(@testdownload)\n        FileDownloadStore._fetch(@testfile)\n        expect(FileDownloadStore._runDownload).toHaveBeenCalled()\n\n      it \"should fail silently since it's called passively\", ->\n        spyOn(FileDownloadStore, '_presentError')\n        spyOn(FileDownloadStore, '_runDownload').andCallFake =>\n          Promise.reject(@testdownload)\n        FileDownloadStore._fetch(@testfile)\n        expect(FileDownloadStore._presentError).not.toHaveBeenCalled()\n\n    describe \"_fetchAndOpen\", ->\n      it \"should open the file once it's been downloaded\", ->\n        @savePath = \"/Users/imaginary/.nylas-mail/Downloads/a.png\"\n        download = {targetPath: @savePath}\n        downloadResolve = null\n\n        spyOn(FileDownloadStore, '_runDownload').andCallFake =>\n          new Promise (resolve, reject) ->\n            downloadResolve = resolve\n\n        FileDownloadStore._fetchAndOpen(@testfile)\n        expect(shell.openItem).not.toHaveBeenCalled()\n        downloadResolve(download)\n        advanceClock(100)\n        expect(shell.openItem).toHaveBeenCalledWith(@savePath)\n\n      it \"should open an error if the download fails\", ->\n        spyOn(FileDownloadStore, '_presentError')\n        spyOn(FileDownloadStore, '_runDownload').andCallFake =>\n          Promise.reject(@testdownload)\n        FileDownloadStore._fetchAndOpen(@testfile)\n        advanceClock(1)\n        expect(FileDownloadStore._presentError).toHaveBeenCalled()\n\n    describe \"_fetchAndSave\", ->\n      beforeEach ->\n        @userSelectedPath = \"/Users/imaginary/.nylas-mail/Downloads/b.png\"\n        spyOn(NylasEnv, 'showSaveDialog').andCallFake (options, callback) => callback(@userSelectedPath)\n\n      it \"should open a save dialog and prompt the user to choose a download path\", ->\n        spyOn(FileDownloadStore, '_runDownload').andCallFake =>\n          new Promise (resolve, reject) -> # never resolve\n        FileDownloadStore._fetchAndSave(@testfile)\n        expect(NylasEnv.showSaveDialog).toHaveBeenCalled()\n        expect(FileDownloadStore._runDownload).toHaveBeenCalledWith(@testfile)\n\n      it \"should open an error if the download fails\", ->\n        spyOn(FileDownloadStore, '_presentError')\n        spyOn(FileDownloadStore, '_runDownload').andCallFake =>\n          Promise.reject(@testdownload)\n        FileDownloadStore._fetchAndSave(@testfile)\n        advanceClock(1)\n        expect(FileDownloadStore._presentError).toHaveBeenCalled()\n\n      describe \"when the user confirms a path\", ->\n        beforeEach ->\n          @download = {targetPath: 'bla'}\n          @onEndEventCallback = null\n          streamStub =\n            pipe: ->\n            on: (eventName, eventCallback) =>\n              @onEndEventCallback = eventCallback\n\n          spyOn(FileDownloadStore, '_runDownload').andCallFake =>\n            Promise.resolve(@download)\n          spyOn(fs, 'createReadStream').andReturn(streamStub)\n          spyOn(fs, 'createWriteStream')\n\n        it \"should copy the file to the download path after it's been downloaded and open it after the stream has ended\", ->\n          FileDownloadStore._fetchAndSave(@testfile)\n          advanceClock(1)\n          expect(fs.createReadStream).toHaveBeenCalledWith(@download.targetPath)\n          expect(shell.showItemInFolder).not.toHaveBeenCalled()\n          @onEndEventCallback()\n          advanceClock(1)\n\n        it \"should show file in folder if download path differs from previous download path\", ->\n          spyOn(FileDownloadStore, '_saveDownload').andCallFake =>\n            Promise.resolve(@testfile)\n          NylasEnv.savedState.lastDownloadDirectory = null\n          @userSelectedPath = \"/Users/imaginary/.nylas-mail/Another Random Folder/file.jpg\"\n          FileDownloadStore._fetchAndSave(@testfile)\n          advanceClock(1)\n          expect(shell.showItemInFolder).toHaveBeenCalledWith(@userSelectedPath)\n\n        it \"should not show the file in the folder if the download path is the previous download path\", ->\n          spyOn(FileDownloadStore, '_saveDownload').andCallFake =>\n            Promise.resolve(@testfile)\n          @userSelectedPath = \"/Users/imaginary/.nylas-mail/Another Random Folder/123.png\"\n          NylasEnv.savedState.lastDownloadDirectory = \"/Users/imaginary/.nylas-mail/Another Random Folder\"\n          FileDownloadStore._fetchAndSave(@testfile)\n          advanceClock(1)\n          expect(shell.showItemInFolder).not.toHaveBeenCalled()\n\n        it \"should update the NylasEnv.savedState.lastDownloadDirectory if is has changed\", ->\n          spyOn(FileDownloadStore, '_saveDownload').andCallFake =>\n            Promise.resolve(@testfile)\n          NylasEnv.savedState.lastDownloadDirectory = null\n          @userSelectedPath = \"/Users/imaginary/.nylas-mail/Another Random Folder/file.jpg\"\n          FileDownloadStore._fetchAndSave(@testfile)\n          advanceClock(1)\n          expect(NylasEnv.savedState.lastDownloadDirectory).toEqual('/Users/imaginary/.nylas-mail/Another Random Folder')\n\n        describe \"file extensions\", ->\n          it \"should allow the user to save the file with a different extension\", ->\n            @userSelectedPath = \"/Users/imaginary/.nylas-mail/Downloads/b-changed.tiff\"\n            FileDownloadStore._fetchAndSave(@testfile)\n            advanceClock(1)\n            expect(fs.createWriteStream).toHaveBeenCalledWith(@userSelectedPath)\n\n          it \"should restore the extension if the user removed it entirely, because it's usually an accident\", ->\n            @userSelectedPath = \"/Users/imaginary/.nylas-mail/Downloads/b-changed\"\n            FileDownloadStore._fetchAndSave(@testfile)\n            advanceClock(1)\n            expect(fs.createWriteStream).toHaveBeenCalledWith(\"#{@userSelectedPath}.png\")\n\n    describe \"_abortFetchFile\", ->\n      beforeEach ->\n        @download =\n          ensureClosed: jasmine.createSpy('abort')\n          fileId: @testfile.id\n        FileDownloadStore._downloads[@testfile.id] = @download\n\n      it \"should cancel the download for the provided file\", ->\n        spyOn(fs, 'exists').andCallFake (path, callback) -> callback(true)\n        spyOn(fs, 'unlink')\n        FileDownloadStore._abortFetchFile(@testfile)\n        expect(fs.unlink).toHaveBeenCalled()\n        expect(@download.ensureClosed).toHaveBeenCalled()\n\n      it \"should not try to delete the file if doesn't exist\", ->\n        spyOn(fs, 'exists').andCallFake (path, callback) -> callback(false)\n        spyOn(fs, 'unlink')\n        FileDownloadStore._abortFetchFile(@testfile)\n        expect(fs.unlink).not.toHaveBeenCalled()\n        expect(@download.ensureClosed).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/stores/file-upload-store-spec.coffee",
    "content": "fs = require 'fs'\n{Message,\n Actions,\n FileUploadStore,\n DraftStore} = require 'nylas-exports'\n{Upload} = FileUploadStore\n\nmsgId = \"local-123\"\nfpath = \"/foo/bar/test123.jpg\"\nfDir = \"/foo/bar\"\nuploadDir = \"/uploads\"\nfilename = \"test123.jpg\"\n\nxdescribe 'FileUploadStore', ->\n  beforeEach ->\n    @draft = new Message()\n    @session =\n      changes:\n        add: jasmine.createSpy('session.changes.add')\n        commit: ->\n      draft: => @draft\n    spyOn(NylasEnv, \"isMainWindow\").andReturn(true)\n    spyOn(DraftStore, \"sessionForClientId\").andReturn(Promise.resolve(@session))\n    spyOn(FileUploadStore, \"_onAttachFileError\").andCallFake (msg) ->\n      throw new Error(msg)\n    spyOn(NylasEnv, \"showOpenDialog\").andCallFake (props, callback) ->\n      callback(fpath)\n\n  describe 'selectAttachment', ->\n    it \"throws if no messageClientId is provided\", ->\n      expect( -> Actions.selectAttachment()).toThrow()\n\n    it \"throws if messageClientId is blank\", ->\n      expect( -> Actions.selectAttachment(\"\")).toThrow()\n\n    it \"dispatches action to attach file\", ->\n      spyOn(Actions, \"addAttachment\")\n\n      Actions.selectAttachment(messageClientId: msgId)\n      expect(NylasEnv.showOpenDialog).toHaveBeenCalled()\n      expect(Actions.addAttachment).toHaveBeenCalled()\n      args = Actions.addAttachment.calls[0].args[0]\n      expect(args.messageClientId).toBe(msgId)\n      expect(args.filePath).toBe(fpath)\n\n\n  describe 'addAttachment', ->\n    beforeEach ->\n      @stats =  {\n        size: 1234,\n        isDirectory: -> false,\n      }\n      @upload = new Upload({\n        messageClientId: msgId,\n        filePath: fpath,\n        stats: @stats,\n        id: 'u1',\n        uploadDir: uploadDir\n      })\n      spyOn(FileUploadStore, '_getFileStats').andCallFake => Promise.resolve(@stats)\n      spyOn(FileUploadStore, '_prepareTargetDir').andCallFake => Promise.resolve()\n      spyOn(FileUploadStore, '_copyUpload').andCallFake => Promise.resolve(@upload)\n      spyOn(FileUploadStore, '_applySessionChanges').andCallThrough()\n\n    it \"throws if no messageClientId or path is provided\", ->\n      expect(-> Actions.addAttachment()).toThrow()\n\n    it 'throws if upload is a directory', ->\n      @stats = {\n        isDirectory: -> true\n      }\n      waitsForPromise ->\n        FileUploadStore._onAddAttachment({messageClientId: msgId, filePath: fpath})\n        .then ->\n          throw new Error('Expected test to land in catch.')\n        .catch (error) ->\n          expect(error.message.indexOf(filename + ' is a directory')).not.toBe(-1)\n\n    it 'throws if the file is more than 25MB', ->\n      @stats = {\n        size: 25*1000000+1,\n        isDirectory: -> false,\n      }\n      waitsForPromise ->\n        FileUploadStore._onAddAttachment({messageClientId: msgId, filePath: fpath})\n        .then ->\n          throw new Error('Expected test to land in catch.')\n        .catch (error) ->\n          expect(error.message.indexOf(filename + ' cannot')).not.toBe(-1)\n\n    it \"executes the required steps and triggers\", ->\n      waitsForPromise ->\n        FileUploadStore._onAddAttachment({messageClientId: msgId, filePath: fpath})\n\n      runs =>\n        expect(FileUploadStore._getFileStats).toHaveBeenCalled()\n        expect(FileUploadStore._prepareTargetDir).toHaveBeenCalled()\n        expect(FileUploadStore._copyUpload).toHaveBeenCalled()\n        expect(FileUploadStore._applySessionChanges).toHaveBeenCalled()\n        expect(@session.changes.add).toHaveBeenCalledWith({uploads: [@upload]})\n\n\n  describe 'removeAttachment', ->\n    beforeEach ->\n      @upload = new Upload({\n        messageClientId: msgId,\n        filePath: fpath,\n        stats: {\n          size: 1234,\n          isDirectory: -> false\n        },\n        id: 'u1',\n        uploadDir: uploadDir\n      })\n      spyOn(FileUploadStore, '_deleteUpload').andCallFake => Promise.resolve(@upload)\n      spyOn(fs, 'rmdir')\n\n    it 'removes the upload from the draft', ->\n      @draft.uploads = [{id: 'u2'}, @upload]\n      waitsForPromise =>\n        FileUploadStore._onRemoveAttachment(@upload)\n        .then =>\n          expect(@session.changes.add).toHaveBeenCalledWith uploads: [{id: 'u2'}]\n          expect(fs.rmdir).not.toHaveBeenCalled()\n\n    it 'calls deleteUpload to clean up the filesystem', ->\n      @draft.uploads = [@upload]\n      waitsForPromise =>\n        FileUploadStore._onRemoveAttachment(@upload)\n        .then =>\n          expect(FileUploadStore._deleteUpload).toHaveBeenCalled()\n\n  describe \"when a draft is sent\", ->\n    it \"should delete its uploads directory\", ->\n      spyOn(FileUploadStore, '_deleteUploadsForClientId')\n      Actions.ensureMessageInSentSuccess({messageClientId: '123'})\n      expect(FileUploadStore._deleteUploadsForClientId).toHaveBeenCalledWith('123')\n\n  describe '_getFileStats', ->\n    it 'returns the correct stats', ->\n      spyOn(fs, 'stat').andCallFake (path, callback) ->\n        callback(null, {size: 1234, isDirectory: -> false})\n      waitsForPromise ->\n        FileUploadStore._getFileStats(fpath)\n        .then (stats) ->\n          expect(stats.size).toEqual 1234\n          expect(stats.isDirectory()).toBe false\n\n    it 'throws when there is an error reading the file', ->\n      spyOn(fs, 'stat').andCallFake (path, callback) ->\n        callback(\"Error!\", null)\n      waitsForPromise ->\n        FileUploadStore._getFileStats(fpath)\n        .then -> throw new Error('It should fail.')\n        .catch (error) ->\n          expect(error.message.indexOf(fpath)).toBe 0\n\n\n  describe '_copyUpload', ->\n    beforeEach ->\n      stream = require 'stream'\n      @upload = new Upload({\n        messageClientId: msgId,\n        filePath: fpath,\n        stats: {\n          size: 1234,\n          isDirectory: -> false\n        },\n        id: null,\n        uploadDir: uploadDir\n      })\n      @readStream = stream.Readable()\n      @writeStream = stream.Writable()\n      spyOn(@readStream, 'pipe')\n      spyOn(fs, 'createReadStream').andReturn @readStream\n      spyOn(fs, 'createWriteStream').andReturn @writeStream\n\n    it 'copies the file correctly', ->\n      waitsForPromise =>\n        promise = FileUploadStore._copyUpload(@upload)\n        @readStream.emit 'end'\n        promise.then (up) =>\n          expect(fs.createReadStream).toHaveBeenCalledWith(fpath)\n          expect(fs.createWriteStream).toHaveBeenCalledWith(@upload.targetPath)\n          expect(@readStream.pipe).toHaveBeenCalledWith(@writeStream)\n          expect(up.id).toEqual @upload.id\n\n    it 'throws when there is an error on the read stream', ->\n      waitsForPromise =>\n        promise = FileUploadStore._copyUpload(@upload)\n        @readStream.emit 'error'\n        promise\n        .then => throw new Error('It should fail.')\n        .catch (msg) =>\n          expect(msg).not.toBeUndefined()\n\n    it 'throws when there is an error on the write stream', ->\n      waitsForPromise =>\n        promise = FileUploadStore._copyUpload(@upload)\n        @writeStream.emit 'error'\n        promise\n        .then => throw new Error('It should fail.')\n        .catch (msg) =>\n          expect(msg).not.toBeUndefined()\n"
  },
  {
    "path": "packages/client-app/spec/stores/focused-contacts-store-spec.coffee",
    "content": "proxyquire = require 'proxyquire'\nReflux = require 'reflux'\n\nFocusedContactsStore = require '../../src/flux/stores/focused-contacts-store'\n\nxdescribe \"FocusedContactsStore\", ->\n  beforeEach ->\n    FocusedContactsStore._currentThreadId = null\n    FocusedContactsStore._clearCurrentParticipants(silent: true)\n\n  it \"returns no contacts with empty\", ->\n    expect(FocusedContactsStore.sortedContacts()).toEqual []\n\n  it \"returns no focused contact when empty\", ->\n    expect(FocusedContactsStore.focusedContact()).toBeNull()\n"
  },
  {
    "path": "packages/client-app/spec/stores/focused-content-store-spec.coffee",
    "content": "_ = require 'underscore'\nThread = require('../../src/flux/models/thread').default\nFocusedContentStore = require '../../src/flux/stores/focused-content-store'\nActions = require('../../src/flux/actions').default\n\ntestThread = new Thread(id: '123', accountId: TEST_ACCOUNT_ID)\n\ndescribe \"FocusedContentStore\", ->\n  describe \"onSetFocus\", ->\n    it \"should not trigger if the thread is already focused\", ->\n      FocusedContentStore._onFocus({collection: 'thread', item: testThread})\n      spyOn(FocusedContentStore, 'triggerAfterAnimationFrame')\n      FocusedContentStore._onFocus({collection: 'thread', item: testThread})\n      expect(FocusedContentStore.triggerAfterAnimationFrame).not.toHaveBeenCalled()\n\n    it \"should not trigger if the focus is already null\", ->\n      FocusedContentStore._onFocus({collection: 'thread', item: null})\n      spyOn(FocusedContentStore, 'triggerAfterAnimationFrame')\n      FocusedContentStore._onFocus({collection: 'thread', item: null})\n      expect(FocusedContentStore.triggerAfterAnimationFrame).not.toHaveBeenCalled()\n\n    it \"should trigger otherwise\", ->\n      FocusedContentStore._onFocus({collection: 'thread', item: null})\n      spyOn(FocusedContentStore, 'triggerAfterAnimationFrame')\n      FocusedContentStore._onFocus({collection: 'thread', item: testThread})\n      expect(FocusedContentStore.triggerAfterAnimationFrame).toHaveBeenCalled()\n\n  describe \"threadId\", ->\n    it \"should return the id of the focused thread\", ->\n      FocusedContentStore._onFocus({collection: 'thread', item: testThread})\n      expect(FocusedContentStore.focusedId('thread')).toBe(testThread.id)\n\n  describe \"thread\", ->\n    it \"should return the focused thread object\", ->\n      FocusedContentStore._onFocus({collection: 'thread', item: testThread})\n      expect(FocusedContentStore.focused('thread')).toBe(testThread)\n"
  },
  {
    "path": "packages/client-app/spec/stores/focused-perspective-store-spec.coffee",
    "content": "_ = require 'underscore'\n\nActions = require('../../src/flux/actions').default\nCategory = require('../../src/flux/models/category').default\nMailboxPerspective = require '../../src/mailbox-perspective'\n\nCategoryStore = require '../../src/flux/stores/category-store'\nAccountStore = require('../../src/flux/stores/account-store').default\nFocusedPerspectiveStore = require('../../src/flux/stores/focused-perspective-store').default\n\ndescribe \"FocusedPerspectiveStore\", ->\n  beforeEach ->\n    spyOn(FocusedPerspectiveStore, 'trigger')\n    FocusedPerspectiveStore._current = MailboxPerspective.forNothing()\n    @account = AccountStore.accounts()[0]\n\n    @inboxCategory = new Category(id: 'id-123', name: 'inbox', displayName: \"INBOX\", accountId: @account.id)\n    @inboxPerspective = MailboxPerspective.forCategory(@inboxCategory)\n    @userCategory = new Category(id: 'id-456', name: null, displayName: \"MyCategory\", accountId: @account.id)\n    @userPerspective = MailboxPerspective.forCategory(@userCategory)\n\n    spyOn(CategoryStore, \"getStandardCategory\").andReturn @inboxCategory\n    spyOn(CategoryStore, \"byId\").andCallFake (aid, cid) =>\n      return {id: 'A'} if aid is 1 and cid is 'A'\n      return @inboxCategory if cid is @inboxCategory.id\n      return @userCategory if cid is @userCategory.id\n      return null\n\n  describe \"_initializeFromSavedState\", ->\n    beforeEach ->\n      @default = MailboxPerspective.forCategory(@inboxCategory)\n      spyOn(AccountStore, 'accountIds').andReturn([1, 2])\n      spyOn(MailboxPerspective, 'fromJSON').andCallFake (json) -> json\n      spyOn(FocusedPerspectiveStore, '_defaultPerspective').andReturn @default\n      spyOn(FocusedPerspectiveStore, '_setPerspective')\n\n    it \"uses default perspective when no perspective has been saved\", ->\n      NylasEnv.savedState.sidebarAccountIds = undefined\n      NylasEnv.savedState.perspective = undefined\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(@default, @default.accountIds)\n\n    it \"uses default if the saved perspective has account ids no longer present\", ->\n      NylasEnv.savedState.sidebarAccountIds = [1, 2, 3]\n      NylasEnv.savedState.perspective =\n        accountIds: [1, 2, 3],\n        categories: => [{accountId: 1, id: 'A'}],\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(@default, @default.accountIds)\n\n      NylasEnv.savedState.sidebarAccountIds = [1, 2, 3]\n      NylasEnv.savedState.perspective =\n        accountIds: [3]\n        categories: => [{accountId: 3, id: 'A'}]\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(@default, @default.accountIds)\n\n    it \"uses default if the saved perspective has category ids no longer present\", ->\n      NylasEnv.savedState.sidebarAccountIds = [2]\n      NylasEnv.savedState.perspective =\n        accountIds: [2]\n        categories: => [{accountId: 2, id: 'C'}]\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(@default, @default.accountIds)\n\n    it \"does not honor sidebarAccountIds if it includes account ids no longer present\", ->\n      NylasEnv.savedState.sidebarAccountIds = [1, 2, 3]\n      NylasEnv.savedState.perspective =\n        accountIds: [1]\n        categories: => [{accountId: 1, id: 'A'}]\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(NylasEnv.savedState.perspective, [1])\n\n    it \"uses the saved perspective if it is still valid\", ->\n      NylasEnv.savedState.sidebarAccountIds = [1, 2]\n      NylasEnv.savedState.perspective =\n        accountIds: [1, 2]\n        categories: => [{accountId: 1, id: 'A'}]\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(NylasEnv.savedState.perspective, [1, 2])\n\n      NylasEnv.savedState.sidebarAccountIds = [1, 2]\n      NylasEnv.savedState.perspective =\n        accountIds: [1]\n        categories: => []\n        type: 'DraftsMailboxPerspective'\n\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(NylasEnv.savedState.perspective, [1, 2])\n\n      NylasEnv.savedState.sidebarAccountIds = [1]\n      NylasEnv.savedState.perspective =\n        accountIds: [1]\n        categories: => []\n        type: 'DraftsMailboxPerspective'\n\n      FocusedPerspectiveStore._initializeFromSavedState()\n      expect(FocusedPerspectiveStore._setPerspective).toHaveBeenCalledWith(NylasEnv.savedState.perspective, [1])\n\n  describe \"_onCategoryStoreChanged\", ->\n    it \"should try to initialize if the curernt perspective hasn't been fully initialized\", ->\n      spyOn(FocusedPerspectiveStore, '_initializeFromSavedState')\n\n      FocusedPerspectiveStore._current = @inboxPerspective\n      FocusedPerspectiveStore._initialized = true\n      FocusedPerspectiveStore._onCategoryStoreChanged()\n      expect(FocusedPerspectiveStore._initializeFromSavedState).not.toHaveBeenCalled()\n\n      FocusedPerspectiveStore._current = MailboxPerspective.forNothing()\n      FocusedPerspectiveStore._initialized = false\n      FocusedPerspectiveStore._onCategoryStoreChanged()\n      expect(FocusedPerspectiveStore._initializeFromSavedState).toHaveBeenCalled()\n\n    it \"should set the current category to default when the current category no longer exists in the CategoryStore\", ->\n      defaultPerspective = @inboxPerspective\n      FocusedPerspectiveStore._initialized = true\n      spyOn(FocusedPerspectiveStore, '_defaultPerspective').andReturn(defaultPerspective)\n\n      otherAccountInbox = @inboxCategory.clone()\n      otherAccountInbox.serverId = 'other-id'\n      FocusedPerspectiveStore._current = MailboxPerspective.forCategory(otherAccountInbox)\n\n      FocusedPerspectiveStore._onCategoryStoreChanged()\n      expect(FocusedPerspectiveStore.current()).toEqual(defaultPerspective)\n\n  describe \"_onFocusPerspective\", ->\n    it \"should focus the category and trigger\", ->\n      FocusedPerspectiveStore._onFocusPerspective(@userPerspective)\n      expect(FocusedPerspectiveStore.trigger).toHaveBeenCalled()\n      expect(FocusedPerspectiveStore.current().categories()).toEqual([@userCategory])\n\n  describe \"_setPerspective\", ->\n    it \"should not trigger if the perspective is already focused\", ->\n      FocusedPerspectiveStore._setPerspective(@inboxPerspective)\n      FocusedPerspectiveStore.trigger.reset()\n      FocusedPerspectiveStore._setPerspective(@inboxPerspective)\n      expect(FocusedPerspectiveStore.trigger).not.toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/stores/folder-sync-progress-store-spec.es6",
    "content": "import {FolderSyncProgressStore} from 'nylas-exports'\n\nconst store = FolderSyncProgressStore\n\nxdescribe('FolderSyncProgressStore', function nylasSyncStatusStore() {\n  beforeEach(() => {\n    store._statesByAccount = {}\n  });\n\n  describe('isSyncCompleteForAccount', () => {\n    describe('when model (collection) provided', () => {\n      it('returns true if syncing for the given model and account is complete', () => {\n        store._statesByAccount = {\n          a1: {\n            labels: {complete: true},\n          },\n        }\n        expect(store.isSyncCompleteForAccount('a1', 'labels')).toBe(true)\n      });\n\n      it('returns false otherwise', () => {\n        const states = [\n          { a1: { labels: {complete: false} } },\n          { a1: {} },\n          {},\n        ]\n        states.forEach((state) => {\n          store._statesByAccount = state\n          expect(store.isSyncCompleteForAccount('a1', 'labels')).toBe(false)\n        })\n      });\n    });\n\n    describe('when model not provided', () => {\n      it('returns true if sync is complete for all models for the given account', () => {\n        store._statesByAccount = {\n          a1: {\n            labels: {complete: true},\n            threads: {complete: true},\n          },\n        }\n        expect(store.isSyncCompleteForAccount('a1')).toBe(true)\n      });\n\n      it('returns false otherwise', () => {\n        store._statesByAccount = {\n          a1: {\n            labels: {complete: true},\n            threads: {complete: false},\n          },\n        }\n        expect(store.isSyncCompleteForAccount('a1')).toBe(false)\n      });\n    });\n  });\n\n  describe('isSyncComplete', () => {\n    it('returns true if sync is complete for all accounts', () => {\n      spyOn(store, 'isSyncCompleteForAccount').andReturn(true)\n      store._statesByAccount = {\n        a1: {},\n        a2: {},\n      }\n      expect(store.isSyncComplete('a1')).toBe(true)\n    });\n\n    it('returns false otherwise', () => {\n      spyOn(store, 'isSyncCompleteForAccount').andCallFake(acctId => acctId === 'a1')\n      store._statesByAccount = {\n        a1: {},\n        a2: {},\n      }\n      expect(store.isSyncComplete('a1')).toBe(false)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/identity-store-spec.es6",
    "content": "import {ipcRenderer} from 'electron';\nimport {Utils, KeyManager, DatabaseWriter, SendFeatureUsageEventTask} from 'nylas-exports'\nimport IdentityStore from '../../src/flux/stores/identity-store'\n\nconst TEST_NYLAS_ID = \"icihsnqh4pwujyqihlrj70vh\"\nconst TEST_TOKEN = \"test-token\"\n\ndescribe(\"IdentityStore\", function identityStoreSpec() {\n  beforeEach(() => {\n    this.identityJSON = {\n      valid_until: 1500093224,\n      firstname: \"Nylas 050\",\n      lastname: \"Test\",\n      free_until: 1500006814,\n      email: \"nylas050test@evanmorikawa.com\",\n      id: TEST_NYLAS_ID,\n      seen_welcome_page: true,\n      feature_usage: {\n        feat: {\n          quota: 10,\n          used_in_period: 1,\n        },\n      },\n      token: \"secret token\",\n    }\n  });\n\n  describe(\"testing saveIdentity\", () => {\n    beforeEach(() => {\n      IdentityStore._identity = this.identityJSON;\n      spyOn(KeyManager, \"deletePassword\")\n      spyOn(KeyManager, \"replacePassword\")\n      spyOn(DatabaseWriter.prototype, \"persistJSONBlob\").andReturn(Promise.resolve())\n      spyOn(ipcRenderer, \"send\")\n      spyOn(IdentityStore, \"trigger\")\n    });\n\n    // TODO: this spec messes with the spec runner!! All specs after here don't\n    // run, and it causes the current run to immediately exit without an error\n    // code, even if there were tests that failed before this point.\n    xit(\"logs out of nylas identity properly\", async () => {\n      spyOn(NylasEnv.config, 'unset')\n      const promise = IdentityStore._onLogoutNylasIdentity()\n      IdentityStore._onIdentityChanged(null)\n      return promise.then(() => {\n        expect(KeyManager.deletePassword).toHaveBeenCalled()\n        expect(KeyManager.replacePassword).not.toHaveBeenCalled()\n        expect(ipcRenderer.send).toHaveBeenCalled()\n        expect(ipcRenderer.send.calls[0].args[1]).toBe(\"onIdentityChanged\")\n        expect(DatabaseWriter.prototype.persistJSONBlob).toHaveBeenCalled()\n        const ident = DatabaseWriter.prototype.persistJSONBlob.calls[0].args[1]\n        expect(ident).toBe(null)\n        expect(IdentityStore.trigger).toHaveBeenCalled()\n      })\n    });\n\n    it(\"makes the Identity synchronously available for fetching right after saving the identity\", async () => {\n      const used = () => {\n        return IdentityStore.identity().feature_usage.feat.used_in_period\n      }\n      expect(used()).toBe(1)\n      const t = new SendFeatureUsageEventTask('feat');\n      await t.performLocal()\n      expect(used()).toBe(2)\n      expect(ipcRenderer.send).not.toHaveBeenCalled()\n      expect(IdentityStore.trigger).toHaveBeenCalled()\n    });\n  });\n\n\n  it(\"can log a feature usage event\", async () => {\n    spyOn(IdentityStore, \"saveIdentity\").andReturn(Promise.resolve());\n    spyOn(IdentityStore, \"nylasIDRequest\");\n    IdentityStore._identity = this.identityJSON\n    IdentityStore._identity.token = TEST_TOKEN;\n    IdentityStore._onEnvChanged()\n    const t = new SendFeatureUsageEventTask(\"snooze\");\n    await t.performRemote()\n    const opts = IdentityStore.nylasIDRequest.calls[0].args[0]\n    expect(opts).toEqual({\n      method: \"POST\",\n      url: \"https://billing.nylas.com/n1/user/feature_usage_event\",\n      body: {\n        feature_name: 'snooze',\n      },\n    })\n  });\n\n  describe(\"returning the identity object\", () => {\n    beforeEach(() => {\n      spyOn(IdentityStore, \"saveIdentity\").andReturn(Promise.resolve());\n    });\n    it(\"returns the identity as null if it looks blank\", () => {\n      IdentityStore._identity = null;\n      expect(IdentityStore.identity()).toBe(null);\n      IdentityStore._identity = {};\n      expect(IdentityStore.identity()).toBe(null);\n      IdentityStore._identity = {token: 'bad'};\n      expect(IdentityStore.identity()).toBe(null);\n    });\n\n    it(\"returns a proper clone of the identity\", () => {\n      IdentityStore._identity = {id: 'bar', deep: {obj: 'baz'}};\n      const ident = IdentityStore.identity();\n      IdentityStore._identity.deep.obj = 'changed';\n      expect(ident.deep.obj).toBe('baz');\n    });\n  });\n\n  describe(\"_fetchIdentity\", () => {\n    beforeEach(() => {\n      IdentityStore._identity = this.identityJSON;\n      spyOn(IdentityStore, \"saveIdentity\")\n      spyOn(NylasEnv, \"reportError\")\n      spyOn(console, \"error\")\n    });\n\n    it(\"saves the identity returned\", async () => {\n      const resp = Utils.deepClone(this.identityJSON);\n      resp.feature_usage.feat.quota = 5\n      spyOn(IdentityStore, \"nylasIDRequest\").andCallFake(() => {\n        return Promise.resolve(resp)\n      })\n      await IdentityStore._fetchIdentity();\n      expect(IdentityStore.nylasIDRequest).toHaveBeenCalled();\n      const options = IdentityStore.nylasIDRequest.calls[0].args[0]\n      expect(options.url).toMatch(/\\/n1\\/user/)\n      expect(IdentityStore.saveIdentity).toHaveBeenCalled()\n      const newIdent = IdentityStore.saveIdentity.calls[0].args[0]\n      expect(newIdent.feature_usage.feat.quota).toBe(5)\n      expect(NylasEnv.reportError).not.toHaveBeenCalled()\n    });\n\n    it(\"errors if the json is invalid\", async () => {\n      spyOn(IdentityStore, \"nylasIDRequest\").andCallFake(() => {\n        return Promise.resolve({})\n      })\n      await IdentityStore._fetchIdentity();\n      expect(NylasEnv.reportError).toHaveBeenCalled()\n      expect(IdentityStore.saveIdentity).not.toHaveBeenCalled()\n    });\n\n    it(\"errors if the json doesn't match the ID\", async () => {\n      const resp = Utils.deepClone(this.identityJSON);\n      resp.id = \"THE WRONG ID\"\n      spyOn(IdentityStore, \"nylasIDRequest\").andCallFake(() => {\n        return Promise.resolve(resp)\n      })\n      await IdentityStore._fetchIdentity();\n      expect(NylasEnv.reportError).toHaveBeenCalled()\n      expect(IdentityStore.saveIdentity).not.toHaveBeenCalled()\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/message-store-spec.coffee",
    "content": "_ = require 'underscore'\nThread = require('../../src/flux/models/thread').default\nCategory = require('../../src/flux/models/category').default\nMessage = require('../../src/flux/models/message').default\nFocusedContentStore = require '../../src/flux/stores/focused-content-store'\nFocusedPerspectiveStore = require('../../src/flux/stores/focused-perspective-store').default\nMessageStore = require '../../src/flux/stores/message-store'\nDatabaseStore = require('../../src/flux/stores/database-store').default\nChangeUnreadTask = require('../../src/flux/tasks/change-unread-task').default\nActions = require('../../src/flux/actions').default\n\ntestThread = new Thread(id: '123', accountId: TEST_ACCOUNT_ID)\ntestMessage1 = new Message(id: 'a', body: '123', files: [], accountId: TEST_ACCOUNT_ID)\ntestMessage2 = new Message(id: 'b', body: '123', files: [], accountId: TEST_ACCOUNT_ID)\ntestMessage3 = new Message(id: 'c', body: '123', files: [], accountId: TEST_ACCOUNT_ID)\n\ndescribe \"MessageStore\", ->\n  describe \"when the receiving focus changes from the FocusedContentStore\", ->\n    beforeEach ->\n      if MessageStore._onFocusChangedTimer\n        clearTimeout(MessageStore._onFocusChangedTimer)\n        MessageStore._onFocusChangedTimer = null\n      spyOn(MessageStore, '_onApplyFocusChange')\n\n    afterEach ->\n      if MessageStore._onFocusChangedTimer\n        clearTimeout(MessageStore._onFocusChangedTimer)\n        MessageStore._onFocusChangedTimer = null\n\n    describe \"if no change has happened in the last 100ms\", ->\n      it \"should apply immediately\", ->\n        FocusedContentStore.trigger(impactsCollection: (c) -> true )\n        expect(MessageStore._onApplyFocusChange).toHaveBeenCalled()\n\n    describe \"if a change has happened in the last 100ms\", ->\n      it \"should not apply immediately\", ->\n        noop = =>\n        MessageStore._onFocusChangedTimer = setTimeout(noop, 100)\n        FocusedContentStore.trigger(impactsCollection: (c) -> true )\n        expect(MessageStore._onApplyFocusChange).not.toHaveBeenCalled()\n\n      it \"should apply 100ms after the last focus change and reset\", ->\n        FocusedContentStore.trigger(impactsCollection: (c) -> true )\n        expect(MessageStore._onApplyFocusChange.callCount).toBe(1)\n        advanceClock(50)\n        FocusedContentStore.trigger(impactsCollection: (c) -> true )\n        expect(MessageStore._onApplyFocusChange.callCount).toBe(1)\n        advanceClock(50)\n        FocusedContentStore.trigger(impactsCollection: (c) -> true )\n        expect(MessageStore._onApplyFocusChange.callCount).toBe(1)\n        advanceClock(150)\n        FocusedContentStore.trigger(impactsCollection: (c) -> true )\n        expect(MessageStore._onApplyFocusChange.callCount).toBe(3)\n        advanceClock(150)\n        FocusedContentStore.trigger(impactsCollection: (c) -> true )\n        expect(MessageStore._onApplyFocusChange.callCount).toBe(5)\n\n  describe \"items\", ->\n    beforeEach ->\n      MessageStore._showingHiddenItems = false\n      MessageStore._items = [\n        new Message(categories: [new Category(displayName: 'bla'), new Category(name: 'trash')]),\n        new Message(categories: [new Category(name: 'inbox')]),\n        new Message(categories: [new Category(name: 'bla'), new Category(name: 'spam')]),\n        new Message(categories: []),\n        new Message(categories: [], draft: true),\n      ]\n\n    describe \"when showing hidden items\", ->\n      it \"should return the entire items array\", ->\n        MessageStore._showingHiddenItems = true\n        expect(MessageStore.items().length).toBe(5)\n\n    describe \"when in trash or spam\", ->\n      it \"should show only the message which are in trash or spam, and drafts\", ->\n        spyOn(FocusedPerspectiveStore, 'current').andReturn({categoriesSharedName: => 'trash'})\n        expect(MessageStore.items()).toEqual([\n          MessageStore._items[0],\n          MessageStore._items[2],\n          MessageStore._items[4],\n        ])\n\n    describe \"when in another folder\", ->\n      it \"should hide all of the messages which are in trash or spam\", ->\n        spyOn(FocusedPerspectiveStore, 'current').andReturn({categoriesSharedName: => 'inbox'})\n        expect(MessageStore.items()).toEqual([\n          MessageStore._items[1],\n          MessageStore._items[3],\n          MessageStore._items[4],\n        ])\n\n  describe \"when applying focus changes\", ->\n    beforeEach ->\n      MessageStore._lastLoadedThreadId = null\n\n      @focus = null\n      spyOn(FocusedContentStore, 'focused').andCallFake (collection) =>\n        if collection is 'thread'\n          @focus\n        else\n          null\n\n      spyOn(FocusedContentStore, 'focusedId').andCallFake (collection) =>\n        if collection is 'thread'\n          @focus?.id\n        else\n          null\n\n      spyOn(DatabaseStore, 'findAll').andCallFake ->\n        include: -> @\n        where: -> @\n        then: (callback) -> callback([testMessage1, testMessage2])\n\n    it \"should retrieve the focused thread\", ->\n      @focus = testThread\n      MessageStore._thread = null\n      MessageStore._onApplyFocusChange()\n      expect(DatabaseStore.findAll).toHaveBeenCalled()\n      expect(DatabaseStore.findAll.mostRecentCall.args[0]).toBe(Message)\n\n    describe \"when the thread is already focused\", ->\n      it \"should do nothing\", ->\n        @focus = testThread\n        MessageStore._thread = @focus\n        MessageStore._onApplyFocusChange()\n        expect(DatabaseStore.findAll).not.toHaveBeenCalled()\n\n    describe \"when the thread is unread\", ->\n      beforeEach ->\n        @focus = null\n        MessageStore._onApplyFocusChange()\n        testThread.unread = true\n        spyOn(Actions, 'setUnreadThreads')\n        spyOn(NylasEnv.config, 'get').andCallFake (key) =>\n          if key is 'core.reading.markAsReadDelay'\n            return 600\n\n      it \"should queue a task to mark the thread as read\", ->\n        @focus = testThread\n        MessageStore._onApplyFocusChange()\n        advanceClock(500)\n        expect(Actions.setUnreadThreads).not.toHaveBeenCalled()\n        advanceClock(500)\n        expect(Actions.setUnreadThreads).toHaveBeenCalled()\n\n      it \"should not queue a task to mark the thread as read if the thread is no longer selected 500msec later\", ->\n        @focus = testThread\n        MessageStore._onApplyFocusChange()\n        advanceClock(500)\n        expect(Actions.setUnreadThreads).not.toHaveBeenCalled()\n        @focus = null\n        MessageStore._onApplyFocusChange()\n        advanceClock(500)\n        expect(Actions.setUnreadThreads).not.toHaveBeenCalled()\n\n      it \"should not re-mark the thread as read when made unread\", ->\n        @focus = testThread\n        testThread.unread = false\n        MessageStore._onApplyFocusChange()\n        advanceClock(500)\n        expect(Actions.setUnreadThreads).not.toHaveBeenCalled()\n\n        # This simulates a DB change or some attribute changing on the\n        # thread.\n        testThread.unread = true\n        MessageStore._fetchFromCache()\n        advanceClock(500)\n        expect(Actions.setUnreadThreads).not.toHaveBeenCalled()\n\n  describe \"when toggling expansion of all messages\", ->\n    beforeEach ->\n      MessageStore._items = [testMessage1, testMessage2, testMessage3]\n      spyOn(MessageStore, '_fetchExpandedAttachments')\n\n    it 'should expand all when at default state', ->\n      MessageStore._itemsExpanded = {c: 'default'}\n      Actions.toggleAllMessagesExpanded()\n      expect(MessageStore._itemsExpanded).toEqual a: 'explicit', b: 'explicit', c: 'explicit'\n\n    it 'should expand all when at least one item is collapsed', ->\n      MessageStore._itemsExpanded = {b: 'explicit', c: 'explicit'}\n      Actions.toggleAllMessagesExpanded()\n      expect(MessageStore._itemsExpanded).toEqual a: 'explicit', b: 'explicit', c: 'explicit'\n\n    it 'should collapse all except the latest message when all expanded', ->\n      MessageStore._itemsExpanded = {a: 'explicit', b: 'explicit', c: 'explicit'}\n      Actions.toggleAllMessagesExpanded()\n      expect(MessageStore._itemsExpanded).toEqual c: 'explicit'\n"
  },
  {
    "path": "packages/client-app/spec/stores/send-actions-store-spec.es6",
    "content": "import {Message, SendActionsStore, ExtensionRegistry} from 'nylas-exports'\n\n\nconst SendAction1 = {\n  title: \"Send Action 1\",\n  isAvailableForDraft: () => true,\n  performSendAction: () => {},\n}\n\nconst SendAction2 = {\n  title: \"Send Action 2\",\n  isAvailableForDraft: () => true,\n  performSendAction: () => {},\n}\n\nconst SendAction3 = {\n  title: \"Send Action 3\",\n  isAvailableForDraft: () => true,\n  performSendAction: () => {},\n}\n\nconst NoTitleAction = {\n  isAvailableForDraft: () => true,\n  performSendAction: () => {},\n}\n\nconst NoPerformAction = {\n  title: \"No Perform\",\n  isAvailableForDraft: () => true,\n}\n\nconst NotAvailableAction = {\n  title: \"Not Available\",\n  isAvailableForDraft: () => false,\n  performSendAction: () => {},\n}\n\nconst GoodExtension = {\n  name: 'GoodExtension',\n  sendActions() {\n    return [SendAction1]\n  },\n}\n\nconst BadExtension = {\n  name: 'BadExtension',\n  sendActions() {\n    return [null]\n  },\n}\n\nconst NoTitleExtension = {\n  name: 'NoTitleExtension',\n  sendActions() {\n    return [NoTitleAction]\n  },\n}\n\nconst NoPerformExtension = {\n  name: 'NoPerformExtension',\n  sendActions() {\n    return [NoPerformAction]\n  },\n}\n\nconst NotAvailableExtension = {\n  name: 'NotAvailableExtension',\n  sendActions() {\n    return [NotAvailableAction]\n  },\n}\n\nconst NullExtension = {\n  name: 'NullExtension',\n  sendActions() {\n    return null\n  },\n}\n\nconst OtherExtension = {\n  name: 'OtherExtension',\n  sendActions() {\n    return [SendAction2, SendAction3]\n  },\n}\n\nconst {DefaultSendActionKey} = SendActionsStore\n\nfunction sendActionKeys() {\n  return SendActionsStore.sendActions().map(({configKey}) => configKey)\n}\n\ndescribe('SendActionsStore', function describeBlock() {\n  beforeEach(() => {\n    this.clientId = \"client-23\"\n    this.draft = new Message({clientId: this.clientId, draft: true})\n    spyOn(NylasEnv, 'reportError')\n  });\n\n  describe('sendActions', () => {\n    it(\"returns default action when no extensions registered\", () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([]);\n      SendActionsStore._onComposerExtensionsChanged()\n      expect(sendActionKeys()).toEqual([DefaultSendActionKey])\n    });\n\n    it('returns correct send actions', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, OtherExtension]);\n      SendActionsStore._onComposerExtensionsChanged()\n      expect(sendActionKeys()).toEqual([DefaultSendActionKey, 'send-action-1', 'send-action-2', 'send-action-3'])\n    });\n\n    it('handles extensions that return null for `sendActions`', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, NullExtension]);\n      SendActionsStore._onComposerExtensionsChanged()\n      expect(sendActionKeys()).toEqual([DefaultSendActionKey, 'send-action-1'])\n    });\n\n    it('handles extensions that return null actions', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, BadExtension]);\n      SendActionsStore._onComposerExtensionsChanged()\n      expect(sendActionKeys()).toEqual([DefaultSendActionKey, 'send-action-1'])\n    });\n\n    it('omits and reports when action is missing a title', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, NoTitleExtension]);\n      SendActionsStore._onComposerExtensionsChanged()\n      expect(sendActionKeys()).toEqual([DefaultSendActionKey, 'send-action-1'])\n      expect(NylasEnv.reportError).toHaveBeenCalled()\n    });\n\n    it('omits reports when action is missing performSendAction', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, NoPerformExtension]);\n      SendActionsStore._onComposerExtensionsChanged()\n      expect(sendActionKeys()).toEqual([DefaultSendActionKey, 'send-action-1'])\n      expect(NylasEnv.reportError).toHaveBeenCalled()\n    });\n\n    it('includes not available actions', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, NotAvailableExtension]);\n      SendActionsStore._onComposerExtensionsChanged()\n      expect(sendActionKeys()).toEqual([DefaultSendActionKey, 'send-action-1', 'not-available'])\n    });\n  });\n\n  describe('availableSendActionsForDraft', () => {\n    it('excludes not available actions', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, NotAvailableExtension]);\n      SendActionsStore._onComposerExtensionsChanged()\n      const actions = SendActionsStore.availableSendActionsForDraft()\n      expect(actions.map(({configKey}) => configKey)).toEqual([DefaultSendActionKey, 'send-action-1'])\n    });\n  });\n\n  describe('orderedSendActionsForDraft', () => {\n    it(\"returns default action when no extensions registered\", () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([]);\n      SendActionsStore._onComposerExtensionsChanged()\n      const {preferred, rest} = SendActionsStore.orderedSendActionsForDraft()\n      expect(preferred.configKey).toBe(DefaultSendActionKey)\n      expect(rest).toEqual([])\n    });\n\n    it('returns actions in correct grouping', () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, OtherExtension, NotAvailableExtension]);\n      spyOn(NylasEnv.config, 'get').andReturn('send-action-1');\n      SendActionsStore._onComposerExtensionsChanged()\n      const {preferred, rest} = SendActionsStore.orderedSendActionsForDraft()\n      const restKeys = rest.map(({configKey}) => configKey)\n      expect(preferred.configKey).toBe('send-action-1')\n      expect(restKeys).toEqual([DefaultSendActionKey, 'send-action-2', 'send-action-3'])\n    });\n\n    it(\"falls back to a default if value in config not present\", () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, OtherExtension]);\n      spyOn(NylasEnv.config, 'get').andReturn(null);\n      SendActionsStore._onComposerExtensionsChanged()\n      const {preferred} = SendActionsStore.orderedSendActionsForDraft()\n      expect(preferred.configKey).toBe(DefaultSendActionKey)\n    });\n\n    it(\"falls back to a default if the primary item can't be found\", () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, OtherExtension]);\n      spyOn(NylasEnv.config, 'get').andReturn('does-not-exist');\n      SendActionsStore._onComposerExtensionsChanged()\n      const {preferred} = SendActionsStore.orderedSendActionsForDraft()\n      expect(preferred.configKey).toBe(DefaultSendActionKey)\n    });\n\n    it(\"falls back to a default if the primary item is not available for draft\", () => {\n      spyOn(ExtensionRegistry.Composer, 'extensions').andReturn([GoodExtension, NotAvailableExtension]);\n      spyOn(NylasEnv.config, 'get').andReturn('not-available');\n      SendActionsStore._onComposerExtensionsChanged()\n      const {preferred} = SendActionsStore.orderedSendActionsForDraft()\n      expect(preferred.configKey).toBe(DefaultSendActionKey)\n    });\n  });\n\n  // TODO Should go Task spec\n  it(\"catches any errors in an extension performSendAction method\", () => {\n\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/stores/task-queue-spec.coffee",
    "content": "Actions = require('../../src/flux/actions').default\nDatabaseStore = require('../../src/flux/stores/database-store').default\nTaskQueue = require '../../src/flux/stores/task-queue'\nTask = require('../../src/flux/tasks/task').default\nTaskRegistry = require('../../src/registries/task-registry').default\n\n{APIError} = require '../../src/flux/errors'\n\n{TaskSubclassA,\n TaskSubclassB,\n KillsTaskA,\n BlockedByTaskA,\n BlockingTask,\n TaskAA,\n TaskBB} = require('./task-subclass')\n\nxdescribe \"TaskQueue\", ->\n\n  makeUnstartedTask = (task) ->\n    task\n\n  makeProcessing = (task) ->\n    task.queueState.isProcessing = true\n    task\n\n  makeRetryInFuture = (task) ->\n    task.queueState.retryAfter = Date.now() + 1000\n    task.queueState.retryDelay = 1000\n    task\n\n  beforeEach ->\n    spyOn(TaskRegistry, 'isInRegistry').andReturn(true)\n    @task              = new Task()\n    @unstartedTask     = makeUnstartedTask(new Task())\n    @processingTask    = makeProcessing(new Task())\n    @retryInFutureTask = makeRetryInFuture(new Task())\n\n  afterEach ->\n    # Flush any throttled or debounced updates\n    advanceClock(1000)\n\n  describe \"restoreQueue\", ->\n    it \"should fetch the queue from the database, reset flags and start processing\", ->\n      queue = [@processingTask, @unstartedTask, @retryInFutureTask]\n      spyOn(DatabaseStore, 'findJSONBlob').andCallFake => Promise.resolve(queue)\n      spyOn(TaskQueue, '_updateSoon')\n\n      waitsForPromise =>\n        TaskQueue._restoreQueue().then =>\n          expect(TaskQueue._queue).toEqual(queue)\n          expect(@processingTask.queueState.isProcessing).toEqual(false)\n          expect(@retryInFutureTask.queueState.retryAfter).toEqual(undefined)\n          expect(@retryInFutureTask.queueState.retryDelay).toEqual(undefined)\n          expect(TaskQueue._updateSoon).toHaveBeenCalled()\n\n    it \"should remove any items in the queue which were not deserialized as tasks\", ->\n      queue = [@processingTask, {type: 'bla'}, @retryInFutureTask]\n      spyOn(DatabaseStore, 'findJSONBlob').andCallFake => Promise.resolve(queue)\n      spyOn(TaskQueue, '_updateSoon')\n      waitsForPromise =>\n        TaskQueue._restoreQueue().then =>\n          expect(TaskQueue._queue).toEqual([@processingTask, @retryInFutureTask])\n\n  describe \"findTask\", ->\n    beforeEach ->\n      @subclassA = new TaskSubclassA()\n      @subclassB1 = new TaskSubclassB(\"B1\")\n      @subclassB2 = new TaskSubclassB(\"B2\")\n      TaskQueue._queue = [@subclassA, @subclassB1, @subclassB2]\n\n    it \"accepts type as a string\", ->\n      expect(TaskQueue.findTask('TaskSubclassB', {bProp: 'B1'})).toEqual(@subclassB1)\n\n    it \"accepts type as a class\", ->\n      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B1'})).toEqual(@subclassB1)\n\n    it \"works without a set of match criteria\", ->\n      expect(TaskQueue.findTask(TaskSubclassA)).toEqual(@subclassA)\n\n    it \"only returns a task that matches the criteria\", ->\n      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B1'})).toEqual(@subclassB1)\n      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B2'})).toEqual(@subclassB2)\n      expect(TaskQueue.findTask(TaskSubclassB, {bProp: 'B3'})).toEqual(undefined)\n\n  describe \"enqueue\", ->\n    beforeEach ->\n      spyOn(@unstartedTask, 'runLocal').andCallFake =>\n        @unstartedTask.queueState.localComplete = true\n        Promise.resolve()\n\n    it \"makes sure you've queued a real task\", ->\n      expect( -> TaskQueue.enqueue(\"asamw\")).toThrow()\n\n    it \"adds it to the queue\", ->\n      spyOn(TaskQueue, '_processQueue').andCallFake ->\n      TaskQueue.enqueue(@unstartedTask)\n      advanceClock()\n      expect(TaskQueue._queue.length).toBe(1)\n\n    it \"immediately calls runLocal\", ->\n      TaskQueue.enqueue(@unstartedTask)\n      expect(@unstartedTask.runLocal).toHaveBeenCalled()\n\n    it \"notifies the queue should be processed\", ->\n      spyOn(TaskQueue, \"_processQueue\").andCallThrough()\n      spyOn(TaskQueue, \"_processTask\")\n\n      TaskQueue.enqueue(@unstartedTask)\n      advanceClock()\n      advanceClock()\n      expect(TaskQueue._processQueue).toHaveBeenCalled()\n      expect(TaskQueue._processTask).toHaveBeenCalledWith(@unstartedTask)\n      expect(TaskQueue._processTask.calls.length).toBe(1)\n\n    it \"throws an exception if the task does not have a queueState\", ->\n      task = new TaskSubclassA()\n      task.queueState = undefined\n      expect( => TaskQueue.enqueue(task)).toThrow()\n\n    it \"throws an exception if the task does not have an ID\", ->\n      task = new TaskSubclassA()\n      task.id = undefined\n      expect( => TaskQueue.enqueue(task)).toThrow()\n\n    it \"dequeues obsolete tasks\", ->\n      task = new TaskSubclassA()\n      spyOn(TaskQueue, '_dequeueObsoleteTasks').andCallFake ->\n      TaskQueue.enqueue(task)\n      expect(TaskQueue._dequeueObsoleteTasks).toHaveBeenCalled()\n\n  describe \"_dequeueObsoleteTasks\", ->\n    it \"should dequeue tasks based on `shouldDequeueOtherTask`\", ->\n      otherTask = new Task()\n      otherTask.queueState.localComplete = true\n      obsoleteTask = new TaskSubclassA()\n      obsoleteTask.queueState.localComplete = true\n      replacementTask = new KillsTaskA()\n      replacementTask.queueState.localComplete = true\n\n      spyOn(TaskQueue, 'dequeue').andCallThrough()\n      TaskQueue._queue = [obsoleteTask, otherTask]\n      TaskQueue._dequeueObsoleteTasks(replacementTask)\n      expect(TaskQueue._queue.length).toBe(1)\n      expect(obsoleteTask.queueState.status).toBe Task.Status.Continue\n      expect(obsoleteTask.queueState.debugStatus).toBe Task.DebugStatus.DequeuedObsolete\n      expect(TaskQueue.dequeue).toHaveBeenCalledWith(obsoleteTask)\n      expect(TaskQueue.dequeue.calls.length).toBe(1)\n\n  describe \"dequeue\", ->\n    beforeEach ->\n      TaskQueue._queue = [@unstartedTask, @processingTask]\n\n    it \"grabs the task by object\", ->\n      found = TaskQueue._resolveTaskArgument(@unstartedTask)\n      expect(found).toBe @unstartedTask\n\n    it \"grabs the task by id\", ->\n      found = TaskQueue._resolveTaskArgument(@unstartedTask.id)\n      expect(found).toBe @unstartedTask\n\n    it \"throws an error if the task isn't found\", ->\n      expect( -> TaskQueue.dequeue(\"bad\")).toThrow()\n\n    describe \"with an unstarted task\", ->\n      it \"moves it from the queue\", ->\n        TaskQueue.dequeue(@unstartedTask)\n        expect(TaskQueue._queue.length).toBe(1)\n        expect(TaskQueue._completed.length).toBe(1)\n\n      it \"notifies the queue has been updated\", ->\n        spyOn(TaskQueue, \"_processQueue\")\n        TaskQueue.dequeue(@unstartedTask)\n        advanceClock(20)\n        advanceClock()\n        expect(TaskQueue._processQueue).toHaveBeenCalled()\n        expect(TaskQueue._processQueue.calls.length).toBe(1)\n\n    describe \"with a processing task\", ->\n      it \"calls cancel() to allow the task to resolve or reject from runRemote()\", ->\n        spyOn(@processingTask, 'cancel')\n        TaskQueue.dequeue(@processingTask)\n        expect(@processingTask.cancel).toHaveBeenCalled()\n        expect(TaskQueue._queue.length).toBe(2)\n        expect(TaskQueue._completed.length).toBe(0)\n\n  describe \"_processQueue\", ->\n    it \"doesn't process blocked tasks\", ->\n      taskA = new TaskSubclassA()\n      otherTask = new Task()\n      blockedByTaskA = new BlockedByTaskA()\n\n      taskA.queueState.localComplete = true\n      otherTask.queueState.localComplete = true\n      blockedByTaskA.queueState.localComplete = true\n\n      spyOn(taskA, \"runRemote\").andCallFake -> new Promise (resolve, reject) ->\n      spyOn(blockedByTaskA, \"runRemote\").andCallFake -> Promise.resolve()\n\n      TaskQueue._queue = [taskA, otherTask, blockedByTaskA]\n      TaskQueue._processQueue()\n\n      advanceClock()\n\n      expect(TaskQueue._queue.length).toBe(2)\n      expect(taskA.runRemote).toHaveBeenCalled()\n      expect(blockedByTaskA.runRemote).not.toHaveBeenCalled()\n\n    it \"doesn't block itself, even if the isDependentOnTask method is implemented naively\", ->\n      blockedTask = new BlockingTask()\n      spyOn(blockedTask, \"runRemote\").andCallFake -> Promise.resolve()\n\n      TaskQueue.enqueue(blockedTask)\n      advanceClock()\n      blockedTask.runRemote.callCount > 0\n\n  describe \"_processTask\", ->\n    it \"doesn't process processing tasks\", ->\n      spyOn(@processingTask, \"runRemote\").andCallFake -> Promise.resolve()\n      TaskQueue._processTask(@processingTask)\n      expect(@processingTask.runRemote).not.toHaveBeenCalled()\n\n    it \"sets the processing bit\", ->\n      task = new Task()\n      task.queueState.localComplete = true\n      TaskQueue._queue = [task]\n      TaskQueue._processTask(task)\n      expect(task.queueState.isProcessing).toBe true\n\n    describe \"when the task returns Task.Status.Retry\", ->\n      beforeEach ->\n        @retryTaskWith = (qs) =>\n          task = new Task()\n          task.performRemote = =>\n            return Promise.resolve(Task.Status.Retry)\n          task.queueState.localComplete = true\n          task.queueState.retryDelay = qs.retryDelay\n          task.queueState.retryAfter = qs.retryAfter\n          return task\n\n      it \"sets retryAfter and retryDelay\", ->\n        task = @retryTaskWith({})\n        TaskQueue._queue = [task]\n        TaskQueue._processTask(task)\n        advanceClock()\n        expect(task.queueState.retryAfter).toBeDefined()\n        expect(task.queueState.retryDelay).toEqual(1000 * 2)\n\n      it \"increases retryDelay\", ->\n        task = @retryTaskWith({retryAfter: Date.now() - 1000, retryDelay: 2000})\n        TaskQueue._queue = [task]\n        TaskQueue._processTask(task)\n        advanceClock()\n        expect(task.queueState.retryAfter).toBeDefined()\n        expect(task.queueState.retryDelay).toEqual(2000 * 2)\n\n      it \"caps retryDelay\", ->\n        task = @retryTaskWith({retryAfter: Date.now() - 1000, retryDelay: 30000})\n        TaskQueue._queue = [task]\n        TaskQueue._processTask(task)\n        advanceClock()\n        expect(task.queueState.retryAfter).toBeDefined()\n        expect(task.queueState.retryDelay).toEqual(30000)\n\n      it \"calls updateSoon\", ->\n        task = @retryTaskWith({})\n        TaskQueue._queue = [task]\n        spyOn(TaskQueue, '_updateSoon')\n        TaskQueue._processTask(task)\n        advanceClock()\n        expect(TaskQueue._updateSoon).toHaveBeenCalled()\n\n  describe \"handling task runRemote task errors\", ->\n    spyBBRemote = jasmine.createSpy(\"performRemote\")\n\n    beforeEach ->\n      @taskAA = new TaskAA\n      @taskAA.queueState.localComplete = true\n      @taskBB = new TaskBB\n      @taskBB.queueState.localComplete = true\n\n      spyOn(TaskQueue, 'trigger')\n\n      # Don't keep processing the queue\n      spyOn(TaskQueue, '_updateSoon')\n\n    it \"catches the error and dequeues the task\", ->\n      spyOn(TaskQueue, 'dequeue')\n      waitsForPromise =>\n        TaskQueue._processTask(@taskAA).then =>\n          expect(TaskQueue.dequeue).toHaveBeenCalledWith(@taskAA)\n          expect(@taskAA.queueState.remoteError.message).toBe \"Test Error\"\n"
  },
  {
    "path": "packages/client-app/spec/stores/task-subclass.es6",
    "content": "import Task from '../../src/flux/tasks/task'\n\n// We need to subclass in ES6 since coffeescript can't subclass an ES6\n// object.\nexport class TaskSubclassA extends Task {\n  constructor(val) {\n    super(val);\n    this.aProp = val\n  }\n}\n\nexport class TaskSubclassB extends Task {\n  constructor(val) {\n    super(val);\n    this.bProp = val\n  }\n}\n\nexport class APITestTask extends Task {\n  performLocal() { return Promise.resolve() }\n  performRemote() { return Promise.resolve(Task.Status.Success) }\n}\n\nexport class KillsTaskA extends Task {\n  shouldDequeueOtherTask(other) { return other instanceof TaskSubclassA }\n  performRemote() { return new Promise(() => {}) }\n}\n\nexport class BlockedByTaskA extends Task {\n  isDependentOnTask(other) { return other instanceof TaskSubclassA }\n}\n\nexport class BlockingTask extends Task {\n  isDependentOnTask(other) { return other instanceof BlockingTask }\n}\n\nexport class TaskAA extends Task {\n  performRemote() {\n    const testError = new Error(\"Test Error\")\n    // We reject instead of `throw` because jasmine thinks this\n    // `throw` is in the context of the test instead of the context\n    // of the calling promise in task-queue.coffee\n    return Promise.reject(testError)\n  }\n}\n\nexport class TaskBB extends Task {\n  isDependentOnTask(other) { return other instanceof TaskAA }\n  performRemote = jasmine.createSpy(\"performRemote\")\n}\n\nexport class OKTask extends Task {\n  performRemote() { return Promise.resolve(Task.Status.Retry) }\n}\n\nexport class BadTask extends Task {\n  performRemote() { return Promise.resolve('lalal') }\n}\n"
  },
  {
    "path": "packages/client-app/spec/stores/undo-redo-store-spec.es6",
    "content": "import {Actions, Task, UndoRedoStore} from 'nylas-exports'\n\nclass Undoable extends Task {\n  canBeUndone() {\n    return true\n  }\n\n  createIdenticalTask() {\n    const t = new Undoable()\n    t.id = this.id\n    return t\n  }\n}\n\nclass PermanentTask extends Task {\n  canBeUndone() {\n    return false\n  }\n}\n\ndescribe(\"UndoRedoStore\", function undoRedoStoreSpec() {\n  beforeEach(() => {\n    UndoRedoStore._undo = []\n    UndoRedoStore._redo = []\n    spyOn(UndoRedoStore, \"trigger\")\n    spyOn(Actions, \"undoTaskId\")\n    spyOn(Actions, \"queueTask\").andCallFake((...args) => {\n      UndoRedoStore._onQueue(...args)\n    });\n    spyOn(Actions, \"queueTasks\").andCallFake((...args) => {\n      UndoRedoStore._onQueue(...args)\n    });\n\n    this.ids = (arrarr) => arrarr.map((arr) => arr.map((itm) => itm.id))\n    this.t1 = new Undoable();\n    this.t2 = new Undoable();\n    this.t3 = new Undoable();\n    this.t4 = new Undoable();\n    this.p1 = new PermanentTask();\n    this.t1.id = \"t1\"\n    this.t2.id = \"t2\"\n    this.t3.id = \"t3\"\n    this.t4.id = \"t4\"\n    this.p1.id = \"p1\"\n  });\n\n  it(\"pushes single tasks onto undo/redo\", () => {\n    Actions.queueTask(this.t1)\n    expect(UndoRedoStore._redo).toEqual([])\n    expect(UndoRedoStore._undo).toEqual([[this.t1]])\n    expect(UndoRedoStore.trigger).toHaveBeenCalled()\n  });\n\n  it(\"pushes multiple tasks onto redo\", () => {\n    Actions.queueTasks([this.t1, this.t2])\n    expect(UndoRedoStore._redo).toEqual([])\n    expect(UndoRedoStore._undo).toEqual([[this.t1, this.t2]])\n    expect(UndoRedoStore.trigger).toHaveBeenCalled()\n  });\n\n  it(\"only undoes task if they're all 'undoable'\", () => {\n    Actions.queueTask([this.t1, this.p1])\n    expect(UndoRedoStore._redo).toEqual([])\n    expect(UndoRedoStore._undo).toEqual([])\n    expect(UndoRedoStore.trigger).not.toHaveBeenCalled()\n  });\n\n  it(\"refreshes redo if we get a new task\", () => {\n    UndoRedoStore._redo = [[this.t1, this.t2], [this.t3]]\n    Actions.queueTask(this.t3)\n    expect(UndoRedoStore._redo).toEqual([])\n  });\n\n  it(\"doesn't refresh redo if our task is itself a redo task\", () => {\n    UndoRedoStore._redo = [[this.t1, this.t2], [this.t3]]\n    const tr = new Undoable()\n    tr.isRedoTask = true\n    Actions.queueTask(tr)\n    expect(UndoRedoStore._redo).toEqual([[this.t1, this.t2], [this.t3]])\n    expect(UndoRedoStore._undo).toEqual([[tr]])\n  });\n\n  it(\"runs undoTask on each group of undo tasks\", () => {\n    UndoRedoStore._undo = [[this.t3], [this.t1, this.t2]]\n    UndoRedoStore.undo()\n    expect(Actions.undoTaskId.calls.length).toBe(2)\n    expect(Actions.undoTaskId.calls[0].args[0]).toBe(\"t1\")\n    expect(Actions.undoTaskId.calls[1].args[0]).toBe(\"t2\")\n    expect(UndoRedoStore._undo).toEqual([[this.t3]])\n  });\n\n  it(\"creates identical redo tasks and pushes on the stack\", () => {\n    UndoRedoStore._undo = [[this.t3], [this.t1, this.t2]]\n    UndoRedoStore.undo()\n    expect(UndoRedoStore._undo).toEqual([[this.t3]])\n    expect(UndoRedoStore._redo[0][0].id).toBe(\"t1\")\n    expect(UndoRedoStore._redo[0][1].id).toBe(\"t2\")\n    expect(UndoRedoStore._redo.length).toBe(1)\n  });\n\n  it(\"redoes the latest task\", () => {\n    UndoRedoStore._undo = [[this.t3], [this.t1, this.t2]]\n    UndoRedoStore.undo()\n    UndoRedoStore.redo()\n    expect(Actions.queueTasks.calls[0].args[0][0].id).toBe('t1')\n    expect(Actions.queueTasks.calls[0].args[0][1].id).toBe('t2')\n    expect(UndoRedoStore._undo[0]).toEqual([this.t3])\n    expect(UndoRedoStore._undo[1][0].id).toBe('t1')\n    expect(UndoRedoStore._undo[1][1].id).toBe('t2')\n  });\n\n  it(\"marks the incoming task as a redo task\", () => {\n    UndoRedoStore._undo = [[this.t3], [this.t1, this.t2]]\n    UndoRedoStore.undo()\n    UndoRedoStore.redo()\n    expect(Actions.queueTasks.calls[0].args[0][0].isRedoTask).toBe(true)\n    expect(Actions.queueTasks.calls[0].args[0][1].isRedoTask).toBe(true)\n  });\n\n  it(\"correctly follows the undo redo sequence of events\", () => {\n    Actions.queueTask(this.t1)\n    Actions.queueTask(this.t2)\n    Actions.queueTasks([this.t3, this.t4])\n    expect(UndoRedoStore._undo).toEqual([[this.t1], [this.t2], [this.t3, this.t4]])\n    UndoRedoStore.undo()\n    UndoRedoStore.undo()\n    UndoRedoStore.undo()\n    UndoRedoStore.undo()\n    UndoRedoStore.undo()\n    expect(UndoRedoStore._undo).toEqual([])\n    expect(this.ids(UndoRedoStore._redo)).toEqual([[\"t3\", \"t4\"], [\"t2\"], [\"t1\"]])\n    UndoRedoStore.redo()\n    UndoRedoStore.redo()\n    UndoRedoStore.redo()\n    UndoRedoStore.redo()\n    UndoRedoStore.redo()\n    UndoRedoStore.redo()\n    expect(this.ids(UndoRedoStore._undo)).toEqual([[\"t1\"], [\"t2\"], [\"t3\", \"t4\"]])\n    expect(this.ids(UndoRedoStore._redo)).toEqual([])\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/tasks/base-draft-task-spec.es6",
    "content": "import {\n  Message,\n  DatabaseStore,\n} from 'nylas-exports';\n\nimport BaseDraftTask from '../../src/flux/tasks/base-draft-task';\n\nxdescribe('BaseDraftTask', function baseDraftTask() {\n  describe(\"shouldDequeueOtherTask\", () => {\n    it(\"should dequeue instances of the same subclass for the same draft which are older\", () => {\n      class ATask extends BaseDraftTask {\n\n      }\n      class BTask extends BaseDraftTask {\n\n      }\n\n      const A = new ATask('localid-A');\n      A.sequentialId = 1;\n      const B1 = new BTask('localid-A');\n      B1.sequentialId = 2;\n      const B2 = new BTask('localid-A');\n      B2.sequentialId = 3;\n      const BOther = new BTask('localid-other');\n      BOther.sequentialId = 4;\n\n      expect(B1.shouldDequeueOtherTask(A)).toBe(false);\n      expect(A.shouldDequeueOtherTask(B1)).toBe(false);\n\n      expect(B2.shouldDequeueOtherTask(B1)).toBe(true);\n      expect(B1.shouldDequeueOtherTask(B2)).toBe(false);\n\n      expect(BOther.shouldDequeueOtherTask(B2)).toBe(false);\n      expect(B2.shouldDequeueOtherTask(BOther)).toBe(false);\n    });\n  });\n\n  describe(\"isDependentOnTask\", () => {\n    it(\"should always wait on older tasks for the same draft\", () => {\n      const A = new BaseDraftTask('localid-A');\n      A.sequentialId = 1;\n      const B = new BaseDraftTask('localid-A');\n      B.sequentialId = 2;\n      expect(B.isDependentOnTask(A)).toBe(true);\n    });\n\n    it(\"should not wait on newer tasks for the same draft\", () => {\n      const A = new BaseDraftTask('localid-A');\n      A.sequentialId = 1;\n      const B = new BaseDraftTask('localid-A');\n      B.sequentialId = 2;\n      expect(A.isDependentOnTask(B)).toBe(false)\n    });\n\n    it(\"should not wait on older tasks for other drafts\", () => {\n      const A = new BaseDraftTask('localid-other');\n      A.sequentialId = 1;\n      const B = new BaseDraftTask('localid-A');\n      B.sequentialId = 2;\n      expect(A.isDependentOnTask(B)).toBe(false);\n      expect(B.isDependentOnTask(A)).toBe(false);\n    });\n  });\n\n  describe(\"performLocal\", () => {\n    it(\"rejects if we we don't pass a draft\", () => {\n      const badTask = new BaseDraftTask(null)\n      badTask.performLocal().then(() => {\n        throw new Error(\"Shouldn't succeed\")\n      }).catch((err) => {\n        expect(err.message).toBe(\"Attempt to call BaseDraftTask.performLocal without a draftClientId\")\n      });\n    });\n  });\n\n  describe(\"refreshDraftReference\", () => {\n    it(\"should retrieve the draft by client ID, with the body, and assign it to @draft\", () => {\n      const draft = new Message({draft: true});\n      const A = new BaseDraftTask('localid-other');\n      spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(draft));\n      waitsForPromise(() => {\n        return A.refreshDraftReference().then((resolvedValue) => {\n          expect(A.draft).toEqual(draft);\n          expect(resolvedValue).toEqual(draft);\n\n          const query = DatabaseStore.run.mostRecentCall.args[0];\n          expect(query.sql()).toEqual(\"SELECT `Message`.`data`, IFNULL(`MessageBody`.`value`, '!NULLVALUE!') AS `body`  FROM `Message` LEFT OUTER JOIN `MessageBody` ON `MessageBody`.`id` = `Message`.`id` WHERE `Message`.`client_id` = 'localid-other'  ORDER BY `Message`.`date` ASC LIMIT 1\");\n        });\n      });\n    });\n\n    it(\"should throw a DraftNotFoundError error if it the response was no longer a draft\", () => {\n      const message = new Message({draft: false});\n      const A = new BaseDraftTask('localid-other');\n      spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(message));\n      waitsForPromise(() => {\n        return A.refreshDraftReference().then(() => {\n          throw new Error(\"Should not have resolved\");\n        }).catch((err) => {\n          expect(err instanceof BaseDraftTask.DraftNotFoundError).toBe(true);\n        })\n      });\n    });\n\n    it(\"should throw a DraftNotFoundError error if nothing was returned\", () => {\n      const A = new BaseDraftTask('localid-other');\n      spyOn(DatabaseStore, 'run').andReturn(Promise.resolve(null));\n      waitsForPromise(() => {\n        return A.refreshDraftReference().then(() => {\n          throw new Error(\"Should not have resolved\");\n        }).catch((err) => {\n          expect(err instanceof BaseDraftTask.DraftNotFoundError).toBe(true);\n        })\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/tasks/change-folder-task-spec.coffee",
    "content": "_ = require 'underscore'\nFolder = require('../../src/flux/models/folder').default\nThread = require('../../src/flux/models/thread').default\nMessage = require('../../src/flux/models/message').default\nActions = require('../../src/flux/actions').default\nNylasAPI = require('../../src/flux/nylas-api').default\nQuery = require('../../src/flux/models/query').default\nDatabaseStore = require('../../src/flux/stores/database-store').default\nChangeFolderTask = require('../../src/flux/tasks/change-folder-task').default\nChangeMailTask = require('../../src/flux/tasks/change-mail-task').default\n\n{APIError} = require '../../src/flux/errors'\n{Utils} = require '../../src/flux/models/utils'\n\ntestFolders = {}\ntestThreads = {}\ntestMessages = {}\n\nxdescribe \"ChangeFolderTask\", ->\n  beforeEach ->\n    # IMPORTANT: These specs do not run the performLocal logic of their superclass!\n    # Tests for that logic are in change-mail-task-spec.\n    spyOn(ChangeMailTask.prototype, 'performLocal').andCallFake =>\n      Promise.resolve()\n\n    spyOn(DatabaseStore, 'modelify').andCallFake (klass, items) =>\n      Promise.resolve items.map (item) =>\n        return testFolders[item] if testFolders[item]\n        return testThreads[item] if testThreads[item]\n        return testMessages[item] if testMessages[item]\n        item\n\n    testFolders = @testFolders =\n      \"f1\": new Folder({name: 'inbox', id: 'f1', displayName: \"INBOX\"}),\n      \"f2\": new Folder({name: 'drafts', id: 'f2', displayName: \"MyDrafts\"})\n      \"f3\": new Folder({name: null, id: 'f3', displayName: \"My Folder\"})\n\n    testThreads = @testThreads =\n      't1': new Thread(id: 't1', categories: [@testFolders['f1']])\n      't2': new Thread(id: 't2', categories: _.values(@testFolders))\n      't3': new Thread(id: 't3', categories: [@testFolders['f2'], @testFolders['f3']])\n\n    testMessages = @testMessages =\n      'm1': new Message(id: 'm1', folder: @testFolders['f1'])\n      'm2': new Message(id: 'm2', folder: @testFolders['f2'])\n      'm3': new Message(id: 'm3', folder: @testFolders['f3'])\n\n  describe \"description\", ->\n    it \"should include the folder name if folder is a folder\", ->\n      taskWithFolderId = new ChangeFolderTask\n        folder: 'f2'\n        messages: ['m1']\n      expect(taskWithFolderId.description()).toEqual(\"Moved to folder\")\n      taskWithFolder = new ChangeFolderTask\n        folder: @testFolders['f2']\n        messages: ['m1']\n      expect(taskWithFolder.description()).toEqual(\"Moved to MyDrafts\")\n\n    it \"should correctly mention threads and messages\", ->\n      taskWithFolderId = new ChangeFolderTask\n        folder: 'f2'\n        threads: ['t1']\n      expect(taskWithFolderId.description()).toEqual(\"Moved to folder\")\n      taskWithFolder = new ChangeFolderTask\n        folder: @testFolders['f2']\n        messages: ['m1', 'm2']\n      expect(taskWithFolder.description()).toEqual(\"Moved 2 messages to MyDrafts\")\n\n  describe \"performLocal\", ->\n    it \"should check that a single folder is provided, and that we have threads or messages\", ->\n      badTasks = [\n        new ChangeFolderTask(),\n        new ChangeFolderTask(threads: [123]),\n        new ChangeFolderTask(threads: [123], messages: [\"foo\"]),\n        new ChangeFolderTask(threads: \"Thread\"),\n      ]\n      goodTasks = [\n        new ChangeFolderTask(\n          folder: 'f2'\n          threads: ['t1', 't2']\n        )\n        new ChangeFolderTask(\n          folder: @testFolders['f2']\n          messages: ['m1']\n        )\n      ]\n      caught = []\n      succeeded = []\n\n      runs ->\n        [].concat(badTasks, goodTasks).forEach (task) ->\n          task.performLocal()\n          .then -> succeeded.push(task)\n          .catch (err) -> caught.push(task)\n      waitsFor ->\n        succeeded.length + caught.length == 6\n      runs ->\n        expect(caught.length).toEqual(badTasks.length)\n        expect(succeeded.length).toEqual(goodTasks.length)\n\n    it 'calls through to super performLocal', ->\n      task = new ChangeFolderTask\n        folder: \"f1\"\n        threads: ['t1']\n      waitsForPromise =>\n        task.performLocal().then =>\n          expect(task.__proto__.__proto__.performLocal).toHaveBeenCalled()\n\n    describe \"retrieveModels\", ->\n      describe \"when object IDs are provided\", ->\n        beforeEach ->\n          @task = new ChangeFolderTask(folder: \"f1\", threads: ['t1'])\n\n        it 'resolves the objects before calling super', ->\n          waitsForPromise =>\n            @task.retrieveModels().then =>\n              expect(@task.folder).toEqual(testFolders['f1'])\n              expect(@task.threads).toEqual([testThreads['t1']])\n\n      describe \"when objects are provided\", ->\n        beforeEach ->\n          @task = new ChangeFolderTask(folder: testFolders['f1'], threads: [testThreads['t1'], testThreads['t2']])\n\n        it 'still has the objects when calling super', ->\n          waitsForPromise =>\n            @task.retrieveModels().then =>\n              expect(@task.folder).toEqual(testFolders['f1'])\n              expect(@task.threads).toEqual([testThreads['t1'],testThreads['t2']])\n\n    describe \"change methods\", ->\n      beforeEach ->\n        @message = testMessages['m1']\n        @thread = testThreads['t1']\n        @task = new ChangeFolderTask(folder: testFolders['f1'], threads: [testThreads['t1'], testThreads['t2']])\n\n      describe \"changesToModel\", ->\n        describe \"if the model is a Thread\", ->\n          it \"returns an object with a categories key, and an array with the folder\", ->\n            expect(@task.changesToModel(@thread)).toEqual({categories: [testFolders['f1']]})\n\n        describe \"if the model is a Message\", ->\n          it \"returns an object with a categories key, and the folder\", ->\n            expect(@task.changesToModel(@message)).toEqual({categories: [testFolders['f1']]})\n\n      describe \"requestBodyForModel\", ->\n        describe \"if the model is a Thread\", ->\n          it \"returns folder: <id>, using the first available folder\", ->\n            @thread.folders = []\n            expect(@task.requestBodyForModel(@thread)).toEqual(folder: null)\n            @thread.folders = [testFolders['f1']]\n            expect(@task.requestBodyForModel(@thread)).toEqual(folder: 'f1')\n            @thread.folders = [testFolders['f2'], testFolders['f1']]\n            expect(@task.requestBodyForModel(@thread)).toEqual(folder: 'f2')\n"
  },
  {
    "path": "packages/client-app/spec/tasks/change-labels-task-spec.coffee",
    "content": "_ = require 'underscore'\nLabel = require('../../src/flux/models/label').default\nThread = require('../../src/flux/models/thread').default\nMessage = require('../../src/flux/models/message').default\nActions = require('../../src/flux/actions').default\nNylasAPI = require('../../src/flux/nylas-api').default\nDatabaseStore = require('../../src/flux/stores/database-store').default\nChangeLabelsTask = require('../../src/flux/tasks/change-labels-task').default\nChangeMailTask = require('../../src/flux/tasks/change-mail-task').default\n\n{AccountStore, CategoryStore} = require 'nylas-exports'\n{APIError} = require '../../src/flux/errors'\n{Utils} = require '../../src/flux/models/utils'\n\ntestLabels = {}\ntestThreads = {}\n\nxdescribe \"ChangeLabelsTask\", ->\n  beforeEach ->\n    # IMPORTANT: These specs do not run the performLocal logic of their superclass!\n    # Tests for that logic are in change-mail-task-spec.\n    spyOn(ChangeMailTask.prototype, 'performLocal').andCallFake =>\n      Promise.resolve()\n\n    spyOn(AccountStore, 'accountForItems').andReturn({id: 'a1'})\n    spyOn(CategoryStore, 'getTrashCategory').andReturn name: 'trash'\n    spyOn(CategoryStore, 'getInboxCategory').andReturn name: 'inbox'\n    spyOn(CategoryStore, 'getSpamCategory').andReturn name: 'spam'\n    spyOn(CategoryStore, 'getAllMailCategory').andReturn name: 'all'\n\n    spyOn(DatabaseStore, 'modelify').andCallFake (klass, items) =>\n      Promise.resolve items.map (item) =>\n        return testLabels[item] if testLabels[item]\n        return testThreads[item] if testThreads[item]\n        item\n\n    testLabels = @testLabels =\n      \"l1\": new Label({name: 'inbox', id: 'l1', displayName: \"INBOX\"}),\n      \"l2\": new Label({name: 'drafts', id: 'l2', displayName: \"MyDrafts\"})\n      \"l3\": new Label({name: null, id: 'l3', displayName: \"My Label\"})\n\n    testThreads = @testThreads =\n      't1': new Thread(id: 't1', categories: [@testLabels['l1']])\n      't2': new Thread(id: 't2', categories: _.values(@testLabels))\n      't3': new Thread(id: 't3', categories: [@testLabels['l2'], @testLabels['l3']])\n\n    @basicThreadTask = new ChangeLabelsTask\n      labelsToAdd: [\"l1\", \"l2\"]\n      labelsToRemove: [\"l3\"]\n      threads: ['t1']\n\n  describe \"description\", ->\n    it \"should include the name of the added label if it's the only mutation and it was provided as an object\", ->\n      task = new ChangeLabelsTask(labelsToAdd: [\"l1\"], labelsToRemove: [], threads: ['t1'])\n      expect(task.description()).toEqual(\"Changed labels\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(id: 'l1', displayName: 'LABEL')], labelsToRemove: [], threads: ['t1'])\n      expect(task.description()).toEqual(\"Added LABEL\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(id: 'l1', displayName: 'LABEL')], labelsToRemove: ['l2'], threads: ['t1', 't2'])\n      expect(task.description()).toEqual(\"Moved 2 threads to LABEL\")\n\n    it \"should include the name of the removed label if it's the only mutation and it was provided as an object\", ->\n      task = new ChangeLabelsTask(labelsToAdd: [], labelsToRemove: [\"l1\"], threads: ['t1'])\n      expect(task.description()).toEqual(\"Changed labels\")\n      task = new ChangeLabelsTask(labelsToAdd: [], labelsToRemove: [new Label(id: 'l1', displayName: 'LABEL')], threads: ['t1'])\n      expect(task.description()).toEqual(\"Removed LABEL\")\n      task = new ChangeLabelsTask(labelsToAdd: ['l2'], labelsToRemove: [new Label(id: 'l1', displayName: 'LABEL')], threads: ['t1'])\n      expect(task.description()).toEqual(\"Changed labels\")\n\n    it \"should pluralize properly\", ->\n      task = new ChangeLabelsTask(labelsToAdd: [\"l2\"], labelsToRemove: [\"l1\"], threads: ['t1', 't2', 't3'])\n      expect(task.description()).toEqual(\"Changed labels on 3 threads\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(id: 'l1', displayName: 'LABEL')], labelsToRemove: [], threads: ['t1', 't2'])\n      expect(task.description()).toEqual(\"Added LABEL to 2 threads\")\n\n    it \"should include special cases for some common cases\", ->\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(name: \"all\")], labelsToRemove: [new Label(name: 'inbox')], threads: ['t1', 't2', 't3'])\n      expect(task.description()).toEqual(\"Archived 3 threads\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(name: \"trash\")], labelsToRemove: [new Label(name: 'inbox')], threads: ['t1', 't2', 't3'])\n      expect(task.description()).toEqual(\"Trashed 3 threads\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(name: \"spam\")], labelsToRemove: [new Label(name: 'inbox')], threads: ['t1', 't2', 't3'])\n      expect(task.description()).toEqual(\"Marked 3 threads as Spam\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(name: \"inbox\")], labelsToRemove: [new Label(name: 'spam')], threads: ['t1', 't2', 't3'])\n      expect(task.description()).toEqual(\"Unmarked 3 threads as Spam\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(name: \"inbox\")], labelsToRemove: [new Label(name: 'all')], threads: ['t1', 't2', 't3'])\n      expect(task.description()).toEqual(\"Unarchived 3 threads\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(name: \"inbox\")], labelsToRemove: [new Label(name: 'trash')], threads: ['t1', 't2', 't3'])\n      expect(task.description()).toEqual(\"Removed 3 threads from Trash\")\n      task = new ChangeLabelsTask(labelsToAdd: [new Label(name: \"inbox\")], labelsToRemove: [new Label(name: 'trash')], threads: ['t1'])\n      expect(task.description()).toEqual(\"Removed from Trash\")\n\n  describe \"_ensureAndUpdateLabels\", ->\n    beforeEach ->\n      @task = new ChangeLabelsTask()\n      @account = {}\n\n    it \"does not remove `all` if attempting to remove `all` without adding `trash` or `spam`\", ->\n      toAdd = []\n      toRemove = [{name: 'all'}]\n      {labelsToAdd, labelsToRemove} = @task._ensureAndUpdateLabels(@account, toAdd, toRemove)\n      expect(labelsToRemove).toEqual([])\n\n    it \"removes `trash` and `spam` if attempting to add `all` and not already removing them\", ->\n      toRemove = []\n      toAdd = [{name: 'all'}]\n      {labelsToAdd, labelsToRemove} = @task._ensureAndUpdateLabels(@account, toAdd, toRemove)\n      expect(labelsToRemove).toEqual([{name: 'trash'}, {name: 'spam'}])\n\n    it \"adds `all` if removing `trash` and not adding to `all` or `spam`\", ->\n      toRemove = [{name: 'trash'}]\n      toAdd = []\n      {labelsToAdd, labelsToRemove} = @task._ensureAndUpdateLabels(@account, toAdd, toRemove)\n      expect(labelsToAdd).toEqual([{name: 'all'}])\n\n    it \"removes `all` and `spam` if attempting to add `trash` and not already removing it\", ->\n      toRemove = []\n      toAdd = [{name: 'trash'}]\n      {labelsToAdd, labelsToRemove} = @task._ensureAndUpdateLabels(@account, toAdd, toRemove)\n      expect(labelsToRemove).toEqual([{name: 'all'}, {name: 'spam'}])\n\n    it \"adds `all` if removing `spam` and not adding to `all` or `trash`\", ->\n      toRemove = [{name: 'spam'}]\n      toAdd = []\n      {labelsToAdd, labelsToRemove} = @task._ensureAndUpdateLabels(@account, toAdd, toRemove)\n      expect(labelsToAdd).toEqual([{name: 'all'}])\n\n    it \"removes `all` and `trash` if attempting to add `spam` and not already removing it\", ->\n      toRemove = []\n      toAdd = [{name: 'spam'}]\n      {labelsToAdd, labelsToRemove} = @task._ensureAndUpdateLabels(@account, toAdd, toRemove)\n      expect(labelsToRemove).toEqual([{name: 'all'}, {name: 'trash'}])\n\n  describe \"performLocal\", ->\n    it \"should throw an exception if task has not been given a label, has been given messages, or no threads\", ->\n      badTasks = [\n        new ChangeLabelsTask(),\n        new ChangeLabelsTask(threads: [123]),\n        new ChangeLabelsTask(threads: [123], messages: [\"foo\"]),\n        new ChangeLabelsTask(labelsToAdd: ['l2'], labelsToRemove: ['l1'], messages: [123]),\n        new ChangeLabelsTask(threads: \"Thread\"),\n      ]\n      goodTasks = [\n        new ChangeLabelsTask(\n          labelsToAdd: ['l2']\n          labelsToRemove: ['l1']\n          threads: ['t1']\n        )\n      ]\n      caught = []\n      succeeded = []\n\n      runs ->\n        [].concat(badTasks, goodTasks).forEach (task) ->\n          task.performLocal()\n          .then -> succeeded.push(task)\n          .catch (err) -> caught.push(task)\n      waitsFor ->\n        succeeded.length + caught.length == 6\n      runs ->\n        expect(caught.length).toEqual(badTasks.length)\n        expect(succeeded.length).toEqual(goodTasks.length)\n\n    it 'calls through to super performLocal', ->\n      task = new ChangeLabelsTask\n        labelsToAdd: ['l2']\n        labelsToRemove: ['l1']\n        threads: ['t1']\n      waitsForPromise =>\n        task.performLocal().then =>\n          expect(task.__proto__.__proto__.performLocal).toHaveBeenCalled()\n\n    describe \"retrieveModels\", ->\n      describe \"when object IDs are provided\", ->\n        beforeEach ->\n          @task = new ChangeLabelsTask\n            labelsToAdd: ['l2']\n            labelsToRemove: ['l1']\n            threads: ['t1']\n\n        it 'resolves the objects before calling super', ->\n          waitsForPromise =>\n            @task.retrieveModels().then =>\n              expect(@task.labelsToAdd).toEqual([testLabels['l2']])\n              expect(@task.labelsToRemove).toEqual([testLabels['l1']])\n              expect(@task.threads).toEqual([testThreads['t1']])\n\n      describe \"when objects are provided\", ->\n        beforeEach ->\n          @task = new ChangeLabelsTask\n            labelsToAdd: [testLabels['l2']]\n            labelsToRemove: [testLabels['l1']]\n            threads: [testThreads['t1']]\n\n        it 'still has the objects when calling super', ->\n          waitsForPromise =>\n            @task.retrieveModels().then =>\n              expect(@task.labelsToAdd).toEqual([testLabels['l2']])\n              expect(@task.labelsToRemove).toEqual([testLabels['l1']])\n              expect(@task.threads).toEqual([testThreads['t1']])\n\n    describe 'change methods', ->\n      describe \"changesToModel\", ->\n        it 'properly adds labels', ->\n          task = new ChangeLabelsTask\n            labelsToAdd: [testLabels['l1'], testLabels['l2']]\n            labelsToRemove: []\n          out = task.changesToModel(testThreads['t1'])\n          expect(out).toEqual(labels: [testLabels['l1'], testLabels['l2']])\n\n        it 'properly removes labels', ->\n          task = new ChangeLabelsTask\n            labelsToAdd: []\n            labelsToRemove: [testLabels['l1'], testLabels['l2']]\n          out = task.changesToModel(testThreads['t3'])\n          expect(out).toEqual(labels: [testLabels['l3']])\n\n        it 'properly adds and removes labels, ignoring labels that are both added and removed', ->\n          task = new ChangeLabelsTask\n            labelsToAdd: [testLabels['l1'], testLabels['l2']]\n            labelsToRemove: [testLabels['l2'], testLabels['l3']]\n          out = task.changesToModel(testThreads['t1'])\n          expect(out).toEqual(labels: [testLabels['l1'], testLabels['l2']])\n\n        it 'should return an == array of labels when no changes have occurred', ->\n          thread = new Thread(id: '1', categories: [testLabels['l2'], testLabels['l3'], testLabels['l1']])\n          task = new ChangeLabelsTask\n            labelsToAdd: [testLabels['l3'], testLabels['l1'], testLabels['l2']]\n            labelsToRemove: []\n          out = task.changesToModel(thread)\n          expect(_.isEqual(thread.labels, out.labels)).toBe(true)\n\n        it 'should not modify the input thread in any way', ->\n          thread = new Thread(id: '1', categories: [testLabels['l2'], testLabels['l1']])\n          task = new ChangeLabelsTask\n            labelsToAdd: []\n            labelsToRemove: [testLabels['l2']]\n          out = task.changesToModel(thread)\n          expect(thread.labels.length).toBe(2)\n          expect(out.labels.length).toBe(1)\n\n      describe \"requestBodyForModel\", ->\n        it 'returns labels:<ids> for both threads and messages', ->\n          task = new ChangeLabelsTask()\n\n          out = task.requestBodyForModel(testThreads['t3'])\n          expect(out).toEqual(labels: ['l2', 'l3'])\n"
  },
  {
    "path": "packages/client-app/spec/tasks/change-mail-task-spec.coffee",
    "content": "_ = require 'underscore'\n\n{APIError,\n Folder,\n Thread,\n Message,\n Actions,\n NylasAPI,\n NylasAPIRequest,\n Query,\n DatabaseStore,\n DatabaseWriter,\n Task,\n Utils,\n ChangeMailTask,\n EnsureMessageInSentFolderTask,\n} = require 'nylas-exports'\n\nxdescribe \"ChangeMailTask\", ->\n  beforeEach ->\n    @threadA = new Thread(id: 'A', folders: [new Folder(id:'folderA')])\n    @threadB = new Thread(id: 'B', folders: [new Folder(id:'folderB')])\n    @threadC = new Thread(id: 'C', folders: [new Folder(id:'folderC')])\n    @threadAChanged = new Thread(id: 'A', folders: [new Folder(id:'folderC')])\n\n    @threadAMesage1 = new Message(id:'A1', threadId: 'A')\n    @threadAMesage2 = new Message(id:'A2', threadId: 'A')\n    @threadBMesage1 = new Message(id:'B1', threadId: 'B')\n\n    threads = [@threadA, @threadB, @threadC]\n    messages = [@threadAMesage1, @threadAMesage2, @threadBMesage1]\n\n    # Instead of spying on find/findAll, we fake the evaluation of the query.\n    # This allows queries to be built with findAll().where().blabla... without\n    # a complex stub chain. Works since query \"matchers\" can be evaluated in JS\n    spyOn(DatabaseStore, 'run').andCallFake (query) =>\n      if query._klass is Message\n        models = messages\n      else if query._klass is Thread\n        models = threads\n      else\n        throw new Error(\"Not stubbed!\")\n\n      models = models.filter (model) ->\n        for matcher in query._matchers\n          if matcher.evaluate(model) is false\n            return false\n        return true\n\n      if query._singular\n        models = models[0]\n      Promise.resolve(models)\n\n    @transaction = new DatabaseWriter()\n    spyOn(@transaction, 'persistModels').andReturn(Promise.resolve())\n    spyOn(@transaction, 'persistModel').andReturn(Promise.resolve())\n\n  it \"leaves subclasses to implement changesToModel\", ->\n    task = new ChangeMailTask()\n    expect( => task.changesToModel() ).toThrow()\n\n  it \"leaves subclasses to implement requestBodyForModel\", ->\n    task = new ChangeMailTask()\n    expect( => task.requestBodyForModel() ).toThrow()\n\n  describe \"performLocal\", ->\n    it \"rejects if it's an undo task and no restore values are present\", ->\n      task = new ChangeMailTask()\n      task._isUndoTask = true\n      spyOn(task, '_performLocalThreads').andReturn(Promise.resolve())\n\n      waitsForPromise =>\n        task.performLocal().catch (err) =>\n          expect(err.message).toEqual(\"ChangeMailTask: No _restoreValues provided for undo task.\")\n\n    it \"should always call _performLocalThreads and then _performLocalMessages\", ->\n      task = new ChangeMailTask()\n      task.threads = [@threadA]\n\n      @messagesResolve = null\n      spyOn(task, '_performLocalThreads').andCallFake => Promise.resolve()\n      spyOn(task, '_performLocalMessages').andCallFake =>\n        new Promise (resolve, reject) => @messagesResolve = resolve\n\n      runs ->\n        task.performLocal()\n      waitsFor ->\n        task._performLocalThreads.callCount > 0\n      runs ->\n        advanceClock()\n        @messagesResolve()\n      waitsFor ->\n        task._performLocalMessages.callCount > 0\n      runs ->\n        expect(task._performLocalThreads).toHaveBeenCalled()\n        expect(task._performLocalMessages).toHaveBeenCalled()\n\n  describe \"_performLocalThreads\", ->\n    beforeEach ->\n      @task = new ChangeMailTask()\n      @task.threads = [@threadA, @threadB]\n      # Note: Simulate applyChanges only changing threadA, not threadB\n      spyOn(@task, '_applyChanges').andReturn([@threadAChanged])\n\n    it \"calls _applyChanges and writes changed threads to the database\", ->\n      waitsForPromise =>\n        @task._performLocalThreads(@transaction).then =>\n          expect(@task._applyChanges).toHaveBeenCalledWith(@task.threads)\n          expect(@transaction.persistModels).toHaveBeenCalledWith([@threadAChanged])\n\n    describe \"when processNestedMessages is overridden to return true\", ->\n      it \"fetches messages on changed threads and appends them to the messages to update\", ->\n        waitsForPromise =>\n          @task.processNestedMessages = => true\n          @task._performLocalThreads(@transaction).then =>\n            expect(@task._applyChanges).toHaveBeenCalledWith(@task.threads)\n            expect(@task.messages).toEqual([@threadAMesage1, @threadAMesage2])\n\n  describe \"_performLocalMessages\", ->\n    beforeEach ->\n      @task = new ChangeMailTask()\n      @task.messages = [@threadAMesage1, @threadAMesage2, @threadBMesage1]\n      # Note: Simulate applyChanges only changing threadBMesage1\n      spyOn(@task, '_applyChanges').andReturn([@threadBMesage1])\n\n    it \"calls _applyChanges and writes changed messages to the database\", ->\n      waitsForPromise =>\n        @task._performLocalMessages(@transaction).then =>\n          expect(@task._applyChanges).toHaveBeenCalledWith(@task.messages)\n          expect(@transaction.persistModels).toHaveBeenCalledWith([@threadBMesage1])\n\n  describe \"_applyChanges\", ->\n    beforeEach ->\n      @task = new ChangeMailTask()\n\n    describe \"when applying forwards\", ->\n      beforeEach ->\n        spyOn(@task, '_shouldChangeBackwards').andReturn(false)\n        spyOn(@task, 'changesToModel').andCallFake (thread) =>\n          if thread is @threadC\n            return {folders: [new Folder(id: \"different!\")]}\n          else\n            return {folders: thread.folders}\n\n      it \"should call changesToModel on each model\", ->\n        @task._applyChanges([@threadA, @threadB])\n        expect(@task.changesToModel.callCount).toBe(2)\n        expect(@task.changesToModel.calls[0].args[0]).toBe(@threadA)\n        expect(@task.changesToModel.calls[1].args[0]).toBe(@threadB)\n\n      it \"should return only the models with new values\", ->\n        out = @task._applyChanges([@threadA, @threadB, @threadC])\n        expect(_.isArray(out)).toBe(true)\n        expect(out.length).toBe(1)\n        expect(out[0].id).toBe('C')\n        expect(out[0].folders[0].id).toBe('different!')\n\n      it \"should save restore values only for changed items\", ->\n        out = @task._applyChanges([@threadA, @threadB, @threadC])\n        expect(@task._restoreValues['A']).toBe(undefined)\n        expect(@task._restoreValues['B']).toBe(undefined)\n        expect(@task._restoreValues['C']).toEqual(folders: @threadC.folders)\n\n      it \"should treat models as if they're frozen, returning new models\", ->\n        out = @task._applyChanges([@threadA, @threadB, @threadC])\n        expect(out[0]).not.toBe(@threadC)\n        expect(out[0].id).toBe(@threadC.id)\n        expect(@task._restoreValues['C']).toEqual(folders: @threadC.folders)\n\n    describe \"when applying backwards (reverting or undoing)\", ->\n      beforeEach ->\n        spyOn(@task, '_shouldChangeBackwards').andReturn(true)\n        @task._restoreValues =\n          'C': {folders: [new Folder(id:'oldFolderC')]}\n\n      it \"should return only models with the restore values, with the restore values applied\", ->\n        out = @task._applyChanges([@threadA, @threadB, @threadC])\n        expect(_.isArray(out)).toBe(true)\n        expect(out.length).toBe(1)\n        expect(out[0].id).toBe('C')\n        expect(out[0].folders[0].id).toBe('oldFolderC')\n\n  describe \"performRemote\", ->\n    describe \"if threads are set\", ->\n      it \"should only call _performRequests with threads\", ->\n        @task = new ChangeMailTask()\n        @task.threads = [@threadA, @threadB]\n        @task.messages = [@threadAMesage1, @threadAMesage2]\n        spyOn(@task, '_performRequests').andReturn(Promise.resolve())\n        waitsForPromise =>\n          @task.performRemote().then =>\n            expect(@task._performRequests).toHaveBeenCalledWith(Thread, @task.threads)\n            expect(@task._performRequests.callCount).toBe(1)\n\n    describe \"if only messages are set\", ->\n      it \"should only call _performRequests with messages\", ->\n        @task = new ChangeMailTask()\n        @task.threads = []\n        @task.messages = [@threadAMesage1, @threadAMesage2]\n        spyOn(@task, '_performRequests').andReturn(Promise.resolve())\n        waitsForPromise =>\n          @task.performRemote().then =>\n            expect(@task._performRequests).toHaveBeenCalledWith(Message, @task.messages)\n            expect(@task._performRequests.callCount).toBe(1)\n\n    describe \"if somehow there are no threads or messages\", ->\n      it \"should resolve\", ->\n        @task = new ChangeMailTask()\n        @task.threads = []\n        @task.messages = []\n        waitsForPromise =>\n          @task.performRemote().then (code) =>\n            expect(code).toEqual(Task.Status.Success)\n\n    describe \"if _performRequests resolves\", ->\n      it \"should resolve with Task.Status.Success\", ->\n        @task = new ChangeMailTask()\n        spyOn(@task, '_performRequests').andReturn(Promise.resolve())\n        waitsForPromise =>\n          @task.performRemote().then (result) =>\n            expect(result).toBe(Task.Status.Success)\n\n    describe \"if _performRequests rejects with a permanent network error\", ->\n      beforeEach ->\n        @task = new ChangeMailTask()\n        @error = new APIError(statusCode: 400)\n        spyOn(@task, '_performRequests').andReturn(Promise.reject(@error))\n        spyOn(@task, 'performLocal').andReturn(Promise.resolve())\n\n      it \"should set isReverting and call performLocal\", ->\n        waitsForPromise =>\n          @task.performRemote().then (result) =>\n            expect(@task.performLocal).toHaveBeenCalled()\n            expect(@task._isReverting).toBe(true)\n\n      it \"should resolve with Task.Status.Failed after reverting\", ->\n        waitsForPromise =>\n          @task.performRemote().then (result) =>\n            expect(result).toEqual([Task.Status.Failed, @error])\n\n    describe \"if _performRequests rejects with a temporary network error\", ->\n      beforeEach ->\n        @task = new ChangeMailTask()\n        spyOn(@task, '_performRequests').andReturn(Promise.reject(new APIError(statusCode: NylasAPI.SampleTemporaryErrorCode)))\n        spyOn(@task, 'performLocal').andReturn(Promise.resolve())\n\n      it \"should not revert\", ->\n        waitsForPromise =>\n          @task.performRemote().then (result) =>\n            expect(@task.performLocal).not.toHaveBeenCalled()\n            expect(@task._isReverting).not.toBe(true)\n\n      it \"should resolve with Task.Status.Retry\", ->\n        waitsForPromise =>\n          @task.performRemote().then (result) =>\n            expect(result).toBe(Task.Status.Retry)\n\n\n    describe \"_performRequests\", ->\n      beforeEach ->\n        @task = new ChangeMailTask()\n        @task._restoreValues =\n          'A': {}\n          'B': {}\n          'C': {}\n          'A1': {}\n        spyOn(@task, 'requestBodyForModel').andCallFake (model) =>\n          if model is @threadA\n            return {field: 'thread-a-body'}\n          if model is @threadB\n            return {field: 'thread-b-body'}\n          if model is @threadAMesage1\n            return {field: 'message-1'}\n\n      it \"should call NylasAPIRequest.run for each model, passing the result of requestBodyForModel\", ->\n        spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.resolve())\n        runs ->\n          @task._performRequests(Thread, [@threadA, @threadB])\n        waitsFor ->\n          NylasAPIRequest.prototype.run.callCount is 2\n        runs ->\n          expect(NylasAPIRequest.prototype.run.calls[0].args[0].body).toEqual({field: 'thread-a-body'})\n          expect(NylasAPIRequest.prototype.run.calls[1].args[0].body).toEqual({field: 'thread-b-body'})\n\n      it \"should resolve when all of the requests complete\", ->\n        promises = []\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) -> promises.push({resolve, reject})\n\n        resolved = false\n        runs ->\n          @task._performRequests(Thread, [@threadA, @threadB]).then =>\n            resolved = true\n        waitsFor ->\n          NylasAPIRequest.prototype.run.callCount is 2\n        runs ->\n          expect(resolved).toBe(false)\n          promises[0].resolve()\n          advanceClock()\n          expect(resolved).toBe(false)\n          promises[1].resolve()\n          advanceClock()\n          expect(resolved).toBe(true)\n\n      it \"should carry on and resolve if a request 404s, since the NylasAPI manager will clean the object from the cache\", ->\n        promises = []\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) -> promises.push({resolve, reject})\n\n        resolved = false\n        runs ->\n          @task._performRequests(Thread, [@threadA, @threadB]).then =>\n            resolved = true\n        waitsFor ->\n          NylasAPIRequest.prototype.run.callCount is 2\n        runs ->\n          promises[0].resolve()\n          promises[1].reject(new APIError(statusCode: 404))\n          advanceClock()\n          expect(resolved).toBe(true)\n\n      it \"should reject with the request error encountered by any request\", ->\n        promises = []\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) -> promises.push({resolve, reject})\n\n        err = null\n        runs ->\n          @task._performRequests(Thread, [@threadA, @threadB]).catch (error) =>\n            err = error\n        waitsFor ->\n          NylasAPIRequest.prototype.run.callCount is 2\n        runs ->\n          expect(err).toBe(null)\n          promises[0].resolve()\n          advanceClock()\n          expect(err).toBe(null)\n          apiError = new APIError(statusCode: NylasAPI.SampleTemporaryErrorCode)\n          promises[1].reject(apiError)\n          advanceClock()\n          expect(err).toBe(apiError)\n\n      it \"should use /threads when the klass provided is Thread\", ->\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) -> #noop\n        runs ->\n          @task._performRequests(Thread, [@threadA, @threadB])\n        waitsFor ->\n          NylasAPIRequest.prototype.run.callCount is 2\n        runs ->\n          path = \"/threads/#{@threadA.id}\"\n          expect(NylasAPIRequest.prototype.run.calls[0].args[0].path).toBe(path)\n          expect(NylasAPIRequest.prototype.run.calls[0].args[0].accountId).toBe(@threadA.accountId)\n\n      it \"should use /messages when the klass provided is Message\", ->\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) -> #noop\n        runs ->\n          @task._performRequests(Message, [@threadAMesage1])\n        waitsFor ->\n          NylasAPIRequest.prototype.run.callCount is 1\n        runs ->\n          path = \"/messages/#{@threadAMesage1.id}\"\n          expect(NylasAPIRequest.prototype.run.calls[0].args[0].path).toBe(path)\n          expect(NylasAPIRequest.prototype.run.calls[0].args[0].accountId).toBe(@threadAMesage1.accountId)\n\n      it \"should decrement change counts as requests complete\", ->\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) -> #noop\n        spyOn(@task, '_removeLock')\n        runs ->\n          @task._performRequests(Thread, [@threadAMesage1])\n        waitsFor ->\n          NylasAPIRequest.prototype.run.callCount is 1\n        runs ->\n          expect(@task._removeLock).toHaveBeenCalledWith(@threadAMesage1)\n\n      it \"should make no more than 10 requests at once\", ->\n        resolves = []\n        spyOn(@task, '_removeLock')\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) -> resolves.push(resolve)\n\n        threads = []\n        threads.push new Thread(id: \"#{idx}\", subject: idx) for idx in [0..100]\n        @task._restoreValues = _.map threads, (t) -> {some: 'data'}\n        @task._performRequests(Thread, threads)\n        advanceClock()\n        expect(resolves.length).toEqual(5)\n        advanceClock()\n        expect(resolves.length).toEqual(5)\n        resolves[0]()\n        resolves[1]()\n        advanceClock()\n        expect(resolves.length).toEqual(7)\n        resolves[idx]() for idx in [2...7]\n        advanceClock()\n        expect(resolves.length).toEqual(12)\n\n\n      it \"should stop making requests after non-404 network errors\", ->\n        resolves = []\n        rejects = []\n        spyOn(@task, '_removeLock')\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          new Promise (resolve, reject) ->\n            resolves.push(resolve)\n            rejects.push(reject)\n\n        threads = []\n        threads.push new Thread(id: \"#{idx}\", subject: idx) for idx in [0..100]\n        @task._restoreValues = _.map threads, (t) -> {some: 'data'}\n        @task._performRequests(Thread, threads).catch (err) ->\n          # Need to catch the error so it's not a\n          # Promise.possiblyUnhandledRejection, which will stop the tests.\n          expect(err.statusCode).toBe 400\n        advanceClock()\n        expect(resolves.length).toEqual(5)\n        resolves[idx]() for idx in [0...4]\n        advanceClock()\n        expect(resolves.length).toEqual(9)\n\n        # simulate request failure\n        reject = rejects[rejects.length - 1]\n        reject(new APIError(statusCode: 400))\n        advanceClock()\n\n        # simulate more requests succeeding\n        resolves[idx]() for idx in [5...9]\n        advanceClock()\n\n        # check that no more requests have been queued\n        expect(resolves.length).toEqual(9)\n\n  describe \"optimistic object locking\", ->\n    beforeEach ->\n      @task = new ChangeMailTask()\n      spyOn(@task, '_performLocalThreads').andReturn(Promise.resolve())\n      spyOn(@task, '_lockAll')\n\n    it \"increments the locks in performLocal\", ->\n      waitsForPromise =>\n        @task.performLocal().then =>\n          expect(@task._lockAll).toHaveBeenCalled()\n\n    describe \"when the task is reverting after request failures\", ->\n      it \"should not increment change locks\", ->\n        @task._isReverting = true\n        waitsForPromise =>\n          @task.performLocal().then =>\n            expect(@task._lockAll).not.toHaveBeenCalled()\n\n    describe \"when the task is undoing\", ->\n      it \"should increment change locks\", ->\n        @task._isUndoTask = true\n        @task._restoreValues = {}\n        waitsForPromise =>\n          @task.performLocal().then =>\n            expect(@task._lockAll).toHaveBeenCalled()\n\n    describe \"when performRemote is returning Task.Status.Success\", ->\n      it \"should clean up locks\", ->\n        spyOn(@task, '_performRequests').andReturn(Promise.resolve())\n        spyOn(@task, '_ensureLocksRemoved')\n        waitsForPromise =>\n          @task.performRemote().then =>\n            expect(@task._ensureLocksRemoved).toHaveBeenCalled()\n\n    describe \"when performRemote is returning Task.Status.Failed after reverting\", ->\n      it \"should clean up locks\", ->\n        spyOn(@task, '_performRequests').andReturn(Promise.reject(new APIError(statusCode: 400)))\n        spyOn(@task, '_ensureLocksRemoved')\n        waitsForPromise =>\n          @task.performRemote().then =>\n            expect(@task._ensureLocksRemoved).toHaveBeenCalled()\n\n    describe \"when performRemote is returning Task.Status.Retry\", ->\n      it \"should not clean up locks\", ->\n        spyOn(@task, '_performRequests').andReturn(Promise.reject(new APIError(statusCode: NylasAPI.SampleTemporaryErrorCode)))\n        spyOn(@task, '_ensureLocksRemoved')\n        waitsForPromise =>\n          @task.performRemote().then =>\n            expect(@task._ensureLocksRemoved).not.toHaveBeenCalled()\n\n  describe \"_lockAll\", ->\n    beforeEach ->\n      @task = new ChangeMailTask()\n      @task.threads = [@threadA, @threadB]\n      spyOn(NylasAPI, 'incrementRemoteChangeLock')\n\n    it \"should keep a hash of the items that it locks\", ->\n      @task._lockAll()\n      expect(NylasAPI.incrementRemoteChangeLock.callCount).toBe(2)\n      expect(@task._locked).toEqual('A': 1, 'B': 1)\n\n    it \"should not break anything if it's accidentally called twice\", ->\n      @task._lockAll()\n      @task._lockAll()\n      expect(NylasAPI.incrementRemoteChangeLock.callCount).toBe(4)\n      expect(@task._locked).toEqual('A': 2, 'B': 2)\n\n  describe \"_ensureLocksRemoved\", ->\n    it \"should decrement locks given any aribtrarily messed up lock state and reset the locked array\", ->\n      @task = new ChangeMailTask()\n      @task.threads = [@threadA, @threadB, @threadC]\n      spyOn(NylasAPI, 'decrementRemoteChangeLock')\n      @task._locked = {'A': 2, 'B': 2, 'C': 1}\n      @task._ensureLocksRemoved()\n      expect(NylasAPI.decrementRemoteChangeLock.callCount).toBe(5)\n      expect(NylasAPI.decrementRemoteChangeLock.calls[0].args[1]).toBe('A')\n      expect(NylasAPI.decrementRemoteChangeLock.calls[1].args[1]).toBe('A')\n      expect(NylasAPI.decrementRemoteChangeLock.calls[2].args[1]).toBe('B')\n      expect(NylasAPI.decrementRemoteChangeLock.calls[3].args[1]).toBe('B')\n      expect(NylasAPI.decrementRemoteChangeLock.calls[4].args[1]).toBe('C')\n      expect(@task._locked).toEqual(null)\n\n  describe \"createIdenticalTask\", ->\n    it \"should return a copy of the task, but with the objects converted into object ids\", ->\n      task = new ChangeMailTask()\n      task.messages = [@threadAMesage1, @threadAMesage2]\n      clone = task.createIdenticalTask()\n      expect(clone.messages).toEqual([@threadAMesage1.id, @threadAMesage2.id])\n\n      task = new ChangeMailTask()\n      task.threads = [@threadA, @threadB]\n      clone = task.createIdenticalTask()\n      expect(clone.threads).toEqual([@threadA.id, @threadB.id])\n\n      task = new ChangeMailTask()\n      task.threads = [@threadA.id, @threadB.id]\n      clone = task.createIdenticalTask()\n      expect(clone.threads).toEqual([@threadA.id, @threadB.id])\n\n  describe \"createUndoTask\", ->\n    it \"should return a task initialized with _isUndoTask and _restoreValues\", ->\n      task = new ChangeMailTask()\n      task.messages = [@threadAMesage1, @threadAMesage2]\n      task._restoreValues = {'A': 'bla'}\n      undo = task.createUndoTask()\n      expect(undo.messages).toEqual([@threadAMesage1.id, @threadAMesage2.id])\n      expect(undo._restoreValues).toBe(task._restoreValues)\n      expect(undo._isUndoTask).toBe(true)\n\n    it \"should throw if you try to make an undo task of an undo task\", ->\n      task = new ChangeMailTask()\n      task._isUndoTask = true\n      expect( -> task.createUndoTask()).toThrow()\n\n    it \"should throw if _restoreValues are not availble\", ->\n      task = new ChangeMailTask()\n      task.messages = [@threadAMesage1, @threadAMesage2]\n      task._restoreValues = null\n      expect( -> task.createUndoTask()).toThrow()\n\n  describe \"isDependentOnTask\", ->\n    it \"should return true if another EnsureMessageInSentFolderTask involves one of the ChangeMailTask's threads\", ->\n      a = new ChangeMailTask()\n      a.threads = ['t1', 't2', 't3']\n      s1 = new EnsureMessageInSentFolderTask({message: {threadId: 't1'}})\n      s2 = new EnsureMessageInSentFolderTask({message: {threadId: 't100'}})\n      expect(a.isDependentOnTask(s1)).toEqual(true)\n      expect(a.isDependentOnTask(s2)).toEqual(false)\n\n    it \"should return true if another EnsureMessageInSentFolderTask involves one of the ChangeMailTask's messages\", ->\n      a = new ChangeMailTask()\n      a.messages = ['m1', 'm2', 'm3']\n      s1 = new EnsureMessageInSentFolderTask({message: {clientId: 'm1'}})\n      s2 = new EnsureMessageInSentFolderTask({message: {serverId: 'm1'}})\n      s3 = new EnsureMessageInSentFolderTask({message: {clientId: 'm100'}})\n      s4 = new EnsureMessageInSentFolderTask({message: {serverId: 'm100'}})\n      expect(a.isDependentOnTask(s1)).toEqual(true)\n      expect(a.isDependentOnTask(s2)).toEqual(true)\n      expect(a.isDependentOnTask(s3)).toEqual(false)\n      expect(a.isDependentOnTask(s4)).toEqual(false)\n\n    it \"should return true if another, older ChangeMailTask involves the same threads\", ->\n      a = new ChangeMailTask()\n      a.threads = ['t1', 't2', 't3']\n      a.sequentialId = 0\n      b = new ChangeMailTask()\n      b.threads = ['t3', 't4', 't7']\n      b.sequentialId = 1\n      c = new ChangeMailTask()\n      c.threads = ['t0', 't7']\n      c.sequentialId = 2\n      expect(a.isDependentOnTask(b)).toEqual(false)\n      expect(a.isDependentOnTask(c)).toEqual(false)\n      expect(b.isDependentOnTask(a)).toEqual(true)\n      expect(c.isDependentOnTask(a)).toEqual(false)\n      expect(c.isDependentOnTask(b)).toEqual(true)\n"
  },
  {
    "path": "packages/client-app/spec/tasks/change-starred-task-spec.coffee",
    "content": "Task = require('../../src/flux/tasks/task').default\nThread = require('../../src/flux/models/thread').default\nChangeStarredTask = require('../../src/flux/tasks/change-starred-task').default\n\ndescribe 'ChangeStarredTask', ->\n  describe \"description\", ->\n    it 'should include special cases for changing starred', ->\n      threads = [\n        new Thread(id:\"id-1\")\n        new Thread(id:\"id-2\")\n        new Thread(id:\"id-3\")\n      ]\n      task = new ChangeStarredTask({threads:threads, starred: true})\n      expect(task.description()).toEqual(\"Starred 3 threads\")\n      task = new ChangeStarredTask({thread: threads[0], starred: true})\n      expect(task.description()).toEqual(\"Starred\")\n      task = new ChangeStarredTask({threads:threads, starred: false})\n      expect(task.description()).toEqual(\"Unstarred 3 threads\")\n      task = new ChangeStarredTask({thread: threads[0], starred: false})\n      expect(task.description()).toEqual(\"Unstarred\")\n"
  },
  {
    "path": "packages/client-app/spec/tasks/change-unread-task-spec.coffee",
    "content": "Task = require('../../src/flux/tasks/task').default\nThread = require('../../src/flux/models/thread').default\nChangeUnreadTask = require('../../src/flux/tasks/change-unread-task').default\n\ndescribe 'ChangeUnreadTask', ->\n  describe \"description\", ->\n    it 'should include special cases for changing unread', ->\n      threads = [\n        new Thread(id:\"id-1\")\n        new Thread(id:\"id-2\")\n        new Thread(id:\"id-3\")\n      ]\n      task = new ChangeUnreadTask({threads, unread: true})\n      expect(task.description()).toEqual(\"Marked 3 threads as unread\")\n      task = new ChangeUnreadTask({thread: threads[0], unread: true})\n      expect(task.description()).toEqual(\"Marked as unread\")\n      task = new ChangeUnreadTask({threads, unread: false})\n      expect(task.description()).toEqual(\"Marked 3 threads as read\")\n      task = new ChangeUnreadTask({thread: threads[0], unread: false})\n      expect(task.description()).toEqual(\"Marked as read\")\n"
  },
  {
    "path": "packages/client-app/spec/tasks/destroy-category-task-spec.coffee",
    "content": "{DestroyCategoryTask,\n NylasAPI,\n NylasAPIRequest,\n Task,\n Category,\n AccountStore,\n APIError,\n Category,\n DatabaseStore,\n DatabaseWriter} = require \"nylas-exports\"\n\nxdescribe \"DestroyCategoryTask\", ->\n  pathOf = (fn) ->\n    fn.calls[0].args[0].path\n\n  methodOf = (fn) ->\n    fn.calls[0].args[0].method\n\n  accountIdOf = (fn) ->\n    fn.calls[0].args[0].accountId\n\n  nameOf = (fn) ->\n    fn.calls[0].args[0].body.display_name\n\n  makeAccount = ({usesFolders, usesLabels} = {}) ->\n    spyOn(AccountStore, \"accountForId\").andReturn {\n      usesFolders: -> usesFolders\n      usesLabels: -> usesLabels\n    }\n  makeTask = ->\n    category = new Category\n      displayName: \"important emails\"\n      accountId: \"account 123\"\n      serverId: \"server-444\"\n    new DestroyCategoryTask\n      category: category\n\n  beforeEach ->\n    spyOn(DatabaseWriter.prototype, 'unpersistModel').andCallFake -> Promise.resolve()\n    spyOn(DatabaseWriter.prototype, 'persistModel').andCallFake -> Promise.resolve()\n\n  describe \"performLocal\", ->\n    it \"sets an `isDeleted` flag and persists the category\", ->\n      task = makeTask()\n      runs =>\n        task.performLocal()\n      waitsFor =>\n        DatabaseWriter.prototype.unpersistModel.callCount > 0\n      runs =>\n        model = DatabaseWriter.prototype.unpersistModel.calls[0].args[0]\n        expect(model.serverId).toEqual \"server-444\"\n\n  describe \"performRemote\", ->\n    it \"throws error when no category present\", ->\n      waitsForPromise ->\n        task = makeTask()\n        task.category = null\n        task.performRemote()\n        .then ->\n          throw new Error('The promise should reject')\n        .catch Error, (err) ->\n          expect(err).toBeDefined()\n\n    it \"throws error when category does not have a serverId\", ->\n      waitsForPromise ->\n        task = makeTask()\n        task.category.serverId = undefined\n        task.performRemote()\n        .then ->\n          throw new Error('The promise should reject')\n        .catch Error, (err) ->\n          expect(err).toBeDefined()\n\n    describe \"when request succeeds\", ->\n      beforeEach ->\n        spyOn(NylasAPIRequest.prototype, \"run\").andCallFake -> Promise.resolve(\"null\")\n        spyOn(NylasAPI, \"incrementRemoteChangeLock\")\n\n      it \"blocks other remote changes to that category\", ->\n        makeAccount()\n        task = makeTask()\n        task.performRemote()\n        expect(NylasAPI.incrementRemoteChangeLock).toHaveBeenCalled()\n      it \"sends API req to /labels if user uses labels\", ->\n        makeAccount(usesLabels: true)\n        task = makeTask()\n        task.performRemote()\n        expect(pathOf(NylasAPIRequest.prototype.run)).toBe \"/labels/server-444\"\n\n      it \"sends API req to /folders if user uses folders\", ->\n        makeAccount(usesFolders: true)\n        task = makeTask()\n        task.performRemote()\n        expect(pathOf(NylasAPIRequest.prototype.run)).toBe \"/folders/server-444\"\n\n      it \"sends DELETE request\", ->\n        makeAccount()\n        task = makeTask()\n        task.performRemote()\n        expect(methodOf(NylasAPIRequest.prototype.run)).toBe \"DELETE\"\n\n      it \"sends the account id\", ->\n        makeAccount()\n        task = makeTask()\n        task.performRemote()\n        expect(accountIdOf(NylasAPIRequest.prototype.run)).toBe \"account 123\"\n\n    describe \"when request fails\", ->\n      beforeEach ->\n        makeAccount()\n        spyOn(NylasAPI, 'decrementRemoteChangeLock')\n        spyOn(NylasEnv, 'reportError')\n        spyOn(NylasAPIRequest.prototype, 'run').andCallFake ->\n          Promise.reject(new APIError({statusCode: 403}))\n\n      it \"persists the category and notifies error\", ->\n        waitsForPromise ->\n          task = makeTask()\n          spyOn(task, \"_notifyUserOfError\")\n\n          task.performRemote().then (status) ->\n            expect(status).toEqual Task.Status.Failed\n            expect(task._notifyUserOfError).toHaveBeenCalled()\n            expect(NylasEnv.reportError).toHaveBeenCalled()\n            expect(DatabaseWriter.prototype.persistModel).toHaveBeenCalled()\n            model = DatabaseWriter.prototype.persistModel.calls[0].args[0]\n            expect(model.serverId).toEqual \"server-444\"\n            expect(NylasAPI.decrementRemoteChangeLock).toHaveBeenCalled\n\n      describe \"_notifyUserOfError\", ->\n        it \"should present an error dialog\", ->\n          spyOn(NylasEnv, 'showErrorDialog')\n          task = makeTask()\n          task._notifyUserOfError(task.category)\n          expect(NylasEnv.showErrorDialog).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/tasks/destroy-model-task-spec.es6",
    "content": "import {\n  Task,\n  Model,\n  NylasAPIRequest,\n  DatabaseStore,\n  DestroyModelTask,\n  DatabaseWriter} from 'nylas-exports'\n\nxdescribe('DestroyModelTask', function destroyModelTask() {\n  beforeEach(() => {\n    this.existingModel = new Model()\n    this.existingModel.clientId = \"local-123\"\n    this.existingModel.serverId = \"server-123\"\n    spyOn(DatabaseWriter.prototype, \"unpersistModel\")\n    spyOn(DatabaseStore, \"findBy\").andCallFake(() => {\n      return Promise.resolve(this.existingModel)\n    })\n\n    this.defaultArgs = {\n      clientId: \"local-123\",\n      accountId: \"a123\",\n      modelName: \"Model\",\n      endpoint: \"/endpoint\",\n    }\n  });\n\n  it(\"constructs without error\", () => {\n    const t = new DestroyModelTask()\n    expect(t._rememberedToCallSuper).toBe(true)\n  });\n\n  describe(\"performLocal\", () => {\n    it(\"throws if basic fields are missing\", () => {\n      const t = new DestroyModelTask()\n      try {\n        t.performLocal()\n        throw new Error(\"Shouldn't succeed\");\n      } catch (e) {\n        expect(e.message).toMatch(/^Must pass.*/)\n      }\n    });\n\n    it(\"throws if the model name can't be found\", () => {\n      this.defaultArgs.modelName = \"dne\"\n      const t = new DestroyModelTask(this.defaultArgs)\n      try {\n        t.performLocal()\n        throw new Error(\"Shouldn't succeed\");\n      } catch (e) {\n        expect(e.message).toMatch(/^Couldn't find the class for.*/)\n      }\n    });\n\n    it(\"throws if it can't find the object\", () => {\n      jasmine.unspy(DatabaseStore, \"findBy\")\n      spyOn(DatabaseStore, \"findBy\").andCallFake(() => {\n        return Promise.resolve(null)\n      })\n      const t = new DestroyModelTask(this.defaultArgs)\n      window.waitsForPromise(() => {\n        return t.performLocal().then(() => {\n          throw new Error(\"Shouldn't succeed\")\n        }).catch((err) => {\n          expect(err.message).toMatch(/^Couldn't find the model with clientId.*/)\n        });\n      });\n    });\n\n    it(\"unpersists the new existing model properly\", () => {\n      const unpersistFn = DatabaseWriter.prototype.unpersistModel\n      const t = new DestroyModelTask(this.defaultArgs)\n      window.waitsForPromise(() => {\n        return t.performLocal().then(() => {\n          expect(unpersistFn).toHaveBeenCalled()\n          const model = unpersistFn.calls[0].args[0]\n          expect(model).toBe(this.existingModel)\n        });\n      });\n    });\n  });\n\n  describe(\"performRemote\", () => {\n    beforeEach(() => {\n      this.task = new DestroyModelTask(this.defaultArgs)\n    });\n\n    const performRemote = (fn) => {\n      window.waitsForPromise(() => {\n        return this.task.performLocal().then(() => {\n          return this.task.performRemote().then(fn)\n        });\n      });\n    }\n\n    it(\"skips request if the serverId is undefined\", () => {\n      window.waitsForPromise(() => {\n        return this.task.performLocal().then(() => {\n          this.task.serverId = null\n          return this.task.performRemote().then((status) => {\n            expect(status).toEqual(Task.Status.Continue)\n          })\n        });\n      });\n    });\n\n    it(\"makes a DELETE request to the Nylas API\", () => {\n      spyOn(NylasAPIRequest.prototype, \"run\").andReturn(Promise.resolve())\n      performRemote(() => {\n        const opts = NylasAPIRequest.prototype.run.calls[0].args[0]\n        expect(opts.method).toBe(\"DELETE\")\n        expect(opts.path).toBe(\"/endpoint/server-123\")\n        expect(opts.accountId).toBe(this.defaultArgs.accountId)\n      })\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/tasks/event-rsvp-task-spec.coffee",
    "content": "_ = require 'underscore'\n\n{NylasAPI,\n NylasAPIRequest,\n Event,\n Actions,\n APIError,\n EventRSVPTask,\n DatabaseStore,\n DatabaseWriter,\n AccountStore} = require 'nylas-exports'\n\nxdescribe \"EventRSVPTask\", ->\n  beforeEach ->\n    spyOn(DatabaseStore, 'find').andCallFake => Promise.resolve(@event)\n    spyOn(DatabaseWriter.prototype, 'persistModel').andCallFake -> Promise.resolve()\n    @myName = \"Ben Tester\"\n    @myEmail = \"tester@nylas.com\"\n    @event = new Event\n      id: '12233AEDF5'\n      accountId: TEST_ACCOUNT_ID\n      title: 'Meeting with Ben Bitdiddle'\n      description: ''\n      location: ''\n      when:\n        end_time: 1408123800\n        start_time: 1408120200\n      start: 1408120200\n      end: 1408123800\n      participants: [\n        {\"name\": \"Ben Bitdiddle\",\n        \"email\": \"ben@bitdiddle.com\",\n        \"status\": \"yes\"},\n        {\"name\": @myName,\n        \"email\": @myEmail,\n        \"status\": 'noreply'}\n      ]\n    @task = new EventRSVPTask(@event, @myEmail, \"no\")\n\n  describe \"performLocal\", ->\n    it \"should mark our status as no\", ->\n      @task.performLocal()\n      advanceClock()\n      expect(@event.participants[1].status).toBe \"no\"\n\n    it \"should trigger an action to persist the change\", ->\n      @task.performLocal()\n      advanceClock()\n      expect(DatabaseWriter.prototype.persistModel).toHaveBeenCalled()\n\n  describe \"performRemote\", ->\n    it \"should make the POST request to the message endpoint\", ->\n      spyOn(NylasAPIRequest.prototype, 'run').andCallFake => new Promise (resolve,reject) ->\n      @task.performRemote()\n      options = NylasAPIRequest.prototype.run.mostRecentCall.object.options\n      expect(options.path).toBe(\"/send-rsvp\")\n      expect(options.method).toBe('POST')\n      expect(options.accountId).toBe(@event.accountId)\n      expect(options.body.event_id).toBe(@event.id)\n      expect(options.body.status).toBe(\"no\")\n\n  describe \"when the remote API request fails\", ->\n    beforeEach ->\n      spyOn(NylasAPIRequest.prototype, 'run').andCallFake -> Promise.reject(new APIError(body: '', statusCode: 400))\n\n    it \"should not be marked with the status\", ->\n      @event = new Event\n        id: '12233AEDF5'\n        title: 'Meeting with Ben Bitdiddle'\n        description: ''\n        location: ''\n        when:\n          end_time: 1408123800\n          start_time: 1408120200\n        start: 1408120200\n        end: 1408123800\n        participants: [\n          {\"name\": \"Ben Bitdiddle\",\n          \"email\": \"ben@bitdiddle.com\",\n          \"status\": \"yes\"},\n          {\"name\": @myName,\n          \"email\": @myEmail,\n          \"status\": 'noreply'}\n        ]\n      @task = new EventRSVPTask(@event, @myEmail, \"no\")\n      @task.performLocal()\n      @task.performRemote()\n      advanceClock()\n      expect(@event.participants[1].status).toBe \"noreply\"\n\n    it \"should trigger an action to persist the change\", ->\n      @task.performLocal()\n      @task.performRemote()\n      advanceClock()\n      expect(DatabaseWriter.prototype.persistModel).toHaveBeenCalled()\n"
  },
  {
    "path": "packages/client-app/spec/tasks/send-draft-task-spec.es6",
    "content": "import {\n  APIError,\n  Actions,\n  AccountStore,\n  DatabaseStore,\n  DatabaseWriter,\n  Message,\n  Contact,\n  Task,\n  SendDraftTask,\n  NylasAPI,\n  NylasAPIHelpers,\n  NylasAPIRequest,\n  SoundRegistry,\n  SyncbackMetadataTask,\n} from 'nylas-exports';\n\n\nconst DBt = DatabaseWriter.prototype;\nconst withoutWhitespace = (s) => s.replace(/[\\n\\r\\s]/g, '');\n\nxdescribe('SendDraftTask', function sendDraftTask() {\n  describe(\"assertDraftValidity\", () => {\n    it(\"rejects if there are still uploads on the draft\", () => {\n      const badTask = new SendDraftTask('1');\n      badTask.draft = new Message({\n        from: [new Contact({email: TEST_ACCOUNT_EMAIL})],\n        accountId: TEST_ACCOUNT_ID,\n        clientId: '1',\n        uploads: ['123'],\n      });\n      badTask.assertDraftValidity().then(() => {\n        throw new Error(\"Shouldn't succeed\");\n      })\n      .catch((err) => {\n        expect(err.message).toBe(\"Files have been added since you started sending this draft. Double-check the draft and click 'Send' again..\");\n      });\n    });\n\n    it(\"rejects if no from address is specified\", () => {\n      const badTask = new SendDraftTask('1');\n      badTask.draft = new Message({from: [],\n        uploads: [],\n        accountId: TEST_ACCOUNT_ID,\n        clientId: '1',\n      })\n      badTask.assertDraftValidity().then(() => {\n        throw new Error(\"Shouldn't succeed\");\n      })\n      .catch((err) => {\n        expect(err.message).toBe(\"SendDraftTask - you must populate `from` before sending.\");\n      });\n    });\n\n    it(\"rejects if the from address does not map to any account\", () => {\n      const badTask = new SendDraftTask('1');\n      badTask.draft = new Message({\n        from: [new Contact({email: 'not-configuredthis.nylas.com'})],\n        accountId: TEST_ACCOUNT_ID,\n        clientId: '1',\n      });\n      badTask.assertDraftValidity().then(() => {\n        throw new Error(\"Shouldn't succeed\");\n      })\n      .catch((err) => {\n        expect(err.message).toBe(\"SendDraftTask - you can only send drafts from a configured account.\");\n      });\n    });\n  });\n\n  describe(\"performRemote\", () => {\n    beforeEach(() => {\n      this.response = {\n        version: 2,\n        id: '1233123AEDF1',\n        account_id: TEST_ACCOUNT_ID,\n        from: [new Contact({email: TEST_ACCOUNT_EMAIL})],\n        subject: 'New Draft',\n        body: 'hello world',\n        to: [new Contact({\n          name: 'Dummy',\n          email: 'dummythis.nylas.com',\n        })],\n      };\n\n      spyOn(NylasAPIRequest.prototype, 'run').andCallFake((options) => {\n        if (options.success) { options.success(this.response); }\n        return Promise.resolve(this.response);\n      })\n      spyOn(NylasAPI, 'incrementRemoteChangeLock');\n      spyOn(NylasAPI, 'decrementRemoteChangeLock');\n      spyOn(NylasAPIHelpers, 'makeDraftDeletionRequest');\n      spyOn(DBt, 'unpersistModel').andReturn(Promise.resolve());\n      spyOn(DBt, 'persistModel').andReturn(Promise.resolve());\n      spyOn(SoundRegistry, \"playSound\");\n      spyOn(Actions, \"draftDeliverySucceeded\");\n    });\n\n    // The tests below are invoked twice, once with a new this.draft and one with a\n    // persisted this.draft.\n    const sharedTests = () => {\n      it(\"should return Task.Status.Success\", () => {\n        waitsForPromise(() => {\n          this.task.performLocal();\n          return this.task.performRemote().then((status) => {\n            expect(status).toBe(Task.Status.Success);\n          });\n        });\n      });\n\n      it(\"makes a send request with the correct data\", () => {\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          expect(NylasAPIRequest.prototype.run).toHaveBeenCalled();\n          expect(NylasAPIRequest.prototype.run.callCount).toBe(1);\n          const options = NylasAPIRequest.prototype.run.mostRecentCall.args[0];\n          expect(options.path).toBe(\"/send\");\n          expect(options.method).toBe('POST');\n          expect(options.accountId).toBe(TEST_ACCOUNT_ID);\n          expect(options.body).toEqual(this.draft.toJSON());\n        }));\n      });\n\n      it(\"should always send the draft body in the request body (joined attribute check)\", () => {\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          expect(NylasAPIRequest.prototype.run.calls.length).toBe(1);\n          const options = NylasAPIRequest.prototype.run.mostRecentCall.args[0];\n          expect(options.body.body).toBe('hello world');\n        }));\n      });\n\n      describe(\"saving the sent message\", () => {\n        it(\"should preserve the draft client id\", () => {\n          waitsForPromise(() => this.task.performRemote().then(() => {\n            expect(DBt.persistModel).toHaveBeenCalled();\n            const model = DBt.persistModel.mostRecentCall.args[0];\n            expect(model.clientId).toEqual(this.draft.clientId);\n            expect(model.serverId).toEqual(this.response.id);\n            expect(model.draft).toEqual(false);\n          }));\n        });\n\n        it(\"should preserve metadata, but not version numbers\", () => {\n          waitsForPromise(() => this.task.performRemote().then(() => {\n            expect(DBt.persistModel).toHaveBeenCalled();\n            const model = DBt.persistModel.mostRecentCall.args[0];\n\n            expect(model.pluginMetadata.length).toEqual(this.draft.pluginMetadata.length);\n\n            for (const {pluginId, value} of this.draft.pluginMetadata) {\n              const updated = model.metadataObjectForPluginId(pluginId);\n              expect(updated.value).toEqual(value);\n              expect(updated.version).toEqual(0);\n            }\n          }));\n        });\n      });\n\n      it(\"should notify the draft was sent\", () => {\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          const args = Actions.draftDeliverySucceeded.calls[0].args[0];\n          expect(args.message instanceof Message).toBe(true)\n          expect(args.messageClientId).toBe(this.draft.clientId)\n        }));\n      });\n\n      it(\"should queue tasks to sync back the metadata on the new message\", () => {\n        spyOn(Actions, 'queueTask')\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          let metadataTasks = Actions.queueTask.calls.map((call) => call.args[0]);\n          metadataTasks = metadataTasks.filter((task) => task instanceof SyncbackMetadataTask)\n          expect(metadataTasks.length).toEqual(this.draft.pluginMetadata.length);\n          this.draft.pluginMetadata.forEach((pluginMetadatum, idx) => {\n            expect(metadataTasks[idx].clientId).toEqual(this.draft.clientId);\n            expect(metadataTasks[idx].modelClassName).toEqual('Message');\n            expect(metadataTasks[idx].pluginId).toEqual(pluginMetadatum.pluginId);\n          });\n        }));\n      });\n\n      it(\"should play a sound\", () => {\n        spyOn(NylasEnv.config, \"get\").andReturn(true)\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          expect(NylasEnv.config.get).toHaveBeenCalledWith(\"core.sending.sounds\");\n          expect(SoundRegistry.playSound).toHaveBeenCalledWith(\"send\");\n        }));\n      });\n\n      it(\"shouldn't play a sound if the config is disabled\", () => {\n        spyOn(NylasEnv.config, \"get\").andReturn(false)\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          expect(NylasEnv.config.get).toHaveBeenCalledWith(\"core.sending.sounds\");\n          expect(SoundRegistry.playSound).not.toHaveBeenCalled();\n        }));\n      });\n\n      describe(\"when there are errors\", () => {\n        beforeEach(() => {\n          spyOn(Actions, 'draftDeliveryFailed');\n          jasmine.unspy(NylasAPIRequest.prototype, \"run\");\n        });\n\n        it(\"notifies of a permanent error of misc error types\", () => {\n          // DB error\n          let thrownError = null;\n          spyOn(NylasEnv, \"reportError\");\n          jasmine.unspy(DBt, \"persistModel\");\n          spyOn(DBt, \"persistModel\").andCallFake(() => {\n            thrownError = new Error('db error');\n            throw thrownError;\n          });\n          waitsForPromise(() => this.task.performRemote().then((status) => {\n            expect(status[0]).toBe(Task.Status.Failed);\n            expect(status[1]).toBe(thrownError);\n            expect(Actions.draftDeliveryFailed).toHaveBeenCalled();\n            expect(NylasEnv.reportError).toHaveBeenCalled();\n          }));\n        });\n\n        it(\"retries the task if 'Invalid message public id'\", () => {\n          spyOn(NylasAPIRequest.prototype, 'run').andCallFake((options) => {\n            if (options.body.reply_to_message_id) {\n              const err = new APIError({body: \"Invalid message public id\"});\n              return Promise.reject(err);\n            }\n            if (options.success) { options.success(this.response) }\n            return Promise.resolve(this.response);\n          });\n\n          this.draft.replyToMessageId = \"reply-123\";\n          this.draft.threadId = \"thread-123\";\n          waitsForPromise(() => {\n            return this.task.performRemote(this.draft)\n            .then(() => {\n              expect(NylasAPIRequest.prototype.run).toHaveBeenCalled();\n              expect(NylasAPIRequest.prototype.run.callCount).toEqual(2);\n              const req1 = NylasAPIRequest.prototype.run.calls[0].args[0];\n              const req2 = NylasAPIRequest.prototype.run.calls[1].args[0];\n              expect(req1.body.reply_to_message_id).toBe(\"reply-123\");\n              expect(req1.body.thread_id).toBe(\"thread-123\");\n\n              expect(req2.body.reply_to_message_id).toBe(null);\n              expect(req2.body.thread_id).toBe(\"thread-123\");\n            })\n          });\n        });\n\n        it(\"retries the task if 'Invalid message public id'\", () => {\n          spyOn(NylasAPIRequest.prototype, 'run').andCallFake((options) => {\n            if (options.body.reply_to_message_id) {\n              return Promise.reject(new APIError({body: \"Invalid thread\"}));\n            }\n            if (options.success) { options.success(this.response) }\n            return Promise.resolve(this.response);\n          });\n\n          this.draft.replyToMessageId = \"reply-123\";\n          this.draft.threadId = \"thread-123\";\n          waitsForPromise(() => this.task.performRemote(this.draft).then(() => {\n            expect(NylasAPIRequest.prototype.run).toHaveBeenCalled();\n            expect(NylasAPIRequest.prototype.run.callCount).toEqual(2);\n            const req1 = NylasAPIRequest.prototype.run.calls[0].args[0];\n            const req2 = NylasAPIRequest.prototype.run.calls[1].args[0];\n            expect(req1.body.reply_to_message_id).toBe(\"reply-123\");\n            expect(req1.body.thread_id).toBe(\"thread-123\");\n\n            expect(req2.body.reply_to_message_id).toBe(null);\n            expect(req2.body.thread_id).toBe(null);\n          }));\n        });\n\n        it(\"notifies of a permanent error on 500 errors\", () => {\n          const thrownError = new APIError({statusCode: 500, body: \"err\"})\n          spyOn(NylasEnv, \"reportError\");\n          spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n\n          waitsForPromise(() => this.task.performRemote().then((status) => {\n            expect(status[0]).toBe(Task.Status.Failed);\n            expect(status[1]).toBe(thrownError);\n            expect(Actions.draftDeliveryFailed).toHaveBeenCalled();\n          }));\n        });\n\n        it(\"notifies us and users of a permanent error on 400 errors\", () => {\n          const thrownError = new APIError({statusCode: 400, body: \"err\"});\n          spyOn(NylasEnv, \"reportError\");\n          spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n\n          waitsForPromise(() => this.task.performRemote().then((status) => {\n            expect(status[0]).toBe(Task.Status.Failed);\n            expect(status[1]).toBe(thrownError);\n            expect(Actions.draftDeliveryFailed).toHaveBeenCalled();\n          }));\n        });\n\n        it(\"presents helpful error messages for 402 errors (security blocked)\", () => {\n          const thrownError = new APIError({\n            statusCode: 402,\n            body: {\n              message: \"Message content rejected for security reasons\",\n              server_error: \"552 : 5.7.0 This message was blocked because its content presents a potential\\n5.7.0 security issue. Please visit\\n5.7.0  https://support.google.com/mail/answer/6590 to review our message\\n5.7.0 content and attachment content guidelines. fk9sm21147314pad.9 - gsmtp\",\n              type: \"api_error\",\n            },\n          });\n\n          const expectedMessage = `\n            Sorry, this message could not be sent because it was rejected by your mail provider. (Message content rejected for security reasons)\n\n            552 : 5.7.0 This message was blocked because its content presents a potential\n            5.7.0 security issue. Please visit\n            5.7.0  https://support.google.com/mail/answer/6590 to review our message\n            5.7.0 content and attachment content guidelines. fk9sm21147314pad.9 - gsmtp\n          `\n\n          spyOn(NylasEnv, \"reportError\");\n          spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n\n          waitsForPromise(() => this.task.performRemote().then((status) => {\n            expect(status[0]).toBe(Task.Status.Failed);\n            expect(status[1]).toBe(thrownError);\n            expect(Actions.draftDeliveryFailed).toHaveBeenCalled();\n\n            const msg = Actions.draftDeliveryFailed.calls[0].args[0].errorMessage;\n            expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage));\n          }));\n        });\n\n        it(\"presents helpful error messages for 402 errors (recipient failed)\", () => {\n          const thrownError = new APIError({\n            statusCode: 402,\n            body: {\n              message: \"Sending to at least one recipient failed.\",\n              server_error: \"<<Don't know what this looks like >>\",\n              type: \"api_error\",\n            },\n          })\n\n          const expectedMessage = \"This message could not be delivered to at least one recipient. (Note: other recipients may have received this message - you should check Sent Mail before re-sending this message.)\"\n\n          spyOn(NylasEnv, \"reportError\");\n          spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n          waitsForPromise(() => this.task.performRemote().then((status) => {\n            expect(status[0]).toBe(Task.Status.Failed);\n            expect(status[1]).toBe(thrownError);\n            expect(Actions.draftDeliveryFailed).toHaveBeenCalled();\n\n            const msg = Actions.draftDeliveryFailed.calls[0].args[0].errorMessage;\n            expect(withoutWhitespace(msg)).toEqual(withoutWhitespace(expectedMessage));\n          }));\n        });\n\n        describe(\"checking the promise chain halts on errors\", () => {\n          beforeEach(() => {\n            spyOn(NylasEnv, 'reportError');\n            spyOn(this.task, \"sendMessage\").andCallThrough();\n            spyOn(this.task, \"onSuccess\").andCallThrough();\n            spyOn(this.task, \"onError\").andCallThrough();\n\n            this.expectBlockedChain = () => {\n              expect(this.task.sendMessage).toHaveBeenCalled();\n              expect(this.task.onSuccess).not.toHaveBeenCalled();\n              expect(this.task.onError).toHaveBeenCalled();\n            };\n          });\n\n          it(\"halts on 500s\", () => {\n            const thrownError = new APIError({statusCode: 500, body: \"err\"});\n            spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n            waitsForPromise(() => this.task.performRemote().then(() =>\n              this.expectBlockedChain()\n            ))\n          });\n\n          it(\"halts on 400s\", () => {\n            const thrownError = new APIError({statusCode: 400, body: \"err\"});\n            spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n            waitsForPromise(() => this.task.performRemote().then(() =>\n              this.expectBlockedChain()\n            ))\n          });\n\n          it(\"halts and retries on not permanent error codes\", () => {\n            const thrownError = new APIError({statusCode: 409, body: \"err\"});\n            spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n            waitsForPromise(() => this.task.performRemote().then(() =>\n              this.expectBlockedChain()\n            ))\n          });\n\n          it(\"halts on other errors\", () => {\n            const thrownError = new Error(\"oh no\");\n            spyOn(NylasAPIRequest.prototype, 'run').andReturn(Promise.reject(thrownError));\n            waitsForPromise(() => this.task.performRemote().then(() =>\n              this.expectBlockedChain()\n            ))\n          });\n\n          it(\"doesn't halt on success\", () => {\n            // Don't spy reportError to make sure to fail the test on unexpected\n            // errors\n            jasmine.unspy(NylasEnv, 'reportError');\n            spyOn(NylasAPIRequest.prototype, 'run').andCallFake((options) => {\n              if (options.success) { options.success(this.response) }\n              return Promise.resolve(this.response);\n            });\n\n            waitsForPromise(() => this.task.performRemote().then((status) => {\n              expect(status).toBe(Task.Status.Success);\n              expect(this.task.sendMessage).toHaveBeenCalled();\n              expect(this.task.onSuccess).toHaveBeenCalled();\n              expect(this.task.onError).not.toHaveBeenCalled();\n            }));\n          });\n        });\n      });\n    };\n\n    describe(\"with a new draft\", () => {\n      beforeEach(() => {\n        this.draft = new Message({\n          version: 1,\n          clientId: 'client-id',\n          accountId: TEST_ACCOUNT_ID,\n          from: [new Contact({email: TEST_ACCOUNT_EMAIL})],\n          subject: 'New Draft',\n          draft: true,\n          body: 'hello world',\n          uploads: [],\n        });\n\n        this.draft.applyPluginMetadata('pluginIdA', {tracked: true});\n        this.draft.applyPluginMetadata('pluginIdB', {a: true, b: 2});\n        this.draft.metadataObjectForPluginId('pluginIdA').version = 2;\n\n        this.task = new SendDraftTask('client-id');\n        this.calledBody = \"ERROR: The body wasn't included!\";\n        spyOn(DatabaseStore, \"run\").andReturn(Promise.resolve(this.draft));\n      });\n\n      sharedTests();\n\n      it(\"should locally convert the draft to a message on send\", () => {\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          expect(DBt.persistModel).toHaveBeenCalled();\n          const model = DBt.persistModel.calls[0].args[0];\n          expect(model.clientId).toBe(this.draft.clientId);\n          expect(model.serverId).toBe(this.response.id);\n          expect(model.draft).toBe(false);\n        }));\n      });\n    });\n\n    describe(\"with an existing persisted draft\", () => {\n      beforeEach(() => {\n        this.draft = new Message({\n          version: 1,\n          clientId: 'client-id',\n          serverId: 'server-123',\n          accountId: TEST_ACCOUNT_ID,\n          from: [new Contact({email: TEST_ACCOUNT_EMAIL})],\n          subject: 'New Draft',\n          draft: true,\n          body: 'hello world',\n          to: [new Contact({\n            name: 'Dummy',\n            email: 'dummythis.nylas.com',\n          })],\n          uploads: [],\n        });\n\n        this.draft.applyPluginMetadata('pluginIdA', {tracked: true});\n        this.draft.applyPluginMetadata('pluginIdB', {a: true, b: 2});\n        this.draft.metadataObjectForPluginId('pluginIdA').version = 2;\n\n        this.task = new SendDraftTask('client-id');\n        this.calledBody = \"ERROR: The body wasn't included!\";\n        spyOn(DatabaseStore, \"run\").andReturn(Promise.resolve(this.draft));\n      });\n\n      sharedTests();\n\n      it(\"should call makeDraftDeletionRequest to delete the draft after sending\", () => {\n        this.task.performLocal();\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          expect(NylasAPIHelpers.makeDraftDeletionRequest).toHaveBeenCalled()\n        }));\n      });\n\n      it(\"should locally convert the existing draft to a message on send\", () => {\n        expect(this.draft.clientId).toBe(this.draft.clientId);\n        expect(this.draft.serverId).toBe(\"server-123\");\n\n        this.task.performLocal();\n        waitsForPromise(() => this.task.performRemote().then(() => {\n          expect(DBt.persistModel).toHaveBeenCalled()\n          const model = DBt.persistModel.calls[0].args[0];\n          expect(model.clientId).toBe(this.draft.clientId);\n          expect(model.serverId).toBe(this.response.id);\n          expect(model.draft).toBe(false);\n        }));\n      });\n    });\n  });\n\n  describe(\"hasCustomBodyPerRecipient\", () => {\n    beforeEach(() => {\n      this.task = new SendDraftTask('client-id');\n      this.task.allowMultiSend = true;\n      this.task.draft = new Message({\n        version: 1,\n        clientId: 'client-id',\n        serverId: 'server-123',\n        accountId: TEST_ACCOUNT_ID,\n        from: [new Contact({email: TEST_ACCOUNT_EMAIL})],\n        subject: 'New Draft',\n        draft: true,\n        body: 'hello world',\n        to: [new Contact({\n          name: 'Dummy',\n          email: 'dummythis.nylas.com',\n        })],\n        uploads: [],\n      });\n      this.task.draft.applyPluginMetadata('open-tracking', true);\n      this.task.draft.applyPluginMetadata('link-tracking', true);\n\n      this.applySpies = (customValues = {}) => {\n        let value = {provider: customValues[\"AccountStore.accountForId\"] || \"gmail\"}\n        spyOn(AccountStore, \"accountForId\").andReturn(value)\n\n        value = customValues[\"NylasEnv.packages.pluginIdFor\"] || (name => name)\n        spyOn(NylasEnv.packages, \"pluginIdFor\").andCallFake(value);\n\n        value = {length: customValues[\"draft.participants\"] || 5}\n        spyOn(this.task.draft, \"participants\").andReturn(value);\n      }\n    });\n\n    it(\"should return false if the provider is eas\", () => {\n      this.applySpies({\"AccountStore.accountForId\": \"eas\"})\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(false);\n    });\n\n    it(\"should return false if allowMultiSend is false\", () => {\n      this.applySpies();\n      this.task.allowMultiSend = false;\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(false);\n    });\n\n    it(\"should return false if the open-tracking id is null\", () => {\n      const fake = (name) => {\n        return name === \"open-tracking\" ? null : name;\n      };\n      this.applySpies({\"NylasEnv.packages.pluginIdFor\": fake});\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(false);\n    });\n\n    it(\"should return false if the link-tracking id is null\", () => {\n      const fake = (name) => {\n        return name === \"link-tracking\" ? null : name;\n      };\n      this.applySpies({\"NylasEnv.packages.pluginIdFor\": fake});\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(false);\n    });\n\n    it(\"should return false if neither open-tracking nor link-tracking is on\", () => {\n      this.applySpies();\n      this.task.draft.applyPluginMetadata('open-tracking', false);\n      this.task.draft.applyPluginMetadata('link-tracking', false);\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(false);\n    });\n\n    it(\"should return true if only open-tracking is on\", () => {\n      this.applySpies();\n      this.task.draft.applyPluginMetadata('link-tracking', false);\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(true);\n    });\n\n    it(\"should return true if only link-tracking is on\", () => {\n      this.applySpies();\n      this.task.draft.applyPluginMetadata('open-tracking', false);\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(true);\n    });\n\n    it(\"should return false if there are too many participants\", () => {\n      this.applySpies({\"draft.participants\": 15});\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(false);\n    });\n\n    it(\"should return true otherwise\", () => {\n      this.applySpies();\n      expect(this.task.hasCustomBodyPerRecipient()).toBe(true);\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/tasks/syncback-category-task-spec.coffee",
    "content": "{NylasAPI,\n NylasAPIRequest,\n Category,\n AccountStore,\n DatabaseStore,\n SyncbackCategoryTask,\n DatabaseWriter} = require \"nylas-exports\"\n\nxdescribe \"SyncbackCategoryTask\", ->\n  describe \"performRemote\", ->\n    pathOf = (fn) ->\n      fn.calls[0].args[0].path\n\n    accountIdOf = (fn) ->\n      fn.calls[0].args[0].accountId\n\n    nameOf = (fn) ->\n      fn.calls[0].args[0].body.display_name\n\n    makeAccount = ({usesFolders, usesLabels} = {}) ->\n      spyOn(AccountStore, \"accountForId\").andReturn {\n        usesFolders: -> usesFolders\n        usesLabels: -> usesLabels\n      }\n\n    makeTask = ->\n      category = new Category\n        displayName: \"important emails\"\n        accountId: \"account 123\"\n        clientId: \"local-444\"\n      new SyncbackCategoryTask\n        category: category\n\n    beforeEach ->\n      spyOn(NylasAPIRequest.prototype, \"run\").andCallFake ->\n        Promise.resolve(id: \"server-444\")\n      spyOn(DatabaseWriter.prototype, \"persistModel\")\n\n    it \"sends API req to /labels if the account uses labels\", ->\n      makeAccount(usesLabels: true)\n      task = makeTask()\n      task.performRemote({})\n      expect(pathOf(NylasAPIRequest.prototype.run)).toBe \"/labels\"\n\n    it \"sends API req to /folders if the account uses folders\", ->\n      makeAccount(usesFolders: true)\n      task = makeTask()\n      task.performRemote({})\n      expect(pathOf(NylasAPIRequest.prototype.run)).toBe \"/folders\"\n\n    it \"sends the account id\", ->\n      makeAccount()\n      task = makeTask()\n      task.performRemote({})\n      expect(accountIdOf(NylasAPIRequest.prototype.run)).toBe \"account 123\"\n\n    it \"sends the display name in the body\", ->\n      makeAccount()\n      task = makeTask()\n      task.performRemote({})\n      expect(nameOf(NylasAPIRequest.prototype.run)).toBe \"important emails\"\n\n    it \"adds server id to the category, then saves the category\", ->\n      makeAccount()\n      waitsForPromise ->\n        task = makeTask()\n        task.performRemote({})\n        .then ->\n          expect(DatabaseWriter.prototype.persistModel).toHaveBeenCalled()\n          model = DatabaseWriter.prototype.persistModel.calls[0].args[0]\n          expect(model.clientId).toBe \"local-444\"\n          expect(model.serverId).toBe \"server-444\"\n"
  },
  {
    "path": "packages/client-app/spec/tasks/syncback-metadata-task-spec.es6",
    "content": "import {NylasAPIRequest, Message, SyncbackMetadataTask, Thread} from 'nylas-exports'\n\ndescribe(\"SyncbackMetadataTask\", () => {\n  it(\"sends messageIds if the object is a Thread\", () => {\n    spyOn(NylasAPIRequest.prototype, 'run').andCallFake(function fakeRun() {\n      return Promise.resolve(this)\n    })\n    const thread = new Thread({serverId: 't:5'})\n    thread.applyPluginMetadata('test-plugin', {key: 'value'})\n    const messages = [\n      new Message({threadId: thread.id}),\n      new Message({threadId: thread.id}),\n    ]\n    thread.messages = () => messages\n    const task = new SyncbackMetadataTask(thread.clientId, 'Thread', 'test-plugin')\n    waitsForPromise(() => task.makeRequest(thread).then(nylasAPIRequest => {\n      expect(nylasAPIRequest.options.body.messageIds).toEqual(messages.map(m => m.id))\n    }))\n  })\n\n  it(\"does not send messageIds if the object is not a Thread\", () => {\n    spyOn(NylasAPIRequest.prototype, 'run').andCallFake(function fakeRun() {\n      return Promise.resolve(this)\n    })\n    const message = new Message({serverId: '5'})\n    message.applyPluginMetadata('test-plugin', {key: 'value'})\n    const task = new SyncbackMetadataTask(message.clientId, 'Message', 'test-plugin')\n    waitsForPromise(() => task.makeRequest(message).then(nylasAPIRequest => {\n      expect(nylasAPIRequest.options.body.messageIds).toBeUndefined()\n    }))\n  })\n})\n"
  },
  {
    "path": "packages/client-app/spec/tasks/syncback-model-task-spec.es6",
    "content": "import {\n  Task,\n  NylasAPIRequest,\n  APIError,\n  Model,\n  DatabaseStore,\n  SyncbackModelTask,\n  DatabaseWriter } from 'nylas-exports'\n\nclass TestTask extends SyncbackModelTask {\n  getModelConstructor() {\n    return Model\n  }\n}\n\nxdescribe('SyncbackModelTask', function syncbackModelTask() {\n  beforeEach(() => {\n    this.testModel = new Model({accountId: 'account-123'})\n    spyOn(DatabaseWriter.prototype, \"persistModel\")\n    spyOn(DatabaseStore, \"findBy\").andReturn(Promise.resolve(this.testModel));\n\n    spyOn(NylasEnv, \"reportError\")\n    spyOn(NylasAPIRequest.prototype, \"run\").andReturn(Promise.resolve({\n      version: 10,\n      id: \"server-123\",\n    }))\n  });\n\n  const performRemote = (fn) => {\n    window.waitsForPromise(() => {\n      return this.task.performRemote().then(fn)\n    });\n  }\n\n  describe(\"performLocal\", () => {\n    it(\"throws if basic fields are missing\", () => {\n      const t = new SyncbackModelTask()\n      try {\n        t.performLocal()\n        throw new Error(\"Shouldn't succeed\");\n      } catch (e) {\n        expect(e.message).toMatch(/^Must pass.*/)\n      }\n    });\n  });\n\n  describe(\"performRemote\", () => {\n    beforeEach(() => {\n      this.task = new TestTask({\n        clientId: \"local-123\",\n        endpoint: \"/test\",\n      })\n    });\n\n    it(\"fetches the latest model\", () => {\n      spyOn(this.task, \"getLatestModel\").andCallThrough()\n      spyOn(this.task, \"verifyModel\").andCallThrough()\n      performRemote(() => {\n        expect(this.task.getLatestModel).toHaveBeenCalled()\n        const model = this.task.verifyModel.calls[0].args[0]\n        expect(model).toBe(this.testModel)\n      })\n    });\n\n    it(\"throws an error if getLatestModel hasn't been implemented\", () => {\n      const bumTask = new SyncbackModelTask({clientId: 'local-123'});\n      spyOn(this.task, \"getModelConstructor\").andCallThrough()\n      window.waitsForPromise(() => {\n        return bumTask.performRemote().then((err) => {\n          expect(err[0]).toBe(Task.Status.Failed)\n          expect(err[1].message).toMatch(/must subclass/)\n        })\n      });\n    });\n\n    it(\"verifies the model\", () => {\n      spyOn(this.task, \"verifyModel\").andCallThrough()\n      spyOn(this.task, \"makeRequest\").andCallThrough()\n      performRemote(() => {\n        expect(this.task.verifyModel).toHaveBeenCalled()\n        const model = this.task.makeRequest.calls[0].args[0]\n        expect(model).toBe(this.testModel)\n      })\n    });\n\n    it(\"gets the correct path and method for existing objects\", () => {\n      jasmine.unspy(DatabaseStore, \"findBy\")\n      const serverModel = new Model({clientId: 'local-123', serverId: 'server-123'})\n\n      spyOn(DatabaseStore, \"findBy\").andReturn(Promise.resolve(serverModel));\n\n      spyOn(this.task, \"getRequestData\").andCallThrough();\n\n      performRemote(() => {\n        expect(this.task.getRequestData).toHaveBeenCalled()\n        const opts = NylasAPIRequest.prototype.run.calls[0].args[0]\n        expect(opts.path).toBe(\"/test/server-123\")\n        expect(opts.method).toBe(\"PUT\")\n      });\n    });\n\n    it(\"gets the correct path and method for new objects\", () => {\n      spyOn(this.task, \"getRequestData\").andCallThrough();\n\n      performRemote(() => {\n        expect(this.task.getRequestData).toHaveBeenCalled()\n        const opts = NylasAPIRequest.prototype.run.calls[0].args[0]\n        expect(opts.path).toBe(\"/test\")\n        expect(opts.method).toBe(\"POST\")\n      });\n    });\n\n    it(\"lets tasks override path and method\", () => {\n      class TaskMethodAndPath extends SyncbackModelTask {\n        getModelConstructor() {\n          return Model\n        }\n        getRequestData = () => {\n          return {\n            path: `/override`,\n            method: \"DELETE\",\n          }\n        };\n      }\n      const task = new TaskMethodAndPath({clientId: 'local-123'});\n      spyOn(task, \"getRequestData\").andCallThrough();\n      spyOn(task, \"getModelConstructor\").andCallThrough()\n      window.waitsForPromise(() => {\n        return task.performRemote().then(() => {\n          expect(task.getRequestData).toHaveBeenCalled()\n          const opts = NylasAPIRequest.prototype.run.calls[0].args[0]\n          expect(opts.path).toBe(\"/override\")\n          expect(opts.method).toBe(\"DELETE\")\n        })\n      });\n    });\n\n    it(\"makes a request with the correct data\", () => {\n      spyOn(this.task, \"makeRequest\").andCallThrough();\n\n      // So it doesn't get changed by the time we inspect it\n      spyOn(this.task, \"updateLocalModel\").andReturn(Promise.resolve())\n\n      performRemote(() => {\n        expect(this.task.makeRequest).toHaveBeenCalled()\n        const opts = NylasAPIRequest.prototype.run.calls[0].args[0]\n        expect(opts.path).toBe(\"/test\")\n        expect(opts.method).toBe(\"POST\")\n        expect(opts.accountId).toBe(\"account-123\")\n        expect(opts.body).toEqual(this.testModel.toJSON())\n      });\n    });\n\n    it(\"updates the local model with only the version and serverId\", () => {\n      spyOn(this.task, \"updateLocalModel\").andCallThrough()\n      performRemote(() => {\n        expect(this.task.updateLocalModel).toHaveBeenCalled();\n        const opts = this.task.updateLocalModel.calls[0].args[0]\n        expect(opts.version).toBe(10)\n        expect(opts.id).toBe(\"server-123\")\n        expect(DatabaseWriter.prototype.persistModel).toHaveBeenCalled()\n        const model = DatabaseWriter.prototype.persistModel.calls[0].args[0]\n        expect(model.serverId).toBe('server-123')\n        expect(model.version).toBe(10)\n      });\n    });\n\n    it(\"retries on retry-able API errors\", () => {\n      jasmine.unspy(NylasAPIRequest.prototype, \"run\");\n      const err = new APIError({statusCode: 420});\n      spyOn(NylasAPIRequest.prototype, \"run\").andReturn(Promise.reject(err))\n      performRemote((status) => {\n        expect(status).toBe(Task.Status.Retry)\n      });\n    });\n\n    it(\"failes on permanent errors\", () => {\n      jasmine.unspy(NylasAPIRequest.prototype, \"run\");\n      const err = new APIError({statusCode: 500});\n      spyOn(NylasAPIRequest.prototype, \"run\").andReturn(Promise.reject(err))\n      performRemote((status) => {\n        expect(status[0]).toBe(Task.Status.Failed)\n        expect(status[1].statusCode).toBe(500)\n      });\n    });\n\n    it(\"fails and notifies us on other types of errors\", () => {\n      const errMsg = \"This is a test error\"\n      spyOn(this.task, \"updateLocalModel\").andCallFake(() => {\n        throw new Error(errMsg)\n      })\n      performRemote((status) => {\n        expect(status[0]).toBe(Task.Status.Failed)\n        expect(status[1].message).toBe(errMsg)\n        expect(NylasEnv.reportError).toHaveBeenCalled()\n      });\n    });\n  });\n\n  describe(\"undo/redo\", () => {\n    it(\"cant be undone\", () => {\n      expect(this.task.canBeUndone()).toBe(false)\n    });\n\n    it(\"isn't an undo task\", () => {\n      expect(this.task.isUndo()).toBe(false)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/tasks/task-factory-spec.es6",
    "content": "import {\n  TaskFactory,\n  AccountStore,\n  CategoryStore,\n  Category,\n  Thread,\n  ChangeFolderTask,\n  ChangeLabelsTask,\n} from 'nylas-exports'\n\n\ndescribe('TaskFactory', function taskFactory() {\n  beforeEach(() => {\n    this.categories = {\n      'ac-1': {\n        archive: new Category({name: 'archive'}),\n        inbox: new Category({name: 'inbox1'}),\n        trash: new Category({name: 'trash1'}),\n      },\n      'ac-2': {\n        archive: new Category({name: 'all'}),\n        inbox: new Category({name: 'inbox2'}),\n        trash: new Category({name: 'trash2'}),\n      },\n    }\n    this.accounts = {\n      'ac-1': {\n        id: 'ac-1',\n        usesFolders: () => true,\n        defaultFinishedCategory: () => this.categories['ac-1'].archive,\n      },\n      'ac-2': {\n        id: 'ac-2',\n        usesFolders: () => false,\n        defaultFinishedCategory: () => this.categories['ac-2'].trash,\n      },\n    }\n    this.threads = [\n      new Thread({accountId: 'ac-1'}),\n      new Thread({accountId: 'ac-2'}),\n    ]\n\n    spyOn(CategoryStore, 'getArchiveCategory').andCallFake((acc) => {\n      return this.categories[acc.id].archive\n    })\n    spyOn(CategoryStore, 'getInboxCategory').andCallFake((acc) => {\n      return this.categories[acc.id].inbox\n    })\n    spyOn(CategoryStore, 'getTrashCategory').andCallFake((acc) => {\n      return this.categories[acc.id].trash\n    })\n    spyOn(AccountStore, 'accountForId').andCallFake((accId) => {\n      return this.accounts[accId];\n    })\n  });\n\n  describe('tasksForApplyingCategories', () => {\n    it('creates the correct tasks', () => {\n      const categoriesToRemove = (accId) => {\n        if (accId === 'ac-1') {\n          return [new Category({displayName: 'folder1', accountId: 'ac-1'})]\n        }\n        return [new Category({displayName: 'label2', accountId: 'ac-2'})]\n      }\n      const categoriesToAdd = (accId) => [this.categories[accId].inbox]\n      const taskDescription = 'dope'\n\n      const tasks = TaskFactory.tasksForApplyingCategories({\n        threads: this.threads,\n        categoriesToAdd,\n        categoriesToRemove,\n        taskDescription,\n      })\n\n      expect(tasks.length).toEqual(2)\n      const taskExchange = tasks[0]\n      const taskGmail = tasks[1]\n\n      expect(taskExchange instanceof ChangeFolderTask).toBe(true)\n      expect(taskExchange.folder.name).toEqual('inbox1')\n      expect(taskExchange.taskDescription).toEqual(taskDescription)\n\n      expect(taskGmail instanceof ChangeLabelsTask).toBe(true)\n      expect(taskGmail.labelsToAdd.length).toEqual(1)\n      expect(taskGmail.labelsToAdd[0].name).toEqual('inbox2')\n      expect(taskGmail.labelsToRemove.length).toEqual(1)\n      expect(taskGmail.labelsToRemove[0].displayName).toEqual('label2')\n      expect(taskGmail.taskDescription).toEqual(taskDescription)\n    });\n\n    it('throws if threads are not instances of Thread', () => {\n      const threads = [\n        {accountId: 'ac-1'},\n        {accountId: 'ac-2'},\n      ]\n      expect(() => {\n        TaskFactory.tasksForApplyingCategories({threads})\n      }).toThrow()\n    });\n\n    it('throws if categoriesToAdd does not return an array', () => {\n      expect(() => {\n        TaskFactory.tasksForApplyingCategories({\n          threads: this.threads,\n          categoriesToAdd: {displayName: 'cat1'},\n        })\n      }).toThrow()\n    });\n\n    it('throws if categoriesToAdd does not return an array', () => {\n      expect(() => {\n        TaskFactory.tasksForApplyingCategories({\n          threads: this.threads,\n          categoriesToRemove: {displayName: 'cat1'},\n        })\n      }).toThrow()\n    });\n\n    it('does not create folder tasks if categoriesToAdd not present', () => {\n      const categoriesToRemove = (accId) => {\n        if (accId === 'ac-1') {\n          return [new Category({displayName: 'folder1', accountId: 'ac-1'})]\n        }\n        return [new Category({displayName: 'label2', accountId: 'ac-2'})]\n      }\n      const taskDescription = 'dope'\n\n      const tasks = TaskFactory.tasksForApplyingCategories({\n        threads: this.threads,\n        categoriesToRemove,\n        taskDescription,\n      })\n      expect(tasks.length).toEqual(1)\n      const taskGmail = tasks[0]\n      expect(taskGmail instanceof ChangeLabelsTask).toBe(true)\n      expect(taskGmail.labelsToRemove.length).toEqual(1)\n    });\n\n    it('does not create label tasks if both categoriesToAdd and categoriesToRemove return empty', () => {\n      const categoriesToAdd = (accId) => {\n        return accId === 'ac-1' ? [this.categories[accId].inbox] : [];\n      }\n      const taskDescription = 'dope'\n\n      const tasks = TaskFactory.tasksForApplyingCategories({\n        threads: this.threads,\n        categoriesToAdd,\n        taskDescription,\n      })\n      expect(tasks.length).toEqual(1)\n      const taskExchange = tasks[0]\n\n      expect(taskExchange instanceof ChangeFolderTask).toBe(true)\n      expect(taskExchange.folder.name).toEqual('inbox1')\n    });\n\n    describe('exchange accounts', () => {\n      it('throws if folder is not a category', () => {\n        expect(() => {\n          TaskFactory.tasksForApplyingCategories({\n            threads: this.threads,\n            categoriesToAdd: () => [{accountId: 'ac-1', name: 'inbox'}],\n          })\n        }).toThrow()\n      });\n\n      it('throws if attempting to add more than one folder', () => {\n        expect(() => {\n          TaskFactory.tasksForApplyingCategories({\n            threads: this.threads,\n            categoriesToAdd: () => [{accountId: 'ac-1', name: 'inbox'}, {}],\n          })\n        }).toThrow()\n      });\n    });\n  });\n\n  describe('taskForInvertingUnread', () => {\n\n  });\n\n  describe('taskForInvertingStarred', () => {\n\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/tasks/task-spec.coffee",
    "content": "Actions = require('../../src/flux/actions').default\nTaskQueue = require '../../src/flux/stores/task-queue'\nTask = require('../../src/flux/tasks/task').default\n\n{APIError} = require '../../src/flux/errors'\n\n{APITestTask, OKTask, BadTask} = require('../stores/task-subclass')\n\nnoop = ->\n\nxdescribe \"Task\", ->\n  describe \"initial state\", ->\n    it \"should set up queue state with additional information about local/remote\", ->\n      task = new Task()\n      expect(task.queueState).toEqual({ isProcessing : false, localError : null, localComplete : false, remoteError : null, remoteAttempts : 0, remoteComplete : false, status: null, debugStatus: Task.DebugStatus.JustConstructed})\n\n  describe \"runLocal\", ->\n    beforeEach ->\n      @task = new APITestTask()\n\n    describe \"when performLocal is not complete\", ->\n      it \"should run performLocal\", ->\n        spyOn(@task, 'performLocal').andCallThrough()\n        @task.runLocal()\n        expect(@task.performLocal).toHaveBeenCalled()\n\n      describe \"when performLocal rejects\", ->\n        beforeEach ->\n          spyOn(NylasEnv, \"reportError\")\n          spyOn(@task, 'performLocal').andCallFake =>\n            Promise.reject(new Error(\"Oh no!\"))\n\n        it \"should save the error to the queueState\", ->\n          @task.runLocal().catch(noop)\n          advanceClock()\n          expect(@task.performLocal).toHaveBeenCalled()\n          expect(@task.queueState.localComplete).toBe(false)\n          expect(@task.queueState.localError.message).toBe(\"Oh no!\")\n\n        it \"should reject with the error\", ->\n          rejection = null\n          runs ->\n            @task.runLocal().catch (err) ->\n              rejection = err\n          waitsFor ->\n            rejection\n          runs ->\n            expect(rejection.message).toBe(\"Oh no!\")\n\n      describe \"when performLocal resolves\", ->\n        beforeEach ->\n          spyOn(@task, 'performLocal').andCallFake -> Promise.resolve('Hooray')\n\n        it \"should save that performLocal is complete\", ->\n          @task.runLocal()\n          advanceClock()\n          expect(@task.queueState.localComplete).toBe(true)\n\n        it \"should save that there was no performLocal error\", ->\n          @task.runLocal()\n          advanceClock()\n          expect(@task.queueState.localError).toBe(null)\n\n    describe \"runRemote\", ->\n      beforeEach ->\n        @task.queueState.localComplete = true\n\n      it \"should run performRemote\", ->\n        spyOn(@task, 'performRemote').andCallThrough()\n        @task.runRemote()\n        advanceClock()\n        expect(@task.performRemote).toHaveBeenCalled()\n\n      it \"it should resolve Continue if it already ran\", ->\n        @task.queueState.remoteComplete = true\n        waitsForPromise =>\n          @task.runRemote().then (status) =>\n            expect(@task.queueState.status).toBe Task.Status.Continue\n            expect(status).toBe Task.Status.Continue\n\n      it \"marks as complete if the task 'continue's\", ->\n        spyOn(@task, 'performRemote').andCallFake ->\n          Promise.resolve(Task.Status.Continue)\n          @task.runRemote()\n          advanceClock()\n          expect(@task.performRemote).toHaveBeenCalled()\n          expect(@task.queueState.remoteError).toBe(null)\n          expect(@task.queueState.remoteComplete).toBe(true)\n          expect(@task.queueState.status).toBe(Task.Status.Continue)\n\n      it \"marks as failed if the task reverts\", ->\n        spyOn(@task, 'performRemote').andCallFake ->\n          Promise.resolve(Task.Status.Failed)\n          @task.runRemote()\n          advanceClock()\n          expect(@task.performRemote).toHaveBeenCalled()\n          expect(@task.queueState.remoteError).toBe(null)\n          expect(@task.queueState.remoteComplete).toBe(true)\n          expect(@task.queueState.status).toBe(Task.Status.Failed)\n\n      describe \"when performRemote resolves\", ->\n        beforeEach ->\n          spyOn(@task, 'performRemote').andCallFake ->\n            Promise.resolve(Task.Status.Success)\n\n        it \"should save that performRemote is complete with no errors\", ->\n          @task.runRemote()\n          advanceClock()\n          expect(@task.performRemote).toHaveBeenCalled()\n          expect(@task.queueState.remoteError).toBe(null)\n          expect(@task.queueState.remoteComplete).toBe(true)\n          expect(@task.queueState.status).toBe(Task.Status.Success)\n\n        it \"should only allow the performRemote method to return a Task.Status\", ->\n          result = null\n          err = null\n\n          @ok = new OKTask()\n          @ok.queueState.localComplete = true\n          @ok.runRemote().then (r) -> result = r\n          advanceClock()\n          expect(@ok.queueState.status).toBe(Task.Status.Retry)\n          expect(result).toBe(Task.Status.Retry)\n\n          @bad = new BadTask()\n          @bad.queueState.localComplete = true\n          @bad.runRemote().catch (e) -> err = e\n          advanceClock()\n          expect(err.message).toBe('performRemote returned lalal, which is not a Task.Status')\n\n      describe \"when performRemote rejects multiple times\", ->\n        beforeEach ->\n          spyOn(@task, 'performRemote').andCallFake =>\n            Promise.resolve(Task.Status.Failed)\n\n        it \"should increment the number of attempts\", ->\n          runs ->\n            @task.runRemote().catch(noop)\n          waitsFor ->\n            @task.queueState.remoteAttempts == 1\n          runs ->\n            @task.runRemote().catch(noop)\n          waitsFor ->\n            @task.queueState.remoteAttempts == 2\n\n      describe \"when performRemote resolves with Task.Status.Failed\", ->\n        beforeEach ->\n          spyOn(NylasEnv, \"reportError\")\n          @error = new APIError(\"Oh no!\")\n          spyOn(@task, 'performRemote').andCallFake =>\n            Promise.resolve(Task.Status.Failed)\n\n        it \"Should handle the error as a caught Failure\", ->\n          waitsForPromise =>\n            @task.runRemote().then ->\n              throw new Error(\"Should not resolve\")\n            .catch (err) =>\n              expect(@task.queueState.remoteError instanceof Error).toBe true\n              expect(@task.queueState.remoteAttempts).toBe(1)\n              expect(@task.queueState.status).toBe(Task.Status.Failed)\n              expect(NylasEnv.reportError).not.toHaveBeenCalled()\n\n      describe \"when performRemote resolves with Task.Status.Failed and an error\", ->\n        beforeEach ->\n          spyOn(NylasEnv, \"reportError\")\n          @error = new APIError(\"Oh no!\")\n          spyOn(@task, 'performRemote').andCallFake =>\n            Promise.resolve([Task.Status.Failed, @error])\n\n        it \"Should handle the error as a caught Failure\", ->\n          waitsForPromise =>\n            @task.runRemote().then ->\n              throw new Error(\"Should not resolve\")\n            .catch (err) =>\n              expect(@task.queueState.remoteError).toBe(@error)\n              expect(@task.queueState.remoteAttempts).toBe(1)\n              expect(@task.queueState.status).toBe(Task.Status.Failed)\n              expect(NylasEnv.reportError).not.toHaveBeenCalled()\n\n      describe \"when performRemote rejects with Task.Status.Failed\", ->\n        beforeEach ->\n          spyOn(NylasEnv, \"reportError\")\n          @error = new APIError(\"Oh no!\")\n          spyOn(@task, 'performRemote').andCallFake =>\n            Promise.reject([Task.Status.Failed, @error])\n\n        it \"Should handle the rejection as normal\", ->\n          waitsForPromise =>\n            @task.runRemote().then ->\n              throw new Error(\"Should not resolve\")\n            .catch (err) =>\n              expect(@task.queueState.remoteError).toBe(@error)\n              expect(@task.queueState.remoteAttempts).toBe(1)\n              expect(@task.queueState.status).toBe(Task.Status.Failed)\n              expect(NylasEnv.reportError).not.toHaveBeenCalled()\n\n      describe \"when performRemote throws an unknown error\", ->\n        beforeEach ->\n          spyOn(NylasEnv, \"reportError\")\n          @error = new Error(\"Oh no!\")\n          spyOn(@task, 'performRemote').andCallFake =>\n            throw @error\n\n        it \"Should handle the error as an uncaught error\", ->\n          waitsForPromise =>\n            @task.runRemote().then ->\n              throw new Error(\"Should not resolve\")\n            .catch (err) =>\n              expect(@task.queueState.remoteError).toBe(@error)\n              expect(@task.queueState.remoteAttempts).toBe(1)\n              expect(@task.queueState.status).toBe(Task.Status.Failed)\n              expect(@task.queueState.debugStatus).toBe(Task.DebugStatus.UncaughtError)\n              expect(NylasEnv.reportError).toHaveBeenCalledWith(@error)\n"
  },
  {
    "path": "packages/client-app/spec/themes/style-manager-spec.coffee",
    "content": "StyleManager = require '../../src/style-manager'\n\ndescribe \"StyleManager\", ->\n  [manager, addEvents, removeEvents, updateEvents] = []\n\n  beforeEach ->\n    manager = new StyleManager\n    addEvents = []\n    removeEvents = []\n    updateEvents = []\n\n    manager.onDidAddStyleElement (event) -> addEvents.push(event)\n    manager.onDidRemoveStyleElement (event) -> removeEvents.push(event)\n    manager.onDidUpdateStyleElement (event) -> updateEvents.push(event)\n\n  describe \"::addStyleSheet(source, params)\", ->\n    it \"adds a stylesheet based on the given source and returns a disposable allowing it to be removed\", ->\n      disposable = manager.addStyleSheet(\"a {color: red;}\")\n\n      expect(addEvents.length).toBe 1\n      expect(addEvents[0].textContent).toBe \"a {color: red;}\"\n\n      styleElements = manager.getStyleElements()\n      expect(styleElements.length).toBe 1\n      expect(styleElements[0].textContent).toBe \"a {color: red;}\"\n\n      disposable.dispose()\n\n      expect(removeEvents.length).toBe 1\n      expect(removeEvents[0].textContent).toBe \"a {color: red;}\"\n      expect(manager.getStyleElements().length).toBe 0\n\n    describe \"when a sourcePath parameter is specified\", ->\n      it \"ensures a maximum of one style element for the given source path, updating a previous if it exists\", ->\n        disposable1 = manager.addStyleSheet(\"a {color: red;}\", sourcePath: '/foo/bar')\n\n        expect(addEvents.length).toBe 1\n        expect(addEvents[0].getAttribute('source-path')).toBe '/foo/bar'\n\n        disposable2 = manager.addStyleSheet(\"a {color: blue;}\", sourcePath: '/foo/bar')\n\n        expect(addEvents.length).toBe 1\n        expect(updateEvents.length).toBe 1\n        expect(updateEvents[0].getAttribute('source-path')).toBe '/foo/bar'\n        expect(updateEvents[0].textContent).toBe \"a {color: blue;}\"\n\n        disposable2.dispose()\n        addEvents = []\n\n        manager.addStyleSheet(\"a {color: yellow;}\", sourcePath: '/foo/bar')\n\n        expect(addEvents.length).toBe 1\n        expect(addEvents[0].getAttribute('source-path')).toBe '/foo/bar'\n        expect(addEvents[0].textContent).toBe \"a {color: yellow;}\"\n\n    describe \"when a priority parameter is specified\", ->\n      it \"inserts the style sheet based on the priority\", ->\n        manager.addStyleSheet(\"a {color: red}\", priority: 1)\n        manager.addStyleSheet(\"a {color: blue}\", priority: 0)\n        manager.addStyleSheet(\"a {color: green}\", priority: 2)\n        manager.addStyleSheet(\"a {color: yellow}\", priority: 1)\n\n        expect(manager.getStyleElements().map (elt) -> elt.textContent).toEqual [\n          \"a {color: blue}\"\n          \"a {color: red}\"\n          \"a {color: yellow}\"\n          \"a {color: green}\"\n        ]\n"
  },
  {
    "path": "packages/client-app/spec/themes/styles-element-spec.coffee",
    "content": "StylesElement = require '../../src/styles-element'\nStyleManager = require '../../src/style-manager'\n\ndescribe \"StylesElement\", ->\n  [element, addedStyleElements, removedStyleElements, updatedStyleElements] = []\n\n  beforeEach ->\n    element = new StylesElement\n    document.querySelector('#jasmine-content').appendChild(element)\n    addedStyleElements = []\n    removedStyleElements = []\n    updatedStyleElements = []\n    element.onDidAddStyleElement (element) -> addedStyleElements.push(element)\n    element.onDidRemoveStyleElement (element) -> removedStyleElements.push(element)\n    element.onDidUpdateStyleElement (element) -> updatedStyleElements.push(element)\n\n  it \"renders a style tag for all currently active stylesheets in the style manager\", ->\n    initialChildCount = element.children.length\n\n    disposable1 = NylasEnv.styles.addStyleSheet(\"a {color: red;}\")\n    expect(element.children.length).toBe initialChildCount + 1\n    expect(element.children[initialChildCount].textContent).toBe \"a {color: red;}\"\n    expect(addedStyleElements).toEqual [element.children[initialChildCount]]\n\n    disposable2 = NylasEnv.styles.addStyleSheet(\"a {color: blue;}\")\n    expect(element.children.length).toBe initialChildCount + 2\n    expect(element.children[initialChildCount + 1].textContent).toBe \"a {color: blue;}\"\n    expect(addedStyleElements).toEqual [element.children[initialChildCount], element.children[initialChildCount + 1]]\n\n    disposable1.dispose()\n    expect(element.children.length).toBe initialChildCount + 1\n    expect(element.children[initialChildCount].textContent).toBe \"a {color: blue;}\"\n    expect(removedStyleElements).toEqual [addedStyleElements[0]]\n\n    disposable2.dispose()\n\n  it \"orders style elements by priority\", ->\n    initialChildCount = element.children.length\n\n    NylasEnv.styles.addStyleSheet(\"a {color: red}\", priority: 1)\n    NylasEnv.styles.addStyleSheet(\"a {color: blue}\", priority: 0)\n    NylasEnv.styles.addStyleSheet(\"a {color: green}\", priority: 2)\n    NylasEnv.styles.addStyleSheet(\"a {color: yellow}\", priority: 1)\n\n    expect(element.children[initialChildCount].textContent).toBe \"a {color: blue}\"\n    expect(element.children[initialChildCount + 1].textContent).toBe \"a {color: red}\"\n    expect(element.children[initialChildCount + 2].textContent).toBe \"a {color: yellow}\"\n    expect(element.children[initialChildCount + 3].textContent).toBe \"a {color: green}\"\n\n  it \"updates existing style nodes when style elements are updated\", ->\n    initialChildCount = element.children.length\n\n    NylasEnv.styles.addStyleSheet(\"a {color: red;}\", sourcePath: '/foo/bar')\n    NylasEnv.styles.addStyleSheet(\"a {color: blue;}\", sourcePath: '/foo/bar')\n\n    expect(element.children.length).toBe initialChildCount + 1\n    expect(element.children[initialChildCount].textContent).toBe \"a {color: blue;}\"\n    expect(updatedStyleElements).toEqual [element.children[initialChildCount]]\n\n  it \"only includes style elements matching the 'context' attribute\", ->\n    initialChildCount = element.children.length\n\n    NylasEnv.styles.addStyleSheet(\"a {color: red;}\", context: 'test-context')\n    NylasEnv.styles.addStyleSheet(\"a {color: green;}\")\n\n    expect(element.children.length).toBe initialChildCount + 2\n    expect(element.children[initialChildCount].textContent).toBe \"a {color: red;}\"\n    expect(element.children[initialChildCount + 1].textContent).toBe \"a {color: green;}\"\n\n    element.setAttribute('context', 'test-context')\n\n    expect(element.children.length).toBe 1\n    expect(element.children[0].textContent).toBe \"a {color: red;}\"\n\n    NylasEnv.styles.addStyleSheet(\"a {color: blue;}\", context: 'test-context')\n    NylasEnv.styles.addStyleSheet(\"a {color: yellow;}\")\n\n    expect(element.children.length).toBe 2\n    expect(element.children[0].textContent).toBe \"a {color: red;}\"\n    expect(element.children[1].textContent).toBe \"a {color: blue;}\"\n"
  },
  {
    "path": "packages/client-app/spec/themes/theme-manager-spec.coffee",
    "content": "path = require 'path'\n\nfs = require 'fs-plus'\ntemp = require 'temp'\n\nThemeManager = require '../../src/theme-manager'\nPackage = require '../../src/package'\n\ndescribe \"ThemeManager\", ->\n  themeManager = null\n  resourcePath = NylasEnv.getLoadSettings().resourcePath\n  configDirPath = NylasEnv.getConfigDirPath()\n\n  beforeEach ->\n    # spyOn(console, \"log\")\n    spyOn(console, \"warn\")\n    spyOn(console, \"error\")\n    theme_dir = path.resolve(__dirname, '../../internal_packages')\n\n    # Don't load ALL of our packages. Some packages may do very expensive\n    # and asynchronous things on require, including hitting the database.\n    packagePaths = [\n      path.resolve(__dirname, '../../internal_packages/ui-light')\n      path.resolve(__dirname, '../../internal_packages/ui-dark')\n    ]\n    spyOn(NylasEnv.packages, \"getAvailablePackagePaths\").andReturn packagePaths\n    NylasEnv.packages.packageDirPaths.unshift(theme_dir)\n    themeManager = new ThemeManager({packageManager: NylasEnv.packages, resourcePath, configDirPath})\n\n  afterEach ->\n    themeManager.deactivateThemes()\n\n  describe \"theme getters and setters\", ->\n    beforeEach ->\n      NylasEnv.packages.loadPackages()\n\n    it 'getLoadedThemes get all the loaded themes', ->\n      themes = themeManager.getLoadedThemes()\n      expect(themes.length).toEqual(2)\n\n    it 'getActiveThemes get all the active themes', ->\n      waitsForPromise ->\n        themeManager.activateThemes()\n\n      runs ->\n        names = NylasEnv.config.get('core.themes')\n        expect(names.length).toBeGreaterThan(0)\n        themes = themeManager.getActiveThemes()\n        expect(themes).toHaveLength(names.length)\n\n  describe \"when the core.themes config value contains invalid entry\", ->\n    it \"ignores theme\", ->\n      NylasEnv.config.set 'core.themes', [\n        'ui-light'\n        null\n        undefined\n        ''\n        false\n        4\n        {}\n        []\n        'ui-dark'\n      ]\n\n      expect(themeManager.getEnabledThemeNames()).toEqual ['ui-dark', 'ui-light']\n\n  describe \"::getImportPaths()\", ->\n    it \"returns the theme directories before the themes are loaded\", ->\n      NylasEnv.config.set('core.themes', ['theme-with-index-less', 'ui-dark', 'ui-light'])\n\n      paths = themeManager.getImportPaths()\n\n      # syntax theme is not a dir at this time, so only two.\n      expect(paths.length).toBe 2\n      expect(paths[0]).toContain 'ui-light'\n      expect(paths[1]).toContain 'ui-dark'\n\n    it \"ignores themes that cannot be resolved to a directory\", ->\n      NylasEnv.config.set('core.themes', ['definitely-not-a-theme'])\n      expect(-> themeManager.getImportPaths()).not.toThrow()\n\n  describe \"when the core.themes config value changes\", ->\n    it \"add/removes stylesheets to reflect the new config value\", ->\n      themeManager.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()\n\n      waitsForPromise ->\n        themeManager.activateThemes()\n\n      runs ->\n        didChangeActiveThemesHandler.reset()\n        NylasEnv.config.set('core.themes', [])\n\n      waitsFor ->\n        didChangeActiveThemesHandler.callCount == 1\n\n      runs ->\n        didChangeActiveThemesHandler.reset()\n        expect(document.querySelectorAll('style.theme')).toHaveLength 0\n        NylasEnv.config.set('core.themes', ['ui-dark'])\n\n      waitsFor ->\n        didChangeActiveThemesHandler.callCount == 1\n\n      runs ->\n        didChangeActiveThemesHandler.reset()\n        sheets = Array.from(document.querySelectorAll('style[priority=\"1\"]'))\n        expect(sheets).toHaveLength 1\n        expect(sheets[0].getAttribute('source-path')).toMatch /ui-dark/\n        NylasEnv.config.set('core.themes', ['ui-light', 'ui-dark'])\n\n      waitsFor ->\n        didChangeActiveThemesHandler.callCount == 1\n\n      runs ->\n        didChangeActiveThemesHandler.reset()\n        sheets = Array.from(document.querySelectorAll('style[priority=\"1\"]'))\n        expect(sheets).toHaveLength 2\n        expect(sheets[0].getAttribute('source-path')).toMatch /ui-dark/\n        expect(sheets[1].getAttribute('source-path')).toMatch /ui-light/\n        NylasEnv.config.set('core.themes', [])\n\n      waitsFor ->\n        didChangeActiveThemesHandler.callCount == 1\n\n      runs ->\n        didChangeActiveThemesHandler.reset()\n        sheets = Array.from(document.querySelectorAll('style[priority=\"1\"]'))\n        expect(sheets).toHaveLength(1)\n        # ui-dark has an directory path, the syntax one doesn't\n        NylasEnv.config.set('core.themes', ['theme-with-index-less', 'ui-light'])\n\n      waitsFor ->\n        didChangeActiveThemesHandler.callCount == 1\n\n      runs ->\n        sheets = Array.from(document.querySelectorAll('style[priority=\"1\"]'))\n        expect(sheets).toHaveLength 2\n        importPaths = themeManager.getImportPaths()\n        expect(importPaths.length).toBe 1\n        expect(importPaths[0]).toContain 'ui-light'\n\n    it 'adds theme-* classes to the workspace for each active theme', ->\n      themeManager.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()\n\n      waitsForPromise ->\n        themeManager.activateThemes()\n\n      runs ->\n        expect(document.body.classList.contains('theme-ui-light')).toBe(true)\n        themeManager.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()\n        NylasEnv.config.set('core.themes', ['theme-with-ui-variables'])\n\n      waitsFor ->\n        didChangeActiveThemesHandler.callCount > 0\n\n      runs ->\n        # `theme-` twice as it prefixes the name with `theme-`\n        expect(document.body.classList.contains('theme-theme-with-ui-variables')).toBe(true)\n        expect(document.body.classList.contains('theme-ui-dark')).toBe(false)\n\n  describe \"when a theme fails to load\", ->\n    it \"logs a warning\", ->\n      NylasEnv.packages.activatePackage('a-theme-that-will-not-be-found')\n      .then () ->\n        expect(\"This should have thrown!!\").toBe(true)\n      .catch (err) ->\n        expect(err.message).toMatch(/Failed to load/)\n        expect(console.warn.callCount).toBe 1\n        expect(console.warn.argsForCall[0][0]).toContain \"Could not resolve 'a-theme-that-will-not-be-found'\"\n\n  describe \"::requireStylesheet(path)\", ->\n    afterEach ->\n      themeManager.removeStylesheet(path.join(__dirname, '..', 'fixtures', 'css.css'))\n      themeManager.removeStylesheet(path.join(__dirname, '..', 'fixtures', 'sample.less'))\n\n    it \"synchronously loads css at the given path and installs a style tag for it in the head\", ->\n      NylasEnv.styles.onDidAddStyleElement styleElementAddedHandler = jasmine.createSpy(\"styleElementAddedHandler\")\n\n      cssPath = path.join(__dirname, '..', 'fixtures', 'css.css')\n      lengthBefore = document.querySelectorAll('head style').length\n\n      themeManager.requireStylesheet(cssPath)\n      expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1\n\n      expect(styleElementAddedHandler).toHaveBeenCalled()\n\n      element = document.querySelector('head style[source-path*=\"css.css\"]')\n      expect(element.getAttribute('source-path')).toBe themeManager.stringToId(cssPath)\n      expect(element.textContent).toBe fs.readFileSync(cssPath, 'utf8')\n\n      # doesn't append twice\n      styleElementAddedHandler.reset()\n      themeManager.requireStylesheet(cssPath)\n      expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1\n      expect(styleElementAddedHandler).not.toHaveBeenCalled()\n\n      element.remove()\n\n    it \"synchronously loads and parses less files at the given path and installs a style tag for it in the head\", ->\n      lessPath = path.join(__dirname, '..', 'fixtures', 'sample.less')\n      lengthBefore = document.querySelectorAll('head style').length\n      themeManager.requireStylesheet(lessPath)\n      lengthAfter = document.querySelectorAll('head style').length\n      expect(lengthAfter).toBe lengthBefore + 1\n\n      element = document.querySelector('head style[source-path*=\"sample.less\"]')\n      expect(element.getAttribute('source-path')).toBe themeManager.stringToId(lessPath)\n      expect(element.textContent).toBe \"\"\"\n      #header {\n        color: #4d926f;\n      }\n      h2 {\n        color: #4d926f;\n      }\n\n      \"\"\"\n\n      # doesn't append twice\n      themeManager.requireStylesheet(lessPath)\n      expect(document.querySelectorAll('head style').length).toBe lengthBefore + 1\n      element.remove()\n\n    it \"supports requiring css and less stylesheets without an explicit extension\", ->\n      themeManager.requireStylesheet path.join(__dirname, '..', 'fixtures', 'css')\n      expect(document.querySelector('head style[source-path*=\"css.css\"]').getAttribute('source-path')).toBe themeManager.stringToId(path.join(__dirname, '..', 'fixtures', 'css.css'))\n      themeManager.requireStylesheet path.join(__dirname, '..', 'fixtures', 'sample')\n      expect(document.querySelector('head style[source-path*=\"sample.less\"]').getAttribute('source-path')).toBe themeManager.stringToId(path.join(__dirname, '..', 'fixtures', 'sample.less'))\n\n      document.querySelector('head style[source-path*=\"css.css\"]').remove()\n      document.querySelector('head style[source-path*=\"sample.less\"]').remove()\n\n    it \"returns a disposable allowing styles applied by the given path to be removed\", ->\n      cssPath = require.resolve('../fixtures/css.css')\n\n      expect(window.getComputedStyle(document.body)['font-weight']).not.toBe(\"bold\")\n      disposable = themeManager.requireStylesheet(cssPath)\n      expect(window.getComputedStyle(document.body)['font-weight']).toBe(\"bold\")\n\n      NylasEnv.styles.onDidRemoveStyleElement styleElementRemovedHandler = jasmine.createSpy(\"styleElementRemovedHandler\")\n      disposable.dispose()\n\n      expect(window.getComputedStyle(document.body)['font-weight']).not.toBe(\"bold\")\n      expect(styleElementRemovedHandler).toHaveBeenCalled()\n\n  describe \"base style sheet loading\", ->\n    workspaceElement = null\n    beforeEach ->\n      workspaceElement = document.createElement('nylas-workspace')\n      workspaceElement.appendChild document.createElement('nylas-theme-wrap')\n      jasmine.attachToDOM(workspaceElement)\n\n      waitsForPromise ->\n        themeManager.activateThemes()\n\n      runs ->\n        themeManager.onDidChangeActiveThemes didChangeActiveThemesHandler = jasmine.createSpy()\n        additionalDelay = null\n        @waitsForThemeRefresh = ->\n          waitsFor ->\n            # We need to wait a bit of additional time for the browser to actually\n            # apply the CSS to the elements we check.\n            if didChangeActiveThemesHandler.callCount > 0\n              additionalDelay ?= Date.now() + 100\n              return Date.now() > additionalDelay\n            return false\n\n    it \"loads the correct values from the theme's ui-variables file\", ->\n      NylasEnv.config.set('core.themes', ['theme-with-ui-variables'])\n\n      @waitsForThemeRefresh()\n      runs ->\n        # an override loaded in the base css of theme-with-ui-variables\n        expect(getComputedStyle(workspaceElement)[\"background-color\"]).toBe \"rgb(0, 0, 255)\"\n\n        # a value that is not overridden in the theme\n        node = document.querySelector('nylas-theme-wrap')\n        nodeStyle = window.getComputedStyle(node)\n        expect(nodeStyle['padding-top']).toBe \"150px\"\n        expect(nodeStyle['padding-right']).toBe \"150px\"\n        expect(nodeStyle['padding-bottom']).toBe \"150px\"\n\n    describe \"when there is a theme with incomplete variables\", ->\n      it \"loads the correct values from the fallback ui-variables\", ->\n        NylasEnv.config.set('core.themes', ['theme-with-incomplete-ui-variables'])\n\n        @waitsForThemeRefresh()\n        runs ->\n          # an override loaded in the base css of theme-with-incomplete-ui-variables\n          expect(getComputedStyle(workspaceElement)[\"background-color\"]).toBe \"rgb(0, 0, 255)\"\n\n            # a value that is not overridden in the theme\n          node = document.querySelector('nylas-theme-wrap')\n          nodeStyle = window.getComputedStyle(node)\n          expect(nodeStyle['background-color']).toBe \"rgb(152, 123, 0)\"\n\n  describe \"when a non-existent theme is present in the config\", ->\n    beforeEach ->\n      NylasEnv.config.set('core.themes', ['non-existent-dark-ui'])\n\n      waitsForPromise ->\n        themeManager.activateThemes()\n\n    it 'uses the default theme and logs a warning', ->\n      activeThemeNames = themeManager.getActiveThemeNames()\n      expect(console.warn.callCount).toBe(1)\n      expect(activeThemeNames.length).toBe(1)\n      expect(activeThemeNames).toContain('ui-light')\n\n  describe \"when in safe mode\", ->\n    beforeEach ->\n      themeManager = new ThemeManager({packageManager: NylasEnv.packages, resourcePath, configDirPath, safeMode: true})\n\n    describe 'when the enabled UI theme is bundled with N1', ->\n      beforeEach ->\n        NylasEnv.config.set('core.themes', ['ui-light'])\n\n        waitsForPromise ->\n          themeManager.activateThemes()\n\n      it 'uses the enabled themes', ->\n        activeThemeNames = themeManager.getActiveThemeNames()\n        expect(activeThemeNames.length).toBe(1)\n        expect(activeThemeNames).toContain('ui-light')\n\n    describe 'when the enabled UI theme is not bundled with N1', ->\n      beforeEach ->\n        NylasEnv.config.set('core.themes', ['installed-dark-ui'])\n\n        waitsForPromise ->\n          themeManager.activateThemes()\n\n      it 'uses the default UI theme', ->\n        activeThemeNames = themeManager.getActiveThemeNames()\n        expect(activeThemeNames.length).toBe(1)\n        expect(activeThemeNames).toContain('ui-light')\n\n    describe 'when the enabled UI theme is not bundled with N1', ->\n      beforeEach ->\n        NylasEnv.config.set('core.themes', ['installed-dark-ui'])\n\n        waitsForPromise ->\n          themeManager.activateThemes()\n\n      it 'uses the default UI theme', ->\n        activeThemeNames = themeManager.getActiveThemeNames()\n        expect(activeThemeNames.length).toBe(1)\n        expect(activeThemeNames).toContain('ui-light')\n"
  },
  {
    "path": "packages/client-app/spec/undo-stack-spec.es6",
    "content": "import UndoStack from \"../src/undo-stack\";\n\ndescribe(\"UndoStack\", function UndoStackSpecs() {\n  beforeEach(() => {\n    this.undoManager = new UndoStack();\n  });\n\n  afterEach(() => {\n    advanceClock(500);\n  })\n\n  describe(\"undo\", () => {\n    it(\"can restore history items, and returns null when none are available\", () => {\n      this.undoManager.save(\"A\")\n      this.undoManager.save(\"B\")\n      this.undoManager.save(\"C\")\n      expect(this.undoManager.current()).toBe(\"C\")\n      expect(this.undoManager.undo()).toBe(\"B\")\n      expect(this.undoManager.current()).toBe(\"B\")\n      expect(this.undoManager.undo()).toBe(\"A\")\n      expect(this.undoManager.current()).toBe(\"A\")\n      expect(this.undoManager.undo()).toBe(null)\n      expect(this.undoManager.current()).toBe(\"A\")\n    });\n\n    it(\"limits the undo stack to the MAX_HISTORY_SIZE\", () => {\n      this.undoManager._MAX_STACK_SIZE = 3\n      this.undoManager.save(\"A\")\n      this.undoManager.save(\"B\")\n      this.undoManager.save(\"C\")\n      this.undoManager.save(\"D\")\n      expect(this.undoManager.current()).toBe(\"D\")\n      expect(this.undoManager.undo()).toBe(\"C\")\n      expect(this.undoManager.undo()).toBe(\"B\")\n      expect(this.undoManager.undo()).toBe(null)\n      expect(this.undoManager.current()).toBe(\"B\")\n    });\n  });\n\n  describe(\"undo followed by redo\", () => {\n    it(\"can restore previously undone history items\", () => {\n      this.undoManager.save(\"A\")\n      this.undoManager.save(\"B\")\n      this.undoManager.save(\"C\")\n      expect(this.undoManager.current()).toBe(\"C\")\n      expect(this.undoManager.undo()).toBe(\"B\")\n      expect(this.undoManager.current()).toBe(\"B\")\n      expect(this.undoManager.redo()).toBe(\"C\")\n      expect(this.undoManager.current()).toBe(\"C\")\n    });\n\n    it(\"cannot be used after pushing additional items\", () => {\n      this.undoManager.save(\"A\")\n      this.undoManager.save(\"B\")\n      this.undoManager.save(\"C\")\n      expect(this.undoManager.current()).toBe(\"C\")\n      expect(this.undoManager.undo()).toBe(\"B\")\n      this.undoManager.save(\"D\")\n      expect(this.undoManager.redo()).toBe(null)\n      expect(this.undoManager.current()).toBe(\"D\")\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/utils/date-utils-spec.es6",
    "content": "import moment from 'moment'\nimport {DateUtils} from 'nylas-exports'\n\n\ndescribe('DateUtils', function dateUtils() {\n  describe('nextWeek', () => {\n    it('returns tomorrow if now is sunday', () => {\n      const sunday = moment(\"03-06-2016\", \"MM-DD-YYYY\")\n      const nextWeek = DateUtils.nextWeek(sunday)\n      expect(nextWeek.format('MM-DD-YYYY')).toEqual('03-07-2016')\n    });\n\n    it('returns next monday if now is monday', () => {\n      const monday = moment(\"03-07-2016\", \"MM-DD-YYYY\")\n      const nextWeek = DateUtils.nextWeek(monday)\n      expect(nextWeek.format('MM-DD-YYYY')).toEqual('03-14-2016')\n    });\n\n    it('returns next monday', () => {\n      const saturday = moment(\"03-05-2016\", \"MM-DD-YYYY\")\n      const nextWeek = DateUtils.nextWeek(saturday)\n      expect(nextWeek.format('MM-DD-YYYY')).toEqual('03-07-2016')\n    });\n  });\n\n  describe('thisWeekend', () => {\n    it('returns tomorrow if now is friday', () => {\n      const friday = moment(\"03-04-2016\", \"MM-DD-YYYY\")\n      const thisWeekend = DateUtils.thisWeekend(friday)\n      expect(thisWeekend.format('MM-DD-YYYY')).toEqual('03-05-2016')\n    });\n\n    it('returns next saturday if now is saturday', () => {\n      const saturday = moment(\"03-05-2016\", \"MM-DD-YYYY\")\n      const thisWeekend = DateUtils.thisWeekend(saturday)\n      expect(thisWeekend.format('MM-DD-YYYY')).toEqual('03-12-2016')\n    });\n\n    it('returns next saturday', () => {\n      const sunday = moment(\"03-06-2016\", \"MM-DD-YYYY\")\n      const thisWeekend = DateUtils.thisWeekend(sunday)\n      expect(thisWeekend.format('MM-DD-YYYY')).toEqual('03-12-2016')\n    });\n  });\n\n  describe('getTimeFormat: 12-hour clock', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv.config, 'get').andReturn(false)\n    });\n\n    it('displays the time format for a 12-hour clock', () => {\n      const time = DateUtils.getTimeFormat(null)\n      expect(time).toBe('h:mm a')\n    });\n\n    it('displays the time format for a 12-hour clock with timezone', () => {\n      const opts = { timeZone: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('h:mm a z')\n    });\n\n    it('displays the time format for a 12-hour clock with seconds', () => {\n      const opts = { seconds: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('h:mm:ss a')\n    });\n\n    it('displays the time format for a 12-hour clock with seconds and timezone', () => {\n      const opts = { seconds: true, timeZone: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('h:mm:ss a z')\n    });\n\n    it('displays the time format for a 12-hour clock in uppercase', () => {\n      const opts = { upperCase: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('h:mm A')\n    });\n\n    it('displays the time format for a 12-hour clock in uppercase with seconds', () => {\n      const opts = { upperCase: true, seconds: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('h:mm:ss A')\n    });\n\n    it('displays the time format for a 12-hour clock in uppercase with timezone', () => {\n      const opts = { upperCase: true, timeZone: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('h:mm A z')\n    });\n\n    it('displays the time format for a 12-hour clock in uppercase with seconds and timezone', () => {\n      const opts = { upperCase: true, seconds: true, timeZone: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('h:mm:ss A z')\n    });\n  });\n\n  describe('getTimeFormat: 24-hour clock', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv.config, 'get').andReturn(true)\n    });\n\n    it('displays the time format for a 24-hour clock', () => {\n      const time = DateUtils.getTimeFormat(null)\n      expect(time).toBe('HH:mm')\n    });\n\n    it('displays the time format for a 24-hour clock with timezone', () => {\n      const opts = { timeZone: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('HH:mm z')\n    });\n\n    it('displays the time format for a 24-hour clock with seconds', () => {\n      const opts = { seconds: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('HH:mm:ss')\n    });\n\n    it('displays the time format for a 24-hour clock with seconds and timezone', () => {\n      const opts = { seconds: true, timeZone: true }\n      const time = DateUtils.getTimeFormat(opts)\n      expect(time).toBe('HH:mm:ss z')\n    });\n  });\n\n  describe('mediumTimeString: 12-hour time', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv.config, 'get').andReturn(false)\n    });\n\n    it('displays a date and time', () => {\n      const datestring = DateUtils.mediumTimeString('1982-10-24 22:45')\n      expect(datestring).toBe('October 24, 1982, 10:45 PM')\n    });\n  });\n\n  describe('mediumTimeString: 24-hour time', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv.config, 'get').andReturn(true)\n    });\n\n    it('displays a date and time', () => {\n      const datestring = DateUtils.mediumTimeString('1982-10-24 22:45')\n      expect(datestring).toBe('October 24, 1982, 22:45')\n    });\n  });\n\n  describe('fullTimeString: 12-hour time', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv.config, 'get').andReturn(false)\n    });\n\n    it('displays a date and time', () => {\n      const datestring = DateUtils.fullTimeString('1982-10-24 22:45')\n      expect(datestring.startsWith(`Sunday, October 24th 1982, 10:45:00 PM`)).toBe(true)\n    });\n  });\n\n  describe('fullTimeString: 24-hour time', () => {\n    beforeEach(() => {\n      spyOn(NylasEnv.config, 'get').andReturn(true)\n    });\n\n    it('displays a date and time', () => {\n      const datestring = DateUtils.fullTimeString('1982-10-24 22:45')\n      expect(datestring.startsWith(`Sunday, October 24th 1982, 22:45:00`)).toBe(true)\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-app/spec/utils/dom-utils-spec.coffee",
    "content": "_ = require 'underscore'\nDOMUtils = require '../../src/dom-utils'\ndescribe 'nodesWithContent', ->\n\n  tests = {\n    \"\": null\n\n    \"<br>\": null\n\n    \"<div><br><br/><p></p></div>\": null\n\n    \"\"\"\n      <br id=\"1\">\n      <img>\n      <br id=\"2\">\n    \"\"\": \"<img>\"\n\n    \"\"\"\n      Hello\n    \"\"\": \"Hello\"\n\n    \"\"\"\n      <div>Hello</div>\n    \"\"\": \"<div>Hello</div>\"\n\n    \"\"\"\n      <div>Hello</div>\n      Foobar\n    \"\"\": \"Foobar\"\n\n    \"\"\"\n      <br>\n      <span>Hello</span>\n      <br>\n    \"\"\": \"<span>Hello</span>\"\n\n    \"\"\"\n      <br>\n      <span id=\"a\">Hello</span>\n      <br>\n      <span id=\"b\">World</span>\n      <br>\n\n      <br>\n    \"\"\": \"\"\"<span id=\"b\">World</span>\"\"\"\n\n    \"\"\"\n      <div>Hello</div>\n      <div>\n        <p></p>\n        <span></span>\n      </div>\n    \"\"\": \"<div>Hello</div>\"\n\n    \"\"\"\n      <div>Hello</div>\n      <div style=\"display:none\">\n        I'm hidden\n      </div>\n    \"\"\": \"<div>Hello</div>\"\n\n    \"\"\"\n      <div>Hello</div>\n      <div style=\"opacity:0\">\n        I'm hidden\n      </div>\n    \"\"\": \"<div>Hello</div>\"\n  }\n\n  it \"tests nodesWithContent\", ->\n    for input, output of tests\n      nodes = DOMUtils.nodesWithContent(input)\n      node = _.last(nodes) ? null\n      if node\n        tmp = document.createElement('div')\n        tmp.appendChild(node)\n        expect(tmp.innerHTML.trim()).toEqual output\n      else\n        expect(node).toEqual output\n"
  },
  {
    "path": "packages/client-app/spec/utils/utils-spec.coffee",
    "content": "_ = require('underscore')\nUtils = require '../../src/flux/models/utils'\nThread = require('../../src/flux/models/thread').default\nContact = require('../../src/flux/models/contact').default\nJSONBlob = require('../../src/flux/models/json-blob').default\n\nclass Foo\n  constructor: (@instanceVar) ->\n  field:\n    a: 1\n    b: 2\n  method: (@stuff) ->\n\nclass Bar extends Foo\n  subMethod: (stuff) ->\n    @moreStuff = stuff\n    @method(stuff)\n\ndescribe 'Utils', ->\n\n  describe \"registeredObjectReviver / registeredObjectReplacer\", ->\n    beforeEach ->\n      @testThread = new Thread\n        id: 'local-1'\n        accountId: '1'\n        pluginMetadata: []\n        isSearchIndexed: false\n        participants: [\n          new Contact(id: 'local-a', name: 'Juan', email:'juan@nylas.com', accountId: '1', isSearchIndexed: false),\n          new Contact(id: 'local-b', name: 'Ben', email:'ben@nylas.com', accountId: '1', isSearchIndexed: false)\n        ]\n        subject: 'Test 1234'\n\n    it \"should serialize and de-serialize models correctly\", ->\n      expectedString = '[{\"client_id\":\"local-1\",\"account_id\":\"1\",\"metadata\":[],\"subject\":\"Test 1234\",\"participants\":[{\"client_id\":\"local-a\",\"account_id\":\"1\",\"name\":\"Juan\",\"email\":\"juan@nylas.com\",\"thirdPartyData\":{},\"is_search_indexed\":false,\"id\":\"local-a\"},{\"client_id\":\"local-b\",\"account_id\":\"1\",\"name\":\"Ben\",\"email\":\"ben@nylas.com\",\"thirdPartyData\":{},\"is_search_indexed\":false,\"id\":\"local-b\"}],\"in_all_mail\":true,\"is_search_indexed\":false,\"id\":\"local-1\",\"__constructorName\":\"Thread\"}]'\n\n      jsonString = JSON.stringify([@testThread], Utils.registeredObjectReplacer)\n      expect(jsonString).toEqual(expectedString)\n      revived = JSON.parse(jsonString, Utils.registeredObjectReviver)\n      expect(revived).toEqual([@testThread])\n\n    it \"should re-inflate Models in places they're not explicitly declared types\", ->\n      b = new JSONBlob({id: \"ThreadsToProcess\", json: [@testThread]})\n      jsonString = JSON.stringify(b, Utils.registeredObjectReplacer)\n      expectedString = '{\"client_id\":\"ThreadsToProcess\",\"server_id\":\"ThreadsToProcess\",\"json\":[{\"client_id\":\"local-1\",\"account_id\":\"1\",\"metadata\":[],\"subject\":\"Test 1234\",\"participants\":[{\"client_id\":\"local-a\",\"account_id\":\"1\",\"name\":\"Juan\",\"email\":\"juan@nylas.com\",\"thirdPartyData\":{},\"is_search_indexed\":false,\"id\":\"local-a\"},{\"client_id\":\"local-b\",\"account_id\":\"1\",\"name\":\"Ben\",\"email\":\"ben@nylas.com\",\"thirdPartyData\":{},\"is_search_indexed\":false,\"id\":\"local-b\"}],\"in_all_mail\":true,\"is_search_indexed\":false,\"id\":\"local-1\",\"__constructorName\":\"Thread\"}],\"id\":\"ThreadsToProcess\",\"__constructorName\":\"JSONBlob\"}'\n\n      expect(jsonString).toEqual(expectedString)\n      revived = JSON.parse(jsonString, Utils.registeredObjectReviver)\n      expect(revived).toEqual(b)\n      expect(revived.json[0] instanceof Thread).toBe(true)\n      expect(revived.json[0].participants[0] instanceof Contact).toBe(true)\n\n  describe \"modelFreeze\", ->\n    it \"should freeze the object\", ->\n      o =\n        a: 1\n        b: 2\n      Utils.modelFreeze(o)\n      expect(Object.isFrozen(o)).toBe(true)\n\n    it \"should not throw an exception when nulls appear in strange places\", ->\n      t = new Thread(participants: [new Contact(email: 'ben@nylas.com'), null], subject: '123')\n      Utils.modelFreeze(t)\n      expect(Object.isFrozen(t)).toBe(true)\n      expect(Object.isFrozen(t.participants[0])).toBe(true)\n\n  describe \"deepClone\", ->\n    beforeEach ->\n      @v1 = [1,2,3]\n      @v2 = [4,5,6]\n      @foo = new Foo(@v1)\n      @bar = new Bar(@v2)\n      @o2 = [\n        @foo,\n        {v1: @v1, v2: @v2, foo: @foo, bar: @bar, baz: \"baz\", fn: Foo},\n        \"abc\"\n      ]\n      @o2.circular = @o2\n      @o2Clone = Utils.deepClone(@o2)\n\n    it \"deep clones dates correctly\", ->\n      d1 = new Date(2016,1,1)\n      d2 = Utils.deepClone(d1)\n      expect(d2.valueOf()).toBe(d1.valueOf())\n\n    it \"makes a deep clone\", ->\n      @v1.push(4)\n      @v2.push(7)\n      @foo.stuff = \"stuff\"\n      @bar.subMethod(\"stuff\")\n      expect(@o2Clone[0].stuff).toBeUndefined()\n      expect(@o2Clone[1].foo.stuff).toBeUndefined()\n      expect(@o2Clone[1].bar.stuff).toBeUndefined()\n      expect(@o2Clone[1].v1.length).toBe 3\n      expect(@o2Clone[1].v2.length).toBe 3\n      expect(@o2Clone[2]).toBe \"abc\"\n\n    it \"does not deep clone the prototype\", ->\n      @foo.field.a = \"changed under the hood\"\n      expect(@o2Clone[0].field.a).toBe \"changed under the hood\"\n\n    it \"clones constructors properly\", ->\n      expect((new @o2Clone[1].fn) instanceof Foo).toBe true\n\n    it \"clones prototypes properly\", ->\n      expect(@o2Clone[1].foo instanceof Foo).toBe true\n      expect(@o2Clone[1].bar instanceof Bar).toBe true\n\n    it \"can take a customizer to edit values as we clone\", ->\n      clone = Utils.deepClone @o2, (key, clonedValue) ->\n        if key is \"v2\"\n          clonedValue.push(\"custom value\")\n          return clonedValue\n        else return clonedValue\n      @v2.push(7)\n      expect(clone[1].v2.length).toBe 4\n      expect(clone[1].v2[3]).toBe \"custom value\"\n\n# Pulled equality tests from underscore\n# https://github.com/jashkenas/underscore/blob/master/test/objects.js\n  describe \"isEqual\", ->\n    describe \"custom behavior\", ->\n      it \"makes functions always equal\", ->\n        f1 = ->\n        f2 = ->\n        expect(Utils.isEqual(f1, f2)).toBe false\n        expect(Utils.isEqual(f1, f2, functionsAreEqual: true)).toBe true\n\n      it \"can ignore keys in objects\", ->\n        o1 =\n          foo: \"bar\"\n          arr: [1,2,3]\n          nest: {a: \"b\", c: 1, ignoreMe: 5}\n          ignoreMe: 123\n        o2 =\n          foo: \"bar\"\n          arr: [1,2,3]\n          nest: {a: \"b\", c: 1, ignoreMe: 10}\n          ignoreMe: 456\n\n        expect(Utils.isEqual(o1, o2)).toBe false\n        expect(Utils.isEqual(o1, o2, ignoreKeys: [\"ignoreMe\"])).toBe true\n\n    it \"passes all underscore equality tests\", ->\n      First = ->\n        @value = 1\n      First.prototype.value = 1\n\n      Second = ->\n        @value = 1\n      Second.prototype.value = 2\n\n      ok = (val) -> expect(val).toBe true\n\n      # Basic equality and identity comparisons.\n      ok(Utils.isEqual(null, null), '`null` is equal to `null`')\n      ok(Utils.isEqual(), '`undefined` is equal to `undefined`')\n\n      ok(!Utils.isEqual(0, -0), '`0` is not equal to `-0`')\n      ok(!Utils.isEqual(-0, 0), 'Commutative equality is implemented for `0` and `-0`')\n      ok(!Utils.isEqual(null, undefined), '`null` is not equal to `undefined`')\n      ok(!Utils.isEqual(undefined, null), 'Commutative equality is implemented for `null` and `undefined`')\n\n      # String object and primitive comparisons.\n      ok(Utils.isEqual('Curly', 'Curly'), 'Identical string primitives are equal')\n      ok(Utils.isEqual(new String('Curly'), new String('Curly')), 'String objects with identical primitive values are equal')\n      ok(Utils.isEqual(new String('Curly'), 'Curly'), 'String primitives and their corresponding object wrappers are equal')\n      ok(Utils.isEqual('Curly', new String('Curly')), 'Commutative equality is implemented for string objects and primitives')\n\n      ok(!Utils.isEqual('Curly', 'Larry'), 'String primitives with different values are not equal')\n      ok(!Utils.isEqual(new String('Curly'), new String('Larry')), 'String objects with different primitive values are not equal')\n      ok(!Utils.isEqual(new String('Curly'), {toString: -> 'Curly'}), 'String objects and objects with a custom `toString` method are not equal')\n\n      # Number object and primitive comparisons.\n      ok(Utils.isEqual(75, 75), 'Identical number primitives are equal')\n      ok(Utils.isEqual(new Number(75), new Number(75)), 'Number objects with identical primitive values are equal')\n      ok(Utils.isEqual(75, new Number(75)), 'Number primitives and their corresponding object wrappers are equal')\n      ok(Utils.isEqual(new Number(75), 75), 'Commutative equality is implemented for number objects and primitives')\n      ok(!Utils.isEqual(new Number(0), -0), '`new Number(0)` and `-0` are not equal')\n      ok(!Utils.isEqual(0, new Number(-0)), 'Commutative equality is implemented for `new Number(0)` and `-0`')\n\n      ok(!Utils.isEqual(new Number(75), new Number(63)), 'Number objects with different primitive values are not equal')\n      ok(!Utils.isEqual(new Number(63), {valueOf: -> 63 }), 'Number objects and objects with a `valueOf` method are not equal')\n\n      # Comparisons involving `NaN`.\n      ok(Utils.isEqual(NaN, NaN), '`NaN` is equal to `NaN`')\n      ok(Utils.isEqual(new Object(NaN), NaN), 'Object(`NaN`) is equal to `NaN`')\n      ok(!Utils.isEqual(61, NaN), 'A number primitive is not equal to `NaN`')\n      ok(!Utils.isEqual(new Number(79), NaN), 'A number object is not equal to `NaN`')\n      ok(!Utils.isEqual(Infinity, NaN), '`Infinity` is not equal to `NaN`')\n\n      # Boolean object and primitive comparisons.\n      ok(Utils.isEqual(true, true), 'Identical boolean primitives are equal')\n      ok(Utils.isEqual(new Boolean, new Boolean), 'Boolean objects with identical primitive values are equal')\n      ok(Utils.isEqual(true, new Boolean(true)), 'Boolean primitives and their corresponding object wrappers are equal')\n      ok(Utils.isEqual(new Boolean(true), true), 'Commutative equality is implemented for booleans')\n      ok(!Utils.isEqual(new Boolean(true), new Boolean), 'Boolean objects with different primitive values are not equal')\n\n      # Common type coercions.\n      ok(!Utils.isEqual(new Boolean(false), true), '`new Boolean(false)` is not equal to `true`')\n      ok(!Utils.isEqual('75', 75), 'String and number primitives with like values are not equal')\n      ok(!Utils.isEqual(new Number(63), new String(63)), 'String and number objects with like values are not equal')\n      ok(!Utils.isEqual(75, '75'), 'Commutative equality is implemented for like string and number values')\n      ok(!Utils.isEqual(0, ''), 'Number and string primitives with like values are not equal')\n      ok(!Utils.isEqual(1, true), 'Number and boolean primitives with like values are not equal')\n      ok(!Utils.isEqual(new Boolean(false), new Number(0)), 'Boolean and number objects with like values are not equal')\n      ok(!Utils.isEqual(false, new String('')), 'Boolean primitives and string objects with like values are not equal')\n      ok(!Utils.isEqual(12564504e5, new Date(2009, 9, 25)), 'Dates and their corresponding numeric primitive values are not equal')\n\n      # Dates.\n      ok(Utils.isEqual(new Date(2009, 9, 25), new Date(2009, 9, 25)), 'Date objects referencing identical times are equal')\n      ok(!Utils.isEqual(new Date(2009, 9, 25), new Date(2009, 11, 13)), 'Date objects referencing different times are not equal')\n      ok(!Utils.isEqual(new Date(2009, 11, 13), {\n        getTime: -> 12606876e5\n      }), 'Date objects and objects with a `getTime` method are not equal')\n      ok(!Utils.isEqual(new Date('Curly'), new Date('Curly')), 'Invalid dates are not equal')\n\n      # Functions.\n      ok(!Utils.isEqual(First, Second), 'Different functions with identical bodies and source code representations are not equal')\n\n      # RegExps.\n      ok(Utils.isEqual(/(?:)/gim, /(?:)/gim), 'RegExps with equivalent patterns and flags are equal')\n      ok(Utils.isEqual(/(?:)/gi, /(?:)/ig), 'Flag order is not significant')\n      ok(!Utils.isEqual(/(?:)/g, /(?:)/gi), 'RegExps with equivalent patterns and different flags are not equal')\n      ok(!Utils.isEqual(/Moe/gim, /Curly/gim), 'RegExps with different patterns and equivalent flags are not equal')\n      ok(!Utils.isEqual(/(?:)/gi, /(?:)/g), 'Commutative equality is implemented for RegExps')\n      ok(!Utils.isEqual(/Curly/g, {source: 'Larry', global: true, ignoreCase: false, multiline: false}), 'RegExps and RegExp-like objects are not equal')\n\n      # Empty arrays, array-like objects, and object literals.\n      ok(Utils.isEqual({}, {}), 'Empty object literals are equal')\n      ok(Utils.isEqual([], []), 'Empty array literals are equal')\n      ok(Utils.isEqual([{}], [{}]), 'Empty nested arrays and objects are equal')\n      ok(!Utils.isEqual({length: 0}, []), 'Array-like objects and arrays are not equal.')\n      ok(!Utils.isEqual([], {length: 0}), 'Commutative equality is implemented for array-like objects')\n\n      ok(!Utils.isEqual({}, []), 'Object literals and array literals are not equal')\n      ok(!Utils.isEqual([], {}), 'Commutative equality is implemented for objects and arrays')\n\n      # Arrays with primitive and object values.\n      ok(Utils.isEqual([1, 'Larry', true], [1, 'Larry', true]), 'Arrays containing identical primitives are equal')\n      ok(Utils.isEqual([/Moe/g, new Date(2009, 9, 25)], [/Moe/g, new Date(2009, 9, 25)]), 'Arrays containing equivalent elements are equal')\n\n      # Multi-dimensional arrays.\n      a = [new Number(47), false, 'Larry', /Moe/, new Date(2009, 11, 13), ['running', 'biking', new String('programming')], {a: 47}]\n      b = [new Number(47), false, 'Larry', /Moe/, new Date(2009, 11, 13), ['running', 'biking', new String('programming')], {a: 47}]\n      ok(Utils.isEqual(a, b), 'Arrays containing nested arrays and objects are recursively compared')\n\n      # Overwrite the methods defined in ES 5.1 section 15.4.4.\n      a.forEach = a.map = a.filter = a.every = a.indexOf = a.lastIndexOf = a.some = a.reduce = a.reduceRight = null\n      b.join = b.pop = b.reverse = b.shift = b.slice = b.splice = b.concat = b.sort = b.unshift = null\n\n      # Array elements and properties.\n      ok(Utils.isEqual(a, b), 'Arrays containing equivalent elements and different non-numeric properties are equal')\n      a.push('White Rocks')\n      ok(!Utils.isEqual(a, b), 'Arrays of different lengths are not equal')\n      a.push('East Boulder')\n      b.push('Gunbarrel Ranch', 'Teller Farm')\n      ok(!Utils.isEqual(a, b), 'Arrays of identical lengths containing different elements are not equal')\n\n      # Sparse arrays.\n      ok(Utils.isEqual(Array(3), Array(3)), 'Sparse arrays of identical lengths are equal')\n      ok(!Utils.isEqual(Array(3), Array(6)), 'Sparse arrays of different lengths are not equal when both are empty')\n\n      sparse = []\n      sparse[1] = 5\n      ok(Utils.isEqual(sparse, [undefined, 5]), 'Handles sparse arrays as dense')\n\n      # Simple objects.\n      ok(Utils.isEqual({a: 'Curly', b: 1, c: true}, {a: 'Curly', b: 1, c: true}), 'Objects containing identical primitives are equal')\n      ok(Utils.isEqual({a: /Curly/g, b: new Date(2009, 11, 13)}, {a: /Curly/g, b: new Date(2009, 11, 13)}), 'Objects containing equivalent members are equal')\n      ok(!Utils.isEqual({a: 63, b: 75}, {a: 61, b: 55}), 'Objects of identical sizes with different values are not equal')\n      ok(!Utils.isEqual({a: 63, b: 75}, {a: 61, c: 55}), 'Objects of identical sizes with different property names are not equal')\n      ok(!Utils.isEqual({a: 1, b: 2}, {a: 1}), 'Objects of different sizes are not equal')\n      ok(!Utils.isEqual({a: 1}, {a: 1, b: 2}), 'Commutative equality is implemented for objects')\n      ok(!Utils.isEqual({x: 1, y: undefined}, {x: 1, z: 2}), 'Objects with identical keys and different values are not equivalent')\n\n      # `A` contains nested objects and arrays.\n      a = {\n        name: new String('Moe Howard'),\n        age: new Number(77),\n        stooge: true,\n        hobbies: ['acting'],\n        film: {\n          name: 'Sing a Song of Six Pants',\n          release: new Date(1947, 9, 30),\n          stars: [new String('Larry Fine'), 'Shemp Howard'],\n          minutes: new Number(16),\n          seconds: 54\n        }\n      }\n\n      # `B` contains equivalent nested objects and arrays.\n      b = {\n        name: new String('Moe Howard'),\n        age: new Number(77),\n        stooge: true,\n        hobbies: ['acting'],\n        film: {\n          name: 'Sing a Song of Six Pants',\n          release: new Date(1947, 9, 30),\n          stars: [new String('Larry Fine'), 'Shemp Howard'],\n          minutes: new Number(16),\n          seconds: 54\n        }\n      }\n      ok(Utils.isEqual(a, b), 'Objects with nested equivalent members are recursively compared')\n\n      # Instances.\n      ok(Utils.isEqual(new First, new First), 'Object instances are equal')\n      ok(!Utils.isEqual(new First, new Second), 'Objects with different constructors and identical own properties are not equal')\n      ok(!Utils.isEqual({value: 1}, new First), 'Object instances and objects sharing equivalent properties are not equal')\n      ok(!Utils.isEqual({value: 2}, new Second), 'The prototype chain of objects should not be examined')\n\n      # Circular Arrays.\n      (a = []).push(a)\n      (b = []).push(b)\n      ok(Utils.isEqual(a, b), 'Arrays containing circular references are equal')\n      a.push(new String('Larry'))\n      b.push(new String('Larry'))\n      ok(Utils.isEqual(a, b), 'Arrays containing circular references and equivalent properties are equal')\n      a.push('Shemp')\n      b.push('Curly')\n      ok(!Utils.isEqual(a, b), 'Arrays containing circular references and different properties are not equal')\n\n      # More circular arrays #767.\n      a = ['everything is checked but', 'this', 'is not']\n      a[1] = a\n      b = ['everything is checked but', ['this', 'array'], 'is not']\n      ok(!Utils.isEqual(a, b), 'Comparison of circular references with non-circular references are not equal')\n\n      # Circular Objects.\n      a = {abc: null}\n      b = {abc: null}\n      a.abc = a\n      b.abc = b\n      ok(Utils.isEqual(a, b), 'Objects containing circular references are equal')\n      a.def = 75\n      b.def = 75\n      ok(Utils.isEqual(a, b), 'Objects containing circular references and equivalent properties are equal')\n      a.def = new Number(75)\n      b.def = new Number(63)\n      ok(!Utils.isEqual(a, b), 'Objects containing circular references and different properties are not equal')\n\n      # More circular objects #767.\n      a = {everything: 'is checked', but: 'this', is: 'not'}\n      a.but = a\n      b = {everything: 'is checked', but: {that: 'object'}, is: 'not'}\n      ok(!Utils.isEqual(a, b), 'Comparison of circular references with non-circular object references are not equal')\n\n      # Cyclic Structures.\n      a = [{abc: null}]\n      b = [{abc: null}]\n      (a[0].abc = a).push(a)\n      (b[0].abc = b).push(b)\n      ok(Utils.isEqual(a, b), 'Cyclic structures are equal')\n      a[0].def = 'Larry'\n      b[0].def = 'Larry'\n      ok(Utils.isEqual(a, b), 'Cyclic structures containing equivalent properties are equal')\n      a[0].def = new String('Larry')\n      b[0].def = new String('Curly')\n      ok(!Utils.isEqual(a, b), 'Cyclic structures containing different properties are not equal')\n\n      # Complex Circular References.\n      a = {foo: {b: {foo: {c: {foo: null}}}}}\n      b = {foo: {b: {foo: {c: {foo: null}}}}}\n      a.foo.b.foo.c.foo = a\n      b.foo.b.foo.c.foo = b\n      ok(Utils.isEqual(a, b), 'Cyclic structures with nested and identically-named properties are equal')\n\n      # Chaining.\n      # NOTE: underscore doesn't support chaining\n      #\n      # ok(!Utils.isEqual(_({x: 1, y: undefined}).chain(), _({x: 1, z: 2}).chain()), 'Chained objects containing different values are not equal')\n      #\n      # a = _({x: 1, y: 2}).chain()\n      # b = _({x: 1, y: 2}).chain()\n      # equal(Utils.isEqual(a.isEqual(b), _(true)), true, '`isEqual` can be chained')\n\n      # Objects without a `constructor` property\n      if Object.create\n        a = Object.create(null, {x: {value: 1, enumerable: true}})\n        b = {x: 1}\n        ok(Utils.isEqual(a, b), 'Handles objects without a constructor (e.g. from Object.create')\n\n      Foo = -> @a = 1\n      Foo.prototype.constructor = null\n\n      other = {a: 1}\n      ok(!Utils.isEqual(new Foo, other))\n\n  describe \"subjectWithPrefix\", ->\n    it \"should replace an existing Re:\", ->\n      expect(Utils.subjectWithPrefix(\"Re: Test Case\", \"Fwd:\")).toEqual(\"Fwd: Test Case\")\n\n    it \"should replace an existing re:\", ->\n      expect(Utils.subjectWithPrefix(\"re: Test Case\", \"Fwd:\")).toEqual(\"Fwd: Test Case\")\n\n    it \"should replace an existing Fwd:\", ->\n      expect(Utils.subjectWithPrefix(\"Fwd: Test Case\", \"Re:\")).toEqual(\"Re: Test Case\")\n\n    it \"should replace an existing fwd:\", ->\n      expect(Utils.subjectWithPrefix(\"fwd: Test Case\", \"Re:\")).toEqual(\"Re: Test Case\")\n\n    it \"should not replace Re: or Fwd: found embedded in the subject\", ->\n      expect(Utils.subjectWithPrefix(\"My questions are: 123\", \"Fwd:\")).toEqual(\"Fwd: My questions are: 123\")\n      expect(Utils.subjectWithPrefix(\"My questions fwd: 123\", \"Fwd:\")).toEqual(\"Fwd: My questions fwd: 123\")\n\n    it \"should work if no existing prefix is present\", ->\n      expect(Utils.subjectWithPrefix(\"My questions\", \"Fwd:\")).toEqual(\"Fwd: My questions\")\n"
  },
  {
    "path": "packages/client-app/src/apm-wrapper.coffee",
    "content": "_ = require 'underscore'\nQ = require 'q'\nsemver = require 'semver'\n\nBufferedProcess = require './buffered-process'\n\nmodule.exports =\nclass APMWrapper\n\n  constructor: ->\n    @packagePromises = []\n\n  runCommand: (args, options, callback) ->\n    command = NylasEnv.packages.getApmPath()\n    outputLines = []\n    stdout = (lines) -> outputLines.push(lines)\n    errorLines = []\n    stderr = (lines) -> errorLines.push(lines)\n    exit = (code) ->\n      callback(code, outputLines.join('\\n'), errorLines.join('\\n'))\n\n    options ||= {}\n    options.env =\n      ATOM_API_URL: 'https://edgehill-packages.nylas.com/api'\n      ATOM_RESOURCE_PATH: NylasEnv.getLoadSettings().resourcePath\n      ATOM_HOME: NylasEnv.getConfigDirPath()\n\n    if process.platform is \"win32\"\n      options.env[\"ProgramFiles\"] = process.env.ProgramFiles\n\n    args.push('--no-color')\n    new BufferedProcess({command, args, stdout, stderr, exit, options})\n\n  runCommandReturningPackages: (args, errorMessage, callback) ->\n    @runCommand args, null, (code, stdout, stderr) ->\n      if code is 0\n        try\n          packages = JSON.parse(stdout) ? []\n        catch parseError\n          error = createJsonParseError(errorMessage, parseError, stdout)\n          return callback(error)\n        callback(null, packages)\n      else\n        error = new Error(errorMessage)\n        error.stdout = stdout\n        error.stderr = stderr\n        callback(error)\n\n  loadInstalled: (callback) ->\n    args = ['ls', '--json']\n    errorMessage = 'Fetching local packages failed.'\n    apmProcess = @runCommandReturningPackages(args, errorMessage, callback)\n\n    handleProcessErrors(apmProcess, errorMessage, callback)\n\n  loadFeatured: (options, callback) ->\n    args = ['featured', '--json']\n    version = NylasEnv.getVersion()\n    args.push('--themes') if options.themes\n    args.push('--compatible', version) if semver.valid(version)\n    errorMessage = 'Fetching featured packages failed.'\n\n    apmProcess = @runCommandReturningPackages(args, errorMessage, callback)\n    handleProcessErrors(apmProcess, errorMessage, callback)\n\n  loadOutdated: (callback) ->\n    args = ['outdated', '--json']\n    version = NylasEnv.getVersion()\n    args.push('--compatible', version) if semver.valid(version)\n    errorMessage = 'Fetching outdated packages and themes failed.'\n\n    apmProcess = @runCommandReturningPackages(args, errorMessage, callback)\n    handleProcessErrors(apmProcess, errorMessage, callback)\n\n  loadPackage: (packageName, callback) ->\n    args = ['view', packageName, '--json']\n    errorMessage = \"Fetching package '#{packageName}' failed.\"\n\n    apmProcess = @runCommandReturningPackages(args, errorMessage, callback)\n    handleProcessErrors(apmProcess, errorMessage, callback)\n\n  loadCompatiblePackageVersion: (packageName, callback) ->\n    args = ['view', packageName, '--json', '--compatible', @normalizeVersion(NylasEnv.getVersion())]\n    errorMessage = \"Fetching package '#{packageName}' failed.\"\n\n    apmProcess = @runCommandReturningPackages(args, errorMessage, callback)\n    handleProcessErrors(apmProcess, errorMessage, callback)\n\n  getInstalled: ->\n    Promise.promisify(@loadInstalled, {context: this})()\n\n  getFeatured: (options = {}) ->\n    Promise.promisify(@loadFeatured, {context: this})(options)\n\n  getOutdated: ->\n    Promise.promisify(@loadOutdated, {context: this})()\n\n  getPackage: (packageName) ->\n    @packagePromises[packageName] ?= Promise.promisify(@loadPackage, {context: this})()\n\n  satisfiesVersion: (version, metadata) ->\n    engine = metadata.engines?.nylas ? '*'\n    return false unless semver.validRange(engine)\n    return semver.satisfies(version, engine)\n\n  normalizeVersion: (version) ->\n    [version] = version.split('-') if typeof version is 'string'\n    version\n\n  search: (query, options = {}) ->\n    deferred = Promise.defer()\n\n    args = ['search', query, '--json']\n    if options.themes\n      args.push '--themes'\n    else if options.packages\n      args.push '--packages'\n    errorMessage = \"Searching for \\u201C#{query}\\u201D failed.\"\n\n    apmProcess = @runCommand args, null, (code, stdout, stderr) ->\n      if code is 0\n        try\n          packages = JSON.parse(stdout) ? []\n          deferred.resolve(packages)\n        catch parseError\n          error = createJsonParseError(errorMessage, parseError, stdout)\n          deferred.reject(error)\n      else\n        error = new Error(errorMessage)\n        error.stdout = stdout\n        error.stderr = stderr\n        deferred.reject(error)\n\n    handleProcessErrors apmProcess, errorMessage, (error) ->\n      deferred.reject(error)\n\n    deferred.promise\n\n  update: (pack, newVersion, callback) ->\n    {name, theme} = pack\n\n    if theme\n      activateOnSuccess = NylasEnv.packages.isPackageActive(name)\n    else\n      activateOnSuccess = not NylasEnv.packages.isPackageDisabled(name)\n    activateOnFailure = NylasEnv.packages.isPackageActive(name)\n    NylasEnv.packages.deactivatePackage(name) if NylasEnv.packages.isPackageActive(name)\n    NylasEnv.packages.unloadPackage(name) if NylasEnv.packages.isPackageLoaded(name)\n\n    errorMessage = \"Updating to \\u201C#{name}@#{newVersion}\\u201D failed.\"\n    onError = (error) =>\n      error.packageInstallError = not theme\n      callback(error)\n\n    args = ['install', \"#{name}@#{newVersion}\"]\n    exit = (code, stdout, stderr) =>\n      if code is 0\n        if activateOnSuccess\n          NylasEnv.packages.activatePackage(name)\n        else\n          NylasEnv.packages.loadPackage(name)\n\n        callback?()\n      else\n        NylasEnv.packages.activatePackage(name) if activateOnFailure\n        error = new Error(errorMessage)\n        error.stdout = stdout\n        error.stderr = stderr\n        onError(error)\n\n    apmProcess = @runCommand(args, null, exit)\n    handleProcessErrors(apmProcess, errorMessage, onError)\n\n  unload: (packageName) ->\n    if NylasEnv.packages.isPackageLoaded(name)\n      NylasEnv.packages.deactivatePackage(name) if NylasEnv.packages.isPackageActive(name)\n      NylasEnv.packages.unloadPackage(name)\n\n  install: (pack, callback) ->\n    {name, version, theme} = pack\n    activateOnSuccess = not theme and not NylasEnv.packages.isPackageDisabled(name)\n    activateOnFailure = NylasEnv.packages.isPackageActive(name)\n\n    @unload(name)\n    args = ['install', \"#{name}@#{version}\"]\n\n    errorMessage = \"Installing \\u201C#{name}@#{version}\\u201D failed.\"\n    onError = (error) =>\n      error.packageInstallError = not theme\n      callback(error)\n\n    exit = (code, stdout, stderr) =>\n      if code is 0\n        if activateOnSuccess\n          NylasEnv.packages.activatePackage(name)\n        else\n          NylasEnv.packages.loadPackage(name)\n\n        callback?()\n      else\n        NylasEnv.packages.activatePackage(name) if activateOnFailure\n        error = new Error(errorMessage)\n        error.stdout = stdout\n        error.stderr = stderr\n        onError(error)\n\n    apmProcess = @runCommand(args, null, exit)\n    handleProcessErrors(apmProcess, errorMessage, onError)\n\n  installDependenciesInPackageDirectory: (dir, callback) ->\n    errorMessage = \"Running apm install failed to install package dependencies.\"\n\n    exit = (code, stdout, stderr) =>\n      if code is 0\n        callback?()\n      else\n        error = new Error(errorMessage)\n        error.stdout = stdout\n        error.stderr = stderr\n        callback(error)\n\n    apmProcess = @runCommand(['install', '--production'], {cwd: dir}, exit)\n    handleProcessErrors(apmProcess, errorMessage, callback)\n\n  uninstall: (pack, callback) ->\n    {name} = pack\n\n    NylasEnv.packages.deactivatePackage(name) if NylasEnv.packages.isPackageActive(name)\n\n    errorMessage = \"Uninstalling \\u201C#{name}\\u201D failed.\"\n    onError = (error) =>\n      callback(error)\n\n    apmProcess = @runCommand ['uninstall', '--hard', name], null, (code, stdout, stderr) =>\n      if code is 0\n        @unload(name)\n        callback?()\n      else\n        error = new Error(errorMessage)\n        error.stdout = stdout\n        error.stderr = stderr\n        onError(error)\n\n    handleProcessErrors(apmProcess, errorMessage, onError)\n\n  canUpgrade: (installedPackage, availableVersion) ->\n    return false unless installedPackage?\n\n    installedVersion = installedPackage.metadata.version\n    return false unless semver.valid(installedVersion)\n    return false unless semver.valid(availableVersion)\n\n    semver.gt(availableVersion, installedVersion)\n\n  checkNativeBuildTools: ->\n    deferred = Promise.defer()\n    apmProcess = @runCommand ['install', '--check'], null, (code, stdout, stderr) ->\n      if code is 0\n        deferred.resolve()\n      else\n        deferred.reject(new Error())\n\n    apmProcess.onWillThrowError ({error, handle}) ->\n      handle()\n      deferred.reject(error)\n\n    deferred.promise\n\ncreateJsonParseError = (message, parseError, stdout) ->\n  error = new Error(message)\n  error.stdout = ''\n  error.stderr = \"#{parseError.message}: #{stdout}\"\n  error\n\ncreateProcessError = (message, processError) ->\n  error = new Error(message)\n  error.stdout = ''\n  error.stderr = processError.message\n  error\n\nhandleProcessErrors = (apmProcess, message, callback) ->\n  apmProcess.onWillThrowError ({error, handle}) ->\n    handle()\n    callback(createProcessError(message, error))\n"
  },
  {
    "path": "packages/client-app/src/browser/application-menu.coffee",
    "content": "{BrowserWindow, Menu, MenuItem, app} = require 'electron'\n_ = require 'underscore'\nUtils = require '../flux/models/utils'\n\n# Used to manage the global application menu.\n#\n# It's created by {Application} upon instantiation and used to add, remove\n# and maintain the state of all menu items.\nmodule.exports =\nclass ApplicationMenu\n  constructor: (@version) ->\n    @windowTemplates = new WeakMap()\n    @setActiveTemplate(@getDefaultTemplate())\n\n  # Public: Updates the entire menu with the given keybindings.\n  #\n  # window - The BrowserWindow this menu template is associated with.\n  # template - The Object which describes the menu to display.\n  # keystrokesByCommand - An Object where the keys are commands and the values\n  #                       are Arrays containing the keystroke.\n  update: (window, template, keystrokesByCommand) ->\n    @translateTemplate(template, keystrokesByCommand)\n    @windowTemplates.set(window, template)\n    @setActiveTemplate(template) if window is @lastFocusedWindow\n\n  setActiveTemplate: (template) ->\n    unless _.isEqual(template, @activeTemplate)\n      @activeTemplate = template\n      @rebuildMenuWithActiveTemplate()\n\n  rebuildMenuWithActiveTemplate: ->\n    fullTemplate = Utils.deepClone(@activeTemplate)\n    @extendTemplateWithVersion(fullTemplate)\n    @extendTemplateWithWindowMenu(fullTemplate)\n\n    @menu = Menu.buildFromTemplate(fullTemplate)\n    Menu.setApplicationMenu(@menu)\n\n    @updateFullscreenMenuItem(@lastFocusedWindow?.isFullScreen())\n    @updateDevModeItem()\n\n  # Register a BrowserWindow with this application menu.\n  addWindow: (window) ->\n    @lastFocusedWindow ?= window\n\n    focusHandler = =>\n      @lastFocusedWindow = window\n      if template = @windowTemplates.get(window)\n        @setActiveTemplate(template)\n\n    window.on 'focus', focusHandler\n    window.on 'enter-full-screen', focusHandler\n    window.on 'leave-full-screen', focusHandler\n    window.once 'closed', =>\n      @lastFocusedWindow = null if window is @lastFocusedWindow\n      @windowTemplates.delete(window)\n      @rebuildMenuWithActiveTemplate()\n      window.removeListener 'focus', focusHandler\n      window.removeListener 'enter-full-screen', focusHandler\n      window.removeListener 'leave-full-screen', focusHandler\n\n    @rebuildMenuWithActiveTemplate()\n    @enableWindowSpecificItems(true)\n\n  # Flattens the given menu and submenu items into an single Array.\n  #\n  # menu - A complete menu configuration object for electron's menu API.\n  #\n  # Returns an Array of native menu items.\n  flattenMenuItems: (menu) ->\n    items = []\n    for index, item of menu.items or {}\n      items.push(item)\n      items = items.concat(@flattenMenuItems(item.submenu)) if item.submenu\n    items\n\n  # Flattens the given menu template into an single Array.\n  #\n  # template - An object describing the menu item.\n  #\n  # Returns an Array of native menu items.\n  flattenMenuTemplate: (template) ->\n    items = []\n    for item in template\n      items.push(item)\n      items = items.concat(@flattenMenuTemplate(item.submenu)) if item.submenu\n    items\n\n  # Public: Used to make all window related menu items are active.\n  #\n  # enable - If true enables all window specific items, if false disables all\n  #          window specific items.\n  enableWindowSpecificItems: (enable) ->\n    for item in @flattenMenuItems(@menu)\n      item.enabled = enable if item.metadata?['windowSpecific']\n\n  # Replaces VERSION with the current version.\n  extendTemplateWithVersion: (template) ->\n    if (item = _.find(@flattenMenuTemplate(template), ({label}) -> label == 'VERSION'))\n      item.label = \"Version #{@version}\"\n\n  extendTemplateWithWindowMenu: (template) ->\n    windowMenu = _.find(template, ({label}) -> label is 'Window')\n    return unless windowMenu\n    idx = _.findIndex(windowMenu.submenu, ({id}) -> id is 'window-list-separator')\n\n    workShortcut = \"CmdOrCtrl+alt+w\"\n    if process.platform is \"win32\"\n      workShortcut = \"ctrl+shift+w\"\n\n    accelerators = {\n      'default': 'CmdOrCtrl+0',\n      'work': workShortcut,\n    }\n    windows = global.application.windowManager.getOpenWindows()\n    windowsItems = windows.map (w) => {\n      label: w.loadSettings().title || \"Window\"\n      accelerator: accelerators[w.windowType]\n      click: ->\n        w.show()\n        w.focus()\n      }\n    windowMenu.submenu.splice(idx, 0, {type: 'separator'}, windowsItems...)\n\n  updateFullscreenMenuItem: (fullscreen) ->\n    enterItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Enter Full Screen')\n    exitItem = _.find(@flattenMenuItems(@menu), ({label}) -> label == 'Exit Full Screen')\n    return unless enterItem and exitItem\n    enterItem.visible = !fullscreen\n    exitItem.visible = fullscreen\n\n  updateDevModeItem: ->\n    devModeItem = _.find(@flattenMenuItems(@menu), ({command}) -> command is 'application:toggle-dev')\n    devModeItem?.checked = global.application.devMode\n\n  # Default list of menu items.\n  #\n  # Returns an Array of menu item Objects.\n  getDefaultTemplate: ->\n    [\n      label: \"N1\"\n      submenu: [\n          { label: \"Check for Update\", metadata: {autoUpdate: true}}\n          { label: 'Reload', accelerator: 'CmdOrCtrl+R', click: => BrowserWindow.getFocusedWindow()?.reload() }\n          { label: 'Close Window', accelerator: 'CmdOrCtrl+Shift+W', click: => BrowserWindow.getFocusedWindow()?.close() }\n          { label: 'Toggle Dev Tools', accelerator: 'CmdOrCtrl+Alt+I', click: => BrowserWindow.getFocusedWindow()?.toggleDevTools() }\n          { label: 'Quit', accelerator: 'CmdOrCtrl+Q', click: -> app.quit() }\n      ]\n    ]\n\n  # Combines a menu template with the appropriate keystroke.\n  #\n  # template - An Object conforming to electron's menu api but lacking\n  #            accelerator and click properties.\n  # keystrokesByCommand - An Object where the keys are commands and the values\n  #                       are Arrays containing the keystroke.\n  #\n  # Returns a complete menu configuration object for electron's menu API.\n  translateTemplate: (template, keystrokesByCommand) ->\n    template.forEach (item) =>\n      item.metadata ?= {}\n      if item.command\n        item.accelerator = @acceleratorForCommand(item.command, keystrokesByCommand)\n        item.click = -> global.application.sendCommand(item.command, item.args)\n        item.metadata['windowSpecific'] = true unless /^application:/.test(item.command)\n      @translateTemplate(item.submenu, keystrokesByCommand) if item.submenu\n    template\n\n  # Determine the accelerator for a given command.\n  #\n  # command - The name of the command.\n  # keystrokesByCommand - An Object where the keys are commands and the values\n  #                       are Arrays containing the keystroke.\n  #\n  # Returns a String containing the keystroke in a format that can be interpreted\n  #   by Electron to provide nice icons where available.\n  acceleratorForCommand: (command, keystrokesByCommand) ->\n    firstKeystroke = keystrokesByCommand[command]?[0]\n    return null unless firstKeystroke\n\n    if /f\\d+/.test(firstKeystroke)\n      firstKeystroke = firstKeystroke.toUpperCase()\n\n    modifiers = firstKeystroke.split('+')\n    key = modifiers.pop()\n\n    modifiers = modifiers.map (modifier) ->\n      modifier.replace(/shift/ig, \"Shift\")\n              .replace(/command/ig, \"Command\")\n              .replace(/mod/ig, \"CmdOrCtrl\")\n              .replace(/ctrl/ig, \"Ctrl\")\n              .replace(/alt/ig, \"Alt\")\n\n    keys = modifiers.concat([key.toUpperCase()])\n    keys.join(\"+\")\n"
  },
  {
    "path": "packages/client-app/src/browser/application.es6",
    "content": "/* eslint global-require: \"off\" */\n\nimport {BrowserWindow, Menu, app, ipcMain, dialog, powerMonitor,\n        crashReporter} from 'electron';\n\nimport fs from 'fs-plus';\nimport url from 'url';\nimport path from 'path';\nimport proc from 'child_process'\nimport {EventEmitter} from 'events';\n\nimport GlobalTimer from './global-timer'\nimport WindowManager from './window-manager';\nimport FileListCache from './file-list-cache';\nimport DatabaseReader from './database-reader';\nimport ConfigMigrator from './config-migrator';\nimport ApplicationMenu from './application-menu';\nimport SystemTrayManager from './system-tray-manager';\nimport DefaultClientHelper from '../default-client-helper';\nimport NylasProtocolHandler from './nylas-protocol-handler';\nimport PackageMigrationManager from './package-migration-manager';\nimport ConfigPersistenceManager from './config-persistence-manager';\nimport preventLegacyN1Migration from './prevent-legacy-n1-migration';\n\nlet clipboard = null;\n\n// The application's singleton class.\n//\nexport default class Application extends EventEmitter {\n  async start(options) {\n    const {resourcePath, configDirPath, version, devMode, specMode, benchmarkMode, safeMode} = options;\n\n    // Initialize GlobalTimer and start timing app boot time\n    this.timer = new GlobalTimer()\n    this.timer.start('app-boot')\n\n    // Normalize to make sure drive letter case is consistent on Windows\n    this.resourcePath = path.normalize(resourcePath);\n    this.configDirPath = configDirPath;\n    this.version = version;\n    this.devMode = devMode;\n    this.benchmarkMode = benchmarkMode;\n    this.specMode = specMode;\n    this.safeMode = safeMode;\n\n    this.fileListCache = new FileListCache();\n    this.nylasProtocolHandler = new NylasProtocolHandler(this.resourcePath, this.safeMode);\n\n    this.databaseReader = new DatabaseReader({configDirPath, specMode});\n    try {\n      await this.databaseReader.open();\n    } catch (err) {\n      // We need to manually handle errors here because\n      // `handleUnrecoverableDatabaseError` will fail because global.app hasn't\n      // been defined at this point\n      dialog.showMessageBox({\n        type: 'warning',\n        buttons: ['Okay'],\n        message: `We encountered a problem with your local email database. We will now attempt to rebuild it.`,\n      });\n      this._deleteDatabase(() => {\n        app.relaunch()\n        app.quit()\n      })\n      return\n    }\n\n    const Config = require('../config');\n    const config = new Config();\n    this.config = config;\n    this.configPersistenceManager = new ConfigPersistenceManager({configDirPath, resourcePath});\n    config.load();\n\n    preventLegacyN1Migration(configDirPath)\n\n    this.configMigrator = new ConfigMigrator(this.config, this.databaseReader);\n    this.configMigrator.migrate()\n\n    this.packageMigrationManager = new PackageMigrationManager({config, configDirPath, version})\n    this.packageMigrationManager.migrate()\n\n    let initializeInBackground = options.background;\n    if (initializeInBackground === undefined) {\n      initializeInBackground = false;\n    }\n\n    this.applicationMenu = new ApplicationMenu(version);\n    this.windowManager = new WindowManager({\n      resourcePath: this.resourcePath,\n      configDirPath: this.configDirPath,\n      config: this.config,\n      devMode: this.devMode,\n      benchmarkMode: this.benchmarkMode,\n      specMode: this.specMode,\n      safeMode: this.safeMode,\n      initializeInBackground: initializeInBackground,\n    });\n    this.systemTrayManager = new SystemTrayManager(process.platform, this);\n    this._databasePhase = 'setup';\n\n    this.setupJavaScriptArguments();\n    this.handleEvents();\n    this.handleLaunchOptions(options);\n\n    if (process.platform === 'linux') {\n      const helper = new DefaultClientHelper();\n      helper.registerForURLScheme('nylas');\n    } else {\n      app.setAsDefaultProtocolClient('nylas');\n    }\n\n    if (process.platform === 'darwin') {\n      const addedToDock = config.get('addedToDock');\n      const appPath = process.argv[0];\n      if (!addedToDock && appPath.includes('/Applications/') && appPath.includes('.app/')) {\n        proc.exec(`defaults write com.apple.dock persistent-apps -array-add \"<dict><key>tile-data</key><dict><key>file-data</key><dict><key>_CFURLString</key><string>${appPath.split('.app/')[0]}.app/</string><key>_CFURLStringType</key><integer>0</integer></dict></dict></dict>\"`);\n        config.set('addedToDock', true);\n      }\n    }\n  }\n\n  getMainWindow() {\n    return this.windowManager.get(WindowManager.MAIN_WINDOW).browserWindow;\n  }\n\n  getAllWindowDimensions() {\n    return this.windowManager.getAllWindowDimensions()\n  }\n\n  isQuitting() {\n    return this.quitting;\n  }\n\n  // Opens a new window based on the options provided.\n  handleLaunchOptions(options) {\n    const {specMode, pathsToOpen, urlsToOpen} = options;\n\n    if (specMode) {\n      const {resourcePath, specDirectory, specFilePattern, logFile, showSpecsInWindow, jUnitXmlPath} = options;\n      const exitWhenDone = true;\n      this.runSpecs({exitWhenDone, showSpecsInWindow, resourcePath, specDirectory, specFilePattern, logFile, jUnitXmlPath});\n      return;\n    }\n\n    this.openWindowsForTokenState();\n\n    if ((pathsToOpen instanceof Array) && (pathsToOpen.length > 0)) {\n      this.openComposerWithFiles(pathsToOpen);\n    }\n    if (urlsToOpen instanceof Array) {\n      for (const urlToOpen of urlsToOpen) {\n        this.openUrl(urlToOpen);\n      }\n    }\n  }\n\n  // On Windows, removing a file can fail if a process still has it open. When\n  // we close windows and log out, we need to wait for these processes to completely\n  // exit and then delete the file. It's hard to tell when this happens, so we just\n  // retry the deletion a few times.\n  deleteFileWithRetry(filePath, callback = () => {}, retries = 5) {\n    const callbackWithRetry = (err) => {\n      if (err && (err.message.indexOf('no such file') === -1)) {\n        console.log(`File Error: ${err.message} - retrying in 150msec`);\n        setTimeout(() => {\n          this.deleteFileWithRetry(filePath, callback, retries - 1);\n        }, 150);\n      } else {\n        callback(null);\n      }\n    }\n\n    if (!fs.existsSync(filePath)) {\n      callback(null);\n      return\n    }\n\n    if (retries > 0) {\n      fs.unlink(filePath, callbackWithRetry);\n    } else {\n      fs.unlink(filePath, callback);\n    }\n  }\n\n  // Configures required javascript environment flags.\n  setupJavaScriptArguments() {\n    app.commandLine.appendSwitch('js-flags', '--harmony');\n  }\n\n  openWindowsForTokenState() {\n    const accounts = this.config.get('nylas.accounts');\n    const hasAccount = accounts && accounts.length > 0;\n\n    if (hasAccount) {\n      this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW);\n      this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);\n    } else {\n      this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {\n        title: \"Welcome to Nylas Mail\",\n      });\n      this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);\n    }\n  }\n\n  _relaunchToInitialWindows = ({resetConfig, resetDatabase} = {}) => {\n    // This will re-fetch the NylasID to update the feed url\n    this.setDatabasePhase('close');\n    this.windowManager.destroyAllWindows();\n\n    let fn = (callback) => callback()\n    if (resetDatabase) {\n      fn = this._deleteDatabase;\n    }\n\n    fn(async () => {\n      if (resetDatabase) {\n        this.databaseReader = new DatabaseReader({configDirPath: this.configDirPath, specMode: this.specMode});\n        await this.databaseReader.open()\n      }\n      if (resetConfig) {\n        this.config.set('nylas', null);\n        this.config.set('edgehill', null);\n      }\n      this.setDatabasePhase('setup');\n      this.openWindowsForTokenState();\n    });\n  }\n\n  _deleteDatabase = (callback) => {\n    this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db'), callback);\n    this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db-wal'));\n    this.deleteFileWithRetry(path.join(this.configDirPath, 'edgehill.db-shm'));\n  }\n\n  databasePhase() {\n    return this._databasePhase;\n  }\n\n  setDatabasePhase(phase) {\n    if (!['setup', 'ready', 'close'].includes(phase)) {\n      throw new Error(`setDatabasePhase: ${phase} is invalid.`);\n    }\n\n    if (phase === this._databasePhase) {\n      return;\n    }\n\n    this._databasePhase = phase;\n    this.windowManager.sendToAllWindows(\"database-phase-change\", {}, phase);\n  }\n\n  rebuildDatabase = ({showErrorDialog = true, detail = ''} = {}) => {\n    if (this._rebuildingDatabase) { return }\n    this._rebuildingDatabase = true\n    if (showErrorDialog) {\n      dialog.showMessageBox({\n        type: 'warning',\n        buttons: ['Okay'],\n        message: `We encountered a problem with your local email database. We will now attempt to rebuild it.`,\n        detail,\n      });\n    }\n\n    // We need to set a timeout so `rebuildDatabases` immediately returns.\n    // If we don't immediately return the main window caller wants to wait\n    // for this function to finish so it can get the return value via ipc.\n    // Unfortunately since this function destroys the main window\n    // immediately, an error will be thrown.\n    setTimeout(() => {\n      if (this._databasePhase === 'close') {\n        return;\n      }\n      this.setDatabasePhase('close');\n      this.windowManager.destroyAllWindows();\n      this._deleteDatabase(async () => {\n        this.databaseReader = new DatabaseReader({configDirPath: this.configDirPath, specMode: this.specMode});\n        await this.databaseReader.open()\n        this.setDatabasePhase('setup');\n        this._rebuildingDatabase = false\n        this.openWindowsForTokenState();\n      });\n    }, 0);\n  }\n\n  // Registers basic application commands, non-idempotent.\n  // Note: If these events are triggered while an application window is open, the window\n  // needs to manually bubble them up to the Application instance via IPC or they won't be\n  // handled. This happens in workspace-element.coffee\n  handleEvents() {\n    this.on('application:run-all-specs', () => {\n      const win = this.windowManager.focusedWindow();\n      this.runSpecs({\n        exitWhenDone: false,\n        showSpecsInWindow: true,\n        resourcePath: this.resourcePath,\n        safeMode: win && win.safeMode,\n      });\n    });\n\n    this.on('application:run-package-specs', () => {\n      dialog.showOpenDialog({\n        title: 'Choose a Package Directory',\n        defaultPath: this.configDirPath,\n        buttonLabel: 'Choose',\n        properties: ['openDirectory'],\n      }, (filenames) => {\n        if (!filenames || filenames.length === 0) {\n          return;\n        }\n        this.runSpecs({\n          exitWhenDone: false,\n          showSpecsInWindow: true,\n          resourcePath: this.resourcePath,\n          specDirectory: filenames[0],\n        });\n      });\n    });\n\n    this.on('application:relaunch-to-initial-windows', this._relaunchToInitialWindows);\n\n    this.on('application:quit', () => {\n      app.quit()\n    });\n\n    this.on('application:inspect', ({x, y, nylasWindow}) => {\n      const win = nylasWindow || this.windowManager.focusedWindow();\n      if (!win) {\n        return;\n      }\n      win.browserWindow.inspectElement(x, y);\n    });\n\n    this.on('application:add-account', ({existingAccount, accountType, source} = {}) => {\n      this.timer.start('open-add-account-window')\n      const onboarding = this.windowManager.get(WindowManager.ONBOARDING_WINDOW);\n      if (onboarding) {\n        if (onboarding.browserWindow.webContents) {\n          onboarding.browserWindow.webContents.send('set-account-type', accountType)\n        }\n        onboarding.show();\n        onboarding.focus();\n      } else {\n        this.windowManager.ensureWindow(WindowManager.ONBOARDING_WINDOW, {\n          title: \"Add an Account\",\n          windowProps: { addingAccount: true, existingAccount, accountType, source },\n        });\n      }\n    });\n\n    this.on('application:new-message', () => {\n      const main = this.windowManager.get(WindowManager.MAIN_WINDOW);\n      if (main) { main.sendMessage('new-message') }\n    });\n\n    this.on('application:view-help', () => {\n      const helpUrl = 'https://support.nylas.com/hc/en-us/categories/200419318-Help-for-N1-users';\n      require('electron').shell.openExternal(helpUrl);\n    });\n\n    this.on('application:open-preferences', () => {\n      const main = this.windowManager.get(WindowManager.MAIN_WINDOW);\n      if (main) { main.sendMessage('open-preferences') }\n    });\n\n    this.on('application:show-main-window', () => {\n      this.openWindowsForTokenState();\n    });\n\n    this.on('application:toggle-dev', () => {\n      let args = process.argv.slice(1);\n      if (args.includes('--dev')) {\n        args = args.filter(a => a !== '--dev');\n      } else {\n        args.push('--dev')\n      }\n      app.relaunch({args});\n      app.quit();\n    });\n\n    if (process.platform === 'darwin') {\n      this.on('application:about', () => {\n        Menu.sendActionToFirstResponder('orderFrontStandardAboutPanel:')\n      });\n      this.on('application:bring-all-windows-to-front', () => {\n        Menu.sendActionToFirstResponder('arrangeInFront:')\n      });\n      this.on('application:hide', () => {\n        Menu.sendActionToFirstResponder('hide:')\n      });\n      this.on('application:hide-other-applications', () => {\n        Menu.sendActionToFirstResponder('hideOtherApplications:')\n      });\n      this.on('application:minimize', () => {\n        Menu.sendActionToFirstResponder('performMiniaturize:')\n      });\n      this.on('application:unhide-all-applications', () => {\n        Menu.sendActionToFirstResponder('unhideAllApplications:')\n      });\n      this.on('application:zoom', () => {\n        Menu.sendActionToFirstResponder('zoom:')\n      });\n    } else {\n      this.on('application:minimize', () => {\n        const win = this.windowManager.focusedWindow();\n        if (win) { win.minimize() }\n      });\n      this.on('application:zoom', () => {\n        const win = this.windowManager.focusedWindow();\n        if (win) { win.maximize() }\n      });\n    }\n\n    powerMonitor.on('resume', () => {\n      this.windowManager.sendToAllWindows('app-resumed-from-sleep', {})\n    })\n\n    app.on('ready', () => {\n      crashReporter.start({\n        productName: 'Nylas Mail',\n        companyName: 'Nylas, Inc.',\n        submitURL: 'https://nylas-breakpad-sentry.herokuapp.com/crashreport',\n        autoSubmit: true,\n      });\n    });\n\n    app.on('window-all-closed', () => {\n      this.windowManager.quitWinLinuxIfNoWindows()\n    });\n\n    // Called before the app tries to close any windows.\n    app.on('before-quit', () => {\n      // Allow the main window to be closed.\n      this.quitting = true;\n      // Destroy hot windows so that they can't block the app from quitting.\n      // (Electron will wait for them to finish loading before quitting.)\n      this.windowManager.cleanupBeforeAppQuit();\n      this.systemTrayManager.destroyTray();\n    });\n\n    // Called after the app has closed all windows.\n    app.on('will-quit', () => {\n      this.setDatabasePhase('close');\n    });\n\n    app.on('will-exit', () => {\n      this.setDatabasePhase('close');\n    });\n\n    app.on('open-file', (event, pathToOpen) => {\n      this.openComposerWithFiles([pathToOpen]);\n      event.preventDefault();\n    });\n\n    app.on('open-url', (event, urlToOpen) => {\n      this.openUrl(urlToOpen);\n      event.preventDefault();\n    });\n\n    // System Tray\n    ipcMain.on('update-system-tray', (event, ...args) => {\n      this.systemTrayManager.updateTraySettings(...args);\n    });\n\n    ipcMain.on('set-badge-value', (event, value) => {\n      if (app.dock && app.dock.setBadge) {\n        app.dock.setBadge(value);\n      } else if (app.setBadgeCount) {\n        app.setBadgeCount(value.length ? (value.replace(\"+\", \"\") / 1) : 0);\n      }\n    });\n\n    ipcMain.on('new-window', (event, options) => {\n      const win = options.windowKey ? this.windowManager.get(options.windowKey) : null;\n      if (win) {\n        win.show();\n        win.focus();\n      } else {\n        this.windowManager.newWindow(options);\n      }\n    });\n\n    ipcMain.on('ensure-worker-window', () => {\n      this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)\n    })\n\n    ipcMain.on('inline-style-parse', (event, {html, key}) => {\n      const juice = require('juice');\n      let out = null;\n      try {\n        out = juice(html);\n      } catch (e) {\n        // If the juicer fails (because of malformed CSS or some other\n        // reason), then just return the body. We will still push it\n        // through the HTML sanitizer which will strip the style tags. Oh\n        // well.\n        out = html\n      }\n      // win = BrowserWindow.fromWebContents(event.sender)\n      event.sender.send('inline-styles-result', {html: out, key});\n    });\n\n    app.on('activate', (event, hasVisibleWindows) => {\n      if (!hasVisibleWindows) {\n        this.openWindowsForTokenState();\n      }\n      event.preventDefault();\n    });\n\n    ipcMain.on('update-application-menu', (event, template, keystrokesByCommand) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      this.applicationMenu.update(win, template, keystrokesByCommand);\n    });\n\n    ipcMain.on('command', (event, command, ...args) => {\n      this.emit(command, ...args);\n    });\n\n    ipcMain.on('window-command', (event, command, ...args) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      win.emit(command, ...args);\n    });\n\n    ipcMain.on('call-window-method', (event, method, ...args) => {\n      const win = BrowserWindow.fromWebContents(event.sender);\n      if (!win[method]) {\n        console.error(`Method ${method} does not exist on BrowserWindow!`);\n      }\n      win[method](...args)\n    });\n\n    ipcMain.on('call-devtools-webcontents-method', (event, method, ...args) => {\n      // If devtools aren't open the `webContents::devToolsWebContents` will be null\n      if (event.sender.devToolsWebContents) {\n        event.sender.devToolsWebContents[method](...args);\n      }\n    });\n\n    ipcMain.on('call-webcontents-method', (event, method, ...args) => {\n      if (!event.sender[method]) {\n        console.error(`Method ${method} does not exist on WebContents!`);\n      }\n      event.sender[method](...args);\n    });\n\n    ipcMain.on('action-bridge-rebroadcast-to-all', (event, ...args) => {\n      const win = BrowserWindow.fromWebContents(event.sender)\n      this.windowManager.sendToAllWindows('action-bridge-message', {except: win}, ...args)\n    });\n\n    ipcMain.on('action-bridge-rebroadcast-to-work', (event, ...args) => {\n      const workWindow = this.windowManager.get(WindowManager.WORK_WINDOW)\n      if (!workWindow || !workWindow.browserWindow.webContents) {\n        return;\n      }\n      if (BrowserWindow.fromWebContents(event.sender) === workWindow) {\n        return;\n      }\n      workWindow.browserWindow.webContents.send('action-bridge-message', ...args);\n    });\n\n    ipcMain.on('write-text-to-selection-clipboard', (event, selectedText) => {\n      clipboard = require('electron').clipboard;\n      clipboard.writeText(selectedText, 'selection');\n    });\n\n    ipcMain.on('account-setup-successful', () => {\n      this.windowManager.ensureWindow(WindowManager.MAIN_WINDOW);\n      this.windowManager.ensureWindow(WindowManager.WORK_WINDOW);\n      const onboarding = this.windowManager.get(WindowManager.ONBOARDING_WINDOW);\n      if (onboarding) {\n        onboarding.close();\n      }\n    });\n\n    ipcMain.on('new-account-added', () => {\n      this.windowManager.ensureWindow(WindowManager.WORK_WINDOW)\n    });\n\n    ipcMain.on('run-in-window', (event, params) => {\n      const sourceWindow = BrowserWindow.fromWebContents(event.sender);\n      this._sourceWindows = this._sourceWindows || {};\n      this._sourceWindows[params.taskId] = sourceWindow\n\n      const targetWindowKey = {\n        work: WindowManager.WORK_WINDOW,\n        main: WindowManager.MAIN_WINDOW,\n      }[params.window];\n      if (!targetWindowKey) {\n        throw new Error(\"We don't support running in that window\");\n      }\n\n      const targetWindow = this.windowManager.get(targetWindowKey);\n      if (!targetWindow || !targetWindow.browserWindow.webContents) {\n        return;\n      }\n      targetWindow.browserWindow.webContents.send('run-in-window', params);\n    });\n\n    ipcMain.on('remote-run-results', (event, params) => {\n      const sourceWindow = this._sourceWindows[params.taskId];\n      sourceWindow.webContents.send('remote-run-results', params);\n      delete this._sourceWindows[params.taskId];\n    });\n\n    ipcMain.on(\"report-error\", (event, params = {}) => {\n      try {\n        const errorParams = JSON.parse(params.errorJSON || \"{}\");\n        const extra = JSON.parse(params.extra || \"{}\");\n        let err = new Error();\n        err = Object.assign(err, errorParams);\n        global.errorLogger.reportError(err, extra)\n      } catch (parseError) {\n        console.error(parseError)\n        global.errorLogger.reportError(parseError, {})\n      }\n      event.returnValue = true\n    })\n\n    ipcMain.on(\"quit-with-error-message\", (event, message) => {\n      if (this.quitting) {\n        event.returnValue = true;\n        return true\n      }\n      this.quitting = true;\n      if (message) {\n        dialog.showMessageBox({\n          type: 'error',\n          buttons: ['Okay'],\n          message: message,\n        });\n      }\n      app.quit();\n      event.returnValue = true;\n      return true\n    })\n\n    ipcMain.on(\"move-to-applications\", () => {\n      if (process.platform !== \"darwin\") {\n        return;\n      }\n      const re = /(^.*?\\.app)/i;\n      const appPath = (re.exec(process.argv[0]) || [])[0];\n      if (!appPath) {\n        throw new Error(`Couldn't find .app in launch path: ${process.argv[0]}`)\n      }\n      let appName = appPath.split(\"/\");\n      appName = appName[appName.length - 1]\n      if (!appName) {\n        throw new Error(`Couldn't find .app in app path: ${appPath}`)\n      }\n      const escapedName = this._escapeShell(appName);\n      const escapedPath = this._escapeShell(appPath);\n\n      if (!escapedName || escapedName.trim().length === 0) {\n        throw new Error(`escapedName is invalid: ${escapedName}`)\n      }\n\n      // We separate the commands with a `;` instead of `&&` so in case the\n      // mv fails, the open will still run.\n      // We need the sleep to let the first app fully finish quitting.\n      // Otherwise it'll attempt to re-open the existing app (the one in\n      // the process of quitting)\n      const newAppDest = `/Applications/${escapedName}`\n      let move = `mv`\n      try { fs.accessSync(appPath, fs.W_OK) } catch (e) { move = `cp -r` }\n      const cmd = `rm -rf ${newAppDest}; ${move} ${escapedPath} ${newAppDest}; sleep 0.5; open ${newAppDest}`;\n      app.once('will-quit', () => {\n        // We need to use `exec` since that will start a new shell process and\n        // allow us to kill this one.\n        proc.exec(cmd)\n      })\n      app.quit()\n    })\n  }\n\n  _escapeShell(cmd) {\n    return cmd.replace(/([\"\\s'$`\\\\])/g, '\\\\$1');\n  }\n\n  // Public: Executes the given command.\n  //\n  // If it isn't handled globally, delegate to the currently focused window.\n  // If there is no focused window (all the windows of the app are hidden),\n  // fire the command to the main window. (This ensures that `application:`\n  // commands, like Cmd-N work when no windows are visible.)\n  //\n  // command - The string representing the command.\n  // args - The optional arguments to pass along.\n  sendCommand(command, ...args) {\n    if (this.emit(command, ...args)) {\n      return;\n    }\n    const focusedWindow = this.windowManager.focusedWindow()\n    if (focusedWindow) {\n      focusedWindow.sendCommand(command, ...args);\n    } else {\n      if (this.sendCommandToFirstResponder(command)) {\n        return;\n      }\n\n      const focusedBrowserWindow = BrowserWindow.getFocusedWindow()\n      const mainWindow = this.windowManager.get(WindowManager.MAIN_WINDOW)\n      if (focusedBrowserWindow) {\n        switch (command) {\n          case 'window:reload':\n            focusedBrowserWindow.reload();\n            break;\n          case 'window:toggle-dev-tools':\n            focusedBrowserWindow.toggleDevTools();\n            break;\n          case 'window:close':\n            focusedBrowserWindow.close();\n            break;\n          default:\n            break;\n        }\n      } else if (mainWindow) {\n        mainWindow.sendCommand(command, ...args);\n      }\n    }\n  }\n\n  // Public: Executes the given command on the given window.\n  //\n  // command - The string representing the command.\n  // nylasWindow - The {NylasWindow} to send the command to.\n  // args - The optional arguments to pass along.\n  sendCommandToWindow = (command, nylasWindow, ...args) => {\n    console.log('sendCommandToWindow');\n    console.log(command);\n    if (this.emit(command, ...args)) {\n      return;\n    }\n    if (nylasWindow) {\n      nylasWindow.sendCommand(command, ...args);\n    } else {\n      this.sendCommandToFirstResponder(command);\n    }\n  };\n\n  // Translates the command into OS X action and sends it to application's first\n  // responder.\n  sendCommandToFirstResponder = (command) => {\n    if (process.platform !== 'darwin') {\n      return false;\n    }\n\n    const commandsToActions = {\n      'core:undo': 'undo:',\n      'core:redo': 'redo:',\n      'core:copy': 'copy:',\n      'core:cut': 'cut:',\n      'core:paste': 'paste:',\n      'core:select-all': 'selectAll:',\n    };\n\n    if (commandsToActions[command]) {\n      Menu.sendActionToFirstResponder(commandsToActions[command]);\n      return true;\n    }\n    return false;\n  };\n\n  // Open a mailto:// url.\n  //\n  openUrl(urlToOpen) {\n    const parts = url.parse(urlToOpen);\n    const main = this.windowManager.get(WindowManager.MAIN_WINDOW);\n\n    if (!main) {\n      console.log(`Ignoring URL - main window is not available, user may not be authed.`);\n      return;\n    }\n\n    if (parts.protocol === 'mailto:') {\n      main.sendMessage('mailto', urlToOpen);\n    } else if (parts.protocol === 'nylas:') {\n      // if (parts.host === 'calendar') {\n      //   this.openCalendarURL(parts.path);\n      if (parts.host === 'plugins') {\n        main.sendMessage('changePluginStateFromUrl', urlToOpen);\n      } else {\n        main.sendMessage('openExternalThread', urlToOpen);\n      }\n    } else {\n      console.log(`Ignoring unknown URL type: ${urlToOpen}`);\n    }\n  }\n\n  // openCalendarURL(command) {\n  //   if (command === '/open') {\n  //     this.windowManager.ensureWindow(WindowManager.CALENDAR_WINDOW, {\n  //       windowKey: WindowManager.CALENDAR_WINDOW,\n  //       windowType: WindowManager.CALENDAR_WINDOW,\n  //       title: \"Calendar\",\n  //       hidden: false,\n  //     });\n  //   } else if (command === '/close') {\n  //     const win = this.windowManager.get(WindowManager.CALENDAR_WINDOW);\n  //     if (win) { win.hide(); }\n  //   }\n  // }\n\n  openComposerWithFiles(pathsToOpen) {\n    const main = this.windowManager.get(WindowManager.MAIN_WINDOW);\n    if (main) { main.sendMessage('mailfiles', pathsToOpen) }\n  }\n\n  // Opens up a new {NylasWindow} to run specs within.\n  //\n  // options -\n  //   :exitWhenDone - A Boolean that, if true, will close the window upon\n  //                   completion and exit the app with the status code of\n  //                   1 if the specs failed and 0 if they passed.\n  //   :showSpecsInWindow - A Boolean that, if true, will run specs in a\n  //                        window\n  //   :resourcePath - The path to include specs from.\n  //   :specPath - The directory to load specs from.\n  //   :safeMode - A Boolean that, if true, won't run specs from ~/.nylas-mail/packages\n  //               and ~/.nylas-mail/dev/packages, defaults to false.\n  //   :jUnitXmlPath - The path to output jUnit XML reports to, if desired.\n  runSpecs(specWindowOptionsArg) {\n    const specWindowOptions = specWindowOptionsArg;\n    let {resourcePath} = specWindowOptions;\n    if ((resourcePath !== this.resourcePath) && (!fs.existsSync(resourcePath))) {\n      resourcePath = this.resourcePath;\n    }\n\n    let bootstrapScript = null;\n    try {\n      bootstrapScript = require.resolve(path.resolve(this.resourcePath, 'spec', 'n1-spec-runner', 'spec-bootstrap'));\n    } catch (error) {\n      bootstrapScript = require.resolve(path.resolve(__dirname, '..', '..', 'spec', 'n1-spec-runner', 'spec-bootstrap'));\n    }\n\n    // Important: Use .nylas-spec instead of .nylas-mail to avoid overwriting the\n    // user's real email config!\n    const configDirPath = path.join(app.getPath('home'), '.nylas-spec');\n\n    specWindowOptions.resourcePath = resourcePath;\n    specWindowOptions.configDirPath = configDirPath;\n    specWindowOptions.bootstrapScript = bootstrapScript;\n\n    this.windowManager.ensureWindow(WindowManager.SPEC_WINDOW, specWindowOptions);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/config-migrator.es6",
    "content": "export default class ConfigMigrator {\n  constructor(config, database) {\n    this.config = config;\n    this.database = database;\n  }\n\n  migrate() {\n    /**\n     * In version before 1.0.21 we stored the Nylas ID Identity in the Config.\n     * After 1.0.21 we moved it into the JSONBlob Database Store.\n     */\n    const oldIdentity = this.config.get(\"nylas.identity\") || {};\n    if (!oldIdentity.id) return;\n    const key = \"NylasID\"\n    const q = `REPLACE INTO JSONBlob (id, data, client_id) VALUES (?,?,?)`;\n    const jsonBlobData = {\n      id: key,\n      clientId: key,\n      serverId: key,\n      json: oldIdentity,\n    }\n    this.database.database.prepare(q).run([key, JSON.stringify(jsonBlobData), key])\n    this.config.set(\"nylas.identity\", null)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/config-persistence-manager.es6",
    "content": "import path from 'path';\nimport fs from 'fs-plus';\nimport {BrowserWindow, dialog, app} from 'electron';\nimport {atomicWriteFileSync} from '../fs-utils'\n\nlet _ = require('underscore');\n_ = _.extend(_, require('../config-utils'));\n\nconst RETRY_SAVES = 3\n\n\nexport default class ConfigPersistenceManager {\n  constructor({configDirPath, resourcePath} = {}) {\n    this.configDirPath = configDirPath;\n    this.resourcePath = resourcePath;\n\n    this.userWantsToPreserveErrors = false\n    this.saveRetries = 0\n    this.configFilePath = path.join(this.configDirPath, 'config.json')\n    this.settings = {};\n\n    this.initializeConfigDirectory();\n    this.load();\n  }\n\n  initializeConfigDirectory() {\n    if (!fs.existsSync(this.configDirPath)) {\n      fs.makeTreeSync(this.configDirPath);\n      const templateConfigDirPath = path.join(this.resourcePath, 'dot-nylas');\n      fs.copySync(templateConfigDirPath, this.configDirPath);\n    }\n\n    if (!fs.existsSync(this.configFilePath)) {\n      this.writeTemplateConfigFile();\n    }\n  }\n\n  writeTemplateConfigFile() {\n    const templateConfigPath = path.join(this.resourcePath, 'dot-nylas', 'config.json');\n    const templateConfig = fs.readFileSync(templateConfigPath);\n    fs.writeFileSync(this.configFilePath, templateConfig);\n  }\n\n  _showLoadErrorDialog(error) {\n    const message = `Failed to load \"${path.basename(this.configFilePath)}\"`;\n    let detail = (error.location) ? error.stack : error.message;\n\n    if (error instanceof SyntaxError) {\n      detail += `\\n\\nThe file ${this.configFilePath} has incorrect JSON formatting or is empty. Fix the formatting to resolve this error, or reset your settings to continue using N1.`\n    } else {\n      detail += `\\n\\nWe were unable to read the file ${this.configFilePath}. Make sure you have permissions to access this file, and check that the file is not open or being edited and try again.`\n    }\n\n    const clickedIndex = dialog.showMessageBox({\n      type: 'error',\n      message,\n      detail,\n      buttons: ['Quit', 'Try Again', 'Reset Configuration'],\n    });\n\n    switch (clickedIndex) {\n      case 0: return 'quit';\n      case 1: return 'tryagain';\n      case 2: return 'reset';\n      default:\n        throw new Error('Unknown button clicked');\n    }\n  }\n\n  load() {\n    this.userWantsToPreserveErrors = false;\n\n    try {\n      const json = JSON.parse(fs.readFileSync(this.configFilePath));\n      if (!json || !json['*']) {\n        throw new Error('config json appears empty');\n      }\n      this.settings = json['*'];\n      this.emitChangeEvent();\n    } catch (error) {\n      error.message = `Failed to load config.json: ${error.message}`;\n      global.errorLogger.reportError(error);\n\n      const action = this._showLoadErrorDialog(error);\n      if (action === 'quit') {\n        this.userWantsToPreserveErrors = true;\n        app.quit();\n        return;\n      }\n\n      if (action === 'tryagain') {\n        this.load();\n        return;\n      }\n\n      if (action !== 'reset') {\n        throw new Error(`Unknown action: ${action}`);\n      }\n\n      if (fs.existsSync(this.configFilePath)) {\n        fs.unlinkSync(this.configFilePath);\n      }\n      this.writeTemplateConfigFile();\n      this.load();\n    }\n  }\n\n  _showSaveErrorDialog() {\n    const clickedIndex = dialog.showMessageBox({\n      type: 'error',\n      message: `Failed to save \"${path.basename(this.configFilePath)}\"`,\n      detail: `\\n\\nWe were unable to save the file ${this.configFilePath}. Make sure you have permissions to access this file, and check that the file is not open or being edited and try again.`,\n      buttons: ['Okay', 'Try again'],\n    })\n    return ['ignore', 'retry'][clickedIndex];\n  }\n\n  save = () => {\n    if (this.userWantsToPreserveErrors) {\n      return;\n    }\n    const allSettings = {'*': this.settings};\n    const allSettingsJSON = JSON.stringify(allSettings, null, 2);\n    this.lastSaveTimestamp = Date.now();\n\n    try {\n      atomicWriteFileSync(this.configFilePath, allSettingsJSON)\n      this.saveRetries = 0\n    } catch (error) {\n      if (this.saveRetries >= RETRY_SAVES) {\n        error.message = `Failed to save config.json: ${error.message}`\n        global.errorLogger.reportError(error);\n        const action = this._showSaveErrorDialog();\n        this.saveRetries = 0;\n\n        if (action === 'retry') {\n          this.save()\n        }\n        return;\n      }\n\n      this.saveRetries++\n      this.save()\n    }\n  }\n\n  getRawValuesString = () => {\n    if (!this.settings || _.isEmpty(this.settings)) {\n      throw new Error('this.settings is empty');\n    }\n    return JSON.stringify(this.settings);\n  }\n\n  setSettings = (value, sourceWebcontentsId) => {\n    this.settings = value;\n    this.emitChangeEvent({sourceWebcontentsId});\n    this.save();\n  }\n\n  setRawValue = (keyPath, value, sourceWebcontentsId) => {\n    if (!keyPath) {\n      throw new Error('keyPath must not be false-y!');\n    }\n    _.setValueForKeyPath(this.settings, keyPath, value);\n    this.emitChangeEvent({sourceWebcontentsId});\n    this.save();\n  }\n\n  emitChangeEvent = ({sourceWebcontentsId} = {}) => {\n    global.application.config.updateSettings(this.settings);\n\n    BrowserWindow.getAllWindows().forEach((win) => {\n      if ((win.webContents) && (win.webContents.getId() !== sourceWebcontentsId)) {\n        win.webContents.send('on-config-reloaded', this.settings);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/database-reader.es6",
    "content": "import {openDatabase, databasePath} from '../database-helpers'\n\nexport default class DatabaseReader {\n  constructor({configDirPath, specMode}) {\n    this.databasePath = databasePath(configDirPath, specMode)\n  }\n\n  async open() {\n    this.database = await openDatabase(this.databasePath)\n  }\n\n  getJSONBlob(key) {\n    const q = `SELECT * FROM JSONBlob WHERE id = '${key}'`;\n    try {\n      const row = this.database.prepare(q).get();\n      if (!row || !row.data) return null\n      return (JSON.parse(row.data) || {}).json\n    } catch (err) {\n      return null\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/file-list-cache.es6",
    "content": "// File operations (like traversing directory trees) are extremely\n// expensive. If any window traverses a tree once, we keep a cache of it\n// on the backend process. That way any new windows don't need to spend\n// their precious load time performing the same expensive operation.\nexport default class FileListCache {\n  constructor() {\n    this.imageData = \"{}\" // A JSON stringified hash\n    this.packagePaths = []\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/global-timer.es6",
    "content": "import Utils from '../../src/flux/models/utils'\nconst BUFFER_SIZE = 100;\n\n/**\n * A benchmarking system to keep track of start and end times for various\n * event types.\n */\nexport default class GlobalTimer {\n  constructor() {\n    this._doneRuns = {}\n    this._pendingRuns = {}\n  }\n\n  start(key, now = Date.now()) {\n    if (!this._pendingRuns[key]) {\n      this._pendingRuns[key] = [now]\n    }\n  }\n\n  /**\n   * This will add split points in an ongoing run (or do nothing if no run\n   * has started). Useful for fine-grained debugging of long timers.\n   */\n  split(key) {\n    if (!this._pendingRuns[key]) { return {} }\n    this._pendingRuns[key].push(Date.now())\n    return {\n      split: this.calcSplit(this._pendingRuns[key]),\n      total: this.calcTotal(this._pendingRuns[key]),\n    }\n  }\n\n  stop(key, now = Date.now()) {\n    if (!this._pendingRuns[key]) { return 0 }\n\n    if (!this._doneRuns[key]) {\n      this._doneRuns[key] = []\n    }\n    this._pendingRuns[key].push(now);\n\n    const total = this.calcTotal(this._pendingRuns[key])\n    this._doneRuns[key].push(this._pendingRuns[key])\n    if (this._doneRuns[key].length > BUFFER_SIZE) {\n      this._doneRuns[key].shift()\n    }\n\n    delete this._pendingRuns[key]\n    return total\n  }\n\n  calcSplit(curRun) {\n    return curRun[curRun.length - 1] - curRun[curRun.length - 2]\n  }\n\n  calcTotal(curRun) {\n    return curRun[curRun.length - 1] - curRun[0]\n  }\n\n  calcMean(key) {\n    return Utils.mean(this.totals(key))\n  }\n\n  calcStdev(key) {\n    return Utils.stdev(this.totals(key))\n  }\n\n  totals(key) {\n    return this._doneRuns[key].map(this.calcTotal)\n  }\n\n  runsFor(key) {\n    return this._doneRuns[key]\n  }\n\n  isPending(key) {\n    return this._pendingRuns[key] != null\n  }\n\n  clear(key) {\n    delete this._doneRuns[key]\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/linux-updater-adapter.es6",
    "content": "import {EventEmitter} from 'events';\nimport request from 'request';\nimport _ from 'underscore';\n\n/*\nCurrently, this class doesn't do much. We don't display update notices within\nthe app because we can't provide a consistent upgrade path for linux users.\nHowever, we still want the app to report to our autoupdate service so we know\nhow many Linux users exist.\n*/\nclass LinuxUpdaterAdapter {\n\n  setFeedURL(feedURL) {\n    this.feedURL = feedURL;\n  }\n\n  checkForUpdates() {\n    if (!this.feedURL) {\n      return;\n    }\n    request(this.feedURL, () => {\n    });\n  }\n\n  quitAndInstall() {\n\n  }\n}\n\n_.extend(LinuxUpdaterAdapter.prototype, EventEmitter.prototype);\nconst adapter = new LinuxUpdaterAdapter();\nexport default adapter\n"
  },
  {
    "path": "packages/client-app/src/browser/main.js",
    "content": "/* eslint dot-notation: 0 */\n/* eslint global-require: 0 */\nglobal.shellStartTime = Date.now();\nvar util = require('util')\n\nconsole.inspect = function consoleInspect(val) {\n  console.log(util.inspect(val, true, depth=7, colorize=true));\n}\n\nconst app = require('electron').app;\nconst path = require('path');\nconst mkdirp = require('mkdirp');\n\nif (typeof process.setFdLimit === 'function') {\n  process.setFdLimit(1024);\n}\n\nconst setupConfigDir = (args) => {\n  let defaultDirName = \".nylas-mail\";\n  if (args.devMode) {\n    defaultDirName = \".nylas-dev\";\n  }\n  if (args.specMode) {\n    defaultDirName = \".nylas-spec\";\n  }\n  if (args.benchmarkMode) {\n    defaultDirName = \".nylas-bench\";\n  }\n  let configDirPath = path.join(app.getPath('home'), defaultDirName);\n\n  if (args.configDirPath) {\n    configDirPath = args.configDirPath;\n  } else if (process.env.NYLAS_HOME) {\n    configDirPath = process.env.NYLAS_HOME;\n  }\n\n  mkdirp.sync(configDirPath);\n  process.env.NYLAS_HOME = configDirPath;\n  return configDirPath;\n};\n\nconst setupCompileCache = (configDirPath) => {\n  const compileCache = require('../compile-cache');\n  return compileCache.setHomeDirectory(configDirPath);\n};\n\nconst setupErrorLogger = (args = {}) => {\n  const ErrorLogger = require('../error-logger');\n  const errorLogger = new ErrorLogger({\n    inSpecMode: args.specMode,\n    inDevMode: args.devMode,\n    inBenchmarkMode: args.benchmarkMode,\n    resourcePath: args.resourcePath,\n  });\n  process.on('uncaughtException', errorLogger.reportError);\n  process.on('unhandledRejection', errorLogger.reportError);\n  return errorLogger;\n};\n\nconst declareOptions = (argv) => {\n  const optimist = require('optimist');\n  const options = optimist(argv);\n  options.usage(\"Nylas Mail v\" + (app.getVersion()) + \"\\n\\nUsage: nylas-mail [options]\\n\\nRun Nylas Mail: The open source extensible email client\\n\\n`nylas-mail --dev` to start the client in dev mode.\\n\\n`n1 --test` to run unit tests.\");\n  options.alias('d', 'dev').boolean('d').describe('d', 'Run in development mode.');\n  options.alias('b', 'benchmark').boolean('b').describe('b', 'Run in benchmark mode.');\n  options.alias('t', 'test').boolean('t').describe('t', 'Run the specified specs and exit with error code on failures.');\n  options.boolean('safe').describe('safe', 'Do not load packages from ~/.nylas-mail/packages or ~/.nylas/dev/packages.');\n  options.alias('h', 'help').boolean('h').describe('h', 'Print this usage message.');\n  options.alias('l', 'log-file').string('l').describe('l', 'Log all test output to file.');\n  options.alias('c', 'config-dir-path').string('c').describe('c', 'Override the path to the Nylas Mail configuration directory');\n  options.alias('s', 'spec-directory').string('s').describe('s', 'Override the directory from which to run package specs');\n  options.alias('f', 'spec-file-pattern').string('f').describe('f', 'Override the default file regex to determine which tests should run (defaults to \"-spec\\.(coffee|js|jsx|cjsx|es6|es)$\" )');\n  options.alias('v', 'version').boolean('v').describe('v', 'Print the version.');\n  options.alias('b', 'background').boolean('b').describe('b', 'Start Nylas Mail in the background');\n  return options;\n};\n\nconst parseCommandLine = (argv) => {\n  const version = app.getVersion();\n  const options = declareOptions(argv.slice(1));\n  const args = options.argv;\n\n  if (args.help) {\n    process.stdout.write(options.help());\n    process.exit(0);\n  }\n  if (args.version) {\n    process.stdout.write(version + \"\\n\");\n    process.exit(0);\n  }\n  const devMode = args['dev'] || args['test'] || args['benchmark'];\n  const benchmarkMode = args['benchmark'];\n  const logFile = args['log-file'];\n  const specMode = args['test'];\n  const jUnitXmlPath = args['junit-xml'];\n  const safeMode = args['safe'];\n  const background = args['background'];\n  const configDirPath = args['config-dir-path'];\n  const specDirectory = args['spec-directory'];\n  const specFilePattern = args['spec-file-pattern'];\n  const showSpecsInWindow = specMode === \"window\";\n  const resourcePath = path.resolve(path.dirname(path.dirname(__dirname)));\n  const urlsToOpen = [];\n  const pathsToOpen = [];\n\n  // On Windows and Linux, mailto and file opens are passed in argv. Go through\n  // the items and pluck out things that look like mailto:, nylas:, file paths\n  let ignoreNext = false;\n  // args._ is all of the non-hyphenated options.\n  for (const arg of args._) {\n    if (ignoreNext) {\n      ignoreNext = false;\n      continue;\n    }\n    if (arg.includes('executed-from') || arg.includes('squirrel')) {\n      ignoreNext = true;\n      continue;\n    }\n    // Skip the argument if it's part of the main electron invocation.\n    if (path.resolve(arg) === resourcePath) {\n      continue;\n    }\n    if (arg.startsWith('mailto:') || arg.startsWith('nylas:')) {\n      urlsToOpen.push(arg);\n    } else if ((arg[0] !== '-') && (/[\\/|\\\\]/.test(arg))) {\n      pathsToOpen.push(arg);\n    }\n  }\n\n  if (args['path-environment']) {\n    process.env.PATH = args['path-environment'];\n  }\n\n  return {\n    version,\n    devMode,\n    background,\n    logFile,\n    specMode,\n    benchmarkMode,\n    jUnitXmlPath,\n    safeMode,\n    configDirPath,\n    specDirectory,\n    specFilePattern,\n    showSpecsInWindow,\n    resourcePath,\n    urlsToOpen,\n    pathsToOpen,\n  };\n};\n\n/*\n * \"Squirrel will spawn your app with command line flags on first run, updates,]\n * and uninstalls.\"\n *\n * Read: https://github.com/electron-archive/grunt-electron-installer#handling-squirrel-events\n * Read: https://github.com/electron/electron/blob/master/docs/api/auto-updater.md#windows\n */\nconst handleStartupEventWithSquirrel = () => {\n  if (process.platform !== 'win32') {\n    return false;\n  }\n  const options = {\n    allowEscalation: false,\n    registerDefaultIfPossible: false,\n  };\n\n  const WindowsUpdater = require('./windows-updater');\n  const squirrelCommand = process.argv[1];\n\n  switch (squirrelCommand) {\n    case '--squirrel-install':\n      WindowsUpdater.createRegistryEntries(options, () =>\n        WindowsUpdater.createShortcuts(() =>\n          app.quit()\n        )\n      )\n      return true\n    case '--squirrel-updated':\n      WindowsUpdater.restartN1(app)\n      return true\n    case '--squirrel-uninstall':\n      WindowsUpdater.removeShortcuts(() =>\n        app.quit()\n      )\n      return true\n    case '--squirrel-obsolete':\n      app.quit()\n      return true\n    default:\n      return false\n  }\n};\n\nconst start = () => {\n  app.setAppUserModelId('com.squirrel.nylas.nylas');\n  if (handleStartupEventWithSquirrel()) {\n    return;\n  }\n\n  const options = parseCommandLine(process.argv);\n\n  if (!options.devMode) {\n    const otherInstanceRunning = app.makeSingleInstance((commandLine) => {\n      const otherOpts = parseCommandLine(commandLine);\n      global.application.handleLaunchOptions(otherOpts);\n    });\n\n    if (otherInstanceRunning) {\n      console.log(\"Exiting because another instance of the app is already running.\")\n      app.quit();\n    }\n  }\n\n  global.errorLogger = setupErrorLogger(options);\n  const configDirPath = setupConfigDir(options);\n  options.configDirPath = configDirPath;\n  setupCompileCache(configDirPath);\n\n  const onOpenFileBeforeReady = (event, file) => {\n    event.preventDefault();\n    options.pathsToOpen.push(file);\n  };\n\n  const onOpenUrlBeforeReady = (event, url) => {\n    event.preventDefault();\n    options.urlsToOpen.push(url);\n  };\n\n  app.on('open-url', onOpenUrlBeforeReady);\n  app.on('open-file', onOpenFileBeforeReady);\n  app.on('ready', () => {\n    app.removeListener('open-file', onOpenFileBeforeReady);\n    app.removeListener('open-url', onOpenUrlBeforeReady);\n    const Application = require(path.join(options.resourcePath, 'src', 'browser', 'application')).default;\n    global.application = new Application();\n    global.application.start(options);\n    if (!options.specMode) {\n      console.log(\"App load time: \" + (Date.now() - global.shellStartTime) + \"ms\");\n    }\n  });\n};\n\nstart();\n"
  },
  {
    "path": "packages/client-app/src/browser/nylas-protocol-handler.es6",
    "content": "import {app, protocol} from 'electron';\nimport fs from 'fs';\nimport path from 'path';\n\n// Handles requests with 'nylas' protocol.\n//\n// It's created by {N1Application} upon instantiation and is used to create a\n// custom resource loader for 'nylas://' URLs.\n//\n// The following directories are searched in order:\n//   * ~/.nylas-mail/assets\n//   * ~/.nylas-mail/dev/packages (unless in safe mode)\n//   * ~/.nylas-mail/packages\n//   * RESOURCE_PATH/node_modules\n//\nexport default class NylasProtocolHandler {\n  constructor(resourcePath, safeMode) {\n    this.loadPaths = [];\n    this.dotNylasDirectory = path.join(app.getPath('home'), '.nylas-mail');\n\n    if (!safeMode) {\n      this.loadPaths.push(path.join(this.dotNylasDirectory, 'dev', 'packages'));\n    }\n\n    this.loadPaths.push(path.join(this.dotNylasDirectory, 'packages'));\n    this.loadPaths.push(path.join(resourcePath, 'internal_packages'));\n\n    this.registerNylasProtocol();\n  }\n\n  // Creates the 'Nylas' custom protocol handler.\n  registerNylasProtocol() {\n    protocol.registerFileProtocol('nylas', (request, callback) => {\n      const relativePath = path.normalize(request.url.substr(7));\n\n      let filePath = null;\n      if (relativePath.indexOf('assets/') === 0) {\n        const assetsPath = path.join(this.dotNylasDirectory, relativePath);\n        const assetsStats = fs.statSyncNoException(assetsPath);\n        if (assetsStats.isFile && assetsStats.isFile()) {\n          filePath = assetsPath;\n        }\n      }\n\n      if (!filePath) {\n        for (const loadPath of this.loadPaths) {\n          filePath = path.join(loadPath, relativePath);\n          const fileStats = fs.statSyncNoException(filePath);\n          if (fileStats.isFile && fileStats.isFile()) {\n            break;\n          }\n        }\n      }\n\n      callback(filePath);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/nylas-window.coffee",
    "content": "{BrowserWindow, app, dialog} = require 'electron'\npath = require 'path'\nfs = require 'fs'\nurl = require 'url'\n_ = require 'underscore'\n{EventEmitter} = require 'events'\n\nWindowIconPath = null\nidNum = 0\n\nmodule.exports =\nclass NylasWindow\n  _.extend @prototype, EventEmitter.prototype\n\n  @includeShellLoadTime: true\n\n  browserWindow: null\n  loaded: null\n  isSpec: null\n\n  constructor: (settings={}) ->\n    {frame,\n     title,\n     width,\n     height,\n     toolbar,\n     resizable,\n     pathToOpen,\n     @isSpec,\n     @devMode,\n     @benchmarkMode,\n     @windowKey,\n     @safeMode,\n     @neverClose,\n     @mainWindow,\n     @windowType,\n     @resourcePath,\n     @exitWhenDone,\n     @configDirPath} = settings\n\n    if !@windowKey\n      @windowKey = \"#{@windowType}-#{idNum}\"\n      idNum += 1\n\n    # Normalize to make sure drive letter case is consistent on Windows\n    @resourcePath = path.normalize(@resourcePath) if @resourcePath\n\n    browserWindowOptions =\n      show: false\n      title: title ? 'Nylas Mail'\n      frame: frame\n      width: width\n      height: height\n      resizable: resizable\n      webPreferences:\n        directWrite: true\n      autoHideMenuBar: true\n\n    if @neverClose\n      # Prevents DOM timers from being suspended when the main window is hidden.\n      # Means there's not an awkward catch-up when you re-show the main window.\n      # TODO\n      # This option is no longer working according to\n      # https://github.com/atom/electron/issues/3225\n      # Look into using option --disable-renderer-backgrounding\n      browserWindowOptions.webPreferences.pageVisibility = true\n\n    # Don't set icon on Windows so the exe's ico will be used as window and\n    # taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.\n    if process.platform is 'linux'\n      unless WindowIconPath\n        WindowIconPath = path.resolve(__dirname, '..', '..', 'nylas.png')\n        unless fs.existsSync(WindowIconPath)\n          WindowIconPath = path.resolve(__dirname, '..', '..', 'build', 'resources', 'nylas.png')\n      browserWindowOptions.icon = WindowIconPath\n\n    @browserWindow = new BrowserWindow(browserWindowOptions)\n    @browserWindow.updateLoadSettings = @updateLoadSettings\n\n    @handleEvents()\n\n    loadSettings = _.extend({}, settings)\n    loadSettings.windowState ?= '{}'\n    loadSettings.appVersion = app.getVersion()\n    loadSettings.resourcePath = @resourcePath\n    loadSettings.devMode ?= false\n    loadSettings.benchmarkMode ?= false\n    loadSettings.safeMode ?= false\n    loadSettings.mainWindow ?= @mainWindow\n    loadSettings.windowType ?= \"default\"\n\n    # Only send to the first non-spec window created\n    if @constructor.includeShellLoadTime and not @isSpec\n      @constructor.includeShellLoadTime = false\n      loadSettings.shellLoadTime ?= Date.now() - global.shellStartTime\n\n    loadSettings.initialPath = pathToOpen\n    if fs.statSyncNoException(pathToOpen).isFile?()\n      loadSettings.initialPath = path.dirname(pathToOpen)\n\n    @browserWindow.loadSettings = loadSettings\n\n    @browserWindow.once 'window:loaded', =>\n      @loaded = true\n      if @mainWindow\n        @browserWindow.setResizable(true)\n      if @browserWindow.loadSettingsChangedSinceGetURL\n        @browserWindow.webContents.send('load-settings-changed', @browserWindow.loadSettings)\n      @emit 'window:loaded'\n\n    @browserWindow.loadURL(@getURL(loadSettings))\n    @browserWindow.focusOnWebView() if @isSpec\n\n  updateLoadSettings: (newSettings={}) =>\n    @loaded = true\n    @setLoadSettings(Object.assign({}, @browserWindow.loadSettings, newSettings))\n\n  loadSettings: ->\n    @browserWindow.loadSettings\n\n  # This gets called when we want to turn a WindowLauncher.EMPTY_WINDOW\n  # into a new kind of custom popout window.\n  #\n  # The windowType will change which will cause a new set of plugins to\n  # load.\n  setLoadSettings: (loadSettings) ->\n    @browserWindow.loadSettings = loadSettings\n    @browserWindow.loadSettingsChangedSinceGetURL = true\n    @browserWindow.webContents.send('load-settings-changed', loadSettings)\n\n  getURL: (loadSettingsObj) ->\n    # Ignore the windowState when passing loadSettings via URL, since it could\n    # be quite large.\n    loadSettings = _.clone(loadSettingsObj)\n    delete loadSettings['windowState']\n\n    @browserWindow.loadSettingsChangedSinceGetURL = false\n\n    url.format\n      protocol: 'file'\n      pathname: \"#{@resourcePath}/static/index.html\"\n      slashes: true\n      query: {loadSettings: JSON.stringify(loadSettings)}\n\n  handleEvents: ->\n    # Also see logic in `NylasEnv::onBeforeUnload` and\n    # `WindowEventHandler::AddUnloadCallback`. Classes like the DraftStore\n    # and ActionBridge intercept the closing of windows and perform\n    # action.\n    #\n    # This uses the DOM's `beforeunload` event.\n    @browserWindow.on 'close', (event) =>\n      if @neverClose and !global.application.isQuitting()\n        # For neverClose windows (like the main window) simply hide and\n        # take out of full screen.\n        event.preventDefault()\n        if @browserWindow.isFullScreen()\n          @browserWindow.once 'leave-full-screen', =>\n            @browserWindow.hide()\n          @browserWindow.setFullScreen(false)\n        else\n          @browserWindow.hide()\n\n        # HOWEVER! If the neverClose window is the last window open, and\n        # it looks like there's no windows actually quit the application\n        # on Linux & Windows.\n        if not @isSpec\n          global.application.windowManager.quitWinLinuxIfNoWindows()\n\n    @browserWindow.on 'scroll-touch-begin', =>\n      @browserWindow.webContents.send('scroll-touch-begin')\n\n    @browserWindow.on 'scroll-touch-end', =>\n      @browserWindow.webContents.send('scroll-touch-end')\n\n    @browserWindow.on 'focus', =>\n      @browserWindow.webContents.send('browser-window-focus')\n\n    @browserWindow.on 'blur', =>\n      @browserWindow.webContents.send('browser-window-blur')\n\n    @browserWindow.webContents.on 'will-navigate', (event, url) =>\n      event.preventDefault()\n\n    @browserWindow.webContents.on 'new-window', (event, url, frameName, disposition) =>\n      event.preventDefault()\n\n    @browserWindow.on 'unresponsive', =>\n      return if @isSpec\n      return if not @loaded\n\n      chosen = dialog.showMessageBox @browserWindow,\n        type: 'warning'\n        buttons: ['Close', 'Keep Waiting']\n        message: 'Nylas Mail is not responding'\n        detail: 'Would you like to force close it or keep waiting?'\n      @browserWindow.destroy() if chosen is 0\n\n    @browserWindow.webContents.on 'crashed', (event, killed) =>\n      if killed\n        # Killed means that the app is exiting and the browser window is being\n        # forceably cleaned up. Carry on, do not try to reload the window.\n        @browserWindow.destroy()\n        return\n\n      app.exit(100) if @exitWhenDone\n\n      if @neverClose\n        @browserWindow.reload()\n      else\n        chosen = dialog.showMessageBox @browserWindow,\n          type: 'warning'\n          buttons: ['Close Window', 'Reload', 'Keep It Open']\n          message: 'Nylas Mail has crashed'\n          detail: 'Please restart Nylas Mail.'\n        switch chosen\n          when 0 then @browserWindow.destroy()\n          when 1 then @browserWindow.reload()\n\n    if @isSpec\n      # Workaround for https://github.com/atom/electron/issues/380\n      # Don't focus the window when it is being blurred during close or\n      # else the app will crash on Windows.\n      if process.platform is 'win32'\n        @browserWindow.on 'close', => @isWindowClosing = true\n\n      # Spec window's web view should always have focus\n      @browserWindow.on 'blur', =>\n        @browserWindow.focusOnWebView() unless @isWindowClosing\n\n  sendMessage: (message, detail) ->\n    @waitForLoad =>\n      @browserWindow.webContents.send(message, detail)\n\n  sendCommand: (command, args...) ->\n    if @isSpecWindow()\n      unless global.application.sendCommandToFirstResponder(command)\n        switch command\n          when 'window:reload' then @reload()\n          when 'window:toggle-dev-tools' then @toggleDevTools()\n          when 'window:close' then @close()\n    else if @isWebViewFocused()\n      @sendCommandToBrowserWindow(command, args...)\n    else\n      unless global.application.sendCommandToFirstResponder(command)\n        @sendCommandToBrowserWindow(command, args...)\n\n  sendCommandToBrowserWindow: (command, args...) ->\n    @browserWindow.webContents.send 'command', command, args...\n\n  getDimensions: ->\n    [x, y] = @browserWindow.getPosition()\n    [width, height] = @browserWindow.getSize()\n    {x, y, width, height}\n\n  close: -> @browserWindow.close()\n\n  hide: -> @browserWindow.hide()\n\n  show: -> @browserWindow.show()\n\n  showWhenLoaded: ->\n    @waitForLoad =>\n      @show()\n      @focus()\n\n  waitForLoad: (fn) ->\n    if @loaded\n      fn()\n    else\n      @once('window:loaded', fn)\n\n  focus: -> @browserWindow.focus()\n\n  minimize: -> @browserWindow.minimize()\n\n  maximize: -> @browserWindow.maximize()\n\n  restore: -> @browserWindow.restore()\n\n  isFocused: -> @browserWindow.isFocused()\n\n  isMinimized: -> @browserWindow.isMinimized()\n\n  isVisible: -> @browserWindow.isVisible()\n\n  isLoaded: -> @loaded\n\n  isWebViewFocused: -> @browserWindow.isWebViewFocused()\n\n  isSpecWindow: -> @isSpec\n\n  reload: -> @browserWindow.reload()\n\n  toggleDevTools: -> @browserWindow.toggleDevTools()\n"
  },
  {
    "path": "packages/client-app/src/browser/package-migration-manager.es6",
    "content": "import fs from 'fs'\nimport path from 'path'\nimport semver from 'semver'\n\n\nconst PACKAGE_MIGRATIONS = [\n  {\n    \"version\": \"0.4.50\",\n    \"package-migrations\": [{\n      \"new-name\": \"composer-markdown\",\n      \"old-name\": \"N1-Markdown-Composer\",\n      \"enabled-by-default\": false,\n    }],\n  },\n  {\n    \"version\": \"0.4.204\",\n    \"package-migrations\": [{\n      \"new-name\": \"nylas-private-salesforce\",\n      \"old-name\": \"nylas-private-salesforce\",\n      \"enabled-by-default\": false,\n    }],\n  },\n  {\n    \"version\": \"2.0.1\",\n    \"package-migrations\": [\n      {\n        \"new-name\": \"thread-snooze\",\n        \"old-name\": \"thread-snooze\",\n        \"enabled-by-default\": true,\n      },\n      {\n        \"new-name\": \"send-reminders\",\n        \"old-name\": \"send-reminders\",\n        \"enabled-by-default\": true,\n      },\n      {\n        \"new-name\": \"send-later\",\n        \"old-name\": \"send-later\",\n        \"enabled-by-default\": true,\n      },\n    ],\n  },\n]\n\nclass PackageMigrationManager {\n\n  constructor({config, version, configDirPath} = {}) {\n    this.config = config\n    this.configDirPath = configDirPath\n    this.version = version\n    this.savedMigrationVersion = this.config.get('core.packageMigrationVersion')\n  }\n\n  getMigrationsToRun() {\n    let migrations;\n    if (this.savedMigrationVersion) {\n      migrations = PACKAGE_MIGRATIONS\n      .filter((migration) => semver.gt(migration.version, this.savedMigrationVersion))\n      .map(migration => migration['package-migrations'])\n    } else {\n      migrations = PACKAGE_MIGRATIONS.map(migration => migration['package-migrations'])\n    }\n    return [].concat(...migrations)\n  }\n\n  migrate() {\n    if (this.savedMigrationVersion === this.version) { return }\n    const migrations = this.getMigrationsToRun()\n    const oldPackNames = migrations.map((mig) => mig['old-name'])\n    const disabledPackNames = this.config.get('core.disabledPackages') || []\n    let oldEnabledPackNames = []\n\n    if (fs.existsSync(path.join(this.configDirPath, 'packages'))) {\n      // Find any external packages that have been manually installed\n      const toMigrate = fs.readdirSync(path.join(this.configDirPath, 'packages'))\n      .filter((packName) => oldPackNames.includes(packName))\n      .filter((packName) => packName[0] !== '.')\n\n      // Move old installed packages to a deprecated folder\n      const deprecatedPath = path.join(this.configDirPath, 'packages-deprecated')\n      if (!fs.existsSync(deprecatedPath)) {\n        fs.mkdirSync(deprecatedPath);\n      }\n      toMigrate.forEach((packName) => {\n        const prevPath = path.join(this.configDirPath, 'packages', packName)\n        const nextPath = path.join(deprecatedPath, packName)\n        fs.renameSync(prevPath, nextPath);\n      });\n\n      oldEnabledPackNames = toMigrate.filter((packName) => (\n        !(disabledPackNames).includes(packName)\n      ))\n    }\n\n    // Enable any packages that were migrated from an old install and were\n    // enabled, or that should be enabled by default\n    migrations.forEach((migration) => {\n      // If the old install was enabled, keep it that way\n      if (oldEnabledPackNames.includes(migration['old-name'])) { return }\n      // If we want to enable the package by default,\n      if (migration['enabled-by-default']) {\n        if (disabledPackNames.includes(migration['old-name'])) {\n          this.config.removeAtKeyPath('core.disabledPackages', migration['old-name'])\n        }\n        return\n      }\n      const newName = migration['new-name']\n      this.config.pushAtKeyPath('core.disabledPackages', newName);\n    })\n\n    this.config.set('core.packageMigrationVersion', this.version)\n  }\n}\n\nexport default PackageMigrationManager\n"
  },
  {
    "path": "packages/client-app/src/browser/prevent-legacy-n1-migration.es6",
    "content": "import fs from 'fs'\nimport path from 'path'\n\n// This function prevents old N1 from destroying its own config and copying the\n// one from Nylas Mail 2.0. The expected workflow now is to migrate from old\n// N1 (1.5.0) to Nylas Mail (2.0) instead of the other way around\n// See https://github.com/nylas/nylas-mail/blob/n1-pro/src/browser/nylas-pro-migrator.es6 for details\nexport default function preventLegacyN1Migration(configDirPath) {\n  try {\n    const legacyConfigPath = path.join(configDirPath, '..', '.nylas', 'config.json')\n    if (!fs.existsSync(legacyConfigPath)) { return }\n    const legacyConfig = require(legacyConfigPath) || {}  // eslint-disable-line\n    if (!legacyConfig['*']) {\n      legacyConfig['*'] = {}\n    }\n    legacyConfig['*'].nylasMailBasicMigrationTime = Date.now()\n    fs.writeFileSync(legacyConfigPath, JSON.stringify(legacyConfig))\n  } catch (err) {\n    console.error('Error preventing legacy N1 migration')\n    console.error(err)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/system-tray-manager.es6",
    "content": "import {Tray, Menu, nativeImage} from 'electron';\n\n\nfunction _getMenuTemplate(platform, application) {\n  const template = [\n    {\n      label: 'New Message',\n      click: () => application.emit('application:new-message'),\n    },\n    {\n      label: 'Preferences',\n      click: () => application.emit('application:open-preferences'),\n    },\n    {\n      type: 'separator',\n    },\n    {\n      label: 'Quit Nylas Mail',\n      click: () => application.emit('application:quit'),\n    },\n  ];\n\n  if (platform !== 'win32') {\n    template.unshift({\n      label: 'Open Inbox',\n      click: () => application.emit('application:show-main-window'),\n    });\n  }\n\n  return template;\n}\n\nfunction _getTooltip(unreadString) {\n  return unreadString ? `${unreadString} unread messages` : '';\n}\n\nfunction _getIcon(iconPath, isTemplateImg) {\n  if (!iconPath) {\n    return nativeImage.createEmpty();\n  }\n  const icon = nativeImage.createFromPath(iconPath)\n  if (isTemplateImg) {\n    icon.setTemplateImage(true);\n  }\n  return icon;\n}\n\n\nclass SystemTrayManager {\n\n  constructor(platform, application) {\n    this._platform = platform;\n    this._application = application;\n    this._iconPath = null;\n    this._unreadString = null;\n    this._tray = null;\n    this.initTray();\n\n    this._application.config.onDidChange('core.workspace.systemTray', ({newValue}) => {\n      if (newValue === false) {\n        this.destroyTray();\n      } else {\n        this.initTray();\n      }\n    });\n  }\n\n  initTray() {\n    const enabled = (this._application.config.get('core.workspace.systemTray') !== false);\n    const created = (this._tray !== null);\n\n    if (enabled && !created) {\n      this._tray = new Tray(_getIcon(this._iconPath));\n      this._tray.setToolTip(_getTooltip(this._unreadString));\n      this._tray.addListener('click', this._onClick);\n      this._tray.setContextMenu(Menu.buildFromTemplate(_getMenuTemplate(this._platform, this._application)));\n    }\n  }\n\n  _onClick = () => {\n    if (this._platform !== 'darwin') {\n      this._application.emit('application:show-main-window');\n    }\n  };\n\n  updateTraySettings(iconPath, unreadString, isTemplateImg) {\n    if ((this._iconPath === iconPath) && (this._unreadString === unreadString)) return;\n\n    this._iconPath = iconPath;\n    this._unreadString = unreadString;\n\n    if (this._tray) {\n      const icon = _getIcon(this._iconPath, isTemplateImg);\n      const tooltip = _getTooltip(unreadString);\n      this._tray.setImage(icon);\n      this._tray.setToolTip(tooltip);\n    }\n  }\n\n  destroyTray() {\n    if (this._tray) {\n      this._tray.removeListener('click', this._onClick);\n      this._tray.destroy();\n      this._tray = null;\n    }\n  }\n}\n\nexport default SystemTrayManager;\n"
  },
  {
    "path": "packages/client-app/src/browser/window-launcher.es6",
    "content": "import NylasWindow from './nylas-window'\n\nconst DEBUG_SHOW_HOT_WINDOW = process.env.SHOW_HOT_WINDOW || false;\nlet winNum = 0;\n\n/**\n * It takes a full second or more to bootup a Nylas window. Most of this\n * is due to sheer amount of time it takes to parse all of the javascript\n * and follow the require tree.\n *\n * Since popout windows need to be more responsive than that, we pre-load\n * \"hot\" windows in the background that have most of the code loaded. Then\n * all we need to do is load the handful of packages the window\n * requires and show it.\n */\nexport default class WindowLauncher {\n  static EMPTY_WINDOW = \"emptyWindow\"\n\n  constructor({devMode, benchmarkMode, safeMode, specMode, resourcePath, configDirPath, onCreatedHotWindow, config}) {\n    this.defaultWindowOpts = {\n      frame: process.platform !== \"darwin\",\n      hidden: false,\n      toolbar: true,\n      devMode,\n      safeMode,\n      benchmarkMode,\n      resizable: true,\n      windowType: WindowLauncher.EMPTY_WINDOW,\n      bootstrapScript: require.resolve(\"../secondary-window-bootstrap\"),\n      resourcePath,\n      configDirPath,\n    }\n    this.config = config;\n    this.onCreatedHotWindow = onCreatedHotWindow;\n    if (specMode) return;\n    this.createHotWindow();\n  }\n\n  newWindow(options) {\n    const opts = Object.assign({}, this.defaultWindowOpts, options);\n\n    let win;\n    if (this._mustUseColdWindow(opts)) {\n      win = new NylasWindow(opts)\n    } else {\n      // Check if the hot window has been deleted. This may happen when we are\n      // relaunching the app\n      if (!this.hotWindow) {\n        this.createHotWindow()\n      }\n      win = this.hotWindow;\n\n      const newLoadSettings = Object.assign({}, win.loadSettings(), opts)\n      if (newLoadSettings.windowType === WindowLauncher.EMPTY_WINDOW) {\n        throw new Error(\"Must specify a windowType\")\n      }\n\n      // Reset the loaded state and update the load settings.\n      // This will fire `NylasEnv::populateHotWindow` and reload the\n      // packages.\n      win.windowKey = opts.windowKey || `${opts.windowType}-${winNum}`;\n      winNum += 1;\n      win.windowType = opts.windowType;\n\n      if (options.bounds) {\n        win.browserWindow.setBounds(options.bounds);\n      }\n      if (options.width && options.height) {\n        win.browserWindow.setSize(options.width, options.height);\n      }\n\n      win.setLoadSettings(newLoadSettings);\n\n      setTimeout(() => {\n        // We need to regen a hot window, but do it in the next event\n        // loop to not hang the opening of the current window.\n        this.createHotWindow();\n      }, 0)\n    }\n\n    if (!opts.hidden && !opts.initializeInBackground) {\n      // NOTE: In the case of a cold window, this will show it once\n      // loaded. If it's a hotWindow, since hotWindows have a\n      // `hidden:true` flag, nothing will show. When `setLoadSettings`\n      // starts populating the window in `populateHotWindow` we'll show or\n      // hide based on the windowOpts\n      win.showWhenLoaded()\n    }\n    return win\n  }\n\n  createHotWindow() {\n    this.hotWindow = new NylasWindow(this._hotWindowOpts());\n    this.onCreatedHotWindow(this.hotWindow);\n    if (DEBUG_SHOW_HOT_WINDOW) {\n      this.hotWindow.showWhenLoaded();\n    }\n  }\n\n  // Note: This method calls `browserWindow.destroy()` which closes\n  // windows without waiting for them to load or firing window lifecycle\n  // events.  This is necessary for the app to quit promptly on Linux.\n  // https://phab.nylas.com/T1282\n  cleanupBeforeAppQuit() {\n    if (this.hotWindow != null) {\n      this.hotWindow.browserWindow.destroy()\n    }\n    this.hotWindow = null\n  }\n\n  // Some properties, like the `frame` or `toolbar` can't be updated once\n  // a window has been setup. If we detect this case we have to bootup a\n  // plain NylasWindow instead of using a hot window.\n  _mustUseColdWindow(opts) {\n    const {bootstrapScript, frame} = this.defaultWindowOpts;\n\n    const usesOtherBootstrap = opts.bootstrapScript !== bootstrapScript;\n    const usesOtherFrame = (!!opts.frame) !== frame;\n    const requestsColdStart = opts.coldStartOnly;\n\n    return usesOtherBootstrap || usesOtherFrame || requestsColdStart;\n  }\n\n  _hotWindowOpts() {\n    const hotWindowOpts = Object.assign({}, this.defaultWindowOpts);\n    hotWindowOpts.packageLoadingDeferred = true;\n    hotWindowOpts.hidden = DEBUG_SHOW_HOT_WINDOW;\n    return hotWindowOpts\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/browser/window-manager.es6",
    "content": "import _ from 'underscore';\nimport {app} from 'electron';\nimport WindowLauncher from './window-launcher';\n\nconst MAIN_WINDOW = \"default\"\nconst WORK_WINDOW = \"work\"\nconst SPEC_WINDOW = \"spec\"\nconst ONBOARDING_WINDOW = \"onboarding\"\n// const CALENDAR_WINDOW = \"calendar\"\n\nexport default class WindowManager {\n\n  constructor({devMode, benchmarkMode, safeMode, specMode, resourcePath, configDirPath, initializeInBackground, config}) {\n    this.initializeInBackground = initializeInBackground;\n    this._windows = {};\n\n    const onCreatedHotWindow = (win) => {\n      this._registerWindow(win);\n      this._didCreateNewWindow(win);\n    }\n    this.windowLauncher = new WindowLauncher({devMode, benchmarkMode, safeMode, specMode, resourcePath, configDirPath, config, onCreatedHotWindow});\n  }\n\n  get(windowKey) {\n    return this._windows[windowKey];\n  }\n\n  getOpenWindows() {\n    const values = [];\n    Object.keys(this._windows).forEach((key) => {\n      const win = this._windows[key];\n      if (win.windowType !== WindowLauncher.EMPTY_WINDOW) {\n        values.push(win);\n      }\n    });\n\n    const score = (win) =>\n      (win.loadSettings().mainWindow ? 1000 : win.browserWindow.id);\n\n    return values.sort((a, b) => score(b) - score(a));\n  }\n\n  getAllWindowDimensions() {\n    const dims = {}\n    Object.keys(this._windows).forEach((key) => {\n      const win = this._windows[key];\n      if (win.windowType !== WindowLauncher.EMPTY_WINDOW) {\n        const {x, y, width, height} = win.browserWindow.getBounds()\n        const maximized = win.browserWindow.isMaximized()\n        const fullScreen = win.browserWindow.isFullScreen()\n        dims[key] = {x, y, width, height, maximized, fullScreen}\n      }\n    });\n    return dims\n  }\n\n  newWindow(options = {}) {\n    const win = this.windowLauncher.newWindow(options);\n    const existingKey = this._registeredKeyForWindow(win);\n\n    if (existingKey) {\n      delete this._windows[existingKey];\n    }\n    this._registerWindow(win);\n\n    if (!existingKey) {\n      this._didCreateNewWindow(win);\n    }\n\n    return win;\n  }\n\n  _registerWindow = (win) => {\n    if (!win.windowKey) {\n      throw new Error(\"WindowManager: You must provide a windowKey\");\n    }\n\n    if (this._windows[win.windowKey]) {\n      throw new Error(`WindowManager: Attempting to register a new window for an existing windowKey (${win.windowKey}). Use 'get()' to retrieve the existing window instead.`);\n    }\n\n    this._windows[win.windowKey] = win;\n  }\n\n  _didCreateNewWindow = (win) => {\n    win.browserWindow.on(\"closed\", () => {\n      delete this._windows[win.windowKey];\n      this.quitWinLinuxIfNoWindows();\n    });\n\n    // Let the applicationMenu know that there's a new window available.\n    // The applicationMenu automatically listens to the `closed` event of\n    // the browserWindow to unregister itself\n    global.application.applicationMenu.addWindow(win.browserWindow);\n  }\n\n  _registeredKeyForWindow = (win) => {\n    for (const key of Object.keys(this._windows)) {\n      const otherWin = this._windows[key];\n      if (win === otherWin) {\n        return key;\n      }\n    }\n    return null;\n  }\n\n  ensureWindow(windowKey, extraOpts) {\n    const win = this._windows[windowKey];\n\n    if (!win) {\n      this.newWindow(this._coreWindowOpts(windowKey, extraOpts));\n      return;\n    }\n\n    if (win.loadSettings().hidden) {\n      return;\n    }\n\n    if (win.isMinimized()) {\n      win.restore();\n      win.focus();\n    } else if (!win.isVisible()) {\n      win.showWhenLoaded();\n    } else {\n      win.focus();\n    }\n  }\n\n  sendToAllWindows(msg, {except}, ...args) {\n    for (const windowKey of Object.keys(this._windows)) {\n      const win = this._windows[windowKey];\n      if (win.browserWindow === except) {\n        continue;\n      }\n      if (!win.browserWindow.webContents) {\n        continue;\n      }\n      win.browserWindow.webContents.send(msg, ...args);\n    }\n  }\n\n  destroyAllWindows() {\n    this.windowLauncher.cleanupBeforeAppQuit();\n    for (const windowKey of Object.keys(this._windows)) {\n      this._windows[windowKey].browserWindow.destroy();\n    }\n    this._windows = {}\n  }\n\n  cleanupBeforeAppQuit() {\n    this.windowLauncher.cleanupBeforeAppQuit();\n  }\n\n  quitWinLinuxIfNoWindows() {\n    // Typically, N1 stays running in the background on all platforms, since it\n    // has a status icon you can use to quit it.\n\n    // However, on Windows and Linux we /do/ want to quit if the app is somehow\n    // put into a state where there are no visible windows and the main window\n    // doesn't exist.\n\n    // This /shouldn't/ happen, but if it does, the only way for them to recover\n    // would be to pull up the Task Manager. Ew.\n\n    if (['win32', 'linux'].includes(process.platform)) {\n      this.quitCheck = this.quitCheck || _.debounce(() => {\n        const visibleWindows = _.filter(this._windows, (win) => win.isVisible())\n        const mainWindow = this.get(WindowManager.MAIN_WINDOW);\n        const noMainWindowLoaded = !mainWindow || !mainWindow.isLoaded();\n        if (visibleWindows.length === 0 && noMainWindowLoaded) {\n          app.quit();\n        }\n      }, 25000);\n      this.quitCheck();\n    }\n  }\n\n  focusedWindow() {\n    return _.find(this._windows, (win) => win.isFocused());\n  }\n\n  _coreWindowOpts(windowKey, extraOpts = {}) {\n    const coreWinOpts = {}\n    coreWinOpts[WindowManager.MAIN_WINDOW] = {\n      windowKey: WindowManager.MAIN_WINDOW,\n      windowType: WindowManager.MAIN_WINDOW,\n      title: \"Message Viewer\",\n      neverClose: true,\n      bootstrapScript: require.resolve(\"../window-bootstrap\"),\n      mainWindow: true,\n      width: 640, // Gets reset once app boots up\n      height: 396, // Gets reset once app boots up\n      center: true, // Gets reset once app boots up\n      resizable: false, // Gets reset once app boots up\n      initializeInBackground: this.initializeInBackground,\n    };\n\n    coreWinOpts[WindowManager.WORK_WINDOW] = {\n      windowKey: WindowManager.WORK_WINDOW,\n      windowType: WindowManager.WORK_WINDOW,\n      coldStartOnly: true, // It's a secondary window, but not a hot window\n      title: \"Activity\",\n      hidden: true,\n      neverClose: true,\n      width: 800,\n      height: 400,\n    }\n\n    coreWinOpts[WindowManager.ONBOARDING_WINDOW] = {\n      windowKey: WindowManager.ONBOARDING_WINDOW,\n      windowType: WindowManager.ONBOARDING_WINDOW,\n      title: \"Account Setup\",\n      hidden: true, // Displayed by PageRouter::_initializeWindowSize\n      frame: false, // Always false on Mac, explicitly set for Win & Linux\n      toolbar: false,\n      resizable: false,\n      width: 900,\n      height: 600,\n    }\n\n    // The SPEC_WINDOW gets passed its own bootstrapScript\n    coreWinOpts[WindowManager.SPEC_WINDOW] = {\n      windowKey: WindowManager.SPEC_WINDOW,\n      windowType: WindowManager.SPEC_WINDOW,\n      title: \"Specs\",\n      frame: true,\n      hidden: true,\n      isSpec: true,\n      devMode: true,\n      benchmarkMode: false,\n      toolbar: false,\n    }\n\n    const defaultOptions = coreWinOpts[windowKey] || {};\n\n    return Object.assign({}, defaultOptions, extraOpts);\n  }\n}\n\nWindowManager.MAIN_WINDOW = MAIN_WINDOW;\nWindowManager.WORK_WINDOW = WORK_WINDOW;\nWindowManager.SPEC_WINDOW = SPEC_WINDOW;\n// WindowManager.CALENDAR_WINDOW = CALENDAR_WINDOW;\nWindowManager.ONBOARDING_WINDOW = ONBOARDING_WINDOW;\n"
  },
  {
    "path": "packages/client-app/src/browser/windows-updater-squirrel-adapter.coffee",
    "content": "{EventEmitter} = require 'events'\n_ = require 'underscore'\nWindowsUpdater = require './windows-updater'\n\nclass WindowsUpdaterSquirrelAdapter\n  _.extend @prototype, EventEmitter.prototype\n\n  setFeedURL: (@updateUrl) ->\n\n  restartN1: ->\n    if WindowsUpdater.existsSync()\n      WindowsUpdater.restartN1(require('electron').app)\n    else\n      console.error(\"SquirrelUpdate does not exist\")\n\n  downloadUpdate: (callback) ->\n    WindowsUpdater.spawn ['--download', @updateUrl], (error, stdout) ->\n      return callback(error) if error?\n\n      try\n        # Last line of output is the JSON details about the releases\n        json = stdout.trim().split('\\n').pop()\n        update = JSON.parse(json)?.releasesToApply?.pop?()\n      catch error\n        error.stdout = stdout\n        return callback(error)\n\n      callback(null, update)\n\n  installUpdate: (callback) ->\n    WindowsUpdater.spawn(['--update', @updateUrl], callback)\n\n  supportsUpdates: ->\n    WindowsUpdater.existsSync()\n\n  downloadAndInstallUpdate: ->\n    throw new Error('Update URL is not set') unless @updateUrl\n\n    @emit 'checking-for-update'\n\n    unless WindowsUpdater.existsSync()\n      @emit 'update-not-available'\n      return\n\n    @downloadUpdate (error, update) =>\n      if error?\n        @emit 'update-not-available'\n        return\n\n      unless update?\n        @emit 'update-not-available'\n        return\n\n      @emit 'update-available'\n      @installUpdate (error) =>\n        if error?\n          @emit 'error', error\n          return\n\n        # During this time, Windows Squirrel will invoke the Nylas.exe\n        # with a variety of flags as event hooks.\n        #\n        # See https://github.com/Squirrel/Squirrel.Windows/blob/master/docs/using/custom-squirrel-events-non-cs.md\n        #\n        # See `handleStartupEventsWithSquirrel` in `src/browser/main.js`\n\n        @emit 'update-downloaded', {}, update.releaseNotes, update.version\n\nmodule.exports = new WindowsUpdaterSquirrelAdapter()\n"
  },
  {
    "path": "packages/client-app/src/browser/windows-updater.js",
    "content": "/*\n * \"Squirrel will spawn your app with command line flags on first run, updates,]\n * and uninstalls.\"\n *\n * Read: https://github.com/electron-archive/grunt-electron-installer#handling-squirrel-events\n * Read: https://github.com/electron/electron/blob/master/docs/api/auto-updater.md#windows\n *\n * When Nylas Mail gets installed on a Windows machine it gets put in:\n * C:\\Users\\<USERNAME>\\AppData\\Local\\NylasMail\\app-x.x.x\n *\n * The `process.execPath` is:\n * C:\\Users\\<USERNAME>\\AppData\\Local\\NylasMail\\app-x.x.x\\nylas.exe\n *\n * We manually copy everything in build/resources/win into a 'resources' folder\n * located inside the main app directory. See runCopyPlatformSpecificResources\n * in package-task.js\n *\n * This means `__dirname` should be:\n * C:\\Users\\<USERNAME>\\AppData\\Local\\NylasMail\\app-x.x.x\\resources\n *\n * We also expect Squirrel Windows to have a file called `nylas.exe` at:\n * C:\\Users\\<USERNAME>\\AppData\\Local\\NylasMail\\nylas.exe\n */\nconst ChildProcess = require('child_process');\nconst fs = require('fs-plus');\nconst path = require('path');\nconst os = require('os');\n\n// C:\\Users\\<USERNAME>\\AppData\\Local\\NylasMail\\app-x.x.x\nconst appFolder = path.resolve(process.execPath, '..');\n\n// C:\\Users\\<USERNAME>\\AppData\\Local\\NylasMail\\\nconst rootN1Folder = path.resolve(appFolder, '..');\n\n// C:\\Users\\<USERNAME>\\AppData\\Local\\NylasMail\\Update.exe\nconst updateDotExe = path.join(rootN1Folder, 'Update.exe');\n\n// \"nylas.exe\"\nconst exeName = path.basename(process.execPath);\n\n// Spawn a command and invoke the callback when it completes with an error\n// and the output from standard out.\nfunction spawn(command, args, callback, options = {}) {\n  let stdout = ''\n  let spawnedProcess = null;\n\n  try {\n    spawnedProcess = ChildProcess.spawn(command, args, options);\n  } catch (error) {\n    // Spawn can throw an error\n    setTimeout(() => callback && callback(error, stdout), 0)\n    return;\n  }\n\n  spawnedProcess.stdout.on('data', (data) => {\n    stdout += data\n  });\n\n  let error = null\n  spawnedProcess.on('error', (processError) => {\n    error = error || processError\n  });\n\n  spawnedProcess.on('close', (code, signal) => {\n    if (code !== 0) {\n      error = error || new Error(`Command failed: ${signal || code}`);\n    }\n    if (error) {\n      error.code = error.code || code;\n      error.stdout = error.stdout || stdout;\n    }\n    if (callback) {\n      callback(error, stdout);\n    }\n  });\n}\n\n// Spawn the Update.exe with the given arguments and invoke the callback when\n// the command completes.\nfunction spawnUpdate(args, callback, options = {}) {\n  spawn(updateDotExe, args, callback, options);\n}\n\n// Create a desktop and start menu shortcut by using the command line API\n// provided by Squirrel's Update.exe\nfunction createShortcuts(callback) {\n  spawnUpdate(['--createShortcut', exeName], callback);\n}\n\nfunction createRegistryEntries({allowEscalation, registerDefaultIfPossible}, callback) {\n  const escapeBackticks = (str) => str.replace(/\\\\/g, '\\\\\\\\');\n\n  const isWindows7 = os.release().startsWith('6.1');\n  const requiresLocalMachine = isWindows7;\n\n  // On Windows 7, we must write to LOCAL_MACHINE and need escalated privileges.\n  // Don't do it at install time - wait for the user to ask N1 to be the default.\n  if (requiresLocalMachine && !allowEscalation) {\n    callback();\n    return;\n  }\n\n  let regPath = 'reg.exe';\n  if (process.env.SystemRoot) {\n    regPath = path.join(process.env.SystemRoot, 'System32', 'reg.exe')\n  }\n\n  let spawnPath = regPath;\n  let spawnArgs = [];\n  if (requiresLocalMachine) {\n    spawnPath = path.join(appFolder, 'resources', 'elevate.cmd');\n    spawnArgs = [regPath];\n  }\n\n  fs.readFile(path.join(appFolder, 'resources', 'nylas-mailto-registration.reg'), (err, data) => {\n    if (err || !data) {\n      callback(err);\n      return;\n    }\n    const importTemplate = data.toString();\n    let importContents = importTemplate.replace(/{{PATH_TO_ROOT_FOLDER}}/g, escapeBackticks(rootN1Folder));\n    importContents = importContents.replace(/{{PATH_TO_APP_FOLDER}}/g, escapeBackticks(appFolder));\n    if (requiresLocalMachine) {\n      importContents = importContents.replace(/{{HKEY_ROOT}}/g, 'HKEY_LOCAL_MACHINE');\n    } else {\n      importContents = importContents.replace(/{{HKEY_ROOT}}/g, 'HKEY_CURRENT_USER');\n    }\n\n    const importTempPath = path.join(os.tmpdir(), `nylas-reg-${Date.now()}.reg`);\n\n    fs.writeFile(importTempPath, importContents, (writeErr) => {\n      if (writeErr) {\n        callback(writeErr);\n        return;\n      }\n\n      spawn(spawnPath, spawnArgs.concat(['import', escapeBackticks(importTempPath)]), (spawnErr) => {\n        if (isWindows7 && registerDefaultIfPossible) {\n          const defaultReg = path.join(appFolder, 'resources', 'nylas-mailto-default.reg')\n          spawn(spawnPath, spawnArgs.concat(['import', escapeBackticks(defaultReg)]), (spawnDefaultErr) => {\n            callback(spawnDefaultErr, true);\n          });\n        } else {\n          callback(spawnErr, false);\n        }\n      });\n    });\n  });\n}\n\n// Remove the desktop and start menu shortcuts by using the command line API\n// provided by Squirrel's Update.exe\nfunction removeShortcuts(callback) {\n  spawnUpdate(['--removeShortcut', exeName], callback);\n}\n\nexports.spawn = spawnUpdate;\nexports.createShortcuts = createShortcuts;\nexports.removeShortcuts = removeShortcuts;\nexports.createRegistryEntries = createRegistryEntries;\n\n// Is the Update.exe installed with N1?\nexports.existsSync = () => fs.existsSync(updateDotExe)\n\n// Restart N1 using the version pointed to by the N1.cmd shim\nexports.restartN1 = (app) => {\n  app.once('will-quit', () => {\n    spawnUpdate(['--processStart', exeName], (() => {}), {detached: true});\n  });\n  app.quit();\n}\n"
  },
  {
    "path": "packages/client-app/src/buffered-process.coffee",
    "content": "_ = require 'underscore'\nChildProcess = require 'child_process'\n{Emitter} = require 'event-kit'\npath = require 'path'\n\n# Extended: A wrapper which provides standard error/output line buffering for\n# Node's ChildProcess.\n#\n# ## Examples\n#\n# ```coffee\n# {BufferedProcess} = require 'nylas-exports'\n#\n# command = 'ps'\n# args = ['-ef']\n# stdout = (output) -> console.log(output)\n# exit = (code) -> console.log(\"ps -ef exited with #{code}\")\n# process = new BufferedProcess({command, args, stdout, exit})\n# ```\nmodule.exports =\nclass BufferedProcess\n  ###\n  Section: Construction\n  ###\n\n  # Public: Runs the given command by spawning a new child process.\n  #\n  # * `options` An {Object} with the following keys:\n  #   * `command` The {String} command to execute.\n  #   * `args` The {Array} of arguments to pass to the command (optional).\n  #   * `options` {Object} (optional) The options {Object} to pass to Node's\n  #     `ChildProcess.spawn` method.\n  #   * `stdout` {Function} (optional) The callback that receives a single\n  #     argument which contains the standard output from the command. The\n  #     callback is called as data is received but it's buffered to ensure only\n  #     complete lines are passed until the source stream closes. After the\n  #     source stream has closed all remaining data is sent in a final call.\n  #     * `data` {String}\n  #   * `stderr` {Function} (optional) The callback that receives a single\n  #     argument which contains the standard error output from the command. The\n  #     callback is called as data is received but it's buffered to ensure only\n  #     complete lines are passed until the source stream closes. After the\n  #     source stream has closed all remaining data is sent in a final call.\n  #     * `data` {String}\n  #   * `exit` {Function} (optional) The callback which receives a single\n  #     argument containing the exit status.\n  #     * `code` {Number}\n  constructor: ({command, args, options, stdout, stderr, exit}={}) ->\n    @emitter = new Emitter\n    options ?= {}\n    @command = command\n    # Related to joyent/node#2318\n    if process.platform is 'win32'\n      # Quote all arguments and escapes inner quotes\n      if args?\n        cmdArgs = args.filter (arg) -> arg?\n        cmdArgs = cmdArgs.map (arg) =>\n          if @isExplorerCommand(command) and /^\\/[a-zA-Z]+,.*$/.test(arg)\n            # Don't wrap /root,C:\\folder style arguments to explorer calls in\n            # quotes since they will not be interpreted correctly if they are\n            arg\n          else\n            \"\\\"#{arg.toString().replace(/\"/g, '\\\\\"')}\\\"\"\n      else\n        cmdArgs = []\n      if /\\s/.test(command)\n        cmdArgs.unshift(\"\\\"#{command}\\\"\")\n      else\n        cmdArgs.unshift(command)\n      cmdArgs = ['/s', '/c', \"\\\"#{cmdArgs.join(' ')}\\\"\"]\n      cmdOptions = _.clone(options)\n      cmdOptions.windowsVerbatimArguments = true\n      @spawn(@getCmdPath(), cmdArgs, cmdOptions)\n    else\n      @spawn(command, args, options)\n\n    @killed = false\n    @handleEvents(stdout, stderr, exit)\n\n  ###\n  Section: Event Subscription\n  ###\n\n  # Public: Will call your callback when an error will be raised by the process.\n  # Usually this is due to the command not being available or not on the PATH.\n  # You can call `handle()` on the object passed to your callback to indicate\n  # that you have handled this error.\n  #\n  # * `callback` {Function} callback\n  #   * `errorObject` {Object}\n  #     * `error` {Object} the error object\n  #     * `handle` {Function} call this to indicate you have handled the error.\n  #       The error will not be thrown if this function is called.\n  #\n  # Returns a {Disposable}\n  onWillThrowError: (callback) ->\n    @emitter.on 'will-throw-error', callback\n\n  ###\n  Section: Helper Methods\n  ###\n\n  # Helper method to pass data line by line.\n  #\n  # * `stream` The Stream to read from.\n  # * `onLines` The callback to call with each line of data.\n  # * `onDone` The callback to call when the stream has closed.\n  bufferStream: (stream, onLines, onDone) ->\n    stream.setEncoding('utf8')\n    buffered = ''\n\n    stream.on 'data', (data) =>\n      return if @killed\n      buffered += data\n      lastNewlineIndex = buffered.lastIndexOf('\\n')\n      if lastNewlineIndex isnt -1\n        onLines(buffered.substring(0, lastNewlineIndex + 1))\n        buffered = buffered.substring(lastNewlineIndex + 1)\n\n    stream.on 'close', =>\n      return if @killed\n      onLines(buffered) if buffered.length > 0\n      onDone()\n\n  # Kill all child processes of the spawned cmd.exe process on Windows.\n  #\n  # This is required since killing the cmd.exe does not terminate child\n  # processes.\n  killOnWindows: ->\n    return unless @process?\n\n    parentPid = @process.pid\n    cmd = 'wmic'\n    args = [\n      'process'\n      'where'\n      \"(ParentProcessId=#{parentPid})\"\n      'get'\n      'processid'\n    ]\n\n    try\n      wmicProcess = ChildProcess.spawn(cmd, args)\n    catch spawnError\n      @killProcess()\n      return\n\n    wmicProcess.on 'error', -> # ignore errors\n    output = ''\n    wmicProcess.stdout.on 'data', (data) -> output += data\n    wmicProcess.stdout.on 'close', =>\n      pidsToKill = output.split(/\\s+/)\n                    .filter (pid) -> /^\\d+$/.test(pid)\n                    .map (pid) -> parseInt(pid)\n                    .filter (pid) -> pid isnt parentPid and 0 < pid < Infinity\n\n      for pid in pidsToKill\n        try\n          process.kill(pid)\n      @killProcess()\n\n  killProcess: ->\n    @process?.kill()\n    @process = null\n\n  isExplorerCommand: (command) ->\n    if command is 'explorer.exe' or command is 'explorer'\n      true\n    else if process.env.SystemRoot\n      command is path.join(process.env.SystemRoot, 'explorer.exe') or command is path.join(process.env.SystemRoot, 'explorer')\n    else\n      false\n\n  getCmdPath: ->\n    if process.env.comspec\n      process.env.comspec\n    else if process.env.SystemRoot\n      path.join(process.env.SystemRoot, 'System32', 'cmd.exe')\n    else\n      'cmd.exe'\n\n  # Public: Terminate the process.\n  kill: ->\n    return if @killed\n\n    @killed = true\n    if process.platform is 'win32'\n      @killOnWindows()\n    else\n      @killProcess()\n\n    undefined\n\n  spawn: (command, args, options) ->\n    try\n      @process = ChildProcess.spawn(command, args, options)\n    catch spawnError\n      setTimeout((=> @handleError(spawnError)), 0)\n\n  handleEvents: (stdout, stderr, exit) ->\n    return unless @process?\n\n    stdoutClosed = true\n    stderrClosed = true\n    processExited = true\n    exitCode = 0\n    triggerExitCallback = ->\n      return if @killed\n      if stdoutClosed and stderrClosed and processExited\n        exit?(exitCode)\n\n    if stdout\n      stdoutClosed = false\n      @bufferStream @process.stdout, stdout, ->\n        stdoutClosed = true\n        triggerExitCallback()\n\n    if stderr\n      stderrClosed = false\n      @bufferStream @process.stderr, stderr, ->\n        stderrClosed = true\n        triggerExitCallback()\n\n    if exit\n      processExited = false\n      @process.on 'exit', (code) ->\n        exitCode = code\n        processExited = true\n        triggerExitCallback()\n\n    @process.on 'error', (error) => @handleError(error)\n    return\n\n  handleError: (error) ->\n    handled = false\n    handle = -> handled = true\n\n    @emitter.emit 'will-throw-error', {error, handle}\n\n    if error.code is 'ENOENT' and error.syscall.indexOf('spawn') is 0\n      error = new Error(\"Failed to spawn command `#{@command}`. Make sure `#{@command}` is installed and on your PATH\", error.path)\n      error.name = 'BufferedProcessError'\n\n    throw error unless handled\n"
  },
  {
    "path": "packages/client-app/src/canvas-utils.coffee",
    "content": "ThreadDragImage = document.createElement(\"img\")\nThreadDragImage.src = \"\"\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAYAAADimHc4AAAJc0lEQVR42u2dSW8URxTHsY0XtgQTspySKIryBRCgALZIIPkA4RL5kkMuufAVcs2VIxKCAycuCBIBYjE7GGOx72bfwg628bAYA536VfpFL+Xume6ebnvkqZb+IswMXfX+v6rXr6pnOlOCIJjiNXHyJngAHoCXB+ABeHkAHoCXB+ABeHkAHoCXB+ABeHkAHoCXB+ABeHkAdQQg5dHg9T8lPrICKNd4Yx0rNZC0AMqZ3WQ0tc7VVAFIVQDGGN/e3v7lvHnzlnZ2di6LUkdHx/LJrLi458+fv3Tu3LlfxYDIBGCM+Q0NDQtWrVr167Nnz3rM518F/pBjZHBwsG/NmjW/NTY2LqwEIQkA13ym2WddXV0/PX/+fMD7HX2USqXhlStXdhmvPlepaQyENADEfLTgxo0bf718+TJ48eJF8P79e++4OvAEb+7du9eNV8q3xrQA3IutXGgXmgbuvXv3LhgeHg6GhoY8BHPggckKVnjz5s2bIbyKuED/ByENADG/2ejb0dHREo28ffs2GBgYCMy1wDZarwex4wFe4Al/BwheGbU4EFIBEGpifksIYJiGzJ/ByMhI8Pjx4+Dhw4f27/V2EDOxP3r0yHrB3wVCCKA19M6FkBiAjP6W8GSLzPSyM0AAvH79Orh//35w9+5dpt6ETH+t8TTf5HorPBAAagYsMmqLmQVlAbjpR0Y/J1ssACCN4TT+6tWr4M6dO8GtW7dsR6KMyVO0X0lFtc1B3MR6+/ZtGzse8JqTghYbTQu9a3bTUFIAkn5aw5Mt0QBkFtAJKoCbN28G165dsx0q0mTarqSigBDv9evXA1MN2piJndciACwxmu6koVQAmiIAdABADNKzgPLL1MAWwOXLl23Hko7WJNLmAr6S4kBkFbEQ55UrV4KrV6/aWIlZRr9OPyGADqMZcdeBSgDc/N8W0uygCtKjlIbpgMwCylM6eenSJdvBaiGkNb4IEMRAfMTU399vY9Sj3zU/+NdIARB5HcgMQM8ADUBmgawPLl68GJw7d86OlKRpo5zZtJFVLow0ou/ERSwXLlywsRGjjH4NIGIGzMwTwIyoGSBmCQRGBguSwcFB2+nTp09bCFlMz8N8DSALBOIhhrNnz9qYiM0d/Tr3qxnQWQSATncGuAD0LGCBcurUqeD48eO242nTSLXGx0FImrqIg74TA7HI6AeAzv06xakZIAAiK6G0AKa5APQM0BWRhsCIefr0qQ3i6NGjNoCkEPI0Py0EMb+vry84duyYjYFYonK/TlW6WhoXAOUgSCpiif7kyRMbTE9Pjw0kicGcJ28lhUS/6St9pu/EwGuk0iTmFw4gqlLRpuqyFMOZvmxZHDlyJDh48KANaDwMzwKB/h46dMgCoM/0XY9+ST1R6UenIQdAa1YAzUkAuBDiUhF7JocPHw727dtnAxtP45OAoZ/79++3AOirpB658ErVU878wgGYDpTKVTEagKwNCADDmc4PHjwIDhw4EOzZs8cGWAsQ6AOzkj4BgD7SV7nwSuqJAhBXuk4IgLhZoCEwrdm8I9Du7u5xgUA/xLw48+nL3r17bd8k9Yj5uuavZP6EA4iDQCBSmgoEUtGuXbvsaNNGiWTU5SHO5Z6PNukPfWD0u+a7C64k5hcOwHSmlLSMdFORvh6wj85WLoHv2LHDBq4h5Gl+FATaoh+0zeinL/SJ16TqkZLTBZCkjJ1wAEkhcB9h9+7dwfbt28dAKEJSRtI+bTL66UNe5tcUgDQQGIUbNmywaaBICJybNmlr586duZtfOADTsVLaFagGINcDvUij6mC5v27dumDjxo22BNQpIy9xTtrbtGlTsHbt2uDEiRORFQ99dAGkXXHXFIBKENg5ZX3An+TkLVu25A5BzN+6datNPWwr0+b58+dzNb9wAKaDpWrKQQEgqYibGyz5ucvEhZC7ahiUJwQxn3Nu27bNtkFb3NWibb3H7wLIuq6oSQAuBIwgDWAGo5BczL1ljMkLgms+56YNyfu0ffLkSTsA8jC/5gEIBG5qs8+OEe71wIXAZwRaGvFvqKxIO9p82pB6n7aBfObMGft+teYXDsBM01K1lQjfJsB8Atc3cPR2RbUQKpnv7vPwGhCYlXlUWjULAPMJFKMxiQsf+TdPCGnMp23Z6+F17nxVC6FmAWjz9T5RGgisE8pB4D1ApTFfLr70KQ8IhQIwHS5luRiS86PMdwFQDnI9wAjZMxIIrFqp4SlXZbGkxWu8t3nzZgtMzJc9Hlls0YYLIA5C1gt/TQEoZ34SCDITMISKBQjMBm7wcC1Bvb29dtTzHpUVn3VHfjnz84RQUwCSmJ8EgmzekcYY2Syg2EPCcMTs4FsYvMdn2GJIa35eEGoGQBrzK0HgHFRN8kVgTOb8mIPku5q8x2f4LP8mrfl5QCgUgAmglKQMlDqfEZjUfA1BQGCcXJhlNmAuoxuj5RvK/Dev8Z6MermfK3v7Sc2PgiDrhKQl8IQCqMb8KAh6NggIRjfn15IRLz+YcKudNOZXA2FCAeRhfjkImIq5AkNLXpdRX635WSHkCWBqGgB5mu9C0CAERpTkfW18NeZngVAoABNcKaqDXKTY08d8veOZl1wQ5ZSn8S4EWTEz0NjAi/tcYV/MigJQtPnlYESpyLaTQCgCQFscgPE2vxZUCUKRADo0gHo0PwkEB0C+P9AwDf5d7+ZXgmCKgcHCfiFjVoV/Uu3Uu/lxEKiO+vv7u0MA07MCiPuR3hfLly//ube394k3PxpCX1/f0IoVK34xXn0d8SvJpmp/psqUWrJ69erfzYLnvml8tN7NVxo1C8BH69ev/yPM/zOcNcDULL8Tdn+oDdFZId3vjL43Wmb0g9KPdSId87LQCzz5JvRIp59UP9SeEvOgDj0LaOBDo3ajj4zmGn0S6tM6kcT7cehBe+jJLGf0t1TzrAj3YR0yC2YqCLPDxueEHREgk1kS55ww9tnK/JkRoz8TgLhZIBBkJnwQNq5h1INmK+M/UCNfzI97UkpqAHEQpikQMiNmKSiTXbPUiBfjp5UxPxWAuEeW6XSkQQgMAVIvmh5hvJt2Mj2yLO6hfS4EASEw2hwok1U61lZlfCXzUwGYEvOwVv2g1mallhi1ThLFxac9mFom7aR+bGU5CO6McNU8yRX39NymJObn/ejiRqfxelSlZ0n7h3dPwIO7c314t398/Xg9vt7L/x80PAAvD8AD8PIAPAAvD8AD8PIAPAAvD8AD8CpO/wAnnXiPa3zSAAAAAABJRU5ErkJggg==\"\"\"\n\nDragCanvas = document.createElement(\"canvas\")\nDragCanvas.style.position = \"absolute\"\nDragCanvas.style.zIndex = 0\ndocument.body.appendChild(DragCanvas)\n\nPercentLoadedCache = {}\nPercentLoadedCanvas = document.createElement(\"canvas\")\nPercentLoadedCanvas.style.position = \"absolute\"\nPercentLoadedCanvas.style.zIndex = 0\ndocument.body.appendChild(PercentLoadedCanvas)\n\nSystemTrayCanvas = document.createElement(\"canvas\")\n\nCanvasUtils =\n  roundRect: (ctx, x, y, width, height, radius = 5, fill, stroke = true) ->\n    ctx.beginPath()\n    ctx.moveTo(x + radius, y)\n    ctx.lineTo(x + width - radius, y)\n    ctx.quadraticCurveTo(x + width, y, x + width, y + radius)\n    ctx.lineTo(x + width, y + height - radius)\n    ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)\n    ctx.lineTo(x + radius, y + height)\n    ctx.quadraticCurveTo(x, y + height, x, y + height - radius)\n    ctx.lineTo(x, y + radius)\n    ctx.quadraticCurveTo(x, y, x + radius, y)\n    ctx.closePath()\n    ctx.stroke() if stroke\n    ctx.fill() if fill\n\n  dataURIForLoadedPercent: (percent) ->\n    percent = Math.floor(percent / 5.0) * 5.0\n    cacheKey = \"#{percent}%\"\n    if not PercentLoadedCache[cacheKey]\n      canvas = PercentLoadedCanvas\n      scale = window.devicePixelRatio\n      canvas.width = 20 * scale\n      canvas.height = 20 * scale\n      canvas.style.width = \"30px\"\n      canvas.style.height = \"30px\"\n\n      half = 10 * scale\n      ctx = canvas.getContext('2d')\n      ctx.strokeStyle = \"#AAA\"\n      ctx.lineWidth = 3 * scale\n      ctx.clearRect(0, 0, 20 * scale, 20 * scale)\n      ctx.beginPath()\n      ctx.arc(half, half, half - ctx.lineWidth, -0.5 * Math.PI, (-0.5 * Math.PI) + (2 * Math.PI) * percent / 100.0)\n      ctx.stroke()\n      PercentLoadedCache[cacheKey] = canvas.toDataURL()\n    return PercentLoadedCache[cacheKey]\n\n  canvasWithThreadDragImage: (count) ->\n    canvas = DragCanvas\n\n    # Make sure the canvas has a 2x pixel density on retina displays\n    scale = window.devicePixelRatio\n    canvas.width = 58 * scale\n    canvas.height = 55 * scale\n    canvas.style.width = \"58px\"\n    canvas.style.height = \"55px\"\n\n    ctx = canvas.getContext('2d')\n\n    # mail background image\n    if count > 1\n      ctx.rotate(-20*Math.PI/180)\n      ctx.drawImage(ThreadDragImage, -10*scale, 2*scale, 48*scale, 48*scale)\n      ctx.rotate(20*Math.PI/180)\n    ctx.drawImage(ThreadDragImage, 0, 0, 48*scale, 48*scale)\n\n    # count bubble\n    dotGradient = ctx.createLinearGradient(0, 0, 0, 15 * scale)\n    dotGradient.addColorStop(0, \"rgb(116, 124, 143)\")\n    dotGradient.addColorStop(1, \"rgb(67, 77, 104)\")\n    ctx.strokeStyle = \"rgba(39, 48, 68, 0.6)\"\n    ctx.lineWidth = 1\n    ctx.fillStyle = dotGradient\n\n    textX = 49\n    text = \"#{count}\"\n\n    if (count < 10)\n      CanvasUtils.roundRect(ctx, 41 * scale, 1 * scale, 16 * scale, 14 * scale, 7 * scale, true, true)\n    else if (count < 100)\n      CanvasUtils.roundRect(ctx, 37 * scale, 1 * scale, 20 * scale, 14 * scale, 7 * scale, true, true)\n      textX = 46\n    else\n      CanvasUtils.roundRect(ctx, 33 * scale, 1 * scale, 25 * scale, 14 * scale, 7 * scale, true, true)\n      text = \"99+\"\n      textX = 46\n\n    # count text\n    ctx.fillStyle = \"rgba(255,255,255,0.9)\"\n    ctx.font = \"#{11 * scale}px sans-serif\"\n    ctx.textAlign = \"center\"\n    ctx.fillText(text, textX * scale, 12 * scale, 30 * scale)\n\n    return DragCanvas\n\n  measureTextInCanvas: (text, font) ->\n    canvas = document.createElement('canvas')\n    context = canvas.getContext('2d')\n    context.font = font\n    return Math.ceil(context.measureText(text).width)\n\n  canvasWithSystemTrayIconAndText: (img, text) ->\n    canvas = SystemTrayCanvas\n    w = img.width\n    h = img.height\n    font = '26px Nylas-Pro, sans-serif'\n\n    textWidth = if text.length > 0 then CanvasUtils.measureTextInCanvas(text, font) + 2 else 0\n    canvas.width = w + textWidth\n    canvas.height = h\n\n    context = canvas.getContext('2d')\n    context.font = font\n    context.fillStyle = 'black'\n    context.textAlign = 'start'\n    context.textBaseline = 'middle'\n\n    context.drawImage(img, 0, 0)\n    # Place after img, vertically aligned\n    context.fillText(text, w, h / 2)\n    return canvas\n\nmodule.exports = CanvasUtils\n"
  },
  {
    "path": "packages/client-app/src/chaos-monkey.coffee",
    "content": "NylasAPI = require('./flux/nylas-api').default\nnock = require 'nock'\n\n# We be wrecking havok in your code\nclass ChaosMonkey\n  @unleashOnAPI: ({errorCode, numMonkeys, makeTimeout}={}) ->\n    errorCode ?= 500\n    numMonkeys ?= \"all the monkeys\"\n    makeTimeout ?= false\n    nGet = nock(NylasAPI.APIRoot)\n    nPut = nock(NylasAPI.APIRoot)\n    nPost = nock(NylasAPI.APIRoot)\n\n    numTimes = 1\n    if numMonkeys.toLowerCase() is \"all the monkeys\"\n      nGet = nGet.persist()\n      nPut = nPut.persist()\n      nPost = nPost.persist()\n    else if _.isNumber(numMonkeys)\n      numTimes = numMonkeys\n\n    nGet = nGet.filteringPath (path) -> '/*'\n      .get('/*')\n\n    nPut = nPut.filteringRequestBody (body) -> '*'\n      .filteringPath (path) -> '/*'\n      .put('/*', '*')\n\n    nPost = nPost.filteringRequestBody (body) -> '*'\n      .filteringPath (path) -> '/*'\n      .post('/*', '*')\n\n    [nGet, nPut, nPost] = [nGet, nPut, nPost].map (n) ->\n      n = n.times(numTimes)\n      if makeTimeout\n        return n.socketDelay(31000)\n      else\n        return n\n\n    if makeTimeout\n      [nGet, nPut, nPost].forEach (n) -> n.reply(200, 'Timed out')\n    else\n      nGet.replyWithError({message:'Monkey GET error!', code: errorCode})\n      nPut.replyWithError({message:'Monkey PUT error!', code: errorCode})\n      nPost.replyWithError({message:'Monkey POST error!', code: errorCode})\n\n  @goHome: ->\n    nock.restore()\n    nock.cleanAll()\n\nwindow.ChaosMonkey = ChaosMonkey\nmodule.exports = ChaosMonkey\n"
  },
  {
    "path": "packages/client-app/src/chrome-user-agent-stylesheet-string.coffee",
    "content": "# This is the Chrome (Blink) default user-agent stylesheet. We need this\n# when we use `automatic/juice` to inline CSS since emails will be\n# assuming they're based off the default stylesheet instead of the Nylas\n# stylesheet.\n#\n# From: https://chromium.googlesource.com/chromium/blink/+/master/Source/core/css/html.css\nmodule.exports = \"\"\"\n@namespace \"http://www.w3.org/1999/xhtml\";\nhtml {\n    display: block\n}\nhead {\n    display: none\n}\nmeta {\n    display: none\n}\ntitle {\n    display: none\n}\nlink {\n    display: none\n}\nstyle {\n    display: none\n}\nscript {\n    display: none\n}\nbody {\n    display: block;\n    margin: 8px\n}\nbody:-webkit-full-page-media {\n    background-color: rgb(0, 0, 0)\n}\np {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1__qem;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n}\ndiv {\n    display: block\n}\nlayer {\n    display: block\n}\narticle, aside, footer, header, hgroup, main, nav, section {\n    display: block\n}\nmarquee {\n    display: inline-block;\n}\naddress {\n    display: block\n}\nblockquote {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 40px;\n    -webkit-margin-end: 40px;\n}\nfigcaption {\n    display: block\n}\nfigure {\n    display: block;\n    -webkit-margin-before: 1em;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 40px;\n    -webkit-margin-end: 40px;\n}\nq {\n    display: inline\n}\nq:before {\n    content: open-quote;\n}\nq:after {\n    content: close-quote;\n}\ncenter {\n    display: block;\n    text-align: -webkit-center\n}\nhr {\n    display: block;\n    -webkit-margin-before: 0.5em;\n    -webkit-margin-after: 0.5em;\n    -webkit-margin-start: auto;\n    -webkit-margin-end: auto;\n    border-style: inset;\n    border-width: 1px\n}\nmap {\n    display: inline\n}\nvideo {\n    object-fit: contain;\n}\nh1 {\n    display: block;\n    font-size: 2em;\n    -webkit-margin-before: 0.67__qem;\n    -webkit-margin-after: 0.67em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\n:-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.5em;\n    -webkit-margin-before: 0.83__qem;\n    -webkit-margin-after: 0.83em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.17em;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: 1.00em;\n    -webkit-margin-before: 1.33__qem;\n    -webkit-margin-after: 1.33em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: .83em;\n    -webkit-margin-before: 1.67__qem;\n    -webkit-margin-after: 1.67em;\n}\n:-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) :-webkit-any(article,aside,nav,section) h1 {\n    font-size: .67em;\n    -webkit-margin-before: 2.33__qem;\n    -webkit-margin-after: 2.33em;\n}\nh2 {\n    display: block;\n    font-size: 1.5em;\n    -webkit-margin-before: 0.83__qem;\n    -webkit-margin-after: 0.83em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh3 {\n    display: block;\n    font-size: 1.17em;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh4 {\n    display: block;\n    -webkit-margin-before: 1.33__qem;\n    -webkit-margin-after: 1.33em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh5 {\n    display: block;\n    font-size: .83em;\n    -webkit-margin-before: 1.67__qem;\n    -webkit-margin-after: 1.67em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\nh6 {\n    display: block;\n    font-size: .67em;\n    -webkit-margin-before: 2.33__qem;\n    -webkit-margin-after: 2.33em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    font-weight: bold\n}\ntable {\n    display: table;\n    border-collapse: separate;\n    border-spacing: 2px;\n    border-color: gray\n}\nthead {\n    display: table-header-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntbody {\n    display: table-row-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntfoot {\n    display: table-footer-group;\n    vertical-align: middle;\n    border-color: inherit\n}\ntable > tr {\n    vertical-align: middle;\n}\ncol {\n    display: table-column\n}\ncolgroup {\n    display: table-column-group\n}\ntr {\n    display: table-row;\n    vertical-align: inherit;\n    border-color: inherit\n}\ntd, th {\n    display: table-cell;\n    vertical-align: inherit\n}\nth {\n    font-weight: bold\n}\ncaption {\n    display: table-caption;\n    text-align: -webkit-center\n}\nul, menu, dir {\n    display: block;\n    list-style-type: disc;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    -webkit-padding-start: 40px\n}\nol {\n    display: block;\n    list-style-type: decimal;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n    -webkit-padding-start: 40px\n}\nli {\n    display: list-item;\n    text-align: -webkit-match-parent;\n}\nul ul, ol ul {\n    list-style-type: circle\n}\nol ol ul, ol ul ul, ul ol ul, ul ul ul {\n    list-style-type: square\n}\ndd {\n    display: block;\n    -webkit-margin-start: 40px\n}\ndl {\n    display: block;\n    -webkit-margin-before: 1__qem;\n    -webkit-margin-after: 1em;\n    -webkit-margin-start: 0;\n    -webkit-margin-end: 0;\n}\ndt {\n    display: block\n}\nol ul, ul ol, ul ul, ol ol {\n    -webkit-margin-before: 0;\n    -webkit-margin-after: 0\n}\nform {\n    display: block;\n    margin-top: 0__qem;\n}\nlabel {\n    cursor: default;\n}\nlegend {\n    display: block;\n    -webkit-padding-start: 2px;\n    -webkit-padding-end: 2px;\n    border: none\n}\nfieldset {\n    display: block;\n    -webkit-margin-start: 2px;\n    -webkit-margin-end: 2px;\n    -webkit-padding-before: 0.35em;\n    -webkit-padding-start: 0.75em;\n    -webkit-padding-end: 0.75em;\n    -webkit-padding-after: 0.625em;\n    border: 2px groove ThreeDFace;\n    min-width: -webkit-min-content;\n}\nbutton {\n    -webkit-appearance: button;\n}\ninput, textarea, keygen, select, button, meter, progress {\n    -webkit-writing-mode: horizontal-tb !important;\n}\ninput, textarea, keygen, select, button {\n    margin: 0__qem;\n    font: -webkit-small-control;\n    text-rendering: auto;\n    color: initial;\n    letter-spacing: normal;\n    word-spacing: normal;\n    line-height: normal;\n    text-transform: none;\n    text-indent: 0;\n    text-shadow: none;\n    display: inline-block;\n    text-align: start;\n}\ninput[type=\"hidden\" i] {\n    display: none\n}\ninput {\n    -webkit-appearance: textfield;\n    padding: 1px;\n    background-color: white;\n    border: 2px inset;\n    -webkit-rtl-ordering: logical;\n    -webkit-user-select: text;\n    cursor: auto;\n}\ninput[type=\"search\" i] {\n    -webkit-appearance: searchfield;\n    box-sizing: border-box;\n}\ninput::-webkit-textfield-decoration-container {\n    display: flex;\n    align-items: center;\n    -webkit-user-modify: read-only !important;\n    content: none !important;\n}\ninput[type=\"search\" i]::-webkit-textfield-decoration-container {\n    direction: ltr;\n}\ninput::-webkit-clear-button {\n    -webkit-appearance: searchfield-cancel-button;\n    display: inline-block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-margin-start: 2px;\n    opacity: 0;\n    pointer-events: none;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-clear-button {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"search\" i]::-webkit-search-cancel-button {\n    -webkit-appearance: searchfield-cancel-button;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-margin-start: 1px;\n    opacity: 0;\n    pointer-events: none;\n}\ninput[type=\"search\" i]:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-search-cancel-button {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"search\" i]::-webkit-search-decoration {\n    -webkit-appearance: searchfield-decoration;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-align-self: flex-start;\n    margin: auto 0;\n}\ninput[type=\"search\" i]::-webkit-search-results-decoration {\n    -webkit-appearance: searchfield-results-decoration;\n    display: block;\n    flex: none;\n    -webkit-user-modify: read-only !important;\n    -webkit-align-self: flex-start;\n    margin: auto 0;\n}\ninput::-webkit-inner-spin-button {\n    -webkit-appearance: inner-spin-button;\n    display: inline-block;\n    cursor: default;\n    flex: none;\n    align-self: stretch;\n    -webkit-user-select: none;\n    -webkit-user-modify: read-only !important;\n    opacity: 0;\n    pointer-events: none;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-inner-spin-button {\n    opacity: 1;\n    pointer-events: auto;\n}\nkeygen, select {\n    border-radius: 5px;\n}\nkeygen::-webkit-keygen-select {\n    margin: 0px;\n}\ntextarea {\n    -webkit-appearance: textarea;\n    background-color: white;\n    border: 1px solid;\n    -webkit-rtl-ordering: logical;\n    -webkit-user-select: text;\n    flex-direction: column;\n    resize: auto;\n    cursor: auto;\n    padding: 2px;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n}\n::-webkit-input-placeholder {\n    -webkit-text-security: none;\n    color: darkGray;\n    pointer-events: none !important;\n}\ninput::-webkit-input-placeholder {\n    white-space: pre;\n    word-wrap: normal;\n    overflow: hidden;\n    -webkit-user-modify: read-only !important;\n}\ninput[type=\"password\" i] {\n    -webkit-text-security: disc !important;\n}\ninput[type=\"hidden\" i], input[type=\"image\" i], input[type=\"file\" i] {\n    -webkit-appearance: initial;\n    padding: initial;\n    background-color: initial;\n    border: initial;\n}\ninput[type=\"file\" i] {\n    align-items: baseline;\n    color: inherit;\n    text-align: start !important;\n}\ninput:-webkit-autofill, textarea:-webkit-autofill, select:-webkit-autofill {\n    background-color: #FAFFBD !important;\n    background-image:none !important;\n    color: #000000 !important;\n}\ninput[type=\"radio\" i], input[type=\"checkbox\" i] {\n    margin: 3px 0.5ex;\n    padding: initial;\n    background-color: initial;\n    border: initial;\n}\ninput[type=\"button\" i], input[type=\"submit\" i], input[type=\"reset\" i] {\n    -webkit-appearance: push-button;\n    -webkit-user-select: none;\n    white-space: pre\n}\ninput[type=\"file\" i]::-webkit-file-upload-button {\n    -webkit-appearance: push-button;\n    -webkit-user-modify: read-only !important;\n    white-space: nowrap;\n    margin: 0;\n    font-size: inherit;\n}\ninput[type=\"button\" i], input[type=\"submit\" i], input[type=\"reset\" i], input[type=\"file\" i]::-webkit-file-upload-button, button {\n    align-items: flex-start;\n    text-align: center;\n    cursor: default;\n    color: ButtonText;\n    padding: 2px 6px 3px 6px;\n    border: 2px outset ButtonFace;\n    background-color: ButtonFace;\n    box-sizing: border-box\n}\ninput[type=\"range\" i] {\n    -webkit-appearance: slider-horizontal;\n    padding: initial;\n    border: initial;\n    margin: 2px;\n    color: #909090;\n}\ninput[type=\"range\" i]::-webkit-slider-container, input[type=\"range\" i]::-webkit-media-slider-container {\n    flex: 1;\n    min-width: 0;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: flex;\n}\ninput[type=\"range\" i]::-webkit-slider-runnable-track {\n    flex: 1;\n    min-width: 0;\n    -webkit-align-self: center;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: block;\n}\ninput[type=\"range\" i]::-webkit-slider-thumb, input[type=\"range\" i]::-webkit-media-slider-thumb {\n    -webkit-appearance: sliderthumb-horizontal;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    display: block;\n}\ninput[type=\"button\" i]:disabled, input[type=\"submit\" i]:disabled, input[type=\"reset\" i]:disabled,\ninput[type=\"file\" i]:disabled::-webkit-file-upload-button, button:disabled,\nselect:disabled, keygen:disabled, optgroup:disabled, option:disabled,\nselect[disabled]>option {\n    color: GrayText\n}\ninput[type=\"button\" i]:active, input[type=\"submit\" i]:active, input[type=\"reset\" i]:active, input[type=\"file\" i]:active::-webkit-file-upload-button, button:active {\n    border-style: inset\n}\ninput[type=\"button\" i]:active:disabled, input[type=\"submit\" i]:active:disabled, input[type=\"reset\" i]:active:disabled, input[type=\"file\" i]:active:disabled::-webkit-file-upload-button, button:active:disabled {\n    border-style: outset\n}\noption:-internal-spatial-navigation-focus {\n    outline: black dashed 1px;\n    outline-offset: -1px;\n}\ndatalist {\n    display: none\n}\narea {\n    display: inline;\n    cursor: pointer;\n}\nparam {\n    display: none\n}\ninput[type=\"checkbox\" i] {\n    -webkit-appearance: checkbox;\n    box-sizing: border-box;\n}\ninput[type=\"radio\" i] {\n    -webkit-appearance: radio;\n    box-sizing: border-box;\n}\ninput[type=\"color\" i] {\n    -webkit-appearance: square-button;\n    width: 44px;\n    height: 23px;\n    background-color: ButtonFace;\n    border: 1px #a9a9a9 solid;\n    padding: 1px 2px;\n}\ninput[type=\"color\" i]::-webkit-color-swatch-wrapper {\n    display:flex;\n    padding: 4px 2px;\n    box-sizing: border-box;\n    -webkit-user-modify: read-only !important;\n    width: 100%;\n    height: 100%\n}\ninput[type=\"color\" i]::-webkit-color-swatch {\n    background-color: #000000;\n    border: 1px solid #777777;\n    flex: 1;\n    min-width: 0;\n    -webkit-user-modify: read-only !important;\n}\ninput[type=\"color\" i][list] {\n    -webkit-appearance: menulist;\n    width: 88px;\n    height: 23px\n}\ninput[type=\"color\" i][list]::-webkit-color-swatch-wrapper {\n    padding-left: 8px;\n    padding-right: 24px;\n}\ninput[type=\"color\" i][list]::-webkit-color-swatch {\n    border-color: #000000;\n}\ninput::-webkit-calendar-picker-indicator {\n    display: inline-block;\n    width: 0.66em;\n    height: 0.66em;\n    padding: 0.17em 0.34em;\n    -webkit-user-modify: read-only !important;\n    opacity: 0;\n    pointer-events: none;\n}\ninput::-webkit-calendar-picker-indicator:hover {\n    background-color: #eee;\n}\ninput:enabled:read-write:-webkit-any(:focus,:hover)::-webkit-calendar-picker-indicator,\ninput::-webkit-calendar-picker-indicator:focus {\n    opacity: 1;\n    pointer-events: auto;\n}\ninput[type=\"date\" i]:disabled::-webkit-clear-button,\ninput[type=\"date\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"datetime-local\" i]:disabled::-webkit-clear-button,\ninput[type=\"datetime-local\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"month\" i]:disabled::-webkit-clear-button,\ninput[type=\"month\" i]:disabled::-webkit-inner-spin-button,\ninput[type=\"week\" i]:disabled::-webkit-clear-button,\ninput[type=\"week\" i]:disabled::-webkit-inner-spin-button,\ninput:disabled::-webkit-calendar-picker-indicator,\ninput[type=\"date\" i][readonly]::-webkit-clear-button,\ninput[type=\"date\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"datetime-local\" i][readonly]::-webkit-clear-button,\ninput[type=\"datetime-local\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"month\" i][readonly]::-webkit-clear-button,\ninput[type=\"month\" i][readonly]::-webkit-inner-spin-button,\ninput[type=\"week\" i][readonly]::-webkit-clear-button,\ninput[type=\"week\" i][readonly]::-webkit-inner-spin-button,\ninput[readonly]::-webkit-calendar-picker-indicator {\n    visibility: hidden;\n}\nselect {\n    -webkit-appearance: menulist;\n    box-sizing: border-box;\n    align-items: center;\n    border: 1px solid;\n    white-space: pre;\n    -webkit-rtl-ordering: logical;\n    color: black;\n    background-color: white;\n    cursor: default;\n}\nselect:not(:-internal-list-box) {\n    overflow: visible !important;\n}\nselect:-internal-list-box {\n    -webkit-appearance: listbox;\n    align-items: flex-start;\n    border: 1px inset gray;\n    border-radius: initial;\n    overflow-x: hidden;\n    overflow-y: scroll;\n    vertical-align: text-bottom;\n    -webkit-user-select: none;\n    white-space: nowrap;\n}\noptgroup {\n    font-weight: bolder;\n    display: block;\n}\noption {\n    font-weight: normal;\n    display: block;\n    padding: 0 2px 1px 2px;\n    white-space: pre;\n    min-height: 1.2em;\n}\nselect:-internal-list-box option,\nselect:-internal-list-box optgroup {\n    line-height: initial !important;\n}\nselect:-internal-list-box:focus option:checked {\n    background-color: -internal-active-list-box-selection !important;\n    color: -internal-active-list-box-selection-text !important;\n}\nselect:-internal-list-box option:checked {\n    background-color: -internal-inactive-list-box-selection !important;\n    color: -internal-inactive-list-box-selection-text !important;\n}\nselect:-internal-list-box:disabled option:checked,\nselect:-internal-list-box option:checked:disabled {\n    color: gray !important;\n}\nselect:-internal-list-box hr {\n    border-style: none;\n}\noutput {\n    display: inline;\n}\nmeter {\n    -webkit-appearance: meter;\n    box-sizing: border-box;\n    display: inline-block;\n    height: 1em;\n    width: 5em;\n    vertical-align: -0.2em;\n}\nmeter::-webkit-meter-inner-element {\n    -webkit-appearance: inherit;\n    box-sizing: inherit;\n    -webkit-user-modify: read-only !important;\n    height: 100%;\n    width: 100%;\n}\nmeter::-webkit-meter-bar {\n    background: linear-gradient(to bottom, #ddd, #eee 20%, #ccc 45%, #ccc 55%, #ddd);\n    height: 100%;\n    width: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-optimum-value {\n    background: linear-gradient(to bottom, #ad7, #cea 20%, #7a3 45%, #7a3 55%, #ad7);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-suboptimum-value {\n    background: linear-gradient(to bottom, #fe7, #ffc 20%, #db3 45%, #db3 55%, #fe7);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nmeter::-webkit-meter-even-less-good-value {\n    background: linear-gradient(to bottom, #f77, #fcc 20%, #d44 45%, #d44 55%, #f77);\n    height: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nprogress {\n    -webkit-appearance: progress-bar;\n    box-sizing: border-box;\n    display: inline-block;\n    height: 1em;\n    width: 10em;\n    vertical-align: -0.2em;\n}\nprogress::-webkit-progress-inner-element {\n    -webkit-appearance: inherit;\n    box-sizing: inherit;\n    -webkit-user-modify: read-only;\n    height: 100%;\n    width: 100%;\n}\nprogress::-webkit-progress-bar {\n    background-color: gray;\n    height: 100%;\n    width: 100%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nprogress::-webkit-progress-value {\n    background-color: green;\n    height: 100%;\n    width: 50%;\n    -webkit-user-modify: read-only !important;\n    box-sizing: border-box;\n}\nu, ins {\n    text-decoration: underline\n}\nstrong, b {\n    font-weight: bold\n}\ni, cite, em, var, address, dfn {\n    font-style: italic\n}\ntt, code, kbd, samp {\n    font-family: monospace\n}\npre, xmp, plaintext, listing {\n    display: block;\n    font-family: monospace;\n    white-space: pre;\n    margin: 1__qem 0\n}\nmark {\n    background-color: yellow;\n    color: black\n}\nbig {\n    font-size: larger\n}\nsmall {\n    font-size: smaller\n}\ns, strike, del {\n    text-decoration: line-through\n}\nsub {\n    vertical-align: sub;\n    font-size: smaller\n}\nsup {\n    vertical-align: super;\n    font-size: smaller\n}\nnobr {\n    white-space: nowrap\n}\n:focus {\n    outline: auto 5px -webkit-focus-ring-color\n}\nhtml:focus, body:focus, input[readonly]:focus {\n    outline: none\n}\napplet:focus, embed:focus, iframe:focus, object:focus {\n    outline: none\n}\n\ninput:focus, textarea:focus, keygen:focus, select:focus {\n    outline-offset: -2px\n}\ninput[type=\"button\" i]:focus,\ninput[type=\"checkbox\" i]:focus,\ninput[type=\"file\" i]:focus,\ninput[type=\"hidden\" i]:focus,\ninput[type=\"image\" i]:focus,\ninput[type=\"radio\" i]:focus,\ninput[type=\"reset\" i]:focus,\ninput[type=\"search\" i]:focus,\ninput[type=\"submit\" i]:focus,\ninput[type=\"file\" i]:focus::-webkit-file-upload-button {\n    outline-offset: 0\n}\n    \na:-webkit-any-link {\n    color: -webkit-link;\n    text-decoration: underline;\n    cursor: auto;\n}\na:-webkit-any-link:active {\n    color: -webkit-activelink\n}\nruby, rt {\n    text-indent: 0;\n}\nrt {\n    line-height: normal;\n    -webkit-text-emphasis: none;\n}\nruby > rt {\n    display: block;\n    font-size: 50%;\n    text-align: start;\n}\nruby > rp {\n    display: none;\n}\nnoframes {\n    display: none\n}\nframeset, frame {\n    display: block\n}\nframeset {\n    border-color: inherit\n}\niframe {\n    border: 2px inset\n}\ndetails {\n    display: block\n}\nsummary {\n    display: block\n}\nsummary::-webkit-details-marker {\n    display: inline-block;\n    width: 0.66em;\n    height: 0.66em;\n    -webkit-margin-end: 0.4em;\n}\ntemplate {\n    display: none\n}\nbdi, output {\n    unicode-bidi: -webkit-isolate;\n}\nbdo {\n    unicode-bidi: bidi-override;\n}\ntextarea[dir=auto i] {\n    unicode-bidi: -webkit-plaintext;\n}\ndialog:not([open]) {\n    display: none\n}\ndialog {\n    position: absolute;\n    left: 0;\n    right: 0;\n    width: -webkit-fit-content;\n    height: -webkit-fit-content;\n    margin: auto;\n    border: solid;\n    padding: 1em;\n    background: white;\n    color: black\n}\ndialog::backdrop {\n    position: fixed;\n    top: 0;\n    right: 0;\n    bottom: 0;\n    left: 0;\n    background: rgba(0,0,0,0.1)\n}\n@page {\n    size: auto;\n    margin: auto;\n    padding: 0px;\n    border-width: 0px;\n}\n@media print {\n    * { -webkit-columns: auto !important; }\n}\n\"\"\"\n"
  },
  {
    "path": "packages/client-app/src/color.coffee",
    "content": "_ = require 'underscore'\nParsedColor = null\n\n# Essential: A simple color class returned from {Config::get} when the value\n# at the key path is of type 'color'.\nmodule.exports =\nclass Color\n  # Essential: Parse a {String} or {Object} into a {Color}.\n  #\n  # * `value` A {String} such as `'white'`, `#ff00ff`, or\n  #   `'rgba(255, 15, 60, .75)'` or an {Object} with `red`, `green`, `blue`,\n  #   and `alpha` properties.\n  #\n  # Returns a {Color} or `null` if it cannot be parsed.\n  @parse: (value) ->\n    return null if _.isArray(value) or _.isFunction(value)\n    return null unless _.isObject(value) or _.isString(value)\n\n    ParsedColor ?= require 'color'\n\n    try\n      parsedColor = new ParsedColor(value)\n    catch error\n      return null\n\n    new Color(parsedColor.red(), parsedColor.green(), parsedColor.blue(), parsedColor.alpha())\n\n  constructor: (red, green, blue, alpha) ->\n    Object.defineProperties this,\n      red:\n        set: (newRed) -> red = parseColor(newRed)\n        get: -> red\n        enumerable: true\n        configurable: false\n      green:\n        set: (newGreen) -> green = parseColor(newGreen)\n        get: -> green\n        enumerable: true\n        configurable: false\n      blue:\n        set: (newBlue) -> blue = parseColor(newBlue)\n        get: -> blue\n        enumerable: true\n        configurable: false\n      alpha:\n        set: (newAlpha) -> alpha = parseAlpha(newAlpha)\n        get: -> alpha\n        enumerable: true\n        configurable: false\n\n    @red = red\n    @green = green\n    @blue = blue\n    @alpha = alpha\n\n  # Essential: Returns a {String} in the form `'#abcdef'`.\n  toHexString: ->\n    \"##{numberToHexString(@red)}#{numberToHexString(@green)}#{numberToHexString(@blue)}\"\n\n  # Essential: Returns a {String} in the form `'rgba(25, 50, 75, .9)'`.\n  toRGBAString: ->\n    \"rgba(#{@red}, #{@green}, #{@blue}, #{@alpha})\"\n\n  isEqual: (color) ->\n    return true if this is color\n    color = Color.parse(color) unless color instanceof Color\n    return false unless color?\n    color.red is @red and color.blue is @blue and color.green is @green and color.alpha is @alpha\n\n  clone: -> new Color(@red, @green, @blue, @alpha)\n\nparseColor = (color) ->\n  color = parseInt(color)\n  color = 0 if isNaN(color)\n  color = Math.max(color, 0)\n  color = Math.min(color, 255)\n  color\n\nparseAlpha = (alpha) ->\n  alpha = parseFloat(alpha)\n  alpha = 1 if isNaN(alpha)\n  alpha = Math.max(alpha, 0)\n  alpha = Math.min(alpha, 1)\n  alpha\n\nnumberToHexString = (number) ->\n  hex = number.toString(16)\n  hex = \"0#{hex}\" if number < 10\n  hex\n"
  },
  {
    "path": "packages/client-app/src/compile-cache.js",
    "content": "/* eslint no-cond-assign: 0 */\nconst path = require('path')\nconst fs = require('fs-plus')\n\nconst babelCompiler = require('./compile-support/babel')\nconst coffeeCompiler = require('./compile-support/coffee-script')\nconst typescriptCompiler = require('./compile-support/typescript')\n\nconst COMPILERS = {\n  '.jsx': babelCompiler,\n  '.es6': babelCompiler,\n  '.ts': typescriptCompiler,\n  '.coffee': coffeeCompiler,\n  '.cjsx': coffeeCompiler,\n}\n\nconst cacheStats = {}\nlet cacheDirectory = null\n\nfunction readCachedJavascript(relativeCachePath) {\n  const cachePath = path.join(cacheDirectory, relativeCachePath)\n  if (fs.isFileSync(cachePath)) {\n    try {\n      return fs.readFileSync(cachePath, 'utf8')\n    } catch (error) {\n      //\n    }\n  }\n  return null\n}\n\n/**\n * We occasionally see EPERM errors on the compile cache during boot on\n * Windows. See https://phab.nylas.com/T8128. This will simply catch these\n * errors and defer writing to the cache instead of taking down the whole\n * app on boot.\n *\n * TODO: Determine root cause of the EPERM lock. Maybe antivirus.  Maybe\n * multiple processes loading at once.\n */\nfunction writeCachedJavascript(relativeCachePath, code) {\n  try {\n    const cacheTmpPath = path.join(cacheDirectory, `${relativeCachePath}.${process.pid}`)\n    const cachePath = path.join(cacheDirectory, relativeCachePath)\n    fs.writeFileSync(cacheTmpPath, code, 'utf8')\n    fs.renameSync(cacheTmpPath, cachePath)\n  } catch (err) {\n    console.error(err)\n  }\n}\n\nfunction addSourceURL(jsCode, filePath) {\n  let finalPath = filePath;\n  if (process.platform === 'win32') {\n    finalPath = `/${path.resolve(filePath).replace(/\\\\/g, '/')}`\n  }\n  return `${jsCode}\\n//# sourceURL=${encodeURI(finalPath)}\\n`;\n}\n\nfunction compileFileAtPath(compiler, filePath, extension) {\n  const sourceCode = fs.readFileSync(filePath, 'utf8')\n  if (compiler.shouldCompile(sourceCode, filePath)) {\n    const cachePath = compiler.getCachePath(sourceCode, filePath)\n    let compiledCode = readCachedJavascript(cachePath)\n    if (compiledCode != null) {\n      cacheStats[extension].hits++\n    } else {\n      cacheStats[extension].misses++\n      compiledCode = addSourceURL(compiler.compile(sourceCode, filePath), filePath)\n      writeCachedJavascript(cachePath, compiledCode)\n    }\n    return compiledCode\n  }\n  return sourceCode\n}\n\nconst INLINE_SOURCE_MAP_REGEXP = /\\/\\/[#@]\\s*sourceMappingURL=([^'\"\\n]+)\\s*$/mg\n\nrequire('source-map-support').install({\n  handleUncaughtExceptions: false,\n\n  // Most of this logic is the same as the default implementation in the\n  // source-map-support module, but we've overridden it to read the javascript\n  // code from our cache directory.\n  retrieveSourceMap: (filePath) => {\n    if (!cacheDirectory || !fs.isFileSync(filePath)) {\n      return null\n    }\n\n    // read the original source\n    let sourceCode = null;\n    try {\n      sourceCode = fs.readFileSync(filePath, 'utf8')\n    } catch (error) {\n      console.warn('Error reading source file', error.stack)\n      return null\n    }\n\n    // retrieve the javascript for the original source\n    const compiler = COMPILERS[path.extname(filePath)]\n    let javascriptCode = null;\n    if (compiler) {\n      try {\n        javascriptCode = readCachedJavascript(compiler.getCachePath(sourceCode, filePath))\n      } catch (error) {\n        console.warn('Error reading compiled file', error.stack)\n        return null\n      }\n    } else {\n      javascriptCode = sourceCode;\n    }\n\n    if (javascriptCode == null) {\n      return null\n    }\n\n    let match;\n    let lastMatch;\n    INLINE_SOURCE_MAP_REGEXP.lastIndex = 0\n    while ((match = INLINE_SOURCE_MAP_REGEXP.exec(javascriptCode))) {\n      lastMatch = match\n    }\n    if (lastMatch == null) {\n      return null\n    }\n\n    const sourceMappingURL = lastMatch[1]\n\n    // check whether this is a file path, or an inline sourcemap and load it\n    let rawData = null;\n    if (sourceMappingURL.includes(',')) {\n      rawData = sourceMappingURL.slice(sourceMappingURL.indexOf(',') + 1);\n    } else {\n      rawData = fs.readFileSync(path.resolve(path.dirname(filePath), sourceMappingURL));\n    }\n\n    let sourceMap = null;\n    try {\n      sourceMap = JSON.parse(new Buffer(rawData, 'base64'))\n    } catch (error) {\n      console.warn('Error parsing source map', error.stack)\n      return null\n    }\n\n    return {\n      map: sourceMap,\n      url: null,\n    }\n  },\n})\n\nObject.keys(COMPILERS).forEach((extension) => {\n  const compiler = COMPILERS[extension]\n\n  Object.defineProperty(require.extensions, extension, {\n    enumerable: true,\n    writable: true,\n    value: (module, filePath) => {\n      const code = compileFileAtPath(compiler, filePath, extension)\n      return module._compile(code, filePath)\n    },\n  })\n})\n\n\nexports.setHomeDirectory = (nylasHome) => {\n  let cacheDir = path.join(nylasHome, 'compile-cache')\n  if (process.env.USER === 'root' && process.env.SUDO_USER && process.env.SUDO_USER !== process.env.USER) {\n    cacheDir = path.join(cacheDir, 'root')\n  }\n  this.setCacheDirectory(cacheDir)\n}\n\nexports.setCacheDirectory = (directory) => {\n  cacheDirectory = directory\n}\n\nexports.getCacheDirectory = () => {\n  return cacheDirectory\n}\n\nexports.addPathToCache = (filePath, nylasHome) => {\n  this.setHomeDirectory(nylasHome)\n  const extension = path.extname(filePath)\n  const compiler = COMPILERS[extension]\n  if (compiler) {\n    compileFileAtPath(compiler, filePath, extension)\n  }\n}\n\nexports.getCacheStats = () => {\n  return cacheStats\n}\n\nexports.resetCacheStats = () => {\n  Object.keys(COMPILERS).forEach((extension) => {\n    cacheStats[extension] = {\n      hits: 0,\n      misses: 0,\n    }\n  })\n}\nexports.resetCacheStats()\n"
  },
  {
    "path": "packages/client-app/src/compile-support/babel.js",
    "content": "'use strict'\n\nvar crypto = require('crypto')\nvar path = require('path')\nvar fs = require('fs')\n\nvar babel = null\nvar babelVersionDirectory = null\n\n// This adds in the regeneratorRuntime for generators to work properly\n// We manually insert it here instead of using the kitchen-sink\n// babel-polyfill.\nrequire('babel-regenerator-runtime');\n\n// We run babel with lots of different working directories (like plugin folders).\n// To make sure presets always resolve to the correct path inside N1, resolve\n// them to their absolute paths ahead of time.\nconst babelPath = path.resolve(path.join(__dirname, \"..\", \"..\", \".babelrc\"))\nvar defaultOptions = JSON.parse(fs.readFileSync(babelPath));\ndefaultOptions.presets = (defaultOptions.presets || []).map((modulename) =>\n  require.resolve(`babel-preset-${modulename}`)\n);\ndefaultOptions.plugins = (defaultOptions.plugins || []).map((modulename) =>\n  require.resolve(`babel-plugin-${modulename}`)\n);\n\nexports.shouldCompile = function (sourceCode, filePath) {\n  return (filePath.endsWith('.es6') || filePath.endsWith('.jsx'))\n}\n\nexports.getCachePath = function (sourceCode) {\n  if (babelVersionDirectory == null) {\n    var babelVersion = require('babel-core/package.json').version\n    babelVersionDirectory = path.join('js', 'babel', createVersionAndOptionsDigest(babelVersion, defaultOptions))\n  }\n\n  return path.join(\n    babelVersionDirectory,\n    crypto\n      .createHash('sha1')\n      .update(sourceCode, 'utf8')\n      .digest('hex') + '.js'\n  )\n}\n\nexports.compile = function (sourceCode, filePath) {\n  if (!babel) {\n    babel = require('babel-core');\n  }\n\n  var options = {filename: filePath}\n  for (var key in defaultOptions) {\n    options[key] = defaultOptions[key]\n  }\n  return babel.transform(sourceCode, options).code\n}\n\nfunction createVersionAndOptionsDigest (version, options) {\n  return crypto\n    .createHash('sha1')\n    .update('babel-core', 'utf8')\n    .update('\\0', 'utf8')\n    .update(version, 'utf8')\n    .update('\\0', 'utf8')\n    .update(JSON.stringify(options), 'utf8')\n    .digest('hex')\n}\n"
  },
  {
    "path": "packages/client-app/src/compile-support/cjsx.js",
    "content": "CoffeeScript = require('coffee-react');\n\n// TODO: Remove react-hot-api (which is deprecated) in favor of react-proxy\n//\n// Note: This uses https://github.com/gaearon/react-hot-api and code from\n// https://github.com/BenoitZugmeyer/chwitt-react/blob/2d62184986c7c183955dcb607dba5ceda70a2221/bootstrap-jsx.js\n\nvar hotCompile = (function () {\n  var fs = require('fs');\n  var React = require('react');\n  var ReactMount = require('react/lib/ReactMount');\n  var reactHotReload;\n  try {\n      reactHotReload = require('react-hot-api')(function () { return ReactMount._instancesByReactRootID; });\n  }\n  catch (e) {\n      console.log('Not using react hot reload');\n  }\n\n  var currentlyCompiling;\n  var watchedModules = new WeakSet();\n  var requiredBy = new Map();\n\n  function compile(module, filename) {\n    return module._compile(CoffeeScript._compileFile(filename, false), filename);\n  }\n\n  function monitorHotReload(module) {\n    if (watchedModules.has(module)) return;\n\n    watchedModules.add(module);\n\n    var timeout;\n    setTimeout(function(){\n      var pathwatcher = require('pathwatcher');\n      pathwatcher.watch(module.filename, /*{persistent: true},*/ function () {\n        clearTimeout(timeout);\n        timeout = setTimeout(function () {\n          hotCompile(module, module.filename, true);\n          console.log('hot reloaded '+module.filename);\n        }, 100);\n      });\n    },100);\n  }\n\n  function isReactComponent(module) {\n    return reactHotReload && (module.exports.prototype instanceof React.Component);\n  }\n\n  function recompileRequirements(module, collection) {\n    if (requiredBy.has(module)) {\n      var requirements = requiredBy.get(module);\n      var m;\n      for (m of requirements) {\n        if (!collection.has(m)) {\n          collection.add(m);\n          hotCompile(m, m.filename);\n        }\n      }\n\n      for (m of requirements) {\n        recompileRequirements(m, collection || new WeakSet());\n      }\n    }\n  }\n\n  function monitorRequire(module) {\n    if (Object.getOwnPropertyDescriptor(require.cache, module.filename).value) {\n      Object.defineProperty(require.cache, module.filename, {\n        get: function () {\n          onRequired(module);\n          return module;\n        }\n      });\n    }\n  }\n\n  function onRequired(module) {\n    if (currentlyCompiling) {\n      if (!requiredBy.has(module)) requiredBy.set(module, new Set());\n      requiredBy.get(module).add(currentlyCompiling);\n    }\n  }\n\n  function removeModuleFromDependencies(module) {\n    for (var mod of requiredBy.values()) {\n      mod.delete(module);\n    }\n  }\n\n  function hotCompile(module, filename, withRequirements) {\n\n    monitorRequire(module);\n    onRequired(module);\n\n    removeModuleFromDependencies(module);\n\n    var previouslyCompiling = currentlyCompiling;\n    currentlyCompiling = module;\n\n    var wasReactComponent = isReactComponent(module);\n\n    var result;\n    var failed = false;\n\n    try {\n      result = compile(module, filename);\n    }\n    catch (e) {\n      console.log('Error while compiling ' + filename);\n      console.log(e.stack);\n      failed = true;\n    }\n\n    currentlyCompiling = previouslyCompiling;\n\n    monitorHotReload(module);\n\n    if (!failed) {\n\n      if (isReactComponent(module)) {\n        reactHotReload(module.exports, module.filename);\n      }\n\n      if ((!wasReactComponent || !isReactComponent(module)) && withRequirements) {\n        recompileRequirements(module, new Set([module]));\n      }\n    }\n\n    return result;\n  }\n\n  return hotCompile;\n}());\n\nfunction registerHotCompile() {\n  require.extensions['.cjsx'] = hotCompile;\n\n  if (process.mainModule === module) {\n    var path = require('path');\n    require(path.resolve(process.argv[2]));\n  }\n}\n\nmodule.exports = {\n  register: registerHotCompile\n};\n"
  },
  {
    "path": "packages/client-app/src/compile-support/coffee-script.js",
    "content": "'use strict'\n\nvar crypto = require('crypto')\nvar path = require('path')\nvar CoffeeScript = null\n\nexports.shouldCompile = function () {\n  return true\n}\n\nexports.getCachePath = function (sourceCode) {\n  return path.join(\n    'coffee',\n    crypto\n      .createHash('sha1')\n      .update(sourceCode, 'utf8')\n      .digest('hex') + '.js'\n  )\n}\n\nexports.compile = function (sourceCode, filePath) {\n  if (!CoffeeScript) {\n    var previousPrepareStackTrace = Error.prepareStackTrace\n    CoffeeScript = require('coffee-react')\n\n    // When it loads, coffee-script reassigns Error.prepareStackTrace. We have\n    // already reassigned it via the 'source-map-support' module, so we need\n    // to set it back.\n    Error.prepareStackTrace = previousPrepareStackTrace\n  }\n\n  if (process.platform === 'win32') {\n    filePath = 'file:///' + path.resolve(filePath).replace(/\\\\/g, '/')\n  }\n\n  var output = CoffeeScript.compile(sourceCode, {\n    filename: filePath,\n    sourceFiles: [filePath],\n    sourceMap: true\n  })\n\n  var js = output.js\n  js += '\\n'\n  js += '//# sourceMappingURL=data:application/json;base64,'\n  js += new Buffer(output.v3SourceMap).toString('base64')\n  js += '\\n'\n  return js\n}\n"
  },
  {
    "path": "packages/client-app/src/compile-support/typescript.js",
    "content": "'use strict'\n\nvar _ = require('underscore')\nvar crypto = require('crypto')\nvar path = require('path')\n\nvar defaultOptions = {\n  target: 1,\n  module: 'commonjs',\n  sourceMap: true\n}\n\nvar TypeScriptSimple = null\nvar typescriptVersionDir = null\n\nexports.shouldCompile = function () {\n  return true\n}\n\nexports.getCachePath = function (sourceCode) {\n  if (typescriptVersionDir == null) {\n    var version = require('typescript-simple/package.json').version\n    typescriptVersionDir = path.join('ts', createVersionAndOptionsDigest(version, defaultOptions))\n  }\n\n  return path.join(\n    typescriptVersionDir,\n    crypto\n      .createHash('sha1')\n      .update(sourceCode, 'utf8')\n      .digest('hex') + '.js'\n  )\n}\n\nexports.compile = function (sourceCode, filePath) {\n  if (!TypeScriptSimple) {\n    TypeScriptSimple = require('typescript-simple').TypeScriptSimple\n  }\n\n  var options = _.defaults({filename: filePath}, defaultOptions)\n  return new TypeScriptSimple(options, false).compile(sourceCode, filePath)\n}\n\nfunction createVersionAndOptionsDigest (version, options) {\n  return crypto\n    .createHash('sha1')\n    .update('typescript', 'utf8')\n    .update('\\0', 'utf8')\n    .update(version, 'utf8')\n    .update('\\0', 'utf8')\n    .update(JSON.stringify(options), 'utf8')\n    .digest('hex')\n}\n"
  },
  {
    "path": "packages/client-app/src/components/attachment-items.jsx",
    "content": "import fs from 'fs'\nimport path from 'path'\nimport classnames from 'classnames'\nimport React, {Component, PropTypes} from 'react'\nimport ReactDOM from 'react-dom'\nimport {pickHTMLProps} from 'pick-react-known-prop'\nimport RetinaImg from './retina-img'\nimport Flexbox from './flexbox'\nimport Spinner from './spinner'\n\n\nconst propTypes = {\n  className: PropTypes.string,\n  draggable: PropTypes.bool,\n  focusable: PropTypes.bool,\n  previewable: PropTypes.bool,\n  filePath: PropTypes.string,\n  contentType: PropTypes.string,\n  download: PropTypes.shape({\n    state: PropTypes.string,\n    percent: PropTypes.number,\n  }),\n  displayName: PropTypes.string,\n  displaySize: PropTypes.string,\n  fileIconName: PropTypes.string,\n  filePreviewPath: PropTypes.string,\n  onOpenAttachment: PropTypes.func,\n  onRemoveAttachment: PropTypes.func,\n  onDownloadAttachment: PropTypes.func,\n  onAbortDownload: PropTypes.func,\n};\n\nconst defaultProps = {\n  draggable: true,\n}\n\nconst SPACE = ' '\n\nfunction ProgressBar(props) {\n  const {download} = props\n  const isDownloading = download ? download.state === 'downloading' : false;\n  if (!isDownloading) {\n    return <span />\n  }\n  const {state: downloadState, percent: downloadPercent} = download\n  const downloadProgressStyle = {\n    width: `${Math.min(downloadPercent, 97.5)}%`,\n  }\n  return (\n    <span className={`progress-bar-wrap state-${downloadState}`}>\n      <span className=\"progress-background\" />\n      <span className=\"progress-foreground\" style={downloadProgressStyle} />\n    </span>\n  )\n}\nProgressBar.propTypes = propTypes\n\n\nfunction AttachmentActionIcon(props) {\n  const {\n    download,\n    removeIcon,\n    downloadIcon,\n    retinaImgMode,\n    onAbortDownload,\n    onRemoveAttachment,\n    onDownloadAttachment,\n  } = props\n\n  const isRemovable = onRemoveAttachment != null\n  const isDownloading = download ? download.state === 'downloading' : false;\n  const actionIconName = isRemovable || isDownloading ? removeIcon : downloadIcon;\n\n  const onClickActionIcon = (event) => {\n    event.stopPropagation() // Prevent 'onOpenAttachment'\n    if (isRemovable) {\n      onRemoveAttachment()\n    } else if (isDownloading && onAbortDownload != null) {\n      onAbortDownload()\n    } else if (onDownloadAttachment != null) {\n      onDownloadAttachment()\n    }\n  }\n\n  return (\n    <div className=\"file-action-icon\" onClick={onClickActionIcon}>\n      <RetinaImg\n        name={actionIconName}\n        mode={retinaImgMode}\n      />\n    </div>\n  )\n}\nAttachmentActionIcon.propTypes = {\n  removeIcon: PropTypes.string,\n  downloadIcon: PropTypes.string,\n  retinaImgMode: PropTypes.string,\n  ...propTypes,\n}\n\n\nexport class AttachmentItem extends Component {\n  static displayName = 'AttachmentItem';\n\n  static containerRequired = false;\n\n  static propTypes = propTypes;\n\n  static defaultProps = defaultProps;\n\n  _canPreview() {\n    const {filePath, previewable} = this.props\n    return (\n      previewable &&\n      process.platform === 'darwin' &&\n      fs.existsSync(filePath)\n    )\n  }\n\n  _previewAttachment() {\n    const {filePath} = this.props\n    const currentWin = NylasEnv.getCurrentWindow()\n    currentWin.previewFile(filePath)\n  }\n\n  _onDragStart = (event) => {\n    const {contentType, filePath} = this.props\n    if (fs.existsSync(filePath)) {\n      // Note: From trial and error, it appears that the second param /MUST/ be the\n      // same as the last component of the filePath URL, or the download fails.\n      const downloadURL = `${contentType}:${path.basename(filePath)}:file://${filePath}`\n      event.dataTransfer.setData(\"DownloadURL\", downloadURL)\n      event.dataTransfer.setData(\"text/nylas-file-url\", downloadURL)\n      const fileIconImg = ReactDOM.findDOMNode(this.refs.fileIconImg)\n      const rect = fileIconImg.getBoundingClientRect()\n      const x = window.devicePixelRatio === 2 ? rect.height / 2 : rect.height\n      const y = window.devicePixelRatio === 2 ? rect.width / 2 : rect.width\n      event.dataTransfer.setDragImage(fileIconImg, x, y)\n    } else {\n      event.preventDefault()\n    }\n  };\n\n  _onOpenAttachment = () => {\n    const {onOpenAttachment} = this.props\n    if (onOpenAttachment != null) {\n      onOpenAttachment()\n    }\n  };\n\n  _onAttachmentKeyDown = (event) => {\n    if (event.key === SPACE) {\n      if (!this._canPreview()) { return; }\n      event.preventDefault()\n      this._previewAttachment()\n    }\n    if (event.key === 'Escape') {\n      const attachmentNode = ReactDOM.findDOMNode(this)\n      if (attachmentNode) {\n        attachmentNode.blur()\n      }\n    }\n  }\n\n  _onClickQuicklookIcon = (event) => {\n    event.preventDefault()\n    event.stopPropagation()\n    this._previewAttachment()\n  }\n\n  render() {\n    const {\n      download,\n      className,\n      focusable,\n      draggable,\n      displayName,\n      displaySize,\n      fileIconName,\n      filePreviewPath,\n      ...extraProps\n    } = this.props\n    const classes = classnames({\n      'nylas-attachment-item': true,\n      'file-attachment-item': true,\n      'has-preview': filePreviewPath,\n      [className]: className,\n    })\n    const style = draggable ? {WebkitUserDrag: 'element'} : null;\n    const tabIndex = focusable ? 0 : null\n    const {devicePixelRatio} = window\n\n    return (\n      <div\n        style={style}\n        className={classes}\n        tabIndex={tabIndex}\n        onKeyDown={focusable ? this._onAttachmentKeyDown : null}\n        {...pickHTMLProps(extraProps)}\n      >\n        {filePreviewPath ?\n          <div className=\"file-thumbnail-preview\">\n            <img\n              role=\"presentation\"\n              src={`file://${filePreviewPath}`}\n              style={{zoom: (1 / devicePixelRatio)}}\n            />\n          </div> :\n          null\n        }\n        <div\n          className=\"inner\"\n          draggable={draggable}\n          onDoubleClick={this._onOpenAttachment}\n          onDragStart={this._onDragStart}\n        >\n          <ProgressBar download={download} />\n          <Flexbox direction=\"row\" style={{alignItems: 'center'}}>\n            <div className=\"file-info-wrap\">\n              <RetinaImg\n                ref=\"fileIconImg\"\n                className=\"file-icon\"\n                fallback=\"file-fallback.png\"\n                mode={RetinaImg.Mode.ContentPreserve}\n                name={fileIconName}\n              />\n              <span className=\"file-name\">{displayName}</span>\n              <span className=\"file-size\">{displaySize ? `(${displaySize})` : ''}</span>\n              {this._canPreview() ?\n                <RetinaImg\n                  className=\"quicklook-icon\"\n                  name=\"attachment-quicklook.png\"\n                  mode={RetinaImg.Mode.ContentIsMask}\n                  onClick={this._onClickQuicklookIcon}\n                /> :\n                null\n              }\n            </div>\n            <AttachmentActionIcon\n              {...this.props}\n              removeIcon=\"remove-attachment.png\"\n              downloadIcon=\"icon-attachment-download.png\"\n              retinaImgMode={RetinaImg.Mode.ContentIsMask}\n            />\n          </Flexbox>\n        </div>\n      </div>\n    )\n  }\n}\n\n\nexport class ImageAttachmentItem extends Component {\n  static displayName = 'ImageAttachmentItem';\n\n  static propTypes = {\n    imgProps: PropTypes.object,\n    ...propTypes,\n  };\n\n  static defaultProps = defaultProps;\n\n  static containerRequired = false;\n\n  _onOpenAttachment = () => {\n    const {onOpenAttachment} = this.props\n    if (onOpenAttachment != null) {\n      onOpenAttachment()\n    }\n  };\n\n  _onImgLoaded = () => {\n    // on load, modify our DOM just /slightly/. This causes DOM mutation listeners\n    // watching the DOM to trigger. This is a good thing, because the image may\n    // change dimensions. (We use this to reflow the draft body when this component\n    // is within an OverlaidComponent)\n    const el = ReactDOM.findDOMNode(this);\n    if (el) {\n      el.classList.add('loaded');\n    }\n  }\n\n  renderImage() {\n    const {download, filePath, draggable} = this.props\n    if (download && download.percent <= 5) {\n      return (\n        <div style={{width: \"100%\", height: \"100px\"}}>\n          <Spinner visible />\n        </div>\n      )\n    }\n    const src = download && download.percent < 100 ? `${filePath}?percent=${download.percent}` : filePath;\n    return (\n      <img draggable={draggable} src={src} role=\"presentation\" onLoad={this._onImgLoaded} />\n    )\n  }\n\n  render() {\n    const {className, displayName, download, ...extraProps} = this.props\n    const classes = `nylas-attachment-item image-attachment-item ${className || ''}`\n    return (\n      <div className={classes} {...pickHTMLProps(extraProps)}>\n        <div>\n          <ProgressBar download={download} />\n          <AttachmentActionIcon\n            {...this.props}\n            removeIcon=\"image-cancel-button.png\"\n            downloadIcon=\"image-download-button.png\"\n            retinaImgMode={RetinaImg.Mode.ContentPreserve}\n            onAbortDownload={null}\n          />\n          <div className=\"file-preview\" onDoubleClick={this._onOpenAttachment}>\n            <div className=\"file-name-container\">\n              <div className=\"file-name\">{displayName}</div>\n            </div>\n            {this.renderImage()}\n          </div>\n        </div>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/billing-modal.jsx",
    "content": "import React from 'react'\nimport Webview from './webview'\nimport Actions from '../flux/actions'\nimport IdentityStore from '../flux/stores/identity-store'\n\nexport default class BillingModal extends React.Component {\n  static propTypes = {\n    upgradeUrl: React.PropTypes.string,\n    source: React.PropTypes.string,\n  }\n\n  constructor(props = {}) {\n    super(props);\n    this.state = {\n      src: props.upgradeUrl,\n    }\n  }\n\n  componentWillMount() {\n    if (!this.state.src) {\n      IdentityStore.fetchSingleSignOnURL(\"/payment?embedded=true\").then((url) => {\n        if (!this._mounted) return;\n        this.setState({src: url})\n      })\n    }\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n  }\n\n  /**\n   * The Billing modal can get closed for any number of reasons. The user\n   * may push escape, click continue below, or click outside of the area.\n   * Regardless of the method, Actions.closeModal will fire. The\n   * FeatureUsageStore listens for Actions.closeModal and looks at the\n   * IdentityStore.hasProAccess to determine if the user succesffully paid\n   * us or not.\n   */\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  _onDidFinishLoad = (webview) => {\n    /**\n     * Ahh webviews…\n     *\n     * First we wait for the payment success screen to pop up and do a\n     * quick assertion on the data that's there.\n     *\n     * We then start listening to the continue button, using the console\n     * as a communication bus.\n     */\n    const receiveUserInfo = `\n      var a = document.querySelector('#payment-success-data');\n      result = a ? a.innerText : null;\n    `;\n    webview.executeJavaScript(receiveUserInfo, false, async (result) => {\n      if (!result) return;\n      if (result !== IdentityStore.identityId()) {\n        NylasEnv.reportError(new Error(\"billing.nylas.com/payment_success did not have a valid #payment-success-data field\"))\n      }\n      const listenForContinue = `\n        var el = document.querySelector('#continue-btn');\n        if (el) {el.addEventListener('click', function(event) {console.log(\"continue clicked\")})}\n      `;\n      webview.executeJavaScript(listenForContinue);\n      webview.addEventListener(\"console-message\", (e) => {\n        if (e.message === \"continue clicked\") {\n          // See comment on componentWillUnmount\n          Actions.closeModal()\n        }\n      })\n      await IdentityStore.asyncRefreshIdentity();\n    });\n\n    /**\n     * If we see any links on the page, we should open them in new\n     * windows\n     */\n    const openExternalLink = `\n      var el = document.querySelector('a');\n      if (el) {el.addEventListener('click', function(event) {console.log(this.href); event.preventDefault(); return false;})}\n    `;\n    webview.executeJavaScript(openExternalLink);\n  }\n\n  render() {\n    return (\n      <div className=\"modal-wrap billing-modal\">\n        <Webview src={this.state.src} onDidFinishLoad={this._onDidFinishLoad} />\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/bolded-search-result.jsx",
    "content": "import React from 'react'\nimport Utils from '../flux/models/utils'\n\nexport default function BoldedSearchResult({query = \"\", value = \"\"} = {}) {\n  const searchTerm = (query || \"\").trim()\n\n  if (searchTerm.length === 0) return <span>{value}</span>;\n\n  const re = Utils.wordSearchRegExp(searchTerm)\n  const parts = value.split(re).map((part) => {\n    // The wordSearchRegExp looks for a leading non-word character to\n    // deterine if it's a valid place to search. As such, we need to not\n    // include that leading character as part of our match.\n    if (re.test(part)) {\n      if (/\\W/.test(part[0])) {\n        return <span>{part[0]}<strong>{part.slice(1)}</strong></span>\n      }\n      return <strong>{part}</strong>\n    }\n    return part\n  });\n  return <span className=\"search-result\">{parts}</span>;\n}\nBoldedSearchResult.propTypes = {\n  query: React.PropTypes.string,\n  value: React.PropTypes.string,\n}\n"
  },
  {
    "path": "packages/client-app/src/components/button-dropdown.cjsx",
    "content": "RetinaImg = require('./retina-img').default\n{Utils} = require 'nylas-exports'\nclassnames = require 'classnames'\n\nReact = require 'react'\nReactDOM = require 'react-dom'\nclass ButtonDropdown extends React.Component\n  @displayName: \"ButtonDropdown\"\n  @propTypes:\n    primaryItem: React.PropTypes.element\n    primaryClick: React.PropTypes.func\n    bordered: React.PropTypes.bool\n    menu: React.PropTypes.element\n    style: React.PropTypes.object\n    closeOnMenuClick: React.PropTypes.bool\n\n  @defaultProps:\n    style: {}\n\n  constructor: (@props) ->\n    @state = open: false\n\n  render: =>\n    classes = classnames\n      'button-dropdown': true\n      'open open-up': @state.open is 'up'\n      'open open-down': @state.open is 'down'\n      'bordered': @props.bordered isnt false\n\n    if @props.primaryClick\n      <div ref=\"button\" onBlur={@_onBlur} tabIndex={-1} className={\"#{classes} #{@props.className ? ''}\"} style={@props.style}>\n        <div className=\"primary-item\"\n             title={@props.primaryTitle ? \"\"}\n             onClick={@props.primaryClick}>\n          {@props.primaryItem}\n        </div>\n        <div className=\"secondary-picker\" onClick={@toggleDropdown}>\n          <RetinaImg name={\"icon-thread-disclosure.png\"} mode={RetinaImg.Mode.ContentIsMask}/>\n        </div>\n        <div ref=\"secondaryItems\" className=\"secondary-items\" onMouseDown={@_onMenuClick}>\n          {@props.menu}\n        </div>\n      </div>\n    else\n      <div ref=\"button\" onBlur={@_onBlur} tabIndex={-1} className={\"#{classes} #{@props.className ? ''}\"} style={@props.style}>\n        <div className=\"only-item\"\n             title={@props.primaryTitle ? \"\"}\n             onClick={@toggleDropdown}>\n          {@props.primaryItem}\n          <RetinaImg name={\"icon-thread-disclosure.png\"} style={marginLeft:12} mode={RetinaImg.Mode.ContentIsMask}/>\n        </div>\n        <div ref=\"secondaryItems\" className=\"secondary-items left\" onMouseDown={@_onMenuClick}>\n          {@props.menu}\n        </div>\n      </div>\n\n  toggleDropdown: =>\n    if @state.open isnt false\n      @setState(open: false)\n    else\n      buttonBottom = ReactDOM.findDOMNode(@).getBoundingClientRect().bottom\n      openHeight = ReactDOM.findDOMNode(@refs.secondaryItems).getBoundingClientRect().height\n      if buttonBottom + openHeight > window.innerHeight\n        @setState(open: 'up')\n      else\n        @setState(open: 'down')\n\n  _onMenuClick: (event) =>\n    if @props.closeOnMenuClick\n      @setState open: false\n\n  _onBlur: (event) =>\n    target = event.nativeEvent.relatedTarget\n    if target? and ReactDOM.findDOMNode(@refs.button).contains(target)\n      return\n    @setState(open: false)\n\nmodule.exports = ButtonDropdown\n"
  },
  {
    "path": "packages/client-app/src/components/code-snippet.jsx",
    "content": "import {React} from 'nylas-exports';\n\nexport default function CodeSnippet(props) {\n  return (\n    <div className={props.className}>\n      {props.intro}\n      <br /><br />\n      <textarea disabled value={props.code} />\n    </div>\n  )\n}\n\nCodeSnippet.propTypes = {\n  intro: React.PropTypes.string,\n  code: React.PropTypes.string,\n  className: React.PropTypes.string,\n}\n"
  },
  {
    "path": "packages/client-app/src/components/config-prop-container.jsx",
    "content": "import React from 'react';\n\nexport default class ConfigPropContainer extends React.Component {\n  static displayName = 'ConfigPropContainer'\n\n  constructor(props) {\n    super(props);\n    this.state = this.getStateFromStores();\n  }\n\n  componentDidMount() {\n    this.subscription = NylasEnv.config.onDidChange(null, () => {\n      this.setState(this.getStateFromStores());\n    });\n  }\n\n  componentWillUnmount() {\n    if (this.subscription) {\n      this.subscription.dispose();\n    }\n  }\n\n  getStateFromStores() {\n    return {\n      config: this.getConfigWithMutators(),\n    };\n  }\n\n  getConfigWithMutators() {\n    return Object.assign(NylasEnv.config.get(), {\n      get: (key) => {\n        return NylasEnv.config.get(key);\n      },\n      set: (key, value) => {\n        NylasEnv.config.set(key, value);\n      },\n      toggle: (key) => {\n        NylasEnv.config.set(key, !NylasEnv.config.get(key));\n      },\n      contains: (key, val) => {\n        const vals = NylasEnv.config.get(key);\n        return (vals && vals instanceof Array) ? vals.includes(val) : false;\n      },\n      toggleContains: (key, val) => {\n        let vals = NylasEnv.config.get(key);\n        if (!vals || !(vals instanceof Array)) {\n          vals = [];\n        }\n        if (vals.includes(val)) {\n          NylasEnv.config.set(key, vals.filter((v) => v !== val));\n        } else {\n          NylasEnv.config.set(key, vals.concat([val]));\n        }\n      },\n    });\n  }\n\n  render() {\n    return React.cloneElement(this.props.children, {\n      config: this.state.config,\n      configSchema: NylasEnv.config.getSchema('core'),\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/blockquote-manager.es6",
    "content": "/* eslint no-cond-assign: 0 */\nimport { DOMUtils, ContenteditableExtension } from 'nylas-exports';\n\nexport default class BlockquoteManager extends ContenteditableExtension {\n  static keyCommandHandlers() {\n    return {\n      \"contenteditable:quote\": this._onCreateBlockquote,\n    };\n  }\n\n  static onKeyDown({editor, event}) {\n    if (event.key === \"Backspace\") {\n      if (this._isInBlockquote(editor) && this._isAtStartOfLine(editor)) {\n        editor.outdent();\n        event.preventDefault();\n      }\n    }\n  }\n\n  static _onCreateBlockquote({editor}) {\n    editor.formatBlock(\"BLOCKQUOTE\");\n  }\n\n  static _isInBlockquote(editor) {\n    const sel = editor.currentSelection();\n    if (!sel.isCollapsed) {\n      return false;\n    }\n    return DOMUtils.closest(sel.anchorNode, \"blockquote\") != null;\n  }\n\n  static _isAtStartOfLine(editor) {\n    const sel = editor.currentSelection();\n    if (!sel.anchorNode) { return false; }\n    if (!sel.isCollapsed) { return false; }\n    if (sel.anchorOffset !== 0) { return false; }\n\n    return this._ancestorRelativeLooksLikeBlock(sel.anchorNode);\n  }\n\n  static _ancestorRelativeLooksLikeBlock(node) {\n    if (DOMUtils.looksLikeBlockElement(node)) {\n      return true;\n    }\n\n    let sibling = node;\n    while (sibling = sibling.previousSibling) {\n      if (DOMUtils.looksLikeBlockElement(sibling)) {\n        return true;\n      }\n\n      if (DOMUtils.looksLikeNonEmptyNode(sibling)) {\n        return false;\n      }\n    }\n\n    // never found block level element\n    return this._ancestorRelativeLooksLikeBlock(node.parentNode);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/clipboard-service.es6",
    "content": "/* eslint global-require: 0 */\nimport {\n  InlineStyleTransformer,\n  SanitizeTransformer,\n  RegExpUtils,\n  Utils,\n} from 'nylas-exports';\n\nimport ContenteditableService from './contenteditable-service';\n\nexport default class ClipboardService extends ContenteditableService {\n  constructor(...args) {\n    super(...args);\n    this.onFilePaste = this.props.onFilePaste;\n  }\n\n  setData(...args) {\n    super.setData(...args);\n    this.onFilePaste = this.props.onFilePaste;\n  }\n\n  eventHandlers() {\n    return {\n      onPaste: this.onPaste,\n    };\n  }\n\n  onPaste = (event) => {\n    if (event.clipboardData.items.length === 0) {\n      return;\n    }\n    event.preventDefault();\n\n    // If the pasteboard has a file on it, stream it to a teporary\n    // file and fire our `onFilePaste` event.\n    const item = event.clipboardData.items[0];\n\n    if (item.kind === 'file') {\n      const temp = require('temp');\n      const path = require('path');\n      const fs = require('fs');\n      const blob = item.getAsFile();\n      const ext = {\n        'image/png': '.png',\n        'image/jpg': '.jpg',\n        'image/tiff': '.tiff',\n      }[item.type] || '';\n\n      const reader = new FileReader();\n      reader.addEventListener('loadend', () => {\n        const buffer = new Buffer(new Uint8Array(reader.result));\n        const tmpFolder = temp.path('-nylas-attachment');\n        const tmpPath = path.join(tmpFolder, `Pasted File${ext}`);\n        fs.mkdir(tmpFolder, () => {\n          fs.writeFile(tmpPath, buffer, () => {\n            if (this.onFilePaste) {\n              this.onFilePaste(tmpPath);\n            }\n          });\n        });\n      });\n      reader.readAsArrayBuffer(blob);\n    } else {\n      const {input, mimetype} = this._getBestRepresentation(event.clipboardData);\n\n      if (mimetype === 'text/plain') {\n        const encoded = Utils.encodeHTMLEntities(input);\n        const htmlified = encoded.replace(/[\\r\\n]|&#1[03];/g, \"<br/>\").replace(/\\s\\s/g, \" &nbsp;\");\n        document.execCommand(\"insertHTML\", false, htmlified);\n      } else if (mimetype === 'text/html') {\n        this._sanitizeHTMLInput(input).then(cleanHtml => document.execCommand(\"insertHTML\", false, cleanHtml));\n      } else {\n        // Do nothing. No appropriate format is available\n      }\n    }\n  }\n\n  _getBestRepresentation(clipboardData) {\n    for (const mimetype of [\"text/html\", \"text/plain\"]) {\n      const data = clipboardData.getData(mimetype) || \"\";\n      if (data.length > 0) {\n        return {input: data, mimetype};\n      }\n    }\n\n    return {input: null, mimetype: null};\n  }\n\n  // This is used when pasting text in\n  _sanitizeHTMLInput(rawInput) {\n    // Check if we are pasting any of our tracked links\n    const withoutTracking = rawInput.replace(RegExpUtils.trackedLinkRegex(), (match, p1) =>\n      decodeURIComponent(p1)\n    );\n\n    return InlineStyleTransformer.run(withoutTracking)\n    .then((inlined) => {\n      return SanitizeTransformer.run(inlined, SanitizeTransformer.Preset.Permissive)\n      .then((sanitized) => {\n        return Promise.resolve(\n          sanitized\n          // We never want more then 2 line breaks in a row.\n          // https://regex101.com/r/gF6bF4/4\n          .replace(/(<br\\s*\\/?>\\s*){3,}/g, \"<br/><br/>\")\n\n          // We never want to keep leading and trailing <brs>, since the user\n          // would have started a new paragraph themselves if they wanted space\n          // before what they paste.\n          // BAD:    \"<p>begins at<br>12AM</p>\" => \"<br><br>begins at<br>12AM<br><br>\"\n          // Better: \"<p>begins at<br>12AM</p>\" => \"begins at<br>12\"\n          .replace(/^(<br ?\\/>)+/, '')\n          .replace(/(<br ?\\/>)+$/, '')\n        );\n      });\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/contenteditable-service.es6",
    "content": "// These are additions to the Contenteditable component that are tightly\n// coupled to the props, state, and innerState of the parent component.\n\n// They're designed to better separate concerns of the Contenteditable\nexport default class ContenteditableService {\n  constructor({data, methods}) {\n    this.data = data;\n    this.methods = methods;\n    ({props: this.props, state: this.state, innerState: this.innerState} = this.data);\n    ({setInnerState: this.setInnerState, dispatchEventToExtensions: this.dispatchEventToExtensions} = this.methods);\n  }\n\n  setData({props, state, innerState}) {\n    this.props = props;\n    this.state = state;\n    this.innerState = innerState;\n  }\n\n  eventHandlers() {\n    return {};\n  }\n\n  teardown() {\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/contenteditable.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\n\n{Utils, DOMUtils} = require 'nylas-exports'\n{KeyCommandsRegion} = require 'nylas-component-kit'\nFloatingToolbar = require './floating-toolbar'\n\nEditorAPI = require './editor-api'\nExtendedSelection = require './extended-selection'\n\nTabManager = require('./tab-manager').default\nLinkManager = require('./link-manager').default\nListManager = require('./list-manager').default\nMouseService = require('./mouse-service').default\nDOMNormalizer = require './dom-normalizer'\nClipboardService = require('./clipboard-service').default\nBlockquoteManager = require('./blockquote-manager').default\nToolbarButtonManager = require('./toolbar-button-manager').default\nEmphasisFormattingExtension = require('./emphasis-formatting-extension').default\nParagraphFormattingExtension = require('./paragraph-formatting-extension').default\n\n###\nPublic: A modern React-compatible contenteditable\n\nThis <Contenteditable /> component is fully React-compatible and behaves\nlike a standard controlled input.\n\n```javascript\ngetInitialState: function() {\n  return {value: '<strong>Hello!</strong>'};\n},\nhandleChange: function(event) {\n  this.setState({value: event.target.value});\n},\nrender: function() {\n  var value = this.state.value;\n  return <Contenteditable type=\"text\" value={value} onChange={this.handleChange} />;\n}\n```\n###\nclass Contenteditable extends React.Component\n  @displayName: \"Contenteditable\"\n\n  @IgnoreMutationClassName: 'ignore-mutations'\n\n  @propTypes:\n    # The current html state, as a string, of the contenteditable.\n    value: React.PropTypes.string\n\n    # Handlers\n    onChange: React.PropTypes.func.isRequired\n    onFilePaste: React.PropTypes.func\n\n    # A list of objects that extend {ContenteditableExtension}\n    extensions: React.PropTypes.array\n\n    spellcheck: React.PropTypes.bool\n\n    floatingToolbar: React.PropTypes.bool\n\n  @defaultProps:\n    extensions: []\n    spellcheck: true\n    floatingToolbar: true\n    onSelectionRestored: =>\n\n  coreServices: [MouseService, ClipboardService]\n\n  coreExtensions: [\n    ToolbarButtonManager\n    ListManager\n    TabManager\n    EmphasisFormattingExtension\n    ParagraphFormattingExtension\n    LinkManager\n    BlockquoteManager\n    DOMNormalizer\n  ]\n\n  ######################################################################\n  ########################### Public Methods ###########################\n  ######################################################################\n\n  ### Public: perform an editing operation on the Contenteditable\n\n  - `editingFunction` A function to mutate the DOM and\n  {ExtendedSelection}. It gets passed an {EditorAPI} object that contains\n  mutating methods.\n\n  If the current selection at the time of running the extension is out of\n  scope, it will be set to the last saved state. This ensures extensions\n  operate on a valid {ExtendedSelection}.\n\n  Edits made within the editing function will eventually fire _onDOMMutated\n  ###\n  atomicEdit: (editingFunction, extraArgsObj={}) =>\n    @_teardownNonMutationListeners()\n\n    editor = new EditorAPI(@_editableNode())\n\n    if not editor.currentSelection().isInScope() and extraArgsObj.methodName isnt 'onBlur'\n      @_restoreSelection()\n\n    argsObj = _.extend(extraArgsObj, {editor})\n\n    try\n      editingFunction(argsObj)\n    catch error\n      NylasEnv.reportError(error)\n\n    @_setupNonMutationListeners()\n\n  focus: => @_editableNode().focus()\n\n  setSelection: (selection) =>\n    @setInnerState\n      exportedSelection: selection\n      previousExportedSelection: @innerState.exportedSelection\n    @_restoreSelection()\n\n  ######################################################################\n  ########################## React Lifecycle ###########################\n  ######################################################################\n\n  constructor: (@props) ->\n    @state = {}\n    @innerState = {\n      dragging: false\n      doubleDown: false\n      hoveringOver: false # see {MouseService}\n      editableNode: null\n      exportedSelection: null\n      previousExportedSelection: null\n    }\n    @_mutationObserver = new MutationObserver(@_onDOMMutated)\n\n  componentWillMount: =>\n    @_setupServices()\n\n  componentDidMount: =>\n    @setInnerState editableNode: @_editableNode()\n    @_setupNonMutationListeners()\n    @_setupEditingActionListeners()\n    @_mutationObserver.observe(@_editableNode(), @_mutationConfig())\n\n  # When we have a composition event in progress, we should not update\n  # because otherwise our composition event will be blown away.\n  shouldComponentUpdate: (nextProps, nextState) ->\n    not @_inCompositionEvent and\n    (not Utils.isEqualReact(nextProps, @props) or\n     not Utils.isEqualReact(nextState, @state))\n\n  componentDidUpdate: =>\n    if @_shouldRestoreSelectionOnUpdate()\n      @_restoreSelection()\n      @_notifyOfSelectionRestoration()\n    @_refreshServices()\n    @_setupEditingActionListeners()\n    @_mutationObserver.disconnect()\n    @_mutationObserver.observe(@_editableNode(), @_mutationConfig())\n    @setInnerState editableNode: @_editableNode()\n\n  componentWillUnmount: =>\n    @_mutationObserver.disconnect()\n    @_teardownNonMutationListeners()\n    @_teardownEditingActionListeners()\n    @_teardownServices()\n\n  setInnerState: (innerState={}) =>\n    return if _.isMatch(@innerState, innerState)\n    @innerState = _.extend @innerState, innerState\n    if @_broadcastInnerStateToToolbar\n      @refs[\"toolbarController\"]?.componentWillReceiveInnerProps(@innerState)\n    @_refreshServices()\n\n  _setupServices: ->\n    @_services = @coreServices.map (Service) =>\n      new Service\n        data: {@props, @state, @innerState}\n        methods: {@setInnerState, @dispatchEventToExtensions}\n\n  _refreshServices: ->\n    service.setData({@props, @state, @innerState}) for service in @_services\n\n  _teardownServices: ->\n    service.teardown() for service in @_services\n\n\n  ######################################################################\n  ############################## Render ################################\n  ######################################################################\n\n  render: =>\n    <KeyCommandsRegion className=\"contenteditable-container\"\n                       localHandlers={@_keymapHandlers()}>\n      {@_renderFloatingToolbar()}\n\n      <div className=\"contenteditable no-open-link-events\"\n           ref=\"contenteditable\"\n           contentEditable\n           spellCheck={false}\n           placeholder={@props.placeholder}\n           dangerouslySetInnerHTML={__html: @props.value}\n           {...@_eventHandlers()}></div>\n    </KeyCommandsRegion>\n\n  _renderFloatingToolbar: ->\n    return unless @props.floatingToolbar\n    <FloatingToolbar\n        ref=\"toolbarController\"\n        atomicEdit={@atomicEdit}\n        extensions={@_extensions()} />\n\n  _editableNode: =>\n    ReactDOM.findDOMNode(@refs.contenteditable)\n\n\n  ######################################################################\n  ########################### Listener Setup ###########################\n  ######################################################################\n\n  _eventHandlers: =>\n    handlers = {}\n    _.extend(handlers, service.eventHandlers()) for service in @_services\n\n    # NOTE: See {MouseService} for more handlers\n    handlers = _.extend handlers,\n      onBlur: @_onBlur\n      onFocus: @_onFocus\n      onKeyDown: @_onKeyDown\n      onCompositionEnd: @_onCompositionEnd\n      onCompositionStart: @_onCompositionStart\n    return handlers\n\n  # This extracts extensions keymap handlers and binds them to be called\n  # through `atomicEdit`. This exposes the `{editor, event}` props to any\n  # keyCommandHandlers callbacks.\n  _boundExtensionKeymapHandlers: ->\n    keymapHandlers = {}\n    @_extensions().forEach (extension) =>\n      return unless _.isFunction(extension.keyCommandHandlers)\n      try\n        extensionHandlers = extension.keyCommandHandlers.call(extension)\n        _.each extensionHandlers, (handler, command) =>\n          keymapHandlers[command] = (event) =>\n            @atomicEdit(handler, {event})\n      catch error\n        NylasEnv.reportError(error)\n    return keymapHandlers\n\n  # NOTE: Keymaps are now broken apart into individual extensions. See the\n  # `EmphasisFormattingExtension`, `ParagraphFormattingExtension`,\n  # `ListManager`, and `LinkManager` for examples of extensions listening\n  # to keymaps.\n  _keymapHandlers: ->\n    defaultKeymaps = {}\n    return _.extend(defaultKeymaps, @_boundExtensionKeymapHandlers())\n\n  _setupNonMutationListeners: =>\n    @_broadcastInnerStateToToolbar = true\n    document.addEventListener(\"selectionchange\", @_saveSelection)\n    @_editableNode().addEventListener('contextmenu', @_onShowContextMenu)\n\n  _teardownNonMutationListeners: =>\n    @_broadcastInnerStateToToolbar = false\n    document.removeEventListener(\"selectionchange\", @_saveSelection)\n    @_editableNode().removeEventListener('contextmenu', @_onShowContextMenu)\n\n  _setupEditingActionListeners: =>\n    if @editingActionUnsubscribers?.length > 0\n      editingActionUnsubscriber() for editingActionUnsubscriber in @editingActionUnsubscribers\n    @editingActionUnsubscribers = []\n\n    @_extensions().forEach (ext) =>\n      try\n        editingActions = ext.editingActions?() ? []\n        editingActions.forEach ({action, callback}) =>\n          @editingActionUnsubscribers.push(action.listen((actionArg) =>\n            @atomicEdit(callback, {actionArg})\n          ))\n      catch error\n        NylasEnv.reportError(error)\n\n  _teardownEditingActionListeners: =>\n    for editingActionUnsubscriber in @editingActionUnsubscribers\n      editingActionUnsubscriber()\n\n  # https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver\n  _mutationConfig: ->\n    subtree: true\n    childList: true\n    attributes: true\n    characterData: true\n    attributeOldValue: true\n    characterDataOldValue: true\n\n\n  ######################################################################\n  ########################### Event Handlers ###########################\n  ######################################################################\n\n  # Every time the contents of the contenteditable DOM node change due to a user\n  # action, the `_onDOMMutated` event gets fired.\n  #\n  # If we are in the middle of an `atomic` change transaction, we ignore\n  # those changes.\n  #\n  # If any target element of the mutations contains `IgnoreMutationClassName` in\n  # its className, we will also ignore the mutations. This is order to support\n  # extensions that might imperatively want to mutate the contenteditable\n  # wihtout it being due to a direct user action, i.e. without wanting to\n  # trigger this callback. For example, `OverlaidComponents` will mutate the\n  # dimensions of its anchor elements without the user explicitly doing so.\n  #\n  # At all other times we take the change, apply various filters to the\n  # new content, then notify our parent that the content has been updated.\n  #\n  _onDOMMutated: (mutations) =>\n    return unless mutations and mutations.length > 0\n\n    runCallbacks = true\n    for mutation in mutations\n      if mutation.target?.className?.includes(Contenteditable.IgnoreMutationClassName)\n        runCallbacks = false\n\n    @_mutationObserver.disconnect()\n    try\n      @setInnerState dragging: false if @innerState.dragging\n      @setInnerState doubleDown: false if @innerState.doubleDown\n      @_broadcastInnerStateToToolbar = false\n\n      if runCallbacks\n        @_runCallbackOnExtensions(\"onContentChanged\", {mutations})\n\n      # NOTE: The DOMNormalizer should be the last extension to run. This\n      # will ensure that when we extract our innerHTML and re-set it during\n      # the next render the contents should look identical.\n      #\n      # Also, remember that our selection listeners have been turned off.\n      # It's very likely that one of our callbacks mutated the DOM and the\n      # selection. We need to be sure to re-save the selection.\n      @_saveSelection()\n\n      @props.onChange(target: {value: @_editableNode().innerHTML})\n\n      @_broadcastInnerStateToToolbar = true\n    finally\n      @_mutationObserver.observe(@_editableNode(), @_mutationConfig())\n\n  _onBlur: (event) =>\n    @setInnerState dragging: false\n    return if @_editableNode().parentElement.contains event.relatedTarget\n    @dispatchEventToExtensions(\"onBlur\", event)\n\n  _onFocus: (event) =>\n    @dispatchEventToExtensions(\"onFocus\", event)\n\n  _onKeyDown: (event) =>\n    @dispatchEventToExtensions(\"onKeyDown\", event)\n\n  _onDragOver: (event) =>\n    @dispatchEventToExtensions(\"onDragOver\", event)\n\n  _onDrop: (event) =>\n    @dispatchEventToExtensions(\"onDrop\", event)\n\n  _shouldAcceptDrop: (event) =>\n    for extension in [].concat(@props.extensions, @coreExtensions)\n      return true if extension.shouldAcceptDrop?.call(extension, {event})\n    return false\n\n  # We must set the `inCompositionEvent` flag in addition to tearing down\n  # the selecton listeners. While the composition event is in progress, we\n  # want to ignore any input events we get.\n  #\n  # It is also possible for a composition event to end and then\n  # immediately start a new composition event. This happens when two\n  # composition event-triggering characters are pressed twice in a row.\n  # When the first composition event ends, the `_onDOMMutated` method fires (as\n  # it's supposed to) and sends off an asynchronous update request when we\n  # `_saveNewHtml`. Before that comes back via new props, the 2nd\n  # composition event starts. Without the `_inCompositionEvent` flag\n  # stopping the re-render, the asynchronous update request will cause us\n  # to re-render and blow away our newly started 2nd composition event.\n  #\n  # While we're in a composition event it's important that `_onDOMMutated`\n  # still get fired so the selection gets updated and the latest body\n  # saved for the next render. However, we want to disable any plugins\n  # since they may inadvertently kill the composition editor by mutating\n  # the DOM.\n  _onCompositionStart: =>\n    @_inCompositionEvent = true\n    @_teardownNonMutationListeners()\n\n  _onCompositionEnd: =>\n    @_inCompositionEvent = false\n    @_setupNonMutationListeners()\n\n  _onShowContextMenu: (event) =>\n    @refs[\"toolbarController\"]?.forceClose()\n    event.preventDefault()\n\n    {remote} = require('electron')\n    {Menu, MenuItem} = remote\n\n    menu = new Menu()\n\n    @dispatchEventToExtensions(\"onShowContextMenu\", event, {menu})\n    menu.append(new MenuItem({ label: 'Cut', role: 'cut'}))\n    menu.append(new MenuItem({ label: 'Copy', role: 'copy'}))\n    menu.append(new MenuItem({ label: 'Paste', role: 'paste'}))\n    menu.append(new MenuItem({ label: 'Paste and Match Style', role: 'pasteandmatchstyle'}))\n    menu.popup(remote.getCurrentWindow())\n\n\n  ######################################################################\n  ############################ Extensions ##############################\n  ######################################################################\n\n  _extensions: ->\n    @props.extensions.concat(@coreExtensions)\n\n  _runCallbackOnExtensions: (method, argsObj={}) =>\n    for extension in @_extensions()\n      @_runExtensionMethod(extension, method, argsObj)\n\n  # Will execute the event handlers on each of the registerd and core\n  # extensions In this context, event.preventDefault and\n  # event.stopPropagation don't refer to stopping default DOM behavior or\n  # prevent event bubbling through the DOM, but rather prevent our own\n  # Contenteditable default behavior, and preventing other extensions from\n  # being called. If any of the extensions calls event.preventDefault()\n  # it will prevent the default behavior for the Contenteditable, which\n  # basically means preventing the core extension handlers from being\n  # called.  If any of the extensions calls event.stopPropagation(), it\n  # will prevent any other extension handlers from being called.\n  dispatchEventToExtensions: (method, event, args={}) =>\n    argsObj = _.extend(args, {event})\n    for extension in @props.extensions\n      break if event?.isPropagationStopped()\n      @_runExtensionMethod(extension, method, argsObj)\n\n    return if event?.defaultPrevented or event?.isPropagationStopped()\n    for extension in @coreExtensions\n      break if event?.isPropagationStopped()\n      @_runExtensionMethod(extension, method, argsObj)\n\n  _runExtensionMethod: (extension, method, argsObj={}) =>\n    return if @_inCompositionEvent\n    return if not extension[method]?\n    editingFunction = extension[method].bind(extension)\n    argsObj = _.extend(argsObj, {methodName: method})\n    @atomicEdit(editingFunction, argsObj)\n\n\n  ######################################################################\n  ############################# Selection ##############################\n  ######################################################################\n  # Saving and restoring a selection is difficult with React.\n  #\n  # React only handles Input and Textarea elements:\n  # https://github.com/facebook/react/blob/master/src/browser/ui/ReactInputSelection.js\n  # This is because they expose a very convenient `selectionStart` and\n  # `selectionEnd` integer.\n  #\n  # Contenteditable regions are trickier. They require the more\n  # sophisticated `Range` and `Selection` APIs. We have an\n  # {ExtendedSelection} class which is a wrapper around the native DOM\n  # Selection API. This exposes convenience methods for manipulating the\n  # Selection object.\n  #\n  # Range docs:\n  # http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html\n  #\n  # Selection API docs:\n  # http://www.w3.org/TR/selection-api/#dfn-range\n  #\n  # A Contenteditable region can have arbitrary html inside of it. This\n  # means that a selection start point can be some node (the `anchorNode`)\n  # and its end point can be a completely different node (the `focusNode`)\n  #\n  # When React re-renders, all of the DOM nodes may change. They may\n  # look exactly the same, but have different object references.\n  #\n  # This means that your old references to `anchorNode` and `focusNode`\n  # may be bad and no longer in scope or painted.\n  #\n  # In order to restore the selection properly we need to re-find the\n  # equivalent `anchorNode` and `focusNode`. Luckily we can use the\n  # `isEqualNode` method to get a shallow comparison of the nodes.\n  #\n  # Unfortunately it's possible for `isEqualNode` to match more than one\n  # node since two nodes may look very similar.\n  #\n  # To fix this we need to keep track of the original indices to determine\n  # which node is most likely the matching one.\n  #\n  # http://www.w3.org/TR/selection-api/#selectstart-event\n\n  ## TODO DEPRECATE ME: This is only necessary because Undo/Redo is still\n  #part of the composer and not a core part of the Contenteditable.\n  getCurrentSelection: => @innerState.exportedSelection\n  getPreviousSelection: => @innerState.previousExportedSelection\n\n  # Every time the selection changes we save its state.\n  #\n  # In an ideal world, the selection state, much like the body, would\n  # behave like any other controlled React input: onchange we'd notify our\n  # parent, they'd update our props, and we'd re-render.\n  #\n  # Unfortunately, Selection is not something React natively keeps track\n  # of in its virtual DOM, the performance would be terrible if we\n  # re-rendered on every selection change (think about dragging a\n  # selection), and having every user of `<Contenteditable>` need to\n  # remember to deal with, save, and set the Selection object is a pain.\n  #\n  # To counter this we save local instance copies of the Selection.\n  #\n  # First of all we wrap the native Selection object in an\n  # [ExtendedSelection} object. This is a pure extension and has all\n  # standard methods.\n  #\n  # We then save out 3 types of selections on `innerState` for us to use\n  # later:\n  #\n  # 1. `selectionSnapshot` - This is accessed by any sub-components of\n  # the Contenteditable such as the `<FloatingToolbar>` and its\n  # extensions.\n  #\n  # It is slightly different from an `exportedSelection` in that the\n  # anchorNode property points to an attached DOM reference and not the\n  # clone of a node. This is necessary for extensions to be able to\n  # traverse the actual current DOM from the anchorNode. The\n  # `exportedSelection`'s, cloned nodes don't have parentNOdes.\n  #\n  # This is crucially not a reference to the `rawSelection` object,\n  # because the anchorNodes of that may change from underneath us at any\n  # time.\n  #\n  # 2. `exportedSelection` - This is an {ExportedSelection} object and is\n  # used to restore the selection even after the DOM has changed. When our\n  # component re-renders the actual DOM objects on the heap will be\n  # different. An {ExportedSelection} contains counting indicies we use to\n  # re-find the correct DOM Nodes in the new document.\n  #\n  # 3. `previousExportedSelection` - This is used for undo / redo so when\n  # you revert to a previous state, the selection updates as well.\n  _saveSelection: =>\n    selection = new ExtendedSelection(@_editableNode())\n    return unless selection?.isInScope()\n\n    @setInnerState\n      selectionSnapshot: selection.selectionSnapshot()\n      exportedSelection: selection.exportSelection()\n      previousExportedSelection: @innerState.exportedSelection\n\n  _restoreSelection: =>\n    @_teardownNonMutationListeners()\n    selection = new ExtendedSelection(@_editableNode())\n    selection.importSelection(@innerState.exportedSelection)\n    if selection.isInScope()\n      # The bounding client rect has changed\n      @setInnerState editableNode: @_editableNode()\n    @_setupNonMutationListeners()\n\n  _notifyOfSelectionRestoration: =>\n    selection = new ExtendedSelection(@_editableNode())\n    if selection.isInScope()\n      @props.onSelectionRestored(selection, @_editableNode())\n\n  # When the component updates, the selection may have changed from our\n  # last known saved position. This can happen for a couple of reasons:\n  #\n  # 1. Some sister-component (like the LinkEditor) grabbed the selection.\n  # 2. A sister-component that used to have the selection was unmounted\n  # causing the selection to be null or the document\n  _shouldRestoreSelectionOnUpdate: ->\n    !@innerState.dragging and document.activeElement is @_editableNode()\n\nmodule.exports = Contenteditable\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/dom-normalizer.coffee",
    "content": "{ContenteditableExtension, DOMUtils} = require 'nylas-exports'\n\nclass DOMNormalizer extends ContenteditableExtension\n\n  # This component works by re-rendering on every change and restoring the\n  # selection. This is also how standard React controlled inputs work too.\n  #\n  # Since the contents of the contenteditable are complex, nested DOM\n  # structures, a simple replacement of the DOM is not easy. There are a\n  # variety of edge cases that we need to correct for and prepare both the\n  # HTML and the selection to be serialized without error.\n  @onContentChanged: ({editor, mutations}) ->\n    @_cleanHTML(editor)\n    @_cleanSelection(editor)\n\n  # We need to clean the HTML on input to fix several edge cases that\n  # arise when we go to save the selection state and restore it on the\n  # next render.\n  @_cleanHTML: (editor) ->\n    return unless editor.rootNode\n\n    # One issue is that we need to pre-normalize the HTML so it looks the\n    # same after it gets re-inserted. If we key selection markers off of\n    # an non normalized DOM, then they won't match up when the HTML gets\n    # reset.\n    #\n    # The Node.normalize() method puts the specified node and all of its\n    # sub-tree into a \"normalized\" form. In a normalized sub-tree, no text\n    # nodes in the sub-tree are empty and there are no adjacent text\n    # nodes.\n    editor.normalize()\n\n    @_fixLeadingBRCondition(editor)\n\n  # An issue arises from <br/> tags immediately inside of divs. In this\n  # case the cursor's anchor node will not be the <br/> tag, but rather\n  # the entire enclosing element. Sometimes, that enclosing element is the\n  # container wrapping all of the content. The browser has a native\n  # built-in feature that will automatically scroll the page to the bottom\n  # of the current element that the cursor is in if the cursor is off the\n  # screen. In the given case, that element is the whole div. The net\n  # effect is that the browser will scroll erroneously to the bottom of\n  # the whole content div, which is likely NOT where the cursor is or the\n  # user wants. The solution to this is to replace this particular case\n  # with <span></span> tags and place the cursor in there.\n  @_fixLeadingBRCondition: (editor) ->\n    treeWalker = document.createTreeWalker editor.rootNode\n    while treeWalker.nextNode()\n      currentNode = treeWalker.currentNode\n      if @_hasLeadingBRCondition(currentNode)\n        newNode = document.createElement(\"div\")\n        newNode.appendChild(document.createElement(\"br\"))\n        currentNode.replaceChild(newNode, currentNode.childNodes[0])\n    return\n\n  @_hasLeadingBRCondition: (node) ->\n    childNodes = node.childNodes\n    return childNodes.length >= 2 and childNodes[0].nodeName is \"BR\"\n\n  # After an input, the selection can sometimes get itself into a state\n  # that either can't be restored properly, or will cause undersirable\n  # native behavior. This method, in combination with `_cleanHTML`, fixes\n  # each of those scenarios before we save and later restore the\n  # selection.\n  @_cleanSelection: (editor) ->\n    selection = editor.currentSelection()\n    return unless selection.anchorNode? and selection.focusNode?\n\n    # The _unselectableNode case only is valid when it's at the very top\n    # (offset 0) of the node. If the offsets are > 0 that means we're\n    # trying to select somewhere within some sort of containing element.\n    # This is okay to do. The odd case only arises at the top of\n    # unselectable elements.\n    return if selection.anchorOffset > 0 or selection.focusOffset > 0\n\n    if selection.isCollapsed and @_unselectableNode(selection.focusNode)\n      treeWalker = document.createTreeWalker(selection.focusNode)\n      while treeWalker.nextNode()\n        currentNode = treeWalker.currentNode\n        if @_unselectableNode(currentNode)\n          selection.setBaseAndExtent(currentNode, 0, currentNode, 0)\n          break\n    return\n\n  @_unselectableNode: (node) ->\n    return true if not node\n    if node.nodeType is Node.TEXT_NODE and DOMUtils.isBlankTextNode(node)\n      return true\n    else if node.nodeType is Node.ELEMENT_NODE\n      child = node.firstChild\n      return true if not child\n      hasText = (child.nodeType is Node.TEXT_NODE and not DOMUtils.isBlankTextNode(node))\n      hasBr = (child.nodeType is Node.ELEMENT_NODE and node.nodeName is \"BR\")\n      return not hasText and not hasBr\n\n    else return false\n\nmodule.exports = DOMNormalizer\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/editor-api.coffee",
    "content": "_ = require 'underscore'\n{DOMUtils} = require 'nylas-exports'\nReact = require 'react'\nExtendedSelection = require './extended-selection'\nOverlaidComponents = null\n\n# An extended interface of execCommand\n#\n# Muates the DOM and Selection in atomic and predictable ways.\n#\n# editor.select(/{{}}/).checkNode().wrapNode(\"code\")\n#\n# codeTags.forEach (tag) ->\n#   if testTag(tag) DOMUtils.unwrap(tag)\n#\n# fn: ->\n#   editor.moveDown().indent().selectRight(2).wrapSelection().bold().moveToEnd()\n#\n#   editor.moveDown()\n#   editor.selectRight()\n#   editor.bold()\n#   editor.indent()\n#   editor.moveToEnd()\n#\n#   moveToEnd bold selectRight moveDown current()\n#\nclass EditorAPI\n  constructor: (@rootNode) ->\n    @_extendedSelection = new ExtendedSelection(@rootNode)\n\n  wrapSelection: (nodeName) ->\n    wrapped = DOMUtils.wrap(@_extendedSelection.getRangeAt(0), nodeName)\n    @select(wrapped)\n    return @\n\n  unwrapNodeAndSelectAll: (node) ->\n    replacedNodes = DOMUtils.unwrapNode(node)\n    return @ if replacedNodes.length is 0\n    first = DOMUtils.findFirstTextNode(replacedNodes[0])\n    last = DOMUtils.findLastTextNode(_.last(replacedNodes))\n    @_extendedSelection.selectFromTo(first, last)\n    return @\n\n  regExpSelectorAll:(regex) ->\n    DOMUtils.regExpSelectorAll(@rootNode, regex)\n\n  currentSelection: -> @_extendedSelection\n\n  whilePreservingSelection: (fn) ->\n    # We only preserve selection if the active element is actually within the\n    # contenteditable. Otherwise, we can unintentionally \"steal\" focus back if\n    # `whilePreservingSelection` is called by a plugin when we are not focused.\n    return fn() unless document.activeElement is @rootNode or @rootNode.contains(document.activeElement)\n    sel = @currentSelection().exportSelection()\n    fn()\n    @select(sel)\n\n  getSelectionTextIndex: (args...) ->\n    @_extendedSelection.getSelectionTextIndex(args...)\n\n  importSelection: (args...) ->\n    @_extendedSelection.importSelection(args...); @\n\n  select: (args...) ->\n    @_extendedSelection.select(args...); @\n\n  selectAllChildren: (args...) ->\n    @_extendedSelection.selectAllChildren(args...); @\n\n  restoreSelectionByTextIndex: (args...) ->\n    @_extendedSelection.restoreSelectionByTextIndex(args...); @\n\n  normalize: -> @rootNode.normalize(); @\n\n  insertCustomComponent: (componentKey, props = {}) ->\n    OverlaidComponents ?= require('../overlaid-components/overlaid-components').default\n    {anchorId, anchorTag} = OverlaidComponents.buildAnchorTag(componentKey, props, props.anchorId)\n    @insertHTML(anchorTag)\n    return anchorId\n\n  removeCustomComponentByAnchorId: (anchorId) ->\n    return unless anchorId\n    node = @rootNode.querySelector(\"img[data-overlay-id=\\\"#{anchorId}\\\"]\")\n    node?.parentNode.removeChild(node)\n\n  ########################################################################\n  ####################### execCommand Delegation #########################\n  ########################################################################\n  backColor: (color) -> @_ec(\"backColor\", false, color)\n  bold: -> @_ec(\"bold\", false)\n  copy: -> @_ec(\"copy\", false)\n  createLink: (uri) -> @_ec(\"createLink\", false, uri)\n  cut: -> @_ec(\"cut\", false)\n  decreaseFontSize: -> @_ec(\"decreaseFontSize\", false)\n  delete: -> @_ec(\"delete\", false)\n  fontName: (fontName) -> @_ec(\"fontName\", false, fontName)\n  fontSize: (fontSize) -> @_ec(\"fontSize\", false, fontSize)\n  foreColor: (color) -> @_ec(\"foreColor\", false, color)\n  formatBlock: (tagName) -> @_ec(\"formatBlock\", false, tagName)\n  forwardDelete: -> @_ec(\"forwardDelete\", false)\n  heading: (tagName) -> @_ec(\"heading\", false, tagName)\n  hiliteColor: (color) -> @_ec(\"hiliteColor\", false, color)\n  increaseFontSize: -> @_ec(\"increaseFontSize\", false)\n  indent: -> @_ec(\"indent\", false)\n  insertHorizontalRule: -> @_ec(\"insertHorizontalRule\", false)\n\n  insertHTML: (html, {selectInsertion} = {}) ->\n    if selectInsertion\n      wrappedHtml = \"\"\"<span id=\"tmp-html-insertion-wrap\">#{html}</span>\"\"\"\n      @_ec(\"insertHTML\", false, wrappedHtml)\n      wrap = @rootNode.querySelector(\"#tmp-html-insertion-wrap\")\n      @unwrapNodeAndSelectAll(wrap)\n      return @\n    else\n      @_ec(\"insertHTML\", false, html)\n\n  insertImage: (uri) -> @_ec(\"insertImage\", false, uri)\n  insertOrderedList: -> @_ec(\"insertOrderedList\", false)\n  insertUnorderedList: -> @_ec(\"insertUnorderedList\", false)\n  insertParagraph: -> @_ec(\"insertParagraph\", false)\n  insertText: (text) -> @_ec(\"insertText\", false, text)\n  italic: -> @_ec(\"italic\", false)\n  justifyCenter: -> @_ec(\"justifyCenter\", false)\n  justifyFull: -> @_ec(\"justifyFull\", false)\n  justifyLeft: -> @_ec(\"justifyLeft\", false)\n  justifyRight: -> @_ec(\"justifyRight\", false)\n  outdent: -> @_ec(\"outdent\", false)\n  paste: -> @_ec(\"paste\", false)\n  redo: -> @_ec(\"redo\", false)\n  removeFormat: -> @_ec(\"removeFormat\", false)\n  selectAll: -> @_ec(\"selectAll\", false)\n  strikeThrough: -> @_ec(\"strikeThrough\", false)\n  subscript: -> @_ec(\"subscript\", false)\n  superscript: -> @_ec(\"superscript\", false)\n  underline: -> @_ec(\"underline\", false)\n  undo: -> @_ec(\"undo\", false)\n  unlink: -> @_ec(\"unlink\", false)\n  styleWithCSS: (style) -> @_ec(\"styleWithCSS\", false, style)\n\n  contentReadOnly: -> @_notImplemented()\n  enableInlineTableEditing: -> @_notImplemented()\n  enableObjectResizing: -> @_notImplemented()\n  insertBrOnReturn: -> @_notImplemented()\n  useCSS: -> @_notImplemented()\n\n  ########################################################################\n  ####################### Private Helper Methods #########################\n  ########################################################################\n  _ec: (args...) -> document.execCommand(args...); return @\n  _notImplemented: -> throw new Error(\"Not implemented\")\n\nmodule.exports = EditorAPI\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/emphasis-formatting-extension.es6",
    "content": "import { ContenteditableExtension } from 'nylas-exports';\n\n// This provides the default baisc formatting options for the\n// Contenteditable using the declarative extension API.\nexport default class EmphasisFormattingExtension extends ContenteditableExtension {\n  static keyCommandHandlers() {\n    return {\n      \"contenteditable:bold\": this._onBold,\n      \"contenteditable:italic\": this._onItalic,\n      \"contenteditable:underline\": this._onUnderline,\n      \"contenteditable:strikeThrough\": this._onStrikeThrough,\n    };\n  }\n\n  static toolbarButtons() {\n    return [\n      {\n        className: \"btn-bold\",\n        onClick: this._onBold,\n        tooltip: \"Bold\",\n        iconUrl: null, // Defined in the css of btn-bold\n      },\n      {\n        className: \"btn-italic\",\n        onClick: this._onItalic,\n        tooltip: \"Italic\",\n        iconUrl: null, // Defined in the css of btn-italic\n      },\n      {\n        className: \"btn-underline\",\n        onClick: this._onUnderline,\n        tooltip: \"Underline\",\n        iconUrl: null, // Defined in the css of btn-underline\n      },\n    ];\n  }\n\n  static _onBold({editor}) {\n    editor.bold();\n  }\n\n  static _onItalic({editor}) {\n    editor.italic();\n  }\n\n  static _onUnderline({editor}) {\n    editor.underline();\n  }\n\n  static _onStrikeThrough({editor}) {\n    editor.strikeThrough();\n  }\n\n  // None of the emphasis formatting buttons need a custom component.\n  //\n  // They use the default <ToolbarButtons> component via the\n  // `toolbarButtons` extension API.\n  //\n  // The <ToolbarButtons> core component is managed by the\n  // {ToolbarButtonManager}\n  static toolbarComponentConfig() {\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/exported-selection.es6",
    "content": "import {DOMUtils} from 'nylas-exports';\n\n// A saved out selection object\n//\n// When exporting a selection we need to be sure to deeply `cloneNode`.\n// This is because sometimes our anchorNodes are divs with nested <br>\n// tags. If we don't do a deep clone then when `isEqualNode` is run it will\n// erroneously return false.\n//\nclass ExportedSelection {\n  constructor(rawSelection, scopeNode) {\n    this.rawSelection = rawSelection;\n    this.scopeNode = scopeNode;\n    this.type = this.rawSelection.type;\n\n    if (this.type !== 'None') {\n      this.anchorNode = this.rawSelection.anchorNode.cloneNode(true);\n      this.anchorOffset = this.rawSelection.anchorOffset;\n      this.anchorNodeIndex = DOMUtils.getNodeIndex(this.scopeNode, this.rawSelection.anchorNode);\n      this.focusNode = this.rawSelection.focusNode.cloneNode(true);\n      this.focusOffset = this.rawSelection.focusOffset;\n      this.focusNodeIndex = DOMUtils.getNodeIndex(this.scopeNode, this.rawSelection.focusNode);\n    }\n    this.isCollapsed = this.rawSelection.isCollapsed;\n  }\n\n  /* Public: Tests for equality amongst exported selections\n\n  When we restore the selection later, we need to find a node that looks\n  the same as the one we saved (since they're different object\n  references).\n\n  Unfortunately there many be many nodes that \"look\" the same (match the\n  `isEqualNode`) test. For example, say I have a bunch of lines with the\n  TEXT_NODE \"Foo\". All of those will match `isEqualNode`. To fix this we\n  assume there will be multiple matches and keep track of the index of the\n  match. e.g. all \"Foo\" TEXT_NODEs may look alike, but I know I want the\n  Nth \"Foo\" TEXT_NODE. We store this information in the `startNodeIndex`\n  and `endNodeIndex` fields via the `DOMUtils.getNodeIndex` method.\n  */\n  isEqual(otherSelection) {\n    if (!otherSelection) {\n      return false;\n    }\n    if (this.type !== otherSelection.type) {\n      return false;\n    }\n\n    if (this.type === 'None' && otherSelection.type === 'None') {\n      return true;\n    }\n    if ((otherSelection.anchorNode == null) || (otherSelection.focusNode == null)) {\n      return false;\n    }\n\n    const anchorIndex = DOMUtils.getNodeIndex(this.scopeNode, otherSelection.anchorNode);\n    const focusIndex = DOMUtils.getNodeIndex(this.scopeNode, otherSelection.focusNode);\n\n    let anchorEqual = otherSelection.anchorNode.isEqualNode(this.anchorNode);\n    let anchorIndexEqual = anchorIndex === this.anchorNodeIndex;\n    let focusEqual = otherSelection.focusNode.isEqualNode(this.focusNode);\n    let focusIndexEqual = focusIndex === this.focusNodeIndex;\n    if (!anchorEqual && !focusEqual) {\n      // This means the otherSelection is the same, but just from the opposite\n      // direction. We don't care in this case, so check the reciprocal as\n      // well.\n      anchorEqual = otherSelection.anchorNode.isEqualNode(this.focusNode);\n      anchorIndexEqual = anchorIndex === this.focusNodeIndex;\n      focusEqual = otherSelection.focusNode.isEqualNode(this.anchorNode);\n      focusIndexEqual = focusIndex === this.anchorndNodeIndex;\n    }\n\n    let anchorOffsetEqual = otherSelection.anchorOffset === this.anchorOffset;\n    let focusOffsetEqual = otherSelection.focusOffset === this.focusOffset;\n    if (!anchorOffsetEqual && !focusOffsetEqual) {\n      // This means the otherSelection is the same, but just from the opposite\n      // direction. We don't care in this case, so check the reciprocal as\n      // well.\n      anchorOffsetEqual = otherSelection.anchorOffset === this.focusOffset;\n      focusOffsetEqual = otherSelection.focusOffset === this.anchorOffset;\n    }\n\n    return (anchorEqual && anchorIndexEqual && anchorOffsetEqual &&\n      focusEqual && focusIndexEqual && focusOffsetEqual);\n  }\n}\n\nexport default ExportedSelection;\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/extended-selection.coffee",
    "content": "_ = require 'underscore'\n{DOMUtils} = require 'nylas-exports'\nExportedSelection = require('./exported-selection').default\n\n# Convenience methods over the DOM's Selection object\n# https://developer.mozilla.org/en-US/docs/Web/API/Selection\nclass ExtendedSelection\n  constructor: (@scopeNode) ->\n    @scopeNode ?= document.body\n    @rawSelection = document.getSelection()\n\n  isInScope: ->\n    @anchorNode? and\n    @focusNode? and\n    @anchorOffset? and\n    @focusOffset? and\n    @scopeNode.contains(@anchorNode) and\n    @scopeNode.contains(@focusNode)\n\n  # Public: Conveniently select nodes.\n  select: (args...) ->\n    if args.length is 0\n      throw @_errBadUsage()\n    else if args.length is 1\n      if args[0] instanceof ExportedSelection\n        @importSelection(args[0])\n      else if args[0] instanceof Range\n        @selectRange(args[0])\n      else\n        @selectAt(args[0])\n    else if args.length is 2\n      @selectFromTo(args...)\n    else if args.length is 3\n      throw @_errBadUsage()\n    else if args.length is 4\n      @selectFromToWithIndex(args...)\n    else if args.length >= 5\n      throw @_errBadUsage()\n    return @\n\n  selectAt: (at) ->\n    nodeAt = @findNodeAt(at)\n    range = new Range()\n    range.selectNode(nodeAt)\n    @rawSelection.removeAllRanges()\n    @rawSelection.addRange(range)\n    return @\n\n  selectRange: (range) ->\n    @setBaseAndExtent(range.startContainer, range.startOffset, range.endContainer, range.endOffset)\n\n  selectFromTo: (from, to) ->\n    fromNode = @findNodeAt(from)\n    toNode = @findNodeAt(to)\n    return unless fromNode and toNode\n    @setBaseAndExtent(fromNode, 0, toNode, (toNode.length ? 0))\n\n  selectFromToWithIndex: (from, fromIndex, to, toIndex) ->\n    fromNode = @findNodeAt(from)\n    toNode = @findNodeAt(to)\n    if (not _.isNumber(fromIndex)) or (not _.isNumber(toIndex))\n      throw @_errBadUsage()\n    return unless fromNode and toNode\n    @setBaseAndExtent(fromNode, fromIndex, toNode, toIndex)\n\n  exportSelection: -> new ExportedSelection(@rawSelection, @scopeNode)\n\n  # A selectionSnapshot is slightly different from an {ExportedSelection}.\n  # An {ExportedSelection} maintains clones of the nodes (which don't have\n  # parentNodes nor are attached to the dcoument). An {ExportedSelection}\n  # also contains counting indices for future restoration.\n  #\n  # This is necessary since references to `rawSelection` can have its\n  # anchorNodes change out from underneath it as the selection changes.\n  selectionSnapshot: ->\n    anchorNode: @rawSelection.anchorNode\n    anchorOffset: @rawSelection.anchorOffset\n    focusNode: @rawSelection.focusNode\n    focusOffset: @rawSelection.focusOffset\n    isCollapsed: @rawSelection.isCollapsed\n\n  # Since the last time we exported the selection, the DOM may have\n  # completely changed due to a re-render. To the user it may look\n  # identical, but the newly rendered region may be comprised of\n  # completely new DOM nodes. Our old node references may not exist\n  # anymore. As such, we have the task of re-finding the nodes again and\n  # creating a new selection that matches as accurately as possible.\n  #\n  # There are multiple ways of setting a new selection with the Selection\n  # API. One very common one is to create a new Range object and then call\n  # `addRange` on a selection instance. This does NOT work for us because\n  # `Range` objects are direction-less. A Selection's start node (aka\n  # anchor node aka base node) can be \"after\" a selection's end node (aka\n  # focus node aka extent node).\n  importSelection: (exportedSelection) ->\n    return unless exportedSelection instanceof ExportedSelection\n    newAnchorNode = DOMUtils.findSimilarNodeAtIndex(@scopeNode, exportedSelection.anchorNode, exportedSelection.anchorNodeIndex)\n\n    newFocusNode = DOMUtils.findSimilarNodeAtIndex(@scopeNode, exportedSelection.focusNode, exportedSelection.focusNodeIndex)\n\n    @setBaseAndExtent(newAnchorNode,\n                      exportedSelection.anchorOffset,\n                      newFocusNode,\n                      exportedSelection.focusOffset)\n\n  findNodeAt: (arg) ->\n    node = null\n    if arg instanceof Node\n      node = arg\n    else if _.isString(arg)\n      node = @scopeNode.querySelector(arg)\n    else if _.isRegExp(arg)\n      ## TODO\n      node = DOMUtils.findNodeByRegex(@scopeNode, arg)\n\n    return node\n\n  # Finds the start and end text index of the current selection relative\n  # to a given Node or Range. Returns an object of the form:\n  #   {startIndex, endIndex}\n  #\n  # Uses getIndexedTextContent to index the text, which accounts for line breaks\n  # from DIVs and BRs. For ranges, the index takes into account the start and end\n  # offsets of the range.\n  getSelectionTextIndex: (refRangeOrNode) ->\n    return null unless DOMUtils.selectionStartsOrEndsIn(refRangeOrNode)\n    sel = @rawSelection\n    return null unless sel\n    startIndex = null\n    endIndex = null\n    range = null\n    rangeOffset = 0\n    if refRangeOrNode instanceof Range\n      range = refRangeOrNode\n      parentNode = range.commonAncestorContainer\n    else\n      parentNode = refRangeOrNode\n\n    # If the selection is directly on the parent node, just return the\n    # selection offsets\n    if parentNode is sel.anchorNode\n      if range then rangeOffset = range.startOffset\n      startIndex = sel.anchorOffset-rangeOffset\n    if parentNode is sel.focusNode\n      if range then rangeOffset = range.startOffset\n      endIndex = sel.focusOffset-rangeOffset\n\n    if parentNode is sel.anchorNode and parentNode is sel.focusNode\n      return {startIndex, endIndex}\n\n    # Otherwise find the start and end index within a text representation of the\n    # parent node\n    for {node, start, end} in DOMUtils.getIndexedTextContent(parentNode)\n      if range?.startContainer is node\n        rangeOffset = start + range.startOffset\n      if sel.anchorNode is node\n        startIndex = start + sel.anchorOffset - rangeOffset\n      if sel.focusNode is node\n        endIndex = start + sel.focusOffset - rangeOffset\n    return {startIndex, endIndex}\n\n  # Sets the current selection to start and end at the specified indices, relative\n  # to the given Range or Node. This is the inverse of getSelectionByTextIndex.\n  #\n  # Uses getIndexedTextContent to index the text, which accounts for line breaks\n  # from DIVs and BRs. For ranges, the index takes into account the start and end\n  # offsets of the range.\n  restoreSelectionByTextIndex: (refRangeOrNode, startIndex, endIndex) ->\n    startNode = null\n    startOffset = null\n    endNode = null\n    endOffset = null\n    range = null\n    sel = @rawSelection\n    if refRangeOrNode instanceof Range\n      range = refRangeOrNode\n      parentNode = range.commonAncestorContainer\n    else\n      parentNode = refRangeOrNode\n\n    if parentNode.childNodes.length == 0 # text node\n      sel.setBaseAndExtent(parentNode, startIndex, parentNode, endIndex)\n\n    inRange = (range is null) # we're not in range yet, unless there is no range\n    items = DOMUtils.getIndexedTextContent(parentNode)\n    for {node, start, end},i in items\n      inRange = inRange or (range.startContainer is node)\n      atEnd = i==(items.length-1)\n      if not inRange\n        continue\n      if range?.startContainer is node\n        rangeOffset = start + range.startIndex\n        if startIndex? then startIndex += rangeOffset\n        if endIndex? then endIndex += rangeOffset\n      if startIndex? and startIndex >= start and (startIndex < end or atEnd and startIndex==end)\n        startNode = node\n        startOffset = startIndex - start\n      if endIndex? and endIndex >= start and (endIndex < end or atEnd and endIndex==end)\n        endNode = node\n        endOffset = endIndex - start\n    sel.setBaseAndExtent(startNode ? sel.anchorNode, startOffset ? sel.anchorOffset, endNode ? sel.focusNode, endOffset ? sel.focusOffset)\n\n\n  Object.defineProperty @prototype, \"anchorNode\",\n    get: -> @rawSelection.anchorNode\n    set: -> throw @_errNoSet(\"anchorNode\")\n  Object.defineProperty @prototype, \"anchorOffset\",\n    get: -> @rawSelection.anchorOffset\n    set: -> throw @_errNoSet(\"anchorOffset\")\n  Object.defineProperty @prototype, \"focusNode\",\n    get: -> @rawSelection.focusNode\n    set: -> throw @_errNoSet(\"focusNode\")\n  Object.defineProperty @prototype, \"focusOffset\",\n    get: -> @rawSelection.focusOffset\n    set: -> throw @_errNoSet(\"focusOffset\")\n  Object.defineProperty @prototype, \"isCollapsed\",\n    get: -> @rawSelection.isCollapsed\n    set: -> throw @_errNoSet(\"isCollapsed\")\n  Object.defineProperty @prototype, \"rangeCount\",\n    get: -> @rawSelection.rangeCount\n    set: -> throw @_errNoSet(\"rangeCount\")\n\n  setBaseAndExtent: (args...) -> @rawSelection.setBaseAndExtent(args...)\n  getRangeAt: (args...) -> @rawSelection.getRangeAt(args...)\n  collapse: (args...) -> @rawSelection.collapse(args...)\n  extend: (args...) -> @rawSelection.extend(args...)\n  modify: (args...) -> @rawSelection.modify(args...)\n  collapseToStart: (args...) -> @rawSelection.collapseToStart(args...)\n  collapseToEnd: (args...) -> @rawSelection.collapseToEnd(args...)\n  selectAllChildren: (args...) -> @rawSelection.selectAllChildren(args...)\n  addRange: (args...) -> @rawSelection.addRange(args...)\n  removeRange: (args...) -> @rawSelection.removeRange(args...)\n  removeAllRanges: (args...) -> @rawSelection.removeAllRanges(args...)\n  deleteFromDocument: (args...) -> @rawSelection.deleteFromDocument(args...)\n  toString: (args...) -> @rawSelection.toString(args...)\n  containsNode: (args...) -> @rawSelection.containsNode(args...)\n\n  _errBadUsage: -> new Error(\"Invalid arguments\")\n  _errNoSet: (property) -> new Error(\"Can't set #{property}\")\n\n\nmodule.exports = ExtendedSelection\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/floating-toolbar.cjsx",
    "content": "_ = require 'underscore'\nclassNames = require 'classnames'\nReact = require 'react'\n\n{Utils, DOMUtils, ExtensionRegistry} = require 'nylas-exports'\n\n# Positions and renders a FloatingToolbar in the composer.\n#\n# The FloatingToolbar declaratively chooses a Component to render. Only\n# extensions that expose a `toolbarComponentConfig` will be considered.\n# Whether or not there's an available component to render determines\n# whether or not the FloatingToolbar is visible.\n#\n# There's no `toolbarVisible` state. It uses the existance of a\n# ToolbarComponent to determine what to display.\n#\n# The {ToolbarButtonManager} and the {LinkManager} are `coreExtensions`\n# that declaratively register the special `<ToolbarButtons/>` component\n# and the `<LinkEditor />` component.\nclass FloatingToolbar extends React.Component\n  @displayName: \"FloatingToolbar\"\n\n  # We are passed an array of Extensions. Those that implement the\n  # `toolbarButton` and/or the `toolbarComponent` methods will be\n  # injected into the Toolbar.\n  #\n  # Every time the `innerState` of the `Contenteditable` change, we get\n  # passed the data as new `innerProps`.\n  @propTypes:\n    atomicEdit: React.PropTypes.func\n    extensions: React.PropTypes.array\n  @innerPropTypes:\n    dragging: React.PropTypes.bool\n    selection: React.PropTypes.object\n    doubleDown: React.PropTypes.bool\n    hoveringOver: React.PropTypes.object\n    editableNode: React.PropTypes.object\n\n  @defaultProps:\n    extensions: []\n  @defaultInnerProps:\n    dragging: false\n    selection: null\n    doubleDown: false\n    hoveringOver: null\n    editableNode: null\n\n  constructor: (@props) ->\n    @state =\n      toolbarTop: 0\n      toolbarMode: \"buttons\"\n      toolbarLeft: 0\n      toolbarPos: \"above\"\n      editAreaWidth: 9999 # This will get set on first selection\n      toolbarWidth: 0\n      toolbarHeight: 0\n      toolbarComponent: null\n      toolbarLocationRef: null\n      toolbarComponentProps: {}\n      hidePointer: false\n    @innerProps = FloatingToolbar.defaultInnerProps\n\n  shouldComponentUpdate: (nextProps, nextState) ->\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  # Some properties (like whether we're dragging or clicking the mouse)\n  # should in a strict-sense be props, but update in a way that's not\n  # performant to got through the full React re-rendering cycle,\n  # especially given the complexity of the composer component.\n  #\n  # We call these performance-optimized props & state innerProps and\n  # innerState.\n  componentWillReceiveInnerProps: (nextInnerProps={}) =>\n    fullProps = _.extend({}, @props, nextInnerProps)\n    @innerProps = _.extend @innerProps, nextInnerProps\n    @setState(@_getStateFromProps(fullProps))\n\n  componentWillReceiveProps: (nextProps) =>\n    fullProps = _.extend(@innerProps, nextProps)\n    @setState(@_getStateFromProps(fullProps))\n\n  # The context menu, when activated, needs to make sure that the toolbar\n  # is closed. Unfortunately, since there's no onClose callback for the\n  # context menu, we can't hook up a reliable declarative state to the\n  # menu. We break our declarative pattern in this one case.\n  forceClose: ->\n    @setState toolbarVisible: false\n\n  # We render a ToolbarComponent in a floating frame.\n  render: ->\n    ToolbarComponent = @state.toolbarComponent\n    return false unless ToolbarComponent\n\n    <div className=\"floating-toolbar-container\">\n      <div ref=\"floatingToolbar\"\n           className={@_toolbarClasses()}\n           style={@_toolbarStyles()}>\n\n        {@_renderPointer()}\n        <ToolbarComponent {...@state.toolbarComponentProps} />\n      </div>\n    </div>\n\n  _getStateFromProps: (props) ->\n    toolbarComponentState = @_getToolbarComponentData(props)\n    if toolbarComponentState.toolbarLocationRef\n      positionState = @_calculatePositionState(props, toolbarComponentState)\n    else positionState = {}\n\n    return _.extend {}, toolbarComponentState, positionState\n\n  # If this returns a `null` component, that means we don't want to show\n  # anything.\n  _getToolbarComponentData: (props) ->\n    toolbarComponent = null\n    toolbarWidth = 0\n    toolbarHeight = 0\n    toolbarLocationRef = null\n    hidePointer = false\n    toolbarComponentProps = {}\n\n    for extension in props.extensions\n      try\n        params = extension.toolbarComponentConfig?(toolbarState: props) ? {}\n        if params.component\n          toolbarComponent = params.component\n          toolbarComponentProps = params.props ? {}\n          toolbarLocationRef = params.locationRefNode\n          toolbarWidth = params.width\n          toolbarHeight = params.height\n          if params.hidePointer\n            hidePointer = params.hidePointer\n      catch error\n        NylasEnv.reportError(error)\n\n    if toolbarComponent and not toolbarLocationRef\n      throw new Error(\"You must provide a locationRefNode for #{toolbarComponent.displayName}. It must be either a DOM Element or a Range.\")\n\n    return {toolbarComponent, toolbarComponentProps, toolbarLocationRef, toolbarWidth, toolbarHeight, hidePointer}\n\n  @CONTENT_PADDING: 15\n\n  _calculatePositionState: (props, {toolbarLocationRef, toolbarWidth, toolbarHeight}) =>\n    editableNode = props.editableNode\n\n    if not _.isFunction(toolbarLocationRef.getBoundingClientRect)\n      throw new Error(\"Your locationRefNode must implement getBoundingClientRect. Be aware that Text nodes do not implement this, but Element nodes do. Find the nearest Element relative.\")\n\n    referenceRect = toolbarLocationRef.getBoundingClientRect()\n\n    if not editableNode or not referenceRect or DOMUtils.isEmptyBoundingRect(referenceRect)\n      return {toolbarTop: 0, toolbarLeft: 0, editAreaWidth: 0, toolbarPos: 'above'}\n\n    TOP_PADDING = 10\n\n    BORDER_RADIUS_PADDING = 15\n\n    editArea = editableNode.getBoundingClientRect()\n\n    calcLeft = (referenceRect.left - editArea.left) + referenceRect.width/2\n    calcLeft = Math.min(Math.max(calcLeft, FloatingToolbar.CONTENT_PADDING+BORDER_RADIUS_PADDING), editArea.width - BORDER_RADIUS_PADDING)\n\n    calcTop = referenceRect.top - editArea.top - toolbarHeight - 14\n    if @state.hidePointer\n      calcTop += 10\n    toolbarPos = \"above\"\n    if calcTop < TOP_PADDING\n      calcTop = referenceRect.top - editArea.top + referenceRect.height + TOP_PADDING + 4\n      if @state.hidePointer\n        calcTop -= 10\n      toolbarPos = \"below\"\n\n    maxWidth = editArea.width - FloatingToolbar.CONTENT_PADDING * 2\n\n    return {\n      toolbarTop: calcTop\n      toolbarLeft: calcLeft\n      toolbarWidth: Math.min(maxWidth, toolbarWidth)\n      toolbarHeight: toolbarHeight\n      editAreaWidth: editArea.width\n      toolbarPos: toolbarPos\n    }\n\n  _toolbarClasses: =>\n    classes = {}\n    classes[@state.toolbarPos] = true\n    classNames _.extend classes,\n      \"floating-toolbar\": true\n      \"toolbar\": true\n\n  _toolbarStyles: =>\n    styles =\n      left: @_toolbarLeft()\n      top: @state.toolbarTop\n      width: @state.toolbarWidth\n      height: @state.toolbarHeight\n    return styles\n\n  _toolbarLeft: =>\n    max = Math.max(@state.editAreaWidth - @state.toolbarWidth - FloatingToolbar.CONTENT_PADDING, FloatingToolbar.CONTENT_PADDING)\n    left = Math.min(Math.max(@state.toolbarLeft - @state.toolbarWidth/2, FloatingToolbar.CONTENT_PADDING), max)\n    return left\n\n  _toolbarPointerStyles: =>\n    POINTER_WIDTH = 6 + 2 #2px of border-radius\n    max = @state.editAreaWidth - FloatingToolbar.CONTENT_PADDING\n    min = FloatingToolbar.CONTENT_PADDING\n    absoluteLeft = Math.max(Math.min(@state.toolbarLeft, max), min)\n    relativeLeft = absoluteLeft - @_toolbarLeft()\n\n    left = Math.max(Math.min(relativeLeft, @state.toolbarWidth-POINTER_WIDTH), POINTER_WIDTH)\n    styles =\n      left: left\n    return styles\n\n  _renderPointer: =>\n    return false if @state.hidePointer\n    <div className=\"toolbar-pointer-container\" style={@_toolbarPointerStyles()}>\n      <div className=\"shadow\"></div>\n      <div className=\"foreground\"></div>\n    </div>\n\nmodule.exports = FloatingToolbar\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/link-editor.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\n{RegExpUtils} = require 'nylas-exports'\n\nclass LinkEditor extends React.Component\n  @displayName = \"LinkEditor\"\n\n  @propTypes:\n    # A callback function we use to save the URL to the Contenteditable\n    onSaveUrl: React.PropTypes.func\n\n    # The current DOM link we are modifying\n    linkToModify: React.PropTypes.object\n\n    # A callback used when a link has been cancled, completed, or escaped\n    # from. Used to notify our parent to switch modes.\n    onDoneWithLink: React.PropTypes.func\n\n  constructor: (@props) ->\n    @state =\n      urlInputValue: @_initialUrl() ? \"\"\n\n  componentWillReceiveProps: (newProps) ->\n    @setState urlInputValue: @_initialUrl(newProps)\n\n  componentDidMount: ->\n    if @props.focusOnMount\n      ReactDOM.findDOMNode(@refs[\"urlInput\"]).focus()\n\n  render: =>\n    widthCorrection = 32 # width of padding and leading icon space\n    checkBtn = \"\"\n    removeBtn = \"\"\n    if @_initialUrl()\n      widthCorrection += 32\n      removeBtn = <button className=\"btn btn-icon\"\n                          ref=\"removeBtn\"\n                          style={float: \"right\"}\n                          onMouseDown={@_removeUrl}><i className=\"fa fa-times\"></i></button>\n\n    if @state.urlInputValue.length is 0 or @state.urlInputValue isnt @_initialUrl()\n      widthCorrection += 32\n      checkBtn = <button className=\"btn btn-icon\"\n                      style={float: \"right\"}\n                      onKeyDown={@_detectEscape}\n                      onKeyPress={@_saveUrlOnEnter}\n                      onMouseDown={@_saveUrl}><i className=\"fa fa-check\"></i></button>\n\n    <div className=\"toolbar-new-link\">\n      <i className=\"fa fa-link preview-btn-icon\"></i>\n      <input type=\"text\"\n             ref=\"urlInput\"\n             style={height: 34, width: \"calc(100% - #{widthCorrection}px)\"}\n             value={@state.urlInputValue}\n             onBlur={@_onBlur}\n             onKeyDown={@_detectEscape}\n             onKeyPress={@_saveUrlOnEnter}\n             onChange={@_onInputChange}\n             className=\"floating-toolbar-input\"\n             placeholder=\"Paste or type a link\" />\n      {removeBtn}\n      {checkBtn}\n    </div>\n\n  # Clicking the save or remove buttons will take precendent over simply\n  # bluring the field.\n  _onBlur: (event) =>\n    targets = []\n    if @refs[\"saveBtn\"]\n      targets.push ReactDOM.findDOMNode(@refs[\"saveBtn\"])\n    if @refs[\"removeBtn\"]\n      targets.push ReactDOM.findDOMNode(@refs[\"removeBtn\"])\n\n    if event.relatedTarget in targets\n      event.preventDefault()\n      return\n    else\n      @_saveUrl()\n\n  _saveUrl: =>\n    if @state.urlInputValue.trim().length > 0\n      @props.onSaveUrl @state.urlInputValue, @props.linkToModify\n    @props.onDoneWithLink()\n\n  _onInputChange: (event) =>\n    @setState urlInputValue: event.target.value\n\n  _detectEscape: (event) =>\n    if event.key is \"Escape\"\n      @props.onDoneWithLink()\n\n  _saveUrlOnEnter: (event) =>\n    if event.key is \"Enter\"\n      if @state.urlInputValue.trim().length > 0\n        @_saveUrl()\n      else\n        @_removeUrl()\n\n  # We signify the removal of a url with an empty string. This protects us\n  # from the case where people delete the url text and hit save. In that\n  # case we also want to remove the link.\n  _removeUrl: =>\n    @setState urlInputValue: \"\"\n    @props.onSaveUrl \"\", @props.linkToModify\n    @props.onDoneWithLink()\n\n  _initialUrl: (props=@props) =>\n    initialUrl = props.linkToModify?.getAttribute('href') ? \"\"\n    if initialUrl.length is 0\n      textContent = props.linkToModify?.textContent ? \"\"\n      if RegExpUtils.urlRegex(matchEntireString: true).test(textContent)\n        initialUrl = textContent\n\n    return initialUrl\n\n\nmodule.exports = LinkEditor\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/link-manager.es6",
    "content": "import { RegExpUtils, DOMUtils, ContenteditableExtension } from 'nylas-exports';\nimport LinkEditor from './link-editor';\n\nexport default class LinkManager extends ContenteditableExtension {\n  static keyCommandHandlers() {\n    return {\n      \"contenteditable:insert-link\": LinkManager._onInsertLink,\n    };\n  }\n\n  static toolbarButtons() {\n    return [{\n      className: \"btn-link\",\n      onClick: LinkManager._onInsertLink,\n      tooltip: \"Edit Link\",\n      iconUrl: null, // Defined in the css of btn-link\n    }];\n  }\n\n  // By default, if you're typing next to an existing anchor tag, it won't\n  // continue the anchor text. This is important for us since we want you\n  // to be able to select and then override the existing anchor text with\n  // something new.\n  static onContentChanged({editor}) {\n    const sel = editor.currentSelection();\n    if (sel.anchorNode && sel.isCollapsed) {\n      const node = sel.anchorNode;\n      const sibling = node.previousSibling;\n\n      if (!sibling) {\n        return;\n      }\n      if (sel.anchorOffset > 1) {\n        return;\n      }\n      if (node.nodeType !== Node.TEXT_NODE) {\n        return;\n      }\n      if (sibling.nodeName !== \"A\") {\n        return;\n      }\n      if (/^\\s+/.test(node.data)) {\n        return;\n      }\n      if (RegExpUtils.punctuation({exclude: ['\\\\-', '_']}).test(node.data[0])) {\n        return;\n      }\n\n      if (node.data.length > 1) {\n        node.splitText(1);\n      }\n      sibling.appendChild(node);\n      sibling.normalize();\n      const text = DOMUtils.findLastTextNode(sibling);\n      editor.select(text, text.length, text, text.length);\n    }\n  }\n\n  static toolbarComponentConfig({toolbarState}) {\n    if (toolbarState.dragging || toolbarState.doubleDown) {\n      return null;\n    }\n\n    let linkToModify = null;\n    if (!linkToModify && toolbarState.selectionSnapshot) {\n      linkToModify = LinkManager._linkAtCursor(toolbarState);\n    }\n\n    if (!linkToModify) {\n      return null;\n    }\n\n    return {\n      component: LinkEditor,\n      props: {\n        onSaveUrl: (url, link) => {\n          toolbarState.atomicEdit(LinkManager._onSaveUrl, {url, linkToModify: link});\n        },\n        onDoneWithLink: () => {\n          toolbarState.atomicEdit(LinkManager._onDoneWithLink)\n        },\n        linkToModify: linkToModify,\n        focusOnMount: LinkManager._shouldFocusOnMount(toolbarState),\n      },\n      locationRefNode: linkToModify,\n      width: LinkManager._linkWidth(linkToModify),\n      height: 34,\n    };\n  }\n\n  static _shouldFocusOnMount(toolbarState) {\n    return !toolbarState.selectionSnapshot.isCollapsed;\n  }\n\n  static _linkAttributeHref(linkToModify) {\n    return ((linkToModify && linkToModify.getAttribute) ? linkToModify.getAttribute('href') : null) || \"\";\n  }\n\n  static _linkWidth(linkToModify) {\n    const href = LinkManager._linkAttributeHref(linkToModify);\n    const WIDTH_PER_CHAR = 8;\n    return Math.max((href.length * WIDTH_PER_CHAR) + 95, 210);\n  }\n\n  static _linkAtCursor(toolbarState) {\n    if (toolbarState.selectionSnapshot.isCollapsed) {\n      const anchor = toolbarState.selectionSnapshot.anchorNode;\n      const node = DOMUtils.closest(anchor, 'a, n1-prompt-link');\n      const lastTextNode = DOMUtils.findLastTextNode(anchor);\n      if (lastTextNode && toolbarState.selectionSnapshot.anchorOffset === lastTextNode.data.length) {\n        return null;\n      }\n      return node;\n    }\n\n    const anchor = toolbarState.selectionSnapshot.anchorNode;\n    const focus = toolbarState.selectionSnapshot.anchorNode;\n    const aPrompt = DOMUtils.closest(anchor, 'n1-prompt-link');\n    const fPrompt = DOMUtils.closest(focus, 'n1-prompt-link');\n    if (aPrompt && fPrompt && aPrompt === fPrompt) {\n      const aTag = DOMUtils.closest(aPrompt, 'a');\n      return aTag || aPrompt;\n    }\n    return null;\n  }\n\n  // TODO FIXME: Unfortunately, the keyCommandHandler fires before the\n  // Contentedtiable onKeyDown.\n  //\n  // Normally this wouldn't matter, but when `_onInsertLink` runs it will\n  // focus on the input box of the link editor.\n  //\n  // If onKeyDown in the Contenteditable runs after this, then\n  // `atomicUpdate` will reset the selection back to the Contenteditable.\n  // This process blurs the link input, which causes the LinkInput to close\n  // and attempt to set or clear the link. The net effect is that the link\n  // insertion appears to not work via keyboard commands.\n  //\n  // This would not be a problem if the rendering of the Toolbar happened\n  // at the same time as the Contenteditable's render cycle. Unfortunatley\n  // since the Contenteditable shouldn't re-render on all Selection\n  // changes, while the Toolbar should, these are out of sync.\n  //\n  // The temporary fix is adding a _.defer block to change the ordering of\n  // these keyboard events.\n  static _onInsertLink({editor}) {\n    setTimeout(() => {\n      if (editor.currentSelection().isCollapsed) {\n        const html = \"<n1-prompt-link>link text</n1-prompt-link>\";\n        editor.insertHTML(html, {selectInsertion: true});\n      } else {\n        editor.wrapSelection(\"n1-prompt-link\");\n      }\n    }, 0);\n  }\n\n  static _onDoneWithLink({editor}) {\n    for (const node of Array.from(editor.rootNode.querySelectorAll(\"n1-prompt-link\"))) {\n      editor.unwrapNodeAndSelectAll(node);\n    }\n  }\n\n  static _onSaveUrl({editor, url, linkToModify}) {\n    let toSelect = null;\n\n    if (linkToModify != null) {\n      const equivalentNode = DOMUtils.findSimilarNodeAtIndex(editor.rootNode, linkToModify, 0);\n      if (equivalentNode == null) {\n        return;\n      }\n      if (LinkManager._linkAttributeHref(linkToModify).trim() === url.trim()) {\n        return;\n      }\n      toSelect = equivalentNode;\n    } else {\n      // When atomicEdit gets run, the exportedSelection is already restored to\n      // the last saved exportedSelection state. Any operation we perform will\n      // apply to the last saved exportedSelection state.\n      toSelect = null;\n    }\n\n    if (url.trim().length === 0) {\n      if (toSelect) {\n        editor.select(toSelect).unlink();\n      } else {\n        editor.unlink();\n      }\n    } else {\n      if (toSelect) {\n        editor.select(toSelect).createLink(url);\n      } else {\n        editor.createLink(url);\n      }\n    }\n\n    for (const node of Array.from(editor.rootNode.querySelectorAll(\"n1-prompt-link\"))) {\n      editor.unwrapNodeAndSelectAll(node);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/list-manager.es6",
    "content": "import { DOMUtils, ContenteditableExtension } from 'nylas-exports';\n\nexport default class ListManager extends ContenteditableExtension {\n  static keyCommandHandlers() {\n    return {\n      \"contenteditable:numbered-list\": this._insertNumberedList,\n      \"contenteditable:bulleted-list\": this._insertBulletedList,\n    };\n  }\n\n  static onContentChanged({editor}) {\n    if (this._spaceEntered && this.hasListStartSignature(editor.currentSelection())) {\n      this.createList(editor);\n    }\n\n    return this._collapseAdjacentLists(editor);\n  }\n\n  static onKeyDown({editor, event}) {\n    this._spaceEntered = event.key === \" \";\n    if (DOMUtils.isInList()) {\n      if (event.key === \"Backspace\" && DOMUtils.atStartOfList()) {\n        event.preventDefault();\n        this.outdentListItem(editor);\n      } else if (event.key === \"Tab\" && editor.currentSelection().isCollapsed) {\n        event.preventDefault();\n        if (event.shiftKey) {\n          this.outdentListItem(editor);\n        } else {\n          editor.indent();\n        }\n      } else {\n        // Do nothing, let the event through.\n        this.originalInput = null;\n      }\n    } else {\n      this.originalInput = null;\n    }\n\n    return event;\n  }\n\n  static bulletRegex() { return /^[*-]\\s[^\\S]*/; }\n\n  static numberRegex() { return /^\\d\\.\\s[^\\S]*/; }\n\n  static hasListStartSignature(selection) {\n    if (!selection || !selection.anchorNode) {\n      return false;\n    }\n    if (!selection.isCollapsed) {\n      return false;\n    }\n\n    const sibling = selection.anchorNode.previousElementSibling;\n    if (!sibling || DOMUtils.looksLikeBlockElement(sibling)) {\n      const text = selection.anchorNode.textContent;\n      return this.numberRegex().test(text) || this.bulletRegex().test(text);\n    }\n    return false;\n  }\n\n  static createList(editor) {\n    const anchorNode = editor.currentSelection().anchorNode;\n    const text = anchorNode ? anchorNode.textContent : null;\n    if (!text) {\n      return;\n    }\n    if (this.numberRegex().test(text)) {\n      this.originalInput = text.slice(0, 3);\n      this.insertList(editor, {ordered: true});\n      this.removeListStarter(this.numberRegex(), editor.currentSelection());\n    } else if (this.bulletRegex().test(text)) {\n      this.originalInput = text.slice(0, 2);\n      this.insertList(editor, {ordered: false});\n      this.removeListStarter(this.bulletRegex(), editor.currentSelection());\n    } else {\n      return;\n    }\n    const el = DOMUtils.closest(editor.currentSelection().anchorNode, \"li\");\n    DOMUtils.Mutating.removeEmptyNodes(el);\n  }\n\n  static removeListStarter(starterRegex, selection) {\n    const el = DOMUtils.closest(selection.anchorNode, \"li\");\n    const textContent = el.textContent.replace(starterRegex, \"\");\n\n    if (textContent.trim().length === 0) {\n      el.innerHTML = \"<br>\";\n    } else {\n      const textNode = DOMUtils.findFirstTextNode(el);\n      textNode.textContent = textNode.textContent.replace(starterRegex, \"\");\n    }\n  }\n\n  // From a newly-created list\n  // Outdent returns to a <div><br/></div> structure\n  // I need to turn into <div>-&nbsp;</div>\n  //\n  // From a list with content\n  // Outent returns to <div>sometext</div>\n  // We need to turn that into <div>-&nbsp;sometext</div>\n  static restoreOriginalInput(editor) {\n    const node = editor.currentSelection().anchorNode;\n    if (!node) {\n      return;\n    }\n\n    if (node.nodeType === Node.TEXT_NODE) {\n      node.textContent = this.originalInput + node.textContent;\n    } else if (node.nodeType === Node.ELEMENT_NODE) {\n      const textNode = DOMUtils.findFirstTextNode(node);\n      if (!textNode) {\n        node.innerHTML = this.originalInput.replace(\" \", \"&nbsp;\") + node.innerHTML;\n      } else {\n        textNode.textContent = this.originalInput + textNode.textContent;\n      }\n    }\n\n    if (this.numberRegex().test(this.originalInput)) {\n      DOMUtils.Mutating.moveSelectionToIndexInAnchorNode(editor.currentSelection(), 3); // digit plus dot\n    }\n    if (this.bulletRegex().test(this.originalInput)) {\n      DOMUtils.Mutating.moveSelectionToIndexInAnchorNode(editor.currentSelection(), 2); // dash or star\n    }\n\n    this.originalInput = null;\n  }\n\n  static insertList(editor, {ordered}) {\n    const node = editor.currentSelection().anchorNode;\n    if (this.isInsideListItem(node)) {\n      editor.indent();\n    } else {\n      if (ordered === true) {\n        editor.insertOrderedList();\n      } else {\n        editor.insertUnorderedList();\n      }\n    }\n  }\n\n  static _insertNumberedList({editor}) {\n    return editor.insertOrderedList();\n  }\n\n  static _insertBulletedList({editor}) {\n    return editor.insertUnorderedList();\n  }\n\n  static outdentListItem(editor) {\n    if (this.originalInput) {\n      editor.outdent();\n      this.restoreOriginalInput(editor);\n    }\n    editor.outdent();\n  }\n\n  static isInsideListItem(node) {\n    return DOMUtils.isDescendantOf(node, parent => parent.tagName === 'LI');\n  }\n\n  // If users ended up with two <ul> lists adjacent to each other, we\n  // collapse them into one. We leave adjacent <ol> lists intact in case\n  // the user wanted to restart the numbering sequence\n  static _collapseAdjacentLists(editor) {\n    const els = editor.rootNode.querySelectorAll('ul, ol');\n\n    // This mutates the DOM in place.\n    return DOMUtils.Mutating.collapseAdjacentElements(els);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/mouse-service.es6",
    "content": "import { DOMUtils } from 'nylas-exports';\nimport ContenteditableService from './contenteditable-service';\n\nclass MouseService extends ContenteditableService {\n  constructor(...args) {\n    super(...args);\n    this.setup();\n    this.timer = null;\n    this._inFrame = true;\n  }\n\n  eventHandlers() {\n    return {\n      onClick: this._onClick,\n      onMouseOver: this._onMouseOver,\n      onMouseEnter: () => { this._inFrame = true },\n      onMouseLeave: () => { this._inFrame = false },\n    };\n  }\n\n  // NOTE: We can't use event.preventDefault() here for <a> tags because\n  // the window-event-handler.coffee file has already caught the event.\n\n  // We use global listeners to determine whether or not dragging is\n  // happening. This is because dragging may stop outside the scope of\n  // this element. Note that the `dragstart` and `dragend` events don't\n  // detect text selection. They are for drag & drop.\n  setup() {\n    window.addEventListener(\"mousedown\", this._onMouseDown);\n    window.addEventListener(\"mouseup\", this._onMouseUp);\n  }\n\n  teardown() {\n    window.removeEventListener(\"mousedown\", this._onMouseDown);\n    window.removeEventListener(\"mouseup\", this._onMouseUp);\n  }\n\n  _onClick = (event) => {\n    // We handle mouseDown, mouseMove, mouseUp, but we want to stop propagation\n    // of `click` to make it clear that we've handled the event.\n    // Note: Related to composer-view#_onClickComposeBody\n    return event.stopPropagation();\n  }\n\n  _onMouseDown = (event) => {\n    this._mouseDownEvent = event;\n    this._mouseHasMoved = false;\n    window.addEventListener(\"mousemove\", this._onMouseMove);\n\n    // We can't use the native double click event because that only fires\n    // on the second up-stroke\n    if (Date.now() - (this._lastMouseDown || 0) < 250) {\n      this._onDoubleDown(event);\n      this._lastMouseDown = 0; // to prevent triple down\n    } else {\n      this._lastMouseDown = Date.now();\n    }\n  }\n\n  _onDoubleDown = (event) => {\n    const editable = this.innerState.editableNode;\n    if (editable == null) {\n      return;\n    }\n    if (editable === event.target || editable.contains(event.target)) {\n      this.setInnerState({doubleDown: true});\n    }\n  }\n\n  _onMouseMove = () => {\n    if (!this._mouseHasMoved) {\n      this._onDragStart(this._mouseDownEvent);\n      this._mouseHasMoved = true;\n    }\n  }\n\n  _onMouseUp = (event) => {\n    window.removeEventListener(\"mousemove\", this._onMouseMove);\n\n    if (this.innerState.doubleDown) {\n      this.setInnerState({doubleDown: false});\n    }\n\n    if (this._mouseHasMoved) {\n      this._mouseHasMoved = false;\n      this._onDragEnd(event);\n    }\n\n    const {editableNode} = this.innerState;\n    const selection = document.getSelection();\n    if (!DOMUtils.selectionInScope(selection, editableNode)) {\n      return;\n    }\n\n    this.dispatchEventToExtensions(\"onClick\", event);\n  }\n\n  _onDragStart = (event) => {\n    const editable = this.innerState.editableNode;\n    if (editable == null) {\n      return;\n    }\n    if (editable === event.target || editable.contains(event.target)) {\n      this.setInnerState({dragging: true});\n    }\n  }\n\n  _onDragEnd = () => {\n    if (this.innerState.dragging) {\n      this.setInnerState({dragging: false});\n    }\n  }\n\n  // Floating toolbar plugins need to know what we're currently hovering\n  // over. We take care of debouncing the event handlers here to prevent\n  // flooding plugins with events.\n  _onMouseOver = () => {\n    // @setInnerState hoveringOver: event.target\n  }\n}\n\nexport default MouseService;\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/paragraph-formatting-extension.es6",
    "content": "import {ContenteditableExtension} from 'nylas-exports';\n\n// This provides the default baisc formatting options for the\n// Contenteditable using the declarative extension API.\n//\n// NOTE: Blockquotes get their own formatting in `BlockquoteManager`\nexport default class ParagraphFormattingExtension extends ContenteditableExtension {\n  static keyCommandHandlers() {\n    return {\n      \"contenteditable:indent\": this._onIndent,\n      \"contenteditable:outdent\": this._onOutdent,\n    };\n  }\n\n  static toolbarButtons() {\n    return [];\n  }\n\n  static _onIndent({editor}) {\n    editor.indent();\n  }\n\n  static _onOutdent({editor}) {\n    editor.outdent();\n  }\n\n  // None of the paragraph formatting buttons need a custom component.\n  //\n  // They use the default <ToolbarButtons> component via the\n  // `toolbarButtons` extension API.\n  //\n  // We can either return `null` or return the requsted object with no\n  // component.\n  static toolbarComponentConfig() {\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/tab-manager.es6",
    "content": "import { DOMUtils, ContenteditableExtension } from 'nylas-exports';\n\nexport default class TabManager extends ContenteditableExtension {\n  static onKeyDown({editor, event}) {\n    // This is a special case where we don't want to bubble up the event to\n    // the keymap manager if the extension prevented the default behavior\n    if (event.defaultPrevented) {\n      event.stopPropagation();\n      return;\n    }\n\n    if (event.key === \"Tab\") {\n      this._onTabDownDefaultBehavior(editor, event);\n      return;\n    }\n  }\n\n  static _onTabDownDefaultBehavior(editor, event) {\n    const selection = editor.currentSelection();\n\n    if (selection && selection.isCollapsed) {\n      if (event.shiftKey) {\n        if (DOMUtils.isAtTabChar(selection)) {\n          this._removeLastCharacter(editor);\n        } else {\n          return; // Don't stop propagation\n        }\n      } else {\n        editor.insertText(\"\\t\");\n      }\n    } else {\n      if (event.shiftKey) {\n        editor.insertText(\"\");\n      } else {\n        editor.insertText(\"\\t\");\n      }\n    }\n    event.preventDefault();\n    event.stopPropagation();\n  }\n\n  static _removeLastCharacter(editor) {\n    if (DOMUtils.isSelectionInTextNode(editor.currentSelection())) {\n      const node = editor.currentSelection().anchorNode;\n      const offset = editor.currentSelection().anchorOffset;\n      editor.currentSelection().setBaseAndExtent(node, offset - 1, node, offset);\n      editor.delete();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/toolbar-button-manager.es6",
    "content": "import { DOMUtils, ContenteditableExtension } from 'nylas-exports';\nimport ToolbarButtons from './toolbar-buttons';\n\n// This contains the logic to declaratively render the core\n// <ToolbarButtons> component in a <FloatingToolbar>\nexport default class ToolbarButtonManager extends ContenteditableExtension {\n\n  // See the {EmphasisFormattingExtension} and {LinkManager} and other\n  // extensions for toolbarButtons.\n  static toolbarButtons() { return []; }\n\n  static toolbarComponentConfig({toolbarState}) {\n    if (toolbarState.dragging || toolbarState.doubleDown) { return null; }\n    if (!toolbarState.selectionSnapshot) { return null; }\n    if (toolbarState.selectionSnapshot.isCollapsed) { return null; }\n\n    const locationRef = DOMUtils.getRangeInScope(toolbarState.editableNode);\n    if (!locationRef) { return null; }\n\n    const buttonConfigs = this._toolbarButtonConfigs(toolbarState);\n    const range = DOMUtils.getRangeInScope(toolbarState.editableNode);\n    if (!range || !range.startContainer) {\n      return null;\n    }\n\n    let locationRefNode = null;\n    if (range.startContainer.nodeType === Node.ELEMENT_NODE) {\n      locationRefNode = range.startContainer.childNodes[range.startOffset];\n    }\n    if (!locationRefNode) {\n      locationRefNode = range;\n    }\n\n    return {\n      locationRefNode: locationRefNode,\n      component: ToolbarButtons,\n      width: buttonConfigs.length * 28.5,\n      height: 34,\n      props: {\n        buttonConfigs,\n      },\n    };\n  }\n\n  static _toolbarButtonConfigs(toolbarState) {\n    const {extensions, atomicEdit} = toolbarState;\n    let buttonConfigs = [];\n\n    for (const extension of extensions) {\n      try {\n        const extensionConfigs = (extension.toolbarButtons ? extension.toolbarButtons({toolbarState}) : null) || [];\n        extensionConfigs.map((config) => {\n          const innerClick = config.onClick || (() => {});\n          config.onClick = (event) => atomicEdit(innerClick, {event});\n          return config;\n        });\n        buttonConfigs = buttonConfigs.concat(extensionConfigs);\n      } catch (error) {\n        NylasEnv.reportError(error);\n      }\n    }\n\n    return buttonConfigs;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/contenteditable/toolbar-buttons.jsx",
    "content": "import React from 'react';\nimport {RetinaImg} from 'nylas-component-kit';\n\n// This component renders buttons and is the default view in the\n// FloatingToolbar.\n\n// Extensions that implement `toolbarButtons` can get their buttons added\n// in.\n\n// The {EmphasisFormattingExtension} extension is an example of one that\n// implements this spec.\nexport default class ToolbarButtons extends React.Component {\n  static displayName = \"ToolbarButtons\"\n\n  static propTypes = {\n    // Declares what buttons should appear in the toolbar. An array of\n    // config objects.\n    buttonConfigs: React.PropTypes.array,\n  }\n\n  static defaultProps = {\n    buttonConfigs: [],\n  }\n\n  render() {\n    const buttons = this.props.buttonConfigs.map((config, i) => {\n      const icon = ((config.iconUrl || '').length > 0) ? (\n        <RetinaImg\n          mode={RetinaImg.Mode.ContentIsMask}\n          url=\"#{config.iconUrl}\"\n        />\n      ) : null;\n\n      return (\n        <button\n          className={`btn toolbar-btn ${config.className || ''}`}\n          key={`btn-${i}`}\n          onClick={config.onClick}\n          title={`${config.tooltip}`}\n        >\n          {icon}\n        </button>\n      );\n    });\n\n    return (\n      <div className=\"toolbar-buttons\">\n        {buttons}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/date-input.jsx",
    "content": "/* eslint jsx-a11y/tabindex-no-positive: 0 */\nimport classnames from 'classnames';\nimport React, {Component, PropTypes} from 'react';\nimport {DateUtils} from 'nylas-exports';\n\n\nclass DateInput extends Component {\n  static displayName = 'DateInput';\n\n  static propTypes = {\n    className: PropTypes.string,\n    dateFormat: PropTypes.string.isRequired,\n    onDateInterpreted: PropTypes.func,\n    onDateSubmitted: PropTypes.func,\n  };\n\n  static defaultProps = {\n    onDateInterpreted: () => {},\n    onDateSubmitted: () => {},\n  };\n\n  constructor(props) {\n    super(props)\n    this._mounted = false\n    this.state = {\n      inputDate: null,\n      inputValue: '',\n    }\n  }\n\n  componentDidMount() {\n    this._mounted = true\n  }\n\n  componentWillUnmount() {\n    this._mounted = false\n  }\n\n  onInputKeyDown = (event) => {\n    const {key, target: {value}} = event;\n    if (value.length > 0 && [\"Enter\", \"Return\"].includes(key)) {\n      // This prevents onInputChange from being fired\n      event.stopPropagation();\n      const date = DateUtils.futureDateFromString(value);\n      this.props.onDateSubmitted(date, value);\n    }\n  };\n\n  onInputChange = (event) => {\n    const {target: {value}} = event\n    const nextDate = DateUtils.futureDateFromString(value)\n    if (nextDate) {\n      this.props.onDateInterpreted(nextDate.clone(), value)\n    }\n    this.setState({inputDate: nextDate, inputValue: value});\n  };\n\n  clearInput() {\n    setImmediate(() => {\n      if (!this._mounted) { return }\n      this.setState({inputValue: '', inputDate: null})\n    })\n  }\n\n  render() {\n    const {className} = this.props\n    const {inputDate, inputValue} = this.state\n    const classes = classnames({\n      \"nylas-date-input\": true,\n      [className]: className != null,\n    })\n    const formatted = (\n      <span className=\"date-interpretation\">\n        {DateUtils.format(this.state.inputDate, this.props.dateFormat)}\n      </span>\n    )\n    const dateInterpretation = inputDate ? formatted : <span />\n\n    return (\n      <div className={classes}>\n        <input\n          tabIndex=\"1\"\n          type=\"text\"\n          value={inputValue}\n          placeholder=\"Or, 'next Monday at 2PM'\"\n          onKeyDown={this.onInputKeyDown}\n          onChange={this.onInputChange}\n        />\n        {dateInterpretation}\n      </div>\n    )\n  }\n}\n\nexport default DateInput\n"
  },
  {
    "path": "packages/client-app/src/components/date-picker-popover.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {Actions, DateUtils} from 'nylas-exports'\nimport DateInput from './date-input'\nimport Menu from './menu'\n\n\nconst {DATE_FORMAT_SHORT, DATE_FORMAT_LONG} = DateUtils\n\nclass DatePickerPopover extends Component {\n  static displayName = 'DatePickerPopover'\n\n  static propTypes = {\n    className: PropTypes.string,\n    footer: PropTypes.node,\n    onSelectDate: PropTypes.func,\n    header: PropTypes.node.isRequired,\n    dateOptions: PropTypes.object.isRequired,\n    shouldSelectDateWhenInterpreted: PropTypes.bool,\n  }\n\n  static defaultProps = {\n    shouldSelectDateWhenInterpreted: false,\n  }\n\n  onEscape() {\n    Actions.closePopover()\n  }\n\n  onSelectMenuOption = (optionKey) => {\n    const {dateOptions} = this.props\n    const date = dateOptions[optionKey]();\n    this.refs.dateInput.clearInput()\n    this.selectDate(date, optionKey);\n  };\n\n  onCustomDateInterpreted = (date) => {\n    const {shouldSelectDateWhenInterpreted} = this.props\n    if (date && shouldSelectDateWhenInterpreted) {\n      this.refs.menu.clearSelection()\n      this.selectDate(date, \"Custom\");\n    }\n  }\n\n  onCustomDateSelected = (date, inputValue) => {\n    if (date) {\n      this.refs.menu.clearSelection()\n      this.selectDate(date, \"Custom\");\n    } else {\n      NylasEnv.showErrorDialog(`Sorry, we can't interpret ${inputValue} as a valid date.`);\n    }\n  };\n\n  selectDate = (date, dateLabel) => {\n    const formatted = DateUtils.format(date.utc());\n    this.props.onSelectDate(formatted, dateLabel);\n  };\n\n  renderMenuOption = (optionKey) => {\n    const {dateOptions} = this.props\n    const date = dateOptions[optionKey]();\n    const formatted = DateUtils.format(date, DATE_FORMAT_SHORT);\n    return (\n      <div className=\"date-picker-popover-option\">\n        {optionKey}\n        <span className=\"time\">{formatted}</span>\n      </div>\n    );\n  }\n\n  render() {\n    const {className, header, footer, dateOptions} = this.props\n\n    let footerComponents = [\n      <div key=\"divider\" className=\"divider\" />,\n      <DateInput\n        ref=\"dateInput\"\n        key=\"custom-section\"\n        className=\"section date-input-section\"\n        dateFormat={DATE_FORMAT_LONG}\n        onDateSubmitted={this.onCustomDateSelected}\n        onDateInterpreted={this.onCustomDateInterpreted}\n      />,\n    ]\n    if (footer) {\n      if (Array.isArray(footer)) {\n        footerComponents = footerComponents.concat(footer)\n      } else {\n        footerComponents = footerComponents.concat([footer])\n      }\n    }\n\n    return (\n      <div className={`date-picker-popover ${className}`}>\n        <Menu\n          ref=\"menu\"\n          items={Object.keys(dateOptions)}\n          itemKey={item => item}\n          itemContent={this.renderMenuOption}\n          defaultSelectedIndex={-1}\n          headerComponents={header}\n          footerComponents={footerComponents}\n          onEscape={this.onEscape}\n          onSelect={this.onSelectMenuOption}\n        />\n      </div>\n    );\n  }\n}\n\nexport default DatePickerPopover\n"
  },
  {
    "path": "packages/client-app/src/components/date-picker.jsx",
    "content": "import React from 'react'\nimport moment from 'moment'\nimport classnames from 'classnames'\nimport {DateUtils} from 'nylas-exports'\nimport {MiniMonthView} from 'nylas-component-kit'\n\nexport default class DatePicker extends React.Component {\n  static displayName = \"DatePicker\";\n\n  static propTypes = {\n    value: React.PropTypes.number,\n    onChange: React.PropTypes.func,\n    dateFormat: React.PropTypes.string,\n  }\n\n  static contextTypes = {\n    parentTabGroup: React.PropTypes.object,\n  }\n\n  static defaultProps = {\n    dateFormat: null, // Default to valueOf\n    onChange: () => {},\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {focused: false}\n  }\n\n  value() {\n    return this.props.value ? moment(this.props.value) : null\n  }\n\n  _onChange(newMoment) {\n    if (this.props.dateFormat) {\n      return this.props.onChange(newMoment.format(this.props.dateFormat))\n    }\n    return this.props.onChange(newMoment.valueOf())\n  }\n\n  _moveDay(numDays) {\n    const val = this.value()\n    const day = val.dayOfYear();\n    this._onChange(val.dayOfYear(day + numDays))\n  }\n\n  _onKeyDown = (event) => {\n    if (event.key === \"ArrowLeft\") {\n      this._moveDay(-1)\n    } else if (event.key === \"ArrowRight\") {\n      this._moveDay(1)\n    } else if (event.key === \"ArrowUp\") {\n      this._moveDay(-7)\n    } else if (event.key === \"ArrowDown\") {\n      this._moveDay(7)\n    } else if (event.key === \"Enter\") {\n      this.context.parentTabGroup.shiftFocus(1);\n    }\n  }\n\n  _onFocus = () => {\n    this.setState({focused: true})\n  }\n\n  _onBlur = () => {\n    this.setState({focused: false})\n  }\n\n  _onSelectDay = (newTimestamp) => {\n    this._onChange(moment(newTimestamp))\n    this.context.parentTabGroup.shiftFocus(1);\n  }\n\n  _renderMiniMonthView() {\n    if (this.state.focused) {\n      return (\n        <div className=\"mini-month-view-wrap\">\n          <MiniMonthView\n            onChange={this._onSelectDay}\n            value={this.value()}\n          />\n        </div>\n      )\n    }\n    return false\n  }\n\n  render() {\n    const className = classnames({\n      'day-text': true,\n      'focused': this.state.focused,\n    })\n\n    const val = this.value();\n    let dayTxt = \"Click to set date\"\n    if (val) {\n      dayTxt = this.value().format(DateUtils.DATE_FORMAT_llll_NO_TIME)\n    }\n\n    return (\n      <div\n        tabIndex={0}\n        className=\"date-picker\"\n        onKeyDown={this._onKeyDown} onFocus={this._onFocus}\n        onBlur={this._onBlur}\n      >\n        <div className={className}>{dayTxt}</div>\n        {this._renderMiniMonthView()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/decorators/auto-focuses.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport {findDOMNode} from 'react-dom'\n\n\nconst FOCUSABLE_SELECTOR = 'input, textarea, [contenteditable], [tabIndex]'\n\nfunction AutoFocuses(ComposedComponent, {onMount = true, onUpdate = true} = {}) {\n  return class extends ComposedComponent {\n    static displayName = ComposedComponent.displayName\n    static containerRequired = ComposedComponent.containerRequired;\n    static containerStyles = ComposedComponent.containerStyles;\n\n    componentDidMount() {\n      this.mounted = true\n      if (onMount) {\n        this.focusElementWithTabIndex()\n      }\n    }\n\n    componentWillUnmount() {\n      this.mounted = false\n    }\n\n    componentDidUpdate() {\n      if (onUpdate) {\n        this.focusElementWithTabIndex()\n      }\n    }\n\n    isFocusable(currentNode = findDOMNode(this)) {\n      currentNode.focus()\n      return document.activeElement === currentNode\n    }\n\n    focusElementWithTabIndex = () => {\n      if (!this.mounted) {\n        return\n      }\n      // Automatically focus the element inside us with the lowest tab index\n      const currentNode = findDOMNode(this);\n      if (currentNode.contains(document.activeElement)) {\n        return\n      }\n\n      if (this.isFocusable(currentNode)) {\n        currentNode.focus()\n        return\n      }\n\n      // _.sortBy ranks in ascending numerical order.\n      const focusable = currentNode.querySelectorAll(FOCUSABLE_SELECTOR);\n      const matches = _.sortBy(focusable, (node) => {\n        if (node.tabIndex > 0) {\n          return node.tabIndex;\n        } else if (node.nodeName === \"INPUT\") {\n          return 1000000\n        }\n        return 1000001\n      })\n      if (matches[0]) {\n        matches[0].focus();\n      }\n    }\n\n    render() {\n      return (\n        <ComposedComponent\n          {...this.props}\n          focusElementWithTabIndex={this.focusElementWithTabIndex}\n        />\n      )\n    }\n  }\n}\n\nexport default AutoFocuses\n"
  },
  {
    "path": "packages/client-app/src/components/decorators/compose.es6",
    "content": "\nexport default function compose(BaseComponent, ...decorators) {\n  const ComposedComponent =\n    decorators.reduce((comp, decorator) => decorator(comp), BaseComponent)\n  ComposedComponent.propTypes = BaseComponent.propTypes\n  ComposedComponent.displayName = BaseComponent.displayName\n  ComposedComponent.containerRequired = BaseComponent.containerRequired;\n  ComposedComponent.containerStyles = BaseComponent.containerStyles;\n  return ComposedComponent\n}\n"
  },
  {
    "path": "packages/client-app/src/components/decorators/has-tutorial-tip.jsx",
    "content": "/* eslint react/no-danger: 0 */\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport _ from 'underscore';\n\nimport {Actions, WorkspaceStore, DOMUtils} from 'nylas-exports';\nimport NylasStore from 'nylas-store';\n\nconst TipsBackgroundEl = document.createElement('tutorial-tip-background');\n\nconst TipsContainerEl = document.createElement('div');\nTipsContainerEl.classList.add('tooltips-container');\ndocument.body.appendChild(TipsContainerEl);\n\n\nclass TipsStoreCls extends NylasStore {\n  constructor() {\n    super();\n\n    this._tipKeys = [];\n  }\n\n  isTipVisible(key) {\n    const seen = NylasEnv.config.get('core.tutorial.seen') || [];\n    return this._tipKeys.find(t => !seen.includes(t)) === key;\n  }\n\n  hasSeenTip(key) {\n    return (NylasEnv.config.get('core.tutorial.seen') || []).includes(key);\n  }\n\n  // Actions: Since this is a private store just inside this file, we call\n  // these methods directly for now.\n\n  mountedTip = (key) => {\n    if (!this._tipKeys.includes(key)) {\n      this._tipKeys.push(key);\n    }\n    this.trigger();\n  }\n\n  seenTip = (key) => {\n    this._tipKeys = this._tipKeys.filter(t => t !== key);\n    NylasEnv.config.pushAtKeyPath('core.tutorial.seen', key);\n    this.trigger();\n  }\n\n  unmountedTip = (key) => {\n    this._tipKeys = this._tipKeys.filter(t => t !== key);\n    this.trigger();\n  }\n}\n\nconst TipsStore = new TipsStoreCls();\n\nclass TipPopoverContents extends React.Component {\n  static propTypes = {\n    title: React.PropTypes.string,\n    tipKey: React.PropTypes.string,\n    instructions: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.element]),\n    onDismissed: React.PropTypes.func,\n  }\n\n  componentDidMount() {\n    if (TipsBackgroundEl.parentNode === null) {\n      document.body.appendChild(TipsBackgroundEl);\n    }\n    window.requestAnimationFrame(() => {\n      TipsBackgroundEl.classList.add('visible');\n    });\n  }\n\n  componentWillUnmount() {\n    TipsBackgroundEl.classList.remove('visible');\n    if (this.props.onDismissed) {\n      this.props.onDismissed();\n    }\n  }\n\n  onDone = () => {\n    TipsStore.seenTip(this.props.tipKey);\n    Actions.closePopover();\n  }\n\n  render() {\n    let content = null;\n\n    if (typeof this.props.instructions === 'string') {\n      content = <p dangerouslySetInnerHTML={{__html: this.props.instructions}} />;\n    } else {\n      content = <p>{this.props.instructions}</p>\n    }\n\n    return (\n      <div style={{width: 250, padding: 20, paddingTop: 0}}>\n        <h2>{this.props.title}</h2>\n        {content}\n        <button className=\"btn\" onClick={this.onDone}>Got it!</button>\n      </div>\n    );\n  }\n}\n\nexport default function HasTutorialTip(ComposedComponent, TipConfig) {\n  const TipKey = ComposedComponent.displayName;\n\n  if (!TipKey) {\n    throw new Error(\"To use the HasTutorialTip decorator, your component must have a displayName.\");\n  }\n  if (TipsStore.hasSeenTip(TipKey)) {\n    return ComposedComponent;\n  }\n\n  return class extends React.Component {\n    static displayName = ComposedComponent.displayName;\n    static containerRequired = ComposedComponent.containerRequired;\n    static containerStyles = ComposedComponent.containerStyles;\n\n    constructor(props) {\n      super(props);\n      this._unlisteners = [];\n      this.state = {visible: false};\n    }\n\n    componentDidMount() {\n      TipsStore.mountedTip(TipKey);\n\n      this._unlisteners = [\n        TipsStore.listen(this._onTooltipStateChanged),\n        WorkspaceStore.listen(() => {\n          this._workspaceTimer = setTimeout(this._onTooltipStateChanged, 0);\n        }),\n      ]\n      this._disposables = [\n        NylasEnv.themes.onDidChangeActiveThemes(() => {\n          this._themesTimer = setTimeout(this._onRecomputeTooltipPosition, 0);\n        }),\n      ]\n      window.addEventListener('resize', this._onRecomputeTooltipPosition);\n\n      // unfortunately, we can't render() a container around ComposedComponent\n      // without modifying the DOM tree and messing with things like flexbox.\n      // Instead, we leave render() unchanged and attach the bubble and hover\n      // listeners to the DOM manually.\n      const el = ReactDOM.findDOMNode(this);\n\n      this.tipNode = document.createElement('div');\n      this.tipNode.classList.add('tutorial-tip');\n\n      this.tipAnchor = el.closest('[data-tooltips-anchor]') || document.body;\n      this.tipAnchor.querySelector('.tooltips-container').appendChild(this.tipNode);\n\n      el.addEventListener('mouseover', this._onMouseOver);\n      this._onTooltipStateChanged();\n    }\n\n    componentDidUpdate() {\n      if (this.state.visible) {\n        this._onRecomputeTooltipPosition();\n      }\n    }\n\n    componentWillUnmount() {\n      this._unlisteners.forEach((unlisten) => unlisten())\n      this._disposables.forEach((disposable) => disposable.dispose())\n\n      window.removeEventListener('resize', this._onRecomputeTooltipPosition);\n      this.tipNode.parentNode.removeChild(this.tipNode);\n      clearTimeout(this._workspaceTimer);\n      clearTimeout(this._themesTimer);\n\n      TipsStore.unmountedTip(TipKey);\n    }\n\n    _containingSheetIsVisible = (el) => {\n      const sheetEl = el.closest('.sheet') || el.closest('.sheet-toolbar-container');\n      if (!sheetEl) {\n        return true;\n      }\n      return (sheetEl.dataset.id === WorkspaceStore.topSheet().id);\n    }\n\n    _isVisible = () => {\n      const el = ReactDOM.findDOMNode(this);\n      return (\n        TipsStore.isTipVisible(TipKey) &&\n        this._containingSheetIsVisible(el) &&\n        DOMUtils.nodeIsVisible(el)\n      )\n    }\n\n    _onTooltipStateChanged = () => {\n      const visible = this._isVisible()\n      if (this.state.visible !== visible) {\n        this.setState({visible});\n        if (visible) {\n          this.tipNode.classList.add('visible');\n          this._onRecomputeTooltipPosition();\n        } else {\n          this.tipNode.classList.remove('visible');\n        }\n      }\n    }\n\n    _onMouseOver = () => {\n      if (!this.state.visible) {\n        return;\n      }\n\n      const el = ReactDOM.findDOMNode(this);\n      el.removeEventListener('mouseover', this._onMouseOver);\n\n      const tipRect = this.tipNode.getBoundingClientRect();\n      const tipFocusCircleRadius = 64;\n      const rect = ReactDOM.findDOMNode(this).getBoundingClientRect();\n      const rectCX = Math.round(rect.left + rect.width / 2 - tipFocusCircleRadius);\n      const rectCY = Math.round(rect.top + rect.height / 2 - tipFocusCircleRadius);\n      TipsBackgroundEl.style.webkitMaskPosition = `0 0, ${rectCX}px ${rectCY}px`;\n\n      Actions.openPopover((\n        <TipPopoverContents\n          tipKey={TipKey}\n          title={TipConfig.title}\n          instructions={TipConfig.instructions}\n          onDismissed={() => {\n            el.addEventListener('mouseover', this._onMouseOver);\n          }}\n        />\n      ), {\n        originRect: tipRect,\n        direction: 'down',\n        fallbackDirection: 'up',\n      })\n    }\n\n    _onRecomputeTooltipPosition = () => {\n      const el = ReactDOM.findDOMNode(this);\n      let settled = 0;\n      let last = {};\n      const attempt = () => {\n        const {left, top} = el.getBoundingClientRect();\n        const anchorRect = this.tipAnchor.getBoundingClientRect();\n\n        this.tipNode.style.left = `${left - anchorRect.left + 5}px`;\n        this.tipNode.style.top = `${Math.max(top - anchorRect.top + 5, 10)}px`;\n\n        if (!_.isEqual(last, {left, top})) {\n          settled = 0;\n          last = {left, top};\n        }\n        settled += 1;\n        if (settled < 5) {\n          window.requestAnimationFrame(attempt);\n        }\n      }\n      attempt();\n    }\n\n    render() {\n      return (\n        <ComposedComponent {...this.props} />\n      );\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/decorators/listens-to-flux-store.jsx",
    "content": "/* eslint no-prototype-builtins: 0 */\nimport React, {Component} from 'react';\n\nfunction ListensToFluxStore(ComposedComponent, {stores, getStateFromStores}) {\n  return class extends Component {\n    static displayName = ComposedComponent.displayName\n\n    static containerRequired = false;\n\n    static propTypes = ComposedComponent.propTypes\n\n    constructor(props) {\n      super(props);\n      this._unlisteners = [];\n      this.state = getStateFromStores(props);\n    }\n\n    componentDidMount() {\n      stores.forEach((store) => {\n        this._unlisteners.push(store.listen(() => {\n          this.setState(getStateFromStores(this.props));\n        }));\n      });\n    }\n\n    componentWillReceiveProps(nextProps) {\n      this.setState(getStateFromStores(nextProps));\n    }\n\n    componentWillUnmount() {\n      for (const unlisten of this._unlisteners) {\n        unlisten();\n      }\n      this._unlisteners = [];\n    }\n\n    render() {\n      const props = {\n        ...this.props,\n        ...this.state,\n      }\n      if (Component.isPrototypeOf(ComposedComponent)) {\n        props.ref = 'composed'\n      }\n      return <ComposedComponent {...props} />;\n    }\n  };\n}\n\nexport default ListensToFluxStore\n"
  },
  {
    "path": "packages/client-app/src/components/decorators/listens-to-movement-keys.jsx",
    "content": "import React from 'react'\nimport KeyCommandsRegion from '../key-commands-region'\n\nfunction ListensToMovementKeys(ComposedComponent) {\n  return class extends ComposedComponent {\n    static displayName = ComposedComponent.displayName\n    static containerRequired = ComposedComponent.containerRequired;\n    static containerStyles = ComposedComponent.containerStyles;\n\n    localKeyHandlers() {\n      return {\n        'core:previous-item': (event) => {\n          if (!(this.refs.composed || {}).onArrowUp) { return }\n          event.stopPropagation();\n          this.refs.composed.onArrowUp(event)\n        },\n        'core:next-item': (event) => {\n          if (!(this.refs.composed || {}).onArrowDown) { return }\n          event.stopPropagation();\n          this.refs.composed.onArrowDown(event)\n        },\n        'core:move-left': (event) => {\n          if (!(this.refs.composed || {}).onArrowDown) { return }\n          event.stopPropagation();\n          this.refs.composed.onArrowLeft(event)\n        },\n        'core:move-right': (event) => {\n          if (!(this.refs.composed || {}).onArrowDown) { return }\n          event.stopPropagation();\n          this.refs.composed.onArrowRight(event)\n        },\n      };\n    }\n\n    onKeyDown = (event) => {\n      if (['Enter', 'Return'].includes(event.key)) {\n        if (!(this.refs.composed || {}).onEnter) { return }\n        event.stopPropagation();\n        this.refs.composed.onEnter(event)\n      }\n      if (event.key === 'Tab') {\n        if (event.shiftKey) {\n          if (!(this.refs.composed || {}).onShiftTab) { return }\n          event.stopPropagation();\n          event.preventDefault();\n          this.refs.composed.onShiftTab(event)\n        } else {\n          if (!(this.refs.composed || {}).onTab) { return }\n          event.stopPropagation();\n          event.preventDefault();\n          this.refs.composed.onTab(event)\n        }\n      }\n    }\n\n    render() {\n      return (\n        <KeyCommandsRegion\n          tabIndex=\"0\"\n          localHandlers={this.localKeyHandlers()}\n          onKeyDown={this.onKeyDown}\n        >\n          <ComposedComponent ref=\"composed\" {...this.props} />\n        </KeyCommandsRegion>\n      )\n    }\n  }\n}\n\nexport default ListensToMovementKeys\n"
  },
  {
    "path": "packages/client-app/src/components/decorators/listens-to-observable.jsx",
    "content": "import React from 'react'\n\nfunction ListensToObservable(ComposedComponent, {getObservable, getStateFromObservable}) {\n  return class extends ComposedComponent {\n    static displayName = ComposedComponent.displayName;\n    static containerRequired = ComposedComponent.containerRequired;\n    static containerStyles = ComposedComponent.containerStyles;\n\n    constructor(props) {\n      super(props)\n      this.state = getStateFromObservable(null, {props})\n      this.disposable = null\n      this.observable = getObservable(props)\n    }\n\n    componentDidMount() {\n      this.unmounted = false\n      this.disposable = this.observable.subscribe(this.onObservableChanged)\n    }\n\n    componentWillReceiveProps(nextProps) {\n      if (this.disposable) {\n        this.disposable.dispose()\n      }\n      this.observable = getObservable(nextProps)\n      this.disposable = this.observable.subscribe(this.onObservableChanged)\n    }\n\n    componentWillUnmount() {\n      this.unmounted = true\n      this.disposable.dispose()\n    }\n\n    onObservableChanged = (data) => {\n      if (this.unmounted) return;\n      this.setState(getStateFromObservable(data, {props: this.props}))\n    };\n\n    render() {\n      return (\n        <ComposedComponent {...this.state} {...this.props} />\n      )\n    }\n  }\n}\n\nexport default ListensToObservable\n"
  },
  {
    "path": "packages/client-app/src/components/disclosure-triangle.cjsx",
    "content": "React = require 'react'\n\nclass DisclosureTriangle extends React.Component\n  @displayName: 'DisclosureTriangle'\n\n  @propTypes:\n    collapsed: React.PropTypes.bool\n    visible: React.PropTypes.bool\n    onCollapseToggled: React.PropTypes.func\n\n  @defaultProps:\n    onCollapseToggled: ->\n\n  render: ->\n    classnames = \"disclosure-triangle\"\n    classnames += \" visible\" if @props.visible\n    classnames += \" collapsed\" if @props.collapsed\n    <div className={classnames} onClick={@props.onCollapseToggled}><div></div></div>\n\nmodule.exports = DisclosureTriangle\n"
  },
  {
    "path": "packages/client-app/src/components/drop-zone.jsx",
    "content": "import React from 'react';\nimport _ from 'underscore';\n\nexport default class DropZone extends React.Component {\n  static propTypes = {\n    shouldAcceptDrop: React.PropTypes.func.isRequired,\n    onDrop: React.PropTypes.func.isRequired,\n    onDragOver: React.PropTypes.func,\n    onDragStateChange: React.PropTypes.func,\n  };\n\n  static defaultProps = {\n    onDragOver: () => {},\n  };\n\n  // We maintain a \"dragCounter\" because dragEnter and dragLeave events *stack*\n  // when the user moves the item in and out of DOM elements inside of our container.\n  // It's really awful and everyone hates it.\n\n  // Alternative solution *maybe* is to set pointer-events:none; during drag.\n\n  _onDragEnter = (e) => {\n    if (!this.props.shouldAcceptDrop(e)) {\n      return;\n    }\n    if (this._dragCounter === undefined) {\n      this._dragCounter = 0;\n    }\n    this._dragCounter += 1;\n    if (this._dragCounter === 1 && this.props.onDragStateChange) {\n      this.props.onDragStateChange({isDropping: true});\n    }\n    e.stopPropagation();\n    return;\n  }\n\n  _onDragLeave = (e) => {\n    if (!this.props.shouldAcceptDrop(e)) {\n      return;\n    }\n    this._dragCounter -= 1;\n    if (this._dragCounter === 0 && this.props.onDragStateChange) {\n      this.props.onDragStateChange({isDropping: false});\n    }\n    e.stopPropagation();\n    return;\n  }\n\n  _onDrop = (e) => {\n    if (!this.props.shouldAcceptDrop(e)) {\n      return;\n    }\n    if (this.props.onDragStateChange) {\n      this.props.onDragStateChange({isDropping: false});\n    }\n    this._dragCounter = 0;\n    this.props.onDrop(e);\n    e.stopPropagation();\n    return;\n  }\n\n  render() {\n    const otherProps = _.omit(this.props, Object.keys(this.constructor.propTypes));\n    return (\n      <div\n        {...otherProps}\n        onDragOver={this.props.onDragOver}\n        onDragEnter={this._onDragEnter}\n        onDragLeave={this._onDragLeave}\n        onDrop={this._onDrop}\n      >\n        {this.props.children}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/dropdown-menu.jsx",
    "content": "import {React, ReactDOM} from 'nylas-exports'\nimport {Menu} from 'nylas-component-kit'\n\nexport default class DropdownMenu extends React.Component {\n  static propTypes = {\n    intitialSelectionItem: React.PropTypes.object,\n    onSelect: React.PropTypes.func,\n    itemContent: React.PropTypes.fun,\n    headerComponents: React.PropTypes.element,\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      expanded: false,\n      currentSelection: this.props.intitialSelectionItem,\n    }\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState({currentSelection: nextProps.intitialSelectionItem})\n  }\n\n  _toggleExpanded = () => {\n    this.setState({expanded: !this.state.expanded})\n  }\n\n  _close = () => {\n    if (this.state.expanded) {\n      this.setState({expanded: false})\n    }\n  }\n\n  _onSelect = (item) => {\n    this.setState({currentSelection: item})\n    if (this.props.onSelect) {\n      this.props.onSelect(item);\n    }\n    this._close()\n  }\n\n  _onBlur = (e) => {\n    const node = ReactDOM.findDOMNode(this)\n    let otherNode = e.relatedTarget\n    if (otherNode) {\n      while (otherNode.parentElement) {\n        // Don't close the dropdown if the related target is a child of this component\n        if (otherNode.parentElement === node) {\n          return;\n        }\n        otherNode = otherNode.parentElement\n      }\n    }\n    this._close()\n  }\n\n  render() {\n    let dropdown = <span />\n    if (this.state.expanded) {\n      dropdown = (\n        <Menu\n          {...this.props}\n          onEscape={this._close}\n          onSelect={this._onSelect}\n        />\n      )\n    }\n    return (\n      <div\n        tabIndex=\"-1\"\n        onBlur={this._onBlur}\n        style={{display: 'inline-block'}}\n      >\n        <div onClick={this._toggleExpanded} style={{cursor: 'pointer', marginLeft: '12px'}}>\n          {this.props.itemContent(this.state.currentSelection)}\n        </div>\n        <div style={{position: 'absolute', zIndex: '10'}}>\n          {dropdown}\n        </div>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/editable-list.jsx",
    "content": "/* eslint jsx-a11y/tabindex-no-positive: 0 */\nimport _ from 'underscore';\nimport uuid from 'node-uuid';\nimport classNames from 'classnames';\nimport React, {Component, PropTypes} from 'react';\nimport ReactDOM from 'react-dom';\n\nimport ScrollRegion from './scroll-region';\nimport KeyCommandsRegion from './key-commands-region';\nimport RetinaImg from './retina-img';\n\n/*\nRenders a list of items and renders controls to add/edit/remove items.\nIt resembles OS X's default list component.\nAn item can be a React Component, a string or number.\n\nEditableList handles:\n- Keyboard and mouse interactions to select an item\n- Input to create a new item when the add button is clicked\n- Callback to remove item when the remove button is clicked\n- Double click to edit item, or use an edit button icon\n\n@param {object} props - props for EditableList\n@param {string} props.className - CSS class to be applied to component\n@param {array} props.items - Items to be rendered by the list\n@param {function} props.itemContent - A function that returns a component\nor string for each item. To be editable, itemContent must be a string.\nIf no function is provided, each value in `items` is coerced to a string.\n@param {(string|object)} props.selected - The selected item. This prop is\noptional unless uou want to control the selection externally.\n@param {boolean} props.showEditIcon - Determines wether to show edit icon\nbutton on selected items\n@param {object} props.createInputProps - Props object to be passed on to\nthe create input element. However, keep in mind that these props can not\noverride the default props that EditableList will pass to the input.\n@param {props.onCreateItem} props.onCreateItem\n@param {props.onDeleteItem} props.onDeleteItem\n@param {props.onSelectItem} props.onSelectItem\n@param {props.onReorderItem} props.onReorderItem\n@param {props.onItemEdited} props.onItemEdited\n@param {props.onItemCreated} props.onItemCreated\n@class EditableList\n */\nclass EditableList extends Component {\n  static displayName = 'EditableList';\n\n  /**\n   * If provided, this function will be called when the add button is clicked,\n   * and will prevent an input to be added at the end of the list\n   * @callback props.onCreateItem\n   */\n  /**\n   * If provided, this function will be called when the delete button is clicked.\n   * @callback props.onDeleteItem\n   * @param {(Component|string|number)} selectedItem - The selected item.\n   * @param {number} idx - The selected item idx\n   */\n  /**\n   * If provided, this function will be called when an item has been edited. This only\n   * applies to items that are not React Components.\n   * @callback props.onItemEdited\n   * @param {string} newValue - The new value for the item\n   * @param {(string|number)} originalValue - The original value for the item\n   * @param {number} idx - The index of the edited item\n   */\n  /**\n   * If provided, this function will be called when an item is selected via click or arrow\n   * keys. If the selection is cleared, it will receive null.\n   * @callback props.onSelectItem\n   * @param {(Component|string|number)} selectedItem - The selected item or null\n   * when selection cleared\n   * @param {number} idx - The index of the selected item or null when selection\n   * cleared\n   */\n  /**\n   * If provided, this function will be called when the user has entered a value to create\n   * a new item in the new item input. This function will be called when the\n   * user presses Enter or when the input is blurred.\n   * @callback props.onItemCreated\n   * @param {string} value - The value for the new item\n   */\n   /**\n    * If provided, the user will be able to drag and drop items to re-arrange them\n    * within the list. Note that dragging between lists is not supported.\n    * @callback props.onReorderItem\n    * @param {Object} item - The item that was dragged\n    * @param {number} startIdx - The index the item was dragged from\n    * @param {number} endIdx - The new index of the item, assuming it was\n      already removed from startIdx.\n    */\n  static propTypes = {\n    items: PropTypes.array.isRequired,\n    itemContent: PropTypes.func,\n    className: PropTypes.string,\n    showEditIcon: PropTypes.bool,\n    createInputProps: PropTypes.object,\n    onCreateItem: PropTypes.func,\n    onDeleteItem: PropTypes.func,\n    onReorderItem: PropTypes.func,\n    onItemEdited: PropTypes.func,\n    onItemCreated: PropTypes.func,\n\n    /* Optional, if you choose to control selection externally */\n    selected: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),\n    onSelectItem: PropTypes.func,\n  };\n\n  static defaultProps = {\n    items: [],\n    itemContent: (item) => item,\n    className: '',\n    createInputProps: {},\n    showEditIcon: false,\n    onDeleteItem: () => {},\n    onItemEdited: () => {},\n    onItemCreated: () => {},\n  };\n\n  constructor(props) {\n    super(props);\n    this.listId = uuid.v4();\n    this.state = {\n      dropInsertionIndex: -1,\n      editingIndex: -1,\n      creatingItem: false,\n    };\n  }\n\n  // Helpers\n\n  _createItem = (value) => {\n    this._clearCreatingState(() => {\n      if (value) {\n        this.props.onItemCreated(value);\n      }\n    });\n  };\n\n  _updateItem = (value, originalItem, idx) => {\n    this._clearEditingState(() => {\n      this.props.onItemEdited(value, originalItem, idx);\n    });\n  };\n\n  _getSelectedItem = () => {\n    if (this.props.onSelectItem) {\n      return this.props.selected;\n    }\n    return this.state.selected;\n  };\n\n  _selectItem = (item, idx) => {\n    if (this.props.onSelectItem) {\n      this.props.onSelectItem(item, idx);\n    } else {\n      this.setState({selected: item});\n    }\n  };\n\n  _clearEditingState = (callback) => {\n    this._setStateAndFocus({editingIndex: -1}, callback);\n  };\n\n  _clearCreatingState = (callback) => {\n    this._setStateAndFocus({creatingItem: false}, callback);\n  };\n\n  _setStateAndFocus = (state, callback = () => {}) => {\n    this.setState(state, () => {\n      this._focusSelf();\n      callback();\n    });\n  };\n\n  _focusSelf = () => {\n    ReactDOM.findDOMNode(this).focus();\n  };\n\n  /**\n   * @private Scrolls to the dom node of the item at the provided index\n   * @param {number} idx - Index of item inside the list to scroll to\n   */\n  _scrollTo = (idx) => {\n    if (!idx) return;\n    const list = this.refs.itemsWrapper;\n    const nodes = ReactDOM.findDOMNode(list).querySelectorAll('.list-item');\n    list.scrollTo(nodes[idx]);\n  };\n\n\n  // Handlers\n\n  _onEditInputBlur = (event, item, idx) => {\n    this._updateItem(event.target.value, item, idx);\n  };\n\n  _onEditInputFocus = (event) => {\n    const input = event.target;\n    // Move cursor to the end of the input\n    input.selectionStart = input.selectionEnd = input.value.length;\n  };\n\n  _onEditInputKeyDown = (event, item, idx) => {\n    event.stopPropagation();\n    if (_.includes(['Enter', 'Return'], event.key)) {\n      this._updateItem(event.target.value, item, idx);\n    } else if (event.key === 'Escape') {\n      this._clearEditingState();\n    }\n  };\n\n  _onCreateInputBlur = (event) => {\n    this._createItem(event.target.value);\n  };\n\n  _onCreateInputKeyDown = (event) => {\n    event.stopPropagation();\n    if (_.includes(['Enter', 'Return'], event.key)) {\n      this._createItem(event.target.value);\n    } else if (event.key === 'Escape') {\n      this._clearCreatingState();\n    }\n  };\n\n  _onItemClick = (event, item, idx) => {\n    this._selectItem(item, idx);\n  };\n\n  _onItemEdit = (event, item, idx) => {\n    this.setState({editingIndex: idx});\n  };\n\n  _listKeymapHandlers = () => {\n    const _shift = (dir) => {\n      const len = this.props.items.length;\n      const index = this.props.items.indexOf(this._getSelectedItem());\n      const newIndex = Math.min(len - 1, Math.max(0, index + dir));\n      if (index === newIndex) {\n        return;\n      }\n      this._scrollTo(newIndex);\n      this._selectItem(this.props.items[newIndex], newIndex);\n    };\n    return {\n      'core:previous-item': (event) => {\n        event.stopPropagation();\n        _shift(-1);\n      },\n      'core:next-item': (event) => {\n        event.stopPropagation();\n        _shift(1);\n      },\n    };\n  };\n\n  _onCreateItem = () => {\n    if (this.props.onCreateItem) {\n      this.props.onCreateItem();\n    } else {\n      this.setState({creatingItem: true});\n    }\n  };\n\n  _onDeleteItem = () => {\n    const selectedItem = this._getSelectedItem();\n    const index = this.props.items.indexOf(selectedItem);\n    if (selectedItem) {\n      // Move the selection 1 up or down after deleting\n      const newIndex = index === 0 ? index + 1 : index - 1\n      this.props.onDeleteItem(selectedItem, index);\n      if (this.props.items[newIndex]) {\n        this._selectItem(this.props.items[newIndex], newIndex);\n      }\n    }\n  };\n\n  _onItemDragStart = (event) => {\n    if (!this.props.onReorderItem) {\n      event.preventDefault();\n      return;\n    }\n\n    const row = event.target.closest('[data-item-idx]') || event.target;\n    if (!row.dataset.itemIdx) {\n      return;\n    }\n\n    if (row.dataset.itemIdx / 1 === this.state.editingIndex / 1) {\n      // dragging the row currently being edited makes text selection impossible\n      event.preventDefault();\n      return;\n    }\n\n    event.dataTransfer.setData('editablelist-index', row.dataset.itemIdx);\n    event.dataTransfer.setData(`editablelist-listid:${this.listId}`, 'true');\n    event.dataTransfer.effectAllowed = \"move\";\n  };\n\n  _onDragOver = (event) => {\n    const wrapperNode = ReactDOM.findDOMNode(this.refs.itemsWrapper);\n\n    // As of Chromium 53, we cannot access the contents of the drag pasteboard\n    // until the user drops for security reasons. Pull the list id from the\n    // drag datatype itself.\n    const originListType = event.dataTransfer.types.find(t => t.startsWith('editablelist-listid:'));\n    const originListId = originListType ? originListType.split(':').pop() : null;\n    const originSameList = (originListId === this.listId);\n    let dropInsertionIndex = 0;\n\n    if ((event.currentTarget === wrapperNode) && originSameList) {\n      const itemNodes = wrapperNode.querySelectorAll('[data-item-idx]')\n      for (let i = 0; i < itemNodes.length; i++) {\n        const itemNode = itemNodes[i];\n        const rect = itemNode.getBoundingClientRect();\n        if (event.clientY > rect.top + (rect.height / 2)) {\n          dropInsertionIndex = itemNode.dataset.itemIdx / 1 + 1;\n        } else {\n          break;\n        }\n      }\n    } else {\n      dropInsertionIndex = -1;\n    }\n\n    if (this.state.dropInsertionIndex !== dropInsertionIndex) {\n      this.setState({dropInsertionIndex: dropInsertionIndex});\n    }\n  };\n\n  _onDragLeave = () => {\n    this.setState({dropInsertionIndex: -1});\n  };\n\n  _onDrop = (event) => {\n    if (this.state.dropInsertionIndex !== -1) {\n      const startIdx = event.dataTransfer.getData('editablelist-index');\n      if (startIdx && (this.state.dropInsertionIndex !== startIdx)) {\n        const item = this.props.items[startIdx];\n\n        let endIdx = this.state.dropInsertionIndex;\n        if (endIdx > startIdx) {\n          endIdx -= 1\n        }\n\n        this.props.onReorderItem(item, startIdx, endIdx);\n        this.setState({dropInsertionIndex: -1});\n      }\n    }\n  };\n\n  // Renderers\n\n  _renderEditInput = (item, itemContent, idx, handlers = {}) => {\n    const onInputBlur = handlers.onInputBlur || this._onEditInputBlur;\n    const onInputFocus = handlers.onInputFocus || this._onEditInputFocus;\n    const onInputKeyDown = handlers.onInputKeyDown || this._onEditInputKeyDown;\n\n    return (\n      <input\n        autoFocus\n        type=\"text\"\n        placeholder={itemContent}\n        defaultValue={itemContent}\n        onBlur={_.partial(onInputBlur, _, item, idx)}\n        onFocus={onInputFocus}\n        onKeyDown={_.partial(onInputKeyDown, _, item, idx)}\n      />\n    );\n  };\n\n  /**\n   * @private Will render the create input with the provided input props.\n   * Provided props will be overriden with the props that EditableList needs to\n   * pass to the input.\n   */\n  _renderCreateInput = () => {\n    const props = _.extend(this.props.createInputProps, {\n      autoFocus: true,\n      type: 'text',\n      onBlur: this._onCreateInputBlur,\n      onKeyDown: this._onCreateInputKeyDown,\n    });\n\n    return (\n      <div className=\"create-item-input\" key=\"create-item-input\">\n        <input {...props} />\n      </div>\n    );\n  };\n\n  // handlers object for testing\n  _renderItem = (item, idx, {editingIndex} = this.state, handlers = {}) => {\n    const onClick = handlers.onClick || this._onItemClick;\n    const onEdit = handlers.onEdit || this._onItemEdit;\n\n    let itemContent = this.props.itemContent(item);\n    const itemIsEditable = !React.isValidElement(itemContent);\n\n    const classes = classNames({\n      'list-item': true,\n      'selected': item === this._getSelectedItem(),\n      'editing': idx === editingIndex,\n      'editable-item': itemIsEditable,\n      'with-edit-icon': this.props.showEditIcon && editingIndex !== idx,\n    });\n\n    if ((editingIndex === idx) && itemIsEditable) {\n      itemContent = this._renderEditInput(item, itemContent, idx, handlers);\n    }\n\n    return (\n      <div\n        className={classes}\n        key={idx}\n        data-item-idx={idx}\n        draggable\n        onDragStart={this._onItemDragStart}\n        onClick={_.partial(onClick, _, item, idx)}\n        onDoubleClick={_.partial(onEdit, _, item, idx)}\n      >\n        {itemContent}\n        <RetinaImg\n          className=\"edit-icon\"\n          name=\"edit-icon.png\"\n          title=\"Edit Item\"\n          mode={RetinaImg.Mode.ContentIsMask}\n          onClick={_.partial(onEdit, _, item, idx)}\n        />\n      </div>\n    );\n  };\n\n  _renderButtons = () => {\n    const deleteClasses = classNames({\n      'btn-editable-list': true,\n      'btn-disabled': !this._getSelectedItem(),\n    });\n    return (\n      <div className=\"buttons-wrapper\">\n        <div className=\"btn-editable-list\" onClick={this._onCreateItem}>\n          <span>+</span>\n        </div>\n        <div className={deleteClasses} onClick={this._onDeleteItem}>\n          <span>—</span>\n        </div>\n      </div>\n    );\n  };\n\n  _renderDropInsertion = () => {\n    return (\n      <div className=\"insertion-point\"><div /></div>\n    )\n  };\n\n  render() {\n    let items = this.props.items.map((item, idx) => this._renderItem(item, idx));\n    if (this.state.creatingItem === true) {\n      items = items.concat(this._renderCreateInput());\n    }\n\n    if (this.state.dropInsertionIndex !== -1) {\n      items.splice(this.state.dropInsertionIndex, 0, this._renderDropInsertion());\n    }\n\n    return (\n      <KeyCommandsRegion\n        tabIndex=\"1\"\n        localHandlers={this._listKeymapHandlers()}\n        className={`nylas-editable-list ${this.props.className}`}\n      >\n        <ScrollRegion\n          className=\"items-wrapper\"\n          ref=\"itemsWrapper\"\n          onDragOver={this._onDragOver}\n          onDragLeave={this._onDragLeave}\n          onDrop={this._onDrop}\n        >\n          {items}\n        </ScrollRegion>\n        {this._renderButtons()}\n      </KeyCommandsRegion>\n    );\n  }\n\n}\n\nexport default EditableList;\n"
  },
  {
    "path": "packages/client-app/src/components/editable-table.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {pickHTMLProps} from 'pick-react-known-prop'\nimport ReactDOM from 'react-dom'\n\nimport RetinaImg from './retina-img'\nimport SelectableTable, {SelectableTableCell} from './selectable-table'\n\n\n/*\n * EditableTable component which renders a {SelectableTable} that supports\n * editing cells, and adding new rows and columns\n *\n * The required props for EditableTable are the same for {SelectableTable} plus\n * the function `onCellEdited`, which is of the form:\n *\n * ```\n *   const onCellEdited = ({rowIdx, colIdx, isHeader, value}) => { ... }\n * ```\n *\n * EditableTable is a controlled component, which means that it does not\n * manage any internal state. In order for the values of cells to be updated, or to add\n * new rows or columns, the functions `onCellEdited`, `onAddRow`, `onAddColumn`,\n * `onRemoveColumn` must be provided as props, and must eventually trigger\n * a re render of this Component with a new set of props.\n *\n * If the function `onAddColumn` and `onRemoveColumns` are provided, this\n * component will render a set of buttons to add and remove columns\n *\n * EditableTable takes the exact same set of props as {SelectableTable}, plus\n * additional props documented below. For {SelectableTable} props, see the docs\n * for {SelectableTable}\n *\n * @param {object} props - props for EditableTable\n * @param {object} props.inputProps - props to pass to the InputRenderer\n * @param {function | string} props.InputRenderer - Function, Class or String used\n * to render the inputs for cells. Defaults to <input />. Must be of any type\n * accepted by React.createElement. E.g. 'div', () => <div />,\n * class Div extends React.Component { render() { return <div /> } }\n * @param {props.onCellEdited} props.onCellEdited\n * @param {props.onAddRow} props.onAddRow\n * @param {props.onAddColumn} props.onAddColumn\n * @param {props.onRemoveColumn} props.onRemoveColumn\n * @class EditableTable\n */\n/*\n * This function will be called when a cell has been edited witha new value\n * @callback props.onCellEdited\n * @param {object} args - object containing indices for the cell and the new\n * value\n * @param {number} args.row - row index for the edited cell\n * @param {number} args.col - column index for the edited cell\n * @param {string} args.value - value for the cell\n */\n/*\n * This function will be called when a row needs to be added\n * @callback props.onAddRow\n */\n/*\n * This function will be called when a column needs to be added\n * @callback props.onAddColumn\n */\n/*\n * This function will be called when the last column needs to be removed\n * @callback props.onRemoveColumn\n */\n\nexport class EditableTableCell extends Component {\n\n  static propTypes = {\n    tableDataSource: SelectableTableCell.propTypes.tableDataSource,\n    rowIdx: SelectableTableCell.propTypes.colIdx,\n    colIdx: SelectableTableCell.propTypes.colIdx,\n    isHeader: PropTypes.bool,\n    inputProps: PropTypes.object,\n    InputRenderer: SelectableTable.propTypes.RowRenderer,\n    onAddRow: PropTypes.func,\n    onCellEdited: PropTypes.func.isRequired,\n  }\n\n  static defaultProps = {\n    inputProps: {},\n    InputRenderer: (props) => <input {...pickHTMLProps(props)} defaultValue={props.defaultValue} />,\n  }\n\n  componentDidMount() {\n    if (this.shouldFocusInput()) {\n      ReactDOM.findDOMNode(this.refs.inputContainer).querySelector('input').focus()\n    }\n  }\n\n  componentDidUpdate() {\n    if (this.shouldFocusInput()) {\n      ReactDOM.findDOMNode(this.refs.inputContainer).querySelector('input').focus()\n    }\n  }\n\n  onInputBlur = (event) => {\n    const {target: {value}} = event\n    const {tableDataSource, isHeader, rowIdx, colIdx, onCellEdited} = this.props\n    const currentValue = tableDataSource.cellAt({rowIdx, colIdx})\n    if (value != null && value !== currentValue) {\n      onCellEdited({rowIdx, colIdx, isHeader, value})\n    }\n  }\n\n  onInputKeyDown = (event) => {\n    const {key} = event\n    const {onAddRow} = this.props\n\n    if (['Enter', 'Return'].includes(key)) {\n      if (this.refs.cell.isInLastRow()) {\n        event.stopPropagation()\n        onAddRow()\n      }\n    } else if (key === 'Escape') {\n      event.stopPropagation()\n      ReactDOM.findDOMNode(this.refs.inputContainer).focus()\n    }\n  }\n\n  shouldFocusInput() {\n    return (\n      this.refs.cell.isSelectedUsingKey('Tab') ||\n      this.refs.cell.isSelectedUsingKey('Enter') ||\n      this.refs.cell.isSelectedUsingKey('Return')\n    )\n  }\n\n  render() {\n    const {rowIdx, colIdx, tableDataSource, isHeader, inputProps, InputRenderer} = this.props\n    const cellValue = tableDataSource.cellAt({rowIdx, colIdx})\n\n    return (\n      <SelectableTableCell ref=\"cell\" {...this.props}>\n        <div ref=\"inputContainer\" tabIndex=\"0\">\n          <InputRenderer\n            type=\"text\"\n            rowIdx={rowIdx}\n            colIdx={colIdx}\n            tableDataSource={tableDataSource}\n            isHeader={isHeader}\n            defaultValue={cellValue}\n            onBlur={this.onInputBlur}\n            onKeyDown={this.onInputKeyDown}\n            {...inputProps}\n          />\n        </div>\n      </SelectableTableCell>\n    )\n  }\n}\n\nfunction EditableTable(props) {\n  const {\n    inputProps,\n    InputRenderer,\n    onCellEdited,\n    onAddRow,\n    onRemoveRow,\n    onAddColumn,\n    onRemoveColumn,\n    ...otherProps\n  } = props\n\n  const tableProps = {\n    ...otherProps,\n    className: \"editable-table\",\n    extraProps: {\n      onAddRow,\n      onRemoveRow,\n      onCellEdited,\n      inputProps,\n      InputRenderer,\n    },\n    CellRenderer: EditableTableCell,\n  }\n\n  if (!onAddColumn || !onRemoveColumn) {\n    return <SelectableTable {...tableProps} />\n  }\n  return (\n    <div className=\"editable-table-container\">\n      <SelectableTable {...tableProps} />\n      <div className=\"column-actions\">\n        <div className=\"btn btn-small\" onClick={onAddColumn}>\n          <RetinaImg\n            name=\"icon-column-plus.png\"\n            mode={RetinaImg.Mode.ContentPreserve}\n          />\n        </div>\n        <div className=\"btn btn-small\" onClick={onRemoveColumn}>\n          <RetinaImg\n            name=\"icon-column-minus.png\"\n            mode={RetinaImg.Mode.ContentPreserve}\n          />\n        </div>\n      </div>\n    </div>\n  )\n}\n\nEditableTable.displayName = 'EditableTable'\n\nEditableTable.propTypes = {\n  tableDataSource: SelectableTable.propTypes.tableDataSource,\n  inputProps: PropTypes.object,\n  InputRenderer: PropTypes.any,\n  onCellEdited: PropTypes.func.isRequired,\n  onAddColumn: PropTypes.func,\n  onRemoveColumn: PropTypes.func,\n  onAddRow: PropTypes.func,\n  onRemoveRow: PropTypes.func,\n}\n\nexport default EditableTable\n"
  },
  {
    "path": "packages/client-app/src/components/empty-list-state.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\nclassNames = require 'classnames'\nRetinaImg = require('./retina-img').default\nEventedIFrame = require './evented-iframe'\n{FolderSyncProgressStore,\n FocusedPerspectiveStore} = require 'nylas-exports'\n{SyncingListState} = require 'nylas-component-kit'\n\n\nINBOX_ZERO_ANIMATIONS = [\n  'gem',\n  'oasis',\n  'tron',\n  'airstrip',\n  'galaxy',\n]\n\nclass EmptyPerspectiveState extends React.Component\n  @displayName: \"EmptyPerspectiveState\"\n\n  @propTypes:\n    perspective: React.PropTypes.object,\n    messageContent: React.PropTypes.node,\n\n  render: ->\n    {messageContent, perspective} = @props\n    name = perspective.categoriesSharedName()\n    name = 'archive' if perspective.isArchive()\n    name = perspective.name if not name\n    name = name.toLowerCase() if name\n\n    <div className=\"perspective-empty-state\">\n      {if name\n        <RetinaImg\n          name={\"ic-emptystate-#{name}.png\"}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      }\n      <div className=\"message\">{messageContent}</div>\n    </div>\n\nclass EmptyInboxState extends React.Component\n  @displayName: \"EmptyInboxState\"\n\n  @propTypes:\n    containerRect: React.PropTypes.object,\n\n  _getScalingFactor: =>\n    {width} = @props.containerRect\n    return null unless width\n    return null if width > 600\n    return (width + 100) / 1000\n\n  _getAnimationName: (now = new Date()) =>\n    msInADay = 8.64e7\n    msSinceEpoch = now.getTime() - (now.getTimezoneOffset() * 1000 * 60)\n    daysSinceEpoch = Math.floor(msSinceEpoch / msInADay)\n    idx = daysSinceEpoch  % INBOX_ZERO_ANIMATIONS.length\n    return INBOX_ZERO_ANIMATIONS[idx]\n\n  render: ->\n    animationName = @_getAnimationName()\n    factor = @_getScalingFactor()\n    style = if factor\n      {transform: \"scale(#{factor})\"}\n    else\n      {}\n\n    <div className=\"inbox-zero-animation\">\n      <div className=\"animation-wrapper\" style={style}>\n        <iframe src={\"animations/inbox-zero/#{animationName}/#{animationName}.html\"}/>\n        <div className=\"message\">Hooray! You’re done.</div>\n      </div>\n    </div>\n\n\nclass EmptyListState extends React.Component\n  @displayName = 'EmptyListState'\n  @propTypes =\n    visible: React.PropTypes.bool.isRequired\n\n  constructor: (@props) ->\n    @_mounted = false\n    @state = Object.assign\n      active: false\n      rect: {}\n      @_getStateFromStores()\n\n  componentDidMount: ->\n    @_mounted = true\n    @_unlisteners = []\n    @_unlisteners.push FolderSyncProgressStore.listen((=> @setState @_getStateFromStores()), @)\n    @_unlisteners.push FocusedPerspectiveStore.listen((=> @setState @_getStateFromStores()), @)\n    window.addEventListener('resize', @_onResize)\n    if @props.visible and not @state.active\n      rect = @_getDimensions()\n      @setState(active:true, rect: rect)\n\n  shouldComponentUpdate: (nextProps, nextState) ->\n    return true if nextProps.visible isnt @props.visible\n    return not _.isEqual(nextState, @state)\n\n  componentWillUnmount: ->\n    @_mounted = false\n    unlisten() for unlisten in @_unlisteners\n    window.removeEventListener('resize', @_onResize)\n\n  componentDidUpdate: ->\n    if @props.visible and not @state.active\n      rect = @_getDimensions()\n      @setState(active:true, rect: rect)\n\n  componentWillReceiveProps: (newProps) ->\n    if newProps.visible is false\n      @setState(active:false)\n\n  render: ->\n    return <span /> unless @props.visible\n    ContentComponent = EmptyPerspectiveState\n    current = FocusedPerspectiveStore.current()\n\n    messageContent = current.emptyMessage()\n    if @state.syncing\n      messageContent = <SyncingListState empty />\n    else if current.isInbox()\n      ContentComponent = EmptyInboxState\n\n    classes = classNames\n      'empty-state': true\n      'active': @state.active\n\n    <div className={classes}>\n      <ContentComponent\n        perspective={current}\n        containerRect={@state.rect}\n        messageContent={messageContent}\n      />\n    </div>\n\n  _getDimensions: =>\n    return null unless @_mounted\n    node = ReactDOM.findDOMNode(@)\n    rect = node.getBoundingClientRect()\n    return {width: rect.width, height: rect.height}\n\n  _onResize: =>\n    rect = @_getDimensions()\n    if rect\n      @setState({rect})\n\n  _getStateFromStores: ->\n    return syncing: FocusedPerspectiveStore.current().hasSyncingCategories()\n\nmodule.exports = EmptyListState\n"
  },
  {
    "path": "packages/client-app/src/components/evented-iframe.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\n{Utils,\n RegExpUtils,\n IdentityStore,\n SearchableComponentMaker,\n SearchableComponentStore}= require 'nylas-exports'\nIFrameSearcher = require('../searchable-components/iframe-searcher').default\nurl = require 'url'\n_ = require \"underscore\"\n\n###\nPublic: EventedIFrame is a thin wrapper around the DOM's standard `<iframe>` element.\nYou should always use EventedIFrame, because it provides important event hooks that\nensure keyboard and mouse events are properly delivered to the application when\nfired within iFrames.\n\n```\n<div className=\"file-frame-container\">\n  <EventedIFrame src={src} />\n  <Spinner visible={!@state.ready} />\n</div>\n```\n\nAny `props` added to the <EventedIFrame> are passed to the iFrame it renders.\n\nSection: Component Kit\n###\nclass EventedIFrame extends React.Component\n  @displayName = 'EventedIFrame'\n\n  @propTypes =\n    searchable: React.PropTypes.bool\n    onResize: React.PropTypes.func\n\n  render: =>\n    otherProps = Utils.fastOmit(@props, Object.keys(@constructor.propTypes))\n    return (\n      <iframe seamless=\"seamless\" {...otherProps} />\n    )\n\n  componentDidMount: =>\n    if @props.searchable\n      @_regionId = Utils.generateTempId()\n      @_searchUsub = SearchableComponentStore.listen @_onSearchableStoreChange\n      SearchableComponentStore.registerSearchRegion(@_regionId, ReactDOM.findDOMNode(this))\n    @_subscribeToIFrameEvents()\n\n  componentWillUnmount: =>\n    @_unsubscribeFromIFrameEvents()\n    if @props.searchable\n      @_searchUsub()\n      SearchableComponentStore.unregisterSearchRegion(@_regionId)\n\n  componentDidUpdate: ->\n    if @props.searchable\n      SearchableComponentStore.registerSearchRegion(@_regionId, ReactDOM.findDOMNode(this))\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  ###\n  Public: Call this method if you replace the contents of the iframe's document.\n  This allows {EventedIframe} to re-attach it's event listeners.\n  ###\n  didReplaceDocument: =>\n    @_unsubscribeFromIFrameEvents()\n    @_subscribeToIFrameEvents()\n\n  setHeightQuietly: (height) =>\n    @_ignoreNextResize = true\n    ReactDOM.findDOMNode(@).height = \"#{height}px\"\n\n  _onSearchableStoreChange: =>\n    return unless @props.searchable\n    node = ReactDOM.findDOMNode(@)\n    doc = node.contentDocument?.body ? node.contentDocument\n    searchIndex = SearchableComponentStore.getCurrentRegionIndex(@_regionId)\n    {searchTerm} = SearchableComponentStore.getCurrentSearchData()\n    if @lastSearchIndex isnt searchIndex or @lastSearchTerm isnt searchTerm\n      IFrameSearcher.highlightSearchInDocument(@_regionId, searchTerm, doc, searchIndex)\n    @lastSearchIndex = searchIndex\n    @lastSearchTerm = searchTerm\n\n  _unsubscribeFromIFrameEvents: =>\n    node = ReactDOM.findDOMNode(@)\n    doc = node.contentDocument\n    return unless doc\n    doc.removeEventListener('click', @_onIFrameClick)\n    doc.removeEventListener('keydown', @_onIFrameKeyEvent)\n    doc.removeEventListener('keypress', @_onIFrameKeyEvent)\n    doc.removeEventListener('keyup', @_onIFrameKeyEvent)\n    doc.removeEventListener('mousedown', @_onIFrameMouseEvent)\n    doc.removeEventListener('mousemove', @_onIFrameMouseEvent)\n    doc.removeEventListener('mouseup', @_onIFrameMouseEvent)\n    doc.removeEventListener(\"contextmenu\", @_onIFrameContextualMenu)\n    if node.contentWindow\n      node.contentWindow.removeEventListener('focus', @_onIFrameFocus)\n      node.contentWindow.removeEventListener('blur', @_onIFrameBlur)\n      node.contentWindow.removeEventListener('resize', @_onIFrameResize)\n\n  _subscribeToIFrameEvents: =>\n    node = ReactDOM.findDOMNode(@)\n    doc = node.contentDocument\n    _.defer =>\n      doc.addEventListener(\"click\", @_onIFrameClick)\n      doc.addEventListener(\"keydown\", @_onIFrameKeyEvent)\n      doc.addEventListener(\"keypress\", @_onIFrameKeyEvent)\n      doc.addEventListener(\"keyup\", @_onIFrameKeyEvent)\n      doc.addEventListener(\"mousedown\", @_onIFrameMouseEvent)\n      doc.addEventListener(\"mousemove\", @_onIFrameMouseEvent)\n      doc.addEventListener(\"mouseup\", @_onIFrameMouseEvent)\n      doc.addEventListener(\"contextmenu\", @_onIFrameContextualMenu)\n      if node.contentWindow\n        node.contentWindow.addEventListener(\"focus\", @_onIFrameFocus)\n        node.contentWindow.addEventListener(\"blur\", @_onIFrameBlur)\n        node.contentWindow.addEventListener('resize', @_onIFrameResize) if @props.onResize\n\n  _getContainingTarget: (event, options) =>\n    target = event.target\n    while target? and (target isnt document) and (target isnt window)\n      return target if target.getAttribute(options.with)?\n      target = target.parentElement\n    return null\n\n  _onIFrameBlur: (event) =>\n    node = ReactDOM.findDOMNode(@)\n    node.contentWindow.getSelection().empty()\n\n  _onIFrameFocus: (event) =>\n    window.getSelection().empty()\n\n  _onIFrameResize: (event) =>\n    if @_ignoreNextResize\n      @_ignoreNextResize = false\n      return\n    @props.onResize?(event)\n\n  # The iFrame captures events that take place over it, which causes some\n  # interesting behaviors. For example, when you drag and release over the\n  # iFrame, the mouseup never fires in the parent window.\n  _onIFrameClick: (e) =>\n    e.stopPropagation()\n    target = @_getContainingTarget(e, {with: 'href'})\n    if target\n\n      # Sometimes urls can have relative, malformed, or malicious href\n      # targets. We test the existence of a valid RFC 3986 scheme and make\n      # sure the protocol isn't blacklisted. We never allow `file:` links\n      # through.\n      rawHref = target.getAttribute('href')\n\n      if @_isBlacklistedHref(rawHref)\n        e.preventDefault()\n        return\n\n      if not url.parse(rawHref).protocol\n        # Check for protocol-relative uri's\n        if (new RegExp(/^\\/\\//)).test(rawHref)\n          target.setAttribute('href', \"https:#{rawHref}\")\n        else\n          target.setAttribute('href', \"http://#{rawHref}\")\n\n        rawHref = target.getAttribute('href')\n\n      e.preventDefault()\n\n      # If this is a link to our billing site, attempt single sign on instead of\n      # just following the link directly\n      if rawHref.startsWith(IdentityStore.URLRoot)\n        path = rawHref.split(IdentityStore.URLRoot).pop()\n        IdentityStore.fetchSingleSignOnURL(path, {source: \"SingleSignOnEmail\"}).then (href) =>\n          NylasEnv.windowEventHandler.openLink(href: href, metaKey: e.metaKey)\n        return\n\n      # It's important to send the raw `href` here instead of the target.\n      # The `target` comes from the document context of the iframe, which\n      # as of Electron 0.36.9, has different constructor function objects\n      # in memory than the main execution context. This means that code\n      # like `e.target instanceof Element` will erroneously return false\n      # since the `e.target.constructor` and the `Element` function are\n      # created in different contexts.\n      NylasEnv.windowEventHandler.openLink(href: rawHref, metaKey: e.metaKey)\n\n  _isBlacklistedHref: (href) ->\n    return (new RegExp(/^file:/i)).test(href)\n\n  _onIFrameMouseEvent: (event) =>\n    node = ReactDOM.findDOMNode(@)\n    nodeRect = node.getBoundingClientRect()\n\n    eventAttrs = {}\n    for key in Object.keys(event)\n      continue if key in ['webkitMovementX', 'webkitMovementY']\n      eventAttrs[key] = event[key]\n\n    node.dispatchEvent(new MouseEvent(event.type, _.extend({}, eventAttrs, {\n      bubbles: true\n      clientX: event.clientX + nodeRect.left\n      clientY: event.clientY + nodeRect.top\n      pageX: event.pageX + nodeRect.left\n      pageY: event.pageY + nodeRect.top\n    })))\n\n  _onIFrameKeyEvent: (event) =>\n    return if event.metaKey or event.altKey or event.ctrlKey\n\n    attrs = ['key', 'code','location', 'ctrlKey', 'shiftKey', 'altKey', 'metaKey', 'repeat', 'isComposing', 'charCode', 'keyCode', 'which']\n    eventInit = Object.assign({bubbles: true}, _.pick(event, attrs))\n    eventInParentDoc = new KeyboardEvent(event.type, eventInit)\n\n    Object.defineProperty(eventInParentDoc, 'which', {value: event.which})\n\n    ReactDOM.findDOMNode(@).dispatchEvent(eventInParentDoc)\n\n  _onIFrameContextualMenu: (event) =>\n    # Build a standard-looking contextual menu with options like \"Copy Link\",\n    # \"Copy Image\" and \"Search Google for 'Bla'\"\n    event.preventDefault()\n\n    {remote, clipboard, shell, nativeImage} = require('electron')\n    {Menu, MenuItem} = remote\n    path = require('path')\n    fs = require('fs')\n    menu = new Menu()\n\n    # Menu actions for links\n    linkTarget = @_getContainingTarget(event, {with: 'href'})\n    if linkTarget\n      href = linkTarget.getAttribute('href')\n      if href.startsWith('mailto')\n        menu.append(new MenuItem({ label: \"Compose Message...\", click:( -> NylasEnv.windowEventHandler.openLink({href}) )}))\n        menu.append(new MenuItem({ label: \"Copy Email Address\", click:( -> clipboard.writeText(href.split('mailto:').pop()) )}))\n      else\n        menu.append(new MenuItem({ label: \"Open Link\", click:( -> NylasEnv.windowEventHandler.openLink({href}) )}))\n        menu.append(new MenuItem({ label: \"Copy Link Address\", click:( -> clipboard.writeText(href) )}))\n      menu.append(new MenuItem({ type: 'separator' }))\n\n    # Menu actions for images\n    imageTarget = @_getContainingTarget(event, {with: 'src'})\n    if imageTarget\n      src = imageTarget.getAttribute('src')\n      srcFilename = path.basename(src)\n      menu.append(new MenuItem({\n        label: \"Save Image...\",\n        click: ->\n          NylasEnv.showSaveDialog {defaultPath: srcFilename}, (path) ->\n            return unless path\n            oReq = new XMLHttpRequest()\n            oReq.open(\"GET\", src, true)\n            oReq.responseType = \"arraybuffer\"\n            oReq.onload = ->\n              buffer = new Buffer(new Uint8Array(oReq.response))\n              fs.writeFile path, buffer, (err) ->\n                shell.showItemInFolder(path)\n            oReq.send()\n      }))\n      menu.append(new MenuItem({\n        label: \"Copy Image\",\n        click: ->\n          img = new Image()\n          img.addEventListener(\"load\", ->\n            canvas = document.createElement(\"canvas\")\n            canvas.width = img.width\n            canvas.height = img.height\n            canvas.getContext(\"2d\").drawImage(imageTarget, 0, 0)\n            imageDataURL = canvas.toDataURL(\"image/png\")\n            img = nativeImage.createFromDataURL(imageDataURL)\n            clipboard.writeImage(img)\n          , false)\n          img.src = src\n      }))\n      menu.append(new MenuItem({ type: 'separator' }))\n\n    # Menu actions for text\n    text = \"\"\n    selection = ReactDOM.findDOMNode(@).contentDocument.getSelection()\n    if selection.rangeCount > 0\n      range = selection.getRangeAt(0)\n      text = range.toString()\n    if not text or text.length is 0\n      text = (linkTarget ? event.target).innerText\n    text = text.trim()\n\n    if text.length > 0\n      if text.length > 45\n        textPreview = text.substr(0, 42) + \"...\"\n      else\n        textPreview = text\n      menu.append(new MenuItem({ label: \"Copy\", click:( -> clipboard.writeText(text) )}))\n      menu.append(new MenuItem({ label: \"Search Google for '#{textPreview}'\", click:( -> shell.openExternal(\"https://www.google.com/search?q=#{encodeURIComponent(text)}\") )}))\n      if process.platform is 'darwin'\n        menu.append(new MenuItem({ label: \"Look Up '#{textPreview}'\", click:( -> NylasEnv.getCurrentWindow().showDefinitionForSelection() )}))\n\n\n    if process.platform is 'darwin'\n      menu.append(new MenuItem({ type: 'separator' }))\n      # Services menu appears here automatically\n\n    menu.popup(remote.getCurrentWindow())\n\nmodule.exports = EventedIFrame\n"
  },
  {
    "path": "packages/client-app/src/components/feature-used-up-modal.jsx",
    "content": "import React from 'react'\nimport {shell} from 'electron'\nimport Actions from '../flux/actions'\nimport RetinaImg from './retina-img'\nimport BillingModal from './billing-modal'\nimport IdentityStore from '../flux/stores/identity-store'\n\nexport default class FeatureUsedUpModal extends React.Component {\n  static propTypes = {\n    modalClass: React.PropTypes.string.isRequired,\n    featureName: React.PropTypes.string.isRequired,\n    headerText: React.PropTypes.string.isRequired,\n    rechargeText: React.PropTypes.string.isRequired,\n    iconUrl: React.PropTypes.string.isRequired,\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    const start = Date.now()\n    IdentityStore.fetchSingleSignOnURL(\"/payment?embedded=true\").then((url) => {\n      console.log(\"Done grabbing url\", Date.now() - start)\n      if (!this._mounted) return\n      this.setState({upgradeUrl: url})\n    })\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  render() {\n    const gotoFeatures = () => shell.openExternal(\"https://nylas.com/nylas-pro\");\n\n    const upgrade = (e) => {\n      e.stopPropagation();\n      Actions.openModal({\n        component: (\n          <BillingModal source=\"feature-limit\" upgradeUrl={this.state.upgradeUrl} />\n        ),\n        height: 575,\n        width: 412,\n      })\n    }\n\n    return (\n      <div className={`feature-usage-modal ${this.props.modalClass}`}>\n        <div className=\"feature-header\">\n          <div className=\"icon\">\n            <RetinaImg\n              url={this.props.iconUrl}\n              style={{position: \"relative\", top: \"-2px\"}}\n              mode={RetinaImg.Mode.ContentPreserve}\n            />\n          </div>\n          <h2 className=\"header-text\">{this.props.headerText}</h2>\n          <p className=\"recharge-text\">{this.props.rechargeText}</p>\n        </div>\n        <div className=\"feature-cta\">\n          <h2>Want to <span className=\"feature-name\">{this.props.featureName} more</span>?</h2>\n          <div className=\"pro-description\">\n            <h3>Nylas Pro includes:</h3>\n            <ul>\n              <li>Unlimited Snoozing</li>\n              <li>Unlimited Reminders</li>\n              <li>Unlimited Mail Merge</li>\n            </ul>\n            <p>&hellip; plus <a onClick={gotoFeatures}>dozens of other features</a></p>\n          </div>\n\n          <button className=\"btn btn-cta btn-emphasis\" onClick={upgrade}>Upgrade</button>\n        </div>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/fixed-popover.jsx",
    "content": "import _ from 'underscore';\nimport React, {Component, PropTypes} from 'react';\nimport {findDOMNode} from 'react-dom';\nimport Actions from '../flux/actions';\nimport compose from './decorators/compose'\nimport AutoFocuses from './decorators/auto-focuses'\n\n\nconst Directions = {\n  Up: 'up',\n  Down: 'down',\n  Left: 'left',\n  Right: 'right',\n};\n\nconst InverseDirections = {\n  [Directions.Up]: Directions.Down,\n  [Directions.Down]: Directions.Up,\n  [Directions.Left]: Directions.Right,\n  [Directions.Right]: Directions.Left,\n};\n\nconst OFFSET_PADDING = 11.5;\n\n/*\n * Renders a popover absultely positioned in the window next to the provided\n * rect.\n * If `Actions.openPopover` is called when the popover is already open, it will\n * close the previous one and open the new one.\n * @class FixedPopover\n **/\nclass FixedPopover extends Component {\n\n  static Directions = Directions;\n\n  static propTypes = {\n    children: PropTypes.element,\n    direction: PropTypes.string,\n    fallbackDirection: PropTypes.string,\n    closeOnAppBlur: PropTypes.bool,\n    originRect: PropTypes.shape({\n      bottom: PropTypes.number,\n      top: PropTypes.number,\n      right: PropTypes.number,\n      left: PropTypes.number,\n      height: PropTypes.number,\n      width: PropTypes.number,\n    }),\n    focusElementWithTabIndex: PropTypes.func,\n  };\n\n  static defaultProps = {\n    closeOnAppBlur: true,\n  }\n\n  constructor(props) {\n    super(props);\n    this.mounted = false;\n    this.updateCount = 0\n    this.fallback = this.props.fallbackDirection;\n    this.state = {\n      offset: {},\n      direction: props.direction,\n      visible: false,\n    };\n  }\n\n  componentDidMount() {\n    this.mounted = true;\n    findDOMNode(this.refs.popoverContainer).addEventListener('animationend', this.onAnimationEnd)\n    window.addEventListener('resize', this.onWindowResize)\n    _.defer(this.onPopoverRendered)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.fallback = nextProps.fallbackDirection;\n    this.setState({direction: nextProps.direction})\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (\n      !_.isEqual(this.state, nextState) ||\n      !_.isEqual(this.props, nextProps)\n    )\n  }\n\n  componentDidUpdate() {\n    _.defer(this.onPopoverRendered)\n  }\n\n  componentWillUnmount() {\n    this.mounted = false;\n    findDOMNode(this.refs.popoverContainer).removeEventListener('animationend', this.onAnimationEnd)\n    window.removeEventListener('resize', this.onWindowResize)\n  }\n\n  onAnimationEnd = () => {\n    _.defer(this.props.focusElementWithTabIndex);\n  }\n\n  onWindowResize() {\n    Actions.closePopover()\n  }\n\n  onPopoverRendered = () => {\n    if (!this.mounted) {\n      return;\n    }\n\n    const {direction} = this.state\n    const currentRect = this.getCurrentRect()\n    const windowDimensions = this.getWindowDimensions()\n    const newState = this.computeAdjustedOffsetAndDirection({direction, windowDimensions, currentRect})\n    if (newState) {\n      if (this.updateCount > 1) {\n        this.setState({direction: this.props.direction, offset: {}, visible: true})\n        return\n      }\n\n      // Reset fallback after using it once\n      this.fallback = null\n      this.updateCount++;\n      this.setState(newState);\n    } else {\n      this.setState({visible: true})\n    }\n  };\n\n  onBlur = (event) => {\n    const target = event.nativeEvent.relatedTarget;\n    if (!this.props.closeOnAppBlur && target === null) {\n      return\n    }\n    if (!target || (!findDOMNode(this).contains(target))) {\n      Actions.closePopover();\n    }\n  };\n\n  onKeyDown = (event) => {\n    if (event.key === \"Escape\") {\n      Actions.closePopover();\n    }\n  };\n\n  getCurrentRect = () => {\n    return findDOMNode(this.refs.popover).getBoundingClientRect();\n  };\n\n  getWindowDimensions = () => {\n    return {\n      width: document.body.clientWidth,\n      height: document.body.clientHeight,\n    }\n  };\n\n  computeOverflows = ({currentRect, windowDimensions}) => {\n    const overflows = {\n      top: currentRect.top < 0,\n      left: currentRect.left < 0,\n      bottom: currentRect.bottom > windowDimensions.height,\n      right: currentRect.right > windowDimensions.width,\n    }\n    const overflowValues = {\n      top: Math.abs(currentRect.top),\n      left: Math.abs(currentRect.left),\n      bottom: Math.abs(currentRect.bottom - windowDimensions.height),\n      right: Math.abs(currentRect.right - windowDimensions.width),\n    }\n    return {overflows, overflowValues}\n  };\n\n  computeAdjustedOffsetAndDirection = ({direction, currentRect, windowDimensions, fallback = this.fallback, offsetPadding = OFFSET_PADDING}) => {\n    const {overflows, overflowValues} = this.computeOverflows({currentRect, windowDimensions})\n    const overflowCount = Object.keys(_.pick(overflows, (val) => val === true)).length\n\n    if (overflowCount > 0) {\n      if (fallback) {\n        return {direction: fallback, offset: {}}\n      }\n\n      const isHorizontalDirection = [Directions.Left, Directions.Right].includes(direction)\n      const isVerticalDirection = [Directions.Up, Directions.Down].includes(direction)\n      const shouldInvertDirection = (\n        (isHorizontalDirection && (overflows.left || overflows.right)) ||\n        (isVerticalDirection && (overflows.top || overflows.bottom))\n      )\n      const offset = {};\n      let newDirection = direction;\n\n      if (shouldInvertDirection) {\n        newDirection = InverseDirections[direction]\n      }\n\n      if (isHorizontalDirection && (overflows.top || overflows.bottom)) {\n        const overflowVal = (overflows.top ? overflowValues.top : overflowValues.bottom)\n        let offsetY = overflowVal + offsetPadding;\n\n        offsetY = overflows.bottom ? -(offsetY) : offsetY;\n        offset.y = offsetY;\n      }\n      if (isVerticalDirection && (overflows.left || overflows.right)) {\n        const overflowVal = (overflows.left ? overflowValues.left : overflowValues.right)\n        let offsetX = overflowVal + offsetPadding;\n\n        offsetX = overflows.right ? -(offsetX) : offsetX;\n        offset.x = offsetX;\n      }\n      return {offset, direction: newDirection}\n    }\n    return null;\n  };\n\n  computePopoverStyles = ({originRect, direction, offset}) => {\n    const {Up, Down, Left, Right} = Directions\n    let containerStyle = {};\n    let popoverStyle = {};\n    let pointerStyle = {};\n\n    switch (direction) {\n      case Up:\n        containerStyle = {\n          // Place container on the top left corner of the rect\n          top: originRect.top,\n          left: originRect.left,\n          width: originRect.width,\n        }\n        popoverStyle = {\n          // Center, place on top of container, and adjust 10px for the pointer\n          transform: `translate(${offset.x || 0}px) translate(-50%, calc(-100% - 10px))`,\n          left: originRect.width / 2,\n        }\n        pointerStyle = {\n          // Center, and place on top of our container\n          transform: 'translate(-50%, -100%)',\n          left: originRect.width, // Don't divide by 2 because of zoom\n        }\n        break;\n      case Down:\n        containerStyle = {\n          // Place container on the bottom left corner of the rect\n          top: originRect.top + originRect.height,\n          left: originRect.left,\n          width: originRect.width,\n        }\n        popoverStyle = {\n          // Center and adjust 10px for the pointer (already positioned at the bottom of container)\n          transform: `translate(${offset.x || 0}px) translate(-50%, 10px)`,\n          left: originRect.width / 2,\n        }\n        pointerStyle = {\n          // Center, already positioned at the bottom of container\n          transform: 'translate(-50%, 0) rotateX(180deg)',\n          left: originRect.width, // Don't divide by 2 because of zoom\n        }\n        break;\n      case Left:\n        containerStyle = {\n          // Place container on the top left corner of the rect\n          top: originRect.top,\n          left: originRect.left,\n          height: originRect.height,\n        }\n        popoverStyle = {\n          // Center, place on left of container, and adjust 10px for the pointer\n          transform: `translate(0, ${offset.y || 0}px) translate(calc(-100% - 10px), -50%)`,\n          top: originRect.height / 2,\n        }\n        pointerStyle = {\n          // Center, and place on left of our container (adjust for rotation)\n          transform: 'translate(calc(-100% + 13px), -50%) rotate(270deg)',\n          top: originRect.height, // Don't divide by 2 because of zoom\n        }\n        break;\n      case Right:\n        containerStyle = {\n          // Place container on the top right corner of the rect\n          top: originRect.top,\n          left: originRect.left + originRect.width,\n          height: originRect.height,\n        }\n        popoverStyle = {\n          // Center and adjust 10px for the pointer\n          transform: `translate(0, ${offset.y || 0}px) translate(10px, -50%)`,\n          top: originRect.height / 2,\n        }\n        pointerStyle = {\n          // Center, already positioned at the right of container (adjust for rotation)\n          transform: 'translate(-12px, -50%) rotate(90deg)',\n          top: originRect.height, // Don't divide by 2 because of zoom\n        }\n        break;\n      default:\n        break;\n    }\n\n    // Set the zoom directly on the style element. Otherwise it won't work with\n    // mask image of our shadow pointer element. This is probably a Chrome bug\n    pointerStyle.zoom = 0.5;\n\n    return {containerStyle, popoverStyle, pointerStyle};\n  };\n\n  render() {\n    const {offset, direction, visible} = this.state;\n    const {children, originRect} = this.props;\n    const blurTrapStyle = {top: originRect.top, left: originRect.left, height: originRect.height, width: originRect.width}\n    const {containerStyle, popoverStyle, pointerStyle} = (\n      this.computePopoverStyles({originRect, direction, offset})\n    );\n    const animateClass = visible ? ' popout' : '';\n\n    return (\n      <div>\n        <div ref=\"blurTrap\" className=\"fixed-popover-blur-trap\" style={blurTrapStyle} />\n        <div\n          ref=\"popoverContainer\"\n          style={containerStyle}\n          className={`fixed-popover-container${animateClass}`}\n          onKeyDown={this.onKeyDown}\n          onBlur={this.onBlur}\n        >\n          <div ref=\"popover\" className={`fixed-popover`} style={popoverStyle}>\n            {children}\n          </div>\n          <div className={`fixed-popover-pointer`} style={pointerStyle} />\n          <div className={`fixed-popover-pointer shadow`} style={pointerStyle} />\n        </div>\n      </div>\n    );\n  }\n}\n\nexport default compose(FixedPopover, AutoFocuses);\n"
  },
  {
    "path": "packages/client-app/src/components/flexbox.jsx",
    "content": "import React from 'react';\nimport {Utils} from \"nylas-exports\";\n\n/*\nPublic: A simple wrapper that provides a Flexbox layout with the given direction and style.\nAny additional props you set on the Flexbox are rendered.\n\nSection: Component Kit\n*/\nexport default class Flexbox extends React.Component {\n  static displayName = 'Flexbox';\n\n  /*\n  Public: React `props` supported by Flexbox:\n\n   - `direction` (optional) A {String} Flexbox direction: either `column` or `row`.\n   - `style` (optional) An {Object} with styles to apply to the flexbox.\n  */\n  static propTypes = {\n    direction: React.PropTypes.string,\n    inline: React.PropTypes.bool,\n    style: React.PropTypes.object,\n    height: React.PropTypes.string,\n  };\n\n  static defaultProps = {\n    height: '100%',\n    style: {},\n  };\n\n  render() {\n    const style = Object.assign({}, {\n      flexDirection: this.props.direction,\n      position: 'relative',\n      display: 'flex',\n      height: this.props.height,\n    }, this.props.style);\n\n    if (this.props.inline === true) {\n      style.display = 'inline-flex';\n    }\n\n    const otherProps = Utils.fastOmit(this.props, Object.keys(this.constructor.propTypes));\n\n    return (\n      <div style={style} {...otherProps}>\n        {this.props.children}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/flux-container.jsx",
    "content": "import React from 'react';\nimport {Utils} from 'nylas-exports';\n\nclass FluxContainer extends React.Component {\n  static displayName = 'FluxContainer';\n  static propTypes = {\n    children: React.PropTypes.element,\n    stores: React.PropTypes.array.isRequired,\n    getStateFromStores: React.PropTypes.func.isRequired,\n  };\n\n  constructor(props) {\n    super(props);\n    this._unlisteners = [];\n    this.state = this.props.getStateFromStores();\n  }\n\n  componentDidMount() {\n    return this.setupListeners();\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState(nextProps.getStateFromStores());\n    return this.setupListeners(nextProps);\n  }\n\n  componentWillUnmount() {\n    for (const unlisten of this._unlisteners) {\n      unlisten();\n    }\n    this._unlisteners = [];\n  }\n\n  setupListeners(props = this.props) {\n    for (const unlisten of this._unlisteners) {\n      unlisten();\n    }\n\n    this._unlisteners = props.stores.map((store) => {\n      return store.listen(() =>\n        this.setState(props.getStateFromStores())\n      );\n    });\n  }\n\n  render() {\n    const otherProps = Utils.fastOmit(this.props, Object.keys(this.constructor.propTypes));\n    return React.cloneElement(this.props.children, Object.assign({}, otherProps, this.state));\n  }\n}\n\nexport default FluxContainer;\n"
  },
  {
    "path": "packages/client-app/src/components/focus-container.jsx",
    "content": "import React from 'react';\nimport {FocusedContentStore, Actions} from 'nylas-exports';\nimport {FluxContainer} from 'nylas-component-kit';\n\nexport default class FocusContainer extends React.Component {\n  static displayName: 'FocusContainer'\n  static propTypes = {\n    children: React.PropTypes.element,\n    collection: React.PropTypes.string,\n  }\n\n  getStateFromStores = () => {\n    const {collection} = this.props;\n    return {\n      focused: FocusedContentStore.focused(collection),\n      focusedId: FocusedContentStore.focusedId(collection),\n      keyboardCursor: FocusedContentStore.keyboardCursor(collection),\n      keyboardCursorId: FocusedContentStore.keyboardCursorId(collection),\n      onFocusItem: (item) => Actions.setFocus({collection: collection, item: item}),\n      onSetCursorPosition: (item) => Actions.setCursorPosition({collection: collection, item: item}),\n    };\n  }\n\n  render() {\n    return (\n      <FluxContainer\n        {...this.props}\n        stores={[FocusedContentStore]}\n        getStateFromStores={this.getStateFromStores}\n      >\n        {this.props.children}\n      </FluxContainer>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/generated-form.cjsx",
    "content": "_ = require 'underscore'\nclassNames = require 'classnames'\nReact = require 'react'\nReactDOM = require 'react-dom'\n{Utils} = require 'nylas-exports'\nDatePicker = require('./date-picker').default\nTabGroupRegion = require('./tab-group-region')\n\nidPropType = React.PropTypes.oneOfType([\n  React.PropTypes.string\n  React.PropTypes.number\n])\n\n# The FormItem acts like a React controlled input.\n# The `value` will set the \"value\" of whatever type of form item it is.\n# The `onChange` handler will get passed this item's unique index (so\n# parents can lookup and change the data appropriately) and the new value.\n# Either direct parents, grandparents, etc are responsible for updating\n# the `value` prop to update the value again.\nclass FormItem extends React.Component\n  @displayName: \"FormItem\"\n\n  @inputElementTypes:\n    \"checkbox\": true\n    \"color\": true\n    \"date\": false # We use Nylas DatePicker instead\n    \"datetime\": true\n    \"datetime-local\": true\n    \"email\": true\n    \"file\": true\n    \"hidden\": true\n    \"month\": true\n    \"number\": true\n    \"password\": true\n    \"radio\": true\n    \"range\": true\n    \"search\": true\n    \"tel\": true\n    \"text\": true\n    \"time\": true\n    \"url\": true\n    \"week\": true\n\n  @propTypes:\n    # Some sort of unique identifier\n    id: idPropType.isRequired\n\n    # Either a type of input or any type that can be passed into\n    # `React.createElement(type, ...)`\n    type: React.PropTypes.string.isRequired\n\n    # The name as POSTed to the eventual endpoint.\n    name: React.PropTypes.string\n\n    # The human-readable display label for the formItem\n    label: React.PropTypes.node\n\n    # Most input types take strings, numbers, and bools. Some types (like\n    # \"reference\" can get passed arrays)\n    value: React.PropTypes.oneOfType([\n      React.PropTypes.string\n      React.PropTypes.number\n      React.PropTypes.bool\n    ])\n    defaultValue: React.PropTypes.string\n\n    # A function that takes two arguments:\n    #   - The id of this FormItem\n    #   - The new value of the FormItem\n    onChange: React.PropTypes.func\n\n    # FormItems can either explicitly set the disabled state, or determine\n    # the disabled state by whether or not this is a 'new' or 'update'\n    # form.\n    formType: React.PropTypes.oneOf(['new', 'update'])\n    disabled: React.PropTypes.bool\n    editableForNew: React.PropTypes.bool\n    editableForUpdate: React.PropTypes.bool\n\n    formItemError: React.PropTypes.shape(\n      id: idPropType # The formItemId\n      message: React.PropTypes.string\n    )\n\n    # selectOptions\n    # An array of options.\n    selectOptions: React.PropTypes.arrayOf(React.PropTypes.shape(\n      label: React.PropTypes.node\n      value: React.PropTypes.string\n    ))\n\n    # Common <input> props.\n    # Anything that can be passed into a standard React <input> item will\n    # be passed along. Here are some common ones. There can be many more\n    multiple: React.PropTypes.bool\n    required: React.PropTypes.bool\n    prefilled: React.PropTypes.bool\n    maxlength: React.PropTypes.number\n    placeholder: React.PropTypes.node\n    tabIndex: React.PropTypes.oneOfType([\n      React.PropTypes.number,\n      React.PropTypes.string,\n    ]),\n\n    #### Used by \"reference\" type objects\n    customComponent: React.PropTypes.func\n    contextData: React.PropTypes.object,\n    referenceTo: React.PropTypes.oneOfType([\n      React.PropTypes.array,\n      React.PropTypes.string,\n    ])\n    referenceType: React.PropTypes.oneOf([\"belongsTo\", \"hasMany\", \"hasManyThrough\"])\n    referenceThrough: React.PropTypes.string\n\n    currentFormValues: React.PropTypes.object\n\n  render: =>\n    classes = classNames\n      \"prefilled\": @props.prefilled\n      \"form-item\": true\n      \"invalid\": !@_isValid()\n      \"valid\": @_isValid()\n\n    label = @props.label\n    if @props.required\n      label = <strong><span className=\"required\">*</span>{@props.label}</strong>\n\n    if @props.type is \"hidden\"\n      @_renderInput()\n    else\n      <div className={classes} ref=\"inputWrap\">\n        <div className=\"label-area\">\n          <label htmlFor={@props.id}>{label}</label>\n        </div>\n        <div className=\"input-area\">\n          {@_renderInput()}\n          {@_renderError()}\n        </div>\n      </div>\n\n  shouldComponentUpdate: (nextProps) =>\n    not Utils.isEqualReact(nextProps, @props)\n\n  componentDidUpdate: (prevProps) ->\n    if !prevProps.formItemError and !@_isValid()\n      ReactDOM.findDOMNode(@refs.inputWrap).scrollIntoView(true)\n\n  _isValid: ->\n    !@props.formItemError\n\n  _renderError: =>\n    return false if @_isValid()\n    msg = @props.formItemError.message\n    <div className=\"form-error\">{msg}</div>\n\n  _isDisabled: =>\n    @props.disabled or\n    (@props.formType is \"new\" and @props.editableForNew is false) or\n    (@props.formType is \"update\" and @props.editableForUpdate is false)\n\n  _renderInput: =>\n    inputProps = _.extend {}, @props,\n      ref: \"input\"\n      onChange: (eventOrValue) =>\n        @props.onChange(@props.id, ((eventOrValue?.target?.value) ? eventOrValue))\n\n    if @_isDisabled() then inputProps.disabled = true\n\n    if FormItem.inputElementTypes[@props.type]\n      React.createElement(\"input\", inputProps)\n    else if @props.type is \"select\"\n      options = (@props.selectOptions ? []).map (optionData) ->\n        <option {...optionData} key={\"#{Utils.generateTempId()}-optionData.value\"} >{optionData.label}</option>\n      options.unshift(<option key={\"#{Utils.generateTempId()}-blank-option\"}></option>)\n      <select {...inputProps}>{options}</select>\n    else if @props.type is \"textarea\"\n      React.createElement(\"textarea\", inputProps)\n    else if @props.type is \"date\"\n      inputProps.dateFormat = \"YYYY-MM-DD\"\n      React.createElement(DatePicker, inputProps)\n    else if @props.type is \"EmptySpace\"\n      React.createElement(\"div\", {className: \"empty-space\"})\n    else if _.isFunction(@props.customComponent)\n      React.createElement(@props.customComponent, inputProps)\n    else\n      console.warn \"We do not support type #{@props.type} with attributes:\", inputProps\n\nclass GeneratedFieldset extends React.Component\n  @displayName: \"GeneratedFieldset\"\n\n  @propTypes:\n    # Some sort of unique identifier\n    id: idPropType.isRequired\n\n    formItems: React.PropTypes.arrayOf(React.PropTypes.shape(\n      _.extend(FormItem.propTypes,\n        row: React.PropTypes.number\n        column: React.PropTypes.number\n      )\n    ))\n\n    # The key is the formItem id, the value is the error object\n    formItemErrors: React.PropTypes.arrayOf(FormItem.propTypes.formItemError)\n\n    # A function that takes two arguments:\n    #   - The id of this GeneratedFieldset\n    #   - A new array of updated formItems with the correct value.\n    onChange: React.PropTypes.func\n\n    heading: React.PropTypes.node\n    useHeading: React.PropTypes.bool\n    formType: React.PropTypes.oneOf(['new', 'update'])\n    zIndex: React.PropTypes.number\n\n    lastFieldset: React.PropTypes.bool\n    firstFieldset: React.PropTypes.bool\n    contextData: React.PropTypes.object\n\n    currentFormValues: React.PropTypes.object\n\n  render: =>\n    classStr = classNames\n      \"first-fieldset\": @props.firstFieldset\n      \"last-fieldset\": @props.lastFieldset\n\n    <fieldset style={{zIndex: @props.zIndex ? 0}} className={classStr} >\n      {@_renderHeader()}\n      <div className=\"fieldset-form-items\">\n        {@_renderFormItems()}\n      </div>\n      {@_renderFooter()}\n    </fieldset>\n\n  shouldComponentUpdate: (nextProps) =>\n    not Utils.isEqualReact(nextProps, @props)\n\n  _renderHeader: =>\n    if @props.useHeading\n      <header><legend>{@props.heading}</legend></header>\n    else <div></div>\n\n  _renderFormItems: =>\n    byRow = _.groupBy(@props.formItems, \"row\")\n    _.map byRow, (itemsInRow=[], rowNum) =>\n      byCol = _.groupBy(itemsInRow, \"column\")\n      numCols = Math.max.apply(null, Object.keys(byCol))\n\n      style = { zIndex: 1000-rowNum }\n      allHidden = _.every(itemsInRow, (item) -> item.type is \"hidden\")\n      if allHidden then style.display = \"none\"\n\n      <div className=\"row\"\n           data-row-num={rowNum}\n           style={style}\n           key={rowNum}>\n        {_.map byCol, (itemsInCol=[], colNum) =>\n          colEls = [<div className=\"column\" data-col-num={colNum} key={colNum}>\n            {itemsInCol.map (formItemData) =>\n              props = @_propsFromFormItemData(formItemData)\n              <FormItem {...props} ref={\"form-item-#{formItemData.id}\"}/>\n            }\n          </div>]\n          if colNum isnt numCols - 1\n            colEls.push(\n              <div className=\"column-spacer\" data-col-num={\"#{colNum}-spacer\"} key={\"#{colNum}-spacer\"}>\n              </div>\n            )\n          return colEls\n        }\n      </div>\n\n  # Given the raw data of an individual FormItem, prepare a set of props\n  # to pass down into the FormItem.\n  _propsFromFormItemData: (formItemData) =>\n    props = _.clone(formItemData)\n    props.key = props.id\n    error = @props.formItemErrors?[props.id]\n    if error then props.formItemError = error\n    props.onChange = _.bind(@_onChangeItem, @)\n    props.formType = @props.formType\n    props.contextData = @props.contextData\n    props.currentFormValues = @props.currentFormValues\n    return props\n\n  _onChangeItem: (itemId, newValue) =>\n    newFormItems = _.map @props.formItems, (formItem) ->\n      if formItem.id is itemId\n        newFormItem = _.clone(formItem)\n        newFormItem.value = newValue\n        return newFormItem\n      else return formItem\n    @props.onChange(@props.id, newFormItems)\n\n  _renderFooter: =>\n    <footer></footer>\n\nclass GeneratedForm extends React.Component\n  @displayName: \"GeneratedForm\"\n\n  @propTypes:\n    # Some sort of unique identifier\n    id: idPropType\n\n    errors: React.PropTypes.shape(\n      formError: React.PropTypes.shape(\n        message: React.PropTypes.string\n        location: React.PropTypes.string # Can be \"header\" (default) or \"footer\"\n      )\n      formItemErrors: GeneratedFieldset.propTypes.formItemErrors\n    )\n\n    fieldsets: React.PropTypes.arrayOf(\n      React.PropTypes.shape(GeneratedFieldset.propTypes)\n    )\n\n    # A function whose argument is a new set of Props\n    onChange: React.PropTypes.func.isRequired\n\n    onSubmit: React.PropTypes.func.isRequired\n\n    style: React.PropTypes.object\n\n    formType: React.PropTypes.oneOf(['new', 'update'])\n    prefilled: React.PropTypes.bool\n    contextData: React.PropTypes.object,\n\n  @defaultProps:\n    style: {}\n    onSubmit: ->\n\n  render: =>\n    submitText = if @props.formType is \"new\" then \"Create\" else \"Update\"\n    <form className=\"generated-form\" ref=\"form\" style={this.props.style} onSubmit={this.props.onSubmit} onKeyDown={this._onKeyDown} noValidate>\n      <TabGroupRegion>\n        {@_renderHeaderFormError()}\n        {@_renderPrefilledMessage()}\n        <div className=\"fieldsets\">\n          {@_renderFieldsets()}\n        </div>\n        <div className=\"form-footer\">\n          <button type=\"button\" className=\"btn btn-emphasis\" onClick={this.props.onSubmit}>\n            {submitText}\n          </button>\n        </div>\n      </TabGroupRegion>\n    </form>\n\n  shouldComponentUpdate: (nextProps) =>\n    not Utils.isEqualReact(nextProps, @props)\n\n  componentDidUpdate: (prevProps) ->\n    if !prevProps.errors?.formError and @props.errors?.formError\n      ReactDOM.findDOMNode(@refs.formHeaderError).scrollIntoView(true)\n\n  _onKeyDown: (event) =>\n    if event.key is \"Enter\" && (event.metaKey || event.ctrlKey)\n      this.props.onSubmit(event)\n\n  _renderPrefilledMessage: =>\n    if @props.prefilled\n      <div className=\"prefilled-message\">\n        The <span className=\"highlighted\">highlighted</span> fields have been prefilled for you!\n      </div>\n\n  _renderHeaderFormError: =>\n    if @props.errors?.formError\n      <div ref=\"formHeaderError\" className=\"form-error form-header-error\">\n        {@props.errors.formError.message}\n      </div>\n    else return false\n\n  _renderFieldsets: =>\n    (@props.fieldsets ? []).map (fieldset, i) =>\n      props = @_propsFromFieldsetData(fieldset)\n      props.zIndex = 100-i\n      props.contextData = @props.contextData\n      props.currentFormValues = @_currentFormValues()\n      props.firstFieldset = i is 0\n      props.lastFieldset = i isnt 0 and i is @props.fieldsets.length - 1\n      <GeneratedFieldset {...props} ref={\"fieldset-#{fieldset.id}\"} />\n\n  _currentFormValues: =>\n    vals = {}\n    for fieldset in @props.fieldsets\n      for formItem in fieldset.formItems\n        vals[formItem.name] = formItem.value\n    return vals\n\n  _propsFromFieldsetData: (fieldsetData) =>\n    props = _.clone(fieldsetData)\n    errors = @props.errors?.formItemErrors\n    if errors then props.formItemErrors = errors\n    props.key = fieldsetData.id\n    props.onChange = _.bind(@_onChangeFieldset, @)\n    props.formType = @props.formType\n    return props\n\n  _onChangeFieldset: (fieldsetId, newFormItems) =>\n    newFieldsets = _.map @props.fieldsets, (fieldset) ->\n      if fieldset.id is fieldsetId\n        newFieldset = _.clone(fieldset)\n        newFieldset.formItems = newFormItems\n        return newFieldset\n      else return fieldset\n\n    @props.onChange _.extend {}, @props,\n      fieldsets: newFieldsets\n\nmodule.exports =\n  FormItem: FormItem\n  GeneratedForm: GeneratedForm\n  GeneratedFieldset: GeneratedFieldset\n"
  },
  {
    "path": "packages/client-app/src/components/injected-component-label.jsx",
    "content": "/* eslint react/prefer-stateless-function: 0 */\nimport React from 'react';\n\nexport default class InjectedComponentLabel extends React.Component {\n  static displayName = 'InjectedComponentLabel';\n\n  static propTypes = {\n    matching: React.PropTypes.object,\n  };\n\n  render() {\n    const matchingDescriptions = [];\n\n    for (const key of Object.keys(this.props.matching)) {\n      let val = this.props.matching[key];\n      if (key === 'location') {\n        val = val.id;\n      }\n      if (key === 'locations') {\n        val = val.map(v => v.id);\n      }\n      matchingDescriptions.push(`${key}: ${val}`);\n    }\n\n    const propDescriptions = [];\n    for (const key of Object.keys(this.props)) {\n      const val = this.props[key];\n      if (key === 'matching') {\n        continue;\n      }\n      propDescriptions.push(`${key}:<${val.constructor ? val.constructor.name : typeof val}>`);\n    }\n\n    let description = ` ${matchingDescriptions.join(', ')}`;\n    if (propDescriptions.length > 0) {\n      description += ` (${propDescriptions.join(', ')})`;\n    }\n\n    return (\n      <span className=\"name\">\n        {description}\n      </span>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/injected-component-set.cjsx",
    "content": "React = require 'react'\n_ = require 'underscore'\nUnsafeComponent = require './unsafe-component'\nFlexbox = require('./flexbox').default\nInjectedComponentLabel = require('./injected-component-label').default\n{Utils,\n Actions,\n WorkspaceStore,\n ComponentRegistry} = require \"nylas-exports\"\n\n\n###\nPublic: InjectedComponent makes it easy to include a set of dynamically registered\ncomponents inside of your React render method. Rather than explicitly render\nan array of buttons, for example, you can use InjectedComponentSet:\n\n```coffee\n<InjectedComponentSet className=\"message-actions\"\n                  matching={role: 'ThreadActionButton'}\n                  exposedProps={thread:@props.thread, message:@props.message}>\n```\n\nInjectedComponentSet will look up components registered for the location you provide,\nrender them inside a {Flexbox} and pass them `exposedProps`. By default, all injected\nchildren are rendered inside {UnsafeComponent} wrappers to prevent third-party code\nfrom throwing exceptions that break React renders.\n\nInjectedComponentSet monitors the ComponentRegistry for changes. If a new component\nis registered into the location you provide, InjectedComponentSet will re-render.\n\nIf no matching components is found, the InjectedComponent renders an empty span.\n\nSection: Component Kit\n###\nclass InjectedComponentSet extends React.Component\n  @displayName: 'InjectedComponentSet'\n\n  ###\n  Public: React `props` supported by InjectedComponentSet:\n\n   - `matching` Pass an {Object} with ComponentRegistry descriptors\n      This set of descriptors is provided to {ComponentRegistry::findComponentsForDescriptor}\n      to retrieve components for display.\n   - `matchLimit` (optional) A {Number} that indicates the max number of matching elements to render\n   - `className` (optional) A {String} class name for the containing element.\n   - `children` (optional) Any React elements rendered inside the InjectedComponentSet\n      will always be displayed.\n   - `onComponentsDidRender` Callback that will be called when the injected component set\n      is successfully rendered onto the DOM.\n   - `exposedProps` (optional) An {Object} with props that will be passed to each\n      item rendered into the set.\n   - `containersRequired` (optional). Pass false to optionally remove the containers\n      placed around injected components to isolate them from the rest of the app.\n\n   -  Any other props you provide, such as `direction`, `data-column`, etc.\n      will be applied to the {Flexbox} rendered by the InjectedComponentSet.\n  ###\n  @propTypes:\n    matching: React.PropTypes.object.isRequired\n    children: React.PropTypes.array\n    className: React.PropTypes.string\n    matchLimit: React.PropTypes.number\n    exposedProps: React.PropTypes.object\n    containersRequired: React.PropTypes.bool\n    onComponentsDidRender: React.PropTypes.func\n\n  @defaultProps:\n    direction: 'row'\n    containersRequired: true\n    onComponentsDidRender: ->\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n    @_renderedComponents = new Set()\n\n  componentDidMount: =>\n    @_componentUnlistener = ComponentRegistry.listen =>\n      @setState(@_getStateFromStores())\n    @props.onComponentsDidRender() if @props.containersRequired is false\n\n  componentWillUnmount: =>\n    @_componentUnlistener() if @_componentUnlistener\n\n  componentWillReceiveProps: (newProps) =>\n    if newProps.location isnt @props?.location\n      @setState(@_getStateFromStores(newProps))\n\n  componentDidUpdate: =>\n    @props.onComponentsDidRender() if @props.containersRequired is false\n\n  render: =>\n    @_renderedComponents = new Set()\n    flexboxProps = Utils.fastOmit(@props, Object.keys(@constructor.propTypes))\n    flexboxClassName = @props.className ? \"\"\n    exposedProps = @props.exposedProps ? {}\n\n    elements = @state.components.map (Component) =>\n      if @props.containersRequired is false or Component.containerRequired is false\n        return <Component key={Component.displayName} {...exposedProps} />\n      else\n        return (\n          <UnsafeComponent\n            key={Component.displayName}\n            component={Component}\n            onComponentDidRender={@_onComponentDidRender.bind(@, Component.displayName)}\n            {...exposedProps} />\n        )\n\n\n    if @state.visible\n      flexboxClassName += \" registered-region-visible\"\n      elements.splice(0,0, <InjectedComponentLabel key=\"_label\" matching={@props.matching} {...exposedProps} />)\n      elements.push(<span key=\"_clear\" style={clear:'both'}/>)\n\n    <Flexbox className={flexboxClassName} {...flexboxProps}>\n      {elements}\n      {@props.children ? []}\n    </Flexbox>\n\n  _onComponentDidRender: (componentName) =>\n    @_renderedComponents.add(componentName)\n    if @_renderedComponents.size is @state.components.length\n      @props.onComponentsDidRender()\n\n  _getStateFromStores: (props) =>\n    props ?= @props\n    limit = props.matchLimit\n\n    components: ComponentRegistry.findComponentsMatching(@props.matching)[...limit]\n    visible: ComponentRegistry.showComponentRegions()\n\n\nmodule.exports = InjectedComponentSet\n"
  },
  {
    "path": "packages/client-app/src/components/injected-component.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\n_ = require 'underscore'\nUnsafeComponent = require './unsafe-component'\nInjectedComponentLabel = require('./injected-component-label').default\n\n{Actions,\n WorkspaceStore,\n ComponentRegistry} = require \"nylas-exports\"\n\n###\nPublic: InjectedComponent makes it easy to include dynamically registered\ncomponents inside of your React render method. Rather than explicitly render\na component, such as a `<Composer>`, you can use InjectedComponent:\n\n```coffee\n<InjectedComponent matching={role:\"Composer\"} exposedProps={draftClientId:123} />\n```\n\nInjectedComponent will look up the component registered with that role in the\n{ComponentRegistry} and render it, passing the exposedProps (`draftClientId={123}`) along.\n\nInjectedComponent monitors the ComponentRegistry for changes. If a new component\nis registered that matches the descriptor you provide, InjectedComponent will refresh.\n\nIf no matching component is found, the InjectedComponent renders an empty div.\n\nSection: Component Kit\n###\nclass InjectedComponent extends React.Component\n  @displayName: 'InjectedComponent'\n\n  ###\n  Public: React `props` supported by InjectedComponent:\n\n   - `matching` Pass an {Object} with ComponentRegistry descriptors.\n      This set of descriptors is provided to {ComponentRegistry::findComponentsForDescriptor}\n      to retrieve the component that will be displayed.\n\n   - `onComponentDidRender` (optional) Callback that will be called when the injected component\n      is successfully rendered onto the DOM.\n\n   - `className` (optional) A {String} class name for the containing element.\n\n   - `exposedProps` (optional) An {Object} with props that will be passed to each\n      item rendered into the set.\n\n   - `fallback` (optional) A {Component} to default to in case there are no matching\n     components in the ComponentRegistry\n\n   - `requiredMethods` (options) An {Array} with a list of methods that should be\n     implemented by the registered component instance. If these are not implemented,\n     an error will be thrown.\n\n  ###\n  @propTypes:\n    matching: React.PropTypes.object.isRequired\n    className: React.PropTypes.string\n    exposedProps: React.PropTypes.object\n    fallback: React.PropTypes.func\n    onComponentDidRender: React.PropTypes.func\n    style: React.PropTypes.object\n    requiredMethods: React.PropTypes.arrayOf(React.PropTypes.string)\n    onComponentDidChange: React.PropTypes.func\n\n  @defaultProps:\n    style: {}\n    exposedProps: {}\n    requiredMethods: []\n    onComponentDidRender: ->\n    onComponentDidChange: ->\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n    @_verifyRequiredMethods()\n    @_setRequiredMethods(@props.requiredMethods)\n\n  componentDidMount: =>\n    @_componentUnlistener = ComponentRegistry.listen =>\n      @setState(@_getStateFromStores())\n    if @state.component?.containerRequired is false\n      @props.onComponentDidRender()\n      @props.onComponentDidChange()\n\n  componentWillUnmount: =>\n    @_componentUnlistener() if @_componentUnlistener\n\n  componentWillReceiveProps: (newProps) =>\n    if not _.isEqual(newProps.matching, @props?.matching)\n      @setState(@_getStateFromStores(newProps))\n\n  componentDidUpdate: (prevProps, prevState) =>\n    @_setRequiredMethods(@props.requiredMethods)\n    if @state.component?.containerRequired is false\n      @props.onComponentDidRender()\n      if @state.component isnt prevState.component\n        @props.onComponentDidChange()\n\n\n  render: =>\n    return <div></div> unless @state.component\n\n    exposedProps = Object.assign({}, @props.exposedProps, {fallback: @props.fallback})\n    className = @props.className ? \"\"\n    className += \" registered-region-visible\" if @state.visible\n\n    Component = @state.component\n    if Component.containerRequired is false\n      privateProps = {\n        key: Component.displayName,\n      }\n      if React.Component.isPrototypeOf(Component)\n        privateProps.ref = 'inner'\n      element = <Component {...privateProps} {...exposedProps} />\n    else\n      element = (\n        <UnsafeComponent\n          ref=\"inner\"\n          style={@props.style}\n          className={className}\n          key={Component.displayName}\n          component={Component}\n          onComponentDidRender={@props.onComponentDidRender}\n          {...exposedProps} />\n      )\n\n    if @state.visible\n      <div className={className} style={@props.style}>\n        {element}\n        <InjectedComponentLabel matching={@props.matching} {...exposedProps} />\n        <span style={clear:'both'}/>\n      </div>\n    else\n      <div className={className} style={@props.style}>\n        {element}\n      </div>\n\n  focus: =>\n    @_runInnerDOMMethod('focus')\n\n  blur: =>\n    @_runInnerDOMMethod('blur')\n\n  # Private: Attempts to run the DOM method, ie 'focus', on\n  # 1. Any implementation provided by the inner component\n  # 2. Any native implementation provided by the DOM\n  # 3. Ourselves, so that the method always has /some/ effect.\n  #\n  _runInnerDOMMethod: (method, args...) =>\n    target = null\n    if @refs.inner instanceof UnsafeComponent and @refs.inner.injected[method]?\n      target = @refs.inner.injected\n    else if @refs.inner and @refs.inner[method]?\n      target = @refs.inner\n    else if @refs.inner\n      target = ReactDOM.findDOMNode(@refs.inner)\n    else\n      target = ReactDOM.findDOMNode(@)\n\n    if target[method]\n      target[method].bind(target)(args...)\n\n  _setRequiredMethods: (methods) =>\n    methods.forEach (method) =>\n      Object.defineProperty(@, method,\n        configurable: true\n        enumerable: true\n        value: (args...) =>\n          @_runInnerDOMMethod(method, args...)\n      )\n\n  _verifyRequiredMethods: =>\n    if @state.component?\n      component = @state.component\n      @props.requiredMethods.forEach (method) =>\n        isMethodDefined = component.prototype[method]?\n        unless isMethodDefined\n          throw new Error(\n            \"#{component.name} must implement method `#{method}` when registering\n            for #{JSON.stringify(@props.matching)}\"\n          )\n\n  _getStateFromStores: (props) =>\n    props ?= @props\n\n    components = ComponentRegistry.findComponentsMatching(props.matching)\n    if components.length > 1\n      console.warn(\"There are multiple components available for \\\n                   #{JSON.stringify(props.matching)}. <InjectedComponent> is \\\n                   only rendering the first one.\")\n    component = if components.length is 0\n      @props.fallback\n    else\n      components[0]\n\n    component: component\n    visible: ComponentRegistry.showComponentRegions()\n\nmodule.exports = InjectedComponent\n"
  },
  {
    "path": "packages/client-app/src/components/key-commands-region.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport classNames from 'classnames'\n\n/*\nPublic: Easily respond to keyboard shortcuts\n\nA keyboard shortcut has two parts to it:\n\n1. A mapping between keyboard actions and a command\n2. A mapping between a command and a callback handler\n\n\n#// Mapping keys to commands (not handled by this component)\n\nThe **keyboard -> command** mapping is defined in a separate `.cson` file.\nA majority of the commands your component would want to listen to you have\nalready been defined by core N1 defaults, as well as custom user\noverrides. See 'keymaps/base.cson' for more information.\n\nYou can define additional, custom keyboard -> command mappings in your own\npackage-specific keymap `.cson` file. The file can be named anything but\nmust exist in a folder called `keymaps` in the root of your package's\ndirectory.\n\n\n#// Mapping commands to callbacks (handled by this component)\n\nWhen a keystroke sequence matches a binding in a given context, a custom\nDOM event with a type based on the command is dispatched on the target of\nthe keyboard event.\n\nThat custom DOM event (whose type is the command you want to listen to)\nwill propagate up from its original target. That original target may or\nmay not be a descendent of your <KeyCommandsRegion> component.\n\nFrequently components will want to listen to a keyboard command regardless\nof where it was fired from. For those, use the `globalHandlers` prop. The\nDOM event will NOT be passed to `globalHandlers` callbacks.\n\nComponents may also want to listen to keyboard commands that originate\nwithin one of their descendents. For those use the `localHandlers` prop.\nThe DOM event WILL be passed to `localHandlers` callback because it is\nsometimes valuable to call `stopPropagataion` on the custom command event.\n\nProps:\n\n- `localHandlers` A mapping between key commands and callbacks for key command events that originate within a descendent of this component.\n- `globalHandlers` A mapping between key commands and callbacks for key\ncommands that originate from anywhere and are global in scope.\n- `className` The unique class name that shows up in your keymap.cson\n\nExample:\n\nIn `my-package/lib/my-component.cjsx`:\n\n```js\nclass MyComponent extends React.Component {\n  render() {\n    return (\n      <KeyCommandsRegion\n        globalHandlers={{\n          \"core:moveDown\": this.onMoveDown\n          \"core:selectItem\": this.onSelectItem\n        }}\n        className=\"my-component\"\n      >\n        <div>... sweet component ...</div>\n      </KeyCommandsRegion>\n    );\n  }\n}\n```\n\nIn `my-package/keymaps/my-package.cson`:\n\n```js\n\".my-component\":\n  \"cmd-t\": \"selectItem\"\n  \"cmd-enter\": \"sendMessage\"\n```\n*/\nexport default class KeyCommandsRegion extends React.Component {\n  static displayName = \"KeyCommandsRegion\";\n\n  static propTypes = {\n    className: React.PropTypes.string,\n    localHandlers: React.PropTypes.object,\n    globalHandlers: React.PropTypes.object,\n    globalMenuItems: React.PropTypes.array,\n    onFocusIn: React.PropTypes.func,\n    onFocusOut: React.PropTypes.func,\n  };\n\n  static defaultProps = {\n    className: \"\",\n    localHandlers: null,\n    globalHandlers: null,\n    globalMenuItems: null,\n    onFocusIn: () => {},\n    onFocusOut: () => {},\n  };\n\n  constructor(props) {\n    super(props);\n    this._lostFocusToElement = null\n    this.state = {\n      focused: false,\n    };\n  }\n\n  componentDidMount() {\n    this._setupListeners(this.props);\n    if (this.props.globalMenuItems) {\n      this._menuDisposable = NylasEnv.menu.add(this.props.globalMenuItems);\n    }\n  }\n\n  componentWillReceiveProps(newProps) {\n    this._unmountListeners()\n    this._setupListeners(newProps)\n\n    // Updating menus in particular is expensive, so avoid teardown / re-add if identical\n    if (!_.isEqual(newProps.globalMenuItems, this.props.globalMenuItems)) {\n      if (this._menuDisposable) {\n        this._menuDisposable.dispose();\n      }\n      this._menuDisposable = NylasEnv.menu.add(newProps.globalMenuItems);\n    }\n  }\n\n  componentWillUnmount() {\n    this._losingFocusToElement = null;\n    this._unmountListeners();\n    if (this._menuDisposable) {\n      this._menuDisposable.dispose();\n    }\n    this._menuDisposable = null;\n  }\n\n  // When the {KeymapManager} finds a valid keymap in a `.cson` file, it\n  // will create a CustomEvent with the command name as its type. That\n  // custom event will be fired at the originating target and propogate\n  // updwards until it reaches the root window level.\n\n    // An event is scoped in the `.cson` files. Since we use that to\n  // determine which keymappings can fire a particular command in a\n  // particular scope, we simply need to listen at the root window level\n  // here for all commands coming in.\n  _setupListeners(props) {\n    const $el = ReactDOM.findDOMNode(this);\n    $el.addEventListener('focusin', this._onFocusIn);\n    $el.addEventListener('focusout', this._onFocusOut);\n\n    if (props.globalHandlers) {\n      this._globalDisposable = NylasEnv.commands.add(document.body, props.globalHandlers);\n    }\n    if (props.localHandlers) {\n      this._localDisposable = NylasEnv.commands.add($el, props.localHandlers);\n    }\n\n    window.addEventListener('browser-window-blur', this._onWindowBlur);\n  }\n\n  _unmountListeners() {\n    if (this._globalDisposable) {\n      this._globalDisposable.dispose();\n      this._globalDisposable = null;\n    }\n    if (this._localDisposable) {\n      this._localDisposable.dispose();\n      this._localDisposable = null;\n    }\n    const $el = ReactDOM.findDOMNode(this);\n    $el.removeEventListener('focusin', this._onFocusIn);\n    $el.removeEventListener('focusout', this._onDidFocusOut);\n    window.removeEventListener('browser-window-blur', this._onWindowBlur);\n    this._goingout = false;\n  }\n\n  _onWindowBlur = () => {\n    this.setState({focused: false});\n  }\n\n  _onFocusIn = (event) => {\n    this._lastFocusElement = event.target;\n    this._losingFocusToElement = null;\n    if (this.state.focused === false) {\n      this.props.onFocusIn(event);\n    }\n    this.setState({focused: true});\n  }\n\n  _onFocusOut = (event) => {\n    this._lastFocusElement = event.target;\n    this._losingFocusToElement = event.relatedTarget;\n\n    // Focus could be lost for a moment and programatically restored. To support\n    // old machines with slow CPUs, it's important we wait N frames rather than X\n    // msec to see if focus is restored before declaring it \"out\" for good.\n    const attempt = () => {\n      if (!this._losingFocusToElement) {\n        this._losingFocusFrames = 0;\n        return;\n      }\n\n      this._losingFocusFrames -= 1;\n      if (this._losingFocusFrames === 0) {\n        this._onDefinitelyFocusedOut();\n      } else {\n        window.requestAnimationFrame(attempt);\n      }\n    };\n\n    if (!this._losingFocusFrames) {\n      window.requestAnimationFrame(attempt);\n    }\n    this._losingFocusFrames = 10; // at 60fps, roughly 150ms\n  }\n\n  _onDefinitelyFocusedOut = () => {\n    if (!this._losingFocusToElement) {\n      return;\n    }\n    if (!this.state.focused) {\n      return;\n    }\n\n    const sel = document.getSelection();\n    const activeEl = document.activeElement;\n\n    // This happens when component that used to have the focus is\n    // unmounted. An example is the url input field of the\n    // FloatingToolbar in the Composer's Contenteditable\n    if (ReactDOM.findDOMNode(this).contains(activeEl)) {\n      return;\n    }\n\n    // In some scenarios, focus has left an element but it is still /selected/.\n    // This can be really confusing, so we guard against it here.\n\n    // To Repro: From the composer body, click the thread list column.\n    // Then keep typing. Notice how the blurred body still accepts the input.\n    if (sel && sel.focusNode && !activeEl.parentElement.contains(sel.focusNode)) {\n      document.getSelection().empty();\n    }\n\n    this.props.onFocusOut(this._lastFocusElement);\n    this.setState({focused: false});\n    this._losingFocusToElement = null;\n  }\n\n  render() {\n    const classname = classNames({\n      'key-commands-region': true,\n      'focused': this.state.focused,\n    });\n    const otherProps = _.omit(this.props, Object.keys(this.constructor.propTypes));\n\n    return (\n      <div className={`${classname} ${this.props.className}`} {...otherProps}>\n        {this.props.children}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/lazy-rendered-list.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {findDOMNode} from 'react-dom'\n\n\nconst MIN_RANGE_SIZE = 2\n\nfunction getRange({total, itemHeight, containerHeight, scrollTop = 0} = {}) {\n  const itemsPerBody = Math.floor((containerHeight) / itemHeight);\n  const start = Math.max(0, Math.floor(scrollTop / itemHeight) - (itemsPerBody * 2));\n  const end = Math.max(MIN_RANGE_SIZE, Math.min(start + (4 * itemsPerBody), total));\n  return {start, end}\n}\n\nclass LazyRenderedList extends Component {\n  static propTypes = {\n    items: PropTypes.array,\n    itemHeight: PropTypes.number,\n    containerHeight: PropTypes.number,\n    BufferTag: PropTypes.string,\n    ItemRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),\n    RootRenderer: PropTypes.oneOfType([PropTypes.func, PropTypes.string]),\n  }\n\n  static defaultProps = {\n    items: [],\n    itemHeight: 30,\n    containerHeight: 150,\n    BufferTag: 'div',\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = this.getRangeState(props)\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.updateRangeState(nextProps)\n  }\n\n  onScroll = () => {\n    this.updateRangeState(this.props)\n  }\n\n  getRangeState({items, itemHeight, containerHeight, scrollTop}) {\n    return getRange({total: items.length, itemHeight, containerHeight, scrollTop})\n  }\n\n  updateRangeState(props) {\n    const {scrollTop} = findDOMNode(this)\n    this.setState(this.getRangeState({...props, scrollTop}))\n  }\n\n  renderItems() {\n    const {items, itemHeight, BufferTag, ItemRenderer} = this.props\n    const {start, end} = this.state\n    const topHeight = start * itemHeight\n    const bottomHeight = (items.length - end) * itemHeight\n\n    const top = <BufferTag key=\"lazy-top\" style={{height: topHeight}} />\n    const bottom = <BufferTag key=\"lazy-bottom\" style={{height: bottomHeight}} />\n    const elements = items.slice(start, end).map((item, idx) => (\n      <ItemRenderer\n        key={`item-${start + idx}`}\n        item={item}\n        idx={start + idx}\n      />\n    ))\n    elements.unshift(top)\n    elements.push(bottom)\n\n    return elements\n  }\n\n  render() {\n    const {RootRenderer, containerHeight} = this.props\n    return (\n      <RootRenderer\n        style={{height: containerHeight, overflowX: 'hidden', overflowY: 'auto'}}\n        onScroll={this.onScroll}\n      >\n        {this.renderItems()}\n      </RootRenderer>\n    )\n  }\n}\n\n\nexport default LazyRenderedList\n"
  },
  {
    "path": "packages/client-app/src/components/list-data-source.es6",
    "content": "/* eslint no-unused-vars: 0 */\nimport {EventEmitter} from 'events'\nimport ListSelection from './list-selection'\n\nexport default class ListDataSource {\n  constructor() {\n    this._emitter = new EventEmitter();\n    this._cleanedup = false;\n    this.selection = new ListSelection(this, this.trigger);\n  }\n\n  // Accessing Data\n\n  trigger = (arg) => {\n    this._emitter.emit('trigger', arg);\n  }\n\n  listen(callback, bindContext) {\n    if (!(callback instanceof Function)) {\n      throw new Error(\"ListDataSource: You must pass a function to `listen`\");\n    }\n    if (this._cleanedup === true) {\n      throw new Error(\"ListDataSource: You cannot listen again after removing the last listener. This is an implementation detail.\");\n    }\n\n    const eventHandler = (...args) => {\n      callback.apply(bindContext, args);\n    }\n    this._emitter.addListener('trigger', eventHandler);\n\n    return () => {\n      this._emitter.removeListener('trigger', eventHandler);\n      setTimeout(() => {\n        if (this._emitter.listenerCount('trigger') === 0) {\n          this._cleanedup = true;\n          this.cleanup();\n        }\n      }, 0);\n    };\n  }\n\n  loaded() {\n    throw new Error(\"ListDataSource base class does not implement loaded()\");\n  }\n\n  empty() {\n    throw new Error(\"ListDataSource base class does not implement empty()\");\n  }\n\n  get(idx) {\n    throw new Error(\"ListDataSource base class does not implement get()\");\n  }\n\n  getById(id) {\n    throw new Error(\"ListDataSource base class does not implement getById()\");\n  }\n\n  indexOfId(id) {\n    throw new Error(\"ListDataSource base class does not implement indexOfId()\");\n  }\n\n  count() {\n    throw new Error(\"ListDataSource base class does not implement count()\");\n  }\n\n  itemsCurrentlyInViewMatching(matchFn) {\n    throw new Error(\"ListDataSource base class does not implement itemsCurrentlyInViewMatching()\");\n  }\n\n  setRetainedRange({start, end}) {\n    throw new Error(\"ListDataSource base class does not implement setRetainedRange()\");\n  }\n\n  cleanup() {\n    this.selection.cleanup();\n  }\n}\n\nclass EmptyListDataSource extends ListDataSource {\n  loaded() { return true }\n  empty() { return true }\n  get() { return null }\n  getById() { return null }\n  indexOfId() { return -1 }\n  count() { return 0 }\n  itemsCurrentlyInViewMatching() { return []; }\n  setRetainedRange() { return }\n}\n\nListDataSource.Empty = EmptyListDataSource\n"
  },
  {
    "path": "packages/client-app/src/components/list-selection.es6",
    "content": "import _ from 'underscore';\n\nimport Model from '../flux/models/model';\nimport DatabaseStore from '../flux/stores/database-store';\n\nexport default class ListSelection {\n\n  constructor(_view, callback) {\n    this._view = _view;\n    if (!this._view) {\n      throw new Error(\"new ListSelection(): You must provide a view.\");\n    }\n    this._unlisten = DatabaseStore.listen(this._applyChangeRecord, this);\n    this._caches = {};\n    this._items = [];\n\n    this.trigger = () => {\n      this._caches = {};\n      callback();\n    };\n  }\n\n  cleanup() {\n    this._unlisten();\n  }\n\n  count() {\n    return this._items.length;\n  }\n\n  ids() {\n    // ListTabular asks for ids /a lot/. Cache this value and clear it on trigger.\n    if (this._caches.ids == null) {\n      this._caches.ids = this._items.map(i => i.id);\n    }\n    return this._caches.ids;\n  }\n\n  items() {\n    return _.clone(this._items);\n  }\n\n  top() {\n    return this._items[this._items.length - 1];\n  }\n\n  clear() {\n    this.set([]);\n  }\n\n  set(items) {\n    this._items = [];\n    for (const item of items) {\n      if (!(item instanceof Model)) { throw new Error(\"set must be called with Models\"); }\n      this._items.push(item);\n    }\n    this.trigger(this);\n  }\n\n  toggle(item) {\n    if (!item) {\n      return;\n    }\n    if (!(item instanceof Model)) {\n      throw new Error(\"toggle must be called with a Model\");\n    }\n\n    const without = _.reject(this._items, t => t.id === item.id);\n    if (without.length < this._items.length) {\n      this._items = without;\n    } else {\n      this._items.push(item);\n    }\n    this.trigger(this);\n  }\n\n  add(item) {\n    if (!item) {\n      return;\n    }\n    if (!(item instanceof Model)) {\n      throw new Error(\"add must be called with a Model\");\n    }\n\n    const updated = this._items.filter(t => t.id !== item.id);\n    updated.push(item);\n\n    if (updated.length !== this._items.length) {\n      this._items = updated;\n      this.trigger(this);\n    }\n  }\n\n  remove(itemOrItems) {\n    if (!itemOrItems) {\n      return;\n    }\n\n    let items = itemOrItems;\n    if (!(items instanceof Array)) {\n      items = [items];\n    }\n\n    for (const item of items) {\n      if (!(item instanceof Model)) {\n        throw new Error(\"remove: Must be passed a model or array of models\");\n      }\n    }\n\n    const itemIds = items.map(i => i.id);\n    const nextItems = this._items.filter(t => !itemIds.includes(t.id));\n    if (nextItems.length < this._items.length) {\n      this._items = nextItems;\n      this.trigger(this);\n    }\n  }\n\n  removeItemsNotMatching(matchers) {\n    const count = this._items.length;\n    this._items = this._items.filter(t => t.matches(matchers));\n    if (this._items.length !== count) {\n      this.trigger(this);\n    }\n  }\n\n  expandTo(item) {\n    if (!item) {\n      return;\n    }\n    if (!(item instanceof Model)) {\n      throw new Error(\"expandTo must be called with a Model\");\n    }\n\n    if (this._items.length === 0) {\n      this._items.push(item);\n    } else {\n      // When expanding selection, you expand from the last selected item\n      // to the item the user clicked on. If the item is already selected,\n      // remove it from the selected array and reselect it so that the\n      // items are in the _items array in the order they were selected.\n      // (important for walking)\n      const relativeTo = this._items[this._items.length - 1];\n      const startIdx = this._view.indexOfId(relativeTo.id);\n      const endIdx = this._view.indexOfId(item.id);\n      if (startIdx === -1 || endIdx === -1) {\n        return;\n      }\n      const count = Math.abs(startIdx - endIdx) + 1\n      const indexes = new Array(count)\n        .fill(0)\n        .map((val, idx) => (startIdx > endIdx ? startIdx - idx : startIdx + idx))\n      indexes.forEach((idx) => {\n        const idxItem = this._view.get(idx);\n        this._items = _.reject(this._items, t => t.id === idxItem.id);\n        this._items.push(idxItem);\n      })\n    }\n    this.trigger();\n  }\n\n  walk({current, next}) {\n    // When the user holds shift and uses the arrow keys to modify their selection,\n    // we call that \"walking\". When walking you're usually selecting items. However,\n    // if you're walking \"back\" through your selection in the same order you selected\n    // them, you're undoing selections you've made. The order of the _items array\n    // is actually important - you can only deselect FROM the head back down the\n    // selection history.\n\n    const ids = this.ids();\n    const noSelection = this._items.length === 0;\n    const neitherSelected = (!current || ids.indexOf(current.id) === -1) && (!next || ids.indexOf(next.id) === -1);\n\n    if (noSelection || neitherSelected) {\n      if (current) { this._items.push(current); }\n      if (next) { this._items.push(next); }\n    } else {\n      let selectionPostPopHeadId = null;\n      if (this._items.length > 1) {\n        selectionPostPopHeadId = this._items[this._items.length - 2].id;\n      }\n\n      if (next.id === selectionPostPopHeadId) {\n        this._items.pop();\n      } else {\n        // Important: As you walk over this item, remove it and re-push it on the selected\n        // array even if it's already there. That way, the items in _items are always\n        // in the order you walked over them, and you can walk back to deselect them.\n        this._items = _.reject(this._items, t => t.id === next.id);\n        this._items.push(next);\n      }\n    }\n\n    return this.trigger();\n  }\n\n  _applyChangeRecord(change) {\n    if (this._items.length === 0) { return; }\n    if (change.objectClass !== this._items[0].constructor.name) { return; }\n\n    if (change.type === 'unpersist') {\n      this.remove(change.objects);\n    } else if (change.type === 'persist') {\n      let touched = 0;\n      for (const newer of change.objects) {\n        for (let idx = 0; idx < this._items.length; idx++) {\n          const existing = this._items[idx];\n          if (existing.id === newer.id) {\n            this._items[idx] = newer;\n            touched += 1;\n            break;\n          }\n        }\n      }\n      if (touched > 0) {\n        this.trigger(this);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/list-tabular-item.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nSwipeContainer = require('./swipe-container').default\n{Utils} = require 'nylas-exports'\n\nclass ListTabularItem extends React.Component\n  @displayName = 'ListTabularItem'\n  @propTypes =\n    metrics: React.PropTypes.object\n    columns: React.PropTypes.arrayOf(React.PropTypes.object).isRequired\n    item: React.PropTypes.object.isRequired\n    itemProps: React.PropTypes.object\n    onSelect: React.PropTypes.func\n    onClick: React.PropTypes.func\n    onDoubleClick: React.PropTypes.func\n\n  # DO NOT DELETE unless you know what you're doing! This method cuts\n  # React.Perf.wasted-time from ~300msec to 20msec by doing a deep\n  # comparison of props before triggering a re-render.\n  shouldComponentUpdate: (nextProps, nextState) =>\n    if not Utils.isEqualReact(@props.item, nextProps.item) or @props.columns isnt nextProps.columns\n      @_columnCache = null\n      return true\n    if not Utils.isEqualReact(Utils.fastOmit(@props, ['item']), Utils.fastOmit(nextProps, ['item']))\n      return true\n    false\n\n  render: =>\n    className = \"list-item list-tabular-item #{@props.itemProps?.className}\"\n    props = Utils.fastOmit(@props.itemProps ? {}, ['className'])\n\n    # It's expensive to compute the contents of columns (format timestamps, etc.)\n    # We only do it if the item prop has changed.\n    @_columnCache ?= @_columns()\n\n    <SwipeContainer {...props} onClick={@_onClick} style={position:'absolute', top: @props.metrics.top, width:'100%', height:@props.metrics.height}>\n      <div className={className} style={height: @props.metrics.height}>\n        {@_columnCache}\n      </div>\n    </SwipeContainer>\n\n  _columns: =>\n    names = {}\n    (@props.columns ? []).map (column) =>\n      if names[column.name]\n        console.warn(\"ListTabular: Columns do not have distinct names, will cause React error! `#{column.name}` twice.\")\n      names[column.name] = true\n\n      <div key={column.name}\n           style={{flex: column.flex, width: column.width}}\n           className=\"list-column list-column-#{column.name}\">\n        {column.resolver(@props.item, @)}\n      </div>\n\n  _onClick: (event) =>\n    @props.onSelect?(@props.item, event)\n\n    @props.onClick?(@props.item, event)\n    if @_lastClickTime? and Date.now() - @_lastClickTime < 350\n      @props.onDoubleClick?(@props.item, event)\n\n    @_lastClickTime = Date.now()\n\n\nmodule.exports = ListTabularItem\n"
  },
  {
    "path": "packages/client-app/src/components/list-tabular.jsx",
    "content": "import _ from 'underscore';\nimport React, {Component, PropTypes} from 'react';\nimport {Utils} from 'nylas-exports';\nimport ReactDOM from 'react-dom';\nimport ScrollRegion from './scroll-region';\nimport Spinner from './spinner';\n\nimport ListDataSource from './list-data-source';\nimport ListSelection from './list-selection';\nimport ListTabularItem from './list-tabular-item';\n\nclass ListColumn {\n  constructor({name, resolver, flex, width}) {\n    this.name = name;\n    this.resolver = resolver;\n    this.flex = flex;\n    this.width = width;\n  }\n}\n\nclass ListTabularRows extends Component {\n  static displayName = 'ListTabularRows';\n\n  static propTypes = {\n    rows: PropTypes.array,\n    columns: PropTypes.array.isRequired,\n    draggable: PropTypes.bool,\n    itemHeight: PropTypes.number,\n    innerStyles: PropTypes.object,\n    onSelect: PropTypes.func,\n    onClick: PropTypes.func,\n    onDoubleClick: PropTypes.func,\n    onDragStart: PropTypes.func,\n    onDragEnd: PropTypes.func,\n  };\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (\n      !Utils.isEqualReact(nextProps, this.props) ||\n      !Utils.isEqualReact(nextState, this.state)\n    )\n  }\n\n  renderRow({item, idx, itemProps = {}} = {}) {\n    if (!item) { return false }\n    const {\n      columns,\n      itemHeight,\n      onClick,\n      onSelect,\n      onDoubleClick,\n    } = this.props\n    return (\n      <ListTabularItem\n        key={item.id || idx}\n        item={item}\n        itemProps={itemProps}\n        metrics={{top: idx * itemHeight, height: itemHeight}}\n        columns={columns}\n        onSelect={onSelect}\n        onClick={onClick}\n        onDoubleClick={onDoubleClick}\n      />\n    )\n  }\n\n  render() {\n    const {rows, innerStyles, draggable, onDragStart, onDragEnd} = this.props\n    return (\n      <div\n        className=\"list-rows\"\n        style={innerStyles}\n        onDragStart={onDragStart}\n        onDragEnd={onDragEnd}\n        draggable={draggable}\n      >\n        {rows.map(r => this.renderRow(r))}\n      </div>\n    )\n  }\n}\n\nclass ListTabular extends Component {\n  static displayName = 'ListTabular';\n\n  static propTypes = {\n    footer: PropTypes.node,\n    draggable: PropTypes.bool,\n    columns: PropTypes.array.isRequired,\n    dataSource: PropTypes.object,\n    itemPropsProvider: PropTypes.func,\n    itemHeight: PropTypes.number,\n    EmptyComponent: PropTypes.func,\n    scrollTooltipComponent: PropTypes.func,\n    onClick: PropTypes.func,\n    onSelect: PropTypes.func,\n    onDoubleClick: PropTypes.func,\n    onDragStart: PropTypes.func,\n    onDragEnd: PropTypes.func,\n    onComponentDidUpdate: PropTypes.func,\n  };\n\n  static defaultProps = {\n    footer: false,\n    EmptyComponent: () => false,\n    itemPropsProvider: () => ({}),\n  }\n\n  static Item = ListTabularItem\n  static Column = ListColumn\n  static Selection = ListSelection\n  static DataSource = ListDataSource\n\n  constructor(props) {\n    super(props)\n    if (!props.itemHeight) {\n      throw new Error(\"ListTabular: You must provide an itemHeight - raising to avoid divide by zero errors.\");\n    }\n\n    this._unlisten = () => {}\n    this.state = this.buildStateForRange({start: -1, end: -1});\n  }\n\n  componentDidMount() {\n    window.addEventListener('resize', this.onWindowResize, true);\n    this.setupDataSource(this.props.dataSource);\n    this.updateRangeState();\n  }\n\n  componentWillReceiveProps(nextProps) {\n    if (nextProps.dataSource !== this.props.dataSource) {\n      this.setupDataSource(nextProps.dataSource);\n    }\n  }\n\n  componentDidUpdate(prevProps) {\n    if (this.props.onComponentDidUpdate) {\n      this.props.onComponentDidUpdate()\n    }\n    // If our view has been swapped out for an entirely different one,\n    // reset our scroll position to the top.\n    if (prevProps.dataSource !== this.props.dataSource) {\n      this.refs.container.scrollTop = 0;\n    }\n\n    if (!this.updateRangeStateFiring) {\n      this.updateRangeState();\n    }\n    this.updateRangeStateFiring = false;\n\n    if (!this._cleanupAnimationTimeout) {\n      this._cleanupAnimationTimeout = window.setTimeout(this.onCleanupAnimatingItems, 50);\n    }\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('resize', this.onWindowResize, true);\n    if (this._cleanupAnimationTimeout) { window.clearTimeout(this._cleanupAnimationTimeout); }\n    this._unlisten()\n  }\n\n  onWindowResize = () => {\n    if (this._onWindowResize == null) { this._onWindowResize = _.debounce(this.updateRangeState, 50); }\n    this._onWindowResize();\n  }\n\n  onScroll = () => {\n    // If we've shifted enough pixels from our previous scrollTop to require\n    // new rows to be rendered, update our state!\n    this.updateRangeState();\n  }\n\n  onCleanupAnimatingItems = () => {\n    this._cleanupAnimationTimeout = null;\n\n    const nextAnimatingOut = {};\n    _.each(this.state.animatingOut, (record, idx) => {\n      if (Date.now() < record.end) {\n        nextAnimatingOut[idx] = record;\n      }\n    })\n\n    if (Object.keys(nextAnimatingOut).length < Object.keys(this.state.animatingOut).length) {\n      this.setState({animatingOut: nextAnimatingOut});\n    }\n\n    if (Object.keys(nextAnimatingOut).length > 0) {\n      this._cleanupAnimationTimeout = window.setTimeout(this.onCleanupAnimatingItems, 50);\n    }\n  }\n\n  setupDataSource(dataSource) {\n    this._unlisten()\n    this._unlisten = dataSource.listen(() => {\n      this.setState(this.buildStateForRange());\n    });\n    this.setState(this.buildStateForRange({start: -1, end: -1, dataSource}));\n  }\n\n  getRowsToRender() {\n    const {itemPropsProvider} = this.props\n    const {items, animatingOut, renderedRangeStart, renderedRangeEnd} = this.state\n    // The ordering of the rows array is important. We want current rows to\n    // slide over rows which are animating out, so we need to render them last.\n    const rows = [];\n    _.each(animatingOut, (record, idx) => {\n      const itemProps = itemPropsProvider(record.item, idx / 1)\n      rows.push({item: record.item, idx: idx / 1, itemProps});\n    })\n\n    Utils.range(renderedRangeStart, renderedRangeEnd).forEach(idx => {\n      const item = items[idx]\n      if (item) {\n        const itemProps = itemPropsProvider(item, idx)\n        rows.push({item, idx, itemProps});\n      }\n    })\n\n    return rows;\n  }\n\n  scrollTo(node) {\n    this.refs.container.scrollTo(node);\n  }\n\n  scrollByPage(direction) {\n    const height = ReactDOM.findDOMNode(this.refs.container).clientHeight;\n    this.refs.container.scrollTop += height * direction;\n  }\n\n  updateRangeState() {\n    const {scrollTop} = this.refs.container;\n    const {itemHeight} = this.props\n\n    // Determine the exact range of rows we want onscreen\n    const rangeSize = Math.ceil(window.innerHeight / itemHeight);\n    let rangeStart = Math.floor(scrollTop / itemHeight);\n    let rangeEnd = rangeStart + rangeSize;\n\n    // Expand the start/end so that you can advance the keyboard cursor fast and\n    // we have items to move to and then scroll to.\n    rangeStart = Math.max(0, rangeStart - 2);\n    rangeEnd = Math.min(rangeEnd + 2, this.state.count + 1);\n\n    // Final sanity check to prevent needless work\n    const shouldNotUpdate = (\n      rangeEnd === this.state.renderedRangeEnd &&\n      rangeStart === this.state.renderedRangeStart\n    )\n    if (shouldNotUpdate) {\n      return;\n    }\n\n    this.updateRangeStateFiring = true;\n\n    this.props.dataSource.setRetainedRange({\n      start: rangeStart,\n      end: rangeEnd,\n    });\n\n    const nextState = this.buildStateForRange({start: rangeStart, end: rangeEnd})\n    this.setState(nextState)\n  }\n\n  buildStateForRange(args = {}) {\n    const {\n      start = this.state.renderedRangeStart,\n      end = this.state.renderedRangeEnd,\n      dataSource = this.props.dataSource,\n    } = args\n\n    const items = {};\n    let animatingOut = {};\n\n    Utils.range(start, end).forEach(idx => {\n      items[idx] = dataSource.get(idx);\n    });\n\n    // If we have a previous state, and the previous range matches the new range,\n    // (eg: we're not scrolling), identify removed items. We'll render them in one\n    // last time but not allocate height to them. This allows us to animate them\n    // being covered by other items, not just disappearing when others start to slide up.\n    if (this.state && (start === this.state.renderedRangeStart)) {\n      const nextIds = _.pluck(_.values(items), 'id');\n      animatingOut = {};\n\n      // Keep items which are still animating out and are still not in the set\n      _.each(this.state.animatingOut, (record, recordIdx) => {\n        if ((Date.now() < record.end) && !(nextIds.includes(record.item.id))) {\n          animatingOut[recordIdx] = record;\n        }\n      })\n\n      // Add items which are no longer found in the set\n      _.each(this.state.items, (previousItem, previousIdx) => {\n        if (!previousItem || nextIds.includes(previousItem.id)) { return; }\n        animatingOut[previousIdx] = {\n          idx: previousIdx,\n          item: previousItem,\n          end: Date.now() + 125,\n        };\n      })\n\n      // If we think /all/ the items are animating out, or a lot of them,\n      // the user probably switched to an entirely different perspective.\n      // Don't bother trying to animate.\n      const animatingCount = Object.keys(animatingOut).length;\n      if ((animatingCount > 8) || (animatingCount === Object.keys(this.state.items).length)) {\n        animatingOut = {};\n      }\n    }\n\n    return {\n      items,\n      animatingOut,\n      renderedRangeStart: start,\n      renderedRangeEnd: end,\n      count: dataSource.count(),\n      loaded: dataSource.loaded(),\n      empty: dataSource.empty(),\n    };\n  }\n\n  render() {\n    const {\n      footer,\n      columns,\n      draggable,\n      itemHeight,\n      EmptyComponent,\n      scrollTooltipComponent,\n      onClick,\n      onSelect,\n      onDragEnd,\n      onDragStart,\n      onDoubleClick,\n    } = this.props\n    const {count, loaded, empty} = this.state\n    const rows = this.getRowsToRender()\n    const innerStyles = {height: count * itemHeight}\n\n    return (\n      <div className=\"list-container list-tabular #{@props.className}\">\n        <ScrollRegion\n          ref=\"container\"\n          onScroll={this.onScroll}\n          tabIndex=\"-1\"\n          scrollTooltipComponent={scrollTooltipComponent}\n        >\n          <ListTabularRows\n            rows={rows}\n            columns={columns}\n            draggable={draggable}\n            itemHeight={itemHeight}\n            innerStyles={innerStyles}\n            onClick={onClick}\n            onSelect={onSelect}\n            onDragEnd={onDragEnd}\n            onDragStart={onDragStart}\n            onDoubleClick={onDoubleClick}\n          />\n          <div className=\"footer\">{footer}</div>\n        </ScrollRegion>\n        <Spinner visible={!loaded && empty} />\n        <EmptyComponent visible={loaded && empty} />\n      </div>\n    )\n  }\n}\n\nexport default ListTabular\n"
  },
  {
    "path": "packages/client-app/src/components/mail-important-icon.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nclassNames = require 'classnames'\n{Actions,\n Utils,\n Thread,\n TaskFactory,\n CategoryStore,\n FocusedPerspectiveStore,\n AccountStore} = require 'nylas-exports'\n\nShowImportantKey = 'core.workspace.showImportant'\n\nclass MailImportantIcon extends React.Component\n  @displayName: 'MailImportantIcon'\n  @propTypes:\n    thread: React.PropTypes.object\n    showIfAvailableForAnyAccount: React.PropTypes.bool\n\n  constructor: (@props) ->\n    @state = @getState()\n\n  getState: (props = @props) =>\n    category = null\n    visible = false\n\n    if props.showIfAvailableForAnyAccount\n      perspective = FocusedPerspectiveStore.current()\n      for accountId in perspective.accountIds\n        account = AccountStore.accountForId(accountId)\n        accountImportant = CategoryStore.getStandardCategory(account, 'important')\n        if accountImportant\n          visible = true\n        if accountId is props.thread.accountId\n          category = accountImportant\n        break if visible and category\n    else\n      category = CategoryStore.getStandardCategory(props.thread.accountId, 'important')\n      visible = category?\n\n    isImportant = category and _.findWhere(props.thread.categories, {id: category.id})?\n\n    {visible, category, isImportant}\n\n  componentDidMount: =>\n    @unsubscribe = FocusedPerspectiveStore.listen =>\n      @setState(@getState())\n    @subscription = NylasEnv.config.onDidChange ShowImportantKey, =>\n      @setState(@getState())\n\n  componentWillReceiveProps: (nextProps) =>\n    @setState(@getState(nextProps))\n\n  componentWillUnmount: =>\n    @unsubscribe?()\n    @subscription?.dispose()\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not _.isEqual(nextState, @state)\n\n  render: =>\n    return false unless @state.visible\n\n    classes = classNames\n      'mail-important-icon': true\n      'enabled': @state.category?\n      'active': @state.isImportant\n\n    if not @state.category\n      title = \"No important folder / label\"\n    else if @state.isImportant\n      title = \"Mark as unimportant\"\n    else\n      title = \"Mark as important\"\n\n    <div className={classes}}\n         title={title}\n         onClick={@_onToggleImportant}></div>\n\n  _onToggleImportant: (event) =>\n    {category} = @state\n\n    if category\n      isImportant = _.findWhere(@props.thread.categories, {id: category.id})?\n      threads = [@props.thread]\n\n      source = \"Important Icon\"\n\n      if !isImportant\n        Actions.applyCategoryToThreads({threads, categoryToApply: category, source})\n      else\n        Actions.removeCategoryFromThreads({threads, categoryToRemove: category, source})\n\n    # Don't trigger the thread row click\n    event.stopPropagation()\n\nmodule.exports = MailImportantIcon\n"
  },
  {
    "path": "packages/client-app/src/components/mail-label-set.jsx",
    "content": "import React from 'react';\nimport FocusedPerspectiveStore from '../flux/stores/focused-perspective-store';\nimport CategoryStore from '../flux/stores/category-store';\nimport MessageStore from '../flux/stores/message-store';\nimport AccountStore from '../flux/stores/account-store';\nimport {MailLabel} from './mail-label';\nimport Actions from '../flux/actions';\nimport InjectedComponentSet from './injected-component-set';\n\nconst LabelComponentCache = {};\n\nexport default class MailLabelSet extends React.Component {\n  static displayName = 'MailLabelSet';\n\n  static propTypes = {\n    thread: React.PropTypes.object.isRequired,\n    messages: React.PropTypes.array,\n    includeCurrentCategories: React.PropTypes.bool,\n    removable: React.PropTypes.bool,\n  };\n\n  _onRemoveLabel(label) {\n    Actions.removeCategoryFromThreads({\n      source: \"Label Remove Icon\",\n      threads: [this.props.thread],\n      categoryToRemove: label,\n    });\n  }\n\n  render() {\n    const {thread, messages, includeCurrentCategories} = this.props;\n    const labels = [];\n\n    if (AccountStore.accountForId(thread.accountId).usesLabels()) {\n      const hidden = CategoryStore.hiddenCategories(thread.accountId);\n      let current = FocusedPerspectiveStore.current().categories();\n\n      if (includeCurrentCategories || !current) {\n        current = [];\n      }\n\n      const ignoredIds = [].concat(hidden, current).map(l => l.id);\n      const ignoredNames = MessageStore.CategoryNamesHiddenByDefault;\n\n      for (const label of thread.sortedCategories()) {\n        const labelExists = CategoryStore.byId(thread.accountId, label.id);\n        if (ignoredNames.includes(label.name) || ignoredIds.includes(label.id) ||\n            !labelExists) {\n          continue;\n        }\n\n        if (this.props.removable) {\n          labels.push(\n            <MailLabel\n              label={label}\n              key={label.id}\n              onRemove={() => this._onRemoveLabel(label)}\n            />\n          )\n        } else {\n          if (LabelComponentCache[label.id] === undefined) {\n            LabelComponentCache[label.id] = (\n              <MailLabel label={label} key={label.id} />\n            );\n          }\n          labels.push(LabelComponentCache[label.id]);\n        }\n      }\n    }\n    return (\n      <InjectedComponentSet\n        inline\n        containersRequired={false}\n        matching={{role: \"Thread:MailLabel\"}}\n        className=\"thread-injected-mail-labels\"\n        exposedProps={{thread, messages}}\n      >\n        {labels}\n      </InjectedComponentSet>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/mail-label.jsx",
    "content": "import React from 'react';\nimport RetinaImg from './retina-img';\n\nexport const LabelColorizer = {\n  color(label) {\n    return `hsl(${label.hue()}, 50%, 34%)`;\n  },\n\n  backgroundColor(label) {\n    return `hsl(${label.hue()}, 62%, 87%)`;\n  },\n\n  backgroundColorDark(label) {\n    return `hsl(${label.hue()}, 62%, 57%)`;\n  },\n\n  styles(label) {\n    const styles = {\n      color: LabelColorizer.color(label),\n      backgroundColor: LabelColorizer.backgroundColor(label),\n      boxShadow: `inset 0 0 1px hsl(${label.hue()}, 62%, 47%), inset 0 1px 1px rgba(255,255,255,0.5), 0 0.5px 0 rgba(255,255,255,0.5)`,\n    };\n    if (process.platform !== \"win32\") {\n      styles.backgroundImage = 'linear-gradient(rgba(255,255,255, 0.4), rgba(255,255,255,0))';\n    }\n    return styles;\n  },\n};\n\nexport class MailLabel extends React.Component {\n  static propTypes = {\n    label: React.PropTypes.object.isRequired,\n    onRemove: React.PropTypes.func,\n  };\n\n  shouldComponentUpdate(nextProps) {\n    if (nextProps.label.id === this.props.label.id) { return false; }\n    return true;\n  }\n\n  _removable() {\n    return this.props.onRemove && !this.props.label.isLockedCategory();\n  }\n\n  render() {\n    let classname = 'mail-label'\n    let content = this.props.label.displayName\n\n    let x = null;\n    if (this._removable()) {\n      classname += ' removable'\n      content = <span className=\"inner\">{content}</span>\n      x = (\n        <RetinaImg\n          className=\"x\"\n          name=\"label-x.png\"\n          style={{backgroundColor: LabelColorizer.color(this.props.label)}}\n          mode={RetinaImg.Mode.ContentIsMask}\n          onClick={this.props.onRemove}\n        />\n      );\n    }\n\n    return (\n      <div\n        className={classname}\n        style={LabelColorizer.styles(this.props.label)}\n      >\n        {content}{x}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/menu.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\nclassNames = require 'classnames'\n_ = require 'underscore'\n{DOMUtils} = require 'nylas-exports'\n{CompositeDisposable} = require 'event-kit'\n\n###\nPublic: `MenuItem` components can be provided to the {Menu} by the `itemContent` function.\nMenuItem's props allow you to display dividers as well as standard items.\n\nSection: Component Kit\n###\nclass MenuItem extends React.Component\n\n  @displayName = 'MenuItem'\n\n  ###\n  Public: React `props` supported by MenuItem:\n\n   - `divider` (optional) Pass a {Boolean} to render the menu item as a section divider.\n   - `key` (optional) Pass a {String} to be the React key to optimize rendering lists of items.\n   - `selected` (optional) Pass a {Boolean} to specify whether the item is selected.\n   - `checked` (optional) Pass a {Boolean} to specify whether the item is checked.\n  ###\n  @propTypes:\n    divider: React.PropTypes.oneOfType([React.PropTypes.string, React.PropTypes.bool])\n    selected: React.PropTypes.bool\n    checked: React.PropTypes.bool\n\n  render: =>\n    if @props.divider\n      dividerLabel = if _.isString(@props.divider) then @props.divider else ''\n      <div className=\"item divider\">\n        {dividerLabel}\n      </div>\n    else\n      className = classNames\n        \"item\": true\n        \"selected\": @props.selected\n        \"checked\": @props.checked\n      <div className={className} onMouseDown={@props.onMouseDown}>\n        {@props.content}\n      </div>\n\n###\nPublic: React component for a {Menu} item that displays a name and email address.\n\nSection: Component Kit\n###\nclass MenuNameEmailItem extends React.Component\n  @displayName: 'MenuNameEmailItem'\n\n  ###\n  Public: React `props` supported by MenuNameEmailItem:\n\n   - `name` (optional) The {String} name to be displayed.\n   - `email` (optional) The {String} email address to be displayed.\n  ###\n  @propTypes:\n    name: React.PropTypes.string\n    email: React.PropTypes.string\n\n  render: =>\n    if @props.name?.length > 0 and @props.name isnt @props.email\n      <span>\n        <span className=\"primary\">{@props.name}</span>\n        <span className=\"secondary\">{\"(#{@props.email})\"}</span>\n      </span>\n    else\n      <span className=\"primary\">{@props.email}</span>\n\n###\nPublic: React component for multi-section Menus with key binding\n\nThe Menu component allows you to display a list of items. Menu takes care of\nseveral important things, ensuring that your menu is consistent with the rest\nof the N1 application and offers a near-native experience:\n\n- Keyboard Interaction with the Up and Down arrow keys, Enter to select\n- Maintaining selection across content changes\n- Highlighted state\n\nMenus are often, but not always, used in conjunction with {Popover} to display\na floating \"popup\" menu. See `template-picker.cjsx` for an example.\n\nThe Menu also exposes \"header\" and \"footer\" regions you can fill with arbitrary\ncomponents by providing the `headerComponents` and `footerComponents` props.\nThese items are nested within `.header-container`. and `.footer-container`,\nand you can customize their appearance by providing CSS selectors scoped to your\ncomponent's Menu instance:\n\n```css\n.template-picker .menu .header-container {\n  height: 100px;\n}\n```\n\nSection: Component Kit\n###\nclass Menu extends React.Component\n  @displayName: 'Menu'\n\n  ###\n  Public: React `props` supported by Menu:\n\n   - `className` (optional) The {String} class name applied to the Menu\n\n   - `itemContent` A {Function} that returns a {MenuItem}, {String}, or\n     React component for the given `item`.\n\n     If you return a {MenuItem}, your item is injected into the list directly.\n\n     If you return a string or React component, the result is placed within a\n     {MenuItem}, resulting in the following DOM:\n     `<div className=\"item [selected]\">{your content}</div>`.\n\n     To create dividers and other special menu items, return an instance of:\n\n     <Menu.Item divider=\"Label\">\n\n   - `itemKey` A {Function} that returns a unique string key for the given `item`.\n     Keys are important for efficient React rendering when `items` is changed, and a\n     key function is required.\n\n   - `itemChecked` A {Function} that returns true if the given item should be shown\n     with a checkmark. If you don't provide an implementation for `itemChecked`, no\n     checkmarks are ever shown.\n\n   - `items` An {Array} of arbitrary objects the menu should display.\n\n   - `onSelect` A {Function} called with the selected item when the user clicks\n     an item in the menu or confirms their selection with the Enter key.\n\n   - `onEscape` A {Function} called when a user presses escape in the input.\n\n   - `defaultSelectedIndex` The index of the item first selected if there\n   was no other previous index. Defaults to 0. Set to -1 if you want\n   nothing selected.\n\n  ###\n  @propTypes:\n    className: React.PropTypes.string,\n    footerComponents: React.PropTypes.node,\n    headerComponents: React.PropTypes.node,\n    itemContext: React.PropTypes.object,\n    itemContent: React.PropTypes.func.isRequired,\n    itemKey: React.PropTypes.func.isRequired,\n    itemChecked: React.PropTypes.func,\n\n    items: React.PropTypes.array.isRequired\n\n    onSelect: React.PropTypes.func.isRequired,\n\n    onEscape: React.PropTypes.func,\n\n    defaultSelectedIndex: React.PropTypes.number\n\n  @defaultProps:\n    onEscape: ->\n\n  constructor: (@props) ->\n    @_mounted = false\n    @state =\n      selectedIndex: @props.defaultSelectedIndex ? 0\n\n  # Public: Returns the currently selected item.\n  #\n  getSelectedItem: =>\n    @props.items[@state.selectedIndex]\n\n  # TODO this is a hack, refactor\n  clearSelection: =>\n    setImmediate(=>\n      return if @_mounted is false\n      @setState({selectedIndex: -1})\n    )\n\n  componentDidMount: =>\n    @_mounted = true\n\n  componentWillUnmount: =>\n    @_mounted = false\n\n  componentWillReceiveProps: (newProps) =>\n    # Attempt to preserve selection across props.items changes by\n    # finding an item in the new list with a key matching the old\n    # selected item's key\n    if @state.selectedIndex >= 0\n      selection = @props.items[@state.selectedIndex]\n      newSelectionIndex = 0\n    else\n      newSelectionIndex = newProps.defaultSelectedIndex ? -1\n\n    if selection?\n      selectionKey = @props.itemKey(selection)\n      newSelection = _.find newProps.items, (item) => @props.itemKey(item) is selectionKey\n      newSelectionIndex = newProps.items.indexOf(newSelection) if newSelection?\n\n    @setState\n      selectedIndex: newSelectionIndex\n\n  componentDidUpdate: =>\n    item = ReactDOM.findDOMNode(@).querySelector(\".selected\")\n    container = ReactDOM.findDOMNode(@).querySelector(\".content-container\")\n    adjustment = DOMUtils.scrollAdjustmentToMakeNodeVisibleInContainer(item, container)\n    if adjustment isnt 0\n      container.scrollTop += adjustment\n\n  render: =>\n    hc = @props.headerComponents ? <span />\n    fc = @props.footerComponents ? <span />\n    className = if @props.className then @props.className else ''\n    <div onKeyDown={@_onKeyDown}\n         className={\"menu #{className}\"}\n         tabIndex=\"-1\">\n      <div className=\"header-container\">\n        {hc}\n      </div>\n      {@_contentContainer()}\n      <div className=\"footer-container\">\n        {fc}\n      </div>\n    </div>\n\n  _onKeyDown: (event) =>\n    return if @props.items.length is 0\n    event.stopPropagation()\n    if event.key in [\"Enter\", \"Return\"]\n      @_onEnter()\n    if event.key is \"Escape\"\n      @_onEscape()\n    else if event.key is \"ArrowUp\" or (event.key is \"Tab\" and event.shiftKey)\n      @_onShiftSelectedIndex(-1)\n      event.preventDefault()\n    else if event.key is \"ArrowDown\" or event.key is \"Tab\"\n      @_onShiftSelectedIndex(1)\n      event.preventDefault()\n\n    return\n\n  _contentContainer: =>\n    seenItemKeys = {}\n\n    items = (@props.items || []).map (item, i) =>\n      content = @props.itemContent(item)\n      if React.isValidElement(content) and content.type is MenuItem\n        return content\n\n      onMouseDown = (event) =>\n        event.preventDefault()\n        @setState({selectedIndex: i}, =>\n          @props.onSelect(item) if @props.onSelect\n        )\n\n      key = @props.itemKey(item)\n      if not key\n        console.warn(\"Menu parent did not return an itemKey for item\", item)\n      if seenItemKeys[key]\n        console.warn(\"Menu items have colliding keys\": item, seenItemKeys[key])\n      seenItemKeys[key] = item\n\n      <MenuItem\n        key={key}\n        onMouseDown={onMouseDown}\n        checked={@props.itemChecked?(item)}\n        content={content}\n        selected={@state.selectedIndex is i}\n      />\n\n    contentClass = classNames\n      'content-container': true\n      'empty': items.length is 0\n\n    <div className={contentClass}>\n      {items}\n    </div>\n\n  _onShiftSelectedIndex: (delta) =>\n    return if @props.items.length is 0\n\n    index = @state.selectedIndex + delta\n\n    isDivider = true\n    while isDivider\n      item = @props.items[index]\n      break unless item\n      if @props.itemContent(item, @props.itemContext).props?.divider\n        if delta > 0 then index += 1\n        else if delta < 0 then index -= 1\n      else isDivider = false\n\n    index = Math.max(0, Math.min(@props.items.length-1, index))\n\n    # Update the selected index\n    @setState selectedIndex: index\n\n  _onEnter: =>\n    item = @props.items[@state.selectedIndex]\n    @props.onSelect(item) if item?\n\n  _onEscape: =>\n    @props.onEscape()\n\n\nMenu.Item = MenuItem\nMenu.NameEmailItem = MenuNameEmailItem\n\nmodule.exports = Menu\n"
  },
  {
    "path": "packages/client-app/src/components/metadata-composer-toggle-button.jsx",
    "content": "import {React, Actions, NylasAPI, NylasAPIHelpers, APIError} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit'\nimport classnames from 'classnames'\nimport _ from 'underscore'\n\nexport default class MetadataComposerToggleButton extends React.Component {\n\n  static displayName = 'MetadataComposerToggleButton';\n\n  static propTypes = {\n    title: React.PropTypes.func.isRequired,\n    iconUrl: React.PropTypes.string,\n    iconName: React.PropTypes.string,\n    pluginId: React.PropTypes.string.isRequired,\n    pluginName: React.PropTypes.string.isRequired,\n    metadataEnabledValue: React.PropTypes.object.isRequired,\n    stickyToggle: React.PropTypes.bool,\n    errorMessage: React.PropTypes.func.isRequired,\n\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n  };\n\n  static defaultProps = {\n    stickyToggle: false,\n  };\n\n  constructor(props) {\n    super(props);\n\n    this.state = {\n      pending: false,\n    };\n  }\n\n  componentWillMount() {\n    if (this._isEnabledByDefault() && !this._isEnabled()) {\n      this._setEnabled(true);\n    }\n  }\n\n  _configKey() {\n    return `plugins.${this.props.pluginId}.defaultOn`\n  }\n\n  _isEnabled() {\n    const {pluginId, draft, metadataEnabledValue} = this.props;\n    const value = draft.metadataForPluginId(pluginId);\n    return _.isEqual(value, metadataEnabledValue) || _.isMatch(value, metadataEnabledValue);\n  }\n\n  _isEnabledByDefault() {\n    return NylasEnv.config.get(this._configKey()) !== false;\n  }\n\n  _setEnabled(enabled) {\n    const {pluginId, pluginName, draft, session, metadataEnabledValue} = this.props;\n\n    const metadataValue = enabled ? metadataEnabledValue : null;\n    this.setState({pending: true});\n\n    NylasAPIHelpers.authPlugin(pluginId, pluginName, draft.accountId)\n    .then(() => {\n      session.changes.addPluginMetadata(pluginId, metadataValue);\n    })\n    .catch((error) => {\n      const {stickyToggle, errorMessage} = this.props;\n\n      if (stickyToggle) {\n        NylasEnv.config.set(this._configKey(), false)\n      }\n\n      let title = \"Error\"\n      if (!(error instanceof APIError)) {\n        NylasEnv.reportError(error);\n      } else if (error.statusCode === 400) {\n        NylasEnv.reportError(error);\n      } else if (NylasAPI.TimeoutErrorCodes.includes(error.statusCode)) {\n        title = \"Offline\"\n      }\n\n      NylasEnv.showErrorDialog({title, message: errorMessage(error)});\n    }).finally(() => {\n      this.setState({pending: false})\n    });\n  }\n\n  _onClick = () => {\n    if (this.state.pending) { return; }\n\n    const enabled = this._isEnabled();\n    const dir = enabled ? \"Disabled\" : \"Enabled\"\n    Actions.recordUserEvent(`${this.props.pluginName} ${dir}`)\n    if (this.props.stickyToggle) {\n      NylasEnv.config.set(this._configKey(), !enabled);\n    }\n    this._setEnabled(!enabled);\n  };\n\n  render() {\n    const enabled = this._isEnabled();\n    const title = this.props.title(enabled);\n\n    const className = classnames({\n      \"btn\": true,\n      \"btn-toolbar\": true,\n      \"btn-pending\": this.state.pending,\n      \"btn-enabled\": enabled,\n    });\n\n    const attrs = {}\n    if (this.props.iconUrl) {\n      attrs.url = this.props.iconUrl\n    } else if (this.props.iconName) {\n      attrs.name = this.props.iconName\n    }\n\n    return (\n      <button\n        className={className}\n        onClick={this._onClick}\n        title={title}\n        tabIndex={-1}\n      >\n        <RetinaImg {...attrs} mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    );\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/src/components/modal.jsx",
    "content": "import _ from 'underscore';\nimport React from 'react';\nimport ReactDOM from 'react-dom';\nimport Actions from '../flux/actions';\nimport RetinaImg from './retina-img';\n\n\nclass Modal extends React.Component {\n\n  static propTypes = {\n    className: React.PropTypes.string,\n    children: React.PropTypes.element,\n    height: React.PropTypes.number,\n    width: React.PropTypes.number,\n  };\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      offset: 0,\n      dimensions: {},\n    };\n  }\n\n  componentDidMount() {\n    this._focusImportantElement();\n  }\n\n  _focusImportantElement = () => {\n    const modalNode = ReactDOM.findDOMNode(this);\n\n    const focusable = modalNode.querySelectorAll(\"[tabIndex], input\");\n    const matches = _.sortBy(focusable, (node) => {\n      if (node.tabIndex > 0) {\n        return node.tabIndex;\n      } else if (node.nodeName === \"INPUT\") {\n        return 1000000\n      }\n      return 1000001\n    })\n    if (matches[0]) {\n      matches[0].focus();\n    }\n  };\n\n  _computeModalStyles = (height, width) => {\n    const modalStyle = {\n      height: height,\n      maxHeight: \"95%\",\n      width: width,\n      maxWidth: \"95%\",\n      overflow: \"auto\",\n      position: \"absolute\",\n      backgroundColor: \"white\",\n      boxShadow: \"0 10px 20px rgba(0,0,0,0.19), inset 0 0 1px rgba(0,0,0,0.5)\",\n      borderRadius: \"5px\",\n    };\n    const containerStyle = {\n      display: \"flex\",\n      alignItems: \"center\",\n      justifyContent: \"center\",\n      height: \"100%\",\n      width: \"100%\",\n      zIndex: 1000,\n      position: \"absolute\",\n      backgroundColor: \"rgba(255,255,255,0.58)\",\n    };\n    return {containerStyle, modalStyle};\n  };\n\n  _onKeyDown = (event) => {\n    if (event.key === \"Escape\") {\n      Actions.closeModal();\n    }\n  };\n\n  render() {\n    const {children, height, width} = this.props;\n    const {containerStyle, modalStyle} = this._computeModalStyles(height, width);\n\n    return (\n      <div\n        style={containerStyle}\n        className=\"modal-container\"\n        onKeyDown={this._onKeyDown}\n        onClick={() => Actions.closeModal()}\n      >\n        <div\n          className=\"modal nylas-modal-container\"\n          style={modalStyle}\n          onClick={(event) => event.stopPropagation()}\n        >\n          <RetinaImg\n            className=\"modal-close\"\n            style={{width: \"14\", WebkitFilter: \"none\", zIndex: \"1\", position: \"relative\"}}\n            name=\"modal-close.png\"\n            mode={RetinaImg.Mode.ContentDark}\n            onClick={(event) => {\n              event.stopPropagation();\n              Actions.closeModal();\n            }}\n          />\n          {children}\n        </div>\n      </div>\n    );\n  }\n\n}\n\nexport default Modal;\n"
  },
  {
    "path": "packages/client-app/src/components/multiselect-action-bar.cjsx",
    "content": "React = require \"react\"\nReactCSSTransitionGroup = require 'react-addons-css-transition-group'\n_ = require 'underscore'\n\n{Utils, Actions} = require \"nylas-exports\"\nInjectedComponentSet = require './injected-component-set'\nFlexbox = require('./flexbox').default\n\n###\nPublic: MultiselectActionBar is a simple component that can be placed in a {Sheet} Toolbar.\nWhen the provided `dataStore` has a selection, it appears over the other items in the toolbar.\n\nGenerally, you wrap {MultiselectActionBar} in your own simple component to provide a dataStore\nand other settings:\n\n```coffee\nclass MultiselectActionBar extends React.Component\n  @displayName: 'MultiselectActionBar'\n\n  render: =>\n    <MultiselectActionBar\n      dataStore={ThreadListStore}\n      className=\"thread-list\"\n      collection=\"thread\" />\n```\n\nThe MultiselectActionBar uses the `ComponentRegistry` to find items to display for the given\ncollection name. To add an item to the bar created in the example above, register it like this:\n\n```coffee\nComponentRegistry.register ThreadBulkTrashButton,\n  role: 'thread:Toolbar'\n```\n\nSection: Component Kit\n###\nclass MultiselectActionBar extends React.Component\n  @displayName: 'MultiselectActionBar'\n\n  ###\n  Public: React `props` supported by MultiselectActionBar:\n\n   - `dataStore` An instance of a {ListDataSource}.\n   - `collection` The name of the collection. The collection name is used for the text\n      that appears in the bar \"1 thread selected\" and is also used to find components\n      in the component registry that should appear in the bar (`thread` => `thread:BulkAtion`)\n  ###\n  @propTypes:\n    collection: React.PropTypes.string.isRequired\n    dataSource: React.PropTypes.object\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @setupForProps(@props)\n\n  componentWillReceiveProps: (newProps) =>\n    return if _.isEqual(@props, newProps)\n    @teardownForProps()\n    @setupForProps(newProps)\n    @setState(@_getStateFromStores(newProps))\n\n  componentWillUnmount: =>\n    @teardownForProps()\n\n  teardownForProps: =>\n    return unless @_unsubscribers\n    unsubscribe() for unsubscribe in @_unsubscribers\n\n  setupForProps: (props) =>\n    @_unsubscribers = []\n    @_unsubscribers.push props.dataSource.listen @_onChange\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  render: =>\n    <ReactCSSTransitionGroup\n      className={\"selection-bar\"}\n      transitionName=\"selection-bar-absolute\"\n      component=\"div\"\n      transitionLeaveTimeout={200}\n      transitionEnterTimeout={200}>\n      { if @state.items.length > 0 then @_renderBar() else [] }\n    </ReactCSSTransitionGroup>\n\n  _renderBar: =>\n    <div className=\"absolute\" key=\"absolute\">\n      <div className=\"inner\">\n        {@_renderActions()}\n\n        <div className=\"centered\">\n          {@_label()}\n        </div>\n\n        <button style={order:100}\n                className=\"btn btn-toolbar\"\n                onClick={@_onClearSelection}>\n          Clear Selection\n        </button>\n      </div>\n    </div>\n\n  _renderActions: =>\n    return <div></div> unless @props.dataSource\n    <InjectedComponentSet matching={role:\"#{@props.collection}:Toolbar\"}\n                          exposedProps={selection: @props.dataSource.selection, items: @state.items} />\n\n  _label: =>\n    if @state.items.length > 1\n      \"#{@state.items.length} #{@props.collection}s selected\"\n    else if @state.items.length is 1\n      \"#{@state.items.length} #{@props.collection} selected\"\n    else\n      \"\"\n\n  _getStateFromStores: (props = @props) =>\n    items: props.dataSource.selection.items() ? []\n\n  _onChange: =>\n    @setState(@_getStateFromStores())\n\n  _onClearSelection: =>\n    @props.dataSource.selection.clear()\n    return\n\nmodule.exports = MultiselectActionBar\n"
  },
  {
    "path": "packages/client-app/src/components/multiselect-dropdown.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport {ButtonDropdown, Menu} from 'nylas-component-kit'\nimport ReactDOM from 'react-dom';\n\n/*\nRenders a drop down of items that can have multiple selected\nItem can be string or object\n\n@param {object} props - props for MultiselectDropdown\n@param {string} props.className - css class applied to the component\n@param {array} props.items - items to be rendered in the dropdown\n@param {props.itemChecked} - props.itemChecked -- a function to determine if the item should be checked or not\n@param {props.onToggleItem} - props.onToggleItem -- function called when an item is clicked\n@param {props.itemKey} - props.itemKey -- function that indicates how to select the key for each MenuItem\n@param {props.buttonText} - props.buttonText -- string to be rendered in the button\n**/\n\nclass MultiselectDropdown extends Component {\n  static displayName = 'MultiselectDropdown'\n\n  static propTypes = {\n    className: PropTypes.string,\n    items: PropTypes.array.isRequired,\n    itemChecked: PropTypes.func,\n    onToggleItem: PropTypes.func,\n    itemKey: PropTypes.func,\n    buttonText: PropTypes.string,\n    itemContent: PropTypes.func,\n  }\n\n  static defaultProps = {\n    className: '',\n    items: [],\n    itemChecked: {},\n    onToggleItem: () => {},\n    itemKey: () => {},\n    buttonText: '',\n    itemContent: () => {},\n  }\n\n  componentDidUpdate() {\n    if (ReactDOM.findDOMNode(this.refs.select)) {\n      ReactDOM.findDOMNode(this.refs.select).focus()\n    }\n  }\n\n\n  _onItemClick = (item) => {\n    this.props.onToggleItem(item)\n  }\n\n  _renderItem = (item) => {\n    const MenuItem = Menu.Item\n    return (\n      <MenuItem onMouseDown={() => this._onItemClick(item)} checked={this.props.itemChecked(item)} key={this.props.itemKey(item)} content={this.props.itemContent(item)} />\n    )\n  }\n\n\n  _renderMenu= (items) => {\n    return (\n      <Menu\n        items={items}\n        itemContent={this._renderItem}\n        itemKey={item => item.id}\n        onSelect={() => {}}\n      />\n    )\n  }\n\n  render() {\n    const {items} = this.props\n    const menu = this._renderMenu(items)\n    return (\n      <ButtonDropdown\n        className={'btn-multiselect'}\n        primaryItem={<span>{this.props.buttonText}</span>}\n        menu={menu}\n      />\n    )\n  }\n}\nexport default MultiselectDropdown\n"
  },
  {
    "path": "packages/client-app/src/components/multiselect-list-interaction-handler.coffee",
    "content": "_ = require 'underscore'\n{Actions,\n WorkspaceStore} = require 'nylas-exports'\n\nmodule.exports =\nclass MultiselectListInteractionHandler\n  constructor: (@props) ->\n    {@onFocusItem, @onSetCursorPosition} = @props\n\n  cssClass: =>\n    'handler-list'\n\n  shouldShowFocus: =>\n    false\n\n  shouldShowCheckmarks: =>\n    true\n\n  shouldShowKeyboardCursor: =>\n    true\n\n  onClick: (item) =>\n    @onFocusItem(item)\n\n  onMetaClick: (item) =>\n    @props.dataSource.selection.toggle(item)\n    @onSetCursorPosition(item)\n\n  onShiftClick: (item) =>\n    @props.dataSource.selection.expandTo(item)\n    @onSetCursorPosition(item)\n\n  onEnter: =>\n    keyboardCursorId = @props.keyboardCursorId\n    if keyboardCursorId\n      item = @props.dataSource.getById(keyboardCursorId)\n      @onFocusItem(item)\n\n  onDeselect: =>\n    @props.dataSource.selection.clear()\n\n  onSelect: (items) =>\n    @props.dataSource.selection.set(items)\n\n  onSelectKeyboardItem: =>\n    {id} = @_keyboardContext()\n    return unless id\n    @props.dataSource.selection.toggle(@props.dataSource.getById(id))\n\n  onShift: (delta, options = {}) =>\n    {id, action} = @_keyboardContext()\n\n    current = @props.dataSource.getById(id)\n    index = @props.dataSource.indexOfId(id)\n    index = Math.max(0, Math.min(index + delta, @props.dataSource.count() - 1))\n    next = @props.dataSource.get(index)\n\n    action(next)\n    if options.select\n      @props.dataSource.selection.walk({current, next})\n\n  _keyboardContext: =>\n    if WorkspaceStore.topSheet().root\n      {id: @props.keyboardCursorId, action: @onSetCursorPosition}\n    else\n      {id: @props.focusedId, action: @onFocusItem}\n"
  },
  {
    "path": "packages/client-app/src/components/multiselect-list.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\nclassNames = require 'classnames'\nListTabular = require('./list-tabular').default\nSpinner = require './spinner'\n{Actions,\n Utils,\n WorkspaceStore,\n AccountStore} = require 'nylas-exports'\n{KeyCommandsRegion} = require 'nylas-component-kit'\nEventEmitter = require('events').EventEmitter\n\nMultiselectListInteractionHandler = require './multiselect-list-interaction-handler'\nMultiselectSplitInteractionHandler = require './multiselect-split-interaction-handler'\n\n###\nPublic: MultiselectList wraps {ListTabular} and makes it easy to present a\n{ListDataSource} with selection support. It adds a checkbox column to the columns\nyou provide, and also handles:\n\n- Command-clicking individual items\n- Shift-clicking to select a range\n- Using the keyboard to select a range\n\nSection: Component Kit\n###\nclass MultiselectList extends React.Component\n  @displayName = 'MultiselectList'\n\n  @propTypes =\n    dataSource: React.PropTypes.object\n    className: React.PropTypes.string.isRequired\n    columns: React.PropTypes.array.isRequired\n    itemPropsProvider: React.PropTypes.func.isRequired\n    keymapHandlers: React.PropTypes.object\n    onComponentDidUpdate: React.PropTypes.func\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @setupForProps(@props)\n\n  componentWillReceiveProps: (newProps) =>\n    return if _.isEqual(@props, newProps)\n    @teardownForProps()\n    @setupForProps(newProps)\n    @setState(@_getStateFromStores(newProps))\n\n  componentDidUpdate: (prevProps, prevState) =>\n    if @props.onComponentDidUpdate\n      @props.onComponentDidUpdate()\n    if prevProps.focusedId isnt @props.focusedId or\n       prevProps.keyboardCursorId isnt @props.keyboardCursorId\n\n      item = ReactDOM.findDOMNode(@).querySelector(\".focused\")\n      item ?= ReactDOM.findDOMNode(@).querySelector(\".keyboard-cursor\")\n      return unless item instanceof Node\n      @refs.list.scrollTo(item)\n\n  componentWillUnmount: =>\n    @teardownForProps()\n\n  teardownForProps: =>\n    return unless @unsubscribers\n    unsubscribe() for unsubscribe in @unsubscribers\n\n  setupForProps: (props) =>\n    @unsubscribers = []\n    @unsubscribers.push WorkspaceStore.listen @_onChange\n\n  _globalKeymapHandlers: ->\n    _.extend({}, @props.keymapHandlers, {\n      'core:focus-item': => @_onEnter()\n      'core:select-item': => @_onSelectKeyboardItem()\n      'core:next-item': => @_onShift(1)\n      'core:previous-item': => @_onShift(-1)\n      'core:select-down': => @_onShift(1, {select: true})\n      'core:select-up': => @_onShift(-1, {select: true})\n      'core:list-page-up': => @_onScrollByPage(-1)\n      'core:list-page-down': => @_onScrollByPage(1)\n      'core:pop-sheet': => @_onDeselect()\n      'multiselect-list:select-all': => @_onSelectAll()\n      'multiselect-list:deselect-all': => @_onDeselect()\n    })\n\n  render: =>\n    # IMPORTANT: DO NOT pass inline functions as props. _.isEqual thinks these\n    # are \"different\", and will re-render everything. Instead, declare them with ?=,\n    # pass a reference. (Alternatively, ignore these in children's shouldComponentUpdate.)\n    #\n    # BAD:   onSelect={ (item) -> Actions.focusThread(item) }\n    # GOOD:  onSelect={@_onSelectItem}\n    #\n    otherProps = Utils.fastOmit(@props, Object.keys(@constructor.propTypes))\n\n    className = @props.className\n    if @props.dataSource and @state.handler\n      className += \" \" + @state.handler.cssClass()\n\n      @itemPropsProvider ?= (item, idx) =>\n        selectedIds = @props.dataSource.selection.ids()\n        selected = item.id in selectedIds\n        if not selected\n          nextId = @props.dataSource.get(idx + 1)?.id\n          nextSelected = nextId in selectedIds\n\n        props = @props.itemPropsProvider(item, idx)\n        props.className ?= ''\n        props.className += \" \" + classNames\n          'selected': selected\n          'next-is-selected': not selected and nextSelected\n          'focused': @state.handler.shouldShowFocus() and item.id is @props.focusedId\n          'keyboard-cursor': @state.handler.shouldShowKeyboardCursor() and item.id is @props.keyboardCursorId\n        props['data-item-id'] = item.id\n        props\n\n      <KeyCommandsRegion globalHandlers={@_globalKeymapHandlers()} className={className}>\n        <ListTabular\n          ref=\"list\"\n          columns={@state.computedColumns}\n          dataSource={@props.dataSource}\n          itemPropsProvider={@itemPropsProvider}\n          onSelect={@_onClickItem}\n          onComponentDidUpdate={@props.onComponentDidUpdate}\n          {...otherProps} />\n      </KeyCommandsRegion>\n    else\n      <div className={className} {...otherProps}>\n        <Spinner visible={true} />\n      </div>\n\n  _onClickItem: (item, event) =>\n    return unless @state.handler\n    if event.metaKey || event.ctrlKey\n      @state.handler.onMetaClick(item)\n    else if event.shiftKey\n      @state.handler.onShiftClick(item)\n    else\n      @state.handler.onClick(item)\n\n  _onEnter: =>\n    return unless @state.handler\n    @state.handler.onEnter()\n\n  _onSelectKeyboardItem: =>\n    return unless @state.handler\n    @state.handler.onSelectKeyboardItem()\n\n  _onSelectAll: =>\n    return unless @state.handler\n    items = @props.dataSource.itemsCurrentlyInViewMatching -> true\n    @state.handler.onSelect(items)\n\n  _onDeselect: =>\n    return unless @_visible() and @state.handler\n    @state.handler.onDeselect()\n\n  _onShift: (delta, options = {}) =>\n    return unless @state.handler\n    @state.handler.onShift(delta, options)\n\n  _onScrollByPage: (delta) =>\n    @refs.list.scrollByPage(delta)\n\n  _onChange: =>\n    @setState(@_getStateFromStores())\n\n  _visible: =>\n    if @state.layoutMode\n      WorkspaceStore.topSheet().root\n    else\n      true\n\n  _getCheckmarkColumn: =>\n    new ListTabular.Column\n      name: 'Check'\n      resolver: (item) =>\n        toggle = (event) =>\n          if event.shiftKey\n            @state.handler.onShiftClick(item)\n          else\n            @state.handler.onMetaClick(item)\n          event.stopPropagation()\n        <div className=\"checkmark\" onClick={toggle}><div className=\"inner\"></div></div>\n\n  _getStateFromStores: (props = @props) =>\n    state = @state ? {}\n\n    layoutMode = WorkspaceStore.layoutMode()\n\n    # Do we need to re-compute columns? Don't do this unless we really have to,\n    # it will cause a re-render of the entire ListTabular. To know whether our\n    # computed columns are still valid, we store the original columns in our state\n    # along with the computed ones.\n    if props.columns isnt state.columns or layoutMode isnt state.layoutMode\n      computedColumns = [].concat(props.columns)\n      if layoutMode is 'list'\n        computedColumns.splice(0, 0, @_getCheckmarkColumn())\n    else\n      computedColumns = state.computedColumns\n\n    if layoutMode is 'list'\n      handler = new MultiselectListInteractionHandler(props)\n    else\n      handler = new MultiselectSplitInteractionHandler(props)\n\n    handler: handler\n    columns: props.columns\n    computedColumns: computedColumns\n    layoutMode: layoutMode\n\n  # Public Methods\n\n  handler: ->\n    return @state.handler\n\n  itemIdAtPoint: (x, y) ->\n    item = document.elementFromPoint(x, y).closest('[data-item-id]')\n    return null unless item\n    return item.dataset.itemId\n\nmodule.exports = MultiselectList\n"
  },
  {
    "path": "packages/client-app/src/components/multiselect-split-interaction-handler.coffee",
    "content": "_ = require 'underscore'\n{Actions,\n WorkspaceStore} = require 'nylas-exports'\n\nmodule.exports =\nclass MultiselectSplitInteractionHandler\n  constructor: (@props) ->\n    {@onFocusItem, @onSetCursorPosition} = @props\n\n  cssClass: =>\n    'handler-split'\n\n  shouldShowFocus: =>\n    true\n\n  shouldShowCheckmarks: =>\n    false\n\n  shouldShowKeyboardCursor: =>\n    @props.dataSource.selection.count() > 1\n\n  onClick: (item) =>\n    @onFocusItem(item)\n    @props.dataSource.selection.clear()\n    @_checkSelectionAndFocusConsistency()\n\n  onMetaClick: (item) =>\n    @_turnFocusIntoSelection()\n    @props.dataSource.selection.toggle(item)\n    @_checkSelectionAndFocusConsistency()\n\n  onShiftClick: (item) =>\n    @_turnFocusIntoSelection()\n    @props.dataSource.selection.expandTo(item)\n    @_checkSelectionAndFocusConsistency()\n\n  onEnter: =>\n    # This concept does not exist in split mode\n\n  onDeselect: =>\n    @props.dataSource.selection.clear()\n    @_checkSelectionAndFocusConsistency()\n\n  onSelect: (items) =>\n    @props.dataSource.selection.set(items)\n    @_checkSelectionAndFocusConsistency()\n\n  onSelectKeyboardItem: =>\n    @_checkSelectionAndFocusConsistency()\n\n  onShift: (delta, options) =>\n    if options.select\n      @_turnFocusIntoSelection()\n\n    if @props.dataSource.selection.count() > 0\n      selection = @props.dataSource.selection\n      keyboardId = @props.keyboardCursorId\n      id = keyboardId ? @props.dataSource.selection.top().id\n      action = @onSetCursorPosition\n    else\n      id = @props.focusedId\n      action = @onFocusItem\n\n    current = @props.dataSource.getById(id)\n    index = @props.dataSource.indexOfId(id)\n    index = Math.max(0, Math.min(index + delta, @props.dataSource.count() - 1))\n    next = @props.dataSource.get(index)\n\n    action(next)\n    if options.select\n      @props.dataSource.selection.walk({current, next})\n\n    @_checkSelectionAndFocusConsistency()\n\n  _turnFocusIntoSelection: =>\n    focused = @props.focused\n    @onFocusItem(null)\n    @props.dataSource.selection.add(focused)\n\n  _checkSelectionAndFocusConsistency: =>\n    focused = @props.focused\n    selection = @props.dataSource.selection\n\n    if focused and selection.count() > 0\n      @props.dataSource.selection.add(focused)\n      @onFocusItem(null)\n\n    if selection.count() is 1 and !focused\n      @onFocusItem(selection.items()[0])\n      @props.dataSource.selection.clear()\n"
  },
  {
    "path": "packages/client-app/src/components/multiselect-toolbar.jsx",
    "content": "import {Utils} from 'nylas-exports'\nimport React, {Component, PropTypes} from 'react'\nimport ReactCSSTransitionGroup from 'react-addons-css-transition-group'\n\n\n/*\n * MultiselectToolbar renders a toolbar inside a horizontal bar and displays\n * a selection count and a button to clear the selection.\n *\n * The toolbar, or set of buttons, must be passed in as props.toolbarElement\n *\n * It will also animate its mounting and unmounting\n * @class MultiselectToolbar\n */\nclass MultiselectToolbar extends Component {\n  static displayName = 'MultiselectToolbar';\n\n  static propTypes = {\n    toolbarElement: PropTypes.element.isRequired,\n    collection: PropTypes.string.isRequired,\n    onClearSelection: PropTypes.func.isRequired,\n    selectionCount: PropTypes.node,\n  };\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (\n      !Utils.isEqualReact(nextProps, this.props) ||\n      !Utils.isEqualReact(nextState, this.state)\n    )\n  }\n\n  selectionLabel = () => {\n    const {selectionCount, collection} = this.props\n    if (selectionCount > 1) {\n      return `${selectionCount} ${collection}s selected`\n    } else if (selectionCount === 1) {\n      return `${selectionCount} ${collection} selected`\n    }\n    return ''\n  };\n\n  renderToolbar() {\n    const {toolbarElement, onClearSelection} = this.props\n    return (\n      <div className=\"absolute\" key=\"absolute\">\n        <div className=\"inner\">\n          {toolbarElement}\n          <div className=\"centered\">\n            {this.selectionLabel()}\n          </div>\n\n          <button\n            style={{order: 100}}\n            className=\"btn btn-toolbar\"\n            onClick={onClearSelection}\n          >\n            Clear Selection\n          </button>\n        </div>\n      </div>\n    )\n  }\n\n  render() {\n    const {selectionCount} = this.props\n    return (\n      <ReactCSSTransitionGroup\n        className={\"selection-bar\"}\n        transitionName=\"selection-bar-absolute\"\n        component=\"div\"\n        transitionLeaveTimeout={200}\n        transitionEnterTimeout={200}\n      >\n        {selectionCount > 0 ? this.renderToolbar() : undefined}\n      </ReactCSSTransitionGroup>\n    )\n  }\n}\n\nexport default MultiselectToolbar\n"
  },
  {
    "path": "packages/client-app/src/components/notification.jsx",
    "content": "import React from 'react'\nimport RetinaImg from './retina-img';\n\nexport default class Notification extends React.Component {\n  static containerRequired = false;\n\n  static propTypes = {\n    className: React.PropTypes.string,\n    displayName: React.PropTypes.string,\n    title: React.PropTypes.string,\n    subtitle: React.PropTypes.string,\n    subtitleAction: React.PropTypes.func,\n    actions: React.PropTypes.array,\n    icon: React.PropTypes.string,\n    priority: React.PropTypes.string,\n    isError: React.PropTypes.bool,\n    isDismissable: React.PropTypes.bool,\n    isPermanentlyDismissable: React.PropTypes.bool,\n  }\n\n  static defaultProps = {\n    className: '',\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      loadingActions: [],\n      isDismissed: this._isDismissed(),\n    }\n  }\n\n  componentDidMount() {\n    this.mounted = true;\n  }\n\n  componentWillUnmount() {\n    this.mounted = false;\n  }\n\n  _isDismissed() {\n    if (this.props.isPermanentlyDismissable) {\n      return this._numAsks() >= 5\n    }\n    return false\n  }\n\n  _numAsks() {\n    if (!NylasEnv.savedState.dismissedNotificationAsks) {\n      NylasEnv.savedState.dismissedNotificationAsks = {}\n    }\n    if (!NylasEnv.savedState.dismissedNotificationAsks[this.props.displayName]) {\n      NylasEnv.savedState.dismissedNotificationAsks[this.props.displayName] = 0\n    }\n    return NylasEnv.savedState.dismissedNotificationAsks[this.props.displayName]\n  }\n\n  _onClick(event, actionId, actionFn) {\n    const result = actionFn(event);\n    if (result instanceof Promise) {\n      this.setState({\n        loadingActions: this.state.loadingActions.concat([actionId]),\n      })\n\n      result.finally(() => {\n        if (this.mounted) {\n          this.setState({\n            loadingActions: this.state.loadingActions.filter(f => f !== actionId),\n          })\n        }\n      })\n    }\n  }\n\n  _subtitle() {\n    if (this.props.isPermanentlyDismissable && this._numAsks() >= 1) {\n      return \"Don't show this again\"\n    }\n    return this.props.subtitle\n  }\n\n  _subtitleAction = () => {\n    if (this.props.isPermanentlyDismissable && this._numAsks() >= 1) {\n      return () => {\n        NylasEnv.savedState.dismissedNotificationAsks[this.props.displayName] = 5\n        this.setState({isDismissed: true})\n      }\n    }\n    return this.props.subtitleAction\n  }\n\n  render() {\n    if (this.state.isDismissed) return <span />\n\n    const actions = this.props.actions || [];\n\n    if (this.props.isDismissable) {\n      actions.push({\n        label: 'Dismiss',\n        fn: () => {\n          NylasEnv.savedState.dismissedNotificationAsks[this.props.displayName] = this._numAsks() + 1\n          this.setState({isDismissed: true})\n        },\n      })\n    }\n\n    const actionElems = actions.map((action, idx) => {\n      const id = `action-${idx}`;\n      let className = 'action'\n      if (this.state.loadingActions.includes(id)) {\n        className += ' loading'\n      }\n      return (\n        <div\n          key={id}\n          id={id}\n          className={className}\n          onClick={(e) => this._onClick(e, id, action.fn)}\n          {...action.props}\n        >\n          {action.label}\n        </div>\n      );\n    })\n\n    const {className, isError, priority, icon, title} = this.props;\n    const subtitle = this._subtitle();\n    const subtitleAction = this._subtitleAction();\n\n    let iconEl = null;\n    if (icon) {\n      iconEl = (\n        <RetinaImg\n          className=\"icon\"\n          name={icon}\n          mode={RetinaImg.Mode.ContentPreserve}\n        />\n      )\n    }\n    return (\n      <div className={`notification${isError ? ' error' : ''} ${className}`} data-priority={priority}>\n        <div className=\"title\">\n          {iconEl} {title} <br />\n          <span\n            className={`subtitle ${subtitleAction ? 'has-action' : ''}`}\n            onClick={subtitleAction}\n          >\n            {subtitle}\n          </span>\n        </div>\n        {actionElems.length > 0 ?\n          <div className=\"actions-wrapper\">{actionElems}</div> : null\n        }\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/calendar-constants.es6",
    "content": "export const DAY_VIEW = \"day\";\nexport const WEEK_VIEW = \"week\";\nexport const MONTH_VIEW = \"month\";\nexport const YEAR_VIEW = \"year\";\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/calendar-data-source.es6",
    "content": "import Rx from 'rx-lite'\nimport Event from '../../flux/models/event'\nimport Matcher from '../../flux/attributes/matcher'\nimport DatabaseStore from '../../flux/stores/database-store'\n\nexport default class CalendarDataSource {\n  buildObservable({startTime, endTime, disabledCalendars}) {\n    const end = Event.attributes.end\n    const start = Event.attributes.start\n\n    const matcher = new Matcher.And([\n      new Matcher.Or([\n        new Matcher.And([start.lte(endTime), end.gte(startTime)]),\n        new Matcher.And([start.lte(endTime), start.gte(startTime)]),\n        new Matcher.And([end.gte(startTime), end.lte(endTime)]),\n        new Matcher.And([end.gte(endTime), start.lte(startTime)]),\n      ]),\n      Event.attributes.calendarId.notIn(disabledCalendars || []),\n    ]);\n\n    const query = DatabaseStore.findAll(Event).where(matcher)\n    this.observable = Rx.Observable.fromQuery(query).flatMapLatest((results) => {\n      return Rx.Observable.from([{events: results}]);\n    });\n    return this.observable;\n  }\n\n  subscribe(callback) {\n    return this.observable.subscribe(callback)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/calendar-event-container.jsx",
    "content": "import moment from 'moment'\n\nimport React from 'react'\nimport ReactDOM from 'react-dom'\n\nexport default class CalendarEventContainer extends React.Component {\n  static displayName = \"CalendarEventContainer\";\n\n  static propTypes = {\n    onCalendarMouseUp: React.PropTypes.func,\n    onCalendarMouseDown: React.PropTypes.func,\n    onCalendarMouseMove: React.PropTypes.func,\n  }\n\n  constructor() {\n    super()\n    this._DOMCache = {}\n  }\n\n  componentDidMount() {\n    window.addEventListener(\"mouseup\", this._onWindowMouseUp)\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener(\"mouseup\", this._onWindowMouseUp)\n  }\n\n  _onCalendarMouseUp = (event) => {\n    this._DOMCache = {};\n    if (!this._mouseIsDown) {\n      return\n    }\n    this._mouseIsDown = false;\n    this._runPropsHandler(\"onCalendarMouseUp\", event)\n  }\n\n  _onCalendarMouseDown = (event) => {\n    this._DOMCache = {};\n    this._mouseIsDown = true;\n    this._runPropsHandler(\"onCalendarMouseDown\", event)\n  }\n\n  _onCalendarMouseMove = (event) => {\n    this._runPropsHandler(\"onCalendarMouseMove\", event)\n  }\n\n  _runPropsHandler(name, event) {\n    const propsFn = this.props[name]\n    if (!propsFn) { return }\n    const {time, x, y, width, height} = this._dataFromMouseEvent(event);\n    try {\n      propsFn({event, time, x, y, width, height, mouseIsDown: this._mouseIsDown})\n    } catch (error) {\n      NylasEnv.reportError(error)\n    }\n  }\n\n  _dataFromMouseEvent(event) {\n    let x = null;\n    let y = null;\n    let width = null;\n    let height = null;\n    let time = null;\n    if (!event.target || !event.target.closest) { return {x, y, width, height, time} }\n    const eventColumn = this._DOMCache.eventColumn || event.target.closest(\".event-column\");\n    const gridWrap = this._DOMCache.gridWrap || event.target.closest(\".event-grid-wrap .scroll-region-content-inner\");\n    const calWrap = this._DOMCache.calWrap || event.target.closest(\".calendar-area-wrap\")\n    if (!gridWrap || !eventColumn) { return {x, y, width, height, time} }\n\n    const rect = this._DOMCache.rect || gridWrap.getBoundingClientRect();\n    const calWrapRect = this._DOMCache.calWrapRect || calWrap.getBoundingClientRect();\n\n    this._DOMCache = {rect, eventColumn, gridWrap, calWrap}\n\n    y = (gridWrap.scrollTop + event.clientY - rect.top);\n    x = (calWrap.scrollLeft + event.clientX - calWrapRect.left);\n    width = gridWrap.scrollWidth;\n    height = gridWrap.scrollHeight;\n    const percentDay = y / height;\n    const diff = ((+eventColumn.dataset.end) - (+eventColumn.dataset.start))\n    time = moment(diff * percentDay + (+eventColumn.dataset.start));\n    return {x, y, width, height, time}\n  }\n\n  _onWindowMouseUp = (event) => {\n    if (ReactDOM.findDOMNode(this).contains(event.target)) {\n      return\n    }\n    this._onCalendarMouseUp(event)\n  }\n\n  render() {\n    return (\n      <div\n        className=\"calendar-mouse-handler\"\n        onMouseUp={this._onCalendarMouseUp}\n        onMouseDown={this._onCalendarMouseDown}\n        onMouseMove={this._onCalendarMouseMove}\n      >\n        {this.props.children}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/calendar-event-popover.jsx",
    "content": "import React from 'react';\nimport moment from 'moment';\nimport {Actions, DatabaseStore, DateUtils, SyncbackEventTask} from 'nylas-exports';\nimport {DatePicker, RetinaImg, ScrollRegion, TabGroupRegion, TimePicker} from 'nylas-component-kit';\nimport EventParticipantsInput from './event-participants-input';\n\n\nexport default class CalendarEventPopover extends React.Component {\n  static propTypes = {\n    event: React.PropTypes.object.isRequired,\n  }\n\n  constructor(props) {\n    super(props)\n    const {description, start, end, location, participants} = this.props.event;\n\n    this.state = {description, start, end, location}\n    this.state.title = this.props.event.displayTitle()\n    this.state.editing = false;\n    this.state.participants = participants || []\n  }\n\n  componentWillReceiveProps = (nextProps) => {\n    const {description, start, end, location, participants} = nextProps.event;\n    this.setState({description, start, end, location})\n    this.setState({\n      participants: participants || [],\n      title: nextProps.event.displayTitle(),\n    })\n  }\n\n  onEdit = () => {\n    this.setState({editing: true})\n  }\n\n  getStartMoment = () => moment(this.state.start * 1000);\n  getEndMoment = () => moment(this.state.end * 1000);\n\n  saveEdits = () => {\n    const event = this.props.event.clone();\n    const keys = ['title', 'description', 'location', 'participants']\n    for (const key of keys) {\n      event[key] = this.state[key]\n    }\n\n    // TODO, this component shouldn't save the event here, we should expose an\n    // `onEditEvent` or similar callback\n    // TODO: How will this affect the event if the when object was originally\n    //   a datespan, with start_date and end_date attributes?\n    event.when.start_time = this.state.start;\n    event.when.end_time = this.state.end;\n\n    DatabaseStore.inTransaction((t) => {\n      this.setState({editing: false}); // TODO: where's the best place to put this?\n      return t.persistModel(event)\n    }).then(() => {\n      const task = new SyncbackEventTask(event.clientId);\n      Actions.queueTask(task);\n    });\n  }\n\n  extractNotesFromDescription(node) {\n    const els = node.querySelectorAll('meta[itemprop=description]');\n    let notes = null;\n    if (els.length) {\n      notes = Array.from(els).map(el => el.content).join('\\n');\n    } else {\n      notes = node.innerText;\n    }\n    while (true) {\n      const nextNotes = notes.replace('\\n\\n', '\\n');\n      if (nextNotes === notes) {\n        break;\n      }\n      notes = nextNotes;\n    }\n    return notes;\n  }\n\n  // If on the hour, formats as \"3 PM\", else formats as \"3:15 PM\"\n  formatTime(momentTime) {\n    const min = momentTime.minutes();\n    if (min === 0) {\n      return momentTime.format(\"h A\");\n    }\n    return momentTime.format(\"h:mm A\");\n  }\n\n  updateParticipants = (participants) => {\n    this.setState({participants})\n  }\n\n  updateField = (key, value) => {\n    const updates = {}\n    updates[key] = value;\n    this.setState(updates)\n  }\n\n  _onChangeDay = (newTimestamp) => {\n    const newDay = moment(newTimestamp)\n    const start = this.getStartMoment();\n    const end = this.getEndMoment();\n    start.year(newDay.year())\n    end.year(newDay.year())\n    start.dayOfYear(newDay.dayOfYear())\n    end.dayOfYear(newDay.dayOfYear())\n    this.setState({start: start.unix(), end: end.unix()})\n  }\n\n  _onChangeStartTime = (newTimestamp) => {\n    const newStart = moment(newTimestamp)\n    let newEnd = this.getEndMoment();\n    if (newEnd.isSameOrBefore(newStart)) {\n      const leftInDay = moment(newStart).endOf('day').diff(newStart)\n      const move = Math.min(leftInDay, moment.duration(1, 'hour').asMilliseconds());\n      newEnd = moment(newStart).add(move, 'ms')\n    }\n    this.setState({start: newStart.unix(), end: newEnd.unix()})\n  }\n\n  _onChangeEndTime = (newTimestamp) => {\n    const newEnd = moment(newTimestamp)\n    let newStart = this.getStartMoment();\n    if (newStart.isSameOrAfter(newEnd)) {\n      const sinceDay = moment(newEnd).diff(newEnd.startOf('day'))\n      const move = Math.min(sinceDay, moment.duration(1, 'hour').asMilliseconds());\n      newStart = moment(newEnd).subtract(move, 'ms');\n    }\n    this.setState({end: newEnd.unix(), start: newStart.unix()})\n  }\n\n  renderTime() {\n    const startMoment = this.getStartMoment();\n    const endMoment = this.getEndMoment();\n    const date = startMoment.format('dddd, MMMM D'); // e.g. Tuesday, February 22\n    const timeRange = `${this.formatTime(startMoment)} - ${this.formatTime(endMoment)}`\n    return (\n      <div>\n        {date}<br />\n        {timeRange}\n      </div>\n    )\n  }\n\n  renderEditableTime() {\n    const startVal = (this.state.start) * 1000;\n    const endVal = (this.state.end) * 1000;\n    return (\n      <div className=\"row time\">\n        <RetinaImg name=\"ic-eventcard-time@2x.png\" mode={RetinaImg.Mode.ContentPreserve} />\n        <span>\n          <TimePicker\n            value={startVal}\n            onChange={this._onChangeStartTime}\n          />\n          to\n          <TimePicker\n            value={endVal}\n            onChange={this._onChangeEndTime}\n          />\n          <span className=\"timezone\">\n            {moment().tz(DateUtils.timeZone).format(\"z\")}\n          </span>\n          &nbsp;\n          on\n          &nbsp;\n          <DatePicker value={startVal} onChange={this._onChangeDay} />\n        </span>\n      </div>\n    )\n  }\n\n  renderParticipants(participants) {\n    const names = []\n    for (let i = 0; i < participants.length; i++) {\n      names.push(<div key={i}> {participants[i].name} </div>);\n    }\n    return names;\n  }\n\n  renderEditable = () => {\n    const {title, description, start, end, location, participants} = this.state;\n\n    const fragment = document.createDocumentFragment();\n    const descriptionRoot = document.createElement('root')\n    fragment.appendChild(descriptionRoot)\n    descriptionRoot.innerHTML = description;\n\n    const notes = this.extractNotesFromDescription(descriptionRoot);\n\n    return (\n      <div className=\"calendar-event-popover\" tabIndex=\"0\">\n        <TabGroupRegion>\n          <div className=\"title-wrapper\">\n            <input\n              className=\"title\"\n              type=\"text\"\n              value={title}\n              onChange={(e) => { this.updateField('title', e.target.value) }}\n            />\n          </div>\n          <input\n            className=\"location\"\n            type=\"text\"\n            value={location}\n            onChange={(e) => { this.updateField('location', e.target.value) }}\n          />\n          <div className=\"section\">\n            {this.renderEditableTime(start, end)}\n          </div>\n          <div className=\"section\">\n            <div className=\"label\">Invitees: </div>\n            <EventParticipantsInput\n              className=\"event-participant-field\"\n              participants={participants}\n              change={(val) => { this.updateField('participants', val) }}\n            />\n          </div>\n          <div className=\"section\">\n            <div className=\"label\">Notes: </div>\n            <input\n              type=\"text\"\n              value={notes}\n              onChange={(e) => { this.updateField('description', e.target.value) }}\n            />\n          </div>\n          <span onClick={this.saveEdits}>Save</span><span onClick={() => Actions.closePopover()}>Cancel</span>\n        </TabGroupRegion>\n      </div>\n    )\n  }\n\n  render() {\n    if (this.state.editing) {\n      return this.renderEditable();\n    }\n    const {title, description, location, participants} = this.state;\n\n    const fragment = document.createDocumentFragment();\n    const descriptionRoot = document.createElement('root')\n    fragment.appendChild(descriptionRoot)\n    descriptionRoot.innerHTML = description;\n\n    const notes = this.extractNotesFromDescription(descriptionRoot);\n\n    return (\n      <div className=\"calendar-event-popover\" tabIndex=\"0\">\n        <div className=\"title-wrapper\">\n          <div className=\"title\">\n            {title}\n          </div>\n          <RetinaImg\n            className=\"edit-icon\"\n            name=\"edit-icon.png\"\n            title=\"Edit Item\"\n            mode={RetinaImg.Mode.ContentIsMask}\n            onClick={this.onEdit}\n          />\n        </div>\n        <div className=\"location\">\n          {location}\n        </div>\n        <div className=\"section\">\n          {this.renderTime()}\n        </div>\n        <div className=\"section\">\n          <div className=\"label\">Invitees: </div>\n          {this.renderParticipants(participants)}\n        </div>\n        <div className=\"section\">\n          <div className=\"description\">\n            <div className=\"label\">Notes: </div>\n            <ScrollRegion>\n              <div>\n                {notes}\n              </div>\n            </ScrollRegion>\n          </div>\n        </div>\n      </div>\n    );\n  }\n}\n\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/calendar-event.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport {Event} from 'nylas-exports'\nimport {InjectedComponentSet} from 'nylas-component-kit'\nimport {calcColor} from './calendar-helpers'\n\nexport default class CalendarEvent extends React.Component {\n  static displayName = \"CalendarEvent\";\n\n  static propTypes = {\n    event: React.PropTypes.instanceOf(Event).isRequired,\n    order: React.PropTypes.number,\n    selected: React.PropTypes.bool,\n    scopeEnd: React.PropTypes.number.isRequired,\n    scopeStart: React.PropTypes.number.isRequired,\n    direction: React.PropTypes.oneOf(['horizontal', 'vertical']),\n    fixedSize: React.PropTypes.number,\n    focused: React.PropTypes.bool,\n    concurrentEvents: React.PropTypes.number,\n    onClick: React.PropTypes.func,\n    onDoubleClick: React.PropTypes.func,\n    onFocused: React.PropTypes.func,\n  }\n\n  static defaultProps = {\n    order: 1,\n    direction: \"vertical\",\n    fixedSize: -1,\n    concurrentEvents: 1,\n    onClick: () => {},\n    onDoubleClick: () => {},\n    onFocused: () => {},\n  }\n\n  componentDidMount() {\n    this._scrollFocusedEventIntoView()\n  }\n\n  componentDidUpdate() {\n    this._scrollFocusedEventIntoView()\n  }\n\n  _scrollFocusedEventIntoView() {\n    const {focused} = this.props\n    if (!focused) { return; }\n    const eventNode = ReactDOM.findDOMNode(this)\n    if (!eventNode) { return; }\n    const {event, onFocused} = this.props\n    eventNode.scrollIntoViewIfNeeded(true)\n    onFocused(event)\n  }\n\n  _getDimensions() {\n    const scopeLen = this.props.scopeEnd - this.props.scopeStart\n    const duration = this.props.event.end - this.props.event.start;\n\n    let top = Math.max((this.props.event.start - this.props.scopeStart) / scopeLen, 0);\n    let height = Math.min((duration - this._overflowBefore()) / scopeLen, 1);\n\n    let width = 1;\n    let left;\n    if (this.props.fixedSize === -1) {\n      width = 1 / this.props.concurrentEvents;\n      left = width * (this.props.order - 1);\n      width = `${width * 100}%`;\n      left = `${left * 100}%`;\n    } else {\n      width = this.props.fixedSize\n      left = this.props.fixedSize * (this.props.order - 1);\n    }\n\n    top = `${top * 100}%`\n    height = `${height * 100}%`\n\n    return {left, width, height, top}\n  }\n\n  _getStyles() {\n    let styles = {}\n    if (this.props.direction === \"vertical\") {\n      styles = this._getDimensions()\n    } else if (this.props.direction === \"horizontal\") {\n      const d = this._getDimensions()\n      styles = {\n        left: d.top,\n        width: d.height,\n        height: d.width,\n        top: d.left,\n      }\n    }\n    styles.backgroundColor = calcColor(this.props.event.calendarId);\n    return styles\n  }\n\n  _overflowBefore() {\n    return Math.max(this.props.scopeStart - this.props.event.start, 0)\n  }\n\n  render() {\n    const {direction, event, onClick, onDoubleClick, selected} = this.props;\n\n    return (\n      <div\n        id={event.id}\n        tabIndex={0}\n        style={this._getStyles()}\n        className={`calendar-event ${direction} ${selected ? 'selected' : null}`}\n        onClick={(e) => onClick(e, event)}\n        onDoubleClick={() => onDoubleClick(event)}\n      >\n        <span className=\"default-header\" style={{order: 0}}>\n          {event.displayTitle()}\n        </span>\n        <InjectedComponentSet\n          className=\"event-injected-components\"\n          style={{position: \"absolute\"}}\n          matching={{role: \"Calendar:Event\"}}\n          exposedProps={{event: event}}\n          direction=\"row\"\n        />\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/calendar-helpers.jsx",
    "content": "import {Utils} from 'nylas-exports'\n\nexport function calcColor(calendarId) {\n  let bgColor = NylasEnv.config.get(`calendar.colors.${calendarId}`)\n  if (!bgColor) {\n    const hue = Utils.hueForString(calendarId);\n    bgColor = `hsla(${hue}, 50%, 45%, 0.35)`\n  }\n  return bgColor\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/calendar-toggles.jsx",
    "content": "/* eslint jsx-a11y/label-has-for: 0 */\nimport _ from 'underscore'\nimport classnames from 'classnames'\nimport React from 'react'\n\nimport {calcColor} from './calendar-helpers'\n\nconst DISABLED_CALENDARS = \"nylas.disabledCalendars\"\n\nfunction renderCalendarToggles(calendars, disabledCalendars) {\n  return calendars.map((calendar) => {\n    const calendarId = calendar.id\n    const onClick = () => {\n      const cals = NylasEnv.config.get(DISABLED_CALENDARS) || []\n      if (cals.includes(calendarId)) {\n        cals.splice(cals.indexOf(calendarId), 1)\n      } else {\n        cals.push(calendarId)\n      }\n      NylasEnv.config.set(DISABLED_CALENDARS, cals)\n    }\n\n    const checked = !disabledCalendars.includes(calendar.id);\n    const checkboxClass = classnames({\n      \"colored-checkbox\": true,\n      \"checked\": checked,\n    })\n    const bgColor = checked ? calcColor(calendar.id) : \"transparent\"\n    return (\n      <div\n        title={calendar.name}\n        onClick={onClick}\n        className=\"toggle-wrap\"\n        key={`check-${calendar.id}`}\n      >\n        <div className={checkboxClass}>\n          <div className=\"bg-color\" style={{backgroundColor: bgColor}} />\n        </div>\n        <label>{calendar.name}</label>\n      </div>\n    )\n  })\n}\n\nexport default function CalendarToggles(props) {\n  const calsByAccountId = _.groupBy(props.calendars, \"accountId\");\n  const accountSections = []\n  for (const accountId of Object.keys(calsByAccountId)) {\n    const calendars = calsByAccountId[accountId]\n    const account = _.findWhere(props.accounts, {accountId})\n    if (!account || !calendars || calendars.length === 0) {\n      continue;\n    }\n    accountSections.push(\n      <div key={accountId} className=\"account-calendars-wrap\">\n        <div className=\"account-label\">{account.label}</div>\n        {renderCalendarToggles(calendars, props.disabledCalendars)}\n      </div>\n    )\n  }\n  return <div className=\"calendar-toggles-wrap\">{accountSections}</div>\n}\n\nCalendarToggles.propTypes = {\n  accounts: React.PropTypes.array,\n  calendars: React.PropTypes.array,\n  disabledCalendars: React.PropTypes.array,\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/current-time-indicator.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport Moment from 'moment';\nimport classNames from 'classnames';\n\nexport default class CurrentTimeIndicator extends React.Component {\n  static propTypes = {\n    gridHeight: React.PropTypes.number,\n    numColumns: React.PropTypes.number,\n    todayColumnIdx: React.PropTypes.number,\n    visible: React.PropTypes.bool,\n  }\n\n  constructor(props) {\n    super(props);\n    this._movementTimer = null;\n    this.state = this.getStateFromTime();\n  }\n\n  componentDidMount() {\n    // update our displayed time once a minute\n    this._movementTimer = setInterval(() => {\n      this.setState(this.getStateFromTime())\n    }, 60 * 1000);\n    ReactDOM.findDOMNode(this).scrollIntoViewIfNeeded(true)\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this._movementTimer);\n    this._movementTimer = null;\n  }\n\n  getStateFromTime() {\n    const now = Moment();\n    return {\n      msecIntoDay: now.millisecond() + (now.second() + (now.minute() + (now.hour() * 60)) * 60) * 1000,\n    }\n  }\n\n  render() {\n    const {gridHeight, numColumns, todayColumnIdx, visible} = this.props;\n    const msecsPerDay = 24 * 60 * 60 * 1000;\n    const {msecIntoDay} = this.state;\n\n    const todayMarker = (todayColumnIdx !== -1) ? <div style={{left: `${Math.round(todayColumnIdx * 100 / numColumns)}%`}} /> : null\n\n    return (\n      <div\n        className={classNames({\"current-time-indicator\": true, \"visible\": visible})}\n        style={{top: gridHeight * (msecIntoDay / msecsPerDay)}}\n      >\n        {todayMarker}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/event-grid-background.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport {Utils} from 'nylas-exports'\n\nexport default class EventGridBackground extends React.Component {\n  static displayName = \"EventGridBackground\";\n\n  static propTypes = {\n    height: React.PropTypes.number,\n    numColumns: React.PropTypes.number,\n    tickGenerator: React.PropTypes.func,\n    intervalHeight: React.PropTypes.number,\n  }\n\n  constructor() {\n    super();\n    this._lastHoverRect = {}\n  }\n\n  componentDidMount() {\n    this._renderEventGridBackground()\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) ||\n            !Utils.isEqualReact(nextState, this.state));\n  }\n\n  componentDidUpdate() {\n    this._renderEventGridBackground()\n  }\n\n  _renderEventGridBackground() {\n    const canvas = ReactDOM.findDOMNode(this.refs.canvas);\n    const ctx = canvas.getContext('2d');\n    ctx.clearRect(0, 0, canvas.width, canvas.height);\n    const height = this.props.height;\n    canvas.height = height;\n\n    const doStroke = (type, strokeStyle) => {\n      ctx.strokeStyle = strokeStyle;\n      ctx.beginPath();\n      for (const {yPos} of this.props.tickGenerator({type})) {\n        ctx.moveTo(0, yPos);\n        ctx.lineTo(canvas.width, yPos);\n      }\n      ctx.stroke();\n    }\n\n    doStroke(\"minor\", \"#f1f1f1\"); // Minor Ticks\n    doStroke(\"major\", \"#e0e0e0\"); // Major ticks\n  }\n\n  mouseMove({x, y, width}) {\n    if (!width || x == null || y == null) { return }\n    const lr = this._lastHoverRect;\n    const xInt = width / this.props.numColumns\n    const yInt = this.props.intervalHeight\n    const r = {\n      x: Math.floor(x / xInt) * xInt + 1,\n      y: Math.floor(y / yInt) * yInt + 1,\n      width: xInt - 2,\n      height: yInt - 2,\n    }\n    if (lr.x === r.x && lr.y === r.y && lr.width === r.width) {\n      return\n    }\n    this._lastHoverRect = r;\n    const cursor = ReactDOM.findDOMNode(this.refs.cursor);\n    cursor.style.left = `${r.x}px`;\n    cursor.style.top = `${r.y}px`;\n    cursor.style.width = `${r.width}px`;\n    cursor.style.height = `${r.height}px`;\n  }\n\n  render() {\n    const styles = {\n      width: \"100%\",\n      height: this.props.height,\n    }\n    return (\n      <div className=\"event-grid-bg-wrap\">\n        <div ref=\"cursor\" className=\"cursor\" />\n        <canvas ref=\"canvas\" className=\"event-grid-bg\" style={styles} />\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/event-participants-input.jsx",
    "content": "import React from 'react';\nimport _ from 'underscore';\nimport {remote, clipboard} from 'electron';\nimport {Utils, Contact, ContactStore, RegExpUtils} from 'nylas-exports';\nimport {TokenizingTextField, Menu, InjectedComponentSet} from 'nylas-component-kit';\n\nconst TokenRenderer = (props) => {\n  const {email, name} = props.token\n  let chipText = email;\n  if (name && (name.length > 0) && (name !== email)) {\n    chipText = name;\n  }\n  return (\n    <div className=\"participant\">\n      <InjectedComponentSet\n        matching={{role: \"Composer:RecipientChip\"}}\n        exposedProps={{contact: props.token}}\n        direction=\"column\"\n        inline\n      />\n      <span className=\"participant-primary\">{chipText}</span>\n    </div>\n  );\n};\n\nTokenRenderer.propTypes = {\n  token: React.PropTypes.object,\n};\n\nexport default class EventParticipantsInput extends React.Component {\n  static displayName = 'EventParticipantsInput';\n\n  static propTypes = {\n    participants: React.PropTypes.array.isRequired,\n    change: React.PropTypes.func.isRequired,\n    className: React.PropTypes.string,\n    onEmptied: React.PropTypes.func,\n    onFocus: React.PropTypes.func,\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);\n  }\n\n  // Public. Can be called by any component that has a ref to this one to\n  // focus the input field.\n  focus = () => {\n    this.refs.textField.focus();\n  }\n\n  _completionNode = (p) => {\n    return (\n      <Menu.NameEmailItem name={p.name} email={p.email} />\n    );\n  }\n\n  _tokensForString = (string, options = {}) => {\n    // If the input is a string, parse out email addresses and build\n    // an array of contact objects. For each email address wrapped in\n    // parentheses, look for a preceding name, if one exists.\n    if (string.length === 0) {\n      return Promise.resolve([]);\n    }\n\n    return ContactStore.parseContactsInString(string, options).then((contacts) => {\n      if (contacts.length > 0) {\n        return Promise.resolve(contacts);\n      }\n      // If no contacts are returned, treat the entire string as a single\n      // (malformed) contact object.\n      return [new Contact({email: string, name: null})];\n    });\n  }\n\n  _remove = (values) => {\n    const updates = _.reject(this.props.participants, (p) =>\n      values.includes(p.email) || values.map(o => o.email).includes(p.email)\n    );\n    this.props.change(updates);\n  }\n\n  _edit = (token, replacementString) => {\n    const tokenIndex = this.props.participants.indexOf(token);\n\n    this._tokensForString(replacementString).then((replacements) => {\n      const updates = this.props.participants.slice(0)\n      updates.splice(tokenIndex, 1, ...replacements);\n      this.props.change(updates);\n    });\n  }\n\n  _add = (values, options = {}) => {\n    // If the input is a string, parse out email addresses and build\n    // an array of contact objects. For each email address wrapped in\n    // parentheses, look for a preceding name, if one exists.\n    let tokensPromise = null;\n    if (_.isString(values)) {\n      tokensPromise = this._tokensForString(values, options);\n    } else {\n      tokensPromise = Promise.resolve(values);\n    }\n\n    tokensPromise.then((tokens) => {\n      // Safety check: remove anything from the incoming tokens that isn't\n      // a Contact. We should never receive anything else in the tokens array.\n      const contactTokens = tokens.filter(value => value instanceof Contact);\n      let updates = this.props.participants.slice(0);\n\n      for (const token of contactTokens) {\n        // add the participant to field. _.union ensures that the token will\n        // only appear once, in case it already exists in the participants.\n        updates = _.union(updates, [token]);\n      }\n\n      this.props.change(updates);\n    });\n  }\n\n  _onShowContextMenu = (participant) => {\n    // Warning: Menu is already initialized as Menu.cjsx!\n    const MenuClass = remote.Menu;\n    const MenuItem = remote.MenuItem;\n\n    const menu = new MenuClass();\n    menu.append(new MenuItem({\n      label: `Copy ${participant.email}`,\n      click: () => clipboard.writeText(participant.email),\n    }))\n    menu.append(new MenuItem({\n      type: 'separator',\n    }))\n    menu.append(new MenuItem({\n      label: 'Remove',\n      click: () => this._remove([participant]),\n    }))\n    menu.popup(remote.getCurrentWindow());\n  }\n\n  _onInputTrySubmit = (inputValue, completions = [], selectedItem) => {\n    if (RegExpUtils.emailRegex().test(inputValue)) {\n      return inputValue // no token default to raw value.\n    }\n    return selectedItem || completions[0] // first completion if any\n  }\n\n  _shouldBreakOnKeydown = (event) => {\n    const val = event.target.value.trim();\n    if (RegExpUtils.emailRegex().test(val) && event.key === \" \") {\n      return true\n    }\n    return [\",\", \";\"].includes(event.key)\n  }\n\n  render() {\n    return (\n      <TokenizingTextField\n        className={this.props.className}\n        ref=\"textField\"\n        tokens={this.props.participants}\n        tokenKey={(p) => p.email}\n        tokenIsValid={(p) => ContactStore.isValidContact(p)}\n        tokenRenderer={TokenRenderer}\n        onRequestCompletions={(input) => ContactStore.searchContacts(input)}\n        shouldBreakOnKeydown={this._shouldBreakOnKeydown}\n        onInputTrySubmit={this._onInputTrySubmit}\n        completionNode={this._completionNode}\n        onAdd={this._add}\n        onRemove={this._remove}\n        onEdit={this._edit}\n        onEmptied={this.props.onEmptied}\n        onFocus={this.props.onFocus}\n        onTokenAction={this._onShowContextMenu}\n      />\n    );\n  }\n}\n\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/event-search-bar.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport {Event, DatabaseStore} from 'nylas-exports'\nimport SearchBar from '../search-bar'\n\n\nclass EventSearchBar extends Component {\n  static displayName = 'EventSearchBar'\n\n  static propTypes = {\n    disabledCalendars: PropTypes.array,\n    onSelectEvent: PropTypes.func,\n  }\n\n  static defaultProps = {\n    disabledCalendars: [],\n    onSelectEvent: () => {},\n  }\n\n  constructor(props) {\n    super(props)\n    this.state = {\n      query: '',\n      suggestions: [],\n    }\n  }\n\n  onSearchQueryChanged = (query) => {\n    const {disabledCalendars} = this.props\n    this.setState({query})\n    if (query.length <= 1) {\n      this.onClearSearchSuggestions()\n      return\n    }\n    let dbQuery = DatabaseStore.findAll(Event).distinct() // eslint-disable-line\n    if (disabledCalendars.length > 0) {\n      dbQuery = dbQuery.where(Event.attributes.calendarId.notIn(disabledCalendars))\n    }\n    dbQuery = dbQuery\n    .search(query)\n    .limit(10)\n    .then((events) => {\n      this.setState({suggestions: events})\n    })\n  }\n\n  onClearSearchQuery = () => {\n    this.setState({query: '', suggestions: []})\n  }\n\n  onClearSearchSuggestions = () => {\n    this.setState({suggestions: []})\n  }\n\n  onSelectEvent = (event) => {\n    this.onClearSearchQuery()\n    setImmediate(() => {\n      const {onSelectEvent} = this.props\n      onSelectEvent(event)\n    })\n  }\n\n  renderEvent(event) {\n    return event.title\n  }\n\n  render() {\n    const {query, suggestions} = this.state\n\n    return (\n      <SearchBar\n        query={query}\n        suggestions={suggestions}\n        placeholder=\"Search all events\"\n        suggestionKey={(event) => event.id}\n        suggestionRenderer={this.renderEvent}\n        onSearchQueryChanged={this.onSearchQueryChanged}\n        onSelectSuggestion={this.onSelectEvent}\n        onClearSearchQuery={this.onClearSearchQuery}\n        onClearSearchSuggestions={this.onClearSearchSuggestions}\n      />\n    )\n  }\n}\n\nexport default EventSearchBar\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/footer-controls.jsx",
    "content": "import React from 'react'\nimport {Utils} from 'nylas-exports'\n\nexport default class FooterControls extends React.Component {\n  static displayName = \"FooterControls\";\n\n  static propTypes = {\n    footerComponents: React.PropTypes.node,\n  }\n\n  static defaultProps = {\n    footerComponents: false,\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) ||\n            !Utils.isEqualReact(nextState, this.state));\n  }\n\n  render() {\n    if (!this.props.footerComponents) {\n      return false\n    }\n    return (\n      <div className=\"footer-controls\">\n        <div className=\"spacer\" style={{order: 0, flex: 1}}>&nbsp;</div>\n        {this.props.footerComponents}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/header-controls.jsx",
    "content": "import React from 'react'\nimport {Utils} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit'\n\nexport default class HeaderControls extends React.Component {\n  static displayName = \"HeaderControls\";\n\n  static propTypes = {\n    title: React.PropTypes.string,\n    headerComponents: React.PropTypes.node,\n    nextAction: React.PropTypes.func,\n    prevAction: React.PropTypes.func,\n  }\n\n  static defaultProps = {\n    headerComonents: false,\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) ||\n            !Utils.isEqualReact(nextState, this.state));\n  }\n\n  _renderNextAction() {\n    if (!this.props.nextAction) { return false; }\n    return (\n      <button\n        className=\"btn btn-icon next\"\n        ref=\"onNextAction\"\n        onClick={this.props.nextAction}\n      >\n        <RetinaImg\n          name=\"ic-calendar-right-arrow.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </button>\n    )\n  }\n\n  _renderPrevAction() {\n    if (!this.props.prevAction) { return false; }\n    return (\n      <button\n        className=\"btn btn-icon prev\"\n        ref=\"onPreviousAction\"\n        onClick={this.props.prevAction}\n      >\n        <RetinaImg\n          name=\"ic-calendar-left-arrow.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </button>\n    )\n  }\n\n  render() {\n    return (\n      <div className=\"header-controls\">\n        <div className=\"center-controls\">\n          {this._renderPrevAction()}\n          <span className=\"title\">{this.props.title}</span>\n          {this._renderNextAction()}\n        </div>\n        {this.props.headerComponents}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/mini-month-view.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport moment from 'moment'\nimport classnames from 'classnames'\n\nexport default class MiniMonthView extends React.Component {\n  static displayName = \"MiniMonthView\";\n\n  static propTypes = {\n    value: React.PropTypes.number,\n    onChange: React.PropTypes.func,\n  }\n\n  static defaultProps = {\n    value: moment().valueOf(),\n    onChange: () => {},\n  }\n\n  constructor(props) {\n    super(props);\n    this.today = moment()\n    this.state = this._stateFromProps(props)\n  }\n\n  componentWillReceiveProps(newProps) {\n    this.setState(this._stateFromProps(newProps))\n  }\n\n  _stateFromProps(props) {\n    const m = props.value ? moment(props.value) : moment()\n    return {\n      shownYear: m.year(),\n      shownMonth: m.month(),\n    }\n  }\n\n  _shownMonthMoment() {\n    return moment([this.state.shownYear, this.state.shownMonth])\n  }\n\n  _changeMonth = (by) => {\n    const newMonth = this.state.shownMonth + by;\n    const newMoment = this._shownMonthMoment().month(newMonth);\n    this.setState({\n      shownYear: newMoment.year(),\n      shownMonth: newMoment.month(),\n    })\n  }\n\n  _renderLegend() {\n    const weekdayGen = moment([2016]);\n    const legendEls = [];\n    for (let i = 0; i < 7; i++) {\n      const dayStr = weekdayGen.weekday(i).format(\"dd\"); // Locale aware!\n      legendEls.push(<span key={i} className=\"weekday\">{dayStr}</span>)\n    }\n    return <div className=\"legend\">{legendEls}</div>;\n  }\n\n  _onClickDay = (event) => {\n    if (!event.target.dataset.timestamp) { return }\n    const newVal = moment(parseInt(event.target.dataset.timestamp, 10)).valueOf()\n    this.props.onChange(newVal)\n  }\n\n  _isSameDay(m1, m2) {\n    return m1.dayOfYear() === m2.dayOfYear() && m1.year() === m2.year()\n  }\n\n  _renderDays() {\n    const dayIter = this._shownMonthMoment().date(1);\n    const startWeek = dayIter.week();\n    const curMonth = this.state.shownMonth;\n    const endWeek = moment(dayIter).date(dayIter.daysInMonth()).week();\n    const weekEls = []\n    const valDay = moment(this.props.value)\n    for (let week = startWeek; week <= endWeek; week++) {\n      dayIter.week(week); // Locale aware!\n      const dayEls = []\n      for (let weekday = 0; weekday < 7; weekday++) {\n        dayIter.weekday(weekday); // Locale aware!\n        const dayStr = dayIter.format(\"D\");\n        const className = classnames({\n          \"day\": true,\n          \"today\": this._isSameDay(dayIter, this.today),\n          \"cur-day\": this._isSameDay(dayIter, valDay),\n          \"cur-month\": dayIter.month() === curMonth,\n        })\n        dayEls.push(\n          <div\n            className={className} key={`${week}-${weekday}`}\n            data-timestamp={dayIter.valueOf()}\n          >{dayStr}</div>\n        )\n      }\n      weekEls.push(<div className=\"week\" key={week}>{dayEls}</div>)\n    }\n    return (\n      <div className=\"day-grid\" onClick={this._onClickDay}>{weekEls}</div>\n    )\n  }\n\n  render() {\n    return (\n      <div className=\"mini-month-view\">\n        <div className=\"header\">\n          <div\n            className=\"btn btn-icon\"\n            onClick={_.partial(this._changeMonth, -1)}\n          >&lsaquo;</div>\n          <span className=\"month-title\">{this._shownMonthMoment().format(\"MMMM YYYY\")}</span>\n          <div\n            className=\"btn btn-icon\"\n            onClick={_.partial(this._changeMonth, 1)}\n          >&rsaquo;</div>\n        </div>\n        {this._renderLegend()}\n        {this._renderDays()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/month-view.jsx",
    "content": "import React from 'react'\n\nexport default class MonthView extends React.Component {\n  static displayName = \"MonthView\";\n\n  static propTypes = {\n    changeView: React.PropTypes.func,\n  }\n\n  _onClick = () => {\n    this.props.changeView(\"WeekView\");\n  }\n\n  render() {\n    return <button onClick={this._onClick}>Change to week</button>\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/nylas-calendar.jsx",
    "content": "import Rx from 'rx-lite'\nimport React from 'react'\nimport moment from 'moment'\nimport {DatabaseStore, AccountStore, Calendar} from 'nylas-exports'\nimport {ScrollRegion, ResizableRegion, MiniMonthView} from 'nylas-component-kit'\nimport WeekView from './week-view'\nimport MonthView from './month-view'\nimport EventSearchBar from './event-search-bar'\nimport CalendarToggles from './calendar-toggles'\nimport CalendarDataSource from './calendar-data-source'\nimport {WEEK_VIEW, MONTH_VIEW} from './calendar-constants'\n\nconst DISABLED_CALENDARS = \"nylas.disabledCalendars\"\n\n/*\n * Nylas Calendar\n */\nexport default class NylasCalendar extends React.Component {\n  static displayName = \"NylasCalendar\";\n\n  static propTypes = {\n    /*\n     * The data source that powers all of the views of the NylasCalendar\n     */\n    dataSource: React.PropTypes.instanceOf(CalendarDataSource).isRequired,\n\n    currentMoment: React.PropTypes.instanceOf(moment),\n\n    /*\n     * Any extra info you want to display on the top banner of calendar\n     * components\n     */\n    bannerComponents: React.PropTypes.shape({\n      day: React.PropTypes.node,\n      week: React.PropTypes.node,\n      month: React.PropTypes.node,\n      year: React.PropTypes.node,\n    }),\n\n    /*\n     * Any extra header components for each of the supported View types of\n     * the NylasCalendar\n     */\n    headerComponents: React.PropTypes.shape({\n      day: React.PropTypes.node,\n      week: React.PropTypes.node,\n      month: React.PropTypes.node,\n      year: React.PropTypes.node,\n    }),\n\n    /*\n     * Any extra footer components for each of the supported View types of\n     * the NylasCalendar\n     */\n    footerComponents: React.PropTypes.shape({\n      day: React.PropTypes.node,\n      week: React.PropTypes.node,\n      month: React.PropTypes.node,\n      year: React.PropTypes.node,\n    }),\n\n    /*\n     * The following are a set of supported interaction handlers.\n     *\n     * These are passed a custom set of arguments in a single object that\n     * includes the `currentView` as well as things like the `time` at the\n     * click coordinate.\n     */\n    onCalendarMouseUp: React.PropTypes.func,\n    onCalendarMouseDown: React.PropTypes.func,\n    onCalendarMouseMove: React.PropTypes.func,\n\n    onEventClick: React.PropTypes.func,\n    onEventDoubleClick: React.PropTypes.func,\n    onEventFocused: React.PropTypes.func,\n\n    selectedEvents: React.PropTypes.arrayOf(React.PropTypes.object),\n  }\n\n  static defaultProps = {\n    bannerComponents: {day: false, week: false, month: false, year: false},\n    headerComponents: {day: false, week: false, month: false, year: false},\n    footerComponents: {day: false, week: false, month: false, year: false},\n    selectedEvents: [],\n  }\n\n  static containerStyles = {\n    height: \"100%\",\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      calendars: [],\n      focusedEvent: null,\n      currentView: WEEK_VIEW,\n      currentMoment: props.currentMoment || this._now(),\n      disabledCalendars: NylasEnv.config.get(DISABLED_CALENDARS) || [],\n    };\n  }\n\n  componentWillMount() {\n    this._disposable = this._subscribeToCalendars()\n  }\n\n  componentWillUnmount() {\n    this._disposable.dispose()\n  }\n\n  _subscribeToCalendars() {\n    const calQuery = DatabaseStore.findAll(Calendar)\n    const calQueryObs = Rx.Observable.fromQuery(calQuery)\n    const accQueryObs = Rx.Observable.fromStore(AccountStore)\n    const configObs = Rx.Observable.fromConfig(DISABLED_CALENDARS)\n    return Rx.Observable.combineLatest([calQueryObs, accQueryObs, configObs])\n    .subscribe(([calendars, accountStore, disabledCalendars]) => {\n      this.setState({\n        accounts: accountStore.accounts() || [],\n        calendars: calendars || [],\n        disabledCalendars: disabledCalendars || [],\n      })\n    })\n  }\n\n  _now() {\n    return moment()\n  }\n\n  _getCurrentViewComponent() {\n    const components = {}\n    components[WEEK_VIEW] = WeekView\n    components[MONTH_VIEW] = MonthView\n    return components[this.state.currentView]\n  }\n\n  _changeCurrentView = (currentView) => {\n    this.setState({currentView});\n  }\n\n  _changeCurrentMoment = (currentMoment) => {\n    this.setState({currentMoment, focusedEvent: null})\n  }\n\n  _changeCurrentMomentFromValue = (value) => {\n    this.setState({currentMoment: moment(value), focusedEvent: null})\n  }\n\n  _focusEvent = (event) => {\n    const value = event.start * 1000\n    this.setState({currentMoment: moment(value), focusedEvent: event})\n  }\n\n  render() {\n    const CurrentView = this._getCurrentViewComponent();\n    return (\n      <div className=\"nylas-calendar\">\n        <ResizableRegion\n          className=\"calendar-toggles\"\n          initialWidth={200}\n          minWidth={200}\n          maxWidth={300}\n          handle={ResizableRegion.Handle.Right}\n          style={{flexDirection: 'column'}}\n        >\n          <ScrollRegion style={{flex: 1}}>\n            <EventSearchBar\n              onSelectEvent={this._focusEvent}\n              disabledCalendars={this.state.disabledCalendars}\n            />\n            <CalendarToggles\n              accounts={this.state.accounts}\n              calendars={this.state.calendars}\n              disabledCalendars={this.state.disabledCalendars}\n            />\n          </ScrollRegion>\n          <div style={{width: \"100%\"}}>\n            <MiniMonthView\n              value={this.state.currentMoment.valueOf()}\n              onChange={this._changeCurrentMomentFromValue}\n            />\n          </div>\n\n        </ResizableRegion>\n        <CurrentView\n          dataSource={this.props.dataSource}\n          currentMoment={this.state.currentMoment}\n          focusedEvent={this.state.focusedEvent}\n          bannerComponents={this.props.bannerComponents[this.state.currentView]}\n          headerComponents={this.props.headerComponents[this.state.currentView]}\n          footerComponents={this.props.footerComponents[this.state.currentView]}\n          changeCurrentView={this._changeCurrentView}\n          disabledCalendars={this.state.disabledCalendars}\n          changeCurrentMoment={this._changeCurrentMoment}\n          onCalendarMouseUp={this.props.onCalendarMouseUp}\n          onCalendarMouseDown={this.props.onCalendarMouseDown}\n          onCalendarMouseMove={this.props.onCalendarMouseMove}\n          selectedEvents={this.props.selectedEvents}\n          onEventClick={this.props.onEventClick}\n          onEventDoubleClick={this.props.onEventDoubleClick}\n          onEventFocused={this.props.onEventFocused}\n        />\n      </div>\n    )\n  }\n}\n\n\nNylasCalendar.WeekView = WeekView;\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/top-banner.jsx",
    "content": "import React from 'react'\n\nexport default class TopBanner extends React.Component {\n  static displayName = \"TopBanner\";\n\n  static propTypes = {\n    bannerComponents: React.PropTypes.node,\n  }\n\n  render() {\n    if (!this.props.bannerComponents) {\n      return false\n    }\n    return (\n      <div className=\"top-banner\">\n        {this.props.bannerComponents}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/week-view-all-day-events.jsx",
    "content": "import React from 'react'\nimport {Utils} from 'nylas-exports'\nimport CalendarEvent from './calendar-event'\n\n/*\n * Displays the all day events across the top bar of the week event view.\n *\n * Putting this in its own component dramatically improves performance so\n * we can use `shouldComponentUpdate` to selectively re-render these\n * events.\n */\nexport default class WeekViewAllDayEvents extends React.Component {\n  static displayName = \"WeekViewAllDayEvents\";\n\n  static propTypes = {\n    end: React.PropTypes.number,\n    start: React.PropTypes.number,\n    height: React.PropTypes.number,\n    minorDim: React.PropTypes.number,\n    allDayEvents: React.PropTypes.array,\n    allDayOverlap: React.PropTypes.object,\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) ||\n            !Utils.isEqualReact(nextState, this.state));\n  }\n\n  render() {\n    const eventComponents = this.props.allDayEvents.map((e) => {\n      return (\n        <CalendarEvent\n          event={e}\n          order={this.props.allDayOverlap[e.id].order}\n          key={e.id}\n          scopeStart={this.props.start}\n          scopeEnd={this.props.end}\n          direction=\"horizontal\"\n          fixedSize={this.props.minorDim}\n          concurrentEvents={this.props.allDayOverlap[e.id].concurrentEvents}\n        />\n      );\n    });\n    return (\n      <div className=\"all-day-events\" style={{height: this.props.height}}>\n        {eventComponents}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/week-view-event-column.jsx",
    "content": "import React from 'react'\nimport moment from 'moment'\nimport classnames from 'classnames'\nimport {Utils} from 'nylas-exports'\nimport CalendarEvent from './calendar-event'\n\n/*\n * This display a single column of events in the Week View.\n * Putting it in its own component dramatically improves render\n * performance since we can run `shouldComponentUpdate` on a\n * column-by-column basis.\n */\nexport default class WeekViewEventColumn extends React.Component {\n  static displayName = \"WeekViewEventColumn\";\n\n  static propTypes = {\n    events: React.PropTypes.array.isRequired,\n    day: React.PropTypes.instanceOf(moment),\n    dayEnd: React.PropTypes.number,\n    focusedEvent: React.PropTypes.object,\n    eventOverlap: React.PropTypes.object,\n    onEventClick: React.PropTypes.func,\n    onEventDoubleClick: React.PropTypes.func,\n    onEventFocused: React.PropTypes.func,\n    selectedEvents: React.PropTypes.arrayOf(React.PropTypes.object),\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return (!Utils.isEqualReact(nextProps, this.props) ||\n            !Utils.isEqualReact(nextState, this.state));\n  }\n\n  renderEvents() {\n    const {events, focusedEvent, selectedEvents, eventOverlap, dayEnd, day, onEventClick, onEventDoubleClick, onEventFocused} = this.props;\n    return events.map((e) =>\n      <CalendarEvent\n        ref={`event-${e.id}`}\n        event={e}\n        selected={selectedEvents.includes(e)}\n        order={eventOverlap[e.id].order}\n        focused={focusedEvent ? focusedEvent.id === e.id : false}\n        key={e.id}\n        scopeEnd={dayEnd}\n        scopeStart={day.unix()}\n        concurrentEvents={eventOverlap[e.id].concurrentEvents}\n        onClick={onEventClick}\n        onDoubleClick={onEventDoubleClick}\n        onFocused={onEventFocused}\n      />\n    );\n  }\n\n  render() {\n    const className = classnames({\n      \"event-column\": true,\n      \"weekend\": this.props.day.day() === 0 || this.props.day.day() === 6,\n    });\n    const end = moment(this.props.day).add(1, 'day').subtract(1, 'millisecond').valueOf()\n    return (\n      <div\n        className={className}\n        key={this.props.day.valueOf()}\n        data-start={this.props.day.valueOf()}\n        data-end={end}\n      >\n        {this.renderEvents()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/nylas-calendar/week-view.jsx",
    "content": "/* eslint react/jsx-no-bind: 0 */\nimport _ from 'underscore'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport moment from 'moment-timezone'\nimport classnames from 'classnames'\nimport {Utils} from 'nylas-exports'\nimport {ScrollRegion} from 'nylas-component-kit'\nimport TopBanner from './top-banner'\nimport HeaderControls from './header-controls'\nimport FooterControls from './footer-controls'\nimport CalendarDataSource from './calendar-data-source'\nimport EventGridBackground from './event-grid-background'\nimport WeekViewEventColumn from './week-view-event-column'\nimport WeekViewAllDayEvents from './week-view-all-day-events'\nimport CalendarEventContainer from './calendar-event-container'\nimport CurrentTimeIndicator from './current-time-indicator'\n\nconst BUFFER_DAYS = 7; // in each direction\nconst DAYS_IN_VIEW = 7;\nconst MIN_INTERVAL_HEIGHT = 21;\nconst DAY_DUR = moment.duration(1, 'day').as('seconds');\nconst INTERVAL_TIME = moment.duration(30, 'minutes').as('seconds');\n\n// This pre-fetches from Utils to prevent constant disc access\nconst overlapsBounds = Utils.overlapsBounds;\n\nexport default class WeekView extends React.Component {\n  static displayName = \"WeekView\";\n\n  static propTypes = {\n    dataSource: React.PropTypes.instanceOf(CalendarDataSource).isRequired,\n    currentMoment: React.PropTypes.instanceOf(moment).isRequired,\n    focusedEvent: React.PropTypes.object,\n    bannerComponents: React.PropTypes.node,\n    headerComponents: React.PropTypes.node,\n    footerComponents: React.PropTypes.node,\n    disabledCalendars: React.PropTypes.array,\n    changeCurrentView: React.PropTypes.func,\n    changeCurrentMoment: React.PropTypes.func,\n    onCalendarMouseUp: React.PropTypes.func,\n    onCalendarMouseDown: React.PropTypes.func,\n    onCalendarMouseMove: React.PropTypes.func,\n    onEventClick: React.PropTypes.func,\n    onEventDoubleClick: React.PropTypes.func,\n    onEventFocused: React.PropTypes.func,\n    selectedEvents: React.PropTypes.arrayOf(React.PropTypes.object),\n  }\n\n  static defaultProps = {\n    changeCurrentView: () => {},\n    bannerComponents: false,\n    headerComponents: false,\n    footerComponents: false,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      events: [],\n      intervalHeight: MIN_INTERVAL_HEIGHT,\n    }\n  }\n\n  componentWillMount() {\n    this._initializeComponent(this.props)\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    this._centerScrollRegion()\n    this._setIntervalHeight()\n    const weekStart = moment(this.state.startMoment).add(BUFFER_DAYS, 'days').unix()\n    this._scrollTime = weekStart\n    this._ensureHorizontalScrollPos()\n    window.addEventListener('resize', this._setIntervalHeight, true)\n  }\n\n  componentWillReceiveProps(props) {\n    this._initializeComponent(props)\n  }\n\n  componentDidUpdate() {\n    this._setIntervalHeight()\n    this._ensureHorizontalScrollPos()\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n    this._sub.dispose();\n    window.removeEventListener('resize', this._setIntervalHeight)\n  }\n\n  // Indirection for testing purposes\n  _now() {\n    return moment()\n  }\n\n  _initializeComponent(props) {\n    this.todayYear = this._now().year()\n    this.todayDayOfYear = this._now().dayOfYear()\n\n    if (this._sub) {\n      this._sub.dispose();\n    }\n\n    const startMoment = this._calculateStartMoment(props);\n    const endMoment = this._calculateEndMoment(props);\n\n    this._sub = this.props.dataSource.buildObservable({\n      disabledCalendars: props.disabledCalendars,\n      startTime: startMoment.unix(),\n      endTime: endMoment.unix(),\n    }).subscribe((state) => {\n      this.setState(state);\n    })\n\n    this.setState({startMoment, endMoment})\n\n    const percent = (this._scrollTime - startMoment.unix()) / (endMoment.unix() - startMoment.unix())\n    if (percent < 0 || percent > 1) {\n      this._scrollTime = startMoment.unix();\n    } else {\n      const weekStart = moment(props.currentMoment).startOf('day').weekday(0).unix()\n      this._scrollTime = weekStart\n    }\n  }\n\n  _calculateStartMoment(props) {\n    let start;\n\n    // NOTE: Since we initialize a new time from one of the properties of\n    // the props.currentMomet, we need to check for the timezone!\n    //\n    // Other relative operations (like adding or subtracting time) are\n    // independent of a timezone.\n    const tz = props.currentMoment.tz()\n    if (tz) {\n      start = moment.tz([props.currentMoment.year()], tz)\n    } else {\n      start = moment([props.currentMoment.year()])\n    }\n\n    start = start.weekday(0).week(props.currentMoment.week())\n      .subtract(BUFFER_DAYS, 'days')\n    return start\n  }\n\n  _calculateEndMoment(props) {\n    const end = moment(this._calculateStartMoment(props))\n      .add(BUFFER_DAYS * 2 + DAYS_IN_VIEW, 'days')\n      .subtract(1, 'millisecond')\n    return end\n  }\n\n  _renderDateLabel = (day) => {\n    const className = classnames({\n      \"day-label-wrap\": true,\n      \"is-today\": this._isToday(day),\n    })\n    return (\n      <div className={className} key={day.valueOf()}>\n        <span className=\"date-label\">{day.format(\"D\")}</span>\n        <span className=\"weekday-label\">{day.format(\"ddd\")}</span>\n      </div>\n    )\n  }\n\n  _isToday(day) {\n    return (this.todayDayOfYear === day.dayOfYear() && this.todayYear === day.year())\n  }\n\n  _renderEventColumn = (eventsByDay, day) => {\n    const dayUnix = day.unix();\n    const events = eventsByDay[dayUnix]\n    return (\n      <WeekViewEventColumn\n        day={day}\n        dayEnd={dayUnix + DAY_DUR - 1}\n        key={day.valueOf()}\n        events={events}\n        eventOverlap={this._eventOverlap(events)}\n        focusedEvent={this.props.focusedEvent}\n        selectedEvents={this.props.selectedEvents}\n        onEventClick={this.props.onEventClick}\n        onEventDoubleClick={this.props.onEventDoubleClick}\n        onEventFocused={this.props.onEventFocused}\n      />\n    )\n  }\n\n  _allDayEventHeight(allDayOverlap) {\n    if (_.size(allDayOverlap) === 0) {\n      return 0\n    }\n    return (this._maxConcurrentEvents(allDayOverlap) * MIN_INTERVAL_HEIGHT) + 1\n  }\n\n  /*\n   * Computes the overlap between a set of events in not O(n^2).\n   *\n   * Returns a hash keyed by event id whose value is an object:\n   *   - concurrentEvents: number of concurrent events\n   *   - order: the order in that series of concurrent events\n   */\n  _eventOverlap(events) {\n    const times = {}\n    for (const event of events) {\n      if (!times[event.start]) { times[event.start] = [] }\n      if (!times[event.end]) { times[event.end] = [] }\n      times[event.start].push(event)\n      times[event.end].push(event)\n    }\n    const sortedTimes = Object.keys(times).map((k) => parseInt(k, 10)).sort();\n    const overlapById = {}\n    let startedEvents = []\n    for (const t of sortedTimes) {\n      for (const e of times[t]) {\n        if (e.start === t) {\n          overlapById[e.id] = {concurrentEvents: 1, order: null}\n          startedEvents.push(e)\n        }\n        if (e.end === t) {\n          startedEvents = _.reject(startedEvents, (o) => o.id === e.id)\n        }\n      }\n      for (const e of startedEvents) {\n        if (!overlapById[e.id]) { overlapById[e.id] = {} }\n        const numEvents = this._findMaxConcurrent(startedEvents, overlapById);\n        overlapById[e.id].concurrentEvents = numEvents;\n        if (overlapById[e.id].order === null) {\n          // Dont' re-assign the order.\n          const order = this._findAvailableOrder(startedEvents, overlapById);\n          overlapById[e.id].order = order;\n        }\n      }\n    }\n    return overlapById\n  }\n\n  _findMaxConcurrent(startedEvents, overlapById) {\n    let max = 1;\n    for (const e of startedEvents) {\n      max = Math.max((overlapById[e.id].concurrentEvents || 1), max);\n    }\n    return Math.max(max, startedEvents.length)\n  }\n\n  _findAvailableOrder(startedEvents, overlapById) {\n    const orders = startedEvents.map((e) => overlapById[e.id].order);\n    let order = 1;\n    while (true) {\n      if (orders.indexOf(order) === -1) { return order }\n      order += 1;\n    }\n  }\n\n  _maxConcurrentEvents(eventOverlap) {\n    let maxConcurrent = -1;\n    _.each(eventOverlap, ({concurrentEvents}) => {\n      maxConcurrent = Math.max(concurrentEvents, maxConcurrent)\n    })\n    return maxConcurrent\n  }\n\n  _daysInView() {\n    const start = this.state.startMoment;\n    const days = []\n    for (let i = 0; i < (DAYS_IN_VIEW + BUFFER_DAYS * 2); i++) {\n      // moment::weekday is locale aware since some weeks start on diff\n      // days. See http://momentjs.com/docs/#/get-set/weekday/\n      days.push(moment(start).weekday(i))\n    }\n    return days\n  }\n\n  _currentWeekText() {\n    const start = moment(this.state.startMoment).add(BUFFER_DAYS, 'days');\n    const end = moment(this.state.endMoment).subtract(BUFFER_DAYS, 'days');\n    return `${start.format(\"MMMM D\")} - ${end.format(\"MMMM D YYYY\")}`\n  }\n\n  _headerComponents() {\n    const left = (\n      <button\n        key=\"today\"\n        className=\"btn\"\n        ref=\"todayBtn\"\n        onClick={this._onClickToday}\n        style={{position: 'absolute', left: 10}}\n      >\n        Today\n      </button>\n    );\n    const right = false\n    return [left, right, this.props.headerComponents]\n  }\n\n  _onClickToday = () => {\n    this.props.changeCurrentMoment(this._now())\n  }\n\n  _onClickNextWeek = () => {\n    const newMoment = moment(this.props.currentMoment).add(1, 'week')\n    this.props.changeCurrentMoment(newMoment)\n  }\n\n  _onClickPrevWeek = () => {\n    const newMoment = moment(this.props.currentMoment).subtract(1, 'week')\n    this.props.changeCurrentMoment(newMoment)\n  }\n\n  _gridHeight() {\n    return DAY_DUR / INTERVAL_TIME * this.state.intervalHeight\n  }\n\n  _centerScrollRegion() {\n    const wrap = ReactDOM.findDOMNode(this.refs.eventGridWrap);\n    wrap.scrollTop = (this._gridHeight() / 2) - (wrap.getBoundingClientRect().height / 2);\n  }\n\n  // This generates the ticks used mark the event grid and the\n  // corresponding legend in the week view.\n  * _tickGenerator({type}) {\n    const height = this._gridHeight();\n\n    let step = INTERVAL_TIME;\n    let stepStart = 0;\n\n    // We only use a moment object so we can properly localize the \"time\"\n    // part. The day is irrelevant. We just need to make sure we're\n    // picking a non-DST boundary day.\n    const start = moment([2015, 1, 1]);\n\n    let duration = INTERVAL_TIME\n    if (type === \"major\") {\n      step = INTERVAL_TIME * 2;\n      duration += INTERVAL_TIME\n    } else if (type === \"minor\") {\n      step = INTERVAL_TIME * 2;\n      stepStart = INTERVAL_TIME;\n      duration += INTERVAL_TIME\n      start.add(INTERVAL_TIME, 'seconds');\n    }\n\n    const curTime = moment(start)\n    for (let tsec = stepStart; tsec <= DAY_DUR; tsec += step) {\n      const y = (tsec / DAY_DUR) * height;\n      yield {time: curTime, yPos: y}\n      curTime.add(duration, 'seconds')\n    }\n  }\n\n  _setIntervalHeight = () => {\n    if (!this._mounted) { return } // Resize unmounting is delayed in tests\n    const wrap = ReactDOM.findDOMNode(this.refs.eventGridWrap);\n    const wrapHeight = wrap.getBoundingClientRect().height;\n    if (this._lastWrapHeight === wrapHeight) {\n      return\n    }\n    this._lastWrapHeight = wrapHeight;\n    const numIntervals = Math.floor(DAY_DUR / INTERVAL_TIME);\n    ReactDOM.findDOMNode(this.refs.eventGridLegendWrap).style.height = `${wrapHeight}px`;\n    this.setState({\n      intervalHeight: Math.max(wrapHeight / numIntervals, MIN_INTERVAL_HEIGHT),\n    });\n  }\n\n  _onScrollGrid = (event) => {\n    ReactDOM.findDOMNode(this.refs.eventGridLegendWrap).scrollTop = event.target.scrollTop\n  }\n\n  _onScrollCalendarArea = (event) => {\n    if (!event.target.scrollLeft) { return }\n    const percent = event.target.scrollLeft / event.target.scrollWidth;\n    const weekStart = this.state.startMoment.unix()\n    const weekEnd = this.state.endMoment.unix()\n    this._scrollTime = weekStart + ((weekEnd - weekStart) * percent)\n\n    if (percent < 0.25) {\n      this._onClickPrevWeek()\n    } else if (percent + (DAYS_IN_VIEW / (BUFFER_DAYS * 2 + DAYS_IN_VIEW)) > 0.95) {\n      this._onClickNextWeek()\n    }\n    this._ensureHorizontalScrollPos();\n  }\n\n  _ensureHorizontalScrollPos() {\n    if (!this._scrollTime) return;\n    const weekStart = this.state.startMoment.unix()\n    const weekEnd = this.state.endMoment.unix()\n    let percent = (this._scrollTime - weekStart) / (weekEnd - weekStart)\n    percent = Math.min(Math.max(percent, 0), 1)\n    const wrap = ReactDOM.findDOMNode(this.refs.calendarAreaWrap)\n    wrap.scrollLeft = wrap.scrollWidth * percent\n  }\n\n  _renderEventGridLabels() {\n    const labels = []\n    let centering = 0;\n    for (const {time, yPos} of this._tickGenerator({type: \"major\"})) {\n      const hr = time.format(\"LT\"); // Locale time. 2:00 pm or 14:00\n      const style = {top: yPos - centering}\n      labels.push(<span className=\"legend-text\" key={yPos} style={style}>{hr}</span>)\n      centering = 8; // center all except the 1st one.\n    }\n    return labels.slice(0, labels.length - 1);\n  }\n\n  _bufferRatio() {\n    return (BUFFER_DAYS * 2 + DAYS_IN_VIEW) / DAYS_IN_VIEW\n  }\n\n  // We calculate events by days so we only need to iterate through all\n  // events in the span once.\n  _eventsByDay(days) {\n    const map = {allDay: []};\n    const unixDays = days.map((d) => d.unix());\n    unixDays.forEach((d) => {\n      map[d] = [];\n      return;\n    });\n    for (const event of this.state.events) {\n      if (event.isAllDay()) {\n        map.allDay.push(event)\n      } else {\n        for (const day of unixDays) {\n          const bounds = {\n            start: day,\n            end: day + DAY_DUR - 1,\n          }\n          if (overlapsBounds(bounds, event)) {\n            map[day].push(event)\n          }\n        }\n      }\n    }\n    return map\n  }\n\n  render() {\n    const days = this._daysInView();\n    const todayColumnIdx = days.findIndex(d => this._isToday(d));\n    const eventsByDay = this._eventsByDay(days)\n    const allDayOverlap = this._eventOverlap(eventsByDay.allDay);\n    const tickGen = this._tickGenerator.bind(this);\n    const gridHeight = this._gridHeight();\n\n    return (\n      <div className=\"calendar-view week-view\">\n        <CalendarEventContainer\n          ref=\"calendarEventContainer\"\n          onCalendarMouseUp={this.props.onCalendarMouseUp}\n          onCalendarMouseDown={this.props.onCalendarMouseDown}\n          onCalendarMouseMove={this.props.onCalendarMouseMove}\n        >\n          <TopBanner bannerComponents={this.props.bannerComponents} />\n\n          <HeaderControls\n            title={this._currentWeekText()}\n            ref=\"headerControls\"\n            headerComponents={this._headerComponents()}\n            nextAction={this._onClickNextWeek}\n            prevAction={this._onClickPrevWeek}\n          />\n\n          <div className=\"calendar-body-wrap\">\n            <div className=\"calendar-legend\">\n              <div\n                className=\"date-label-legend\"\n                style={{height: this._allDayEventHeight(allDayOverlap) + 75 + 1}}\n              >\n                <span className=\"legend-text\">All Day</span>\n              </div>\n              <div className=\"event-grid-legend-wrap\" ref=\"eventGridLegendWrap\">\n                <div className=\"event-grid-legend\" style={{height: gridHeight}}>\n                  {this._renderEventGridLabels()}\n                </div>\n              </div>\n            </div>\n\n            <div\n              className=\"calendar-area-wrap\"\n              ref=\"calendarAreaWrap\"\n              onScroll={this._onScrollCalendarArea}\n            >\n              <div className=\"week-header\" style={{width: `${this._bufferRatio() * 100}%`}}>\n                <div className=\"date-labels\">\n                  {days.map(this._renderDateLabel)}\n                </div>\n\n                <WeekViewAllDayEvents\n                  ref=\"weekViewAllDayEvents\"\n                  minorDim={MIN_INTERVAL_HEIGHT}\n                  end={this.state.endMoment.unix()}\n                  height={this._allDayEventHeight(allDayOverlap)}\n                  start={this.state.startMoment.unix()}\n                  allDayEvents={eventsByDay.allDay}\n                  allDayOverlap={allDayOverlap}\n                />\n              </div>\n              <ScrollRegion\n                className=\"event-grid-wrap\"\n                ref=\"eventGridWrap\"\n                getScrollbar={() => this.refs.scrollbar}\n                onScroll={this._onScrollGrid}\n                style={{width: `${this._bufferRatio() * 100}%`}}\n              >\n                <div className=\"event-grid\" style={{height: gridHeight}}>\n                  {days.map(_.partial(this._renderEventColumn, eventsByDay))}\n                  <CurrentTimeIndicator\n                    visible={(todayColumnIdx > BUFFER_DAYS && todayColumnIdx <= (BUFFER_DAYS + DAYS_IN_VIEW))}\n                    gridHeight={gridHeight}\n                    numColumns={BUFFER_DAYS * 2 + DAYS_IN_VIEW}\n                    todayColumnIdx={todayColumnIdx}\n                  />\n                  <EventGridBackground\n                    height={gridHeight}\n                    intervalHeight={this.state.intervalHeight}\n                    numColumns={BUFFER_DAYS * 2 + DAYS_IN_VIEW}\n                    ref=\"eventGridBg\"\n                    tickGenerator={tickGen}\n                  />\n                </div>\n              </ScrollRegion>\n            </div>\n            <ScrollRegion.Scrollbar\n              ref=\"scrollbar\"\n              getScrollRegion={() => this.refs.eventGridWrap}\n            />\n          </div>\n\n          <FooterControls footerComponents={this.props.footerComponents} />\n        </CalendarEventContainer>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/oauth-signin-page.jsx",
    "content": "import React from 'react';\nimport {ipcRenderer, shell} from 'electron';\nimport {Actions} from 'nylas-exports'\nimport {RetinaImg} from 'nylas-component-kit';\n\nconst clipboard = require('electron').clipboard\n\nexport default class OAuthSignInPage extends React.Component {\n  static displayName = \"OAuthSignInPage\";\n\n  static propTypes = {\n    /**\n     * Step 1: Open a webpage in the user's browser letting them login on\n     * the native provider's website. We pass along a key and a redirect\n     * url to a Nylas-owned server\n     */\n    providerAuthPageUrl: React.PropTypes.string,\n\n    /**\n     * Step 2: Poll a Nylas server with this function looking for the key.\n     * Once users complete the auth successfully, Nylas servers will get\n     * the token and vend it back to us via this url. We need to poll\n     * since we don't know how long it'll take users to log in on their\n     * provider's website.\n     */\n    tokenRequestPollFn: React.PropTypes.func,\n\n    /**\n     * Once we have the token, we can use that to retrieve the full\n     * account credentials or establish a direct connection ourselves.\n     * Some Nylas backends vend all account credentials along with the\n     * token making this function unnecessary and a no-op. Nylas Mail\n     * local sync needs to use the returned OAuth token to establish an\n     * IMAP connection directly that may have its own set of failure\n     * cases.\n     */\n    accountFromTokenFn: React.PropTypes.func,\n\n    /**\n     * Called once we have successfully received the account data from\n     * `accountFromTokenFn`\n     */\n    onSuccess: React.PropTypes.func,\n\n    onTryAgain: React.PropTypes.func,\n    iconName: React.PropTypes.string,\n    sessionKey: React.PropTypes.string,\n    serviceName: React.PropTypes.string,\n    accountInfo: React.PropTypes.object,\n  };\n\n  constructor() {\n    super()\n    this.state = {\n      authStage: \"initial\",\n      showAlternative: false,\n      isCertificateError: false,\n    }\n    this._tokenData = null\n  }\n\n  componentDidMount() {\n    // Show the \"Sign in to ...\" prompt for a moment before bouncing\n    // to URL. (400msec animation + 200msec to read)\n    this._pollTimer = null;\n    this._startTimer = setTimeout(() => {\n      shell.openExternal(this.props.providerAuthPageUrl);\n      this.startPollingForResponse();\n    }, 600);\n    setTimeout(() => {\n      this.setState({showAlternative: true})\n    }, 1500);\n  }\n\n  componentWillUnmount() {\n    if (this._startTimer) clearTimeout(this._startTimer);\n    if (this._pollTimer) clearTimeout(this._pollTimer);\n  }\n\n  _handleError(err) {\n    const isCertificateError = err.statusCode === 495\n    this.setState({authStage: \"error\", errorMessage: err.message, isCertificateError})\n    Actions.recordUserEvent('Email Account Auth Failed', {\n      erroredEmail: this.props.accountInfo.email,\n      errorMessage: err.message,\n      errorLocation: \"client\",\n      provider: \"gmail\",\n    })\n  }\n\n  startPollingForResponse() {\n    let delay = 1000;\n    let onWindowFocused = null;\n    let poll = null;\n    this.setState({authStage: \"polling\"})\n\n    onWindowFocused = () => {\n      delay = 1000;\n      if (this._pollTimer) {\n        clearTimeout(this._pollTimer);\n        this._pollTimer = setTimeout(poll, delay);\n      }\n    };\n\n    poll = async () => {\n      clearTimeout(this._pollTimer);\n      try {\n        this._tokenData = await this.props.tokenRequestPollFn(this.props.sessionKey)\n        ipcRenderer.removeListener('browser-window-focus', onWindowFocused);\n        this.fetchAccountDataWithToken(this._tokenData)\n      } catch (err) {\n        if (err.statusCode === 404) {\n          delay = Math.min(delay * 1.1, 3000);\n          this._pollTimer = setTimeout(poll, delay);\n        } else {\n          ipcRenderer.removeListener('browser-window-focus', onWindowFocused);\n          this._handleError(err)\n        }\n      }\n    }\n\n    ipcRenderer.on('browser-window-focus', onWindowFocused);\n    this._pollTimer = setTimeout(poll, 3000);\n  }\n\n  async fetchAccountDataWithToken(tokenData, {forceTrustCertificate = false} = {}) {\n    if (!tokenData) {\n      throw new Error('fetchAccountDataWithToken: `tokenData` is required')\n    }\n    try {\n      this.setState({authStage: \"fetchingAccount\"})\n      const accountData = await this.props.accountFromTokenFn(tokenData, {forceTrustCertificate});\n      this.props.onSuccess(accountData)\n      this.setState({authStage: 'accountSuccess'})\n    } catch (err) {\n      this._handleError(err)\n    }\n  }\n\n  _renderCertificateErrorHeader() {\n    const {onTryAgain} = this.props\n    const {errorMessage} = this.state\n    return (\n      <div>\n        <h2>Sorry, we had trouble logging you in</h2>\n        <div className=\"error-region\">\n          <p className=\"message error error-message\">{errorMessage}</p>\n          <p className=\"message error error-message\">\n            The certificate for this server is invalid. Would you like to connect to the server anyway?\n          </p>\n          <br />\n          <div>\n            <button\n              className=\"btn btn-large btn-gradient btn-add-account\"\n              onClick={onTryAgain}\n            >\n              Try again\n            </button>\n            <button\n              className=\"btn btn-large btn-gradient btn-add-account\"\n              onClick={() => this.fetchAccountDataWithToken(this._tokenData, {forceTrustCertificate: true})}\n            >\n              Connect anyway\n            </button>\n          </div>\n        </div>\n      </div>\n    )\n  }\n\n  _renderHeader() {\n    const {serviceName, onTryAgain} = this.props\n    const {authStage, isCertificateError, errorMessage} = this.state\n    if (authStage === 'initial' || authStage === 'polling') {\n      return (<h2>\n        Sign in with {serviceName} in<br />your browser.\n      </h2>)\n    } else if (authStage === 'fetchingAccount') {\n      return <h2>Connecting to {serviceName}…</h2>\n    } else if (authStage === 'accountSuccess') {\n      return (\n        <div>\n          <h2>Successfully connected to {serviceName}!</h2>\n          <h3>Adding your account to Nylas Mail…</h3>\n        </div>\n      )\n    }\n\n    if (isCertificateError) {\n      return this._renderCertificateErrorHeader()\n    }\n\n    // Error\n    return (\n      <div>\n        <h2>Sorry, we had trouble logging you in</h2>\n        <div className=\"error-region\">\n          <p className=\"message error error-message\">{errorMessage}</p>\n          <p className=\"extra\">Please <a onClick={onTryAgain}>try again</a> later.</p>\n        </div>\n      </div>\n    )\n  }\n\n  _renderAlternative() {\n    let classnames = \"input hidden\"\n    if (this.state.authStage === \"polling\" && this.state.showAlternative) {\n      classnames += \" fadein\"\n    }\n\n    return (\n      <div className=\"alternative-auth\">\n        <div className={classnames}>\n          <div style={{marginTop: 40}}>\n            Page didn&#39;t open? Paste this URL into your browser:\n          </div>\n          <input\n            type=\"url\"\n            className=\"url-copy-target\"\n            value={this.props.providerAuthPageUrl}\n            readOnly\n          />\n          <div\n            className=\"copy-to-clipboard\"\n            onClick={() => clipboard.writeText(this.props.providerAuthPageUrl)}\n            onMouseDown={() => this.setState({pressed: true})}\n            onMouseUp={() => this.setState({pressed: false})}\n          >\n            <RetinaImg\n              name=\"icon-copytoclipboard.png\"\n              mode={RetinaImg.Mode.ContentIsMask}\n            />\n          </div>\n        </div>\n      </div>\n    )\n  }\n\n  render() {\n    return (\n      <div className={`page account-setup ${this.props.serviceName.toLowerCase()}`}>\n        <div className=\"logo-container\">\n          <RetinaImg\n            name={this.props.iconName}\n            mode={RetinaImg.Mode.ContentPreserve}\n            className=\"logo\"\n          />\n        </div>\n        {this._renderHeader()}\n        {this._renderAlternative()}\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/open-identity-page-button.jsx",
    "content": "import React from 'react';\nimport {shell} from 'electron';\nimport classnames from 'classnames'\nimport RetinaImg from './retina-img'\nimport IdentityStore from '../flux/stores/identity-store';\n\nexport default class OpenIdentityPageButton extends React.Component {\n  static propTypes = {\n    path: React.PropTypes.string,\n    label: React.PropTypes.string,\n    source: React.PropTypes.string,\n    campaign: React.PropTypes.string,\n    img: React.PropTypes.string,\n    isCTA: React.PropTypes.bool,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      loading: false,\n    };\n  }\n\n  _onClick = () => {\n    this.setState({loading: true});\n    IdentityStore.fetchSingleSignOnURL(this.props.path, {\n      source: this.props.source,\n      campaign: this.props.campaign,\n      content: this.props.label,\n    }).then((url) => {\n      this.setState({loading: false});\n      shell.openExternal(url);\n    });\n  }\n\n  render() {\n    if (this.state.loading) {\n      return (\n        <div className=\"btn btn-disabled\">\n          <RetinaImg name=\"sending-spinner.gif\" width={15} height={15} mode={RetinaImg.Mode.ContentPreserve} />\n          &nbsp;{this.props.label}&hellip;\n        </div>\n      );\n    }\n    if (this.props.img) {\n      return (\n        <div className=\"btn\" onClick={this._onClick}>\n          <RetinaImg name={this.props.img} mode={RetinaImg.Mode.ContentPreserve} />\n          &nbsp;&nbsp;{this.props.label}\n        </div>\n      )\n    }\n    const cls = classnames({\n      \"btn\": true,\n      \"btn-emphasis\": this.props.isCTA,\n    })\n    return (\n      <div className={cls} onClick={this._onClick}>{this.props.label}</div>\n    );\n  }\n}\n\n"
  },
  {
    "path": "packages/client-app/src/components/outline-view-item.jsx",
    "content": "/* eslint global-require:0 */\n/* eslint jsx-a11y/tabindex-no-positive:0 */\n\nimport _ from 'underscore';\nimport {Utils} from 'nylas-exports'\nimport classnames from 'classnames';\nimport React, {Component, PropTypes} from 'react';\nimport ReactDOM from 'react-dom';\nimport DisclosureTriangle from './disclosure-triangle';\nimport DropZone from './drop-zone';\nimport RetinaImg from './retina-img';\n\n/*\n * Enum for counter styles\n * @readonly\n * @enum {string}\n */\nconst CounterStyles = {\n  Default: 'def',\n  Alt: 'alt',\n};\n\n\n/*\n * Renders an item that may contain more arbitrarily nested items\n * This component resembles OS X's default OutlineView or Sourcelist\n *\n * An OutlineViewItem behaves like a controlled React component; it controls no\n * state internally. All of the desired state must be passed in through props.\n *\n *\n * OutlineView handles:\n * - Collapsing and uncollapsing\n * - Editing value for item\n * - Deleting item\n * - Selecting the item\n * - Displaying an associated count\n * - Dropping elements\n *\n * @param {object} props - props for OutlineViewItem\n * @param {object} props.item - props for OutlineViewItem\n * @param {string} props.item.id - Unique id for the item.\n * @param {string} props.item.name - Name to display\n * @param {string} props.item.contextMenuLabel - Label to be displayed in context menu\n * @param {string} props.item.className - Extra classes to add to the item\n * @param {string} props.item.iconName - Icon name for icon. See {@link RetinaImg} for further reference.\n * @param {array} props.item.children - Array of children of the same type to be\n * displayed.\n * @param {number} props.item.count - Count to display. If falsy, wont display a\n * count.\n * @param {CounterStyles} props.item.counterStyle - One of the possible\n * CounterStyles\n * @param {string} props.item.inputPlaceholder - Placehodler to use when editing\n * item\n * @param {boolean} props.item.collapsed - Whether the OutlineViewItem is collapsed or\n * not\n * @param {boolean} props.item.editing - Whether the OutlineViewItem is being\n * edited\n * @param {boolean} props.item.selected - Whether the OutlineViewItem is selected\n * @param {props.item.shouldAcceptDrop} props.item.shouldAcceptDrop\n * @param {props.item.onCollapseToggled} props.item.onCollapseToggled\n * @param {props.item.onInputCleared} props.item.onInputCleared\n * @param {props.item.onDrop} props.item.onDrop\n * @param {props.item.onSelect} props.item.onSelect\n * @param {props.item.onDelete} props.item.onDelete\n * @param {props.item.onEdited} props.item.onEdited\n * @class OutlineViewItem\n */\nclass OutlineViewItem extends Component {\n  static displayName = 'OutlineView';\n\n  /*\n   * If provided, this function will be called when receiving a drop. It must\n   * return true if it should accept it or false otherwise.\n   * @callback props.item.shouldAcceptDrop\n   * @param {object} item - The current item\n   * @param {object} event - The drag event\n   * @return {boolean}\n   */\n  /*\n   * If provided, this function will be called when the action to collapse or\n   * uncollapse the OutlineViewItem is executed.\n   * @callback props.item.onCollapseToggled\n   * @param {object} item - The current item\n   */\n  /*\n   * If provided, this function will be called when the editing input is cleared\n   * via Esc key, blurring, or submiting the edit.\n   * @callback props.item.onInputCleared\n   * @param {object} item - The current item\n   * @param {object} event - The associated event\n   */\n  /*\n   * If provided, this function will be called when an element is dropped in the\n   * item\n   * @callback props.item.onDrop\n   * @param {object} item - The current item\n   * @param {object} event - The associated event\n   */\n  /*\n   * If provided, this function will be called when the item is selected\n   * @callback props.item.onSelect\n   * @param {object} item - The current item\n   */\n  /*\n   * If provided, this function will be called when the the delete action is\n   * executed\n   * @callback props.item.onDelete\n   * @param {object} item - The current item\n   */\n  /*\n   * If provided, this function will be called when the item is edited\n   * @callback props.item.onEdited\n   * @param {object} item - The current item\n   * @param {string} value - The new value\n   */\n  static propTypes = {\n    item: PropTypes.shape({\n      className: PropTypes.string,\n      id: PropTypes.string.isRequired,\n      children: PropTypes.array.isRequired,\n      name: PropTypes.string.isRequired,\n      iconName: PropTypes.string.isRequired,\n      count: PropTypes.number,\n      counterStyle: PropTypes.string,\n      inputPlaceholder: PropTypes.string,\n      collapsed: PropTypes.bool,\n      editing: PropTypes.bool,\n      selected: PropTypes.bool,\n      shouldAcceptDrop: PropTypes.func,\n      onCollapseToggled: PropTypes.func,\n      onInputCleared: PropTypes.func,\n      onDrop: PropTypes.func,\n      onSelect: PropTypes.func,\n      onDelete: PropTypes.func,\n      onEdited: PropTypes.func,\n    }).isRequired,\n  };\n\n  static CounterStyles = CounterStyles;\n\n  constructor(props) {\n    super(props);\n    this._expandTimeout = null;\n    this.state = {\n      isDropping: false,\n      editing: props.item.editing || false,\n    }\n  }\n\n  componentDidMount() {\n    if (this._shouldShowContextMenu()) {\n      ReactDOM.findDOMNode(this).addEventListener('contextmenu', this._onShowContextMenu);\n    }\n  }\n\n  componentWillReceiveProps(newProps) {\n    if (newProps.editing) {\n      this.setState({editing: newProps.editing});\n    }\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) ||\n      !Utils.isEqualReact(nextState, this.state);\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this._expandTimeout);\n    if (this._shouldShowContextMenu()) {\n      ReactDOM.findDOMNode(this).removeEventListener('contextmenu', this._onShowContextMenu);\n    }\n  }\n\n  // Helpers\n\n  _runCallback = (method, ...args) => {\n    const item = this.props.item;\n    if (item[method]) {\n      return item[method](item, ...args);\n    }\n    return undefined;\n  };\n\n  _shouldShowContextMenu = () => {\n    return this.props.item.onDelete != null || this.props.item.onEdited != null;\n  };\n\n  _shouldAcceptDrop = (event) => {\n    return this._runCallback('shouldAcceptDrop', event);\n  };\n\n  _clearEditingState = (event) => {\n    this.setState({editing: false});\n    this._runCallback('onInputCleared', event);\n  };\n\n\n  // Handlers\n\n  _onDragStateChange = ({isDropping}) => {\n    this.setState({isDropping});\n\n    const {item} = this.props;\n    if ((isDropping === true) && (item.children.length > 0) && (item.collapsed)) {\n      this._expandTimeout = setTimeout(this._onCollapseToggled, 650);\n    } else if (isDropping === false && this._expandTimeout) {\n      clearTimeout(this._expandTimeout);\n      this._expandTimeout = null;\n    }\n  };\n\n  _onDrop = (event) => {\n    this._runCallback('onDrop', event);\n  };\n\n  _onCollapseToggled = () => {\n    this._runCallback('onCollapseToggled');\n  };\n\n  _onClick = (event) => {\n    event.preventDefault();\n    this._runCallback('onSelect');\n  };\n\n  _onDelete = () => {\n    this._runCallback('onDelete');\n  };\n\n  _onEdited = (value) => {\n    this._runCallback('onEdited', value);\n  };\n\n  _onEdit = () => {\n    if (this.props.item.onEdited) {\n      this.setState({editing: true});\n    }\n  };\n\n  _onInputFocus = (event) => {\n    const input = event.target;\n    input.selectionStart = input.selectionEnd = input.value.length;\n  };\n\n  _onInputBlur = (event) => {\n    this._clearEditingState(event);\n  };\n\n  _onInputKeyDown = (event) => {\n    if (event.key === 'Escape') {\n      this._clearEditingState(event);\n    }\n    if (_.includes(['Enter', 'Return'], event.key)) {\n      this._onEdited(event.target.value);\n      this._clearEditingState(event);\n    }\n  };\n\n  _onShowContextMenu = (event) => {\n    event.stopPropagation()\n    const item = this.props.item;\n    const contextMenuLabel = item.contextMenuLabel || item.name\n    const {remote} = require('electron');\n    const {Menu, MenuItem} = remote;\n    const menu = new Menu();\n\n    if (this.props.item.onEdited) {\n      menu.append(new MenuItem({\n        label: `Rename ${contextMenuLabel}`,\n        click: this._onEdit,\n      }));\n    }\n\n    if (this.props.item.onDelete) {\n      menu.append(new MenuItem({\n        label: `Delete ${contextMenuLabel}`,\n        click: this._onDelete,\n      }));\n    }\n    menu.popup(remote.getCurrentWindow());\n  };\n\n\n  // Renderers\n\n  _renderCount(item = this.props.item) {\n    if (!item.count) {\n      return <span />;\n    }\n    const className = classnames({\n      'item-count-box': true,\n      'alt-count': item.counterStyle === CounterStyles.Alt,\n    });\n    return <div className={className}>{item.count}</div>;\n  }\n\n  _renderIcon(item = this.props.item) {\n    return (\n      <div className=\"icon\">\n        <RetinaImg\n          name={item.iconName}\n          fallback={'folder.png'}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </div>\n    );\n  }\n\n  _renderItemContent(item = this.props.item, state = this.state) {\n    if (state.editing) {\n      const placeholder = item.inputPlaceholder || '';\n      return (\n        <input\n          autoFocus\n          type=\"text\"\n          tabIndex=\"1\"\n          className=\"item-input\"\n          placeholder={placeholder}\n          defaultValue={item.name}\n          onBlur={this._onInputBlur}\n          onFocus={this._onInputFocus}\n          onKeyDown={this._onInputKeyDown}\n        />\n      );\n    }\n    return <div className=\"name\" title={item.name}>{item.name}</div>;\n  }\n\n  _renderItem(item = this.props.item, state = this.state) {\n    const containerClass = classnames({\n      item: true,\n      selected: item.selected,\n      editing: state.editing,\n      [item.className]: item.className,\n    });\n\n    return (\n      <DropZone\n        id={item.id}\n        className={containerClass}\n        onDrop={this._onDrop}\n        onClick={this._onClick}\n        onDoubleClick={this._onEdit}\n        shouldAcceptDrop={this._shouldAcceptDrop}\n        onDragStateChange={this._onDragStateChange}\n      >\n        {this._renderCount()}\n        {this._renderIcon()}\n        {this._renderItemContent()}\n      </DropZone>\n    );\n  }\n\n  _renderChildren(item = this.props.item) {\n    if (item.children.length > 0 && !item.collapsed) {\n      return (\n        <section className=\"item-children\" key={`${item.id}-children`}>\n          {item.children.map(\n            child => <OutlineViewItem key={child.id} item={child} />\n          )}\n        </section>\n      );\n    }\n    return <span />;\n  }\n\n  render() {\n    const item = this.props.item;\n    const containerClasses = classnames({\n      'item-container': true,\n      'dropping': this.state.isDropping,\n    })\n    return (\n      <div>\n        <span className={containerClasses}>\n          <DisclosureTriangle\n            collapsed={item.collapsed}\n            visible={item.children.length > 0}\n            onCollapseToggled={this._onCollapseToggled}\n          />\n          {this._renderItem()}\n        </span>\n        {this._renderChildren()}\n      </div>\n    );\n  }\n}\n\nexport default OutlineViewItem;\n"
  },
  {
    "path": "packages/client-app/src/components/outline-view.jsx",
    "content": "import {Utils} from 'nylas-exports'\nimport React, {Component, PropTypes} from 'react';\nimport DropZone from './drop-zone';\nimport RetinaImg from './retina-img';\nimport OutlineViewItem from './outline-view-item';\n\n\n/*\n * Renders a section that contains a list of {@link OutlineViewItem}s. These items can\n * be arbitrarily nested. See docs for {@link OutlineViewItem}.\n * An OutlineView behaves like a controlled React component, with callbacks for\n * collapsing and creating items, and respective props for their value. Is it up\n * to the parent component to determine the state of the OutlineView.\n *\n * This component resembles OS X's default OutlineView or Sourcelist\n *\n * OutlineView supports:\n * - Collapsing and uncollapsing\n * - Adding new items to the outline view\n *\n * @param {object} props - props for OutlineView\n * @param {string} props.title - Title to display\n * @param {string} props.iconName - Icon name to use when displaying input to\n * add a new item. See {@link RetinaImg} for further reference.\n * @param {array} props.items - Array of strings or numbers to display as {@link\n * OutlineViewItem}s\n * @param {boolean} props.collapsed - Whether the OutlineView is collapsed or\n * not\n * @param {props.onItemCreated} props.onItemCreated\n * @param {props.onCollapseToggled} props.onCollapseToggled\n * @class OutlineView\n */\nclass OutlineView extends Component {\n  static displayName = 'OutlineView';\n\n  /*\n   * If provided, this function will be called when an item has been created.\n   * @callback props.onItemCreated\n   * @param {string} value - The value for the created item\n   */\n  /*\n   * If provided, this function will be called when the user clicks the action\n   * to collapse or uncollapse the OutlineView\n   * @callback props.onCollapseToggled\n   * @param {object} props - The entire props object for this OutlineView\n   */\n  static propTypes = {\n    title: PropTypes.string,\n    iconName: PropTypes.string,\n    items: PropTypes.array,\n    collapsed: PropTypes.bool,\n    onItemCreated: PropTypes.func,\n    onCollapseToggled: PropTypes.func,\n  };\n\n  static defaultProps = {\n    title: '',\n    items: [],\n  };\n\n  state = {\n    showCreateInput: false,\n  };\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) ||\n      !Utils.isEqualReact(nextState, this.state);\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this._expandTimeout);\n  }\n\n  // Handlers\n\n  _onCreateButtonMouseDown = () => {\n    this._clickingCreateButton = true;\n  };\n\n  _onCreateButtonClicked = () => {\n    this._clickingCreateButton = false;\n    this.setState({showCreateInput: !this.state.showCreateInput});\n  };\n\n  _onCollapseToggled = () => {\n    if (this.props.onCollapseToggled) {\n      this.props.onCollapseToggled(this.props);\n    }\n  };\n\n  _onDragStateChange = ({isDropping}) => {\n    if (this.props.collapsed && !this._expandTimeout && isDropping) {\n      this._expandTimeout = setTimeout(this._onCollapseToggled, 650);\n    } else if (this._expandTimeout && !isDropping) {\n      clearTimeout(this._expandTimeout);\n      this._expandTimeout = null;\n    }\n  }\n\n  _onItemCreated = (item, value) => {\n    this.setState({showCreateInput: false});\n    this.props.onItemCreated(value)\n  };\n\n  _onCreateInputCleared = () => {\n    if (!this._clickingCreateButton) {\n      this.setState({showCreateInput: false});\n    }\n  };\n\n\n  // Renderers\n\n  _renderCreateInput(props = this.props) {\n    const item = {\n      id: `add-item-${props.title}`,\n      name: '',\n      children: [],\n      editing: true,\n      iconName: props.iconName,\n      onEdited: this._onItemCreated,\n      inputPlaceholder: 'Create new item',\n      onInputCleared: this._onCreateInputCleared,\n    }\n    return <OutlineViewItem item={item} />;\n  }\n\n  _renderCreateButton() {\n    return (\n      <span\n        className=\"add-item-button\"\n        onMouseDown={this._onCreateButtonMouseDown}\n        onMouseUp={this._onCreateButtonClicked}\n      >\n        <RetinaImg\n          url=\"nylas://account-sidebar/assets/icon-sidebar-addcategory@2x.png\"\n          style={{height: 15, width: 14}}\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n      </span>\n    );\n  }\n\n  _renderHeading(allowCreate, collapsed, collapsible) {\n    const collapseLabel = collapsed ? 'Show' : 'Hide';\n    return (\n      <DropZone\n        className=\"heading\"\n        onDrop={() => true}\n        onDragStateChange={this._onDragStateChange}\n        shouldAcceptDrop={() => true}\n      >\n        <span className=\"text\" title={this.props.title}>\n          {this.props.title}\n        </span>\n        {allowCreate ? this._renderCreateButton() : null}\n        {collapsible ?\n          <span\n            className=\"collapse-button\"\n            onClick={this._onCollapseToggled}\n          >\n            {collapseLabel}\n          </span>\n          : null\n        }\n      </DropZone>\n    );\n  }\n\n  _renderItems() {\n    return this.props.items.map(item => (\n      <OutlineViewItem key={item.id} item={item} />\n    ));\n  }\n\n  _renderOutline(allowCreate, collapsed) {\n    if (collapsed) {\n      return <span />;\n    }\n\n    const showInput = allowCreate && this.state.showCreateInput;\n    return (\n      <div>\n        {showInput ? this._renderCreateInput() : null}\n        {this._renderItems()}\n      </div>\n    );\n  }\n\n  render() {\n    const collapsible = this.props.onCollapseToggled;\n    const collapsed = this.props.collapsed;\n    const allowCreate = this.props.onItemCreated != null && !collapsed;\n\n    return (\n      <section className=\"nylas-outline-view\">\n        {this._renderHeading(allowCreate, collapsed, collapsible)}\n        {this._renderOutline(allowCreate, collapsed)}\n      </section>\n    );\n  }\n}\n\nexport default OutlineView;\n"
  },
  {
    "path": "packages/client-app/src/components/overlaid-components/anchor-constants.es6",
    "content": "\n// The \"Anchor\" is the element we place in the actual contenteditable\n// component to keep track of where we should overlay the main\n// component.\nexport const ANCHOR_CLASS = \"n1-overlaid-component-anchor-container\";\n\nexport const IMG_SRC = \"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7\";\n"
  },
  {
    "path": "packages/client-app/src/components/overlaid-components/custom-contenteditable-components.es6",
    "content": "class CustomContenteditableComponents {\n  constructor() {\n    this._components = {}\n  }\n\n  get(componentKey) {\n    return this._components[componentKey]\n  }\n\n  register(componentKey, component) {\n    if (!component) {\n      throw new Error(\"Must register a component.\")\n    }\n    this._components[componentKey] = component\n  }\n\n  unregister(componentKey) {\n    delete this._components[componentKey]\n  }\n}\n\nconst store = new CustomContenteditableComponents();\nexport default store\n"
  },
  {
    "path": "packages/client-app/src/components/overlaid-components/overlaid-components.jsx",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport ReactDOM from 'react-dom'\nimport Utils from '../../flux/models/utils'\nimport CustomContenteditableComponents from './custom-contenteditable-components'\nimport {IgnoreMutationClassName} from '../contenteditable/contenteditable'\nimport {ANCHOR_CLASS, IMG_SRC} from './anchor-constants'\n\nconst MUTATION_CONFIG = {\n  subtree: true,\n  childList: true,\n  attributes: true,\n  characterData: true,\n  attributeOldValue: true,\n  characterDataOldValue: true,\n}\n\nexport default class OverlaidComponents extends React.Component {\n  static displayName = \"OverlaidComponents\";\n\n  static propTypes = {\n    children: React.PropTypes.node,\n    className: React.PropTypes.string,\n    exposedProps: React.PropTypes.object,\n  }\n\n  static defaultProps = {\n    children: false,\n    exposedProps: {},\n  }\n\n  static WRAP_CLASS = \"n1-overlaid-component-wrap\";\n\n  static propsToDOMAttr(props) {\n    return JSON.stringify(props).replace(/\"/g, \"&quot;\")\n  }\n\n  static buildAnchorTag(componentKey, props = {}, existingId = null, style = \"\") {\n    const overlayId = existingId || Utils.generateTempId()\n    let className = `${IgnoreMutationClassName} ${ANCHOR_CLASS}`\n    if (props.className) {\n      className = `${className} ${props.className}`\n    }\n    const propsStr = OverlaidComponents.propsToDOMAttr(props);\n    return {\n      anchorId: overlayId,\n      anchorTag:\n        `<img class=\"${className}\" src=\"${IMG_SRC}\" data-overlay-id=\"${overlayId}\" ` +\n        `data-component-props=\"${propsStr}\" data-component-key=\"${componentKey}\" ` +\n        `style=\"${style}\">`,\n    }\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      anchorRectIds: [],\n      previewMode: false,\n    }\n    this._anchorData = {}\n    this._overlayData = {}\n    this.observeOverlays = new MutationObserver(this._updateAnchors)\n    this.observeAnchors = new MutationObserver(this._updateOverlays)\n  }\n\n  componentDidMount() {\n    this._updateAnchors();\n    this._updateOverlays();\n    this._setupMutationObservers()\n  }\n\n  componentWillUpdate() {\n    this._teardownMutationObservers()\n  }\n\n  componentDidUpdate() {\n    this._updateAnchors();\n    this._updateOverlays();\n    this._setupMutationObservers();\n  }\n\n  componentWillUnmount() {\n    this._teardownMutationObservers()\n  }\n\n  _setupMutationObservers() {\n    this.observeOverlays.disconnect()\n    this.observeOverlays.observe(\n      ReactDOM.findDOMNode(this.refs.overlaidComponents),\n      MUTATION_CONFIG\n    )\n    this.observeAnchors.disconnect()\n    this.observeAnchors.observe(\n      ReactDOM.findDOMNode(this.refs.anchorContainer),\n      MUTATION_CONFIG\n    )\n  }\n\n  _teardownMutationObservers() {\n    this.observeOverlays.disconnect()\n    this.observeAnchors.disconnect()\n  }\n\n  _updateAnchors = () => {\n    this._teardownMutationObservers();\n    const lastRects = _.clone(this._overlayData);\n    this._overlayData = this._dataFromNodes({\n      root: this.refs.overlaidComponents,\n      selector: `.${OverlaidComponents.WRAP_CLASS}`,\n      dataFields: [],\n    })\n    if (_.isEqual(lastRects, this._overlayData)) { return }\n    this._adjustNodes(\"anchorContainer\", this._overlayData, [\"width\", \"height\"]);\n    this._setupMutationObservers()\n  }\n\n  _updateOverlays = () => {\n    this._teardownMutationObservers();\n    const lastRects = _.clone(this._anchorData)\n    this._anchorData = this._dataFromNodes({\n      root: this.refs.anchorContainer,\n      selector: `.${ANCHOR_CLASS}`,\n      dataFields: [\"componentProps\", \"componentKey\"],\n    })\n    if (_.isEqual(lastRects, this._anchorData)) { return }\n    this._adjustNodes(\"overlaidComponents\", this._anchorData, [\"top\", \"left\"]);\n    this._setupMutationObservers();\n    if (!_.isEqual(this.state.anchorRectIds, Object.keys(this._anchorData))) {\n      this.setState({anchorRectIds: Object.keys(this._anchorData)})\n    }\n  }\n\n  _adjustNodes(ref, rects, dims) {\n    const root = ReactDOM.findDOMNode(this.refs[ref]);\n    for (const id of Object.keys(rects)) {\n      const node = root.querySelector(`[data-overlay-id=${id}]`);\n      if (!node) { continue }\n      for (const dim of dims) {\n        const dimVal = rects[id][dim];\n        node.style[dim] = `${dimVal}px`\n      }\n    }\n  }\n\n  _dataFromNodes({root, selector, dataFields}) {\n    const updatedRegistry = {}\n    const nodes = Array.from(root.querySelectorAll(selector));\n    if (nodes.length === 0) { return updatedRegistry }\n    const rootRect = root.getBoundingClientRect();\n    for (const node of nodes) {\n      const id = node.dataset.overlayId;\n      const rawRect = node.getBoundingClientRect();\n      const adjustedRect = {\n        left: rawRect.left - rootRect.left,\n        top: rawRect.top - rootRect.top,\n        width: rawRect.width,\n        height: rawRect.height,\n      }\n      updatedRegistry[id] = adjustedRect;\n      for (const field of dataFields) {\n        updatedRegistry[id][field] = node.dataset[field]\n      }\n    }\n    return updatedRegistry\n  }\n\n  _onTogglePreview = () => {\n    this.setState({previewMode: !this.state.previewMode})\n  }\n\n  _renderPreviewToggle() {\n    let msg = \"Preview as recipient\";\n    if (this.state.previewMode) {\n      msg = \"Return to editor\"\n    }\n    return (\n      <a className=\"toggle-preview\" onClick={this._onTogglePreview} >\n        {msg}\n      </a>\n    )\n  }\n\n  _renderOverlaidComponents() {\n    const els = [];\n    let previewToggleVisible = false;\n\n    for (const id of this.state.anchorRectIds) {\n      const data = this._anchorData[id];\n      if (!data) { throw new Error(`No mounted rect for ${id}`) }\n\n      const style = {left: data.left, top: data.top, position: 'absolute'}\n      const component = CustomContenteditableComponents.get(data.componentKey);\n\n      if (!component) {\n        // It's possible that the plugin with that will register this\n        // componentKey hasn't loaded yet. This is common in popout\n        // composers where 3rd party plugins are loaded later.\n        continue\n      }\n\n      const supportsPreviewWithinEditor = component.supportsPreviewWithinEditor !== false;\n      const props = Object.assign({},\n        this.props.exposedProps,\n        JSON.parse(data.componentProps),\n        {isPreview: supportsPreviewWithinEditor && this.state.previewMode}\n      );\n\n      previewToggleVisible = previewToggleVisible || supportsPreviewWithinEditor;\n\n      els.push(\n        <span\n          key={id}\n          className={OverlaidComponents.WRAP_CLASS}\n          style={style}\n          data-overlay-id={id}\n        >\n          {React.createElement(component, props)}\n        </span>\n      )\n    }\n\n    const toggle = (previewToggleVisible) ? this._renderPreviewToggle() : false;\n\n    return (\n      <div ref=\"overlaidComponents\" className=\"overlaid-components\">\n        {toggle}\n        {els}\n      </div>\n    )\n  }\n\n  render() {\n    const {className} = this.props\n    return (\n      <div className={`overlaid-components-wrap ${className || ''}`} style={{position: \"relative\"}}>\n        <div className=\"anchor-container\" ref=\"anchorContainer\">\n          {this.props.children}\n        </div>\n        {this._renderOverlaidComponents()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/overlaid-components/overlaid-composer-extension.es6",
    "content": "import React from 'react'\nimport ReactDOMServer from 'react-dom/server'\nimport ComposerExtension from '../../extensions/composer-extension'\nimport OverlaidComponents from './overlaid-components'\nimport CustomContenteditableComponents from './custom-contenteditable-components'\n\n// In this code, \"anchor\" refers to the \"img\" tag used in the draft while the\n// user is editing it.\n\n// <overlay> is used when the draft is sent.\nexport default class OverlaidComposerExtension extends ComposerExtension {\n\n  static applyTransformsForSending({draftBodyRootNode, draft}) {\n    const overlayImgEls = Array.from(draftBodyRootNode.querySelectorAll('img[data-overlay-id]'));\n    for (const imgEl of overlayImgEls) {\n      const Component = CustomContenteditableComponents.get(imgEl.dataset.componentKey);\n      if (!Component) {\n        continue;\n      }\n      const props = Object.assign({draft, isPreview: true}, JSON.parse(imgEl.dataset.componentProps));\n      const reactElement = React.createElement(Component, props);\n\n      const overlayEl = document.createElement('overlay');\n      overlayEl.innerHTML = ReactDOMServer.renderToStaticMarkup(reactElement);\n      Object.assign(overlayEl.dataset, imgEl.dataset);\n\n      imgEl.parentNode.replaceChild(overlayEl, imgEl);\n    }\n  }\n\n  static unapplyTransformsForSending({draftBodyRootNode}) {\n    const overlayEls = Array.from(draftBodyRootNode.querySelectorAll('overlay[data-overlay-id]'));\n    for (const overlayEl of overlayEls) {\n      const {componentKey, componentProps, overlayId, style} = overlayEl.dataset;\n      const {anchorTag} = OverlaidComponents.buildAnchorTag(componentKey, JSON.parse(componentProps), overlayId, style);\n      const anchorFragment = document.createRange().createContextualFragment(anchorTag);\n      overlayEl.parentNode.replaceChild(anchorFragment, overlayEl);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/participants-text-field.jsx",
    "content": "import React from 'react';\nimport _ from 'underscore';\n\nimport {remote, clipboard} from 'electron';\nimport {Utils, Contact, ContactStore, RegExpUtils} from 'nylas-exports';\nimport {TokenizingTextField, Menu, InjectedComponent, InjectedComponentSet} from 'nylas-component-kit';\n\nconst TokenRenderer = (props) => {\n  const {email, name} = props.token\n  let chipText = email;\n  if (name && (name.length > 0) && (name !== email)) {\n    chipText = name;\n  }\n  return (\n    <div className=\"participant\">\n      <InjectedComponentSet\n        matching={{role: \"Composer:RecipientChip\"}}\n        exposedProps={{contact: props.token, collapsed: false}}\n        direction=\"row\"\n        inline\n      />\n      <span className=\"participant-primary\">{chipText}</span>\n    </div>\n  );\n};\n\nTokenRenderer.propTypes = {\n  token: React.PropTypes.object,\n};\n\nexport default class ParticipantsTextField extends React.Component {\n  static displayName = 'ParticipantsTextField';\n\n  static propTypes = {\n    // The name of the field, used for both display purposes and also\n    // to modify the `participants` provided.\n    field: React.PropTypes.string,\n\n    // An object containing arrays of participants. Typically, this is\n    // {to: [], cc: [], bcc: []}. Each ParticipantsTextField needs all of\n    // the values, because adding an element to one field may remove it\n    // from another.\n    participants: React.PropTypes.object.isRequired,\n\n    // The function to call with an updated `participants` object when\n    // changes are made.\n    change: React.PropTypes.func.isRequired,\n\n    className: React.PropTypes.string,\n\n    onEmptied: React.PropTypes.func,\n\n    onFocus: React.PropTypes.func,\n\n    draft: React.PropTypes.object,\n\n    session: React.PropTypes.object,\n  }\n\n  static defaultProps = {\n    visible: true,\n  }\n\n  shouldComponentUpdate(nextProps, nextState) {\n    return !Utils.isEqualReact(nextProps, this.props) || !Utils.isEqualReact(nextState, this.state);\n  }\n\n  // Public. Can be called by any component that has a ref to this one to\n  // focus the input field.\n  focus = () => {\n    this.refs.textField.focus();\n  }\n\n  _completionNode = (p) => {\n    const CustomComponent = p.customComponent\n    if (CustomComponent) return (<CustomComponent token={p} />)\n    return (\n      <Menu.NameEmailItem name={p.name} email={p.email} key={p.id} />\n    );\n  }\n\n  _tokensForString = (string, options = {}) => {\n    // If the input is a string, parse out email addresses and build\n    // an array of contact objects. For each email address wrapped in\n    // parentheses, look for a preceding name, if one exists.\n    if (string.length === 0) {\n      return Promise.resolve([]);\n    }\n\n    return ContactStore.parseContactsInString(string, options).then((contacts) => {\n      if (contacts.length > 0) {\n        return Promise.resolve(contacts);\n      }\n      // If no contacts are returned, treat the entire string as a single\n      // (malformed) contact object.\n      return [new Contact({email: string, name: null})];\n    });\n  }\n\n  _remove = (values) => {\n    const field = this.props.field;\n    const updates = {};\n    updates[field] = _.reject(this.props.participants[field], (p) =>\n      values.includes(p.email) || values.map(o => o.email).includes(p.email)\n    );\n    this.props.change(updates);\n  }\n\n  _edit = (token, replacementString) => {\n    const field = this.props.field;\n    const tokenIndex = this.props.participants[field].indexOf(token);\n\n    this._tokensForString(replacementString).then((replacements) => {\n      const updates = {};\n      updates[field] = [].concat(this.props.participants[field]);\n      updates[field].splice(tokenIndex, 1, ...replacements);\n      this.props.change(updates);\n    });\n  }\n\n  _add = (values, options = {}) => {\n    // It's important we return here (as opposed to ignoring the\n    // `this.props.change` callback) because this method is asynchronous.\n\n    // The `tokensPromise` may be formed with an empty draft, but resolved\n    // after a draft was prepared. This would cause the bad data to be\n    // propagated.\n\n    // If the input is a string, parse out email addresses and build\n    // an array of contact objects. For each email address wrapped in\n    // parentheses, look for a preceding name, if one exists.\n    let tokensPromise = null;\n    if (_.isString(values)) {\n      tokensPromise = this._tokensForString(values, options);\n    } else {\n      tokensPromise = Promise.resolve(values);\n    }\n\n    tokensPromise.then((tokens) => {\n      // Safety check: remove anything from the incoming tokens that isn't\n      // a Contact. We should never receive anything else in the tokens array.\n      const contactTokens = tokens.filter(value => value instanceof Contact);\n\n      const updates = {}\n      for (const field of Object.keys(this.props.participants)) {\n        updates[field] = [].concat(this.props.participants[field]);\n      }\n\n      for (const token of contactTokens) {\n        // first remove the participant from all the fields. This ensures\n        // that drag and drop isn't \"drag and copy.\" and you can't have the\n        // same recipient in multiple places.\n        for (const field of Object.keys(this.props.participants)) {\n          updates[field] = _.reject(updates[field], p => p.email === token.email)\n        }\n\n        // add the participant to field\n        updates[this.props.field] = _.union(updates[this.props.field], [token]);\n      }\n\n      this.props.change(updates);\n    });\n\n    return \"\";\n  }\n\n  _onShowContextMenu = (participant) => {\n    // Warning: Menu is already initialized as Menu.cjsx!\n    const MenuClass = remote.Menu;\n    const MenuItem = remote.MenuItem;\n\n    const menu = new MenuClass();\n    menu.append(new MenuItem({\n      label: `Copy ${participant.email}`,\n      click: () => clipboard.writeText(participant.email),\n    }))\n    menu.append(new MenuItem({\n      type: 'separator',\n    }))\n    menu.append(new MenuItem({\n      label: 'Remove',\n      click: () => this._remove([participant]),\n    }))\n    menu.popup(remote.getCurrentWindow());\n  }\n\n  _onInputTrySubmit = (inputValue, completions = [], selectedItem) => {\n    if (RegExpUtils.emailRegex().test(inputValue)) {\n      return inputValue // no token default to raw value.\n    }\n    return selectedItem || completions[0] // first completion if any\n  }\n\n  _shouldBreakOnKeydown = (event) => {\n    const val = event.target.value.trim();\n    if (RegExpUtils.emailRegex().test(val) && event.key === \" \") {\n      return true\n    }\n    return [\",\", \";\"].includes(event.key)\n  }\n\n  render() {\n    const classSet = {\n      [this.props.field]: true,\n    };\n    const draftId = this.props.draft ? this.props.draft.clientId : null\n    // TODO Ahh now that this component is part of the component kit this\n    // injected region feels out of place\n    return (\n      <div className={this.props.className}>\n        <InjectedComponent\n          ref=\"textField\"\n          matching={{role: 'Composer:ParticipantsTextField'}}\n          fallback={TokenizingTextField}\n          requiredMethods={['focus']}\n          exposedProps={{\n            tokens: this.props.participants[this.props.field],\n            tokenKey: (p) => p.email,\n            tokenIsValid: (p) => ContactStore.isValidContact(p),\n            tokenRenderer: TokenRenderer,\n            onRequestCompletions: (input) => ContactStore.searchContacts(input),\n            shouldBreakOnKeydown: this._shouldBreakOnKeydown,\n            onInputTrySubmit: this._onInputTrySubmit,\n            completionNode: this._completionNode,\n            onAdd: this._add,\n            onRemove: this._remove,\n            onEdit: this._edit,\n            onEmptied: this.props.onEmptied,\n            onFocus: this.props.onFocus,\n            onTokenAction: this._onShowContextMenu,\n            menuClassSet: classSet,\n            menuPrompt: this.props.field,\n            field: this.props.field,\n            draft: this.props.draft,\n            draftClientId: draftId,\n            session: this.props.session,\n          }}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/resizable-region.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\n_ = require 'underscore'\n{Utils,\n Actions,\n ComponentRegistry,\n PriorityUICoordinator} = require \"nylas-exports\"\n\nResizableHandle =\n  Top:\n    axis: 'vertical'\n    className: 'flexbox-handle-vertical flexbox-handle-top'\n    transform: (state, props, event) ->\n      'height': Math.min(props.maxHeight ? 10000, Math.max(props.minHeight ? 0, state.bcr.bottom - event.pageY))\n  Bottom:\n    axis: 'vertical'\n    className: 'flexbox-handle-vertical flexbox-handle-bottom'\n    transform: (state, props, event) ->\n      'height': Math.min(props.maxHeight ? 10000, Math.max(props.minHeight ? 0, event.pageY - state.bcr.top))\n  Left:\n    axis: 'horizontal'\n    className: 'flexbox-handle-horizontal flexbox-handle-left'\n    transform: (state, props, event) ->\n      'width': Math.min(props.maxWidth ? 10000, Math.max(props.minWidth ? 0, state.bcr.right - event.pageX))\n  Right:\n    axis: 'horizontal'\n    className: 'flexbox-handle-horizontal flexbox-handle-right'\n    transform: (state, props, event) ->\n      'width': Math.min(props.maxWidth ? 10000, Math.max(props.minWidth ? 0, event.pageX - state.bcr.left))\n\n###\nPublic: ResizableRegion wraps it's `children` in a div with a fixed width or height, and a\ndraggable edge. It is used throughout N1 to implement resizable columns, trays, etc.\n\nSection: Component Kit\n###\nclass ResizableRegion extends React.Component\n  @displayName = 'ResizableRegion'\n\n  ###\n  Public: React `props` supported by ResizableRegion:\n\n   - `handle` Provide a {ResizableHandle} to indicate which edge of the\n     region should be draggable.\n   - `onResize` A {Function} that will be called continuously as the region is resized.\n   - `initialWidth` (optional) Initial width, if the handle indicates a horizontal resizing axis.\n   - `minWidth` (optional) Minimum width, if the handle indicates a horizontal resizing axis.\n   - `maxWidth` (optional) Maximum width, if the handle indicates a horizontal resizing axis.\n   - `initialHeight` (optional) Initial height, if the handle indicates a vertical resizing axis.\n   - `minHeight` (optional) Minimum height, if the handle indicates a vertical resizing axis.\n   - `maxHeight` (optional) Maximum height, if the handle indicates a vertical resizing axis.\n  ###\n  @propTypes =\n    handle: React.PropTypes.object.isRequired\n    onResize: React.PropTypes.func\n\n    initialWidth: React.PropTypes.number\n    minWidth: React.PropTypes.number\n    maxWidth: React.PropTypes.number\n\n    initialHeight: React.PropTypes.number\n    minHeight: React.PropTypes.number\n    maxHeight: React.PropTypes.number\n\n    style: React.PropTypes.object\n\n  constructor: (@props = {}) ->\n    @props.handle ?= ResizableHandle.Right\n    @state =\n      dragging: false\n      width: @props.initialWidth\n      height: @props.initialHeight\n\n  render: =>\n    if @props.handle.axis is 'horizontal'\n      containerStyle = _.extend {}, @props.style,\n        'minWidth': @props.minWidth\n        'maxWidth': @props.maxWidth\n        'position': 'relative'\n\n      if @state.width?\n        containerStyle.width = @state.width\n      else\n        containerStyle.flex = 1\n\n    else\n      containerStyle = _.extend {}, @props.style,\n        'minHeight': @props.minHeight\n        'maxHeight': @props.maxHeight\n        'position': 'relative'\n        'width': '100%'\n\n      if @state.height?\n        containerStyle.height = @state.height\n      else\n        containerStyle.flex = 1\n\n    otherProps = Utils.fastOmit(@props, Object.keys(@constructor.propTypes))\n\n    <div style={containerStyle} {...otherProps}>\n      {@props.children}\n      <div className={@props.handle.className}\n           onMouseDown={@_mouseDown}><div></div>\n      </div>\n    </div>\n\n  componentDidUpdate: (lastProps, lastState) =>\n    if lastState.dragging and not @state.dragging\n      document.removeEventListener('mousemove', @_mouseMove)\n      document.removeEventListener('mouseup', @_mouseUp)\n    else if not lastState.dragging and @state.dragging\n      document.addEventListener('mousemove', @_mouseMove)\n      document.addEventListener('mouseup', @_mouseUp)\n\n  componentWillReceiveProps: (nextProps) =>\n    if nextProps.handle.axis is 'vertical' and nextProps.initialHeight != @props.initialHeight\n      @setState(height: nextProps.initialHeight)\n    if nextProps.handle.axis is 'horizontal' and nextProps.initialWidth != @props.initialWidth\n      @setState(width: nextProps.initialWidth)\n\n  componentWillUnmount: =>\n    PriorityUICoordinator.endPriorityTask(@_taskId) if @_taskId\n    @_taskId = null\n\n  _mouseDown: (event) =>\n    return if event.button != 0\n    bcr = ReactDOM.findDOMNode(@).getBoundingClientRect()\n    @setState\n      dragging: true\n      bcr: bcr\n    event.stopPropagation()\n    event.preventDefault()\n\n    PriorityUICoordinator.endPriorityTask(@_taskId) if @_taskId\n    @_taskId = PriorityUICoordinator.beginPriorityTask()\n\n  _mouseUp: (event) =>\n    return if event.button != 0\n    @setState\n      dragging: false\n    @props.onResize(@state.height ? @state.width) if @props.onResize\n    event.stopPropagation()\n    event.preventDefault()\n\n    PriorityUICoordinator.endPriorityTask(@_taskId)\n    @_taskId = null\n\n  _mouseMove: (event) =>\n    return if !@state.dragging\n    @setState @props.handle.transform(@state, @props, event)\n    @props.onResize(@state.height ? @state.width) if @props.onResize\n    event.stopPropagation()\n    event.preventDefault()\n\nResizableRegion.Handle = ResizableHandle\n\nmodule.exports = ResizableRegion\n"
  },
  {
    "path": "packages/client-app/src/components/retina-img.jsx",
    "content": "import _ from 'underscore';\nimport React from 'react';\n\nimport {Utils} from \"nylas-exports\";\n\nconst StylesImpactedByZoom = [\n  'top',\n  'left',\n  'right',\n  'bottom',\n  'paddingTop',\n  'paddingLeft',\n  'paddingRight',\n  'paddingBottom',\n  'marginTop',\n  'marginBottom',\n  'marginLeft',\n  'marginRight',\n];\n\nconst Mode = {\n  ContentPreserve: 'original',\n  ContentLight: 'light',\n  ContentDark: 'dark',\n  ContentIsMask: 'mask',\n}\n\n/*\nPublic: RetinaImg wraps the DOM's standard `<img`> tag and implements a `UIImage` style\ninterface. Rather than specifying an image `src`, RetinaImg allows you to provide\nan image name. Like UIImage on iOS, it automatically finds the best image for the current\ndisplay based on pixel density. Given `image.png`, on a Retina screen, it looks for\n`image@2x.png`, `image.png`, `image@1x.png` in that order. It uses a lookup table and caches\nimage names, so images generally resolve immediately.\n\nRetinaImg also introduces the concept of image `modes`. Specifying an image mode\nis important for theming: it describes the content of your image, allowing theme\ndevelopers to properly adjust it. The four modes are described below:\n\n- ContentPreserve: Your image contains color or should not be adjusted by any theme.\n\n- ContentLight: Your image is a grayscale image with light colors, intended to be shown\n  against a dark background. If a theme developer changes the background to be light, they\n  can safely apply CSS filters to invert or darken this image. This mode adds the\n  `content-light` CSS class to the image.\n\n- ContentDark: Your image is a grayscale image with dark colors, intended to be shown\n  against a light background. If a theme developer changes the background to be dark, they\n  can safely apply CSS filters to invert or brighten this image. This mode adds the\n  `content-dark` CSS class to the image.\n\n- ContentIsMask: This image provides alpha information only, and color should\n  be based on the `background-color` of the RetinaImg. This mode adds the\n  `content-mask` CSS class to the image, and leverages `-webkit-mask-image`.\n\n  Example: Icons displayed within buttons specify ContentIsMask, and their\n  color is declared via CSS to be the same as the button text color. Changing\n  `@text-color-subtle` in a theme changes both button text and button icons!\n\n   ```css\n   .btn-icon {\n     color: @text-color-subtle;\n     img.content-mask { background-color: @text-color-subtle; }\n   }\n   ```\n\nSection: Component Kit\n*/\nclass RetinaImg extends React.Component {\n\n  static displayName = 'RetinaImg';\n\n  /*\n  Public: React `props` supported by RetinaImg:\n\n   - `mode` (required) One of the RetinaImg.Mode constants. See above for details.\n   - `name` (optional) A {String} image name to display.\n   - `url` (optional) A {String} url of an image to display.\n      May be an http, https, or `nylas://<packagename>/<path within package>` URL.\n   - `fallback` (optional) A {String} image name to use when `name` cannot be found.\n   - `selected` (optional) Appends \"-selected\" to the end of the image name when when true\n   - `active` (optional) Appends \"-active\" to the end of the image name when when true\n   - `style` (optional) An {Object} with additional styles to apply to the image.\n   - `resourcePath` (options) Changes the default lookup location used to find the images.\n  */\n  static propTypes = {\n    mode: React.PropTypes.string.isRequired,\n    name: React.PropTypes.string,\n    url: React.PropTypes.string,\n    className: React.PropTypes.string,\n    style: React.PropTypes.object,\n    fallback: React.PropTypes.string,\n    selected: React.PropTypes.bool,\n    active: React.PropTypes.bool,\n    resourcePath: React.PropTypes.string,\n  }\n\n  static Mode = Mode;\n\n  shouldComponentUpdate = (nextProps) => {\n    return !(_.isEqual(this.props, nextProps));\n  }\n\n  _pathFor = (name) => {\n    if (!name || !_.isString(name)) return null;\n    let pathName = name;\n\n    const [basename, ext] = name.split('.');\n    if (this.props.active === true) {\n      pathName = `${basename}-active.${ext}`;\n    }\n    if (this.props.selected === true) {\n      pathName = `${basename}-selected.${ext}`\n    }\n\n    return Utils.imageNamed(pathName, this.props.resourcePath);\n  }\n\n  render() {\n    const path = this.props.url ||\n                 this._pathFor(this.props.name) ||\n                 this._pathFor(this.props.fallback) ||\n                 '';\n    const pathIsRetina = path.indexOf('@2x') > 0;\n    let className = this.props.className || '';\n\n    const style = this.props.style || {};\n    style.WebkitUserDrag = 'none';\n    style.zoom = pathIsRetina ? 0.5 : 1;\n    if (style.width) style.width /= style.zoom;\n    if (style.height) style.height /= style.zoom\n\n    if (this.props.mode === Mode.ContentIsMask) {\n      style.WebkitMaskImage = `url('${path}')`;\n      style.WebkitMaskRepeat = \"no-repeat\";\n      style.objectPosition = \"10000px\";\n      className += \" content-mask\";\n    } else if (this.props.mode === Mode.ContentDark) {\n      className += \" content-dark\";\n    } else if (this.props.mode === Mode.ContentLight) {\n      className += \" content-light\";\n    }\n\n    for (const key of Object.keys(style)) {\n      const val = style[key].toString();\n      if (StylesImpactedByZoom.indexOf(key) !== -1 && val.indexOf(\"%\") === -1) {\n        style[key] = val.replace('px', '') / style.zoom;\n      }\n    }\n\n    const otherProps = Utils.fastOmit(this.props, Object.keys(this.constructor.propTypes));\n    return (\n      <img alt={this.props.name} className={className} src={path} style={style} {...otherProps} />\n    );\n  }\n\n}\n\nexport default RetinaImg;\n"
  },
  {
    "path": "packages/client-app/src/components/scenario-editor-models.es6",
    "content": "import _ from 'underscore';\n\nexport class Comparator {\n  constructor(name, fn) {\n    this.name = name;\n    this.fn = fn;\n  }\n\n  evaluate({actual, desired}) {\n    if (actual instanceof Array) {\n      return actual.some((item) => this.fn({actual: item, desired}));\n    }\n    return this.fn({actual, desired});\n  }\n}\n\nComparator.Default = new Comparator('Default', ({actual, desired}) =>\n  _.isEqual(actual, desired)\n);\n\nconst Types = {\n  None: 'None',\n  Enum: 'Enum',\n  String: 'String',\n};\n\nexport const Comparators = {\n  String: {\n    contains: new Comparator('contains', ({actual, desired}) => {\n      if (!actual || !desired) { return false; }\n      return actual.toLowerCase().includes(desired.toLowerCase());\n    }),\n\n    doesNotContain: new Comparator('does not contain', ({actual, desired}) => {\n      if (!actual || !desired) { return false; }\n      return !actual.toLowerCase().includes(desired.toLowerCase());\n    }),\n\n    beginsWith: new Comparator('begins with', ({actual, desired}) => {\n      if (!actual || !desired) { return false; }\n      return actual.toLowerCase().startsWith(desired.toLowerCase());\n    }),\n\n    endsWith: new Comparator('ends with', ({actual, desired}) => {\n      if (!actual || !desired) { return false; }\n      return actual.toLowerCase().endsWith(desired.toLowerCase());\n    }),\n\n    equals: new Comparator('equals', ({actual, desired}) =>\n      actual === desired\n    ),\n\n    matchesExpression: new Comparator('matches expression', ({actual, desired}) => {\n      if (!actual || !desired) { return false; }\n      return new RegExp(desired, \"gi\").test(actual);\n    }),\n  },\n};\n\nexport class Template {\n  static Type = Types;\n  static Comparator = Comparator;\n  static Comparators = Comparators;\n\n  constructor(key, type, options = {}) {\n    this.key = key;\n    this.type = type;\n\n    const defaults = {\n      name: this.key,\n      values: undefined,\n      valueLabel: undefined,\n      comparators: Comparators[this.type] || {},\n    };\n\n    Object.assign(this, defaults, options);\n\n    if (!this.key) {\n      throw new Error(\"You must provide a valid key.\");\n    }\n    if (!(this.type in Types)) {\n      throw new Error(\"You must provide a valid type.\");\n    }\n    if (this.type === Types.Enum && !this.values) {\n      throw new Error(\"You must provide `values` when creating an enum.\");\n    }\n  }\n\n  createDefaultInstance() {\n    return {\n      templateKey: this.key,\n      comparatorKey: Object.keys(this.comparators)[0],\n      value: undefined,\n    };\n  }\n\n  coerceInstance(instance) {\n    instance.templateKey = this.key;\n    if (!this.comparators) {\n      instance.comparatorKey = undefined;\n    } else if (!Object.keys(this.comparators).includes(instance.comparatorKey)) {\n      instance.comparatorKey = Object.keys(this.comparators)[0];\n    }\n    return instance;\n  }\n\n  evaluate(instance, value) {\n    let comparator = this.comparators[instance.comparatorKey];\n    if (typeof comparator === 'undefined' || comparator === null) {\n      comparator = Comparator.Default;\n    }\n    return comparator.evaluate({\n      actual: value,\n      desired: instance.value,\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/scenario-editor-row.cjsx",
    "content": "React = require 'react'\n_ = require 'underscore'\nRx = require 'rx-lite'\n{RetinaImg, Flexbox} = require 'nylas-component-kit'\n{CategoryStore, Actions, Utils} = require 'nylas-exports'\n\n{Comparator, Template} = require './scenario-editor-models'\n\nSOURCE_SELECT_NULL = 'NULL'\n\nclass SourceSelect extends React.Component\n  @displayName: 'SourceSelect'\n  @propTypes:\n    value: React.PropTypes.string\n    onChange: React.PropTypes.func.isRequired\n    options: React.PropTypes.oneOfType([\n      React.PropTypes.object\n      React.PropTypes.array\n    ]).isRequired\n\n  constructor: (@props) ->\n    @state =\n      options: []\n\n  componentDidMount: =>\n    @_setupValuesSubscription()\n\n  componentWillReceiveProps: (nextProps) =>\n    @_setupValuesSubscription(nextProps)\n\n  componentWillUnmount: =>\n    @_subscription?.dispose()\n    @_subscription = null\n\n  _setupValuesSubscription: (props = @props) =>\n    @_subscription?.dispose()\n    @_subscription = null\n    if props.options instanceof Rx.Observable\n      @_subscription = props.options.subscribe (options) =>\n        @setState({options})\n    else\n      @setState(options: props.options)\n\n  render: =>\n    options = @state.options\n\n    # The React <select> component won't select the correct option if the value\n    # is null or undefined - it just leaves the selection whatever it was in the\n    # previous render. To work around this, we coerce null/undefined to SOURCE_SELECT_NULL.\n\n    <select value={@props.value || SOURCE_SELECT_NULL} onChange={@_onChange}>\n      <option key={SOURCE_SELECT_NULL} value={SOURCE_SELECT_NULL}></option>\n      { @state.options.map ({value, name}) =>\n        <option key={value} value={value}>{name}</option>\n      }\n    </select>\n\n  _onChange: (event) =>\n    value = event.target.value\n    value = null if value is SOURCE_SELECT_NULL\n    @props.onChange(target: {value})\n\nclass ScenarioEditorRow extends React.Component\n  @displayName: 'ScenarioEditorRow'\n  @propTypes:\n    instance: React.PropTypes.object.isRequired\n    removable: React.PropTypes.bool\n    templates: React.PropTypes.array.isRequired\n    onChange: React.PropTypes.func\n    onInsert: React.PropTypes.func\n    onRemove: React.PropTypes.func\n\n  constructor: (@props) ->\n\n  render: =>\n    template = _.findWhere(@props.templates, {key: @props.instance.templateKey})\n    unless template\n      return <span> Could not find template for instance key: {@props.instance.templateKey}</span>\n\n    <Flexbox direction=\"row\" className=\"well-row\">\n      <span>\n        {@_renderTemplateSelect(template)}\n        {@_renderComparator(template)}\n        <span>{template.valueLabel}</span>\n        {@_renderValue(template)}\n      </span>\n      <div style={flex: 1}></div>\n      {@_renderActions()}\n    </Flexbox>\n\n  _renderTemplateSelect: (template) =>\n    options = @props.templates.map ({key, name}) =>\n      <option value={key} key={key}>{name}</option>\n\n    <select\n      value={@props.instance.templateKey}\n      onChange={@_onChangeTemplate}>\n      {options}\n    </select>\n\n  _renderComparator: (template) =>\n    options = _.map template.comparators, ({name}, key) =>\n      <option key={key} value={key}>{name}</option>\n\n    return false unless options.length > 0\n\n    <select\n      value={@props.instance.comparatorKey}\n      onChange={@_onChangeComparator}>\n      {options}\n    </select>\n\n  _renderValue: (template) =>\n    if template.type is Template.Type.Enum\n      <SourceSelect\n        value={@props.instance.value}\n        onChange={@_onChangeValue}\n        options={template.values} />\n\n    else if template.type is Template.Type.String\n      <input\n        type=\"text\"\n        value={@props.instance.value}\n        onChange={@_onChangeValue} />\n\n    else\n      false\n\n  _renderActions: =>\n    <div className=\"actions\">\n      { if @props.removable then <div className=\"btn\" onClick={@props.onRemove}>&minus;</div> }\n      <div className=\"btn\" onClick={@props.onInsert}>+</div>\n    </div>\n\n  _onChangeValue: (event) =>\n    instance = _.clone(@props.instance)\n    instance.value = event.target.value\n    @props.onChange(instance)\n\n  _onChangeComparator: (event) =>\n    instance = _.clone(@props.instance)\n    instance.comparatorKey = event.target.value\n    @props.onChange(instance)\n\n  _onChangeTemplate: (event) =>\n    instance = _.clone(@props.instance)\n\n    existingTemplate = _.findWhere(@props.templates, key: instance.key)\n    newTemplate = _.findWhere(@props.templates, key: event.target.value)\n\n    instance = newTemplate.coerceInstance(instance)\n\n    @props.onChange(instance)\n\nmodule.exports = ScenarioEditorRow\n"
  },
  {
    "path": "packages/client-app/src/components/scenario-editor.cjsx",
    "content": "React = require 'react'\n_ = require 'underscore'\n{Comparator, Template} = require './scenario-editor-models'\nScenarioEditorRow = require './scenario-editor-row'\n{RetinaImg, Flexbox} = require 'nylas-component-kit'\n{Actions, Utils} = require 'nylas-exports'\n\n###\nThe ScenarioEditor takes an array of ScenarioTemplate objects which define the\nscenario value space. Each ScenarioTemplate defines a `key` and it's valid\n`comparators` and `values`. The ScenarioEditor gives the user the option to\ncreate and combine instances of different templates to create a scenario.\n\nFor example:\n\n  Scenario Space:\n   - ScenarioFactory(\"user-name\", \"The name of the user\")\n      + valueType: String\n      + comparators: \"contains\", \"starts with\", etc.\n    - SecnarioFactor(\"profession\", \"The profession of the user\")\n      + valueType: Enum\n      + comparators: 'is'\n\n  Scenario Value:\n    [{\n      'key': 'user-name'\n      'comparator': 'contains'\n      'value': 'Ben'\n    },{\n      'key': 'profession'\n      'comparator': 'is'\n      'value': 'Engineer'\n    }]\n###\n\nclass ScenarioEditor extends React.Component\n  @displayName: 'ScenarioEditor'\n\n  @propTypes:\n    instances: React.PropTypes.array\n    className: React.PropTypes.string\n    onChange: React.PropTypes.func\n    templates: React.PropTypes.array\n\n  @Template: Template\n  @Comparator: Comparator\n\n  constructor: (@props) ->\n    @state =\n      collapsed: true\n\n  render: =>\n    <div className={@props.className}>\n    { (@props.instances || []).map (instance, idx) =>\n      <ScenarioEditorRow\n        key={idx}\n        instance={instance}\n        removable={@props.instances.length > 1}\n        templates={@props.templates}\n        onRemove={ => @_onRemoveRule(idx) }\n        onInsert={ => @_onInsertRule(idx) }\n        onChange={ (instance) => @_onChangeRowValue(instance, idx) } />\n    }\n    </div>\n\n  _performChange: (block) =>\n    instances = JSON.parse(JSON.stringify(@props.instances))\n    block(instances)\n    @props.onChange(instances)\n\n  _onRemoveRule: (idx) =>\n    @_performChange (instances) =>\n      return if instances.length is 1\n      instances.splice(idx, 1)\n\n  _onInsertRule: (idx) =>\n    @_performChange (instances) =>\n      instances.push @props.templates[0].createDefaultInstance()\n\n  _onChangeRowValue: (newInstance, idx) =>\n    @_performChange (instances) =>\n      instances[idx] = newInstance\n\nmodule.exports = ScenarioEditor\n"
  },
  {
    "path": "packages/client-app/src/components/scroll-region.cjsx",
    "content": "_ = require 'underscore'\nReact = require 'react'\nReactDOM = require 'react-dom'\n{Utils} = require 'nylas-exports'\nclassNames = require 'classnames'\nScrollbarTicks = require('./scrollbar-ticks').default\n\nclass Scrollbar extends React.Component\n  @displayName: 'Scrollbar'\n  @propTypes:\n    scrollTooltipComponent: React.PropTypes.func\n    # A scrollbarTickProvider is any object that has the `listen` and\n    # `scrollbarTicks` method. Since ScrollRegions tend to encompass large\n    # render trees it's more efficent for the scrollbar to listen for its\n    # own state then have it passed down as new props and potentially\n    # cause re-renders of the whole scroll region. The `scrollbarTicks`\n    # method must return an array of numbers between 0 and 1 which\n    # represent the height percentages at which tick marks will be\n    # rendered.\n    scrollbarTickProvider: React.PropTypes.object\n    getScrollRegion: React.PropTypes.func\n\n  constructor: (@props) ->\n    @state =\n      totalHeight: 0\n      trackHeight: 0\n      viewportHeight: 0\n      viewportScrollTop: 0\n      dragging: false\n      scrolling: false\n      scrollbarTicks: []\n\n  componentDidMount: ->\n    if @props.scrollbarTickProvider?.listen\n      @_tickUnsub = @props.scrollbarTickProvider.listen(@_onTickProviderChange)\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  componentWillUnmount: =>\n    @_onHandleUp({preventDefault: -> })\n    @_tickUnsub?()\n\n  setStateFromScrollRegion: (state) ->\n    @setState(state)\n\n  render: ->\n    containerClasses = classNames\n      'scrollbar-track': true\n      'dragging': @state.dragging\n      'scrolling': @state.scrolling\n      'with-ticks': @state.scrollbarTicks.length > 0\n\n    tooltip = []\n    if @props.scrollTooltipComponent and @state.dragging\n      tooltip = <@props.scrollTooltipComponent viewportCenter={@state.viewportScrollTop + @state.viewportHeight / 2} totalHeight={@state.totalHeight} />\n\n    <div className={containerClasses} style={@_scrollbarWrapStyles()} onMouseEnter={@recomputeDimensions}>\n      <div className=\"scrollbar-track-inner\" ref=\"track\" onClick={@_onScrollJump}>\n        {@_renderScrollbarTicks()}\n        <div className=\"scrollbar-handle\" onMouseDown={@_onHandleDown} style={@_scrollbarHandleStyles()} ref=\"handle\" onClick={@_onHandleClick} >\n          <div className=\"tooltip\">{tooltip}</div>\n        </div>\n      </div>\n    </div>\n\n  recomputeDimensions: (options = {}) =>\n    if @props.getScrollRegion?\n      @props.getScrollRegion()._recomputeDimensions(options)\n    @_recomputeDimensions(options)\n\n  _onTickProviderChange: =>\n    if not @props.scrollbarTickProvider?.scrollbarTicks\n      throw new Error(\"The scrollbarTickProvider must implement `scrollbarTicks`\")\n    @setState scrollbarTicks: @props.scrollbarTickProvider.scrollbarTicks()\n\n  _renderScrollbarTicks: ->\n    return false unless @state.scrollbarTicks.length > 0\n    <ScrollbarTicks ticks={@state.scrollbarTicks}/>\n\n  _recomputeDimensions: ({useCachedValues}) =>\n    if not useCachedValues\n      trackNode = ReactDOM.findDOMNode(@refs.track)\n      return unless trackNode\n      trackHeight = trackNode.clientHeight\n      if trackHeight isnt @state.trackHeight\n        @setState({trackHeight})\n\n  _scrollbarHandleStyles: =>\n    handleHeight = @_getHandleHeight()\n    handleTop = (@state.viewportScrollTop / (@state.totalHeight - @state.viewportHeight)) * (@state.trackHeight - handleHeight)\n\n    position:'relative'\n    height: handleHeight || 0\n    top: handleTop || 0\n\n  _scrollbarWrapStyles: =>\n    position:'absolute'\n    top: 0\n    bottom: 0\n    right: 0\n    zIndex: 2\n    visibility: \"hidden\" if @state.totalHeight != 0 && @state.totalHeight == @state.viewportHeight\n\n  _onHandleDown: (event) =>\n    handleNode = ReactDOM.findDOMNode(@refs.handle)\n    @_trackOffset = ReactDOM.findDOMNode(@refs.track).getBoundingClientRect().top\n    @_mouseOffsetWithinHandle = event.pageY - handleNode.getBoundingClientRect().top\n    window.addEventListener(\"mousemove\", @_onHandleMove)\n    window.addEventListener(\"mouseup\", @_onHandleUp)\n    @setState(dragging: true)\n    event.preventDefault()\n\n  _onHandleMove: (event) =>\n    trackY = event.pageY - @_trackOffset - @_mouseOffsetWithinHandle\n    trackPxToViewportPx = (@state.totalHeight - @state.viewportHeight) / (@state.trackHeight - @_getHandleHeight())\n    @props.getScrollRegion().scrollTop = trackY * trackPxToViewportPx\n    event.preventDefault()\n\n  _onHandleUp: (event) =>\n    window.removeEventListener(\"mousemove\", @_onHandleMove)\n    window.removeEventListener(\"mouseup\", @_onHandleUp)\n    @setState(dragging: false)\n    event.preventDefault()\n\n  _onHandleClick: (event) =>\n    # Avoid event propogating up to track\n    event.stopPropagation()\n\n  _onScrollJump: (event) =>\n    @_trackOffset = ReactDOM.findDOMNode(@refs.track).getBoundingClientRect().top\n    @_mouseOffsetWithinHandle = @_getHandleHeight() / 2\n    @_onHandleMove(event)\n\n  _getHandleHeight: =>\n    Math.min(@state.totalHeight, Math.max(40, (@state.trackHeight / @state.totalHeight) * @state.trackHeight))\n\n\n###\nThe ScrollRegion component attaches a custom scrollbar.\n###\nclass ScrollRegion extends React.Component\n  @displayName: \"ScrollRegion\"\n\n  @propTypes:\n    onScroll: React.PropTypes.func\n    onScrollEnd: React.PropTypes.func\n    className: React.PropTypes.string\n    scrollTooltipComponent: React.PropTypes.func\n    scrollbarTickProvider: React.PropTypes.object\n    children: React.PropTypes.oneOfType([React.PropTypes.element, React.PropTypes.array])\n    getScrollbar: React.PropTypes.func\n\n  # Concept from https://developer.apple.com/library/prerelease/ios/documentation/UIKit/Reference/UITableView_Class/#//apple_ref/c/tdef/UITableViewScrollPosition\n\n  @ScrollPosition:\n    # Scroll so that the desired region is at the top of the viewport\n    Top: 'Top'\n    # Scroll so that the desired region is at the bottom of the viewport\n    Bottom: 'Bottom'\n    # Scroll so that the desired region is visible in the viewport, with the\n    # least movement possible.\n    Visible: 'Visible'\n    # Scroll so that the desired region is centered in the viewport\n    Center: 'Center'\n    # Scroll so that the desired region is centered in the viewport, only if it\n    # is currently not visible\n    CenterIfInvisible: 'CenterIfInvisible'\n\n  constructor: (@props) ->\n    @_scrollToTaskId = 0\n    @_scrollbarComponent = null\n    @state =\n      totalHeight:0\n      viewportHeight: 0\n      viewportScrollTop: 0\n      scrolling: false\n\n    Object.defineProperty(@, 'scrollTop', {\n      get: -> ReactDOM.findDOMNode(@refs.content).scrollTop\n      set: (val) -> ReactDOM.findDOMNode(@refs.content).scrollTop = val\n    })\n\n  componentDidMount: =>\n    @_mounted = true\n    @recomputeDimensions()\n\n    @_heightObserver = new MutationObserver (mutations) =>\n      recompute = false\n      mutations.forEach (mutation) ->\n        recompute ||= !mutation.oldValue or mutation.oldValue.indexOf('height:') isnt -1\n      @recomputeDimensions({useCachedValues: false}) if recompute\n\n    @_heightObserver.observe(ReactDOM.findDOMNode(@refs.content), {\n      subtree: true,\n      attributes: true,\n      attributeOldValue: true,\n      attributeFilter: ['style']\n    })\n\n  componentDidUpdate: (prevProps, prevState) =>\n    if not @state.scrolling and @props.children isnt prevProps.children\n      @recomputeDimensions()\n\n  componentWillReceiveProps: (props) =>\n    if @shouldInvalidateScrollbarComponent(props)\n      @_scrollbarComponent = null\n\n  componentWillUnmount: =>\n    @_heightObserver.disconnect()\n    @_mounted = false\n\n  shouldComponentUpdate: (newProps, newState) =>\n    # Because this component renders @props.children, it needs to update\n    # on props.children changes. Unfortunately, computing isEqual on the\n    # @props.children tree extremely expensive. Just let React's algorithm do it's work.\n    true\n\n  shouldInvalidateScrollbarComponent: (newProps) =>\n    return true if newProps.scrollTooltipComponent isnt @props.scrollTooltipComponent\n    return true if newProps.getScrollbar isnt @props.getScrollbar\n    return false\n\n  render: =>\n    containerClasses =  \"#{@props.className ? ''} \" + classNames\n      'scroll-region': true\n      'dragging': @state.dragging\n      'scrolling': @state.scrolling\n\n    if not @props.getScrollbar\n      @_scrollbarComponent ?= <Scrollbar\n        ref=\"scrollbar\"\n        scrollbarTickProvider={@props.scrollbarTickProvider}\n        scrollTooltipComponent={@props.scrollTooltipComponent}\n        getScrollRegion={@_getSelf} />\n\n    otherProps = Utils.fastOmit(@props, Object.keys(@constructor.propTypes))\n\n    <div className={containerClasses} {...otherProps}>\n      {@_scrollbarComponent}\n      <div className=\"scroll-region-content\" onScroll={@_onScroll} ref=\"content\">\n        <div className=\"scroll-region-content-inner\">\n          {@props.children}\n        </div>\n      </div>\n    </div>\n\n  # Public: Scroll to the DOM Node provided.\n  #\n  scrollTo: (node, {position, settle, done} = {}) =>\n    if node instanceof React.Component\n      node = ReactDOM.findDOMNode(node)\n    unless node instanceof Node\n      throw new Error(\"ScrollRegion.scrollTo: requires a DOM node or React element. Maybe you meant scrollToRect?\")\n    @_scroll {position, settle, done}, =>\n      node.getBoundingClientRect()\n\n  # Public: Scroll to the client rectangle provided. Note: This method expects\n  # a ClientRect or similar object with top, left, width, height relative to the\n  # window, not the scroll region. This is designed to make it easy to use with\n  # node.getBoundingClientRect()\n  scrollToRect: (rect, {position, settle, done} = {}) ->\n    if rect instanceof Node\n      throw new Error(\"ScrollRegion.scrollToRect: requires a rect. Maybe you meant scrollTo?\")\n    if not rect.top? or not rect.height?\n      throw new Error(\"ScrollRegion.scrollToRect: requires a rect with `top` and `height` attributes.\")\n    @_scroll {position, settle, done}, => rect\n\n  _scroll: ({position, settle, done}, clientRectProviderCallback) ->\n    contentNode = ReactDOM.findDOMNode(@refs.content)\n    position ?= ScrollRegion.ScrollPosition.Visible\n\n    if settle is true\n      settleFn = @_settleHeight\n    else\n      settleFn = (callback) -> callback()\n\n    @_scrollToTaskId += 1\n    taskId = @_scrollToTaskId\n\n    settleFn =>\n      # If another scroll call has been made since ours, don't do anything.\n      return done?(false) unless @_scrollToTaskId is taskId\n\n      contentClientRect = contentNode.getBoundingClientRect()\n      rect = _.clone(clientRectProviderCallback())\n\n      # For sanity's sake, convert the client rectangle we get into a rect\n      # relative to the contentRect of our scroll region.\n      rect.top = rect.top - contentClientRect.top + contentNode.scrollTop\n      rect.bottom = rect.bottom - contentClientRect.top + contentNode.scrollTop\n\n      # Also give ourselves a representation of the visible region, in the same\n      # coordinate space as `rect`\n      contentVisibleRect = _.clone(contentClientRect)\n      contentVisibleRect.top += contentNode.scrollTop\n      contentVisibleRect.bottom += contentNode.scrollTop\n\n      if position is ScrollRegion.ScrollPosition.Top\n        @scrollTop = rect.top\n      else if position is ScrollRegion.ScrollPosition.Bottom\n        @scrollTop = (rect.top + rect.height) - contentClientRect.height\n      else if position is ScrollRegion.ScrollPosition.Center\n        @scrollTop = rect.top - (contentClientRect.height - rect.height) / 2\n      else if position is ScrollRegion.ScrollPosition.CenterIfInvisible\n        if not Utils.rectVisibleInRect(rect, contentVisibleRect)\n          @scrollTop = rect.top - (contentClientRect.height - rect.height) / 2\n      else if position is ScrollRegion.ScrollPosition.Visible\n        distanceBelowBottom = (rect.top + rect.height) - (contentClientRect.height + contentNode.scrollTop)\n        distanceAboveTop = @scrollTop - rect.top\n        if distanceBelowBottom >= 0\n          @scrollTop += distanceBelowBottom\n        else if distanceAboveTop >= 0\n          @scrollTop -= distanceAboveTop\n\n      done?(true)\n\n  _settleHeight: (callback) =>\n    contentNode = ReactDOM.findDOMNode(@refs.content)\n    lastContentHeight = -1\n    scrollIfSettled = =>\n      return unless @_mounted\n      contentRect = contentNode.getBoundingClientRect()\n      if contentRect.height isnt lastContentHeight\n        lastContentHeight = contentRect.height\n      else\n        return callback()\n      window.requestAnimationFrame(scrollIfSettled)\n    scrollIfSettled()\n\n  recomputeDimensions: (options = {}) =>\n    scrollbar = @props.getScrollbar?() ? @refs.scrollbar\n    scrollbar._recomputeDimensions(options) if scrollbar\n    @_recomputeDimensions(options)\n\n  _recomputeDimensions: ({useCachedValues}) =>\n    return unless @refs.content\n    contentNode = ReactDOM.findDOMNode(@refs.content)\n    return unless contentNode\n\n    viewportScrollTop = contentNode.scrollTop\n\n    # While we're scrolling, calls to contentNode.scrollHeight / clientHeight\n    # force the browser to immediately flush any DOM changes and compute the\n    # height of the node. This hurts performance and also kind of unnecessary,\n    # since it's unlikely these values will change while scrolling.\n    if useCachedValues\n      totalHeight = @state.totalHeight ? contentNode.scrollHeight\n      trackHeight = @state.trackHeight ? contentNode.scrollHeight\n      viewportHeight = @state.viewportHeight ? contentNode.clientHeight\n    else\n      totalHeight = contentNode.scrollHeight\n      viewportHeight = contentNode.clientHeight\n\n    if @state.totalHeight != totalHeight or\n       @state.viewportHeight != viewportHeight or\n       @state.viewportScrollTop != viewportScrollTop\n      @_setSharedState({totalHeight, viewportScrollTop, viewportHeight})\n\n  _setSharedState: (state) ->\n    scrollbar = @props.getScrollbar?() ? @refs.scrollbar\n    if scrollbar\n      scrollbar.setStateFromScrollRegion(state)\n    @setState(state)\n\n  _onScroll: (event) =>\n    # onScroll events propogate, which is a bit strange. We could actually be\n    # receiving a scroll event for a textarea inside the scroll region.\n    # See Preferences > Signatures > textarea\n    return unless event.target is ReactDOM.findDOMNode(@refs.content)\n\n    if @state.scrolling\n      @recomputeDimensions({useCachedValues: true})\n    else\n      @recomputeDimensions()\n      @_setSharedState(scrolling: true)\n\n    @props.onScroll?(event)\n\n    @_onScrollEnd ?= _.debounce =>\n      @_setSharedState(scrolling: false)\n      @recomputeDimensions()\n      @props.onScrollEnd?(event)\n    , 250\n    @_onScrollEnd()\n\n  _getSelf: =>\n    @\n\n\nScrollRegion.Scrollbar = Scrollbar\n\nmodule.exports = ScrollRegion\n"
  },
  {
    "path": "packages/client-app/src/components/scrollbar-ticks.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\n\nexport default class ScrollbarTicks extends React.Component {\n  static displayName = \"ScrollbarTicks\";\n\n  static propTypes = {\n    ticks: React.PropTypes.array,\n  }\n\n  componentDidMount() {\n    this._updateTicks()\n  }\n\n  componentDidUpdate() {\n    this._updateTicks()\n  }\n\n  _updateTicks() {\n    const html = this.props.ticks.map((percentData) => {\n      let percent;\n      let className = \"\"\n      if (typeof percentData === \"number\") {\n        percent = percentData;\n      } else {\n        percent = percentData.percent;\n        className = ` ${percentData.className}`;\n      }\n      return `<div class=\"t${className}\" style=\"top: ${percent * 100}%\" />`\n    }).join(\"\")\n    ReactDOM.findDOMNode(this).innerHTML = html\n  }\n\n  render() {\n    return <div className=\"scrollbar-ticks\" />\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/search-bar.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport ReactDOM from 'react-dom'\nimport classnames from 'classnames'\nimport Menu from './menu'\nimport RetinaImg from './retina-img'\nimport KeyCommandsRegion from './key-commands-region'\n\n\nclass SearchBar extends Component {\n  static displayName = 'SearchBar';\n\n  static propTypes = {\n    className: PropTypes.string,\n    query: PropTypes.string,\n    isSearching: PropTypes.bool,\n    placeholder: PropTypes.string,\n    inputProps: PropTypes.object,\n    suggestions: PropTypes.array,\n    suggestionRenderer: PropTypes.func,\n    suggestionKey: PropTypes.func,\n    onClearSearchQuery: PropTypes.func,\n    onClearSearchSuggestions: PropTypes.func,\n    onSearchQueryChanged: PropTypes.func,\n    onSubmitSearchQuery: PropTypes.func,\n    onSelectSuggestion: PropTypes.func,\n  }\n\n  static defaultProps = {\n    query: '',\n    className: '',\n    isSearching: false,\n    inputProps: {},\n    placeholder: 'Search',\n    onSubmitSearchQuery: () => {},\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  clearAndBlur() {\n    const {onClearSearchQuery} = this.props\n    onClearSearchQuery()\n\n    const inputEl = ReactDOM.findDOMNode(this.refs.searchInput);\n    if (inputEl) {\n      inputEl.blur();\n    }\n  }\n\n  _onFocusSearch = () => {\n    ReactDOM.findDOMNode(this.refs.searchInput).focus();\n  }\n\n  _onInputKeyDown = (event) => {\n    const {key, target: {value}} = event;\n    if (value.length > 0 && key === 'Escape') {\n      this.clearAndBlur();\n    }\n  }\n\n  _onInputChange = (event) => {\n    const {onSearchQueryChanged} = this.props\n    onSearchQueryChanged(event.target.value);\n  }\n\n  _onInputBlur = () => {\n    // Don't immediately hide the menu when the text input is blurred,\n    // because the user might have clicked an item in the menu. Wait to\n    // handle the touch event, then dismiss the menu.\n    setTimeout(() => {\n      if (!this._mounted) { return; }\n      const {onClearSearchSuggestions} = this.props\n      onClearSearchSuggestions()\n    }, 150);\n  }\n\n  renderSuggestion = (item) => {\n    if (item.divider) {\n      return <Menu.Item divider={item.divider} key={item.divider} />;\n    }\n    const {suggestionRenderer} = this.props\n    if (suggestionRenderer) {\n      return suggestionRenderer(item)\n    }\n    return item.label || '';\n  }\n\n  render() {\n    const {\n      query,\n      className,\n      inputProps,\n      isSearching,\n      suggestions,\n      placeholder,\n      suggestionKey,\n      onSelectSuggestion,\n      onSubmitSearchQuery,\n      onClearSearchQuery,\n    } = this.props\n\n    const inputClass = classnames({\n      empty: query.length === 0,\n    });\n\n    const loupeImg = isSearching ? (\n      <RetinaImg\n        className=\"search-accessory search loading\"\n        name=\"inline-loading-spinner.gif\"\n        key=\"accessory\"\n        mode={RetinaImg.Mode.ContentPreserve}\n      />\n    ) : (\n      <RetinaImg\n        className=\"search-accessory search\"\n        name=\"searchloupe.png\"\n        key=\"accessory\"\n        mode={RetinaImg.Mode.ContentDark}\n        onClick={() => onSubmitSearchQuery(query)}\n      />\n    );\n\n    const headerComponents = [\n      <input\n        ref=\"searchInput\"\n        type=\"text\"\n        key=\"input\"\n        className={inputClass}\n        placeholder={placeholder}\n        value={query}\n        onBlur={this._onInputBlur}\n        onChange={this._onInputChange}\n        onKeyDown={this._onInputKeyDown}\n        {...inputProps}\n      />,\n      loupeImg,\n      <RetinaImg\n        className=\"search-accessory clear\"\n        name=\"searchclear.png\"\n        key=\"clear\"\n        mode={RetinaImg.Mode.ContentDark}\n        onClick={onClearSearchQuery}\n      />,\n    ]\n\n\n    return (\n      <KeyCommandsRegion\n        className={`nylas-search-bar ${className}`}\n        globalHandlers={{\n          'core:focus-search': this._onFocusSearch,\n        }}\n      >\n        <div>\n          <Menu\n            ref=\"menu\"\n            className={classnames({\n              'showing-query': query && query.length > 0,\n              'search-container': true,\n              'showing-suggestions': suggestions && suggestions.length > 0,\n            })}\n            headerComponents={headerComponents}\n            items={suggestions}\n            itemKey={suggestionKey}\n            itemContent={this.renderSuggestion}\n            onSelect={onSelectSuggestion}\n          />\n        </div>\n      </KeyCommandsRegion>\n    );\n  }\n}\n\nexport default SearchBar\n"
  },
  {
    "path": "packages/client-app/src/components/selectable-table.jsx",
    "content": "import _ from 'underscore'\nimport React, {Component, PropTypes} from 'react'\nimport ReactDOM from 'react-dom'\nimport classnames from 'classnames';\nimport compose from './decorators/compose';\nimport AutoFocuses from './decorators/auto-focuses';\nimport ListensToMovementKeys from './decorators/listens-to-movement-keys';\nimport Table, {TableRow, TableCell} from './table/table'\n\n\n/*\nSelectableTable component which renders a {Table} that supports selecting\ncells and rows.\n\nThe required props for SelectableTable are `tableDataSource`, `selection`,\n`onSetSelection`, `onShiftSelection`, which are of the form:\n\n```\nconst tableDataSource = new TableDataSource()\ntableDataSource.rows()\n// returns\n// [\n//   [1, 2],\n//   [3, 4],\n// ]\n\nconst selection = {rowIdx: 1, colIdx: 0, key: 'Enter'}\n\nconst onSetSelection = ({rowIdx, colIdx, key}) => { ... }\n\nconst onShiftSelection = ({row, col, key}) => { ... }\n```\n\nSelectableTable is a controlled component, which means that it does not\nmanage any internal state. In order for the selection to be updated, the\nfunctions `onShiftSelection` and `onSetSelection` must be provided as props,\nand must eventually trigger a re render of this Component with a new set of\nprops.\n\nThe SelectableTable Component can be extended via passing custom `RowRenderer` and\n`CellRenderer` components as props, in the same manner that the {Table}\ncomponent can be extended. See the docs for {Table} for more details\n\nSelectableTable takes the exact same set of props as {Table}, plus additional\nprops documented below. For {Table} props, see the docs for {Table}\n\n@param {object} props - props for SelectableTable\n@param {string} props.className - CSS class to be applied to component\n@param {object} props.selection - Object representing selection indices, plus\nthe key with which the selection was established. It * is of the form {row,\ncol, key}\n@param {props.onSetSelection} props.onSetSelection\n@param {props.onShiftSelection} props.onSetSelection\n@class SelectableTable\n\n\n\nThis function will be called when the selection needs to be set to the\nselection passed in as a parameter\n@callback props.onSetSelection\n@param {object} selection - selection object of the form {rowIdx, colIdx, key}\n@param {number} selection.rowIdx - rowIdx for selection\n@param {number} selection.colIdx - colIds for selection\n\n\n\nThis function will be called when the selection row and col indices need to\nbe shifted by a specific delta\n@callback props.onShiftSelection\n@param {object} selectionDeltas - selection object of the form {row, col, key}\n@param {number} selectionDeltas.row - number representing by how many rows to\nmove the selection. E.g. 1, -2.\n@param {number} selectionDeltas.col - number representing by how many columns to\nmove the selection. E.g. 1, -2.\n@param {string} selectionDeltas.key - string that represents the key used to\nshift the selection\n */\nexport class SelectableTableCell extends Component {\n\n  static propTypes = {\n    className: PropTypes.string,\n    tableDataSource: Table.propTypes.tableDataSource,\n    rowIdx: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),\n    colIdx: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),\n    selection: PropTypes.object,\n    onSetSelection: PropTypes.func.isRequired,\n  }\n\n  static defaultProps = {\n    className: '',\n  }\n\n  shouldComponentUpdate(nextProps) {\n    const cellValueChanged = (\n      this.props.tableDataSource.cellAt({rowIdx: this.props.rowIdx, colIdx: this.props.colIdx}) !==\n      nextProps.tableDataSource.cellAt({rowIdx: nextProps.rowIdx, colIdx: nextProps.colIdx})\n    )\n    const selectionStateChanged = (\n      this.isSelected(this.props) !== this.isSelected(nextProps)\n    )\n    return cellValueChanged || selectionStateChanged\n  }\n\n  onClickCell = () => {\n    const {selection, rowIdx, colIdx, onSetSelection} = this.props\n    if (_.isEqual(selection, {row: rowIdx, col: colIdx})) { return }\n    onSetSelection({rowIdx, colIdx, key: null})\n  }\n\n  isSelected({selection, rowIdx, colIdx}) {\n    return (\n      selection && selection.rowIdx === rowIdx && selection.colIdx === colIdx\n    )\n  }\n\n  isSelectedUsingKey(key) {\n    const {selection} = this.props\n    return this.isSelected(this.props) && selection.key === key\n  }\n\n  isInLastRow() {\n    const {rowIdx, tableDataSource} = this.props\n    const rows = tableDataSource.rows()\n    return rowIdx === rows.length - 1;\n  }\n\n  render() {\n    const {className} = this.props\n    const classes = classnames({\n      [className]: true,\n      selected: this.isSelected(this.props),\n    })\n    return (\n      <TableCell\n        {...this.props}\n        className={classes}\n        onClick={this.onClickCell}\n      />\n    )\n  }\n}\n\n\nexport class SelectableTableRow extends Component {\n\n  static propTypes = {\n    className: PropTypes.string,\n    tableDataSource: Table.propTypes.tableDataSource,\n    selection: PropTypes.object,\n    rowIdx: TableRow.propTypes.rowIdx,\n  }\n\n  static defaultProps = {\n    className: '',\n  }\n\n  shouldComponentUpdate(nextProps) {\n    const rowChanged = (\n      this.props.tableDataSource.rowAt(this.props.rowIdx) !==\n      nextProps.tableDataSource.rowAt(nextProps.rowIdx)\n    )\n    const selectionStateChanged = (\n      this.isSelected(this.props) !== this.isSelected(nextProps)\n    )\n    const selectedColChanged = (\n      this.props.selection.colIdx !== nextProps.selection.colIdx\n    )\n\n    return rowChanged || selectionStateChanged || selectedColChanged\n  }\n\n  componentDidUpdate() {\n    if (this.isSelected(this.props)) {\n      ReactDOM.findDOMNode(this)\n      .scrollIntoViewIfNeeded(false)\n    }\n  }\n\n  isSelected({selection, rowIdx}) {\n    return selection && selection.rowIdx === rowIdx\n  }\n\n  render() {\n    const {className} = this.props\n    const classes = classnames({\n      [className]: true,\n      selected: this.isSelected(this.props),\n    })\n    return (\n      <TableRow\n        {...this.props}\n        className={classes}\n      />\n    )\n  }\n}\n\n\nclass SelectableTable extends Component {\n  static displayName = 'SelectableTable'\n\n  static propTypes = {\n    tableDataSource: Table.propTypes.tableDataSource,\n    extraProps: PropTypes.object,\n    RowRenderer: Table.propTypes.RowRenderer,\n    CellRenderer: Table.propTypes.CellRenderer,\n    selection: PropTypes.shape({\n      rowIdx: PropTypes.number,\n      colIdx: PropTypes.number,\n    }).isRequired,\n    onSetSelection: PropTypes.func.isRequired,\n    onShiftSelection: PropTypes.func.isRequired,\n  }\n\n  static defaultProps = {\n    extraProps: {},\n    RowRenderer: SelectableTableRow,\n    CellRenderer: SelectableTableCell,\n  }\n\n  shouldComponentUpdate(nextProps) {\n    return (\n      this.props.tableDataSource !== nextProps.tableDataSource ||\n      this.props.selection !== nextProps.selection\n    )\n  }\n\n  onArrowUp({key}) {\n    const {onShiftSelection} = this.props\n    onShiftSelection({row: -1, key})\n  }\n\n  onArrowDown({key}) {\n    const {onShiftSelection} = this.props\n    onShiftSelection({row: 1, key})\n  }\n\n  onArrowLeft({key}) {\n    const {onShiftSelection} = this.props\n    onShiftSelection({col: -1, key})\n  }\n\n  onArrowRight({key}) {\n    const {onShiftSelection} = this.props\n    onShiftSelection({col: 1, key})\n  }\n\n  onEnter({key}) {\n    const {onShiftSelection} = this.props\n    onShiftSelection({row: 1, key})\n  }\n\n  onTab({key}) {\n    const {tableDataSource, selection, onShiftSelection} = this.props\n    const colLen = tableDataSource.columns().length\n    if (selection.colIdx === colLen - 1) {\n      onShiftSelection({row: 1, col: -(colLen - 1), key})\n    } else {\n      onShiftSelection({col: 1, key})\n    }\n  }\n\n  onShiftTab({key}) {\n    const {tableDataSource, selection, onShiftSelection} = this.props\n    const colLen = tableDataSource.columns().length\n    if (selection.colIdx === 0) {\n      onShiftSelection({row: -1, col: colLen - 1, key})\n    } else {\n      onShiftSelection({col: -1, key})\n    }\n  }\n\n  render() {\n    const {selection, onSetSelection, onShiftSelection, extraProps, RowRenderer, CellRenderer} = this.props\n    const selectionProps = {\n      selection,\n      onSetSelection,\n      onShiftSelection,\n    }\n\n    return (\n      <Table\n        {...this.props}\n        extraProps={{...extraProps, ...selectionProps}}\n        RowRenderer={RowRenderer}\n        CellRenderer={CellRenderer}\n      />\n    )\n  }\n}\n\nexport default compose(\n  SelectableTable,\n  ListensToMovementKeys,\n  (Comp) => AutoFocuses(Comp, {onUpdate: false})\n)\n"
  },
  {
    "path": "packages/client-app/src/components/spinner.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\n_ = require 'underscore'\nclassNames = require 'classnames'\n\n###\nPublic: Displays an indeterminate progress indicator in the center of it's\nparent component.\n\nSection: Component Kit\n###\nclass Spinner extends React.Component\n\n  ###\n  Public: React `props` supported by Spinner:\n\n   - `visible` (optional) Pass true to display the spinner and false to hide it.\n   - `withCover` (optiona) Pass true to dim the content behind the spinner.\n   - `style` (optional) Additional styles to apply to the spinner.\n  ###\n  @propTypes =\n    visible: React.PropTypes.bool\n    withCover: React.PropTypes.bool\n    style: React.PropTypes.object\n\n  constructor: (@props) ->\n    @timer = null\n    @state =\n      hidden: true\n      paused: true\n\n  componentDidMount: =>\n    # The spinner always starts hidden. After it's mounted, it unhides itself\n    # if it's set to visible. This is a bit strange, but ensures that the CSS\n    # transition from .spinner.hidden => .spinner always happens, along with\n    # it's associated animation delay.\n    if @props.visible and @state.hidden\n      @showAfterDelay()\n\n  componentWillUnmount: =>\n    clearTimeout(@timer) if @timer\n\n  componentWillReceiveProps: (nextProps) =>\n    # If we have a cover, show right away.\n    if nextProps.withCover\n      @setState hidden: !nextProps.visible\n      return\n\n    hidden = if nextProps.visible? then !nextProps.visible else false\n\n    if @state.hidden is false and hidden is true\n      @setState({hidden: true})\n      @pauseAfterDelay()\n    else if @state.hidden is true and hidden is false\n      @showAfterDelay()\n\n  pauseAfterDelay: =>\n    clearTimeout(@timer) if @timer\n    @timer = setTimeout =>\n      return if @props.visible\n      @setState({paused: true})\n    ,250\n\n  showAfterDelay: =>\n    clearTimeout(@timer) if @timer\n    @timer = setTimeout =>\n      return if @props.visible isnt true\n      @setState({paused: false, hidden: false})\n    , 300\n\n  render: =>\n    if @props.withCover\n      @_renderDotsWithCover()\n    else\n      @_renderSpinnerDots()\n\n  # This displays an extra div that's a partially transparent white cover.\n  # If you don't want to make your own background for the loading state,\n  # this is a convenient default.\n  _renderDotsWithCover: =>\n    coverClasses = classNames\n      \"spinner-cover\": true\n      \"hidden\": @state.hidden\n\n    style = Object.assign {}, (@props.style ? {}),\n      'position':'absolute'\n      'display': if @state.hidden then \"none\" else \"block\"\n      'top': 0\n      'left': 0\n      'width': '100%'\n      'height': '100%'\n      'background': 'rgba(255,255,255,0.9)'\n      'zIndex': 1000\n\n    <div className={coverClasses} style={style}>\n      {@_renderSpinnerDots()}\n    </div>\n\n  _renderSpinnerDots: =>\n    spinnerClass = classNames\n      'spinner': true\n      'hidden': @state.hidden\n      'paused': @state.paused\n\n    style = _.extend {}, (@props.style ? {}),\n      'position':'absolute'\n      'left': '50%'\n      'top': '50%'\n      'zIndex': 1001\n      'transform':'translate(-50%,-50%)'\n\n    otherProps = _.omit(@props, Object.keys(@constructor.propTypes))\n\n    <div className={spinnerClass} {...otherProps} style={style}>\n      <div className=\"bounce1\"></div>\n      <div className=\"bounce2\"></div>\n      <div className=\"bounce3\"></div>\n      <div className=\"bounce4\"></div>\n    </div>\n\nmodule.exports = Spinner\n"
  },
  {
    "path": "packages/client-app/src/components/swipe-container.jsx",
    "content": "import React, {Component, PropTypes} from 'react';\nimport ReactDOM from 'react-dom';\nimport _ from 'underscore';\nimport {exec} from 'child_process';\nimport {Utils} from 'nylas-exports';\n\n// This is a stripped down version of\n// https://github.com/michaelvillar/dynamics.js/blob/master/src/dynamics.coffee#L1179,\n//\nconst SpringBounceFactory = (options) => {\n  const frequency = Math.max(1, options.frequency / 20);\n  const friction = 20 ** (options.friction / 100);\n  return (t) => {\n    return 1 - ((friction / 10) ** (-t) * (1 - t) * Math.cos(frequency * t));\n  };\n};\nconst SpringBounceFunction = SpringBounceFactory({\n  frequency: 360,\n  friction: 440,\n})\n\nconst Phase = {\n  // No wheel events received yet, container is inactive.\n  None: 'none',\n\n  // Wheel events received\n  GestureStarting: 'gesture-starting',\n\n  // Wheel events received and we are stopping event propagation.\n  GestureConfirmed: 'gesture-confirmed',\n\n  // Fingers lifted, we are animating to a final state.\n  Settling: 'settling',\n}\n\nlet SwipeInverted = false;\n\nif (process.platform === 'darwin') {\n  exec(\"defaults read -g com.apple.swipescrolldirection\", (err, stdout) => {\n    SwipeInverted = (stdout.toString().trim() !== '0');\n  });\n} else if (process.platform === 'win32') {\n  // Currently does not matter because we don't support trackpad gestures on Win.\n  // It appears there's a config key called FlipFlopWheel which we might have to\n  // check, but it also looks like disabling natural scroll on Win32 only changes\n  // vertical, not horizontal, behavior.\n}\n\n\nexport default class SwipeContainer extends Component {\n  static displayName = 'SwipeContainer';\n\n  static propTypes = {\n    children: PropTypes.object.isRequired,\n    shouldEnableSwipe: React.PropTypes.func,\n    onSwipeLeft: React.PropTypes.func,\n    onSwipeLeftClass: React.PropTypes.oneOfType([\n      PropTypes.string,\n      PropTypes.func,\n    ]),\n    onSwipeRight: React.PropTypes.func,\n    onSwipeRightClass: React.PropTypes.oneOfType([\n      PropTypes.string,\n      PropTypes.func,\n    ]),\n    onSwipeCenter: React.PropTypes.func,\n  };\n\n  static defaultProps = {\n    shouldEnableSwipe: () => true,\n  };\n\n  constructor(props) {\n    super(props);\n    this.mounted = false;\n    this.tracking = false;\n    this.trackingInitialTargetX = 0;\n    this.trackingTouchIdentifier = null;\n    this.phase = Phase.None;\n    this.fired = false;\n    this.isEnabled = null;\n    this.state = {\n      fullDistance: 'unknown',\n      currentX: 0,\n      targetX: 0,\n    };\n  }\n\n  componentDidMount() {\n    this.mounted = true;\n    window.addEventListener('scroll-touch-begin', this._onScrollTouchBegin);\n    window.addEventListener('scroll-touch-end', this._onScrollTouchEnd);\n  }\n\n  componentWillReceiveProps() {\n    this.isEnabled = null;\n  }\n\n  componentDidUpdate() {\n    if (this.phase === Phase.Settling) {\n      window.requestAnimationFrame(() => {\n        if (this.phase === Phase.Settling) {\n          this._settle();\n        }\n      });\n    }\n  }\n\n  componentWillUnmount() {\n    this.phase = Phase.None;\n    this.mounted = false;\n    window.removeEventListener('scroll-touch-begin', this._onScrollTouchBegin);\n    window.removeEventListener('scroll-touch-end', this._onScrollTouchEnd);\n  }\n\n  _isEnabled = () => {\n    if (this.isEnabled === null) {\n      // Cache this value so we don't have to recalculate on every swipe\n      this.isEnabled = (\n        (this.props.onSwipeLeft || this.props.onSwipeRight) &&\n        this.props.shouldEnableSwipe()\n      );\n    }\n    return this.isEnabled;\n  };\n\n  _onWheel = (e) => {\n    let velocity = e.deltaX / 3;\n    if (SwipeInverted) {\n      velocity = -velocity;\n    }\n    this._onDragWithVelocity(velocity);\n\n    if (this.phase === Phase.GestureConfirmed) {\n      e.preventDefault();\n    }\n  };\n\n  _onDragWithVelocity = (velocity) => {\n    if ((this.tracking === false) || !this._isEnabled()) {\n      return;\n    }\n    const velocityConfirmsGesture = Math.abs(velocity) > 3;\n\n    if (this.phase === Phase.None) {\n      this.phase = Phase.GestureStarting;\n    }\n\n    if (velocityConfirmsGesture || (this.phase === Phase.Settling)) {\n      this.phase = Phase.GestureConfirmed;\n    }\n\n    let {fullDistance, thresholdDistance} = this.state;\n\n    if (fullDistance === 'unknown') {\n      fullDistance = ReactDOM.findDOMNode(this).clientWidth;\n      thresholdDistance = 110;\n    }\n\n    const clipToMax = (v) => Math.max(-fullDistance, Math.min(fullDistance, v))\n    const currentX = clipToMax(this.state.currentX + velocity);\n    const estimatedSettleX = clipToMax(currentX + velocity * 8);\n    const lastDragX = currentX;\n    let targetX = 0;\n\n    // If you started from the center, you can swipe left or right. If you start\n    // from the left or right \"Activated\" state, you can only swipe back to the\n    // center.\n\n    if (this.trackingInitialTargetX === 0) {\n      if (this.props.onSwipeRight && (estimatedSettleX > thresholdDistance)) {\n        targetX = fullDistance;\n      }\n      if (this.props.onSwipeLeft && (estimatedSettleX < -thresholdDistance)) {\n        targetX = -fullDistance;\n      }\n    } else if (this.trackingInitialTargetX < 0) {\n      if (fullDistance - Math.abs(estimatedSettleX) < thresholdDistance) {\n        targetX = -fullDistance;\n      }\n    } else if (this.trackingInitialTargetX > 0) {\n      if (fullDistance - Math.abs(estimatedSettleX) < thresholdDistance) {\n        targetX = fullDistance;\n      }\n    }\n    this.setState({thresholdDistance, fullDistance, currentX, targetX, lastDragX});\n  };\n\n  _onScrollTouchBegin = () => {\n    this.tracking = true;\n    this.trackingInitialTargetX = this.state.targetX;\n  };\n\n  _onScrollTouchEnd = () => {\n    this.tracking = false;\n    if ((this.phase !== Phase.None) && (this.phase !== Phase.Settling)) {\n      this.phase = Phase.Settling;\n      this.fired = false;\n      this.setState({\n        settleStartTime: Date.now(),\n      });\n    }\n  };\n\n  _onTouchStart = (e) => {\n    if ((this.trackingTouchIdentifier === null) && (e.targetTouches.length > 0)) {\n      const touch = e.targetTouches.item(0);\n      this.trackingTouchIdentifier = touch.identifier;\n      this.trackingTouchX = touch.clientX;\n      this._onScrollTouchBegin();\n    }\n  };\n\n  _onTouchMove = (e) => {\n    if (this.trackingTouchIdentifier === null) {\n      return;\n    }\n    if (e.cancelable === false) {\n      // Chrome has already started interpreting these touch events as a scroll.\n      // We can no longer call preventDefault to make them ours.\n      return;\n    }\n    let trackingTouch = null;\n    for (let ii = 0; ii < e.changedTouches.length; ii++) {\n      const touch = e.changedTouches.item(ii);\n      if (touch.identifier === this.trackingTouchIdentifier) {\n        trackingTouch = touch;\n        break;\n      }\n    }\n    if (trackingTouch !== null) {\n      const velocity = (trackingTouch.clientX - this.trackingTouchX);\n      this.trackingTouchX = trackingTouch.clientX;\n      this._onDragWithVelocity(velocity);\n\n      if (this.phase === Phase.GestureConfirmed) {\n        e.preventDefault();\n      }\n    }\n  };\n\n  _onTouchEnd = (e) => {\n    if (this.trackingTouchIdentifier === null) {\n      return;\n    }\n    for (let ii = 0; ii < e.changedTouches.length; ii++) {\n      if (e.changedTouches.item(ii).identifier === this.trackingTouchIdentifier) {\n        this.trackingTouchIdentifier = null;\n        this._onScrollTouchEnd();\n        break;\n      }\n    }\n  };\n\n  _onSwipeActionCompleted = (rowWillDisappear) => {\n    let delay = 0;\n    if (rowWillDisappear) {\n      delay = 550;\n    }\n\n    setTimeout(() => {\n      if (this.mounted === true) {\n        this._onReset();\n      }\n    }, delay);\n  };\n\n  _onReset() {\n    this.phase = Phase.Settling;\n    this.setState({\n      targetX: 0,\n      settleStartTime: Date.now(),\n    });\n  }\n\n  _settle() {\n    const {targetX, settleStartTime, lastDragX} = this.state;\n    let {currentX} = this.state;\n\n    const f = (Date.now() - settleStartTime) / 1400.0;\n    currentX = lastDragX + SpringBounceFunction(f) * (targetX - lastDragX);\n\n    const shouldFinish = (f >= 1.0);\n    const mostlyFinished = ((Math.abs(currentX) / Math.abs(targetX)) > 0.8);\n    const shouldFire = mostlyFinished && (this.fired === false) && (this.trackingInitialTargetX !== targetX);\n\n    if (shouldFire) {\n      this.fired = true;\n      if (targetX > 0) {\n        this.props.onSwipeRight(this._onSwipeActionCompleted);\n      } else if (targetX < 0) {\n        this.props.onSwipeLeft(this._onSwipeActionCompleted);\n      } else if ((targetX === 0) && this.props.onSwipeCenter) {\n        this.props.onSwipeCenter();\n      }\n    }\n\n    if (shouldFinish) {\n      this.phase = Phase.None;\n      this.setState({\n        currentX: targetX,\n        targetX: targetX,\n        thresholdDistance: 'unknown',\n        fullDistance: 'unknown',\n      });\n    } else {\n      this.phase = Phase.Settling;\n      this.setState({currentX, lastDragX});\n    }\n  }\n\n  render() {\n    const {currentX, targetX} = this.state;\n    const otherProps = Utils.fastOmit(this.props, Object.keys(this.constructor.propTypes));\n    const backingStyles = {top: 0, bottom: 0, position: 'absolute'};\n    let backingClass = 'swipe-backing';\n\n    if ((currentX < 0) && (this.trackingInitialTargetX <= 0)) {\n      const {onSwipeLeftClass} = this.props\n      const swipeLeftClass = _.isFunction(onSwipeLeftClass) ? onSwipeLeftClass() : onSwipeLeftClass || ''\n\n      backingClass += ` ${swipeLeftClass}`;\n      backingStyles.right = 0;\n      backingStyles.width = -currentX + 1;\n      if (targetX < 0) {\n        backingClass += ' confirmed';\n      }\n    } else if ((currentX > 0) && (this.trackingInitialTargetX >= 0)) {\n      const {onSwipeRightClass} = this.props\n      const swipeRightClass = _.isFunction(onSwipeRightClass) ? onSwipeRightClass() : onSwipeRightClass || ''\n\n      backingClass += ` ${swipeRightClass}`;\n      backingStyles.left = 0;\n      backingStyles.width = currentX + 1;\n      if (targetX > 0) {\n        backingClass += ' confirmed';\n      }\n    }\n    return (\n      <div\n        onWheel={this._onWheel}\n        onTouchStart={this._onTouchStart}\n        onTouchMove={this._onTouchMove}\n        onTouchEnd={this._onTouchEnd}\n        onTouchCancel={this._onTouchEnd}\n        {...otherProps}\n      >\n        <div style={backingStyles} className={backingClass} />\n        <div style={{transform: `translate3d(${currentX}px, 0, 0)`}}>\n          {this.props.children}\n        </div>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/switch.jsx",
    "content": "import React from 'react';\n\n/* Public: A small React component which renders as a horizontal on/off switch.\n   Provide it with `onChange` and `checked` props just like a checkbox:\n\n  ```\n  <Switch onChange={this._onToggleChecked} checked={this.state.form.isChecked} />\n  ```\n*/\n\nconst Switch = (props) => {\n  let classnames = `${props.className || \"\"} slide-switch`;\n  if (props.checked) {\n    classnames += \" active\";\n  }\n\n  return (\n    <div className={classnames} onClick={props.onChange}>\n      <div className=\"handle\" />\n    </div>\n  );\n}\n\nSwitch.propTypes = {\n  checked: React.PropTypes.bool,\n  onChange: React.PropTypes.func.isRequired,\n  className: React.PropTypes.string,\n};\n\nexport default Switch;\n"
  },
  {
    "path": "packages/client-app/src/components/syncing-list-state.jsx",
    "content": "import {Actions, React} from 'nylas-exports';\n\nfunction SyncingListState(props) {\n  let message = \"Looking for more messages\"\n  if (props.empty) {\n    message = \"Looking for messages\"\n  }\n  return (\n    <div className=\"syncing-list-state\" style={{width: \"100%\", textAlign: \"center\"}}>\n      {message}&hellip;\n      <br />\n      <a onClick={Actions.expandInitialSyncState}>\n        Show Progress\n      </a>\n    </div>\n  )\n}\n\nSyncingListState.propTypes = {\n  empty: React.PropTypes.bool,\n}\n\nexport default SyncingListState;\n"
  },
  {
    "path": "packages/client-app/src/components/tab-group-region.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\n\nclass TabGroupRegion extends React.Component\n  @childContextTypes:\n    parentTabGroup: React.PropTypes.object\n\n  _onKeyDown: (event) =>\n    if event.key is \"Tab\" and not event.defaultPrevented\n      dir = if event.shiftKey then -1 else 1\n      @shiftFocus(dir)\n      event.preventDefault()\n      event.stopPropagation()\n    return\n\n  shiftFocus: (dir) =>\n    nodes = ReactDOM.findDOMNode(@).querySelectorAll('input, textarea, [contenteditable], [tabIndex]')\n    current = document.activeElement\n    idx = Array.from(nodes).indexOf(current)\n\n    for i in [0..nodes.length]\n      idx = idx + dir\n      if idx < 0\n        idx = nodes.length - 1\n      else\n        idx = idx % nodes.length\n\n      continue if nodes[idx].tabIndex is -1\n      nodes[idx].focus()\n      if @_shouldSelectEnd(nodes[idx])\n        nodes[idx].setSelectionRange(nodes[idx].value.length, nodes[idx].value.length)\n      return\n\n  _shouldSelectEnd: (node) ->\n    node.nodeName is \"INPUT\" and\n    node.type is \"text\" and\n    \"no-select-end\" not in node.classList\n\n  getChildContext: =>\n    parentTabGroup: @\n\n  render: ->\n    <div {...@props} onKeyDown={@_onKeyDown}>{@props.children}</div>\n\nmodule.exports = TabGroupRegion\n"
  },
  {
    "path": "packages/client-app/src/components/table/table-data-source.es6",
    "content": "/**\n * Base class that defines an interface to access table data.\n * All methods that modify data are immutable, which means a new instance of\n * `TableDataSource` is returned with the new data.\n *\n * This class can be used as is for a default implementation of table\n * data operations, but is meant be extended for different implementations\n *\n * @class TableDataSource\n */\nexport default class TableDataSource {\n\n  /**\n   * Takes an Object of the form:\n   *\n   * ```\n   * const tableData = {\n   *   columns: ['col1', 'col2'],\n   *   rows: [\n   *     [1, 2],\n   *     [3, 4],\n   *     [5, null]\n   *   ],\n   * }\n   *\n   * @param {object} tableData\n   * @param {array} tableData.columns - Array of columns\n   * @param {array} tableData.rows - Array of rows\n   * @method constructor\n   */\n  constructor(tableData) {\n    this._tableData = tableData || {\n      columns: [],\n      rows: [[]],\n    }\n  }\n\n  /**\n   * ```\n   * source.colAt(2)\n   * ```\n   *\n   * @param {number} colIdx - Index of column name to retrieve\n   * @return {any} - value for column at given index or null if does not exist\n   * @method colAt\n   */\n  colAt(colIdx) {\n    const col = this._tableData.columns[colIdx]\n    return col != null ? col : null\n  }\n\n  /**\n   * Returns the row at the given index. If rowIdx is null or undefined, returns\n   * the array of columns.\n   *\n   * If the row at the given rowIdx does not exists, returns null\n   *\n   * ```\n   * source.rowAt(2)\n   * ```\n   *\n   * @param {number} rowIdx - Index of row to retrieve\n   * @return {array} - row for given index or null if does not exist\n   * @method rowAt\n   */\n  rowAt(rowIdx) {\n    if (rowIdx == null) {\n      return this.columns()\n    }\n    return this._tableData.rows[rowIdx] || null\n  }\n\n  /**\n   * Returns the cell data at the given indixes. If rowIdx is null or undefined,\n   * returns the value for the column at colIdx\n   *\n   * If the cell at the given indices does not exists, returns null\n   *\n   * ```\n   * source.cellAt({rowIdx: 1, colIdx: 2})\n   * ```\n   *\n   * @param {object} arg\n   * @param {number} arg.rowIdx - Row index of cell\n   * @param {number} arg.colIdx - Col index of cell\n   * @return {any} - value for cell at given indices or null if it does not exist\n   * @method cellAt\n   */\n  cellAt({rowIdx, colIdx} = {}) {\n    if (rowIdx == null) {\n      return this.colAt(colIdx)\n    }\n    const row = this.rowAt(rowIdx)\n    const cell = row ? row[colIdx] : null\n    return cell != null ? cell : null\n  }\n\n  /**\n   * Returns true if the given cell, column, or row is empty\n   *\n   * ```\n   * source.isEmpty({rowIdx: 1}) // true if row 1 is empty\n   * ```\n   *\n   * @param {object} arg\n   * @param {number} arg.rowIdx - Row index of cell\n   * @param {number} arg.colIdx - Col index of cell\n   * @return {any} - value for cell at given indices or null if it does not exist\n   * @method cellAt\n   */\n  isEmpty({rowIdx, colIdx} = {}) {\n    if (rowIdx == null && colIdx == null) {\n      throw new Error('TableDataSource::isEmpty - Must provide rowIdx and/or colIdx')\n    }\n    if (rowIdx == null) {\n      const col = this.colAt(colIdx)\n      if (col == null) {\n        throw new Error('TableDataSource::isEmpty - Must provide a valid colIdx')\n      }\n    }\n    const row = this.rowAt(rowIdx)\n    if (!row) {\n      throw new Error('TableDataSource::isEmpty - Must provide a valid rowIdx')\n    }\n    if (colIdx == null) {\n      return row.every((el) => !el)\n    }\n    return !this.cellAt({rowIdx, colIdx})\n  }\n\n  /**\n   * ```\n   * source.rows()\n   * ```\n   *\n   * @return {array} - all table rows\n   * @method rows\n   */\n  rows() {\n    return this._tableData.rows\n  }\n\n  /**\n   * ```\n   * source.columns()\n   * ```\n   *\n   * @return {array} - table columns (headers)\n   * @method rows\n   */\n  columns() {\n    return this._tableData.columns\n  }\n\n  /**\n   * Adds column\n   *\n   * @return {TableDataSource} - updated data source instance\n   * @method addColumn\n   */\n  addColumn(name = null) {\n    const rows = this.rows()\n    const columns = this.columns()\n    return new TableDataSource({\n      ...this._tableData,\n      rows: rows.map(row => row.concat(null)),\n      columns: columns.concat([name]),\n    })\n  }\n\n  /**\n   * Removes last column and all of its data.\n   *\n   * @return {TableDataSource} - updated data source instance\n   * @method removeLastColumn\n   */\n  removeLastColumn() {\n    const nextNumColumns = this.columns().length - 1\n    const nextRows = this.rows().map(row => row.slice(0, nextNumColumns))\n    const nextColumns = this.columns().slice(0, nextNumColumns)\n    return new TableDataSource({\n      ...this._tableData,\n      rows: nextRows,\n      columns: nextColumns,\n    })\n  }\n\n  /**\n   * Adds row\n   *\n   * @return {TableDataSource} - updated data source instance\n   * @method addRow\n   */\n  addRow() {\n    const rows = this.rows()\n    const nextRows = rows.concat([rows[0].map(() => null)])\n    return new TableDataSource({\n      ...this._tableData,\n      rows: nextRows,\n    })\n  }\n\n  /**\n   * Removes last row\n   *\n   * @return {TableDataSource} - updated data source instance\n   * @method removeRow\n   */\n  removeRow() {\n    const rows = this.rows()\n    return new TableDataSource({\n      ...this._tableData,\n      rows: rows.slice(0, rows.length - 1),\n    })\n  }\n\n  /**\n   * Updates value for cell at given indices\n   *\n   * @param {object} args - args object\n   * @param {number} args.rowIdx - rowIdx for cell\n   * @param {number} args.colIdx - colIdx for cell\n   * @param {boolean} args.isHeader - indicates whether cell is a header (column)\n   * @param {any} args.value - new value for cell\n   * @return {TableDataSource} - updated data source instance\n   * @method updateCell\n   */\n  updateCell({rowIdx, colIdx, isHeader, value} = {}) {\n    if (isHeader) {\n      const nextColumns = this.columns().slice()\n      nextColumns.splice(colIdx, 1, value)\n      return new TableDataSource({\n        ...this._tableData,\n        columns: nextColumns,\n      })\n    }\n\n    const nextRows = this.rows().slice()\n    const nextRow = nextRows[rowIdx].slice()\n    nextRow.splice(colIdx, 1, value)\n    nextRows[rowIdx] = nextRow\n    return new TableDataSource({\n      ...this._tableData,\n      rows: nextRows,\n    })\n  }\n\n  /**\n   * Clears all table data\n   *\n   * @return {TableDataSource} - updated data source instance\n   * @method clear\n   */\n  clear() {\n    return new TableDataSource()\n  }\n\n  filterRows(filterFn) {\n    const rows = this.rows()\n    const nextRows = rows.filter(filterFn)\n    return new TableDataSource({\n      ...this._tableData,\n      rows: nextRows,\n    })\n  }\n\n  toJSON() {\n    return {...this._tableData}\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/table/table.jsx",
    "content": "import classnames from 'classnames'\nimport React, {Component, PropTypes} from 'react'\nimport {pickHTMLProps} from 'pick-react-known-prop'\nimport LazyRenderedList from '../lazy-rendered-list'\nimport TableDataSource from './table-data-source'\n\n\n/*\n * Scrollable Table component which supports headers, numbering and lazily rendering rows.\n * The only required prop is `tableDataSource` which must be an instance of\n * {TableDataSource}:\n *\n * ```\n * const tableDataSource = new TableDataSource()\n * tableDataSource.rows()\n * // returns\n * // [\n * //   [1, 2],\n * //   [3, 4],\n * // ]\n * ```\n *\n * In order to lazily render rows, the props `rowHeight` and `tableBodyHeight`\n * are required.\n *\n * The Table Component can be extended via passing custom `RowRenderer` and\n * `CellRenderer` components as props. Any `RowRenderer` or `CellRenderer`\n * passed to Table must internally render a {TableRow} or {TableCell} component,\n * which are also exported in this module.\n *\n * See {SelectableTable} for an example of extending this component\n *\n * @param {object} props - props for Table\n * @param {string} props.className - CSS class to be applied to component\n * @param {boolean} props.displayNumbers - Wether to display a column with row\n * numberings\n * @param {boolean} props.displayHeader - Wether to display the first row in the\n * table data as a header\n * @param {number} props.rowHeight - Height of every row in the table\n * @param {number} props.tableBodyHeight - Height of the table body, excluding\n * the header\n * @param {object} props.tableDataSource - Instance of {TableDataSource} which\n * provides table data to be rendered\n * @param {object} props.extraProps - Additional props to be passed down to\n * `RowRenderer` and `CellRenderer` components\n * @param {function | string} props.RowRenderer - Function, Class or String used\n * to render Rows. Must be of any type accepted by React.createElement. E.g.\n * 'div', () => <div />, class Div extends React.Component { render() { return <div /> } }\n * @param {function | string} props.CellRenderer - Function, Class or String used\n * to render Cells. Must be of any type accepted by React.createElement. E.g.\n * 'div', () => <div />, class Div extends React.Component { render() { return <div /> } }\n * @class Table\n */\n\nconst RendererType = PropTypes.oneOfType([PropTypes.func, PropTypes.string])\nconst IndexType = PropTypes.oneOfType([PropTypes.number, PropTypes.string])\nconst TablePropTypes = {\n  idx: IndexType,\n  renderer: RendererType,\n  tableDataSource: PropTypes.instanceOf(TableDataSource),\n}\n\nexport function TableCell(props) {\n  const {className, isHeader, children, ...extraProps} = props;\n  const CellTag = isHeader ? 'th' : 'td'\n  return (\n    <CellTag {...pickHTMLProps(extraProps)} className={`table-cell ${className}`} >\n      {children}\n    </CellTag>\n  )\n}\n\nTableCell.propTypes = {\n  isHeader: PropTypes.bool,\n  className: PropTypes.string,\n};\n\nexport class TableRow extends Component {\n\n  static propTypes = {\n    className: PropTypes.string,\n    isHeader: PropTypes.bool,\n    displayNumbers: PropTypes.bool,\n    tableDataSource: TablePropTypes.tableDataSource.isRequired,\n    rowIdx: TablePropTypes.idx,\n    extraProps: PropTypes.object,\n    CellRenderer: TablePropTypes.renderer,\n  }\n\n  static defaultProps = {\n    className: '',\n    extraProps: {},\n    CellRenderer: TableCell,\n  }\n\n  render() {\n    const {className, displayNumbers, isHeader, tableDataSource, rowIdx, extraProps, CellRenderer, ...props} = this.props\n    const classes = classnames({\n      'table-row': true,\n      'table-row-header': isHeader,\n      [className]: true,\n    })\n\n    return (\n      <tr className={classes} {...pickHTMLProps(props)}>\n        {displayNumbers ?\n          <TableCell\n            className=\"numbered-cell\"\n            isHeader={isHeader}\n          >\n            {isHeader ? '' : rowIdx + 1}\n          </TableCell> :\n          null\n        }\n        {tableDataSource.columns().map((colName, colIdx) => {\n          const cellProps = {tableDataSource, rowIdx, colIdx, ...extraProps}\n          return (\n            <CellRenderer key={`cell-${rowIdx}-${colIdx}`} {...cellProps}>\n              {tableDataSource.cellAt({rowIdx, colIdx})}\n            </CellRenderer>\n          )\n        })}\n      </tr>\n    )\n  }\n}\n\n\nexport default class Table extends Component {\n  static displayName = 'Table'\n\n  static propTypes = {\n    className: PropTypes.string,\n    displayHeader: PropTypes.bool,\n    displayNumbers: PropTypes.bool,\n    rowHeight: PropTypes.number,\n    bodyHeight: PropTypes.number,\n    tableDataSource: TablePropTypes.tableDataSource.isRequired,\n    extraProps: PropTypes.object,\n    RowRenderer: TablePropTypes.renderer,\n    CellRenderer: TablePropTypes.renderer,\n  }\n\n  static defaultProps = {\n    className: '',\n    extraProps: {},\n    RowRenderer: TableRow,\n    CellRenderer: TableCell,\n  }\n\n  static TableDataSource = TableDataSource\n\n  renderRow = ({idx}) => {\n    const {tableDataSource, displayNumbers, extraProps, RowRenderer, CellRenderer} = this.props\n    return (\n      <RowRenderer\n        key={`row-${idx}`}\n        rowIdx={idx}\n        displayNumbers={displayNumbers}\n        tableDataSource={tableDataSource}\n        extraProps={extraProps}\n        CellRenderer={CellRenderer}\n        {...extraProps}\n      />\n    )\n  }\n\n  renderBody() {\n    const {tableDataSource, rowHeight, bodyHeight} = this.props\n    const rows = tableDataSource.rows()\n\n    return (\n      <LazyRenderedList\n        items={rows}\n        itemHeight={rowHeight}\n        containerHeight={bodyHeight}\n        BufferTag=\"tr\"\n        RootRenderer=\"tbody\"\n        ItemRenderer={this.renderRow}\n      />\n    )\n  }\n\n  renderHeader() {\n    const {tableDataSource, displayNumbers, displayHeader, extraProps, RowRenderer, CellRenderer} = this.props\n    if (!displayHeader) { return false }\n\n    const extraHeaderProps = {...extraProps, isHeader: true}\n    return (\n      <thead>\n        <RowRenderer\n          rowIdx={null}\n          tableDataSource={tableDataSource}\n          displayNumbers={displayNumbers}\n          extraProps={extraHeaderProps}\n          CellRenderer={CellRenderer}\n          {...extraHeaderProps}\n        />\n      </thead>\n    )\n  }\n\n  render() {\n    const {className, ...otherProps} = this.props\n\n    return (\n      <div className={`nylas-table ${className}`} {...pickHTMLProps(otherProps)}>\n        <table>\n          {this.renderHeader()}\n          {this.renderBody()}\n        </table>\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/time-picker.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport moment from 'moment'\nimport classnames from 'classnames'\n\nrequire('moment-round') // overrides moment\n\nconst INTERVAL = [30, 'minutes']\n\nexport default class TimePicker extends React.Component {\n  static displayName = \"TimePicker\";\n\n  static propTypes = {\n    value: React.PropTypes.number,\n    onChange: React.PropTypes.func,\n    relativeTo: React.PropTypes.number, // TODO For `renderTimeOptions`\n  }\n\n  static contextTypes = {\n    parentTabGroup: React.PropTypes.object,\n  }\n\n  static defaultProps = {\n    value: moment().valueOf(),\n    onChange: () => {},\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      focused: false,\n      rawText: this._valToTimeString(props.value),\n    }\n  }\n\n  componentDidMount() {\n    this._fixTimeOptionScroll()\n  }\n\n  componentWillReceiveProps(newProps) {\n    this.setState({rawText: this._valToTimeString(newProps.value)})\n  }\n\n  componentDidUpdate() {\n    if (this._gotoScrollStartOnUpdate) {\n      this._fixTimeOptionScroll()\n    }\n  }\n\n  _valToTimeString(value) {\n    return moment(value).format(\"LT\")\n  }\n\n  _onKeyDown = (event) => {\n    if (event.key === \"ArrowUp\") {\n      event.preventDefault()\n      this._onArrow(event.key)\n    } else if (event.key === \"ArrowDown\") {\n      event.preventDefault()\n      this._onArrow(event.key)\n    } else if (event.key === \"Enter\") {\n      this.context.parentTabGroup.shiftFocus(1);\n    }\n  }\n\n  _onArrow(key) {\n    let newT = moment(this.props.value);\n    newT = newT.round(...INTERVAL);\n    if (key === \"ArrowUp\") {\n      newT = newT.subtract(...INTERVAL);\n    } else if (key === \"ArrowDown\") {\n      newT = newT.add(...INTERVAL);\n    }\n    if (moment(this.props.value).day() !== newT.day()) {\n      return\n    }\n    this._gotoScrollStartOnUpdate = true\n    this.props.onChange(newT);\n  }\n\n  _onFocus = () => {\n    this.setState({focused: true});\n    this._gotoScrollStartOnUpdate = true\n    const el = ReactDOM.findDOMNode(this.refs.input);\n    el.setSelectionRange(0, el.value.length)\n  }\n\n  _onBlur = (event) => {\n    this.setState({focused: false});\n    if (event.relatedTarget && Array.from(event.relatedTarget.classList).includes(\"time-options\")) {\n      return\n    }\n    this._saveIfValid(this.state.rawText)\n  }\n\n  _onRawTextChange = (event) => {\n    this.setState({rawText: event.target.value});\n  }\n\n  _saveIfValid(rawText = \"\") {\n    // Locale-aware am/pm parsing!!\n    const parsedMoment = moment(rawText, \"h:ma\");\n    if (parsedMoment.isValid()) {\n      if (this._shouldAddTwelve(rawText) && parsedMoment.hour() < 12) {\n        parsedMoment.add(12, 'hours');\n      }\n      this.props.onChange(parsedMoment.valueOf())\n    }\n  }\n\n  /*\n   * If you're going to punch only \"2\" into the time field, you probably\n   * mean 2pm instead of 2am. The regex explicitly checks for only digits\n   * (no meridiem indicators) and very basic use cases.\n   */\n  _shouldAddTwelve(rawText) {\n    const simpleDigitMatch = rawText.match(/^(\\d{1,2})(:\\d{1,2})?$/);\n    if (simpleDigitMatch && simpleDigitMatch.length > 0) {\n      const hr = parseInt(simpleDigitMatch[1], 10);\n      if (hr <= 7) {\n        return true\n      }\n    }\n    return false\n  }\n\n  _fixTimeOptionScroll() {\n    this._gotoScrollStartOnUpdate = false\n    const el = ReactDOM.findDOMNode(this);\n    const scrollTo = el.querySelector(\".scroll-start\");\n    const scrollWrap = el.querySelector(\".time-options\");\n    if (scrollTo && scrollWrap) {\n      scrollWrap.scrollTop = scrollTo.offsetTop\n    }\n  }\n\n  _onSelectOption(val) {\n    this.props.onChange(val)\n  }\n\n  _renderTimeOptions() {\n    if (!this.state.focused) {\n      return false\n    }\n\n    const enteredMoment = moment(this.props.value);\n\n    const roundedMoment = moment(enteredMoment);\n    roundedMoment.ceil(...INTERVAL);\n\n    const firstVisibleMoment = moment(roundedMoment);\n    firstVisibleMoment.add(...INTERVAL);\n\n    let startVal = moment(this.props.value).startOf('day').valueOf();\n    startVal = Math.max(startVal, (this.props.relativeTo || 0));\n\n    const startMoment = moment(startVal)\n    if (this.props.relativeTo) {\n      startMoment.ceil(...INTERVAL).add(...INTERVAL)\n    }\n    const endMoment = moment(startVal).endOf('day');\n    const opts = []\n\n    const relStart = moment(this.props.relativeTo);\n    const timeIter = moment(startMoment)\n    while (timeIter.isSameOrBefore(endMoment)) {\n      const val = timeIter.valueOf();\n      const className = classnames({\n        \"option\": true,\n        \"selected\": timeIter.isSame(enteredMoment),\n        \"scroll-start\": timeIter.isSame(firstVisibleMoment),\n      })\n\n      let relTxt = false\n      if (this.props.relativeTo) {\n        relTxt = (\n          <span className=\"rel-text\">\n            {`(${timeIter.diff(relStart, 'hours', true)}hr)`}\n          </span>\n        )\n      }\n\n      opts.push(\n        <div\n          className={className} key={val}\n          onMouseDown={() => this._onSelectOption(val)}\n        >\n          {timeIter.format(\"LT\")}{relTxt}\n        </div>\n      )\n      timeIter.add(...INTERVAL)\n    }\n\n    const className = classnames({\n      \"time-options\": true,\n      \"relative-to\": this.props.relativeTo,\n    })\n\n    return (\n      <div className={className} tabIndex={-1}>{opts}</div>\n    )\n  }\n\n  render() {\n    const className = classnames({\n      \"time-picker\": true,\n      \"no-select-end\": true,\n      \"invalid\": !moment(this.state.rawText, \"h:ma\").isValid(),\n    })\n    return (\n      <div className=\"time-picker-wrap\">\n        <input\n          className={className}\n          type=\"text\"\n          ref=\"input\"\n          value={this.state.rawText}\n          onChange={this._onRawTextChange}\n          onKeyDown={this._onKeyDown} onFocus={this._onFocus}\n          onBlur={this._onBlur}\n        />\n        {this._renderTimeOptions()}\n      </div>\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/toast.jsx",
    "content": "import React, {Component, PropTypes} from 'react'\nimport ReactCSSTransitionGroup from 'react-addons-css-transition-group'\n\n\nclass Toast extends Component {\n  static displayName = 'Toast'\n\n  static propTypes = {\n    className: PropTypes.string,\n    visible: PropTypes.bool,\n    visibleDuration: PropTypes.number,\n    onDidHide: PropTypes.func,\n  }\n\n  static defaultProps = {\n    visible: false,\n    visibleDuration: 3000,\n    onDidHide: () => {},\n  }\n\n  constructor(props) {\n    super(props)\n    this._timeout = null\n    this._mounted = false\n    this.state = {\n      visible: props.visible,\n    }\n  }\n\n  componentDidMount() {\n    this._mounted = true\n    this._ensureTimeout()\n  }\n\n  componentWillReceiveProps(nextProps) {\n    this.setState({visible: nextProps.visible})\n  }\n\n  componentDidUpdate() {\n    this._ensureTimeout()\n  }\n\n  componentWillUnmount() {\n    const {onDidHide} = this.props\n    this._mounted = false\n    onDidHide()\n  }\n\n  _clearTimeout() {\n    clearTimeout(this._timeout)\n    this._timeout = null\n  }\n\n  _ensureTimeout() {\n    const {visible} = this.state\n    const {visibleDuration, onDidHide} = this.props\n    this._clearTimeout()\n    if (visible) {\n      if (visibleDuration == null) { return }\n      this._timeout = setTimeout(() => {\n        this._mounted = false\n        this.setState({visible: false}, onDidHide)\n      }, visibleDuration)\n    }\n  }\n\n  _onMouseEnter = () => {\n    this._clearTimeout()\n  }\n\n  _onMouseLeave = () => {\n    this._ensureTimeout()\n  }\n\n  render() {\n    const {className, children} = this.props\n    const {visible} = this.state\n    return (\n      <ReactCSSTransitionGroup\n        className={`nylas-toast ${className}`}\n        transitionLeaveTimeout={150}\n        transitionEnterTimeout={150}\n        transitionName=\"nylas-toast-item\"\n      >\n        {visible ?\n          <div\n            className=\"nylas-toast-wrap\"\n            onMouseEnter={this._onMouseEnter}\n            onMouseLeave={this._onMouseLeave}\n          >\n            {children}\n          </div> :\n          null\n        }\n      </ReactCSSTransitionGroup>\n    )\n  }\n}\n\nexport default Toast\n"
  },
  {
    "path": "packages/client-app/src/components/tokenizing-text-field.jsx",
    "content": "import React from 'react'\nimport ReactDOM from 'react-dom'\nimport classNames from 'classnames'\nimport _ from 'underscore'\nimport {remote} from 'electron';\nimport {Utils, RegExpUtils} from 'nylas-exports'\nimport {Menu} from 'nylas-component-kit';\n\nimport RetinaImg from './retina-img';\nimport KeyCommandsRegion from './key-commands-region';\n\nclass SizeToFitInput extends React.Component {\n  static propTypes = {\n    value: React.PropTypes.string,\n  };\n\n  constructor(props) {\n    super(props)\n    this.state = {};\n  }\n\n  componentDidMount() {\n    this._sizeToFit()\n  }\n\n  componentDidUpdate() {\n    this._sizeToFit()\n  }\n\n  _sizeToFit() {\n    if (this.props.value.length === 0) {\n      return;\n    }\n    // Measure the width of the text in the input and\n    // resize the input field to fit.\n    const inputEl = ReactDOM.findDOMNode(this.refs.input)\n    const measureEl = ReactDOM.findDOMNode(this.refs.measure)\n    measureEl.innerText = inputEl.value;\n    measureEl.style.top = `${inputEl.offsetTop}px`;\n    measureEl.style.left = `${inputEl.offsetLeft}px`;\n    // The 10px comes from the 7.5px left padding and 2.5px more of\n    // breathing room.\n    inputEl.style.width = `${measureEl.offsetWidth + 10}px`;\n  }\n\n  select() {\n    ReactDOM.findDOMNode(this.refs.input).select();\n  }\n\n  selectionRange() {\n    const inputEl = ReactDOM.findDOMNode(this.refs.input);\n    return {\n      start: inputEl.selectionStart,\n      end: inputEl.selectionEnd,\n    };\n  }\n\n  focus() {\n    ReactDOM.findDOMNode(this.refs.input).focus();\n  }\n\n  render() {\n    return (\n      <span>\n        <span ref=\"measure\" style={{visibility: 'hidden', position: 'absolute'}} />\n        <input ref=\"input\" type=\"text\" style={{width: 1}} {...this.props} />\n      </span>\n    );\n  }\n}\n\nclass Token extends React.Component {\n  static displayName = \"Token\";\n\n  static propTypes = {\n    className: React.PropTypes.string,\n    selected: React.PropTypes.bool,\n    valid: React.PropTypes.bool,\n    item: React.PropTypes.object,\n    onClick: React.PropTypes.func.isRequired,\n    onDragStart: React.PropTypes.func.isRequired,\n    onEdited: React.PropTypes.func,\n    onAction: React.PropTypes.func,\n    disabled: React.PropTypes.bool,\n    onEditMotion: React.PropTypes.func,\n  }\n\n  static defaultProps = {\n    className: '',\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      editing: false,\n      editingValue: this.props.item.toString(),\n    };\n  }\n\n  componentWillReceiveProps(props) {\n    // never override the text the user is editing if they're looking at it\n    if (this.state.editing) {\n      return;\n    }\n    this.setState({editingValue: props.item.toString()});\n  }\n\n  componentDidUpdate(prevProps, prevState) {\n    if (this.state.editing && !prevState.editing) {\n      this.refs.input.select();\n    }\n  }\n\n  _renderEditing() {\n    return (\n      <SizeToFitInput\n        ref=\"input\"\n        className=\"token-editing-input\"\n        spellCheck=\"false\"\n        value={this.state.editingValue}\n        onKeyDown={this._onEditKeydown}\n        onBlur={this._onEditFinished}\n        onChange={(event) => this.setState({editingValue: event.target.value})}\n      />\n    );\n  }\n\n  _renderViewing() {\n    const classes = classNames({\n      token: true,\n      disabled: this.props.disabled,\n      dragging: this.state.dragging,\n      invalid: !this.props.valid,\n      selected: this.props.selected,\n    });\n\n    let actionButton = null;\n    if (this.props.onAction && !this.props.disabled) {\n      actionButton = (\n        <button type=\"button\" className=\"action\" onClick={this._onAction} tabIndex={-1}>\n          <RetinaImg mode={RetinaImg.Mode.ContentIsMask} name=\"composer-caret.png\" />\n        </button>\n      );\n    }\n\n    return (\n      <div\n        className={`${classes} ${this.props.className}`}\n        onDragStart={this._onDragStart}\n        onDragEnd={this._onDragEnd}\n        draggable={!this.props.disabled}\n        onDoubleClick={this._onDoubleClick}\n        onClick={this._onClick}\n      >\n        {actionButton}\n        {this.props.children}\n      </div>\n    );\n  }\n\n  _onDragStart = (event) => {\n    if (this.props.disabled) return;\n    this.props.onDragStart(event, this.props.item);\n    this.setState({dragging: true});\n  }\n\n  _onDragEnd = () => {\n    if (this.props.disabled) return;\n    this.setState({dragging: false})\n  }\n\n  _onClick = (event) => {\n    if (this.props.disabled) return;\n    this.props.onClick(event, this.props.item);\n  }\n\n  _onDoubleClick = () => {\n    if (this.props.disabled) return;\n    if (this.props.onEditMotion) {\n      this.props.onEditMotion(this.props.item);\n    }\n    if (this.props.onEdited) {\n      this.setState({editing: true});\n    }\n  }\n\n  _onEditKeydown = (event) => {\n    if (this.props.disabled) return;\n    if (event.key === \"Enter\" && this.props.selected && this.props.onEditMotion) {\n      this.props.onEditMotion(this.props.item);\n    }\n    if (['Escape', 'Enter'].includes(event.key)) {\n      this._onEditFinished();\n    }\n  }\n\n  _onEditFinished = () => {\n    if (this.props.disabled) return;\n    if (this.props.onEdited) {\n      this.props.onEdited(this.props.item, this.state.editingValue);\n    }\n    this.setState({editing: false});\n  }\n\n  _onAction = () => {\n    if (this.props.disabled) return;\n    this.props.onAction(this.props.item);\n    event.preventDefault();\n  }\n\n  render() {\n    return this.state.editing ? this._renderEditing() : this._renderViewing();\n  }\n}\n\n/*\nPublic: The TokenizingTextField component displays a list of options as you type and converts them into stylable tokens.\n\nIt wraps the Menu component, which takes care of the typing and keyboard\ninteractions.\n\nSee documentation on the propTypes for usage info.\n\nSection: Component Kit\n*/\nexport default class TokenizingTextField extends React.Component {\n  static displayName = \"TokenizingTextField\";\n\n  static containerRequired = false;\n\n  static Token = Token;\n\n  static propTypes = {\n    className: React.PropTypes.string,\n\n    disabled: React.PropTypes.bool,\n\n    placeholder: React.PropTypes.node,\n\n    // An array of current tokens.\n    //\n    // A token is usually an object type like a `Contact`. The set of\n    // tokens is stored as a prop instead of `state`. This means that when\n    // the set of tokens needs to be changed, it is the parent's\n    // responsibility to make that change.\n    tokens: React.PropTypes.arrayOf(React.PropTypes.object),\n\n    // The maximum number of tokens allowed. When null (the default) and\n    // unlimited number of tokens may be given\n    maxTokens: React.PropTypes.number,\n\n    // A string to pre-fill the input with when the tokens are empty.\n    defaultValue: React.PropTypes.string,\n\n    // A function that, given an object used for tokens, returns a unique\n    // id (key) for that object.\n    //\n    // This is necessary for React to assign each of the subitems and\n    // unique key.\n    tokenKey: React.PropTypes.func.isRequired,\n\n    // A function that, given a token, returns true if the token is valid\n    // and false if the token is invalid. Useful if your implementation of\n    // onAdd allows invalid tokens to be added to the field (ie malformed\n    // email addresses.) Optional.\n    //\n    tokenIsValid: React.PropTypes.func,\n\n    // What each token looks like\n    //\n    // A function that is passed an object and should return React elements\n    // to display that individual token.\n    tokenRenderer: React.PropTypes.func.isRequired,\n\n    tokenClassNames: React.PropTypes.func,\n\n    // The function responsible for providing a list of possible options\n    // given the current input.\n    //\n    // It takes the current input as a value and should return an array of\n    // candidate objects. These objects must be the same type as are passed\n    // to the `tokens` prop.\n    //\n    // The function may either directly return tokens, or may return a\n    // Promise, that resolves with the requested tokens\n    onRequestCompletions: React.PropTypes.func.isRequired,\n\n    // What each suggestion looks like.\n    //\n    // This is passed through to the Menu component's `itemContent` prop.\n    // See components/menu.cjsx for more info.\n    completionNode: React.PropTypes.func.isRequired,\n\n    // Gets called when we we're ready to add whatever it is we're\n    // completing\n    //\n    // It's either passed an array of objects (the same ones used to\n    // render tokens)\n    //\n    // OR\n    //\n    // It's passed the string currently in the input field. The string case\n    // happens on paste and blur.\n    //\n    // The function doesn't need to return anything, but it is generally\n    // responible for mutating the parent's state in a way that eventually\n    // updates this component's `tokens` prop.\n    onAdd: React.PropTypes.func.isRequired,\n\n    // This gets fired when people try and submit a query with a break\n    // character (tab, comma, semicolon, etc). It lets us the caller\n    // determine how to best deal with available options.\n\n    // If this method is not implemented we'll pick the first available\n    // option in the completions\n    onInputTrySubmit: React.PropTypes.func,\n\n    // If implemented lets the caller determine when to cut a token based\n    // on the current input value and the current keydown.\n    shouldBreakOnKeydown: React.PropTypes.func,\n\n    // Gets called when we remove a token\n    //\n    // It's passed an array of objects (the same ones used to render\n    // tokens)\n    //\n    // The function doesn't need to return anything, but it is generally\n    // responible for mutating the parent's state in a way that eventually\n    // updates this component's `tokens` prop.\n    onRemove: React.PropTypes.func.isRequired,\n\n    // Gets called when an existing token is double-clicked and edited.\n    // Do not provide this method if you want to disable editing.\n    //\n    // It's passed a token index, and the new text typed in that location.\n    //\n    // The function doesn't need to return anything, but it is generally\n    // responible for mutating the parent's state in a way that eventually\n    // updates this component's `tokens` prop.\n    onEdit: React.PropTypes.func,\n\n    // This is slightly different than onEdit. onEditMotion gets fired if\n    // the user does an editing-like action on a Token. Double clicking,\n    // etc. This is usefulf for when you don't want the text of the tokens\n    // themselves to be editable, but want to perform some action when the\n    // tokens are double clicked.\n    onEditMotion: React.PropTypes.func,\n\n    // Called when we remove and there's nothing left to remove\n    onEmptied: React.PropTypes.func,\n\n    // Called when the secondary action of the token gets invoked.\n    onTokenAction: React.PropTypes.oneOfType([\n      React.PropTypes.func,\n      React.PropTypes.bool,\n    ]),\n\n    // Called when the input is focused\n    onFocus: React.PropTypes.func,\n\n    // A Prompt used in the head of the menu\n    menuPrompt: React.PropTypes.string,\n\n    // A classSet hash applied to the Menu item\n    menuClassSet: React.PropTypes.object,\n\n    tabIndex: React.PropTypes.number,\n  };\n\n  static defaultProps = {\n    tokens: [],\n    className: '',\n    defaultValue: '',\n    tokenClassNames: () => '',\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      inputValue: props.defaultValue || \"\",\n      completions: [],\n      selectedKeys: [],\n    }\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    if (this.props.tokens.length === 0) {\n      if (this.state.inputValue && this.state.inputValue.length > 0) {\n        this._refreshCompletions(this.state.inputValue);\n      }\n    }\n  }\n\n  componentWillReceiveProps(newProps) {\n    if (this.props.tokens.length === 0 && this.state.inputValue.length === 0) {\n      const newDefaultValue = newProps.defaultValue || \"\"\n      this.setState({inputValue: newDefaultValue});\n      if (newDefaultValue.length > 0) {\n        this._refreshCompletions(newDefaultValue);\n      }\n    }\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  // Maintaining Input State\n\n  _onClick = (event) => {\n    // Don't focus if the focus is already on an input within our field,\n    // like an editable token's input\n    if (event.target.tagName === 'INPUT' && ReactDOM.findDOMNode(this).contains(event.target)) {\n      return;\n    }\n    this.focus();\n  }\n\n  _onDrop = (event) => {\n    if (!event.dataTransfer.types.includes('nylas-token-items')) {\n      return;\n    }\n\n    const data = event.dataTransfer.getData('nylas-token-items');\n    this._onAddItemsFromJSON(data);\n  }\n\n  _onAddItemsFromJSON = (json) => {\n    let items = null;\n\n    try {\n      items = JSON.parse(json, Utils.registeredObjectReviver);\n    } catch (err) {\n      console.error(err)\n      items = null;\n    }\n\n    if (items) {\n      this._addTokens(items);\n    }\n  }\n\n  _onInputFocused = ({noCompletions} = {}) => {\n    this.setState({focus: true});\n    if (this.props.onFocus) {\n      this.props.onFocus();\n    }\n    if (!noCompletions) {\n      this._refreshCompletions();\n    }\n  }\n\n  _onInputKeydown = (event) => {\n    if ([\"Backspace\", \"Delete\"].includes(event.key)) {\n      this._removeTokens(this._selectedTokens());\n    } else if ([\"Escape\"].includes(event.key)) {\n      this._refreshCompletions(\"\", {clear: true})\n    } else if ([\"Tab\", \"Enter\"].includes(event.key)) {\n      this._onInputTrySubmit(event);\n    } else if ([\"ArrowLeft\", \"ArrowRight\"].includes(event.key)) {\n      const delta = event.key === 'ArrowLeft' ? -1 : 1;\n      const {start} = this.refs.input.selectionRange();\n\n      // with tokens selected, arrow keys manipulate the selection\n      if (this.state.selectedKeys.length > 0) {\n        this._onShiftSelection(delta, event);\n        event.preventDefault();\n      // without tokens selected, left arrow key at position 0 selects item\n      } else if ((delta === -1) && (start === 0)) {\n        this._onShiftSelection(delta, event);\n        event.preventDefault();\n      }\n    }\n\n    if (this.props.shouldBreakOnKeydown) {\n      if (this.props.shouldBreakOnKeydown(event)) {\n        event.preventDefault();\n        this._onInputTrySubmit(event);\n      }\n    } else if (event.key === ',') { // comma\n      event.preventDefault();\n      this._onInputTrySubmit(event);\n    }\n  }\n\n  _onSelectAll = () => {\n    const {tokens, tokenKey} = this.props;\n    this.setState({selectedKeys: tokens.map(t => tokenKey(t))});\n  }\n\n  _onSelectNone = () => {\n    this.setState({selectedKeys: []});\n  }\n\n  _onShiftSelection = (delta, event) => {\n    const multiselectModifierPresent = event.shiftKey || event.metaKey;\n    const {tokenKey, tokens} = this.props;\n    const {selectedKeys} = this.state;\n\n    // select the last token on left arrow press if no tokens are selected\n    if (selectedKeys.length === 0) {\n      if (delta === -1) {\n        const key = tokenKey(_.last(tokens));\n        this.setState({selectedKeys: [key]});\n      }\n      return;\n    }\n\n    const headKey = _.last(selectedKeys);\n    const headIdx = tokens.map(t => tokenKey(t)).indexOf(headKey)\n    const nextToken = tokens[headIdx + delta];\n\n    if (multiselectModifierPresent) {\n      if (!nextToken) { return; }\n      const nextKey = tokenKey(nextToken);\n      const beneathHeadKey = selectedKeys[selectedKeys.length - 2];\n\n      if (nextKey === beneathHeadKey) {\n        // If the user is \"walking back\" their selection, deselect the head item\n        // Ex: Shift+Left, Shift+Right undoes prev. Shift+left.\n        this.setState({\n          selectedKeys: selectedKeys.filter(t => t !== headKey),\n        });\n      } else {\n        // If the user is expanding their selection, always filter then add to\n        // ensure the last item in the array is the most recently selected.\n        this.setState({\n          selectedKeys: selectedKeys.filter(t => t !== nextKey).concat([nextKey]),\n        });\n      }\n    } else {\n      this.setState({\n        selectedKeys: nextToken ? [tokenKey(nextToken)] : [],\n      });\n    }\n  }\n\n  _onInputTrySubmit = (event) => {\n    if ((this.state.inputValue || \"\").trim().length === 0) {\n      return;\n    }\n    event.preventDefault();\n    event.stopPropagation();\n\n    const {inputValue, completions} = this.state;\n\n    // default behavior\n    let token = null;\n    if (completions.length > 0) {\n      token = this.refs.completions.getSelectedItem() || completions[0];\n    }\n\n    // allow our container to override behavior\n    if (this.props.onInputTrySubmit) {\n      token = this.props.onInputTrySubmit(inputValue, completions, token);\n      if (typeof token === 'string') {\n        this._addInputValue(token, {skipNameLookup: true});\n        return;\n      }\n    }\n\n    if (token) {\n      this._addToken(token);\n    } else {\n      this._addInputValue()\n    }\n  }\n\n  _onInputChanged = (event) => {\n    const val = event.target.value.trimLeft()\n    this.setState({\n      selectedKeys: [],\n      inputValue: val,\n    });\n\n    this._refreshCompletions(val);\n  }\n\n  _onInputBlurred = (event) => {\n    // Not having a relatedTarget can happen when the whole app blurs. When\n    // this happens we want to leave the field as-is\n    if (!event.relatedTarget) {\n      return;\n    }\n\n    if (event.relatedTarget === ReactDOM.findDOMNode(this)) {\n      return;\n    }\n\n    this._addInputValue();\n    this._refreshCompletions(\"\", {clear: true})\n    this.setState({\n      selectedKeys: [],\n      focus: false,\n    });\n  }\n\n  _clearInput() {\n    this.setState({inputValue: \"\"});\n    this._refreshCompletions(\"\", {clear: true});\n  }\n\n  focus() {\n    this.refs.input.focus();\n  }\n\n  // Managing Tokens\n\n  _addInputValue = (input = this.state.inputValue, options = {}) => {\n    if (this._atMaxTokens()) {\n      return;\n    }\n    if (input.length === 0) {\n      return;\n    }\n    this.props.onAdd(input, options);\n    this._clearInput();\n  }\n\n  _onClickToken = (event, token) => {\n    const {tokenKey, tokens} = this.props;\n    let {selectedKeys} = this.state;\n\n    if (event.shiftKey) {\n      // Expand selection from the currently selected item to the one the user\n      // has clicked. We must walk the items in order so selectedKeys is\n      // an ordered list.\n      let headKey = _.last(selectedKeys);\n      let headIdx = tokens.map(t => tokenKey(t)).indexOf(headKey);\n      const clickedIdx = tokens.indexOf(token);\n\n      if ((clickedIdx === -1) || (clickedIdx === headIdx)) {\n        return;\n      }\n\n      const step = Math.max(-1, Math.min(1, clickedIdx - headIdx));\n\n      do {\n        headIdx += step;\n        headKey = tokenKey(tokens[headIdx]);\n        selectedKeys = selectedKeys.filter(t => t !== headKey).concat([headKey]);\n      } while (headIdx !== clickedIdx);\n    } else if (event.metaKey) {\n      // Expand the selection to include the clicked item, without selecting\n      // the items in between. If the item is already selected, deselect it.\n      const key = tokenKey(token);\n      if (selectedKeys.includes(key)) {\n        selectedKeys = selectedKeys.filter(t => t !== key)\n      } else {\n        selectedKeys = selectedKeys.concat([key]);\n      }\n    } else {\n      // Clear the selection and select just the new token\n      selectedKeys = [tokenKey(token)];\n    }\n\n    this.setState({selectedKeys})\n  }\n\n  _onDragToken = (event, token) => {\n    let tokens = this._selectedTokens()\n    if (tokens.length === 0) {\n      tokens = [token];\n    }\n    const json = JSON.stringify(tokens, Utils.registeredObjectReplacer);\n    event.dataTransfer.setData('nylas-token-items', json);\n    event.dataTransfer.setData('text/plain', tokens.map(t => t.toString()).join(', '));\n    event.dataTransfer.dropEffect = \"move\";\n    event.dataTransfer.effectAllowed = \"move\";\n  }\n\n  _selectedTokens() {\n    return this.props.tokens.filter((t) =>\n      this.state.selectedKeys.includes(this.props.tokenKey(t))\n    );\n  }\n\n  _addToken = (token) => {\n    if (!token) { return; }\n    this._addTokens([token]);\n  }\n\n  _addTokens = (tokens) => {\n    this.props.onAdd(tokens);\n    // It's possible for `_addTokens` to be fired by the menu\n    // asynchronously. When the tokenizing text field is in a popover it's\n    // possible for it to be unmounted before the add tokens fires.\n    if (this._mounted) {\n      this._clearInput();\n      this.focus();\n    }\n  }\n\n  _removeTokens = (tokensToDelete) => {\n    const {inputValue, selectedKeys} = this.state;\n    const {onEmptied, onRemove, tokens, tokenKey} = this.props;\n\n    if ((inputValue.trim().length === 0) && (tokens.length === 0) && onEmptied) {\n      onEmptied();\n    }\n\n    if (tokensToDelete.length) {\n      const tokensToDeleteKeys = tokensToDelete.map(t => tokenKey(t));\n      onRemove(tokensToDelete);\n      this.setState({\n        selectedKeys: selectedKeys.filter(k => !tokensToDeleteKeys.includes(k)),\n      });\n    } else {\n      const lastToken = _.last(tokens);\n      if (lastToken) {\n        const lastTokenKey = tokenKey(lastToken);\n        this.setState({\n          selectedKeys: selectedKeys.filter(k => k !== lastTokenKey).concat([lastTokenKey]),\n        });\n      }\n    }\n  }\n\n  _showDefaultTokenMenu = (token) => {\n    const menu = new remote.Menu()\n    menu.append(new remote.MenuItem({\n      click: () => this._removeTokens([token]),\n      label: 'Remove',\n    }));\n\n    if (this.props.onEditMotion) {\n      menu.append(new remote.MenuItem({\n        label: 'Edit',\n        click: () => this.props.onEditMotion(token),\n      }))\n    }\n    menu.popup(remote.getCurrentWindow());\n  }\n\n  // Copy and Paste\n\n  _onCut = (event) => {\n    if (this.state.selectedKeys.length) {\n      this._onAttachToClipboard(event);\n      // clear the tokens which were selected\n      this._removeTokens(this._selectedTokens())\n      // clear the text in the input if some was selected\n      document.execCommand('delete');\n    }\n  }\n\n  _onCopy = (event) => {\n    if (this.state.selectedKeys.length) {\n      this._onAttachToClipboard(event);\n      event.preventDefault();\n    }\n  }\n\n  _onAttachToClipboard = (event) => {\n    const text = this.state.selectedKeys.join(', ')\n    if (event.clipboardData) {\n      const json = JSON.stringify(this._selectedTokens(), Utils.registeredObjectReplacer);\n      event.clipboardData.setData('text/plain', text);\n      event.clipboardData.setData('nylas-token-items', json);\n\n      const range = this.refs.input.selectionRange();\n      if (range.end > 0) {\n        const inputSelection = this.state.inputValue.substr(range.start, range.end - range.start);\n        event.clipboardData.setData('nylas-token-input', inputSelection);\n      } else {\n        event.clipboardData.setData('nylas-token-input', 'null');\n      }\n    }\n    event.preventDefault()\n  }\n\n  _onPaste = (event) => {\n    const json = event.clipboardData.getData('nylas-token-items');\n    const inputValue = event.clipboardData.getData('nylas-token-input');\n    if (json) {\n      this._onAddItemsFromJSON(json)\n      if (inputValue && inputValue !== 'null') {\n        this.setState({inputValue})\n      }\n      event.preventDefault();\n      return;\n    }\n\n    const text = event.clipboardData.getData('text/plain');\n    if (text) {\n      const newInputValue = this.state.inputValue + text\n      if (RegExpUtils.emailRegex().test(newInputValue)) {\n        this._addInputValue(newInputValue, {skipNameLookup: true});\n        event.preventDefault();\n      } else {\n        this._refreshCompletions(newInputValue);\n      }\n    }\n  }\n\n  // Managing Suggestions\n\n  // Asks `this.props.onRequestCompletions` for new completions given the\n  // current inputValue. Since `onRequestCompletions` can be asynchronous,\n  // this function will handle calling `setState` on `completions` when\n  // `onRequestCompletions` returns.\n  _refreshCompletions = (val = this.state.inputValue, {clear} = {}) => {\n    const usedKeys = this.props.tokens.map(this.props.tokenKey);\n    const removeUsedTokens = (tokens) => {\n      return tokens.filter((t) => !usedKeys.includes(this.props.tokenKey(t)));\n    }\n\n    const tokensOrPromise = this.props.onRequestCompletions(val, {clear});\n\n    if (_.isArray(tokensOrPromise)) {\n      this.setState({completions: removeUsedTokens(tokensOrPromise)})\n    } else if (tokensOrPromise instanceof Promise) {\n      tokensOrPromise.then((tokens) => {\n        if (!this._mounted) { return; }\n        this.setState({completions: removeUsedTokens(tokens)});\n      });\n    } else {\n      console.warn(\"onRequestCompletions returned an invalid type. It must return an Array of tokens or a Promise that resolves to an array of tokens\");\n      this.setState({completions: []});\n    }\n  }\n\n  // Rendering\n\n  _inputComponent() {\n    const props = {\n      onCopy: this._onCopy,\n      onCut: this._onCut,\n      onPaste: this._onPaste,\n      onKeyDown: this._onInputKeydown,\n      onBlur: this._onInputBlurred,\n      onFocus: this._onInputFocused,\n      onChange: this._onInputChanged,\n      disabled: this.props.disabled,\n      tabIndex: this.props.tabIndex || 0,\n      value: this.state.inputValue,\n    };\n\n    // If we can't accept additional tokens, override the events that would\n    // enable additional items to be inserted\n    if (this._atMaxTokens()) {\n      props.className = \"noop-input\"\n      props.onFocus = () => this._onInputFocused({noCompletions: true})\n      props.onPaste = () => 'noop-input'\n      props.onChange = () => 'noop'\n      props.value = ''\n    }\n    return (\n      <SizeToFitInput ref=\"input\" spellCheck=\"false\" {...props} />\n    )\n  }\n\n  _placeholderComponent() {\n    if (this.state.inputValue.length > 0 ||\n        this.props.placeholder === undefined ||\n        this.props.tokens.length > 0) {\n      return false;\n    }\n    return (<div className=\"placeholder\">{this.props.placeholder}</div>)\n  }\n\n  _atMaxTokens() {\n    const {tokens, maxTokens} = this.props;\n    return !maxTokens ? false : (tokens.length >= maxTokens);\n  }\n\n  _renderPromptComponent() {\n    if (!this.props.menuPrompt) {\n      return false;\n    }\n    return (<div className=\"tokenizing-field-label\">{`${this.props.menuPrompt}:`}</div>)\n  }\n\n  _fieldComponents() {\n    const {tokens, tokenKey, tokenIsValid, tokenRenderer, tokenClassNames, onTokenAction, onEdit} = this.props;\n\n    return tokens.map((item) => {\n      const key = tokenKey(item);\n      const valid = tokenIsValid ? tokenIsValid(item) : true;\n\n      const TokenRenderer = tokenRenderer\n      const onAction = (onTokenAction === false) ? null : (onTokenAction || this._showDefaultTokenMenu)\n\n      return (\n        <Token\n          className={tokenClassNames(item)}\n          item={item}\n          key={key}\n          valid={valid}\n          disabled={this.props.disabled}\n          selected={this.state.selectedKeys.includes(key)}\n          onDragStart={this._onDragToken}\n          onClick={this._onClickToken}\n          onEditMotion={this.props.onEditMotion}\n          onEdited={onEdit}\n          onAction={onAction}\n        >\n          <TokenRenderer token={item} />\n        </Token>\n      );\n    });\n  }\n\n  _fieldComponent() {\n    const fieldClasses = classNames({\n      \"tokenizing-field-input\": true,\n      \"at-max-tokens\": this._atMaxTokens(),\n    })\n    return (\n      <KeyCommandsRegion\n        key=\"field-component\"\n        ref=\"field-drop-target\"\n        localHandlers={{\n          \"core:select-all\": this._onSelectAll,\n        }}\n        className=\"tokenizing-field-wrap\"\n        onClick={this._onClick}\n        onDrop={this._onDrop}\n      >\n        {this._renderPromptComponent()}\n        <div className={fieldClasses}>\n          {this._placeholderComponent()}\n          {this._fieldComponents()}\n          {this._inputComponent()}\n        </div>\n      </KeyCommandsRegion>\n    );\n  }\n\n  render() {\n    const classSet = {};\n    classSet[this.props.className] = true;\n\n    const classes = classNames(_.extend({}, classSet, (this.props.menuClassSet || {}), {\n      \"tokenizing-field\": true,\n      \"disabled\": this.props.disabled,\n      \"focused\": this.state.focus,\n      \"empty\": (this.state.inputValue || \"\").trim().length === 0,\n    }));\n\n    return (\n      <Menu\n        className={classes}\n        ref=\"completions\"\n        items={this.state.completions}\n        itemKey={(item) => item.id}\n        itemContext={{inputValue: this.state.inputValue}}\n        itemContent={this.props.completionNode}\n        headerComponents={[this._fieldComponent()]}\n        onFocus={this._onInputFocused}\n        onBlur={this._onInputBlurred}\n        onSelect={this._addToken}\n      />\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/components/undo-toast.jsx",
    "content": "import React, {PropTypes} from 'react'\nimport Toast from './toast'\nimport RetinaImg from './retina-img'\n\n\nfunction UndoToast(props) {\n  const {className, onUndo, undoMessage, ...toastProps} = props\n  return (\n    <Toast\n      {...toastProps}\n      className={`nylas-undo-toast ${className}`}\n    >\n      <div className=\"undo-message-wrapper\">\n        {undoMessage}\n      </div>\n      <div className=\"undo-action-wrapper\" onClick={onUndo}>\n        <RetinaImg\n          name=\"undo-icon@2x.png\"\n          mode={RetinaImg.Mode.ContentIsMask}\n        />\n        <span className=\"undo-action-text\">Undo</span>\n      </div>\n    </Toast>\n  )\n}\n\nUndoToast.propTypes = {\n  className: PropTypes.string,\n  undoMessage: PropTypes.string,\n  onUndo: PropTypes.func,\n}\n\nexport default UndoToast\n"
  },
  {
    "path": "packages/client-app/src/components/unsafe-component.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\n{Utils} = require 'nylas-exports'\n_ = require 'underscore'\n\n###\nPublic: Renders a component provided via the `component` prop, and ensures that\nfailures in the component's code do not cause state inconsistencies elsewhere in\nthe application. This component is used by {InjectedComponent} and\n{InjectedComponentSet} to isolate third party code that could be buggy.\n\nOccasionally, having your component wrapped in {UnsafeComponent} can cause style\nissues. For example, in a Flexbox, the `div.unsafe-component-wrapper` will cause\nyour `flex` and `order` values to be one level too deep. For these scenarios,\nUnsafeComponent looks for `containerStyles` on your React component and attaches\nthem to the wrapper div:\n\n```coffee\nclass MyComponent extends React.Component\n  @displayName: 'MyComponent'\n  @containerStyles:\n    flex: 1\n    order: 2\n```\n\nSection: Component Kit\n###\nclass UnsafeComponent extends React.Component\n  @displayName: 'UnsafeComponent'\n\n  ###\n  Public: React `props` supported by UnsafeComponent:\n\n   - `component` The {React.Component} to display. All other props will be\n     passed on to this component.\n  ###\n  @propTypes:\n    component: React.PropTypes.func.isRequired\n    onComponentDidRender: React.PropTypes.func\n\n  @defaultProps:\n    onComponentDidRender: ->\n\n  componentDidMount: =>\n    @renderInjected()\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not Utils.isEqualReact(nextProps, @props) or\n    not Utils.isEqualReact(nextState, @state)\n\n  componentDidUpdate: =>\n    @renderInjected()\n\n  componentWillUnmount: =>\n    @unmountInjected()\n\n  render: =>\n    <div name=\"unsafe-component-wrapper\" style={@props.component?.containerStyles}></div>\n\n  renderInjected: =>\n    node = ReactDOM.findDOMNode(@)\n    element = null\n    try\n      props = Utils.fastOmit(@props, Object.keys(@constructor.propTypes))\n      Component = @props.component\n      element = <Component key={name} {...props} />\n      @injected = ReactDOM.render(element, node, @props.onComponentDidRender)\n    catch err\n      if NylasEnv.inDevMode()\n        stack = err.stack\n        stackEnd = stack.indexOf('react/lib/')\n        if stackEnd > 0\n          stackEnd = stack.lastIndexOf('\\n', stackEnd)\n          stack = stack.substr(0,stackEnd)\n\n        element = (\n          <div className=\"unsafe-component-exception\">\n            <div className=\"message\">{@props.component.displayName} could not be displayed.</div>\n            <div className=\"trace\">{stack}</div>\n          </div>\n        )\n      else\n        ## TODO\n        # Add some sort of notification code here that lets us know when\n        # production builds are having issues!\n        #\n        element = <div></div>\n\n        @injected = ReactDOM.render(element, node)\n      NylasEnv.reportError(err)\n\n  unmountInjected: =>\n    try\n      node = ReactDOM.findDOMNode(@)\n      ReactDOM.unmountComponentAtNode(node)\n    catch err\n\n  focus: =>\n    @_runInjectedDOMMethod('focus')\n\n  blur: =>\n    @_runInjectedDOMMethod('blur')\n\n  # Private: Attempts to run the DOM method, ie 'focus', on\n  # 1. Any implementation provided by the inner component\n  # 2. Any native implementation provided by the DOM\n  # 3. Ourselves, so that the method always has /some/ effect.\n  #\n  _runInjectedDOMMethod: (method) =>\n    target = null\n    if @injected and @injected[method]\n      target = @injected\n    else if @injected\n      target = ReactDOM.findDOMNode(@injected)\n    else\n      target = ReactDOM.findDOMNode(@)\n\n    target[method]?()\n\nmodule.exports = UnsafeComponent\n"
  },
  {
    "path": "packages/client-app/src/components/webview.jsx",
    "content": "import url from 'url'\nimport React from 'react'\nimport {shell} from 'electron'\nimport ReactDOM from 'react-dom'\nimport classnames from 'classnames';\nimport networkErrors from 'chromium-net-errors';\n\nimport RetinaImg from './retina-img'\n\nclass InitialLoadingCover extends React.Component {\n  static propTypes = {\n    ready: React.PropTypes.bool,\n    error: React.PropTypes.string,\n    onTryAgain: React.PropTypes.func,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {};\n  }\n\n  componentDidMount() {\n    this._slowTimeout = setTimeout(() => {\n      this.setState({slow: true});\n    }, 3500);\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this._slowTimeout);\n    this._slowTimeout = null;\n  }\n\n  render() {\n    const classes = classnames({\n      'webview-cover': true,\n      'ready': this.props.ready,\n      'error': this.props.error,\n      'slow': this.state.slow,\n    });\n\n    let message = this.props.error;\n    if (this.props.error) {\n      message = this.props.error;\n    } else if (this.state.slow) {\n      message = \"Still trying to reach Nylas…\";\n    } else {\n      message = '&nbsp;'\n    }\n\n    return (\n      <div className={classes}>\n        <div style={{flex: 1}} />\n        <RetinaImg\n          className=\"spinner\"\n          style={{width: 20, height: 20}}\n          name=\"inline-loading-spinner.gif\"\n          mode={RetinaImg.Mode.ContentPreserve}\n        />\n        <div className=\"message\">{message}</div>\n        <div className=\"btn try-again\" onClick={this.props.onTryAgain}>Try Again</div>\n        <div style={{flex: 1}} />\n      </div>\n    );\n  }\n}\n\n\nexport default class Webview extends React.Component {\n  static displayName = \"Webview\";\n\n  static propTypes = {\n    src: React.PropTypes.string,\n    onDidFinishLoad: React.PropTypes.func,\n  }\n\n  constructor(props) {\n    super(props);\n    this.state = {\n      ready: false,\n      error: null,\n    };\n  }\n\n  componentDidMount() {\n    this._mounted = true;\n    this._setupWebview(this.props);\n  }\n\n  componentWillReceiveProps(nextProps = {}) {\n    if (this.props.src !== nextProps.src) {\n      this.setState({error: null, webviewLoading: true, ready: false});\n      this._setupWebview(nextProps)\n    }\n  }\n\n  componentWillUnmount() {\n    this._mounted = false;\n  }\n\n  _setupWebview(props) {\n    if (!props.src) return\n    const webview = ReactDOM.findDOMNode(this.refs.webview);\n    const listeners = {\n      'did-fail-load': this._webviewDidFailLoad,\n      'did-finish-load': this._webviewDidFinishLoad,\n      'did-get-response-details': this._webviewDidGetResponseDetails,\n      'console-message': this._onConsoleMessage,\n    }\n    for (const event of Object.keys(listeners)) {\n      webview.removeEventListener(event, listeners[event]);\n    }\n    webview.partition = 'in-memory-only';\n    webview.src = props.src;\n    for (const event of Object.keys(listeners)) {\n      webview.addEventListener(event, listeners[event]);\n    }\n  }\n\n  _onTryAgain = () => {\n    const webview = ReactDOM.findDOMNode(this.refs.webview);\n    webview.reload();\n  }\n\n  _onConsoleMessage = (e) => {\n    if (/^http.+/i.test(e.message)) { shell.openExternal(e.message) }\n    console.log('Guest page logged a message:', e.message);\n  }\n\n  _webviewDidGetResponseDetails = ({httpResponseCode, originalURL}) => {\n    if (!this._mounted) return;\n    if (!originalURL.includes(url.parse(this.props.src).host)) {\n      // This means that some other secondarily loaded resource (like\n      // analytics or Linkedin, etc) got a response. We don't care about\n      // that.\n      return\n    }\n    if (httpResponseCode >= 400) {\n      const error = `\n        Could not reach Nylas. Please try again later.\n        (${originalURL}: ${httpResponseCode})\n      `;\n      this.setState({ready: false, error: error, webviewLoading: false});\n    }\n    this.setState({webviewLoading: false})\n  };\n\n  _webviewDidFailLoad = ({errorCode, validatedURL}) => {\n    if (!this._mounted) return;\n    // \"Operation was aborted\" can be fired when we move between pages quickly.\n    if (errorCode === -3) {\n      return;\n    }\n\n    const e = networkErrors.createByCode(errorCode);\n    const error = `Could not reach ${validatedURL}. ${e ? e.message : errorCode}`;\n    this.setState({ready: false, error: error, webviewLoading: false});\n  }\n\n  _webviewDidFinishLoad = () => {\n    if (!this._mounted) return;\n    // this is sometimes called right after did-fail-load\n    if (this.state.error) return;\n    this.setState({ready: true, webviewLoading: false});\n\n    if (!this.props.onDidFinishLoad) return;\n    const webview = ReactDOM.findDOMNode(this.refs.webview);\n    this.props.onDidFinishLoad(webview)\n  }\n\n  render() {\n    return (\n      <div className=\"webview-wrap\">\n        <webview ref=\"webview\" is partition=\"in-memory-only\" />\n        <div className={`webview-loading-spinner loading-${this.state.webviewLoading}`}>\n          <RetinaImg\n            style={{width: 20, height: 20}}\n            name=\"inline-loading-spinner.gif\"\n            mode={RetinaImg.Mode.ContentPreserve}\n          />\n        </div>\n        <InitialLoadingCover\n          ready={this.state.ready}\n          error={this.state.error}\n          onTryAgain={this._onTryAgain}\n        />\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/config-schema.es6",
    "content": "export default {\n  core: {\n    type: 'object',\n    properties: {\n      workspace: {\n        type: 'object',\n        properties: {\n          mode: {\n            'type': 'string',\n            'default': 'list',\n            'enum': ['split', 'list'],\n          },\n          systemTray: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Show icon in menu bar / system tray\",\n            'platforms': ['darwin', 'linux'],\n          },\n          showImportant: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Show Gmail-style important markers (Gmail Only)\",\n          },\n          showUnreadForAllCategories: {\n            'type': 'boolean',\n            'default': false,\n            'title': \"Show unread counts for all folders / labels\",\n          },\n          use24HourClock: {\n            'type': 'boolean',\n            'default': false,\n            'title': \"Use 24-hour clock\",\n          },\n          interfaceZoom: {\n            'title': \"Override standard interface scaling\",\n            'type': 'number',\n            'default': 1,\n            'advanced': true,\n          },\n        },\n      },\n      disabledPackages: {\n        'type': 'array',\n        'default': [],\n        'items': {\n          type: 'string',\n        },\n      },\n      themes: {\n        'type': 'array',\n        'default': ['ui-light'],\n        'items': {\n          type: 'string',\n        },\n      },\n      keymapTemplate: {\n        'type': 'string',\n        'default': 'Gmail',\n      },\n      attachments: {\n        type: 'object',\n        properties: {\n          downloadPolicy: {\n            'type': 'string',\n            'default': 'on-read',\n            'enum': ['on-read', 'manually'],\n            'enumLabels': ['When Read', 'Manually'],\n            'title': \"Download attachments for new mail\",\n          },\n          displayFilePreview: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Display thumbnail previews for attachments when available. (macOS only)\",\n          },\n        },\n      },\n      reading: {\n        type: 'object',\n        properties: {\n          markAsReadDelay: {\n            'type': 'integer',\n            'default': 500,\n            'enum': [0, 500, 2000, -1],\n            'enumLabels': ['Instantly', 'After ½ Second', 'After 2 Seconds', 'Manually'],\n            'title': \"When reading messages, mark as read\",\n          },\n          autoloadImages: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Automatically load images in viewed messages\",\n          },\n          backspaceDelete: {\n            'type': 'boolean',\n            'default': false,\n            'title': \"Swipe gesture and backspace / delete move messages to trash\",\n          },\n        },\n      },\n      composing: {\n        type: 'object',\n        properties: {\n          spellcheck: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Check messages for spelling\",\n          },\n        },\n      },\n      sending: {\n        type: 'object',\n        properties: {\n          sounds: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Play sound when a message is sent\",\n          },\n          defaultReplyType: {\n            'type': 'string',\n            'default': 'reply-all',\n            'enum': ['reply', 'reply-all'],\n            'enumLabels': ['Reply', 'Reply All'],\n            'title': \"Default reply behavior\",\n          },\n          undoSend: {\n            'type': 'number',\n            'default': 5000,\n            'enum': [5000, 15000, 30000, 60000, 0],\n            'enumLabels': ['5 seconds', '15 seconds', '30 seconds', '60 seconds', 'Disable'],\n            'title': \"After sending, enable undo for\",\n          },\n        },\n      },\n      notifications: {\n        type: 'object',\n        properties: {\n          enabled: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Show notifications for new unread messages\",\n          },\n          sounds: {\n            'type': 'boolean',\n            'default': true,\n            'title': \"Play sound when receiving new mail\",\n          },\n          countBadge: {\n            'type': 'string',\n            'default': 'unread',\n            'enum': ['hide', 'unread', 'total'],\n            'enumLabels': ['Hide Badge', 'Show Unread Count', 'Show Total Count'],\n            'title': \"Show badge on the app icon\",\n          },\n        },\n      },\n    },\n  },\n};\n"
  },
  {
    "path": "packages/client-app/src/config-utils.js",
    "content": "(function() {\n  var isPlainObject, plus, splitKeyPath\n\n  splitKeyPath = function(keyPath) {\n    var char, i, keyPathArray, startIndex, _i, _len;\n    startIndex = 0;\n    keyPathArray = [];\n    if (keyPath == null) {\n      return keyPathArray;\n    }\n    for (i = _i = 0, _len = keyPath.length; _i < _len; i = ++_i) {\n      char = keyPath[i];\n      if (char === '.' && (i === 0 || keyPath[i - 1] !== '\\\\')) {\n        keyPathArray.push(keyPath.substring(startIndex, i));\n        startIndex = i + 1;\n      }\n    }\n    keyPathArray.push(keyPath.substr(startIndex, keyPath.length));\n    return keyPathArray;\n  };\n  isPlainObject = function(value) {\n    return _.isObject(value) && !_.isArray(value);\n  };\n\n  plus = {\n    remove: function(array, element) {\n      var index;\n      index = array.indexOf(element);\n      if (index >= 0) {\n        array.splice(index, 1);\n      }\n      return array;\n    },\n    deepClone: function(object) {\n      if (_.isArray(object)) {\n        return object.map(function(value) {\n          return plus.deepClone(value);\n        });\n      } else if (_.isObject(object) && !_.isFunction(object)) {\n        return _.mapObject(object, (function(value) {\n          return plus.deepClone(value);\n        }))\n      } else {\n        return object;\n      }\n    },\n    deepExtend: function(target) {\n      var i, key, object, result, _i, _len, _ref;\n      result = target;\n      i = 0;\n      while (++i < arguments.length) {\n        object = arguments[i];\n        if (isPlainObject(result) && isPlainObject(object)) {\n          _ref = Object.keys(object);\n          for (_i = 0, _len = _ref.length; _i < _len; _i++) {\n            key = _ref[_i];\n            result[key] = plus.deepExtend(result[key], object[key]);\n          }\n        } else {\n          result = plus.deepClone(object);\n        }\n      }\n      return result;\n    },\n    valueForKeyPath: function(object, keyPath) {\n      var key, keys, _i, _len;\n      keys = splitKeyPath(keyPath);\n      for (_i = 0, _len = keys.length; _i < _len; _i++) {\n        key = keys[_i];\n        object = object[key];\n        if (object == null) {\n          return;\n        }\n      }\n      return object;\n    },\n    setValueForKeyPath: function(object, keyPath, value) {\n      var key, keys;\n      keys = splitKeyPath(keyPath);\n      while (keys.length > 1) {\n        key = keys.shift();\n        if (object[key] == null) {\n          object[key] = {};\n        }\n        object = object[key];\n      }\n      if (value != null) {\n        return object[keys.shift()] = value;\n      } else {\n        return delete object[keys.shift()];\n      }\n    }\n  }\n\n  module.exports = plus\n\n}).call(this)\n"
  },
  {
    "path": "packages/client-app/src/config.coffee",
    "content": "_ = require 'underscore'\n_ = _.extend(_, require('./config-utils'))\n{remote} = require 'electron'\nfs = require 'fs-plus'\nEmitterMixin = require('emissary').Emitter\n{CompositeDisposable, Disposable, Emitter} = require 'event-kit'\n\nColor = require './color'\n\nif process.type is 'renderer'\n  app = remote.getGlobal('application')\n  webContentsId = remote.getCurrentWebContents().getId()\n  errorLogger = NylasEnv.errorLogger\nelse\n  app = global.application\n  webContentsId = null\n  errorLogger = global.errorLogger\n\n# Essential: Used to access all of N1's configuration details.\n#\n# An instance of this class is always available as the `NylasEnv.config` global.\n#\n# ## Getting and setting config settings.\n#\n# ```coffee\n# # Note that with no value set, ::get returns the setting's default value.\n# NylasEnv.config.get('my-package.myKey') # -> 'defaultValue'\n#\n# NylasEnv.config.set('my-package.myKey', 'value')\n# NylasEnv.config.get('my-package.myKey') # -> 'value'\n# ```\n#\n# You may want to watch for changes. Use {::observe} to catch changes to the setting.\n#\n# ```coffee\n# NylasEnv.config.set('my-package.myKey', 'value')\n# NylasEnv.config.observe 'my-package.myKey', (newValue) ->\n#   # `observe` calls immediately and every time the value is changed\n#   console.log 'My configuration changed:', newValue\n# ```\n#\n# If you want a notification only when the value changes, use {::onDidChange}.\n#\n# ```coffee\n# NylasEnv.config.onDidChange 'my-package.myKey', ({newValue, oldValue}) ->\n#   console.log 'My configuration changed:', newValue, oldValue\n# ```\n#\n# ### Value Coercion\n#\n# Config settings each have a type specified by way of a\n# [schema](json-schema.org). For example we might an integer setting that only\n# allows integers greater than `0`:\n#\n# ```coffee\n# # When no value has been set, `::get` returns the setting's default value\n# NylasEnv.config.get('my-package.anInt') # -> 12\n#\n# # The string will be coerced to the integer 123\n# NylasEnv.config.set('my-package.anInt', '123')\n# NylasEnv.config.get('my-package.anInt') # -> 123\n#\n# # The string will be coerced to an integer, but it must be greater than 0, so is set to 1\n# NylasEnv.config.set('my-package.anInt', '-20')\n# NylasEnv.config.get('my-package.anInt') # -> 1\n# ```\n#\n# ## Defining settings for your package\n#\n# Define a schema under a `config` key in your package main.\n#\n# ```coffee\n# module.exports =\n#   # Your config schema\n#   config:\n#     someInt:\n#       type: 'integer'\n#       default: 23\n#       minimum: 1\n#\n#   activate: (state) -> # ...\n#   # ...\n# ```\n#\n# ## Config Schemas\n#\n# We use [json schema](http://json-schema.org) which allows you to define your value's\n# default, the type it should be, etc. A simple example:\n#\n# ```coffee\n# # We want to provide an `enableThing`, and a `thingVolume`\n# config:\n#   enableThing:\n#     type: 'boolean'\n#     default: false\n#   thingVolume:\n#     type: 'integer'\n#     default: 5\n#     minimum: 1\n#     maximum: 11\n# ```\n#\n# The type keyword allows for type coercion and validation. If a `thingVolume` is\n# set to a string `'10'`, it will be coerced into an integer.\n#\n# ```coffee\n# NylasEnv.config.set('my-package.thingVolume', '10')\n# NylasEnv.config.get('my-package.thingVolume') # -> 10\n#\n# # It respects the min / max\n# NylasEnv.config.set('my-package.thingVolume', '400')\n# NylasEnv.config.get('my-package.thingVolume') # -> 11\n#\n# # If it cannot be coerced, the value will not be set\n# NylasEnv.config.set('my-package.thingVolume', 'cats')\n# NylasEnv.config.get('my-package.thingVolume') # -> 11\n# ```\n#\n# ### Supported Types\n#\n# The `type` keyword can be a string with any one of the following. You can also\n# chain them by specifying multiple in an an array. For example\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: ['boolean', 'integer']\n#     default: 5\n#\n# # Then\n# NylasEnv.config.set('my-package.someSetting', 'true')\n# NylasEnv.config.get('my-package.someSetting') # -> true\n#\n# NylasEnv.config.set('my-package.someSetting', '12')\n# NylasEnv.config.get('my-package.someSetting') # -> 12\n# ```\n#\n# #### string\n#\n# Values must be a string.\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: 'string'\n#     default: 'hello'\n# ```\n#\n# #### integer\n#\n# Values will be coerced into integer. Supports the (optional) `minimum` and\n# `maximum` keys.\n#\n#   ```coffee\n#   config:\n#     someSetting:\n#       type: 'integer'\n#       default: 5\n#       minimum: 1\n#       maximum: 11\n#   ```\n#\n# #### number\n#\n# Values will be coerced into a number, including real numbers. Supports the\n# (optional) `minimum` and `maximum` keys.\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: 'number'\n#     default: 5.3\n#     minimum: 1.5\n#     maximum: 11.5\n# ```\n#\n# #### boolean\n#\n# Values will be coerced into a Boolean. `'true'` and `'false'` will be coerced into\n# a boolean. Numbers, arrays, objects, and anything else will not be coerced.\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: 'boolean'\n#     default: false\n# ```\n#\n# #### array\n#\n# Value must be an Array. The types of the values can be specified by a\n# subschema in the `items` key.\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: 'array'\n#     default: [1, 2, 3]\n#     items:\n#       type: 'integer'\n#       minimum: 1.5\n#       maximum: 11.5\n# ```\n#\n# #### object\n#\n# Value must be an object. This allows you to nest config options. Sub options\n# must be under a `properties key`\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: 'object'\n#     properties:\n#       myChildIntOption:\n#         type: 'integer'\n#         minimum: 1.5\n#         maximum: 11.5\n# ```\n#\n# #### color\n#\n# Values will be coerced into a {Color} with `red`, `green`, `blue`, and `alpha`\n# properties that all have numeric values. `red`, `green`, `blue` will be in\n# the range 0 to 255 and `value` will be in the range 0 to 1. Values can be any\n# valid CSS color format such as `#abc`, `#abcdef`, `white`,\n# `rgb(50, 100, 150)`, and `rgba(25, 75, 125, .75)`.\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: 'color'\n#     default: 'white'\n# ```\n#\n# ### Other Supported Keys\n#\n# #### enum\n#\n# All types support an `enum` key. The enum key lets you specify all values\n# that the config setting can possibly be. `enum` _must_ be an array of values\n# of your specified type. Schema:\n#\n# ```coffee\n# config:\n#   someSetting:\n#     type: 'integer'\n#     default: 4\n#     enum: [2, 4, 6, 8]\n# ```\n#\n# Usage:\n#\n# ```coffee\n# NylasEnv.config.set('my-package.someSetting', '2')\n# NylasEnv.config.get('my-package.someSetting') # -> 2\n#\n# # will not set values outside of the enum values\n# NylasEnv.config.set('my-package.someSetting', '3')\n# NylasEnv.config.get('my-package.someSetting') # -> 2\n#\n# # If it cannot be coerced, the value will not be set\n# NylasEnv.config.set('my-package.someSetting', '4')\n# NylasEnv.config.get('my-package.someSetting') # -> 4\n# ```\n#\n# #### title and description\n#\n# The settings view will use the `title` and `description` keys to display your\n# config setting in a readable way. By default the settings view humanizes your\n# config key, so `someSetting` becomes `Some Setting`. In some cases, this is\n# confusing for users, and a more descriptive title is useful.\n#\n# Descriptions will be displayed below the title in the settings view.\n#\n# ```coffee\n# config:\n#   someSetting:\n#     title: 'Setting Magnitude'\n#     description: 'This will affect the blah and the other blah'\n#     type: 'integer'\n#     default: 4\n# ```\n#\n# __Note__: You should strive to be so clear in your naming of the setting that\n# you do not need to specify a title or description!\n#\n# ## Best practices\n#\n# * Don't depend on (or write to) configuration keys outside of your keypath.\n#\nmodule.exports =\nclass Config\n  EmitterMixin.includeInto(this)\n  @schemaEnforcers = {}\n\n  @addSchemaEnforcer: (typeName, enforcerFunction) ->\n    @schemaEnforcers[typeName] ?= []\n    @schemaEnforcers[typeName].push(enforcerFunction)\n\n  @addSchemaEnforcers: (filters) ->\n    for typeName, functions of filters\n      for name, enforcerFunction of functions\n        @addSchemaEnforcer(typeName, enforcerFunction)\n\n  @executeSchemaEnforcers: (keyPath, value, schema) ->\n    error = null\n    types = schema.type\n    types = [types] unless Array.isArray(types)\n    for type in types\n      try\n        enforcerFunctions = @schemaEnforcers[type].concat(@schemaEnforcers['*'])\n        for enforcer in enforcerFunctions\n          # At some point in one's life, one must call upon an enforcer.\n          value = enforcer.call(this, keyPath, value, schema)\n        error = null\n        break\n      catch e\n        error = e\n\n    throw error if error?\n    value\n\n  # Created during initialization, available as `NylasEnv.config`\n  constructor: ->\n    @emitter = new Emitter\n    @schema =\n      type: 'object'\n      properties: {}\n\n    @settings = {}\n    @defaultSettings = {}\n    @transactDepth = 0\n\n    if process.type is 'renderer'\n      {ipcRenderer} = require 'electron'\n      # If new Config() has already been called, unmount it's listener when\n      # we attach ourselves. This is only done during specs, Config is a singleton.\n      ipcRenderer.removeAllListeners('on-config-reloaded')\n      ipcRenderer.on 'on-config-reloaded', (event, settings) =>\n        @updateSettings(settings)\n\n  _logError: (prefix, error) ->\n    error.message = \"#{prefix}: #{error.message}\"\n    console.error(error.message)\n    errorLogger.reportError(error)\n\n  load: ->\n    @updateSettings(@getRawValues())\n\n  ###\n  Section: Config Subscription\n  ###\n\n  # Essential: Add a listener for changes to a given key path. This is different\n  # than {::onDidChange} in that it will immediately call your callback with the\n  # current value of the config entry.\n  #\n  # ### Examples\n  #\n  # You might want to be notified when the themes change. We'll watch\n  # `core.themes` for changes\n  #\n  # ```coffee\n  # NylasEnv.config.observe 'core.themes', (value) ->\n  #   # do stuff with value\n  # ```\n  #\n  # * `keyPath` {String} name of the key to observe\n  # * `callback` {Function} to call when the value of the key changes.\n  #   * `value` the new value of the key\n  #\n  # Returns a {Disposable} with the following keys on which you can call\n  # `.dispose()` to unsubscribe.\n  observe: (keyPath, callback) ->\n    callback(@get(keyPath))\n    @onDidChangeKeyPath keyPath, (event) -> callback(event.newValue)\n\n  # Essential: Add a listener for changes to a given key path. If `keyPath` is\n  # not specified, your callback will be called on changes to any key.\n  #\n  # * `keyPath` (optional) {String} name of the key to observe.\n  # * `callback` {Function} to call when the value of the key changes.\n  #   * `event` {Object}\n  #     * `newValue` the new value of the key\n  #     * `oldValue` the prior value of the key.\n  #     * `keyPath` the keyPath of the changed key\n  #\n  # Returns a {Disposable} with the following keys on which you can call\n  # `.dispose()` to unsubscribe.\n  onDidChange: ->\n    if arguments.length is 1\n      [callback] = arguments\n    else if arguments.length is 2\n      [keyPath, callback] = arguments\n    @onDidChangeKeyPath(keyPath, callback)\n\n  ###\n  Section: Managing Settings\n  ###\n\n  # Essential: Retrieves the setting for the given key.\n  #\n  # ### Examples\n  #\n  # You might want to know what themes are enabled, so check `core.themes`\n  #\n  # ```coffee\n  # NylasEnv.config.get('core.themes')\n  # ```\n  #\n  # * `keyPath` The {String} name of the key to retrieve.\n  #\n  # Returns the value from N1's default settings, the user's configuration\n  # file in the type specified by the configuration schema.\n  get: (keyPath) ->\n    @getRawValue(keyPath)\n\n  # Essential: Sets the value for a configuration setting.\n  #\n  # This value is stored in N1's internal configuration file.\n  #\n  # ### Examples\n  #\n  # You might want to change the themes programmatically:\n  #\n  # ```coffee\n  # NylasEnv.config.set('core.themes', ['ui-light', 'my-custom-theme'])\n  # ```\n  #\n  # * `keyPath` The {String} name of the key.\n  # * `value` The value of the setting. Passing `undefined` will revert the\n  #   setting to the default value.\n  #\n  # Returns a {Boolean}\n  # * `true` if the value was set.\n  # * `false` if the value was not able to be coerced to the type specified in the setting's schema.\n  set: (keyPath, value) ->\n    if value is undefined\n      value = _.valueForKeyPath(@defaultSettings, keyPath)\n    else\n      try\n        value = @makeValueConformToSchema(keyPath, value)\n      catch e\n        @_logError(\"Failed to set keyPath: #{keyPath} = #{value}\", e)\n        return false\n\n    # Ensure that we never send anything but plain javascript objects through\n    # remote. Specifically, we don't want to serialize and send function bodies\n    # across the IPC bridge.\n    if _.isObject(value)\n      value = JSON.parse(JSON.stringify(value))\n\n    @setRawValue(keyPath, value)\n    return true\n\n  # Essential: Restore the setting at `keyPath` to its default value.\n  #\n  # * `keyPath` The {String} name of the key.\n  # * `options` (optional) {Object}\n  unset: (keyPath) ->\n    @set(keyPath, _.valueForKeyPath(@defaultSettings, keyPath))\n\n  # Extended: Retrieve the schema for a specific key path. The schema will tell\n  # you what type the keyPath expects, and other metadata about the config\n  # option.\n  #\n  # * `keyPath` The {String} name of the key.\n  #\n  # Returns an {Object} eg. `{type: 'integer', default: 23, minimum: 1}`.\n  # Returns `null` when the keyPath has no schema specified.\n  getSchema: (keyPath) ->\n    keys = splitKeyPath(keyPath)\n    schema = @schema\n    for key in keys\n      break unless schema?\n      schema = schema.properties?[key]\n    schema\n\n  # Extended: Suppress calls to handler functions registered with {::onDidChange}\n  # and {::observe} for the duration of `callback`. After `callback` executes,\n  # handlers will be called once if the value for their key-path has changed.\n  #\n  # * `callback` {Function} to execute while suppressing calls to handlers.\n  transact: (callback) ->\n    @transactDepth++\n    try\n      callback()\n    finally\n      @transactDepth--\n      @emitChangeEvent()\n\n  ###\n  Section: Internal methods used by core\n  ###\n\n  pushAtKeyPath: (keyPath, value) ->\n    arrayValue = @get(keyPath) ? []\n    unless arrayValue instanceof Array\n      throw new Error(\"Config.pushAtKeyPath is intended for array values. Value #{JSON.stringify(arrayValue)} is not an array.\")\n    result = arrayValue.push(value)\n    @set(keyPath, arrayValue)\n    result\n\n  unshiftAtKeyPath: (keyPath, value) ->\n    arrayValue = @get(keyPath) ? []\n    unless arrayValue instanceof Array\n      throw new Error(\"Config.unshiftAtKeyPath is intended for array values. Value #{JSON.stringify(arrayValue)} is not an array.\")\n    result = arrayValue.unshift(value)\n    @set(keyPath, arrayValue)\n    result\n\n  removeAtKeyPath: (keyPath, value) ->\n    arrayValue = @get(keyPath) ? []\n    unless arrayValue instanceof Array\n      throw new Error(\"Config.removeAtKeyPath is intended for array values. Value #{JSON.stringify(arrayValue)} is not an array.\")\n    result = _.remove(arrayValue, value)\n    @set(keyPath, arrayValue)\n    result\n\n  setSchema: (keyPath, schema) ->\n    unless isPlainObject(schema)\n      throw new Error(\"Error loading schema for #{keyPath}: schemas can only be objects!\")\n\n    unless typeof schema.type?\n      throw new Error(\"Error loading schema for #{keyPath}: schema objects must have a type attribute\")\n\n    rootSchema = @schema\n    if keyPath\n      for key in splitKeyPath(keyPath)\n        rootSchema.type = 'object'\n        rootSchema.properties ?= {}\n        properties = rootSchema.properties\n        properties[key] ?= {}\n        rootSchema = properties[key]\n\n    _.extend rootSchema, schema\n    @setDefaults(keyPath, @extractDefaultsFromSchema(schema))\n    @resetSettingsForSchemaChange()\n\n  ###\n  Section: Private methods managing global settings\n  ###\n\n  updateSettings: (newSettings) =>\n    if !newSettings or _.isEmpty(newSettings)\n      throw new Error(\"Tried to update settings with false-y value: #{newSettings}\")\n    @settings = newSettings\n    @emitChangeEvent()\n\n  getRawValue: (keyPath) ->\n    value = _.valueForKeyPath(@settings, keyPath)\n    defaultValue = _.valueForKeyPath(@defaultSettings, keyPath)\n\n    if value?\n      value = @deepClone(value)\n      _.defaults(value, defaultValue) if isPlainObject(value) and isPlainObject(defaultValue)\n    else\n      value = @deepClone(defaultValue)\n\n    value\n\n  onDidChangeKeyPath: (keyPath, callback) ->\n    oldValue = @get(keyPath)\n    @emitter.on 'did-change', =>\n      newValue = @get(keyPath)\n      unless _.isEqual(oldValue, newValue)\n        event = {oldValue, newValue}\n        oldValue = newValue\n        callback(event)\n\n  isSubKeyPath: (keyPath, subKeyPath) ->\n    return false unless keyPath? and subKeyPath?\n    pathSubTokens = splitKeyPath(subKeyPath)\n    pathTokens = splitKeyPath(keyPath).slice(0, pathSubTokens.length)\n    _.isEqual(pathTokens, pathSubTokens)\n\n  setRawDefault: (keyPath, value) ->\n    _.setValueForKeyPath(@defaultSettings, keyPath, value)\n    @emitChangeEvent()\n\n  setDefaults: (keyPath, defaults) ->\n    if defaults? and isPlainObject(defaults)\n      keys = splitKeyPath(keyPath)\n      for key, childValue of defaults\n        continue unless defaults.hasOwnProperty(key)\n        @setDefaults(keys.concat([key]).join('.'), childValue)\n    else\n      try\n        defaults = @makeValueConformToSchema(keyPath, defaults)\n        @setRawDefault(keyPath, defaults)\n      catch e\n        @_logError(\"Failed to set keypath to default: #{keyPath} = #{JSON.stringify(defaults)}\", e)\n\n  deepClone: (object) ->\n    if object instanceof Color\n      object.clone()\n    else if _.isArray(object)\n      object.map (value) => @deepClone(value)\n    else if isPlainObject(object)\n      _.mapObject object, (value) => @deepClone(value)\n    else\n      object\n\n  extractDefaultsFromSchema: (schema) ->\n    if schema.default?\n      schema.default\n    else if schema.type is 'object' and schema.properties? and isPlainObject(schema.properties)\n      defaults = {}\n      properties = schema.properties or {}\n      defaults[key] = @extractDefaultsFromSchema(value) for key, value of properties\n      defaults\n\n  makeValueConformToSchema: (keyPath, value) ->\n    value = @constructor.executeSchemaEnforcers(keyPath, value, schema) if schema = @getSchema(keyPath)\n    value\n\n  # When the schema is changed / added, there may be values set in the config\n  # that do not conform to the schema. This reset will make them conform.\n  resetSettingsForSchemaChange: ->\n    @transact =>\n      settings = @getRawValues()\n      settings = @makeValueConformToSchema(null, settings)\n      if !settings or _.isEmpty(settings)\n        throw new Error(\"settings is falsey or empty\")\n      app.configPersistenceManager.setSettings(settings, webContentsId)\n      return\n\n  emitChangeEvent: ->\n    @emitter.emit 'did-change' unless @transactDepth > 0\n\n  getRawValues: ->\n    try\n      result = JSON.parse(app.configPersistenceManager.getRawValuesString())\n      if !result or _.isEmpty(result)\n        throw new Error('settings is falsey or empty')\n      return result\n    catch error\n      @_logError('Failed to parse response from getRawValuesString', error)\n      throw error\n\n  setRawValue: (keyPath, value) ->\n    app.configPersistenceManager.setRawValue(keyPath, value, webContentsId)\n    @load()\n\n# Base schema enforcers. These will coerce raw input into the specified type,\n# and will throw an error when the value cannot be coerced. Throwing the error\n# will indicate that the value should not be set.\n#\n# Enforcers are run from most specific to least. For a schema with type\n# `integer`, all the enforcers for the `integer` type will be run first, in\n# order of specification. Then the `*` enforcers will be run, in order of\n# specification.\nConfig.addSchemaEnforcers\n  'integer':\n    coerce: (keyPath, value, schema) ->\n      value = parseInt(value)\n      throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into an int\") if isNaN(value) or not isFinite(value)\n      value\n\n  'number':\n    coerce: (keyPath, value, schema) ->\n      value = parseFloat(value)\n      throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a number\") if isNaN(value) or not isFinite(value)\n      value\n\n  'boolean':\n    coerce: (keyPath, value, schema) ->\n      switch typeof value\n        when 'string'\n          if value.toLowerCase() is 'true'\n            true\n          else if value.toLowerCase() is 'false'\n            false\n          else\n            throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'\")\n        when 'boolean'\n          value\n        else\n          throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a boolean or the string 'true' or 'false'\")\n\n  'string':\n    validate: (keyPath, value, schema) ->\n      unless typeof value is 'string'\n        throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} must be a string\")\n      value\n\n  'null':\n    # null sort of isnt supported. It will just unset in this case\n    coerce: (keyPath, value, schema) ->\n      throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} must be null\") unless value in [undefined, null]\n      value\n\n  'object':\n    coerce: (keyPath, value, schema) ->\n      throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an object\") unless isPlainObject(value)\n      return value unless schema.properties?\n\n      newValue = {}\n      for prop, propValue of value\n        childSchema = schema.properties[prop]\n        if childSchema?\n          try\n            newValue[prop] = @executeSchemaEnforcers(\"#{keyPath}.#{prop}\", propValue, childSchema)\n          catch error\n            console.warn \"Error setting item in object: #{error.message}\"\n        else\n          # Just pass through un-schema'd values\n          newValue[prop] = propValue\n\n      newValue\n\n  'array':\n    coerce: (keyPath, value, schema) ->\n      throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} must be an array\") unless Array.isArray(value)\n      itemSchema = schema.items\n      if itemSchema?\n        newValue = []\n        for item in value\n          try\n            newValue.push @executeSchemaEnforcers(keyPath, item, itemSchema)\n          catch error\n            console.warn \"Error setting item in array: #{error.message}\"\n        newValue\n      else\n        value\n\n  'color':\n    coerce: (keyPath, value, schema) ->\n      color = Color.parse(value)\n      unless color?\n        throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} cannot be coerced into a color\")\n      color\n\n  '*':\n    coerceMinimumAndMaximum: (keyPath, value, schema) ->\n      return value unless typeof value is 'number'\n      if schema.minimum? and typeof schema.minimum is 'number'\n        value = Math.max(value, schema.minimum)\n      if schema.maximum? and typeof schema.maximum is 'number'\n        value = Math.min(value, schema.maximum)\n      value\n\n    validateEnum: (keyPath, value, schema) ->\n      possibleValues = schema.enum\n      return value unless possibleValues? and Array.isArray(possibleValues) and possibleValues.length\n\n      for possibleValue in possibleValues\n        # Using `isEqual` for possibility of placing enums on array and object schemas\n        return value if _.isEqual(possibleValue, value)\n\n      throw new Error(\"Validation failed at #{keyPath}, #{JSON.stringify(value)} is not one of #{JSON.stringify(possibleValues)}\")\n\nisPlainObject = (value) ->\n  _.isObject(value) and not _.isArray(value) and not _.isFunction(value) and not _.isString(value) and not (value instanceof Color)\n\nsplitKeyPath = (keyPath) ->\n  return [] unless keyPath?\n  startIndex = 0\n  keyPathArray = []\n  for char, i in keyPath\n    if char is '.' and (i is 0 or keyPath[i-1] != '\\\\')\n      keyPathArray.push keyPath.substring(startIndex, i)\n      startIndex = i + 1\n  keyPathArray.push keyPath.substr(startIndex, keyPath.length)\n  keyPathArray\n\nwithoutEmptyObjects = (object) ->\n  resultObject = undefined\n  if isPlainObject(object)\n    for key, value of object\n      newValue = withoutEmptyObjects(value)\n      if newValue?\n        resultObject ?= {}\n        resultObject[key] = newValue\n  else\n    resultObject = object\n  resultObject\n"
  },
  {
    "path": "packages/client-app/src/database-helpers.es6",
    "content": "import path from 'path';\nimport Sqlite3 from 'better-sqlite3';\n\nlet app;\nlet errorLogger;\n\nif (process.type === 'renderer') {\n  const remote = require('electron').remote // eslint-disable-line\n  app = remote.getGlobal('application')\n  errorLogger = NylasEnv.errorLogger;\n} else {\n  app = global.application\n  errorLogger = global.errorLogger;\n}\n\nexport function handleUnrecoverableDatabaseError(err = (new Error(`Manually called handleUnrecoverableDatabaseError`))) {\n  const fingerprint = [\"{{ default }}\", \"unrecoverable database error\", err.message];\n  errorLogger.reportError(err, {fingerprint,\n    rateLimit: {\n      ratePerHour: 30,\n      key: `handleUnrecoverableDatabaseError:${err.message}`,\n    },\n  });\n  if (!app) {\n    throw new Error('handleUnrecoverableDatabaseError: `app` is not ready!')\n  }\n  app.rebuildDatabase({detail: err.toString()});\n}\n\nexport async function openDatabase(dbPath) {\n  try {\n    const database = await new Promise((resolve, reject) => {\n      const db = new Sqlite3(dbPath, {});\n      db.on('close', reject)\n      db.on('open', () => {\n        // https://www.sqlite.org/wal.html\n        // WAL provides more concurrency as readers do not block writers and a writer\n        // does not block readers. Reading and writing can proceed concurrently.\n        db.pragma(`journal_mode = WAL`);\n\n        // Note: These are properties of the connection, so they must be set regardless\n        // of whether the database setup queries are run.\n\n        // https://www.sqlite.org/intern-v-extern-blob.html\n        // A database page size of 8192 or 16384 gives the best performance for large BLOB I/O.\n        db.pragma(`main.page_size = 8192`);\n        db.pragma(`main.cache_size = 20000`);\n        db.pragma(`main.synchronous = NORMAL`);\n\n        resolve(db);\n      });\n    })\n    return database\n  } catch (err) {\n    handleUnrecoverableDatabaseError(err);\n    return null\n  }\n}\n\nexport function databasePath(configDirPath, specMode = false) {\n  let dbPath = path.join(configDirPath, 'edgehill.db');\n  if (specMode) {\n    dbPath = path.join(configDirPath, 'edgehill.test.db');\n  }\n  return dbPath\n}\n"
  },
  {
    "path": "packages/client-app/src/date-utils.es6",
    "content": "import moment from 'moment-timezone'\nimport chrono from 'chrono-node'\nimport _ from 'underscore'\n\n// Init locale for moment\nmoment.locale(navigator.language)\n\n// Initialise moment timezone\nconst tz = moment.tz.guess()\nif (!tz) {\n  console.error(\"DateUtils: TimeZone could not be determined. This should not happen!\")\n}\n\nconst yearRegex = / ?YY(YY)?/\n\nconst Hours = {\n  Morning: 9,\n  Evening: 20,\n  Midnight: 24,\n}\n\nconst Days = {\n  // The value for next monday and next weekend varies depending if the current\n  // day is saturday or sunday. See http://momentjs.com/docs/#/get-set/day/\n  NextMonday: day => (day === 0 ? 1 : 8),\n  ThisWeekend: day => (day === 6 ? 13 : 6),\n}\n\nfunction oclock(momentDate) {\n  return momentDate.minute(0).second(0)\n}\n\nfunction morning(momentDate, morningHour = Hours.Morning) {\n  return oclock(momentDate.hour(morningHour))\n}\n\nfunction evening(momentDate, eveningHour = Hours.Evening) {\n  return oclock(momentDate.hour(eveningHour))\n}\n\nfunction midnight(momentDate, midnightHour = Hours.Midnight) {\n  return oclock(momentDate.hour(midnightHour))\n}\n\nfunction isPastDate(inputDateObj, currentDate) {\n  const inputMoment = moment({...inputDateObj, month: inputDateObj.month - 1})\n  const currentMoment = moment(currentDate)\n\n  return inputMoment.isBefore(currentMoment)\n}\n\nconst EnforceFutureDate = new chrono.Refiner();\nEnforceFutureDate.refine = (text, results) => {\n  results.forEach((result) => {\n    const current = _.extend({}, result.start.knownValues, result.start.impliedValues);\n\n    if (result.start.isCertain('weekday') && !result.start.isCertain('day')) {\n      if (isPastDate(current, result.ref)) {\n        result.start.imply('day', result.start.impliedValues.day + 7);\n      }\n    }\n\n    if (result.start.isCertain('day') && !result.start.isCertain('month')) {\n      if (isPastDate(current, result.ref)) {\n        result.start.imply('month', result.start.impliedValues.month + 1);\n      }\n    }\n    if (result.start.isCertain('month') && !result.start.isCertain('year')) {\n      if (isPastDate(current, result.ref)) {\n        result.start.imply('year', result.start.impliedValues.year + 1);\n      }\n    }\n  });\n  return results;\n};\n\nconst chronoFuture = new chrono.Chrono(chrono.options.casualOption());\nchronoFuture.refiners.push(EnforceFutureDate);\n\n\nconst DateUtils = {\n\n  // Localized format: ddd, MMM D, YYYY h:mmA\n  DATE_FORMAT_LONG: 'llll',\n\n  DATE_FORMAT_LONG_NO_YEAR: moment.localeData().longDateFormat('llll').replace(yearRegex, ''),\n\n  // Localized format: MMM D, h:mmA\n  DATE_FORMAT_SHORT: moment.localeData().longDateFormat('lll').replace(yearRegex, ''),\n\n  DATE_FORMAT_llll_NO_TIME: moment.localeData().longDateFormat(\"llll\").replace(/h:mm/, \"\").replace(\" A\", \"\"),\n\n  DATE_FORMAT_LLLL_NO_TIME: moment.localeData().longDateFormat(\"LLLL\").replace(/h:mm/, \"\").replace(\" A\", \"\"),\n\n  timeZone: tz,\n\n  format(momentDate, formatString) {\n    if (!momentDate) return null;\n    return momentDate.format(formatString);\n  },\n\n  utc(momentDate) {\n    if (!momentDate) return null;\n    return momentDate.utc();\n  },\n\n  minutesFromNow(minutes, now = moment()) {\n    return now.add(minutes, 'minutes');\n  },\n\n  hoursFromNow(hours, now = moment()) {\n    return now.add(hours, 'hours');\n  },\n\n  in1Hour() {\n    return DateUtils.minutesFromNow(60);\n  },\n\n  in2Hours() {\n    return DateUtils.minutesFromNow(120);\n  },\n\n  laterToday(now = moment()) {\n    return oclock(now.add(3, 'hours'));\n  },\n\n  tonight(now = moment()) {\n    if (now.hour() >= Hours.Evening) {\n      return midnight(now);\n    }\n    return evening(now)\n  },\n\n  tomorrow(now = moment()) {\n    return morning(now.add(1, 'day'));\n  },\n\n  tomorrowEvening(now = moment()) {\n    return evening(now.add(1, 'day'));\n  },\n\n  thisWeekend(now = moment()) {\n    return morning(now.day(Days.ThisWeekend(now.day())))\n  },\n\n  weeksFromNow(weeks, now = moment()) {\n    return now.add(weeks, 'weeks');\n  },\n\n  nextWeek(now = moment()) {\n    return morning(now.day(Days.NextMonday(now.day())))\n  },\n\n  monthsFromNow(months, now = moment()) {\n    return now.add(months, 'months');\n  },\n\n  nextMonth(now = moment()) {\n    return morning(now.add(1, 'month').date(1))\n  },\n\n  parseDateString(dateLikeString) {\n    const parsed = chrono.parse(dateLikeString)\n    const gotTime = {start: false, end: false};\n    const gotDay = {start: false, end: false};\n    const now = moment();\n    const results = {start: moment(now), end: moment(now), leftoverText: dateLikeString};\n    for (const item of parsed) {\n      for (const val of ['start', 'end']) {\n        if (!(val in item)) {\n          continue;\n        }\n        const {day: knownDay, weekday: knownWeekday, hour: knownHour} = item[val].knownValues;\n        const {year, month, day, hour, minute} = Object.assign(item[val].knownValues, item[val].impliedValues)\n        if (!gotTime[val] && knownHour) {\n          gotTime[val] = true;\n          results[val].minute(minute)\n          results[val].hour(hour)\n\n          if (!gotDay[val]) {\n            results[val].date(day)\n            results[val].month(month - 1) // moment zero-indexes month\n            results[val].year(year)\n          }\n\n          results.leftoverText = results.leftoverText.replace(item.text, '')\n        }\n        if (!gotDay[val] && (knownDay || knownWeekday)) {\n          gotDay[val] = true\n          results[val].year(year)\n          results[val].month(month - 1) // moment zero-indexes month\n          results[val].date(day)\n\n          if (!gotTime) {\n            results[val].hour(hour)\n            results[val].minute(minute)\n          }\n\n          results.leftoverText = results.leftoverText.replace(item.text, '')\n        }\n      }\n    }\n\n    // Make the event a default 1 hour long if it looks like the end date\n    // wasn't assigned, or if it's before the start date.\n    if (results.end.valueOf() === now.valueOf() || results.end <= results.start) {\n      results.end = moment(results.start);\n      results.end.hour(results.end.hour() + 1);\n    }\n\n    return results;\n  },\n\n  /**\n   * Can take almost any string.\n   * e.g. \"Next Monday at 2pm\"\n   * @param {string} dateLikeString - a string representing a date.\n   * @return {moment} - moment object representing date\n   */\n  futureDateFromString(dateLikeString) {\n    const date = chronoFuture.parseDate(dateLikeString)\n    if (!date) {\n      return null\n    }\n    const inThePast = date.valueOf() < Date.now()\n    if (inThePast) {\n      return null\n    }\n    return moment(date)\n  },\n\n\n  /**\n   * Return a formatting string for displaying time\n   *\n   * @param {Date} opts - Object with different properties for customising output\n   * @return {String} The format string based on syntax used by Moment.js\n   *\n   * seconds, upperCase and timeZone are the supported extra options to the format string.\n   * Checks whether or not to use 24 hour time format.\n   */\n  getTimeFormat(opts) {\n    const use24HourClock = NylasEnv.config.get('core.workspace.use24HourClock')\n    let timeFormat = use24HourClock ? \"HH:mm\" : \"h:mm\"\n\n    if (opts && opts.seconds) {\n      timeFormat += \":ss\"\n    }\n\n    // Append meridian if not using 24 hour clock\n    if (!use24HourClock) {\n      if (opts && opts.upperCase) {\n        timeFormat += \" A\"\n      } else {\n        timeFormat += \" a\"\n      }\n    }\n\n    if (opts && opts.timeZone) {\n      timeFormat += \" z\"\n    }\n\n    return timeFormat\n  },\n\n\n  /**\n   * Return a short format date/time\n   *\n   * @param {Date} datetime - Timestamp\n   * @return {String} Formated date/time\n   *\n   * The returned date/time format depends on how long ago the timestamp is.\n   */\n  shortTimeString(datetime) {\n    const now = moment()\n    const diff = now.diff(datetime, 'days', true)\n    const isSameDay = now.isSame(datetime, 'days')\n    let format = null\n\n    if (diff <= 1 && isSameDay) {\n      // Time if less than 1 day old\n      format = DateUtils.getTimeFormat(null)\n    } else if (diff < 2 && !isSameDay) {\n      // Month and day with time if up to 2 days ago\n      format = `MMM D, ${DateUtils.getTimeFormat(null)}`\n    } else if (diff >= 2 && diff < 365) {\n      // Month and day up to 1 year old\n      format = \"MMM D\"\n    } else {\n      // Month, day and year if over a year old\n      format = \"MMM D YYYY\"\n    }\n\n    return moment(datetime).format(format)\n  },\n\n\n  /**\n   * Return a medium format date/time\n   *\n   * @param {Date} datetime - Timestamp\n   * @return {String} Formated date/time\n   */\n  mediumTimeString(datetime) {\n    let format = \"MMMM D, YYYY, \"\n    format += DateUtils.getTimeFormat({seconds: false, upperCase: true, timeZone: false})\n\n    return moment(datetime).format(format)\n  },\n\n\n  /**\n   * Return a long format date/time\n   *\n   * @param {Date} datetime - Timestamp\n   * @return {String} Formated date/time\n   */\n  fullTimeString(datetime) {\n    let format = \"dddd, MMMM Do YYYY, \"\n    format += DateUtils.getTimeFormat({seconds: true, upperCase: true, timeZone: true})\n\n    return moment(datetime).tz(tz).format(format)\n  },\n\n};\n\nexport default DateUtils\n"
  },
  {
    "path": "packages/client-app/src/decorators/inflates-draft-client-id.jsx",
    "content": "import React from 'react';\nimport DraftStore from '../flux/stores/draft-store'\nimport Actions from '../flux/actions'\nimport Utils from '../flux/models/utils'\n\nfunction InflatesDraftClientId(ComposedComponent) {\n  return class extends React.Component {\n    static displayName = ComposedComponent.displayName;\n\n    static propTypes = {\n      draftClientId: React.PropTypes.string,\n      onDraftReady: React.PropTypes.func,\n    }\n\n    static defaultProps = {\n      onDraftReady: () => {},\n    }\n\n    static containerRequired = false;\n\n    constructor(props) {\n      super(props);\n      this.state = {\n        session: null,\n        draft: null,\n      };\n    }\n\n    componentDidMount() {\n      this._mounted = true;\n      this._prepareForDraft(this.props.draftClientId);\n    }\n\n    componentWillUnmount() {\n      this._mounted = false;\n      this._teardownForDraft();\n      this._deleteDraftIfEmpty();\n    }\n\n    componentWillReceiveProps(newProps) {\n      if (newProps.draftClientId !== this.props.draftClientId) {\n        this._teardownForDraft();\n        this._prepareForDraft(newProps.draftClientId);\n      }\n    }\n\n    _prepareForDraft(draftClientId) {\n      if (!draftClientId) {\n        return;\n      }\n      DraftStore.sessionForClientId(draftClientId).then((session) => {\n        const shouldSetState = () =>\n          this._mounted && session.draftClientId === this.props.draftClientId\n\n        if (!shouldSetState()) { return; }\n        this._sessionUnlisten = session.listen(() => {\n          if (!shouldSetState()) { return; }\n          this.setState({draft: session.draft()});\n        });\n\n        this.setState({\n          session: session,\n          draft: session.draft(),\n        });\n        this.props.onDraftReady()\n      });\n    }\n\n    _teardownForDraft() {\n      if (this.state.session) {\n        this.state.session.changes.commit();\n      }\n      if (this._sessionUnlisten) {\n        this._sessionUnlisten();\n      }\n    }\n\n    _deleteDraftIfEmpty() {\n      if (!this.state.draft) {\n        return;\n      }\n      if (this.state.draft.pristine) {\n        Actions.destroyDraft(this.props.draftClientId);\n      }\n    }\n\n    // Returns a promise for use in composer/main.es6, to show the window\n    // once the composer is rendered and focused.\n    focus() {\n      return Utils.waitFor(() => this.refs.composed)\n      .then(() => this.refs.composed.focus())\n      .catch(() => {});\n    }\n\n    render() {\n      if (!this.state.draft) {\n        return <span />;\n      }\n      return <ComposedComponent ref=\"composed\" {...this.props} {...this.state} />;\n    }\n  };\n}\n\nexport default InflatesDraftClientId\n"
  },
  {
    "path": "packages/client-app/src/default-client-helper.coffee",
    "content": "exec = require('child_process').exec\nfs = require('fs')\n{remote, shell} = require('electron')\n\nbundleIdentifier = 'com.nylas.nylas-mail'\n\nclass Windows\n  available: ->\n    true\n\n  isRegisteredForURLScheme: (scheme, callback) ->\n    throw new Error \"isRegisteredForURLScheme is async, provide a callback\" unless callback\n    output = \"\"\n    exec \"reg.exe query HKCU\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\Roaming\\\\OpenWith\\\\UrlAssociations\\\\#{scheme}\\\\UserChoice\", (err, stdout, stderr) ->\n      output += stdout.toString()\n      exec \"reg.exe query HKCU\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\Shell\\\\Associations\\\\UrlAssociations\\\\#{scheme}\\\\UserChoice\", (err, stdout, stderr) ->\n        output += stdout.toString()\n        return callback(err) if callback and err\n        callback(stdout.includes('Nylas'))\n\n  resetURLScheme: (scheme, callback) ->\n    remote.dialog.showMessageBox null, {\n      type: 'info',\n      buttons: ['Learn More'],\n      message: \"Visit Windows Settings to change your default mail client\",\n      detail: \"You'll find Nylas Mail, along with other options, listed in Default Apps > Mail.\",\n    }, ->\n      shell.openExternal('https://support.nylas.com/hc/en-us/articles/229277648')\n\n  registerForURLScheme: (scheme, callback) ->\n    # Ensure that our registry entires are present\n    WindowsUpdater = remote.require('./windows-updater')\n    WindowsUpdater.createRegistryEntries({\n      allowEscalation: true,\n      registerDefaultIfPossible: true,\n    }, (err, didMakeDefault) =>\n      if err\n        remote.dialog.showMessageBox(null, {\n          type: 'error',\n          buttons: ['OK'],\n          message: 'Sorry, an error occurred.',\n          detail: err.message,\n        })\n\n      if not didMakeDefault\n        remote.dialog.showMessageBox null, {\n          type: 'info',\n          buttons: ['Learn More'],\n          defaultId: 1,\n          message: \"Visit Windows Settings to finish making Nylas Mail your mail client\",\n          detail: \"Click 'Learn More' to view instructions in our knowledge base.\",\n        }, ->\n          shell.openExternal('https://support.nylas.com/hc/en-us/articles/229277648')\n\n      callback(null, null) if callback\n    )\n\nclass Linux\n  available: ->\n    true\n\n  isRegisteredForURLScheme: (scheme, callback) ->\n    throw new Error \"isRegisteredForURLScheme is async, provide a callback\" unless callback\n    exec \"xdg-mime query default x-scheme-handler/#{scheme}\", (err, stdout, stderr) ->\n      return callback(err) if err\n      callback(stdout.trim() is 'nylas.desktop')\n\n  resetURLScheme: (scheme, callback) ->\n    exec \"xdg-mime default thunderbird.desktop x-scheme-handler/#{scheme}\", (err, stdout, stderr) ->\n      return callback(err) if callback and err\n      callback(null, null) if callback\n\n  registerForURLScheme: (scheme, callback) ->\n    exec \"xdg-mime default nylas.desktop x-scheme-handler/#{scheme}\", (err, stdout, stderr) ->\n      return callback(err) if callback and err\n      callback(null, null) if callback\n\nclass Mac\n  constructor: ->\n    @secure = false\n\n  available: ->\n    true\n\n  getLaunchServicesPlistPath: (callback) ->\n    secure = \"#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist\"\n    insecure = \"#{process.env.HOME}/Library/Preferences/com.apple.LaunchServices.plist\"\n\n    fs.exists secure, (exists) ->\n      if exists\n        callback(secure)\n      else\n        callback(insecure)\n\n  readDefaults: (callback) ->\n    @getLaunchServicesPlistPath (plistPath) ->\n      tmpPath = \"#{plistPath}.#{Math.random()}\"\n      exec \"plutil -convert json \\\"#{plistPath}\\\" -o \\\"#{tmpPath}\\\"\", (err, stdout, stderr) ->\n        return callback(err) if callback and err\n        fs.readFile tmpPath, (err, data) ->\n          return callback(err) if callback and err\n          try\n            data = JSON.parse(data)\n            callback(data['LSHandlers'], data)\n            fs.unlink(tmpPath)\n          catch e\n            callback(e) if callback and err\n\n  writeDefaults: (defaults, callback) ->\n    @getLaunchServicesPlistPath (plistPath) ->\n      tmpPath = \"#{plistPath}.#{Math.random()}\"\n      exec \"plutil -convert json \\\"#{plistPath}\\\" -o \\\"#{tmpPath}\\\"\", (err, stdout, stderr) ->\n        return callback(err) if callback and err\n        try\n          data = fs.readFileSync(tmpPath)\n          data = JSON.parse(data)\n          data['LSHandlers'] = defaults\n          data = JSON.stringify(data)\n          fs.writeFileSync(tmpPath, data)\n        catch error\n          return callback(error) if callback and error\n\n        exec \"plutil -convert binary1 \\\"#{tmpPath}\\\" -o \\\"#{plistPath}\\\"\", ->\n          fs.unlink(tmpPath)\n          exec \"/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister -kill -r -domain local -domain system -domain user\", (err, stdout, stderr) ->\n            callback(err) if callback\n\n  isRegisteredForURLScheme: (scheme, callback) ->\n    throw new Error \"isRegisteredForURLScheme is async, provide a callback\" unless callback\n    @readDefaults (defaults) ->\n      for def in defaults\n        if def.LSHandlerURLScheme is scheme\n          return callback(def.LSHandlerRoleAll is bundleIdentifier)\n      callback(false)\n\n  resetURLScheme: (scheme, callback) ->\n    @readDefaults (defaults) =>\n      # Remove anything already registered for the scheme\n      for ii in [defaults.length-1..0] by -1\n        if defaults[ii].LSHandlerURLScheme is scheme\n          defaults.splice(ii, 1)\n      @writeDefaults(defaults, callback)\n\n  registerForURLScheme: (scheme, callback) ->\n    @readDefaults (defaults) =>\n      # Remove anything already registered for the scheme\n      for ii in [defaults.length-1..0] by -1\n        if defaults[ii].LSHandlerURLScheme is scheme\n          defaults.splice(ii, 1)\n\n      # Add our scheme default\n      defaults.push\n        LSHandlerURLScheme: scheme\n        LSHandlerRoleAll: bundleIdentifier\n\n      @writeDefaults(defaults, callback)\n\n\nif process.platform is 'darwin'\n  module.exports = Mac\nelse if process.platform is 'linux'\n  module.exports = Linux\nelse if process.platform is 'win32'\n  module.exports = Windows\nelse\n  module.exports = {}\n\nmodule.exports.Mac = Mac\nmodule.exports.Linux = Linux\nmodule.exports.Windows = Windows\n"
  },
  {
    "path": "packages/client-app/src/deprecate-utils.coffee",
    "content": "class DeprecateUtils\n  # See\n  # http://www.codeovertones.com/2011/08/how-to-print-stack-trace-anywhere-in.html\n  @parseStack: (stackString) ->\n    stack = stackString\n      .replace(/^[^\\(]+?[\\n$]/gm, '')\n      .replace(/^\\s+at\\s+/gm, '')\n      .replace(/^Object.<anonymous>\\s*\\(/gm, '{anonymous}()@')\n      .split('\\n')\n    return stack\n\n  @warn: (condition, message) ->\n    console.warn message if condition\n\n  @deprecate: (fnName, newName, ctx, fn) ->\n    if NylasEnv.inDevMode() and not NylasEnv.inSpecMode()\n      warn = true\n      newFn = =>\n        stack = DeprecateUtils.parseStack((new Error()).stack)\n        DeprecateUtils.warn(\n          warn,\n          \"Deprecation warning! #{fnName} is deprecated and will be removed soon.\n          Use #{newName} instead.\\nCheck your code at #{stack[1]}\"\n        )\n        warn = false\n        return fn.apply(ctx, arguments)\n      return Object.assign(newFn, fn)\n    return fn\n\nmodule.exports = DeprecateUtils\n"
  },
  {
    "path": "packages/client-app/src/dom-utils.coffee",
    "content": "_ = require 'underscore'\n_s = require 'underscore.string'\n\nDOMUtils =\n  Mutating:\n    replaceFirstListItem: (li, replaceWith) ->\n      list = DOMUtils.closest(li, \"ul, ol\")\n\n      if replaceWith.length is 0\n        replaceWith = replaceWith.replace /\\s/g, \"&nbsp;\"\n        text = document.createElement(\"div\")\n        text.innerHTML = \"<br>\"\n      else\n        replaceWith = replaceWith.replace /\\s/g, \"&nbsp;\"\n        text = document.createElement(\"span\")\n        text.innerHTML = \"#{replaceWith}\"\n\n      if list.querySelectorAll('li').length <= 1\n        # Delete the whole list and replace with text\n        list.parentNode.replaceChild(text, list)\n      else\n        # Delete the list item and prepend the text before the rest of the\n        # list\n        li.parentNode.removeChild(li)\n        list.parentNode.insertBefore(text, list)\n\n      child = text.childNodes[0] ? text\n      index = Math.max(replaceWith.length - 1, 0)\n      selection = document.getSelection()\n      selection.setBaseAndExtent(child, index, child, index)\n\n    removeEmptyNodes: (node) ->\n      Array::slice.call(node.childNodes).forEach (child) ->\n        if child.textContent is ''\n          node.removeChild(child)\n        else\n          DOMUtils.Mutating.removeEmptyNodes(child)\n\n    # Given a bunch of elements, it will go through and find all elements\n    # that are adjacent to that one of the same type. For each set of\n    # adjacent elements, it will put all children of those elements into\n    # the first one and delete the remaining elements.\n    collapseAdjacentElements: (els=[]) ->\n      return if els.length is 0\n      els = Array::slice.call(els)\n\n      seenEls = []\n      toMerge = []\n\n      for el in els\n        continue if el in seenEls\n        adjacent = DOMUtils.collectAdjacent(el)\n        seenEls = seenEls.concat(adjacent)\n        continue if adjacent.length <= 1\n        toMerge.push(adjacent)\n\n      anchors = []\n      for mergeSet in toMerge\n        anchor = mergeSet[0]\n        remaining = mergeSet[1..-1]\n        for el in remaining\n          while (el.childNodes.length > 0)\n            anchor.appendChild(el.childNodes[0])\n        DOMUtils.Mutating.removeElements(remaining)\n        anchors.push(anchor)\n\n      return anchors\n\n    removeElements: (elements=[]) ->\n      for el in elements\n        try\n          if el.parentNode then el.parentNode.removeChild(el)\n        catch\n          # This can happen if we've already removed ourselves from the\n          # node or it no longer exists\n          continue\n      return elements\n\n    applyTextInRange: (range, selection, newText) ->\n      range.deleteContents()\n      node = document.createTextNode(newText)\n      range.insertNode(node)\n      range.selectNode(node)\n      selection.removeAllRanges()\n      selection.addRange(range)\n\n    getRangeAtAndSelectWord: (selection, index) ->\n      range = selection.getRangeAt(index)\n\n      # On Windows, right-clicking a word does not select it at the OS-level.\n      if range.collapsed\n        DOMUtils.Mutating.selectWordContainingRange(range)\n        range = selection.getRangeAt(index)\n      return range\n\n    # This method finds the bounding points of the word that the range\n    # is currently within and selects that word.\n    selectWordContainingRange: (range) ->\n      selection = document.getSelection()\n      node = selection.focusNode\n      text = node.textContent\n      wordStart = _s.reverse(text.substring(0, selection.focusOffset)).search(/\\s/)\n      if wordStart is -1\n        wordStart = 0\n      else\n        wordStart = selection.focusOffset - wordStart\n      wordEnd = text.substring(selection.focusOffset).search(/\\s/)\n      if wordEnd is -1\n        wordEnd = text.length\n      else\n        wordEnd += selection.focusOffset\n\n      selection.removeAllRanges()\n      range = new Range()\n      range.setStart(node, wordStart)\n      range.setEnd(node, wordEnd)\n      selection.addRange(range)\n\n    moveSelectionToIndexInAnchorNode: (selection, index) ->\n      return unless selection.isCollapsed\n      node = selection.anchorNode\n      selection.setBaseAndExtent(node, index, node, index)\n\n    moveSelectionToEnd: (selection) ->\n      return unless selection.isCollapsed\n      node = DOMUtils.findLastTextNode(selection.anchorNode)\n      index = node.length\n      selection.setBaseAndExtent(node, index, node, index)\n\n  getSelectionRectFromDOM: (selection) ->\n    selection ?= document.getSelection()\n    node = selection.anchorNode\n    if node.nodeType is Node.TEXT_NODE\n      r = document.createRange()\n      r.selectNodeContents(node)\n      return r.getBoundingClientRect()\n    else if node.nodeType is Node.ELEMENT_NODE\n      return node.getBoundingClientRect()\n    else\n      return null\n\n  isSelectionInTextNode: (selection) ->\n    selection ?= document.getSelection()\n    return false unless selection\n    return selection.isCollapsed and selection.anchorNode.nodeType is Node.TEXT_NODE and selection.anchorOffset > 0\n\n  isAtTabChar: (selection) ->\n    selection ?= document.getSelection()\n    if DOMUtils.isSelectionInTextNode(selection)\n      return selection.anchorNode.textContent[selection.anchorOffset - 1] is \"\\t\"\n    else return false\n\n  isAtBeginningOfDocument: (dom, selection) ->\n    selection ?= document.getSelection()\n    return false if not selection.isCollapsed\n    return false if selection.anchorOffset > 0\n    return true if dom.childNodes.length is 0\n    return true if selection.anchorNode is dom\n    firstChild = dom.childNodes[0]\n    return selection.anchorNode is firstChild\n\n  atStartOfList: ->\n    selection = document.getSelection()\n    anchor = selection.anchorNode\n    return false if not selection.isCollapsed\n    return true if anchor?.nodeName is \"LI\"\n    return false if selection.anchorOffset > 0\n    li = DOMUtils.closest(anchor, \"li\")\n    return unless li\n    return DOMUtils.isFirstChild(li, anchor)\n\n  # Selectors for input types\n  inputTypes: -> \"input, textarea, *[contenteditable]\"\n\n  # https://developer.mozilla.org/en-US/docs/Web/API/Element/closest\n  # Only Elements (not Text nodes) have the `closest` method\n  closest: (node, selector) ->\n    if node instanceof HTMLElement\n      return node.closest(selector)\n    else if node?.parentNode\n      return DOMUtils.closest(node.parentNode, selector)\n    else return null\n\n  closestAtCursor: (selector) ->\n    selection = document.getSelection()\n    return unless selection?.isCollapsed\n    return DOMUtils.closest(selection.anchorNode, selector)\n\n  closestElement: (node) ->\n    if node instanceof HTMLElement\n      return node\n    else if node?.parentNode\n      return DOMUtils.closestElement(node.parentNode)\n    else return null\n\n  isInList: ->\n    li = DOMUtils.closestAtCursor(\"li\")\n    list = DOMUtils.closestAtCursor(\"ul, ol\")\n    return li and list\n\n  # Returns an array of all immediately adjacent nodes of a particular\n  # nodeName relative to the root. Includes the root if it has the correct\n  # nodeName.\n  #\n  # nodName is optional. if left blank it'll be the nodeName of the root\n  collectAdjacent: (root, nodeName) ->\n    nodeName ?= root.nodeName\n    adjacent = []\n\n    node = root\n    while node.nextSibling?.nodeName is nodeName\n      adjacent.push(node.nextSibling)\n      node = node.nextSibling\n\n    if root.nodeName is nodeName\n      adjacent.unshift(root)\n\n    node = root\n    while node.previousSibling?.nodeName is nodeName\n      adjacent.unshift(node.previousSibling)\n      node = node.previousSibling\n\n    return adjacent\n\n  getNodeIndex: (context, nodeToFind) =>\n    DOMUtils.indexOfNodeInSimilarNodes(context, nodeToFind)\n\n  getRangeInScope: (scope) =>\n    selection = document.getSelection()\n    return null if not DOMUtils.selectionInScope(selection, scope)\n    try\n      range = selection.getRangeAt(0)\n    catch\n      console.warn \"Selection is not returning a range\"\n      return document.createRange()\n    range\n\n  selectionInScope: (selection, scope) ->\n    return false if not selection?\n    return false if not scope?\n    return (scope.contains(selection.anchorNode) and\n            scope.contains(selection.focusNode))\n\n  isEmptyBoundingRect: (rect) ->\n    rect.top is 0 and rect.bottom is 0 and rect.left is 0 and rect.right is 0\n\n  atEndOfContent: (selection, rootScope, containerScope) ->\n    containerScope ?= rootScope\n    if selection.isCollapsed\n\n      # We need to use `lastChild` instead of `lastElementChild` because\n      # we need to eventually check if the `selection.focusNode`, which is\n      # usually a TEXT node, is equal to the returned `lastChild`.\n      # `lastElementChild` will not return TEXT nodes.\n      #\n      # Unfortunately, `lastChild` can sometime return COMMENT nodes and\n      # other blank TEXT nodes that we don't want to compare to.\n      #\n      # For example, if you have the structure:\n      # <div>\n      #   <p>Foo</p>\n      # </div>\n      #\n      # The div may have 2 childNodes and 1 childElementNode. The 2nd\n      # hidden childNode is a TEXT node with a data of \"\\n\". I actually\n      # want to return the <p></p>.\n      #\n      # However, The <p> element may have 1 childNode and 0\n      # childElementNodes. In that case I DO want to return the TEXT node\n      # that has the data of \"foo\"\n      lastChild = DOMUtils.lastNonBlankChildNode(containerScope)\n\n      # Special case for a completely empty contenteditable.\n      # In this case `lastChild` will be null, but we are definitely at\n      # the end of the content.\n      if containerScope is rootScope\n        return true if containerScope.childNodes.length is 0\n\n      return false unless lastChild\n\n      # NOTE: `.contains` returns true if `lastChild` is equal to\n      # `selection.focusNode`\n      #\n      # See: http://ejohn.org/blog/comparing-document-position/\n      inLastChild = lastChild.contains(selection.focusNode)\n\n      # We should do true object identity here instead of `.isEqualNode`\n      isLastChild = lastChild is selection.focusNode\n\n      if isLastChild\n        if selection.focusNode?.length\n          atEndIndex = selection.focusOffset is selection.focusNode.length\n        else\n          atEndIndex = selection.focusOffset is 0\n        return atEndIndex\n      else if inLastChild\n        DOMUtils.atEndOfContent(selection, rootScope, lastChild)\n      else return false\n\n    else return false\n\n  lastNonBlankChildNode: (node) ->\n    lastNode = null\n    for childNode in node.childNodes by -1\n      if childNode.nodeType is Node.TEXT_NODE\n        if DOMUtils.isBlankTextNode(childNode)\n          continue\n        else\n          return childNode\n      else if childNode.nodeType is Node.ELEMENT_NODE\n        return childNode\n      else continue\n    return lastNode\n\n  lastDescendent: (node) ->\n    return null unless node\n    if node.childNodes.length > 0\n      return DOMUtils.lastNode(node.childNodes[node.childNodes.length - 1])\n    else return null\n\n  findLastTextNode: (node) ->\n    return null unless node\n    return node if node.nodeType is Node.TEXT_NODE\n    for childNode in node.childNodes by -1\n      if childNode.nodeType is Node.TEXT_NODE\n        return childNode\n      else if childNode.nodeType is Node.ELEMENT_NODE\n        return DOMUtils.findLastTextNode(childNode)\n      else continue\n    return null\n\n  # Only looks down node trees with one child for a text node.\n  # Returns null if there's no single text node\n  findOnlyChildTextNode: (node) ->\n    return null unless node\n    return node if node.nodeType is Node.TEXT_NODE\n    return null if node.childNodes.length > 1\n    return DOMUtils.findOnlyChildTextNode(node.childNodes[0])\n\n  findFirstTextNode: (node) ->\n    return null unless node\n    return node if node.nodeType is Node.TEXT_NODE\n    for childNode in node.childNodes\n      if childNode.nodeType is Node.TEXT_NODE\n        return childNode\n      else if childNode.nodeType is Node.ELEMENT_NODE\n        return DOMUtils.findFirstTextNode(childNode)\n      else continue\n    return null\n\n  isBlankTextNode: (node) ->\n    return if not node?.data\n    # \\u00a0 is &nbsp;\n    node.data.replace(/\\u00a0/g, \"x\").trim().length is 0\n\n  indexOfNodeInSimilarNodes: (context, nodeToFind) ->\n    if nodeToFind.isEqualNode(context)\n      return 0\n\n    treeWalker = document.createTreeWalker context\n    idx = 0\n    while treeWalker.nextNode()\n      if treeWalker.currentNode.isEqualNode nodeToFind\n        if treeWalker.currentNode.isSameNode nodeToFind\n          return idx\n        idx += 1\n\n    return -1\n\n  # This is an optimization of findSimilarNodes which avoids tons of extra work\n  # scanning a large DOM if all we're going to do is get item at index [0]. It\n  # returns once it has found the similar node at the index desired.\n  findSimilarNodeAtIndex: (context, nodeToFind, desiredIdx) ->\n    if desiredIdx is 0 and nodeToFind.isEqualNode(context)\n      return context\n\n    treeWalker = document.createTreeWalker context\n    idx = 0\n    while treeWalker.nextNode()\n      if treeWalker.currentNode.isEqualNode nodeToFind\n        return treeWalker.currentNode if desiredIdx is idx\n        idx += 1\n\n    return null\n\n  findCharacter: (context, character) ->\n    node = null\n    index = null\n    treeWalker = document.createTreeWalker(context, NodeFilter.SHOW_TEXT)\n    while currentNode = treeWalker.nextNode()\n      i = currentNode.data.indexOf(character)\n      if i >= 0\n        node = currentNode\n        index = i\n        break\n    return {node, index}\n\n  escapeHTMLCharacters: (text) ->\n    map =\n      '&': '&amp;',\n      '<': '&lt;',\n      '>': '&gt;',\n      '\"': '&quot;',\n      \"'\": '&#039;'\n    text.replace /[&<>\"']/g, (m) -> map[m]\n\n  # Checks to see if a particular node is visible and any of its parents\n  # are visible.\n  #\n  # WARNING. This is a fairly expensive operation and should be used\n  # sparingly.\n  nodeIsVisible: (node) ->\n    while node and node.nodeType is Node.ELEMENT_NODE\n      style = window.getComputedStyle(node)\n      node = node.parentNode\n      continue unless style?\n      isInvisible = (\n        [0, \"0\"].includes(style.opacity) or\n        style.visibility is \"hidden\" or\n        style.display is \"none\" or\n        [0, \"0\", \"0px\"].includes(style.width) or\n        [0, \"0\", \"0px\"].includes(style.height)\n      )\n      if isInvisible\n        return false\n    return true\n\n  # This checks for the `offsetParent` to be null. This will work for\n  # hidden elements, but not if they are in a `position:fixed` container.\n  #\n  # It is less thorough then Utils.nodeIsVisible, but is ~16x faster!!\n  # http://jsperf.com/check-hidden\n  # http://stackoverflow.com/a/21696585/793472\n  nodeIsLikelyVisible: (node) -> node.offsetParent isnt null\n\n  # Finds all of the non blank node in a {Document} object or HTML string.\n  #\n  # - `elementOrHTML` a dom element or an HTML string. If passed a\n  # string, it will use `DOMParser` to convert it into a DOM object.\n  #\n  # \"Non blank\" is defined as any node whose `textContent` returns a\n  # whitespace string.\n  #\n  # It will also reject nodes we see are invisible due to basic CSS\n  # properties.\n  #\n  # Returns an array of DOM Nodes\n  nodesWithContent: (elementOrHTML) ->\n    nodes = []\n    if _.isString(elementOrHTML)\n      domParser = new DOMParser()\n      doc = domParser.parseFromString(elementOrHTML, \"text/html\")\n      allNodes = doc.body.childNodes\n    else if elementOrHTML?.childNodes\n      allNodes = elementOrHTML.childNodes\n    else return nodes\n\n    # We need to check `childNodes` instead of `children` to look for\n    # plain Text nodes.\n    for node in allNodes by -1\n      if node.nodeName is \"IMG\" or node.nodeName is \"HR\"\n        nodes.unshift node\n\n      # It's important to use `textContent` and NOT `innerText`.\n      # `innerText` causes a full reflow on every call because it\n      # calcaultes CSS styles to determine if the text is truly visible or\n      # not. This utility method must NOT cause a reflow. We instead will\n      # check for basic cases ourselves.\n      if (node.textContent ? \"\").trim().length is 0\n        continue\n\n      if node.style?.opacity is 0 or node.style?.opacity is \"0\" or node.style?.visibility is \"hidden\" or node.style?.display is \"none\"\n        continue\n\n      nodes.unshift node\n\n    # No nodes with content found!\n    return nodes\n\n  parents: (node) ->\n    nodes = []\n    nodes.unshift(node) while node = node.parentNode\n    return nodes\n\n  # Returns true if the node is the first child of the root, is the root,\n  # or is the first child of the first child of the root, etc.\n  isFirstChild: (root, node) ->\n    return false unless root and node\n    return true if root is node\n    return false unless root.childNodes[0]\n    return true if root.childNodes[0] is node\n    return DOMUtils.isFirstChild(root.childNodes[0], node)\n\n  commonAncestor: (nodes=[], parentFilter) ->\n    return null if nodes.length is 0\n\n    nodes = Array::slice.call(nodes)\n\n    minDepth = Number.MAX_VALUE\n    # Sometimes we can potentially have tons of REALLY deeply nested\n    # nodes. Since we're looking for a common ancestor we can really speed\n    # this up by keeping track of the min depth reached. We know that we\n    # won't need to check past that.\n    getParents = (node) ->\n      parentNodes = [node]\n      depth = 0\n      while node = node.parentNode\n        if parentFilter\n          parentNodes.unshift(node) if parentFilter(node)\n        else\n          parentNodes.unshift(node)\n        depth += 1\n        if depth > minDepth then break\n      minDepth = Math.min(minDepth, depth)\n      return parentNodes\n\n    # _.intersection will preserve the ordering of the parent node arrays.\n    # parents are ordered top to bottom, so the last node is the most\n    # specific common ancenstor\n    _.last(_.intersection.apply(null, nodes.map(getParents)))\n\n  scrollAdjustmentToMakeNodeVisibleInContainer: (node, container) ->\n    return unless node\n    nodeRect = node.getBoundingClientRect()\n    containerRect = container.getBoundingClientRect()\n    return @scrollAdjustmentToMakeRectVisibleInRect(nodeRect, containerRect)\n\n  scrollAdjustmentToMakeRectVisibleInRect: (nodeRect, containerRect) ->\n    distanceBelowBottom = (nodeRect.top + nodeRect.height) - (containerRect.top + containerRect.height)\n    if distanceBelowBottom >= 0\n      return distanceBelowBottom\n\n    distanceAboveTop = containerRect.top - nodeRect.top\n    if distanceAboveTop >= 0\n      return -distanceAboveTop\n\n    return 0\n\n  # Produces a list of indexed text contained within a given node. Returns a\n  # list of objects of the form:\n  #   {start, end, node, text}\n  #\n  # The text being indexed is intended to approximate the rendered content visible\n  # to the user. This includes the nodeValue of any text nodes, and \"\\n\" for any\n  # DIV or BR elements.\n  getIndexedTextContent: (node) ->\n    items = []\n    treeWalker = document.createTreeWalker(node, NodeFilter.SHOW_ELEMENT + NodeFilter.SHOW_TEXT)\n    position = 0\n    while treeWalker.nextNode()\n      node = treeWalker.currentNode\n      if node.tagName is \"BR\" or node.nodeType is Node.TEXT_NODE or node.tagName is \"DIV\"\n        text = if node.nodeType is Node.TEXT_NODE then node.nodeValue else \"\\n\"\n        item =\n          start: position\n          end: position + text.length\n          node: node\n          text: text\n        items.push(item)\n        position += text.length\n    return items\n\n  # Returns true if the inner range is fully contained within the outer range\n  rangeInRange: (inner, outer) ->\n    return outer.isPointInRange(inner.startContainer, inner.startOffset) and outer.isPointInRange(inner.endContainer, inner.endOffset)\n\n  # Returns true if the given ranges overlap\n  rangeOverlapsRange: (range1, range2) ->\n    return range2.isPointInRange(range1.startContainer, range1.startOffset) or range1.isPointInRange(range2.startContainer, range2.startOffset)\n\n  # Returns true if the first range starts or ends within the second range.\n  # Unlike rangeOverlapsRange, returns false if range2 is fully within range1.\n  rangeStartsOrEndsInRange: (range1, range2) ->\n    return range2.isPointInRange(range1.startContainer, range1.startOffset) or range2.isPointInRange(range1.endContainer, range1.endOffset)\n\n  # Accepts a Range or a Node, and returns true if the current selection starts\n  # or ends within it. Useful for knowing if a DOM modification will break the\n  # current selection.\n  selectionStartsOrEndsIn: (rangeOrNode) ->\n    selection = document.getSelection()\n    return false unless (selection and selection.rangeCount>0)\n    if rangeOrNode instanceof Range\n      return @rangeStartsOrEndsInRange(selection.getRangeAt(0), rangeOrNode)\n    else if rangeOrNode instanceof Node\n      range = new Range()\n      range.selectNode(rangeOrNode)\n      return @rangeStartsOrEndsInRange(selection.getRangeAt(0), range)\n    else\n      return false\n\n  # Accepts a Range or a Node, and returns true if the current selection is fully\n  # contained within it.\n  selectionIsWithin: (rangeOrNode) ->\n    selection = document.getSelection()\n    return false unless (selection and selection.rangeCount>0)\n    if rangeOrNode instanceof Range\n      return @rangeInRange(selection.getRangeAt(0), rangeOrNode)\n    else if rangeOrNode instanceof Node\n      range = new Range()\n      range.selectNode(rangeOrNode)\n      return @rangeInRange(selection.getRangeAt(0), range)\n    else\n      return false\n\n  # Finds all matches to a regex within a node's text content (including line\n  # breaks from DIVs and BRs, as \\n), and returns a list of corresponding Range\n  # objects.\n  regExpSelectorAll: (node, regex) ->\n\n    # Generate a text representation of the node's content\n    nodeTextList = @getIndexedTextContent(node)\n    text = nodeTextList.map( ({text}) -> text ).join(\"\")\n\n    # Build a list of range objects by looping over regex matches in the\n    # text content string, and then finding the node those match indexes\n    # point to.\n    ranges = []\n    listPosition = 0\n    while (result = regex.exec(text)) isnt null\n      from = result.index\n      to = regex.lastIndex\n      item = nodeTextList[listPosition]\n      range = document.createRange()\n\n      while from >= item.end\n        item = nodeTextList[++listPosition]\n      start = if item.node.nodeType is Node.TEXT_NODE then from - item.start else 0\n      range.setStart(item.node,start)\n\n      while to > item.end\n        item = nodeTextList[++listPosition]\n      end = if item.node.nodeType is Node.TEXT_NODE then to - item.start else 0\n      range.setEnd(item.node, end)\n\n      ranges.push(range)\n\n    return ranges\n\n  # Returns true if the given range is the sole content of a node with the given\n  # nodeName. If the range's parent has a different nodeName or contains any other\n  # content, returns false.\n  isWrapped: (range, nodeName) ->\n    return false unless range and nodeName\n    startNode = range.startContainer\n    endNode = range.endContainer\n    return false unless startNode.parentNode is endNode.parentNode # must have same parent\n    return false if startNode.previousSibling or endNode.nextSibling # selection must span all sibling nodes\n    return false if range.startOffset > 0 or range.endOffset < endNode.textContent.length # selection must span all text\n    return startNode.parentNode.nodeName is nodeName\n\n  # Modifies the DOM to wrap the given range with a new node, of name nodeName.\n  #\n  # If the range starts or ends in the middle of an node, that node will be split.\n  # This will likely break selections that contain any of the affected nodes.\n  wrap: (range, nodeName) ->\n    newNode = document.createElement(nodeName)\n    try\n      range.surroundContents(newNode)\n    catch\n      newNode.appendChild(range.extractContents())\n      range.insertNode(newNode)\n    return newNode\n\n  # Modifies the DOM to \"unwrap\" a given node, replacing that node with its contents.\n  # This may break selections containing the affected nodes.\n  # We don't use `document.createFragment` because the returned `fragment`\n  # would be empty and useless after its children get replaced.\n  unwrapNode: (node) ->\n    return node if node.childNodes.length is 0\n    replacedNodes = []\n    parent = node.parentNode\n    return node if not parent?\n\n    lastChild = _.last(node.childNodes)\n    replacedNodes.unshift(lastChild)\n    parent.replaceChild(lastChild, node)\n\n    while child = _.last(node.childNodes)\n      replacedNodes.unshift(child)\n      parent.insertBefore(child, lastChild)\n      lastChild = child\n\n    return replacedNodes\n\n  isDescendantOf: (node, matcher = -> false) ->\n    parent = node?.parentElement\n    while parent\n      return true if matcher(parent)\n      parent = parent.parentElement\n    false\n\n  looksLikeBlockElement: (node) ->\n    return node.nodeName in [\"BR\", \"P\", \"BLOCKQUOTE\", \"DIV\", \"TABLE\"]\n\n  # When detecting if we're at the start of a \"visible\" line, we need to look\n  # for text nodes that have visible content in them.\n  looksLikeNonEmptyNode: (node) ->\n    textNode = DOMUtils.findFirstTextNode(node)\n    if textNode\n      if /^[\\n ]*$/.test(textNode.data)\n        return false\n      else return true\n    else\n      return false\n\n  previousTextNode: (node) ->\n    curNode = node\n    while curNode.parentNode\n      if curNode.previousSibling\n        return this.findLastTextNode(curNode.previousSibling)\n      curNode = curNode.parentNode\n    return null\n\nmodule.exports = DOMUtils\n"
  },
  {
    "path": "packages/client-app/src/dom-walkers.es6",
    "content": "const DOMWalkers = {\n  * walk(...treeWalkerArgs) {\n    const walker = document.createTreeWalker(...treeWalkerArgs);\n    let node = walker.nextNode();\n    while (node) {\n      yield node;\n      node = walker.nextNode();\n    }\n    return;\n  },\n\n  * walkBackwards(node) {\n    if (!node) { return; }\n    if (node.childNodes.length > 0) {\n      for (let i = node.childNodes.length - 1; i >= 0; i--) {\n        yield* this.walkBackwards(node.childNodes[i]);\n      }\n    }\n    yield node;\n    return;\n  },\n}\nexport default DOMWalkers\n"
  },
  {
    "path": "packages/client-app/src/error-logger-extensions/nylas-private-error-reporter.js",
    "content": "/* eslint global-require: 0 */\nconst getMac = require('getmac').getMac\nconst crypto = require('crypto')\nconst Raven = require('raven');\n//\n// NOTE: This file is manually copied over from the K2 repo into Nylas Mail.\n// You must manually update both files. We can't use a sym-link because require\n// paths don't work properly.\n//\n\nlet app;\n\nconst sentryDSN = null; // \"https://XXX:XXX@sentry.io/XXX\";\n\nclass ErrorReporter {\n\n  constructor(modes) {\n    this.reportError = this.reportError.bind(this)\n    this.onDidLogAPIError = this.onDidLogAPIError.bind(this);\n    this.inSpecMode = modes.inSpecMode\n    this.inDevMode = modes.inDevMode\n    this.resourcePath = modes.resourcePath\n    this.deviceHash = \"Unknown Device Hash\"\n    this.rateLimitDataByKey = new Map()\n\n    if (!this.inSpecMode) {\n      try {\n        getMac((err, macAddress) => {\n          if (!err && macAddress) {\n            this.deviceHash = crypto.createHash('md5').update(macAddress).digest('hex')\n          }\n          this._setupSentry();\n        })\n      } catch (err) {\n        console.error(err);\n        this._setupSentry();\n      }\n    }\n  }\n\n  onDidLogAPIError(error, statusCode, message) { // eslint-disable-line\n\n  }\n\n  getVersion() {\n    if (process.type === 'renderer') {\n      return NylasEnv.getVersion();\n    }\n    return require('electron').app.getVersion()\n  }\n\n  reportError(err, extra) {\n    if (this.inSpecMode || this.inDevMode) { return }\n    if (!this._shouldReportError(extra)) { return }\n\n    // It's possible for there to be more than 1 sentry capture object.\n    // If an error comes from multiple plugins, we report a unique event\n    // for each plugin since we want to group by individual pluginId\n    const captureObjects = this._prepareSentryCaptureObjects(err, extra)\n\n    if (process.type === 'renderer') {\n      app = require('electron').remote.getGlobal('application')\n    } else {\n      app = global.application\n    }\n\n    const errData = {}\n    if (typeof app !== 'undefined' && app && app.databaseReader) {\n      const fullIdent = app.databaseReader.getJSONBlob(\"NylasID\")\n      // We may not have an identity available yet\n      if (fullIdent) {\n        errData.user = {\n          id: fullIdent.id,\n          email: fullIdent.email,\n          name: `${fullIdent.firstname} ${fullIdent.lastname}`,\n        }\n      }\n    }\n\n    for (const obj of captureObjects) {\n      if (!sentryDSN) {\n        return;\n      }\n      Raven.captureException(err, Object.assign(errData, obj))\n    }\n  }\n\n  _shouldReportError(extra = {}) {\n    const {rateLimit} = extra\n    delete extra.rateLimit\n    const {key: rateLimitKey, ratePerHour} = rateLimit || {}\n    if (!rateLimitKey || !ratePerHour) {\n      return true\n    }\n    if (!this.rateLimitDataByKey.has(rateLimitKey)) {\n      this.rateLimitDataByKey.set(rateLimitKey, {\n        leftInHour: ratePerHour - 1,\n        hourStarted: Date.now(),\n      })\n      return true\n    }\n\n    const {leftInHour, hourStarted} = this.rateLimitDataByKey.get(rateLimitKey)\n    const oneHourTimestamp = hourStarted + (60 * 60 * 1000)\n    const now = Date.now()\n    if (now >= oneHourTimestamp) {\n      this.rateLimitDataByKey.set(rateLimitKey, {\n        leftInHour: ratePerHour - 1,\n        hourStarted: now,\n      })\n      return true\n    }\n    if (leftInHour > 0) {\n      this.rateLimitDataByKey.set(rateLimitKey, {\n        hourStarted,\n        leftInHour: leftInHour - 1,\n      })\n      return true\n    }\n    return false\n  }\n\n  _setupSentry() {\n    // Initialize the Sentry connector\n    if (!sentryDSN) {\n      return;\n    }\n    Raven.disableConsoleAlerts();\n    Raven.config(sentryDSN, {\n      name: this.deviceHash,\n      release: this.getVersion(),\n    }).install();\n\n    Raven.on('error', (e) => {\n      console.log(e.reason);\n      console.log(e.statusCode);\n      return console.log(e.response);\n    });\n  }\n\n  _prepareSentryCaptureObjects(error, extra) {\n    // Never send user auth tokens\n    if (error.requestOptions && error.requestOptions.auth) {\n      delete error.requestOptions.auth;\n    }\n\n    // Never send message bodies\n    if (error.requestOptions && error.requestOptions.body && error.requestOptions.body.body) {\n      delete error.requestOptions.body.body;\n    }\n\n    // https://docs.sentry.io/learn/rollups/#customize-grouping-with-fingerprints\n    const fingerprint = extra.fingerprint;\n    // The error-handling codepath of Nylas Mail involves several steps which\n    // end with reporter extensions being called always with two arguments:\n    // the Error object, and `extra`, which is an arbitrary object that\n    // contains any extra params. The Sentry API _also_ takes an `extra`\n    // param which contains arbitrary extra data. In order to avoid changing\n    // the whole error reporting extension API to be less generic, when\n    // reporting data to Sentry we pass in some `extra` params that should be\n    // sent to Sentry in non-`extra` params, and then delete them from the\n    // `extra` object so it's not sending confusing duplicate information.\n    // This should not affect other plugins as these deleted params are\n    // Sentry-specific.\n    if (extra.fingerprint) delete extra.fingerprint;\n\n    if (extra && extra.pluginIds && extra.pluginIds.length > 0) {\n      const captureObjects = [];\n      for (const pluginId of extra.pluginIds) {\n        const obj = {\n          extra: extra,\n          tags: {\n            platform: process.platform,\n            version: this.getVersion(),\n            pluginId: pluginId,\n          },\n        };\n        if (fingerprint) {\n          obj.fingerprint = fingerprint;\n        }\n        captureObjects.push(obj);\n      }\n      if (extra.pluginIds) delete extra.pluginIds;\n      return captureObjects\n    }\n    const objs = [{\n      extra: extra,\n      tags: {\n        platform: process.platform,\n        version: this.getVersion(),\n      },\n    }];\n    if (fingerprint) objs[0].fingerprint = fingerprint;\n    return objs;\n  }\n}\n\nmodule.exports = ErrorReporter;\n"
  },
  {
    "path": "packages/client-app/src/error-logger.js",
    "content": "// This file cannot be Coffeescript because it loads before the\n// Coffeescript interpreter. Note that it runs in both browser and\n// renderer processes.\n\nvar ErrorLogger, _, fs, path, app, os, remote;\nos = require('os');\nfs = require('fs-plus');\npath = require('path');\nlet ipcRenderer = null;\nif (process.type === 'renderer') {\n  ipcRenderer = require('electron').ipcRenderer;\n  remote = require('electron').remote;\n  app = remote.app;\n} else {\n  app = require('electron').app;\n}\n\nvar crashReporter = require('electron').crashReporter\n\n// A globally available ErrorLogger that can report errors to various\n// sources and enhance error functionality.\n//\n// This runs in both the backend browser process and each and every\n// renderer process.\n//\n// This is available as `global.errorLogger` in the backend browser\n// process.\n//\n// It is available at `NylasEnv.errorLogger` in each renderer process.\n// You should almost always use `NylasEnv.reportError` in the renderer\n// processes instead of manually accessing the `errorLogger`\n//\n// The errorLogger will report errors to a log file as well as to 3rd\n// party reporting services if enabled.\nmodule.exports = ErrorLogger = (function() {\n\n  function ErrorLogger(args) {\n    this.reportError = this.reportError.bind(this)\n    this.inSpecMode = args.inSpecMode\n    this.inDevMode = args.inDevMode\n    this.resourcePath = args.resourcePath\n\n    this._startCrashReporter()\n\n    this._extendErrorObject()\n\n    this._extendNativeConsole()\n\n    this.extensions = this._setupErrorLoggerExtensions(args)\n\n    if (this.inSpecMode) { return }\n\n    this._cleanOldLogFiles();\n    this._setupNewLogFile();\n    this._hookProcessOutputsToLogFile();\n  }\n\n  /////////////////////////////////////////////////////////////////////\n  /////////////////////////// PUBLIC METHODS //////////////////////////\n  /////////////////////////////////////////////////////////////////////\n\n  ErrorLogger.prototype.reportError = function(error, extra = {}) {\n    if (this.inSpecMode) { return }\n    if (!error) { error = {stack: \"\"} }\n    this._appendLog(error.stack)\n    if (extra) { this._appendLog(extra) }\n    if (process.type === \"renderer\") {\n      var errorJSON = \"{}\";\n      try {\n        errorJSON = JSON.stringify(error);\n      } catch (err) {\n        var recoveredError = new Error();\n        recoveredError.stack = error.stack;\n        recoveredError.message = `Recovered Error: ${error.message}`;\n        errorJSON = JSON.stringify(recoveredError)\n      }\n\n      var extraJSON;\n      try {\n        extraJSON = JSON.stringify(extra);\n      } catch (err) {\n        extraJSON = \"{}\";\n      }\n\n      /**\n       * We synchronously send all errors to the backend main process.\n       *\n       * This is important because errors can frequently happen right\n       * before a renderer window is closing. Since error reporting hits\n       * APIs and is asynchronous it's possible for the window to be\n       * destroyed before the report makes it.\n       *\n       * This is a rare use of `sendSync` to ensure the command has made\n       * it before the window closes.\n       */\n      ipcRenderer.sendSync(\"report-error\", {errorJSON: errorJSON, extra: extraJSON})\n\n    } else {\n      this._notifyExtensions(\"reportError\", error, extra)\n    }\n    console.error(error, extra);\n  }\n\n  ErrorLogger.prototype.openLogs = function() {\n    var shell = require('electron').shell;\n    shell.openItem(this._logPath());\n  };\n\n  ErrorLogger.prototype.apiDebug = function(error) {\n    this._appendLog(error, error.statusCode, error.message);\n    this._notifyExtensions(\"onDidLogAPIError\", error);\n  }\n\n\n  /////////////////////////////////////////////////////////////////////\n  ////////////////////////// PRIVATE METHODS //////////////////////////\n  /////////////////////////////////////////////////////////////////////\n\n  ErrorLogger.prototype._startCrashReporter = function(args) {\n    crashReporter.start({\n      productName: 'Nylas Mail',\n      companyName: 'Nylas',\n      submitURL: 'https://electron-crash-report-server.herokuapp.com/',\n      autoSubmit: true,\n    })\n  }\n\n  ErrorLogger.prototype._extendNativeConsole = function(args) {\n    console.debug = this._consoleDebug.bind(this)\n\n    if (process.type === 'browser' && process.platform === 'darwin') {\n      var nslog = require('nslog');\n      console.log = nslog;\n      console.error = nslog;\n    }\n  }\n\n  // globally define Error.toJSON. This allows us to pass errors via IPC\n  // and through the Action Bridge. Note:they are not re-inflated into\n  // Error objects automatically.\n  ErrorLogger.prototype._extendErrorObject = function(args) {\n    Object.defineProperty(Error.prototype, 'toJSON', {\n      value: function () {\n        var alt = {};\n\n        Object.getOwnPropertyNames(this).forEach(function (key) {\n          alt[key] = this[key];\n        }, this);\n\n        return alt;\n      },\n      configurable: true\n    });\n  }\n\n  ErrorLogger.prototype._setupErrorLoggerExtensions = function(args) {\n    var extension, extensionConstructor, extensionPath, extensions, extensionsPath, i, len, ref;\n    if (args == null) {\n      args = {};\n    }\n    extensions = [];\n    extensionsPath = path.join(args.resourcePath, 'src', 'error-logger-extensions');\n    ref = fs.listSync(extensionsPath);\n    for (i = 0, len = ref.length; i < len; i++) {\n      extensionPath = ref[i];\n      if (path.basename(extensionPath)[0] === '.') {\n        continue;\n      }\n      extensionConstructor = require(extensionPath);\n      if (!(typeof extensionConstructor === \"function\")) {\n        throw new Error(\"Logger Extensions must return an extension constructor\");\n      }\n      extension = new extensionConstructor({\n        inSpecMode: args.inSpecMode,\n        inDevMode: args.inDevMode,\n        resourcePath: args.resourcePath\n      });\n      extensions.push(extension);\n    }\n    return extensions;\n  };\n\n  ErrorLogger.prototype._logPath = function() {\n    var tmpPath = app.getPath('temp');\n\n    var logpid = process.pid;\n    if (process.type === 'renderer') {\n      logpid = remote.process.pid + \".\" +  process.pid;\n    }\n    return path.join(tmpPath, 'Nylas-Mail-' + logpid + '.log');\n  }\n\n  // If we're the browser process, remove log files that are more than\n  // two days old. These log files get pretty big because we're logging\n  // so verbosely.\n  ErrorLogger.prototype._cleanOldLogFiles = function() {\n    if (process.type === 'browser') {\n      var tmpPath = app.getPath('temp');\n      fs.readdir(tmpPath, function(err, files) {\n        if (err) {\n          console.error(err);\n          return;\n        }\n\n        var logFilter = new RegExp(\"Nylas-Mail-[.0-9]*.log$\");\n        files.forEach(function(file) {\n          if (logFilter.test(file) === true) {\n            var filepath = path.join(tmpPath, file);\n            fs.stat(filepath, function(err, stats) {\n              if (!err && stats) {\n                var lastModified = new Date(stats['mtime']);\n                var fileAge = Date.now() - lastModified.getTime();\n                if (fileAge > (1000 * 60 * 60 * 24 * 2)) { // two days\n                  fs.unlink(filepath);\n                }\n              }\n            });\n          }\n        });\n      });\n    }\n  }\n\n  ErrorLogger.prototype._setupNewLogFile = function() {\n    // Open a file write stream to log output from this process\n    console.log(\"Streaming log data to \"+this._logPath());\n\n    this.loghost = os.hostname();\n    this.logstream = fs.createWriteStream(this._logPath(), {\n      flags: 'a',\n      encoding: 'utf8',\n      fd: null,\n      mode: 666,\n    });\n  }\n\n  ErrorLogger.prototype._hookProcessOutputsToLogFile = function() {\n    var self = this;\n    // Override stdout and stderr to pipe their output to the file\n    // in addition to calling through to the existing implementation\n    function hook_process_output(channel, callback) {\n      var old_write = process[channel].write;\n      process[channel].write = (function(write) {\n          return function(string, encoding, fd) {\n              write.apply(process[channel], arguments)\n              callback(string, encoding, fd)\n          }\n      })(process[channel].write)\n\n      // Return a function that can be used to undo this change\n      return function() {\n        process[channel].write = old_write\n      };\n    }\n\n    hook_process_output('stdout', function(string, encoding, fd) {\n      self._appendLog.apply(self, [string]);\n    });\n    hook_process_output('stderr', function(string, encoding, fd) {\n      self._appendLog.apply(self, [string]);\n    });\n  }\n\n  ErrorLogger.prototype._notifyExtensions = function() {\n    var command, args;\n    command = arguments[0]\n    args = 2 <= arguments.length ? Array.prototype.slice.call(arguments, 1) : [];\n    for (var i=0; i < this.extensions.length; i++) {\n      const extension = this.extensions[i]\n      extension[command].apply(this, args);\n    }\n  }\n\n  // Create a new console.debug option, which takes `true` (print)\n  // or `false`, don't print in console as the first parameter.\n  // This makes it easy for developers to turn on and off\n  // \"verbose console\" mode.\n  ErrorLogger.prototype._consoleDebug = function() {\n    var args = [];\n    var showIt = arguments[0];\n    for (var ii = 1; ii < arguments.length; ii++) {\n      args.push(arguments[ii]);\n    }\n    if ((this.inDevMode === true) && (showIt === true)) {\n      console.log.apply(console, args);\n    }\n    this._appendLog.apply(this, [args]);\n  }\n\n  ErrorLogger.prototype._appendLog = function(obj) {\n    if (this.inSpecMode) { return };\n\n    try {\n      var message = JSON.stringify({\n        host: this.loghost,\n        timestamp: (new Date()).toISOString(),\n        payload: obj\n      })+\"\\n\";\n\n      this.logstream.write(message, 'utf8', function (err) {\n        if (err) {\n          console.error(\"ErrorLogger: Unable to write to the log stream!\" + err.toString());\n        }\n      });\n    } catch (err) {\n      console.error(\"ErrorLogger: Unable to write to the log stream.\" + err.toString());\n    }\n  };\n\n  return ErrorLogger;\n\n})();\n"
  },
  {
    "path": "packages/client-app/src/extensions/account-sidebar-extension.es6",
    "content": "\nclass AccountSidebarExtension {\n\n  /**\n   * @param accountIds\n   * @return {\n   *    id,\n   *    name,\n   *    iconName,\n   *    perspective: {MailboxPerspective},\n   * }\n   */\n  static sidebarItem() {}\n\n}\n\nexport default AccountSidebarExtension\n"
  },
  {
    "path": "packages/client-app/src/extensions/composer-extension.coffee",
    "content": "ContenteditableExtension = require('./contenteditable-extension')\n###\nPublic: To create ComposerExtensions that enhance the composer experience,\nyou should create objects that implement the interface defined at\n{ComposerExtension}.\n\n{ComposerExtension} extends {ContenteditableExtension}, so you can also\nimplement the methods defined there to further enhance the composer\nexperience.\n\nTo register your extension with the ExtensionRegistry, call\n{ExtensionRegistry::Composer::register}.  When your package is being\nunloaded, you *must* call the corresponding\n{ExtensionRegistry::Composer::unregister} to unhook your extension.\n\n```\ncoffee activate: -> ExtensionRegistry.Composer.register(MyExtension)\n\n...\n\ndeactivate: -> ExtensionRegistry.Composer.unregister(MyExtension)\n```\n\n**Your ComposerExtension should be stateless**. The user may have multiple\ndrafts open at any time, and the methods of your ComposerExtension may be\ncalled for different drafts at any time. You should not expect that the\nsession you receive in {::applyTransformsToDraft} is for the same\ndraft you previously received in {::warningsForSending}, etc.\n\nThe ComposerExtension API does not currently expose any asynchronous or\n{Promise}-based APIs, except for applyTransformsToDraft and unapplyTransformsToDraft,\nwhich can optionally return a promsie. This will likely change in the future.\nIf you have a use-case for a ComposerExtension that is not possible with the current\nAPI, please let us know.\n\nSection: Extensions\n###\nclass ComposerExtension extends ContenteditableExtension\n\n  ###\n  Public: Allows the addition of new types of send actions such as \"Send\n  Later\"\n\n  - `draft`: A fully populated {Message} object that is about to be sent.\n\n  Return an array of objects that adhere to the following spec. If the draft data\n  indicates that your action should not be available, then return null.\n\n    - `title`: A short, single string that is displayed to users when\n    describing your component. It is used in the hover title text of your\n    option in the dropdown menu. It is also used in the \"Default Send\n    Behavior\" dropdown setting. If your string is selected, then the\n    `core.sending.defaultSendType` will be set to your string and your\n    option will appear as the default.\n\n    - `performSendAction`: Callback for when your option is clicked as the primary\n    action. The function will be passed `{draft}` as its only argument.\n    It does not need to return anything. It may be asynchronous and likely\n    queue Tasks.\n\n    - `isEnabled`: Callback to determine if this send action should be rendered\n    for the given draft. Takes a draft: A fully populated {Message} object that\n    is about to be sent.\n\n    - `iconUrl`: A custom icon to be placed in the Send button. SendAction\n    extensions have the form \"Send + {ICON}\"\n  ###\n  @sendActions: ->\n    []\n\n  ###\n  Public: Inspect the draft, and return any warnings that need to be\n  displayed before the draft is sent. Warnings should be string phrases,\n  such as \"without an attachment\" that fit into a message of the form:\n  \"Send #{phase1} and #{phase2}?\"\n\n  - `draft`: A fully populated {Message} object that is about to be sent.\n\n  Returns a list of warning strings, or an empty array if no warnings need\n  to be displayed.\n  ###\n  @warningsForSending: ({draft}) ->\n    []\n\n  # ###\n  # Public: declare an icon to be displayed in the composer's toolbar (where\n  # bold, italic, underline, etc are).\n  #\n  # You must return an object that contains the following properties:\n  #\n  # - `mutator`: A function that's called when your toolbar button is\n  # clicked. The mutator will be passed: `(contenteditableDOM, selection,\n  # event)`.  It will be executed in a wrapped transaction block where it is\n  # safe to mutate the DOM and the selection object.\n  #\n  # - `className`: The button will already have the `btn` and `toolbar-btn`\n  # classes.\n  #\n  # - `tooltip`: A one or two word description of what your icon does\n  #\n  # - `iconUrl`: The url of your icon. It should be in the `nylas://`\n  # scheme.  For example: `nylas://your-package-name/assets/my-icon@2x.png`.\n  # Note, we will downsample your image by 2x (for Retina screens), so make\n  # sure it's twice the resolution. The icon should be black and white. We\n  # will directly pass the `url` prop of a {RetinaImg}\n  # ###\n  # @composerToolbar: ->\n  #   return\n\n  ###\n  Public: Override prepareNewDraft to modify a brand new draft before it\n  is displayed in a composer. This is one of the only places in the\n  application where it's safe to modify the draft object you're given\n  directly to add participants to the draft, add a signature, etc.\n\n  By default, new drafts are considered `pristine`. If the user leaves the\n  composer without making any changes, the draft is discarded. If your\n  extension populates the draft in a way that makes it \"populated\" in a\n  valuable way, you should set `draft.pristine = false` so the draft\n  saves, even if no further changes are made.\n  ###\n  @prepareNewDraft: ({draft}) ->\n    return\n\n  ###\n  Public: applyTransformsToDraft is called when a draft the user is editing\n  is saved to the server and/or sent. This method gives you an opportunity to\n  remove any annotations you've inserted into the draft body, apply final changes\n  to the body, etc.\n\n  Note that your extension /must/ be able to reverse the changes it applies to\n  the draft in `applyTransformsToDraft`. If the user re-opens the draft,\n  `unapplyTransformsToDraft` will be called and must restore the draft to it's\n  previous edit-ready state.\n\n  Examples:\n  - `applyTransformsToDraft`: Add tracking pixel to the email body.\n  - `unapplyTransformsToDraft`: Remove the tracking pixel from the email body.\n\n  - `applyTransformsToDraft`: Encrypt the message body.\n  - `unapplyTransformsToDraft`: Decrypt the message body.\n\n  This method should return a modified {Message} object, or a {Promise} which resolves\n  to a modified Message object.\n\n  - `draft`: A {Message} the user is about to finish editing.\n  ###\n  @applyTransformsForSending: ({draft, draftBodyRootNode}) ->\n    return\n\n  ###\n  Public: unapplyTransformsToDraft should revert the changes made in\n  `applyTransformsToDraft`. See the documentation for that method for more\n  information.\n  ###\n  @unapplyTransformsForSending: ({draft, draftBodyRootNode}) ->\n    return\n\nmodule.exports = ComposerExtension\n"
  },
  {
    "path": "packages/client-app/src/extensions/contenteditable-extension.coffee",
    "content": "###\nPublic: ContenteditableExtension is an abstract base class.\nImplementations of this are used to make additional changes to a\n<Contenteditable /> component beyond a user's input intents. The hooks in\nthis class provide the contenteditable DOM Node itself, allowing you to\nadjust selection ranges and change content as necessary.\n\nWhile some ContenteditableExtension are included with the core\n<{Contenteditable} /> component, others may be added via the `plugins`\nprop when you use it inside your own components.\n\nExample:\n\n```javascript\nrender() {\n  return(\n    <div>\n      <Contenteditable extensions={[MyAwesomeExtension]}>\n    </div>\n  );\n}\n```\n\nIf you specifically want to enhance the Composer experience you should\nregister a {ComposerExtension}\n\nSection: Extensions\n###\nclass ContenteditableExtension\n\n  ###\n  Public: Gets called anytime any atomic change is made to the DOM of the\n  contenteditable.\n\n  When a user types a key, deletes some text, or does anything that\n  changes the DOM. it will trigger `onContentChanged`. It is wrapper over\n  a native DOM {MutationObserver}. It only gets called if there are\n  mutations\n\n  This also gets called at the end of callbacks that mutate the DOM. If\n  another extension overrides `onClick` and performs several mutations to\n  the DOM during that callback, those changes will be batched and then\n  `onContentChanged` will be called once at the end of the callback with\n  those mutations.\n\n  Callback params:\n    - editor: The {Editor} controller that provides a host of convenience\n    methods for manipulating the selection and DOM\n    - mutations: An array of DOM Mutations as returned by the\n    {MutationObserver}. Note that these may not always be populated\n\n  You may mutate the contenteditable in place, we do not expect any return\n  value from this method.\n\n  The onContentChanged event can be triggered by a variety of events, some\n  of which could have been already been looked at by a callback. Any DOM\n  mutation will fire this event. Sometimes those mutations are the cause\n  of other callbacks.\n\n  Example:\n\n  The Nylas `templates` package uses this method to see if the user has\n  populated a `<code>` tag placed in the body and change it's CSS class to\n  reflect that it is no longer empty.\n\n  ```coffee\n  onContentChanged: ({editor, mutations}) ->\n    isWithinNode = (node) ->\n      test = selection.baseNode\n      while test isnt editableNode\n        return true if test is node\n        test = test.parentNode\n      return false\n\n    codeTags = editableNode.querySelectorAll('code.var.empty')\n    for codeTag in codeTags\n      if selection.containsNode(codeTag) or isWithinNode(codeTag)\n        codeTag.classList.remove('empty')\n  ```\n  ###\n  @onContentChanged: ({editor, mutations}) ->\n\n  @onContentStoppedChanging: ({editor, mutations}) ->\n\n  ###\n  Public: Override onBlur to mutate the contenteditable DOM node whenever the\n  onBlur event is fired on it. You may mutate the contenteditable in place, we\n  not expect any return value from this method.\n\n  - editor: The {Editor} controller that provides a host of convenience\n  methods for manipulating the selection and DOM\n  - event: DOM event fired on the contenteditable\n  ###\n  @onBlur: ({editor, event}) ->\n\n  ###\n  Public: Override onFocus to mutate the contenteditable DOM node whenever the\n  onFocus event is fired on it. You may mutate the contenteditable in place, we\n  not expect any return value from this method.\n\n  - editor: The {Editor} controller that provides a host of convenience\n  methods for manipulating the selection and DOM\n  - event: DOM event fired on the contenteditable\n  ###\n  @onFocus: ({editor, event}) ->\n\n  ###\n  Public: Override onClick to mutate the contenteditable DOM node whenever the\n  onClick event is fired on it. You may mutate the contenteditable in place, we\n  not expect any return value from this method.\n\n  - editor: The {Editor} controller that provides a host of convenience\n  methods for manipulating the selection and DOM\n  - event: DOM event fired on the contenteditable\n  ###\n  @onClick: ({editor, event}) ->\n\n  ###\n  Public: Override onKeyDown to mutate the contenteditable DOM node whenever the\n  onKeyDown event is fired on it.\n  Public: Called when the user presses a key while focused on the contenteditable's body field.\n  Override onKeyDown in your ContenteditableExtension to adjust the selection or\n  perform other actions.\n\n  If your package implements key down behavior for a particular scenario, you\n  should prevent the default behavior of the key via `event.preventDefault()`.\n  You may mutate the contenteditable in place, we not expect any return value\n  from this method.\n\n  Important: You should prevent the default key down behavior with great care.\n\n  - editor: The {Editor} controller that provides a host of convenience\n  methods for manipulating the selection and DOM\n  - event: DOM event fired on the contenteditable\n  ###\n  @onKeyDown: ({editor, event}) ->\n\n  @onDrop: ({editor, event}) ->\n\n  @onDragOver: ({editor, event}) ->\n\n  ###\n  Public: Override onShowContextMenu to add new menu items to the right click menu\n  inside the contenteditable.\n\n  - editor: The {Editor} controller that provides a host of convenience\n  methods for manipulating the selection and DOM\n  - event: DOM event fired on the contenteditable\n  - menu: [Menu](https://github.com/atom/electron/blob/master/docs/api/menu.md)\n  object you can mutate in order to add new [MenuItems](https://github.com/atom/electron/blob/master/docs/api/menu-item.md)\n  to the context menu that will be displayed when you right click the contenteditable.\n  ###\n  @onShowContextMenu: ({editor, event, menu}) ->\n\n  ###\n  Public: Override `keyCommandHandlers` to declaratively map keyboard\n  commands to callbacks.\n\n  Return an object keyed by the command name whose values are the\n  callbacks.\n\n  Callbacks are automatically bound to the Contenteditable context and\n  passed `({editor, event})` as its argument.\n\n  New commands are defined in keymap.cson files.\n  ###\n  @keyCommandHandlers: =>\n\n  ###\n  Public: Override `toolbarButtons` to declaratively add your own button\n  to the composer's toolbar.\n\n  - toolbarState: The current state of the Toolbar and Composer. This is\n  Read only.\n\n  Must return an array of objects obeying the following spec:\n    - className: A string class name\n    - onClick: Callback to fire when your button is clicked. The callback\n    is automatically bound to the editor and will get passed an single\n    object with the following args.\n      - editor - The {Editor} controller for manipulating the DOM\n      - event - The click Event object\n    - tooltip: A string to display when users hover over your button\n    - iconUrl: A url for the icon.\n  ###\n  @toolbarButtons: ({toolbarState}) ->\n\n  ###\n  Public: Override `toolbarComponentConfig` to declaratively show your own\n  toolbar when certain conditions are met.\n\n  If you want to hide your toolbar component, return null.\n\n  If you want to display your toolbar, then return an object with the\n  signature indicated below.\n\n  This methods gets called anytime the `toolbarState` changes. Since\n  `toolbarState` includes the current value of the Selection and any\n  objects a user is hovering over, you should expect it to change very\n  frequently.\n\n  - toolbarState: The current state of the Toolbar and Composer. This is\n  Read only.\n    - dragging\n    - doubleDown\n    - hoveringOver\n    - editableNode\n    - exportedSelection\n    - extensions\n    - atomicEdit\n\n  Must return an object with the following signature\n    - component: A React component or null.\n    - props: Props to be passed into your custom Component\n    - locationRefNode: Anything (usually a DOM Node) that responds to\n    `getBoundingClientRect`. This is used to determine where to display\n    your component.\n    - width: The width of your component. This is necessary because when\n    your component is displayed in the {FloatingToolbar}, the position is\n    pre-computed based on the absolute width of the item.\n    - height: The height of your component. This is necessary for the same\n    reason listed above; the position of the toolbar will be determined\n    by the absolute height given.\n  ###\n  @toolbarComponentConfig: ({toolbarState}) ->\n\nmodule.exports = ContenteditableExtension\n"
  },
  {
    "path": "packages/client-app/src/extensions/extension-utils.es6",
    "content": "import RegExpUtils from '../regexp-utils';\n\nexport function getFunctionArgs(func) {\n  const match = func.toString().match(RegExpUtils.functionArgs());\n  if (!match) return [[]];\n  const matchStr = match[1] || match[2]\n  return matchStr.split(/\\s*,\\s*/);\n}\n"
  },
  {
    "path": "packages/client-app/src/extensions/message-view-extension.coffee",
    "content": "###\nPublic: To create MessageViewExtension that customize message viewing, you\nshould create objects that implement the interface defined at {MessageViewExtension}.\n\nTo register your extension with the ExtensionRegistry, call {ExtensionRegistry::MessageView::registerExtension}.\nWhen your package is being unloaded, you *must* call the corresponding\n{ExtensionRegistry::MessageView::unregisterExtension} to unhook your extension.\n\n```coffee\nactivate: ->\n  ExtensionRegistry.MessageView.register(MyExtension)\n\n...\n\ndeactivate: ->\n  ExtensionRegistry.MessageView.unregister(MyExtension)\n```\n\nThe MessageViewExtension API does not currently expose any asynchronous or {Promise}-based APIs.\nThis will likely change in the future. If you have a use-case for a Message Store extension that\nis not possible with the current API, please let us know.\n\nSection: Extensions\n###\nclass MessageViewExtension\n\n  ###\n  Public: Modify the body of the message provided. Note that you're provided\n  the entire message object, but you can only change `message.body`.\n  ###\n  @formatMessageBody: ({message}) ->\n\n  ###\n  Public: Modify the rendered message body using the DOM.\n  Runs after messages goes through `formatMessageBody` and is placed\n  into the DOM.\n  ###\n  @renderedMessageBodyIntoDocument: ({document, message, iframe}) ->\n\nmodule.exports = MessageViewExtension\n"
  },
  {
    "path": "packages/client-app/src/extensions/thread-list-extension.es6",
    "content": "\nclass ThreadListExtension {\n\n  static cssClassNamesForThreadListItem() {}\n\n  static cssClassNamesForThreadListIcon() {}\n\n}\n\nexport default ThreadListExtension\n"
  },
  {
    "path": "packages/client-app/src/flux/action-bridge.es6",
    "content": "import _ from 'underscore';\nimport Actions from './actions';\nimport DatabaseStore from './stores/database-store';\nimport DatabaseChangeRecord from './stores/database-change-record';\n\nimport Utils from './models/utils';\n\nconst Role = {\n  WORK: 'work',\n  SECONDARY: 'secondary',\n};\n\nconst TargetWindows = {\n  ALL: 'all',\n  WORK: 'work',\n};\n\nconst Message = {\n  DATABASE_STORE_TRIGGER: 'db-store-trigger',\n};\n\nconst printToConsole = false;\n\n// Public: ActionBridge\n//\n// The ActionBridge has two responsibilities:\n// 1. When you're in a secondary window, the ActionBridge observes all Root actions. When a\n//    Root action is fired, it converts it's payload to JSON, tunnels it to the main window\n//    via IPC, and re-fires the Action in the main window. This means that calls to actions\n//    like Actions.queueTask(task) can be fired in secondary windows and consumed by the\n//    TaskQueue, which only lives in the main window.\n\n// 2. The ActionBridge listens to the DatabaseStore and re-broadcasts it's trigger() event\n//    into all of the windows of the application. This is important, because the DatabaseStore\n//    in all secondary windows is a read-replica. Only the DatabaseStore in the main window\n//    of the application consumes persistModel actions and writes changes to the database.\n\nclass ActionBridge {\n  static Role = Role;\n  static Message = Message;\n  static TargetWindows = TargetWindows;\n\n  constructor(ipc) {\n    this.registerGlobalActions = this.registerGlobalActions.bind(this);\n    this.onIPCMessage = this.onIPCMessage.bind(this);\n    this.onRebroadcast = this.onRebroadcast.bind(this);\n    this.onBeforeUnload = this.onBeforeUnload.bind(this);\n    this.globalActions = [];\n    this.ipc = ipc;\n    this.ipcLastSendTime = null;\n    this.initiatorId = NylasEnv.getWindowType();\n    this.role = NylasEnv.isWorkWindow() ? Role.WORK : Role.SECONDARY;\n\n    NylasEnv.onBeforeUnload(this.onBeforeUnload);\n\n    // Listen for action bridge messages from other windows\n    if (NylasEnv.isEmptyWindow()) {\n      NylasEnv.onWindowPropsReceived(() => {\n        this.ipc.on('action-bridge-message', this.onIPCMessage);\n      });\n    } else {\n      this.ipc.on('action-bridge-message', this.onIPCMessage);\n    }\n\n    // Observe all global actions and re-broadcast them to other windows\n    Actions.globalActions.forEach(name => {\n      const callback = (...args) => this.onRebroadcast(TargetWindows.ALL, name, args);\n      return Actions[name].listen(callback, this);\n    });\n\n    // Observe the database store (possibly other stores in the future), and\n    // rebroadcast it's trigger() event.\n    const databaseCallback = change => {\n      if (DatabaseStore.triggeringFromActionBridge) { return; }\n      this.onRebroadcast(TargetWindows.ALL, Message.DATABASE_STORE_TRIGGER, [change]);\n    };\n    DatabaseStore.listen(databaseCallback, this);\n\n    if (this.role !== Role.WORK) {\n      // Observe all mainWindow actions fired in this window and re-broadcast\n      // them to other windows so the central application stores can take action\n      Actions.workWindowActions.forEach(name => {\n        const callback = (...args) => this.onRebroadcast(TargetWindows.WORK, name, args);\n        return Actions[name].listen(callback, this);\n      });\n    }\n  }\n\n  registerGlobalActions({pluginName, actions}) {\n    return _.each(actions, (actionFn, name) => {\n      this.globalActions.push({name, actionFn, scope: pluginName});\n      const callback = (...args) => {\n        const broadcastName = `${pluginName}::${name}`;\n        return this.onRebroadcast(TargetWindows.ALL, broadcastName, args);\n      };\n      return actionFn.listen(callback, this);\n    }\n    );\n  }\n\n  _isExtensionAction(name) {\n    return name.split(\"::\").length === 2;\n  }\n\n  _globalExtensionAction(broadcastName) {\n    const [scope, name] = broadcastName.split(\"::\");\n    return (_.findWhere(this.globalActions, {scope, name}) || {}).actionFn\n  }\n\n  onIPCMessage(event, initiatorId, name, json) {\n    if (NylasEnv.isEmptyWindow()) {\n      throw new Error(\"Empty windows shouldn't receive IPC messages\");\n    }\n    // There's something very strange about IPC event handlers. The ReactRemoteParent\n    // threw React exceptions when calling setState from an IPC callback, and the debugger\n    // often refuses to stop at breakpoints immediately inside IPC callbacks.\n\n    // These issues go away when you add a setTimeout. So here's that.\n    // I believe this resolves issues like https://sentry.nylas.com/sentry/edgehill/group/2735/,\n    // which are React exceptions in a direct stack (no next ticks) from an IPC event.\n    setTimeout(() => {\n      console.debug(printToConsole, `ActionBridge: ${this.initiatorId} Action Bridge Received: ${name}`);\n\n      const args = JSON.parse(json, Utils.registeredObjectReviver);\n\n      if (name === Message.DATABASE_STORE_TRIGGER) {\n        DatabaseStore.triggeringFromActionBridge = true;\n        DatabaseStore.trigger(new DatabaseChangeRecord(args[0]));\n        DatabaseStore.triggeringFromActionBridge = false;\n      } else if (Actions[name]) {\n        Actions[name].firing = true;\n        Actions[name](...args);\n      } else if (this._isExtensionAction(name)) {\n        const fn = this._globalExtensionAction(name);\n        if (fn) {\n          fn.firing = true;\n          fn(...args);\n        }\n      } else {\n        throw new Error(`${this.initiatorId} received unknown action-bridge event: ${name}`);\n      }\n    }, 0);\n  }\n\n  onRebroadcast(target, name, args) {\n    if (Actions[name] && Actions[name].firing) {\n      Actions[name].firing = false;\n      return;\n    }\n\n    const globalExtAction = this._globalExtensionAction(name);\n    if (globalExtAction && globalExtAction.firing) {\n      globalExtAction.firing = false;\n      return;\n    }\n\n    const params = [];\n    args.forEach((arg) => {\n      if (arg instanceof Function) {\n        throw new Error(\"ActionBridge cannot forward action argument of type `function` to work window.\");\n      }\n      return params.push(arg);\n    });\n\n    const json = JSON.stringify(params, Utils.registeredObjectReplacer);\n\n    console.debug(printToConsole, `ActionBridge: ${this.initiatorId} Action Bridge Broadcasting: ${name}`);\n    this.ipc.send(`action-bridge-rebroadcast-to-${target}`, this.initiatorId, name, json);\n    this.ipcLastSendTime = Date.now();\n  }\n\n  onBeforeUnload(readyToUnload) {\n    // Unfortunately, if you call ipc.send and then immediately close the window,\n    // Electron won't actually send the message. To work around this, we wait an\n    // arbitrary amount of time before closing the window after the last IPC event\n    // was sent. https://github.com/atom/electron/issues/4366\n    if (this.ipcLastSendTime && Date.now() - this.ipcLastSendTime < 100) {\n      setTimeout(readyToUnload, 100);\n      return false;\n    }\n    return true;\n  }\n}\n\nexport default ActionBridge;\n"
  },
  {
    "path": "packages/client-app/src/flux/actions.es6",
    "content": "import Reflux from 'reflux';\n\nconst ActionScopeWindow = 'window';\nconst ActionScopeGlobal = 'global';\nconst ActionScopeWorkWindow = 'work';\n\n/*\nPublic: In the Flux {Architecture.md}, almost every user action\nis translated into an Action object and fired globally. Stores in the app observe\nthese actions and perform business logic. This loose coupling means that your\npackages can observe actions and perform additional logic, or fire actions which\nthe rest of the app will handle.\n\nIn Reflux, each {Action} is an independent object that acts as an event emitter.\nYou can listen to an Action, or invoke it as a function to fire it.\n\n## Action Scopes\n\nN1 is a multi-window application. The `scope` of an Action dictates\nhow it propogates between windows.\n\n- **Global**: These actions can be listened to from any window and fired from any\n  window. The action is sent from the originating window to all other windows via\n  IPC, so they should be used with care. Firing this action from anywhere will\n  cause all listeners in all windows to fire.\n\n- **Main Window**: You can fire these actions in any window. They'll be sent\n  to the main window and triggered there.\n\n- **Window**: These actions only trigger listeners in the window they're fired in.\n\n## Firing Actions\n\n```coffee\nActions.queueTask(new ChangeStarredTask(thread: @_thread, starred: true))\n```\n\n## Listening for Actions\n\nIf you're using Reflux to create your own Store, you can use the `listenTo`\nconvenience method to listen for an Action. If you're creating your own class\nthat is not a Store, you can still use the `listen` method provided by Reflux:\n\n```coffee\nsetup: ->\n  @unlisten = Actions.onNewMailDeltas.listen(@onNewMailReceived, @)\n\nonNewMailReceived: (data) ->\n  console.log(\"You've got mail!\", data)\n\nteardown: ->\n  @unlisten()\n```\n\nSection: General\n*/\nclass Actions {\n\n  /*\n  Public: Fired when the Nylas API Connector receives new data from the API.\n\n  *Scope: Global*\n\n  Receives an {Object} of {Array}s of {Model}s, for example:\n\n  ```json\n  {\n    'thread': [<Thread>, <Thread>]\n    'contact': [<Contact>]\n  }\n  ```\n  */\n  static onNewMailDeltas = ActionScopeGlobal;\n\n  static didReceiveSyncbackRequestDeltas = ActionScopeWindow;\n\n  static downloadStateChanged = ActionScopeGlobal;\n\n  static sendToAllWindows = ActionScopeGlobal;\n\n  /*\n  Public: Queue a {Task} object to the {TaskQueue}.\n\n  *Scope: Work Window*\n  */\n  static queueTask = ActionScopeWorkWindow;\n\n  /*\n  Public: Queue multiple {Task} objects to the {TaskQueue}, which should be\n  undone as a single user action.\n\n  *Scope: Work Window*\n  */\n  static queueTasks = ActionScopeWorkWindow;\n\n  static undoTaskId = ActionScopeWorkWindow;\n\n  /*\n  Public: Dequeue all {Task}s from the {TaskQueue}. Use with care.\n\n  *Scope: Work Window*\n  */\n  static dequeueAllTasks = ActionScopeWorkWindow;\n  static dequeueTask = ActionScopeWorkWindow;\n\n  /*\n  Public: Dequeue a {Task} matching the description provided.\n\n  *Scope: Work Window*\n  */\n  static dequeueMatchingTask = ActionScopeWorkWindow;\n\n  static longPollReceivedRawDeltas = ActionScopeWorkWindow;\n  static longPollProcessedDeltas = ActionScopeWorkWindow;\n  static willMakeAPIRequest = ActionScopeWorkWindow;\n  static didMakeAPIRequest = ActionScopeWorkWindow;\n  static checkOnlineStatus = ActionScopeWindow;\n\n\n  static wakeLocalSyncWorkerForAccount = ActionScopeGlobal;\n\n  /*\n  Public: Retry the initial sync\n\n  *Scope: Global*\n  */\n  static retryDeltaConnection = ActionScopeGlobal;\n\n  /*\n  Public: Open the preferences view.\n\n  *Scope: Global*\n  */\n  static openPreferences = ActionScopeGlobal;\n\n  /*\n  Public: Switch to the preferences tab with the specific name\n\n  *Scope: Global*\n  */\n  static switchPreferencesTab = ActionScopeGlobal;\n\n  /*\n  Public: Clear the developer console for the current window.\n\n  *Scope: Window*\n  */\n  static clearDeveloperConsole = ActionScopeWindow;\n\n  /*\n  Public: Manage the Nylas identity\n  */\n  static logoutNylasIdentity = ActionScopeWindow;\n\n  /*\n  Public: Remove the selected account\n\n  *Scope: Window*\n  */\n  static removeAccount = ActionScopeWindow;\n\n  /*\n  Public: Update the provided account\n\n  *Scope: Window*\n\n  ```\n  Actions.updateAccount(account.id, {accountName: 'new'})\n  ```\n  */\n  static updateAccount = ActionScopeWindow;\n  static apiAuthError = ActionScopeWindow;\n\n  /*\n  Public: Re-order the provided account in the account list.\n\n  *Scope: Window*\n\n  ```\n  Actions.reorderAccount(account.id, newIndex)\n  ```\n  */\n  static reorderAccount = ActionScopeWindow;\n\n  /*\n  Public: Select the provided sheet in the current window. This action changes\n  the top level sheet.\n\n  *Scope: Window*\n\n  ```\n  Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)\n  ```\n  */\n  static selectRootSheet = ActionScopeWindow;\n\n  /*\n  Public: Toggle whether a particular column is visible. Call this action\n  with one of the Sheet location constants:\n\n  ```\n  Actions.toggleWorkspaceLocationHidden(WorkspaceStore.Location.MessageListSidebar)\n  ```\n  */\n  static toggleWorkspaceLocationHidden = ActionScopeWindow;\n\n  /*\n  Public: Focus the keyboard on an item in a collection. This action moves the\n  `keyboard focus` element in lists and other components,  but does not change\n  the focused DOM element.\n\n  *Scope: Window*\n\n  ```\n  Actions.setCursorPosition(collection: 'thread', item: <Thread>)\n  ```\n  */\n  static setCursorPosition = ActionScopeWindow;\n\n  /*\n  Public: Focus on an item in a collection. This action changes the selection\n  in lists and other components, but does not change the focused DOM element.\n\n  *Scope: Window*\n\n  ```\n  Actions.setFocus(collection: 'thread', item: <Thread>)\n  ```\n  */\n  static setFocus = ActionScopeWindow;\n\n  /*\n  Public: Focus the interface on a specific {MailboxPerspective}.\n\n  *Scope: Window*\n\n  ```\n  Actions.focusMailboxPerspective(<Category>)\n  ```\n  */\n  static focusMailboxPerspective = ActionScopeWindow;\n\n  /*\n  Public: Focus the interface on the default mailbox perspective for the provided\n  account id.\n\n  *Scope: Window*\n  */\n  static focusDefaultMailboxPerspectiveForAccounts = ActionScopeWindow;\n\n  /*\n  Public: Focus the mailbox perspective for the given account id and category names\n\n  *Scope: Window*\n\n  ```\n  Actions.ensureCategoryIsFocused(accountIds, categoryName)\n  ```\n  */\n  static ensureCategoryIsFocused = ActionScopeWindow;\n\n  /*\n  Public: If the message with the provided id is currently beign displayed in the\n  thread view, this action toggles whether it's full content or snippet is shown.\n\n  *Scope: Window*\n\n  ```\n  message = <Message>\n  Actions.toggleMessageIdExpanded(message.id)\n  ```\n  */\n  static toggleMessageIdExpanded = ActionScopeWindow;\n\n  /*\n  Public: Toggle whether messages from trash and spam are shown in the current\n  message view.\n  */\n  static toggleHiddenMessages = ActionScopeWindow;\n\n  /*\n  Public: This action toggles wether to collapse or expand all messages in a\n  thread depending on if there are currently collapsed messages.\n\n  *Scope: Window*\n\n  ```\n  Actions.toggleAllMessagesExpanded()\n  ```\n  */\n  static toggleAllMessagesExpanded = ActionScopeWindow;\n\n  /*\n  Public: Print the currently selected thread.\n\n  *Scope: Window*\n\n  ```\n  thread = <Thread>\n  Actions.printThread(thread)\n  ```\n  */\n  static printThread = ActionScopeWindow;\n\n  /*\n  Public: Display the thread in a new popout window\n\n  *Scope: Window*\n\n  ```\n  thread = <Thread>\n  Actions.popoutThread(thread)\n  ```\n  */\n  static popoutThread = ActionScopeWindow;\n\n  /*\n  Public: Display the thread in the main window\n\n  *Scope: Global*\n\n  ```\n  thread = <Thread>\n  Actions.focusThreadMainWindow(thread)\n  ```\n  */\n  static focusThreadMainWindow = ActionScopeGlobal;\n\n  /*\n  Public: Create a new reply to the provided threadId and messageId and populate\n  it with the body provided.\n\n  *Scope: Window*\n\n  ```\n  message = <Message>\n  Actions.sendQuickReply({threadId: '123', messageId: '234'}, \"Thanks Ben!\")\n  ```\n  */\n  static sendQuickReply = ActionScopeWindow;\n\n  /*\n  Public: Create a new reply to the provided threadId and messageId. Note that\n  this action does not focus on the thread, so you may not be able to see the new draft\n  unless you also call {::setFocus}.\n\n  *Scope: Window*\n\n  ```\n  * Compose a reply to the last message in the thread\n  Actions.composeReply({threadId: '123'})\n\n  * Compose a reply to a specific message in the thread\n  Actions.composeReply({threadId: '123', messageId: '123'})\n  ```\n  */\n  static composeReply = ActionScopeWindow;\n\n  /*\n  Public: Create a new draft for forwarding the provided threadId and messageId. See\n  {::composeReply} for parameters and behavior.\n\n  *Scope: Window*\n  */\n  static composeForward = ActionScopeWindow;\n\n  /*\n  Public: Pop out the draft with the provided ID so the user can edit it in another\n  window.\n\n  *Scope: Window*\n\n  ```\n  messageId = '123'\n  Actions.composePopoutDraft(messageId)\n  ```\n  */\n  static composePopoutDraft = ActionScopeWindow;\n\n  static focusDraft = ActionScopeWindow;\n\n  /*\n  Public: Open a new composer window for creating a new draft from scratch.\n\n  *Scope: Window*\n\n  ```\n  Actions.composeNewBlankDraft()\n  ```\n  */\n  static composeNewBlankDraft = ActionScopeWindow;\n\n  /*\n  Public: Open a new composer window for a new draft addressed to the given recipient\n\n  *Scope: Window*\n\n  ```\n  Actions.composeNewDraftToRecipient(contact)\n  ```\n  */\n  static composeNewDraftToRecipient = ActionScopeWindow;\n\n  /*\n  Public: Send the draft with the given ID. This Action is handled by the {DraftStore},\n  which finalizes the {DraftChangeSet} and allows {ComposerExtension}s to display\n  warnings and do post-processing. To change send behavior, you should consider using\n  one of these objects rather than listening for the {sendDraft} action.\n\n  *Scope: Window*\n\n  ```\n  Actions.sendDraft('123', action)\n  ```\n  */\n  static sendDraft = ActionScopeWindow;\n  static willPerformSendAction = ActionScopeGlobal;\n  static didPerformSendAction = ActionScopeGlobal;\n  static didCancelSendAction = ActionScopeGlobal;\n  /*\n  Public: Fired when a draft is successfully sent\n  *Scope: Global*\n\n  Recieves the clientId of the message that was sent\n  */\n  static draftDeliverySucceeded = ActionScopeGlobal;\n  static draftDeliveryFailed = ActionScopeGlobal;\n\n  static ensureMessageInSentSuccess = ActionScopeGlobal;\n\n  static sendManyDrafts = ActionScopeWindow;\n  static finalizeDraftAndSyncbackMetadata = ActionScopeWindow;\n\n  /*\n  Public: Destroys the draft with the given ID. This Action is handled by the {DraftStore},\n  and does not display any confirmation UI.\n\n  *Scope: Window*\n  */\n  static destroyDraft = ActionScopeWindow;\n\n  /*\n  Public: Submits the user's response to an RSVP event.\n\n  *Scope: Window*\n  */\n  static RSVPEvent = ActionScopeWindow;\n\n  // FullContact Sidebar\n  static getFullContactDetails = ActionScopeWindow;\n  static focusContact = ActionScopeWindow;\n\n  // Templates\n  static insertTemplateId = ActionScopeWindow;\n  static createTemplate = ActionScopeWindow;\n  static showTemplates = ActionScopeWindow;\n\n  // Account Sidebar\n  static setCollapsedSidebarItem = ActionScopeWindow;\n\n  /*\n  Public: Remove a file from a draft.\n\n  *Scope: Window*\n\n  ```\n  Actions.removeFile\n    file: fileObject\n    messageClientId: draftClientId\n  ```\n  */\n  static removeFile = ActionScopeWindow;\n\n  // File Actions\n  // Some file actions only need to be processed in their current window\n  static addAttachment = ActionScopeWindow;\n  static selectAttachment = ActionScopeWindow;\n  static removeAttachment = ActionScopeWindow;\n  static insertAttachmentIntoDraft = ActionScopeWindow;\n\n  static fetchAndOpenFile = ActionScopeWindow;\n  static fetchAndSaveFile = ActionScopeWindow;\n  static fetchAndSaveAllFiles = ActionScopeWindow;\n  static fetchFile = ActionScopeWindow;\n  static abortFetchFile = ActionScopeWindow;\n\n  /*\n  Public: Pop the current sheet off the Sheet stack maintained by the {WorkspaceStore}.\n  This action has no effect if the window is currently showing a root sheet.\n\n  *Scope: Window*\n  */\n  static popSheet = ActionScopeWindow;\n\n  /*\n  Public: Pop the to the root sheet currently selected.\n\n  *Scope: Window*\n  */\n  static popToRootSheet = ActionScopeWindow;\n\n  /*\n  Public: Push a sheet of a specific type onto the Sheet stack maintained by the\n  {WorkspaceStore}. Note that sheets have no state. To show a *specific* thread,\n  you should push a Thread sheet and call `setFocus` to select the thread.\n\n  *Scope: Window*\n\n  ```\n  WorkspaceStore.defineSheet 'Thread', {},\n      list: ['MessageList', 'MessageListSidebar']\n\n  ...\n\n  @pushSheet(WorkspaceStore.Sheet.Thread)\n  ```\n  */\n  static pushSheet = ActionScopeWindow;\n\n  /*\n  Public: Publish a user event to any analytics services linked to N1.\n  */\n  static recordUserEvent = ActionScopeWorkWindow;\n  static recordPerfMetric = ActionScopeWorkWindow;\n\n  static addMailRule = ActionScopeWindow;\n  static reorderMailRule = ActionScopeWindow;\n  static updateMailRule = ActionScopeWindow;\n  static deleteMailRule = ActionScopeWindow;\n  static disableMailRule = ActionScopeWindow;\n\n  static openPopover = ActionScopeWindow;\n  static closePopover = ActionScopeWindow;\n\n  static openModal = ActionScopeWindow;\n  static closeModal = ActionScopeWindow;\n\n  /*\n  Public: Set metadata for a specified model and pluginId.\n\n  *Scope: Window*\n\n  Receives an {Model} or {Array} of {Model}s, a plugin id, and an Object that\n  represents the metadata value.\n  */\n  static setMetadata = ActionScopeWindow;\n\n  static draftParticipantsChanged = ActionScopeWindow;\n\n  static findInThread = ActionScopeWindow;\n  static nextSearchResult = ActionScopeWindow;\n  static previousSearchResult = ActionScopeWindow;\n\n\n  // Actions for the signature preferences and shared with the composer\n  static addSignature = ActionScopeWindow;\n  static removeSignature = ActionScopeWindow;\n  static updateSignature = ActionScopeWindow;\n  static selectSignature = ActionScopeWindow;\n  static toggleAccount = ActionScopeWindow;\n\n  static notifyPluginsChanged = ActionScopeGlobal;\n\n  static expandInitialSyncState = ActionScopeWindow;\n\n  static resetEmailCache = ActionScopeGlobal;\n\n  static debugSync = ActionScopeGlobal;\n\n  // Thread list actions\n  static archiveThreads = ActionScopeWindow;\n  static trashThreads = ActionScopeWindow;\n  static markAsSpamThreads = ActionScopeWindow;\n  static toggleStarredThreads = ActionScopeWindow;\n  static toggleUnreadThreads = ActionScopeWindow;\n  static setUnreadThreads = ActionScopeWindow;\n  static removeThreadsFromView = ActionScopeWindow;\n  static moveThreadsToPerspective = ActionScopeWindow;\n  static applyCategoryToThreads = ActionScopeWindow;\n  static removeCategoryFromThreads = ActionScopeWindow;\n  static threadListDidUpdate = ActionScopeWindow;\n\n  // Usage: Actions.runSendRequest({syncbackRequestJSON, onSuccess, onError})\n  //\n  // This allows us to communicate between client-sync and client-app without\n  // having to go through the API. The API request was adding a couple hundred\n  // milliseconds to our send time. Note that the scope of this has to remain\n  // within the window because onSuccess and onError are non-serializable\n  // functions. See https://phab.nylas.com/D4437 for more details.\n  static runSendRequest = ActionScopeWindow;\n}\n\n\n// Read the actions we declared on the dummy Actions object above\n// and translate them into Reflux Actions\n\n// This helper method exists to trick the Donna lexer so it doesn't\n// try to understand what we're doing to the Actions object.\nconst create = (obj, name, scope) => {\n  obj[name] = Reflux.createAction(name);\n  obj[name].scope = scope;\n  obj[name].sync = true;\n};\n\nconst scopes = {\n  window: [],\n  global: [],\n  work: [],\n};\n\nfor (const name of Object.getOwnPropertyNames(Actions)) {\n  if (name === 'length' || name === 'name' || name === 'arguments' || name === 'caller' || name === 'prototype') {\n    continue;\n  }\n  if (Actions[name] !== 'window' && Actions[name] !== 'global' && Actions[name] !== 'work') {\n    continue;\n  }\n  const scope = Actions[name];\n  scopes[scope].push(name);\n  create(Actions, name, scope);\n}\n\nActions.windowActions = scopes.window;\nActions.workWindowActions = scopes.work;\nActions.globalActions = scopes.global;\n\nexport default Actions;\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-boolean.es6",
    "content": "import Attribute from './attribute';\n\n/*\nPublic: The value of this attribute is always a boolean. Null values are coerced to false.\n\nString attributes can be queries using `equal` and `not`. Matching on\n`greaterThan` and `lessThan` is not supported.\n\nSection: Database\n*/\nexport default class AttributeBoolean extends Attribute {\n  toJSON(val) {\n    return val;\n  }\n  fromJSON(val) {\n    return ((val === 'true') || (val === true)) || false;\n  }\n  fromColumn(val) {\n    return (val === 1) || false;\n  }\n  columnSQL() {\n    const defaultValue = this.defaultValue ? 1 : 0;\n    return `${this.jsonKey} INTEGER DEFAULT ${defaultValue}`;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-collection.es6",
    "content": "import Attribute from './attribute';\nimport Matcher from './matcher';\n\n/*\nPublic: Collection attributes provide basic support for one-to-many relationships.\nFor example, Threads in N1 have a collection of Labels or Folders.\n\nWhen Collection attributes are marked as `queryable`, the DatabaseStore\nautomatically creates a join table and maintains it as you create, save,\nand delete models. When you call `persistModel`, entries are added to the\njoin table associating the ID of the model with the IDs of models in the collection.\n\nCollection attributes have an additional clause builder, `contains`:\n\n```coffee\nDatabaseStore.findAll(Thread).where([Thread.attributes.categories.contains('inbox')])\n```\n\nThis is equivalent to writing the following SQL:\n\n```sql\nSELECT `Thread`.`data` FROM `Thread`\nINNER JOIN `ThreadLabel` AS `M1` ON `M1`.`id` = `Thread`.`id`\nWHERE `M1`.`value` = 'inbox'\nORDER BY `Thread`.`last_message_received_timestamp` DESC\n```\n\nThe value of this attribute is always an array of other model objects.\n\nSection: Database\n*/\nexport default class AttributeCollection extends Attribute {\n  constructor({modelKey, jsonKey, itemClass, joinOnField, joinQueryableBy, queryable}) {\n    super({modelKey, jsonKey, queryable});\n    this.ItemClass = this.itemClass = itemClass;\n    this.joinOnField = joinOnField;\n    this.joinQueryableBy = joinQueryableBy || [];\n  }\n\n  toJSON(vals) {\n    if (!vals) {\n      return [];\n    }\n\n    if (!(vals instanceof Array)) {\n      throw new Error(`AttributeCollection::toJSON: ${this.modelKey} is not an array.`);\n    }\n\n    const json = []\n    for (const val of vals) {\n      if (!(val instanceof this.ItemClass)) {\n        throw new Error(`AttributeCollection::toJSON: Value \\`${val}\\` in ${this.modelKey} is not an ${this.ItemClass.name}`);\n      }\n      if (val.toJSON !== undefined) {\n        json.push(val.toJSON());\n      } else {\n        json.push(val);\n      }\n    }\n    return json;\n  }\n\n  fromJSON(json) {\n    if (!json || !(json instanceof Array)) {\n      return [];\n    }\n    const objs = [];\n\n    for (const objJSON of json) {\n      // Note: It's possible for a malformed API request to return an array\n      // of null values. N1 is tolerant to this type of error, but shouldn't\n      // happen on the API end.\n      if (!objJSON) {\n        continue;\n      }\n\n      if (this.ItemClass.prototype.fromJSON) {\n        const obj = new this.ItemClass();\n        // Important: if no ids are in the JSON, don't make them up\n        // randomly.  This causes an object to be \"different\" each time it's\n        // de-serialized even if it's actually the same, makes React\n        // components re-render!\n        obj.clientId = undefined;\n        obj.fromJSON(objJSON);\n        objs.push(obj);\n      } else {\n        const obj = new this.ItemClass(objJSON);\n        obj.clientId = undefined;\n        objs.push(obj);\n      }\n    }\n    return objs;\n  }\n\n  // Public: Returns a {Matcher} for objects containing the provided value.\n  contains(val) {\n    this._assertPresentAndQueryable('contains', val);\n    return new Matcher(this, 'contains', val);\n  }\n\n  containsAny(vals) {\n    this._assertPresentAndQueryable('contains', vals);\n    return new Matcher(this, 'containsAny', vals);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-datetime.es6",
    "content": "import Attribute from './attribute';\nimport Matcher from './matcher';\n\n/*\nPublic: The value of this attribute is always a Javascript `Date`, or `null`.\n\nSection: Database\n*/\nexport default class AttributeDateTime extends Attribute {\n  toJSON(val) {\n    if (!val) {\n      return null;\n    }\n    if (!(val instanceof Date)) {\n      throw new Error(`Attempting to toJSON AttributeDateTime which is not a date: ${this.modelKey} = ${val}`);\n    }\n    return val.getTime() / 1000.0;\n  }\n\n  fromJSON(val) {\n    return val ? new Date(val * 1000) : null;\n  }\n\n  columnSQL() {\n    return `${this.jsonKey} INTEGER`;\n  }\n\n  // Public: Returns a {Matcher} for objects greater than the provided value.\n  greaterThan(val) {\n    this._assertPresentAndQueryable('greaterThan', val);\n    return new Matcher(this, '>', val)\n  }\n\n  // Public: Returns a {Matcher} for objects less than the provided value.\n  lessThan(val) {\n    this._assertPresentAndQueryable('lessThan', val);\n    return new Matcher(this, '<', val);\n  }\n\n  // Public: Returns a {Matcher} for objects greater than the provided value.\n  greaterThanOrEqualTo(val) {\n    this._assertPresentAndQueryable('greaterThanOrEqualTo', val);\n    return new Matcher(this, '>=', val);\n  }\n\n  // Public: Returns a {Matcher} for objects less than the provided value.\n  lessThanOrEqualTo(val) {\n    this._assertPresentAndQueryable('lessThanOrEqualTo', val);\n    return new Matcher(this, '<=', val);\n  }\n\n  gt = AttributeDateTime.greaterThan;\n  lt = AttributeDateTime.lessThan;\n  gte = AttributeDateTime.greaterThanOrEqualTo;\n  lte = AttributeDateTime.lessThanOrEqualTo;\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-joined-data.es6",
    "content": "import Attribute from './attribute';\n\nconst NullPlaceholder = \"!NULLVALUE!\";\n\n/*\nPublic: Joined Data attributes allow you to store certain attributes of an\nobject in a separate table in the database. We use this attribute\ntype for Message bodies. Storing message bodies, which can be very\nlarge, in a separate table allows us to make queries on message\nmetadata extremely fast, and inflate Message objects without their\nbodies to build the thread list.\n\nWhen building a query on a model with a JoinedData attribute, you need\nto call `include` to explicitly load the joined data attribute.\nThe query builder will automatically perform a `LEFT OUTER JOIN` with\nthe secondary table to retrieve the attribute:\n\n```coffee\nDatabaseStore.find(Message, '123').then (message) ->\n  // message.body is undefined\n\nDatabaseStore.find(Message, '123').include(Message.attributes.body).then (message) ->\n  // message.body is defined\n```\n\nWhen you call `persistModel`, JoinedData attributes are automatically\nwritten to the secondary table.\n\nJoinedData attributes cannot be `queryable`.\n\nSection: Database\n*/\nexport default class AttributeJoinedData extends Attribute {\n  static NullPlaceholder = NullPlaceholder;\n\n  constructor({modelKey, jsonKey, modelTable, queryable, serializeFn, deserializeFn}) {\n    super({modelKey, jsonKey, queryable});\n    this.modelTable = modelTable;\n    this.serializeFn = serializeFn;\n    this.deserializeFn = deserializeFn;\n  }\n\n  serialize(thisValue, val) {\n    if (this.serializeFn) {\n      return this.serializeFn.call(thisValue, val);\n    }\n    return val;\n  }\n\n  deserialize(thisValue, val) {\n    if (this.deserializeFn) {\n      return this.deserializeFn.call(thisValue, val);\n    }\n    return val;\n  }\n\n  toJSON(val) {\n    return val;\n  }\n\n  fromJSON(val) {\n    return (val === null || val === undefined || val === false) ? null : `${val}`;\n  }\n\n  selectSQL() {\n    // NullPlaceholder is necessary because if the LEFT JOIN returns nothing, it leaves the field\n    // blank, and it comes through in the result row as \"\" rather than NULL\n    return `IFNULL(\\`${this.modelTable}\\`.\\`value\\`, '${NullPlaceholder}') AS \\`${this.modelKey}\\``;\n  }\n\n  includeSQL(klass) {\n    return `LEFT OUTER JOIN \\`${this.modelTable}\\` ON \\`${this.modelTable}\\`.\\`id\\` = \\`${klass.name}\\`.\\`id\\``;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-number.es6",
    "content": "import Attribute from './attribute';\nimport Matcher from './matcher';\n\n/*\nPublic: The value of this attribute is always a number, or null.\n\nSection: Database\n*/\nexport default class AttributeNumber extends Attribute {\n  toJSON(val) {\n    return val;\n  }\n\n  fromJSON(val) {\n    return isNaN(val) ? null : Number(val);\n  }\n\n  columnSQL() {\n    return `${this.jsonKey} INTEGER`;\n  }\n\n  // Public: Returns a {Matcher} for objects greater than the provided value.\n  greaterThan(val) {\n    this._assertPresentAndQueryable('greaterThan', val);\n    return new Matcher(this, '>', val);\n  }\n\n  // Public: Returns a {Matcher} for objects less than the provided value.\n  lessThan(val) {\n    this._assertPresentAndQueryable('lessThan', val);\n    return new Matcher(this, '<', val);\n  }\n\n  // Public: Returns a {Matcher} for objects greater than the provided value.\n  greaterThanOrEqualTo(val) {\n    this._assertPresentAndQueryable('greaterThanOrEqualTo', val);\n    return new Matcher(this, '>=', val);\n  }\n\n  // Public: Returns a {Matcher} for objects less than the provided value.\n  lessThanOrEqualTo(val) {\n    this._assertPresentAndQueryable('lessThanOrEqualTo', val);\n    return new Matcher(this, '<=', val);\n  }\n\n  gt = AttributeNumber.prototype.greaterThan;\n  lt = AttributeNumber.prototype.lessThan;\n  gte = AttributeNumber.prototype.greaterThanOrEqualTo;\n  lte = AttributeNumber.prototype.lessThanOrEqualTo;\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-object.es6",
    "content": "import Attribute from './attribute';\n\n/*\nPublic: An object that can be cast to `itemClass`\nSection: Database\n*/\nexport default class AttributeObject extends Attribute {\n  constructor({modelKey, jsonKey, itemClass, queryable}) {\n    super({modelKey, jsonKey, queryable});\n    this.ItemClass = itemClass;\n  }\n\n  toJSON(val) {\n    return (val && val.toJSON) ? val.toJSON() : val;\n  }\n\n  fromJSON(val) {\n    if (!this.ItemClass) {\n      return val || \"\";\n    }\n    const obj = new this.ItemClass(val);\n\n    // Important: if no ids are in the JSON, don't make them up randomly.\n    // This causes an object to be \"different\" each time it's de-serialized\n    // even if it's actually the same, makes React components re-render!\n    obj.clientId = undefined;\n\n    // Warning: typeof null is object\n    if (obj.fromJSON && !!val && (typeof val === 'object')) {\n      obj.fromJSON(val);\n    }\n\n    return obj;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-serverid.es6",
    "content": "import AttributeString from './attribute-string';\nimport Utils from '../models/utils';\n\n/*\nPublic: The value of this attribute is always a string or `null`.\n\nString attributes can be queries using `equal`, `not`, and `startsWith`. Matching on\n`greaterThan` and `lessThan` is not supported.\n\nSection: Database\n*/\nexport default class AttributeServerId extends AttributeString {\n  toJSON(val) {\n    if (val && Utils.isTempId(val)) {\n      throw new Error(`AttributeServerId::toJSON (${this.modelKey}) ${val} does not look like a valid server id`);\n    }\n    return super.toJSON(val);\n  }\n\n  equal(val) {\n    if (val && Utils.isTempId(val)) {\n      throw new Error(`AttributeServerId::equal (${this.modelKey}) ${val} is not a valid value for this field.`);\n    }\n    return super.equal(val);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute-string.es6",
    "content": "import Attribute from './attribute';\nimport Matcher from './matcher';\n\n/*\nPublic: The value of this attribute is always a string or `null`.\n\nString attributes can be queries using `equal`, `not`, and `startsWith`. Matching on\n`greaterThan` and `lessThan` is not supported.\n\nSection: Database\n*/\nexport default class AttributeString extends Attribute {\n  toJSON(val) {\n    return val;\n  }\n\n  fromJSON(val) {\n    return (val === null || val === undefined || val === false) ? null : `${val}`;\n  }\n\n  // Public: Returns a {Matcher} for objects starting with the provided value.\n  startsWith(val) {\n    return new Matcher(this, 'startsWith', val);\n  }\n\n  columnSQL() {\n    return `${this.jsonKey} TEXT`;\n  }\n\n  like(val) {\n    this._assertPresentAndQueryable('like', val);\n    return new Matcher(this, 'like', val);\n  }\n\n  lessThan(val) {\n    this._assertPresentAndQueryable('lessThanOrEqualTo', val);\n    return new Matcher(this, '<', val);\n  }\n\n  lessThanOrEqualTo(val) {\n    this._assertPresentAndQueryable('lessThanOrEqualTo', val);\n    return new Matcher(this, '<=', val);\n  }\n\n  greaterThan(val) {\n    this._assertPresentAndQueryable('greaterThanOrEqualTo', val);\n    return new Matcher(this, '>', val);\n  }\n\n  greaterThanOrEqualTo(val) {\n    this._assertPresentAndQueryable('greaterThanOrEqualTo', val);\n    return new Matcher(this, '>=', val);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/attribute.es6",
    "content": "import Matcher from './matcher';\nimport SortOrder from './sort-order';\n\n/*\nPublic: The Attribute class represents a single model attribute, like 'account_id'.\nSubclasses of {Attribute} like {AttributeDateTime} know how to covert between\nthe JSON representation of that type and the javascript representation.\nThe Attribute class also exposes convenience methods for generating {Matcher} objects.\n\nSection: Database\n*/\nexport default class Attribute {\n  constructor({modelKey, queryable, jsonKey, defaultValue, loadFromColumn}) {\n    this.modelKey = modelKey;\n    this.jsonKey = jsonKey || modelKey;\n    this.queryable = queryable;\n    this.defaultValue = defaultValue;\n    if (loadFromColumn && !queryable) {\n      throw new Error('loadFromColumn requires queryable');\n    }\n    this.loadFromColumn = loadFromColumn;\n  }\n\n  _assertPresentAndQueryable(fnName, val) {\n    if (val === undefined) {\n      throw new Error(`Attribute::${fnName} (${this.modelKey}) - you must provide a value`);\n    }\n    if (!this.queryable) {\n      throw new Error(`Attribute::${fnName} (${this.modelKey}) - this field cannot be queried against`);\n    }\n  }\n\n  // Public: Returns a {Matcher} for objects `=` to the provided value.\n  equal(val) {\n    this._assertPresentAndQueryable('equal', val);\n    return new Matcher(this, '=', val);\n  }\n\n  // Public: Returns a {Matcher} for objects `=` to the provided value.\n  in(val, {notIn} = {}) {\n    this._assertPresentAndQueryable('in', val);\n\n    if (!(val instanceof Array)) {\n      throw new Error(`Attribute.in: you must pass an array of values.`);\n    }\n    if (val.length === 0) {\n      console.warn(`Attribute::in (${this.modelKey}) called with an empty set. You should avoid this useless query!`);\n    }\n    if (val.length === 1) {\n      const testChar = notIn ? \"!=\" : \"=\"\n      return new Matcher(this, testChar, val[0])\n    }\n    const matcherType = notIn ? \"not in\" : \"in\";\n    return new Matcher(this, matcherType, val);\n  }\n\n  notIn(val) {\n    return this.in(val, {notIn: true})\n  }\n\n  // Public: Returns a {Matcher} for objects `!=` to the provided value.\n  not(val) {\n    this._assertPresentAndQueryable('not', val);\n    return new Matcher(this, '!=', val);\n  }\n\n  // Public: Returns a descending {SortOrder} for this attribute.\n  descending() {\n    return new SortOrder(this, 'DESC');\n  }\n\n  // Public: Returns an ascending {SortOrder} for this attribute.\n  ascending() {\n    return new SortOrder(this, 'ASC');\n  }\n\n  toJSON(val) {\n    return val;\n  }\n\n  fromJSON(val) {\n    return val || null;\n  }\n\n  fromColumn(val) {\n    return this.fromJSON(val);\n  }\n\n  needsColumn() {\n    return this.queryable && this.columnSQL && this.jsonKey !== 'id'\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/matcher.es6",
    "content": "import {tableNameForJoin} from '../models/utils';\nimport LocalSearchQueryBackend from '../../services/search/search-query-backend-local'\n\n// https://www.sqlite.org/faq.html#q14\n// That's right. Two single quotes in a row…\nconst singleQuoteEscapeSequence = \"''\";\n\n// https://www.sqlite.org/fts5.html#section_3\nconst doubleQuoteEscapeSequence = '\"\"';\n\n\n/*\nPublic: The Matcher class encapsulates a particular comparison clause on an {Attribute}.\nMatchers can evaluate whether or not an object matches them, and also compose\nSQL clauses for the DatabaseStore. Each matcher has a reference to a model\nattribute, a comparator and a value.\n\n```coffee\n\n// Retrieving Matchers\n\nisUnread = Thread.attributes.unread.equal(true)\n\nhasLabel = Thread.attributes.categories.contains('label-id-123')\n\n// Using Matchers in Database Queries\n\nDatabaseStore.findAll(Thread).where(isUnread)...\n\n// Using Matchers to test Models\n\nthreadA = new Thread(unread: true)\nthreadB = new Thread(unread: false)\n\nisUnread.evaluate(threadA)\n// => true\nisUnread.evaluate(threadB)\n// => false\n\n```\n\nSection: Database\n*/\nclass Matcher {\n  constructor(attr, comparator, val) {\n    this.attr = attr;\n    this.comparator = comparator;\n    this.val = val;\n\n    this.muid = Matcher.muid;\n    Matcher.muid = (Matcher.muid + 1) % 50;\n  }\n\n  attribute() {\n    return this.attr;\n  }\n\n  value() {\n    return this.val;\n  }\n\n  evaluate(model) {\n    let modelValue = model[this.attr.modelKey];\n    if (modelValue instanceof Function) {\n      modelValue = modelValue()\n    }\n    const matcherValue = this.val;\n\n    // Given an array of strings or models, and a string or model search value,\n    // will find if a match exists.\n    const modelArrayContainsValue = (array, searchItem) => {\n      const asId = (v) => ((v && v.id) ? v.id : v);\n      const search = asId(searchItem)\n      for (const item of array) {\n        if (asId(item) === search) {\n          return true;\n        }\n      }\n      return false;\n    }\n\n    switch (this.comparator) {\n      case '=':\n        return modelValue === matcherValue\n      case '<':\n        return modelValue < matcherValue\n      case '>':\n        return modelValue > matcherValue\n      case '<=':\n        return modelValue <= matcherValue\n      case '>=':\n        return modelValue >= matcherValue\n      case 'in':\n        return matcherValue.includes(modelValue)\n      case 'not in':\n        return !(matcherValue.includes(modelValue))\n      case 'contains':\n        return modelArrayContainsValue(modelValue, matcherValue)\n      case 'containsAny':\n        return !!matcherValue.find((submatcherValue) => modelArrayContainsValue(modelValue, submatcherValue))\n      case 'startsWith':\n        return modelValue.startsWith(matcherValue)\n      case 'like':\n        return modelValue.search(new RegExp(`.*${matcherValue}.*`, \"gi\")) >= 0\n      default:\n        throw new Error(`Matcher.evaulate() not sure how to evaluate ${this.attr.modelKey} with comparator ${this.comparator}`)\n    }\n  }\n\n  joinTableRef() {\n    return `M${this.muid}`;\n  }\n\n  joinSQL(klass) {\n    switch (this.comparator) {\n      case 'contains':\n      case 'containsAny': {\n        const joinTable = tableNameForJoin(klass, this.attr.itemClass);\n        const joinTableRef = this.joinTableRef();\n        return `INNER JOIN \\`${joinTable}\\` AS \\`${joinTableRef}\\` ON \\`${joinTableRef}\\`.\\`id\\` = \\`${klass.name}\\`.\\`id\\``;\n      }\n      default:\n        return false;\n    }\n  }\n\n  whereSQL(klass) {\n    const val = (this.comparator === \"like\") ? `%${this.val}%` : this.val;\n    let escaped = null;\n\n    if (typeof val === 'string') {\n      escaped = `'${val.replace(/'/g, singleQuoteEscapeSequence)}'`;\n    } else if (val === true) {\n      escaped = 1\n    } else if (val === false) {\n      escaped = 0\n    } else if (val instanceof Date) {\n      escaped = val.getTime() / 1000\n    } else if (val instanceof Array) {\n      const escapedVals = []\n      for (const v of val) {\n        if (typeof v !== 'string') {\n          throw new Error(`${this.attr.jsonKey} value ${v} must be a string.`);\n        }\n        escapedVals.push(`'${v.replace(/'/g, singleQuoteEscapeSequence)}'`);\n      }\n      escaped = `(${escapedVals.join(',')})`;\n    } else {\n      escaped = val;\n    }\n\n    switch (this.comparator) {\n      case '=': {\n        if (escaped === null) {\n          return `\\`${klass.name}\\`.\\`${this.attr.jsonKey}\\` IS NULL`;\n        }\n        return `\\`${klass.name}\\`.\\`${this.attr.jsonKey}\\` = ${escaped}`;\n      }\n      case '!=': {\n        if (escaped === null) {\n          return `\\`${klass.name}\\`.\\`${this.attr.jsonKey}\\` IS NOT NULL`;\n        }\n        return `\\`${klass.name}\\`.\\`${this.attr.jsonKey}\\` != ${escaped}`;\n      }\n      case 'startsWith':\n        return \" RAISE `TODO`; \";\n      case 'contains':\n        return `\\`${this.joinTableRef()}\\`.\\`value\\` = ${escaped}`;\n      case 'containsAny':\n        return `\\`${this.joinTableRef()}\\`.\\`value\\` IN ${escaped}`;\n      default:\n        return `\\`${klass.name}\\`.\\`${this.attr.jsonKey}\\` ${this.comparator} ${escaped}`;\n    }\n  }\n}\n\nMatcher.muid = 0\n\nclass OrCompositeMatcher extends Matcher {\n  constructor(children) {\n    super();\n    this.children = children;\n  }\n\n  attribute() {\n    return null;\n  }\n\n  value() {\n    return null;\n  }\n\n  evaluate(model) {\n    return this.children.some((matcher) => matcher.evaluate(model));\n  }\n\n  joinSQL(klass) {\n    const joins = []\n    for (const matcher of this.children) {\n      const join = matcher.joinSQL(klass);\n      if (join) {\n        joins.push(join);\n      }\n    }\n    return (joins.length) ? joins.join(\" \") : false;\n  }\n\n  whereSQL(klass) {\n    const wheres = this.children.map((matcher) => matcher.whereSQL(klass));\n    return `(${wheres.join(\" OR \")})`;\n  }\n}\n\nclass AndCompositeMatcher extends Matcher {\n  constructor(children) {\n    super();\n    this.children = children;\n  }\n\n  attribute() {\n    return null;\n  }\n\n  value() {\n    return null;\n  }\n\n  evaluate(model) {\n    return this.children.every((m) => m.evaluate(model));\n  }\n\n  joinSQL(klass) {\n    const joins = []\n    for (const matcher of this.children) {\n      const join = matcher.joinSQL(klass);\n      if (join) {\n        joins.push(join);\n      }\n    }\n    return joins;\n  }\n\n  whereSQL(klass) {\n    const wheres = this.children.map((m) => m.whereSQL(klass));\n    return `(${wheres.join(\" AND \")})`;\n  }\n}\n\nclass NotCompositeMatcher extends AndCompositeMatcher {\n  whereSQL(klass) {\n    return `NOT (${super.whereSQL(klass)})`;\n  }\n}\n\nclass StructuredSearchMatcher extends Matcher {\n  constructor(searchQuery) {\n    super(null, null, null);\n    this._searchQuery = searchQuery;\n  }\n\n  attribute() {\n    return null;\n  }\n\n  value() {\n    return null\n  }\n\n  // The only way to truly check if a model matches this matcher is to run the query\n  // again and check if the model is in the results. This is too expensive, so we\n  // will always return true so models aren't excluded from the\n  // SearchQuerySubscription result set\n  evaluate() {\n    return true;\n  }\n\n  whereSQL(klass) {\n    return (new LocalSearchQueryBackend(klass.name)).compile(this._searchQuery)\n  }\n}\n\nclass SearchMatcher extends Matcher {\n  constructor(searchQuery) {\n    if ((typeof searchQuery !== 'string') || (searchQuery.length === 0)) {\n      throw new Error(\"You must pass a string with non-zero length to search.\")\n    }\n    super(null, null, null);\n    this.searchQuery = (\n      searchQuery.trim()\n      .replace(/^['\"]/, \"\")\n      .replace(/['\"]$/, \"\")\n      .replace(/'/g, singleQuoteEscapeSequence)\n      .replace(/\"/g, doubleQuoteEscapeSequence)\n    )\n  }\n\n  attribute() {\n    return null;\n  }\n\n  value() {\n    return null\n  }\n\n  // The only way to truly check if a model matches this matcher is to run the query\n  // again and check if the model is in the results. This is too expensive, so we\n  // will always return true so models aren't excluded from the\n  // SearchQuerySubscription result set\n  evaluate() {\n    return true;\n  }\n\n  whereSQL(klass) {\n    const searchTable = `${klass.name}Search`\n    return `\\`${klass.name}\\`.\\`id\\` IN (SELECT \\`content_id\\` FROM \\`${searchTable}\\` WHERE \\`${searchTable}\\` MATCH '\"${this.searchQuery}\"*' LIMIT 1000)`;\n  }\n}\n\nMatcher.Or = OrCompositeMatcher\nMatcher.And = AndCompositeMatcher\nMatcher.Not = NotCompositeMatcher\nMatcher.Search = SearchMatcher\nMatcher.StructuredSearch = StructuredSearchMatcher\n\nexport default Matcher;\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes/sort-order.es6",
    "content": "/*\nPublic: Represents a particular sort direction on a particular column. You should not\ninstantiate SortOrders manually. Instead, call {Attribute::ascending} or\n{Attribute::descending} to obtain a sort order instance:\n\n```coffee\nDatabaseStore.findBy(Message)\n  .where({threadId: threadId, draft: false})\n  .order(Message.attributes.date.descending()).then (messages) ->\n\n```\n\nSection: Database\n*/\nexport default class SortOrder {\n  constructor(attr, direction = 'DESC') {\n    this.attr = attr;\n    this.direction = direction;\n  }\n\n  orderBySQL(klass) {\n    return `\\`${klass.name}\\`.\\`${this.attr.jsonKey}\\` ${this.direction}`;\n  }\n\n  attribute() {\n    return this.attr;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/attributes.es6",
    "content": "import Matcher from './attributes/matcher'\nimport SortOrder from './attributes/sort-order'\nimport AttributeNumber from './attributes/attribute-number'\nimport AttributeString from './attributes/attribute-string'\nimport AttributeObject from './attributes/attribute-object'\nimport AttributeBoolean from './attributes/attribute-boolean'\nimport AttributeDateTime from './attributes/attribute-datetime'\nimport AttributeCollection from './attributes/attribute-collection'\nimport AttributeJoinedData from './attributes/attribute-joined-data'\nimport AttributeServerId from './attributes/attribute-serverid'\n\nexport default {\n  Matcher: Matcher,\n  SortOrder: SortOrder,\n\n  Number: (...args) => new AttributeNumber(...args),\n  String: (...args) => new AttributeString(...args),\n  Object: (...args) => new AttributeObject(...args),\n  Boolean: (...args) => new AttributeBoolean(...args),\n  DateTime: (...args) => new AttributeDateTime(...args),\n  Collection: (...args) => new AttributeCollection(...args),\n  JoinedData: (...args) => new AttributeJoinedData(...args),\n  ServerId: (...args) => new AttributeServerId(...args),\n\n  AttributeNumber: AttributeNumber,\n  AttributeString: AttributeString,\n  AttributeObject: AttributeObject,\n  AttributeBoolean: AttributeBoolean,\n  AttributeDateTime: AttributeDateTime,\n  AttributeCollection: AttributeCollection,\n  AttributeJoinedData: AttributeJoinedData,\n  AttributeServerId: AttributeServerId,\n};\n"
  },
  {
    "path": "packages/client-app/src/flux/coffee-helpers.coffee",
    "content": "_ = require 'underscore'\n\nmodule.exports = CoffeeHelpers =\n  # This copied out CoffeeScript\n  includeModule: (mixin) ->\n    if not mixin\n      return throw new Error('Supplied mixin was not found')\n\n    if not _\n      return throw new Error('Underscore was not found')\n\n    mixin = mixin.prototype if _.isFunction(mixin)\n\n    # Make a copy of the superclass with the same constructor and use it\n    # instead of adding functions directly to the superclass.\n    if @.__super__\n      tmpSuper = _.extend({}, @.__super__)\n      tmpSuper.constructor = @.__super__.constructor\n\n    @.__super__ = tmpSuper || {}\n\n    # Copy function over to prototype and the new intermediate superclass.\n    for methodName, funct of mixin when methodName not in ['included']\n      @.__super__[methodName] = funct\n\n      if not @prototype.hasOwnProperty(methodName)\n        @prototype[methodName] = funct\n\n    mixin.included?.apply(this)\n    this\n\n  # Allows the root objects to extend other objects as class methods via the\n  # object.\n  extendModule: (module) ->\n    if not module?\n      console.warn \"The module you are trying to extend does not exist. Ensure you have put it on this page's manifest.\"\n\n    if _.isFunction(module) then module = module()\n\n    @[key] = value for key, value of module\n    return @\n\n  # Allows the root objects to include other objects as instance methods via\n  # the prototype\n  simpleInclude: (module) ->\n    if not module?\n      console.warn \"The module you are trying to include does not exist. Ensure you have put it on this page's manifest.\"\n\n    if _.isFunction(module) then module = module()\n\n    @::[key] = value for key, value of module\n    return @\n\n  # This should be called as the first item from the constructor of an\n  # object.\n  #\n  # You can optionally pass a refernce to a super's prototype.\n  boundInclude: (module, _super) ->\n    if not module?\n      console.warn \"The module you are trying to include does not exist. Ensure you have put it on this page's manifest.\"\n      return\n\n    if not _.isFunction(module)\n      console.warn \"To do a scoped include the Module must be a function instead of a plain old javascript object thereby allowing `this` to be bound properly.\"\n      return\n\n    for key, value of module.call(@, _super)\n      @[key] = value unless @[key]?\n    return @\n"
  },
  {
    "path": "packages/client-app/src/flux/errors.es6",
    "content": "// This file contains custom Nylas error classes.\n//\n// In general I think these should be created as sparingly as possible.\n// Only add one if you really can't use native `new Error(\"my msg\")`\n\n\n// A wrapper around the three arguments we get back from node's `request`\n// method. We wrap it in an error object because Promises can only call\n// `reject` or `resolve` with one argument (not three).\nexport class APIError extends Error {\n\n  static NonReportableStatusCodes = [\n    0,   // When errors like ETIMEDOUT, ECONNABORTED or ESOCKETTIMEDOUT occur from the client\n    401, // Don't report `Incorrect username or password`\n    404, // Don't report not-founds\n    408, // Timeout error code\n    429, // Too many requests\n  ]\n\n  constructor({error, message, response, body, requestOptions, statusCode} = {}) {\n    super();\n\n    this.name = \"APIError\";\n    this.error = error;\n    this.body = body;\n    this.requestOptions = requestOptions;\n    this.statusCode = statusCode;\n    this.message = message;\n\n    if (this.statusCode == null) {\n      if (response && response.statusCode != null) {\n        this.statusCode = response.statusCode\n      }\n\n      if (error) {\n        // If the server returns anything (including 500s and other bad\n        // responses, the `error` object for the `request` will be null)\n        //\n        // The Node `request` library emits a special type of timeout\n        // error for ESOCKETTIMEDOUT and ETIMEDOUT. When it does this it\n        // sets the `code` param on the error object. These errors are\n        // retryable and we use out special `0` status code.\n        //\n        // It may also emit normal `Error` objects for other unforseen\n        // issues. In this case we set a `500` status code.\n        if (error.code) {\n          this.statusCode = 0;\n        } else {\n          this.statusCode = 500;\n        }\n      }\n    }\n    if (this.requestOptions == null) {\n      this.requestOptions = response ? response.requestOptions : null;\n    }\n\n    this.stack = (new Error()).stack;\n    if (!this.message) {\n      if (this.body) {\n        this.message = this.body.message || this.body.error || JSON.stringify(this.body)\n      } else {\n        this.message = this.error ? this.error.message || this.error.toString() : null;\n      }\n    }\n    this.errorType = (this.body ? this.body.type : null);\n  }\n\n  shouldReportError() {\n    return !APIError.NonReportableStatusCodes.includes(this.statusCode)\n  }\n\n  fromJSON(json = {}) {\n    for (const key of Object.keys(json)) {\n      this[key] = json[key];\n    }\n    return this;\n  }\n}\n\nexport class RequestEnsureOnceError extends Error {\n\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/account.es6",
    "content": "/* eslint global-require:0 */\nimport Attributes from '../attributes'\nimport ModelWithMetadata from './model-with-metadata'\n\nlet CategoryStore = null\nlet Contact = null\n\n/*\n * Public: The Account model represents a Account served by the Nylas Platform API.\n * Every object on the Nylas platform exists within a Account, which typically represents\n * an email account.\n *\n * For more information about Accounts on the Nylas Platform, read the\n * [Account API Documentation](https://nylas.com/cloud/docs#account)\n *\n * ## Attributes\n *\n * `name`: {AttributeString} The name of the Account.\n *\n * `provider`: {AttributeString} The Account's mail provider  (ie: `gmail`)\n *\n * `emailAddress`: {AttributeString} The Account's email address\n * (ie: `ben@nylas.com`). Queryable.\n *\n * `organizationUnit`: {AttributeString} Either \"label\" or \"folder\".\n * Depending on the provider, the account may be organized by folders or\n * labels.\n *\n * This class also inherits attributes from {Model}\n *\n * Section: Models\n */\nexport default class Account extends ModelWithMetadata {\n\n  static SYNC_STATE_RUNNING = \"running\"\n\n  static SYNC_STATE_AUTH_FAILED = \"invalid\"\n\n  static SYNC_STATE_ERROR = \"sync_error\"\n\n  static N1_CLOUD_STATE_RUNNING = \"n1_cloud_running\"\n\n  static N1_CLOUD_STATE_AUTH_FAILED = \"n1_cloud_auth_failed\"\n\n  static attributes = Object.assign({}, ModelWithMetadata.attributes, {\n    name: Attributes.String({\n      modelKey: 'name',\n    }),\n\n    provider: Attributes.String({\n      modelKey: 'provider',\n    }),\n\n    emailAddress: Attributes.String({\n      queryable: true,\n      modelKey: 'emailAddress',\n      jsonKey: 'email_address',\n    }),\n\n    organizationUnit: Attributes.String({\n      modelKey: 'organizationUnit',\n      jsonKey: 'organization_unit',\n    }),\n\n    label: Attributes.String({\n      modelKey: 'label',\n    }),\n\n    aliases: Attributes.Object({\n      modelKey: 'aliases',\n    }),\n\n    defaultAlias: Attributes.Object({\n      modelKey: 'defaultAlias',\n      jsonKey: 'default_alias',\n    }),\n\n    syncState: Attributes.String({\n      modelKey: 'syncState',\n      jsonKey: 'sync_state',\n    }),\n\n    syncError: Attributes.Object({\n      modelKey: 'syncError',\n      jsonKey: 'sync_error',\n    }),\n\n    n1CloudState: Attributes.String({\n      modelKey: 'n1CloudState',\n      jsonKey: 'n1_cloud_state',\n    }),\n  });\n\n  constructor(args) {\n    super(args)\n    this.aliases = this.aliases || [];\n    this.label = this.label || this.emailAddress;\n    this.syncState = this.syncState || \"running\";\n  }\n\n  fromJSON(json) {\n    super.fromJSON(json);\n    if (!this.label) {\n      this.label = this.emailAddress;\n    }\n    return this;\n  }\n\n  // Returns a {Contact} model that represents the current user.\n  me() {\n    Contact = Contact || require('./contact').default\n\n    return new Contact({\n      accountId: this.id,\n      name: this.name,\n      email: this.emailAddress,\n    })\n  }\n\n  meUsingAlias(alias) {\n    Contact = Contact || require('./contact').default\n\n    if (!alias) {\n      return this.me()\n    }\n    return Contact.fromString(alias, {accountId: this.id})\n  }\n\n  defaultMe() {\n    if (this.defaultAlias) {\n      return this.meUsingAlias(this.defaultAlias)\n    }\n    return this.me()\n  }\n\n  usesLabels() {\n    return this.organizationUnit === \"label\"\n  }\n\n  usesFolders() {\n    return this.organizationUnit === \"folder\"\n  }\n\n  categoryLabel() {\n    if (this.usesFolders()) {\n      return 'Folders'\n    } else if (this.usesLabels()) {\n      return 'Labels'\n    }\n    return 'Unknown'\n  }\n\n  categoryCollection() {\n    return `${this.organizationUnit}s`\n  }\n\n  categoryIcon() {\n    if (this.usesFolders()) {\n      return 'folder.png'\n    } else if (this.usesLabels()) {\n      return 'tag.png'\n    }\n    return 'folder.png'\n  }\n\n  // Public: Returns the localized, properly capitalized provider name,\n  // like Gmail, Exchange, or Outlook 365\n  displayProvider() {\n    if (this.provider === 'eas') {\n      return 'Exchange'\n    } else if (this.provider === 'gmail') {\n      return 'Gmail'\n    }\n    return this.provider\n  }\n\n  canArchiveThreads() {\n    CategoryStore = CategoryStore || require('../stores/category-store')\n\n    return CategoryStore.getArchiveCategory(this)\n  }\n\n  canTrashThreads() {\n    CategoryStore = CategoryStore || require('../stores/category-store')\n\n    return CategoryStore.getTrashCategory(this)\n  }\n\n  defaultFinishedCategory() {\n    CategoryStore = CategoryStore || require('../stores/category-store')\n\n    const preferDelete = NylasEnv.config.get('core.reading.backspaceDelete')\n    const archiveCategory = CategoryStore.getArchiveCategory(this)\n    const trashCategory = CategoryStore.getTrashCategory(this)\n\n    if (preferDelete || !archiveCategory) {\n      return trashCategory\n    }\n    return archiveCategory\n  }\n\n  hasN1CloudError() {\n    return this.n1CloudState === Account.N1_CLOUD_STATE_AUTH_FAILED\n  }\n\n  hasSyncStateError() {\n    return this.syncState !== Account.SYNC_STATE_RUNNING\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/calendar.es6",
    "content": "import Model from './model';\nimport Attributes from '../attributes';\n\n/**\nPublic: The Calendar model represents a Calendar object served by the\nNylas Platform API.  For more information about Calendar on the Nylas\nPlatform, read the [Calendar API\nDocumentation](https://nylas.com/cloud/docs#calendars)\n\n## Attributes\n\n`name`: {AttributeString} The name of the calendar.\n\n`description`: {AttributeString} The description of the calendar.\n\nThis class also inherits attributes from {Model}\n\nSection: Models\n*/\nexport default class Calendar extends Model {\n  static attributes = Object.assign({}, Model.attributes, {\n    name: Attributes.String({\n      modelKey: 'name',\n      jsonKey: 'name',\n    }),\n    description: Attributes.String({\n      modelKey: 'description',\n      jsonKey: 'description',\n    }),\n    readOnly: Attributes.Boolean({\n      modelKey: 'readOnly',\n      jsonKey: 'read_only',\n    }),\n  });\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/category.es6",
    "content": "/* eslint global-require: 0 */\nimport {FolderSyncProgressStore} from 'nylas-exports';\nimport Model from './model';\nimport Attributes from '../attributes';\nlet AccountStore = null\n\n// We look for a few standard categories and display them in the Mailboxes\n// portion of the left sidebar. Note that these may not all be present on\n// a particular account.\nconst ToObject = (arr) => {\n  return arr.reduce((o, v) => {\n    o[v] = v;\n    return o;\n  }, {});\n}\n\nconst StandardCategories = ToObject([\n  \"inbox\",\n  \"important\",\n  \"sent\",\n  \"drafts\",\n  \"all\",\n  \"spam\",\n  \"archive\",\n  \"trash\",\n]);\n\nconst LockedCategories = ToObject([\n  \"sent\",\n  \"drafts\",\n  \"N1-Snoozed\",\n]);\n\nconst HiddenCategories = ToObject([\n  \"sent\",\n  \"drafts\",\n  \"all\",\n  \"archive\",\n  \"starred\",\n  \"important\",\n  \"N1-Snoozed\",\n]);\n\n/**\nPrivate:\nThis abstract class has only two concrete implementations:\n  - `Folder`\n  - `Label`\n\nSee the equivalent models for details.\n\nFolders and Labels have different semantics. The `Category` class only exists to help DRY code where they happen to behave the same\n\n## Attributes\n\n`name`: {AttributeString} The internal name of the label or folder. Queryable.\n\n`displayName`: {AttributeString} The display-friendly name of the label or folder. Queryable.\n\nSection: Models\n*/\nexport default class Category extends Model {\n\n  static attributes = Object.assign({}, Model.attributes, {\n    name: Attributes.String({\n      queryable: true,\n      modelKey: 'name',\n    }),\n    displayName: Attributes.String({\n      queryable: true,\n      modelKey: 'displayName',\n      jsonKey: 'display_name',\n    }),\n    imapName: Attributes.String({\n      modelKey: 'imapName',\n      jsonKey: 'imap_name',\n    }),\n    syncProgress: Attributes.Object({\n      modelKey: 'syncProgress',\n      jsonKey: 'sync_progress',\n    }),\n  });\n\n  static Types = {\n    Standard: 'standard',\n    Locked: 'locked',\n    User: 'user',\n    Hidden: 'hidden',\n  }\n\n  static StandardCategoryNames = Object.keys(StandardCategories)\n  static LockedCategoryNames = Object.keys(LockedCategories)\n  static HiddenCategoryNames = Object.keys(HiddenCategories)\n\n  static categoriesSharedName(cats) {\n    if (!cats || cats.length === 0) {\n      return null;\n    }\n    const name = cats[0].name\n    if (!cats.every((cat) => cat.name === name)) {\n      return null;\n    }\n    return name;\n  }\n\n  static additionalSQLiteConfig = {\n    setup: () => {\n      return [\n        'CREATE INDEX IF NOT EXISTS CategoryNameIndex ON Category(account_id,name)',\n        'CREATE UNIQUE INDEX IF NOT EXISTS CategoryClientIndex ON Category(client_id)',\n      ];\n    },\n  };\n\n  fromJSON(json) {\n    super.fromJSON(json);\n\n    if (this.displayName && this.displayName.startsWith('INBOX.')) {\n      this.displayName = this.displayName.substr(6);\n    }\n    if (this.displayName && this.displayName === 'INBOX') {\n      this.displayName = 'Inbox';\n    }\n    return this;\n  }\n\n  displayType() {\n    AccountStore = AccountStore || require('../stores/account-store').default;\n    if (AccountStore.accountForId(this.accountId).usesLabels()) {\n      return 'label';\n    }\n    return 'folder';\n  }\n\n  hue() {\n    if (!this.displayName) {\n      return 0;\n    }\n\n    let hue = 0;\n    for (let i = 0; i < this.displayName.length; i++) {\n      hue += this.displayName.charCodeAt(i);\n    }\n    hue *= (396.0 / 512.0);\n    return hue;\n  }\n\n  isStandardCategory(forceShowImportant) {\n    let showImportant = forceShowImportant;\n    if (showImportant === undefined) {\n      showImportant = NylasEnv.config.get('core.workspace.showImportant');\n    }\n    if (showImportant === true) {\n      return !!StandardCategories[this.name];\n    }\n    return !!StandardCategories[this.name] && (this.name !== 'important');\n  }\n\n  isLockedCategory() {\n    return !!LockedCategories[this.name] || !!LockedCategories[this.displayName];\n  }\n\n  isHiddenCategory() {\n    return !!HiddenCategories[this.name] || !!HiddenCategories[this.displayName];\n  }\n\n  isUserCategory() {\n    return !this.isStandardCategory() && !this.isHiddenCategory();\n  }\n\n  isInbox() {\n    return this.name === 'inbox'\n  }\n\n  isArchive() {\n    return ['all', 'archive'].includes(this.name);\n  }\n\n  isSyncComplete() {\n    // We sync by folders, not labels. If the category is a label, or hasn't been\n    // assigned an object type yet, just return based on the sync status for the\n    // entire account.\n    if (this.object !== 'folder') {\n      return FolderSyncProgressStore.isSyncCompleteForAccount(this.accountId);\n    }\n    return FolderSyncProgressStore.isSyncCompleteForAccount(\n      this.accountId,\n      this.name || this.displayName\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/contact.es6",
    "content": "/* eslint global-require: 0 */\nimport _str from 'underscore.string'\nimport Model from './model';\nimport Attributes from '../attributes';\nimport Utils from './utils'\nimport RegExpUtils from '../../regexp-utils'\nimport AccountStore from '../stores/account-store'\n\nlet FocusedPerspectiveStore = null; // Circular Dependency\n\nconst namePrefixes = {};\nconst nameSuffixes = {};\n\n['2dlt', '2lt', '2nd lieutenant', 'adm', 'administrative', 'admiral', 'amb', 'ambassador', 'attorney', 'atty', 'baron', 'baroness', 'bishop', 'br', 'brig gen or bg', 'brigadier general', 'brnss', 'brother', 'capt', 'captain', 'chancellor', 'chaplain', 'chapln', 'chief petty officer', 'cmdr', 'cntss', 'coach', 'col', 'colonel', 'commander', 'corporal', 'count', 'countess', 'cpl', 'cpo', 'cpt', 'doctor', 'dr', 'dr and mrs', 'drs', 'duke', 'ens', 'ensign', 'estate of', 'father', 'father', 'fr', 'frau', 'friar', 'gen', 'general', 'gov', 'governor', 'hon', 'honorable', 'judge', 'justice', 'lieutenant', 'lieutenant colonel', 'lieutenant commander', 'lieutenant general', 'lieutenant junior grade', 'lord', 'lt', 'ltc', 'lt cmdr', 'lt col', 'lt gen', 'ltg', 'lt jg', 'm', 'madame', 'mademoiselle', 'maj', 'maj', 'master sergeant', 'master sgt', 'miss', 'miss', 'mlle', 'mme', 'monsieur', 'monsignor', 'monsignor', 'mr', 'mr', 'mr & dr', 'mr and dr', 'mr & mrs', 'mr and mrs', 'mrs & mr', 'mrs and mr', 'ms', 'ms', 'msgr', 'msgr', 'ofc', 'officer', 'president', 'princess', 'private', 'prof', 'prof & mrs', 'professor', 'pvt', 'rabbi', 'radm', 'rear admiral', 'rep', 'representative', 'rev', 'reverend', 'reverends', 'revs', 'right reverend', 'rtrev', 's sgt', 'sargent', 'sec', 'secretary', 'sen', 'senator', 'senor', 'senora', 'senorita', 'sergeant', 'sgt', 'sgt', 'sheikh', 'sir', 'sister', 'sister', 'sr', 'sra', 'srta', 'staff sergeant', 'superintendent', 'supt', 'the hon', 'the honorable', 'the venerable', 'treas', 'treasurer', 'trust', 'trustees of', 'vadm', 'vice admiral'].forEach((prefix) => {\n  namePrefixes[prefix] = true;\n});\n\n['1', '2', '3', '4', '5', '6', '7', 'i', 'ii', 'iii', 'iv', 'v', 'vi', 'vii', 'viii', 'ix', '1st', '2nd', '3rd', '4th', '5th', '6th', '7th', 'cfx', 'cnd', 'cpa', 'csb', 'csc', 'csfn', 'csj', 'dc', 'dds', 'esq', 'esquire', 'first', 'fs', 'fsc', 'ihm', 'jd', 'jr', 'md', 'ocd', 'ofm', 'op', 'osa', 'osb', 'osf', 'phd', 'pm', 'rdc', 'ret', 'rsm', 'second', 'sj', 'sm', 'snd', 'sp', 'sr', 'ssj', 'us army', 'us army ret', 'usa', 'usa ret', 'usaf', 'usaf ret', 'usaf us air force', 'usmc us marine corp', 'usmcr us marine reserves', 'usn', 'usn ret', 'usn us navy', 'vm'].forEach((suffix) => {\n  nameSuffixes[suffix] = true;\n});\n\n/*\nPublic: The Contact model represents a Contact object served by the Nylas Platform API.\nFor more information about Contacts on the Nylas Platform, read the\n[Contacts API Documentation](https://nylas.com/cloud/docs#contacts)\n\nAttributes\n\n`name`: {AttributeString} The name of the contact. Queryable.\n\n`email`: {AttributeString} The email address of the contact. Queryable.\n\n`thirdPartyData`: {AttributeObject} Extra data that we find out about a\ncontact.  The data is keyed by the 3rd party service that dumped the data\nthere. The value is an object of raw data in the form that the service\nprovides\n\nWe also have \"normalized\" optional data for each contact. This list may\ngrow as the needs of a contact become more complex.\n\nThis class also inherits attributes from {Model}\n\nSection: Models\n*/\nexport default class Contact extends Model {\n  static attributes = Object.assign({}, Model.attributes, {\n    name: Attributes.String({\n      queryable: true,\n      modelKey: 'name',\n    }),\n\n    email: Attributes.String({\n      queryable: true,\n      modelKey: 'email',\n    }),\n\n    // Contains the raw thirdPartyData (keyed by the vendor name) about\n    // this contact.\n    thirdPartyData: Attributes.Object({\n      modelKey: 'thirdPartyData',\n    }),\n\n    // The following are \"normalized\" fields that we can use to consolidate\n    // various thirdPartyData source. These list of attributes should\n    // always be optional and may change as the needs of a Nylas contact\n    // change over time.\n    title: Attributes.String({\n      modelKey: 'title',\n    }),\n\n    phone: Attributes.String({\n      modelKey: 'phone',\n    }),\n\n    company: Attributes.String({\n      modelKey: 'company',\n    }),\n\n    isSearchIndexed: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'isSearchIndexed',\n      jsonKey: 'is_search_indexed',\n      defaultValue: false,\n    }),\n\n    // This corresponds to the rowid in the FTS table. We need to use the FTS\n    // rowid when updating and deleting items in the FTS table because otherwise\n    // these operations would be way too slow on large FTS tables.\n    searchIndexId: Attributes.Number({\n      modelKey: 'searchIndexId',\n      jsonKey: 'search_index_id',\n    }),\n  });\n\n  static additionalSQLiteConfig = {\n    setup: () => {\n      return [\n        'CREATE INDEX IF NOT EXISTS ContactEmailIndex ON Contact(email)',\n        'CREATE INDEX IF NOT EXISTS ContactAccountEmailIndex ON Contact(account_id, email)',\n        'CREATE INDEX IF NOT EXISTS ContactIsSearchIndexedIndex ON `Contact` (is_search_indexed, id)',\n      ];\n    },\n  };\n\n  static searchable = true;\n\n  static searchFields = ['content'];\n\n  static sortOrderAttribute = () => {\n    return Contact.attributes.id\n  }\n\n  static naturalSortOrder = () => {\n    return Contact.sortOrderAttribute().descending()\n  }\n\n  static fromString(string, {accountId} = {}) {\n    const emailRegex = RegExpUtils.emailRegex();\n    const match = emailRegex.exec(string);\n    if (emailRegex.exec(string)) {\n      throw new Error('Error while calling Contact.fromString: string contains more than one email');\n    }\n    const email = match[0];\n    let name = string.substr(0, match.index - 1);\n    if (name.endsWith('<') || name.endsWith('(')) {\n      name = name.substr(0, name.length - 1);\n    }\n    return new Contact({\n      accountId: accountId,\n      name: name.trim(),\n      email: email,\n    });\n  }\n\n  constructor(...args) {\n    super(...args);\n    this.thirdPartyData = this.thirdPartyData || {};\n  }\n\n  // Public: Returns a string of the format `Full Name <email@address.com>` if\n  // the contact has a populated name, just the email address otherwise.\n  toString() {\n    // Note: This is used as the drag-drop text of a Contact token, in the\n    // creation of message bylines \"From Ben Gotow <ben@nylas>\", and several other\n    // places. Change with care.\n    return (this.name && this.name !== this.email) ? `${this.name} <${this.email}>` : this.email;\n  }\n\n  toJSON(...args) {\n    const json = super.toJSON(...args);\n    json.name = json.name || json.email;\n    return json;\n  }\n\n\n  // Public: Returns true if the contact provided is a {Contact} instance and\n  // contains a properly formatted email address.\n  isValid() {\n    if (!this.email) {\n      return false;\n    }\n\n    // The email regexp must match the /entire/ email address\n    const result = RegExpUtils.emailRegex().exec(this.email);\n    return (result && result instanceof Array) ? (result[0] === this.email) : false;\n  }\n\n  // Public: Returns true if the contact is the current user, false otherwise.\n  // You should use this method instead of comparing the user's email address to\n  // the account email, since it is case-insensitive and future-proof.\n  isMe() {\n    return !!AccountStore.accountForEmail(this.email);\n  }\n\n  hasSameDomainAsMe() {\n    for (const myEmail of AccountStore.emailAddresses()) {\n      if (Utils.emailsHaveSameDomain(this.email, myEmail)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  isMePhrase({includeAccountLabel, forceAccountLabel} = {}) {\n    const account = AccountStore.accountForEmail(this.email);\n    if (!account) {\n      return null;\n    }\n\n    if (includeAccountLabel) {\n      FocusedPerspectiveStore = FocusedPerspectiveStore || require('../stores/focused-perspective-store').default;\n      if (account && (FocusedPerspectiveStore.current().accountIds.length > 1 || forceAccountLabel)) {\n        return `You (${account.label})`;\n      }\n    }\n    return \"You\";\n  }\n\n  // Returns a {String} display name.\n  // - \"You\" if the contact is the current user or an alias for the current user.\n  // - `name` if the contact has a populated name\n  // - `email` in all other cases.\n\n  // You can pass several options to customize the name:\n  // - includeAccountLabel: If the contact represents the current user, include\n  //   the account label afer \"You\"\n  // - forceAccountLabel: Always include the account label\n  // - compact: If the contact has a name, make the name as short as possible\n  //   (generally returns just the first name.)\n  displayName(options = {}) {\n    let includeAccountLabel = options.includeAccountLabel;\n    const forceAccountLabel = options.forceAccountLabel;\n    const compact = options.compact || false;\n\n    if (includeAccountLabel === undefined) {\n      includeAccountLabel = !compact;\n    }\n\n    const fallback = compact ? this.firstName() : this.fullName();\n    return this.isMePhrase({forceAccountLabel, includeAccountLabel}) || fallback;\n  }\n\n  fullName() {\n    return this._nameParts().join(' ');\n  }\n\n  firstName() {\n    const exclusions = ['a', 'the', 'dr.', 'mrs.', 'mr.', 'mx.', 'prof.', 'ph.d.'];\n    return this._nameParts().find((p) => !exclusions.includes(p.toLowerCase())) || \"\";\n  }\n\n  lastName() {\n    return this._nameParts().slice(1).join(\" \") || \"\";\n  }\n\n  nameAbbreviation() {\n    const c1 = (this.firstName()[0] || \"\").toUpperCase();\n    const c2 = (this.lastName()[0] || \"\").toUpperCase();\n    return c1 + c2;\n  }\n\n  guessCompanyFromEmail(email = this.email) {\n    if (Utils.emailHasCommonDomain(email)) {\n      return \"\";\n    }\n    const domain = email.toLowerCase().trim().split(\"@\").pop();\n    const domainParts = domain.split(\".\");\n    if (domainParts.length >= 2) {\n      return _str.titleize(_str.humanize(domainParts[domainParts.length - 2]));\n    }\n    return \"\";\n  }\n\n  _nameParts() {\n    let name = this.name;\n\n    // At this point, if the name is empty we'll use the email address\n    if (!name || name.length === 0) {\n      name = this.email || \"\";\n\n      // If the phrase has an '@', use everything before the @ sign\n      // Unless there that would result in an empty string.\n      if (name.indexOf('@') > 0) {\n        name = name.split('@').shift();\n      }\n    }\n\n    // Take care of phrases like \"Mike Kaylor via LinkedIn\" that should be displayed\n    // as the contents before the separator word. Do not break \"Olivia\"\n    name = name.split(/(\\svia\\s)/i).shift();\n\n    // Take care of whitespace\n    name = name.trim();\n\n    // Handle last name, first name\n    let parts = this._parseReverseNames(name);\n\n    // Split the name into words and remove parts that are prefixes and suffixes\n    if (parts.join('').length === 0) {\n      parts = [];\n      parts = name.split(/\\s+/);\n      if (parts.length > 0 && namePrefixes[parts[0].toLowerCase().replace(/\\./, '')]) {\n        parts = parts.slice(1);\n      }\n      if (parts.length > 0 && nameSuffixes[parts[parts.length - 1].toLowerCase().replace(/\\./, '')]) {\n        parts = parts.slice(0, parts.length - 1);\n      }\n    }\n\n    // If we've removed all the parts, just return the whole name\n    if (parts.join('').length === 0) {\n      parts = [name];\n    }\n\n    // If all that failed, fall back to email\n    if (parts.join('').length === 0) {\n      parts = [this.email];\n    }\n\n    return parts;\n  }\n\n  _parseReverseNames(name) {\n    const parts = [];\n    const [lastName, remainder] = name.split(', ');\n    if (remainder) {\n      const [firstName, description] = remainder.split('(');\n\n      parts.push(firstName.trim());\n      parts.push(lastName.trim());\n      if (description) {\n        parts.push(`(${description.trim()}`);\n      }\n    }\n    return parts;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/event.es6",
    "content": "import chrono from 'chrono-node';\nimport moment from 'moment';\n\nimport Model from './model';\nimport Attributes from '../attributes';\nimport Contact from './contact';\n\nexport default class Event extends Model {\n\n  static attributes = Object.assign({}, Model.attributes, {\n    calendarId: Attributes.String({\n      queryable: true,\n      modelKey: 'calendarId',\n      jsonKey: 'calendar_id',\n    }),\n    title: Attributes.String({\n      modelKey: 'title',\n      jsonKey: 'title',\n    }),\n    description: Attributes.String({\n      modelKey: 'description',\n      jsonKey: 'description',\n    }),\n    // Can Have 1 of 4 types of subobjects. The Type can be:\n    //\n    // time\n    //   object: \"time\"\n    //   time: (unix timestamp)\n    //\n    // timestamp\n    //   object: \"timestamp\"\n    //   start_time: (unix timestamp)\n    //   end_time: (unix timestamp)\n    //\n    // date\n    //   object: \"date\"\n    //   date: (ISO 8601 date format. i.e. 1912-06-23)\n    //\n    // datespan\n    //   object: \"datespan\"\n    //   start_date: (ISO 8601 date)\n    //   end_date: (ISO 8601 date)\n    when: Attributes.Object({\n      modelKey: 'when',\n    }),\n\n    location: Attributes.String({\n      modelKey: 'location',\n      jsonKey: 'location',\n    }),\n\n    owner: Attributes.String({\n      modelKey: 'owner',\n      jsonKey: 'owner',\n    }),\n\n    // Subobject:\n    // name (string) - The participant's full name (optional)\n    // email (string) - The participant's email address\n    // status (string) - Attendance status. Allowed values are yes, maybe,\n    //                   no and noreply. Defaults is noreply\n    // comment (string) - A comment by the participant (optional)\n    participants: Attributes.Object({\n      modelKey: 'participants',\n      jsonKey: 'participants',\n    }),\n    status: Attributes.String({\n      modelKey: 'status',\n      jsonKey: 'status',\n    }),\n    readOnly: Attributes.Boolean({\n      modelKey: 'readOnly',\n      jsonKey: 'read_only',\n    }),\n    busy: Attributes.Boolean({\n      modelKey: 'busy',\n      jsonKey: 'busy',\n    }),\n\n    // Has a sub object of the form:\n    // rrule: (array) - Array of recurrence rule (RRULE) strings. See RFC-2445\n    // timezone: (string) - IANA time zone database formatted string\n    //                      (e.g. America/New_York)\n    recurrence: Attributes.Object({\n      modelKey: 'recurrence',\n      jsonKey: 'recurrence',\n    }),\n\n    // ----  EXTRACTED ATTRIBUTES -----\n\n    // The \"object\" type of the \"when\" object. Can be either \"time\",\n    // \"timestamp\", \"date\", or \"datespan\"\n    type: Attributes.String({\n      modelKey: 'type',\n      jsonKey: '_type',\n    }),\n\n    // The calculated Unix start time. See the implementation for how we\n    // treat each type of \"when\" attribute.\n    start: Attributes.Number({\n      queryable: true,\n      modelKey: 'start',\n      jsonKey: '_start',\n    }),\n\n    // The calculated Unix end time. See the implementation for how we\n    // treat each type of \"when\" attribute.\n    end: Attributes.Number({\n      queryable: true,\n      modelKey: 'end',\n      jsonKey: '_end',\n    }),\n\n    isSearchIndexed: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'isSearchIndexed',\n      jsonKey: 'is_search_indexed',\n      defaultValue: false,\n    }),\n\n    // This corresponds to the rowid in the FTS table. We need to use the FTS\n    // rowid when updating and deleting items in the FTS table because otherwise\n    // these operations would be way too slow on large FTS tables.\n    searchIndexId: Attributes.Number({\n      modelKey: 'searchIndexId',\n      jsonKey: 'search_index_id',\n    }),\n  });\n\n  static additionalSQLiteConfig = {\n    setup: () => {\n      return [\n        'CREATE UNIQUE INDEX IF NOT EXISTS EventClientIndex ON Event(client_id)',\n        'CREATE INDEX IF NOT EXISTS EventIsSearchIndexedIndex ON `Event` (is_search_indexed, id)',\n      ];\n    },\n  };\n\n  static searchable = true\n\n  static searchFields = ['title', 'description', 'location', 'participants']\n\n  static sortOrderAttribute = () => {\n    return Event.attributes.id\n  }\n\n  static naturalSortOrder = () => {\n    return Event.sortOrderAttribute().descending()\n  }\n\n  // We use moment to parse the date so we can more easily pick up the\n  // current timezone of the current locale.\n  // We also create a start and end times that span the full day without\n  // bleeding into the next.\n  _unixRangeForDatespan(startDate, endDate) {\n    return {\n      start: moment(startDate).unix(),\n      end: moment(endDate).add(1, 'day').subtract(1, 'second').unix(),\n    };\n  }\n\n  fromJSON(json) {\n    super.fromJSON(json)\n\n    const when = this.when;\n\n    if (!when) {\n      return this;\n    }\n\n    if (when.time) {\n      this.start = when.time;\n      this.end = when.time;\n    } else if (when.start_time && when.end_time) {\n      this.start = when.start_time;\n      this.end = when.end_time;\n    } else if (when.date) {\n      const range = this._unixRangeForDatespan(when.date, when.date);\n      this.start = range.start;\n      this.end = range.end;\n    } else if (when.start_date && when.end_date) {\n      const range = this._unixRangeForDatespan(when.start_date, when.end_date);\n      this.start = range.start;\n      this.end = range.end;\n    }\n\n    return this;\n  }\n\n  fromDraft(draft) {\n    if (!this.title || this.title.length === 0) {\n      this.title = draft.subject;\n    }\n\n    if (!this.participants || this.participants.length === 0) {\n      this.participants = draft.participants().map((contact) => {\n        return {\n          name: contact.name,\n          email: contact.email,\n          status: \"noreply\",\n        };\n      });\n    }\n    return this;\n  }\n\n  isAllDay() {\n    const daySpan = 86400 - 1;\n    return (this.end - this.start) >= daySpan;\n  }\n\n  displayTitle() {\n    const displayTitle = this.title.replace(/.*Invitation: /, \"\")\n    const [displayTitleWithoutDate, date] = displayTitle.split(\" @ \")\n    if (date && chrono.parseDate(date)) {\n      return displayTitleWithoutDate\n    }\n    return displayTitle\n  }\n\n  participantForMe = () => {\n    for (const p of this.participants) {\n      if ((new Contact({email: p.email})).isMe()) {\n        return p;\n      }\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/file.es6",
    "content": "/* eslint global-require: 0 */\nimport path from 'path';\nimport Model from './model';\nimport Attributes from '../attributes';\nlet RegExpUtils = null\n\n/**\nPublic: File model represents a File object served by the Nylas Platform API.\nFor more information about Files on the Nylas Platform, read the\n[Files API Documentation](https://nylas.com/cloud/docs#files)\n\n#// Attributes\n\n`filename`: {AttributeString} The display name of the file. Queryable.\n\n`size`: {AttributeNumber} The size of the file, in bytes.\n\n`contentType`: {AttributeString} The content type of the file (ex: `image/png`)\n\n`contentId`: {AttributeString} If this file is an inline attachment, contentId\nis a string that matches a cid:<value> found in the HTML body of a {Message}.\n\nThis class also inherits attributes from {Model}\n\nSection: Models\n*/\nexport default class File extends Model {\n\n  static attributes = Object.assign({}, Model.attributes, {\n    filename: Attributes.String({\n      modelKey: 'filename',\n      jsonKey: 'filename',\n      queryable: true,\n    }),\n    size: Attributes.Number({\n      modelKey: 'size',\n      jsonKey: 'size',\n    }),\n    contentType: Attributes.String({\n      modelKey: 'contentType',\n      jsonKey: 'content_type',\n    }),\n    messageIds: Attributes.Collection({\n      modelKey: 'messageIds',\n      jsonKey: 'message_ids',\n      itemClass: String,\n    }),\n    contentId: Attributes.String({\n      modelKey: 'contentId',\n      jsonKey: 'content_id',\n    }),\n  });\n\n  // Public: Files can have empty names, or no name. `displayName` returns the file's\n  // name if one is present, and falls back to appropriate default name based on\n  // the contentType. It will always return a non-empty string.\n  displayName() {\n    const defaultNames = {\n      'text/calendar': \"Event.ics\",\n      'image/png': 'Unnamed Image.png',\n      'image/jpg': 'Unnamed Image.jpg',\n      'image/jpeg': 'Unnamed Image.jpg',\n    };\n    if (this.filename && this.filename.length) {\n      return this.filename;\n    }\n    if (defaultNames[this.contentType]) {\n      return defaultNames[this.contentType];\n    }\n    return \"Unnamed Attachment\";\n  }\n\n  safeDisplayName() {\n    RegExpUtils = RegExpUtils || require('../../regexp-utils');\n    return this.displayName().replace(RegExpUtils.illegalPathCharactersRegexp(), '-');\n  }\n\n  // Public: Returns the file extension that should be used for this file.\n  // Note that asking for the displayExtension is more accurate than trying to read\n  // the extension directly off the filename. The returned extension may be based\n  // on contentType and is always lowercase.\n\n  // Returns the extension without the leading '.' (ex: 'png', 'pdf')\n  displayExtension() {\n    return path.extname(this.displayName().toLowerCase()).substr(1);\n  }\n\n  displayFileSize(bytes = this.size) {\n    const units = ['B', 'KB', 'MB', 'GB'];\n    let threshold = 1000000000;\n    let idx = units.length - 1;\n\n    let result = bytes / threshold;\n    while (result < 1 && idx >= 0) {\n      threshold /= 1000;\n      result = bytes / threshold;\n      idx--;\n    }\n\n    // parseFloat will remove trailing zeros\n    const decimalPoints = idx >= 2 ? 1 : 0;\n    const rounded = parseFloat(result.toFixed(decimalPoints))\n    return `${rounded} ${units[idx]}`;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/folder.es6",
    "content": "import Category from './category';\nexport default Category;\n"
  },
  {
    "path": "packages/client-app/src/flux/models/json-blob.es6",
    "content": "import Model from './model';\nimport Query from './query';\nimport Attributes from '../attributes';\n\nclass JSONBlobQuery extends Query {\n  formatResult(objects) {\n    return objects[0] ? objects[0].json : null;\n  }\n}\n\nexport default class JSONBlob extends Model {\n  static Query = JSONBlobQuery;\n\n  static attributes = {\n    id: Attributes.String({\n      queryable: true,\n      modelKey: 'id',\n    }),\n\n    clientId: Attributes.String({\n      queryable: true,\n      modelKey: 'clientId',\n      jsonKey: 'client_id',\n    }),\n\n    serverId: Attributes.ServerId({\n      modelKey: 'serverId',\n      jsonKey: 'server_id',\n    }),\n\n    json: Attributes.Object({\n      modelKey: 'json',\n      jsonKey: 'json',\n    }),\n  };\n\n  get key() {\n    return this.serverId;\n  }\n\n  set key(val) {\n    this.serverId = val;\n  }\n\n  get clientId() {\n    return this.serverId;\n  }\n\n  set clientId(val) {\n    this.serverId = val\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/label.es6",
    "content": "import Category from './category';\nexport default Category;\n"
  },
  {
    "path": "packages/client-app/src/flux/models/message-utils.es6",
    "content": "const cidRegexString = \"src=['\\\"]cid:([^'\\\"]*)['\\\"]\";\nconst cidRegex = new RegExp(cidRegexString, \"g\");\n\nexport default {cidRegexString, cidRegex};\n"
  },
  {
    "path": "packages/client-app/src/flux/models/message.es6",
    "content": "import _ from 'underscore'\nimport moment from 'moment'\nimport {MessageBodyUtils} from 'isomorphic-core'\n\nimport File from './file'\nimport Utils from './utils'\nimport Event from './event'\nimport Contact from './contact'\nimport Category from './category'\nimport Attributes from '../attributes'\nimport ModelWithMetadata from './model-with-metadata'\nimport QuotedHTMLTransformer from '../../services/quoted-html-transformer'\n\n\n/*\nPublic: The Message model represents a Message object served by the Nylas Platform API.\nFor more information about Messages on the Nylas Platform, read the\n[Messages API Documentation](https://nylas.com/cloud/docs#messages)\n\nMessages are a sub-object of threads. The content of a message === immutable (with the\nexception being drafts). Nylas does not support operations such as move || delete on\nindividual messages; those operations should be performed on the message’s thread.\nAll messages are part of a thread, even if that thread has only one message.\n\n## Attributes\n\n`to`: {AttributeCollection} A collection of {Contact} objects\n\n`cc`: {AttributeCollection} A collection of {Contact} objects\n\n`bcc`: {AttributeCollection} A collection of {Contact} objects\n\n`from`: {AttributeCollection} A collection of {Contact} objects.\n\n`replyTo`: {AttributeCollection} A collection of {Contact} objects.\n\n`date`: {AttributeDateTime} When the message was delivered. Queryable.\n\n`subject`: {AttributeString} The subject of the thread. Queryable.\n\n`snippet`: {AttributeString} A short, 140-character plain-text summary of the message body.\n\n`unread`: {AttributeBoolean} True if the message === unread. Queryable.\n\n`starred`: {AttributeBoolean} True if the message === starred. Queryable.\n\n`draft`: {AttributeBoolean} True if the message === a draft. Queryable.\n\n`version`: {AttributeNumber} The version number of the message. Message\n   versions are used for drafts, && increment when attributes are changed.\n\n`files`: {AttributeCollection} A set of {File} models representing\n   the attachments on this thread.\n\n`body`: {AttributeJoinedData} The HTML body of the message. You must specifically\n request this attribute when querying for a Message using the {{AttributeJoinedData::include}}\n method.\n\n`pristine`: {AttributeBoolean} True if the message === a draft which has not been\n edited since it was created.\n\n`threadId`: {AttributeString} The ID of the Message's parent {Thread}. Queryable.\n\n`replyToMessageId`: {AttributeString} The ID of a {Message} that this message\n === in reply to.\n\nThis class also inherits attributes from {Model}\n\nSection: Models\n*/\nexport default class Message extends ModelWithMetadata {\n\n  static attributes = Object.assign({}, ModelWithMetadata.attributes, {\n    to: Attributes.Collection({\n      modelKey: 'to',\n      itemClass: Contact,\n    }),\n\n    cc: Attributes.Collection({\n      modelKey: 'cc',\n      itemClass: Contact,\n    }),\n\n    bcc: Attributes.Collection({\n      modelKey: 'bcc',\n      itemClass: Contact,\n    }),\n\n    from: Attributes.Collection({\n      modelKey: 'from',\n      itemClass: Contact,\n    }),\n\n    replyTo: Attributes.Collection({\n      modelKey: 'replyTo',\n      jsonKey: 'reply_to',\n      itemClass: Contact,\n    }),\n\n    date: Attributes.DateTime({\n      queryable: true,\n      modelKey: 'date',\n    }),\n\n    body: Attributes.JoinedData({\n      modelTable: 'MessageBody',\n      modelKey: 'body',\n      serializeFn: function serializeBody(val) {\n        return MessageBodyUtils.writeBody({\n          msgId: this.id,\n          body: val,\n        });\n      },\n      deserializeFn: function deserializeBody(val) {\n        const result = MessageBodyUtils.tryReadBody(val);\n        if (result) {\n          return result;\n        }\n        return val;\n      },\n    }),\n\n    files: Attributes.Collection({\n      modelKey: 'files',\n      itemClass: File,\n    }),\n\n    uploads: Attributes.Object({\n      queryable: false,\n      modelKey: 'uploads',\n    }),\n\n    unread: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'unread',\n    }),\n\n    events: Attributes.Collection({\n      modelKey: 'events',\n      itemClass: Event,\n    }),\n\n    starred: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'starred',\n    }),\n\n    snippet: Attributes.String({\n      modelKey: 'snippet',\n    }),\n\n    threadId: Attributes.ServerId({\n      queryable: true,\n      modelKey: 'threadId',\n      jsonKey: 'thread_id',\n    }),\n\n    messageIdHeader: Attributes.ServerId({\n      modelKey: 'messageIdHeader',\n      jsonKey: 'message_id_header',\n    }),\n\n    subject: Attributes.String({\n      modelKey: 'subject',\n    }),\n\n    draft: Attributes.Boolean({\n      modelKey: 'draft',\n      jsonKey: 'draft',\n      queryable: true,\n    }),\n\n    pristine: Attributes.Boolean({\n      modelKey: 'pristine',\n      jsonKey: 'pristine',\n      queryable: false,\n    }),\n\n    version: Attributes.Number({\n      modelKey: 'version',\n      queryable: true,\n    }),\n\n    replyToMessageId: Attributes.ServerId({\n      modelKey: 'replyToMessageId',\n      jsonKey: 'reply_to_message_id',\n    }),\n\n    categories: Attributes.Collection({\n      modelKey: 'categories',\n      itemClass: Category,\n    }),\n  });\n\n  static naturalSortOrder() {\n    return Message.attributes.date.ascending()\n  }\n\n  static additionalSQLiteConfig = {\n    setup: () => [\n      `CREATE INDEX IF NOT EXISTS MessageListThreadIndex ON Message(thread_id, date ASC)`,\n      `CREATE UNIQUE INDEX IF NOT EXISTS MessageDraftIndex ON Message(client_id)`,\n      `CREATE INDEX IF NOT EXISTS MessageListDraftIndex ON \\\nMessage(account_id, date DESC) WHERE draft = 1`,\n      `CREATE INDEX IF NOT EXISTS MessageListUnifiedDraftIndex ON \\\nMessage(date DESC) WHERE draft = 1`,\n      `CREATE UNIQUE INDEX IF NOT EXISTS MessageBodyIndex ON MessageBody(id)`,\n    ],\n  }\n\n  constructor(args) {\n    super(args);\n    this.subject = this.subject || \"\"\n    this.to = this.to || []\n    this.cc = this.cc || []\n    this.bcc = this.bcc || []\n    this.from = this.from || []\n    this.replyTo = this.replyTo || []\n    this.files = this.files || []\n    this.uploads = this.uploads || []\n    this.events = this.events || []\n    this.categories = this.categories || []\n  }\n\n  toJSON(options) {\n    const json = super.toJSON(options)\n    json.file_ids = this.fileIds()\n    if (this.draft) {\n      json.object = 'draft'\n    }\n\n    if (this.events && this.events.length) {\n      json.event_id = this.events[0].serverId\n    }\n\n    return json\n  }\n\n  fromJSON(json = {}) {\n    super.fromJSON(json)\n\n    // Only change the `draft` bit if the incoming json has an `object`\n    // property. Because of `DraftChangeSet`, it's common for incoming json\n    // to be an empty hash. In this case we want to leave the pre-existing\n    // draft bit alone.\n    if (json.object) {\n      this.draft = (json.object === 'draft')\n    }\n\n    let categories = []\n    if (json.categories) {\n      categories = this.constructor.attributes.categories.fromJSON(json.categories)\n    } else {\n      if (json.folder) {\n        categories = categories.concat(this.constructor.attributes.categories.fromJSON([json.folder]))\n      }\n      if (json.labels) {\n        categories = categories.concat(this.constructor.attributes.categories.fromJSON(json.labels))\n      }\n    }\n    this.categories = categories\n\n    for (const attr of ['to', 'from', 'cc', 'bcc', 'files', 'categories']) {\n      const values = this[attr]\n      if (!(values && values instanceof Array)) {\n        continue;\n      }\n      for (const item of values) {\n        item.accountId = this.accountId\n      }\n    }\n\n    return this\n  }\n\n  canReplyAll() {\n    const {to, cc} = this.participantsForReplyAll()\n    return to.length > 1 || cc.length > 0\n  }\n\n  // Public: Returns a set of uniqued message participants by combining the\n  // `to`, `cc`, `bcc` && (optionally) `from` fields.\n  participants({includeFrom = true, includeBcc = false} = {}) {\n    const seen = {}\n    const all = []\n    let contacts = [].concat(this.to, this.cc)\n    if (includeFrom) {\n      contacts = _.union(contacts, (this.from || []))\n    }\n    if (includeBcc) {\n      contacts = _.union(contacts, (this.bcc || []))\n    }\n    for (const contact of contacts) {\n      if (!contact.email) {\n        continue\n      }\n      const key = contact.toString().trim().toLowerCase()\n      if (seen[key]) {\n        continue;\n      }\n      seen[key] = true\n      all.push(contact)\n    }\n    return all\n  }\n\n  // Public: Returns a hash with `to` && `cc` keys for authoring a new draft in\n  // \"reply all\" to this message. This method takes into account whether the\n  // message === from the current user, && also looks at the replyTo field.\n  participantsForReplyAll() {\n    const excludedFroms = this.from.map((c) =>\n      Utils.toEquivalentEmailForm(c.email)\n    );\n\n    const excludeMeAndFroms = (cc) =>\n      _.reject(cc, (p) =>\n        p.isMe() || _.contains(excludedFroms, Utils.toEquivalentEmailForm(p.email))\n      );\n\n    let to = null\n    let cc = null\n\n    if (this.replyTo.length) {\n      to = this.replyTo\n      cc = excludeMeAndFroms([].concat(this.to, this.cc))\n    } else if (this.isFromMe()) {\n      to = this.to\n      cc = excludeMeAndFroms(this.cc)\n    } else {\n      to = this.from\n      cc = excludeMeAndFroms([].concat(this.to, this.cc))\n    }\n\n    to = _.uniq(to, (p) => Utils.toEquivalentEmailForm(p.email))\n    cc = _.uniq(cc, (p) => Utils.toEquivalentEmailForm(p.email))\n    return {to, cc}\n  }\n\n  // Public: Returns a hash with `to` && `cc` keys for authoring a new draft in\n  // \"reply\" to this message. This method takes into account whether the\n  // message === from the current user, && also looks at the replyTo field.\n  participantsForReply() {\n    let to = []\n    const cc = []\n\n    if (this.replyTo.length) {\n      to = this.replyTo;\n    } else if (this.isFromMe()) {\n      to = this.to\n    } else {\n      to = this.from\n    }\n\n    to = _.uniq(to, (p) => Utils.toEquivalentEmailForm(p.email))\n    return {to, cc}\n  }\n\n  // Public: Returns an {Array} of {File} IDs\n  fileIds() {\n    return _.map(this.files, (file) => file.id)\n  }\n\n  // Public: Returns true if this message === from the current user's email\n  // address. In the future, this method will take into account all of the\n  // user's email addresses && accounts.\n  isFromMe() {\n    return this.from[0] ? this.from[0].isMe() : false\n  }\n\n  // Public: Returns a plaintext version of the message body using Chromium's\n  // DOMParser. Use with care.\n  computePlainText(options = {}) {\n    if ((this.body || \"\").trim().length === 0) {\n      return \"\"\n    }\n    if (options.includeQuotedText) {\n      return (new DOMParser()).parseFromString(this.body, \"text/html\").body.innerText\n    }\n    const doc = this.computeDOMWithoutQuotes()\n    return this.cleanPlainTextBody(doc.body.innerText)\n  }\n\n  cleanPlainTextBody(body) {\n    let cleanBody = body;\n    const leadingOrTrailingTabs = /(?:^\\t+|\\t+$)/gmi\n    cleanBody = cleanBody.replace(leadingOrTrailingTabs, \"\")\n    const manyNewlines = /\\n{3,}/gi\n    cleanBody = cleanBody.replace(manyNewlines, \"\\n\\n\")\n    const manySpaces = /\\n{5,}/gi\n    cleanBody = cleanBody.replace(manySpaces, \"    \")\n    return cleanBody\n  }\n\n  // Separated out so callers (like SyncbackThreadToSalesforce) can only\n  // run an expensive parse once, but use the DOM to load both HTML and\n  // PlainText versions of the body.\n  computeDOMWithoutQuotes() {\n    return QuotedHTMLTransformer.removeQuotedHTML(this.body, {asDOM: true});\n  }\n\n  fromContact() {\n    return ((this.from || [])[0] || new Contact({name: 'Unknown', email: 'Unknown'}))\n  }\n\n  // Public: Returns the standard attribution line for this message,\n  // localized for the current user.\n  // ie \"On Dec. 12th, 2015 at 4:00PM, Ben Gotow wrote:\"\n  replyAttributionLine() {\n    return `On ${this.formattedDate()}, ${this.fromContact().toString()} wrote:`\n  }\n\n  formattedDate() {\n    return moment(this.date).format(\"MMM D YYYY, [at] h:mm a\")\n  }\n\n  hasEmptyBody() {\n    if (!this.body) { return true }\n\n    // https://regex101.com/r/hR7zN3/1\n    const re = /(?:<signature>.*<\\/signature>)|(?:<.+?>)|\\s/gmi;\n    return this.body.replace(re, \"\").length === 0;\n  }\n\n  isHidden() {\n    return (\n      this.to.length === 1 && this.from.length === 1 &&\n      this.to[0].email === this.from[0].email &&\n      (this.snippet || \"\").startsWith('Nylas Mail Reminder:')\n    )\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/model-with-metadata.es6",
    "content": "import Model from './model'\nimport Attributes from '../attributes'\n\n/**\n Cloud-persisted data that is associated with a single Nylas API object\n (like a `Thread`, `Message`, or `Account`).\n */\nclass PluginMetadata extends Model {\n  static attributes = {\n    pluginId: Attributes.String({\n      modelKey: 'pluginId',\n      jsonKey: \"plugin_id\",\n    }),\n    version: Attributes.Number({\n      modelKey: 'version',\n    }),\n    value: Attributes.Object({\n      modelKey: 'value',\n    }),\n  };\n\n  constructor(...args) {\n    super(...args)\n    this.version = this.version || 0;\n  }\n\n  get id() {\n    return this.pluginId\n  }\n\n  set id(pluginId) {\n    this.pluginId = pluginId\n  }\n}\n\n\n/**\n Plugins can attach arbitrary JSON data to any model that subclasses\n ModelWithMetadata, like {{Thread}} or {{Message}}. You must get and set\n metadata using your plugin's ID, and any metadata you set overwrites the\n metadata previously on the object for the given plugin id.\n\n Reading the metadata of other plugins is discouraged and may become impossible\n in the future.\n*/\nexport default class ModelWithMetadata extends Model {\n  static attributes = Object.assign({}, Model.attributes, {\n    pluginMetadata: Attributes.Collection({\n      queryable: true,\n      joinOnField: 'pluginId',\n      itemClass: PluginMetadata,\n      modelKey: 'pluginMetadata',\n      jsonKey: 'metadata',\n    }),\n  });\n\n  static naturalSortOrder() {\n    return null\n  }\n\n  constructor(...args) {\n    super(...args)\n    this.pluginMetadata = this.pluginMetadata || [];\n  }\n\n  // Public accessors\n\n  metadataForPluginId(pluginId) {\n    const metadata = this.metadataObjectForPluginId(pluginId);\n    if (!metadata) {\n      return null;\n    }\n    return JSON.parse(JSON.stringify(metadata.value));\n  }\n\n  // Private helpers\n\n  metadataObjectForPluginId(pluginId) {\n    if (typeof pluginId !== \"string\") {\n      throw new Error(`Invalid pluginId. Must be a valid string: '${pluginId}'`, pluginId)\n    }\n    return this.pluginMetadata.find(metadata => metadata.pluginId === pluginId);\n  }\n\n  applyPluginMetadata(pluginId, metadataValue) {\n    let metadata = this.metadataObjectForPluginId(pluginId);\n    if (!metadata) {\n      metadata = new PluginMetadata({pluginId});\n      this.pluginMetadata.push(metadata);\n    }\n    metadata.value = metadataValue;\n    return this;\n  }\n\n  clonePluginMetadataFrom(otherModel) {\n    this.pluginMetadata = otherModel.pluginMetadata.map(({pluginId, value}) => {\n      return new PluginMetadata({pluginId, value});\n    })\n    return this;\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/model.coffee",
    "content": "_ = require 'underscore'\nUtils = require './utils'\nAttributes = require('../attributes').default\n\n###\nPublic: A base class for API objects that provides abstract support for\nserialization and deserialization, matching by attributes, and ID-based equality.\n\n## Attributes\n\n`id`: {AttributeString} The resolved canonical ID of the model used in the\ndatabase and generally throughout the app. The id property is a custom\ngetter that resolves to the serverId first, and then the clientId.\n\n`clientId`: {AttributeString} An ID created at object construction and\npersists throughout the lifetime of the object. This is extremely useful\nfor optimistically creating objects (like drafts and categories) and\nhaving a constant reference to it. In all other cases, use the resolved\n`id` field.\n\n`serverId`: {AttributeServerId} The server ID of the model. In most cases,\nexcept optimistic creation, this will also be the canonical id of the\nobject.\n\n`object`: {AttributeString} The model's type. This field is used by the JSON\n deserializer to create an instance of the correct class when inflating the object.\n\n`accountId`: {AttributeString} The string Account Id this model belongs to.\n\nSection: Models\n###\nclass Model\n\n  Object.defineProperty @prototype, \"id\",\n    enumerable: false\n    get: -> @serverId ? @clientId\n    set: ->\n      throw new Error(\"You may not directly set the ID of an object. Set either the `clientId` or the `serverId` instead.\")\n\n  @attributes:\n    # Lookups will go through the custom getter.\n    'id': Attributes.String\n      queryable: true\n      modelKey: 'id'\n\n    'clientId': Attributes.String\n      queryable: true\n      modelKey: 'clientId'\n      jsonKey: 'client_id'\n\n    'serverId': Attributes.ServerId\n      modelKey: 'serverId'\n      jsonKey: 'server_id'\n\n    'object': Attributes.String\n      modelKey: 'object'\n\n    'accountId': Attributes.ServerId\n      queryable: true\n      modelKey: 'accountId'\n      jsonKey: 'account_id'\n\n  @naturalSortOrder: -> null\n\n  constructor: (values = {}) ->\n    if values[\"id\"] and Utils.isTempId(values[\"id\"])\n      values[\"clientId\"] ?= values[\"id\"]\n    else\n      values[\"serverId\"] ?= values[\"id\"]\n\n    for key in Object.keys(@constructor.attributes)\n      continue if key is 'id'\n      continue unless values[key]?\n      @[key] = values[key]\n\n    @clientId ?= Utils.generateTempId()\n    @\n\n  isSavedRemotely: ->\n    @serverId?\n\n  clone: ->\n    (new @constructor).fromJSON(@toJSON())\n\n  # Public: Returns an {Array} of {Attribute} objects defined on the Model's constructor\n  #\n  attributes: ->\n    attrs = _.clone(@constructor.attributes)\n    delete attrs[\"id\"]\n    return attrs\n\n  ##\n  # Public: Inflates the model object from JSON, using the defined attributes to\n  # guide type coercision.\n  #\n  # - `json` A plain Javascript {Object} with the JSON representation of the model.\n  #\n  # This method is chainable.\n  #\n  fromJSON: (json) ->\n    # Note: The loop in this function has been optimized for the V8 'fast case'\n    # https://github.com/petkaantonov/bluebird/wiki/Optimization-killers\n    #\n    if json[\"id\"] and not Utils.isTempId(json[\"id\"])\n      @serverId = json[\"id\"]\n    for key in Object.keys(@constructor.attributes)\n      continue if key is 'id'\n      attr = @constructor.attributes[key]\n      attrValue = json[attr.jsonKey]\n      @[key] = attr.fromJSON(attrValue) unless attrValue is undefined\n    @\n\n  # Public: Deflates the model to a plain JSON object. Only attributes defined\n  # on the model are included in the JSON.\n  #\n  # - `options` (optional) An {Object} with additional options. To skip joined\n  #    data attributes in the toJSON representation, pass the `joined:false`\n  #\n  # Returns an {Object} with the JSON representation of the model.\n  #\n  toJSON: (options = {}) ->\n    json = {}\n    for key in Object.keys(@constructor.attributes)\n      continue if key is 'id'\n      attr = @constructor.attributes[key]\n      attrValue = @[key]\n      if attrValue is undefined\n        attrValue = attr.defaultValue\n      continue if attrValue is undefined\n      continue if attr instanceof Attributes.AttributeJoinedData and options.joined is false\n      json[attr.jsonKey] = attr.toJSON(attrValue)\n    json[\"id\"] = @id\n    json\n\n  toString: ->\n    JSON.stringify(@toJSON())\n\n  # Public: Evaluates the model against one or more {Matcher} objects.\n  #\n  # - `criteria` An {Array} of {Matcher}s to run on the model.\n  #\n  # Returns true if the model matches the criteria.\n  #\n  matches: (criteria) ->\n    return false unless criteria instanceof Array\n    for matcher in criteria\n      return false unless matcher.evaluate(@)\n    true\n\n\nmodule.exports = Model\n"
  },
  {
    "path": "packages/client-app/src/flux/models/mutable-query-result-set.es6",
    "content": "import QueryResultSet from './query-result-set';\nimport AttributeJoinedData from '../attributes/attribute-joined-data';\n\n// TODO: Make mutator methods QueryResultSet.join(), QueryResultSet.clip...\nexport default class MutableQueryResultSet extends QueryResultSet {\n\n  immutableClone() {\n    const set = new QueryResultSet({\n      _ids: [].concat(this._ids),\n      _modelsHash: Object.assign({}, this._modelsHash),\n      _query: this._query,\n      _offset: this._offset,\n    });\n    Object.freeze(set._ids);\n    Object.freeze(set._modelsHash);\n    return set;\n  }\n\n  clipToRange(range) {\n    if (range.isInfinite()) {\n      return;\n    }\n    if (range.offset > this._offset) {\n      this._ids = this._ids.slice(range.offset - this._offset);\n      this._offset = range.offset;\n    }\n\n    const rangeEnd = range.offset + range.limit;\n    const selfEnd = this._offset + this._ids.length;\n    if (rangeEnd < selfEnd) {\n      this._ids.length = Math.max(0, rangeEnd - this._offset);\n    }\n\n    const models = this.models();\n    this._modelsHash = {};\n    this._idToIndexHash = null;\n    models.forEach((m) => this.updateModel(m));\n  }\n\n  addModelsInRange(rangeModels, range) {\n    this.addIdsInRange(rangeModels.map(m => m.id), range);\n    rangeModels.forEach((m) => this.updateModel(m));\n  }\n\n  addIdsInRange(rangeIds, range) {\n    if ((this._offset === null) || range.isInfinite()) {\n      this._ids = rangeIds;\n      this._idToIndexHash = null;\n      this._offset = range.offset;\n    } else {\n      const currentEnd = this._offset + this._ids.length;\n      const rangeIdsEnd = range.offset + rangeIds.length;\n\n      if (rangeIdsEnd < this._offset) {\n        throw new Error(`addIdsInRange: You can only add adjacent values (${rangeIdsEnd} < ${this._offset})`)\n      }\n      if (range.offset > currentEnd) {\n        throw new Error(`addIdsInRange: You can only add adjacent values (${range.offset} > ${currentEnd})`);\n      }\n\n      let existingBefore = []\n      if (range.offset > this._offset) {\n        existingBefore = this._ids.slice(0, range.offset - this._offset);\n      }\n\n      let existingAfter = []\n      if ((rangeIds.length === range.limit) && (currentEnd > rangeIdsEnd)) {\n        existingAfter = this._ids.slice(rangeIdsEnd - this._offset);\n      }\n\n      this._ids = [].concat(existingBefore, rangeIds, existingAfter);\n      this._idToIndexHash = null;\n      this._offset = Math.min(this._offset, range.offset);\n    }\n  }\n\n  updateModel(item) {\n    if (!item) {\n      return;\n    }\n\n    // Sometimes the new copy of `item` doesn't contain the joined data present\n    // in the old one, since it's not provided by default and may not have changed.\n    // Make sure we never drop joined data by pulling it over.\n    const existing = this._modelsHash[item.clientId];\n    if (existing) {\n      const attrs = existing.constructor.attributes\n      for (const key of Object.keys(attrs)) {\n        const attr = attrs[key];\n        if ((attr instanceof AttributeJoinedData) && (item[attr.modelKey] === undefined)) {\n          item[attr.modelKey] = existing[attr.modelKey];\n        }\n      }\n\n      // There can briefly be two rows in the database that are actually the\n      // same item (due to optimistic client-side updates). When these rows are\n      // merged together, _ids is not properly updated because there is never\n      // an 'unpersist' change record. We attempt to catch this scenario here.\n      // First check that the existing item was optimistic (no serverId) and the\n      // new item is not.\n      if (!existing.serverId && item.serverId) {\n        // Now check that _ids does in fact have both IDs.\n        const clientIndex = this._ids.indexOf(existing.clientId)\n        const serverIndex = this._ids.indexOf(item.serverId)\n        if (clientIndex > -1 && serverIndex > -1) {\n          // Remove the clientID\n          this._ids.splice(clientIndex, 1)\n        }\n      }\n    }\n\n    this._modelsHash[item.clientId] = item;\n    this._modelsHash[item.id] = item;\n    this._idToIndexHash = null;\n  }\n\n  removeModelAtOffset(item, offset) {\n    const idx = offset - this._offset;\n    delete this._modelsHash[item.clientId];\n    delete this._modelsHash[item.id];\n    this._ids.splice(idx, 1);\n    this._idToIndexHash = null;\n  }\n\n  setQuery(query) {\n    this._query = query.clone();\n    this._query.finalize();\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/mutable-query-subscription.es6",
    "content": "import QuerySubscription from './query-subscription'\n\nclass MutableQuerySubscription extends QuerySubscription {\n\n  replaceQuery(nextQuery) {\n    if (this._query && this._query.sql() === nextQuery.sql()) {\n      return\n    }\n\n    let rangeIsOnlyChange = false;\n    if (this._query) {\n      rangeIsOnlyChange = this._query.clone().offset(0).limit(0).sql() === nextQuery.clone().offset(0).limit(0).sql()\n    }\n\n    this.cancelPendingUpdate()\n\n    nextQuery.finalize()\n    this._query = nextQuery\n    if (!(this._set && rangeIsOnlyChange)) {\n      this._set = null\n    }\n    this.update()\n  }\n\n  replaceRange = ({start, end}) => {\n    if (!this._query) {\n      return\n    }\n\n    const next = this._query.clone().page(start, end);\n    if (!next.range().isEqual(this._query.range())) {\n      this.replaceQuery(next);\n    }\n  }\n}\n\nexport default MutableQuerySubscription\n"
  },
  {
    "path": "packages/client-app/src/flux/models/provider-syncback-request.es6",
    "content": "import Model from './model'\nimport Attributes from '../attributes'\n\nexport default class ProviderSyncbackRequest extends Model {\n  static attributes = Object.assign({}, Model.attributes, {\n    type: Attributes.String({\n      queryable: true,\n      modelKey: 'type',\n    }),\n\n    error: Attributes.Object({\n      modelKey: 'error',\n    }),\n\n    props: Attributes.Object({\n      modelKey: 'props',\n    }),\n\n    responseJSON: Attributes.Object({\n      modelKey: 'responseJSON',\n      jsonKey: 'response_json',\n    }),\n\n    // The following are \"normalized\" fields that we can use to consolidate\n    // various thirdPartyData source. These list of attributes should\n    // always be optional and may change as the needs of a Nylas contact\n    // change over time.\n    status: Attributes.String({\n      modelKey: 'status',\n    }),\n  });\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/query-range.es6",
    "content": "\nexport default class QueryRange {\n  static infinite() {\n    return new QueryRange({limit: null, offset: null});\n  }\n\n  static rangeWithUnion(a, b) {\n    if (a.isInfinite() || b.isInfinite()) {\n      return QueryRange.infinite();\n    }\n    if (!a.isContiguousWith(b)) {\n      throw new Error('You cannot union ranges which do not touch or intersect.');\n    }\n\n    return new QueryRange({\n      start: Math.min(a.start, b.start),\n      end: Math.max(a.end, b.end),\n    });\n  }\n\n  static rangesBySubtracting(a, b) {\n    if (!b) {\n      return [];\n    }\n\n    if (a.isInfinite() || b.isInfinite()) {\n      throw new Error(\"You cannot subtract infinite ranges.\");\n    }\n\n    const uncovered = []\n    if (b.start > a.start) {\n      uncovered.push(new QueryRange({start: a.start, end: Math.min(a.end, b.start)}))\n    }\n    if (b.end < a.end) {\n      uncovered.push(new QueryRange({start: Math.max(a.start, b.end), end: a.end}));\n    }\n    return uncovered;\n  }\n\n  get start() {\n    return this.offset;\n  }\n\n  get end() {\n    return this.offset + this.limit;\n  }\n\n  constructor({limit, offset, start, end} = {}) {\n    this.limit = limit;\n    this.offset = offset;\n\n    if ((start !== undefined) && (offset === undefined)) {\n      this.offset = start;\n    }\n    if ((end !== undefined) && (limit === undefined)) {\n      this.limit = end - this.offset;\n    }\n\n    if (this.limit === undefined) {\n      throw new Error(\"You must specify a limit\");\n    }\n    if (this.offset === undefined) {\n      throw new Error(\"You must specify an offset\");\n    }\n  }\n\n  clone() {\n    const {limit, offset} = this;\n    return new QueryRange({limit, offset});\n  }\n\n  isInfinite() {\n    return (this.limit === null) && (this.offset === null);\n  }\n\n  isEqual(b) {\n    return (this.start === b.start) && (this.end === b.end);\n  }\n\n  // Returns true if joining the two ranges would not result in empty space.\n  // ie: they intersect or touch\n  isContiguousWith(b) {\n    if (this.isInfinite() || b.isInfinite()) {\n      return true;\n    }\n    return ((this.start <= b.start) && (b.start <= this.end)) || ((this.start <= b.end) && (b.end <= this.end));\n  }\n\n  toString() {\n    return `QueryRange{${this.start} - ${this.end}}`;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/query-result-set.es6",
    "content": "import QueryRange from './query-range';\n\n/*\nPublic: Instances of QueryResultSet hold a set of models retrieved\nfrom the database at a given offset.\n\nComplete vs Incomplete:\n\nQueryResultSet keeps an array of item ids and a lookup table of models.\nThe lookup table may be incomplete if the QuerySubscription isn't finished\npreparing results. You can use `isComplete` to determine whether the set\nhas every model.\n\nOffset vs Index:\n\nTo avoid confusion, \"index\" refers to an item's position in an\narray, and \"offset\" refers to it's position in the query result set. For example,\nan item might be at index 20 in the _ids array, but at offset 120 in the result.\n\nIds and clientIds:\n\nQueryResultSet calways returns object `ids` when asked for ids, but lookups\nfor models by clientId work once models are loaded.\n*/\nexport default class QueryResultSet {\n\n  static setByApplyingModels(set, models) {\n    if (models instanceof Array) {\n      throw new Error(\"setByApplyingModels: A hash of models is required.\");\n    }\n    const out = set.clone();\n    out._modelsHash = models;\n    out._idToIndexHash = null;\n    return out;\n  }\n\n  constructor(other = {}) {\n    this._offset = (other._offset !== undefined) ? other._offset : null;\n    this._query = (other._query !== undefined) ? other._query : null;\n    this._idToIndexHash = (other._idToIndexHash !== undefined) ? other._idToIndexHash : null;\n    // Clone, since the others may be frozen\n    this._modelsHash = Object.assign({}, other._modelsHash || {})\n    this._ids = [].concat(other._ids || []);\n  }\n\n  clone() {\n    return new this.constructor({\n      _ids: [].concat(this._ids),\n      _modelsHash: Object.assign({}, this._modelsHash),\n      _idToIndexHash: Object.assign({}, this._idToIndexHash),\n      _query: this._query,\n      _offset: this._offset,\n    });\n  }\n\n  isComplete() {\n    return this._ids.every((id) => this._modelsHash[id]);\n  }\n\n  range() {\n    return new QueryRange({offset: this._offset, limit: this._ids.length});\n  }\n\n  query() {\n    return this._query;\n  }\n\n  count() {\n    return this._ids.length;\n  }\n\n  empty() {\n    return this.count() === 0;\n  }\n\n  ids() {\n    return this._ids;\n  }\n\n  idAtOffset(offset) {\n    return this._ids[offset - this._offset];\n  }\n\n  models() {\n    return this._ids.map((id) => this._modelsHash[id]);\n  }\n\n  modelCacheCount() {\n    return Object.keys(this._modelsHash).length;\n  }\n\n  modelAtOffset(offset) {\n    if (!Number.isInteger(offset)) {\n      throw new Error(\"QueryResultSet.modelAtOffset() takes a numeric index. Maybe you meant modelWithId()?\");\n    }\n    return this._modelsHash[this._ids[offset - this._offset]];\n  }\n\n  modelWithId(id) {\n    return this._modelsHash[id];\n  }\n\n  buildIdToIndexHash() {\n    this._idToIndexHash = {}\n    this._ids.forEach((id, idx) => {\n      this._idToIndexHash[id] = idx;\n      const model = this._modelsHash[id];\n      if (model) {\n        this._idToIndexHash[model.clientId] = idx;\n      }\n    });\n  }\n\n  offsetOfId(id) {\n    if (this._idToIndexHash === null) {\n      this.buildIdToIndexHash();\n    }\n\n    if (this._idToIndexHash[id] === undefined) {\n      return -1;\n    }\n    return this._idToIndexHash[id] + this._offset\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/query-subscription-pool.es6",
    "content": "/* eslint global-require: 0 */\nimport QuerySubscription from './query-subscription';\nlet DatabaseStore = null;\n\n/*\nPublic: The QuerySubscriptionPool maintains a list of all of the query\nsubscriptions in the app. In the future, this class will monitor performance,\nmerge equivalent subscriptions, etc.\n*/\nclass QuerySubscriptionPool {\n  constructor() {\n    this._subscriptions = {};\n    this._cleanupChecks = [];\n    this._setup();\n  }\n\n  add(query, callback) {\n    if (NylasEnv.inDevMode()) {\n      callback._registrationPoint = this._formatRegistrationPoint((new Error()).stack);\n    }\n\n    const key = this._keyForQuery(query);\n    let subscription = this._subscriptions[key];\n    if (!subscription) {\n      subscription = new QuerySubscription(query);\n      this._subscriptions[key] = subscription;\n    }\n\n    subscription.addCallback(callback);\n    return () => {\n      subscription.removeCallback(callback);\n      this._scheduleCleanupCheckForSubscription(key);\n    };\n  }\n\n  addPrivateSubscription(key, subscription, callback) {\n    this._subscriptions[key] = subscription;\n    subscription.addCallback(callback);\n    return () => {\n      subscription.removeCallback(callback);\n      this._scheduleCleanupCheckForSubscription(key);\n    };\n  }\n\n  printSubscriptions() {\n    if (!NylasEnv.inDevMode()) {\n      console.log(\"printSubscriptions is only available in developer mode.\");\n      return;\n    }\n\n    for (const key of Object.keys(this._subscriptions)) {\n      const subscription = this._subscriptions[key];\n      console.log(key);\n      console.group();\n      for (const callback of subscription._callbacks) {\n        console.log(`${callback._registrationPoint}`);\n      }\n      console.groupEnd();\n    }\n  }\n\n  _scheduleCleanupCheckForSubscription(key) {\n    // We unlisten / relisten to lots of subscriptions and setTimeout is actually\n    // /not/ that fast. Create one timeout for all checks, not one for each.\n    if (this._cleanupChecks.length === 0) {\n      setTimeout(() => this._runCleanupChecks(), 1);\n    }\n    this._cleanupChecks.push(key);\n  }\n\n  _runCleanupChecks() {\n    for (const key of this._cleanupChecks) {\n      const subscription = this._subscriptions[key];\n      if (subscription && (subscription.callbackCount() === 0)) {\n        delete this._subscriptions[key];\n      }\n    }\n    this._cleanupChecks = [];\n  }\n\n  _formatRegistrationPoint(stackString) {\n    const stack = stackString.split('\\n');\n    let ii = 0;\n    let seenRx = false;\n    while (ii < stack.length) {\n      const hasRx = stack[ii].indexOf('rx.lite') !== -1;\n      seenRx = seenRx || hasRx;\n      if (seenRx === true && !hasRx) {\n        break;\n      }\n      ii += 1;\n    }\n\n    return stack.slice(ii, ii + 4).join('\\n');\n  }\n\n  _keyForQuery(query) {\n    return query.sql();\n  }\n\n  _setup() {\n    DatabaseStore = DatabaseStore || require('../stores/database-store').default;\n    DatabaseStore.listen(this._onChange);\n  }\n\n  _onChange = (record) => {\n    for (const key of Object.keys(this._subscriptions)) {\n      const subscription = this._subscriptions[key];\n      subscription.applyChangeRecord(record);\n    }\n  }\n}\n\nconst pool = new QuerySubscriptionPool();\nexport default pool;\n"
  },
  {
    "path": "packages/client-app/src/flux/models/query-subscription.es6",
    "content": "import _ from 'underscore';\nimport DatabaseStore from '../stores/database-store';\nimport QueryRange from './query-range';\nimport MutableQueryResultSet from './mutable-query-result-set';\n\nexport default class QuerySubscription {\n  constructor(query, options = {}) {\n    this._query = query;\n    this._options = options;\n\n    this._set = null;\n    this._callbacks = [];\n    this._lastResult = null;\n    this._updateInFlight = false;\n    this._queuedChangeRecords = [];\n    this._queryVersion = 1;\n\n    if (this._query) {\n      if (this._query._count) {\n        throw new Error(\"QuerySubscription::constructor - You cannot listen to count queries.\")\n      }\n\n      this._query.finalize();\n\n      if (this._options.initialModels) {\n        this._set = new MutableQueryResultSet();\n        this._set.addModelsInRange(this._options.initialModels, new QueryRange({\n          limit: this._options.initialModels.length,\n          offset: 0,\n        }));\n        this._createResultAndTrigger();\n      } else {\n        this.update();\n      }\n    }\n  }\n\n  query = () => {\n    return this._query;\n  }\n\n  addCallback = (callback) => {\n    if (!(callback instanceof Function)) {\n      throw new Error(`QuerySubscription:addCallback - expects a function, received ${callback}`);\n    }\n    this._callbacks.push(callback);\n\n    if (this._lastResult) {\n      setTimeout(() => {\n        if (!this._lastResult) { return; }\n        callback(this._lastResult);\n      }, 0);\n    }\n  }\n\n  hasCallback = (callback) => {\n    return (this._callbacks.indexOf(callback) !== -1);\n  }\n\n  removeCallback(callback) {\n    if (!(callback instanceof Function)) {\n      throw new Error(`QuerySubscription:removeCallback - expects a function, received ${callback}`)\n    }\n    this._callbacks = _.without(this._callbacks, callback);\n    if (this.callbackCount() === 0) {\n      this.onLastCallbackRemoved()\n    }\n  }\n\n  onLastCallbackRemoved() {\n\n  }\n\n  callbackCount = () => {\n    return this._callbacks.length;\n  }\n\n  applyChangeRecord = (record) => {\n    if (!this._query || record.objectClass !== this._query.objectClass()) {\n      return;\n    }\n    if (record.objects.length === 0) {\n      return;\n    }\n\n    this._queuedChangeRecords.push(record);\n    if (!this._updateInFlight) {\n      this._processChangeRecords();\n    }\n  }\n\n  cancelPendingUpdate = () => {\n    this._queryVersion += 1;\n    this._updateInFlight = false;\n  }\n\n  // Scan through change records and apply them to the last result set.\n  _processChangeRecords = () => {\n    if (this._queuedChangeRecords.length === 0) {\n      return;\n    }\n    if (!this._set) {\n      this.update();\n      return;\n    }\n\n    let knownImpacts = 0;\n    let unknownImpacts = 0;\n\n    this._queuedChangeRecords.forEach((record) => {\n      if (record.type === 'unpersist') {\n        for (const item of record.objects) {\n          const offset = this._set.offsetOfId(item.clientId)\n          if (offset !== -1) {\n            this._set.removeModelAtOffset(item, offset);\n            unknownImpacts += 1;\n          }\n        }\n      } else if (record.type === 'persist') {\n        for (const item of record.objects) {\n          const offset = this._set.offsetOfId(item.clientId);\n          const itemIsInSet = offset !== -1;\n          const itemShouldBeInSet = item.matches(this._query.matchers());\n\n          if (itemIsInSet && !itemShouldBeInSet) {\n            this._set.removeModelAtOffset(item, offset)\n            unknownImpacts += 1\n          } else if (itemShouldBeInSet && !itemIsInSet) {\n            this._set.updateModel(item)\n            unknownImpacts += 1;\n          } else if (itemIsInSet) {\n            const oldItem = this._set.modelWithId(item.clientId);\n            this._set.updateModel(item);\n\n            if (this._itemSortOrderHasChanged(oldItem, item)) {\n              unknownImpacts += 1\n            } else {\n              knownImpacts += 1\n            }\n          }\n        }\n        // If we're not at the top of the result set, we can't be sure whether an\n        // item previously matched the set and doesn't anymore, impacting the items\n        // in the query range. We need to refetch IDs to be sure our set === correct.\n        if ((this._query.range().offset > 0) && (unknownImpacts + knownImpacts) < record.objects.length) {\n          unknownImpacts += 1\n        }\n      }\n    });\n\n    this._queuedChangeRecords = [];\n\n    if (unknownImpacts > 0) {\n      this.update({mustRefetchEntireRange: true});\n    } else if (knownImpacts > 0) {\n      this._createResultAndTrigger();\n    }\n  }\n\n  _itemSortOrderHasChanged(old, updated) {\n    for (const descriptor of this._query.orderSortDescriptors()) {\n      const oldSortValue = old[descriptor.attr.modelKey];\n      const updatedSortValue = updated[descriptor.attr.modelKey];\n\n      // http://stackoverflow.com/questions/4587060/determining-date-equality-in-javascript\n      if (!(oldSortValue >= updatedSortValue && oldSortValue <= updatedSortValue)) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  update({mustRefetchEntireRange} = {}) {\n    this._updateInFlight = true;\n\n    const desiredRange = this._query.range();\n    const currentRange = this._set ? this._set.range() : null;\n    const hasNonInfiniteRange = currentRange && !currentRange.isInfinite() && !desiredRange.isInfinite();\n\n    // If we have a limited range, and changes don't require that we refetch\n    // the entire range, just fetch the missing items. This is the path typically\n    // used while scrolling.\n    if (hasNonInfiniteRange && !mustRefetchEntireRange) {\n      const missingRange = this._getMissingRange(desiredRange, currentRange);\n      this._fetchRange(missingRange, {\n        version: this._queryVersion,\n        fetchEntireModels: true,\n      });\n    } else {\n      const haveNoModels = !this._set || this._set.modelCacheCount() === 0;\n      this._fetchRange(desiredRange, {\n        version: this._queryVersion,\n        fetchEntireModels: haveNoModels,\n      });\n    }\n  }\n\n  _getMissingRange = (desiredRange, currentRange) => {\n    if (currentRange && !currentRange.isInfinite() && !desiredRange.isInfinite()) {\n      const ranges = QueryRange.rangesBySubtracting(desiredRange, currentRange);\n      return (ranges.length === 1) ? ranges[0] : desiredRange;\n    }\n    return desiredRange;\n  }\n\n  _getQueryForRange = (range, fetchEntireModels) => {\n    let rangeQuery = null;\n    if (!range.isInfinite()) {\n      rangeQuery = rangeQuery || this._query.clone();\n      rangeQuery.offset(range.offset).limit(range.limit);\n    }\n    if (!fetchEntireModels) {\n      rangeQuery = rangeQuery || this._query.clone();\n      rangeQuery.idsOnly();\n    }\n    rangeQuery = rangeQuery || this._query;\n    return rangeQuery;\n  }\n\n  _fetchRange(range, {version, fetchEntireModels}) {\n    const rangeQuery = this._getQueryForRange(range, fetchEntireModels);\n\n    DatabaseStore.run(rangeQuery, {format: false}).then((results) => {\n      if (this._queryVersion !== version) {\n        return;\n      }\n\n      if (this._set && !this._set.range().isContiguousWith(range)) {\n        this._set = null;\n      }\n      this._set = this._set || new MutableQueryResultSet();\n\n      if (fetchEntireModels) {\n        this._set.addModelsInRange(results, range);\n      } else {\n        this._set.addIdsInRange(results, range);\n      }\n\n      this._set.clipToRange(this._query.range());\n\n      this._fetchMissingModels().then((models) => {\n        if (this._queryVersion !== version) {\n          return;\n        }\n        for (const m of models) {\n          this._set.updateModel(m);\n        }\n        this._createResultAndTrigger();\n      });\n    });\n  }\n\n  _fetchMissingModels() {\n    const missingIds = this._set.ids().filter(id => !this._set.modelWithId(id));\n    if (missingIds.length === 0) {\n      return Promise.resolve([]);\n    }\n    return DatabaseStore.findAll(this._query._klass, {id: missingIds});\n  }\n\n  _createResultAndTrigger = () => {\n    const allCompleteModels = this._set.isComplete()\n    const allUniqueIds = _.uniq(this._set.ids()).length === this._set.ids().length\n\n    let error = null;\n    if (!allCompleteModels) {\n      error = new Error(\"QuerySubscription: Applied all changes and result set is missing models.\");\n    }\n    if (!allUniqueIds) {\n      error = new Error(\"QuerySubscription: Applied all changes and result set contains duplicate IDs.\");\n    }\n\n    if (error) {\n      NylasEnv.reportError(error);\n      this._set = null;\n      this.update();\n      return;\n    }\n\n    if (this._options.emitResultSet) {\n      this._set.setQuery(this._query);\n      this._lastResult = this._set.immutableClone();\n    } else {\n      this._lastResult = this._query.formatResult(this._set.models());\n    }\n\n    this._callbacks.forEach((callback) => callback(this._lastResult));\n\n    // process any additional change records that have arrived\n    if (this._updateInFlight) {\n      this._updateInFlight = false;\n      this._processChangeRecords();\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/query.es6",
    "content": "/* eslint global-require: 0 */\nimport Attributes from '../attributes';\nimport QueryRange from './query-range';\nimport Utils from './utils';\n\nconst {Matcher, AttributeJoinedData, AttributeCollection} = Attributes;\n\n/*\nPublic: ModelQuery exposes an ActiveRecord-style syntax for building database queries\nthat return models and model counts. Model queries are returned from the factory methods\n{DatabaseStore::find}, {DatabaseStore::findBy}, {DatabaseStore::findAll}, and {DatabaseStore::count}, and are the primary interface for retrieving data\nfrom the app's local cache.\n\nModelQuery does not allow you to modify the local cache. To create, update or\ndelete items from the local cache, see {DatabaseStore::persistModel}\nand {DatabaseStore::unpersistModel}.\n\n**Simple Example:** Fetch a thread\n\n```coffee\nquery = DatabaseStore.find(Thread, '123a2sc1ef4131')\nquery.then (thread) ->\n  // thread or null\n```\n\n**Advanced Example:** Fetch 50 threads in the inbox, in descending order\n\n```coffee\nquery = DatabaseStore.findAll(Thread)\nquery.where([Thread.attributes.categories.contains('label-id')])\n     .order([Thread.attributes.lastMessageReceivedTimestamp.descending()])\n     .limit(100).offset(50)\n     .then (threads) ->\n  // array of threads\n```\n\nSection: Database\n*/\nexport default class ModelQuery {\n\n  // Public\n  // - `class` A {Model} class to query\n  // - `database` (optional) An optional reference to a {DatabaseStore} the\n  //   query will be executed on.\n  //\n  constructor(klass, database) {\n    this._klass = klass;\n    this._database = database || require('./database-store').default;\n    this._matchers = [];\n    this._orders = [];\n    this._background = false;\n    this._backgroundable = true;\n    this._distinct = false;\n    this._range = QueryRange.infinite();\n    this._returnOne = false;\n    this._returnIds = false;\n    this._includeJoinedData = [];\n    this._count = false;\n    this._logQueryPlanDebugOutput = true;\n  }\n\n  clone() {\n    const q = new ModelQuery(this._klass, this._database).where(this._matchers).order(this._orders);\n    q._orders = [].concat(this._orders);\n    q._includeJoinedData = [].concat(this._includeJoinedData);\n    q._range = this._range.clone();\n    q._background = this._background;\n    q._backgroundable = this._backgroundable;\n    q._distinct = this._distinct;\n    q._returnOne = this._returnOne;\n    q._returnIds = this._returnIds;\n    q._count = this._count;\n    return q;\n  }\n\n  distinct() {\n    this._distinct = true;\n    return this;\n  }\n\n  background() {\n    if (!this._backgroundable) {\n      throw new Error(\"Queries within transactions cannot be moved to the background.\");\n    }\n    this._background = true;\n    return this;\n  }\n\n  markNotBackgroundable() {\n    this._backgroundable = false;\n    return this;\n  }\n\n  silenceQueryPlanDebugOutput() {\n    this._logQueryPlanDebugOutput = false;\n    return this;\n  }\n\n  // Public: Add one or more where clauses to the query\n  //\n  // - `matchers` An {Array} of {Matcher} objects that add where clauses to the underlying query.\n  //\n  // This method is chainable.\n  //\n  where(matchers) {\n    this._assertNotFinalized();\n\n    if (matchers instanceof Matcher) {\n      this._matchers.push(matchers);\n    } else if (matchers instanceof Array) {\n      for (const m of matchers) {\n        if (!(m instanceof Matcher)) {\n          throw new Error(\"You must provide instances of `Matcher`\");\n        }\n      }\n      this._matchers = this._matchers.concat(matchers);\n    } else if (matchers instanceof Object) {\n      // Support a shorthand format of {id: '123', accountId: '123'}\n      for (const key of Object.keys(matchers)) {\n        const value = matchers[key];\n        const attr = this._klass.attributes[key];\n        if (!attr) {\n          const msg = `Cannot create where clause \\`${key}:${value}\\`. ${key} is not an attribute of ${this._klass.name}`;\n          throw new Error(msg);\n        }\n\n        if (value instanceof Array) {\n          this._matchers.push(attr.in(value));\n        } else {\n          this._matchers.push(attr.equal(value));\n        }\n      }\n    }\n    return this;\n  }\n\n  whereAny(matchers) {\n    this._assertNotFinalized();\n    this._matchers.push(new Matcher.Or(matchers));\n    return this;\n  }\n\n  search(query) {\n    this._assertNotFinalized();\n    this._matchers.push(new Matcher.Search(query));\n    return this;\n  }\n\n  structuredSearch(query) {\n    this._assertNotFinalized();\n    this._matchers.push(new Matcher.StructuredSearch(query));\n    return this;\n  }\n\n  // Public: Include specific joined data attributes in result objects.\n  // - `attr` A {AttributeJoinedData} that you want to be populated in\n  //  the returned models. Note: This results in a LEFT OUTER JOIN.\n  //  See {AttributeJoinedData} for more information.\n  //\n  // This method is chainable.\n  //\n  include(attr) {\n    this._assertNotFinalized();\n    if (!(attr instanceof AttributeJoinedData)) {\n      throw new Error(\"query.include() must be called with a joined data attribute\");\n    }\n    this._includeJoinedData.push(attr);\n    return this;\n  }\n\n  // Public: Include all of the available joined data attributes in returned models.\n  //\n  // This method is chainable.\n  //\n  includeAll() {\n    this._assertNotFinalized()\n    for (const key of Object.keys(this._klass.attributes)) {\n      const attr = this._klass.attributes[key];\n      if (attr instanceof AttributeJoinedData) {\n        this.include(attr);\n      }\n    }\n    return this;\n  }\n\n  // Public: Apply a sort order to the query.\n  // - `orders` An {Array} of one or more {SortOrder} objects that determine the\n  //   sort order of returned models.\n  //\n  // This method is chainable.\n  //\n  order(ordersOrOrder) {\n    this._assertNotFinalized();\n    const orders = (ordersOrOrder instanceof Array) ? ordersOrOrder : [ordersOrOrder];\n    this._orders = this._orders.concat(orders);\n    return this;\n  }\n\n  // Public: Set the `singular` flag - only one model will be returned from the\n  // query, and a `LIMIT 1` clause will be used.\n  //\n  // This method is chainable.\n  //\n  one() {\n    this._assertNotFinalized();\n    this._returnOne = true;\n    return this;\n  }\n\n  // Public: Limit the number of query results.\n  //\n  // - `limit` {Number} The number of models that should be returned.\n  //\n  // This method is chainable.\n  //\n  limit(limit) {\n    this._assertNotFinalized()\n    if (this._returnOne && limit > 1) {\n      throw new Error(\"Cannot use limit > 1 with one()\");\n    }\n    this._range = this._range.clone();\n    this._range.limit = limit;\n    return this;\n  }\n\n  // Public:\n  //\n  // - `offset` {Number} The start offset of the query.\n  //\n  // This method is chainable.\n  //\n  offset(offset) {\n    this._assertNotFinalized();\n    this._range = this._range.clone();\n    this._range.offset = offset;\n    return this;\n  }\n\n  // Public:\n  //\n  // A convenience method for setting both limit and offset given a desired page size.\n  //\n  page(start, end, pageSize = 50, pagePadding = 100) {\n    const roundToPage = (n) => Math.max(0, Math.floor(n / pageSize) * pageSize)\n    this.offset(roundToPage(start - pagePadding));\n    this.limit(roundToPage((end - start) + pagePadding * 2));\n    return this;\n  }\n\n  // Public: Set the `count` flag - instead of returning inflated models,\n  // the query will return the result `COUNT`.\n  //\n  // This method is chainable.\n  //\n  count() {\n    this._assertNotFinalized();\n    this._count = true;\n    return this;\n  }\n\n  idsOnly() {\n    this._assertNotFinalized();\n    this._returnIds = true;\n    return this;\n  }\n\n  // Query Execution\n\n  // Public: Short-hand syntax that calls run().then(fn) with the provided function.\n  //\n  // Returns a {Promise} that resolves with the Models returned by the\n  // query, or rejects with an error from the Database layer.\n  //\n  then(next) {\n    return this.run(this).then(next);\n  }\n\n  // Public: Returns a {Promise} that resolves with the Models returned by the\n  // query, or rejects with an error from the Database layer.\n  //\n  run() {\n    return this._database.run(this);\n  }\n\n  inflateResult(result) {\n    if (!result) {\n      return null;\n    }\n\n    if (this._count) {\n      return result[0].count / 1;\n    }\n    if (this._returnIds) {\n      return result.map(row => row.id);\n    }\n\n    try {\n      return result.map((row) => {\n        const json = JSON.parse(row.data, Utils.registeredObjectReviver)\n        const object = (new this._klass()).fromJSON(json);\n        for (const attrName of Object.keys(this._klass.attributes)) {\n          const attr = this._klass.attributes[attrName];\n          if (!attr.needsColumn() || !attr.loadFromColumn) {\n            continue;\n          }\n          object[attr.modelKey] = attr.fromColumn(row[attr.jsonKey]);\n        }\n        for (const attr of this._includeJoinedData) {\n          let value = row[attr.jsonKey];\n          if (value === AttributeJoinedData.NullPlaceholder) {\n            value = null;\n          }\n          object[attr.modelKey] = attr.deserialize(object, value);\n        }\n        return object;\n      });\n    } catch (error) {\n      throw new Error(`Query could not parse the database result. Query: ${this.sql()}, Error: ${error.toString()}`);\n    }\n  }\n\n  formatResult(inflated) {\n    if (this._returnOne) {\n      return inflated[0];\n    }\n    if (this._count) {\n      return inflated;\n    }\n    return [].concat(inflated);\n  }\n\n  // Query SQL Building\n\n  // Returns a {String} with the SQL generated for the query.\n  //\n  sql() {\n    this.finalize();\n\n    let result = null;\n\n    if (this._count) {\n      result = `COUNT(*) as count`;\n    } else if (this._returnIds) {\n      result = `\\`${this._klass.name}\\`.\\`id\\``;\n    } else {\n      result = `\\`${this._klass.name}\\`.\\`data\\``;\n      for (const attrName of Object.keys(this._klass.attributes)) {\n        const attr = this._klass.attributes[attrName];\n        if (!attr.needsColumn() || !attr.loadFromColumn) {\n          continue;\n        }\n        result += `, ${attr.jsonKey} `;\n      }\n      this._includeJoinedData.forEach((attr) => {\n        result += `, ${attr.selectSQL(this._klass)} `;\n      })\n    }\n\n    const order = this._count ? '' : this._orderClause();\n\n    let limit = '';\n    if (Number.isInteger(this._range.limit)) {\n      limit = `LIMIT ${this._range.limit}`;\n    } else {\n      limit = ''\n    }\n    if (Number.isInteger(this._range.offset)) {\n      limit += ` OFFSET ${this._range.offset}`;\n    }\n\n    const distinct = this._distinct ? ' DISTINCT' : '';\n    const allMatchers = this.matchersFlattened();\n\n    const joins = allMatchers.filter((matcher) => matcher.attr instanceof AttributeCollection)\n\n    if ((joins.length === 1) && this._canSubselectForJoin(joins[0], allMatchers)) {\n      const subSql = this._subselectSQL(joins[0], this._matchers, order, limit);\n      return `SELECT${distinct} ${result} FROM \\`${this._klass.name}\\` WHERE \\`id\\` IN (${subSql}) ${order}`;\n    }\n\n    return `SELECT${distinct} ${result} FROM \\`${this._klass.name}\\` ${this._whereClause()} ${order} ${limit}`;\n  }\n\n  // If one of our matchers requires a join, and the attribute configuration lists\n  // all of the other order and matcher attributes in \\`joinQueryableBy\\`, it means\n  // we can make the entire WHERE and ORDER BY on a sub-query, which improves\n  // performance considerably vs. finding all results from the join table and then\n  // doing the ordering after pulling the results in the main table.\n  //\n  // Note: This is currently only intended for use in the thread list\n  //\n  _canSubselectForJoin(matcher, allMatchers) {\n    const joinAttribute = matcher.attribute();\n\n    if (!Number.isInteger(this._range.limit)) {\n      return false;\n    }\n\n    const allMatchersOnJoinTable = allMatchers.every((m) =>\n      (m === matcher) || (joinAttribute.joinQueryableBy.includes(m.attr.modelKey)) || (m.attr.modelKey === 'id')\n    );\n    const allOrdersOnJoinTable = this._orders.every((o) =>\n      (joinAttribute.joinQueryableBy.includes(o.attr.modelKey))\n    );\n\n    return (allMatchersOnJoinTable && allOrdersOnJoinTable);\n  }\n\n  _subselectSQL(returningMatcher, subselectMatchers, order, limit) {\n    const returningAttribute = returningMatcher.attribute()\n\n    const table = Utils.tableNameForJoin(this._klass, returningAttribute.itemClass);\n    const wheres = subselectMatchers.map(c => c.whereSQL(this._klass)).filter(c => !!c);\n\n    let innerSQL = `SELECT \\`id\\` FROM \\`${table}\\` WHERE ${wheres.join(' AND ')} ${order} ${limit}`;\n    innerSQL = innerSQL.replace(new RegExp(`\\`${this._klass.name}\\``, 'g'), `\\`${table}\\``);\n    innerSQL = innerSQL.replace(new RegExp(`\\`${returningMatcher.joinTableRef()}\\``, 'g'), `\\`${table}\\``);\n    return innerSQL;\n  }\n\n  _whereClause() {\n    const joins = [];\n    this._matchers.forEach((c) => {\n      const join = c.joinSQL(this._klass)\n      if (join) {\n        joins.push(join);\n      }\n    });\n\n    this._includeJoinedData.forEach((attr) => {\n      const join = attr.includeSQL(this._klass)\n      if (join) {\n        joins.push(join);\n      }\n    });\n\n    const wheres = [];\n    this._matchers.forEach(c => {\n      const where = c.whereSQL(this._klass);\n      if (where) {\n        wheres.push(where)\n      }\n    });\n\n    let sql = joins.join(' ')\n    if (wheres.length > 0) {\n      sql += ` WHERE ${wheres.join(' AND ')}`;\n    }\n    return sql;\n  }\n\n  _orderClause() {\n    if (this._orders.length === 0) {\n      return ''\n    }\n\n    let sql = ' ORDER BY '\n    this._orders.forEach((sort) => {\n      sql += sort.orderBySQL(this._klass);\n    });\n    return sql;\n  }\n\n  // Private: Marks the object as final, preventing any changes to the where\n  // clauses, orders, etc.\n  finalize() {\n    if (this._finalized) {\n      return this;\n    }\n\n    if (this._orders.length === 0) {\n      const natural = this._klass.naturalSortOrder();\n      if (natural) {\n        this._orders.push(natural);\n      }\n    }\n\n    if (this._returnOne && !this._range.limit) {\n      this.limit(1);\n    }\n\n    this._finalized = true;\n    return this;\n  }\n\n  // Private: Throws an exception if the query has been frozen.\n  _assertNotFinalized() {\n    if (this._finalized) {\n      throw new Error(`ModelQuery: You cannot modify a query after calling \\`then\\` or \\`listen\\``);\n    }\n  }\n\n  // Introspection\n  // (These are here to make specs easy)\n\n  matchers() {\n    return this._matchers;\n  }\n\n  matchersFlattened() {\n    const all = []\n    const traverse = (matchers) => {\n      if (!(matchers instanceof Array)) {\n        return;\n      }\n      for (const m of matchers) {\n        if (m.children) {\n          traverse(m.children);\n        } else {\n          all.push(m);\n        }\n      }\n    }\n    traverse(this._matchers);\n    return all;\n  }\n\n  matcherValueForModelKey(key) {\n    const matcher = this._matchers.find(m => m.attr.modelKey === key)\n    return matcher ? matcher.val : null;\n  }\n\n  range() {\n    return this._range;\n  }\n\n  orderSortDescriptors() {\n    return this._orders;\n  }\n\n  objectClass() {\n    return this._klass.name;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/thread.es6",
    "content": "import _ from 'underscore'\nimport Message from './message'\nimport Contact from './contact'\nimport Category from './category'\nimport Attributes from '../attributes'\nimport DatabaseStore from '../stores/database-store'\nimport ModelWithMetadata from './model-with-metadata'\n\n\n/*\nPublic: The Thread model represents a Thread object served by the Nylas Platform API.\nFor more information about Threads on the Nylas Platform, read the\n[Threads API Documentation](https://nylas.com/cloud/docs#threads)\n\nAttributes\n\n`snippet`: {AttributeString} A short, ~140 character string with the content\n  of the last message in the thread. Queryable.\n\n`subject`: {AttributeString} The subject of the thread. Queryable.\n\n`unread`: {AttributeBoolean} True if the thread is unread. Queryable.\n\n`starred`: {AttributeBoolean} True if the thread is starred. Queryable.\n\n`version`: {AttributeNumber} The version number of the thread.\n\n`participants`: {AttributeCollection} A set of {Contact} models\n  representing the participants in the thread.\n  Note: Contacts on Threads do not have IDs.\n\n`lastMessageReceivedTimestamp`: {AttributeDateTime} The timestamp of the\n  last message on the thread.\n\nThis class also inherits attributes from {Model}\n\nSection: Models\n@class Thread\n*/\nclass Thread extends ModelWithMetadata {\n\n  static attributes = _.extend({}, ModelWithMetadata.attributes, {\n    snippet: Attributes.String({\n      modelKey: 'snippet',\n    }),\n\n    subject: Attributes.String({\n      queryable: true,\n      modelKey: 'subject',\n    }),\n\n    unread: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'unread',\n    }),\n\n    starred: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'starred',\n    }),\n\n    version: Attributes.Number({\n      queryable: true,\n      modelKey: 'version',\n    }),\n\n    categories: Attributes.Collection({\n      queryable: true,\n      modelKey: 'categories',\n      joinOnField: 'id',\n      joinQueryableBy: ['inAllMail', 'lastMessageReceivedTimestamp', 'lastMessageSentTimestamp', 'unread'],\n      itemClass: Category,\n    }),\n\n    categoriesType: Attributes.String({\n      modelKey: 'categoriesType',\n    }),\n\n    participants: Attributes.Collection({\n      queryable: true,\n      modelKey: 'participants',\n      joinOnField: 'email',\n      joinQueryableBy: ['lastMessageReceivedTimestamp'],\n      itemClass: Contact,\n    }),\n\n    hasAttachments: Attributes.Boolean({\n      modelKey: 'has_attachments',\n    }),\n\n    lastMessageReceivedTimestamp: Attributes.DateTime({\n      queryable: true,\n      modelKey: 'lastMessageReceivedTimestamp',\n      jsonKey: 'last_message_received_timestamp',\n    }),\n\n    lastMessageSentTimestamp: Attributes.DateTime({\n      queryable: true,\n      modelKey: 'lastMessageSentTimestamp',\n      jsonKey: 'last_message_sent_timestamp',\n    }),\n\n    inAllMail: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'inAllMail',\n      jsonKey: 'in_all_mail',\n    }),\n\n    isSearchIndexed: Attributes.Boolean({\n      queryable: true,\n      modelKey: 'isSearchIndexed',\n      jsonKey: 'is_search_indexed',\n      defaultValue: false,\n      loadFromColumn: true,\n    }),\n\n    // This corresponds to the rowid in the FTS table. We need to use the FTS\n    // rowid when updating and deleting items in the FTS table because otherwise\n    // these operations would be way too slow on large FTS tables.\n    searchIndexId: Attributes.Number({\n      modelKey: 'searchIndexId',\n      jsonKey: 'search_index_id',\n    }),\n  })\n\n  static sortOrderAttribute = () => {\n    return Thread.attributes.lastMessageReceivedTimestamp\n  }\n\n  static naturalSortOrder = () => {\n    return Thread.sortOrderAttribute().descending()\n  }\n\n  static additionalSQLiteConfig = {\n    setup: () => [\n      // ThreadCounts\n      'CREATE TABLE IF NOT EXISTS `ThreadCounts` (`category_id` TEXT PRIMARY KEY, `unread` INTEGER, `total` INTEGER)',\n      'CREATE UNIQUE INDEX IF NOT EXISTS ThreadCountsIndex ON `ThreadCounts` (category_id DESC)',\n\n      // ThreadContact\n      'CREATE INDEX IF NOT EXISTS ThreadContactDateIndex ON `ThreadContact` (last_message_received_timestamp DESC, value, id)',\n\n      // ThreadCategory\n      'CREATE INDEX IF NOT EXISTS ThreadListCategoryIndex ON `ThreadCategory` (last_message_received_timestamp DESC, value, in_all_mail, unread, id)',\n      'CREATE INDEX IF NOT EXISTS ThreadListCategorySentIndex ON `ThreadCategory` (last_message_sent_timestamp DESC, value, in_all_mail, unread, id)',\n\n      // Thread: General index\n      'CREATE INDEX IF NOT EXISTS ThreadDateIndex ON `Thread` (last_message_received_timestamp DESC)',\n      'CREATE INDEX IF NOT EXISTS ThreadClientIdIndex ON `Thread` (client_id)',\n\n      // Thread: Partial indexes for specific views\n      'CREATE INDEX IF NOT EXISTS ThreadUnreadIndex ON `Thread` (account_id, last_message_received_timestamp DESC) WHERE unread = 1 AND in_all_mail = 1',\n      'CREATE INDEX IF NOT EXISTS ThreadUnifiedUnreadIndex ON `Thread` (last_message_received_timestamp DESC) WHERE unread = 1 AND in_all_mail = 1',\n\n      'DROP INDEX IF EXISTS `Thread`.ThreadStarIndex',\n      'CREATE INDEX IF NOT EXISTS ThreadStarredIndex ON `Thread` (account_id, last_message_received_timestamp DESC) WHERE starred = 1 AND in_all_mail = 1',\n      'CREATE INDEX IF NOT EXISTS ThreadUnifiedStarredIndex ON `Thread` (last_message_received_timestamp DESC) WHERE starred = 1 AND in_all_mail = 1',\n\n      'CREATE INDEX IF NOT EXISTS ThreadIsSearchIndexedIndex ON `Thread` (is_search_indexed, id)',\n      'CREATE INDEX IF NOT EXISTS ThreadIsSearchIndexedLastMessageReceivedIndex ON `Thread` (is_search_indexed, last_message_received_timestamp)',\n    ],\n  }\n\n  static searchable = true\n\n  static searchFields = ['subject', 'to_', 'from_', 'categories', 'body']\n\n  async messages({includeHidden} = {}) {\n    const messages = await DatabaseStore.findAll(Message)\n      .where({threadId: this.id})\n      .include(Message.attributes.body)\n    if (!includeHidden) {\n      return messages.filter(message => !message.isHidden())\n    }\n    return messages\n  }\n\n  /** Computes the plaintext version of ALL messages.\n   * WARNING: This method is VERY expensive.\n   * Parsing a thread with ~50 messages took ~2-3 seconds!\n   */\n  computePlainText() {\n    return Promise.map(this.messages(), (message) => {\n      return new Promise((resolve) => {\n        // Add a defer tick so we don't COMPLETELY hang the thread.\n        setTimeout(() => {\n          resolve(`${message.replyAttributionLine()}\\n\\n${message.computePlainText()}`)\n        }, 1)\n      })\n    }).then((plainTextBodies = []) => {\n      const msgDivider = \"\\n\\n--------------------------------------------------\\n\"\n      return plainTextBodies.join(msgDivider)\n    })\n  }\n\n  get labels() {\n    return this.categories;\n  }\n\n  set labels(labels) {\n    this.categories = labels;\n  }\n\n  get folders() {\n    return this.categories;\n  }\n\n  set folders(folders) {\n    this.categories = folders;\n  }\n\n  get inAllMail() {\n    if (this.categoriesType === 'labels') {\n      const inAllMail = _.any(this.categories, cat => cat.name === 'all')\n      if (inAllMail) {\n        return true;\n      }\n      const inTrashOrSpam = _.any(this.categories, cat => cat.name === 'trash' || cat.name === 'spam')\n      if (!inTrashOrSpam) {\n        return true;\n      }\n      return false\n    }\n    return true\n  }\n\n  /**\n   * In the `clone` case, there are `categories` set, but no `folders` nor\n   * `labels`\n   *\n   * When loading data from the API, there are `folders` AND `labels` but\n   * no `categories` yet.\n   */\n  fromJSON(json) {\n    super.fromJSON(json)\n\n    if (json.folders) {\n      this.categoriesType = 'folders'\n      this.categories = Thread.attributes.categories.fromJSON(json.folders)\n    }\n\n    if (json.labels && json.labels.length > 0) {\n      this.categoriesType = 'labels'\n      if (!this.categories) this.categories = [];\n      this.categories = this.categories.concat(Thread.attributes.categories.fromJSON(json.labels))\n    }\n\n    ['participants'].forEach((attr) => {\n      const value = this[attr]\n      if (!(value && value instanceof Array)) {\n        return;\n      }\n      value.forEach((item) => {\n        item.accountId = this.accountId\n      })\n    })\n\n    return this\n  }\n\n  /**\n  * Public: Returns true if the thread has a {Category} with the given\n  * name. Note, only catgories of type `Category.Types.Standard` have valid\n  * `names`\n  * - `id` A {String} {Category} name\n  */\n  categoryNamed(name) {\n    return _.findWhere(this.categories, {name})\n  }\n\n  sortedCategories() {\n    if (!this.categories) {\n      return []\n    }\n    let out = []\n    const isImportant = (l) => l.name === 'important'\n    const isStandardCategory = (l) => l.isStandardCategory()\n    const isUnhiddenStandardLabel = (l) => (\n      !isImportant(l) &&\n      isStandardCategory(l) &&\n      !(l.isHiddenCategory())\n    )\n\n    const importantLabel = _.find(this.categories, isImportant)\n    if (importantLabel) {\n      out = out.concat(importantLabel)\n    }\n\n    const standardLabels = _.filter(this.categories, isUnhiddenStandardLabel)\n    if (standardLabels.length > 0) {\n      out = out.concat(standardLabels)\n    }\n\n    const userLabels = _.filter(this.categories, (l) => (\n      !isImportant(l) && !isStandardCategory(l)\n    ))\n    if (userLabels.length > 0) {\n      out = out.concat(_.sortBy(userLabels, 'displayName'))\n    }\n    return out\n  }\n}\n\nObject.defineProperty(Thread.attributes, \"labels\", {\n  enumerable: false,\n  get: () => Thread.attributes.categories,\n})\n\nObject.defineProperty(Thread.attributes, \"folders\", {\n  enumerable: false,\n  get: () => Thread.attributes.categories,\n})\n\nexport default Thread;\n"
  },
  {
    "path": "packages/client-app/src/flux/models/unread-query-subscription.es6",
    "content": "import MutableQuerySubscription from './mutable-query-subscription';\nimport DatabaseStore from '../stores/database-store';\nimport RecentlyReadStore from '../stores/recently-read-store';\nimport Matcher from '../attributes/matcher';\nimport Thread from '../models/thread';\n\nconst buildQuery = (categoryIds) => {\n  const unreadMatchers = new Matcher.And([\n    Thread.attributes.categories.containsAny(categoryIds),\n    Thread.attributes.unread.equal(true),\n    Thread.attributes.inAllMail.equal(true),\n  ]);\n\n  const query = DatabaseStore.findAll(Thread).limit(0);\n\n  // The \"Unread\" view shows all threads which are unread. When you read a thread,\n  // it doesn't disappear until you leave the view and come back. This behavior\n  // is implemented by keeping track of messages being rea and manually\n  // whitelisting them in the query.\n  if (RecentlyReadStore.ids.length === 0) {\n    query.where(unreadMatchers);\n  } else {\n    query.where(new Matcher.Or([\n      unreadMatchers,\n      Thread.attributes.id.in(RecentlyReadStore.ids),\n    ]));\n  }\n\n  return query;\n}\n\nexport default class UnreadQuerySubscription extends MutableQuerySubscription {\n\n  constructor(categoryIds) {\n    super(buildQuery(categoryIds), {emitResultSet: true})\n    this._categoryIds = categoryIds;\n    this._unlisten = RecentlyReadStore.listen(this.onRecentlyReadChanged);\n  }\n\n  onRecentlyReadChanged = () => {\n    const {limit, offset} = this._query.range()\n    this._query = buildQuery(this._categoryIds).limit(limit).offset(offset);\n  }\n\n  onLastCallbackRemoved() {\n    this._unlisten();\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/models/utils.coffee",
    "content": "_ = require 'underscore'\nfs = require('fs-plus')\npath = require('path')\nmoment = require('moment-timezone')\n\nDefaultResourcePath = null\nTaskRegistry = require('../../registries/task-registry').default\nDatabaseObjectRegistry = require('../../registries/database-object-registry').default\n\nimageData = null\n\nmodule.exports =\nUtils =\n  waitFor: (latch, options = {}) ->\n    timeout = options.timeout || 400\n    expire = Date.now() + timeout\n    return new Promise (resolve, reject) ->\n      attempt = ->\n        if Date.now() > expire\n          return reject(new Error(\"Utils.waitFor hit timeout (#{timeout}ms) without firing.\"))\n        if latch()\n          return resolve()\n        window.requestAnimationFrame(attempt)\n      attempt()\n\n  showIconForAttachments: (files) ->\n    return false unless files instanceof Array\n    return files.find (f) -> !f.contentId or f.size > 12 * 1024\n\n  extractTextFromHtml: (html, {maxLength} = {}) ->\n    if (html ? \"\").trim().length is 0 then return \"\"\n    if maxLength and html.length > maxLength\n      html = html.slice(0, maxLength)\n    (new DOMParser()).parseFromString(html, \"text/html\").body.innerText\n\n  registeredObjectReviver: (k,v) ->\n    type = v?.__constructorName\n    return v unless type\n\n    if DatabaseObjectRegistry.isInRegistry(type)\n      return DatabaseObjectRegistry.deserialize(type, v)\n\n    if TaskRegistry.isInRegistry(type)\n      return TaskRegistry.deserialize(type, v)\n\n    return v\n\n  registeredObjectReplacer: (k, v) ->\n    if _.isObject(v)\n      type = this[k].constructor.name\n      if DatabaseObjectRegistry.isInRegistry(type) or TaskRegistry.isInRegistry(type)\n        v.__constructorName = type\n    return v\n\n  fastOmit: (props, without) ->\n    otherProps = Object.assign({}, props)\n    delete otherProps[w] for w in without\n    otherProps\n\n  isHash: (object) ->\n    _.isObject(object) and not _.isFunction(object) and not _.isArray(object)\n\n  escapeRegExp: (str) ->\n    str.replace(/[\\-\\[\\]\\/\\{\\}\\(\\)\\*\\+\\?\\.\\\\\\^\\$\\|]/g, \"\\\\$&\")\n\n  range: (start, end, inclusive = true) ->\n    if inclusive\n      return [start..end]\n    return [start...end]\n\n  # Generates a new RegExp that is great for basic search fields. It\n  # checks if the test string is at the start of words\n  #\n  # See regex explanation and test here:\n  # https://regex101.com/r/zG7aW4/2\n  wordSearchRegExp: (str=\"\") ->\n    new RegExp(\"((?:^|\\\\W|$)#{Utils.escapeRegExp(str.trim())})\", \"ig\")\n\n  # Takes an optional customizer. The customizer is passed the key and the\n  # new cloned value for that key. The customizer is expected to either\n  # modify the value and return it or simply be the identity function.\n  deepClone: (object, customizer, stackSeen=[], stackRefs=[]) ->\n    return object unless _.isObject(object)\n    return object if _.isFunction(object)\n\n    if _.isArray(object)\n      # http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/\n      newObject = []\n    else if object instanceof Date\n      # You can't clone dates by iterating through `getOwnPropertyNames`\n      # of the Date object. We need to special-case Dates.\n      newObject = new Date(object)\n    else\n      newObject = Object.create(Object.getPrototypeOf(object))\n\n    # Circular reference check\n    seenIndex = stackSeen.indexOf(object)\n    if seenIndex >= 0 then return stackRefs[seenIndex]\n    stackSeen.push(object); stackRefs.push(newObject)\n\n    # It's important to use getOwnPropertyNames instead of Object.keys to\n    # get the non-enumerable items as well.\n    for key in Object.getOwnPropertyNames(object)\n      newVal = Utils.deepClone(object[key], customizer, stackSeen, stackRefs)\n      if _.isFunction(customizer)\n        newObject[key] = customizer(key, newVal)\n      else\n        newObject[key] = newVal\n    return newObject\n\n  toSet: (arr=[]) ->\n    set = {}\n    set[item] = true for item in arr\n    return set\n\n  # Given a File object or uploadData of an uploading file object,\n  # determine if it looks like an image and is in the size range for previews\n  shouldDisplayAsImage: (file={}) ->\n    name = file.filename ? file.fileName ? file.name ? \"\"\n    size = file.size ? file.fileSize ? 0\n    ext = path.extname(name).toLowerCase()\n    extensions = ['.jpg', '.bmp', '.gif', '.png', '.jpeg']\n\n    return ext in extensions and size > 512 and size < 1024*1024*5\n\n\n  # Escapes potentially dangerous html characters\n  # This code is lifted from Angular.js\n  # See their specs here:\n  # https://github.com/angular/angular.js/blob/master/test/ngSanitize/sanitizeSpec.js\n  # And the original source here: https://github.com/angular/angular.js/blob/master/src/ngSanitize/sanitize.js#L451\n  encodeHTMLEntities: (value) ->\n    SURROGATE_PAIR_REGEXP = /[\\uD800-\\uDBFF][\\uDC00-\\uDFFF]/g\n    pairFix = (value) ->\n      hi = value.charCodeAt(0)\n      low = value.charCodeAt(1)\n      return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';'\n\n    # Match everything outside of normal chars and \" (quote character)\n    NON_ALPHANUMERIC_REGEXP = /([^\\#-~| |!])/g\n    alphaFix = (value) -> '&#' + value.charCodeAt(0) + ';'\n\n    value.replace(/&/g, '&amp;').\n          replace(SURROGATE_PAIR_REGEXP, pairFix).\n          replace(NON_ALPHANUMERIC_REGEXP, alphaFix).\n          replace(/</g, '&lt;').\n          replace(/>/g, '&gt;')\n\n  modelFreeze: (o) ->\n    Object.freeze(o)\n    Object.getOwnPropertyNames(o).forEach (key) ->\n      val = o[key]\n      if typeof val is 'object' and val isnt null and not Object.isFrozen(val)\n        Utils.modelFreeze(val)\n\n  generateTempId: ->\n    s4 = ->\n      Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1)\n    'local-' + s4() + s4() + '-' + s4()\n\n  generateFakeServerId: ->\n    s5 = ->\n      Math.floor((1 + Math.random()) * 0x10000000).toString(36).substring(1)\n    return s5() + s5() + s5() + s5() + s5()\n\n  isTempId: (id) ->\n    return false unless id and _.isString(id)\n    id[0..5] is 'local-'\n\n  tableNameForJoin: (primaryKlass, secondaryKlass) ->\n    \"#{primaryKlass.name}#{secondaryKlass.name}\"\n\n  imageNamed: (fullname, resourcePath) ->\n    [name, ext] = fullname.split('.')\n\n    DefaultResourcePath ?= NylasEnv.getLoadSettings().resourcePath\n    resourcePath ?= DefaultResourcePath\n\n    if not imageData\n      imageData = NylasEnv.fileListCache().imageData ? \"{}\"\n      Utils.images = JSON.parse(imageData) ? {}\n\n    if not Utils?.images?[resourcePath]\n      Utils.images ?= {}\n      Utils.images[resourcePath] ?= {}\n      imagesPath = path.join(resourcePath, 'static', 'images')\n      files = fs.listTreeSync(imagesPath)\n      for file in files\n        # On Windows, we get paths like C:\\images\\compose.png, but\n        # Chromium doesn't accept the backward slashes. Convert to\n        # C:/images/compose.png\n        file = file.replace(/\\\\/g, '/')\n        basename = path.basename(file)\n        Utils.images[resourcePath][path.basename(file)] = file\n      NylasEnv.fileListCache().imageData = JSON.stringify(Utils.images)\n\n    plat = process.platform ? \"\"\n    ratio = window.devicePixelRatio ? 1\n\n    return Utils.images[resourcePath][\"#{name}-#{plat}@#{ratio}x.#{ext}\"] ?\n           Utils.images[resourcePath][\"#{name}@#{ratio}x.#{ext}\"] ?\n           Utils.images[resourcePath][\"#{name}-#{plat}.#{ext}\"] ?\n           Utils.images[resourcePath][\"#{name}.#{ext}\"] ?\n           Utils.images[resourcePath][\"#{name}-#{plat}@2x.#{ext}\"] ?\n           Utils.images[resourcePath][\"#{name}@2x.#{ext}\"] ?\n           Utils.images[resourcePath][\"#{name}-#{plat}@1x.#{ext}\"] ?\n           Utils.images[resourcePath][\"#{name}@1x.#{ext}\"]\n\n  subjectWithPrefix: (subject, prefix) ->\n    if subject.search(/fwd:/i) is 0\n      return subject.replace(/fwd:/i, prefix)\n    else if subject.search(/re:/i) is 0\n      return subject.replace(/re:/i, prefix)\n    else\n      return \"#{prefix} #{subject}\"\n\n  # True of all arguments have the same domains\n  emailsHaveSameDomain: (args...) ->\n    return false if args.length < 2\n    domains = args.map (email=\"\") ->\n      _.last(email.toLowerCase().trim().split(\"@\"))\n    toMatch = domains[0]\n    return _.every(domains, (domain) -> domain.length > 0 and toMatch is domain)\n\n  emailHasCommonDomain: (email=\"\") ->\n    domain = _.last(email.toLowerCase().trim().split(\"@\"))\n    return (Utils.commonDomains[domain] ? false)\n\n  # This looks for and removes plus-ing, it taks a VERY liberal approach\n  # to match an email address. We'd rather let false positives through.\n  toEquivalentEmailForm: (email) ->\n    # https://regex101.com/r/iS7kD5/3\n    [ignored, user, domain] = /^([^+]+).*@(.+)$/gi.exec(email) || [null, \"\", \"\"]\n    \"#{user}@#{domain}\".trim().toLowerCase()\n\n  emailIsEquivalent: (email1=\"\", email2=\"\") ->\n    email1 = email1.toLowerCase().trim()\n    email2 = email2.toLowerCase().trim()\n    return true if email1 is email2\n    email1 = Utils.toEquivalentEmailForm(email1)\n    email2 = Utils.toEquivalentEmailForm(email2)\n    return email1 is email2\n\n  rectVisibleInRect: (r1, r2) ->\n    return !(r2.left > r1.right ||  r2.right < r1.left ||  r2.top > r1.bottom || r2.bottom < r1.top)\n\n  isEqualReact: (a, b, options={}) ->\n    options.functionsAreEqual = true\n    options.ignoreKeys = (options.ignoreKeys ? []).push(\"clientId\")\n    Utils.isEqual(a, b, options)\n\n  # Customized version of Underscore 1.8.2's isEqual function\n  # You can pass the following options:\n  #   - functionsAreEqual: if true then all functions are equal\n  #   - keysToIgnore: an array of object keys to ignore checks on\n  #   - logWhenFalse: logs when isEqual returns false\n  isEqual: (a, b, options={}) ->\n    value = Utils._isEqual(a, b, [], [], options)\n    if options.logWhenFalse\n      if value is false then console.log \"isEqual is false\", a, b, options\n      return value\n    else\n    return value\n\n  _isEqual: (a, b, aStack, bStack, options={}) ->\n    # Identical objects are equal. `0 is -0`, but they aren't identical.\n    # See the [Harmony `egal`\n    # proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).\n    if (a is b) then return a isnt 0 or 1 / a is 1 / b\n    # A strict comparison is necessary because `null == undefined`.\n    if (a == null or b == null) then return a is b\n    # Unwrap any wrapped objects.\n    if (a?._wrapped?) then a = a._wrapped\n    if (b?._wrapped?) then b = b._wrapped\n\n    if options.functionsAreEqual\n      if _.isFunction(a) and _.isFunction(b) then return true\n\n    # Compare `[[Class]]` names.\n    className = toString.call(a)\n    if (className isnt toString.call(b)) then return false\n    switch (className)\n      # Strings, numbers, regular expressions, dates, and booleans are\n      # compared by value.\n      # RegExps are coerced to strings for comparison (Note: '' + /a/i is '/a/i')\n      when '[object RegExp]', '[object String]'\n        # Primitives and their corresponding object wrappers are equivalent;\n        # thus, `\"5\"` is equivalent to `new String(\"5\")`.\n        return '' + a is '' + b\n      when '[object Number]'\n        # `NaN`s are equivalent, but non-reflexive.\n        # Object(NaN) is equivalent to NaN\n        if (+a isnt +a) then return +b isnt +b\n        # An `egal` comparison is performed for other numeric values.\n        return if +a is 0 then 1 / +a is 1 / b else +a is +b\n      when '[object Date]', '[object Boolean]'\n        # Coerce dates and booleans to numeric primitive values. Dates are\n        # compared by their millisecond representations. Note that invalid\n        # dates with millisecond representations of `NaN` are not\n        # equivalent.\n        return +a is +b\n\n    areArrays = className is '[object Array]'\n    if (!areArrays)\n      if (typeof a != 'object' or typeof b != 'object') then return false\n\n      # Objects with different constructors are not equivalent, but\n      # `Object`s or `Array`s from different frames are.\n      aCtor = a.constructor\n      bCtor = b.constructor\n      if (aCtor isnt bCtor && !(_.isFunction(aCtor) && aCtor instanceof aCtor &&\n                               _.isFunction(bCtor) && bCtor instanceof bCtor) && ('constructor' of a && 'constructor' of b))\n        return false\n    # Assume equality for cyclic structures. The algorithm for detecting cyclic\n    # structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.\n\n    # Initializing stack of traversed objects.\n    # It's done here since we only need them for objects and arrays comparison.\n    aStack = aStack ? []\n    bStack = bStack ? []\n    length = aStack.length\n    while length--\n      # Linear search. Performance is inversely proportional to the number of\n      # unique nested structures.\n      if (aStack[length] is a) then return bStack[length] is b\n\n    # Add the first object to the stack of traversed objects.\n    aStack.push(a)\n    bStack.push(b)\n\n    # Recursively compare objects and arrays.\n    if (areArrays)\n      # Compare array lengths to determine if a deep comparison is necessary.\n      length = a.length\n      if (length isnt b.length) then return false\n        # Deep compare the contents, ignoring non-numeric properties.\n      while (length--)\n        if (!Utils._isEqual(a[length], b[length], aStack, bStack, options)) then return false\n    else\n      # Deep compare objects.\n      key = undefined\n      keys = Object.keys(a)\n      length = keys.length\n      # Ensure that both objects contain the same number of properties\n      # before comparing deep equality.\n      if (Object.keys(b).length isnt length) then return false\n      keysToIgnore = {}\n      if options.ignoreKeys and _.isArray(options.ignoreKeys)\n        keysToIgnore[key] = true for key in options.ignoreKeys\n      while length--\n        # Deep compare each member\n        key = keys[length]\n        if key of keysToIgnore then continue\n        if (!(_.has(b, key) && Utils._isEqual(a[key], b[key], aStack, bStack, options)))\n          return false\n    # Remove the first object from the stack of traversed objects.\n    aStack.pop()\n    bStack.pop()\n    return true\n\n  # https://github.com/mailcheck/mailcheck/wiki/list-of-popular-domains\n  # As a hash for instant lookup.\n  commonDomains:\n    \"aol.com\": true\n    \"att.net\": true\n    \"comcast.net\": true\n    \"facebook.com\": true\n    \"gmail.com\": true\n    \"gmx.com\": true\n    \"googlemail.com\": true\n    \"google.com\": true\n    \"hotmail.com\": true\n    \"hotmail.co.uk\": true\n    \"mac.com\": true\n    \"me.com\": true\n    \"mail.com\": true\n    \"msn.com\": true\n    \"live.com\": true\n    \"sbcglobal.net\": true\n    \"verizon.net\": true\n    \"yahoo.com\": true\n    \"yahoo.co.uk\": true\n    \"email.com\": true\n    \"games.com\": true\n    \"gmx.net\": true\n    \"hush.com\": true\n    \"hushmail.com\": true\n    \"inbox.com\": true\n    \"lavabit.com\": true\n    \"love.com\": true\n    \"pobox.com\": true\n    \"rocketmail.com\": true\n    \"safe-mail.net\": true\n    \"wow.com\": true\n    \"ygm.com\": true\n    \"ymail.com\": true\n    \"zoho.com\": true\n    \"fastmail.fm\": true\n    \"bellsouth.net\": true\n    \"charter.net\": true\n    \"cox.net\": true\n    \"earthlink.net\": true\n    \"juno.com\": true\n    \"btinternet.com\": true\n    \"virginmedia.com\": true\n    \"blueyonder.co.uk\": true\n    \"freeserve.co.uk\": true\n    \"live.co.uk\": true\n    \"ntlworld.com\": true\n    \"o2.co.uk\": true\n    \"orange.net\": true\n    \"sky.com\": true\n    \"talktalk.co.uk\": true\n    \"tiscali.co.uk\": true\n    \"virgin.net\": true\n    \"wanadoo.co.uk\": true\n    \"bt.com\": true\n    \"sina.com\": true\n    \"qq.com\": true\n    \"naver.com\": true\n    \"hanmail.net\": true\n    \"daum.net\": true\n    \"nate.com\": true\n    \"yahoo.co.jp\": true\n    \"yahoo.co.kr\": true\n    \"yahoo.co.id\": true\n    \"yahoo.co.in\": true\n    \"yahoo.com.sg\": true\n    \"yahoo.com.ph\": true\n    \"hotmail.fr\": true\n    \"live.fr\": true\n    \"laposte.net\": true\n    \"yahoo.fr\": true\n    \"wanadoo.fr\": true\n    \"orange.fr\": true\n    \"gmx.fr\": true\n    \"sfr.fr\": true\n    \"neuf.fr\": true\n    \"free.fr\": true\n    \"gmx.de\": true\n    \"hotmail.de\": true\n    \"live.de\": true\n    \"online.de\": true\n    \"t-online.de\": true\n    \"web.de\": true\n    \"yahoo.de\": true\n    \"mail.ru\": true\n    \"rambler.ru\": true\n    \"yandex.ru\": true\n    \"hotmail.be\": true\n    \"live.be\": true\n    \"skynet.be\": true\n    \"voo.be\": true\n    \"tvcablenet.be\": true\n    \"hotmail.com.ar\": true\n    \"live.com.ar\": true\n    \"yahoo.com.ar\": true\n    \"fibertel.com.ar\": true\n    \"speedy.com.ar\": true\n    \"arnet.com.ar\": true\n    \"hotmail.com\": true\n    \"gmail.com\": true\n    \"yahoo.com.mx\": true\n    \"live.com.mx\": true\n    \"yahoo.com\": true\n    \"hotmail.es\": true\n    \"live.com\": true\n    \"hotmail.com.mx\": true\n    \"prodigy.net.mx\": true\n    \"msn.com\": true\n\n  # This method ensures that the provided function `fn` is only executing\n  # once at any given time. `fn` should have the following signature:\n  #\n  # (finished, reinvoked, arg1, arg2, ...)\n  #\n  # During execution, the function can call reinvoked() to see if\n  # it has been called again since it was invoked. When it stops\n  # or finishes execution, it should call finished()\n  #\n  # If the wrapped function is called again while `fn` is still executing,\n  # another invocation of the function is queued up. The paramMerge\n  # function allows you to control the params that are passed to\n  # the next invocation.\n  #\n  # For example,\n  #\n  # fetchFromCache({shallow: true})\n  #\n  # fetchFromCache({shallow: true})\n  #  -- will be executed once the initial call finishes\n  #\n  # fetchFromCache({})\n  #  -- `paramMerge` is called with `[{}]` and `[{shallow:true}]`. At this\n  #     point it should return `[{}]` since calling fetchFromCache with no\n  #     options is a more significant refresh.\n  #\n  ensureSerialExecution: (fn, paramMerge) ->\n    fnRun = null\n    fnReinvoked = ->\n      fn.next\n    fnFinished = ->\n      fn.executing = false\n      if fn.next\n        args = fn.next\n        fn.next = null\n        fnRun(args...)\n    fnRun = ->\n      if fn.executing\n        if fn.next\n          fn.next = paramMerge(fn.next, arguments)\n        else\n          fn.next = arguments\n      else\n        fn.executing = true\n        fn.apply(@, [fnFinished, fnReinvoked, arguments...])\n    fnRun\n\n\n  hueForString: (str='') ->\n    str.split('').map((c) -> c.charCodeAt()).reduce((n,a) -> n+a) % 360\n\n  # Emails that nave no-reply or similar phrases in them are likely not a\n  # human. As such it's not worth the cost to do a lookup on that person.\n  #\n  # Also emails that are really long are likely computer-generated email\n  # strings used for bcc-based automated teasks.\n  likelyNonHumanEmail: (email) ->\n    at = \"[-@+=]\"\n    prefixes = [\n      \"noreply\"\n      \"no-reply\"\n      \"donotreply\"\n      \"do-not-reply\"\n      \"bounce[s]?#{at}\"\n      \"notification[s]?#{at}\"\n      \"support#{at}\"\n      \"alert[s]?#{at}\"\n      \"news#{at}\"\n      \"info#{at}\"\n      \"automated#{at}\"\n      \"list[s]?#{at}\"\n      \"distribute[s]?#{at}\"\n      \"catchall#{at}\"\n      \"catch-all#{at}\"\n    ]\n    reStr = \"(#{prefixes.join(\"|\")})\"\n    re = new RegExp(reStr, \"gi\")\n    return re.test(email) or email.length > 64\n\n  # Does the several tests you need to determine if a test range is within\n  # a bounds. Expects both objects to have `start` and `end` keys.\n  # Compares any values with <= and >=.\n  overlapsBounds: (bounds, test) ->\n    # Fully enclosed\n    (test.start <= bounds.end and test.end >= bounds.start) or\n\n    # Starts in bounds. Ends out of bounds\n    (test.start <= bounds.end and test.start >= bounds.start) or\n\n    # Ends in bounds. Starts out of bounds\n    (test.end >= bounds.start and test.end <= bounds.end) or\n\n    # Spans entire boundary\n    (test.end >= bounds.end and test.start <= bounds.start)\n\n  mean: (values = []) ->\n    if values.length is 0 then throw new Error(\"Can't average zero values\")\n    sum = values.reduce(((sum, value) -> sum + value), 0)\n    return sum / values.length\n\n  stdev: (values = []) ->\n    if values.length is 0 then throw new Error(\"Can't stdev zero values\")\n    avg = Utils.mean(values)\n    squareDiffs = values.map((val) -> Math.pow((val - avg), 2))\n    return Math.sqrt(Utils.mean(squareDiffs))\n\n  # Resolves nested paths in objects of the form \"key.subKey.subKey\".\n  # Null checks along the way.\n  #\n  # If the result is a function, this will call it with no arguments.\n  #\n  # Also supports \"key1,key2.subkey,key3\". The commas lookup those paths\n  # in order and takes the first non-blank one.\n  #\n  # Also supports \"key1+key2\". This will attempt to concatenate the\n  # lookup.\n  #\n  # Order of operations is \",\" then \"+\", then \".\"\n  resolvePath: (fullPath=\"\", model) ->\n    commaPaths = fullPath.split(\",\")\n    for commaPath in commaPaths\n      joinedVals = []\n      paths = commaPath.split(\"+\")\n      for filePath in paths\n        parts = filePath.split(\".\")\n        curVal = model\n        for part in parts\n          if _.isFunction(curVal[part])\n            curVal = curVal[part]()\n          else\n            curVal = curVal[part]\n          if not curVal or curVal.length is 0\n            curVal = null\n            break\n        joinedVals.push(curVal)\n      joinedVals = _.compact(joinedVals)\n      continue if joinedVals.length is 0\n      return joinedVals.join(\" \")\n    return null\n"
  },
  {
    "path": "packages/client-app/src/flux/modules/reflux-coffee.coffee",
    "content": "_ = require('underscore')\n_str = require('underscore.string')\nEventEmitter = require('events').EventEmitter\n\ncallbackName = (string) ->\n  \"on\"+string.charAt(0).toUpperCase()+string.slice(1)\n\n###*\n# Extract child listenables from a parent from their\n# children property and return them in a keyed Object\n#\n# @param {Object} listenable The parent listenable\n###\n\nmapChildListenables = (listenable) ->\n  i = 0\n  children = {}\n  childName = undefined\n  while i < (listenable.children or []).length\n    childName = listenable.children[i]\n    if listenable[childName]\n      children[childName] = listenable[childName]\n    ++i\n  children\n\n###*\n# Make a flat dictionary of all listenables including their\n# possible children (recursively), concatenating names in camelCase.\n#\n# @param {Object} listenables The top-level listenables\n###\n\nflattenListenables = (listenables) ->\n  flattened = {}\n  for key of listenables\n    listenable = listenables[key]\n    childMap = mapChildListenables(listenable)\n    # recursively flatten children\n    children = flattenListenables(childMap)\n    # add the primary listenable and chilren\n    flattened[key] = listenable\n    for childKey of children\n      childListenable = children[childKey]\n      flattened[key + _str.capitalize(childKey)] = childListenable\n  flattened\n\n\nmodule.exports =\n\n  Listener:\n    hasListener: (listenable) ->\n      i = 0\n      j = undefined\n      listener = undefined\n      listenables = undefined\n      while i < (@subscriptions or []).length\n        listenables = [].concat(@subscriptions[i].listenable)\n        j = 0\n        while j < listenables.length\n          listener = listenables[j]\n          if listener == listenable or listener.hasListener and listener.hasListener(listenable)\n            return true\n          j++\n        ++i\n      false\n\n    listenToMany: (listenables) ->\n      allListenables = flattenListenables(listenables)\n      for key of allListenables\n        cbname = callbackName(key)\n        localname = if @[cbname] then cbname else if @[key] then key else undefined\n        if localname\n          @listenTo allListenables[key], localname, @[cbname + 'Default'] or @[localname + 'Default'] or localname\n      return\n\n    validateListening: (listenable) ->\n      if listenable == this\n        return 'Listener is not able to listen to itself'\n      if !_.isFunction(listenable.listen)\n        console.log require('util').inspect(listenable)\n        console.log((new Error()).stack)\n        return listenable + ' is missing a listen method'\n      if listenable.hasListener and listenable.hasListener(this)\n        return 'Listener cannot listen to this listenable because of circular loop'\n      return\n\n    listenTo: (listenable, callback, defaultCallback) ->\n      desub = undefined\n      unsubscriber = undefined\n      subscriptionobj = undefined\n      subs = @subscriptions = @subscriptions or []\n      err = @validateListening(listenable)\n      throw err if err\n      @fetchInitialState listenable, defaultCallback\n\n      resolvedCallback = @[callback] or callback\n      if not resolvedCallback\n        throw new Error(\"@listenTo called with undefined callback\")\n      desub = listenable.listen(resolvedCallback, this)\n\n      unsubscriber = ->\n        index = subs.indexOf(subscriptionobj)\n        if index == -1\n          throw new Error('Tried to remove listen already gone from subscriptions list!')\n        subs.splice index, 1\n        desub()\n        return\n\n      subscriptionobj =\n        stop: unsubscriber\n        listenable: listenable\n      subs.push subscriptionobj\n      subscriptionobj\n\n    stopListeningTo: (listenable) ->\n      sub = undefined\n      i = 0\n      subs = @subscriptions or []\n      while i < subs.length\n        sub = subs[i]\n        if sub.listenable == listenable\n          sub.stop()\n          if subs.indexOf(sub) != -1\n            throw new Error('Failed to remove listen from subscriptions list!')\n          return true\n        i++\n      false\n\n    stopListeningToAll: ->\n      remaining = undefined\n      subs = @subscriptions or []\n      while remaining = subs.length\n        subs[0].stop()\n        if subs.length != remaining - 1\n          throw new Error('Failed to remove listen from subscriptions list!')\n      return\n\n    fetchInitialState: (listenable, defaultCallback) ->\n      defaultCallback = defaultCallback and @[defaultCallback] or defaultCallback\n      me = this\n      if _.isFunction(defaultCallback) and _.isFunction(listenable.getInitialState)\n        data = listenable.getInitialState()\n        if data and _.isFunction(data.then)\n          data.then ->\n            defaultCallback.apply me, arguments\n            return\n        else\n          defaultCallback.call this, data\n      return\n\n\n  Publisher:\n    setupEmitter: ->\n      return if @_emitter\n      @_emitter ?= new EventEmitter()\n      @_emitter.setMaxListeners(50)\n\n    listen: (callback, bindContext) ->\n      if not callback\n        throw new Error(\"@listen called with undefined callback\")\n\n      @setupEmitter()\n      bindContext ?= @\n      aborted = false\n      eventHandler = (args) ->\n        return if aborted\n        callback.apply(bindContext, args)\n      @_emitter.addListener('trigger', eventHandler)\n      return =>\n        aborted = true\n        @_emitter.removeListener('trigger', eventHandler)\n\n    trigger: ->\n      @setupEmitter()\n      @_emitter.emit('trigger', arguments)\n"
  },
  {
    "path": "packages/client-app/src/flux/nylas-api-helpers.es6",
    "content": "/* eslint global-require: 0 */\nimport _ from 'underscore'\nimport NylasAPI from './nylas-api'\nimport NylasAPIRequest from './nylas-api-request'\nimport DatabaseStore from './stores/database-store'\nimport Message from './models/message'\nimport Thread from './models/thread'\n\n\nfunction attachMetadataToResponse(jsons, metadataToAttach) {\n  if (!metadataToAttach) return\n  for (const obj of jsons) {\n    if (metadataToAttach[obj.id]) {\n      obj.metadata = metadataToAttach[obj.id]\n    }\n  }\n}\n\nexport const apiObjectToClassMap = {\n  file: require('./models/file').default,\n  event: require('./models/event').default,\n  label: require('./models/label').default,\n  folder: require('./models/folder').default,\n  thread: require('./models/thread').default,\n  draft: require('./models/message').default,\n  account: require('./models/account').default,\n  message: require('./models/message').default,\n  contact: require('./models/contact').default,\n  calendar: require('./models/calendar').default,\n  syncbackRequest: require('./models/provider-syncback-request').default,\n}\n\n/*\n Returns a Promise that resolves when any parsed out models (if any)\n have been created and persisted to the database.\n*/\nexport async function handleModelResponse(jsons) {\n  if (!jsons) {\n    throw new Error(\"handleModelResponse with no JSON provided\")\n  }\n\n  let response = jsons\n  if (!(response instanceof Array)) {\n    response = [response]\n  }\n  if (response.length === 0) {\n    return []\n  }\n\n  const type = response[0].object\n  const Klass = apiObjectToClassMap[type]\n  if (!Klass) {\n    console.warn(`NylasAPI::handleModelResponse: Received unknown API object type: ${type}`)\n    return []\n  }\n\n  // Step 1: Make sure the list of objects contains no duplicates, which cause\n  // problems downstream when we try to write to the database.\n  const uniquedJSONs = _.uniq(response, false, (model) => { return model.id })\n  if (uniquedJSONs.length < response.length) {\n    console.warn(\"NylasAPI::handleModelResponse: called with non-unique object set. Maybe an API request returned the same object more than once?\")\n  }\n\n  // Step 2: Filter out any objects we've locked (usually because we successfully\n  // deleted them moments ago).\n  const unlockedJSONs = _.filter(uniquedJSONs, (json) => {\n    if (NylasAPI.lockTracker.acceptRemoteChangesTo(Klass, json.id) === false) {\n      if (json && json._delta) {\n        json._delta.ignoredBecause = \"Model is locked, possibly because it's already been deleted.\"\n      }\n      return false\n    }\n    return true\n  })\n\n  if (unlockedJSONs.length === 0) {\n    return []\n  }\n\n  // Step 3: Retrieve any existing models from the database for the given IDs.\n  let ids = []\n  const localIdToJSONId = {}\n  if (Klass === Thread) {\n    // Thread ids can be any of their message ids prefixed with \"t:\". To figure\n    // out if we already have an equivalent thread, we have to check all possible\n    // thread ids.\n    unlockedJSONs.forEach(json => {\n      json.message_ids.forEach(messageId => {\n        const possibleThreadId = `t:${messageId}`\n        ids.push(possibleThreadId)\n        localIdToJSONId[possibleThreadId] = json.id\n      })\n    })\n  } else {\n    ids = _.pluck(unlockedJSONs, 'id')\n  }\n\n  if (ids.length === 0) {\n    // This case will happen when the jsons are for threads, and all the threads\n    // are brand new. There should be deltas right after this with the initial\n    // message association.\n    return []\n  }\n  const models = await DatabaseStore.findAll(Klass).where(Klass.attributes.id.in(ids))\n  const existingModels = {}\n  for (const model of models) {\n    const jsonId = localIdToJSONId[model.id] || model.id;\n    existingModels[jsonId] = model\n  }\n\n  const responseModels = []\n  const changedModels = []\n  const unpersistModels = []\n\n  // Step 4: Merge the response data into the existing data for each model,\n  // skipping changes when we already have the given version\n  unlockedJSONs.forEach((json) => {\n    let model = existingModels[json.id]\n\n    const isSameOrNewerVersion = model && model.version && json.version && model.version >= json.version\n    const isAlreadySent = model && model.draft === false && json.draft === true\n\n    if (isSameOrNewerVersion) {\n      if (json && json._delta) {\n        json._delta.ignoredBecause = `JSON v${json.version} <= model v${model.version}`\n      }\n    } else if (isAlreadySent) {\n      if (json && json._delta) {\n        json._delta.ignoredBecause = `Model ${model.id} is already sent!`\n      }\n    } else {\n      if (model && model.id !== json.id) {\n        unpersistModels.push(model.clone())\n      }\n      model = model || new Klass()\n      model.fromJSON(json)\n      changedModels.push(model)\n    }\n    responseModels.push(model)\n  })\n\n  // Step 5: Save models that have changed, and then return all of the models\n  // that were in the response body.\n  await DatabaseStore.inTransaction(async (t) => {\n    await t.persistModels(changedModels)\n    await Promise.all(unpersistModels.map(model => t.unpersistModel(model)))\n  })\n  return responseModels\n}\n\n/*\nIf we make a request for a model and we get a 404, we want to handle it\nintelligently and in a centralized way. This method identifies the object\nthat could not be found and purges it from local cache.\n\nHandles: /account/<nid>/<collection>/<id>\n*/\nexport function handleModel404(modelUrl) {\n  const url = require('url')\n  const {pathname} = url.parse(modelUrl, true)\n  const components = pathname.split('/')\n\n  let collection = null\n  let klassId = null\n  let klass = null\n  if (components.length === 3) {\n    collection = components[1]\n    klassId = components[2]\n    klass = apiObjectToClassMap[collection.slice(0, -1)] // Warning: threads => thread\n  }\n\n  if (klass && klassId && klassId.length > 0) {\n    if (!NylasEnv.inSpecMode()) {\n      console.warn(`Deleting ${klass.name}:${klassId} due to API 404`)\n    }\n\n    return DatabaseStore.inTransaction((t) =>\n      t.find(klass, klassId).then((model) => {\n        if (model) {\n          return t.unpersistModel(model)\n        }\n        return Promise.resolve()\n      })\n    )\n  }\n  return Promise.resolve()\n}\n\nexport function makeDraftDeletionRequest(draft) {\n  if (!draft.serverId) return\n  NylasAPI.incrementRemoteChangeLock(Message, draft.serverId)\n  new NylasAPIRequest({\n    api: NylasAPI,\n    options: {\n      path: `/drafts/${draft.serverId}`,\n      accountId: draft.accountId,\n      method: \"DELETE\",\n      body: {version: draft.version},\n    },\n  }).run()\n  return\n}\n\nexport function getCollection(accountId, collection, params = {}, requestOptions = {}) {\n  if (!accountId) {\n    throw (new Error(\"getCollection requires accountId\"))\n  }\n  const req = new NylasAPIRequest({\n    api: NylasAPI,\n    options: Object.assign({}, requestOptions, {\n      path: `/${collection}`,\n      accountId: accountId,\n      qs: params,\n    }),\n  })\n  return req.run()\n  .then((jsons) => {\n    attachMetadataToResponse(jsons, requestOptions.metadataToAttach)\n    handleModelResponse(jsons)\n  })\n}\n\nexport function authPlugin() {\n  return Promise.resolve();\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/nylas-api-request.es6",
    "content": "import request from 'request'\n\nimport Utils from './models/utils'\nimport Actions from './actions'\nimport {APIError} from './errors'\nimport IdentityStore from './stores/identity-store'\n\n\nexport default class NylasAPIRequest {\n\n  constructor({api, options}) {\n    const defaults = {\n      url: `${options.APIRoot || api.APIRoot}${options.path}`,\n      method: 'GET',\n      json: true,\n      timeout: 30000,\n      started: () => {},\n    }\n\n    this.api = api;\n    this.options = Object.assign(defaults, options);\n    this.response = null\n\n    const bodyIsRequired = (this.options.method !== 'GET' && !this.options.formData);\n    if (bodyIsRequired) {\n      const fallback = this.options.json ? {} : '';\n      this.options.body = this.options.body || fallback;\n    }\n  }\n\n  async run() {\n    if (NylasEnv.getLoadSettings().isSpec) return Promise.resolve([]);\n    try {\n      this.options.auth = this.options.auth || this._defaultAuth();\n      return await this._asyncRequest(this.options)\n    } catch (error) {\n      let apiError = error\n      if (!(apiError instanceof APIError)) {\n        apiError = new APIError({error: apiError, statusCode: 500})\n      }\n      this._notifyOfAPIError(apiError)\n      throw apiError\n    }\n  }\n\n  /**\n   * An async wrapper around `request`. We reject on any non 2xx codes or\n   * other errors.\n   *\n   * Resolves to the JSON body or rejects with an APIError object.\n   */\n  async _asyncRequest(options = {}) {\n    return new Promise((resolve, reject) => {\n      const requestId = Utils.generateTempId();\n      const reqTrackingArgs = {request: options, requestId}\n\n      // Blob requests can potentially contain megabytes of binary data.\n      // it doesn't make sense to send them through the action bridge.\n      if (!options.blob) {\n        Actions.willMakeAPIRequest(reqTrackingArgs);\n      }\n\n      const req = request(options, (error, response, body) => {\n        this.response = response;\n        const statusCode = (response || {}).statusCode;\n\n        if (statusCode >= 200 && statusCode <= 299) {\n          if (!options.blob) {\n            Actions.didMakeAPIRequest({statusCode, ...reqTrackingArgs});\n          }\n          return resolve(body)\n        }\n\n        const apiError = new APIError({\n          body: body,\n          error: error,\n          response: response,\n          statusCode: statusCode,\n          requestOptions: options,\n        });\n        Actions.didMakeAPIRequest({...reqTrackingArgs, statusCode, error: apiError});\n        return reject(apiError)\n      });\n      req.on('abort', () => {\n        // Use a status code of 0 because we don't want to report the error when\n        // we manually abort the request\n        const statusCode = 0\n        const abortedError = new APIError({\n          statusCode,\n          body: 'Request aborted by client',\n        });\n        Actions.didMakeAPIRequest({...reqTrackingArgs, statusCode, error: abortedError});\n        reject(abortedError);\n      });\n\n      req.on('aborted', () => {\n        const statusCode = \"ECONNABORTED\"\n        const abortedError = new APIError({\n          statusCode,\n          body: 'Request aborted by server',\n        });\n        Actions.didMakeAPIRequest({...reqTrackingArgs, statusCode, error: abortedError});\n        reject(abortedError);\n      });\n      options.started(req);\n    })\n  }\n\n\n  async _notifyOfAPIError(apiError) {\n    const {statusCode} = apiError\n    // TODO move this check into NylasEnv.reportError()?\n    if (apiError.shouldReportError()) {\n      const msg = apiError.message || `Unknown Error: ${apiError}`\n      const fingerprint = [\"{{ default }}\", \"api error\", this.options.url, apiError.statusCode, msg];\n      NylasEnv.reportError(apiError, {fingerprint,\n        rateLimit: {\n          ratePerHour: 30,\n          key: `APIError:${this.options.url}:${statusCode}:${msg}`,\n        },\n      });\n      apiError.reported = true\n    }\n\n    if ([401, 403].includes(statusCode)) {\n      Actions.apiAuthError(apiError, this.options, this.api.constructor.name)\n    }\n  }\n\n  /**\n   * Generates the basic auth username from the account token and the\n   * basic auth password from the NylasID token.\n   *\n   * This asserts if any of these pieces are missing and throws an\n   * APIError object.\n   */\n  _defaultAuth() {\n    try {\n      if (!this.options.accountId) {\n        throw new Error(\"Cannot make Nylas request without specifying `auth` or an `accountId`.\");\n      }\n\n      const identity = IdentityStore.identity();\n      const accountToken = this.api.accessTokenForAccountId(this.options.accountId);\n      if (!accountToken) {\n        throw new Error(`Auth token missing for account`);\n      }\n\n      return {\n        user: accountToken,\n        pass: identity ? identity.token : 'noop',\n        sendImmediately: true,\n      };\n    } catch (error) {\n      throw new APIError({error, statusCode: 400});\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/nylas-api.es6",
    "content": "import {AccountStore} from 'nylas-exports'\nimport NylasLongConnection from './nylas-long-connection'\n\n// A 0 code is when an error returns without a status code, like \"ESOCKETTIMEDOUT\"\nconst TimeoutErrorCodes = [0, 408, \"ETIMEDOUT\", \"ESOCKETTIMEDOUT\", \"ECONNRESET\", \"ENETDOWN\", \"ENETUNREACH\"]\nconst PermanentErrorCodes = [400, 401, 402, 403, 404, 405, 429, 500, \"ENOTFOUND\", \"ECONNREFUSED\", \"EHOSTDOWN\", \"EHOSTUNREACH\"]\nconst CanceledErrorCodes = [-123, \"ECONNABORTED\"]\nconst SampleTemporaryErrorCode = 504\n\n\nclass NylasAPIChangeLockTracker {\n  constructor() {\n    this._locks = {}\n  }\n\n  acceptRemoteChangesTo(klass, id) {\n    const key = `${klass.name}-${id}`\n    return this._locks[key] === undefined\n  }\n\n  increment(klass, id) {\n    const key = `${klass.name}-${id}`\n    this._locks[key] = this._locks[key] || 0\n    this._locks[key] += 1\n  }\n\n  decrement(klass, id) {\n    const key = `${klass.name}-${id}`\n    if (!this._locks[key]) return\n    this._locks[key] -= 1\n    if (this._locks[key] <= 0) {\n      delete this._locks[key]\n    }\n  }\n\n  print() {\n    console.log(\"The following models are locked:\")\n    console.log(this._locks)\n  }\n}\n\n\nclass NylasAPI {\n\n  constructor() {\n    this.lockTracker = new NylasAPIChangeLockTracker()\n    let port = 2578;\n    if (NylasEnv.inDevMode()) port = 1337;\n    this.APIRoot = `http://localhost:${port}`\n\n    this.TimeoutErrorCodes = TimeoutErrorCodes\n    this.PermanentErrorCodes = PermanentErrorCodes\n    this.CanceledErrorCodes = CanceledErrorCodes\n    this.SampleTemporaryErrorCode = SampleTemporaryErrorCode\n    this.LongConnectionStatus = NylasLongConnection.Status\n  }\n\n  accessTokenForAccountId(aid) {\n    return AccountStore.tokensForAccountId(aid).localSync\n  }\n\n  incrementRemoteChangeLock = (klass, id) => {\n    this.lockTracker.increment(klass, id)\n  }\n\n  decrementRemoteChangeLock = (klass, id) => {\n    this.lockTracker.decrement(klass, id)\n  }\n}\n\nexport default new NylasAPI()\n"
  },
  {
    "path": "packages/client-app/src/flux/nylas-long-connection.es6",
    "content": "/* eslint global-require: 0 */\nimport _ from 'underscore'\nimport url from 'url'\nimport {Emitter} from 'event-kit'\nimport {IdentityStore, APIError} from 'nylas-exports'\n\n\nconst CONNECTION_TIMEOUT = 60 * 60 * 1000\nconst PROCESS_BUFFER_THROTTLE = 400\nconst Status = {\n  None: 'none',\n  Connecting: 'connecting',\n  Connected: 'connected',\n  Closed: 'closed', // Socket has been closed for any reason\n  Ended: 'ended', // We have received 'end()' and will never open again.\n}\n\nclass NylasLongConnection {\n  static Status = Status\n\n  constructor({api, accountId, ...opts} = {}) {\n    const {\n      path,\n      timeout,\n      onError,\n      onResults,\n      onStatusChanged,\n      throttleResultsInterval,\n      closeIfDataStopsInterval,\n    } = opts\n\n    this._api = api\n    this._accountId = accountId\n    this._status = Status.None\n    this._emitter = new Emitter()\n    this._req = null\n    this._buffer = ''\n    this._results = []\n    this._pingTimeout = null\n    this._httpStatusCode = null\n\n    // Options\n    this._path = path\n    this._timeout = timeout || CONNECTION_TIMEOUT\n    this._onError = onError || (() => {})\n    this._onResults = onResults || (() => {})\n    this._onStatusChanged = onStatusChanged || (() => {})\n    this._closeIfDataStopsInterval = closeIfDataStopsInterval\n\n    this._emitter.on('results-stopped-arriving', this._onResults)\n    this._processBufferThrottled = _.throttle(this._processBuffer, PROCESS_BUFFER_THROTTLE, {leading: false})\n    this._flushResultsSoon = () => {\n      if (this._results.length === 0) { return }\n      this._emitter.emit('results-stopped-arriving', this._results);\n      this._results = []\n    }\n    if (throttleResultsInterval != null) {\n      this._flushResultsSoon = _.throttle(this._flushResultsSoon, throttleResultsInterval)\n    }\n  }\n\n  _processBuffer() {\n    const bufferJSONs = this._buffer.split('\\n')\n\n    // We can't parse the last block - we don't know whether we've\n    // received the entire result or only part of it. Wait\n    // until we have more.\n    this._buffer = bufferJSONs.pop()\n\n    bufferJSONs.forEach((resultJSON) => {\n      if (resultJSON.length === 0) { return }\n      let result = null\n      try {\n        result = JSON.parse(resultJSON)\n      } catch (e) {\n        console.warn(`${resultJSON} could not be parsed as JSON.`, e)\n      }\n      if (result) {\n        this._results.push(result)\n      }\n    })\n    this._flushResultsSoon()\n  }\n\n  get accountId() {\n    return this._accountId;\n  }\n\n  get status() {\n    return this._status;\n  }\n\n  setStatus(status) {\n    if (this._status === status) { return }\n    this._status = status\n    this._onStatusChanged(this._status, this._httpStatusCode)\n  }\n\n  onError(error) {\n    this._onError(error)\n    this.close()\n  }\n\n  canStart() {\n    return [Status.None, Status.Closed].includes(this._status)\n  }\n\n  start() {\n    if (!this.canStart()) { return this }\n    if (this._req != null) { return this }\n\n    try {\n      const accountToken = this._api.accessTokenForAccountId(this._accountId)\n      const identityToken = (IdentityStore.identity() || {}).token || ''\n      if (!accountToken) {\n        throw new APIError({\n          statusCode: 401,\n          message: `Can't establish NylasLongConnection: No account token available for account ${this._accountId}`,\n        })\n      }\n\n      const options = url.parse(`${this._api.APIRoot}${this._path}`)\n      options.auth = `${accountToken}:${identityToken}`\n\n      let lib;\n      if (this._api.APIRoot.indexOf('https') === -1) {\n        lib = require('http')\n      } else {\n        lib = require('https')\n      }\n\n      this._req = lib.request(options, (responseStream) => {\n        this._req.responseStream = responseStream\n        this._httpStatusCode = responseStream.statusCode\n        if (responseStream.statusCode !== 200) {\n          responseStream.on('data', (chunk) => {\n            const error = new APIError({\n              response: responseStream,\n              message: chunk.toString('utf8'),\n              statusCode: responseStream.statusCode,\n            })\n            this.onError(error)\n          })\n          return\n        }\n\n        responseStream.setEncoding('utf8')\n        responseStream.on('error', (error) => {\n          this.onError(new APIError({error}))\n        })\n        responseStream.on('close', () => this.close())\n        responseStream.on('end', () => this.close())\n        responseStream.on('data', (chunk) => {\n          this.closeIfDataStops()\n          // Ignore redundant newlines sent as pings. Want to avoid\n          // calls to this.onProcessBuffer that contain no actual updates\n          if (chunk === '\\n' && (this._buffer.length === 0 || _.last(this._buffer) === '\\n')) {\n            return\n          }\n          this._buffer += chunk\n          this._processBufferThrottled()\n        })\n      })\n      this._req.setTimeout(60 * 60 * 1000)\n      this._req.setSocketKeepAlive(true)\n      this._req.on('error', (error) => {\n        this.onError(new APIError({error}))\n      })\n      this._req.on('socket', (socket) => {\n        this.setStatus(Status.Connecting)\n        socket.on('connect', () => {\n          this.setStatus(Status.Connected)\n          this.closeIfDataStops()\n        })\n        socket.on('error', (error) => {\n          this.onError(new APIError({error}))\n        })\n        socket.on('close', () => this.close())\n        socket.on('end', () => this.close())\n      })\n      // We `end` the request to start it.\n      // See https://github.com/nylas/nylas-mail/pull/2004\n      this._req.end()\n      return this\n    } catch (err) {\n      // start() should not throw any errors synchronously. Any errors should be\n      // asynchronously transmitted to the caller via `onError`\n      setTimeout(() => this.onError(err), 0)\n      return this\n    }\n  }\n\n  closeIfDataStops() {\n    if (this._closeIfDataStopsInterval != null) {\n      clearTimeout(this._pingTimeout)\n      this._pingTimeout = setTimeout(() => {\n        this._pingTimeout = null\n        this.close()\n      }, this._closeIfDataStopsInterval)\n    }\n  }\n\n  dispose(status) {\n    // Process the buffer one last time before disposing of the connection\n    // in case there is any data left that we haven't processed\n    this._processBuffer()\n    if (this._status !== status) {\n      this.setStatus(status)\n    }\n    clearTimeout(this._pingTimeout)\n    this._pingTimeout = null\n    this._httpStatusCode = null\n    this._buffer = ''\n    if (this._req) {\n      this._req.end()\n      this._req.abort()\n      this._req.removeAllListeners()\n      if (this._req.responseStream) {\n        this._req.responseStream.removeAllListeners()\n      }\n      if (this._req.socket) {\n        this._req.socket.removeAllListeners()\n      }\n\n      // Keep an error handler to prevent from logging and reporting uncaught\n      // errors that may occur after aborting this current request.\n      // For example, if we manually close this connection before any data has\n      // been received (frequently happens when searching threads), this will\n      // throw an uncaught socket hang up error that will get unnecessarily\n      // reported to sentry\n      this._req.on('error', () => {})\n      this._req = null\n    }\n    return this\n  }\n\n  close() {\n    return this.dispose(Status.Closed)\n  }\n\n  end() {\n    return this.dispose(Status.Ended)\n  }\n}\n\nexport default NylasLongConnection\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/account-store.es6",
    "content": "/* eslint global-require: 0 */\n\nimport _ from 'underscore'\n\nimport NylasStore from 'nylas-store'\nimport KeyManager from '../../key-manager'\nimport Actions from '../actions'\nimport Account from '../models/account'\nimport Utils from '../models/utils'\nimport DatabaseStore from './database-store'\n\nconst configAccountsKey = \"nylas.accounts\"\nconst configVersionKey = \"nylas.accountsVersion\"\n\n\n/*\nPublic: The AccountStore listens to changes to the available accounts in\nthe database and exposes the currently active Account via {::current}\n\nSection: Stores\n*/\nclass AccountStore extends NylasStore {\n\n  constructor(props) {\n    super(props)\n    this._loadAccounts()\n    this.listenTo(Actions.removeAccount, this._onRemoveAccount)\n    this.listenTo(Actions.updateAccount, this._onUpdateAccount)\n    this.listenTo(Actions.reorderAccount, this._onReorderAccount)\n    this.listenTo(Actions.apiAuthError, this._onAPIAuthError)\n\n    NylasEnv.config.onDidChange(configVersionKey, async (change) => {\n      // If we already have this version of the accounts config, it means we\n      // are the ones who saved the change, and we don't need to reload.\n      if (this._version / 1 === change.newValue / 1) {\n        return;\n      }\n\n      const oldAccountIds = _.pluck(this._accounts, 'id')\n      this._loadAccounts()\n      const accountIds = _.pluck(this._accounts, 'id')\n      const newAccountIds = _.difference(accountIds, oldAccountIds)\n\n      if (NylasEnv.isMainWindow() && newAccountIds.length > 0) {\n        const newId = newAccountIds[0]\n        Actions.focusDefaultMailboxPerspectiveForAccounts([newId], {sidebarAccountIds: accountIds})\n        const FolderSyncProgressStore = require('./folder-sync-progress-store').default\n        await FolderSyncProgressStore.whenCategoryListSynced(newId)\n        Actions.focusDefaultMailboxPerspectiveForAccounts([newId], {sidebarAccountIds: accountIds})\n        // TODO:\n        // This Action is a hack, get rid of it in sidebar refactor\n        // Wait until the FocusedPerspectiveStore triggers and the sidebar is\n        // updated to uncollapse the inbox for the new account\n        Actions.setCollapsedSidebarItem('Inbox', false)\n      }\n    })\n  }\n\n  isMyEmail(emailOrEmails = []) {\n    const myEmails = this.emailAddresses()\n    let emails = emailOrEmails;\n    if (typeof emails === 'string') {\n      emails = [emailOrEmails];\n    }\n    for (const email of emails) {\n      if (_.any(myEmails, (myEmail) => Utils.emailIsEquivalent(myEmail, email))) {\n        return true;\n      }\n    }\n    return false\n  }\n\n  _onAPIAuthError = (apiError, apiOptions, apiName) => {\n    // Prevent /auth errors from presenting auth failure notices\n    const apiToken = apiOptions.auth.user\n    if (!apiToken) {\n      return Promise.resolve()\n    }\n\n    const account = this.accounts().find((acc) => {\n      const tokens = this.tokensForAccountId(acc.id);\n      if (!tokens) return false\n      const localMatch = tokens.localSync === apiToken;\n      const cloudMatch = tokens.n1Cloud === apiToken;\n      return localMatch || cloudMatch;\n    })\n\n    if (account) {\n      if (apiName === \"N1CloudAPI\") {\n        const n1CloudState = Account.N1_CLOUD_STATE_AUTH_FAILED\n        this._onUpdateAccount(account.id, {n1CloudState})\n      } else {\n        const syncState = Account.SYNC_STATE_AUTH_FAILED\n        this._onUpdateAccount(account.id, {syncState})\n      }\n    }\n    return Promise.resolve()\n  }\n\n  _loadAccounts = () => {\n    try {\n      this._caches = {}\n      this._tokens = this._tokens || {};\n      this._version = NylasEnv.config.get(configVersionKey) || 0\n\n      const oldAccountIds = _.pluck(this._accounts, 'id')\n      this._accounts = []\n      for (const json of NylasEnv.config.get(configAccountsKey) || []) {\n        this._accounts.push((new Account()).fromJSON(json))\n      }\n      const accountIds = _.pluck(this._accounts, 'id')\n\n      // Loading passwords from the KeyManager is expensive so only do it if\n      // we really have to (i.e. we're loading a new Account)\n      const addedAccountIds = _.difference(accountIds, oldAccountIds);\n      const addedAccounts = _.filter(this._accounts, (a) => addedAccountIds.includes(a.id));\n      const removedAccountIds = _.difference(oldAccountIds, accountIds);\n      const removedAccounts = _.filter(this._accounts, (a) => removedAccountIds.includes(a.id));\n\n      // Run a few checks on account consistency. We want to display useful error\n      // messages and these can result in very strange exceptions downstream otherwise.\n      this._enforceAccountsValidity()\n\n      for (const account of addedAccounts) {\n        const credentials = {\n          n1Cloud: KeyManager.getPassword(`${account.emailAddress}.n1Cloud`, {migrateFromService: \"Nylas\"}),\n          localSync: KeyManager.getPassword(`${account.emailAddress}.localSync`, {migrateFromService: \"Nylas\"}),\n        }\n        this._tokens[account.id] = credentials;\n\n        // TODO HACK. For some reason we're getting passed the wrong\n        // id. Figure this out after launch.\n        this._tokens[account.emailAddress] = credentials;\n      }\n      for (const removedAccount of removedAccounts) {\n        const {id, emailAddress} = removedAccount\n        if (this._tokens[id]) {\n          delete this._tokens[id]\n        }\n        if (this._tokens[emailAddress]) {\n          delete this._tokens[emailAddress]\n        }\n      }\n    } catch (error) {\n      NylasEnv.reportError(error)\n    }\n\n    this._trigger()\n  }\n\n  _enforceAccountsValidity = () => {\n    const seenIds = {}\n    const seenEmails = {}\n    let message = null\n\n    this._accounts = this._accounts.filter((account) => {\n      if (!account.emailAddress) {\n        message = \"Assertion failure: One of the accounts in config.json did not have an emailAddress, and was removed. You should re-link the account.\"\n        return false\n      }\n      if (seenIds[account.id]) {\n        message = \"Assertion failure: Two of the accounts in config.json had the same ID and one was removed. Please give each account a separate ID.\"\n        return false\n      }\n      if (seenEmails[account.emailAddress]) {\n        message = \"Assertion failure: Two of the accounts in config.json had the same email address and one was removed.\"\n        return false\n      }\n\n      seenIds[account.id] = true\n      seenEmails[account.emailAddress] = true\n      return true\n    })\n\n    if (message && NylasEnv.isMainWindow()) {\n      NylasEnv.showErrorDialog(`N1 was unable to load your account preferences.\\n\\n${message}`)\n    }\n  }\n\n  _trigger() {\n    for (const account of this._accounts) {\n      if (!account || !account.id) {\n        const err = new Error(\"An invalid account was added to `this._accounts`\")\n        NylasEnv.reportError(err)\n        this._accounts = _.compact(this._accounts)\n      }\n    }\n    this.trigger()\n  }\n\n  _save = () => {\n    this._version += 1\n    const configAccounts = this._accounts.map(a => a.toJSON())\n    configAccounts.forEach(a => delete a.sync_error)\n    NylasEnv.config.set(configAccountsKey, configAccounts)\n    NylasEnv.config.set(configVersionKey, this._version)\n    this._trigger()\n  }\n\n  /**\n   * Actions.updateAccount is called directly from the local-sync worker.\n   * This will update the account with its updated sync state\n   */\n  _onUpdateAccount = (id, updated) => {\n    const idx = _.findIndex(this._accounts, (a) => a.id === id)\n    let account = this._accounts[idx]\n    if (!account) return\n    account = _.extend(account, updated)\n    this._caches = {}\n    this._accounts[idx] = account\n    this._save()\n  }\n\n  /**\n   * When an account is removed from Nylas Mail, the AccountStore\n   * triggers. The local-sync/src/local-sync-worker/index.js listens to\n   * the AccountStore and runs `ensureK2Consistency`. This will actually\n   * delete the Account on the local sync side.\n   */\n  _onRemoveAccount = (id) => {\n    const account = _.findWhere(this._accounts, {id})\n    if (!account) return\n    KeyManager.deletePassword(account.emailAddress)\n\n    this._caches = {}\n\n    const remainingAccounts = _.without(this._accounts, account)\n    // This action is called before saving because we need to unfocus the\n    // perspective of the account that is being removed before removing the\n    // account, otherwise when we trigger with the new set of accounts, the\n    // current perspective will still reference a stale accountId which will\n    // cause things to break\n    Actions.focusDefaultMailboxPerspectiveForAccounts(remainingAccounts)\n    _.defer(() => {\n      Actions.setCollapsedSidebarItem('Inbox', true)\n    })\n\n    this._accounts = remainingAccounts\n    this._save()\n\n    if (remainingAccounts.length === 0) {\n      const ipc = require('electron').ipcRenderer\n      ipc.send('command', 'application:relaunch-to-initial-windows', {\n        resetDatabase: true,\n      })\n    }\n  }\n\n  _onReorderAccount = (id, newIdx) => {\n    const existingIdx = _.findIndex(this._accounts, (a) => a.id === id)\n    if (existingIdx === -1) return\n    const account = this._accounts[existingIdx]\n    this._caches = {}\n    this._accounts.splice(existingIdx, 1)\n    this._accounts.splice(newIdx, 0, account)\n    this._save()\n  }\n\n  addAccountFromJSON = (json, localToken, cloudToken) => {\n    if (!json.email_address || !json.provider) {\n      console.error(\"Returned account data is invalid\", json)\n      console.log(JSON.stringify(json))\n      throw new Error(\"Returned account data is invalid\")\n    }\n\n    this._loadAccounts()\n\n    this._tokens[json.id] = {\n      n1Cloud: cloudToken,\n      localSync: localToken,\n    }\n    KeyManager.replacePassword(`${json.email_address}.n1Cloud`, cloudToken)\n    KeyManager.replacePassword(`${json.email_address}.localSync`, localToken)\n\n    const existingIdx = _.findIndex(this._accounts, (a) =>\n      a.id === json.id || a.emailAddress === json.email_address\n    )\n\n    if (existingIdx === -1) {\n      const account = (new Account()).fromJSON(json)\n      this._accounts.push(account)\n    } else {\n      const account = this._accounts[existingIdx]\n      account.syncState = Account.SYNC_STATE_RUNNING\n      account.fromJSON(json)\n      // Restart the connection in case account credentials have changed\n      Actions.retryDeltaConnection()\n    }\n\n    this._save()\n  }\n\n  _cachedGetter(key, fn) {\n    this._caches[key] = this._caches[key] || fn()\n    return this._caches[key]\n  }\n\n  // Public: Returns an {Array} of {Account} objects\n  accounts = () => {\n    return this._accounts\n  }\n\n  accountIds = () => {\n    return _.pluck(this._accounts, 'id')\n  }\n\n  accountsForItems = (items) => {\n    const accounts = {}\n    items.forEach(({accountId}) => {\n      accounts[accountId] = accounts[accountId] || this.accountForId(accountId)\n    })\n    return _.compact(_.values(accounts))\n  }\n\n  accountForItems = (items) => {\n    const accounts = this.accountsForItems(items)\n    if (accounts.length > 1) return null\n    return accounts[0]\n  }\n\n  // Public: Returns the {Account} for the given email address, or null.\n  accountForEmail = (email) => {\n    for (const account of this.accounts()) {\n      if (Utils.emailIsEquivalent(email, account.emailAddress)) {\n        return account\n      }\n    }\n    for (const alias of this.aliases()) {\n      if (Utils.emailIsEquivalent(email, alias.email)) {\n        return this.accountForId(alias.accountId)\n      }\n    }\n    return null\n  }\n\n  // Public: Returns the {Account} for the given account id, or null.\n  accountForId(id) {\n    return this._cachedGetter(`accountForId:${id}`, () => _.findWhere(this._accounts, {id}))\n  }\n\n  accountIsSyncing(accountId) {\n    const account = this.accountForId(accountId)\n    return !account.hasSyncStateError()\n  }\n\n  accountsAreSyncing() {\n    return this.accounts().every(acc => !acc.hasSyncStateError())\n  }\n\n  emailAddresses() {\n    let addresses = _.pluck((this.accounts() ? this.accounts() : []), \"emailAddress\")\n    addresses = addresses.concat(_.pluck((this.aliases() ? this.aliases() : []), \"email\"))\n    return _.unique(addresses)\n  }\n\n  aliases() {\n    return this._cachedGetter(\"aliases\", () => {\n      const aliases = []\n      for (const acc of this._accounts) {\n        aliases.push(acc.me())\n        for (const alias of acc.aliases) {\n          const aliasContact = acc.meUsingAlias(alias)\n          aliasContact.isAlias = true\n          aliases.push(aliasContact)\n        }\n      }\n      return aliases\n    })\n  }\n\n  aliasesFor(accountsOrIds) {\n    const ids = accountsOrIds.map((accOrId) => {\n      return accOrId instanceof Account ? accOrId.id : accOrId\n    })\n    return this.aliases().filter((contact) => ids.includes(contact.accountId))\n  }\n\n  // Public: Returns the currently active {Account}.\n  current() {\n    throw new Error(\"AccountStore.current() has been deprecated.\")\n  }\n\n  // Private: This method is going away soon, do not rely on it.\n  tokensForAccountId(id) {\n    return this._tokens[id]\n  }\n\n  // Private: Load fake data from a directory for taking nice screenshots\n  _importFakeData = (dir) => {\n    const fs = require('fs-plus')\n    const path = require('path')\n    const Message = require('../models/message').default\n    const Thread = require('../models/thread').default\n\n    this._caches = {}\n\n    let labels = []\n    const threads = []\n    let messages = []\n\n    let account = this.accountForEmail('nora@nylas.com')\n    if (!account) {\n      account = new Account()\n      account.serverId = account.clientId\n      account.emailAddress = \"nora@nylas.com\"\n      account.organizationUnit = 'label'\n      account.label = \"Nora's Email\"\n      account.aliases = []\n      account.name = \"Nora\"\n      account.provider = \"gmail\"\n      const json = account.toJSON()\n      json.token = 'nope'\n      this.addAccountFromJSON(json)\n    }\n\n    const filenames = fs.readdirSync(path.join(dir, 'threads'))\n    for (const filename of filenames) {\n      const threadJSON = fs.readFileSync(path.join(dir, 'threads', filename))\n      const threadMessages = JSON.parse(threadJSON).map((j) => (new Message()).fromJSON(j))\n      let threadLabels = []\n      let threadParticipants = []\n      let threadAttachment = false\n      let threadUnread = false\n\n      for (const m of threadMessages) {\n        m.accountId = account.id\n        for (const l of m.categories) {\n          l.accountId = account.id\n        }\n        for (const l of m.files) {\n          l.accountId = account.id\n        }\n        threadParticipants = threadParticipants.concat(m.participants())\n        threadLabels = threadLabels.concat(m.categories)\n        threadAttachment = threadAttachment || m.files.length > 0\n        threadUnread = threadUnread || m.unread\n      }\n\n      threadParticipants = _.uniq(threadParticipants, (p) => p.email)\n      threadLabels = _.uniq(threadLabels, (l) => l.id)\n      labels = _.uniq(labels.concat(threadLabels), (l) => l.id)\n\n      const lastMsg = _.last(threadMessages)\n      const thread = new Thread({\n        accountId: account.id,\n        serverId: lastMsg.threadId,\n        clientId: lastMsg.threadId,\n        subject: lastMsg.subject,\n        lastMessageReceivedTimestamp: lastMsg.date,\n        hasAttachment: threadAttachment,\n        categories: threadLabels,\n        participants: threadParticipants,\n        unread: threadUnread,\n        snippet: lastMsg.snippet,\n        starred: lastMsg.starred,\n      })\n      messages = messages.concat(threadMessages)\n      threads.push(thread)\n    }\n\n    const downloadsDir = path.join(dir, 'downloads')\n    if (fs.existsSync(downloadsDir)) {\n      for (const filename of fs.readdirSync(downloadsDir)) {\n        const destPath = path.join(NylasEnv.getConfigDirPath(), 'downloads', filename)\n        if (fs.existsSync(destPath)) {\n          fs.removeSync(destPath)\n        }\n        fs.copySync(path.join(downloadsDir, filename), destPath)\n      }\n    }\n\n    DatabaseStore.inTransaction((t) =>\n      Promise.all([\n        t.persistModel(account),\n        t.persistModels(labels),\n        t.persistModels(messages),\n        t.persistModels(threads),\n      ])\n    ).then(() => {\n      Actions.focusDefaultMailboxPerspectiveForAccounts([account.id])\n    })\n    .then(() => {\n      return new Promise((resolve) => setTimeout(resolve, 1000))\n    })\n  }\n}\n\nexport default new AccountStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/badge-store.es6",
    "content": "/* eslint global-require:0 */\nimport NylasStore from 'nylas-store';\nimport FocusedPerspectiveStore from './focused-perspective-store';\nimport ThreadCountsStore from './thread-counts-store';\nimport CategoryStore from './category-store';\n\nclass BadgeStore extends NylasStore {\n\n  constructor() {\n    super();\n\n    this.listenTo(FocusedPerspectiveStore, this._updateCounts);\n    this.listenTo(ThreadCountsStore, this._updateCounts);\n\n    NylasEnv.config.onDidChange('core.notifications.countBadge', ({newValue}) => {\n      if (newValue !== 'hide') {\n        this._setBadgeForCount();\n      } else {\n        this._setBadge(\"\");\n      }\n    });\n\n    this._updateCounts();\n  }\n\n  // Public: Returns the number of unread threads in the user's mailbox\n  unread() {\n    return this._unread;\n  }\n\n  total() {\n    return this._total;\n  }\n\n  _updateCounts = () => {\n    let unread = 0;\n    let total = 0;\n\n    const accountIds = FocusedPerspectiveStore.current().accountIds;\n    for (const cat of CategoryStore.getStandardCategories(accountIds, 'inbox')) {\n      unread += ThreadCountsStore.unreadCountForCategoryId(cat.id)\n      total += ThreadCountsStore.totalCountForCategoryId(cat.id)\n    }\n\n    if ((this._unread === unread) && (this._total === total)) {\n      return;\n    }\n    this._unread = unread;\n    this._total = total;\n    this._setBadgeForCount();\n    this.trigger();\n  }\n\n  _setBadgeForCount = () => {\n    const badgePref = NylasEnv.config.get('core.notifications.countBadge');\n    if (!badgePref || badgePref === 'hide') {\n      return;\n    }\n    if (!NylasEnv.isMainWindow() && !NylasEnv.inSpecMode()) {\n      return;\n    }\n\n    const count = badgePref === 'unread' ? this._unread : this._total;\n    if (count > 999) {\n      this._setBadge(\"999+\");\n    } else if (count > 0) {\n      this._setBadge(`${count}`);\n    } else {\n      this._setBadge(\"\");\n    }\n  }\n\n  _setBadge = (val) => {\n    require('electron').ipcRenderer.send('set-badge-value', val);\n  }\n}\n\nconst badgeStore = new BadgeStore()\nexport default badgeStore\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/category-store.coffee",
    "content": "_ = require 'underscore'\nRx = require 'rx-lite'\nNylasStore = require 'nylas-store'\nAccountStore = require('./account-store').default\nAccount = require('../models/account').default\n{StandardCategoryNames} = require('../models/category').default\n{Categories} = require 'nylas-observables'\n\nasAccount = (a) ->\n  throw new Error(\"You must pass an Account or Account Id\") unless a\n  if a instanceof Account then a else AccountStore.accountForId(a)\n\nasAccountId = (a) ->\n  throw new Error(\"You must pass an Account or Account Id\") unless a\n  if a instanceof Account then a.id else a\n\nclass CategoryStore extends NylasStore\n\n  constructor: ->\n    @_categoryCache = {}\n    @_standardCategories = {}\n    @_userCategories = {}\n    @_hiddenCategories = {}\n\n    NylasEnv.config.onDidChange 'core.workspace.showImportant', =>\n      return unless @_categoryResult\n      @_onCategoriesChanged(@_categoryResult)\n\n    Categories\n      .forAllAccounts()\n      .sort()\n      .subscribe(@_onCategoriesChanged)\n\n  byId: (accountOrId, categoryId) ->\n    categories = @_categoryCache[asAccountId(accountOrId)] ? {}\n    categories[categoryId]\n\n  # Public: Returns an array of all categories for an account, both\n  # standard and user generated. The items returned by this function will be\n  # either {Folder} or {Label} objects.\n  #\n  categories: (accountOrId = null) ->\n    if accountOrId\n      _.values(@_categoryCache[asAccountId(accountOrId)]) ? []\n    else\n      all = []\n      for accountId, categories of @_categoryCache\n        all = all.concat(_.values(categories))\n      all\n\n  # Public: Returns all of the standard categories for the given account.\n  #\n  standardCategories: (accountOrId) ->\n    @_standardCategories[asAccountId(accountOrId)] ? []\n\n  hiddenCategories: (accountOrId) ->\n    @_hiddenCategories[asAccountId(accountOrId)] ? []\n\n  # Public: Returns all of the categories that are not part of the standard\n  # category set.\n  #\n  userCategories: (accountOrId) ->\n    @_userCategories[asAccountId(accountOrId)] ? []\n\n  # Public: Returns the Folder or Label object for a standard category name and\n  # for a given account.\n  # ('inbox', 'drafts', etc.) It's possible for this to return `null`.\n  # For example, Gmail likely doesn't have an `archive` label.\n  #\n  getStandardCategory: (accountOrId, name) =>\n    return null unless accountOrId\n\n    unless name in StandardCategoryNames\n      throw new Error(\"'#{name}' is not a standard category\")\n\n    return _.findWhere(@_standardCategories[asAccountId(accountOrId)], {name})\n\n  # Public: Returns the set of all standard categories that match the given\n  # names for each of the provided accounts\n  getStandardCategories: (accountsOrIds, names...) =>\n    if Array.isArray(accountsOrIds)\n      res = []\n      for accOrId in accountsOrIds\n        cats = names.map((name) => @getStandardCategory(accOrId, name))\n        res = res.concat(_.compact(cats))\n      res\n    else\n      _.compact(names.map((name) => @getStandardCategory(accountsOrIds, name)))\n\n  # Public: Returns the Folder or Label object that should be used for \"Archive\"\n  # actions. On Gmail, this is the \"all\" label. On providers using folders, it\n  # returns any available \"Archive\" folder, or null if no such folder exists.\n  #\n  getArchiveCategory: (accountOrId) =>\n    return null unless accountOrId\n    account = asAccount(accountOrId)\n    return null unless account\n\n    if account.usesFolders()\n      return @getStandardCategory(account.id, \"archive\")\n    else\n      return @getStandardCategory(account.id, \"all\")\n\n  # Public: Returns Label object for \"All mail\"\n  #\n  getAllMailCategory: (accountOrId) =>\n    return null unless accountOrId\n    account = asAccount(accountOrId)\n    return null unless account\n    return null unless account.usesLabels()\n\n    return @getStandardCategory(account.id, \"all\")\n\n  # Public: Returns the Folder or Label object that should be used for\n  # the inbox or null if it doesn't exist\n  #\n  getInboxCategory: (accountOrId) =>\n    @getStandardCategory(accountOrId, \"inbox\")\n\n  # Public: Returns the Folder or Label object that should be used for\n  # \"Move to Trash\", or null if no trash folder exists.\n  #\n  getTrashCategory: (accountOrId) =>\n    @getStandardCategory(accountOrId, \"trash\")\n\n  # Public: Returns the Folder or Label object that should be used for\n  # \"Move to Spam\", or null if no trash folder exists.\n  #\n  getSpamCategory: (accountOrId) =>\n    @getStandardCategory(accountOrId, \"spam\")\n\n  _onCategoriesChanged: (categories) =>\n    @_categoryResult = categories\n    @_categoryCache = {}\n    for cat in categories\n      @_categoryCache[cat.accountId] ?= {}\n      @_categoryCache[cat.accountId][cat.id] = cat\n\n    filteredByAccount = (fn) ->\n      result = {}\n      for cat in categories\n        continue unless fn(cat)\n        result[cat.accountId] ?= []\n        result[cat.accountId].push(cat)\n      result\n\n    @_standardCategories = filteredByAccount (cat) -> cat.isStandardCategory()\n    @_userCategories = filteredByAccount (cat) -> cat.isUserCategory()\n    @_hiddenCategories = filteredByAccount (cat) -> cat.isHiddenCategory()\n\n    # Ensure standard categories are always sorted in the correct order\n    for accountId, items of @_standardCategories\n      @_standardCategories[accountId].sort (a, b) ->\n        StandardCategoryNames.indexOf(a.name) - StandardCategoryNames.indexOf(b.name)\n\n    @trigger()\n\nmodule.exports = new CategoryStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/contact-ranking-store.coffee",
    "content": "Rx = require 'rx-lite'\nNylasStore = require 'nylas-store'\nDatabaseStore = require('./database-store').default\nAccountStore = require('./account-store').default\n\nclass ContactRankingStore extends NylasStore\n\n  constructor: ->\n    @_values = {}\n    @_valuesAllAccounts = null\n    @_disposables = {}\n    @listenTo AccountStore, @_onAccountsChanged\n    @_registerObservables(AccountStore.accounts())\n\n  _registerObservables: (accounts) =>\n    nextDisposables = {}\n\n    # Create new observables, reusing existing ones when possible\n    # (so they don't trigger with initial state unnecesarily)\n    for acct in accounts\n      if @_disposables[acct.id]\n        nextDisposables[acct.id] = @_disposables[acct.id]\n        delete @_disposables[acct.id]\n      else\n        query = DatabaseStore.findJSONBlob(\"ContactRankingsFor#{acct.id}\")\n        callback = @_onRankingsChanged.bind(@, acct.id)\n        nextDisposables[acct.id] = Rx.Observable.fromQuery(query).subscribe(callback)\n\n    # Remove unused observables in the old set\n    for key, disposable of @_disposables\n      disposable.dispose()\n\n    @_disposables = nextDisposables\n\n  _onRankingsChanged: (accountId, json) =>\n    @_values[accountId] = if json then json.value else null\n    @_valuesAllAccounts = null\n    @trigger()\n\n  _onAccountsChanged: =>\n    @_registerObservables(AccountStore.accounts())\n\n  valueFor: (accountId) =>\n    @_values[accountId]\n\n  valuesForAllAccounts: =>\n    unless @_valuesAllAccounts\n      combined = {}\n      for acctId, values of @_values\n        for email, score of values\n          if combined[email]\n            combined[email] = Math.max(combined[email], score)\n          else\n            combined[email] = score\n      @_valuesAllAccounts = combined\n\n    return @_valuesAllAccounts\n\n  reset: =>\n    @_valuesAllAccounts = null\n    @_values = {}\n\nmodule.exports = new ContactRankingStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/contact-store.coffee",
    "content": "fs = require 'fs'\npath = require 'path'\nReflux = require 'reflux'\nRx = require 'rx-lite'\nActions = require('../actions').default\nContact = require('../models/contact').default\nUtils = require '../models/utils'\nNylasStore = require 'nylas-store'\nRegExpUtils = require '../../regexp-utils'\nDatabaseStore = require('./database-store').default\nAccountStore = require('./account-store').default\nComponentRegistry = require('../../registries/component-registry')\nContactRankingStore = require './contact-ranking-store'\n_ = require 'underscore'\n\nWindowBridge = require '../../window-bridge'\n\n###\nPublic: ContactStore provides convenience methods for searching contacts and\nformatting contacts. When Contacts become editable, this store will be expanded\nwith additional actions.\n\nSection: Stores\n###\nclass ContactStore extends NylasStore\n\n  constructor: ->\n    @_rankedContacts = []\n    @listenTo ContactRankingStore, => @_updateRankedContactCache()\n    @_updateRankedContactCache()\n\n  # Public: Search the user's contact list for the given search term.\n  # This method compares the `search` string against each Contact's\n  # `name` and `email`.\n  #\n  # - `search` {String} A search phrase, such as `ben@n` or `Ben G`\n  # - `options` (optional) {Object} If you will only be displaying a few results,\n  #   you should pass a limit value. {::searchContacts} will return as soon\n  #   as `limit` matches have been found.\n  #\n  # Returns an {Array} of matching {Contact} models\n  #\n  searchContacts: (search, options={}) =>\n    {limit} = options\n    limit ?= 5\n    limit = Math.max(limit, 0)\n\n    search = search.toLowerCase()\n    accountCount = AccountStore.accounts().length\n\n    if not search or search.length is 0\n      return Promise.resolve([])\n\n    # Search ranked contacts which are stored in order in memory\n    results = []\n    for contact in @_rankedContacts\n      if (contact.email.toLowerCase().indexOf(search) isnt -1 or\n          contact.name.toLowerCase().indexOf(search) isnt -1)\n        results.push(contact)\n      if results.length is limit\n        break\n\n    # If we haven't found enough items in memory, query for more from the\n    # database. Note that we ask for LIMIT * accountCount because we want to\n    # return contacts with distinct email addresses, and the same contact\n    # could exist in every account. Rather than make SQLite do a SELECT DISTINCT\n    # (which is very slow), we just ask for more items.\n    query = DatabaseStore.findAll(Contact)\n      .search(search)\n      .limit(limit * accountCount)\n    query.then (queryResults) =>\n      existingEmails = _.pluck(results, 'email')\n\n      # remove query results that were already found in ranked contacts\n      queryResults = _.reject queryResults, (c) -> c.email in existingEmails\n      queryResults = @_distinctByEmail(queryResults)\n\n      results = results.concat(queryResults)\n\n      extensions = ComponentRegistry.findComponentsMatching({\n        role: \"ContactSearchResults\"\n      })\n      return Promise.each extensions, (ext) =>\n        return ext.findAdditionalContacts(search, results).then (contacts) =>\n          results = contacts\n      .then =>\n        if (results.length > limit) then results.length = limit\n        return Promise.resolve(results)\n\n  isValidContact: (contact) =>\n    return false unless contact instanceof Contact\n    return contact.isValid()\n\n  parseContactsInString: (contactString, options={}) =>\n    {skipNameLookup} = options\n\n    detected = []\n    emailRegex = RegExpUtils.emailRegex()\n    lastMatchEnd = 0\n\n    while (match = emailRegex.exec(contactString))\n      email = match[0]\n      name = null\n\n      startsWithQuote = email[0] in ['\\'','\"']\n      hasTrailingQuote = contactString[match.index+email.length] in ['\\'','\"']\n      if startsWithQuote and hasTrailingQuote\n        email = email[1..-1]\n\n      hasLeadingParen  = contactString[match.index-1] in ['(','<']\n      hasTrailingParen = contactString[match.index+email.length] in [')','>']\n\n      if hasLeadingParen and hasTrailingParen\n        nameStart = lastMatchEnd\n        for char in [',', '\\n', '\\r']\n          i = contactString.lastIndexOf(char, match.index)\n          nameStart = i+1 if i+1 > nameStart\n        name = contactString.substr(nameStart, match.index - 1 - nameStart).trim()\n\n      # The \"nameStart\" for the next match must begin after lastMatchEnd\n      lastMatchEnd = match.index+email.length\n      if hasTrailingParen\n        lastMatchEnd += 1\n\n      if not name or name.length is 0\n        name = email\n\n      # If the first and last character of the name are quotation marks, remove them\n      [firstChar,...,lastChar] = name\n      if firstChar in ['\"', \"'\"] and lastChar in ['\"', \"'\"]\n        name = name[1...-1]\n\n      detected.push(new Contact({email, name}))\n\n    if skipNameLookup\n      return Promise.resolve(detected)\n\n    Promise.all detected.map (contact) =>\n      return contact if contact.name isnt contact.email\n      @searchContacts(contact.email, {limit: 1}).then ([match]) =>\n        return match if match and match.email is contact.email\n        return contact\n\n  _updateRankedContactCache: =>\n    rankings = ContactRankingStore.valuesForAllAccounts()\n    emails = Object.keys(rankings)\n\n    if emails.length is 0\n      @_rankedContacts = []\n      return\n\n    # Sort the emails by rank and then clip to 400 so that our ranked cache\n    # has a bounded size.\n    emails = _.sortBy emails, (email) ->\n      (- (rankings[email.toLowerCase()] ? 0) / 1)\n    emails.length = 400 if emails.length > 400\n\n    DatabaseStore.findAll(Contact, {email: emails}).background().then (contacts) =>\n      contacts = @_distinctByEmail(contacts)\n      for contact in contacts\n        contact._rank = (- (rankings[contact.email.toLowerCase()] ? 0) / 1)\n      @_rankedContacts = _.sortBy contacts, (contact) -> contact._rank\n\n  _distinctByEmail: (contacts) =>\n    # remove query results that are duplicates, prefering ones that have names\n    uniq = {}\n    for contact in contacts\n      continue unless contact.email\n      key = contact.email.toLowerCase()\n      existing = uniq[key]\n      if not existing or (not existing.name or existing.name is existing.email)\n        uniq[key] = contact\n    _.values(uniq)\n\n  _resetCache: =>\n    @_rankedContacts = []\n    ContactRankingStore.reset()\n    @trigger(@)\n\nmodule.exports = new ContactStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/database-agent.js",
    "content": "const Sqlite3 = require('better-sqlite3');\nconst dbs = {};\n\nconst deathDelay = 5000;\nlet deathTimer = setTimeout(() => process.exit(0), deathDelay);\n\nconst getDatabase = (dbpath) => {\n  if (dbs[dbpath]) {\n    return dbs[dbpath].openPromise;\n  }\n\n  let openResolve = null;\n\n  dbs[dbpath] = new Sqlite3(dbpath, {});\n  dbs[dbpath].on('close', (err) => {\n    console.error(err);\n    process.exit(1);\n  });\n  dbs[dbpath].on('open', () => {\n    openResolve(dbs[dbpath]);\n  });\n\n  dbs[dbpath].openPromise = new Promise((resolve) => {\n    openResolve = resolve;\n  });\n\n  return dbs[dbpath].openPromise;\n}\n\nprocess.on('message', (m) => {\n  clearTimeout(deathTimer);\n  const {query, values, id, dbpath} = m;\n  const start = Date.now();\n\n  getDatabase(dbpath).then((db) => {\n    clearTimeout(deathTimer);\n    const fn = query.startsWith('SELECT') ? 'all' : 'run';\n    const stmt = db.prepare(query);\n    const results = stmt[fn](values);\n    process.send({type: 'results', results, id, agentTime: Date.now() - start});\n\n    clearTimeout(deathTimer);\n    deathTimer = setTimeout(() => process.exit(0), deathDelay);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/database-change-record.es6",
    "content": "import Utils from '../models/utils';\n\n/*\nDatabaseChangeRecord is the object emitted from the DatabaseStore when it triggers.\nThe DatabaseChangeRecord contains information about what type of model changed,\nand references to the new model values. All mutations to the database produce these\nchange records.\n*/\nexport default class DatabaseChangeRecord {\n\n  constructor(options) {\n    this.options = options;\n    this._objects = options.objects\n    this._objectsString = options.objectsString;\n    this._objects = this._objects || JSON.parse(this._objectsString, Utils.registeredObjectReviver);\n\n    /**\n     * We notify the entire app in ALL windows when anything in the\n     * database changes. This is normally okay except for Messages because\n     * their bodies might contain millions of charcters that will have to\n     * be serialized, sent over IPC, and deserialized in each and every\n     * window! We make an exception for message bodies here to\n     * dramatically reduce the processing overhead of sending object\n     * changes across the deltas.\n     */\n    if (options.objectClass === \"Message\") {\n      this._objects = this._objects.map((o) => {\n        if (!o.draft) {\n          o.body = null;\n        }\n        return o;\n      })\n    }\n\n    Object.defineProperty(this, 'type', {\n      get: () => options.type,\n    })\n    Object.defineProperty(this, 'objectClass', {\n      get: () => options.objectClass,\n    })\n    Object.defineProperty(this, 'objects', {\n      get: () => {\n        return this._objects;\n      },\n    })\n  }\n\n  toJSON() {\n    this._objectsString = this._objectsString || JSON.stringify(this._objects, Utils.registeredObjectReplacer);\n    return {\n      type: this.type,\n      objectClass: this.objectClass,\n      objectsString: this._objectsString,\n    };\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/database-setup-query-builder.es6",
    "content": "/* eslint global-require:0 */\nimport DatabaseObjectRegistry from '../../registries/database-object-registry';\nimport {tableNameForJoin} from '../models/utils';\n\nimport Attributes from '../attributes';\nconst {AttributeCollection, AttributeJoinedData} = Attributes;\n\n// The DatabaseConnection dispatches queries to the Browser process via IPC and listens\n// for results. It maintains a hash of `_queryRecords` representing queries that are\n// currently running and fires promise callbacks when complete.\n//\nexport default class DatabaseSetupQueryBuilder {\n\n  setupQueries() {\n    let queries = []\n    for (const klass of DatabaseObjectRegistry.getAllConstructors()) {\n      queries = queries.concat(this.setupQueriesForTable(klass));\n    }\n    return queries;\n  }\n\n  setupQueriesForTable(klass) {\n    const attributes = Object.keys(klass.attributes).map(k => klass.attributes[k]);\n    let queries = [];\n\n    // Identify attributes of this class that can be matched against. These\n    // attributes need their own columns in the table\n    const columnAttributes = attributes.filter(attr => attr.needsColumn())\n\n    const columns = ['id TEXT PRIMARY KEY', 'data BLOB']\n    columnAttributes.forEach(attr => columns.push(attr.columnSQL()));\n\n    const columnsSQL = columns.join(',');\n    queries.unshift(`CREATE TABLE IF NOT EXISTS \\`${klass.name}\\` (${columnsSQL})`);\n    queries.push(`CREATE UNIQUE INDEX IF NOT EXISTS \\`${klass.name}_id\\` ON \\`${klass.name}\\` (\\`id\\`)`);\n\n    // Identify collection attributes that can be matched against. These require\n    // JOIN tables. (Right now the only one of these is Thread.folders or\n    // Thread.categories)\n    const collectionAttributes = attributes.filter(attr =>\n      attr.queryable && attr instanceof AttributeCollection\n    );\n    collectionAttributes.forEach((attribute) => {\n      const joinTable = tableNameForJoin(klass, attribute.itemClass);\n      const joinColumns = attribute.joinQueryableBy.map((name) =>\n        klass.attributes[name].columnSQL()\n      );\n      joinColumns.unshift('id TEXT KEY', '`value` TEXT');\n\n      queries.push(`CREATE TABLE IF NOT EXISTS \\`${joinTable}\\` (${joinColumns.join(',')})`);\n      queries.push(`CREATE INDEX IF NOT EXISTS \\`${joinTable.replace('-', '_')}_id\\` ON \\`${joinTable}\\` (\\`id\\` ASC)`);\n      queries.push(`CREATE UNIQUE INDEX IF NOT EXISTS \\`${joinTable.replace('-', '_')}_val_id\\` ON \\`${joinTable}\\` (\\`value\\` ASC, \\`id\\` ASC)`);\n    });\n\n    const joinedDataAttributes = attributes.filter(attr =>\n      attr instanceof AttributeJoinedData\n    )\n\n    joinedDataAttributes.forEach((attribute) => {\n      queries.push(`CREATE TABLE IF NOT EXISTS \\`${attribute.modelTable}\\` (id TEXT PRIMARY KEY, \\`value\\` TEXT)`);\n    });\n\n    if (klass.additionalSQLiteConfig && klass.additionalSQLiteConfig.setup) {\n      queries = queries.concat(klass.additionalSQLiteConfig.setup());\n    }\n\n    if (klass.searchable === true) {\n      const DatabaseStore = require('./database-store').default;\n      queries.push(DatabaseStore.createSearchIndexSql(klass));\n    }\n\n    return queries;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/database-store.es6",
    "content": "/* eslint global-require: 0 */\nimport path from 'path';\nimport createDebug from 'debug'\nimport childProcess from 'child_process';\nimport PromiseQueue from 'promise-queue';\nimport {remote, ipcRenderer} from 'electron';\nimport LRU from \"lru-cache\";\nimport {StringUtils, ExponentialBackoffScheduler} from 'isomorphic-core';\n\nimport NylasStore from '../../global/nylas-store';\nimport Utils from '../models/utils';\nimport Query from '../models/query';\nimport DatabaseChangeRecord from './database-change-record';\nimport DatabaseWriter from './database-writer';\nimport DatabaseSetupQueryBuilder from './database-setup-query-builder';\nimport {openDatabase, handleUnrecoverableDatabaseError, databasePath} from '../../database-helpers'\n\nconst debug = createDebug('app:RxDB')\nconst debugVerbose = createDebug('app:RxDB:all')\n\nconst DatabaseVersion = \"23\";\nconst DatabasePhase = {\n  Setup: 'setup',\n  Ready: 'ready',\n  Close: 'close',\n}\n\nconst DEBUG_QUERY_PLANS = NylasEnv.inDevMode();\n\nconst BASE_RETRY_LOCK_DELAY = 50;\nconst MAX_RETRY_LOCK_DELAY = 500;\n\nlet JSONBlob = null;\n\n/*\nPublic: N1 is built on top of a custom database layer modeled after\nActiveRecord. For many parts of the application, the database is the source\nof truth. Data is retrieved from the API, written to the database, and changes\nto the database trigger Stores and components to refresh their contents.\n\nThe DatabaseStore is available in every application window and allows you to\nmake queries against the local cache. Every change to the local cache is\nbroadcast as a change event, and listening to the DatabaseStore keeps the\nrest of the application in sync.\n\n#// Listening for Changes\n\nTo listen for changes to the local cache, subscribe to the DatabaseStore and\ninspect the changes that are sent to your listener method.\n\n```coffeescript\nthis.unsubscribe = DatabaseStore.listen(this._onDataChanged, this.)\n\n...\n\n_onDataChanged: (change) ->\n  return unless change.objectClass is Message\n  return unless this._myMessageID in _.map change.objects, (m) -> m.id\n\n  // Refresh Data\n\n```\n\nThe local cache changes very frequently, and your stores and components should\ncarefully choose when to refresh their data. The \\`change\\` object passed to your\nevent handler allows you to decide whether to refresh your data and exposes\nthe following keys:\n\n\\`objectClass\\`: The {Model} class that has been changed. If multiple types of models\nwere saved to the database, you will receive multiple change events.\n\n\\`objects\\`: An {Array} of {Model} instances that were either created, updated or\ndeleted from the local cache. If your component or store presents a single object\nor a small collection of objects, you should look to see if any of the objects\nare in your displayed set before refreshing.\n\nSection: Database\n*/\nclass DatabaseStore extends NylasStore {\n\n  static ChangeRecord = DatabaseChangeRecord;\n\n  constructor() {\n    super();\n\n    this._triggerPromise = null;\n    this._inflightTransactions = 0;\n    this._open = false;\n    this._waiting = [];\n\n    this._preparedStatementCache = LRU({max: 500});\n\n    this.setupEmitter();\n    this._emitter.setMaxListeners(100);\n\n    this._databasePath = databasePath(NylasEnv.getConfigDirPath(), NylasEnv.inSpecMode())\n\n    this._databaseMutationHooks = [];\n\n    // Listen to events from the application telling us when the database is ready,\n    // should be closed so it can be deleted, etc.\n    ipcRenderer.on('database-phase-change', () => this._onPhaseChange());\n    setTimeout(() => this._onPhaseChange(), 0);\n  }\n\n  async _asyncWaitForReady() {\n    return new Promise((resolve) => {\n      const app = remote.getGlobal('application')\n      const phase = app.databasePhase()\n      if (phase === DatabasePhase.Setup) {\n        resolve()\n        return\n      }\n\n      const listener = () => {\n        this._emitter.removeListener('ready', listener);\n        resolve()\n      }\n      this._emitter.on('ready', listener)\n    })\n  }\n\n  async _onPhaseChange() {\n    if (NylasEnv.inSpecMode()) {\n      this._emitter.emit('ready')\n      return;\n    }\n\n    const app = remote.getGlobal('application')\n    const phase = app.databasePhase()\n\n    if (phase === DatabasePhase.Setup && NylasEnv.isWorkWindow()) {\n      await this._openDatabase()\n      this._checkDatabaseVersion({allowUnset: true}, () => {\n        this._runDatabaseSetup(() => {\n          app.setDatabasePhase(DatabasePhase.Ready);\n        });\n      });\n    } else if (phase === DatabasePhase.Ready) {\n      await this._openDatabase()\n      this._checkDatabaseVersion({}, () => {\n        this._open = true;\n        for (const w of this._waiting) {\n          w();\n        }\n        this._waiting = [];\n        this._emitter.emit('ready')\n      });\n    } else if (phase === DatabasePhase.Close) {\n      this._open = false;\n      if (this._db) {\n        // https://sqlite.org/pragma.html#pragma_optimize\n        // We do this instead of holding up initial booting by running\n        // potentially very expensive `ANALYZE` queries.\n        this._db.pragma('optimize');\n        this._db.close();\n        this._db = null;\n      }\n    }\n  }\n\n  // When 3rd party components register new models, we need to refresh the\n  // database schema to prepare those tables. This method may be called\n  // extremely frequently as new models are added when packages load.\n  refreshDatabaseSchema() {\n    if (!NylasEnv.isWorkWindow()) {\n      return Promise.resolve();\n    }\n    const app = remote.getGlobal('application');\n    const phase = app.databasePhase();\n    if (phase !== DatabasePhase.Setup) {\n      app.setDatabasePhase(DatabasePhase.Setup);\n    }\n    return this._asyncWaitForReady()\n  }\n\n  async _openDatabase() {\n    if (this._db) return\n    this._db = await openDatabase(this._databasePath)\n  }\n\n  _checkDatabaseVersion({allowUnset} = {}, ready) {\n    const result = this._db.pragma('user_version', true);\n    const isUnsetVersion = (result === '0');\n    const isWrongVersion = (result !== DatabaseVersion);\n    if (isWrongVersion && !(isUnsetVersion && allowUnset)) {\n      return handleUnrecoverableDatabaseError(new Error(`Incorrect database schema version: ${result} not ${DatabaseVersion}`));\n    }\n    return ready();\n  }\n\n  _runDatabaseSetup(ready) {\n    const builder = new DatabaseSetupQueryBuilder()\n\n    try {\n      for (const query of builder.setupQueries()) {\n        debug(`DatabaseStore: ${query}`);\n        this._db.prepare(query).run();\n      }\n    } catch (err) {\n      return handleUnrecoverableDatabaseError(err);\n    }\n\n    this._db.pragma(`user_version=${DatabaseVersion}`);\n    return ready();\n  }\n\n  _prettyConsoleLog(qa) {\n    let q = qa.replace(/%/g, '%%');\n    q = `color:black |||%c ${q}`;\n    q = q.replace(/`(\\w+)`/g, \"||| color:purple |||%c$&||| color:black |||%c\");\n\n    const colorRules = {\n      'color:green': ['SELECT', 'INSERT INTO', 'VALUES', 'WHERE', 'FROM', 'JOIN', 'ORDER BY', 'DESC', 'ASC', 'INNER', 'OUTER', 'LIMIT', 'OFFSET', 'IN'],\n      'color:red; background-color:#ffdddd;': ['SCAN TABLE'],\n    };\n\n    for (const style of Object.keys(colorRules)) {\n      for (const keyword of colorRules[style]) {\n        q = q.replace(new RegExp(`\\\\b${keyword}\\\\b`, 'g'), `||| ${style} |||%c${keyword}||| color:black |||%c`);\n      }\n    }\n\n    q = q.split('|||');\n    const colors = [];\n    const msg = [];\n    for (let i = 0; i < q.length; i++) {\n      if (i % 2 === 0) {\n        colors.push(q[i]);\n      } else {\n        msg.push(q[i]);\n      }\n    }\n\n    console.log(msg.join(''), ...colors);\n  }\n\n  // Returns a Promise that resolves when the query has been completed and\n  // rejects when the query has failed.\n  //\n  // If a query is made before the database has been opened, the query will be\n  // held in a queue and run / resolved when the database is ready.\n  _query(query, values = [], background = false) {\n    return new Promise(async (resolve, reject) => {\n      if (!this._open) {\n        this._waiting.push(() => this._query(query, values).then(resolve, reject));\n        return;\n      }\n\n      // Undefined, True, and False are not valid SQLite datatypes:\n      // https://www.sqlite.org/datatype3.html\n      values.forEach((val, idx) => {\n        if (val === false) {\n          values[idx] = 0;\n        } else if (val === true) {\n          values[idx] = 1;\n        } else if (val === undefined) {\n          values[idx] = null;\n        }\n      });\n\n      const start = Date.now();\n\n      if (!background) {\n        const results = await this._executeLocally(query, values);\n        const msec = Date.now() - start;\n        if (msec > 100) {\n          this._prettyConsoleLog(`DatabaseStore._executeLocally took more than 100ms - ${msec}msec: ${query}`);\n        }\n        resolve(results);\n      } else {\n        const forbidden = ['INSERT ', 'UPDATE ', 'DELETE ', 'DROP ', 'ALTER ', 'CREATE '];\n        for (const key of forbidden) {\n          if (query.startsWith(key)) {\n            throw new Error(\"Transactional queries cannot be made in the background because they would not execute in the current transaction.\")\n          }\n        }\n        this._executeInBackground(query, values).then(({results, backgroundTime}) => {\n          const msec = Date.now() - start;\n          if (debugVerbose.enabled) {\n            const q = `🔶 (${msec}ms) Background: ${query}`;\n            debugVerbose(StringUtils.trimTo(q))\n          }\n\n          if (msec > 100) {\n            const msgPrefix = msec > 100 ? 'DatabaseStore._executeInBackground took more than 100ms - ' : ''\n            this._prettyConsoleLog(`${msgPrefix}${msec}msec (${backgroundTime}msec in background): ${query}`);\n          }\n          resolve(results);\n        });\n      }\n    });\n  }\n\n  async _executeLocally(query, values) {\n    const fn = query.startsWith('SELECT') ? 'all' : 'run';\n    let results = null;\n    const scheduler = new ExponentialBackoffScheduler({\n      baseDelay: BASE_RETRY_LOCK_DELAY,\n      maxDelay: MAX_RETRY_LOCK_DELAY,\n    })\n\n    const schemaChangedStr = 'database schema has changed'\n\n    const retryableRegexp = new RegExp(\n      `(database is locked)||` +\n      `(${schemaChangedStr})`,\n    'i')\n\n    // Because other processes may be writing to the database and modifying the\n    // schema (running ANALYZE, etc.), we may `prepare` a statement and then be\n    // unable to execute it. Handle this case silently unless it's persistent.\n    while (!results) {\n      try {\n        if (scheduler.currentDelay() > 0) {\n          // Setting a timeout for 0 will still defer execution of this function\n          // to the next tick of the event loop.\n          // We don't want to unnecessarily defer and delay every single query,\n          // so we only set the timer when we are actually backing off for a\n          // retry.\n          await new Promise((resolve) => setTimeout(resolve, scheduler.currentDelay()))\n        }\n\n        let stmt = this._preparedStatementCache.get(query);\n        if (!stmt) {\n          stmt = this._db.prepare(query);\n          this._preparedStatementCache.set(query, stmt)\n        }\n\n        const start = Date.now();\n        results = stmt[fn](values);\n        const msec = Date.now() - start;\n        if (debugVerbose.enabled) {\n          const q = `(${msec}ms) ${query}`;\n          debugVerbose(StringUtils.trimTo(q))\n        }\n\n        if (msec > 100) {\n          const msgPrefix = msec > 100 ? 'DatabaseStore: query took more than 100ms - ' : ''\n          if (query.startsWith(`SELECT `) && DEBUG_QUERY_PLANS) {\n            const plan = this._db.prepare(`EXPLAIN QUERY PLAN ${query}`).all(values);\n            const planString = `${plan.map(row => row.detail).join('\\n')} for ${query}`;\n            const quiet = ['ThreadCounts', 'ThreadSearch', 'ContactSearch', 'COVERING INDEX'];\n\n            if (!quiet.find(str => planString.includes(str))) {\n              this._prettyConsoleLog(`${msgPrefix}${msec}msec: ${planString}`);\n            }\n          } else {\n            this._prettyConsoleLog(`${msgPrefix}${msec}msec: ${query}`);\n          }\n        }\n      } catch (err) {\n        const errString = err.toString()\n        if (/database disk image is malformed/gi.test(errString)) {\n          handleUnrecoverableDatabaseError(err)\n          return results\n        }\n\n        if (scheduler.numTries() > 5 || !retryableRegexp.test(errString)) {\n          throw new Error(`DatabaseStore: Query ${query}, ${JSON.stringify(values)} failed ${err.toString()}`);\n        }\n\n        // Some errors require action before the query can be retried\n        if ((new RegExp(schemaChangedStr, 'i')).test(errString)) {\n          this._preparedStatementCache.del(query);\n        }\n      }\n      scheduler.nextDelay()\n    }\n    return results;\n  }\n\n  _executeInBackground(query, values) {\n    if (!this._agent) {\n      this._agentOpenQueries = {};\n      this._agent = childProcess.fork(path.join(path.dirname(__filename), 'database-agent.js'), [], {\n        silent: true,\n      });\n      this._agent.stdout.on('data', (data) =>\n        console.log(data.toString())\n      );\n      this._agent.stderr.on('data', (data) =>\n        console.error(data.toString())\n      );\n      this._agent.on('close', (code) => {\n        debug(`Query Agent: exited with code ${code}`);\n        this._agent = null;\n      });\n      this._agent.on('error', (err) => {\n        console.error(`Query Agent: failed to start or receive message: ${err.toString()}`);\n        this._agent.kill('SIGTERM');\n        this._agent = null;\n      });\n      this._agent.on('message', ({type, id, results, agentTime}) => {\n        if (type === 'results') {\n          this._agentOpenQueries[id]({results, backgroundTime: agentTime});\n          delete this._agentOpenQueries[id];\n        }\n      });\n    }\n    return new Promise((resolve) => {\n      const id = Utils.generateTempId();\n      this._agentOpenQueries[id] = resolve;\n      this._agent.send({ query, values, id, dbpath: this._databasePath });\n    });\n  }\n\n  // PUBLIC METHODS #############################\n\n  // ActiveRecord-style Querying\n\n  // Public: Creates a new Model Query for retrieving a single model specified by\n  // the class and id.\n  //\n  // - \\`class\\` The class of the {Model} you're trying to retrieve.\n  // - \\`id\\` The {String} id of the {Model} you're trying to retrieve\n  //\n  // Example:\n  // ```coffee\n  // DatabaseStore.find(Thread, 'id-123').then (thread) ->\n  //   // thread is a Thread object, or null if no match was found.\n  // ```\n  //\n  // Returns a {Query}\n  //\n  find(klass, id) {\n    if (!klass) {\n      throw new Error(`DatabaseStore::find - You must provide a class`);\n    }\n    if (typeof id !== 'string') {\n      throw new Error(`DatabaseStore::find - You must provide a string id. You may have intended to use findBy.`);\n    }\n    return new Query(klass, this).where({id}).one();\n  }\n\n  // Public: Creates a new Model Query for retrieving a single model matching the\n  // predicates provided.\n  //\n  // - \\`class\\` The class of the {Model} you're trying to retrieve.\n  // - \\`predicates\\` An {Array} of {matcher} objects. The set of predicates the\n  //    returned model must match.\n  //\n  // Returns a {Query}\n  //\n  findBy(klass, predicates = []) {\n    if (!klass) {\n      throw new Error(`DatabaseStore::findBy - You must provide a class`);\n    }\n    return new Query(klass, this).where(predicates).one();\n  }\n\n  // Public: Creates a new Model Query for retrieving all models matching the\n  // predicates provided.\n  //\n  // - \\`class\\` The class of the {Model} you're trying to retrieve.\n  // - \\`predicates\\` An {Array} of {matcher} objects. The set of predicates the\n  //    returned model must match.\n  //\n  // Returns a {Query}\n  //\n  findAll(klass, predicates = []) {\n    if (!klass) {\n      throw new Error(`DatabaseStore::findAll - You must provide a class`);\n    }\n    return new Query(klass, this).where(predicates);\n  }\n\n  // Public: Creates a new Model Query that returns the {Number} of models matching\n  // the predicates provided.\n  //\n  // - \\`class\\` The class of the {Model} you're trying to retrieve.\n  // - \\`predicates\\` An {Array} of {matcher} objects. The set of predicates the\n  //    returned model must match.\n  //\n  // Returns a {Query}\n  //\n  count(klass, predicates = []) {\n    if (!klass) {\n      throw new Error(`DatabaseStore::count - You must provide a class`);\n    }\n    return new Query(klass, this).where(predicates).count();\n  }\n\n  // Public: Modelify converts the provided array of IDs or models (or a mix of\n  // IDs and models) into an array of models of the \\`klass\\` provided by querying for the missing items.\n  //\n  // Modelify is efficient and uses a single database query. It resolves Immediately\n  // if no query is necessary.\n  //\n  // - \\`class\\` The {Model} class desired.\n  // - 'arr' An {Array} with a mix of string model IDs and/or models.\n  //\n  modelify(klass, arr) {\n    if (!(arr instanceof Array) || (arr.length === 0)) {\n      return Promise.resolve([]);\n    }\n\n    const ids = []\n    const clientIds = []\n    for (const item of arr) {\n      if (item instanceof klass) {\n        if (!item.serverId) {\n          clientIds.push(item.clientId);\n        } else {\n          continue;\n        }\n      } else if (typeof item === 'string') {\n        if (Utils.isTempId(item)) {\n          clientIds.push(item);\n        } else {\n          ids.push(item);\n        }\n      } else {\n        throw new Error(`modelify: Not sure how to convert ${item} into a ${klass.name}`);\n      }\n    }\n    if ((ids.length === 0) && (clientIds.length === 0)) {\n      return Promise.resolve(arr);\n    }\n\n    const queries = {\n      modelsFromIds: [],\n      modelsFromClientIds: [],\n    }\n\n    if (ids.length) {\n      queries.modelsFromIds = this.findAll(klass).where(klass.attributes.id.in(ids)).markNotBackgroundable();\n    }\n    if (clientIds.length) {\n      queries.modelsFromClientIds = this.findAll(klass).where(klass.attributes.clientId.in(clientIds)).markNotBackgroundable();\n    }\n\n    return Promise.props(queries).then(({modelsFromIds, modelsFromClientIds}) => {\n      const modelsByString = {};\n      for (const model of modelsFromIds) {\n        modelsByString[model.id] = model;\n      }\n      for (const model of modelsFromClientIds) {\n        modelsByString[model.clientId] = model;\n      }\n\n      return Promise.resolve(arr.map(item =>\n        (item instanceof klass ? item : modelsByString[item]))\n      );\n    });\n  }\n\n  // Public: Executes a {Query} on the local database.\n  //\n  // - \\`modelQuery\\` A {Query} to execute.\n  //\n  // Returns a {Promise} that\n  //   - resolves with the result of the database query.\n  //\n  run(modelQuery, options = {format: true}) {\n    return this._query(modelQuery.sql(), [], modelQuery._background, modelQuery._logQueryPlanDebugOutput).then((result) => {\n      let transformed = modelQuery.inflateResult(result);\n      if (options.format !== false) {\n        transformed = modelQuery.formatResult(transformed)\n      }\n      return Promise.resolve(transformed);\n    });\n  }\n\n  findJSONBlob(id) {\n    JSONBlob = JSONBlob || require('../models/json-blob').default;\n    return new JSONBlob.Query(JSONBlob, this).where({id}).one();\n  }\n\n  // Private: Mutation hooks allow you to observe changes to the database and\n  // add additional functionality before and after the REPLACE / INSERT queries.\n  //\n  // beforeDatabaseChange: Run queries, etc. and return a promise. The DatabaseStore\n  // will proceed with changes once your promise has finished. You cannot call\n  // persistModel or unpersistModel from this hook.\n  //\n  // afterDatabaseChange: Run queries, etc. after the REPLACE / INSERT queries\n  //\n  // Warning: this is very low level. If you just want to watch for changes, You\n  // should subscribe to the DatabaseStore's trigger events.\n  //\n  addMutationHook({beforeDatabaseChange, afterDatabaseChange}) {\n    if (!beforeDatabaseChange) {\n      throw new Error(`DatabaseStore:addMutationHook - You must provide a beforeDatabaseChange function`);\n    }\n    if (!afterDatabaseChange) {\n      throw new Error(`DatabaseStore:addMutationHook - You must provide a afterDatabaseChange function`);\n    }\n    this._databaseMutationHooks.push({beforeDatabaseChange, afterDatabaseChange});\n  }\n\n  removeMutationHook(hook) {\n    this._databaseMutationHooks = this._databaseMutationHooks.filter(h => h !== hook);\n  }\n\n  mutationHooks() {\n    return this._databaseMutationHooks;\n  }\n\n\n  // Public: Opens a new database transaction for writing changes.\n  // DatabaseStore.inTransacion makes the following guarantees:\n  //\n  // - No other calls to \\`inTransaction\\` will run until the promise has finished.\n  //\n  // - No other process will be able to write to sqlite while the provided function\n  //   is running. `BEGIN IMMEDIATE TRANSACTION` semantics are:\n  //     + No other connection will be able to write any changes.\n  //     + Other connections can read from the database, but they will not see\n  //       pending changes.\n  //\n  // this.param fn {function} callback that will be executed inside a database transaction\n  // Returns a {Promise} that resolves when the transaction has successfully\n  // completed.\n  inTransaction(fn) {\n    const t = new DatabaseWriter(this);\n    this._transactionQueue = this._transactionQueue || new PromiseQueue(1, Infinity);\n    return this._transactionQueue.add(() =>\n      t.executeInTransaction(fn)\n    );\n  }\n\n  async write(fn) {\n    const t = new DatabaseWriter(this);\n    await t.execute(fn)\n  }\n\n  // _accumulateAndTrigger is a guarded version of trigger that can accumulate changes.\n  // This means that even if you're a bad person and call \\`persistModel\\` 100 times\n  // from 100 task objects queued at the same time, it will only create one\n  // \\`trigger\\` event. This is important since the database triggering impacts\n  // the entire application.\n  accumulateAndTrigger(change) {\n    this._triggerPromise = this._triggerPromise || new Promise((resolve) => {\n      this._resolve = resolve;\n    });\n\n    const flush = () => {\n      if (!this._changeAccumulated) {\n        return;\n      }\n      if (this._changeFireTimer) {\n        clearTimeout(this._changeFireTimer);\n      }\n      this.trigger(new DatabaseChangeRecord(this._changeAccumulated));\n      this._changeAccumulated = null;\n      this._changeAccumulatedLookup = null;\n      this._changeFireTimer = null;\n      if (this._resolve) {\n        this._resolve();\n      }\n      this._triggerPromise = null;\n    };\n\n    const set = (_change) => {\n      if (this._changeFireTimer) {\n        clearTimeout(this._changeFireTimer);\n      }\n      this._changeAccumulated = _change;\n      this._changeAccumulatedLookup = {};\n      this._changeAccumulated.objects.forEach((obj, idx) => {\n        this._changeAccumulatedLookup[obj.id] = idx;\n      });\n      this._changeFireTimer = setTimeout(flush, 10);\n    };\n\n    const concat = (_change) => {\n      // When we join new models into our set, replace existing ones so the same\n      // model cannot exist in the change record set multiple times.\n      for (const obj of _change.objects) {\n        const idx = this._changeAccumulatedLookup[obj.id]\n        if (idx) {\n          this._changeAccumulated.objects[idx] = obj;\n        } else {\n          this._changeAccumulatedLookup[obj.id] = this._changeAccumulated.objects.length\n          this._changeAccumulated.objects.push(obj);\n        }\n      }\n    };\n\n    if (!this._changeAccumulated) {\n      set(change);\n    } else if ((this._changeAccumulated.objectClass === change.objectClass) && (this._changeAccumulated.type === change.type)) {\n      concat(change);\n    } else {\n      flush();\n      set(change);\n    }\n\n    return this._triggerPromise;\n  }\n\n\n  // Search Index Operations\n\n  createSearchIndexSql(klass) {\n    if (!klass) {\n      throw new Error(`DatabaseStore::createSearchIndex - You must provide a class`);\n    }\n    if (!klass.searchFields) {\n      throw new Error(`DatabaseStore::createSearchIndex - ${klass.name} must expose an array of \\`searchFields\\``);\n    }\n    const searchTableName = `${klass.name}Search`;\n    const searchFields = klass.searchFields;\n    return (\n      `CREATE VIRTUAL TABLE IF NOT EXISTS \\`${searchTableName}\\` ` +\n      `USING fts5(\n        tokenize='porter unicode61',\n        content_id UNINDEXED,\n        ${searchFields.join(', ')}\n      )`\n    );\n  }\n\n  createSearchIndex(klass) {\n    const sql = this.createSearchIndexSql(klass);\n    return this._query(sql);\n  }\n\n  dropSearchIndex(klass) {\n    if (!klass) {\n      throw new Error(`DatabaseStore::createSearchIndex - You must provide a class`);\n    }\n    const searchTableName = `${klass.name}Search`\n    const dropSql = `DROP TABLE IF EXISTS \\`${searchTableName}\\``\n    const clearIsSearchIndexedSql = `UPDATE \\`${klass.name}\\` SET \\`is_search_indexed\\` = 0 WHERE \\`is_search_indexed\\` = 1`\n    return this._query(dropSql).then(() => {\n      return this._query(clearIsSearchIndexedSql);\n    });\n  }\n\n  isModelIndexed(model, isIndexed) {\n    if (isIndexed === true) {\n      return Promise.resolve(true);\n    }\n    return Promise.resolve(!!model.isSearchIndexed);\n  }\n\n  indexModel(model, indexData, isModelIndexed) {\n    const searchTableName = `${model.constructor.name}Search`;\n    return this.isModelIndexed(model, isModelIndexed).then((isIndexed) => {\n      if (isIndexed) {\n        return this.updateModelIndex(model, indexData, isIndexed);\n      }\n\n      const indexFields = Object.keys(indexData)\n      const keysSql = `content_id, ${indexFields.join(`, `)}`\n      const valsSql = `?, ${indexFields.map(() => '?').join(', ')}`\n      const values = [model.id].concat(indexFields.map(k => indexData[k]))\n      const sql = (\n        `INSERT INTO \\`${searchTableName}\\`(${keysSql}) VALUES (${valsSql})`\n      )\n      return this._query(sql, values).then(({lastInsertROWID}) => {\n        model.isSearchIndexed = true;\n        model.searchIndexId = lastInsertROWID;\n        return this.inTransaction((t) => t.persistModel(model, {silent: true, affectsJoins: false}))\n      });\n    });\n  }\n\n  updateModelIndex(model, indexData, isModelIndexed) {\n    const searchTableName = `${model.constructor.name}Search`;\n    this.isModelIndexed(model, isModelIndexed).then((isIndexed) => {\n      if (!isIndexed) {\n        return this.indexModel(model, indexData, isIndexed);\n      }\n\n      const indexFields = Object.keys(indexData);\n      const values = indexFields.map(key => indexData[key]).concat([model.searchIndexId]);\n      const setSql = (\n        indexFields\n        .map((key) => `\\`${key}\\` = ?`)\n        .join(', ')\n      );\n      const sql = (\n        `UPDATE \\`${searchTableName}\\` SET ${setSql} WHERE \\`${searchTableName}\\`.\\`rowid\\` = ?`\n      );\n      return this._query(sql, values);\n    });\n  }\n\n  // opts can have a boolean isBeingUnpersisted value, which when true prevents\n  // this function from re-persisting the model.\n  unindexModel(model, opts = {}) {\n    const searchTableName = `${model.constructor.name}Search`;\n    const sql = (\n      `DELETE FROM \\`${searchTableName}\\` WHERE \\`${searchTableName}\\`.\\`rowid\\` = ?`\n    );\n    const query = this._query(sql, [model.searchIndexId]);\n    if (opts.isBeingUnpersisted) {\n      return query;\n    }\n    return query.then(() => {\n      model.isSearchIndexed = false;\n      model.searchIndexId = 0;\n      return this.inTransaction((t) => t.persistModel(model, {silent: true, affectsJoins: false}))\n    });\n  }\n\n  unindexModelsForAccount() {\n    // const modelTable = modelKlass.name;\n    // const searchTableName = `${modelTable}Search`;\n    /* TODO: We don't correctly clean up the model tables right now, so we don't\n     * want to destroy the index until we do so.\n    const sql = (\n      `DELETE FROM \\`${searchTableName}\\` WHERE \\`${searchTableName}\\`.\\`content_id\\` IN\n      (SELECT \\`id\\` FROM \\`${modelTable}\\` WHERE \\`${modelTable}\\`.\\`account_id\\` = ?)`\n    );\n    return this._query(sql, [accountId])\n   */\n    return Promise.resolve()\n  }\n}\n\nexport default new DatabaseStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/database-writer.es6",
    "content": "/* eslint global-require:0 */\nimport Model from '../models/model';\nimport ModelQuery from '../models/query';\nimport {tableNameForJoin, registeredObjectReplacer} from '../models/utils';\n\nimport Attributes from '../attributes';\n\nconst {AttributeCollection, AttributeJoinedData} = Attributes;\n\nexport default class DatabaseWriter {\n  constructor(database) {\n    this.database = database;\n    this._changeRecords = [];\n    this._opened = false;\n  }\n\n  find(...args) { return this._forwardOperationToDatabase('find', ...args) }\n  findBy(...args) { return this._forwardOperationToDatabase('findBy', ...args) }\n  findAll(...args) { return this._forwardOperationToDatabase('findAll', ...args) }\n  modelify(...args) { return this._forwardOperationToDatabase('modelify', ...args) }\n  count(...args) { return this._forwardOperationToDatabase('count', ...args) }\n  findJSONBlob(...args) { return this._forwardOperationToDatabase('findJSONBlob', ...args) }\n\n  async execute(fn) {\n    try {\n      await fn(this);\n    } finally {\n      for (const record of this._changeRecords) {\n        this.database.accumulateAndTrigger(record);\n      }\n    }\n  }\n\n  executeInTransaction(fn) {\n    if (this._opened) {\n      throw new Error(\"DatabaseWriter:executeInTransaction was already called\");\n    }\n\n    return this._query(\"BEGIN IMMEDIATE TRANSACTION\").then(() => {\n      this._opened = true;\n      const transactionReturn = fn(this);\n      if (!transactionReturn || !transactionReturn.then) {\n        console.error(fn)\n        throw new Error(\"The Database Transaction function shown above must return a Promise. It Returned:\", transactionReturn)\n      }\n      return transactionReturn;\n    }).finally(() => {\n      if (!this._opened) {\n        return null;\n      }\n      this._opened = false;\n      return this._query(\"COMMIT\").then(() => {\n        for (const record of this._changeRecords) {\n          this.database.accumulateAndTrigger(record);\n        }\n      });\n    });\n  }\n\n  // Mutating the Database\n\n  persistJSONBlob(id, json) {\n    const JSONBlob = require('../models/json-blob').default;\n\n    return this.persistModel(new JSONBlob({id, json}));\n  }\n\n  // Public: Asynchronously writes `model` to the cache and triggers a change event.\n  //\n  // - `model` A {Model} to write to the database.\n  //\n  // Returns a {Promise} that\n  //   - resolves after the database queries are complete and any listening\n  //     database callbacks have finished\n  //   - rejects if any databse query fails or one of the triggering\n  //     callbacks failed\n  persistModel(model, opts = {}) {\n    if (!model || !(model instanceof Model)) {\n      throw new Error(\"DatabaseWriter::persistModel - You must pass an instance of the Model class.\");\n    }\n    return this.persistModels([model], opts);\n  }\n\n  // Public: Asynchronously writes `models` to the cache and triggers a single change\n  // event. Note: Models must be of the same class to be persisted in a batch operation.\n  //\n  // - `models` An {Array} of {Model} objects to write to the database.\n  //\n  // - options:\n  //  - `silent`: default false. Will notify hooks and downstream\n  //    listeners that the models have changed. Always have it set to\n  //    false unless you're VERY sure no one needs to know about the\n  //    changes that happen. This could cause DBs to become out of sync if\n  //    careless.\n  //  - `affectsJoins`: defaults to true. Any model that has joined\n  //    properties via Attributes.Collections almost always needs to get\n  //    updated. If you're VERY sure the change does not affect any join\n  //    tables, you can set this to `false` to save some DB queries.\n  //\n  // Returns a {Promise} that\n  //   - resolves after the database queries are complete and any listening\n  //     database callbacks have finished\n  //   - rejects if any databse query fails or one of the triggering\n  //     callbacks failed\n  persistModels(models = [], {silent = false, affectsJoins = true} = {}) {\n    if (models.length === 0) {\n      return Promise.resolve();\n    }\n\n    const klass = models[0].constructor;\n    const clones = [];\n    const ids = {};\n\n    if (!(models[0] instanceof Model)) {\n      throw new Error(`DatabaseWriter::persistModels - You must pass an array of items which descend from the Model class.`);\n    }\n\n    for (const model of models) {\n      if (!model || (model.constructor !== klass)) {\n        throw new Error(`DatabaseWriter::persistModels - When you batch persist objects, they must be of the same type`);\n      }\n      if (ids[model.id]) {\n        throw new Error(`DatabaseWriter::persistModels - You must pass an array of models with different ids. ID ${model.id} is in the set multiple times.`)\n      }\n      clones.push(model.clone());\n      ids[model.id] = true;\n    }\n\n    // Note: It's important that we clone the objects since other code could mutate\n    // them during the save process. We want to guaruntee that the models you send to\n    // persistModels are saved exactly as they were sent.\n    const metadata = {\n      objectClass: clones[0].constructor.name,\n      objectIds: Object.keys(ids),\n      objects: clones,\n      type: 'persist',\n    }\n\n    if (silent) {\n      return this._writeModels(clones, {affectsJoins})\n    }\n    return this._runMutationHooks('beforeDatabaseChange', metadata).then((data) => {\n      return this._writeModels(clones, {affectsJoins}).then(() => {\n        this._runMutationHooks('afterDatabaseChange', metadata, data);\n        return this._changeRecords.push(metadata);\n      });\n    });\n  }\n\n  // Public: Asynchronously removes `model` from the cache and triggers a change event.\n  //\n  // - `model` A {Model} to write to the database.\n  //\n  // Returns a {Promise} that\n  //   - resolves after the database queries are complete and any listening\n  //     database callbacks have finished\n  //   - rejects if any databse query fails or one of the triggering\n  //     callbacks failed\n  unpersistModel(model, {silent = false} = {}) {\n    const clone = model.clone();\n    const metadata = {\n      objectClass: clone.constructor.name,\n      objectIds: [clone.id],\n      objects: [clone],\n      type: 'unpersist',\n    }\n\n    if (silent) {\n      return this._deleteModel(clone)\n    }\n    return this._runMutationHooks('beforeDatabaseChange', metadata).then((data) => {\n      return this._deleteModel(clone).then(() => {\n        this._runMutationHooks('afterDatabaseChange', metadata, data);\n        return this._changeRecords.push(metadata);\n      });\n    });\n  }\n\n  removeAllOfClass(klass) {\n    return this._query(`DELETE FROM ${klass.name}`)\n  }\n\n  // PRIVATE METHODS ////////////////////////////////////////////////////////\n\n  _query = (...args) => {\n    return this.database._query(...args);\n  }\n\n  _runMutationHooks(selectorName, metadata, data = []) {\n    const beforePromises = this.database.mutationHooks().map((hook, idx) =>\n      Promise.try(() => hook[selectorName](this._query, metadata, data[idx]))\n    );\n\n    return Promise.all(beforePromises).catch((e) => {\n      if (!NylasEnv.inSpecMode()) {\n        console.warn(`DatabaseWriter Hook: ${selectorName} failed`, e);\n      }\n      return Promise.resolve([]);\n    });\n  }\n\n  // Fires the queries required to write models to the DB\n  //\n  // Returns a promise that:\n  //   - resolves when all write queries are complete\n  //   - rejects if any query fails\n  _writeModels(models, {affectsJoins = true} = {}) {\n    const promises = [];\n\n    // IMPORTANT: This method assumes that all the models you\n    // provide are of the same class, and have different ids!\n\n    // Avoid trying to write too many objects a time - sqlite can only handle\n    // value sets `(?,?)...` of less than SQLITE_MAX_COMPOUND_SELECT (500),\n    // and we don't know ahead of time whether we'll hit that or not.\n    if (models.length > 50) {\n      return Promise.all([\n        this._writeModels(models.slice(0, 50), {affectsJoins}),\n        this._writeModels(models.slice(50), {affectsJoins}),\n      ]);\n    }\n\n    const klass = models[0].constructor;\n    const attributes = Object.keys(klass.attributes).map(key => klass.attributes[key])\n\n    const columnAttributes = attributes.filter((attr) =>\n      attr.queryable && attr.columnSQL && attr.jsonKey !== 'id'\n    );\n\n    // Compute the columns in the model table and a question mark string\n    const columns = ['id', 'data'];\n    const columnMarks = ['?', '?'];\n    columnAttributes.forEach((attr) => {\n      columns.push(attr.jsonKey);\n      columnMarks.push('?');\n    });\n    const columnsSQL = columns.join(',');\n    const marksSet = `(${columnMarks.join(',')})`;\n\n    // Prepare a batch insert VALUES (?,?,?), (?,?,?)... by assembling\n    // an array of the values and a corresponding question mark set\n    const values = [];\n    const marks = [];\n    const ids = [];\n    const modelsJSONs = [];\n    for (const model of models) {\n      const json = model.toJSON({joined: false});\n      modelsJSONs.push(json);\n      ids.push(model.id);\n      values.push(model.id, JSON.stringify(json, registeredObjectReplacer));\n      columnAttributes.forEach((attr) => {\n        values.push(json[attr.jsonKey]);\n      });\n      marks.push(marksSet);\n    }\n\n    const marksSQL = marks.join(',');\n\n    promises.push(this._query(`REPLACE INTO \\`${klass.name}\\` (${columnsSQL}) VALUES ${marksSQL}`, values));\n\n    if (!affectsJoins) {\n      return Promise.all(promises);\n    }\n\n    // For each join table property, find all the items in the join table for this\n    // model and delete them. Insert each new value back into the table.\n    const collectionAttributes = attributes.filter((attr) =>\n      attr.queryable && attr instanceof AttributeCollection\n    )\n\n    collectionAttributes.forEach((attr) => {\n      const joinTable = tableNameForJoin(klass, attr.itemClass);\n\n      promises.push(this._query(`DELETE FROM \\`${joinTable}\\` WHERE \\`id\\` IN ('${ids.join(\"','\")}')`));\n\n      const joinMarks = [];\n      const joinedValues = [];\n      const joinMarkUnit = `(${[\"?\", \"?\"].concat(attr.joinQueryableBy.map(() => '?')).join(',')})`;\n      const joinQueryableByJSONKeys = attr.joinQueryableBy.map(joinedModelKey =>\n        klass.attributes[joinedModelKey].jsonKey\n      );\n      const joinColumns = ['id', 'value'].concat(joinQueryableByJSONKeys);\n\n      // https://www.sqlite.org/limits.html: SQLITE_MAX_VARIABLE_NUMBER\n      const valuesPerRow = joinColumns.length;\n      const rowsPerInsert = Math.floor(600 / valuesPerRow);\n      const valuesPerInsert = rowsPerInsert * valuesPerRow;\n\n      models.forEach((model, idx) => {\n        const joinedModels = model[attr.modelKey] || [];\n        for (const joined of joinedModels) {\n          if (!attr.joinOnField) {\n            throw new Error(`Queryable collection attribute ${attr.modelKey} must specify a joinOnField`);\n          }\n          const joinValue = joined[attr.joinOnField];\n          joinMarks.push(joinMarkUnit);\n          joinedValues.push(model.id, joinValue);\n          for (const joinedJsonKey of joinQueryableByJSONKeys) {\n            joinedValues.push(modelsJSONs[idx][joinedJsonKey]);\n          }\n        }\n      });\n\n      if (joinedValues.length !== 0) {\n        // Write no more than 200 items (400 values) at once to avoid sqlite limits\n        // 399 values: slices:[0..0]\n        // 400 values: slices:[0..0]\n        // 401 values: slices:[0..1]\n        const slicePageCount = Math.ceil(joinMarks.length / rowsPerInsert) - 1;\n        for (let slice = 0; slice <= slicePageCount; slice++) {\n          const [ms, me] = [slice * rowsPerInsert, slice * rowsPerInsert + rowsPerInsert];\n          const [vs, ve] = [slice * valuesPerInsert, slice * valuesPerInsert + valuesPerInsert];\n          promises.push(this._query(`INSERT OR IGNORE INTO \\`${joinTable}\\` (\\`${joinColumns.join('`,`')}\\`) VALUES ${joinMarks.slice(ms, me).join(',')}`, joinedValues.slice(vs, ve)));\n        }\n      }\n    });\n\n    // For each joined data property stored in another table...\n    const joinedDataAttributes = attributes.filter(attr =>\n      attr instanceof AttributeJoinedData\n    )\n\n    joinedDataAttributes.forEach((attr) => {\n      for (const model of models) {\n        const value = model[attr.modelKey];\n        if (value !== undefined) {\n          promises.push(this._query(`REPLACE INTO \\`${attr.modelTable}\\` (\\`id\\`, \\`value\\`) VALUES (?, ?)`, [model.id, attr.serialize(model, value)]));\n        }\n      }\n    });\n\n    return Promise.all(promises);\n  }\n\n  // Fires the queries required to delete models to the DB\n  //\n  // Returns a promise that:\n  //   - resolves when all deltion queries are complete\n  //   - rejects if any query fails\n  _deleteModel(model) {\n    const promises = []\n\n    const klass = model.constructor;\n    const attributes = Object.keys(klass.attributes).map(key => klass.attributes[key]);\n\n    // Delete the primary record\n    promises.push(this._query(`DELETE FROM \\`${klass.name}\\` WHERE \\`id\\` = ?`, [model.id]))\n\n    // For each join table property, find all the items in the join table for this\n    // model and delte them. Insert each new value back into the table.\n    const collectionAttributes = attributes.filter(attr =>\n      attr.queryable && attr instanceof AttributeCollection\n    );\n\n    collectionAttributes.forEach((attr) => {\n      const joinTable = tableNameForJoin(klass, attr.itemClass);\n      promises.push(this._query(`DELETE FROM \\`${joinTable}\\` WHERE \\`id\\` = ?`, [model.id]))\n    });\n\n    const joinedDataAttributes = attributes.filter(attr =>\n      attr instanceof AttributeJoinedData\n    );\n\n    joinedDataAttributes.forEach((attr) => {\n      promises.push(this._query(`DELETE FROM \\`${attr.modelTable}\\` WHERE \\`id\\` = ?`, [model.id]));\n    });\n\n    return Promise.all(promises);\n  }\n\n  _forwardOperationToDatabase(operation, ...args) {\n    try {\n      const query = this.database[operation](...args)\n      if (query instanceof ModelQuery) {\n        return query.markNotBackgroundable()\n      }\n      return query\n    } catch (error) {\n      throw new Error(`DatabaseWriter: Error trying to perform ${operation} on database. Is it defined?`)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/delta-connection-store.es6",
    "content": "import _ from 'underscore'\nimport Rx from 'rx-lite'\nimport NylasStore from 'nylas-store'\nimport AccountStore from './account-store'\nimport DatabaseStore from './database-store'\nimport DeltaStreamingConnection from '../../services/delta-streaming-connection'\n\n\n/**\n * DeltaConnectionStore manages delta connections and\n * keeps track of the status of delta connections\n * per account. It will  trigger whenever delta conenction\n * status changes.\n *\n * The connection status for any given account has the following shape:\n *\n * {\n *   cursor: 0,\n *   status: 'connected',\n * }\n *\n */\nclass DeltaConnectionStore extends NylasStore {\n\n  constructor() {\n    super()\n    this._unsubscribers = []\n    this._connectionStatesByAccountId = {}\n    this._connectionsByAccountId = new Map()\n    this._connectionStatusSubscriptionsByAccountId = new Map()\n\n    this._isBuildingDeltaConnections = false\n\n    this._triggerDebounced = _.debounce(this.trigger, 100)\n  }\n\n  async activate() {\n    if (!NylasEnv.isWorkWindow()) { return }\n    this._unsubsribers = [\n      this.listenTo(AccountStore, () => this._onAccountsChanged()),\n    ]\n    const accountIds = AccountStore.accountIds()\n    this._setupConnectionStatusSubscriptions({newAccountIds: accountIds})\n    await this._setupDeltaStreamingConnections({newAccountIds: accountIds})\n  }\n\n  deactivate() {\n    if (!NylasEnv.isWorkWindow()) { return }\n    this._unsubsribers.forEach(usub => usub())\n    for (const subscription of this._connectionStatusSubscriptionsByAccountId.values()) {\n      subscription.dispose()\n    }\n    this._connectionStatusSubscriptionsByAccountId.clear()\n  }\n\n  getDeltaConnectionStates() {\n    return this._connectionStatesByAccountId\n  }\n\n  _updateState(accountId, nextState) {\n    const currentState = this._connectionStatesByAccountId[accountId] || {}\n    if (_.isEqual(currentState, nextState)) { return }\n    this._connectionStatesByAccountId[accountId] = nextState\n    this._triggerDebounced()\n  }\n\n  async _onAccountsChanged() {\n    const currentIds = Array.from(this._connectionStatusSubscriptionsByAccountId.keys())\n    const nextIds = AccountStore.accountIds()\n    const newAccountIds = _.difference(nextIds, currentIds)\n    const removedAccountIds = _.difference(currentIds, nextIds)\n\n    this._setupConnectionStatusSubscriptions({newAccountIds, removedAccountIds})\n    await this._setupDeltaStreamingConnections({newAccountIds, removedAccountIds})\n  }\n\n  _setupConnectionStatusSubscriptions({newAccountIds = [], removedAccountIds = []} = {}) {\n    removedAccountIds.forEach((accountId) => {\n      if (this._connectionStatusSubscriptionsByAccountId.has(accountId)) {\n        this._connectionStatusSubscriptionsByAccountId.get(accountId).dispose()\n      }\n\n      if (this._connectionStatesByAccountId[accountId]) {\n        delete this._connectionStatesByAccountId[accountId]\n        this._triggerDebounced()\n      }\n    })\n\n    newAccountIds.forEach((accountId) => {\n      if (this._connectionStatusSubscriptionsByAccountId.has(accountId)) { return; }\n      const query = DatabaseStore.findJSONBlob(`DeltaStreamingConnectionStatus:${accountId}`)\n      const subscription = Rx.Observable.fromQuery(query)\n      .subscribe((json) => {\n        // We need to copy `json` otherwise the query observable will mutate\n        // the reference to that object\n        this._updateState(accountId, {...json})\n      })\n      this._connectionStatusSubscriptionsByAccountId.set(accountId, subscription)\n    })\n  }\n\n  async _setupDeltaStreamingConnections({newAccountIds = [], removedAccountIds = []} = {}) {\n    if (NylasEnv.inSpecMode()) { return; }\n\n    // We need a function lock on this because on bootup, many legitimate\n    // events coming in may result in this function being called multiple times\n    // in quick succession, which can cause us to start multiple syncs for the\n    // same account\n    if (this._isBuildingDeltaConnections) { return }\n    this._isBuildingDeltaConnections = true;\n\n    try {\n      for (const accountId of newAccountIds) {\n        const account = AccountStore.accountForId(accountId)\n        const newDeltaConnection = new DeltaStreamingConnection(account);\n        await newDeltaConnection.start()\n        this._connectionsByAccountId.set(accountId, newDeltaConnection)\n      }\n      for (const accountId of removedAccountIds) {\n        if (this._connectionsByAccountId.has(accountId)) {\n          const connection = this._connectionsByAccountId.get(accountId)\n          connection.end()\n          this._connectionsByAccountId.delete(accountId)\n        }\n      }\n    } finally {\n      this._isBuildingDeltaConnections = false;\n    }\n  }\n}\n\nexport default new DeltaConnectionStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/draft-editing-session.coffee",
    "content": "Message = require('../models/message').default\nActions = require('../actions').default\nNylasAPIHelpers = require '../nylas-api-helpers'\nAccountStore = require('./account-store').default\nContactStore = require './contact-store'\nDatabaseStore = require('./database-store').default\nUndoStack = require('../../undo-stack').default\nDraftHelpers = require('../stores/draft-helpers').default\nExtensionRegistry = require '../../registries/extension-registry'\n{Listener, Publisher} = require '../modules/reflux-coffee'\nCoffeeHelpers = require '../coffee-helpers'\nDraftStore = null\n_ = require 'underscore'\n\nMetadataChangePrefix = 'metadata.'\n\n###\nPublic: As the user interacts with the draft, changes are accumulated in the\nDraftChangeSet associated with the store session. The DraftChangeSet does two things:\n\n1. It debounces changes and calls Actions.saveDraft() at a reasonable interval.\n\n2. It exposes `applyToModel`, which allows you to optimistically apply changes\n  to a draft object. When the session vends the draft, it passes it through this\n  function to apply uncommitted changes. This means the Draft provided by the\n  DraftEditingSession will always relfect recent changes, even though they're\n  written to the database intermittently.\n\nSection: Drafts\n###\nclass DraftChangeSet\n  @include: CoffeeHelpers.includeModule\n  @include Publisher\n\n  constructor: (@callbacks) ->\n    @_commitChain = Promise.resolve()\n    @_pending = {}\n    @_saving = {}\n    @_timer = null\n\n  teardown: ->\n    @_pending = {}\n    @_saving = {}\n    if @_timer\n      clearTimeout(@_timer)\n      @_timer = null\n\n  add: (changes, {doesNotAffectPristine}={}) =>\n    @callbacks.onWillAddChanges(changes)\n    @_pending = _.extend(@_pending, changes)\n    @_pending.pristine = false unless doesNotAffectPristine\n    @callbacks.onDidAddChanges(changes)\n\n    clearTimeout(@_timer) if @_timer\n    @_timer = setTimeout(@commit, 10000)\n\n  addPluginMetadata: (pluginId, metadata) =>\n    changes = {}\n    changes[\"#{MetadataChangePrefix}#{pluginId}\"] = metadata\n    @add(changes, {doesNotAffectPristine: true})\n\n  commit: () =>\n    clearTimeout(@_timer) if @_timer\n    @_commitChain = @_commitChain.finally =>\n      if Object.keys(@_pending).length is 0\n        return Promise.resolve(true)\n\n      @_saving = @_pending\n      @_pending = {}\n      return @callbacks.onCommit().then =>\n        @_saving = {}\n\n    return @_commitChain\n\n  applyToModel: (model) =>\n    if model\n      changesToApply = _.pairs(@_saving).concat(_.pairs(@_pending))\n      for [key, val] in changesToApply\n        if key.startsWith(MetadataChangePrefix)\n          model.applyPluginMetadata(key.split(MetadataChangePrefix).pop(), val)\n        else\n          model[key] = val\n    model\n\n###\nPublic: DraftEditingSession is a small class that makes it easy to implement components\nthat display Draft objects or allow for interactive editing of Drafts.\n\n1. It synchronously provides an instance of a draft via `draft()`, and\n   triggers whenever that draft instance has changed.\n\n2. It provides an interface for modifying the draft that transparently\n   batches changes, and ensures that the draft provided via `draft()`\n   always has pending changes applied.\n\nSection: Drafts\n###\nclass DraftEditingSession\n  @include: CoffeeHelpers.includeModule\n\n  @include Publisher\n  @include Listener\n\n  constructor: (@draftClientId, draft = null) ->\n    DraftStore ?= require('./draft-store').default\n    @listenTo DraftStore, @_onDraftChanged\n\n    @_draft = false\n    @_draftPristineBody = null\n    @_destroyed = false\n    @_undoStack = new UndoStack()\n\n    @changes = new DraftChangeSet({\n      onWillAddChanges: @changeSetWillAddChanges\n      onDidAddChanges: @changeSetDidAddChanges\n      onCommit: @changeSetCommit\n    })\n\n    if draft\n      @_draftPromise = @_setDraft(draft)\n\n    @prepare()\n\n  # Public: Returns the draft object with the latest changes applied.\n  #\n  draft: ->\n    return null if not @_draft\n    @changes.applyToModel(@_draft)\n    @_draft.clone()\n\n  # Public: Returns the initial body of the draft when it was pristine, or null if the\n  # draft was never pristine in this editing session. Useful for determining if the\n  # body is still in an unchanged / empty state.\n  #\n  draftPristineBody: ->\n    @_draftPristineBody\n\n  prepare: ->\n    @_draftPromise ?= DatabaseStore.findBy(Message, clientId: @draftClientId).include(Message.attributes.body).then (draft) =>\n      return Promise.reject(new Error(\"Draft has been destroyed.\")) if @_destroyed\n      return Promise.reject(new Error(\"Assertion Failure: Draft #{@draftClientId} not found.\")) if not draft\n      return @_setDraft(draft)\n\n  teardown: ->\n    @stopListeningToAll()\n    @changes.teardown()\n    @_destroyed = true\n\n  validateDraftForSending: =>\n    warnings = []\n    errors = []\n    allRecipients = [].concat(@_draft.to, @_draft.cc, @_draft.bcc)\n    bodyIsEmpty = @_draft.body is @draftPristineBody() or @_draft.body is \"<br>\"\n    forwarded = DraftHelpers.isForwardedMessage(@_draft)\n    hasAttachment = @_draft.files?.length > 0 or @_draft.uploads?.length > 0\n\n    for contact in allRecipients\n      if not ContactStore.isValidContact(contact)\n        errors.push(\"#{contact.email} is not a valid email address - please remove or edit it before sending.\")\n\n    if allRecipients.length is 0\n      errors.push('You need to provide one or more recipients before sending the message.')\n\n    if errors.length > 0\n      return {errors, warnings}\n\n    if @_draft.subject.length is 0\n      warnings.push('without a subject line')\n\n    if DraftHelpers.messageMentionsAttachment(@_draft) and not hasAttachment\n      warnings.push('without an attachment')\n\n    if bodyIsEmpty and not forwarded and not hasAttachment\n      warnings.push('without a body')\n\n    ## Check third party warnings added via Composer extensions\n    for extension in ExtensionRegistry.Composer.extensions()\n      continue if not extension.warningsForSending\n      warnings = warnings.concat(extension.warningsForSending({draft: @_draft}))\n\n    return {errors, warnings}\n\n  # This function makes sure the draft is attached to a valid account, and changes\n  # it's accountId if the from address does not match the account for the from\n  # address\n  #\n  # If the account is updated it makes a request to delete the draft with the\n  # old accountId\n  ensureCorrectAccount: =>\n    account = AccountStore.accountForEmail(@_draft.from[0].email)\n    if !account\n      return Promise.reject(new Error(\"DraftEditingSession::ensureCorrectAccount - you can only send drafts from a configured account.\"))\n\n    if account.id isnt @_draft.accountId\n      NylasAPIHelpers.makeDraftDeletionRequest(@_draft)\n      @changes.add({\n        accountId: account.id,\n        version: null,\n        serverId: null,\n        threadId: null,\n        replyToMessageId: null,\n      })\n      return @changes.commit()\n      .thenReturn(@)\n    return Promise.resolve(@)\n\n  _setDraft: (draft) ->\n    if !draft.body?\n      throw new Error(\"DraftEditingSession._setDraft - new draft has no body!\")\n\n    extensions = ExtensionRegistry.Composer.extensions()\n\n    # Run `extensions[].unapplyTransformsForSending`\n    fragment = document.createDocumentFragment()\n    draftBodyRootNode = document.createElement('root')\n    fragment.appendChild(draftBodyRootNode)\n    draftBodyRootNode.innerHTML = draft.body\n\n    return Promise.each extensions, (ext) ->\n      if ext.applyTransformsForSending and ext.unapplyTransformsForSending\n        Promise.resolve(ext.unapplyTransformsForSending({\n          draftBodyRootNode: draftBodyRootNode,\n          draft: draft}))\n    .then =>\n      draft.body = draftBodyRootNode.innerHTML\n      @_draft = draft\n\n      # We keep track of the draft's initial body if it's pristine when the editing\n      # session begins. This initial value powers things like \"are you sure you want\n      # to send with an empty body?\"\n      if draft.pristine\n        @_draftPristineBody = draft.body\n        @_undoStack.save(@_snapshot())\n\n      @trigger()\n      Promise.resolve(@)\n\n  _onDraftChanged: (change) ->\n    return if not change?\n\n    # We don't accept changes unless our draft object is loaded\n    return unless @_draft\n\n    # If our draft has been changed, only accept values which are present.\n    # If `body` is undefined, assume it's not loaded. Do not overwrite old body.\n    nextDraft = _.filter(change.objects, (obj) => obj.clientId is @_draft.clientId).pop()\n    if nextDraft\n      nextValues = {}\n      for key, attr of Message.attributes\n        continue if key is 'id'\n        continue if nextDraft[key] is undefined\n        nextValues[key] = nextDraft[key]\n      @_setDraft(Object.assign(new Message(), @_draft, nextValues))\n      @trigger()\n\n  changeSetCommit: () =>\n    if @_destroyed or not @_draft\n      return Promise.resolve(true)\n\n    # Set a variable here to protect againg @_draft getting set from\n    # underneath us\n    inMemoryDraft = @_draft\n\n    DatabaseStore.inTransaction (t) =>\n      t.findBy(Message, clientId: inMemoryDraft.clientId).include(Message.attributes.body).then (draft) =>\n        draft ?= inMemoryDraft\n        updatedDraft = @changes.applyToModel(draft)\n        return t.persistModel(updatedDraft)\n\n  # Undo / Redo\n\n  changeSetWillAddChanges: (changes) =>\n    return if @_restoring\n    hasBeen300ms = Date.now() - @_lastAddTimestamp > 300\n    hasChangedFields = !_.isEqual(Object.keys(changes), @_lastChangedFields)\n\n    @_lastChangedFields = Object.keys(changes)\n    @_lastAddTimestamp = Date.now()\n    if hasBeen300ms || hasChangedFields\n      @_undoStack.save(@_snapshot())\n\n  changeSetDidAddChanges: =>\n    return if @_destroyed\n    if !@_draft\n      throw new Error(\"DraftChangeSet was modified before the draft was prepared.\")\n\n    @changes.applyToModel(@_draft)\n    @trigger()\n\n  restoreSnapshot: (snapshot) =>\n    return unless snapshot\n    @_restoring = true\n    @changes.add(snapshot.draft)\n    if @_composerViewSelectionRestore\n      @_composerViewSelectionRestore(snapshot.selection)\n    @_restoring = false\n\n  undo: =>\n    @restoreSnapshot(@_undoStack.saveAndUndo(@_snapshot()))\n\n  redo: =>\n    @restoreSnapshot(@_undoStack.redo())\n\n  _snapshot: =>\n    snapshot = {\n      selection: @_composerViewSelectionRetrieve?()\n      draft: Object.assign({}, @draft())\n    }\n    for {pluginId, value} in snapshot.draft.pluginMetadata\n      snapshot.draft[\"#{MetadataChangePrefix}#{pluginId}\"] = value\n    delete snapshot.draft.pluginMetadata\n    return snapshot\n\n\nDraftEditingSession.DraftChangeSet = DraftChangeSet\n\nmodule.exports = DraftEditingSession\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/draft-factory.coffee",
    "content": "_ = require 'underscore'\npath = require 'path'\n\n{FileDownloadStore} = require 'nylas-exports'\n\nActions = require('../actions').default\nDatabaseStore = require('./database-store').default\nAccountStore = require('./account-store').default\nContactStore = require './contact-store'\nMessageStore = require './message-store'\nFocusedPerspectiveStore = require('./focused-perspective-store').default\n\nDraftStore = null\nDraftHelpers = require('./draft-helpers').default\n\nThread = require('../models/thread').default\nContact = require('../models/contact').default\nMessage = require('../models/message').default\nUtils = require '../models/utils'\n\n{subjectWithPrefix} = require '../models/utils'\nDOMUtils = require '../../dom-utils'\n\nclass DraftFactory\n  createDraft: (fields = {}) =>\n    account = @_accountForNewDraft()\n    Promise.resolve(new Message(_.extend({\n      body: ''\n      subject: ''\n      clientId: Utils.generateTempId()\n      from: [account.defaultMe()]\n      date: (new Date)\n      draft: true\n      pristine: true\n      accountId: account.id\n    }, fields)))\n\n  createDraftForMailto: (urlString) =>\n    account = @_accountForNewDraft()\n\n    try\n      urlString = decodeURI(urlString)\n\n    match = /mailto:\\/*([^\\?\\&]*)((.|\\n|\\r)*)/.exec(urlString)\n    if not match\n      return Promise.reject(new Error(\"#{urlString} is not a valid mailto URL.\"))\n\n    [whole, to, queryString] = match\n\n    if to.length > 0 and to.indexOf('@') is -1\n      to = decodeURIComponent(to)\n\n    # /many/ mailto links are malformed and do things like:\n    #   &body=https://github.com/atom/electron/issues?utf8=&q=is%3Aissue+is%3Aopen+123&subject=...\n    #   (note the unescaped ? and & in the URL).\n    #\n    # To account for these scenarios, we parse the query string manually and only\n    # split on params we expect to be there. (Jumping from &body= to &subject=\n    # in the above example.) We only decode values when they appear to be entirely\n    # URL encoded. (In the above example, decoding the body would cause the URL\n    # to fall apart.)\n    #\n    query = {}\n    query.to = to\n\n    querySplit = /[&|?](subject|body|cc|to|from|bcc)+\\s*=/gi\n\n    openKey = null\n    openValueStart = null\n\n    until match is null\n      match = querySplit.exec(queryString)\n      openValueEnd = match?.index || queryString.length\n\n      if openKey\n        value = queryString.substr(openValueStart, openValueEnd - openValueStart)\n        valueIsntEscaped = value.indexOf('?') isnt -1 or value.indexOf('&') isnt -1\n        try\n          value = decodeURIComponent(value) unless valueIsntEscaped\n        query[openKey] = value\n\n      if match\n        openKey = match[1].toLowerCase()\n        openValueStart = querySplit.lastIndex\n\n    contacts = {}\n    for attr in ['to', 'cc', 'bcc']\n      if query[attr]\n        contacts[attr] = ContactStore.parseContactsInString(query[attr])\n\n    if query.body\n      query.body = query.body.replace(/[\\n\\r]/g, '<br/>')\n\n    Promise.props(contacts).then (contacts) =>\n      @createDraft(_.extend(query, contacts))\n\n  createOrUpdateDraftForReply: ({message, thread, type, behavior}) =>\n    unless type in ['reply', 'reply-all']\n      throw new Error(\"createOrUpdateDraftForReply called with #{type}, not reply or reply-all\")\n\n    @candidateDraftForUpdating(message, behavior).then (existingDraft) =>\n      if existingDraft\n        @updateDraftForReply(existingDraft, {message, thread, type})\n      else\n        @createDraftForReply({message, thread, type})\n\n  createDraftForReply: ({message, thread, type}) =>\n    if type is 'reply'\n      {to, cc} = message.participantsForReply()\n    else if type is 'reply-all'\n      {to, cc} = message.participantsForReplyAll()\n\n    @createDraft(\n      subject: subjectWithPrefix(message.subject, 'Re:')\n      to: to,\n      cc: cc,\n      from: [@_fromContactForReply(message)],\n      threadId: thread.id,\n      accountId: message.accountId,\n      replyToMessageId: message.id,\n      body: \"\" # quoted html is managed by the composer via the replyToMessageId\n    )\n\n  createDraftForForward: ({thread, message}) =>\n    # Start downloading the attachments, if they haven't been already\n    message.files.forEach((f) => Actions.fetchFile(f))\n\n    contactsAsHtml = (cs) ->\n      DOMUtils.escapeHTMLCharacters(_.invoke(cs, \"toString\").join(\", \"))\n    fields = []\n    fields.push(\"From: #{contactsAsHtml(message.from)}\") if message.from.length > 0\n    fields.push(\"Subject: #{message.subject}\")\n    fields.push(\"Date: #{message.formattedDate()}\")\n    fields.push(\"To: #{contactsAsHtml(message.to)}\") if message.to.length > 0\n    fields.push(\"Cc: #{contactsAsHtml(message.cc)}\") if message.cc.length > 0\n\n    DraftHelpers.prepareBodyForQuoting(message.body).then (body) =>\n      @createDraft(\n        subject: subjectWithPrefix(message.subject, 'Fwd:')\n        from: [@_fromContactForReply(message)],\n        threadId: thread.id,\n        accountId: message.accountId,\n        body: \"\"\"\n          <br><br>\n          <div class=\"gmail_quote\">\n            <br>\n            ---------- Forwarded message ---------\n            <br><br>\n            #{fields.join('<br>')}\n            <br><br>\n            #{body}\n          </div>\"\"\"\n      ).then (draft) =>\n        draft.uploads = message.files.map((f) =>\n          {fileId, filename, filesize, targetPath} = FileDownloadStore.getDownloadDataForFile(f.id)\n          # Return an object that can act as an Upload instance.\n          return (\n            messageClientId: draft.clientId,\n            id: fileId,\n            filename: filename,\n            size: filesize,\n            targetPath: targetPath,\n            targetDir: path.dirname(targetPath)\n          )\n        )\n        return draft\n\n\n  candidateDraftForUpdating: (message, behavior) =>\n    if behavior not in ['prefer-existing-if-pristine', 'prefer-existing']\n      return Promise.resolve(null)\n\n    getMessages = DatabaseStore.findAll(Message, {threadId: message.threadId})\n    if message.threadId is MessageStore.threadId()\n      getMessages = Promise.resolve(MessageStore.items())\n\n    getMessages.then (messages) =>\n      candidateDrafts = messages.filter (other) =>\n        other.replyToMessageId is message.id and other.draft is true\n\n      if candidateDrafts.length is 0\n        return Promise.resolve(null)\n\n      if behavior is 'prefer-existing'\n        return Promise.resolve(candidateDrafts.pop())\n\n      else if behavior is 'prefer-existing-if-pristine'\n        DraftStore ?= require('./draft-store').default\n        return Promise.all(candidateDrafts.map (candidateDraft) =>\n          DraftStore.sessionForClientId(candidateDraft.clientId)\n        ).then (sessions) =>\n          for session in sessions\n            if session.draft().pristine\n              return Promise.resolve(session.draft())\n          return Promise.resolve(null)\n\n\n  updateDraftForReply: (draft, {type, message}) =>\n    unless message and draft\n      return Promise.reject(\"updateDraftForReply: Expected message and existing draft.\")\n\n    updated = {to: [].concat(draft.to), cc: [].concat(draft.cc)}\n    replySet = message.participantsForReply()\n    replyAllSet = message.participantsForReplyAll()\n\n    if type is 'reply'\n      targetSet = replySet\n\n      # Remove participants present in the reply-all set and not the reply set\n      for key in ['to', 'cc']\n        updated[key] = _.reject updated[key], (contact) ->\n          inReplySet = _.findWhere(replySet[key], {email: contact.email})\n          inReplyAllSet = _.findWhere(replyAllSet[key], {email: contact.email})\n          return inReplyAllSet and not inReplySet\n    else\n      # Add participants present in the reply-all set and not on the draft\n      # Switching to reply-all shouldn't really ever remove anyone.\n      targetSet = replyAllSet\n\n    for key in ['to', 'cc']\n      for contact in targetSet[key]\n        updated[key].push(contact) unless _.findWhere(updated[key], {email: contact.email})\n\n    draft.to = updated.to\n    draft.cc = updated.cc\n\n    DatabaseStore.inTransaction (t) =>\n      t.persistModel(draft)\n    .thenReturn(draft)\n\n  _fromContactForReply: (message) =>\n    account = AccountStore.accountForId(message.accountId)\n    defaultMe = account.defaultMe()\n    result = defaultMe\n\n    for aliasString in account.aliases\n      alias = account.meUsingAlias(aliasString)\n      for recipient in [].concat(message.to, message.cc)\n        emailIsNotDefault = alias.email isnt defaultMe.email\n        emailsMatch = recipient.email is alias.email\n        nameIsNotDefault = alias.name isnt defaultMe.name\n        namesMatch = recipient.name is alias.name\n\n        # No better match is possible\n        if emailsMatch and emailIsNotDefault and namesMatch and nameIsNotDefault\n          return alias\n\n        # A better match is possible. eg: the user may have two aliases with the same\n        # email but different phrases, and we'll get an exact match on the other one.\n        # Continue iterating and wait to see.\n        if (emailsMatch and emailIsNotDefault) or (namesMatch and nameIsNotDefault)\n          result = alias\n\n    return result\n\n  _accountForNewDraft: =>\n    defAccountId = NylasEnv.config.get('core.sending.defaultAccountIdForSend')\n    account = AccountStore.accountForId(defAccountId)\n    if account\n      account\n    else\n      focusedAccountId = FocusedPerspectiveStore.current().accountIds[0]\n      if focusedAccountId\n        AccountStore.accountForId(focusedAccountId)\n      else\n        AccountStore.accounts()[0]\n\nmodule.exports = new DraftFactory()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/draft-helpers.es6",
    "content": "import {DatabaseStore} from 'nylas-exports'\nimport Message from '../models/message'\nimport * as ExtensionRegistry from '../../registries/extension-registry'\nimport DOMUtils from '../../dom-utils'\n\nimport QuotedHTMLTransformer from '../../services/quoted-html-transformer'\nimport InlineStyleTransformer from '../../services/inline-style-transformer'\nimport SanitizeTransformer from '../../services/sanitize-transformer'\nimport MessageUtils from '../models/message-utils'\n\nclass DraftHelpers {\n  AllowedTransformFields = ['to', 'from', 'cc', 'bcc', 'subject', 'body']\n\n  DraftNotFoundError = class DraftNotFoundError extends Error { }\n\n  /**\n   * Returns true if the message contains \"Forwarded\" or \"Fwd\" in the first\n   * 250 characters.  A strong indicator that the quoted text should be\n   * shown. Needs to be limited to first 250 to prevent replies to\n   * forwarded messages from also being expanded.\n  */\n  isForwardedMessage({body, subject} = {}) {\n    let bodyFwd = false\n    let bodyForwarded = false\n    let subjectFwd = false\n\n    if (body) {\n      const indexFwd = body.search(/fwd/i)\n      const indexForwarded = body.search(/forwarded/i)\n      bodyForwarded = indexForwarded >= 0 && indexForwarded < 250\n      bodyFwd = indexFwd >= 0 && indexFwd < 250\n    }\n    if (subject) {\n      subjectFwd = subject.slice(0, 3).toLowerCase() === \"fwd\"\n    }\n\n    return bodyForwarded || bodyFwd || subjectFwd\n  }\n\n  shouldAppendQuotedText({body = '', replyToMessageId = false} = {}) {\n    return replyToMessageId &&\n      !body.includes('<div id=\"n1-quoted-text-marker\">') &&\n      !body.includes(`nylas-quote-id-${replyToMessageId}`)\n  }\n\n  prepareBodyForQuoting(body = \"\") {\n    // TODO: Fix inline images\n    const cidRE = MessageUtils.cidRegexString;\n\n    // Be sure to match over multiple lines with [\\s\\S]*\n    // Regex explanation here: https://regex101.com/r/vO6eN2/1\n    body.replace(new RegExp(`<img.*${cidRE}[\\\\s\\\\S]*?>`, \"igm\"), \"\")\n\n    return InlineStyleTransformer.run(body).then((inlineStyled) =>\n      SanitizeTransformer.run(inlineStyled, SanitizeTransformer.Preset.UnsafeOnly)\n    )\n  }\n\n  messageMentionsAttachment({body} = {}) {\n    if (body == null) { throw new Error('DraftHelpers::messageMentionsAttachment - Message has no body loaded') }\n    let cleaned = QuotedHTMLTransformer.removeQuotedHTML(body.toLowerCase().trim());\n    const signatureIndex = cleaned.indexOf('<signature>');\n    if (signatureIndex !== -1) {\n      cleaned = cleaned.substr(0, signatureIndex - 1);\n    }\n    return (cleaned.indexOf(\"attach\") >= 0);\n  }\n\n  async refreshDraftReference(clientId) {\n    const message = await DatabaseStore\n      .findBy(Message, {clientId: clientId})\n      .include(Message.attributes.body)\n\n    if (!message || !message.draft) {\n      throw new this.DraftNotFoundError()\n    }\n\n    return message\n  }\n\n  async removeStaleUploads(draft) {\n    if (!(draft.uploads instanceof Array) || draft.uploads.length === 0) {\n      // The async keyword makes it so this is returned as a promise, which\n      // allows us to always treat the return value of this function as a\n      // promise-like object.\n      return draft;\n    }\n    return DatabaseStore.inTransaction(async (t) => {\n      // Inline uploads that are no longer referenced in the body are stale\n      draft.uploads = draft.uploads.filter(u => {\n        return !(u.inline && !draft.body.includes(`cid:${u.id}`))\n      });\n\n      await t.persistModel(draft);\n      return draft;\n    });\n  }\n\n  appendQuotedTextToDraft(draft) {\n    const query = DatabaseStore.find(Message, draft.replyToMessageId).include(Message.attributes.body);\n\n    return query.then((prevMessage) => {\n      if (!prevMessage) {\n        return Promise.resolve(draft);\n      }\n      return this.prepareBodyForQuoting(prevMessage.body).then((prevBodySanitized) => {\n        draft.body = `${draft.body}\n          <div class=\"gmail_quote nylas-quote nylas-quote-id-${draft.replyToMessageId}\">\n            <br>\n            ${DOMUtils.escapeHTMLCharacters(prevMessage.replyAttributionLine())}\n            <br>\n            <blockquote class=\"gmail_quote\"\n              style=\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\">\n              ${prevBodySanitized}\n            </blockquote>\n          </div>`;\n        return Promise.resolve(draft);\n      });\n    })\n  }\n\n  applyExtensionTransforms(draft) {\n    const extensions = ExtensionRegistry.Composer.extensions();\n\n    const fragment = document.createDocumentFragment();\n    const draftBodyRootNode = document.createElement('root');\n    fragment.appendChild(draftBodyRootNode);\n    draftBodyRootNode.innerHTML = draft.body;\n\n    return Promise.each(extensions, (ext) => {\n      const extApply = ext.applyTransformsForSending;\n      const extUnapply = ext.unapplyTransformsForSending;\n\n      if (!extApply || !extUnapply) {\n        return Promise.resolve();\n      }\n\n      return Promise.resolve(extUnapply({draft, draftBodyRootNode})).then(() => {\n        return Promise.resolve(extApply({draft, draftBodyRootNode}));\n      });\n    }).then(() => {\n      draft.body = draftBodyRootNode.innerHTML;\n      return draft;\n    });\n  }\n\n  async finalizeDraft(session) {\n    await session.ensureCorrectAccount()\n    const transformed = await this.applyExtensionTransforms(session.draft())\n    let draft;\n    if (!transformed.replyToMessageId || !this.shouldAppendQuotedText(transformed)) {\n      draft = transformed;\n    } else {\n      draft = await this.appendQuotedTextToDraft(transformed);\n    }\n    draft = await this.removeStaleUploads(draft);\n    await DatabaseStore.inTransaction((t) => t.persistModel(draft))\n    return draft;\n  }\n}\n\nexport default new DraftHelpers();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/draft-store-extension.coffee",
    "content": "###\nPublic: DraftStoreExtension is deprecated. Use {ComposerExtension} instead.\nSection: Extensions\n###\nclass DraftStoreExtension\n\nmodule.exports = DraftStoreExtension\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/draft-store.es6",
    "content": "import _ from 'underscore';\nimport {ipcRenderer} from 'electron';\nimport NylasStore from 'nylas-store';\nimport DraftEditingSession from './draft-editing-session';\nimport DraftHelpers from './draft-helpers';\nimport DraftFactory from './draft-factory';\nimport DatabaseStore from './database-store';\nimport SendActionsStore from './send-actions-store';\nimport TaskQueueStatusStore from './task-queue-status-store';\nimport FocusedContentStore from './focused-content-store';\nimport BaseDraftTask from '../tasks/base-draft-task';\nimport PerformSendActionTask from '../tasks/perform-send-action-task';\nimport SyncbackMetadataTask from '../tasks/syncback-metadata-task'\nimport DestroyDraftTask from '../tasks/destroy-draft-task';\nimport Thread from '../models/thread';\nimport Message from '../models/message';\nimport Utils from '../models/utils';\nimport Actions from '../actions';\nimport SoundRegistry from '../../registries/sound-registry';\nimport * as ExtensionRegistry from '../../registries/extension-registry';\n\n\nconst {DefaultSendActionKey} = SendActionsStore\n/*\nPublic: DraftStore responds to Actions that interact with Drafts and exposes\npublic getter methods to return Draft objects and sessions.\n\nIt also creates and queues {Task} objects to persist changes to the Nylas\nAPI.\n\nRemember that a \"Draft\" is actually just a \"Message\" with `draft: true`.\n\nSection: Drafts\n*/\nclass DraftStore extends NylasStore {\n\n  constructor() {\n    super()\n    this.listenTo(DatabaseStore, this._onDataChanged);\n    this.listenTo(Actions.composeReply, this._onComposeReply);\n    this.listenTo(Actions.composeForward, this._onComposeForward);\n    this.listenTo(Actions.composePopoutDraft, this._onPopoutDraftClientId);\n    this.listenTo(Actions.composeNewBlankDraft, this._onPopoutBlankDraft);\n    this.listenTo(Actions.composeNewDraftToRecipient, this._onPopoutNewDraftToRecipient);\n    this.listenTo(Actions.draftDeliveryFailed, this._onSendDraftFailed);\n    this.listenTo(Actions.draftDeliverySucceeded, this._onSendDraftSuccess);\n    this.listenTo(Actions.didCancelSendAction, this._onDidCancelSendAction);\n    this.listenTo(Actions.sendQuickReply, this._onSendQuickReply);\n\n    if (NylasEnv.isMainWindow()) {\n      ipcRenderer.on('new-message', () => {\n        Actions.composeNewBlankDraft();\n      });\n    }\n\n    // Remember that these two actions only fire in the current window and\n    // are picked up by the instance of the DraftStore in the current\n    // window.\n    this.listenTo(Actions.finalizeDraftAndSyncbackMetadata, this._onFinalizeDraftAndSyncbackMetadata);\n    this.listenTo(Actions.sendDraft, this._onSendDraft);\n    this.listenTo(Actions.destroyDraft, this._onDestroyDraft);\n    this.listenTo(Actions.removeFile, this._onRemoveFile);\n\n    NylasEnv.onBeforeUnload(this._onBeforeUnload);\n\n    this._draftSessions = {};\n\n    // We would ideally like to be able to calculate the sending state\n    // declaratively from the existence of the SendDraftTask on the\n    // TaskQueue.\n    //\n    // Unfortunately it takes a while for the Task to end up on the Queue.\n    // Before it's there, the Draft session is fetched, changes are\n    // applied, it's saved to the DB, and performLocal is run. In the\n    // meantime, several triggers from the DraftStore may fire (like when\n    // it's saved to the DB). At the time of those triggers, the task is\n    // not yet on the Queue and the DraftStore incorrectly says\n    // `isSendingDraft` is false.\n    //\n    // As a result, we keep track of the intermediate time between when we\n    // request to queue something, and when it appears on the queue.\n    this._draftsSending = {};\n\n    ipcRenderer.on('mailto', this._onHandleMailtoLink);\n    ipcRenderer.on('mailfiles', this._onHandleMailFiles);\n  }\n\n  /**\n  Fetch a {DraftEditingSession} for displaying and/or editing the\n  draft with `clientId`.\n\n  @param {String} clientId - The clientId of the draft.\n  @returns {Promise} - Resolves to an {DraftEditingSession} for the draft once it has been prepared\n  */\n  sessionForClientId(clientId) {\n    if (!clientId) {\n      throw new Error(\"DraftStore::sessionForClientId requires a clientId\");\n    }\n    if (this._draftSessions[clientId] == null) {\n      this._draftSessions[clientId] = this._createSession(clientId);\n    }\n    return this._draftSessions[clientId].prepare();\n  }\n\n  // Public: Look up the sending state of the given draftClientId.\n  // In popout windows the existance of the window is the sending state.\n  isSendingDraft(draftClientId) {\n    return this._draftsSending[draftClientId] || false;\n  }\n\n\n  _doneWithSession(session) {\n    session.teardown();\n    delete this._draftSessions[session.draftClientId];\n  }\n\n  _cleanupAllSessions() {\n    _.each(this._draftSessions, (session) => {\n      this._doneWithSession(session)\n    })\n  }\n\n  _onBeforeUnload = (readyToUnload) => {\n    const promises = [];\n\n    // Normally we'd just append all promises, even the ones already\n    // fulfilled (nothing to save), but in this case we only want to\n    // block window closing if we have to do real work. Calling\n    // window.close() within on onbeforeunload could do weird things.\n    _.each(this._draftSessions, (session) => {\n      const draft = session.draft()\n      if (draft && draft.pristine) {\n        Actions.queueTask(new DestroyDraftTask(session.draftClientId));\n      } else {\n        promises.push(session.changes.commit());\n      }\n    })\n\n    if (promises.length > 0) {\n      // Important: There are some scenarios where all the promises resolve instantly.\n      // Firing NylasEnv.close() does nothing if called within an existing beforeUnload\n      // handler, so we need to always defer by one tick before re-firing close.\n      // NOTE: this replaces Promise.settle:\n      // http://bluebirdjs.com/docs/api/reflect.html\n      Promise.all(promises.map(p => p.reflect())).then(() => {\n        this._draftSessions = {};\n        // We have to wait for accumulateAndTrigger() in the DatabaseStore to\n        // send events to ActionBridge before closing the window.\n        setTimeout(readyToUnload, 15);\n      });\n\n      // Stop and wait before closing\n      return false;\n    }\n    // Continue closing\n    return true;\n  }\n\n  _onDataChanged = (change) => {\n    if (change.objectClass !== Message.name) { return; }\n    const containsDraft = change.objects.some((msg) => msg.draft);\n    if (!containsDraft) { return; }\n    this.trigger(change);\n  }\n\n  _onSendQuickReply = ({thread, threadId, message, messageId}, body) => {\n    return Promise.props(\n      this._modelifyContext({thread, threadId, message, messageId})\n    )\n    .then(({message: m, thread: t}) => {\n      return DraftFactory.createDraftForReply({message: m, thread: t, type: 'reply'});\n    })\n    .then((draft) => {\n      draft.body = `${body}\\n\\n${draft.body}`\n      draft.pristine = false;\n      return DatabaseStore.inTransaction((t) => {\n        return t.persistModel(draft);\n      })\n      .then(() => {\n        Actions.sendDraft(draft.clientId);\n      });\n    });\n  }\n\n  _onComposeReply = ({thread, threadId, message, messageId, popout, type, behavior}) => {\n    // Actions.recordUserEvent(\"Draft Created\", {type});\n    return Promise.props(\n      this._modelifyContext({thread, threadId, message, messageId})\n    )\n    .then(({message: m, thread: t}) => {\n      if (['reply', 'reply-all'].includes(type)) {\n        NylasEnv.timer.start(`compose-reply-${m.id}`)\n      }\n      return DraftFactory.createOrUpdateDraftForReply({message: m, thread: t, type, behavior});\n    })\n    .then(draft => {\n      return this._finalizeAndPersistNewMessage(draft, {popout});\n    });\n  }\n\n  _onComposeForward = ({thread, threadId, message, messageId, popout}) => {\n    // Actions.recordUserEvent(\"Draft Created\", {type: \"forward\"});\n    return Promise.props(\n      this._modelifyContext({thread, threadId, message, messageId})\n    )\n    .then(({thread: t, message: m}) => {\n      NylasEnv.timer.start(`compose-forward-${t.id}`)\n      return DraftFactory.createDraftForForward({thread: t, message: m})\n    })\n    .then((draft) => {\n      return this._finalizeAndPersistNewMessage(draft, {popout});\n    });\n  }\n\n  _modelifyContext({thread, threadId, message, messageId}) {\n    const queries = {};\n    if (thread) {\n      if (!(thread instanceof Thread)) {\n        throw new Error(\"newMessageWithContext: `thread` present, expected a Model. Maybe you wanted to pass `threadId`?\");\n      }\n      queries.thread = thread;\n    } else {\n      queries.thread = DatabaseStore.find(Thread, threadId);\n    }\n\n    if (message) {\n      if (!(message instanceof Message)) {\n        throw new Error(\"newMessageWithContext: `message` present, expected a Model. Maybe you wanted to pass `messageId`?\");\n      }\n      queries.message = message;\n    } else if (messageId != null) {\n      queries.message = DatabaseStore\n        .find(Message, messageId)\n        .include(Message.attributes.body);\n    } else {\n      queries.message = DatabaseStore\n        .findBy(Message, {threadId: threadId || thread.id})\n        .order(Message.attributes.date.descending())\n        .limit(1)\n        .include(Message.attributes.body);\n    }\n\n    return queries;\n  }\n\n  _finalizeAndPersistNewMessage(draft, {popout} = {}) {\n    // Give extensions an opportunity to perform additional setup to the draft\n    ExtensionRegistry.Composer.extensions().forEach((extension) => {\n      if (!extension.prepareNewDraft) { return; }\n      extension.prepareNewDraft({draft});\n    })\n\n    // Optimistically create a draft session and hand it the draft so that it\n    // doesn't need to do a query for it a second from now when the composer wants it.\n    this._createSession(draft.clientId, draft);\n\n    return DatabaseStore.inTransaction((t) => {\n      return t.persistModel(draft);\n    })\n    .then(() => {\n      if (popout) {\n        this._onPopoutDraftClientId(draft.clientId);\n      } else {\n        Actions.focusDraft({draftClientId: draft.clientId});\n      }\n    })\n    .thenReturn({draftClientId: draft.clientId, draft});\n  }\n\n  _createSession(clientId, draft) {\n    this._draftSessions[clientId] = new DraftEditingSession(clientId, draft);\n    return this._draftSessions[clientId]\n  }\n\n  _onPopoutNewDraftToRecipient = (contact) => {\n    // Actions.recordUserEvent(\"Draft Created\", {type: \"new\"});\n    const timerId = Utils.generateTempId()\n    NylasEnv.timer.start(`open-composer-window-${timerId}`);\n    return DraftFactory.createDraft({to: [contact]}).then((draft) => {\n      return this._finalizeAndPersistNewMessage(draft).then(({draftClientId}) => {\n        return this._onPopoutDraftClientId(draftClientId, {timerId, newDraft: true});\n      });\n    });\n  }\n\n  _onPopoutBlankDraft = () => {\n    // Actions.recordUserEvent(\"Draft Created\", {type: \"new\"});\n    const timerId = Utils.generateTempId()\n    NylasEnv.timer.start(`open-composer-window-${timerId}`);\n    return DraftFactory.createDraft().then((draft) => {\n      return this._finalizeAndPersistNewMessage(draft).then(({draftClientId}) => {\n        return this._onPopoutDraftClientId(draftClientId, {timerId, newDraft: true});\n      });\n    });\n  }\n\n  _onHandleMailtoLink = (event, urlString) => {\n    // Actions.recordUserEvent(\"Draft Created\", {type: \"mailto\"});\n    const timerId = Utils.generateTempId()\n    NylasEnv.timer.start(`open-composer-window-${timerId}`);\n    return DraftFactory.createDraftForMailto(urlString).then((draft) => {\n      return this._finalizeAndPersistNewMessage(draft).then(({draftClientId}) => {\n        return this._onPopoutDraftClientId(draftClientId, {timerId, newDraft: true});\n      });\n    }).catch((err) => {\n      NylasEnv.showErrorDialog(err.toString())\n    });\n  }\n\n  _onHandleMailFiles = (event, paths) => {\n    // Actions.recordUserEvent(\"Draft Created\", {type: \"dropped-file-in-dock\"});\n    const timerId = Utils.generateTempId()\n    NylasEnv.timer.start(`open-composer-window-${timerId}`);\n    return DraftFactory.createDraft().then((draft) => {\n      return this._finalizeAndPersistNewMessage(draft);\n    })\n    .then(({draftClientId}) => {\n      let remaining = paths.length;\n      const callback = () => {\n        remaining -= 1;\n        if (remaining === 0) {\n          this._onPopoutDraftClientId(draftClientId, {timerId});\n        }\n      };\n\n      paths.forEach((path) => {\n        Actions.addAttachment({\n          filePath: path,\n          messageClientId: draftClientId,\n          onUploadCreated: callback,\n        });\n      })\n    });\n  }\n\n  _onPopoutDraftClientId = (draftClientId, options = {}) => {\n    if (draftClientId == null) {\n      throw new Error(\"DraftStore::onPopoutDraftId - You must provide a draftClientId\");\n    }\n    const {timerId} = options\n    if (!timerId) {\n      NylasEnv.timer.start(`open-composer-window-${draftClientId}`);\n    }\n\n    const title = options.newDraft ? \"New Message\" : \"Message\";\n    return this.sessionForClientId(draftClientId).then((session) => {\n      return session.changes.commit().then(() => {\n        const draftJSON = session.draft().toJSON();\n        // Since we pass a windowKey, if the popout composer draft already\n        // exists we'll simply show that one instead of spawning a whole new\n        // window.\n        NylasEnv.newWindow({\n          title,\n          hidden: true, // We manually show in ComposerWithWindowProps::onDraftReady\n          timerId: timerId || draftClientId,\n          windowKey: `composer-${draftClientId}`,\n          windowType: 'composer-preload',\n          windowProps: _.extend(options, {draftClientId, draftJSON}),\n        });\n      });\n    });\n  }\n\n  _onDestroyDraft = (draftClientId) => {\n    const session = this._draftSessions[draftClientId];\n\n    // Immediately reset any pending changes so no saves occur\n    if (session) {\n      this._doneWithSession(session);\n    }\n\n    // Stop any pending tasks related ot the draft\n    TaskQueueStatusStore.queue().forEach((task) => {\n      if (task instanceof BaseDraftTask && task.draftClientId === draftClientId) {\n        Actions.dequeueTask(task.id);\n      }\n    })\n\n    // Queue the task to destroy the draft\n    Actions.queueTask(new DestroyDraftTask(draftClientId));\n\n    if (NylasEnv.isComposerWindow()) {\n      NylasEnv.close();\n    }\n  }\n\n  _onFinalizeDraftAndSyncbackMetadata = async (draftClientId) => {\n    const session = await this.sessionForClientId(draftClientId)\n    const draft = await DraftHelpers.finalizeDraft(session)\n    for (const {pluginId} of draft.pluginMetadata) {\n      const task = new SyncbackMetadataTask(draft.clientId, Message.name, pluginId);\n      Actions.queueTask(task);\n    }\n  }\n\n  _onSendDraft = (draftClientId, sendActionKey = DefaultSendActionKey) => {\n    this._draftsSending[draftClientId] = true;\n    return this.sessionForClientId(draftClientId).then((session) => {\n      return DraftHelpers.finalizeDraft(session)\n      .then(() => {\n        Actions.queueTask(new PerformSendActionTask(draftClientId, sendActionKey));\n        this._doneWithSession(session);\n        if (NylasEnv.config.get(\"core.sending.sounds\")) {\n          SoundRegistry.playSound('hit-send');\n        }\n        if (NylasEnv.isComposerWindow()) {\n          NylasEnv.close();\n        }\n      });\n    });\n  }\n\n  __testExtensionTransforms() {\n    const clientId = NylasEnv.getWindowProps().draftClientId;\n    return this.sessionForClientId(clientId).then((session) => {\n      return this._prepareForSyncback(session).then(() => {\n        window.__draft = session.draft();\n        console.log(\"Done transforming draft. Available at window.__draft\");\n      });\n    });\n  }\n\n  _onRemoveFile = ({file, messageClientId}) => {\n    return this.sessionForClientId(messageClientId).then((session) => {\n      let files = _.clone(session.draft().files) || [];\n      files = _.reject(files, (f) => f.id === file.id);\n      session.changes.add({files});\n      return session.changes.commit();\n    });\n  }\n\n  _onDidCancelSendAction = ({draftClientId}) => {\n    delete this._draftsSending[draftClientId];\n    this.trigger(draftClientId);\n  }\n\n  _onSendDraftSuccess = ({draftClientId}) => {\n    delete this._draftsSending[draftClientId];\n    this.trigger(draftClientId);\n  }\n\n  _onSendDraftFailed = ({draftClientId, threadId, errorMessage, errorDetail}) => {\n    this._draftsSending[draftClientId] = false;\n    this.trigger(draftClientId);\n    if (NylasEnv.isMainWindow()) {\n      // We delay so the view has time to update the restored draft. If we\n      // don't delay the modal may come up in a state where the draft looks\n      // like it hasn't been restored or has been lost.\n      //\n      // We also need to delay because the old draft window needs to fully\n      // close. It takes windows currently (June 2016) 100ms to close by\n      setTimeout(() => {\n        this._notifyUserOfError({draftClientId, threadId, errorMessage, errorDetail});\n      }, 300);\n    }\n  }\n\n  _notifyUserOfError({draftClientId, threadId, errorMessage, errorDetail}) {\n    const focusedThread = FocusedContentStore.focused('thread');\n    if (threadId && focusedThread && focusedThread.id === threadId) {\n      NylasEnv.showErrorDialog(errorMessage, {detail: errorDetail});\n    } else {\n      Actions.composePopoutDraft(draftClientId, {errorMessage, errorDetail});\n    }\n  }\n}\n\nexport default new DraftStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/feature-usage-store.jsx",
    "content": "import Rx from 'rx-lite'\nimport React from 'react'\nimport NylasStore from 'nylas-store'\nimport {FeatureUsedUpModal} from 'nylas-component-kit'\nimport Actions from '../actions'\nimport IdentityStore from './identity-store'\nimport TaskQueueStatusStore from './task-queue-status-store'\nimport SendFeatureUsageEventTask from '../tasks/send-feature-usage-event-task'\n\nclass NoProAccess extends Error { }\n\n/**\n * FeatureUsageStore is backed by the IdentityStore\n *\n * The billing site is responsible for returning with the Identity object\n * a usage hash that includes all supported features, their quotas for the\n * user, and the current usage of that user. We keep a cache locally\n *\n * The Identity object (aka Nylas ID or N1User) has a field called\n * `feature_usage`. The schema for `feature_usage` is computed dynamically\n * in `compute_feature_usage` here:\n * https://github.com/nylas/cloud-core/blob/master/redwood/models/n1.py#L175-207\n *\n * The schema of each feature is determined by the `FeatureUsage` model in\n * redwood here:\n * https://github.com/nylas/cloud-core/blob/master/redwood/models/feature_usage.py#L14-32\n *\n * The final schema looks like (Feb 7, 2017):\n *\n * NylasID = {\n *   ...\n *   \"feature_usage\": {\n *     \"snooze\": {\n *       \"quota\": 15,\n *       \"period\": \"monthly\",\n *       \"used_in_period\": 10,\n *       \"feature_limit_name\": \"snooze-experiment-A\",\n *     },\n *     \"send-later\": {\n *       \"quota\": 99999,\n *       \"period\": \"unlimited\",\n *       \"used_in_period\": 228,\n *       \"feature_limit_name\": \"send-later-unlimited-A\",\n *     },\n *     \"reminders\": {\n *       \"quota\": 10,\n *       \"period\": \"daily\",\n *       \"used_in_period\": 10,\n *       \"feature_limit_name\": null,\n *     },\n *   },\n *   ...\n * }\n *\n * Valid periods are:\n * 'hourly', 'daily', 'weekly', 'monthly', 'yearly', 'unlimited'\n */\nclass FeatureUsageStore extends NylasStore {\n  constructor() {\n    super()\n    this._waitForModalClose = []\n    this.NoProAccess = NoProAccess\n  }\n\n  activate() {\n    /**\n     * The IdentityStore triggers both after we update it, and when it\n     * polls for new data every several minutes or so.\n     */\n    this._disp = Rx.Observable.fromStore(IdentityStore).subscribe(() => {\n      this.trigger()\n    })\n    this._usub = Actions.closeModal.listen(this._onModalClose)\n  }\n\n  deactivate() {\n    this._disp.dispose();\n    this._usub()\n  }\n\n  async asyncUseFeature(feature, {lexicon = {}} = {}) {\n    if (IdentityStore.hasProAccess() || this._isUsable(feature)) {\n      return this._markFeatureUsed(feature)\n    }\n\n    const {headerText, rechargeText} = this._modalText(feature, lexicon)\n    Actions.openModal({\n      component: (\n        <FeatureUsedUpModal\n          modalClass={feature}\n          featureName={lexicon.displayName}\n          headerText={headerText}\n          iconUrl={lexicon.iconUrl}\n          rechargeText={rechargeText}\n        />\n      ),\n      height: 575,\n      width: 412,\n    })\n    return new Promise((resolve, reject) => {\n      this._waitForModalClose.push({resolve, reject, feature})\n    })\n  }\n\n  _onModalClose = async () => {\n    for (const {feature, resolve, reject} of this._waitForModalClose) {\n      if (IdentityStore.hasProAccess() || this._isUsable(feature)) {\n        await this._markFeatureUsed(feature)\n        resolve()\n      } else {\n        reject(new NoProAccess(feature))\n      }\n    }\n    this._waitForModalClose = []\n  }\n\n  _modalText(feature, lexicon = {}) {\n    const featureData = this._featureData(feature);\n\n    let headerText = \"\";\n    let rechargeText = \"\"\n    if (!featureData.quota) {\n      headerText = `${lexicon.displayName} not yet enabled`;\n      rechargeText = `Upgrade to Pro to use ${lexicon.displayName}`\n    } else {\n      headerText = lexicon.usedUpHeader || \"You've reached your quota\";\n      let time = \"later\";\n      if (featureData.period === \"hourly\") {\n        time = \"next hour\"\n      } else if (featureData.period === \"daily\") {\n        time = \"tomorrow\"\n      } else if (featureData.period === \"weekly\") {\n        time = \"next week\"\n      } else if (featureData.period === \"monthly\") {\n        time = \"next month\"\n      } else if (featureData.period === \"yearly\") {\n        time = \"next year\"\n      } else if (featureData.period === \"unlimited\") {\n        time = \"if you upgrade to Pro\"\n      }\n      rechargeText = `You’ll have ${featureData.quota} more ${time}`\n    }\n    return {headerText, rechargeText}\n  }\n\n  _featureData(feature) {\n    const usage = this._featureUsage()\n    if (!usage[feature]) {\n      NylasEnv.reportError(new Error(`${feature} isn't supported`));\n      return {}\n    }\n    return usage[feature]\n  }\n\n  _isUsable(feature) {\n    const usage = this._featureUsage()\n    if (!usage[feature]) {\n      NylasEnv.reportError(new Error(`${feature} isn't supported`));\n      return false\n    }\n    return usage[feature].used_in_period < usage[feature].quota\n  }\n\n  async _markFeatureUsed(featureName) {\n    const task = new SendFeatureUsageEventTask(featureName)\n    Actions.queueTask(task);\n    await TaskQueueStatusStore.waitForPerformLocal(task)\n    const feat = IdentityStore.identity().feature_usage[featureName]\n    return feat.quota - feat.used_in_period\n  }\n\n  _featureUsage() {\n    return Object.assign({}, IdentityStore.identity().feature_usage) || {}\n  }\n}\n\nexport default new FeatureUsageStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/file-download-store.es6",
    "content": "import _ from 'underscore';\nimport os from 'os';\nimport fs from 'fs';\nimport path from 'path';\nimport {ShellUtils} from 'isomorphic-core';\nimport {remote, shell} from 'electron';\nimport mkdirp from 'mkdirp';\nimport progress from 'request-progress';\nimport NylasStore from 'nylas-store';\nimport Actions from '../actions';\nimport NylasAPI from '../nylas-api';\nimport NylasAPIRequest from '../nylas-api-request';\n\n\nPromise.promisifyAll(fs);\nconst mkdirpAsync = Promise.promisify(mkdirp);\n\nconst State = {\n  Unstarted: 'unstarted',\n  Downloading: 'downloading',\n  Finished: 'finished',\n  Failed: 'failed',\n};\n\n// TODO make this list more exhaustive\nconst NonPreviewableExtensions = [\n  'jpg',\n  'bmp',\n  'gif',\n  'png',\n  'jpeg',\n  'zip',\n  'tar',\n  'gz',\n  'bz2',\n  'dmg',\n  'exe',\n  'ics',\n]\n\nconst THUMBNAIL_WIDTH = 320\n\n\n// Expose the Download class for our tests, and possibly for other things someday\nexport class Download {\n  static State = State\n\n  constructor({accountId, fileId, targetPath, filename, filesize, progressCallback, retryWithBackoff}) {\n    this.accountId = accountId;\n    this.fileId = fileId;\n    this.targetPath = targetPath;\n    this.filename = filename;\n    this.filesize = filesize;\n    this.progressCallback = progressCallback;\n    this.retryWithBackoff = retryWithBackoff || false;\n    this.timeout = 15000;\n    this.maxTimeout = 2 * 60 * 1000;\n    this.attempts = 0;\n    this.maxAttempts = 10;\n    if (!this.accountId) {\n      throw new Error(\"Download.constructor: You must provide a non-empty accountId.\");\n    }\n    if (!this.filename || this.filename.length === 0) {\n      throw new Error(\"Download.constructor: You must provide a non-empty filename.\");\n    }\n    if (!this.fileId) {\n      throw new Error(\"Download.constructor: You must provide a fileID to download.\");\n    }\n    if (!this.targetPath) {\n      throw new Error(\"Download.constructor: You must provide a target path to download.\");\n    }\n\n    this.percent = 0;\n    this.promise = null;\n    this.state = State.Unstarted;\n  }\n\n  // We need to pass a plain object so we can have fresh references for the\n  // React views while maintaining the single object with the running\n  // request.\n  data() {\n    return Object.freeze(_.clone({\n      state: this.state,\n      fileId: this.fileId,\n      percent: this.percent,\n      filename: this.filename,\n      filesize: this.filesize,\n      targetPath: this.targetPath,\n    }));\n  }\n\n  run() {\n    // If run has already been called, return the existing promise. Never\n    // initiate multiple downloads for the same file\n    if (this.promise) { return this.promise; }\n\n    // Note: we must resolve or reject with `this`\n    this.promise = new Promise((resolve, reject) => {\n      const stream = fs.createWriteStream(this.targetPath);\n      this.state = State.Downloading;\n\n      let startRequest = null;\n\n      const before = Date.now();\n\n      const onFailed = (err) => {\n        Actions.recordPerfMetric({\n          action: 'file-download',\n          accountId: this.accountId,\n          actionTimeMs: Date.now() - before,\n          maxValue: 10 * 60 * 1000,\n          succeeded: false,\n        })\n        this.request = null;\n        stream.end();\n        if (!this.retryWithBackoff || this.attempts >= this.maxAttempts) {\n          this.state = State.Failed;\n          if (fs.existsSync(this.targetPath)) {\n            fs.unlinkSync(this.targetPath);\n          }\n          reject(err);\n          return;\n        }\n\n        this.timeout = Math.min(this.maxTimeout, this.timeout * 2);\n        startRequest();\n      };\n\n      const onSuccess = () => {\n        Actions.recordPerfMetric({\n          action: 'file-download',\n          accountId: this.accountId,\n          actionTimeMs: Date.now() - before,\n          maxValue: 10 * 60 * 1000,\n          succeeded: true,\n        })\n        this.request = null;\n        stream.end();\n        this.state = State.Finished;\n        this.percent = 100;\n        resolve(this);\n      };\n\n      startRequest = () => {\n        console.info(`starting download with ${this.timeout}ms timeout`);\n        const request = new NylasAPIRequest({\n          api: NylasAPI,\n          options: {\n            json: false,\n            path: `/files/${this.fileId}/download`,\n            accountId: this.accountId,\n            encoding: null, // Tell `request` not to parse the response data\n            timeout: this.timeout,\n            started: (req) => {\n              this.attempts += 1;\n              this.request = req;\n              return progress(this.request, {throtte: 250})\n              .on('progress', (prog) => {\n                this.percent = prog.percent;\n                this.progressCallback();\n              })\n\n              // This is a /socket/ error event, not an HTTP error event. It fires\n              // when the conn is dropped, user if offline, but not on HTTP status codes.\n              // It is sometimes called in place of \"end\", not before or after.\n              .on('error', onFailed)\n\n              .on('end', () => {\n                if (this.state === State.Failed) { return; }\n\n                const {response} = this.request\n                const statusCode = response ? response.statusCode : null;\n                if ([200, 202, 204].includes(statusCode)) {\n                  onSuccess();\n                } else {\n                  onFailed(new Error(`Server returned a ${statusCode}`));\n                }\n              })\n\n              .pipe(stream);\n            },\n          },\n        });\n\n        request.run()\n      };\n\n      startRequest();\n    });\n    return this.promise\n  }\n\n  ensureClosed() {\n    if (this.request) {\n      this.request.abort()\n    }\n  }\n}\n\n\nclass FileDownloadStore extends NylasStore {\n\n  constructor() {\n    super()\n    this.listenTo(Actions.fetchFile, this._fetch);\n    this.listenTo(Actions.fetchAndOpenFile, this._fetchAndOpen);\n    this.listenTo(Actions.fetchAndSaveFile, this._fetchAndSave);\n    this.listenTo(Actions.fetchAndSaveAllFiles, this._fetchAndSaveAll);\n    this.listenTo(Actions.abortFetchFile, this._abortFetchFile);\n\n    this._downloads = {};\n    this._filePreviewPaths = {};\n    this._downloadDirectory = path.join(NylasEnv.getConfigDirPath(), 'downloads');\n    mkdirp(this._downloadDirectory);\n  }\n\n  // Returns a path on disk for saving the file. Note that we must account\n  // for files that don't have a name and avoid returning <downloads/dir/\"\">\n  // which causes operations to happen on the directory (badness!)\n  //\n  pathForFile(file) {\n    if (!file) { return null; }\n    return path.join(this._downloadDirectory, file.id, file.safeDisplayName());\n  }\n\n  getDownloadDataForFile(fileId) {\n    const download = this._downloads[fileId]\n    if (!download) { return null; }\n    return download.data()\n  }\n\n  // Returns a hash of download objects keyed by fileId\n  //\n  getDownloadDataForFiles(fileIds = []) {\n    const downloadData = {};\n    fileIds.forEach((fileId) => {\n      downloadData[fileId] = this.getDownloadDataForFile(fileId);\n    });\n    return downloadData;\n  }\n\n  previewPathsForFiles(fileIds = []) {\n    const previewPaths = {};\n    fileIds.forEach((fileId) => {\n      previewPaths[fileId] = this.previewPathForFile(fileId);\n    });\n    return previewPaths;\n  }\n\n  previewPathForFile(fileId) {\n    return this._filePreviewPaths[fileId];\n  }\n\n  // Returns a promise with a Download object, allowing other actions to be\n  // daisy-chained to the end of the download operation.\n  _runDownload(file) {\n    const targetPath = this.pathForFile(file);\n\n    // is there an existing download for this file? If so,\n    // return that promise so users can chain to the end of it.\n    let download = this._downloads[file.id];\n    if (download) { return download.run(); }\n\n    // create a new download for this file\n    download = new Download({\n      accountId: file.accountId,\n      fileId: file.id,\n      filesize: file.size,\n      filename: file.displayName(),\n      targetPath,\n      progressCallback: () => this.trigger(),\n      retryWithBackoff: true,\n    });\n\n    // Do we actually need to queue and run the download? Queuing a download\n    // for an already-downloaded file has side-effects, like making the UI\n    // flicker briefly.\n    return this._prepareFolder(file).then(() => {\n      return this._checkForDownloadedFile(file)\n      .then((alreadyHaveFile) => {\n        if (alreadyHaveFile) {\n          // If we have the file, just resolve with a resolved download representing the file.\n          download.promise = Promise.resolve();\n          download.state = State.Finished;\n          return Promise.resolve(download);\n        }\n        this._downloads[file.id] = download;\n        this.trigger();\n        return download.run().finally(() => {\n          download.ensureClosed();\n          if (download.state === State.Failed) {\n            delete this._downloads[file.id];\n          }\n          this.trigger();\n        });\n      })\n      .then(() => this._generatePreview(file))\n      .then(() => Promise.resolve(download))\n    });\n  }\n\n  _generatePreview(file) {\n    if (process.platform !== 'darwin') { return Promise.resolve() }\n    if (!NylasEnv.config.get('core.attachments.displayFilePreview')) {\n      return Promise.resolve()\n    }\n    if (NonPreviewableExtensions.includes(file.displayExtension())) {\n      return Promise.resolve()\n    }\n\n    const filePath = this.pathForFile(file)\n    const previewPath = `${filePath}.png`\n    return fs.accessAsync(filePath, fs.F_OK)\n    .then(() => {\n      fs.accessAsync(previewPath, fs.F_OK)\n      .then(() => {\n        // If the preview file already exists, set our state and bail\n        this._filePreviewPaths[file.id] = previewPath\n        this.trigger()\n      })\n      .catch(() => {\n        // If the preview file doesn't exist yet, generate it\n        const fileDir = path.dirname(filePath)\n        const previewSize = THUMBNAIL_WIDTH * (11 / 8.5)\n\n        const qlManageArgs = ['-t', '-f', window.devicePixelRatio, '-s', previewSize, '-o', fileDir, filePath]\n        return ShellUtils.spawn(`qlmanage`, qlManageArgs)\n        .then(({stdout, stderr}) => {\n          if (/No thumbnail created/i.test(stdout) || stderr.length > 0) {\n            return\n          }\n          this._filePreviewPaths[file.id] = previewPath\n          this.trigger()\n        })\n        .catch((err) => {\n          // Ignore errors, we don't really mind if we can't generate a preview\n          // for a file\n          NylasEnv.reportError(err)\n        })\n      })\n    })\n    // If the file doesn't exist, ignore the error.\n    .catch(() => Promise.resolve())\n  }\n\n  // Returns a promise that resolves with true or false. True if the file has\n  // been downloaded, false if it should be downloaded.\n  //\n  _checkForDownloadedFile(file) {\n    return fs.statAsync(this.pathForFile(file))\n    .then((stats) => {\n      return Promise.resolve(stats.size >= file.size);\n    })\n    .catch(() => {\n      return Promise.resolve(false);\n    })\n  }\n\n  // Checks that the folder for the download is ready. Returns a promise that\n  // resolves when the download directory for the file has been created.\n  //\n  _prepareFolder(file) {\n    const targetFolder = path.join(this._downloadDirectory, file.id);\n    return fs.statAsync(targetFolder)\n    .catch(() => {\n      return mkdirpAsync(targetFolder);\n    });\n  }\n\n  _fetch = (file) => {\n    return this._runDownload(file)\n    .catch(this._catchFSErrors)\n    // Passively ignore\n    .catch(() => {});\n  }\n\n  _fetchAndOpen = (file) => {\n    return this._runDownload(file)\n    .then((download) => shell.openItem(download.targetPath))\n    .catch(this._catchFSErrors)\n    .catch((error) => {\n      return this._presentError({file, error});\n    });\n  }\n\n  _saveDownload = (download, savePath) => {\n    return new Promise((resolve, reject) => {\n      const stream = fs.createReadStream(download.targetPath);\n      stream.pipe(fs.createWriteStream(savePath));\n      stream.on('error', err => reject(err));\n      stream.on('end', () => resolve());\n    });\n  }\n\n  _fetchAndSave = (file) => {\n    const defaultPath = this._defaultSavePath(file);\n    const defaultExtension = path.extname(defaultPath);\n\n    NylasEnv.showSaveDialog({defaultPath}, (savePath) => {\n      if (!savePath) { return; }\n\n      const saveExtension = path.extname(savePath);\n      const newDownloadDirectory = path.dirname(savePath);\n      const didLoseExtension = defaultExtension !== '' && saveExtension === '';\n      let actualSavePath = savePath\n      if (didLoseExtension) {\n        actualSavePath += defaultExtension;\n      }\n\n      this._runDownload(file)\n      .then((download) => this._saveDownload(download, actualSavePath))\n      .then(() => {\n        if (NylasEnv.savedState.lastDownloadDirectory !== newDownloadDirectory) {\n          shell.showItemInFolder(actualSavePath);\n          NylasEnv.savedState.lastDownloadDirectory = newDownloadDirectory;\n        }\n      })\n      .catch(this._catchFSErrors)\n      .catch(error => {\n        this._presentError({file, error});\n      });\n    });\n  }\n\n  _fetchAndSaveAll = (files) => {\n    const defaultPath = this._defaultSaveDir();\n    const options = {\n      defaultPath,\n      title: 'Save Into...',\n      buttonLabel: 'Download All',\n      properties: ['openDirectory', 'createDirectory'],\n    };\n\n    return new Promise((resolve) => {\n      NylasEnv.showOpenDialog(options, (selected) => {\n        if (!selected) { return; }\n        const dirPath = selected[0];\n        if (!dirPath) { return; }\n        NylasEnv.savedState.lastDownloadDirectory = dirPath;\n\n        const lastSavePaths = [];\n        const savePromises = files.map((file) => {\n          const savePath = path.join(dirPath, file.safeDisplayName());\n          return this._runDownload(file)\n          .then((download) => this._saveDownload(download, savePath))\n          .then(() => lastSavePaths.push(savePath));\n        });\n\n        Promise.all(savePromises)\n        .then(() => {\n          if (lastSavePaths.length > 0) {\n            shell.showItemInFolder(lastSavePaths[0]);\n          }\n          return resolve(lastSavePaths);\n        })\n        .catch(this._catchFSErrors)\n        .catch((error) => {\n          return this._presentError({error});\n        });\n      });\n    });\n  }\n\n  _abortFetchFile = (file) => {\n    const download = this._downloads[file.id];\n    if (!download) { return; }\n    download.ensureClosed();\n    this.trigger();\n\n    const downloadPath = this.pathForFile(file);\n    fs.exists(downloadPath, (exists) => {\n      if (exists) {\n        fs.unlink(downloadPath);\n      }\n    });\n  }\n\n  _defaultSaveDir() {\n    let home = ''\n    if (process.platform === 'win32') {\n      home = process.env.USERPROFILE;\n    } else {\n      home = process.env.HOME;\n    }\n\n    let downloadDir = path.join(home, 'Downloads');\n    if (!fs.existsSync(downloadDir)) {\n      downloadDir = os.tmpdir();\n    }\n\n    if (NylasEnv.savedState.lastDownloadDirectory) {\n      if (fs.existsSync(NylasEnv.savedState.lastDownloadDirectory)) {\n        downloadDir = NylasEnv.savedState.lastDownloadDirectory;\n      }\n    }\n\n    return downloadDir;\n  }\n\n  _defaultSavePath(file) {\n    const downloadDir = this._defaultSaveDir();\n    return path.join(downloadDir, file.safeDisplayName());\n  }\n\n  _presentError({file, error} = {}) {\n    const name = file ? file.displayName() : \"one or more files\";\n    const errorString = error ? error.toString() : \"\";\n\n    return remote.dialog.showMessageBox({\n      type: 'warning',\n      message: \"Download Failed\",\n      detail: `Unable to download ${name}. Check your network connection and try again. ${errorString}`,\n      buttons: [\"OK\"],\n    });\n  }\n\n  _catchFSErrors(error) {\n    let message = null;\n    if (['EPERM', 'EMFILE', 'EACCES'].includes(error.code)) {\n      message = \"N1 could not save an attachment. Check that permissions are set correctly and try restarting N1 if the issue persists.\";\n    }\n    if (['ENOSPC'].includes(error.code)) {\n      message = \"N1 could not save an attachment because you have run out of disk space.\";\n    }\n\n    if (message) {\n      remote.dialog.showMessageBox({\n        type: 'warning',\n        message: \"Download Failed\",\n        detail: `${message}\\n\\n${error.message}`,\n        buttons: [\"OK\"],\n      });\n      return Promise.resolve();\n    }\n    return Promise.reject(error);\n  }\n}\n\nexport default new FileDownloadStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/file-upload-store.es6",
    "content": "import _ from 'underscore';\nimport fs from 'fs';\nimport path from 'path';\nimport rimraf from 'rimraf';\nimport mkdirp from 'mkdirp';\nimport NylasStore from 'nylas-store';\nimport Actions from '../actions';\nimport Utils from '../models/utils';\nimport Message from '../models/message';\nimport DraftStore from './draft-store';\nimport DatabaseStore from './database-store';\n\nPromise.promisifyAll(fs);\nconst mkdirpAsync = Promise.promisify(mkdirp);\nconst UPLOAD_DIR = path.join(NylasEnv.getConfigDirPath(), 'uploads');\nconst MAX_UPLOAD_SIZE = 25 * 1000000 // 25 MB\n\n\nclass Upload {\n  constructor({messageClientId, filePath, stats, id, inline, uploadDir} = {}) {\n    this.inline = inline;\n    this.stats = stats;\n    this.uploadDir = uploadDir || UPLOAD_DIR;\n    this.messageClientId = messageClientId;\n    this.originPath = filePath;\n    this.id = id || Utils.generateTempId();\n    this.filename = path.basename(filePath);\n    this.targetDir = path.join(this.uploadDir, this.messageClientId, this.id);\n    this.targetPath = path.join(this.targetDir, this.filename);\n    this.size = this.stats.size;\n  }\n\n  get extension() {\n    const ext = path.extname(this.filename.toLowerCase())\n    return ext.slice(1); // remove leading .\n  }\n}\n\n\nclass FileUploadStore extends NylasStore {\n\n  Upload = Upload;\n\n  constructor() {\n    super()\n    this.listenTo(Actions.addAttachment, this._onAddAttachment);\n    this.listenTo(Actions.selectAttachment, this._onSelectAttachment);\n    this.listenTo(Actions.removeAttachment, this._onRemoveAttachment);\n    this.listenTo(DatabaseStore, this._onDataChanged);\n\n    mkdirp.sync(UPLOAD_DIR);\n    if (NylasEnv.isMainWindow() || NylasEnv.inSpecMode()) {\n      this.listenTo(Actions.ensureMessageInSentSuccess, ({messageClientId}) => {\n        this._deleteUploadsForClientId(messageClientId);\n      });\n    }\n  }\n\n  // Helpers\n\n  _assertIdPresent(messageClientId) {\n    if (!messageClientId) {\n      throw new Error(\"You need to pass the ID of the message (draft) this Action refers to\");\n    }\n  }\n\n  _getFileStats(filePath) {\n    return fs.statAsync(filePath)\n    .catch(() => Promise.reject(new Error(`${filePath} could not be found, or has invalid file permissions.`)));\n  }\n\n  async _getTotalDirSize(dirpath) {\n    const items = await fs.readdirAsync(dirpath)\n    let total = 0\n    for (const filename of items) {\n      const filepath = path.join(dirpath, filename)\n      const stats = await this._getFileStats(filepath)\n      total += stats.size\n    }\n    return total\n  }\n\n  _copyUpload(upload) {\n    return new Promise((resolve, reject) => {\n      const {originPath, targetPath} = upload;\n      const readStream = fs.createReadStream(originPath);\n      const writeStream = fs.createWriteStream(targetPath);\n\n      readStream.on('error', () => reject(new Error(`Could not read file at path: ${originPath}`)));\n      writeStream.on('error', () => reject(new Error(`Could not write ${upload.filename} to uploads directory.`)));\n      readStream.on('end', () => resolve(upload));\n      readStream.pipe(writeStream);\n    });\n  }\n\n  _deleteUpload(upload) {\n    // Delete the upload file\n    return fs.unlinkAsync(upload.targetPath).then(() =>\n      // Delete the containing folder\n      fs.rmdirAsync(upload.targetDir).then(() => {\n        // Try to remove the directory for the associated message if this was the\n        // last upload\n        // Will fail if it's not empty, which is fine.\n        fs.rmdir(path.join(UPLOAD_DIR, upload.messageClientId), () => {});\n        return Promise.resolve(upload);\n      })\n    )\n    .catch((err) => Promise.reject(new Error(`Error deleting file upload ${upload.filename}:\\n\\n${err.message}`)));\n  }\n\n  _deleteUploadsForClientId(messageClientId) {\n    rimraf(path.join(UPLOAD_DIR, messageClientId), {disableGlob: true}, (err) => {\n      if (err) {\n        console.warn(err);\n      }\n    });\n  }\n\n  _applySessionChanges(messageClientId, changeFunction) {\n    return DraftStore.sessionForClientId(messageClientId).then((session) => {\n      const uploads = changeFunction(session.draft().uploads);\n      session.changes.add({uploads});\n    });\n  }\n\n\n  // Handlers\n\n  _onDataChanged = (change) => {\n    if (!NylasEnv.isMainWindow()) { return; }\n    if (change.objectClass !== Message.name || change.type !== 'unpersist') { return; }\n\n    change.objects.forEach((message) => {\n      this._deleteUploadsForClientId(message.clientId);\n    });\n  }\n\n  _onSelectAttachment = ({messageClientId}) => {\n    this._assertIdPresent(messageClientId);\n\n    // When the dialog closes, it triggers `Actions.addAttachment`\n    return NylasEnv.showOpenDialog({properties: ['openFile', 'multiSelections']},\n      (paths) => {\n        if (paths == null) { return; }\n        let pathsToOpen = paths\n        if (_.isString(pathsToOpen)) {\n          pathsToOpen = [pathsToOpen];\n        }\n\n        pathsToOpen.forEach((filePath) => Actions.addAttachment({messageClientId, filePath}));\n      }\n    );\n  }\n\n  _onAddAttachment = ({messageClientId, filePath, inline = false, onUploadCreated = (() => {})}) => {\n    this._assertIdPresent(messageClientId);\n\n    return this._getFileStats(filePath)\n    .then(async (stats) => {\n      const upload = new Upload({messageClientId, filePath, stats, inline});\n      if (stats.isDirectory()) {\n        throw new Error(`${upload.filename} is a directory. Try compressing it and attaching it again.`);\n      } else if (stats.size > MAX_UPLOAD_SIZE) {\n        throw new Error(`${upload.filename} cannot be attached because it is larger than 25MB.`);\n      }\n      await mkdirpAsync(upload.targetDir)\n\n      const totalSize = await this._getTotalDirSize(upload.targetDir)\n      if (totalSize > MAX_UPLOAD_SIZE) {\n        throw new Error(`Can't upload more than 15MB of attachments`);\n      }\n\n      await this._copyUpload(upload)\n      await this._applySessionChanges(upload.messageClientId, (uploads) => uploads.concat([upload]))\n      onUploadCreated(upload)\n    })\n    .catch(this._onAttachFileError);\n  }\n\n  _onRemoveAttachment = (uploadToRemove) => {\n    if (!uploadToRemove) { return Promise.resolve(); }\n    this._applySessionChanges(uploadToRemove.messageClientId, (uploads) => {\n      return uploads.filter(({id}) => id !== uploadToRemove.id)\n    });\n    return this._deleteUpload(uploadToRemove)\n    .catch(this._onAttachFileError);\n  }\n\n  _onAttachFileError = (error) => {\n    NylasEnv.showErrorDialog(error.message);\n  }\n}\n\nexport default new FileUploadStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/focused-contacts-store.es6",
    "content": "import _ from 'underscore';\nimport Rx from 'rx-lite';\nimport NylasStore from 'nylas-store';\n\nimport Utils from '../models/utils';\nimport Thread from '../models/thread';\nimport Actions from '../actions';\nimport Contact from '../models/contact';\nimport MessageStore from './message-store';\nimport AccountStore from './account-store';\nimport DatabaseStore from './database-store';\n\n// A store that handles the focuses collections of and individual contacts\nclass FocusedContactsStore extends NylasStore {\n  constructor() {\n    super()\n    this.listenTo(MessageStore, this._onMessageStoreChanged);\n    this.listenTo(Actions.focusContact, this._onFocusContact);\n    this._clearCurrentParticipants();\n    this._triggerLater = _.debounce(this.trigger, 250);\n    this._loadCurrentParticipantThreads = _.debounce(this._loadCurrentParticipantThreads, 250);\n  }\n\n  sortedContacts() { return this._currentContacts; }\n\n  focusedContact() { return this._currentFocusedContact; }\n\n  focusedContactThreads() { return this._currentParticipantThreads || []; }\n\n  // We need to wait now for the MessageStore to grab all of the\n  // appropriate messages for the given thread.\n\n  _onMessageStoreChanged = () => {\n    const threadId = MessageStore.itemsLoading() ? null : MessageStore.threadId();\n\n    // Always clear data immediately when we're showing the wrong thread\n    if (this._currentThread && this._currentThread.id !== threadId) {\n      this._clearCurrentParticipants();\n      this.trigger();\n    }\n\n    // Wait to populate until the user has stopped moving through threads. This is\n    // important because the FocusedContactStore powers tons of third-party extensions,\n    // which could do /horrible/ things when we trigger.\n    const thread = MessageStore.itemsLoading() ? null : MessageStore.thread();\n    if (thread && thread.id !== ((this._currentThread || {}).id)) {\n      this._currentThread = thread;\n      this._populateCurrentParticipants();\n    }\n  }\n\n  // For now we take the last message\n  _populateCurrentParticipants() {\n    this._scoreAllParticipants();\n    const sorted = _.sortBy(_.values(this._contactScores), \"score\").reverse();\n    this._currentContacts = _.map(sorted, obj => obj.contact);\n    return this._onFocusContact(this._currentContacts[0]);\n  }\n\n  _clearCurrentParticipants() {\n    this._contactScores = {};\n    this._currentContacts = [];\n    if (this._unsubFocusedContact) this._unsubFocusedContact.dispose();\n    this._unsubFocusedContact = null;\n    this._currentFocusedContact = null;\n    this._currentThread = null;\n    this._currentParticipantThreads = [];\n  }\n\n  _onFocusContact = (contact) => {\n    if (this._unsubFocusedContact) this._unsubFocusedContact.dispose();\n    this._unsubFocusedContact = null;\n    this._currentParticipantThreads = [];\n\n    if (contact) {\n      const query = DatabaseStore.findBy(Contact, {\n        accountId: this._currentThread.accountId,\n        email: contact.email,\n        name: contact.name,\n      });\n      this._unsubFocusedContact = Rx.Observable.fromQuery(query).subscribe(match => {\n        this._currentFocusedContact = match || contact;\n        return this._triggerLater();\n      });\n      this._loadCurrentParticipantThreads();\n    } else {\n      this._currentFocusedContact = null;\n      this._triggerLater();\n    }\n  }\n\n  _loadCurrentParticipantThreads() {\n    const currentContact = this._currentFocusedContact || {}\n    const email = currentContact.email;\n    if (!email) {\n      return\n    }\n    DatabaseStore.findAll(Thread)\n    .where(Thread.attributes.participants.contains(email))\n    .limit(100).background()\n    .then((threads = []) => {\n      if (currentContact.email !== email) {\n        return\n      }\n      this._currentParticipantThreads = threads;\n      this.trigger();\n    });\n  }\n\n  // We score everyone to determine who's the most relevant to display in\n  // the sidebar.\n  _scoreAllParticipants() {\n    const score = (message, msgNum, field, multiplier) => {\n      (message[field] || []).forEach((contact, j) => {\n        const bonus = message[field].length - j\n        this._assignScore(contact, (msgNum + 1) * multiplier + bonus)\n      });\n    };\n\n    const iterable = MessageStore.items();\n    for (let msgNum = iterable.length - 1; msgNum >= 0; msgNum--) {\n      const message = iterable[msgNum];\n      if (message.draft) {\n        score(message, msgNum, \"to\", 10000);\n        score(message, msgNum, \"cc\", 1000);\n      } else {\n        score(message, msgNum, \"from\", 100);\n        score(message, msgNum, \"to\", 10);\n        score(message, msgNum, \"cc\", 1);\n      }\n    }\n\n    return this._contactScores;\n  }\n\n  // Self always gets a score of 0\n  _assignScore(contact, score = 0) {\n    if (!contact || !contact.email) { return; }\n    if (contact.email.trim().length === 0) { return; }\n\n    const key = Utils.toEquivalentEmailForm(contact.email);\n\n    if (!this._contactScores[key]) {\n      this._contactScores[key] = {\n        contact: contact,\n        score: score - this._calculatePenalties(contact, score),\n      }\n    }\n  }\n\n  _calculatePenalties(contact, score) {\n    let penalties = 0;\n    const email = contact.email.toLowerCase().trim();\n\n    const accountId = (this._currentThread || {}).accountId;\n    const account = AccountStore.accountForId(accountId) || {}\n    const myEmail = account.emailAddress\n\n    if (email === myEmail) {\n      // The whole thing which will penalize to zero\n      penalties += score;\n    }\n\n    const notCommonDomain = !Utils.emailHasCommonDomain(myEmail);\n    const sameDomain = Utils.emailsHaveSameDomain(myEmail, email);\n    if (notCommonDomain && sameDomain) {\n      penalties += score * 0.9;\n    }\n\n    return Math.max(penalties, 0);\n  }\n}\n\nexport default new FocusedContactsStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/focused-content-store.coffee",
    "content": "_ = require 'underscore'\nReflux = require 'reflux'\nAccountStore = require('./account-store').default\nWorkspaceStore = require './workspace-store'\nDatabaseStore = require('./database-store').default\nFocusedPerspectiveStore = require('./focused-perspective-store').default\nMailboxPerspective = require '../../mailbox-perspective'\nActions = require('../actions').default\nThread = require('../models/thread').default\nModel = require '../models/model'\n\n{Listener, Publisher} = require '../modules/reflux-coffee'\nCoffeeHelpers = require '../coffee-helpers'\n\n###\nPublic: The FocusedContentStore provides access to the objects currently selected\nor otherwise focused in the window. Normally, focus would be maintained internally\nby components that show models. The FocusedContentStore makes the concept of\nselection public so that you can observe focus changes and trigger your own changes\nto focus.\n\nSince {FocusedContentStore} is a Flux-compatible Store, you do not call setters\non it directly. Instead, use {Actions::setFocus} or\n{Actions::setCursorPosition} to set focus. The FocusedContentStore observes\nthese models, changes it's state, and broadcasts to it's observers.\n\nNote: The {FocusedContentStore} triggers when a focused model is changed, even if\nit's ID has not. For example, if the user has a {Thread} selected and removes a tag,\n{FocusedContentStore} will trigger so you can fetch the new version of the\n{Thread}. If you observe the {FocusedContentStore} properly, you should always\nhave the latest version of the the selected object.\n\n**Standard Collections**:\n\n   - thread\n   - file\n\n**Example: Observing the Selected Thread**\n\n```coffeescript\n@unsubscribe = FocusedContentStore.listen(@_onFocusChanged, @)\n\n...\n\n# Called when focus has changed, or when the focused model has been modified.\n_onFocusChanged: ->\n  thread = FocusedContentStore.focused('thread')\n  if thread\n    console.log(\"#{thread.subject} is selected!\")\n  else\n    console.log(\"No thread is selected!\")\n```\n\nSection: Stores\n###\nclass FocusedContentStore\n  @include: CoffeeHelpers.includeModule\n\n  @include Publisher\n  @include Listener\n\n  constructor: ->\n    @_resetInstanceVars()\n    @listenTo AccountStore, @_onAccountsChange\n    @listenTo WorkspaceStore, @_onWorkspaceChange\n    @listenTo DatabaseStore, @_onDataChange\n    @listenTo Actions.setFocus, @_onFocus\n    @listenTo Actions.setCursorPosition, @_onFocusKeyboard\n\n  triggerAfterAnimationFrame: (payload) =>\n    window.requestAnimationFrame =>\n      @trigger(payload)\n\n  _resetInstanceVars: =>\n    @_focused = {}\n    @_focusedUsingClick = {}\n    @_keyboardCursor = {}\n    @_keyboardCursorEnabled = WorkspaceStore.layoutMode() is 'list'\n\n  # Inbound Events\n\n  _onAccountsChange: =>\n    # Ensure internal consistency by removing any focused items that belong\n    # to accounts which no longer exist.\n    changed = []\n\n    for dict in [@_focused, @_keyboardCursor]\n      for collection, item of dict\n        if item and item.accountId and !AccountStore.accountForId(item.accountId)\n          delete dict[collection]\n          changed.push(collection)\n\n    if changed.length > 0\n      @trigger({ impactsCollection: (c) -> changed.includes(c) })\n\n  _onFocusKeyboard: ({collection, item}) =>\n    throw new Error(\"focusKeyboard() requires a Model or null\") if item and not (item instanceof Model)\n    throw new Error(\"focusKeyboard() requires a collection\") unless collection\n    return if @_keyboardCursor[collection]?.id is item?.id\n\n    @_keyboardCursor[collection] = item\n    @triggerAfterAnimationFrame({ impactsCollection: (c) -> c is collection })\n\n  _onFocus: ({collection, item, usingClick}) =>\n    throw new Error(\"focus() requires a Model or null\") if item and not (item instanceof Model)\n    throw new Error(\"focus() requires a collection\") unless collection\n    return if @_focused[collection]?.id is item?.id\n\n    if collection is 'thread' and item\n      NylasEnv.timer.start(\"select-thread-#{item.id}\")\n\n    @_focused[collection] = item\n    @_focusedUsingClick[collection] = usingClick\n    @_keyboardCursor[collection] = item if item\n    @triggerAfterAnimationFrame({ impactsCollection: (c) -> c is collection })\n\n  _onWorkspaceChange: =>\n    keyboardCursorEnabled = WorkspaceStore.layoutMode() is 'list'\n\n    if keyboardCursorEnabled isnt @_keyboardCursorEnabled\n      @_keyboardCursorEnabled = keyboardCursorEnabled\n\n      if keyboardCursorEnabled\n        for collection, item of @_focused\n          @_keyboardCursor[collection] = item\n        @_focused = {}\n      else\n        for collection, item of @_keyboardCursor\n          @_onFocus({collection, item})\n\n    @trigger({ impactsCollection: -> true })\n\n  _onDataChange: (change) =>\n    # If one of the objects we're storing in our focused or keyboard cursor\n    # dictionaries has changed, we need to let our observers know, since they\n    # may now be holding on to outdated data.\n    return unless change and change.objectClass\n\n    touched = []\n\n    for data in [@_focused, @_keyboardCursor]\n      for key, val of data\n        continue unless val and val.constructor.name is change.objectClass\n        for obj in change.objects\n          if val.id is obj.id\n            if change.type is 'unpersist'\n              data[key] = null\n            else\n              data[key] = obj\n            touched.push(key)\n\n    if touched.length > 0\n      @trigger({ impactsCollection: (c) -> c in touched })\n\n  # Public Methods\n\n  ###\n  Public: Returns the focused {Model} in the collection specified,\n  or undefined if no item is focused.\n\n  - `collection` The {String} name of a collection. Standard collections are\n    listed above.\n  ###\n  focused: (collection) =>\n    @_focused[collection]\n\n  ###\n  Public: Returns the ID of the focused {Model} in the collection specified,\n  or undefined if no item is focused.\n\n  - `collection` The {String} name of a collection. Standard collections are\n    listed above.\n  ###\n  focusedId: (collection) =>\n    @_focused[collection]?.id\n\n  ###\n  Public: Returns true if the item for the collection was focused via a click or\n  false otherwise.\n\n  - `collection` The {String} name of a collection. Standard collections are\n    listed above.\n  ###\n  didFocusUsingClick: (collection) =>\n    @_focusedUsingClick[collection] ? false\n\n  ###\n  Public: Returns the {Model} the keyboard is currently focused on\n  in the collection specified. Keyboard focus is not always separate from\n  primary focus (selection). You can use {::keyboardCursorEnabled} to determine\n  whether keyboard focus is enabled.\n\n  - `collection` The {String} name of a collection. Standard collections are\n    listed above.\n  ###\n  keyboardCursor: (collection) =>\n    @_keyboardCursor[collection]\n\n  ###\n  Public: Returns the ID of the {Model} the keyboard is currently focused on\n  in the collection specified. Keyboard focus is not always separate from\n  primary focus (selection). You can use {::keyboardCursorEnabled} to determine\n  whether keyboard focus is enabled.\n\n  - `collection` The {String} name of a collection. Standard collections are\n    listed above.\n  ###\n  keyboardCursorId: (collection) =>\n    @_keyboardCursor[collection]?.id\n\n  ###\n  Public: Returns a {Boolean} - `true` if the keyboard cursor concept applies in\n  the current {WorkspaceStore} layout mode. The keyboard cursor is currently only\n  enabled in `list` mode.\n  ###\n  keyboardCursorEnabled: =>\n    @_keyboardCursorEnabled\n\n\nmodule.exports = new FocusedContentStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/focused-perspective-store.es6",
    "content": "import _ from 'underscore'\nimport NylasStore from 'nylas-store'\nimport AccountStore from './account-store'\nimport WorkspaceStore from './workspace-store'\nimport MailboxPerspective from '../../mailbox-perspective'\nimport CategoryStore from './category-store'\nimport Actions from '../actions'\n\n\nclass FocusedPerspectiveStore extends NylasStore {\n\n  constructor() {\n    super()\n    this._current = MailboxPerspective.forNothing();\n    this._initialized = false\n\n    this.listenTo(CategoryStore, this._onCategoryStoreChanged);\n    this.listenTo(Actions.focusMailboxPerspective, this._onFocusPerspective);\n    this.listenTo(Actions.focusDefaultMailboxPerspectiveForAccounts, this._onFocusDefaultPerspectiveForAccounts);\n    this.listenTo(Actions.ensureCategoryIsFocused, this._onEnsureCategoryIsFocused);\n    this._listenToCommands();\n  }\n\n  current() {\n    return this._current;\n  }\n\n  sidebarAccountIds() {\n    let ids = NylasEnv.savedState.sidebarAccountIds;\n    if (!ids || !ids.length || !ids.every((id) => AccountStore.accountForId(id))) {\n      ids = NylasEnv.savedState.sidebarAccountIds = AccountStore.accountIds();\n    }\n\n    // Always defer to the AccountStore for the desired order of accounts in\n    // the sidebar - users can re-arrange them!\n    const order = AccountStore.accountIds();\n    ids = ids.sort((a, b) => order.indexOf(a) - order.indexOf(b));\n\n    return ids;\n  }\n\n  _listenToCommands() {\n    NylasEnv.commands.add(document.body, {\n      'navigation:go-to-inbox': () => this._setPerspectiveByName(\"inbox\"),\n      'navigation:go-to-sent': () => this._setPerspectiveByName(\"sent\"),\n      'navigation:go-to-starred': () => this._setPerspective(MailboxPerspective.forStarred(this._current.accountIds)),\n      'navigation:go-to-drafts': () => this._setPerspective(MailboxPerspective.forDrafts(this._current.accountIds)),\n      'navigation:go-to-all': () => {\n        const categories = this._current.accountIds.map((id) => CategoryStore.getArchiveCategory(id))\n        this._setPerspective(MailboxPerspective.forCategories(categories))\n      },\n      'navigation:go-to-contacts': () => {}, // TODO,\n      'navigation:go-to-tasks': () => {}, // TODO,\n      'navigation:go-to-label': () => {}, // TODO,\n    })\n  }\n\n  _isValidAccountSet(ids) {\n    const accountIds = AccountStore.accountIds();\n    return ids.every((a) => accountIds.includes(a));\n  }\n\n  _isValidPerspective(perspective) {\n    // Ensure all the accountIds referenced in the perspective still exist\n    if (!this._isValidAccountSet(perspective.accountIds)) { return false; }\n\n    // Ensure all the categories referenced in the perspective still exist\n    const categoriesStillExist = perspective.categories().every((c) => {\n      return !!CategoryStore.byId(c.accountId, c.id);\n    })\n    if (!categoriesStillExist) { return false; }\n\n    return true;\n  }\n\n  _initializeFromSavedState() {\n    const json = NylasEnv.savedState.perspective;\n    let {sidebarAccountIds} = NylasEnv.savedState;\n    let perspective;\n\n    if (json) {\n      perspective = MailboxPerspective.fromJSON(json);\n    }\n    this._initialized = true\n\n    if (!perspective || !this._isValidPerspective(perspective)) {\n      perspective = this._defaultPerspective();\n      sidebarAccountIds = perspective.accountIds;\n      this._initialized = false\n    }\n\n    if (!sidebarAccountIds || !this._isValidAccountSet(sidebarAccountIds) || sidebarAccountIds.length < perspective.accountIds.length) {\n      sidebarAccountIds = perspective.accountIds;\n      this._initialized = false\n    }\n\n    this._setPerspective(perspective, sidebarAccountIds);\n  }\n\n  // Inbound Events\n  _onCategoryStoreChanged = () => {\n    if (!this._initialized) {\n      this._initializeFromSavedState();\n    } else if (!this._isValidPerspective(this._current)) {\n      this._setPerspective(this._defaultPerspective(this._current.accountIds));\n    }\n  }\n\n  _onFocusPerspective = (perspective) => {\n    // If looking at unified inbox, don't attempt to change the sidebar accounts\n    const sidebarIsUnifiedInbox = this.sidebarAccountIds().length > 1;\n    if (sidebarIsUnifiedInbox) {\n      this._setPerspective(perspective);\n    } else {\n      this._setPerspective(perspective, perspective.accountIds);\n    }\n  }\n\n  /*\n  * Takes an optional array of `sidebarAccountIds`. By default, this method will\n  * set the sidebarAccountIds to the perspective's accounts if no value is\n  * provided\n  */\n  _onFocusDefaultPerspectiveForAccounts = (accountsOrIds, {sidebarAccountIds} = {}) => {\n    if (!accountsOrIds) { return; }\n    const perspective = this._defaultPerspective(accountsOrIds);\n    this._setPerspective(perspective, sidebarAccountIds || perspective.accountIds);\n  }\n\n  _onEnsureCategoryIsFocused = (categoryName, accountIds = []) => {\n    const ids = accountIds instanceof Array ? accountIds : [accountIds]\n    const categories = ids.map((id) => (\n      CategoryStore.getStandardCategory(id, categoryName)\n    ))\n    const perspective = MailboxPerspective.forCategories(categories)\n    this._onFocusPerspective(perspective)\n  }\n\n  _defaultPerspective(accountsOrIds = AccountStore.accountIds()) {\n    const perspective = MailboxPerspective.forInbox(accountsOrIds);\n\n    // If no account ids were selected, or the categories for these accounts have\n    // not loaded yet, return forNothing(). This means that the next time the\n    // CategoryStore triggers, we'll try again.\n    if (perspective.categories().length === 0) {\n      return MailboxPerspective.forNothing();\n    }\n    return perspective;\n  }\n\n  _setPerspective(perspective, sidebarAccountIds) {\n    let shouldTrigger = false;\n\n    if (!perspective.isEqual(this._current)) {\n      NylasEnv.savedState.perspective = perspective.toJSON();\n      this._current = perspective;\n      shouldTrigger = true;\n    }\n\n    const shouldSaveSidebarAccountIds = (\n      sidebarAccountIds &&\n      !_.isEqual(NylasEnv.savedState.sidebarAccountIds, sidebarAccountIds) &&\n      this._initialized === true\n    )\n    if (shouldSaveSidebarAccountIds) {\n      NylasEnv.savedState.sidebarAccountIds = sidebarAccountIds;\n      shouldTrigger = true;\n    }\n\n    if (shouldTrigger) { this.trigger(); }\n\n    let desired;\n    if (perspective.drafts) {\n      desired = WorkspaceStore.Sheet.Drafts;\n    } else {\n      desired = WorkspaceStore.Sheet.Threads;\n    }\n\n    // Always switch to the correct sheet and pop to root when perspective set\n    if (desired && WorkspaceStore.rootSheet() !== desired) {\n      Actions.selectRootSheet(desired);\n    }\n    Actions.popToRootSheet();\n  }\n\n  _setPerspectiveByName(categoryName) {\n    let categories = this._current.accountIds.map((id) => {\n      return CategoryStore.getStandardCategory(id, categoryName);\n    });\n    categories = _.compact(categories);\n    if (categories.length === 0) { return; }\n    this._setPerspective(MailboxPerspective.forCategories(categories));\n  }\n}\n\nexport default new FocusedPerspectiveStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/folder-sync-progress-store.es6",
    "content": "import _ from 'underscore'\nimport NylasStore from 'nylas-store'\nimport AccountStore from './account-store'\nimport CategoryStore from './category-store'\n\n\n/**\n * FolderSyncProgressStore keeps track of the sync state per account, and will\n * trigger whenever it changes.\n *\n * The sync state for any given account has the following shape:\n *\n * {\n *   folderSyncProgress: {\n *     inbox: {\n *       progress: 0.5,\n *       total: 100,\n *     }\n *     archive: {\n *       progress: 0.2,\n *       total: 600,\n *     },\n *     ...\n *   }\n * }\n *\n */\nclass FolderSyncProgressStore extends NylasStore {\n\n  constructor() {\n    super()\n    this._statesByAccount = {}\n    this._triggerDebounced = _.debounce(this.trigger, 100)\n  }\n\n  activate() {\n    this.listenTo(AccountStore, () => this._onAccountsChanged())\n    this.listenTo(CategoryStore, () => this._onCategoriesChanged())\n\n    this._onCategoriesChanged()\n  }\n\n  _onAccountsChanged() {\n    const currentIds = Object.keys(this._statesByAccount)\n    const nextIds = AccountStore.accountIds()\n    const removedIds = _.difference(currentIds, nextIds)\n\n    removedIds.forEach((accountId) => {\n      if (this._statesByAccount[accountId]) {\n        delete this._statesByAccount[accountId]\n        this._triggerDebounced()\n      }\n    })\n  }\n\n  _onCategoriesChanged() {\n    const accountIds = AccountStore.accountIds()\n    for (const accountId of accountIds) {\n      const folders = CategoryStore.categories(accountId)\n      .filter(cat => cat.object === 'folder')\n\n      const updates = {}\n      for (const folder of folders) {\n        const name = folder.name || folder.displayName\n        const {approxPercentComplete, approxTotal, oldestProcessedDate} = folder.syncProgress || {};\n        updates[name] = {\n          progress: approxPercentComplete || 0,\n          total: approxTotal || 0,\n          oldestProcessedDate: oldestProcessedDate ? new Date(oldestProcessedDate) : new Date(),\n        }\n      }\n      this._updateState(accountId, {folderSyncProgress: updates})\n    }\n  }\n\n  _updateState(accountId, nextState) {\n    const currentState = this._statesByAccount[accountId] || {}\n    if (_.isEqual(currentState, nextState)) { return }\n    this._statesByAccount[accountId] = nextState\n    this._triggerDebounced()\n  }\n\n  getSyncState() {\n    return this._statesByAccount\n  }\n\n  /**\n   * Returns true if N1's local cache contains the entire list of available\n   * folders and labels.\n   * This will be true if any of the available folders have started syncing,\n   * given that K2 wont commence folder sync until it has fetched the whole list\n   * of folders and labels\n   */\n  isCategoryListSynced(accountId) {\n    const state = this._statesByAccount[accountId]\n    if (!state) { return false }\n    const folderNames = Object.keys(state.folderSyncProgress || {})\n    if (folderNames.length === 0) { return false }\n    return folderNames.some((fname) => state.folderSyncProgress[fname].progress !== 0)\n  }\n\n  whenCategoryListSynced(accountId) {\n    if (this.isCategoryListSynced(accountId)) {\n      return Promise.resolve()\n    }\n    return new Promise((resolve) => {\n      const unsubscribe = this.listen(() => {\n        if (this.isCategoryListSynced(accountId)) {\n          unsubscribe()\n          resolve()\n        }\n      })\n    })\n  }\n\n  isSyncCompleteForAccount(accountId, folderName) {\n    const state = this._statesByAccount[accountId]\n    if (!state) { return false }\n\n    if (!this.isCategoryListSynced(accountId)) {\n      return false\n    }\n\n    if (folderName) {\n      return state.folderSyncProgress[folderName].progress >= 1\n    }\n    const folderNames = Object.keys(state.folderSyncProgress)\n    for (const fname of folderNames) {\n      const syncProgress = state.folderSyncProgress[fname].progress\n      if (syncProgress < 1) {\n        return false\n      }\n    }\n    return true\n  }\n\n  isSyncComplete() {\n    const accountIds = Object.keys(this._statesByAccount)\n    if (accountIds.length === 0) { return false }\n    for (const accountId of accountIds) {\n      if (!this.isSyncCompleteForAccount(accountId)) {\n        return false\n      }\n    }\n    return true\n  }\n\n  whenSyncComplete() {\n    if (this.isSyncComplete()) {\n      return Promise.resolve()\n    }\n    return new Promise((resolve) => {\n      const unsubscribe = this.listen(() => {\n        if (this.isSyncComplete()) {\n          unsubscribe()\n          resolve()\n        }\n      })\n    })\n  }\n}\n\nexport default new FolderSyncProgressStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/identity-store.es6",
    "content": "import Rx from 'rx-lite'\nimport NylasStore from 'nylas-store';\nimport {ipcRenderer, remote} from 'electron';\nimport request from 'request';\nimport url from 'url'\n\nimport Utils from '../models/utils';\nimport Actions from '../actions';\nimport {APIError} from '../errors'\nimport KeyManager from '../../key-manager'\nimport DatabaseStore from './database-store'\n\n// Note this key name is used when migrating to Nylas Pro accounts from\n// old N1.\nconst KEYCHAIN_NAME = 'Nylas Account';\n\nclass IdentityStore extends NylasStore {\n\n  constructor() {\n    super();\n    this._identity = null\n  }\n\n  async activate() {\n    if (NylasEnv.isEmptyWindow()) {\n      NylasEnv.onWindowPropsReceived(() => {\n        this.deactivate();\n        this.activate();\n      })\n      return\n    }\n\n    NylasEnv.config.onDidChange('env', this._onEnvChanged);\n    this._onEnvChanged();\n\n    this.listenTo(Actions.logoutNylasIdentity, this._onLogoutNylasIdentity);\n\n    const q = DatabaseStore.findJSONBlob(\"NylasID\");\n    this._disp = Rx.Observable.fromQuery(q).subscribe(this._onIdentityChanged)\n\n    const identity = await DatabaseStore.run(q)\n    this._onIdentityChanged(identity)\n\n    this._fetchAndPollRemoteIdentity()\n  }\n\n  deactivate() {\n    if (this._disp) this._disp.dispose();\n    this.stopListeningToAll()\n  }\n\n  identity() {\n    if (!this._identity || !this._identity.id) return null\n    return Utils.deepClone(this._identity);\n  }\n\n  hasProAccess() {\n    return this._identity && this._identity.has_pro_access\n  }\n\n  identityId() {\n    if (!this._identity) {\n      return null;\n    }\n    return this._identity.id;\n  }\n\n  _fetchAndPollRemoteIdentity() {\n    if (!NylasEnv.isMainWindow()) return;\n    /**\n     * We only need to re-fetch the identity to synchronize ourselves\n     * with any changes a user did on a separate computer. Any updates\n     * they do on their primary computer will be optimistically updated.\n     * We also update from the server's version every\n     * `SendFeatureUsageEventTask`\n     */\n    setInterval(this._fetchIdentity.bind(this), 1000 * 60 * 60); // 60 minutes\n    // Don't await for this!\n    this._fetchIdentity();\n  }\n\n  /**\n   * Saves the identity to the database. The local cache will be updated\n   * once the database change comes back through\n   */\n  async saveIdentity(identity) {\n    if (identity && identity.token) {\n      KeyManager.replacePassword(KEYCHAIN_NAME, identity.token)\n      delete identity.token;\n    }\n    if (!identity) {\n      KeyManager.deletePassword(KEYCHAIN_NAME)\n    }\n    await DatabaseStore.inTransaction((t) => {\n      return t.persistJSONBlob(\"NylasID\", identity)\n    });\n    this._onIdentityChanged(identity)\n  }\n\n  /**\n   * When the identity changes in the database, update our local store\n   * cache and set the token from the keychain.\n   */\n  _onIdentityChanged = (newIdentity) => {\n    const oldId = ((this._identity || {}).id)\n    this._identity = newIdentity\n    if (this._identity && this._identity.id) {\n      if (!this._identity.token) {\n        this._identity.token = KeyManager.getPassword(KEYCHAIN_NAME);\n      }\n    } else {\n      // It's possible the identity exists as an empty object. If the\n      // object looks blank, set the identity to null.\n      this._identity = null\n    }\n    const newId = ((this._identity || {}).id);\n    if (oldId !== newId) {\n      ipcRenderer.send('command', 'onIdentityChanged');\n    }\n    this.trigger();\n  }\n\n  _onLogoutNylasIdentity = async () => {\n    await this.saveIdentity(null)\n    // We need to relaunch the app to clear the webview session\n    // and prevent the webview from re signing in with the same NylasID\n    remote.app.relaunch()\n    remote.app.quit()\n  }\n\n  _onEnvChanged = () => {\n    const env = NylasEnv.config.get('env')\n    if (['development', 'local'].includes(env)) {\n      if (process.env.BILLING_URL) {\n        this.URLRoot = process.env.BILLING_URL;\n      } else {\n        this.URLRoot = \"https://billing-staging.nylas.com\";\n      }\n    } else if (env === 'experimental') {\n      this.URLRoot = \"https://billing-experimental.nylas.com\";\n    } else if (env === 'staging') {\n      this.URLRoot = \"https://billing-staging.nylas.com\";\n    } else {\n      this.URLRoot = \"https://billing.nylas.com\";\n    }\n  }\n\n  /**\n   * This passes utm_source, utm_campaign, and utm_content params to the\n   * N1 billing site. Please reference:\n   * https://paper.dropbox.com/doc/Analytics-ID-Unification-oVDTkakFsiBBbk9aeuiA3\n   * for the full list of utm_ labels.\n   */\n  fetchSingleSignOnURL(path, {source, campaign, content} = {}) {\n    if (!this._identity) {\n      return Promise.reject(new Error(\"fetchSingleSignOnURL: no identity set.\"));\n    }\n\n    const qs = {utm_medium: \"N1\"}\n    if (source) { qs.utm_source = source }\n    if (campaign) { qs.utm_campaign = campaign }\n    if (content) { qs.utm_content = content }\n\n    let pathWithUtm = url.parse(path, true);\n    pathWithUtm.query = Object.assign({}, qs, (pathWithUtm.query || {}));\n    pathWithUtm = url.format({\n      pathname: pathWithUtm.pathname,\n      query: pathWithUtm.query,\n    })\n\n    if (!pathWithUtm.startsWith('/')) {\n      return Promise.reject(new Error(\"fetchSingleSignOnURL: path must start with a leading slash.\"));\n    }\n\n    return new Promise((resolve) => {\n      request({\n        method: 'POST',\n        url: `${this.URLRoot}/n1/login-link`,\n        qs: qs,\n        json: true,\n        timeout: 1500,\n        body: {\n          next_path: pathWithUtm,\n          account_token: this._identity.token,\n        },\n      }, (error, response = {}, body) => {\n        if (error || !body.startsWith('http')) {\n          // Single-sign on attempt failed. Rather than churn the user right here,\n          // at least try to open the page directly in the browser.\n          resolve(`${this.URLRoot}${path}`);\n        } else {\n          resolve(body);\n        }\n      });\n    });\n  }\n\n  async asyncRefreshIdentity() {\n    await this._fetchIdentity();\n    return true\n  }\n\n  async _fetchIdentity() {\n    if (!this._identity || !this._identity.token) {\n      return Promise.resolve();\n    }\n    const json = await this.fetchPath('/n1/user');\n    if (!json || !json.id || json.id !== this._identity.id) {\n      console.error(json)\n      NylasEnv.reportError(new Error(\"Remote Identity returned invalid json\"), json || {})\n      return Promise.resolve(this._identity)\n    }\n    const nextIdentity = Object.assign({}, this._identity, json);\n    return this.saveIdentity(nextIdentity);\n  }\n\n  fetchPath = async (path) => {\n    const options = {\n      method: 'GET',\n      url: `${this.URLRoot}${path}`,\n      startTime: Date.now(),\n    };\n    try {\n      const newIdentity = await this.nylasIDRequest(options);\n      return newIdentity\n    } catch (err) {\n      const error = err || new Error(`IdentityStore.fetchPath: ${path} ${err.message}.`)\n      NylasEnv.reportError(error)\n      return null\n    }\n  }\n\n  nylasIDRequest(options) {\n    return new Promise((resolve, reject) => {\n      options.formData = false\n      options.json = true\n      options.auth = {\n        username: this._identity.token,\n        password: '',\n        sendImmediately: true,\n      }\n      const requestId = Utils.generateTempId();\n      Actions.willMakeAPIRequest({\n        request: options,\n        requestId: requestId,\n      });\n      request(options, (error, response = {}, body) => {\n        Actions.didMakeAPIRequest({\n          request: options,\n          statusCode: response.statusCode,\n          error: error,\n          requestId: requestId,\n        });\n        if (error || response.statusCode > 299) {\n          const apiError = new APIError({\n            error, response, body, requestOptions: options});\n          return reject(apiError)\n        }\n        return resolve(body);\n      });\n    })\n  }\n}\n\nexport default new IdentityStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/mail-rules-store.coffee",
    "content": "NylasStore = require 'nylas-store'\n_ = require 'underscore'\nRx = require 'rx-lite'\nAccountStore = require('./account-store').default\nDatabaseStore = require('./database-store').default\nTaskQueueStatusStore = require './task-queue-status-store'\nReprocessMailRulesTask = require('../tasks/reprocess-mail-rules-task').default\nUtils = require '../models/utils'\nActions = require('../actions').default\n\n{ConditionMode, ConditionTemplates, ActionTemplates} = require '../../mail-rules-templates'\n\nRulesJSONBlobKey = \"MailRules-V2\"\n\nclass MailRulesStore extends NylasStore\n  constructor: ->\n    @_rules = []\n\n    query = DatabaseStore.findJSONBlob(RulesJSONBlobKey)\n    @_subscription = Rx.Observable.fromQuery(query).subscribe (rules) =>\n      @_rules = rules ? []\n      @trigger()\n\n    @listenTo Actions.addMailRule, @_onAddMailRule\n    @listenTo Actions.deleteMailRule, @_onDeleteMailRule\n    @listenTo Actions.reorderMailRule, @_onReorderMailRule\n    @listenTo Actions.updateMailRule, @_onUpdateMailRule\n    @listenTo Actions.disableMailRule, @_onDisableMailRule\n\n  rules: =>\n    @_rules\n\n  rulesForAccountId: (accountId) =>\n    @_rules.filter (f) => f.accountId is accountId\n\n  disabledRules: (accountId) =>\n    @_rules.filter (f) => f.disabled\n\n  _onDeleteMailRule: (id) =>\n    @_rules = @_rules.filter (f) -> f.id isnt id\n    @_saveMailRules()\n    @trigger()\n\n  _onReorderMailRule: (id, newIdx) =>\n    currentIdx = _.findIndex(@_rules, _.matcher({id}))\n    return if currentIdx is -1\n    rule = @_rules[currentIdx]\n    @_rules.splice(currentIdx, 1)\n    @_rules.splice(newIdx, 0, rule)\n    @_saveMailRules()\n    @trigger()\n\n  _onAddMailRule: (properties) =>\n    defaults =\n      id: Utils.generateTempId()\n      name: \"Untitled Rule\"\n      conditionMode: ConditionMode.All\n      conditions: [ConditionTemplates[0].createDefaultInstance()]\n      actions: [ActionTemplates[0].createDefaultInstance()]\n      disabled: false\n\n    unless properties.accountId\n      throw new Error(\"AddMailRule: you must provide an account id.\")\n\n    @_rules.push(_.extend(defaults, properties))\n    @_saveMailRules()\n    @trigger()\n\n  _onUpdateMailRule: (id, properties) =>\n    existing = _.find @_rules, (f) -> id is f.id\n    existing[key] = val for key, val of properties\n    @_saveMailRules()\n    @trigger()\n\n  _onDisableMailRule: (id, reason) =>\n    existing = _.find @_rules, (f) -> id is f.id\n    return if not existing or existing.disabled is true\n\n    # Disable the task\n    existing.disabled = true\n    existing.disabledReason = reason\n    @_saveMailRules()\n\n    # Cancel all bulk processing jobs\n    for task in TaskQueueStatusStore.tasksMatching(ReprocessMailRulesTask, {})\n      Actions.dequeueTask(task.id)\n\n    @trigger()\n\n  _saveMailRules: =>\n    @_saveMailRulesDebounced ?= _.debounce =>\n      DatabaseStore.inTransaction (t) =>\n        t.persistJSONBlob(RulesJSONBlobKey, @_rules)\n    ,1000\n    @_saveMailRulesDebounced()\n\n\nmodule.exports = new MailRulesStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/message-body-processor.es6",
    "content": "import _ from 'underscore';\nimport Message from '../models/message';\nimport MessageStore from './message-store';\nimport DatabaseStore from './database-store';\nimport SanitizeTransformer from '../../services/sanitize-transformer';\n\nconst SanitizeSettings = Object.assign({}, SanitizeTransformer.Preset.UnsafeOnly);\n\n// We do not want to filter any URL schemes except file://. (We may add file URLs,\n// but we do it ourselves after sanitizing the body.)\nSanitizeSettings.allowedSchemes = {\n  indexOf: (scheme) => (scheme !== 'file'),\n};\n\nclass MessageBodyProcessor {\n  constructor() {\n    this._subscriptions = [];\n    this.resetCache();\n\n    DatabaseStore.listen((change) => {\n      if (change.objectClass === Message.name) {\n        change.objects.forEach(this.updateCacheForMessage);\n      }\n    });\n  }\n\n  resetCache() {\n    // Store an object for recently processed items. Put the item reference into\n    // both data structures so we can access it in O(1) and also delete in O(1)\n    this._recentlyProcessedA = [];\n    this._recentlyProcessedD = {};\n\n    this._subscriptions.forEach(({message, callback}) => {\n      this.retrieve(message).then(callback)\n    });\n  }\n\n  updateCacheForMessage = (changedMessage) => {\n    // check that the message exists in the cache\n    const changedKey = this._key(changedMessage);\n    if (!this._recentlyProcessedD[changedKey]) {\n      return;\n    }\n\n    // grab the old value\n    const oldOutput = this._recentlyProcessedD[changedKey].body;\n\n    // remove the message from the cache\n    delete this._recentlyProcessedD[changedKey];\n    this._recentlyProcessedA = this._recentlyProcessedA.filter(({key}) =>\n      key !== changedKey\n    );\n\n    // reprocess any subscription using the new message data. Note that\n    // changedMessage may not have a loaded body if it wasn't changed. In\n    // that case, we use the previous body.\n    const subscriptions = this._subscriptions.filter(({message}) =>\n      message.id === changedMessage.id\n    );\n\n    if (subscriptions.length > 0) {\n      const updatedMessage = changedMessage.clone();\n      updatedMessage.body = updatedMessage.body || subscriptions[0].message.body;\n\n      this.retrieve(updatedMessage).then((output) => {\n        // only trigger if the output has really changed\n        if (output !== oldOutput) {\n          for (const subscription of subscriptions) {\n            subscription.callback(output);\n            subscription.message = updatedMessage;\n          }\n        }\n      });\n    }\n  }\n\n  version() {\n    return this._version;\n  }\n\n  subscribe(message, callback) {\n    const sub = {message, callback};\n\n    // Extra defer to ensure that subscribe never calls it's callback synchronously,\n    // (In Node, callbacks should always be called after caller execution has finished)\n    _.defer(() => this.retrieve(message).then((output) => {\n      if (this._subscriptions.includes(sub)) {\n        callback(output);\n      }\n    }));\n\n    this._subscriptions.push(sub);\n    return () => {\n      this._subscriptions.splice(this._subscriptions.indexOf(sub), 1);\n    }\n  }\n\n  retrieve(message) {\n    const key = this._key(message);\n    if (this._recentlyProcessedD[key]) {\n      return Promise.resolve(this._recentlyProcessedD[key].body);\n    }\n\n    return this._process(message).then((body) => {\n      this._addToCache(key, body)\n      return body;\n    });\n  }\n\n  // Private Methods\n\n  _key(message) {\n    // It's safe to key off of the message ID alone because we invalidate the\n    // cache whenever the message is persisted to the database.\n    return message.id;\n  }\n\n  _process(message) {\n    if (!_.isString(message.body)) {\n      return Promise.resolve(\"\");\n    }\n\n    // Sanitizing <script> tags, etc. isn't necessary because we use CORS rules\n    // to prevent their execution and sandbox content in the iFrame, but we still\n    // want to remove contenteditable attributes and other strange things.\n    return SanitizeTransformer.run(message.body, SanitizeSettings).then((sanitized) => {\n      let body = sanitized;\n      for (const extension of MessageStore.extensions()) {\n        if (!extension.formatMessageBody) {\n          continue;\n        }\n\n        // Give each extension the message object to process the body, but don't\n        // allow them to modify anything but the body for the time being.\n        const previousBody = body;\n        try {\n          const virtual = message.clone();\n          virtual.body = body;\n          extension.formatMessageBody({message: virtual});\n          body = virtual.body;\n        } catch (err) {\n          NylasEnv.reportError(err);\n          body = previousBody;\n        }\n      }\n      return body;\n    });\n  }\n\n  _addToCache(key, body) {\n    if (this._recentlyProcessedA.length > 50) {\n      const removed = this._recentlyProcessedA.pop()\n      delete this._recentlyProcessedD[removed.key]\n    }\n    const item = {key, body};\n    this._recentlyProcessedA.unshift(item);\n    this._recentlyProcessedD[key] = item;\n  }\n}\n\nconst store = new MessageBodyProcessor();\nexport default store\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/message-store-extension.coffee",
    "content": "###\nPublic: MessageStoreExtension is deprecated. Use {MessageViewExtension} instead.\nSection: Extensions\n###\nclass MessageStoreExtension\n\nmodule.exports = MessageStoreExtension\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/message-store.coffee",
    "content": "NylasStore = require \"nylas-store\"\nActions = require(\"../actions\").default\nMessage = require(\"../models/message\").default\nThread = require(\"../models/thread\").default\nUtils = require '../models/utils'\nDatabaseStore = require(\"./database-store\").default\nFocusedPerspectiveStore = require('./focused-perspective-store').default\nFocusedContentStore = require \"./focused-content-store\"\nNylasAPIHelpers = require '../nylas-api-helpers'\nExtensionRegistry = require('../../registries/extension-registry')\n{deprecate} = require '../../deprecate-utils'\nasync = require 'async'\n_ = require 'underscore'\n\nCategoryNamesHiddenByDefault = ['spam', 'trash']\n\nclass MessageStore extends NylasStore\n\n  constructor: ->\n    @_setStoreDefaults()\n    @_registerListeners()\n\n  ########### PUBLIC #####################################################\n\n  items: ->\n    return @_items if @_showingHiddenItems\n\n    viewing = FocusedPerspectiveStore.current().categoriesSharedName()\n    viewingHiddenCategory = viewing in CategoryNamesHiddenByDefault\n\n    if viewingHiddenCategory\n      return @_items.filter (item) ->\n        inHidden = _.any item.categories, (cat) -> cat.name in CategoryNamesHiddenByDefault\n        return inHidden or item.draft is true\n    else\n      return @_items.filter (item) ->\n        inHidden = _.any item.categories, (cat) -> cat.name in CategoryNamesHiddenByDefault\n        return not inHidden\n\n  threadId: -> @_thread?.id\n\n  thread: -> @_thread\n\n  itemsExpandedState: =>\n    # ensure that we're always serving up immutable objects.\n    # this.state == nextState is always true if we modify objects in place.\n    _.clone @_itemsExpanded\n\n  hasCollapsedItems: ->\n    _.size(@_itemsExpanded) < @_items.length\n\n  numberOfHiddenItems: ->\n    @_items.length - @items().length\n\n  itemClientIds: ->\n    _.pluck(@_items, \"clientId\")\n\n  itemsLoading: ->\n    @_itemsLoading\n\n  ###\n  Message Store Extensions\n  ###\n\n  # Public: Returns the extensions registered with the MessageStore.\n  extensions: =>\n    ExtensionRegistry.MessageView.extensions()\n\n  # Public: Deprecated, use {ExtensionRegistry.MessageView.register} instead.\n  # Registers a new extension with the MessageStore. MessageStore extensions\n  # make it possible to customize message body parsing, and will do more in\n  # the future.\n  #\n  # - `ext` A {MessageViewExtension} instance.\n  #\n  registerExtension: (ext) =>\n    ExtensionRegistry.MessageView.register(ext)\n\n  # Public: Deprecated, use {ExtensionRegistry.MessageView.unregister} instead.\n  # Unregisters the extension provided from the MessageStore.\n  #\n  # - `ext` A {MessageViewExtension} instance.\n  #\n  unregisterExtension: (ext) =>\n    ExtensionRegistry.MessageView.unregister(ext)\n\n  _onExtensionsChanged: (role) ->\n    MessageBodyProcessor = require('./message-body-processor').default\n    MessageBodyProcessor.resetCache()\n\n\n  ########### PRIVATE ####################################################\n\n  _setStoreDefaults: =>\n    @_items = []\n    @_itemsExpanded = {}\n    @_itemsLoading = false\n    @_showingHiddenItems = false\n    @_thread = null\n\n  _registerListeners: ->\n    @listenTo ExtensionRegistry.MessageView, @_onExtensionsChanged\n    @listenTo DatabaseStore, @_onDataChanged\n    @listenTo FocusedContentStore, @_onFocusChanged\n    @listenTo Actions.toggleMessageIdExpanded, @_onToggleMessageIdExpanded\n    @listenTo Actions.toggleAllMessagesExpanded, @_onToggleAllMessagesExpanded\n    @listenTo Actions.toggleHiddenMessages, @_onToggleHiddenMessages\n    @listenTo FocusedPerspectiveStore, @_onPerspectiveChanged\n    @listenTo Actions.popoutThread, @_onPopoutThread\n    @listenTo Actions.focusThreadMainWindow, @_onFocusThreadMainWindow\n\n  _onPerspectiveChanged: =>\n    @trigger()\n\n  _onDataChanged: (change) =>\n    return unless @_thread\n\n    if change.objectClass is Message.name\n      inDisplayedThread = _.some change.objects, (obj) => obj.threadId is @_thread.id\n      return unless inDisplayedThread\n\n      if change.objects.length is 1 and change.objects[0].draft is true\n        item = change.objects[0]\n        itemIndex = _.findIndex @_items, (msg) -> msg.id is item.id or msg.clientId is item.clientId\n\n        if change.type is 'persist' and itemIndex is -1\n          @_items = [].concat(@_items, [item]).filter((m) => !m.isHidden())\n          @_items = @_sortItemsForDisplay(@_items)\n          @_expandItemsToDefault()\n          @trigger()\n          return\n\n        if change.type is 'unpersist' and itemIndex isnt -1\n          @_items = [].concat(@_items).filter((m) => !m.isHidden())\n          @_items.splice(itemIndex, 1)\n          @_expandItemsToDefault()\n          @trigger()\n          return\n\n      @_fetchFromCache()\n\n    if change.objectClass is Thread.name\n      updatedThread = _.find change.objects, (obj) => obj.id is @_thread.id\n      if updatedThread\n        @_thread = updatedThread\n        @_fetchFromCache()\n\n  _onFocusChanged: (change) =>\n    return unless change.impactsCollection('thread')\n\n    # This implements a debounce that fires on the leading and trailing edge.\n    #\n    # If we haven't changed focus in the last 100ms, do it immediately. This means\n    # there is no delay when moving to the next thread, deselecting a thread, etc.\n    #\n    # If we have changed focus in the last 100ms, wait for focus changes to\n    # stop arriving for 100msec before applying. This means that flying\n    # through threads doesn't cause is to make a zillion queries for messages.\n    #\n    if not @_onFocusChangedTimer\n      @_onApplyFocusChange()\n    else\n      clearTimeout(@_onFocusChangedTimer)\n\n    @_onFocusChangedTimer = setTimeout =>\n      @_onFocusChangedTimer = null\n      @_onApplyFocusChange()\n    , 100\n\n  _onApplyFocusChange: =>\n    focused = FocusedContentStore.focused('thread')\n    return if @_thread?.id is focused?.id\n\n    @_thread = focused\n    @_items = []\n    @_itemsLoading = true\n    @_showingHiddenItems = false\n    @_itemsExpanded = {}\n    @trigger()\n\n    @_fetchFromCache()\n\n  _markAsRead: ->\n    # Mark the thread as read if necessary. Make sure it's still the\n    # current thread after the timeout.\n    #\n    # Override canBeUndone to return false so that we don't see undo\n    # prompts (since this is a passive action vs. a user-triggered\n    # action.)\n    return if not @_thread\n    return if @_lastLoadedThreadId is @_thread.id\n    @_lastLoadedThreadId = @_thread.id\n\n    if @_thread.unread\n      markAsReadDelay = NylasEnv.config.get('core.reading.markAsReadDelay')\n      markAsReadId = @_thread.id\n      return if markAsReadDelay < 0\n\n      setTimeout =>\n        return unless markAsReadId is @_thread?.id and @_thread.unread\n        Actions.setUnreadThreads({\n          threads: [@_thread],\n          source: \"Thread Selected\",\n          canBeUndone: false,\n          unread: false,\n        })\n      , markAsReadDelay\n\n  _onToggleAllMessagesExpanded: =>\n    if @hasCollapsedItems()\n      @_items.forEach @_expandItem\n    else\n      # Do not collapse the latest message, i.e. the last one\n      @_items[...-1].forEach @_collapseItem\n    @trigger()\n\n  _onToggleHiddenMessages: =>\n    @_showingHiddenItems = !@_showingHiddenItems\n    @_expandItemsToDefault()\n    @_fetchExpandedAttachments(@_items)\n    @trigger()\n\n  _onToggleMessageIdExpanded: (id) =>\n    item = _.findWhere(@_items, {id})\n    return unless item\n\n    if @_itemsExpanded[id]\n      @_collapseItem(item)\n    else\n      @_expandItem(item)\n    @trigger()\n\n  _expandItem: (item) =>\n    @_itemsExpanded[item.id] = \"explicit\"\n    @_fetchExpandedAttachments([item])\n\n  _collapseItem: (item) =>\n    delete @_itemsExpanded[item.id]\n\n  _fetchFromCache: (options = {}) ->\n    return unless @_thread\n\n    loadedThreadId = @_thread.id\n\n    query = DatabaseStore.findAll(Message)\n    query.where(threadId: loadedThreadId)\n    query.include(Message.attributes.body)\n    query.then (items) =>\n      # Check to make sure that our thread is still the thread we were\n      # loading items for. Necessary because this takes a while.\n      return unless loadedThreadId is @_thread?.id\n\n      loaded = true\n\n      @_items = items.filter((m) => !m.isHidden())\n      @_items = @_sortItemsForDisplay(@_items)\n\n      # If no items were returned, attempt to load messages via the API. If items\n      # are returned, this will trigger a refresh here.\n      if @_items.length is 0\n        @_fetchMessages()\n        loaded = false\n\n      @_expandItemsToDefault()\n\n      # Download the attachments on expanded messages.\n      @_fetchExpandedAttachments(@_items)\n\n      # Normally, we would trigger often and let the view's\n      # shouldComponentUpdate decide whether to re-render, but if we\n      # know we're not ready, don't even bother.  Trigger once at start\n      # and once when ready. Many third-party stores will observe\n      # MessageStore and they'll be stupid and re-render constantly.\n      if loaded\n        @_itemsLoading = false\n        @_markAsRead()\n        @trigger(@)\n\n  _fetchExpandedAttachments: (items) ->\n    policy = NylasEnv.config.get('core.attachments.downloadPolicy')\n    return if policy is 'manually'\n\n    for item in items\n      continue unless @_itemsExpanded[item.id]\n      for file in item.files\n        Actions.fetchFile(file)\n\n  # Expand all unread messages, all drafts, and the last message\n  _expandItemsToDefault: ->\n    visibleItems = @items()\n    for item, idx in visibleItems\n      if item.unread or item.draft or idx is visibleItems.length - 1\n        @_itemsExpanded[item.id] = \"default\"\n\n  _fetchMessages: ->\n    NylasAPIHelpers.getCollection(@_thread.accountId, 'messages', {thread_id: @_thread.id})\n\n  _sortItemsForDisplay: (items) ->\n    # Re-sort items in the list so that drafts appear after the message that\n    # they are in reply to, when possible. First, identify all the drafts\n    # with a replyToMessageId and remove them\n    itemsInReplyTo = []\n    for item, index in items by -1\n      if item.draft and item.replyToMessageId\n        itemsInReplyTo.push(item)\n        items.splice(index, 1)\n\n    # For each item with the reply header, re-inset it into the list after\n    # the message which it was in reply to. If we can't find it, put it at the end.\n    for item in itemsInReplyTo\n      for other, index in items\n        if item.replyToMessageId is other.id\n          items.splice(index+1, 0, item)\n          item = null\n          break\n      if item\n        items.push(item)\n\n    items\n\n  _onPopoutThread: (thread) ->\n    NylasEnv.newWindow\n      title: false, # MessageList already displays the thread subject\n      hidden: false,\n      windowKey: \"thread-#{thread.id}\",\n      windowType: 'thread-popout',\n      windowProps:\n        threadId: thread.id,\n        perspectiveJSON: FocusedPerspectiveStore.current().toJSON()\n\n  _onFocusThreadMainWindow: (thread) ->\n    if NylasEnv.isMainWindow()\n      Actions.setFocus({collection: 'thread', item: thread})\n      NylasEnv.focus()\n\n\nstore = new MessageStore()\nstore.registerExtension = deprecate(\n  'MessageStore.registerExtension',\n  'ExtensionRegistry.MessageView.register',\n  store,\n  store.registerExtension\n)\nstore.unregisterExtension = deprecate(\n  'MessageStore.unregisterExtension',\n  'ExtensionRegistry.MessageView.unregister',\n  store,\n  store.unregisterExtension\n)\nstore.CategoryNamesHiddenByDefault = CategoryNamesHiddenByDefault\n\nmodule.exports = store\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/metadata-store.es6",
    "content": "import _ from 'underscore';\nimport NylasStore from 'nylas-store';\nimport Actions from '../actions';\nimport DatabaseStore from './database-store';\nimport SyncbackMetadataTask from '../tasks/syncback-metadata-task';\n\nclass MetadataStore extends NylasStore {\n\n  constructor() {\n    super();\n    this.listenTo(Actions.setMetadata, this._onSetMetadata);\n  }\n\n  _onSetMetadata(modelOrModels, pluginId, metadataValue) {\n    const models = (modelOrModels instanceof Array) ? modelOrModels : [modelOrModels];\n    const modelClass = models[0].constructor\n    if (!models.every(m => m.constructor === modelClass)) {\n      throw new Error('Actions.setMetadata - All models provided must be of the same type')\n    }\n    DatabaseStore.inTransaction((t) => {\n      // Get the latest version of the models from the database before applying\n      // metadata in case other plugins also saved metadata, and we don't want\n      // to overwrite it\n      return (\n        t.modelify(modelClass, _.pluck(models, 'clientId'))\n        .then((latestModels) => {\n          const updatedModels = _.compact(latestModels).map(m => m.applyPluginMetadata(pluginId, metadataValue));\n          return (\n            t.persistModels(updatedModels)\n            .then(() => Promise.resolve(updatedModels))\n          )\n        })\n      )\n    }).then((updatedModels) => {\n      updatedModels.forEach((updated) => {\n        if (updated.isSavedRemotely()) {\n          const task = new SyncbackMetadataTask(updated.clientId, updated.constructor.name, pluginId);\n          Actions.queueTask(task);\n        }\n      })\n    });\n  }\n}\n\nconst store = new MetadataStore();\nexport default store\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/modal-store.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport {Modal} from 'nylas-component-kit';\nimport NylasStore from 'nylas-store'\n\nimport Actions from '../actions'\n\nconst CONTAINER_ID = \"nylas-modal-container\";\n\nfunction createContainer(id) {\n  const element = document.createElement(id);\n  document.body.appendChild(element);\n  return element;\n}\n\nclass ModalStore extends NylasStore {\n\n  constructor(containerId = CONTAINER_ID) {\n    super()\n    this.isOpen = false;\n    this.container = createContainer(containerId);\n    ReactDOM.render(<span />, this.container);\n\n    this.listenTo(Actions.openModal, this.openModal);\n    this.listenTo(Actions.closeModal, this.closeModal);\n  }\n\n  isModalOpen = () => {\n    return this.isOpen;\n  };\n\n  renderModal = (child, props, callback) => {\n    const modal = (\n      <Modal {...props}>{child}</Modal>\n    );\n\n    ReactDOM.render(modal, this.container, () => {\n      this.isOpen = true;\n      this.trigger();\n      callback();\n    });\n  };\n\n  openModal = ({component, height, width}, callback = () => {}) => {\n    const props = {\n      height: height,\n      width: width,\n    };\n\n    if (this.isOpen) {\n      this.closeModal(() => {\n        this.renderModal(component, props, callback);\n      })\n    } else {\n      this.renderModal(component, props, callback);\n    }\n  };\n\n  closeModal = (callback = () => {}) => {\n    ReactDOM.render(<span />, this.container, () => {\n      this.isOpen = false;\n      this.trigger();\n      callback();\n    });\n  };\n\n}\n\nexport default new ModalStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/observable-list-data-source.es6",
    "content": "import {ListTabular} from 'nylas-component-kit'\n\n/**\nThis class takes an observable which vends QueryResultSets and adapts it so that\nyou can make it the data source of a MultiselectList.\n\nWhen the MultiselectList is refactored to take an Observable, this class should\ngo away!\n*/\nexport default class ObservableListDataSource extends ListTabular.DataSource {\n\n  constructor(resultSetObservable, setRetainedRange) {\n    super()\n    this._$resultSetObservable = resultSetObservable;\n    this._setRetainedRange = setRetainedRange;\n    this._countEstimate = -1\n    this._resultSet = null\n    this._resultDesiredLast = null\n\n    // Wait until a retained range is set before subscribing to result sets\n  }\n\n  _attach = () => {\n    this._subscription = this._$resultSetObservable.subscribe((nextResultSet) => {\n      if (nextResultSet.range().end === this._resultDesiredLast) {\n        this._countEstimate = Math.max(this._countEstimate, nextResultSet.range().end)\n      } else {\n        this._countEstimate = nextResultSet.range().end\n      }\n\n      const previousResultSet = this._resultSet;\n      this._resultSet = nextResultSet;\n\n      // If the result set is derived from a query, remove any items in the selection\n      // that do not match the query. This ensures that items \"removed from the view\"\n      // are removed from the selection.\n      const query = nextResultSet.query();\n      if (query) {\n        this.selection.removeItemsNotMatching(query.matchers());\n      }\n\n      this.trigger({previous: previousResultSet, next: nextResultSet});\n    });\n  }\n\n  setRetainedRange({start, end}) {\n    this._resultDesiredLast = end;\n    this._setRetainedRange({start, end});\n    if (!this._subscription) {\n      this._attach();\n    }\n  }\n\n  // Retrieving Data\n\n  count() {\n    return this._countEstimate;\n  }\n\n  loaded() {\n    return this._resultSet !== null;\n  }\n\n  empty = () => {\n    return !this._resultSet || this._resultSet.empty();\n  }\n\n  get = (offset) => {\n    if (!this._resultSet) {\n      return null;\n    }\n    return this._resultSet.modelAtOffset(offset);\n  }\n\n  getById(id) {\n    if (!this._resultSet) {\n      return null;\n    }\n    return this._resultSet.modelWithId(id)\n  }\n\n  indexOfId(id) {\n    if (!this._resultSet || !id) {\n      return -1;\n    }\n    return this._resultSet.offsetOfId(id);\n  }\n\n  itemsCurrentlyInView() {\n    if (!this._resultSet) { return [] }\n    return this._resultSet.models()\n  }\n\n  itemsCurrentlyInViewMatching(matchFn) {\n    if (!this._resultSet) {\n      return [];\n    }\n    return this._resultSet.models().filter(matchFn);\n  }\n\n  cleanup() {\n    if (this._subscription) {\n      this._subscription.dispose();\n    }\n    return super.cleanup();\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/online-status-store.es6",
    "content": "import isOnline from 'is-online'\nimport NylasStore from 'nylas-store'\nimport {ExponentialBackoffScheduler} from 'isomorphic-core'\nimport Actions from '../actions'\n\n\nconst CHECK_ONLINE_INTERVAL = 30 * 1000\n\nclass OnlineStatusStore extends NylasStore {\n\n  constructor() {\n    super()\n    this._isOnline = true\n    this._retryingInSeconds = 0\n    this._countdownInterval = null\n    this._checkOnlineTimeout = null\n    this._backoffScheduler = new ExponentialBackoffScheduler({jitter: false})\n\n    this.setupEmitter()\n\n    if (NylasEnv.isMainWindow()) {\n      Actions.checkOnlineStatus.listen(() => this._checkOnlineStatus())\n      this._checkOnlineStatus()\n    }\n  }\n\n  isOnline() {\n    return this._isOnline\n  }\n\n  retryingInSeconds() {\n    return this._retryingInSeconds\n  }\n\n  async _setNextOnlineState() {\n    const nextIsOnline = await isOnline()\n    if (this._isOnline !== nextIsOnline) {\n      this._isOnline = nextIsOnline\n      this.trigger()\n    }\n  }\n\n  async _checkOnlineStatus() {\n    this._clearCheckOnlineInterval()\n    this._clearRetryCountdown()\n\n    // If we are currently offline, this trigger will show the `Retrying now...`\n    // message\n    this._retryingInSeconds = 0\n    this.trigger()\n\n    await this._setNextOnlineState()\n\n    if (!this._isOnline) {\n      this._checkOnlineStatusAfterBackoff()\n    } else {\n      this._backoffScheduler.reset()\n      this._checkOnlineTimeout = setTimeout(() => {\n        this._checkOnlineStatus()\n      }, CHECK_ONLINE_INTERVAL)\n    }\n  }\n\n  async _checkOnlineStatusAfterBackoff() {\n    const nextDelayMs = this._backoffScheduler.nextDelay()\n    try {\n      await this._countdownRetryingInSeconds(nextDelayMs)\n      this._checkOnlineStatus()\n    } catch (err) {\n      // This means the retry countdown was cleared, in which case we don't\n      // want to do anything\n    }\n  }\n\n  async _countdownRetryingInSeconds(nextDelayMs) {\n    this._retryingInSeconds = Math.ceil(nextDelayMs / 1000)\n    this.trigger()\n\n    return new Promise((resolve, reject) => {\n      this._clearRetryCountdown()\n      this._emitter.once('clear-retry-countdown', () => reject(new Error('Retry countdown cleared')))\n\n      this._countdownInterval = setInterval(() => {\n        this._retryingInSeconds = Math.max(0, this._retryingInSeconds - 1)\n        this.trigger()\n\n        if (this._retryingInSeconds === 0) {\n          this._clearCountdownInterval()\n          resolve()\n        }\n      }, 1000)\n    })\n  }\n\n  _clearCheckOnlineInterval() {\n    clearInterval(this._checkOnlineTimeout)\n    this._checkOnlineTimeout = null\n  }\n\n  _clearCountdownInterval() {\n    clearInterval(this._countdownInterval)\n    this._countdownInterval = null\n  }\n\n  _clearRetryCountdown() {\n    this._clearCountdownInterval()\n    this._emitter.emit('clear-retry-countdown')\n  }\n}\n\nexport default new OnlineStatusStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/outbox-store.es6",
    "content": "import NylasStore from 'nylas-store';\nimport SendDraftTask from '../tasks/send-draft-task';\nimport TaskQueueStatusStore from './task-queue-status-store';\n\nclass OutboxStore extends NylasStore {\n  constructor() {\n    super();\n    this._tasks = [];\n    this.listenTo(TaskQueueStatusStore, this._populate);\n    this._populate();\n  }\n\n  _populate() {\n    const nextTasks = TaskQueueStatusStore.queue().filter((task) =>\n      (task instanceof SendDraftTask)\n    );\n    if ((this._tasks.length === 0) && (nextTasks.length === 0)) {\n      return;\n    }\n    this._tasks = nextTasks;\n    this.trigger();\n  }\n\n  itemsForAccount(accountId) {\n    return this._tasks.filter((task) => task.draftAccountId === accountId);\n  }\n}\n\nconst store = new OutboxStore()\nexport default store\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/popover-store.jsx",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport NylasStore from 'nylas-store'\nimport Actions from '../actions'\nimport FixedPopover from '../../components/fixed-popover'\n\n\nconst CONTAINER_ID = \"nylas-popover-container\";\n\nfunction createContainer(id) {\n  const element = document.createElement(id);\n  document.body.appendChild(element);\n  return element;\n}\n\nclass PopoverStore extends NylasStore {\n\n  constructor(containerId = CONTAINER_ID) {\n    super()\n    this.isOpen = false;\n    this.container = createContainer(containerId);\n    ReactDOM.render(<span />, this.container);\n\n    this.listenTo(Actions.openPopover, this.openPopover);\n    this.listenTo(Actions.closePopover, this.closePopover);\n  }\n\n  renderPopover = (child, props, callback) => {\n    const popover = (\n      <FixedPopover {...props}>{child}</FixedPopover>\n    );\n\n    ReactDOM.render(popover, this.container, () => {\n      this.isOpen = true;\n      this.trigger();\n      callback();\n    });\n  };\n\n  openPopover = (element, {originRect, direction, fallbackDirection, closeOnAppBlur, callback = () => {}}) => {\n    const props = {\n      direction,\n      originRect,\n      fallbackDirection,\n      closeOnAppBlur,\n    };\n\n    if (this.isOpen) {\n      this.closePopover(() => {\n        this.renderPopover(element, props, callback);\n      })\n    } else {\n      this.renderPopover(element, props, callback);\n    }\n  };\n\n  closePopover = (callback = () => {}) => {\n    ReactDOM.render(<span />, this.container, () => {\n      this.isOpen = false;\n      this.trigger();\n      callback();\n    });\n  };\n\n}\n\nexport default new PopoverStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/preferences-ui-store.es6",
    "content": "import {ipcRenderer} from 'electron';\nimport _ from 'underscore'\nimport NylasStore from 'nylas-store'\nimport Immutable from 'immutable'\nimport WorkspaceStore from './workspace-store'\nimport FocusedPerspectiveStore from './focused-perspective-store'\nimport Actions from '../actions'\n\n\nconst MAIN_TAB_ITEM_ID = 'General'\n\nclass TabItem {\n  constructor(opts = {}) {\n    opts.order = opts.order || Infinity;\n    _.extend(this, opts);\n  }\n}\n\nclass PreferencesUIStore extends NylasStore {\n\n  constructor() {\n    super();\n\n    const perspective = FocusedPerspectiveStore.current()\n    this._tabs = Immutable.List();\n    this._selection = Immutable.Map({\n      tabId: null,\n      accountId: perspective.account ? perspective.account.id : null,\n    })\n\n    this._triggerDebounced = _.debounce(() => this.trigger(), 20)\n    this.setupListeners()\n  }\n\n  get TabItem() {\n    return TabItem\n  }\n\n  setupListeners() {\n    if (NylasEnv.isMainWindow()) {\n      this.listenTo(Actions.openPreferences, this.openPreferences);\n      ipcRenderer.on('open-preferences', this.openPreferences);\n\n      this.listenTo(Actions.switchPreferencesTab, this.switchPreferencesTab);\n    }\n\n    NylasEnv.commands.add(document.body, 'core:show-keybindings', () => {\n      Actions.openPreferences();\n      Actions.switchPreferencesTab('Shortcuts');\n    });\n  }\n\n  tabs() {\n    return this._tabs;\n  }\n\n  selection() {\n    return this._selection;\n  }\n\n  openPreferences = () => {\n    ipcRenderer.send('command', 'application:show-main-window');\n    if (WorkspaceStore.topSheet() !== WorkspaceStore.Sheet.Preferences) {\n      Actions.pushSheet(WorkspaceStore.Sheet.Preferences);\n    }\n  }\n\n  switchPreferencesTab = (tabId, options = {}) => {\n    this._selection = this._selection.set('tabId', tabId);\n    if (options.accountId) {\n      this._selection = this._selection.set('accountId', options.accountId);\n    }\n    this.trigger();\n  }\n\n  /*\n  Public: Register a new top-level section to preferences\n\n  - `tabItem` a `PreferencesUIStore.TabItem` object\n    schema definitions on the PreferencesUIStore.Section.MySectionId\n    - `tabId` A unique name to access the Section by\n    - `displayName` The display name. This may go through i18n.\n    - `component` The Preference section's React Component.\n\n  Most Preference sections include an area where a {PreferencesForm} is\n  rendered. This is a type of {GeneratedForm} that uses the schema passed\n  into {PreferencesUIStore::registerPreferences}\n\n  */\n  registerPreferencesTab = (tabItem) => {\n    this._tabs = this._tabs.push(tabItem).sort((a, b) =>\n      a.order > b.order\n    )\n    if (tabItem.tabId === MAIN_TAB_ITEM_ID) {\n      this._selection = this._selection.set('tabId', tabItem.tabId);\n    }\n    this._triggerDebounced();\n  }\n\n  unregisterPreferencesTab = (tabItemOrId) => {\n    this._tabs = this._tabs.filter(s => (s.tabId !== tabItemOrId) && (s !== tabItemOrId));\n    this._triggerDebounced();\n  }\n}\n\nexport default new PreferencesUIStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/recently-read-store.es6",
    "content": "import NylasStore from 'nylas-store';\nimport ChangeUnreadTask from '../tasks/change-unread-task';\nimport ChangeLabelsTask from '../tasks/change-labels-task';\nimport ChangeFolderTask from '../tasks/change-folder-task';\nimport Actions from '../actions';\n\n// The \"Unread\" view shows all threads which are unread. When you read a thread,\n// it doesn't disappear until you leave the view and come back. This behavior\n// is implemented by keeping track of messages being rea and manually\n// whitelisting them in the query.\n\nclass RecentlyReadStore extends NylasStore {\n  constructor() {\n    super();\n    this.ids = [];\n    this.listenTo(Actions.focusMailboxPerspective, () => {\n      this.ids = [];\n      this.trigger();\n    });\n    this.listenTo(Actions.queueTasks, (tasks) => {\n      this.tasksQueued(tasks);\n    });\n    this.listenTo(Actions.queueTask, (task) => {\n      this.tasksQueued([task]);\n    });\n  }\n\n  tasksQueued(tasks) {\n    let changed = false;\n\n    tasks.filter(task =>\n      task instanceof ChangeUnreadTask\n    ).forEach(({threads}) => {\n      const threadIds = threads.map(t => (t.id ? t.id : t));\n      this.ids = this.ids.concat(threadIds);\n      changed = true;\n    });\n\n    tasks.filter(task =>\n      task instanceof ChangeLabelsTask || task instanceof ChangeFolderTask\n    ).forEach(({threads}) => {\n      const threadIds = threads.map(t => (t.id ? t.id : t));\n      this.ids = this.ids.filter(id => !threadIds.includes(id));\n      changed = true;\n    });\n\n    if (changed) {\n      this.trigger();\n    }\n  }\n}\n\nconst store = new RecentlyReadStore()\nexport default store\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/searchable-component-store.es6",
    "content": "import _ from 'underscore'\nimport NylasStore from 'nylas-store'\nimport DOMUtils from '../../dom-utils'\nimport Actions from '../actions'\nimport {MAX_MATCHES, CHAR_THRESHOLD} from '../../searchable-components/search-constants'\nimport FocusedContentStore from './focused-content-store'\n\nclass SearchableComponentStore extends NylasStore {\n  constructor() {\n    super();\n    this.currentMatch = null\n    this.matches = []\n    this.globalIndex = null // null means nothing is selected\n    this.scrollAncestor = null\n\n    // null and empty string are different. Null means that search isn't\n    // even activated. Empty string means we're active but just not\n    // searching anything.\n    this.searchTerm = null\n\n    this.searchRegions = {}\n\n    this._lastThread = FocusedContentStore.focused('thread')\n\n    this.listenTo(Actions.findInThread, this._findInThread)\n    this.listenTo(Actions.nextSearchResult, this._nextSearchResult)\n    this.listenTo(Actions.previousSearchResult, this._previousSearchResult)\n    this.listenTo(FocusedContentStore, () => {\n      const newThread = FocusedContentStore.focused('thread')\n      if (newThread !== this._lastThread) {\n        this._findInThread(null);\n        this._lastThread = newThread\n      }\n    })\n  }\n\n  getCurrentRegionIndex(regionId) {\n    let regionOffset = null;\n    if (regionId && this.currentMatch && this.currentMatch.node.getAttribute('data-region-id') === regionId) {\n      regionOffset = +this.currentMatch.node.getAttribute('data-render-index')\n    }\n    return regionOffset\n  }\n\n  getCurrentSearchData() {\n    return {\n      searchTerm: this.searchTerm,\n      globalIndex: this.globalIndex,\n      resultsLength: this.matches.length,\n    }\n  }\n\n  scrollbarTicks() {\n    let ticks = []\n    if (this.matches.length > 0 && this.scrollAncestor && this.scrollAncestor.scrollHeight > -1) {\n      ticks = this.matches.map((match) => {\n        if (match === this.currentMatch) {\n          return {\n            percent: match.top / this.scrollAncestor.scrollHeight,\n            className: \"match\",\n          }\n        }\n        return match.top / this.scrollAncestor.scrollHeight\n      })\n    }\n    return ticks\n  }\n\n  _nextSearchResult = () => {\n    this._moveGlobalIndexBy(1);\n  }\n\n  _previousSearchResult = () => {\n    this._moveGlobalIndexBy(-1);\n  }\n\n  // This needs to be debounced since it's called when all of our\n  // components are mounting and unmounting. It also is very expensive\n  // since it calls `getBoundingClientRect` and will trigger repaints.\n  _recalculateMatches = _.debounce(() => {\n    this.matches = []\n\n    // searchNodes need to all be under the root document. matches\n    // may contain nodes inside of iframes which are not attached ot the\n    // root document.\n    const searchNodes = []\n\n    if (this.searchTerm && this.searchTerm.length >= CHAR_THRESHOLD) {\n      _.each(this.searchRegions, (node) => {\n        if (this.matches.length >= MAX_MATCHES) {\n          return;\n        }\n        let refNode;\n        let topOffset = 0;\n        let leftOffset = 0;\n        if (node.nodeName === \"IFRAME\") {\n          searchNodes.push(node)\n          const iframeRect = node.getBoundingClientRect();\n          topOffset = iframeRect.top\n          leftOffset = iframeRect.left\n          refNode = node.contentDocument.body\n          if (!refNode) { refNode = node.contentDocument; }\n        } else {\n          refNode = node\n        }\n        const matches = refNode.querySelectorAll('search-match, .search-match');\n        for (let i = 0; i < matches.length; i++) {\n          if (!DOMUtils.nodeIsLikelyVisible(matches[i])) {\n            continue;\n          }\n          const rect = matches[i].getBoundingClientRect();\n          if (node.nodeName !== \"IFRAME\") {\n            searchNodes.push(matches[i])\n          }\n          this.matches.push({\n            node: matches[i],\n            top: rect.top + topOffset,\n            left: rect.left + leftOffset,\n            height: rect.height,\n          });\n          if (this.matches.length >= MAX_MATCHES) {\n            break;\n          }\n        }\n      });\n      this.matches.sort((nodeA, nodeB) => {\n        const aScore = nodeA.top + nodeA.left / 1000\n        const bScore = nodeB.top + nodeB.left / 1000\n        return aScore - bScore\n      });\n\n      if (this.globalIndex !== null) {\n        this.globalIndex = Math.min(this.matches.length - 1, this.globalIndex);\n        this.currentMatch = this.matches[this.globalIndex]\n      }\n\n      const parentFilter = (node) => {\n        return _.contains(node.classList, \"scroll-region-content\")\n      }\n      this.scrollAncestor = DOMUtils.commonAncestor(searchNodes, parentFilter);\n      this.scrollAncestor = this.scrollAncestor.closest(\".scroll-region-content\")\n\n      if (this.scrollAncestor) {\n        const scrollRect = this.scrollAncestor.getBoundingClientRect();\n        const scrollTop = scrollRect.top - this.scrollAncestor.scrollTop;\n        // We save the position relative to the top of the scrollAncestor\n        // instead of the current getBoudingClientRect (which is dependent\n        // on the current scroll position)\n        this.matches.forEach((match) => {\n          match.top -= scrollTop\n        });\n      }\n    } else {\n      this.currentMatch = null;\n      this.globalIndex = null;\n      this.scrollAncestor = null;\n    }\n\n    if (this.matches.length > 0) {\n      if (this.globalIndex === null) {\n        this._moveGlobalIndexBy(1)\n      } else {\n        this._scrollIntoView()\n      }\n    }\n\n    this.trigger()\n  }, 33);\n\n  _moveGlobalIndexBy(amount) {\n    if (this.matches.length === 0) {\n      return\n    }\n    if (this.globalIndex === null) {\n      this.globalIndex = 0;\n    } else {\n      this.globalIndex += amount;\n      if (this.globalIndex < 0) {\n        this.globalIndex += this.matches.length\n      } else {\n        this.globalIndex = this.globalIndex % this.matches.length\n      }\n    }\n    this.currentMatch = this.matches[this.globalIndex]\n    this._scrollIntoView()\n    this.trigger()\n  }\n\n  _scrollIntoView() {\n    if (!this.currentMatch || !this.currentMatch.node || !this.scrollAncestor) {\n      return\n    }\n\n    const visibleRect = this.scrollAncestor.getBoundingClientRect();\n    const scrollTop = this.scrollAncestor.scrollTop\n    const matchMid = this.currentMatch.top + this.currentMatch.height / 2\n\n    if (matchMid < scrollTop || matchMid > scrollTop + visibleRect.height) {\n      const viewportMid = scrollTop + visibleRect.height / 2\n      const delta = matchMid - viewportMid\n      this.scrollAncestor.scrollTop = this.scrollAncestor.scrollTop + delta\n    }\n  }\n\n  _findInThread = (search) => {\n    if (search !== this.searchTerm) {\n      this.searchTerm = search;\n      this.trigger()\n      this._recalculateMatches()\n    }\n  }\n\n  registerSearchRegion(regionId, domNode) {\n    this.searchRegions[regionId] = domNode\n    this._recalculateMatches()\n  }\n\n  unregisterSearchRegion(regionId) {\n    delete this.searchRegions[regionId]\n    this._recalculateMatches()\n  }\n}\nexport default new SearchableComponentStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/send-actions-store.es6",
    "content": "import _ from 'underscore'\nimport _str from 'underscore.string'\nimport NylasStore from 'nylas-store'\nimport Actions from '../actions'\nimport SendDraftTask from '../tasks/send-draft-task';\nimport * as ExtensionRegistry from '../../registries/extension-registry';\n\n\nconst ACTION_CONFIG_KEY = \"core.sending.defaultSendType\";\nconst DefaultSendActionKey = 'send'\nconst DefaultSendAction = {\n  title: \"Send\",\n  iconUrl: null,\n  configKey: DefaultSendActionKey,\n  isAvailableForDraft: () => true,\n  performSendAction: ({draft}) => Actions.queueTask(new SendDraftTask(draft.clientId)),\n}\n\nfunction verifySendAction(sendAction = {}, extension = {}) {\n  const {name} = extension\n  if (!_.isString(sendAction.title)) {\n    throw new Error(`${name}.sendActions must return objects containing a string \"title\"`);\n  }\n  if (!_.isFunction(sendAction.performSendAction)) {\n    throw new Error(`${name}.sendActions must return objects containing an \"performSendAction\" function that will be called when the action is selected`);\n  }\n  return true;\n}\n\nfunction configKeyFromTitle(title) {\n  return _str.dasherize(title.toLowerCase());\n}\n\nfunction getSendActions() {\n  return [DefaultSendAction].concat(\n    ExtensionRegistry.Composer.extensions()\n    .filter((extension) => extension.sendActions != null)\n    .reduce((accum, extension) => {\n      const sendActions = (extension.sendActions() || [])\n      .filter((sendAction) => sendAction != null)\n      .map((sendAction) => {\n        try {\n          verifySendAction(sendAction, extension);\n          sendAction.configKey = configKeyFromTitle(sendAction.title);\n          return sendAction\n        } catch (err) {\n          NylasEnv.reportError(err);\n          return null\n        }\n      })\n      .filter((sendAction) => sendAction != null)\n      return accum.concat(sendActions)\n    }, [])\n  )\n}\n\nclass SendActionsStore extends NylasStore {\n\n  constructor() {\n    super()\n    this._sendActions = []\n    this._onComposerExtensionsChanged()\n    this._unsubscribers = [\n      ExtensionRegistry.Composer.listen(this._onComposerExtensionsChanged),\n    ]\n  }\n\n  get DefaultSendActionKey() {\n    return DefaultSendActionKey\n  }\n\n  get DefaultSendAction() {\n    return DefaultSendAction\n  }\n\n  sendActions() {\n    return this._sendActions\n  }\n\n  sendActionForKey(configKey) {\n    return _.findWhere(this._sendActions, {configKey});\n  }\n\n  availableSendActionsForDraft(draft) {\n    return this._sendActions.filter((sendAction) => sendAction.isAvailableForDraft({draft}))\n  }\n\n  orderedSendActionsForDraft(draft) {\n    const configKeys = this._sendActions.map(({configKey} = {}) => configKey);\n\n    let preferredKey = NylasEnv.config.get(ACTION_CONFIG_KEY);\n    if (!preferredKey || !configKeys.includes(preferredKey)) {\n      preferredKey = DefaultSendActionKey;\n    }\n\n    let preferred = _.findWhere(this._sendActions, {configKey: preferredKey});\n    if (!preferred || !preferred.isAvailableForDraft({draft})) {\n      preferred = DefaultSendAction\n    }\n    const rest = (\n      _.without(this._sendActions, preferred)\n      .filter((sendAction) => sendAction.isAvailableForDraft({draft}))\n    )\n\n    return {preferred, rest};\n  }\n\n  _onComposerExtensionsChanged = () => {\n    this._sendActions = getSendActions()\n    this.trigger()\n  }\n}\n\nexport default new SendActionsStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/signature-store.es6",
    "content": "import {Utils, Actions, AccountStore} from 'nylas-exports';\nimport NylasStore from 'nylas-store'\nimport _ from 'underscore'\n\nconst DefaultSignatureText = \"Sent from <a href=\\\"https://nylas.com?ref=n1\\\">Nylas Mail</a>, the best free email app for work\";\n\nclass SignatureStore extends NylasStore {\n\n  activate() {\n    this.unsubscribers = [\n      Actions.addSignature.listen(this._onAddSignature),\n      Actions.removeSignature.listen(this._onRemoveSignature),\n      Actions.updateSignature.listen(this._onEditSignature),\n      Actions.selectSignature.listen(this._onSelectSignature),\n      Actions.toggleAccount.listen(this._onToggleAccount),\n    ];\n\n    NylasEnv.config.onDidChange(`nylas.signatures`, () => {\n      this.signatures = NylasEnv.config.get(`nylas.signatures`)\n      this.trigger()\n    });\n    NylasEnv.config.onDidChange(`nylas.defaultSignatures`, () => {\n      this.defaultSignatures = NylasEnv.config.get(`nylas.defaultSignatures`)\n      this.trigger()\n    });\n    this.signatures = NylasEnv.config.get(`nylas.signatures`) || {}\n    this.defaultSignatures = NylasEnv.config.get(`nylas.defaultSignatures`) || {}\n\n    // backfill the new signatures structure with old signatures from < v0.4.45\n    let changed = false;\n    for (const account of AccountStore.accounts()) {\n      const signature = NylasEnv.config.get(`nylas.account-${account.id}.signature`)\n      if (signature) {\n        const newId = Utils.generateTempId();\n        this.signatures[newId] = {id: newId, title: account.label, body: signature};\n        this.defaultSignatures[account.emailAddress] = newId;\n        NylasEnv.config.unset(`nylas.account-${account.id}.signature`);\n        changed = true;\n      }\n    }\n    if (changed) {\n      this._saveSignatures();\n      this._saveDefaultSignatures();\n    }\n\n    this.selectedSignatureId = this._setSelectedSignatureId()\n\n    this.trigger()\n  }\n\n  deactivate() {\n    this.unsubscribers.forEach(unsub => unsub());\n  }\n\n  getSignatures() {\n    return this.signatures;\n  }\n\n  selectedSignature() {\n    return this.signatures[this.selectedSignatureId]\n  }\n\n  getDefaults() {\n    return this.defaultSignatures\n  }\n\n  signatureForEmail = (email) => {\n    return this.signatures[this.defaultSignatures[email]] || {id: 'default', body: DefaultSignatureText, title: 'Default'}\n  }\n\n  _saveSignatures() {\n    _.debounce(NylasEnv.config.set(`nylas.signatures`, this.signatures), 500)\n  }\n\n  _saveDefaultSignatures() {\n    _.debounce(NylasEnv.config.set(`nylas.defaultSignatures`, this.defaultSignatures), 500)\n  }\n\n\n  _onSelectSignature = (id) => {\n    this.selectedSignatureId = id\n    this.trigger()\n  }\n\n  _removeByKey = (obj, keyToDelete) => {\n    return Object.keys(obj)\n      .filter(key => key !== keyToDelete)\n      .reduce((result, current) => {\n        result[current] = obj[current];\n        return result;\n      }, {})\n  }\n\n  _setSelectedSignatureId() {\n    const sigIds = Object.keys(this.signatures)\n    if (sigIds.length) {\n      return sigIds[0]\n    }\n    return null\n  }\n\n  _onRemoveSignature = (signatureToDelete) => {\n    this.signatures = this._removeByKey(this.signatures, signatureToDelete.id)\n    this.selectedSignatureId = this._setSelectedSignatureId()\n    this.trigger()\n    this._saveSignatures()\n  }\n\n  _onAddSignature = (sigTitle = \"Untitled\") => {\n    const newId = Utils.generateTempId()\n    this.signatures[newId] = {id: newId, title: sigTitle, body: DefaultSignatureText}\n    this.selectedSignatureId = newId\n    this.trigger()\n    this._saveSignatures()\n  }\n\n  _onEditSignature = (editedSig, oldSigId) => {\n    this.signatures[oldSigId].title = editedSig.title\n    this.signatures[oldSigId].body = editedSig.body\n    this.trigger()\n    this._saveSignatures()\n  }\n\n  _onToggleAccount = (email) => {\n    if (this.defaultSignatures[email] === this.selectedSignatureId) {\n      this.defaultSignatures[email] = null\n    } else {\n      this.defaultSignatures[email] = this.selectedSignatureId\n    }\n\n    this.trigger()\n    this._saveDefaultSignatures()\n  }\n\n}\n\nexport default new SignatureStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/task-queue-status-store.coffee",
    "content": "_ = require 'underscore'\nRx = require 'rx-lite'\nNylasStore = require 'nylas-store'\nDatabaseStore = require('./database-store').default\nAccountStore = require('./account-store').default\nTaskQueue = require './task-queue'\n\n# Public: The TaskQueueStatusStore allows you to inspect the task queue from\n# any window, even though the queue itself only runs in the work window.\n#\nclass TaskQueueStatusStore extends NylasStore\n\n  constructor: ->\n    @_queue = []\n    @_waitingLocals = []\n    @_waitingRemotes = []\n\n    query = DatabaseStore.findJSONBlob(TaskQueue.JSONBlobStorageKey)\n    Rx.Observable.fromQuery(query).subscribe (json) =>\n      @_queue = json || []\n      @_waitingLocals = @_waitingLocals.filter ({task, resolve}) =>\n        queuedTask = _.findWhere(@_queue, {id: task.id})\n        if not queuedTask or queuedTask.queueState.localComplete\n          resolve(task)\n          return false\n        return true\n      @_waitingRemotes = @_waitingRemotes.filter ({task, resolve}) =>\n        queuedTask = _.findWhere(@_queue, {id: task.id})\n        if not queuedTask\n          resolve(task)\n          return false\n        return true\n      @trigger()\n\n  queue: ->\n    @_queue\n\n  waitForPerformLocal: (task) =>\n    new Promise (resolve, reject) =>\n      @_waitingLocals.push({task, resolve})\n\n  waitForPerformRemote: (task) =>\n    new Promise (resolve, reject) =>\n      @_waitingRemotes.push({task, resolve})\n\n  tasksMatching: (type, matching = {}) ->\n    type = type.name unless _.isString(type)\n    @_queue.filter (task) -> task.constructor.name is type and _.isMatch(task, matching)\n\nmodule.exports = new TaskQueueStatusStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/task-queue.coffee",
    "content": "_ = require 'underscore'\nfs = require 'fs-plus'\npath = require 'path'\n\n{Listener, Publisher} = require '../modules/reflux-coffee'\nCoffeeHelpers = require '../coffee-helpers'\n\nTask = require(\"../tasks/task\").default\nTaskRegistry = require('../../registries/task-registry').default\nUtils = require \"../models/utils\"\nReflux = require 'reflux'\nActions = require('../actions').default\nDatabaseStore = require('./database-store').default\n\n{APIError} = require '../errors'\n\nJSONBlobStorageKey = 'task-queue'\n\nif not NylasEnv.isWorkWindow() and not NylasEnv.inSpecMode()\n  module.exports = {JSONBlobStorageKey}\n  return\n\n###\nPublic: The TaskQueue is a Flux-compatible Store that manages a queue of {Task}\nobjects. Each {Task} represents an individual API action, like sending a draft\nor marking a thread as \"read\". Tasks optimistically make changes to the app's\nlocal cache and encapsulate logic for performing changes on the server, rolling\nback in case of failure, and waiting on dependent tasks.\n\nThe TaskQueue is essential to offline mode in N1. It automatically pauses\nwhen the user's internet connection is unavailable and resumes when online.\n\nThe task queue is persisted to disk, ensuring that tasks are executed later,\neven if the user quits N1.\n\nThe TaskQueue is only available in the app's main window. Rather than directly\nqueuing tasks, you should use the {Actions} to interact with the {TaskQueue}.\nTasks queued from secondary windows are serialized and sent to the application's\nmain window via IPC.\n\n## Queueing a Task\n\n```coffee\nif @_thread && @_thread.unread\n  Actions.queueTask(new ChangeStarredTask(thread: @_thread, starred: true))\n```\n\n## Dequeueing a Task\n\n```coffee\nActions.dequeueMatchingTask({\n  type: 'DestroyCategoryTask',\n  matching: {\n    categoryId: 'bla'\n  }\n})\n```\n\nSection: Stores\n###\nclass TaskQueue\n  @include: CoffeeHelpers.includeModule\n\n  @include Publisher\n  @include Listener\n\n  constructor: ->\n    @_queue = []\n    @_completed = []\n    @_updatePeriodicallyTimeout = null\n    @_currentSequentialId = Date.now()\n\n    @_restoreQueue()\n\n    @_savedOnUnload = false\n    NylasEnv.onBeforeUnload((finishUnload) =>\n      if @_savedOnUnload then return true\n      @_saveQueue()\n      .finally(=>\n        @_savedOnUnload = true\n        finishUnload()\n      )\n      return false\n    )\n\n    @listenTo Actions.queueTask, @enqueue\n    @listenTo Actions.queueTasks, (tasks) =>\n      return unless tasks and tasks.length > 0\n      @enqueue(t) for t in tasks\n    @listenTo Actions.undoTaskId, @enqueueUndoOfTaskId\n    @listenTo Actions.dequeueTask, @dequeue\n    @listenTo Actions.dequeueAllTasks, @dequeueAll\n    @listenTo Actions.dequeueMatchingTask, @dequeueMatching\n    @listenTo Actions.clearDeveloperConsole,  @clearCompleted\n\n  queue: =>\n    @_queue\n\n  completed: =>\n    @_completed\n\n  allTasks: =>\n    [].concat(@_queue, @_completed)\n\n  ###\n  Public: Returns an existing task in the queue that matches the type you provide,\n  and any other match properties. Useful for checking to see if something, like\n  a \"SendDraft\" task is in-flight.\n\n  - `type`: The string name of the task class, or the Task class itself. (ie:\n    {SaveDraftTask} or 'SaveDraftTask')\n\n  - `matching`: Optional An {Object} with criteria to pass to _.isMatch. For a\n     SaveDraftTask, this could be {draftClientId: \"123123\"}\n\n  Returns a matching {Task}, or null.\n  ###\n  findTask: (type, matching = {}) ->\n    @findTasks(type, matching)[0]\n\n  findTasks: (type, matching = {}, {includeCompleted}={}) ->\n    type = type.name unless _.isString(type)\n    tasks = if includeCompleted then @_queue.concat(@_completed) else @_queue\n    matches = _.filter tasks, (task) ->\n      return false if task.constructor.name isnt type\n      isMatch = false\n      if _.isFunction(matching) then isMatch = matching(task)\n      else isMatch = _.isMatch(task, matching)\n      return isMatch\n    return matches ? []\n\n  enqueue: (task) =>\n    if not (task instanceof Task)\n      console.log(task)\n      throw new Error(\"You must queue a `Task` instance. Be sure you have the task registered with the TaskRegistry. If this is a task for a custom plugin, you must export a `taskConstructors` array with your `Task` constructors in it. You must all subclass the base Nylas `Task`.\")\n    if not (TaskRegistry.isInRegistry(task.constructor.name))\n      console.log(task)\n      throw new Error(\"You must queue a `Task` instance which is registred with the TaskRegistry\")\n    if not task.id\n      console.log(task)\n      throw new Error(\"Tasks must have an ID prior to being queued. Check that your Task constructor is calling `super`\")\n    if not task.queueState\n      console.log(task)\n      throw new Error(\"Tasks must have a queueState prior to being queued. Check that your Task constructor is calling `super`\")\n    task.sequentialId = ++@_currentSequentialId\n\n    @_dequeueObsoleteTasks(task)\n    runLocalStart = Date.now()\n    task.runLocal()\n    .then =>\n      runLocalTime = Date.now() - runLocalStart\n      @_reportRunLocalTime(task, runLocalTime)\n      @_queue.push(task)\n      @_updateSoon()\n\n  enqueueUndoOfTaskId: (taskId) =>\n    task = _.findWhere(@_queue, {id: taskId})\n    task ?= _.findWhere(@_completed, {id: taskId})\n    if task\n      @enqueue(task.createUndoTask())\n\n  dequeue: (taskOrId) =>\n    task = @_resolveTaskArgument(taskOrId)\n    if not task\n      throw new Error(\"Couldn't find task in queue to dequeue\")\n\n    if task.queueState.isProcessing\n      # We cannot remove a task from the queue while it's running and pretend\n      # things have stopped. Ask the task to cancel. It's promise will resolve\n      # or reject, and then we'll end up back here.\n      task.cancel()\n    else\n      @_queue.splice(@_queue.indexOf(task), 1)\n      @_completed.push(task)\n      @_completed.shift() if @_completed.length > 1000\n      @_updateSoon()\n\n  dequeueTaskAndDependents: (taskOrId) ->\n    task = @_resolveTaskArgument(taskOrId)\n    if not task\n      throw new Error(\"Couldn't find task in queue to dequeue\")\n\n  dequeueAll: =>\n    for task in @_queue by -1\n      @dequeue(task)\n\n  dequeueMatching: ({type, matching}) =>\n    task = @findTask(type, matching)\n\n    if not task\n      console.warn(\"Could not find matching task: #{type}\", matching)\n      return\n\n    @dequeue(task)\n\n  clearCompleted: =>\n    @_completed = []\n    @trigger()\n\n  # Helper Methods\n\n  _processQueue: =>\n    started = 0\n\n    if @_processQueueTimeout\n      clearTimeout(@_processQueueTimeout)\n      @_processQueueTimeout = null\n\n    now = Date.now()\n    reprocessIn = Number.MAX_VALUE\n\n    for task in @_queue by -1\n      if @_taskIsBlocked(task)\n        task.queueState.debugStatus = Task.DebugStatus.WaitingOnDependency\n        continue\n\n      if task.queueState.retryAfter and task.queueState.retryAfter > now\n        reprocessIn = Math.min(task.queueState.retryAfter - now, reprocessIn)\n        task.queueState.debugStatus = Task.DebugStatus.WaitingToRetry\n        continue\n\n      @_processTask(task)\n      started += 1\n\n    if started > 0\n      @trigger()\n\n    if reprocessIn isnt Number.MAX_VALUE\n      @_processQueueTimeout = setTimeout(@_processQueue, reprocessIn + 500)\n\n  _reportRunLocalTime: (task, runLocalTime) =>\n    taskJSON = JSON.parse(JSON.stringify(task.toJSON()))\n    taskData = _.mapObject(taskJSON, (val, key) =>\n      if key is 'folder'\n        return val.display_name\n      if key in ['labelsToAdd', 'labelsToRemove']\n        return val.map((l) => l.display_name)\n      return val\n    )\n    taskData = _.omit(taskData, (val, key) =>\n      if key in ['thread', 'message', 'draft', 'messages', 'threads', 'queueState']\n        return true\n      return key.startsWith('_')\n    )\n    eventData = Object.assign({}, taskData, {\n      action: 'perform-local-task'\n      actionTimeMs: runLocalTime,\n      taskName: task.constructor.name,\n      maxValue: 1000,\n      sample: 0.1,\n    })\n    Actions.recordPerfMetric(eventData)\n\n  _processTask: (task) =>\n    return if task.queueState.isProcessing\n\n    task.queueState.isProcessing = true\n    task.runRemote()\n    .finally =>\n      task.queueState.isProcessing = false\n      @trigger()\n    .then (status) =>\n      if status is Task.Status.Retry\n        task.queueState.retryDelay = Math.round(Math.min((task.queueState.retryDelay ? 1000) * 2, 30000))\n        task.queueState.retryAfter = Date.now() + task.queueState.retryDelay\n      else\n        @dequeue(task)\n      @_updateSoon()\n\n    .catch (err) =>\n      @_seenDownstream = {}\n      @_notifyOfDependentError(task, err)\n      .then (responses) =>\n        @_dequeueDownstreamTasks(responses)\n        @dequeue(task)\n\n  # When we `_notifyOfDependentError`s, we collect a nested array of\n  # responses of the tasks we notified. We need to responses to determine\n  # whether or not we should dequeue that task.\n  _dequeueDownstreamTasks: (responses=[]) ->\n    # Responses are nested arrays due to the recursion\n    responses = _.flatten(responses)\n\n    # A response may be `null` if it hit our infinite recursion check.\n    responses = _.filter responses, (r) -> r?\n\n    responses.forEach (resp) =>\n      resp.downstreamTask.queueState.status = Task.Status.Continue\n      resp.downstreamTask.queueState.debugStatus = Task.DebugStatus.DequeuedDependency\n      @dequeue(resp.downstreamTask)\n\n  # Recursively notifies tasks of dependent errors\n  _notifyOfDependentError: (failedTask, err) ->\n    downstream = @_tasksToDequeueOnFailure(failedTask) ? []\n    Promise.map downstream, (downstreamTask) =>\n\n      return Promise.resolve(null) unless downstreamTask\n\n      # Infinte recursion check!\n      # These will get removed later\n      return Promise.resolve(null) if @_seenDownstream[downstreamTask.id]\n      @_seenDownstream[downstreamTask.id] = true\n\n      responseHash = Promise.props\n        returnValue: downstreamTask.onDependentTaskError(failedTask, err)\n        downstreamTask: downstreamTask\n\n      return Promise.all([\n        responseHash\n        @_notifyOfDependentError(downstreamTask, err)\n      ])\n\n  _dequeueObsoleteTasks: (task) =>\n    obsolete = _.filter @_queue, (otherTask) =>\n      # Do not interrupt tasks which are currently processing\n      return false if otherTask.queueState.isProcessing\n      # Do not remove ourselves from the queue\n      return false if otherTask is task\n      # Dequeue tasks which our new task indicates it makes obsolete\n      return task.shouldDequeueOtherTask(otherTask)\n\n    for otherTask in obsolete\n      otherTask.queueState.status = Task.Status.Continue\n      otherTask.queueState.debugStatus = Task.DebugStatus.DequeuedObsolete\n      @dequeue(otherTask)\n\n  _tasksToDequeueOnFailure: (failedTask) ->\n    _.filter @_queue, (otherTask) ->\n      failedTask isnt otherTask and\n      otherTask.isDependentOnTask(failedTask) and\n      otherTask.shouldBeDequeuedOnDependencyFailure()\n\n  _taskIsBlocked: (task) =>\n    _.any @_queue, (otherTask) ->\n      task isnt otherTask and task.isDependentOnTask(otherTask)\n\n  _resolveTaskArgument: (taskOrId) =>\n    if not taskOrId\n      return null\n    else if taskOrId instanceof Task\n      return _.find @_queue, (task) -> task is taskOrId\n    else\n      return _.findWhere(@_queue, id: taskOrId)\n\n  _restoreQueue: =>\n    DatabaseStore.findJSONBlob(JSONBlobStorageKey).then (queue = []) =>\n      # We need to set the processing bit back to false so it gets\n      # re-retried upon inflation\n      for task in queue\n        task.queueState ?= {}\n        task.queueState.isProcessing = false\n        delete task.queueState['retryAfter']\n        delete task.queueState['retryDelay']\n\n      # The Task queue is completely wrecked if an item in the queue is not a\n      # task instance. This can happen if we removed or renamed the Task class,\n      # or if it was not registred with the TaskRegistry properly.\n      queue = queue.filter (task) => task instanceof Task\n\n      @_queue = queue\n      @_updateSoon()\n\n  _saveQueue: =>\n    return DatabaseStore.inTransaction((t) =>\n      return t.persistJSONBlob(JSONBlobStorageKey, @_queue ? [])\n    )\n\n  _updateSoon: =>\n    @_updateSoonThrottled ?= _.throttle =>\n      @_saveQueue()\n      _.defer =>\n        @_processQueue()\n        @_ensurePeriodicUpdates()\n    , 10\n\n    @_updateSoonThrottled()\n\n  _ensurePeriodicUpdates: =>\n    anyIsProcessing = _.any @_queue, (task) -> task.queueState.isProcessing\n\n    # The task queue triggers periodically as tasks are processed, even if no\n    # major events have occurred. This allows tasks which have state, like\n    # SendDraftTask.progress to be propogated through the app and inspected.\n    if anyIsProcessing and not @_updatePeriodicallyTimeout\n      @_updatePeriodicallyTimeout = setInterval =>\n        @_updateSoon()\n      , 1000\n    else if not anyIsProcessing and @_updatePeriodicallyTimeout\n      clearTimeout(@_updatePeriodicallyTimeout)\n      @_updatePeriodicallyTimeout = null\n\nmodule.exports = new TaskQueue()\nmodule.exports.JSONBlobStorageKey = JSONBlobStorageKey\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/thread-counts-store.coffee",
    "content": "_ = require 'underscore'\nNylasStore = require 'nylas-store'\nDatabaseStore = require('./database-store').default\nThread = require('../models/thread').default\n\n###\nAre running two nested SELECT statements really the best option? Yup.\nFor a performance assessment of these queries and other options, see:\nhttps://gist.github.com/bengotow/c8b5cd8989c9149ded56\n\nNote: SUM(unread) works because unread is represented as an int: 0 or 1.\n###\n\nReadCountsQuery = ->\n  \"SELECT * FROM `ThreadCounts`\"\n\nSetCountsQuery = ->\n  \"\"\"\n  REPLACE INTO `ThreadCounts` (category_id, unread, total)\n  SELECT\n    `ThreadCategory`.`value` as category_id,\n    SUM(`ThreadCategory`.`unread`) as unread,\n    COUNT(*) as total\n  FROM `ThreadCategory`\n  WHERE\n    `ThreadCategory`.in_all_mail = 1\n  GROUP BY `ThreadCategory`.`value`;\n  \"\"\"\n\nUpdateCountsQuery = (objectIds, operator) ->\n  objectIdsString = \"'\" + objectIds.join(\"','\") +  \"'\"\n  \"\"\"\n  REPLACE INTO `ThreadCounts` (category_id, unread, total)\n  SELECT\n    `ThreadCategory`.`value` as category_id,\n    COALESCE((SELECT unread FROM `ThreadCounts` WHERE category_id = `ThreadCategory`.`value`), 0) #{operator} SUM(`ThreadCategory`.`unread`) as unread,\n    COALESCE((SELECT total  FROM `ThreadCounts` WHERE category_id = `ThreadCategory`.`value`), 0) #{operator} COUNT(*) as total\n  FROM `ThreadCategory`\n  WHERE\n    `ThreadCategory`.id IN (#{objectIdsString}) AND\n    `ThreadCategory`.in_all_mail = 1\n  GROUP BY `ThreadCategory`.`value`\n  \"\"\"\n\nclass CategoryDatabaseMutationObserver\n  beforeDatabaseChange: (query, {type, objects, objectIds, objectClass}) =>\n    if objectClass is Thread.name\n      query(UpdateCountsQuery(objectIds, '-'))\n    else\n      Promise.resolve()\n\n  afterDatabaseChange: (query, {type, objects, objectIds, objectClass}, beforeResolveValue) =>\n    if objectClass is Thread.name\n      query(UpdateCountsQuery(objectIds, '+'))\n    else\n      Promise.resolve()\n\nclass ThreadCountsStore extends NylasStore\n  CategoryDatabaseMutationObserver: CategoryDatabaseMutationObserver\n\n  constructor: ->\n    @_counts = {}\n    @_observer = new CategoryDatabaseMutationObserver()\n    DatabaseStore.addMutationHook(@_observer)\n\n    if NylasEnv.isMainWindow()\n      # For now, unread counts are only retrieved in the main window.\n      @_onCountsChangedDebounced = _.throttle(@_onCountsChanged, 1000)\n      DatabaseStore.listen (change) =>\n        if change.objectClass is Thread.name\n          @_onCountsChangedDebounced()\n      @_onCountsChangedDebounced()\n\n    if NylasEnv.isWorkWindow() and not NylasEnv.config.get('nylas.threadCountsValid')\n      @reset()\n\n  reset: =>\n    countsStartTime = null\n    DatabaseStore.inTransaction (t) =>\n      countsStartTime = Date.now()\n      DatabaseStore._query(SetCountsQuery())\n    .then =>\n      NylasEnv.config.set('nylas.threadCountsValid', true)\n      console.log(\"Recomputed all thread counts in #{Date.now() - countsStartTime}ms\")\n\n  _onCountsChanged: =>\n    DatabaseStore._query(ReadCountsQuery()).then (results) =>\n      nextCounts = {}\n\n      foundNegative = false\n      for {category_id, unread, total} in results\n        nextCounts[category_id] = {unread, total}\n        if unread < 0 or total < 0\n          foundNegative = true\n\n      if foundNegative\n        NylasEnv.reportError(new Error('Assertion Failure: Negative Count'))\n        @reset()\n        return\n\n      if _.isEqual(nextCounts, @_counts)\n        return\n\n      @_counts = nextCounts\n      @trigger()\n\n  unreadCountForCategoryId: (catId) =>\n    return null if @_counts[catId] is undefined\n    @_counts[catId]['unread']\n\n  totalCountForCategoryId: (catId) =>\n    return null if @_counts[catId] is undefined\n    @_counts[catId]['total']\n\nmodule.exports = new ThreadCountsStore\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/thread-list-actions-store.es6",
    "content": "import NylasStore from 'nylas-store'\nimport Actions from '../actions'\nimport Utils from '../models/utils'\nimport TaskFactory from '../tasks/task-factory'\nimport AccountStore from '../stores/account-store'\nimport FocusedPerspectiveStore from '../stores/focused-perspective-store'\n\n\nclass ThreadListActionsStore extends NylasStore {\n\n  constructor() {\n    super()\n    this._timers = new Map()\n  }\n\n  activate() {\n    if (!NylasEnv.isMainWindow()) { return }\n    this.listenTo(Actions.archiveThreads, this._onArchiveThreads)\n    this.listenTo(Actions.trashThreads, this._onTrashThreads)\n    this.listenTo(Actions.markAsSpamThreads, this._onMarkAsSpamThreads)\n    this.listenTo(Actions.toggleStarredThreads, this._onToggleStarredThreads)\n    this.listenTo(Actions.toggleUnreadThreads, this._onToggleUnreadThreads)\n    this.listenTo(Actions.setUnreadThreads, this._onSetUnreadThreads)\n    this.listenTo(Actions.removeThreadsFromView, this._onRemoveThreadsFromView)\n    this.listenTo(Actions.moveThreadsToPerspective, this._onMoveThreadsToPerspective)\n    this.listenTo(Actions.removeCategoryFromThreads, this._onRemoveCategoryFromThreads)\n    this.listenTo(Actions.applyCategoryToThreads, this._onApplyCategoryToThreads)\n    this.listenTo(Actions.threadListDidUpdate, this._onThreadListDidUpdate)\n  }\n\n  deactivate() {\n    this.stopListeningToAll()\n  }\n\n  _onThreadListDidUpdate = (threads) => {\n    const updatedAt = Date.now()\n    const threadIdsInList = new Set(threads.map(t => t.id))\n\n    for (const [timerId, timerData] of this._timers.entries()) {\n      const {threadIds, provider, source, action, targetCategory} = timerData\n      const threadsHaveBeenRemoved = threadIds.every(id => !threadIdsInList.has(id))\n      if (threadsHaveBeenRemoved) {\n        const actionTimeMs = NylasEnv.timer.stop(timerId, updatedAt)\n        Actions.recordPerfMetric({\n          action,\n          source,\n          provider,\n          actionTimeMs,\n          targetCategory,\n          threadCount: threadIds.length,\n          sample: 0.9,\n        })\n        this._timers.delete(timerId)\n      }\n    }\n  }\n\n  _setNewTimer({threads, threadIds, accountIds, source, action, targetCategory = 'unknown'} = {}) {\n    if (!threads && !threadIds) {\n      return\n    }\n    if (threadIds && !accountIds) {\n      throw new Error('ThreadListActionStore._setNewTimer: Must pass accountIds along with threadIds')\n    }\n    const tIds = threadIds || threads.map(t => t.id);\n    const timerId = Utils.generateTempId()\n    let accounts\n    if (!threads) {\n      accounts = accountIds\n        .map(id => AccountStore.accountForId(id))\n        .filter(Boolean)\n    } else {\n      accounts = AccountStore.accountsForItems(threads)\n    }\n    const firstProvider = accounts[0].provider\n    const haveSameProvider = accounts\n      .reduce((provider, acct) => (acct.provider === provider ? provider : false), firstProvider)\n    const provider = haveSameProvider ? firstProvider : 'mixed'\n    const timerData = {\n      source,\n      action,\n      provider,\n      targetCategory,\n      threadIds: tIds,\n    }\n    this._timers.set(timerId, timerData)\n    NylasEnv.timer.start(timerId)\n  }\n\n  _onArchiveThreads = ({threads, source} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    this._setNewTimer({threads, source, action: 'remove-threads-from-list', targetCategory: 'archive'})\n    const tasks = TaskFactory.tasksForArchiving({threads, source})\n    Actions.queueTasks(tasks)\n  }\n\n  _onTrashThreads = ({threads, source} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    this._setNewTimer({threads, source, action: 'remove-threads-from-list', targetCategory: 'trash'})\n    const tasks = TaskFactory.tasksForMovingToTrash({threads, source})\n    Actions.queueTasks(tasks)\n  }\n\n  _onMarkAsSpamThreads = ({threads, source} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    this._setNewTimer({threads, source, action: 'remove-threads-from-list', targetCategory: 'spam'})\n    const tasks = TaskFactory.tasksForMarkingAsSpam({threads, source})\n    Actions.queueTasks(tasks)\n  }\n\n  _onToggleStarredThreads = ({threads, source} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    const task = TaskFactory.taskForInvertingStarred({threads, source})\n    Actions.queueTask(task)\n  }\n\n  _onToggleUnreadThreads = ({threads, canBeUndone, source} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    const task = TaskFactory.taskForInvertingUnread({threads, source, canBeUndone})\n    Actions.queueTask(task)\n  }\n\n  _onSetUnreadThreads = ({threads, unread, canBeUndone, source} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    const task = TaskFactory.taskForSettingUnread({threads, unread, source, canBeUndone})\n    Actions.queueTask(task)\n  }\n\n  _onRemoveThreadsFromView = ({threads, ruleset, source} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    const currentPerspective = FocusedPerspectiveStore.current()\n    const tasks = currentPerspective.tasksForRemovingItems(threads, ruleset, source)\n\n    // This action can encompass many different actions, e.g.:\n    // - unstarring in starred view\n    // - changing unread in unread view\n    // - Moving to inbox from trash\n    // - archiving a search result (which won't actually remove it from the thread-list)\n    // For now, we are only interested in timing actions that remove threads\n    // from the inbox\n    if (currentPerspective.isInbox()) {\n      // TODO figure out the `targetCategory`\n      this._setNewTimer({threads, source, action: 'remove-threads-from-list'})\n    }\n    Actions.queueTasks(tasks)\n  }\n\n  _onMoveThreadsToPerspective = ({targetPerspective, threadIds, accountIds} = {}) => {\n    if (!threadIds) { return }\n    if (threadIds.length === 0) { return }\n    const currentPerspective = FocusedPerspectiveStore.current()\n\n    // For now, we are only interested in timing actions that remove threads\n    // from the inbox\n    const targetCategories = targetPerspective.categories()\n    const targetCategoryIsFolder = (\n      targetCategories && targetCategories.length > 0 &&\n      targetCategories.every(c => c.object === 'folder')\n    )\n    const isRemovingFromInbox = currentPerspective.isInbox() && targetCategoryIsFolder\n    if (isRemovingFromInbox) {\n      const targetCategory = targetPerspective.isArchive() ? 'archive' : targetPerspective.categoriesSharedName();\n      this._setNewTimer({\n        threadIds,\n        accountIds,\n        targetCategory,\n        source: \"Dragged to Sidebar\",\n        action: 'remove-threads-from-list',\n      })\n    }\n    targetPerspective.receiveThreads(threadIds)\n  }\n\n  _onApplyCategoryToThreads = ({threads, source, categoryToApply} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    const task = TaskFactory.taskForApplyingCategory({\n      threads,\n      source,\n      category: categoryToApply,\n    })\n    Actions.queueTask(task)\n  }\n\n  _onRemoveCategoryFromThreads = ({threads, source, categoryToRemove} = {}) => {\n    if (!threads) { return }\n    if (threads.length === 0) { return }\n    // For now, we are only interested in timing actions that remove threads\n    // from the inbox\n    if (categoryToRemove.isInbox()) {\n      this._setNewTimer({\n        source,\n        threads,\n        targetCategory: 'archive',\n        action: 'remove-threads-from-list',\n      })\n    }\n    const task = TaskFactory.taskForRemovingCategory({\n      threads,\n      source,\n      category: categoryToRemove,\n    })\n    Actions.queueTask(task)\n  }\n}\n\nexport default new ThreadListActionsStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/undo-redo-store.es6",
    "content": "import _ from 'underscore';\nimport NylasStore from 'nylas-store';\n\nimport Actions from '../actions';\n\nclass UndoRedoStore extends NylasStore {\n\n  constructor() {\n    super()\n    this._undo = [];\n    this._redo = [];\n\n    this._mostRecentTasks = [];\n\n    this.listenTo(Actions.queueTask, this._onQueue);\n    this.listenTo(Actions.queueTasks, this._onQueue);\n\n    NylasEnv.commands.add(document.body, {'core:undo': this.undo });\n    NylasEnv.commands.add(document.body, {'core:redo': this.redo });\n  }\n\n  _onQueue = (taskArg) => {\n    if (!taskArg) { return; }\n    let tasks = taskArg;\n    if (!(tasks instanceof Array)) { tasks = [tasks]; }\n    if (tasks.length <= 0) { return; }\n    const undoable = _.every(tasks, t => t.canBeUndone());\n    const isRedoTask = _.every(tasks, t => t.isRedoTask);\n\n    if (undoable) {\n      if (!isRedoTask) { this._redo = []; }\n      this._undo.push(tasks);\n      this._mostRecentTasks = tasks;\n      this.trigger();\n    }\n  }\n\n  undo = () => {\n    const topTasks = this._undo.pop();\n    if (!topTasks) { return; }\n\n    this._mostRecentTasks = [];\n    this.trigger();\n\n    for (const task of topTasks) {\n      Actions.undoTaskId(task.id);\n    }\n\n    const redoTasks = topTasks.map((t) => {\n      const redoTask = t.createIdenticalTask();\n      redoTask.isRedoTask = true;\n      return redoTask;\n    });\n    this._redo.push(redoTasks);\n  }\n\n  redo = () => {\n    const redoTasks = this._redo.pop();\n    if (!redoTasks) { return; }\n    Actions.queueTasks(redoTasks);\n  }\n\n  getMostRecent = () => {\n    return this._mostRecentTasks;\n  }\n\n  print() {\n    console.log(\"Undo Stack\");\n    console.log(this._undo);\n    console.log(\"Redo Stack\");\n    console.log(this._redo);\n  }\n}\n\nexport default new UndoRedoStore();\n"
  },
  {
    "path": "packages/client-app/src/flux/stores/workspace-store.coffee",
    "content": "_ = require 'underscore'\nActions = require('../actions').default\nAccountStore = require('./account-store').default\nCategoryStore = require './category-store'\nMailboxPerspective = require '../../mailbox-perspective'\nNylasStore = require 'nylas-store'\n\nSheet = {}\nLocation = {}\n\n###\nPublic: The WorkspaceStore manages Sheets and layout modes in the application.\nObserving the WorkspaceStore makes it easy to monitor the sheet stack. To learn\nmore about sheets and layout in N1, see the {InterfaceConcepts.md}\ndocumentation.\n\nSection: Stores\n###\nclass WorkspaceStore extends NylasStore\n  constructor: ->\n    @_resetInstanceVars()\n    @_preferredLayoutMode = NylasEnv.config.get('core.workspace.mode')\n\n    @listenTo Actions.selectRootSheet, @_onSelectRootSheet\n    @listenTo Actions.setFocus, @_onSetFocus\n    @listenTo Actions.toggleWorkspaceLocationHidden, @_onToggleLocationHidden\n    @listenTo Actions.popSheet, @popSheet\n    @listenTo Actions.popToRootSheet, @popToRootSheet\n    @listenTo Actions.pushSheet, @pushSheet\n\n    {windowType} = NylasEnv.getLoadSettings()\n    unless windowType is 'onboarding'\n      require('electron').webFrame.setZoomLevelLimits(1, 1)\n      NylasEnv.config.observe 'core.workspace.interfaceZoom', (z) =>\n        require('electron').webFrame.setZoomFactor(z) if z and _.isNumber(z)\n\n    if NylasEnv.isMainWindow()\n      @_rebuildMenu()\n      NylasEnv.commands.add(document.body, {\n        'core:pop-sheet': => @popSheet()\n        'application:select-list-mode' : => @_onSelectLayoutMode(\"list\")\n        'application:select-split-mode' : => @_onSelectLayoutMode(\"split\")\n      })\n\n\n  _rebuildMenu: =>\n    @_menuDisposable?.dispose()\n    @_menuDisposable = NylasEnv.menu.add([\n      {\n        \"label\": \"View\",\n        \"submenu\": [\n          {\n            \"label\": \"Reading Pane Off\",\n            \"type\": \"radio\",\n            \"command\": \"application:select-list-mode\",\n            \"checked\": @_preferredLayoutMode is 'list',\n            \"position\": \"before=mailbox-navigation\"\n          },\n          {\n            \"label\": \"Reading Pane On\",\n            \"type\": \"radio\",\n            \"command\": \"application:select-split-mode\",\n            \"checked\": @_preferredLayoutMode is 'split'\n            \"position\": \"before=mailbox-navigation\"\n          }\n        ]\n      }\n    ])\n\n  _resetInstanceVars: =>\n    @Location = Location = {}\n    @Sheet = Sheet = {}\n\n    @_hiddenLocations = NylasEnv.config.get('core.workspace.hiddenLocations') || {}\n    @_sheetStack = []\n\n    if NylasEnv.isMainWindow()\n      @defineSheet 'Global'\n      @defineSheet 'Threads', {root: true},\n        list: ['RootSidebar', 'ThreadList']\n        split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']\n      @defineSheet 'Thread', {},\n        list: ['MessageList', 'MessageListSidebar']\n    else\n      @defineSheet 'Global'\n\n  ###\n  Inbound Events\n  ###\n\n  _onSelectRootSheet: (sheet) =>\n    if not sheet\n      throw new Error(\"Actions.selectRootSheet - #{sheet} is not a valid sheet.\")\n    if not sheet.root\n      throw new Error(\"Actions.selectRootSheet - #{sheet} is not registered as a root sheet.\")\n\n    @_sheetStack = []\n    @_sheetStack.push(sheet)\n    @trigger(@)\n\n  _onToggleLocationHidden: (location) =>\n    if not location.id\n      throw new Error(\"Actions.toggleWorkspaceLocationHidden - pass a WorkspaceStore.Location\")\n\n    if @_hiddenLocations[location.id]\n      if location is @Location.MessageListSidebar\n        Actions.recordUserEvent(\"Sidebar Opened\")\n      delete @_hiddenLocations[location.id]\n    else\n      if location is @Location.MessageListSidebar\n        Actions.recordUserEvent(\"Sidebar Closed\")\n      @_hiddenLocations[location.id] = location\n\n    NylasEnv.config.set('core.workspace.hiddenLocations', @_hiddenLocations)\n\n    @trigger(@)\n\n  _onSetFocus: ({collection, item}) =>\n    if collection is 'thread'\n      if @layoutMode() is 'list'\n        if item and @topSheet() isnt Sheet.Thread\n          @pushSheet(Sheet.Thread)\n        if not item and @topSheet() is Sheet.Thread\n          @popSheet()\n\n    if collection is 'file'\n      if @layoutMode() is 'list'\n        if item and @topSheet() isnt Sheet.File\n          @pushSheet(Sheet.File)\n        if not item and @topSheet() is Sheet.File\n          @popSheet()\n\n  _onSelectLayoutMode: (mode) =>\n    return if mode is @_preferredLayoutMode\n    @_preferredLayoutMode = mode\n    NylasEnv.config.set('core.workspace.mode', @_preferredLayoutMode)\n    @_rebuildMenu()\n    @popToRootSheet()\n    @trigger()\n\n  ###\n  Accessing Data\n  ###\n\n  # Returns a {String}: The current layout mode. Either `split` or `list`\n  #\n  layoutMode: =>\n    root = @rootSheet()\n    if not root\n      'list'\n    else if @_preferredLayoutMode in root.supportedModes\n      @_preferredLayoutMode\n    else\n      root.supportedModes[0]\n\n  preferredLayoutMode: =>\n    @_preferredLayoutMode\n\n  # Public: Returns The top {Sheet} in the current stack. Use this method to determine\n  # the sheet the user is looking at.\n  #\n  topSheet: =>\n    @_sheetStack[@_sheetStack.length - 1]\n\n  # Public: Returns The {Sheet} at the root of the current stack.\n  #\n  rootSheet: =>\n    @_sheetStack[0]\n\n  # Public: Returns an {Array<Sheet>} The stack of sheets\n  #\n  sheetStack: =>\n    @_sheetStack\n\n  # Public: Returns an {Array} of locations that have been hidden.\n  #\n  hiddenLocations: =>\n    _.values(@_hiddenLocations)\n\n  # Public: Returns a {Boolean} indicating whether the location provided is hidden.\n  # You should provide one of the WorkspaceStore.Location constant values.\n  isLocationHidden: (loc) =>\n    return false unless loc\n    @_hiddenLocations[loc.id]?\n\n\n  ###\n  Managing Sheets\n  ###\n\n  # * `id` {String} The ID of the Sheet being defined.\n  # * `options` {Object} If the sheet should be listed in the left sidebar,\n  #      pass `{root: true, name: 'Label'}`.\n  # *`columns` An {Object} with keys for each layout mode the Sheet\n  #      supports. For each key, provide an array of column names.\n  #\n  defineSheet: (id, options = {}, columns = {}) =>\n    # Make sure all the locations have definitions so that packages\n    # can register things into these locations and their toolbars.\n    for layout, cols of columns\n      for col, idx in cols\n        Location[col] ?= {id: \"#{col}\", Toolbar: {id: \"#{col}:Toolbar\"}}\n        cols[idx] = Location[col]\n\n    Sheet[id] =\n      id: id\n      columns: columns\n      supportedModes: Object.keys(columns)\n\n      icon: options.icon\n      name: options.name\n      root: options.root\n      sidebarComponent: options.sidebarComponent\n\n      Toolbar:\n        Left: {id: \"Sheet:#{id}:Toolbar:Left\"}\n        Right: {id: \"Sheet:#{id}:Toolbar:Right\"}\n      Header: {id: \"Sheet:#{id}:Header\"}\n      Footer: {id: \"Sheet:#{id}:Footer\"}\n\n    if (options.root and not @rootSheet()) and not options.silent\n      @_onSelectRootSheet(Sheet[id])\n\n    @triggerDebounced()\n\n  undefineSheet: (id) =>\n    delete Sheet[id]\n    @triggerDebounced()\n\n  # Push the sheet on top of the current sheet, with a quick animation.\n  # A back button will appear in the top left of the pushed sheet.\n  # This method triggers, allowing observers to update.\n  #\n  # * `sheet` The {Sheet} type to push onto the stack.\n  #\n  pushSheet: (sheet) =>\n    @_sheetStack.push(sheet)\n    @trigger()\n\n  # Remove the top sheet, with a quick animation. This method triggers,\n  # allowing observers to update.\n  popSheet: =>\n    sheet = @topSheet()\n\n    if @_sheetStack.length > 1\n      @_sheetStack.pop()\n      @trigger()\n\n    if Sheet.Thread and sheet is Sheet.Thread\n      Actions.setFocus(collection: 'thread', item: null)\n\n  # Return to the root sheet. This method triggers, allowing observers\n  # to update.\n  popToRootSheet: =>\n    if @_sheetStack.length > 1\n      @_sheetStack.length = 1\n      @trigger()\n\n  triggerDebounced: _.debounce(( -> @trigger(@)), 1)\n\nmodule.exports = new WorkspaceStore()\n"
  },
  {
    "path": "packages/client-app/src/flux/syncback-task-api-request.es6",
    "content": "import Actions from './actions'\nimport {APIError} from './errors'\nimport DatabaseStore from './stores/database-store'\nimport NylasAPIRequest from './nylas-api-request'\nimport * as NylasAPIHelpers from './nylas-api-helpers'\nimport ProviderSyncbackRequest from './models/provider-syncback-request'\n\n/**\n * This API request is meant to be used for requests that create a\n * SyncbackRequest inside K2. When the initial http request succeeds,\n * this means that the task was created, but we cant tell if the task\n * actually succeeded or failed until some time in the future when its\n * processed inside K2's sync loop.\n *\n * A SyncbackTaskAPIRequest will only resolve until the underlying K2\n * syncback request has actually succeeded, or reject when it fails, by\n * listening to deltas for ProviderSyncbackRequests\n */\nclass SyncbackTaskAPIRequest {\n\n  static listenForRequest(syncbackRequestId) {\n    return new Promise((resolve, reject) => {\n      const unsubscribe = Actions.didReceiveSyncbackRequestDeltas\n      .listen((syncbackRequests) => {\n        const failed = syncbackRequests.find(r => r.id === syncbackRequestId && r.status === 'FAILED')\n        const succeeded = syncbackRequests.find(r => r.id === syncbackRequestId && r.status === 'SUCCEEDED')\n        if (failed) {\n          unsubscribe()\n          // TODO fix/standardize this error format with K2\n          const error = new APIError({\n            error: failed.error,\n            body: {\n              message: failed.error.message,\n              data: failed.error.data,\n            },\n            statusCode: failed.error.statusCode || 500,\n          })\n          reject(error)\n        } else if (succeeded) {\n          unsubscribe()\n          resolve(succeeded.responseJSON || {})\n        }\n      });\n    })\n  }\n\n  static waitForQueuedRequest(syncbackRequestId) {\n    return new Promise(async (resolve, reject) => {\n      const syncbackRequest = await DatabaseStore.find(ProviderSyncbackRequest, syncbackRequestId);\n\n      if (syncbackRequest) {\n        if (syncbackRequest.status === \"SUCCEEDED\") {\n          return resolve(syncbackRequest.responseJSON)\n        } else if (syncbackRequest.status === \"FAILED\") {\n          return reject(syncbackRequest.error)\n        } // else continue so we listen for it on the delta\n      }\n\n      return SyncbackTaskAPIRequest.listenForRequest(syncbackRequestId)\n      .then(resolve).catch(reject)\n    })\n  }\n\n  constructor({api, options}) {\n    this._request = new NylasAPIRequest({api, options})\n    this._onSyncbackRequestCreated = options.onSyncbackRequestCreated || (() => {})\n  }\n\n  run() {\n    return new Promise(async (resolve, reject) => {\n      try {\n        const syncbackRequest = await this._request.run();\n        await NylasAPIHelpers.handleModelResponse(syncbackRequest)\n        await this._onSyncbackRequestCreated(syncbackRequest)\n        const syncbackRequestId = syncbackRequest.id\n        SyncbackTaskAPIRequest.listenForRequest(syncbackRequestId)\n        .then(resolve).catch(reject)\n      } catch (err) {\n        if (err.response && err.response.statusCode === 404) {\n          NylasAPIHelpers.handleModel404(this._request.options.url)\n        }\n        reject(err)\n      }\n    })\n  }\n}\n\nexport default SyncbackTaskAPIRequest\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/base-draft-task.es6",
    "content": "import Task from './task';\nimport DraftHelpers from '../stores/draft-helpers';\n\nexport default class BaseDraftTask extends Task {\n\n  static DraftNotFoundError = DraftHelpers.DraftNotFoundError;\n\n  constructor(draftClientId) {\n    super();\n    this.draftClientId = draftClientId;\n    this.draft = null;\n  }\n\n  shouldDequeueOtherTask(other) {\n    const isSameDraft = (other.draftClientId === this.draftClientId);\n    const isOlderTask = (other.sequentialId < this.sequentialId);\n    const isExactClass = (other.constructor.name === this.constructor.name);\n    return (isSameDraft && isOlderTask && isExactClass);\n  }\n\n  isDependentOnTask(other) {\n    // Set this task to be dependent on any and SendDraftTasks for the\n    // same draft that were created first.  This, in conjunction with this\n    // method on SendDraftTask, ensures that a send and a syncback never\n    // run at the same time for a draft.\n\n    // Require here rather than on top to avoid a circular dependency\n    const isSameDraft = (other.draftClientId === this.draftClientId);\n    const isOlderTask = (other.sequentialId < this.sequentialId);\n    const isSaveOrSend = (other instanceof BaseDraftTask);\n    return (isSameDraft && isOlderTask && isSaveOrSend);\n  }\n\n  performLocal() {\n    if (!this.draftClientId) {\n      const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a draftClientId`;\n      return Promise.reject(new Error(errMsg));\n    }\n    return Promise.resolve();\n  }\n\n  async refreshDraftReference() {\n    this.draft = await DraftHelpers.refreshDraftReference(this.draftClientId)\n    return this.draft;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/change-folder-task.es6",
    "content": "import _ from 'underscore';\nimport Thread from '../models/thread';\nimport Category from '../models/category';\nimport Message from '../models/message';\nimport Actions from '../actions'\nimport DatabaseStore from '../stores/database-store';\nimport ChangeMailTask from './change-mail-task';\nimport SyncbackCategoryTask from './syncback-category-task';\n\n// Public: Create a new task to apply labels to a message or thread.\n//\n// Takes an options object of the form:\n//   - folder: The {Folder} or {Folder} IDs to move to\n//   - threads: An array of {Thread}s or {Thread} IDs\n//   - threads: An array of {Message}s or {Message} IDs\n//   - undoData: Since changing the folder is a destructive action,\n//   undo tasks need to store the configuration of what folders messages\n//   were in. When creating an undo task, we fill this parameter with\n//   that configuration\n//\nexport default class ChangeFolderTask extends ChangeMailTask {\n\n  constructor(options = {}) {\n    super(options);\n    this.source = options.source\n    this.taskDescription = options.taskDescription;\n    this.folder = options.folder;\n  }\n\n  label() {\n    if (this.folder) {\n      return `Moving to ${this.folder.displayName}`;\n    }\n    return \"Moving to folder\";\n  }\n\n  categoriesToAdd() {\n    return [this.folder];\n  }\n\n  description() {\n    if (this.taskDescription) {\n      return this.taskDescription;\n    }\n\n    let folderText = \" to folder\";\n    if (this.folder instanceof Category) {\n      folderText = ` to ${this.folder.displayName}`;\n    }\n\n    if (this.threads.length > 1) {\n      return `Moved ${this.threads.length} threads${folderText}`;\n    } else if (this.messages.length > 1) {\n      return `Moved ${this.messages.length} messages${folderText}`;\n    }\n    return `Moved${folderText}`;\n  }\n\n  isDependentOnTask(other) {\n    return super.isDependentOnTask(other) || (other instanceof SyncbackCategoryTask);\n  }\n\n  performLocal() {\n    if (!this.folder) {\n      return Promise.reject(new Error(\"Must specify a `folder`\"))\n    }\n    if (this.threads.length > 0 && this.messages.length > 0) {\n      return Promise.reject(new Error(\"ChangeFolderTask: You can move `threads` or `messages` but not both\"))\n    }\n    if (this.threads.length === 0 && this.messages.length === 0) {\n      return Promise.reject(new Error(\"ChangeFolderTask: You must provide a `threads` or `messages` Array of models or IDs.\"))\n    }\n\n    return super.performLocal();\n  }\n\n  _isArchive() {\n    return this.folder.name === \"archive\" || this.folder.name === \"all\"\n  }\n\n  recordUserEvent() {\n    if (this.source === \"Mail Rules\") {\n      return\n    }\n    Actions.recordUserEvent(\"Threads Moved to Folder\", {\n      source: this.source,\n      isArchive: this._isArchive(),\n      folderType: this.folder.name || \"custom\",\n      folderDisplayName: this.folder.displayName,\n      numThreads: this.threads.length,\n      numMessages: this.messages.length,\n      description: this.description(),\n      isUndo: this._isUndoTask,\n    })\n  }\n\n  retrieveModels() {\n    return Promise.props({\n      folder: DatabaseStore.modelify(Category, [this.folder]),\n      threads: DatabaseStore.modelify(Thread, this.threads),\n      messages: DatabaseStore.modelify(Message, this.messages),\n\n    }).then(({folder, threads, messages}) => {\n      // Remove any objects we weren't able to find. This can happen pretty easily\n      // if (you undo an action && other things have happened.)\n      this.folder = folder[0];\n      this.threads = _.compact(threads);\n      this.messages = _.compact(messages);\n\n      if (!this.folder) {\n        return Promise.reject(new Error(\"The specified folder could not be found.\"));\n      }\n      return Promise.resolve();\n    });\n  }\n\n  processNestedMessages() {\n    return false;\n  }\n\n  changesToModel(model) {\n    if (model instanceof Thread) {\n      return {categories: [this.folder]}\n    }\n    if (model instanceof Message) {\n      return {categories: [this.folder]}\n    }\n    return null;\n  }\n\n  requestBodyForModel(model) {\n    if (model instanceof Thread) {\n      return {folder: model.folders[0] ? model.folders[0].id : null};\n    }\n    if (model instanceof Message) {\n      return {folder: model.folder ? model.folder.id : null};\n    }\n    return null;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/change-labels-task.es6",
    "content": "import _ from 'underscore';\nimport Thread from '../models/thread';\nimport Message from '../models/message';\nimport Category from '../models/category';\nimport DatabaseStore from '../stores/database-store';\nimport CategoryStore from '../stores/category-store';\nimport AccountStore from '../stores/account-store';\nimport ChangeMailTask from './change-mail-task';\nimport SyncbackCategoryTask from './syncback-category-task';\n\n// Public: Create a new task to apply labels to a message or thread.\n//\n// Takes an options object of the form:\n// - labelsToAdd: An {Array} of {Category}s or {Category} ids to add\n// - labelsToRemove: An {Array} of {Category}s or {Category} ids to remove\n// - threads: An {Array} of {Thread}s or {Thread} ids\n// - messages: An {Array} of {Message}s or {Message} ids\nexport default class ChangeLabelsTask extends ChangeMailTask {\n\n  constructor(options = {}) {\n    super(options);\n    this.source = options.source\n    this.labelsToAdd = options.labelsToAdd || [];\n    this.labelsToRemove = options.labelsToRemove || [];\n    this.taskDescription = options.taskDescription;\n  }\n\n  label() {\n    return \"Applying labels\";\n  }\n\n  categoriesToAdd() {\n    return this.labelsToAdd;\n  }\n\n  categoriesToRemove() {\n    return this.labelsToRemove;\n  }\n\n  description() {\n    if (this.taskDescription) {\n      return this.taskDescription;\n    }\n\n    let countString = \"\";\n    if (this.threads.length > 1) {\n      countString = ` ${this.threads.length} threads`;\n    }\n\n    const removed = this.labelsToRemove[0];\n    const added = this.labelsToAdd[0];\n    const objectsAvailable = (added || removed) instanceof Category;\n\n    // Note: In the future, we could move this logic to the task\n    // factory and pass the string in as this.taskDescription (ala Snooze), but\n    // it's nice to have them declaratively based on the actual labels.\n    if (objectsAvailable) {\n      const looksLikeMove = (this.labelsToAdd.length === 1 && this.labelsToRemove.length > 0);\n\n      // Spam / trash interactions are always \"moves\" because they're the three\n      // folders of Gmail. If another folder is involved, we need to decide to\n      // return either \"Moved to Bla\" or \"Added Bla\".\n      if (added && added.name === 'spam') {\n        return `Marked${countString} as Spam`;\n      } else if (removed && removed.name === 'spam') {\n        return `Unmarked${countString} as Spam`;\n      } else if (added && added.name === 'trash') {\n        return `Trashed${countString}`;\n      } else if (removed && removed.name === 'trash') {\n        return `Removed${countString} from Trash`;\n      }\n      if (looksLikeMove) {\n        if (added.name === 'all') {\n          return `Archived${countString}`;\n        } else if (removed.name === 'all') {\n          return `Unarchived${countString}`;\n        }\n        return `Moved${countString} to ${added.displayName}`;\n      }\n      if (this.labelsToAdd.length === 1 && this.labelsToRemove.length === 0) {\n        return `Added ${added.displayName}${countString ? ' to' : ''}${countString}`;\n      }\n      if (this.labelsToAdd.length === 0 && this.labelsToRemove.length === 1) {\n        return `Removed ${removed.displayName}${countString ? ' from' : ''}${countString}`;\n      }\n    }\n    return `Changed labels${countString ? ' on' : ''}${countString}`;\n  }\n\n  isDependentOnTask(other) {\n    return super.isDependentOnTask(other) || (other instanceof SyncbackCategoryTask);\n  }\n\n  // In Gmail all threads /must/ belong to either All Mail, Trash and Spam, and\n  // they are mutually exclusive, so we need to make sure that any add/remove\n  // label operation still guarantees that constraint\n  _ensureAndUpdateLabels(account, existingLabelsToAdd, existingLabelsToRemove = {}) {\n    const labelsToAdd = existingLabelsToAdd;\n    let labelsToRemove = existingLabelsToRemove;\n\n    const setToAdd = new Set(_.compact(_.pluck(labelsToAdd, 'name')));\n    const setToRemove = new Set(_.compact(_.pluck(labelsToRemove, 'name')));\n\n    if (setToRemove.has('all')) {\n      if (!setToAdd.has('spam') && !setToAdd.has('trash')) {\n        labelsToRemove = _.reject(labelsToRemove, label => label.name === 'all');\n      }\n    } else if (setToAdd.has('all')) {\n      if (!setToRemove.has('trash')) {\n        labelsToRemove.push(CategoryStore.getTrashCategory(account));\n      }\n      if (!setToRemove.has('spam')) {\n        labelsToRemove.push(CategoryStore.getSpamCategory(account));\n      }\n    }\n\n    if (setToRemove.has('trash')) {\n      if (!setToAdd.has('spam') && !setToAdd.has('all')) {\n        labelsToAdd.push(CategoryStore.getAllMailCategory(account));\n      }\n    } else if (setToAdd.has('trash')) {\n      if (!setToRemove.has('all')) {\n        labelsToRemove.push(CategoryStore.getAllMailCategory(account))\n      }\n      if (!setToRemove.has('spam')) {\n        labelsToRemove.push(CategoryStore.getSpamCategory(account))\n      }\n    }\n\n    if (setToRemove.has('spam')) {\n      if (!setToAdd.has('trash') && !setToAdd.has('all')) {\n        labelsToAdd.push(CategoryStore.getAllMailCategory(account));\n      }\n    } else if (setToAdd.has('spam')) {\n      if (!setToRemove.has('all')) {\n        labelsToRemove.push(CategoryStore.getAllMailCategory(account))\n      }\n      if (!setToRemove.has('trash')) {\n        labelsToRemove.push(CategoryStore.getTrashCategory(account))\n      }\n    }\n\n    // This should technically not be possible, but we like to keep it safe\n    return {\n      labelsToAdd: _.compact(labelsToAdd),\n      labelsToRemove: _.compact(labelsToRemove),\n    };\n  }\n\n  performLocal() {\n    if (this.messages.length > 0) {\n      return Promise.reject(new Error(\"ChangeLabelsTask: N1 does not support viewing or changing labels on individual messages.\"))\n    }\n    if (this.labelsToAdd.length === 0 && this.labelsToRemove.length === 0) {\n      return Promise.reject(new Error(\"ChangeLabelsTask: Must specify `labelsToAdd` or `labelsToRemove`\"))\n    }\n    if (this.threads.length > 0 && this.messages.length > 0) {\n      return Promise.reject(new Error(\"ChangeLabelsTask: You can move `threads` or `messages` but not both\"))\n    }\n    if (this.threads.length === 0 && this.messages.length === 0) {\n      return Promise.reject(new Error(\"ChangeLabelsTask: You must provide a `threads` or `messages` Array of models or IDs.\"))\n    }\n\n    return super.performLocal();\n  }\n\n  _isArchive() {\n    const toAdd = this.labelsToAdd.map(l => l.name)\n    return toAdd.includes(\"all\") || toAdd.includes(\"archive\")\n  }\n\n  recordUserEvent() {\n    if (this.source === \"Mail Rules\") {\n      return\n    }\n    // Actions.recordUserEvent(\"Threads Changed Labels\", {\n    //   source: this.source,\n    //   isArchive: this._isArchive(),\n    //   labelTypesToAdd: this.labelsToAdd.map(l => l.name || \"custom\"),\n    //   labelTypesToRemove: this.labelsToRemove.map(l => l.name || \"custom\"),\n    //   labelDisplayNamesToAdd: this.labelsToAdd.map(l => l.displayName),\n    //   labelDisplayNamesToRemove: this.labelsToRemove.map(l => l.displayName),\n    //   numThreads: this.threads.length,\n    //   numMessages: this.messages.length,\n    //   description: this.description(),\n    //   isUndo: this._isUndoTask,\n    // })\n  }\n\n  retrieveModels() {\n    // Convert arrays of IDs or models to models.\n    // modelify returns immediately if (no work is required)\n    return Promise.props({\n      labelsToAdd: DatabaseStore.modelify(Category, this.labelsToAdd),\n      labelsToRemove: DatabaseStore.modelify(Category, this.labelsToRemove),\n      threads: DatabaseStore.modelify(Thread, this.threads),\n      messages: DatabaseStore.modelify(Message, this.messages),\n\n    }).then(({labelsToAdd, labelsToRemove, threads, messages}) => {\n      if (_.any([].concat(labelsToAdd, labelsToRemove), _.isUndefined)) {\n        return Promise.reject(new Error(\"One or more of the specified labels could not be found.\"))\n      }\n      const account = AccountStore.accountForItems(threads);\n      if (!account) {\n        return Promise.reject(new Error(\"ChangeLabelsTask: You must provide a set of `threads` from the same Account\"))\n      }\n      // In Gmail all threads /must/ belong to either All Mail, Trash and Spam, and\n      // they are mutually exclusive, so we need to make sure that any add/remove\n      // label operation still guarantees that constraint\n      const updated = this._ensureAndUpdateLabels(account, labelsToAdd, labelsToRemove)\n\n      // Remove any objects we weren't able to find. This can happen pretty easily\n      // if (you undo an action && other things have happened.)\n      this.labelsToAdd = updated.labelsToAdd;\n      this.labelsToRemove = updated.labelsToRemove;\n      this.threads = _.compact(threads);\n      this.messages = _.compact(messages);\n\n      // The base class does the heavy lifting and calls changesToModel\n      return Promise.resolve();\n    });\n  }\n\n  processNestedMessages() {\n    return false;\n  }\n\n  changesToModel(model) {\n    const labelsToRemoveIds = _.pluck(this.labelsToRemove, 'id')\n\n    let labels = _.reject(model.labels, ({id}) => labelsToRemoveIds.includes(id));\n    labels = labels.concat(this.labelsToAdd);\n    labels = _.uniq(labels, false, label => label.id);\n    return {labels};\n  }\n\n  requestBodyForModel(model) {\n    const folder = model.labels.find(l => l.object === 'folder')\n    const labels = model.labels.filter(l => l.object === 'label')\n\n    if (folder) {\n      return {\n        folder: folder.id,\n        labels: labels.map(l => l.id),\n      }\n    }\n    return {labels};\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/change-mail-task.es6",
    "content": "import _ from 'underscore';\nimport Task from './task';\nimport Thread from '../models/thread';\nimport Message from '../models/message';\nimport NylasAPI from '../nylas-api';\nimport SyncbackTaskAPIRequest from '../syncback-task-api-request';\nimport DatabaseStore from '../stores/database-store';\nimport {APIError} from '../errors';\nimport EnsureMessageInSentFolderTask from './ensure-message-in-sent-folder-task'\nimport BaseDraftTask from './base-draft-task'\n\n/*\nPublic: The ChangeMailTask is a base class for all tasks that modify sets\nof threads or messages.\n\nSubclasses implement {ChangeMailTask::changesToModel} and\n{ChangeMailTask::requestBodyForModel} to define the specific transforms\nthey provide, and override {ChangeMailTask::performLocal} to perform\nadditional consistency checks.\n\nChangeMailTask aims to be fast and efficient. It does not write changes to\nthe database or make API requests for models that are unmodified by\n{ChangeMailTask::changesToModel}\n\nChangeMailTask stores the previous values of all models it changes into\nthis._restoreValues and handles undo/redo. When undoing, it restores previous\nvalues and calls {ChangeMailTask::requestBodyForModel} to make undo API\nrequests. It does not call {ChangeMailTask::changesToModel}.\n*/\nexport default class ChangeMailTask extends Task {\n\n  constructor({threads, thread, messages, message} = {}) {\n    super();\n\n    this.threads = threads || [];\n    if (thread) {\n      this.threads.push(thread);\n    }\n    this.messages = messages || [];\n    if (message) {\n      this.messages.push(message);\n    }\n  }\n\n  // Functions for subclasses\n\n  // Public: Override this method and return an object with key-value pairs\n  // representing changed values. For example, if (your task sets unread:)\n  // false, return {unread: false}.\n  //\n  // - `model` an individual {Thread} or {Message}\n  //\n  // Returns an object whos key-value pairs represent the desired changed\n  // object.\n  changesToModel() {\n    throw new Error(\"You must override this method.\");\n  }\n\n  // Public: Override this method and return an object that will be the\n  // request body used for saving changes to `model`.\n  //\n  // - `model` an individual {Thread} or {Message}\n  //\n  // Returns an object that will be passed as the `body` to the actual API\n  // `request` object\n  requestBodyForModel() {\n    throw new Error(\"You must override this method.\");\n  }\n\n  // Public: Override to indicate whether actions need to be taken for all\n  // messages of each thread.\n  //\n  // Generally, you cannot provide both messages and threads at the same\n  // time. However, ChangeMailTask runs for provided threads first and then\n  // messages. Override and return true, and you will receive\n  // `changesToModel` for messages in changed threads, and any changes you\n  // make will be written to the database and undone during undo.\n  //\n  // Note that API requests are only made for threads if (threads are)\n  // present.\n  processNestedMessages() {\n    return false;\n  }\n\n  // Public: Returns categories that this task will add to the set of threads\n  // Must be overriden\n  categoriesToAdd() {\n    return [];\n  }\n\n  // Public: Returns categories that this task will remove the set of threads\n  // Must be overriden\n  categoriesToRemove() {\n    return [];\n  }\n\n  // Public: Subclasses should override `performLocal` and call super once\n  // they've prepared the data they need and verified that requirements are\n  // met.\n\n  // See {Task::performLocal} for more usage info\n\n  performLocal() {\n    if (this._isUndoTask && !this._restoreValues) {\n      return Promise.reject(new Error(\"ChangeMailTask: No _restoreValues provided for undo task.\"))\n    }\n    // Lock the models with the optimistic change tracker so they aren't reverted\n    // while the user is seeing our optimistic changes.\n    if (!this._isReverting) {\n      this._lockAll();\n    }\n\n    return DatabaseStore.inTransaction((t) => {\n      return this.retrieveModels().then(() => {\n        return this._performLocalThreads(t)\n      }).then(() => {\n        return this._performLocalMessages(t)\n      })\n    }).then(() => {\n      try {\n        this.recordUserEvent()\n      } catch (err) {\n        NylasEnv.reportError(err);\n        // don't throw\n      }\n    });\n  }\n\n  recordUserEvent() {\n    throw new Error(\"Override recordUserEvent\")\n  }\n\n  retrieveModels() {\n    // Note: Currently, *ALL* subclasses must use `DatabaseStore.modelify`\n    // to convert `threads` and `messages` from models or ids to models.\n    return Promise.resolve();\n  }\n\n  _performLocalThreads(transaction) {\n    const changed = this._applyChanges(this.threads);\n    const changedIds = _.pluck(changed, 'id');\n\n    if (changed.length === 0) {\n      return Promise.resolve();\n    }\n\n    return transaction.persistModels(changed).then(() => {\n      if (!this.processNestedMessages()) {\n        return Promise.resolve();\n      }\n      return DatabaseStore.findAll(Message).where(Message.attributes.threadId.in(changedIds)).then((messages) => {\n        this.messages = [].concat(messages, this.messages);\n        return Promise.resolve()\n      })\n    });\n  }\n\n  _performLocalMessages(transaction) {\n    const changed = this._applyChanges(this.messages);\n    return (changed.length > 0) ? transaction.persistModels(changed) : Promise.resolve();\n  }\n\n  _applyChanges(modelArray) {\n    const changed = [];\n\n    if (this._shouldChangeBackwards()) {\n      modelArray.forEach((model, idx) => {\n        if (this._restoreValues[model.id]) {\n          const updated = _.extend(model.clone(), this._restoreValues[model.id]);\n          modelArray[idx] = updated;\n          changed.push(updated);\n        }\n      });\n    } else {\n      this._restoreValues = this._restoreValues || {};\n      modelArray.forEach((model, idx) => {\n        const fieldsNew = this.changesToModel(model);\n        const fieldsCurrent = _.pick(model, Object.keys(fieldsNew));\n        if (!_.isEqual(fieldsCurrent, fieldsNew)) {\n          this._restoreValues[model.id] = fieldsCurrent;\n          const updated = _.extend(model.clone(), fieldsNew);\n          modelArray[idx] = updated;\n          changed.push(updated);\n        }\n      });\n    }\n\n    return changed;\n  }\n\n  _shouldChangeBackwards() {\n    return this._isReverting || this._isUndoTask;\n  }\n\n  performRemote() {\n    return this._performRequests(this.objectClass(), this.objectArray())\n    .then(() => {\n      this._ensureLocksRemoved();\n      return Promise.resolve(Task.Status.Success);\n    })\n    .catch((err) => {\n      if (err instanceof APIError && !NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {\n        return Promise.resolve(Task.Status.Retry);\n      }\n      this._isReverting = true;\n      return this.performLocal()\n      .then(() => {\n        this._ensureLocksRemoved();\n        NylasEnv.showErrorDialog({\n          title: \"Error\",\n          message: `We were unable to apply the changes to your thread${this.threads.length > 1 ? 's' : ''}, please try again later.\\n\\nError message: ${err.message}`,\n        })\n        return Promise.resolve([Task.Status.Failed, err]);\n      });\n    });\n  }\n\n  _performRequests(klass, models) {\n    const alreadyQueued = Object.assign({}, this._syncbackRequestIds || {})\n    return Promise.map(models, (model) => {\n      if (alreadyQueued[model.id]) {\n        return SyncbackTaskAPIRequest.waitForQueuedRequest(alreadyQueued[model.id])\n      }\n\n      const endpoint = (klass === Thread) ? 'threads' : 'messages';\n\n      return new SyncbackTaskAPIRequest({\n        api: NylasAPI,\n        options: {\n          path: `/${endpoint}/${model.id}`,\n          accountId: model.accountId,\n          method: 'PUT',\n          body: this.requestBodyForModel(model),\n          onSyncbackRequestCreated: (syncbackRequest) => {\n            if (!this._syncbackRequestIds) this._syncbackRequestIds = {}\n            this._syncbackRequestIds[model.id] = syncbackRequest.id\n          },\n        },\n      })\n      .run()\n      .catch((err) => {\n        if (err instanceof APIError && err.statusCode === 404) {\n          return Promise.resolve();\n        }\n        return Promise.reject(err);\n      })\n    })\n  }\n\n  // Task lifecycle\n\n  canBeUndone() {\n    return true;\n  }\n\n  isUndo() {\n    return this._isUndoTask === true;\n  }\n\n  createUndoTask() {\n    if (this._isUndoTask) {\n      throw new Error(\"ChangeMailTask::createUndoTask Cannot create an undo task from an undo task.\");\n    }\n    if (!this._restoreValues) {\n      throw new Error(\"ChangeMailTask::createUndoTask Cannot undo a task which has not finished performLocal yet.\");\n    }\n\n    const task = this.createIdenticalTask();\n    task._restoreValues = this._restoreValues;\n    task._isUndoTask = true;\n    return task;\n  }\n\n  createIdenticalTask() {\n    const task = new this.constructor(this);\n\n    // Never give the undo task the Model objects - make it look them up!\n    // This ensures that they never revert other fields\n    const toIds = (arr) => _.map(arr, v => (_.isString(v) ? v : v.id));\n    task.threads = toIds(this.threads);\n    task.messages = (this.threads.length > 0) ? [] : toIds(this.messages);\n    return task;\n  }\n\n  objectIds() {\n    return [].concat(this.threads, this.messages).map((v) =>\n      (_.isString(v) ? v : v.id)\n    );\n  }\n\n  objectClass() {\n    return (this.threads && this.threads.length) ? Thread : Message;\n  }\n\n  objectArray() {\n    return (this.threads && this.threads.length) ? this.threads : this.messages;\n  }\n\n  numberOfImpactedItems() {\n    return this.objectArray().length;\n  }\n\n  // To ensure that complex offline actions are synced correctly, label/folder additions\n  // and removals need to be applied in order. (For example, star many threads,\n  // and then unstar one.)\n  isDependentOnTask(other) {\n    // objectIds() in practice never contains draftClientIds, so in order to\n    // avoid making this function async w/a db lookup, we always depend on any\n    // send-related tasks no matter if they are in the same thread\n    if (other instanceof BaseDraftTask) {\n      return true;\n    }\n    // Wait on EnsureMessageInSentFolderTask if it involves a message that\n    // belongs to a thread we are trying to operate on\n    if (other instanceof EnsureMessageInSentFolderTask && other.message) {\n      const objectIds = this.objectIds()\n      if (objectIds.includes(other.message.threadId)) {\n        return true;\n      }\n    }\n    // Only wait on other tasks that are older and also involve the same threads\n    if (!(other instanceof ChangeMailTask)) {\n      return false;\n    }\n    const otherOlder = other.sequentialId < this.sequentialId;\n    const otherSameObjs = _.intersection(other.objectIds(), this.objectIds()).length > 0;\n    return otherOlder && otherSameObjs;\n  }\n\n  // Helpers used in subclasses\n\n  _lockAll() {\n    const klass = this.objectClass();\n    this._locked = this._locked || {};\n    for (const item of this.objectArray()) {\n      this._locked[item.id] = this._locked[item.id] || 0;\n      this._locked[item.id] += 1;\n      NylasAPI.incrementRemoteChangeLock(klass, item.id);\n    }\n  }\n\n  _removeLock(item) {\n    const klass = this.objectClass();\n    NylasAPI.decrementRemoteChangeLock(klass, item.id);\n    this._locked[item.id] -= 1;\n  }\n\n  _ensureLocksRemoved() {\n    const klass = this.objectClass()\n    if (!this._locked) {\n      return;\n    }\n\n    for (const id of Object.keys(this._locked)) {\n      let count = this._locked[id];\n      while (count > 0) {\n        NylasAPI.decrementRemoteChangeLock(klass, id);\n        count -= 1;\n      }\n    }\n    this._locked = null;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/change-starred-task.es6",
    "content": "/* eslint no-unused-vars: 0*/\nimport _ from 'underscore';\nimport Thread from '../models/thread';\nimport Actions from '../actions'\nimport DatabaseStore from '../stores/database-store';\nimport ChangeMailTask from './change-mail-task';\n\nexport default class ChangeStarredTask extends ChangeMailTask {\n  constructor(options = {}) {\n    super(options);\n    this.source = options.source;\n    this.starred = options.starred;\n  }\n\n  label() {\n    return this.starred ? \"Starring\" : \"Unstarring\";\n  }\n\n  description() {\n    const count = this.threads.length;\n    const type = count > 1 ? \"threads\" : \"thread\";\n\n    if (this._isUndoTask) {\n      return `Undoing changes to ${count} ${type}`\n    }\n\n    const verb = this.starred ? \"Starred\" : \"Unstarred\";\n    if (count > 1) {\n      return `${verb} ${count} ${type}`;\n    }\n    return `${verb}`;\n  }\n\n  performLocal() {\n    if (this.threads.length === 0) {\n      return Promise.reject(new Error(\"ChangeStarredTask: You must provide a `threads` Array of models or IDs.\"));\n    }\n    return super.performLocal();\n  }\n\n  recordUserEvent() {\n    if (this.source === \"Mail Rules\") {\n      return\n    }\n    const eventName = this.unread ? \"Starred\" : \"Unstarred\";\n    Actions.recordUserEvent(`Threads ${eventName}`, {\n      source: this.source,\n      numThreads: this.threads.length,\n      description: this.description(),\n      isUndo: this._isUndoTask,\n    })\n  }\n\n  retrieveModels() {\n    return Promise.props({\n      threads: DatabaseStore.modelify(Thread, this.threads),\n    }).then(({threads}) => {\n      this.threads = _.compact(threads);\n      return Promise.resolve();\n    })\n  }\n\n  changesToModel(model) {\n    return {starred: this.starred};\n  }\n\n  requestBodyForModel(model) {\n    return {starred: model.starred};\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/change-unread-task.es6",
    "content": "/* eslint no-unused-vars: 0*/\nimport _ from 'underscore';\nimport Thread from '../models/thread';\nimport Actions from '../actions'\nimport DatabaseStore from '../stores/database-store';\nimport ChangeMailTask from './change-mail-task';\n\nexport default class ChangeUnreadTask extends ChangeMailTask {\n  constructor(options = {}) {\n    super(options);\n    this.source = options.source;\n    this.unread = options.unread;\n    this._canBeUndone = options.canBeUndone;\n  }\n\n  label() {\n    return this.unread ? \"Marking as unread\" : \"Marking as read\";\n  }\n\n  description() {\n    const count = this.threads.length;\n    const type = count > 1 ? 'threads' : 'thread';\n\n    if (this._isUndoTask) {\n      return `Undoing changes to ${count} ${type}`;\n    }\n\n    const newState = this.unread ? \"unread\" : \"read\";\n    if (count > 1) {\n      return `Marked ${count} ${type} as ${newState}`;\n    }\n    return `Marked as ${newState}`;\n  }\n\n  canBeUndone() {\n    if (this._canBeUndone == null) {\n      return super.canBeUndone()\n    }\n    return this._canBeUndone\n  }\n\n  performLocal() {\n    if (this.threads.length === 0) {\n      return Promise.reject(new Error(\"ChangeUnreadTask: You must provide a `threads` Array of models or IDs.\"))\n    }\n    return super.performLocal();\n  }\n\n  recordUserEvent() {\n    if (this.source === \"Mail Rules\") {\n      return\n    }\n    // const eventName = this.unread ? \"Unread\" : \"Read\";\n    // Actions.recordUserEvent(`Threads Marked as ${eventName}`, {\n    //   source: this.source,\n    //   numThreads: this.threads.length,\n    //   description: this.description(),\n    //   isUndo: this._isUndoTask,\n    // })\n  }\n\n  retrieveModels() {\n    // Convert arrays of IDs or models to models.\n    // modelify returns immediately if (no work is required)\n    return Promise.props({\n      threads: DatabaseStore.modelify(Thread, this.threads),\n    }).then(({threads}) => {\n      this.threads = _.compact(threads);\n      return Promise.resolve();\n    });\n  }\n\n  processNestedMessages() {\n    return true;\n  }\n\n  changesToModel(model) {\n    return {unread: this.unread};\n  }\n\n  requestBodyForModel(model) {\n    return {unread: model.unread};\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/destroy-category-task.es6",
    "content": "import DatabaseStore from '../stores/database-store';\nimport AccountStore from '../stores/account-store';\nimport Task from './task';\nimport Category from '../models/category';\nimport ChangeFolderTask from './change-folder-task';\nimport ChangeLabelTask from './change-labels-task';\nimport SyncbackCategoryTask from './syncback-category-task';\nimport NylasAPI from '../nylas-api';\nimport SyncbackTaskAPIRequest from '../syncback-task-api-request';\nimport {APIError} from '../errors';\n\nexport default class DestroyCategoryTask extends Task {\n\n  constructor({category} = {}) {\n    super();\n    this.category = category;\n  }\n\n  label() {\n    return `Deleting ${this.category.displayType()} ${this.category.displayName}`\n  }\n\n  isDependentOnTask(other) {\n    return (other instanceof ChangeFolderTask) ||\n           (other instanceof ChangeLabelTask) ||\n           (other instanceof SyncbackCategoryTask)\n  }\n\n  performLocal() {\n    if (!this.category) {\n      return Promise.reject(new Error(\"Attempt to call DestroyCategoryTask.performLocal without this.category.\"));\n    }\n\n    return DatabaseStore.inTransaction((t) =>\n      t.unpersistModel(this.category)\n    );\n  }\n\n  performRemote() {\n    if (!this.category) {\n      return Promise.reject(new Error(\"Attempt to call DestroyCategoryTask.performRemote without this.category.\"));\n    }\n    if (!this.category.serverId) {\n      return Promise.reject(new Error(\"Attempt to call DestroyCategoryTask.performRemote without this.category.serverId.\"));\n    }\n\n    const {serverId, accountId} = this.category;\n    const account = AccountStore.accountForId(accountId);\n    const path = account.usesLabels() ? `/labels/${serverId}` : `/folders/${serverId}`;\n\n    // We need to lock this model here to prevent it from beifly showing up\n    // on the modify delta and then correctly disappearing on the delete\n    // delta which comes after a delay\n    NylasAPI.incrementRemoteChangeLock(Category, this.category.serverId);\n\n    let runPromise = Promise.resolve();\n\n    if (this._syncbackRequestId) {\n      runPromise = SyncbackTaskAPIRequest.waitForQueuedRequest(this._syncbackRequestId)\n    } else {\n      runPromise = new SyncbackTaskAPIRequest({\n        api: NylasAPI,\n        options: {\n          accountId,\n          path,\n          method: 'DELETE',\n          onSyncbackRequestCreated: (syncbackRequest) => {\n            this._syncbackRequestId = syncbackRequest.id\n          },\n        },\n      }).run()\n    }\n\n    return runPromise.thenReturn(Task.Status.Success)\n    .catch(APIError, (err) => {\n      if (!NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {\n        return Promise.resolve(Task.Status.Retry);\n      }\n      NylasAPI.decrementRemoteChangeLock(Category, this.category.serverId);\n      return DatabaseStore.inTransaction((t) =>\n        t.persistModel(this.category)\n      ).then(() => {\n        NylasEnv.reportError(\n          new Error(`Deleting category responded with ${err.statusCode}!`)\n        );\n        this._notifyUserOfError(this.category, err);\n        return Promise.resolve(Task.Status.Failed);\n      });\n    })\n  }\n\n  _notifyUserOfError(category, err) {\n    const displayName = category.displayName;\n    const displayType = category.displayType();\n\n    let msg = `The ${displayType} ${displayName} could not be deleted.`;\n    if (displayType === 'folder') {\n      msg += \" Make sure the folder you want to delete is empty before deleting it.\";\n    }\n\n    NylasEnv.showErrorDialog(msg, {detail: JSON.stringify(err)});\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/destroy-draft-task.es6",
    "content": "import Task from './task';\nimport {APIError} from '../errors';\nimport Message from '../models/message';\nimport DatabaseStore from '../stores/database-store';\nimport NylasAPI from '../nylas-api';\nimport NylasAPIRequest from '../nylas-api-request';\nimport BaseDraftTask from './base-draft-task';\n\nexport default class DestroyDraftTask extends BaseDraftTask {\n\n  shouldDequeueOtherTask(other) {\n    return (other instanceof BaseDraftTask && other.draftClientId === this.draftClientId);\n  }\n\n  performLocal() {\n    super.performLocal();\n    return this.refreshDraftReference()\n    .then(() => DatabaseStore.inTransaction((t) => t.unpersistModel(this.draft)))\n    .catch(BaseDraftTask.DraftNotFoundError, () => Promise.resolve());\n  }\n\n  performRemote() {\n    // We don't need to do anything if we weren't able to find the draft\n    // when we performed locally, or if the draft has never been synced to\n    // the server (id is still self-assigned)\n    if (!this.draft) {\n      return Promise.resolve(Task.Status.Continue);\n    }\n    if (!this.draft.serverId) {\n      return Promise.resolve(Task.Status.Continue);\n    }\n    if (!this.draft.version) {\n      const err = new Error(\"Can't destroy draft without a version or serverId\");\n      return Promise.resolve([Task.Status.Failed, err]);\n    }\n\n    NylasAPI.incrementRemoteChangeLock(Message, this.draft.serverId);\n\n    return new NylasAPIRequest({\n      api: NylasAPI,\n      options: {\n        path: `/drafts/${this.draft.serverId}`,\n        accountId: this.draft.accountId,\n        method: \"DELETE\",\n        body: {\n          version: this.draft.version,\n        },\n      },\n    })\n    .run()\n    // We deliberately do not decrement the change count, ensuring no deltas\n    // about this object are received that could restore it.\n    .thenReturn(Task.Status.Success)\n    .catch(APIError, (err) => {\n      NylasAPI.decrementRemoteChangeLock(Message, this.draft.serverId);\n\n      const inboxMsg = (err.body && err.body.message) ? err.body.message : '';\n\n      // Draft has already been deleted, this is not really an error\n      if ([404, 409].includes(err.statusCode)) {\n        return Promise.resolve(Task.Status.Continue);\n      }\n      // Draft has been sent, and can't be deleted. Not much we can do but finish\n      if (inboxMsg.indexOf(\"is not a draft\") >= 0) {\n        return Promise.resolve(Task.Status.Continue);\n      }\n      if (!NylasAPI.PermanentErrorCodes.inclue(err.statusCode)) {\n        return Promise.resolve(Task.Status.Retry);\n      }\n\n      NylasEnv.showErrorDialog(\"Unable to delete this draft. Restoring...\");\n\n      return DatabaseStore.inTransaction((t) =>\n        t.persistModel(this.draft)\n      ).then(() =>\n        Promise.resolve(Task.Status.Failed)\n      )\n    })\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/destroy-model-task.es6",
    "content": "/* eslint global-require:0 */\nimport _ from 'underscore'\nimport Task from './task'\nimport NylasAPI from '../nylas-api'\nimport NylasAPIRequest from '../nylas-api-request'\nimport DatabaseStore from '../stores/database-store'\n\nexport default class DestroyModelTask extends Task {\n\n  constructor({clientId, modelName, endpoint, accountId} = {}) {\n    super()\n    this.clientId = clientId\n    this.endpoint = endpoint\n    this.modelName = modelName\n    this.accountId = accountId\n  }\n\n  shouldDequeueOtherTask(other) {\n    return (other instanceof DestroyModelTask &&\n            this.modelName === other.modelName &&\n            this.accountId === other.accountId &&\n            this.endpoint === other.endpoint &&\n            this.clientId === other.clientId)\n  }\n\n  getModelConstructor() {\n    return require('nylas-exports')[this.modelName]\n  }\n\n  performLocal() {\n    this.validateRequiredFields([\"clientId\", \"accountId\", \"endpoint\"])\n\n    const klass = this.getModelConstructor()\n    if (!_.isFunction(klass)) {\n      throw new Error(`Couldn't find the class for ${this.modelName}`)\n    }\n\n    return DatabaseStore.findBy(klass, {clientId: this.clientId}).then((model) => {\n      if (!model) {\n        throw new Error(`Couldn't find the model with clientId ${this.clientId}`)\n      }\n      this.serverId = model.serverId\n      this.oldModel = model.clone()\n      return DatabaseStore.inTransaction((t) => {\n        return t.unpersistModel(model)\n      });\n    })\n  }\n\n  performRemote() {\n    if (!this.serverId) {\n      return Promise.resolve(Task.Status.Continue)\n    }\n    return new NylasAPIRequest({\n      api: new NylasAPI(),\n      options: {\n        path: `${this.endpoint}/${this.serverId}`,\n        method: \"DELETE\",\n        accountId: this.accountId,\n      },\n    })\n    .run()\n    .then(() => {\n      return Promise.resolve(Task.Status.Success)\n    }).catch(this.apiErrorHandler)\n  }\n\n  canBeUndone() { return false }\n\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/ensure-message-in-sent-folder-task.es6",
    "content": "import Task from './task';\nimport {APIError} from '../errors';\nimport Actions from '../actions';\nimport NylasAPI from '../nylas-api';\nimport SyncbackTaskAPIRequest from '../syncback-task-api-request';\nimport SendDraftTask from './send-draft-task';\n\n\nexport default class EnsureMessageInSentFolderTask extends Task {\n  constructor(opts = {}) {\n    super(opts);\n    this.message = opts.message;\n    this.customSentMessage = opts.customSentMessage;\n  }\n\n  label() {\n    return \"Saving to sent folder\";\n  }\n\n  isDependentOnTask(other) {\n    return (other instanceof SendDraftTask) && (other.message) && (other.message.clientId === this.message.clientId);\n  }\n\n  performLocal() {\n    if (!this.message) {\n      const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a message`;\n      return Promise.reject(new Error(errMsg));\n    }\n    return Promise.resolve();\n  }\n\n  performRemote() {\n    let runPromise = Promise.resolve();\n    if (this._syncbackRequestId) {\n      runPromise = SyncbackTaskAPIRequest.waitForQueuedRequest(this._syncbackRequestId)\n    } else {\n      runPromise = new SyncbackTaskAPIRequest({\n        api: NylasAPI,\n        options: {\n          path: `/ensure-message-in-sent-folder/${this.message.id}`,\n          method: \"POST\",\n          body: {\n            customSentMessage: this.customSentMessage,\n          },\n          accountId: this.message.accountId,\n          onSyncbackRequestCreated: (syncbackRequest) => {\n            this._syncbackRequestId = syncbackRequest.id\n          },\n        },\n      }).run()\n    }\n\n    return runPromise.then(() => {\n      Actions.ensureMessageInSentSuccess({messageClientId: this.message.clientId})\n      return Task.Status.Success\n    })\n    .catch((err) => {\n      const errorMessage = `Your message successfully sent; however, we had trouble saving your message, \"${this.message.subject}\", to your Sent folder.\\n\\n${err.message}`;\n      if (err instanceof APIError) {\n        if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {\n          NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true, detail: err.stack});\n          return Promise.resolve([Task.Status.Failed, err]);\n        }\n        return Promise.resolve(Task.Status.Retry);\n      }\n      NylasEnv.reportError(err);\n      NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true, detail: err.stack});\n      return Promise.resolve([Task.Status.Failed, err]);\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/event-rsvp-task.es6",
    "content": "import Task from './task';\nimport Event from '../models/event';\nimport {APIError} from '../errors';\nimport Utils from '../models/utils';\nimport DatabaseStore from '../stores/database-store';\nimport NylasAPI from '../nylas-api';\nimport NylasAPIRequest from '../nylas-api-request';\n\n\nexport default class EventRSVPTask extends Task {\n  constructor(event, RSVPEmail, RSVPResponse) {\n    super();\n    this.event = event;\n    this.RSVPEmail = RSVPEmail;\n    this.RSVPResponse = RSVPResponse;\n  }\n\n  performLocal() {\n    return DatabaseStore.inTransaction((t) => {\n      return t.find(Event, this.event.id).then((updated) => {\n        this.event = updated || this.event;\n        this._previousParticipantsState = Utils.deepClone(this.event.participants);\n\n        for (const p of this.event.participants) {\n          if (p.email === this.RSVPEmail) {\n            p.status = this.RSVPResponse;\n          }\n        }\n\n        return t.persistModel(this.event);\n      })\n    });\n  }\n\n  performRemote() {\n    const {accountId, id} = this.event;\n\n    return new NylasAPIRequest({\n      api: NylasAPI,\n      options: {\n        accountId,\n        timeout: 1000 * 60 * 5, // We cannot hang up a send - won't know if it sent\n        path: \"/send-rsvp\",\n        method: \"POST\",\n        body: {\n          event_id: id,\n          status: this.RSVPResponse,\n        },\n      },\n    })\n    .run()\n    .thenReturn(Task.Status.Success)\n    .catch(APIError, (err) => {\n      this.event.participants = this._previousParticipantsState;\n      return DatabaseStore.inTransaction((t) =>\n        t.persistModel(this.event)\n      ).thenReturn(Task.Status.Failed, err);\n    });\n  }\n\n  onOtherError() {\n    return Promise.resolve();\n  }\n\n  onTimeoutError() {\n    return Promise.resolve();\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/perform-send-action-task.es6",
    "content": "import Task from './task';\nimport Actions from '../actions';\nimport BaseDraftTask from './base-draft-task';\nimport TaskQueue from '../stores/task-queue';\nimport SendActionsStore from '../stores/send-actions-store';\n\n\nclass PerformSendActionTask extends BaseDraftTask {\n\n  constructor(draftClientId, sendActionKey) {\n    super(draftClientId)\n    this._sendActionKey = sendActionKey\n    this._sendTimer = null\n    this._taskResolve = () => {}\n  }\n\n  label() {\n    return \"Sending message\";\n  }\n\n  shouldDequeueOtherTask(otherTask) {\n    return (\n      otherTask instanceof PerformSendActionTask &&\n      this.draftClientId === otherTask.draftClientId\n    )\n  }\n\n  performLocal() {\n    if (!this.draftClientId) {\n      const errMsg = `Attempt to call ${this.constructor.name}.performLocal without a draftClientId`;\n      return Promise.reject(new Error(errMsg));\n    }\n    return Promise.resolve()\n  }\n\n  performRemote() {\n    const sameTasks = TaskQueue.findTasks(PerformSendActionTask, {draftClientId: this.draftClientId})\n    if (sameTasks.length > 1) {\n      return Promise.resolve(Task.Status.Continue)\n    }\n    const undoSendTimeout = NylasEnv.config.get('core.sending.undoSend')\n    if (!undoSendTimeout) {\n      return this._performSendAction()\n      .then(() => Task.Status.Success)\n      .catch((err) => [Task.Status.Failed, err])\n    }\n\n    return new Promise((resolve, reject) => {\n      const {id: taskId, draftClientId} = this\n      this._taskResolve = resolve\n\n      Actions.willPerformSendAction({taskId, draftClientId})\n      this._sendTimer = setTimeout(() => {\n        this._performSendAction()\n        .then(() => resolve(Task.Status.Success))\n        .catch((err) => reject([Task.Status.Failed, err]))\n        .finally(() => Actions.didPerformSendAction({taskId, draftClientId}))\n      }, undoSendTimeout)\n    })\n  }\n\n  cancel() {\n    const {id: taskId, draftClientId} = this\n    clearTimeout(this._sendTimer)\n    Actions.didCancelSendAction({taskId, draftClientId})\n    this._taskResolve(Task.Status.Continue)\n  }\n\n  _performSendAction() {\n    return this.refreshDraftReference()\n    .then((draft) => {\n      const sendAction = SendActionsStore.sendActionForKey(this._sendActionKey)\n      if (!sendAction) {\n        return Promise.reject(new Error(`Cant find send action ${this._sendActionKey} `))\n      }\n      const {performSendAction} = sendAction\n      return performSendAction({draft})\n    })\n  }\n}\n\nexport default PerformSendActionTask\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/reprocess-mail-rules-task.es6",
    "content": "import _ from 'underscore';\nimport async from 'async';\n\nimport Task from './task';\nimport Thread from '../models/thread';\nimport Message from '../models/message';\nimport DatabaseStore from '../stores/database-store';\nimport CategoryStore from '../stores/category-store';\nimport MailRulesProcessor from '../../mail-rules-processor';\n\nexport default class ReprocessMailRulesTask extends Task {\n  constructor(accountId) {\n    super();\n    this.accountId = accountId;\n    this._processed = this._processed || 0;\n    this._offset = this._offset || 0;\n    this._lastTimestamp = this._lastTimestamp || null;\n    this._finished = false;\n  }\n\n  label() {\n    return \"Applying Mail Rules\";\n  }\n\n  numberOfImpactedItems() {\n    return this._offset;\n  }\n\n  cancel() {\n    this._finished = true;\n  }\n\n  performRemote() {\n    return Promise.fromCallback(this._processAllMessages).thenReturn(Task.Status.Success);\n  }\n\n  _processAllMessages = (callback) => {\n    async.until(() => this._finished, this._processSomeMessages, callback);\n  }\n\n  _processSomeMessages = (callback) => {\n    const inboxCategory = CategoryStore.getStandardCategory(this.accountId, 'inbox');\n    if (!inboxCategory) {\n      return callback(new Error(\"ReprocessMailRulesTask: No inbox category found.\"));\n    }\n\n    // Fetching threads first, and then getting their messages allows us to use\n    // The same indexes as the thread list / message list in the app\n\n    // Note that we look for \"50 after X\" rather than \"offset 150\", because\n    // running mail rules can move things out of the inbox!\n    const query = DatabaseStore\n      .findAll(Thread, {accountId: this.accountId})\n      .where(Thread.attributes.categories.contains(inboxCategory.id))\n      .order(Thread.attributes.lastMessageReceivedTimestamp.descending())\n      .limit(50)\n\n    if (this._lastTimestamp !== null) {\n      query.where(Thread.attributes.lastMessageReceivedTimestamp.lessThan(this._lastTimestamp))\n    }\n\n    return query.then((threads) => {\n      if (threads.length === 0) {\n        this._finished = true;\n      }\n\n      if (this._finished) {\n        return Promise.resolve(null);\n      }\n\n      return DatabaseStore.findAll(Message, {threadId: _.pluck(threads, 'id')}).then((messages) => {\n        if (this._finished) {\n          return Promise.resolve(null);\n        }\n\n        return MailRulesProcessor.processMessages(messages).finally(() => {\n          this._processed += messages.length;\n          this._offset += threads.length;\n          this._lastTimestamp = threads.pop().lastMessageReceivedTimestamp;\n        });\n      });\n    })\n    .delay(500)\n    .asCallback(callback)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/send-draft-task.es6",
    "content": "/* eslint global-require: 0 */\nimport Task from './task';\nimport Actions from '../actions';\nimport Message from '../models/message';\nimport Account from '../models/account';\nimport * as NylasAPIHelpers from '../nylas-api-helpers';\nimport {APIError, RequestEnsureOnceError} from '../errors';\nimport SoundRegistry from '../../registries/sound-registry';\nimport DatabaseStore from '../stores/database-store';\nimport AccountStore from '../stores/account-store';\nimport BaseDraftTask from './base-draft-task';\nimport SyncbackMetadataTask from './syncback-metadata-task';\nimport EnsureMessageInSentFolderTask from './ensure-message-in-sent-folder-task';\n\nconst OPEN_TRACKING_ID = NylasEnv.packages.pluginIdFor('open-tracking')\nconst LINK_TRACKING_ID = NylasEnv.packages.pluginIdFor('link-tracking')\n\nexport default class SendDraftTask extends BaseDraftTask {\n\n  constructor(draftClientId, {playSound = true, emitError = true, allowMultiSend = true, performRemoteAlreadyCalled = false} = {}) {\n    super(draftClientId);\n    this.draft = null;\n    this.message = null;\n    this.emitError = emitError\n    this.playSound = playSound\n    this.allowMultiSend = allowMultiSend\n    this.performRemoteAlreadyCalled = performRemoteAlreadyCalled\n  }\n\n  label() {\n    return \"Sending message\";\n  }\n\n  performLocal() {\n    return super.performLocal()\n    .then(() => {\n      this._timerKey = `send-draft-${this.draftClientId}`\n      NylasEnv.timer.start(this._timerKey)\n    })\n  }\n\n  performRemote() {\n    if (this.performRemoteAlreadyCalled) {\n      const error = new Error('App was closed while sending was in progress.')\n      return this.onError(error)\n    }\n    this.performRemoteAlreadyCalled = true\n    return this.refreshDraftReference()\n    .then(this.assertDraftValidity)\n    .then(this.sendMessage)\n    .then(this.ensureInSentFolder)\n    .then(this.updatePluginMetadata)\n    .then(this.onSuccess)\n    .catch(this.onError);\n  }\n\n  assertDraftValidity = () => {\n    if (!this.draft.from[0]) {\n      return Promise.reject(new Error(\"SendDraftTask - you must populate `from` before sending.\"));\n    }\n\n    const account = AccountStore.accountForEmail(this.draft.from[0].email);\n    if (!account) {\n      return Promise.reject(new Error(\"SendDraftTask - you can only send drafts from a configured account.\"));\n    }\n    if (this.draft.accountId !== account.id) {\n      return Promise.reject(new Error(\"The from address has changed since you started sending this draft. Double-check the draft and click 'Send' again.\"));\n    }\n    return Promise.resolve();\n  }\n\n  _trackingPluginsInUse() {\n    const pluginsAvailable = (OPEN_TRACKING_ID && LINK_TRACKING_ID);\n    if (!pluginsAvailable) {\n      return false;\n    }\n    return (!!this.draft.metadataForPluginId(OPEN_TRACKING_ID) || !!this.draft.metadataForPluginId(LINK_TRACKING_ID)) || false;\n  }\n\n  hasCustomBodyPerRecipient = () => {\n    if (!this.allowMultiSend) {\n      return false;\n    }\n\n    // Sending individual bodies for too many participants can cause us\n    // to hit the smtp rate limit.\n    const participants = this.draft.participants({includeFrom: false, includeBcc: true})\n    if (participants.length === 1 || participants.length > 10) {\n      return false;\n    }\n\n    const providerCompatible = (AccountStore.accountForId(this.draft.accountId).provider !== \"eas\");\n    return this._trackingPluginsInUse() && providerCompatible;\n  }\n\n  sendMessage = async () => {\n    if (this.hasCustomBodyPerRecipient()) {\n      await this._sendPerRecipient();\n    } else {\n      await this._sendWithSingleBody()\n    }\n  }\n\n  ensureInSentFolder = () => {\n    const t = new EnsureMessageInSentFolderTask({\n      message: this.message,\n      customSentMessage: this.hasCustomBodyPerRecipient() || this._trackingPluginsInUse(),\n    })\n    Actions.queueTask(t)\n  }\n\n  _sendWithSingleBody = async () => {\n    const responseJSON = await new Promise((resolve, reject) => {\n      // See comments on the Actions.runSendRequest() definition before changing\n      Actions.runSendRequest({\n        syncbackRequestJSON: {\n          type: \"SendMessage\",\n          accountId: this.draft.accountId,\n          props: {\n            messagePayload: this.draft.toJSON(),\n          },\n        },\n        onSuccess: resolve,\n        onError: reject,\n      })\n    })\n    await this._createMessageFromResponse(responseJSON)\n  }\n\n  _sendPerRecipient = async () => {\n    const responseJSON = await new Promise((resolve, reject) => {\n      // See comments on the Actions.runSendRequest() definition before changing\n      Actions.runSendRequest({\n        syncbackRequestJSON: {\n          type: \"SendMessagePerRecipient\",\n          accountId: this.draft.accountId,\n          props: {\n            messagePayload: this.draft.toJSON(),\n            usesOpenTracking: this.draft.metadataForPluginId(OPEN_TRACKING_ID) != null,\n            usesLinkTracking: this.draft.metadataForPluginId(LINK_TRACKING_ID) != null,\n          },\n        },\n        onSuccess: resolve,\n        onError: reject,\n      })\n    })\n    await this._createMessageFromResponse(responseJSON)\n  }\n\n  updatePluginMetadata = () => {\n    this.message.pluginMetadata.forEach((m) => {\n      const t1 = new SyncbackMetadataTask(this.message.clientId,\n          this.message.constructor.name, m.pluginId);\n      Actions.queueTask(t1);\n    });\n\n    return Promise.resolve();\n  }\n\n  _createMessageFromResponse = (responseJSON) => {\n    const {failedRecipients, message} = responseJSON\n    if (failedRecipients && failedRecipients.length > 0) {\n      const errorMessage = `We had trouble sending this message to all recipients. ${failedRecipients} may not have received this email.`;\n      NylasEnv.showErrorDialog(errorMessage, {showInMainWindow: true});\n    }\n    if (!message || !message.id || !message.account_id) {\n      const errorMessage = `Your message successfully sent; however, we had trouble saving your message, \"${message.subject}\", to your Sent folder.`;\n      if (!message) {\n        throw new Error(`${errorMessage}\\n\\nError: Did not return message`)\n      }\n      if (!message.id) {\n        throw new Error(`${errorMessage}\\n\\nError: Returned a message without id`)\n      }\n      if (!message.accountId) {\n        throw new Error(`${errorMessage}\\n\\nError: Returned a message without accountId`)\n      }\n    }\n\n    this.message = new Message().fromJSON(message);\n    this.message.clientId = this.draft.clientId;\n    this.message.body = this.draft.body;\n    this.message.draft = false;\n    this.message.clonePluginMetadataFrom(this.draft);\n\n    return DatabaseStore.inTransaction((t) =>\n      this.refreshDraftReference().then(() => {\n        return t.persistModel(this.message);\n      })\n    );\n  }\n\n  onSuccess = () => {\n    Actions.recordUserEvent(\"Draft Sent\")\n    Actions.draftDeliverySucceeded({message: this.message, messageClientId: this.message.clientId, draftClientId: this.draft.clientId});\n    // TODO we shouldn't need to do this anymore\n    NylasAPIHelpers.makeDraftDeletionRequest(this.draft);\n\n    // Play the sending sound\n    if (this.playSound && NylasEnv.config.get(\"core.sending.sounds\")) {\n      SoundRegistry.playSound('send');\n    }\n    if (NylasEnv.timer.isPending(this._timerKey)) {\n      const account = AccountStore.accountForId(this.draft.accountId)\n      const provider = account ? account.provider : 'Unknown provider'\n      Actions.recordPerfMetric({\n        provider,\n        action: 'send-draft',\n        actionTimeMs: NylasEnv.timer.stop(this._timerKey),\n        maxValue: 60 * 1000,\n        succeeded: true,\n      })\n    }\n    return Promise.resolve(Task.Status.Success);\n  }\n\n  onError = (err) => {\n    if (NylasEnv.timer.isPending(this._timerKey)) {\n      const account = AccountStore.accountForId(this.draft.accountId)\n      const provider = account ? account.provider : 'Unknown provider'\n      Actions.recordPerfMetric({\n        provider,\n        action: 'send-draft',\n        actionTimeMs: NylasEnv.timer.stop(this._timerKey),\n        maxValue: 60 * 1000,\n        succeeded: false,\n      })\n    }\n    if (err instanceof BaseDraftTask.DraftNotFoundError) {\n      return Promise.resolve(Task.Status.Continue);\n    }\n\n    let message = err.message;\n\n    // TODO Handle errors in a cleaner way\n    if (err instanceof APIError) {\n      const errorMessage = (err.body && err.body.message) || ''\n      message = `Sorry, this message could not be sent, please try again.`;\n      message += `\\n\\nReason: ${err.message}`\n      if (errorMessage.includes('unable to reach your SMTP server')) {\n        message = `Sorry, this message could not be sent. There was a network error, please make sure you are online.`\n      }\n      if (errorMessage.includes('Incorrect SMTP username or password') ||\n          errorMessage.includes('SMTP protocol error') ||\n          errorMessage.includes('unable to look up your SMTP host')) {\n        Actions.updateAccount(this.draft.accountId, {syncState: Account.SYNC_STATE_AUTH_FAILED})\n        message = `Sorry, this message could not be sent due to an authentication error. Please re-authenticate your account and try again.`\n      }\n      if (err.statusCode === 402) {\n        if (errorMessage.includes('at least one recipient')) {\n          message = `This message could not be delivered to at least one recipient. (Note: other recipients may have received this message - you should check Sent Mail before re-sending this message.)`;\n        } else {\n          message = `Sorry, this message could not be sent because it was rejected by your mail provider. (${errorMessage})`;\n          if (err.body.server_error) {\n            message += `\\n\\n${err.body.server_error}`;\n          }\n        }\n      }\n    }\n\n    if (this.emitError) {\n      if (err instanceof RequestEnsureOnceError) {\n        Actions.draftDeliveryFailed({\n          threadId: this.draft.threadId,\n          draftClientId: this.draft.clientId,\n          errorMessage: `WARNING: Your message MIGHT have sent. We encountered a network problem while the send was in progress. Please wait a few minutes then check your sent folder and try again if necessary.`,\n        });\n      } else {\n        Actions.draftDeliveryFailed({\n          threadId: this.draft.threadId,\n          draftClientId: this.draft.clientId,\n          errorMessage: message,\n          errorDetail: err.message + (err.error ? err.error.stack : '') + err.stack,\n        });\n      }\n    }\n    Actions.recordUserEvent(\"Draft Sending Errored\", {\n      error: err.message,\n      errorClass: err.constructor.name,\n    })\n    err.message = `Send failed (client): ${err.message}`\n    NylasEnv.reportError(err);\n\n    return Promise.resolve([Task.Status.Failed, err]);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/send-feature-usage-event-task.es6",
    "content": "import Task from './task';\nimport NylasAPI from '../nylas-api'\nimport {APIError} from '../errors'\nimport IdentityStore from '../stores/identity-store'\n\nexport default class SendFeatureUsageEventTask extends Task {\n  constructor(featureName) {\n    super();\n    this.featureName = featureName\n  }\n\n  async performLocal(increment = 1) {\n    const newIdent = IdentityStore.identity();\n    if (!newIdent.feature_usage[this.featureName]) {\n      throw new Error(`Can't use ${this.featureName}. Does not exist on identity`)\n    }\n    newIdent.feature_usage[this.featureName].used_in_period += increment\n    await IdentityStore.saveIdentity(newIdent)\n  }\n\n  revert() {\n    this.performLocal(-1)\n  }\n\n  async performRemote() {\n    const options = {\n      method: 'POST',\n      url: `${IdentityStore.URLRoot}/n1/user/feature_usage_event`,\n      body: {feature_name: this.featureName},\n    };\n    try {\n      const updatedIdentity = await IdentityStore.nylasIDRequest(options);\n      await IdentityStore.saveIdentity(updatedIdentity);\n      return Promise.resolve(Task.Status.Success)\n    } catch (err) {\n      if (err instanceof APIError) {\n        if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {\n          this.revert()\n          return Promise.resolve([Task.Status.Failed, err])\n        }\n        return Promise.resolve(Task.Status.Retry)\n      }\n\n      this.revert()\n      NylasEnv.reportError(err);\n      return Promise.resolve([Task.Status.Failed, err])\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/syncback-category-task.es6",
    "content": "import DatabaseStore from '../stores/database-store';\nimport AccountStore from '../stores/account-store';\nimport Task from './task';\nimport NylasAPI from '../nylas-api';\nimport SyncbackTaskAPIRequest from '../syncback-task-api-request';\nimport {APIError} from '../errors';\n\nexport default class SyncbackCategoryTask extends Task {\n\n  constructor({category, displayName} = {}) {\n    super()\n    this.category = category;\n    this.displayName = displayName;\n  }\n\n  label() {\n    const verb = this.category.serverId ? 'Updating' : 'Creating new';\n    return `${verb} ${this.category.displayType()}`;\n  }\n\n  _revertLocal() {\n    return DatabaseStore.inTransaction((t) => {\n      if (this.isUpdate) {\n        this.category.displayName = this._initialDisplayName;\n        return t.persistModel(this.category)\n      }\n      return t.unpersistModel(this.category)\n    })\n  }\n\n  performLocal() {\n    if (!this.category) {\n      return Promise.reject(new Error(\"Attempt to call SyncbackCategoryTask.performLocal without this.category.\"));\n    }\n    this.isUpdate = !!this.category.serverId; // True if updating an existing category\n    return DatabaseStore.inTransaction((t) => {\n      if (this.isUpdate && this.displayName) {\n        this._initialDisplayName = this.category.displayName;\n        this.category.displayName = this.displayName;\n      }\n      return t.persistModel(this.category);\n    });\n  }\n\n  performRemote() {\n    if (!this.category) {\n      return Promise.reject(new Error(\"Attempted to call SyncbackCategoryTask.performRemote without this.category.\"));\n    }\n    const {serverId, accountId} = this.category;\n    const account = AccountStore.accountForId(accountId);\n    const collection = account.usesLabels() ? \"labels\" : \"folders\";\n\n    const method = serverId ? \"PUT\" : \"POST\";\n    const path = serverId ? `/${collection}/${serverId}` : `/${collection}`;\n\n    let runPromise = Promise.resolve();\n\n    if (this._syncbackRequestId) {\n      runPromise = SyncbackTaskAPIRequest.waitForQueuedRequest(this._syncbackRequestId)\n    } else {\n      runPromise = new SyncbackTaskAPIRequest({\n        api: NylasAPI,\n        options: {\n          path,\n          method,\n          accountId,\n          body: {\n            display_name: this.displayName || this.category.displayName,\n          },\n          onSyncbackRequestCreated: (syncbackRequest) => {\n            this._syncbackRequestId = syncbackRequest.id\n          },\n        },\n      }).run()\n    }\n\n    return runPromise.then((responseJSON) => {\n      this.category.serverId = responseJSON.id\n      if (!this.category.serverId) {\n        throw new Error('SyncbackRequest for creating category did not return a serverId!')\n      }\n      return DatabaseStore.inTransaction(t => t.persistModel(this.category))\n    })\n    .thenReturn(Task.Status.Success)\n    .catch(APIError, async (err) => {\n      if (!NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {\n        return Task.Status.Retry;\n      }\n      await this._revertLocal()\n      try {\n        if (/command argument error/gi.test(err.message)) {\n          const action = this.isUpdate ? 'update' : 'create';\n          const type = this.category.displayType();\n          NylasEnv.showErrorDialog(`Could not ${action} ${type}. Your mail provider has placed restrictions on this ${type}.`);\n        }\n      } catch (e) {\n        // If notifying the user fails, just move on and mark the task as failed.\n      }\n      return [Task.Status.Failed, err];\n    })\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/syncback-event-task.es6",
    "content": "import {Event} from 'nylas-exports';\nimport SyncbackModelTask from './syncback-model-task'\n\nconst EVENTS_ENDPOINT = \"/events\"\n\nexport default class SyncbackEventTask extends SyncbackModelTask {\n  constructor(clientId) {\n    super({clientId, endpoint: EVENTS_ENDPOINT})\n  }\n\n  getModelConstructor() {\n    return Event;\n  }\n\n  // Removes the 'object' field from the event's 'when' data. This is only\n  // necessary because the current events API doesn't accept requests\n  // when this field is defined.\n  getRequestData(model) {\n    const data = super.getRequestData(model);\n    delete data.body.when.object;\n    return data;\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/syncback-metadata-task.es6",
    "content": "import SyncbackModelTask from './syncback-model-task'\nimport DatabaseObjectRegistry from '../../registries/database-object-registry'\nimport N1CloudAPI from '../../n1-cloud-api'\nimport NylasAPIRequest from '../nylas-api-request'\n\nexport default class SyncbackMetadataTask extends SyncbackModelTask {\n\n  constructor(modelClientId, modelClassName, pluginId) {\n    super({clientId: modelClientId});\n    this.modelClassName = modelClassName;\n    this.pluginId = pluginId;\n  }\n\n  getModelConstructor() {\n    return DatabaseObjectRegistry.get(this.modelClassName);\n  }\n\n  isDependentOnTask(otherTask) {\n    return (\n      otherTask instanceof SyncbackMetadataTask &&\n      otherTask.pluginId === this.pluginId &&\n      otherTask.sequentialId < this.sequentialId\n    )\n  }\n\n  makeRequest = async (model) => {\n    if (!model.serverId) {\n      throw new Error(`Can't syncback metadata for a ${this.modelClassName} instance that doesn't have a serverId`)\n    }\n    const metadata = model.metadataObjectForPluginId(this.pluginId);\n\n    const objectType = this.modelClassName.toLowerCase();\n    let messageIds;\n    if (objectType === 'thread') {\n      const messages = await model.messages();\n      messageIds = messages.map(message => message.id)\n    }\n    const options = {\n      accountId: model.accountId,\n      path: `/metadata/${model.serverId}/${this.pluginId}`,\n      method: 'POST',\n      body: {\n        version: metadata.version,\n        value: JSON.stringify(metadata.value),\n        objectType: objectType,\n        messageIds: messageIds,\n      },\n    };\n    return new NylasAPIRequest({\n      api: N1CloudAPI,\n      options,\n    }).run()\n  }\n\n  applyRemoteChangesToModel = (model, {version}) => {\n    const metadata = model.metadataObjectForPluginId(this.pluginId);\n    metadata.version = version;\n    return model;\n  };\n\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/syncback-model-task.es6",
    "content": "import _ from 'underscore'\nimport Task from './task'\nimport NylasAPI from '../nylas-api'\nimport NylasAPIRequest from '../nylas-api-request'\nimport {APIError} from '../errors'\nimport DatabaseStore from '../stores/database-store'\n\nexport default class SyncbackModelTask extends Task {\n\n  constructor({clientId, endpoint} = {}) {\n    super()\n    this.clientId = clientId\n    this.endpoint = endpoint\n  }\n\n  shouldDequeueOtherTask(other) {\n    return (other instanceof SyncbackModelTask &&\n            this.clientId === other.clientId)\n  }\n\n  getModelConstructor() {\n    throw new Error(\"You must subclass and implement `getModelConstructor`. Return a constructor class\")\n  }\n\n  performLocal() {\n    this.validateRequiredFields([\"clientId\"])\n    return Promise.resolve()\n  }\n\n  performRemote() {\n    return Promise.resolve()\n    .then(this.getLatestModel)\n    .then(this.verifyModel)\n    .then(this.makeRequest)\n    .then(this.updateLocalModel)\n    .thenReturn(Task.Status.Success)\n    .catch(this.handleRemoteError)\n  }\n\n  getLatestModel = () => {\n    return DatabaseStore.findBy(this.getModelConstructor(),\n                                {clientId: this.clientId})\n  }\n\n  verifyModel = (model) => {\n    if (model) {\n      return Promise.resolve(model)\n    }\n    throw new Error(`Can't find a '${this.getModelConstructor().name}' model for clientId: ${this.clientId}'`)\n  }\n\n  makeRequest = (model) => {\n    try {\n      const options = _.extend({\n        accountId: model.accountId,\n      }, this.getRequestData(model));\n      return new NylasAPIRequest({\n        api: NylasAPI,\n        options,\n      }).run()\n    } catch (error) {\n      return Promise.reject(error)\n    }\n  }\n\n  getRequestData(model) {\n    if (model.isSavedRemotely()) {\n      return {\n        path: `${this.endpoint}/${model.serverId}`,\n        body: model.toJSON(),\n        method: \"PUT\",\n      }\n    }\n    return {\n      path: `${this.endpoint}`,\n      body: model.toJSON(),\n      method: \"POST\",\n    }\n  }\n\n  updateLocalModel = (responseJSON) => {\n    /*\n    Important: There could be a significant delay between us initiating\n    the save and getting JSON back from the server. Our local copy of\n    the model may have already changed more.\n\n    The only fields we want to update from the server are the `id` and\n    `version`.\n    */\n    return DatabaseStore.inTransaction((t) => {\n      return this.getLatestModel().then((model) => {\n        // Model may have been deleted\n        if (!model) { return Promise.resolve() }\n        const changed = this.applyRemoteChangesToModel(model, responseJSON);\n        return t.persistModel(changed);\n      })\n    }).thenReturn(true)\n  };\n\n  applyRemoteChangesToModel = (model, {version, id}) => {\n    model.version = version;\n    model.serverId = id;\n    return model;\n  };\n\n  handleRemoteError = (err) => {\n    if (err instanceof APIError) {\n      if (NylasAPI.PermanentErrorCodes.includes(err.statusCode)) {\n        return Promise.resolve([Task.Status.Failed, err])\n      }\n      if (err.statusCode === 409) {\n        return this.handleRemoteVersionConflict(err);\n      }\n      return Promise.resolve(Task.Status.Retry)\n    }\n\n    NylasEnv.reportError(err);\n    return Promise.resolve([Task.Status.Failed, err])\n  };\n\n  handleRemoteVersionConflict = (err) => {\n    return Promise.resolve([Task.Status.Failed, err])\n  };\n\n  canBeUndone() { return false }\n\n  isUndo() { return false }\n}\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/task-factory.es6",
    "content": "import _ from 'underscore'\nimport ChangeFolderTask from './change-folder-task'\nimport ChangeLabelsTask from './change-labels-task'\nimport ChangeUnreadTask from './change-unread-task'\nimport ChangeStarredTask from './change-starred-task'\nimport AccountStore from '../stores/account-store'\nimport CategoryStore from '../stores/category-store'\nimport Thread from '../models/thread'\nimport Category from '../models/category'\n\n\nconst TaskFactory = {\n\n  tasksForApplyingCategories({threads, categoriesToRemove, categoriesToAdd, taskDescription, source}) {\n    const byAccount = {}\n    const tasks = []\n\n    threads.forEach((thread) => {\n      if (!(thread instanceof Thread)) {\n        throw new Error(\"tasksForApplyingCategories: `threads` must be instances of Thread\")\n      }\n      const {accountId} = thread\n      if (!byAccount[accountId]) {\n        byAccount[accountId] = {\n          categoriesToRemove: categoriesToRemove ? categoriesToRemove(accountId) : [],\n          categoriesToAdd: categoriesToAdd ? categoriesToAdd(accountId) : [],\n          threadsToUpdate: [],\n        }\n      }\n      byAccount[accountId].threadsToUpdate.push(thread)\n    })\n\n    _.each(byAccount, (data, accountId) => {\n      const catsToAdd = data.categoriesToAdd;\n      const catsToRemove = data.categoriesToRemove;\n      const threadsToUpdate = data.threadsToUpdate;\n      const account = AccountStore.accountForId(accountId);\n      if (!(catsToAdd instanceof Array)) {\n        throw new Error(\"tasksForApplyingCategories: `categoriesToAdd` must return an array of Categories\")\n      }\n      if (!(catsToRemove instanceof Array)) {\n        throw new Error(\"tasksForApplyingCategories: `categoriesToRemove` must return an array of Categories\")\n      }\n\n      if (account.usesFolders()) {\n        if (catsToAdd.length === 0) return;\n        if (catsToAdd.length > 1) {\n          throw new Error(\"tasksForApplyingCategories: `categoriesToAdd` must return a single `Category` (folder) for Exchange accounts\")\n        }\n        const folder = catsToAdd[0]\n        if (!(folder instanceof Category)) {\n          throw new Error(\"tasksForApplyingCategories: `categoriesToAdd` must return a Category\")\n        }\n\n        tasks.push(new ChangeFolderTask({\n          folder,\n          source,\n          threads: threadsToUpdate,\n          taskDescription,\n        }))\n      } else {\n        const labelsToAdd = catsToAdd\n        const labelsToRemove = catsToRemove\n        if (labelsToAdd.length === 0 && labelsToRemove.length === 0) return;\n\n        tasks.push(new ChangeLabelsTask({\n          source,\n          threads: threadsToUpdate,\n          labelsToRemove,\n          labelsToAdd,\n          taskDescription,\n        }))\n      }\n    })\n\n    return tasks;\n  },\n\n  taskForApplyingCategory({threads, category, source}) {\n    const tasks = TaskFactory.tasksForApplyingCategories({\n      source,\n      threads,\n      categoriesToAdd: () => [category],\n    })\n\n    if (tasks.length > 1) {\n      throw new Error(\"taskForApplyingCategory: Threads must be from the same account.\")\n    }\n\n    return tasks[0];\n  },\n\n  taskForRemovingCategory({threads, category, source}) {\n    const tasks = TaskFactory.tasksForApplyingCategories({\n      source,\n      threads,\n      categoriesToRemove: () => [category],\n    })\n\n    if (tasks.length > 1) {\n      throw new Error(\"taskForRemovingCategory: Threads must be from the same account.\")\n    }\n\n    return tasks[0];\n  },\n\n  tasksForMarkingAsSpam({threads, source}) {\n    return TaskFactory.tasksForApplyingCategories({\n      source,\n      threads,\n      categoriesToAdd: (accountId) => [CategoryStore.getSpamCategory(accountId)],\n      categoriesToRemove: (accountId) => [CategoryStore.getInboxCategory(accountId)],\n    })\n  },\n\n  tasksForArchiving({threads, source}) {\n    return TaskFactory.tasksForApplyingCategories({\n      source,\n      threads,\n      categoriesToRemove: (accountId) => [\n        CategoryStore.getInboxCategory(accountId),\n      ],\n      categoriesToAdd: (accountId) => [CategoryStore.getArchiveCategory(accountId)],\n    })\n  },\n\n  tasksForMovingToTrash({threads, source}) {\n    return TaskFactory.tasksForApplyingCategories({\n      source,\n      threads,\n      categoriesToAdd: (accountId) => [CategoryStore.getTrashCategory(accountId)],\n      categoriesToRemove: (accountId) => [CategoryStore.getInboxCategory(accountId)],\n    })\n  },\n\n  taskForInvertingUnread({threads, source, canBeUndone}) {\n    const unread = _.every(threads, (t) => _.isMatch(t, {unread: false}))\n    return new ChangeUnreadTask({threads, unread, source, canBeUndone})\n  },\n\n  taskForSettingUnread({threads, unread, source, canBeUndone}) {\n    return new ChangeUnreadTask({threads, unread, source, canBeUndone})\n  },\n\n  taskForInvertingStarred({threads, source}) {\n    const starred = _.every(threads, (t) => _.isMatch(t, {starred: false}))\n    return new ChangeStarredTask({threads, starred, source})\n  },\n}\n\nexport default TaskFactory\n"
  },
  {
    "path": "packages/client-app/src/flux/tasks/task.es6",
    "content": "/* eslint no-unused-vars: 0*/\nimport _ from 'underscore';\nimport {generateTempId} from '../models/utils';\nimport {PermanentErrorCodes} from '../nylas-api';\nimport {APIError} from '../errors';\n\nconst TaskStatus = {\n  Retry: \"RETRY\",\n  Success: \"SUCCESS\",\n  Continue: \"CONTINUE\",\n  Failed: \"FAILED\",\n};\n\nconst TaskDebugStatus = {\n  JustConstructed: \"JUST CONSTRUCTED\",\n  UncaughtError: \"UNCAUGHT ERROR\",\n  DequeuedObsolete: \"DEQUEUED (Obsolete)\",\n  DequeuedDependency: \"DEQUEUED (Dependency Failure)\",\n  WaitingOnQueue: \"WAITING ON QUEUE\",\n  WaitingToRetry: \"WAITING TO RETRY\",\n  WaitingOnDependency: \"WAITING ON DEPENDENCY\",\n  RunningLocal: \"RUNNING LOCAL\",\n  ProcessingRemote: \"PROCESSING REMOTE\",\n};\n\n// Public: Tasks are a robust way to handle any mutating changes that need\n// to interface with a remote API.\n//\n// Tasks help you handle and encapsulate optimistic updates, rollbacks,\n// undo/redo, API responses, API errors, queuing, and multi-step\n// dependencies.\n//\n// They are especially useful in offline mode. Users may have taken tons of\n// actions that we've queued up to process when they come back online.\n//\n// Tasks represent individual changes to the datastore that alter the local\n// cache and need to be synced back to the server.\n//\n// To create your own task, subclass Task and implement the following\n// required methods:\n//\n// - {Task::performLocal}\n// - {Task::performRemote}\n//\n// See their usage in the documentation below.\n//\n// ## Task Dependencies\n//\n// The Task system handles dependencies between multiple queued tasks.  To\n// establish dependencies between tasks, your subclass may implement one\n// or more of the following methods:\n//\n// - {Task::isDependentOnTask}\n// - {Task::onDependentTaskError}\n// - {Task::shouldDequeueOtherTask}\n//\n// ## Undo / Redo\n//\n// The Task system also supports undo/redo handling. Your subclass must\n// implement the following methods to enable this:\n//\n// - {Task::isUndo}\n// - {Task::canBeUndone}\n// - {Task::createUndoTask}\n// - {Task::createIdenticalTask}\n//\n// ## Offline Considerations\n//\n// All tasks should gracefully handle the case when there is no network\n// connection.\n//\n// if we're offline the common behavior is for a task to:\n//\n// 1. Perform its local change\n// 2. Attempt the remote request, which will fail\n// 3. Have `performRemote` resolve a `Task.Status.Retry`\n// 3. Sit queued up waiting to be retried\n//\n// Remember that a user may be offline for hours and perform thousands of\n// tasks in the meantime. It's important that your tasks implement\n// `shouldDequeueOtherTask` and `isDependentOnTask` to make sure ordering\n// always remains correct.\n//\n// ## Serialization and Window Considerations\n//\n// The whole {TaskQueue} and all of its Tasks are serialized and stored in\n// the Database. This allows the {TaskQueue} to work across windows and\n// ensures we don't lose any pending tasks if (a user is offline for a while)\n// and quits and relaunches the application.\n//\n// All instance variables you create must be able to be serialized to a\n// JSON string and re-inflated. Notably, **`function` objects will not be\n// properly re-inflated**.\n//\n// if (you have instance variables that are instances of core {Model})\n// classes or {Task} classes, they will be automatically re-inflated to the\n// correct class via {Utils::deserializeRegisteredObject}. if (you create)\n// your own custom classes, they must be registered once per window via\n// {TaskRegistry::register}\n//\n// ## Example Task\n//\n// **Task Definition**:\n//\n// ```js\n// import _ from 'underscore'\n// import request from 'request'\n// import {Task, DatabaseStore} from 'nylas-exports'\n//\n// class UpdateTodoTask extends Task {\n//   constructor(existingTodo, newData) {\n//     super()\n//     this.existingTodo = existingTodo;\n//     this.newData = newData;\n//   }\n//\n//   performLocal() {\n//     this.updatedTodo = _.extend(_.clone(this.existingTodo), this.newData);\n//     return DatabaseStore.inTransaction((t) => t.persistModel(this.updatedTodo));\n//   }\n//\n//   performRemote() {\n//     return new Promise (resolve, reject) => {\n//       const options = {url: \"https://myapi.co\", method: 'PUT', json: this.newData}\n//       request(options, (error, response, body) => {\n//         if (error) {\n//           resolve(Task.Status.Failed);\n//         }\n//         resolve(Task.Status.Success);\n//       });\n//     };\n//   };\n// }\n// ```\n//\n// **Task Usage**:\n//\n// ```coffee\n// import {Actions} from 'nylas-exports';\n// import UpdateTodoTask from './update-todo-task';\n//\n// someMethod() {\n//   ...\n//   const task = new UpdateTodoTask(existingTodo, {name: \"Test\"});\n//   Actions.queueTask(task);\n//   ...\n// }\n// ```\n//\n// This example `UpdateTodoTask` does not handle undo/redo, nor does it\n// rollback the changes if (there's an API error. See examples in)\n// {Task::performLocal} for ideas on how to handle this.\n//\nexport default class Task {\n\n  static Status = TaskStatus;\n  static DebugStatus = TaskDebugStatus;\n\n  // Public: Override the constructor to pass initial args to your Task and\n  // initialize instance variables.\n  //\n  // **IMPORTANT:** if (you override the constructor, be sure to call)\n  // `super`.\n  //\n  // On construction, all Tasks instances are given a unique `id`.\n  constructor() {\n    this._rememberedToCallSuper = true;\n    this.id = generateTempId();\n    this.sequentialId = null; // set when queued\n    this.queueState = {\n      isProcessing: false,\n      localError: null,\n      localComplete: false,\n      remoteError: null,\n      remoteAttempts: 0,\n      remoteComplete: false,\n      status: null,\n      debugStatus: Task.DebugStatus.JustConstructed,\n    };\n  }\n\n  // Private: This is a internal wrapper around `performLocal`\n  runLocal() {\n    if (!this._rememberedToCallSuper) {\n      throw new Error(\"Your must call `super` from your Task's constructors\");\n    }\n\n    if (this.queueState.localComplete) {\n      return Promise.resolve();\n    }\n\n    this.queueState.debugStatus = Task.DebugStatus.RunningLocal;\n    try {\n      return this.performLocal()\n      .then(() => {\n        this.queueState.localComplete = true;\n        this.queueState.localError = null;\n        this.queueState.debugStatus = Task.DebugStatus.WaitingOnQueue;\n        return Promise.resolve();\n      })\n      .catch(this._handleLocalError);\n    } catch (err) {\n      return this._handleLocalError(err);\n    }\n  }\n\n  _handleLocalError = (err) => {\n    this.queueState.localError = err;\n    this.queueState.status = Task.Status.Failed;\n    this.queueState.debugStatus = Task.DebugStatus.UncaughtError;\n    NylasEnv.reportError(err);\n    return Promise.reject(err);\n  }\n\n  // Private: This is an internal wrapper around `performRemote`\n  runRemote() {\n    this.queueState.debugStatus = Task.DebugStatus.ProcessingRemote;\n\n    if (this.queueState.localComplete === false) {\n      throw new Error(\"runRemote called before performLocal complete, this is an assertion failure.\")\n    }\n\n    if (this.queueState.remoteComplete) {\n      this.queueState.status = Task.Status.Continue;\n      return Promise.resolve(Task.Status.Continue);\n    }\n\n    try {\n      return this.performRemote()\n      .then((compositeStatus) => {\n        const [status, err] = this._compositeStatus(compositeStatus);\n\n        if (status === Task.Status.Failed) {\n          // We reject here to end up on the same path as people who may\n          // have manually `reject`ed the promise\n          return Promise.reject(compositeStatus);\n        }\n\n        this.queueState.status = status;\n        this.queueState.remoteAttempts += 1;\n        this.queueState.remoteComplete = [Task.Status.Success, Task.Status.Continue].includes(status);\n        this.queueState.remoteError = null;\n        return Promise.resolve(status);\n      })\n      .catch((compositeStatus) => {\n        const [status, err] = this._compositeStatus(compositeStatus);\n        return this._handleRemoteError(err, status);\n      })\n    } catch (err) {\n      return this._handleRemoteError(err);\n    }\n  }\n\n  // When resolving from performRemote, people can resolve one of the\n  // `Task.Status` constants. In the case of `Task.Status.Failed`, they can\n  // return an array with the constant as the first item and the error\n  // object as the second item. We are also resilient to accidentally\n  // getting passed malformed values or error objects.\n  //\n  // This always returns in the form of `[status, err]`\n  _compositeStatus(compositeStatus) {\n    if (compositeStatus instanceof Error) {\n      return [Task.Status.Failed, compositeStatus];\n    }\n\n    if (_.isString(compositeStatus)) {\n      if (_.values(Task.Status).includes(compositeStatus)) {\n        return [compositeStatus, null];\n      }\n      const err = new Error(`performRemote returned ${compositeStatus}, which is not a Task.Status`);\n      return [Task.Status.Failed, err];\n    }\n\n    if (_.isArray(compositeStatus)) {\n      const [status, err] = compositeStatus;\n      return [status, err];\n    }\n    const err = new Error(`performRemote returned ${compositeStatus}, which is not a Task.Status`);\n    return [Task.Status.Failed, err];\n  }\n\n  _handleRemoteError = (err, status) => {\n    // Sometimes users just indicate that a task Failed, but don't provide\n    // the error object\n    const exitError = err || new Error(`Unspecified error in ${Task.constructor.name}.performRemote`);\n\n    if (status !== Task.Status.Failed) {\n      this.queueState.debugStatus = Task.DebugStatus.UncaughtError;\n      NylasEnv.reportError(exitError);\n    }\n\n    this.queueState.status = Task.Status.Failed;\n    this.queueState.remoteAttempts += 1;\n    this.queueState.remoteError = exitError;\n\n    return Promise.reject(exitError);\n  }\n\n  // HELPER METHODS\n  validateRequiredFields = (fields = []) => {\n    for (const field of fields) {\n      if (!this[field]) {\n        throw new Error(`Must pass ${field}`);\n      }\n    }\n  }\n\n  // Most tasks that interact with a RESTful API will want to behave in a\n  // similar way. We retry on temproary API error codes and permenantly\n  // fail on others.\n  apiErrorHandler = (err = {}) => {\n    if (err instanceof APIError) {\n      if (PermanentErrorCodes.includes(err.statusCode)) {\n        return Promise.resolve([Task.Status.Failed, err])\n      }\n      return Promise.resolve(Task.Status.Retry)\n    }\n    return Promise.resolve([Task.Status.Failed, err]);\n  }\n\n\n  // METHODS TO OBSERVE\n  //\n  // Public: **Required** | Override to perform local, optimistic updates.\n  //\n  // Most tasks will put code in here that updates the {DatabaseStore}\n  //\n  // You should also implement the rollback behavior inside of\n  // `performLocal` or in some helper method. It's common practice (but not\n  // automatic) for `performLocal` to be re-called at the end of an API\n  // failure from `performRemote`.\n  //\n  // That rollback behavior is also likely the same when you want to undo a\n  // task. It's common practice (but not automatic) for `createUndoTask` to\n  // set some flag that `performLocal` will recognize to implement the\n  // rollback behavior.\n  //\n  // `performLocal` will complete BEFORE the task actually enters the\n  // {TaskQueue}.\n  //\n  // if (you would like to do work after `performLocal` has run, you can use)\n  // {TaskQueueStatusStore::waitForPerformLocal}. Pass it the task and it\n  // will return a Promise that resolves once the local action has\n  // completed. This is contained in the {TaskQueueStatusStore} so you can\n  // listen to tasks across windows.\n  //\n  // ## Examples\n  //\n  // ### Simple Optimistic Updating\n  //\n  // ```js\n  // class MyTask extends Task {\n  //   performLocal() {\n  //     this.updatedModel = this._myModelUpdateCode()\n  //     return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));\n  //   }\n  // }\n  // ```\n  //\n  // ### Handling rollback on API failure\n  //\n  // ```js\n  // class MyTask extends Task\n  //   performLocal() {\n  //     if (this._reverting) {\n  //       this.updatedModel = this._myModelRollbackCode();\n  //     } else {\n  //       this.updatedModel = this._myModelUpdateCode();\n  //     }\n  //     return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));\n  //   }\n  //   performRemote() {\n  //     return this._APIPutHelperMethod(this.updatedModel).catch((apiError) => {\n  //       if (apiError.statusCode === 500) {\n  //         this._reverting = true;\n  //         return this.performLocal();\n  //       }\n  //     }\n  //   }\n  // }\n  // ```\n  //\n  // ### Handling an undo task\n  //\n  // ```js\n  // class MyTask extends Task {\n  //   performLocal() {\n  //     if (this._isUndoTask) {\n  //       this.updatedModel = this._myModelRollbackCode();\n  //     } else {\n  //       this.updatedModel = this._myModelUpdateCode();\n  //     }\n  //     return DatabaseStore.inTransaction((t) => persistModel(this.updatedModel));\n  //   }\n  //\n  //   createUndoTask() {\n  //     undoTask = this.createIdenticalTask();\n  //     undoTask._isUndoTask = true;\n  //     return undoTask;\n  //   }\n  // }\n  // ```\n  //\n  // Also see the documentation on the required undo methods\n  //\n  // Returns a {Promise} that resolves when your updates are complete.\n  performLocal() {\n    return Promise.resolve();\n  }\n\n  // Public: **Required** | Put the actual API request code here.\n  //\n  // You must return a {Promise} that resolves to one of the following\n  // status constants:\n  //\n  //   - `Task.Status.Success`\n  //   - `Task.Status.Retry`\n  //   - `Task.Status.Continue`\n  //   - `Task.Status.Failed`\n  //\n  // The resolved status will determine what the {TaskQueue} does with this\n  // task when it is finished.\n  //\n  // This is where you should put your actual API code. You can use the\n  // node `request` library to easily hit APIs, or use the {NylasAPI} class\n  // to talk to the [Nylas Platform API](https://nylas.com/cloud/docs/).\n  //\n  // Here is a more detailed explanation of Task Statuses:\n  //\n  // ### Task.Status.Success\n  //\n  // Resolve to `Task.Status.Success` when the task successfully completes.\n  // Once done, the task will be dequeued and logged as a success.\n  //\n  // ### Task.Status.Retry\n  //\n  // if (you resolve `Task.Status.Retry`, the task will remain on the queue)\n  // and tried again later. Any other task dependent on the current one\n  // will also continue waiting.\n  //\n  // `Task.Status.Retry` is useful if (it looks like we're offline, or you)\n  // get an API error code that indicates temporary failure.\n  //\n  // ### Task.Status.Continue\n  //\n  // Resolving `Task.Status.Continue` will silently dequeue the task, allow\n  // dependent tasks through, but not mark it as successfully resolved.\n  //\n  // This is useful if (you get permanent API errors, but don't really care)\n  // if (the task failed.)\n  //\n  // ### Task.Status.Failed\n  //\n  // if (you catch a permanent API error code (like a 500), or something)\n  // else goes wrong then resolve to `Task.Status.Failed`.\n  //\n  // Resolving `Task.Status.Failed` will dequeue this task, and **dequeue\n  // all dependent tasks**.\n  //\n  // You can optionally return the error object itself for debugging\n  // purposes by resolving an array of the form: `[Task.Status.Failed,\n  // errorObject]`\n  //\n  // You should not `throw` exceptions. Catch all cases yourself and\n  // determine which `Task.Status` to resolve to. if (due to programmer)\n  // error an exception is thrown, our {TaskQueue} will catch it, log it,\n  // and deal with the task as if (it resolved `Task.Status.Failed`.)\n  //\n  // Returns a {Promise} that resolves to a valid `Task.Status` type.\n  performRemote() {\n    return Promise.resolve(Task.Status.Success);\n  }\n\n  // Public: determines which other tasks this one is dependent on.\n  //\n  // - `other` An instance of a {Task} you must test to see if (it's a)\n  // dependency of this one.\n  //\n  // Any task that passes the truth test will be considered a \"dependency\".\n  //\n  // if a \"dependency\" has a `Task.Status.Failed`, then all downstream\n  // tasks will get dequeued recursively for any of the downstream tasks that\n  // return true for `shouldBeDequeuedOnDependencyFailure`\n  //\n  // A task will also never be run at the same time as one of its\n  // dependencies.\n  //\n  // Returns `true` (is dependent on) or `false` (is not dependent on)\n  isDependentOnTask(other) {\n    return false;\n  }\n\n  // Public: determines which other tasks this one should dequeue when\n  // it is first queued.\n  //\n  // - `other` An instance of a {Task} you must test to see if (it's now)\n  // obsolete.\n  //\n  // Any task that passes the truth test will be considered \"obsolete\" and\n  // dequeued immediately.\n  //\n  // This is particularly useful in offline mode. Users may queue up tons\n  // of tasks but when we come back online to process them, we only want to\n  // process the latest one.\n  //\n  // Returns `true` (should dequeue) or `false` (should not dequeue)\n  shouldDequeueOtherTask(other) {\n    return false;\n  }\n\n  // Public: determines if the current task should be dequeued if one of the\n  // tasks it depends on fails.\n  //\n  // Returns `true` (should dequeue) or `false` (should not dequeue)\n  shouldBeDequeuedOnDependencyFailure() {\n    return true;\n  }\n\n  onDependentTaskError(other, error) {\n\n  }\n\n  // Public: It's up to you to determine how you want to indicate whether\n  // or not you have an instance of an \"Undo Task\". We commonly use a\n  // simple instance variable boolean flag.\n  //\n  // Returns `true` (is an Undo Task) or `false` (is not an Undo Task)\n  isUndo() {\n    return false;\n  }\n\n  // Public: Determines whether or not this task can be undone via the\n  // {UndoRedoStore}\n  //\n  // Returns `true` (can be undone) or `false` (can't be undone)\n  canBeUndone() {\n    return false;\n  }\n\n  // Public: Return from `createIdenticalTask` and set a flag so your\n  // `performLocal` and `performRemote` methods know that this is an undo\n  // task.\n  createUndoTask() {\n    throw new Error(\"Unimplemented\");\n  }\n\n  // Public: Return a deep-cloned task to be used for an undo task\n  createIdenticalTask() {\n    const json = this.toJSON();\n    delete json.queueState;\n    return (new this.constructor()).fromJSON(json);\n  }\n\n  // Public: code to run if (someone tries to dequeue your task while it is)\n  // in flight.\n  //\n  cancel() {\n\n  }\n\n  // Public: (optional) A string displayed to users when your task is run.\n  //\n  // When tasks are run, we automatically display a notification to users\n  // of the form \"label (numberOfImpactedItems)\". if (this does not a return)\n  // a string, no notification is displayed\n  label() {\n\n  }\n\n  // Public: A string displayed to users indicating how many items your\n  // task affected.\n  numberOfImpactedItems() {\n    return 1;\n  }\n\n  // Private: Allows for serialization of tasks\n  toJSON() {\n    return this;\n  }\n\n  // Private: Allows for deserialization of tasks\n  fromJSON(json) {\n    for (const key of Object.keys(json)) {\n      this[key] = json[key];\n    }\n    return this;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/fs-utils.es6",
    "content": "import fs from 'fs'\nimport Utils from './flux/models/utils'\n\nexport function atomicWriteFileSync(filepath, content) {\n  const randomId = Utils.generateTempId()\n  const backupPath = `${filepath}.${randomId}.bak`\n  fs.writeFileSync(backupPath, content)\n  fs.renameSync(backupPath, filepath)\n}\n"
  },
  {
    "path": "packages/client-app/src/global/nylas-component-kit.coffee",
    "content": "# Publically exposed Nylas UI Components\nclass NylasComponentKit\n\n  @default = (requireValue) -> requireValue.default ? requireValue\n\n  @load = (prop, path) ->\n    Object.defineProperty @prototype, prop,\n      get: ->\n        NylasComponentKit.default(require \"../components/#{path}\")\n\n  # We use require to load the component immediately (instead of lazy loading)\n  # to improve visible latency,\n  # Sometimes a component won't be loaded until the user performs an action\n  # (e.g. opening a popover), so we don't want to wait until that happens to load the\n  # component. In our example, the popover would take a long time to open the first time\n  # if it was lazy loaded\n  @require = (prop, path) ->\n    exported = NylasComponentKit.default(require \"../components/#{path}\")\n    Object.defineProperty @prototype, prop,\n      get: -> exported\n\n  @requireFrom = (prop, path) ->\n    exported = require \"../components/#{path}\"\n    Object.defineProperty @prototype, prop,\n      get: -> exported[prop]\n\n  @loadFrom = (prop, path) ->\n    Object.defineProperty @prototype, prop,\n      get: ->\n        exported = require \"../components/#{path}\"\n        return exported[prop]\n\n  @loadDeprecated = (prop, path, {instead} = {}) ->\n    {deprecate} = require '../deprecate-utils'\n    Object.defineProperty @prototype, prop,\n      get: deprecate prop, instead, @, ->\n        exported = NylasComponentKit.default(require \"../components/#{path}\")\n        return exported\n      enumerable: true\n\n  @load \"Menu\", 'menu'\n  @load \"DropZone\", 'drop-zone'\n  @load \"Spinner\", 'spinner'\n  @load \"Switch\", 'switch'\n  @loadDeprecated \"Popover\", 'popover', instead: 'Actions.openPopover'\n  @load \"FixedPopover\", 'fixed-popover'\n  @require \"DatePickerPopover\", 'date-picker-popover'\n  @load \"Modal\", 'modal'\n  @load \"Webview\", 'webview'\n  @load \"FeatureUsedUpModal\", 'feature-used-up-modal'\n  @load \"BillingModal\", 'billing-modal'\n  @load \"OpenIdentityPageButton\", 'open-identity-page-button'\n  @load \"Flexbox\", 'flexbox'\n  @load \"RetinaImg\", 'retina-img'\n  @load \"SwipeContainer\", 'swipe-container'\n  @load \"FluxContainer\", 'flux-container'\n  @load \"FocusContainer\", 'focus-container'\n  @load \"SyncingListState\", 'syncing-list-state'\n  @load \"EmptyListState\", 'empty-list-state'\n  @load \"ListTabular\", 'list-tabular'\n  @load \"Notification\", 'notification'\n  @load \"NylasCalendar\", 'nylas-calendar/nylas-calendar'\n  @load \"MiniMonthView\", 'nylas-calendar/mini-month-view'\n  @load \"CalendarEventPopover\", 'nylas-calendar/calendar-event-popover'\n  @load \"EventedIFrame\", 'evented-iframe'\n  @load \"ButtonDropdown\", 'button-dropdown'\n  @load \"Contenteditable\", 'contenteditable/contenteditable'\n  @load \"MultiselectList\", 'multiselect-list'\n  @load \"BoldedSearchResult\", 'bolded-search-result'\n  @load \"MultiselectDropdown\", \"multiselect-dropdown\"\n  @load \"KeyCommandsRegion\", 'key-commands-region'\n  @load \"TabGroupRegion\", 'tab-group-region'\n  @load \"InjectedComponent\", 'injected-component'\n  @load \"TokenizingTextField\", 'tokenizing-text-field'\n  @load \"ParticipantsTextField\", 'participants-text-field'\n  @loadDeprecated \"MultiselectActionBar\", 'multiselect-action-bar', instead: 'MultiselectToolbar'\n  @load \"MultiselectToolbar\", 'multiselect-toolbar'\n  @load \"InjectedComponentSet\", 'injected-component-set'\n  @load \"MetadataComposerToggleButton\", 'metadata-composer-toggle-button'\n  @load \"ConfigPropContainer\", \"config-prop-container\"\n  @load \"DisclosureTriangle\", \"disclosure-triangle\"\n  @load \"EditableList\", \"editable-list\"\n  @load \"OutlineViewItem\", \"outline-view-item\"\n  @load \"OutlineView\", \"outline-view\"\n  @load \"DateInput\", \"date-input\"\n  @load \"DatePicker\", \"date-picker\"\n  @load \"TimePicker\", \"time-picker\"\n  @load \"Table\", \"table/table\"\n  @loadFrom \"TableRow\", \"table/table\"\n  @loadFrom \"TableCell\", \"table/table\"\n  @load \"SelectableTable\", \"selectable-table\"\n  @loadFrom \"SelectableTableRow\", \"selectable-table\"\n  @loadFrom \"SelectableTableCell\", \"selectable-table\"\n  @load \"EditableTable\", \"editable-table\"\n  @loadFrom \"EditableTableCell\", \"editable-table\"\n  @load \"Toast\", \"toast\"\n  @load \"UndoToast\", \"undo-toast\"\n  @load \"LazyRenderedList\", \"lazy-rendered-list\"\n  @load \"OverlaidComponents\", \"overlaid-components/overlaid-components\"\n  @load \"OverlaidComposerExtension\", \"overlaid-components/overlaid-composer-extension\"\n  @load \"OAuthSignInPage\", \"oauth-signin-page\"\n  @requireFrom \"AttachmentItem\", \"attachment-items\"\n  @requireFrom \"ImageAttachmentItem\", \"attachment-items\"\n  @load \"CodeSnippet\", \"code-snippet\"\n  @load \"DropdownMenu\", \"dropdown-menu\"\n\n  @load \"ScrollRegion\", 'scroll-region'\n  @load \"ResizableRegion\", 'resizable-region'\n\n  @loadFrom \"MailLabel\", \"mail-label\"\n  @loadFrom \"LabelColorizer\", \"mail-label\"\n  @load \"MailLabelSet\", \"mail-label-set\"\n  @load \"MailImportantIcon\", 'mail-important-icon'\n\n  @loadFrom \"FormItem\", \"generated-form\"\n  @loadFrom \"GeneratedForm\", \"generated-form\"\n  @loadFrom \"GeneratedFieldset\", \"generated-form\"\n\n  @load \"ScenarioEditor\", 'scenario-editor'\n\n  @load \"SearchBar\", 'search-bar'\n\n  # Higher order components\n  @load \"ListensToObservable\", 'decorators/listens-to-observable'\n  @load \"ListensToFluxStore\", 'decorators/listens-to-flux-store'\n  @load \"ListensToMovementKeys\", 'decorators/listens-to-movement-keys'\n  @load \"HasTutorialTip\", 'decorators/has-tutorial-tip'\n\nmodule.exports = new NylasComponentKit()\n"
  },
  {
    "path": "packages/client-app/src/global/nylas-exports.es6",
    "content": "/* eslint global-require: 0 */\n/* eslint import/no-dynamic-require: 0 */\nimport TaskRegistry from '../registries/task-registry'\nimport StoreRegistry from '../registries/store-registry'\nimport DatabaseObjectRegistry from '../registries/database-object-registry'\n\nconst resolveExport = (requireValue) => {\n  return requireValue.default || requireValue;\n}\n\n// This module exports an empty object, with a ton of defined properties that\n// `require` files the first time they're called.\nmodule.exports = exports = window.$n = {};\n\n// Calling require() repeatedly isn't free! Even though it has it's own cache,\n// it still needs to resolve the path to a file based on the current __dirname,\n// match it against it's cache, etc. We can shortcut all this work.\nconst RequireCache = {};\n\n// Will lazy load when requested\nconst lazyLoadWithGetter = (prop, getter) => {\n  const key = `${prop}`;\n\n  if (exports[key]) {\n    throw new Error(`Fatal error: Duplicate entry in nylas-exports: ${key}`)\n  }\n  Object.defineProperty(exports, prop, {\n    get: () => {\n      RequireCache[key] = RequireCache[key] || getter();\n      return RequireCache[key];\n    },\n    enumerable: true,\n  });\n}\n\nconst lazyLoad = (prop, path) => {\n  lazyLoadWithGetter(prop, () => resolveExport(require(`../${path}`)));\n};\n\nconst lazyLoadAndRegisterStore = (klassName, path) => {\n  lazyLoad(klassName, `flux/stores/${path}`);\n  StoreRegistry.register(klassName, () => exports[klassName]);\n}\n\nconst lazyLoadAndRegisterModel = (klassName, path) => {\n  lazyLoad(klassName, `flux/models/${path}`);\n  DatabaseObjectRegistry.register(klassName, () => exports[klassName]);\n};\n\nconst lazyLoadAndRegisterTask = (klassName, path) => {\n  lazyLoad(klassName, `flux/tasks/${path}`);\n  TaskRegistry.register(klassName, () => exports[klassName]);\n};\n\nconst lazyLoadDeprecated = (prop, path, {instead} = {}) => {\n  const {deprecate} = require('../deprecate-utils');\n  Object.defineProperty(exports, prop, {\n    get: deprecate(prop, instead, exports, () => {\n      return resolveExport(require(`../${path}`));\n    }),\n    enumerable: true,\n  });\n};\n\n// Actions\nlazyLoad(`Actions`, 'flux/actions');\n\n// API Endpoints\nlazyLoad(`NylasAPI`, 'flux/nylas-api');\nlazyLoad(`N1CloudAPI`, 'n1-cloud-api');\nlazyLoad(`NylasAPIHelpers`, 'flux/nylas-api-helpers');\nlazyLoad(`NylasAPIRequest`, 'flux/nylas-api-request');\nlazyLoad(`NylasLongConnection`, 'flux/nylas-long-connection');\n\n// The Database\nlazyLoad(`Matcher`, 'flux/attributes/matcher');\nlazyLoad(`DatabaseStore`, 'flux/stores/database-store');\nlazyLoad(`QueryResultSet`, 'flux/models/query-result-set');\nlazyLoad(`QuerySubscription`, 'flux/models/query-subscription');\nlazyLoad(`CalendarDataSource`, 'components/nylas-calendar/calendar-data-source');\nlazyLoad(`DatabaseWriter`, 'flux/stores/database-writer');\nlazyLoad(`MutableQueryResultSet`, 'flux/models/mutable-query-result-set');\nlazyLoad(`QuerySubscriptionPool`, 'flux/models/query-subscription-pool');\nlazyLoad(`ObservableListDataSource`, 'flux/stores/observable-list-data-source');\nlazyLoad(`MutableQuerySubscription`, 'flux/models/mutable-query-subscription');\n\n// Database Objects\nexports.DatabaseObjectRegistry = DatabaseObjectRegistry;\nlazyLoad(`Model`, 'flux/models/model');\nlazyLoad(`Attributes`, 'flux/attributes');\nlazyLoadAndRegisterModel(`File`, 'file');\nlazyLoadAndRegisterModel(`Event`, 'event');\nlazyLoadAndRegisterModel(`Label`, 'label');\nlazyLoadAndRegisterModel(`Folder`, 'folder');\nlazyLoadAndRegisterModel(`Thread`, 'thread');\nlazyLoadAndRegisterModel(`Account`, 'account');\nlazyLoadAndRegisterModel(`Message`, 'message');\nlazyLoadAndRegisterModel(`Contact`, 'contact');\nlazyLoadAndRegisterModel(`Category`, 'category');\nlazyLoadAndRegisterModel(`Calendar`, 'calendar');\nlazyLoadAndRegisterModel(`JSONBlob`, 'json-blob');\nlazyLoadAndRegisterModel(`ProviderSyncbackRequest`, 'provider-syncback-request');\n\n// Search Query Interfaces\nlazyLoad(`SearchQueryAST`, 'services/search/search-query-ast');\nlazyLoad(`SearchQueryParser`, 'services/search/search-query-parser');\nlazyLoad(`IMAPSearchQueryBackend`, 'services/search/search-query-backend-imap');\n\n// Tasks\nexports.TaskRegistry = TaskRegistry;\nlazyLoad(`Task`, 'flux/tasks/task');\nlazyLoad(`TaskFactory`, 'flux/tasks/task-factory');\nlazyLoadAndRegisterTask(`EventRSVPTask`, 'event-rsvp-task');\nlazyLoadAndRegisterTask(`BaseDraftTask`, 'base-draft-task');\nlazyLoadAndRegisterTask(`SendDraftTask`, 'send-draft-task');\nlazyLoadAndRegisterTask(`ChangeMailTask`, 'change-mail-task');\nlazyLoadAndRegisterTask(`DestroyDraftTask`, 'destroy-draft-task');\nlazyLoadAndRegisterTask(`ChangeLabelsTask`, 'change-labels-task');\nlazyLoadAndRegisterTask(`ChangeFolderTask`, 'change-folder-task');\nlazyLoadAndRegisterTask(`ChangeUnreadTask`, 'change-unread-task');\nlazyLoadAndRegisterTask(`DestroyModelTask`, 'destroy-model-task');\nlazyLoadAndRegisterTask(`ChangeStarredTask`, 'change-starred-task');\nlazyLoadAndRegisterTask(`SyncbackModelTask`, 'syncback-model-task');\nlazyLoadAndRegisterTask(`SyncbackEventTask`, 'syncback-event-task');\nlazyLoadAndRegisterTask(`DestroyCategoryTask`, 'destroy-category-task');\nlazyLoadAndRegisterTask(`SyncbackCategoryTask`, 'syncback-category-task');\nlazyLoadAndRegisterTask(`SyncbackMetadataTask`, 'syncback-metadata-task');\nlazyLoadAndRegisterTask(`PerformSendActionTask`, 'perform-send-action-task');\nlazyLoadAndRegisterTask(`ReprocessMailRulesTask`, 'reprocess-mail-rules-task');\nlazyLoadAndRegisterTask(`SendFeatureUsageEventTask`, 'send-feature-usage-event-task');\nlazyLoadAndRegisterTask(`EnsureMessageInSentFolderTask`, 'ensure-message-in-sent-folder-task');\n\n// Stores\n// These need to be required immediately since some Stores are\n// listen-only and not explicitly required from anywhere. Stores\n// currently set themselves up on require.\nlazyLoadAndRegisterStore(`TaskQueue`, 'task-queue');\nlazyLoadAndRegisterStore(`BadgeStore`, 'badge-store');\nlazyLoadAndRegisterStore(`DraftStore`, 'draft-store');\nlazyLoadAndRegisterStore(`ModalStore`, 'modal-store');\nlazyLoadAndRegisterStore(`OutboxStore`, 'outbox-store');\nlazyLoadAndRegisterStore(`PopoverStore`, 'popover-store');\nlazyLoadAndRegisterStore(`AccountStore`, 'account-store');\nlazyLoadAndRegisterStore(`SignatureStore`, 'signature-store');\nlazyLoadAndRegisterStore(`MessageStore`, 'message-store');\nlazyLoadAndRegisterStore(`ContactStore`, 'contact-store');\nlazyLoadAndRegisterStore(`IdentityStore`, 'identity-store');\nlazyLoadAndRegisterStore(`MetadataStore`, 'metadata-store');\nlazyLoadAndRegisterStore(`CategoryStore`, 'category-store');\nlazyLoadAndRegisterStore(`UndoRedoStore`, 'undo-redo-store');\nlazyLoadAndRegisterStore(`WorkspaceStore`, 'workspace-store');\nlazyLoadAndRegisterStore(`MailRulesStore`, 'mail-rules-store');\nlazyLoadAndRegisterStore(`FileUploadStore`, 'file-upload-store');\nlazyLoadAndRegisterStore(`SendActionsStore`, 'send-actions-store');\nlazyLoadAndRegisterStore(`FeatureUsageStore`, 'feature-usage-store');\nlazyLoadAndRegisterStore(`ThreadCountsStore`, 'thread-counts-store');\nlazyLoadAndRegisterStore(`FileDownloadStore`, 'file-download-store');\nlazyLoadAndRegisterStore(`OnlineStatusStore`, 'online-status-store');\nlazyLoadAndRegisterStore(`PreferencesUIStore`, 'preferences-ui-store');\nlazyLoadAndRegisterStore(`FocusedContentStore`, 'focused-content-store');\nlazyLoadAndRegisterStore(`MessageBodyProcessor`, 'message-body-processor');\nlazyLoadAndRegisterStore(`FocusedContactsStore`, 'focused-contacts-store');\nlazyLoadAndRegisterStore(`DeltaConnectionStore`, 'delta-connection-store');\nlazyLoadAndRegisterStore(`TaskQueueStatusStore`, 'task-queue-status-store');\nlazyLoadAndRegisterStore(`FolderSyncProgressStore`, 'folder-sync-progress-store');\nlazyLoadAndRegisterStore(`ThreadListActionsStore`, 'thread-list-actions-store');\nlazyLoadAndRegisterStore(`FocusedPerspectiveStore`, 'focused-perspective-store');\nlazyLoadAndRegisterStore(`SearchableComponentStore`, 'searchable-component-store');\nlazyLoad(`CustomContenteditableComponents`, 'components/overlaid-components/custom-contenteditable-components');\n\nlazyLoad(`ServiceRegistry`, `registries/service-registry`);\n\n// Decorators\nlazyLoad(`InflatesDraftClientId`, 'decorators/inflates-draft-client-id');\n\n// Extensions\nlazyLoad(`ExtensionRegistry`, 'registries/extension-registry');\nlazyLoad(`ComposerExtension`, 'extensions/composer-extension');\nlazyLoad(`MessageViewExtension`, 'extensions/message-view-extension');\nlazyLoad(`ContenteditableExtension`, 'extensions/contenteditable-extension');\n\n// 3rd party libraries\nlazyLoadWithGetter(`Rx`, () => require('rx-lite'));\nlazyLoadWithGetter(`React`, () => require('react'));\nlazyLoadWithGetter(`Reflux`, () => require('reflux'));\nlazyLoadWithGetter(`ReactDOM`, () => require('react-dom'));\nlazyLoadWithGetter(`ReactTestUtils`, () => require('react-addons-test-utils'));\n\n// React Components\nlazyLoad(`ComponentRegistry`, 'registries/component-registry');\nlazyLoad(`PriorityUICoordinator`, 'priority-ui-coordinator');\n\n// Utils\nlazyLoad(`Utils`, 'flux/models/utils');\nlazyLoad(`DOMUtils`, 'dom-utils');\nlazyLoad(`DateUtils`, 'date-utils');\nlazyLoad(`FsUtils`, 'fs-utils');\nlazyLoad(`CanvasUtils`, 'canvas-utils');\nlazyLoad(`RegExpUtils`, 'regexp-utils');\nlazyLoad(`MenuHelpers`, 'menu-helpers');\nlazyLoad(`DeprecateUtils`, 'deprecate-utils');\nlazyLoad(`VirtualDOMUtils`, 'virtual-dom-utils');\nlazyLoad(`Spellchecker`, 'spellchecker');\nlazyLoad(`DraftHelpers`, 'flux/stores/draft-helpers');\nlazyLoad(`MessageUtils`, 'flux/models/message-utils');\nlazyLoad(`EditorAPI`, 'components/contenteditable/editor-api');\n\n// Services\nlazyLoad(`KeyManager`, 'key-manager');\nlazyLoad(`SoundRegistry`, 'registries/sound-registry');\nlazyLoad(`MailRulesTemplates`, 'mail-rules-templates');\nlazyLoad(`MailRulesProcessor`, 'mail-rules-processor');\nlazyLoad(`MailboxPerspective`, 'mailbox-perspective');\nlazyLoad(`DeltaProcessor`, 'services/delta-processor');\nlazyLoad(`NativeNotifications`, 'native-notifications');\nlazyLoad(`ModelSearchIndexer`, 'services/model-search-indexer');\nlazyLoad(`SearchIndexScheduler`, 'services/search-index-scheduler');\nlazyLoad(`SanitizeTransformer`, 'services/sanitize-transformer');\nlazyLoad(`QuotedHTMLTransformer`, 'services/quoted-html-transformer');\nlazyLoad(`InlineStyleTransformer`, 'services/inline-style-transformer');\nlazyLoad(`SearchableComponentMaker`, 'searchable-components/searchable-component-maker');\nlazyLoad(`QuotedPlainTextTransformer`, 'services/quoted-plain-text-transformer');\nlazyLoad(`BatteryStatusManager`, 'services/battery-status-manager');\n\n// Errors\nlazyLoadWithGetter(`APIError`, () => require('../flux/errors').APIError);\n\n// Process Internals\nlazyLoad(`DefaultClientHelper`, 'default-client-helper');\nlazyLoad(`BufferedProcess`, 'buffered-process');\nlazyLoad(`SystemStartService`, 'system-start-service');\nlazyLoadWithGetter(`APMWrapper`, () => require('../apm-wrapper'));\n\n// Testing\nlazyLoadWithGetter(`NylasTestUtils`, () => require('../../spec/nylas-test-utils'));\n\n// Deprecated\nlazyLoadDeprecated(`QuotedHTMLParser`, 'services/quoted-html-transformer', {\n  instead: 'QuotedHTMLTransformer',\n});\nlazyLoadDeprecated(`DraftStoreExtension`, 'flux/stores/draft-store-extension', {\n  instead: 'ComposerExtension',\n});\nlazyLoadDeprecated(`MessageStoreExtension`, 'flux/stores/message-store-extension', {\n  instead: 'MessageViewExtension',\n});\n"
  },
  {
    "path": "packages/client-app/src/global/nylas-observables.coffee",
    "content": "Rx = require 'rx-lite'\n_ = require 'underscore'\nCategory = require('../flux/models/category').default\nQuerySubscriptionPool = require('../flux/models/query-subscription-pool').default\nDatabaseStore = require('../flux/stores/database-store').default\n\nCategoryOperators =\n  sort: ->\n    obs = @.map (categories) ->\n      return categories.sort (catA, catB) ->\n        nameA = catA.displayName\n        nameB = catB.displayName\n\n        # Categories that begin with [, like [Mailbox]/For Later\n        # should appear at the bottom, because they're likely autogenerated.\n        nameA = \"ZZZ\"+nameA if nameA[0] is '['\n        nameB = \"ZZZ\"+nameB if nameB[0] is '['\n\n        nameA.localeCompare(nameB)\n    _.extend(obs, CategoryOperators)\n\n  categoryFilter: (filter) ->\n    obs = @.map (categories) ->\n      return categories.filter filter\n    _.extend(obs, CategoryOperators)\n\nCategoryObservables =\n\n  forAllAccounts: =>\n    observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category))\n    _.extend(observable, CategoryOperators)\n    observable\n\n  forAccount: (account) =>\n    if account\n      observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category).where(accountId: account.id))\n    else\n      observable = Rx.Observable.fromQuery(DatabaseStore.findAll(Category))\n    _.extend(observable, CategoryOperators)\n    observable\n\n  standard: (account) =>\n    observable = Rx.Observable.fromConfig('core.workspace.showImportant')\n      .flatMapLatest (showImportant) =>\n        return CategoryObservables.forAccount(account).sort()\n          .categoryFilter (cat) -> cat.isStandardCategory(showImportant)\n    _.extend(observable, CategoryOperators)\n    observable\n\n  user: (account) =>\n    CategoryObservables.forAccount(account).sort()\n      .categoryFilter (cat) -> cat.isUserCategory()\n\n  hidden: (account) =>\n    CategoryObservables.forAccount(account).sort()\n      .categoryFilter (cat) -> cat.isHiddenCategory()\n\nmodule.exports =\n  Categories: CategoryObservables\n\n# Attach a few global helpers\n\nRx.Observable.fromStore = (store) =>\n  return Rx.Observable.create (observer) =>\n    unsubscribe = store.listen =>\n      observer.onNext(store)\n    observer.onNext(store)\n    return Rx.Disposable.create(unsubscribe)\n\n# Takes a store that provides an {ObservableListDataSource} via `dataSource()`\n# Returns an observable that provides array of selected items on subscription\nRx.Observable.fromListSelection = (originStore) =>\n  return Rx.Observable.create((observer) =>\n    dataSourceDisposable = null\n    storeObservable = Rx.Observable.fromStore(originStore)\n\n    disposable = storeObservable.subscribe( =>\n      dataSource = originStore.dataSource()\n      dataSourceObservable = Rx.Observable.fromStore(dataSource)\n\n      if dataSourceDisposable\n        dataSourceDisposable.dispose()\n\n      dataSourceDisposable = dataSourceObservable.subscribe( =>\n        observer.onNext(dataSource.selection.items())\n      )\n      return\n    )\n    dispose = =>\n      if dataSourceDisposable\n        dataSourceDisposable.dispose()\n      disposable.dispose()\n    return Rx.Disposable.create(dispose)\n  )\n\nRx.Observable.fromConfig = (configKey) =>\n  return Rx.Observable.create (observer) =>\n    disposable = NylasEnv.config.onDidChange configKey, =>\n      observer.onNext(NylasEnv.config.get(configKey))\n    observer.onNext(NylasEnv.config.get(configKey))\n    return Rx.Disposable.create(disposable.dispose)\n\nRx.Observable.fromAction = (action) =>\n  return Rx.Observable.create (observer) =>\n    unsubscribe = action.listen (args...) =>\n      observer.onNext(args...)\n    return Rx.Disposable.create(unsubscribe)\n\nRx.Observable.fromQuery = (query) =>\n  return Rx.Observable.create (observer) =>\n    unsubscribe = QuerySubscriptionPool.add query, (result) =>\n      observer.onNext(result)\n    return Rx.Disposable.create(unsubscribe)\n\nRx.Observable.fromNamedQuerySubscription = (name, subscription) =>\n  return Rx.Observable.create (observer) =>\n    unsubscribe = QuerySubscriptionPool.addPrivateSubscription name, subscription, (result) =>\n      observer.onNext(result)\n    return Rx.Disposable.create(unsubscribe)\n"
  },
  {
    "path": "packages/client-app/src/global/nylas-store.coffee",
    "content": "{Listener, Publisher} = require '../flux/modules/reflux-coffee'\nCoffeeHelpers = require '../flux/coffee-helpers'\n\n# A simple Flux implementation\nclass NylasStore\n  @include: CoffeeHelpers.includeModule\n\n  @include Publisher\n  @include Listener\n\nmodule.exports = NylasStore\n"
  },
  {
    "path": "packages/client-app/src/key-manager.es6",
    "content": "import {remote} from 'electron'\nimport keytar from 'keytar'\n\n/**\n * A basic wrap around keytar's secure key management. Consolidates all of\n * our keys under a single namespaced keymap and provides migration\n * support.\n *\n * Consolidating this prevents a ton of key authorization popups for each\n * and every key we want to access.\n */\nclass KeyManager {\n  constructor() {\n    /**\n     * NOTE: Old N1 includes a migration system that manually looks for\n     * the names of these keys. If you change them be sure that old N1 is\n     * fully deprecated or updated as well.\n     */\n    this.SERVICE_NAME = \"Nylas Mail\";\n    if (NylasEnv.inDevMode()) {\n      this.SERVICE_NAME = \"Nylas Mail Dev\";\n    }\n    this.KEY_NAME = \"Nylas Mail Keys\"\n    this._alreadyMigrated = new Set()\n  }\n\n  replacePassword(keyName, newVal) {\n    this._try(() => {\n      const keys = this._getKeyHash();\n      keys[keyName] = newVal;\n      return keytar.replacePassword(this.SERVICE_NAME, this.KEY_NAME, JSON.stringify(keys))\n    })\n  }\n\n  deletePassword(keyName) {\n    this._try(() => {\n      const keys = this._getKeyHash();\n      delete keys[keyName];\n      return keytar.replacePassword(this.SERVICE_NAME, this.KEY_NAME, JSON.stringify(keys))\n    })\n  }\n\n  getPassword(keyName, {migrateFromService} = {}) {\n    const keys = this._getKeyHash();\n    if (!keys[keyName] && migrateFromService &&\n        !this._alreadyMigrated.has(migrateFromService)) {\n      const oldVal = keytar.getPassword(migrateFromService, keyName);\n      if (oldVal) {\n        this.replacePassword(keyName, oldVal)\n        this._alreadyMigrated.add(migrateFromService)\n      }\n    }\n    return keys[keyName]\n  }\n\n  _getKeyHash() {\n    const raw = keytar.getPassword(this.SERVICE_NAME, this.KEY_NAME) || \"{}\";\n    try {\n      return JSON.parse(raw)\n    } catch (err) {\n      return {}\n    }\n  }\n\n  _try(fn) {\n    const ERR_MSG = \"We couldn't store your password securely! For more information, visit https://support.nylas.com/hc/en-us/articles/223790028\";\n    try {\n      if (!fn()) {\n        remote.dialog.showErrorBox(\"Password Management Error\", ERR_MSG)\n        NylasEnv.reportError(new Error(`Password Management Error: ${ERR_MSG}`))\n      }\n    } catch (err) {\n      remote.dialog.showErrorBox(\"Password Management Error\", ERR_MSG)\n      NylasEnv.reportError(err)\n    }\n  }\n}\nexport default new KeyManager();\n"
  },
  {
    "path": "packages/client-app/src/keymap-manager.es6",
    "content": "import fs from 'fs-plus'\nimport path from 'path'\nimport mousetrap from 'mousetrap'\nimport {ipcRenderer} from 'electron'\nimport {Emitter, Disposable} from 'event-kit'\n\nlet suspended = false\nconst templateConfigKey = 'core.keymapTemplate'\n\n/*\nBy default, Mousetrap stops all hotkeys within text inputs. Override this to\nmore specifically block only hotkeys that have no modifier keys (things like\nGmail's \"x\", while allowing standard hotkeys.)\n*/\nmousetrap.prototype.stopCallback = (e, element, combo) => {\n  if (suspended) {\n    return true;\n  }\n  const withinTextInput = element.tagName === 'INPUT' || element.tagName === 'SELECT' || element.tagName === 'TEXTAREA' || element.isContentEditable\n  const withinWebview = element.tagName === 'WEBVIEW';\n  if (withinWebview) {\n    return true;\n  }\n  if (withinTextInput) {\n    const isPlainKey = !/(mod|command|ctrl)/.test(combo);\n    const isReservedTextEditingShortcut = /(mod|command|ctrl)\\+(a|x|c|v)/.test(combo);\n    return isPlainKey || isReservedTextEditingShortcut;\n  }\n  return false;\n}\n\nclass KeymapFile {\n  constructor(manager, filePath) {\n    this._manager = manager;\n    this._path = filePath;\n    this._bindings = {};\n    this._disposable = null;\n  }\n\n  load = () => {\n    let keymaps = null;\n    try {\n      keymaps = JSON.parse(fs.readFileSync(this._path))\n    } catch (e) {\n      if (e.code === 'ENOENT') {\n        return;\n      }\n      console.error(e);\n      return;\n    }\n\n    this._bindings = {};\n    Object.keys(keymaps).forEach((command) => {\n      let keystrokesArray = keymaps[command];\n      if (!(keystrokesArray instanceof Array)) {\n        keystrokesArray = [keystrokesArray];\n      }\n      for (const keystrokes of keystrokesArray) {\n        this._manager.ensureKeystrokesRegistered(keystrokes);\n        this._bindings[command] = this._bindings[command] || [];\n        this._bindings[command].push(keystrokes);\n      }\n    });\n    this._manager.keymapCacheInvalidated();\n  }\n\n  watch() {\n    fs.watch(this._path, this.load);\n  }\n\n  bindings() {\n    return this._bindings;\n  }\n}\n\nexport default class KeymapManager {\n\n  constructor({configDirPath, resourcePath}) {\n    this.configDirPath = configDirPath;\n    this.resourcePath = resourcePath;\n    this._emitter = new Emitter();\n    this._registered = {};\n    this._files = [];\n  }\n\n  getUserKeymapPath() {\n    return path.join(this.configDirPath, 'keymap.json');\n  }\n\n  suspendAllKeymaps() {\n    NylasEnv.menu.sendToBrowserProcess(NylasEnv.menu.template, {});\n    suspended = true;\n  }\n\n  resumeAllKeymaps() {\n    NylasEnv.menu.update();\n    suspended = false;\n  }\n\n  loadKeymaps = () => {\n    // Load the base keymap and the base.platform keymap\n    this.loadKeymap(path.join(this.resourcePath, 'keymaps', 'base.json'))\n    this.loadKeymap(path.join(this.resourcePath, 'keymaps', `base-${process.platform}.json`))\n\n    // Load the template keymap (Gmail, Mail.app, etc.) the user has chosen\n    if (this._unobserveTemplate) {\n      this._unobserveTemplate.dispose();\n    }\n    this._unobserveTemplate = NylasEnv.config.observe(templateConfigKey, this.loadTemplateKeymap);\n\n    const userKeymapPath = this.getUserKeymapPath()\n    if (!fs.existsSync(userKeymapPath)) {\n      fs.writeFileSync(userKeymapPath, \"{}\");\n    }\n    this.userKeymap = new KeymapFile(this, userKeymapPath)\n    this.userKeymap.load()\n    this.userKeymap.watch()\n  }\n\n  loadTemplateKeymap = () => {\n    if (this._removeTemplate) {\n      this._removeTemplate.dispose();\n    }\n    let templateFile = NylasEnv.config.get(templateConfigKey);\n    if (templateFile) {\n      templateFile = templateFile.replace(\"GoogleInbox\", \"Inbox by Gmail\");\n      const templateKeymapPath = path.join(this.resourcePath, 'keymaps', 'templates', `${templateFile}.json`);\n      this._removeTemplate = this.loadKeymap(templateKeymapPath);\n    }\n  }\n\n  loadKeymap(filePath) {\n    const file = new KeymapFile(this, filePath);\n    this._files.push(file);\n    file.load();\n\n    return new Disposable(() => {\n      this._files = this._files.filter(f => f !== file);\n      this.keymapCacheInvalidated();\n    });\n  }\n\n  ensureKeystrokesRegistered(keystrokes) {\n    if (this._registered[keystrokes]) {\n      return;\n    }\n    this._registered[keystrokes] = true;\n\n    mousetrap.bind(keystrokes, () => {\n      for (const command of (this._commandsCache[keystrokes] || [])) {\n        if (command.startsWith('application:')) {\n          ipcRenderer.send('command', command);\n        } else {\n          NylasEnv.commands.dispatch(command);\n        }\n      }\n      return false\n    });\n  }\n\n  keymapCacheInvalidated() {\n    this._bindingsCache = {};\n\n    for (const file of this._files) {\n      const fileBindings = file.bindings();\n      for (const command of Object.keys(fileBindings)) {\n        const keystrokesArray = fileBindings[command];\n        this._bindingsCache[command] = (this._bindingsCache[command] || []).concat(keystrokesArray);\n      }\n    }\n    if (this.userKeymap) {\n      const userBindings = this.userKeymap.bindings();\n      for (const command of Object.keys(userBindings)) {\n        this._bindingsCache[command] = userBindings[command];\n      }\n    }\n\n    this._commandsCache = {};\n    for (const command of Object.keys(this._bindingsCache)) {\n      for (const keystrokes of this._bindingsCache[command]) {\n        if (!this._commandsCache[keystrokes]) {\n          this._commandsCache[keystrokes] = [];\n        }\n        if (!this._commandsCache[keystrokes].includes(command)) {\n          this._commandsCache[keystrokes].push(command);\n        }\n      }\n    }\n\n    this._emitter.emit('on-did-reload-keymap');\n  }\n\n  onDidReloadKeymap = (callback) => {\n    return this._emitter.on('on-did-reload-keymap', callback);\n  }\n\n  getBindingsForAllCommands() {\n    return this._bindingsCache;\n  }\n\n  getBindingsForCommand(command) {\n    return this._bindingsCache[command] || [];\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/less-compile-cache.es6",
    "content": "import _ from 'underscore';\nimport path from 'path';\nimport LessCache from 'less-cache';\n\n// {LessCache} wrapper used by {ThemeManager} to read stylesheets.\nexport default class LessCompileCache {\n  constructor({configDirPath, resourcePath, importPaths}) {\n    this.lessSearchPaths = [\n      path.join(resourcePath, 'static', 'variables'),\n      path.join(resourcePath, 'static'),\n    ];\n\n    let allImportPaths = importPaths;\n    if (allImportPaths != null) {\n      allImportPaths = importPaths.concat(this.lessSearchPaths);\n    } else {\n      allImportPaths = this.lessSearchPaths;\n    }\n\n    this.cache = new LessCache({\n      cacheDir: path.join(configDirPath, 'compile-cache', 'less'),\n      fallbackDir: path.join(resourcePath, 'less-compile-cache'),\n      importPaths: allImportPaths,\n      resourcePath: resourcePath,\n    });\n  }\n\n  // Setting the import paths is a VERY expensive operation (200ms +)\n  // because it walks the entire file tree and does a file state for each\n  // and every importPath. If we already have the imports, then load it\n  // from our backend FileListCache.\n  setImportPaths(importPaths = []) {\n    const fullImportPaths = importPaths.concat(this.lessSearchPaths);\n    if (!_.isEqual(fullImportPaths, this.cache.importPaths)) {\n      try {\n        this.cache.setImportPaths(fullImportPaths);\n      } catch (err) {\n        // We occasionally see ENOENT: no such file or directory errors\n        // when trying to access the cache on boot. See\n        // https://phab.nylas.com/T8128\n        console.error(err)\n      }\n    }\n  }\n\n  read(stylesheetPath) {\n    return this.cache.readFileSync(stylesheetPath);\n  }\n\n  cssForFile(stylesheetPath, lessContent) {\n    return this.cache.cssForFile(stylesheetPath, lessContent);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/mail-rules-processor.coffee",
    "content": "_ = require 'underscore'\n\nTask = require('./flux/tasks/task').default\nActions = require('./flux/actions').default\nCategory = require('./flux/models/category').default\nThread = require('./flux/models/thread').default\nMessage = require('./flux/models/message').default\nAccountStore = require('./flux/stores/account-store').default\nDatabaseStore = require('./flux/stores/database-store').default\nTaskQueueStatusStore = require './flux/stores/task-queue-status-store'\n\n{ConditionMode, ConditionTemplates} = require './mail-rules-templates'\n\nChangeUnreadTask = require('./flux/tasks/change-unread-task').default\nChangeFolderTask = require('./flux/tasks/change-folder-task').default\nChangeStarredTask = require('./flux/tasks/change-starred-task').default\nChangeLabelsTask = require('./flux/tasks/change-labels-task').default\nMailRulesStore = null\n\n###\nNote: At first glance, it seems like these task factory methods should use the\nTaskFactory. Unfortunately, the TaskFactory uses the CategoryStore and other\ninformation about the current view. Maybe after the unified inbox refactor...\n###\nMailRulesActions =\n  markAsImportant: (message, thread) ->\n    DatabaseStore.findBy(Category, {\n      name: 'important',\n      accountId: thread.accountId\n    }).then (important) ->\n      return Promise.reject(new Error(\"Could not find `important` label\")) unless important\n      return new ChangeLabelsTask(labelsToAdd: [important], threads: [thread.id], source: \"Mail Rules\")\n\n  moveToTrash: (message, thread) ->\n    if AccountStore.accountForId(thread.accountId).usesLabels()\n      return MailRulesActions.moveToLabel(message, thread, 'trash')\n    else\n      DatabaseStore.findBy(Category, { name: 'trash', accountId: thread.accountId }).then (folder) ->\n        return Promise.reject(new Error(\"The folder could not be found.\")) unless folder\n        return new ChangeFolderTask(folder: folder, threads: [thread.id], source: \"Mail Rules\")\n\n  markAsRead: (message, thread) ->\n    new ChangeUnreadTask(unread: false, threads: [thread.id], source: \"Mail Rules\")\n\n  star: (message, thread) ->\n    new ChangeStarredTask(starred: true, threads: [thread.id], source: \"Mail Rules\")\n\n  changeFolder: (message, thread, value) ->\n    return Promise.reject(new Error(\"A folder is required.\")) unless value\n    DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId }).then (folder) ->\n      return Promise.reject(new Error(\"The folder could not be found.\")) unless folder\n      return new ChangeFolderTask(folder: folder, threads: [thread.id], source: \"Mail Rules\")\n\n  applyLabel: (message, thread, value) ->\n    return Promise.reject(new Error(\"A label is required.\")) unless value\n    DatabaseStore.findBy(Category, { id: value, accountId: thread.accountId }).then (label) ->\n      return Promise.reject(new Error(\"The label could not be found.\")) unless label\n      return new ChangeLabelsTask(labelsToAdd: [label], threads: [thread.id], source: \"Mail Rules\")\n\n  # Should really be moveToArchive but stuck with legacy name\n  applyLabelArchive: (message, thread) ->\n    return MailRulesActions.moveToLabel(message, thread, 'all')\n\n  moveToLabel: (message, thread, nameOrId) ->\n    return Promise.reject(new Error(\"A label is required.\")) unless nameOrId\n\n    Promise.props(\n      withId: DatabaseStore.findBy(Category, { id: nameOrId, accountId: thread.accountId })\n      withName: DatabaseStore.findBy(Category, { name: nameOrId, accountId: thread.accountId })\n    ).then ({withId, withName}) ->\n      label = withId || withName\n      return Promise.reject(new Error(\"The label could not be found.\")) unless label\n      return new ChangeLabelsTask({\n        source: \"Mail Rules\"\n        labelsToRemove: [].concat(thread.labels).filter((l) =>\n          !l.isLockedCategory() and l.id isnt label.id\n        ),\n        labelsToAdd: [label],\n        threads: [thread.id]\n      })\n\n\nclass MailRulesProcessor\n  constructor: ->\n\n  processMessages: (messages) =>\n    MailRulesStore ?= require './flux/stores/mail-rules-store'\n    return Promise.resolve() unless messages.length > 0\n\n    enabledRules = MailRulesStore.rules().filter (r) -> not r.disabled\n\n    # When messages arrive, we process all the messages in parallel, but one\n    # rule at a time. This is important, because users can order rules which\n    # may do and undo a change. Ie: \"Star if from Ben, Unstar if subject is \"Bla\"\n    return Promise.each enabledRules, (rule) =>\n      matching = messages.filter (message) =>\n        @_checkRuleForMessage(rule, message)\n\n      # Rules are declared at the message level, but actions are applied to\n      # threads. To ensure we don't apply the same action 50x on the same thread,\n      # just process one match per thread.\n      matching = _.uniq matching, false, (message) ->\n        message.threadId\n\n      return Promise.map matching, (message) =>\n        # We always pull the thread from the database, even though it may be in\n        # `incoming.thread`, because rules may be modifying it as they run!\n        DatabaseStore.find(Thread, message.threadId).then (thread) =>\n          return console.warn(\"Cannot find thread #{message.threadId} to process mail rules.\") unless thread\n          return @_applyRuleToMessage(rule, message, thread)\n\n  _checkRuleForMessage: (rule, message) =>\n    if rule.conditionMode is ConditionMode.All\n      fn = _.every\n    else\n      fn = _.any\n\n    return false unless message.accountId is rule.accountId\n\n    fn rule.conditions, (condition) =>\n      template = _.findWhere(ConditionTemplates, {key: condition.templateKey})\n      value = template.valueForMessage(message)\n      template.evaluate(condition, value)\n\n  _applyRuleToMessage: (rule, message, thread) =>\n    actionPromises = rule.actions.map (action) =>\n      actionFunction = MailRulesActions[action.templateKey]\n      if not actionFunction\n        return Promise.reject(new Error(\"#{action.templateKey} is not a supported action.\"))\n      return actionFunction(message, thread, action.value)\n\n    Promise.all(actionPromises).then (actionResults) ->\n      performLocalPromises = []\n\n      actionTasks = actionResults.filter (r) -> r instanceof Task\n      actionTasks.forEach (task) ->\n        performLocalPromises.push TaskQueueStatusStore.waitForPerformLocal(task)\n        Actions.queueTask(task)\n\n      return Promise.all(performLocalPromises)\n\n    .catch (err) ->\n      # Errors can occur if a mail rule specifies an invalid label or folder, etc.\n      # Disable the rule. Disable the mail rule so the failure is reflected in the\n      # interface.\n      Actions.disableMailRule(rule.id, err.toString())\n      return Promise.resolve()\n\nmodule.exports = new MailRulesProcessor\n"
  },
  {
    "path": "packages/client-app/src/mail-rules-templates.coffee",
    "content": "NylasObservables = require 'nylas-observables'\n{Template} = require './components/scenario-editor-models'\n\nConditionTemplates = [\n  new Template('from', Template.Type.String, {\n    name: 'From',\n    valueForMessage: (message) ->\n      [].concat(message.from.map((c) -> c.email), message.from.map((c) -> c.name))\n  })\n\n  new Template('to', Template.Type.String, {\n    name: 'To',\n    valueForMessage: (message) ->\n      [].concat(message.to.map((c) -> c.email), message.to.map((c) -> c.name))\n  })\n\n  new Template('cc', Template.Type.String, {\n    name: 'Cc',\n    valueForMessage: (message) ->\n      [].concat(message.cc.map((c) -> c.email), message.cc.map((c) -> c.name))\n  })\n\n  new Template('bcc', Template.Type.String, {\n    name: 'Bcc',\n    valueForMessage: (message) ->\n      [].concat(message.bcc.map((c) -> c.email), message.bcc.map((c) -> c.name))\n  })\n\n  new Template('anyRecipient', Template.Type.String, {\n    name: 'Any Recipient',\n    valueForMessage: (message) ->\n      recipients = [].concat(message.to, message.cc, message.bcc, message.from)\n      [].concat(recipients.map((c) -> c.email), recipients.map((c) -> c.name))\n  })\n\n  new Template('anyAttachmentName', Template.Type.String, {\n    name: 'Any attachment name',\n    valueForMessage: (message) ->\n      message.files.map((f) -> f.filename)\n  })\n\n  new Template('starred', Template.Type.Enum, {\n    name: 'Starred',\n    values: [{name: 'True', value: 'true'}, {name: 'False', value: 'false'}]\n    valueLabel: 'is:'\n    valueForMessage: (message) ->\n      if message.starred then return 'true' else return 'false'\n  })\n\n  new Template('subject', Template.Type.String, {\n    name: 'Subject',\n    valueForMessage: (message) ->\n      message.subject\n  })\n\n  new Template('body', Template.Type.String, {\n    name: 'Body',\n    valueForMessage: (message) ->\n      message.body\n  })\n]\n\nActionTemplates = [\n  new Template('markAsRead', Template.Type.None, {name: 'Mark as Read'})\n  new Template('moveToTrash', Template.Type.None, {name: 'Move to Trash'})\n  new Template('star', Template.Type.None, {name: 'Star'})\n]\n\n\nmodule.exports =\n  ConditionMode:\n    Any: 'any'\n    All: 'all'\n\n  ConditionTemplates: ConditionTemplates\n\n  ConditionTemplatesForAccount: (account) ->\n    return [] unless account\n    return ConditionTemplates\n\n  ActionTemplates: ActionTemplates\n\n  ActionTemplatesForAccount: (account) ->\n    return [] unless account\n\n    templates = [].concat(ActionTemplates)\n\n    CategoryNamesObservable = NylasObservables.Categories\n      .forAccount(account)\n      .sort()\n      .map (cats) ->\n        cats.filter (cat) -> not cat.isLockedCategory()\n      .map (cats) ->\n        cats.map (cat) ->\n          name: cat.displayName || cat.name\n          value: cat.id\n\n    if account.usesLabels()\n      templates.unshift new Template('markAsImportant', Template.Type.None, {\n        name: 'Mark as Important'\n      })\n      templates.unshift new Template('applyLabelArchive', Template.Type.None, {\n        name: 'Archive'\n      })\n      templates.unshift new Template('applyLabel', Template.Type.Enum, {\n        name: 'Apply Label'\n        values: CategoryNamesObservable\n      })\n      templates.unshift new Template('moveToLabel', Template.Type.Enum, {\n        name: 'Move to Label'\n        values: CategoryNamesObservable\n      })\n\n    else\n      templates.push new Template('changeFolder', Template.Type.Enum, {\n        name: 'Move Message'\n        valueLabel: 'to folder:'\n        values: CategoryNamesObservable\n      })\n\n    templates\n"
  },
  {
    "path": "packages/client-app/src/mailbox-perspective.coffee",
    "content": "_ = require 'underscore'\n\nUtils = require './flux/models/utils'\nTaskFactory = require('./flux/tasks/task-factory').default\nAccountStore = require('./flux/stores/account-store').default\nCategoryStore = require './flux/stores/category-store'\nDatabaseStore = require('./flux/stores/database-store').default\nOutboxStore = require('./flux/stores/outbox-store').default\nThreadCountsStore = require './flux/stores/thread-counts-store'\nRecentlyReadStore = require('./flux/stores/recently-read-store').default\nMutableQuerySubscription = require('./flux/models/mutable-query-subscription').default\nUnreadQuerySubscription = require('./flux/models/unread-query-subscription').default\nMatcher = require('./flux/attributes/matcher').default\nThread = require('./flux/models/thread').default\nCategory = require('./flux/models/category').default\nActions = require('./flux/actions').default\nChangeUnreadTask = null\n\n# This is a class cluster. Subclasses are not for external use!\n# https://developer.apple.com/library/ios/documentation/General/Conceptual/CocoaEncyclopedia/ClassClusters/ClassClusters.html\n\n\nclass MailboxPerspective\n\n  # Factory Methods\n  @forNothing: ->\n    new EmptyMailboxPerspective()\n\n  @forDrafts: (accountsOrIds) ->\n    new DraftsMailboxPerspective(accountsOrIds)\n\n  @forCategory: (category) ->\n    return @forNothing() unless category\n    new CategoryMailboxPerspective([category])\n\n  @forCategories: (categories) ->\n    return @forNothing() if categories.length is 0\n    new CategoryMailboxPerspective(categories)\n\n  @forStandardCategories: (accountsOrIds, names...) ->\n    # TODO this method is broken\n    categories = CategoryStore.getStandardCategories(accountsOrIds, names...)\n    @forCategories(categories)\n\n  @forStarred: (accountsOrIds) ->\n    new StarredMailboxPerspective(accountsOrIds)\n\n  @forUnread: (categories) ->\n    return @forNothing() if categories.length is 0\n    new UnreadMailboxPerspective(categories)\n\n  @forInbox: (accountsOrIds) =>\n    @forStandardCategories(accountsOrIds, 'inbox')\n\n  @fromJSON: (json) =>\n    try\n      if json.type is CategoryMailboxPerspective.name\n        categories = JSON.parse(json.serializedCategories, Utils.registeredObjectReviver)\n        return @forCategories(categories)\n      else if json.type is UnreadMailboxPerspective.name\n        categories = JSON.parse(json.serializedCategories, Utils.registeredObjectReviver)\n        return @forUnread(categories)\n      else if json.type is StarredMailboxPerspective.name\n        return @forStarred(json.accountIds)\n      else if json.type is DraftsMailboxPerspective.name\n        return @forDrafts(json.accountIds)\n      else\n        return @forInbox(json.accountIds)\n    catch error\n      NylasEnv.reportError(new Error(\"Could not restore mailbox perspective: #{error}\"))\n      return null\n\n  # Instance Methods\n\n  constructor: (@accountIds) ->\n    unless @accountIds instanceof Array and _.every(@accountIds, (aid) =>\n      (typeof aid is 'string') or (typeof aid is 'number')\n    )\n      throw new Error(\"#{@constructor.name}: You must provide an array of string `accountIds`\")\n    @\n\n  toJSON: =>\n    return {accountIds: @accountIds, type: @constructor.name}\n\n  isEqual: (other) =>\n    return false unless other and @constructor is other.constructor\n    return false unless other.name is @name\n    return false unless _.isEqual(@accountIds, other.accountIds)\n    true\n\n  isInbox: =>\n    @categoriesSharedName() is 'inbox'\n\n  isSent: =>\n    @categoriesSharedName() is 'sent'\n\n  isTrash: =>\n    @categoriesSharedName() is 'trash'\n\n  isSpam: =>\n    @categoriesSharedName() is 'spam'\n\n  isArchive: =>\n    false\n\n  emptyMessage: =>\n    \"No Messages\"\n\n  categories: =>\n    []\n\n  # overwritten in CategoryMailboxPerspective\n  hasSyncingCategories: =>\n    false\n\n  categoriesSharedName: =>\n    @_categoriesSharedName ?= Category.categoriesSharedName(@categories())\n    @_categoriesSharedName\n\n  category: =>\n    return null unless @categories().length is 1\n    return @categories()[0]\n\n  threads: =>\n    throw new Error(\"threads: Not implemented in base class.\")\n\n  unreadCount: =>\n    0\n\n  # Public:\n  # - accountIds {Array} Array of unique account ids associated with the threads\n  # that want to be included in this perspective\n  #\n  # Returns true if the accountIds are part of the current ids, or false\n  # otherwise. This means that it checks if I am attempting to move threads\n  # between the same set of accounts:\n  #\n  # E.g.:\n  # perpective = Starred for accountIds: a1, a2\n  # thread1 has accountId a3\n  # thread2 has accountId a2\n  #\n  # perspective.canReceiveThreadsFromAccountIds([a2, a3]) -> false -> I cant move those threads to Starred\n  # perspective.canReceiveThreadsFromAccountIds([a2]) -> true -> I can move that thread to # Starred\n  canReceiveThreadsFromAccountIds: (accountIds) =>\n    return false unless accountIds and accountIds.length > 0\n    areIncomingIdsInCurrent = _.difference(accountIds, @accountIds).length is 0\n    return areIncomingIdsInCurrent\n\n  receiveThreads: (threadIds) =>\n    throw new Error(\"receiveThreads: Not implemented in base class.\")\n\n  canArchiveThreads: (threads) =>\n    return false if @isArchive()\n    accounts = AccountStore.accountsForItems(threads)\n    return _.every(accounts, (acc) -> acc.canArchiveThreads())\n\n  canTrashThreads: (threads) =>\n    @canMoveThreadsTo(threads, 'trash')\n\n  canMoveThreadsTo: (threads, standardCategoryName) =>\n    return false if @categoriesSharedName() is standardCategoryName\n    return _.every AccountStore.accountsForItems(threads), (acc) ->\n      CategoryStore.getStandardCategory(acc, standardCategoryName)?\n\n  tasksForRemovingItems: (threads) =>\n    if not threads instanceof Array\n      throw new Error(\"tasksForRemovingItems: you must pass an array of threads or thread ids\")\n    []\n\n\nclass DraftsMailboxPerspective extends MailboxPerspective\n  constructor: (@accountIds) ->\n    super(@accountIds)\n    @name = \"Drafts\"\n    @iconName = \"drafts.png\"\n    @drafts = true # The DraftListStore looks for this\n    @\n\n  threads: =>\n    null\n\n  unreadCount: =>\n    count = 0\n    count += OutboxStore.itemsForAccount(aid).length for aid in @accountIds\n    count\n\n  canReceiveThreadsFromAccountIds: =>\n    false\n\n\nclass StarredMailboxPerspective extends MailboxPerspective\n  constructor: (@accountIds) ->\n    super(@accountIds)\n    @name = \"Starred\"\n    @iconName = \"starred.png\"\n    @\n\n  threads: =>\n    query = DatabaseStore.findAll(Thread).where([\n      Thread.attributes.starred.equal(true),\n      Thread.attributes.inAllMail.equal(true),\n    ]).limit(0)\n\n    # Adding a \"account_id IN (a,b,c)\" clause to our query can result in a full\n    # table scan. Don't add the where clause if we know we want results from all.\n    if @accountIds.length < AccountStore.accounts().length\n      query.where(Thread.attributes.accountId.in(@accountIds))\n\n    return new MutableQuerySubscription(query, {emitResultSet: true})\n\n  canReceiveThreadsFromAccountIds: =>\n    super\n\n  receiveThreads: (threadIds) =>\n    ChangeStarredTask = require('./flux/tasks/change-starred-task').default\n    task = new ChangeStarredTask({threads:threadIds, starred: true, source: \"Dragged Into List\"})\n    Actions.queueTask(task)\n\n  tasksForRemovingItems: (threads, ruleset, source) =>\n    task = TaskFactory.taskForInvertingStarred({\n      threads: threads\n      source: source || \"Removed From List\"\n    })\n    return [task]\n\n\nclass EmptyMailboxPerspective extends MailboxPerspective\n  constructor: ->\n    @accountIds = []\n\n  threads: =>\n    # We need a Thread query that will not return any results and take no time.\n    # We use lastMessageReceivedTimestamp because it is the first column on an\n    # index so this returns zero items nearly instantly. In the future, we might\n    # want to make a Query.forNothing() to go along with MailboxPerspective.forNothing()\n    query = DatabaseStore.findAll(Thread).where(lastMessageReceivedTimestamp: -1).limit(0)\n    return new MutableQuerySubscription(query, {emitResultSet: true})\n\n  canReceiveThreadsFromAccountIds: =>\n    false\n\n\nclass CategoryMailboxPerspective extends MailboxPerspective\n  constructor: (@_categories) ->\n    super(_.uniq(_.pluck(@_categories, 'accountId')))\n\n    if @_categories.length is 0\n      throw new Error(\"CategoryMailboxPerspective: You must provide at least one category.\")\n\n    # Note: We pick the display name and icon assuming that you won't create a\n    # perspective with Inbox and Sent or anything crazy like that... todo?\n    @name = @_categories[0].displayName\n    if @_categories[0].name\n      @iconName = \"#{@_categories[0].name}.png\"\n    else\n      account = AccountStore.accountForId(@accountIds[0])\n      @iconName = \"folder.png\"\n      @iconName = account.categoryIcon() if account\n    @\n\n  toJSON: =>\n    json = super\n    json.serializedCategories = JSON.stringify(@_categories, Utils.registeredObjectReplacer)\n    json\n\n  isEqual: (other) =>\n    super(other) and _.isEqual(_.pluck(@categories(), 'id'), _.pluck(other.categories(), 'id'))\n\n  threads: =>\n    query = DatabaseStore.findAll(Thread)\n      .where([Thread.attributes.categories.containsAny(_.pluck(@categories(), 'id'))])\n      .limit(0)\n\n    if @isSent()\n      query.order(Thread.attributes.lastMessageSentTimestamp.descending())\n\n    unless @categoriesSharedName() in ['spam', 'trash']\n      query.where(inAllMail: true)\n\n    if @_categories.length > 1 and @accountIds.length < @_categories.length\n      # The user has multiple categories in the same account selected, which\n      # means our result set could contain multiple copies of the same threads\n      # (since we do an inner join) and we need SELECT DISTINCT. Note that this\n      # can be /much/ slower and we shouldn't do it if we know we don't need it.\n      query.distinct()\n\n    return new MutableQuerySubscription(query, {emitResultSet: true})\n\n  unreadCount: =>\n    sum = 0\n    for cat in @_categories\n      sum += ThreadCountsStore.unreadCountForCategoryId(cat.id)\n    sum\n\n  categories: =>\n    @_categories\n\n  hasSyncingCategories: =>\n    for cat in @_categories\n      if not cat.isSyncComplete()\n        return true\n    return false\n\n  isArchive: =>\n    _.every(@_categories, (cat) -> cat.isArchive())\n\n  canReceiveThreadsFromAccountIds: =>\n    super and not _.any @_categories, (c) -> c.isLockedCategory()\n\n  receiveThreads: (threadIds) =>\n    FocusedPerspectiveStore = require('./flux/stores/focused-perspective-store').default\n    current = FocusedPerspectiveStore.current()\n\n    # This assumes that the we don't have more than one category per accountId\n    # attached to this perspective\n    DatabaseStore.modelify(Thread, threadIds).then (threads) =>\n      tasks = TaskFactory.tasksForApplyingCategories\n        source: \"Dragged Into List\",\n        threads: threads\n        categoriesToRemove: (accountId) ->\n          if current.categoriesSharedName() in Category.LockedCategoryNames\n            return []\n          return _.filter(current.categories(), _.matcher({accountId}))\n        categoriesToAdd: (accountId) => [_.findWhere(@_categories, {accountId})]\n      Actions.queueTasks(tasks)\n\n  # Public:\n  # Returns the tasks for removing threads from this perspective and moving them\n  # to a given target/destination based on a {RemovalTargetRuleset}.\n  #\n  # A RemovalTargetRuleset for categories is a map that represents the\n  # target/destination Category when removing threads from another given\n  # category, i.e., when removing them the current CategoryPerspective.\n  # Rulesets are of the form:\n  #\n  #   [categoryName] -> function(accountId): Category\n  #\n  # Keys correspond to category names, e.g.`{'inbox', 'trash',...}`, which\n  # correspond to the name of the categories associated with the current perspective\n  # Values are functions with the following signature:\n  #\n  #   `function(accountId): Category`\n  #\n  # If the value of the category name of the current perspective is null instead\n  # of a function, this method will return an empty array of tasks\n  #\n  # RemovalRulesets should also contain a key `other`, that is meant to be used\n  # when a key cannot be found for the current category name\n  #\n  # Example:\n  # perspective.tasksForRemovingItems(\n  #   threads,\n  #   {\n  #     # Move to trash if the current perspective is inbox\n  #     inbox: (accountId) -> CategoryStore.getTrashCategory(accountId),\n  #\n  #     # Do nothing if the current perspective is trash\n  #     trash: null,\n  #   }\n  # )\n  #\n  tasksForRemovingItems: (threads, ruleset, source) =>\n    if threads.length is 0\n      return []\n    if not ruleset\n      throw new Error(\"tasksForRemovingItems: you must pass a ruleset object to determine the destination of the threads\")\n\n    name = if @isArchive()\n      # TODO this is an awful hack\n      'archive'\n    else\n      @categoriesSharedName()\n\n    if ruleset[name] is null\n      return []\n\n    return TaskFactory.tasksForApplyingCategories(\n      source: source || \"Removed From List\",\n      threads: threads,\n      categoriesToRemove: (accountId) =>\n        # Remove all categories from this perspective that match the accountId\n        return _.filter(@_categories, _.matcher({accountId}))\n      categoriesToAdd: (accId) =>\n        category = (ruleset[name] ? ruleset.other)(accId)\n        return if category then [category] else []\n    )\n\n\nclass UnreadMailboxPerspective extends CategoryMailboxPerspective\n  constructor: (categories) ->\n    super(categories)\n    @name = \"Unread\"\n    @iconName = \"unread.png\"\n    @\n\n  threads: =>\n    return new UnreadQuerySubscription(_.pluck(@categories(), 'id'))\n\n  unreadCount: =>\n    0\n\n  receiveThreads: (threadIds) =>\n    super(threadIds)\n    ChangeUnreadTask ?= require('./flux/tasks/change-unread-task').default\n    task = new ChangeUnreadTask({threads:threadIds, unread: true, source: \"Dragged Into List\"})\n    Actions.queueTask(task)\n\n  tasksForRemovingItems: (threads, ruleset, source) =>\n    ChangeUnreadTask ?= require('./flux/tasks/change-unread-task').default\n    tasks = super(threads, ruleset)\n    tasks.push new ChangeUnreadTask({threads, unread: false, source: source || \"Removed From List\"})\n    return tasks\n\n\nmodule.exports = MailboxPerspective\n"
  },
  {
    "path": "packages/client-app/src/menu-helpers.coffee",
    "content": "_ = require 'underscore'\n\nItemSpecificities = new WeakMap\n\nmerge = (menu, item, itemSpecificity=Infinity) ->\n  item = cloneMenuItem(item)\n  ItemSpecificities.set(item, itemSpecificity) if itemSpecificity\n  matchingItemIndex = findMatchingItemIndex(menu, item)\n  matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1\n\n  if matchingItem?\n    if item.submenu?\n      merge(matchingItem.submenu, submenuItem, itemSpecificity) for submenuItem in item.submenu\n    else if itemSpecificity\n      unless itemSpecificity < ItemSpecificities.get(matchingItem)\n        menu[matchingItemIndex] = item\n  else unless item.type is 'separator' and _.last(menu)?.type is 'separator'\n    menu.push(item)\n\nunmerge = (menu, item) ->\n  matchingItemIndex = findMatchingItemIndex(menu, item)\n  matchingItem = menu[matchingItemIndex] unless matchingItemIndex is - 1\n\n  if matchingItem?\n    if item.submenu?\n      unmerge(matchingItem.submenu, submenuItem) for submenuItem in item.submenu\n\n    if (matchingItem.submenu ? []).length is 0 or matchingItem.isOptional\n      menu.splice(matchingItemIndex, 1)\n\nfindMatchingItemIndex = (menu, {type, label, submenu}) ->\n  return -1 if type is 'separator'\n  for item, index in menu\n    if normalizeLabel(item.label) is normalizeLabel(label) and item.submenu? is submenu?\n      return index\n  -1\n\nnormalizeLabel = (label) ->\n  return undefined unless label?\n\n  if process.platform is 'darwin'\n    label\n  else\n    label.replace(/\\&/g, '')\n\ncloneMenuItem = (item) ->\n  item = Object.assign({}, item)\n  if item.submenu?\n    item.submenu = item.submenu.map (submenuItem) -> cloneMenuItem(submenuItem)\n  item\n\nforEachMenuItem = (menu, callback) ->\n  for item in menu\n    if item.submenu?\n      forEachMenuItem(item.submenu, callback)\n    callback(item)\n\nmodule.exports = {merge, unmerge, normalizeLabel, cloneMenuItem, forEachMenuItem}\n"
  },
  {
    "path": "packages/client-app/src/menu-manager.es6",
    "content": "/* eslint global-require: 0 */\n/* eslint import/no-dynamic-require: 0 */\nimport path from 'path';\nimport fs from 'fs-plus';\nimport { ipcRenderer } from 'electron';\nimport { Disposable } from 'event-kit';\nimport Utils from './flux/models/utils';\n\nimport MenuHelpers from './menu-helpers';\n\nexport default class MenuManager {\n  constructor({resourcePath}) {\n    this.resourcePath = resourcePath;\n    this.template = [];\n    this.loadPlatformItems();\n\n    NylasEnv.keymaps.onDidReloadKeymap(() => this.update());\n    NylasEnv.commands.onRegistedCommandsChanged(() => this.update());\n  }\n\n  // Public: Adds the given items to the application menu.\n  //\n  // ## Examples\n  //\n  // ```coffee\n  //   NylasEnv.menu.add [\n  //     {\n  //       label: 'Hello'\n  //       submenu : [{label: 'World!', command: 'hello:world'}]\n  //     }\n  //   ]\n  // ```\n  //\n  // * `items` An {Array} of menu item {Object}s containing the keys:\n  //   * `label` The {String} menu label.\n  //   * `submenu` An optional {Array} of sub menu items.\n  //   * `command` An optional {String} command to trigger when the item is\n  //     clicked.\n  //\n  // Returns a {Disposable} on which `.dispose()` can be called to remove the\n  // added menu items.\n  add(items) {\n    const cloned = Utils.deepClone(items);\n    for (const item of cloned) {\n      this.merge(this.template, item);\n    }\n    this.update();\n\n    return new Disposable(() => this.remove(items));\n  }\n\n  remove(items) {\n    for (const item of items) {\n      this.unmerge(this.template, item);\n    }\n    return this.update();\n  }\n\n  // Public: Refreshes the currently visible menu.\n  update = () => {\n    if (this.pendingUpdateOperation) {\n      return;\n    }\n    this.pendingUpdateOperation = true;\n    window.requestAnimationFrame(() => {\n      this.pendingUpdateOperation = false;\n      MenuHelpers.forEachMenuItem(this.template, (item) => {\n        if (item.command && item.command.startsWith('application:') === false) {\n          item.enabled = NylasEnv.commands.listenerCountForCommand(item.command) > 0;\n        }\n        if (item.submenu != null) {\n          item.enabled = !item.submenu.every((subitem) => subitem.enabled === false);\n        }\n        if (item.hideWhenDisabled) { item.visible = item.enabled; }\n      });\n      return this.sendToBrowserProcess(this.template, NylasEnv.keymaps.getBindingsForAllCommands());\n    });\n  }\n\n  loadPlatformItems() {\n    const menusDirPath = path.join(this.resourcePath, 'menus');\n    const platformMenuPath = fs.resolve(menusDirPath, process.platform, ['json']);\n    const {menu} = require(platformMenuPath);\n    return this.add(menu);\n  }\n\n  // Merges an item in a submenu aware way such that new items are always\n  // appended to the bottom of existing menus where possible.\n  merge(menu, item) {\n    return MenuHelpers.merge(menu, item);\n  }\n\n  unmerge(menu, item) {\n    return MenuHelpers.unmerge(menu, item);\n  }\n\n  // OSX can't handle displaying accelerators for multiple keystrokes.\n  // If they are sent across, it will stop processing accelerators for the rest\n  // of the menu items.\n  filterMultipleKeystroke(keystrokesByCommand) {\n    if (!keystrokesByCommand) {\n      return {};\n    }\n    const filtered = {};\n\n    for (const key of Object.keys(keystrokesByCommand)) {\n      const bindings = keystrokesByCommand[key];\n      for (const binding of bindings) {\n        if (binding.includes(' ')) {\n          continue;\n        }\n        if (!/(cmd|ctrl|shift|alt|mod)/.test(binding) && !/f\\d+/.test(binding)) {\n          continue;\n        }\n        if (!filtered[key]) {\n          filtered[key] = [];\n        }\n        filtered[key].push(binding);\n      }\n    }\n\n    return filtered;\n  }\n\n  sendToBrowserProcess(template, keystrokesByCommand) {\n    const filtered = this.filterMultipleKeystroke(keystrokesByCommand);\n    return ipcRenderer.send('update-application-menu', template, filtered);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/module-cache.coffee",
    "content": "Module = require 'module'\npath = require 'path'\nsemver = require 'semver'\n\n# Extend semver.Range to memoize matched versions for speed\nclass Range extends semver.Range\n  constructor: ->\n    super\n    @matchedVersions = new Set()\n    @unmatchedVersions = new Set()\n\n  test: (version) ->\n    return true if @matchedVersions.has(version)\n    return false if @unmatchedVersions.has(version)\n\n    matches = super\n    if matches\n      @matchedVersions.add(version)\n    else\n      @unmatchedVersions.add(version)\n    matches\n\nnativeModules = process.binding('natives')\n\ncache =\n  builtins: {}\n  debug: false\n  dependencies: {}\n  extensions: {}\n  folders: {}\n  ranges: {}\n  registered: false\n  resourcePath: null\n  resourcePathWithTrailingSlash: null\n\n# isAbsolute is inlined from fs-plus so that fs-plus itself can be required\n# from this cache.\nif process.platform is 'win32'\n  isAbsolute = (pathToCheck) ->\n    pathToCheck and (pathToCheck[1] is ':' or (pathToCheck[0] is '\\\\' and pathToCheck[1] is '\\\\'))\nelse\n  isAbsolute = (pathToCheck) ->\n    pathToCheck and pathToCheck[0] is '/'\n\nisCorePath = (pathToCheck) ->\n  pathToCheck.startsWith(cache.resourcePathWithTrailingSlash)\n\nloadDependencies = (modulePath, rootPath, rootMetadata, moduleCache) ->\n  fs = require 'fs-plus'\n\n  for childPath in fs.listSync(path.join(modulePath, 'node_modules'))\n    continue if path.basename(childPath) is '.bin'\n    continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath))\n\n    childMetadataPath = path.join(childPath, 'package.json')\n    continue unless fs.isFileSync(childMetadataPath)\n\n    childMetadata = JSON.parse(fs.readFileSync(childMetadataPath))\n    if childMetadata?.version\n      try\n        mainPath = require.resolve(childPath)\n      catch error\n        mainPath = null\n\n      if mainPath\n        moduleCache.dependencies.push\n          name: childMetadata.name\n          version: childMetadata.version\n          path: path.relative(rootPath, mainPath)\n\n      loadDependencies(childPath, rootPath, rootMetadata, moduleCache)\n\n  return\n\nloadFolderCompatibility = (modulePath, rootPath, rootMetadata, moduleCache) ->\n  fs = require 'fs-plus'\n\n  metadataPath = path.join(modulePath, 'package.json')\n  return unless fs.isFileSync(metadataPath)\n\n  dependencies = JSON.parse(fs.readFileSync(metadataPath))?.dependencies ? {}\n\n  for name, version of dependencies\n    try\n      new Range(version)\n    catch error\n      delete dependencies[name]\n\n  onDirectory = (childPath) ->\n    path.basename(childPath) isnt 'node_modules'\n\n  extensions = ['.js', '.coffee', '.json', '.node']\n  paths = {}\n  onFile = (childPath) ->\n    if path.extname(childPath) in extensions\n      relativePath = path.relative(rootPath, path.dirname(childPath))\n      paths[relativePath] = true\n  fs.traverseTreeSync(modulePath, onFile, onDirectory)\n\n  paths = Object.keys(paths)\n  if paths.length > 0 and Object.keys(dependencies).length > 0\n    moduleCache.folders.push({paths, dependencies})\n\n  for childPath in fs.listSync(path.join(modulePath, 'node_modules'))\n    continue if path.basename(childPath) is '.bin'\n    continue if rootPath is modulePath and rootMetadata.packageDependencies?.hasOwnProperty(path.basename(childPath))\n\n    loadFolderCompatibility(childPath, rootPath, rootMetadata, moduleCache)\n\n  return\n\nloadExtensions = (modulePath, rootPath, rootMetadata, moduleCache) ->\n  fs = require 'fs-plus'\n  extensions = ['.js', '.coffee', '.json', '.node']\n  nodeModulesPath = path.join(rootPath, 'node_modules')\n\n  onFile = (filePath) ->\n    filePath = path.relative(rootPath, filePath)\n    segments = filePath.split(path.sep)\n    return if 'test' in segments\n    return if 'tests' in segments\n    return if 'spec' in segments\n    return if 'specs' in segments\n    return if segments.length > 1 and not (segments[0] in ['lib', 'node_modules', 'src', 'static', 'vendor'])\n\n    extension = path.extname(filePath)\n    if extension in extensions\n      moduleCache.extensions[extension] ?= []\n      moduleCache.extensions[extension].push(filePath)\n\n  onDirectory = (childPath) ->\n    # Don't include extensions from bundled packages\n    # These are generated and stored in the package's own metadata cache\n    if rootMetadata.name is 'nylas'\n      parentPath = path.dirname(childPath)\n      if parentPath is nodeModulesPath\n        packageName = path.basename(childPath)\n        return false if rootMetadata.packageDependencies?.hasOwnProperty(packageName)\n\n    true\n\n  fs.traverseTreeSync(rootPath, onFile, onDirectory)\n\n  return\n\nsatisfies = (version, rawRange) ->\n  unless parsedRange = cache.ranges[rawRange]\n    parsedRange = new Range(rawRange)\n    cache.ranges[rawRange] = parsedRange\n  parsedRange.test(version)\n\nresolveFilePath = (relativePath, parentModule) ->\n  return unless relativePath\n  return unless parentModule?.filename\n  return unless relativePath[0] is '.' or isAbsolute(relativePath)\n\n  resolvedPath = path.resolve(path.dirname(parentModule.filename), relativePath)\n  return unless isCorePath(resolvedPath)\n\n  extension = path.extname(resolvedPath)\n  if extension\n    return resolvedPath if cache.extensions[extension]?.has(resolvedPath)\n  else\n    for extension, paths of cache.extensions\n      resolvedPathWithExtension = \"#{resolvedPath}#{extension}\"\n      return resolvedPathWithExtension if paths.has(resolvedPathWithExtension)\n\n  return\n\nresolveModulePath = (relativePath, parentModule) ->\n  return unless relativePath\n  return unless parentModule?.filename\n\n  return if nativeModules.hasOwnProperty(relativePath)\n  return if relativePath[0] is '.'\n  return if isAbsolute(relativePath)\n\n  folderPath = path.dirname(parentModule.filename)\n\n  range = cache.folders[folderPath]?[relativePath]\n  unless range?\n    if builtinPath = cache.builtins[relativePath]\n      return builtinPath\n    else\n      return\n\n  candidates = cache.dependencies[relativePath]\n  return unless candidates?\n\n  for version, resolvedPath of candidates\n    if Module._cache.hasOwnProperty(resolvedPath) or isCorePath(resolvedPath)\n      return resolvedPath if satisfies(version, range)\n\n  return\n\nregisterBuiltins = (devMode) ->\n  electronRoot = path.join(process.resourcesPath, 'atom.asar')\n\n  commonRoot = path.join(electronRoot, 'common', 'api', 'lib')\n  commonBuiltins = ['callbacks-registry', 'clipboard', 'crash-reporter', 'screen', 'shell']\n  for builtin in commonBuiltins\n    cache.builtins[builtin] = path.join(commonRoot, \"#{builtin}.js\")\n\n  rendererRoot = path.join(electronRoot, 'renderer', 'api', 'lib')\n  rendererBuiltins = ['ipc', 'remote']\n  for builtin in rendererBuiltins\n    cache.builtins[builtin] = path.join(rendererRoot, \"#{builtin}.js\")\n\nif cache.debug\n  cache.findPathCount = 0\n  cache.findPathTime = 0\n  cache.loadCount = 0\n  cache.requireTime = 0\n  global.moduleCache = cache\n\n  originalLoad = Module::load\n  Module::load = ->\n    cache.loadCount++\n    originalLoad.apply(this, arguments)\n\n  originalRequire = Module::require\n  Module::require = ->\n    startTime = Date.now()\n    exports = originalRequire.apply(this, arguments)\n    cache.requireTime += Date.now() - startTime\n    exports\n\n  originalFindPath = Module._findPath\n  Module._findPath = (request, paths) ->\n    cacheKey = JSON.stringify({request, paths})\n    cache.findPathCount++ unless Module._pathCache[cacheKey]\n\n    startTime = Date.now()\n    foundPath = originalFindPath.apply(global, arguments)\n    cache.findPathTime += Date.now() - startTime\n    foundPath\n\nexports.create = (modulePath) ->\n  fs = require 'fs-plus'\n\n  modulePath = fs.realpathSync(modulePath)\n  metadataPath = path.join(modulePath, 'package.json')\n  metadata = JSON.parse(fs.readFileSync(metadataPath))\n\n  moduleCache =\n    version: 1\n    dependencies: []\n    extensions: {}\n    folders: []\n\n  loadDependencies(modulePath, modulePath, metadata, moduleCache)\n  loadFolderCompatibility(modulePath, modulePath, metadata, moduleCache)\n  loadExtensions(modulePath, modulePath, metadata, moduleCache)\n\n  metadata._nylasModuleCache = moduleCache\n  fs.writeFileSync(metadataPath, JSON.stringify(metadata, null, 2))\n\n  return\n\nexports.register = ({resourcePath, devMode}={}) ->\n  return if cache.registered\n\n  originalResolveFilename = Module._resolveFilename\n  Module._resolveFilename = (relativePath, parentModule) ->\n    resolvedPath = resolveModulePath(relativePath, parentModule)\n    resolvedPath ?= resolveFilePath(relativePath, parentModule)\n    resolvedPath ? originalResolveFilename(relativePath, parentModule)\n\n  cache.registered = true\n  cache.resourcePath = resourcePath\n  cache.resourcePathWithTrailingSlash = \"#{resourcePath}#{path.sep}\"\n  registerBuiltins(devMode)\n\n  return\n\nexports.add = (directoryPath, metadata) ->\n  # path.join isn't used in this function for speed since path.join calls\n  # path.normalize and all the paths are already normalized here.\n\n  unless metadata?\n    try\n      metadata = require(\"#{directoryPath}#{path.sep}package.json\")\n    catch error\n      return\n\n  cacheToAdd = metadata?._nylasModuleCache\n  return unless cacheToAdd?\n\n  for dependency in cacheToAdd.dependencies ? []\n    cache.dependencies[dependency.name] ?= {}\n    cache.dependencies[dependency.name][dependency.version] ?= \"#{directoryPath}#{path.sep}#{dependency.path}\"\n\n  for entry in cacheToAdd.folders ? []\n    for folderPath in entry.paths\n      if folderPath\n        cache.folders[\"#{directoryPath}#{path.sep}#{folderPath}\"] = entry.dependencies\n      else\n        cache.folders[directoryPath] = entry.dependencies\n\n  for extension, paths of cacheToAdd.extensions\n    cache.extensions[extension] ?= new Set()\n    for filePath in paths\n      cache.extensions[extension].add(\"#{directoryPath}#{path.sep}#{filePath}\")\n\n  return\n\nexports.cache = cache\n"
  },
  {
    "path": "packages/client-app/src/multi-request-progress-monitor.es6",
    "content": "import fs from 'fs';\n\nexport default class MultiRequestProgressMonitor {\n  constructor() {\n    this._requests = {};\n    this._expected = {};\n  }\n\n  add(filepath, filesize, request) {\n    this._requests[filepath] = request\n    this._expected[filepath] = filesize || fs.statSync(filepath).size || 0;\n  }\n\n  remove(filepath) {\n    delete this._requests[filepath];\n    delete this._expected[filepath];\n  }\n\n  requests() {\n    return Object.keys(this._requests).map(k => this._requests[k]);\n  }\n\n  value() {\n    let sent = 0;\n    let expected = 1;\n    for (const filepath of Object.keys(this._requests)) {\n      const request = this._requests[filepath];\n      if (request.req && request.req.connection) {\n        sent += request.req.connection._bytesDispatched || 0\n      }\n      expected += this._expected[filepath];\n    }\n    return sent / expected;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/n1-cloud-api.es6",
    "content": "import {AccountStore} from 'nylas-exports'\n\nclass N1CloudAPI {\n  constructor() {\n    NylasEnv.config.onDidChange('env', this._onConfigChanged);\n    this._onConfigChanged();\n  }\n\n  _onConfigChanged = () => {\n    const env = NylasEnv.config.get('env')\n    if (['development', 'local'].includes(env)) {\n      this.APIRoot = \"http://lvh.me:5100\";\n    } else if (env === 'staging') {\n      this.APIRoot = \"https://n1-staging.nylas.com\";\n    } else {\n      this.APIRoot = \"https://n1.nylas.com\";\n    }\n  }\n\n  accessTokenForAccountId = (aid) => {\n    return AccountStore.tokensForAccountId(aid).n1Cloud\n  }\n}\n\nexport default new N1CloudAPI();\n"
  },
  {
    "path": "packages/client-app/src/native-notifications.es6",
    "content": "/* eslint global-require: 0 */\nlet MacNotifierNotification = null;\nif (process.platform === 'darwin') {\n  try {\n    MacNotifierNotification = require('node-mac-notifier');\n  } catch (err) {\n    console.error(\"node-mac-notifier (a platform-specific optionalDependency) was not installed correctly! Check the Travis build log for errors.\")\n  }\n}\n\nclass NativeNotifications {\n  constructor() {\n    if (MacNotifierNotification) {\n      this._macNotificationsByTag = {};\n      NylasEnv.onBeforeUnload(() => {\n        Object.keys(this._macNotificationsByTag).forEach((key) => {\n          this._macNotificationsByTag[key].close();\n        });\n        return true;\n      });\n    }\n  }\n  displayNotification({title, subtitle, body, tag, canReply, onActivate} = {}) {\n    let notif = null;\n\n    if (MacNotifierNotification) {\n      if (tag && this._macNotificationsByTag[tag]) {\n        this._macNotificationsByTag[tag].close();\n      }\n      notif = new MacNotifierNotification(title, {\n        bundleId: 'com.nylas.nylas-mail',\n        canReply: canReply,\n        subtitle: subtitle,\n        body: body,\n        id: tag,\n      });\n      notif.addEventListener('reply', ({response}) => {\n        onActivate({response, activationType: 'replied'});\n      });\n      notif.addEventListener('click', () => {\n        onActivate({response: null, activationType: 'clicked'});\n      });\n      if (tag) {\n        this._macNotificationsByTag[tag] = notif;\n      }\n    } else {\n      notif = new Notification(title, {\n        silent: true,\n        body: subtitle,\n        tag: tag,\n      });\n      notif.onclick = onActivate;\n    }\n    return notif;\n  }\n}\n\nexport default new NativeNotifications()\n"
  },
  {
    "path": "packages/client-app/src/nylas-env.es6",
    "content": "/* eslint global-require: 0 */\n/* eslint import/no-dynamic-require: 0 */\nimport path from 'path';\n\nimport { ipcRenderer, remote, shell } from 'electron';\n\nimport _ from 'underscore';\nimport { Emitter } from 'event-kit';\nimport fs from 'fs-plus';\nimport { convertStackTrace } from 'coffeestack';\nimport { mapSourcePosition } from 'source-map-support';\n\nimport WindowEventHandler from './window-event-handler';\nimport StylesElement from './styles-element';\nimport StoreRegistry from './registries/store-registry';\n\nimport Utils from './flux/models/utils';\n\nfunction ensureInteger(f, fallback) {\n  let int = f;\n  if (isNaN(f) || (f === undefined) || (f === null)) {\n    int = fallback;\n  }\n  return Math.round(int);\n}\n\n// Essential: NylasEnv global for dealing with packages, themes, menus, and the window.\n//\n// The singleton of this class is always available as the `NylasEnv` global.\nexport default class NylasEnvConstructor {\n  static initClass() {\n    this.version = 1;\n\n    this.prototype.workspaceViewParentSelector = 'body';\n    this.prototype.lastUncaughtError = null;\n\n    /*\n    Section: Properties\n    */\n\n    // Public: A {CommandRegistry} instance\n    this.prototype.commands = null;\n\n    // Public: A {Config} instance\n    this.prototype.config = null;\n\n    // Public: A {MenuManager} instance\n    this.prototype.menu = null;\n\n    // Public: A {KeymapManager} instance\n    this.prototype.keymaps = null;\n\n    // Public: A {PackageManager} instance\n    this.prototype.packages = null;\n\n    // Public: A {ThemeManager} instance\n    this.prototype.themes = null;\n\n    // Public: A {StyleManager} instance\n    this.prototype.styles = null;  // Increment this when the serialization format changes\n  }\n\n  assert(bool, msg) {\n    if (!bool) { throw new Error(`Assertion error: ${msg}`); }\n  }\n\n  // Load or create the application environment\n  // Returns an NylasEnv instance, fully initialized\n  static loadOrCreate() {\n    let app;\n\n    const savedState = this._loadSavedState();\n    if (savedState && (savedState.version === this.version)) {\n      app = new this(savedState);\n    } else {\n      app = new this({version: this.version});\n    }\n\n    return app;\n  }\n\n  // Loads and returns the serialized state corresponding to this window\n  // if it exists; otherwise returns undefined.\n  static _loadSavedState() {\n    let stateString;\n    const statePath = this.getStatePath();\n\n    if (fs.existsSync(statePath)) {\n      try {\n        stateString = fs.readFileSync(statePath, 'utf8');\n      } catch (error) {\n        console.warn(`Error reading window state: ${statePath}`, error.stack, error);\n      }\n    } else {\n      stateString = this.getLoadSettings().windowState;\n    }\n\n    try {\n      if (stateString != null) { return JSON.parse(stateString); }\n    } catch (error) {\n      console.warn(`Error parsing window state: ${statePath} ${error.stack}`, error);\n    }\n    return null;\n  }\n\n  // Returns the path where the state for the current window will be\n  // located if it exists.\n  static getStatePath() {\n    const {isSpec, mainWindow, configDirPath} = this.getLoadSettings();\n    if (isSpec) {\n      return 'spec-saved-state.json';\n    } else if (mainWindow) {\n      return path.join(configDirPath, 'main-window-state.json');\n    }\n    return null;\n  }\n\n  // Returns the load settings hash associated with the current window.\n  static getLoadSettings() {\n    if (this.loadSettings == null) {\n      this.loadSettings = JSON.parse(decodeURIComponent(location.search.substr(14)));\n    }\n\n    const cloned = Utils.deepClone(this.loadSettings);\n    // The loadSettings.windowState could be large, request it only when needed.\n    Object.defineProperty(cloned, 'windowState', {\n      get: () => { return this.getCurrentWindow().loadSettings.windowState },\n      set: (value) => {\n        this.getCurrentWindow().loadSettings.windowState = value;\n        return value;\n      },\n    });\n    return cloned;\n  }\n\n  static getCurrentWindow() {\n    return remote.getCurrentWindow();\n  }\n\n  /*\n  Section: Construction and Destruction\n  */\n\n  // Call .loadOrCreate instead\n  constructor(savedState = {}) {\n    this.reportError = this.reportError.bind(this);\n    this.getConfigDirPath = this.getConfigDirPath.bind(this);\n    this.storeColumnWidth = this.storeColumnWidth.bind(this);\n    this.getColumnWidth = this.getColumnWidth.bind(this);\n    this.startWindow = this.startWindow.bind(this);\n    this.populateHotWindow = this.populateHotWindow.bind(this);\n    this.savedState = savedState;\n    ({version: this.version} = this.savedState);\n    this.emitter = new Emitter();\n  }\n\n  // Sets up the basic services that should be available in all modes\n  // (both spec and application).\n  //\n  // Call after this instance has been assigned to the `NylasEnv` global.\n  initialize() {\n    this.enhanceEventObject();\n\n    this.setupErrorLogger();\n\n    this.loadTime = null;\n\n    const Config = require('./config');\n    const KeymapManager = require('./keymap-manager').default;\n    const CommandRegistry = require('./registries/command-registry').default;\n    const PackageManager = require('./package-manager');\n    const ThemeManager = require('./theme-manager');\n    const StyleManager = require('./style-manager');\n    const ActionBridge = require('./flux/action-bridge').default;\n    const MenuManager = require('./menu-manager').default;\n\n    const {devMode, benchmarkMode, safeMode, resourcePath, configDirPath, windowType} = this.getLoadSettings();\n\n    document.body.classList.add(`platform-${process.platform}`);\n    document.body.classList.add(`window-type-${windowType}`);\n\n    // Add 'src/global' to module search path.\n    const globalPath = path.join(resourcePath, 'src', 'global');\n    require('module').globalPaths.push(globalPath);\n\n    // Our client-private-plugins get sym-linked into internal_packages.\n    // However, when we require anything from those files, the require chain is\n    // relative to their original location. Their original location is a sibling\n    // (not a child) of the client-app repo. This means the node_modules that\n    // they should see aren't actually there due to the symlink. We manually add\n    // node_modules to the global require path (even though it's already there\n    // by default) to support these symlinked modules\n    require('module').globalPaths.push(path.join(resourcePath, 'node_modules'));\n\n    // Still set NODE_PATH since tasks may need it.\n    process.env.NODE_PATH = globalPath;\n\n    // Make react.js faster\n    if (!devMode && process.env.NODE_ENV == null) process.env.NODE_ENV = 'production';\n\n    // Set NylasEnv's home so packages don't have to guess it\n    process.env.NYLAS_HOME = configDirPath;\n\n    // Setup config and load it immediately so it's available to our singletons\n    this.config = new Config({configDirPath, resourcePath});\n\n    this.keymaps = new KeymapManager({configDirPath, resourcePath});\n\n    const specMode = this.inSpecMode();\n\n    this.commands = new CommandRegistry();\n    this.packages = new PackageManager({devMode, benchmarkMode, configDirPath, resourcePath, safeMode, specMode});\n    this.styles = new StyleManager();\n    document.head.appendChild(new StylesElement());\n    this.themes = new ThemeManager({packageManager: this.packages, configDirPath, resourcePath, safeMode});\n    this.menu = new MenuManager({resourcePath});\n    if (process.platform === 'win32') {\n      this.getCurrentWindow().setMenuBarVisibility(false);\n    }\n\n    // initialize spell checking\n    this.spellchecker = require('./spellchecker').default;\n\n    this.packages.onDidActivateInitialPackages(() => this.watchThemes());\n    this.windowEventHandler = new WindowEventHandler();\n\n    this.timer = remote.getGlobal('application').timer;\n\n    this.globalWindowEmitter = new Emitter();\n\n    if (!this.inSpecMode()) {\n      this.actionBridge = new ActionBridge(ipcRenderer);\n    }\n\n    this.extendRxObservables();\n\n    // Nylas exports is designed to provide a lazy-loaded set of globally\n    // accessible objects to all packages. Upon require, nylas-exports will\n    // fill the TaskRegistry, StoreRegistry, and DatabaseObjectRegistries\n    // with various constructors.\n    //\n    // We initialize all of the stores loaded into the StoreRegistry once\n    // the window starts loading.\n    require('nylas-exports');\n\n    process.title = `Nylas Mail ${this.getWindowType()}`;\n    return this.onWindowPropsReceived(() => {\n      process.title = `Nylas Mail ${this.getWindowType()}`;\n      return process.title;\n    });\n  }\n\n  // This ties window.onerror and process.un{caughtException,handledRejection}\n  // to the publically callable `reportError` method. This will take care of\n  // reporting errors if necessary and hooking into error handling\n  // callbacks.\n  //\n  // Start our error reporting to the backend and attach error handlers\n  // to the window and the Bluebird Promise library, converting things\n  // back through the sourcemap as necessary.\n  setupErrorLogger() {\n    const ErrorLogger = require('./error-logger');\n    this.errorLogger = new ErrorLogger({\n      inSpecMode: this.inSpecMode(),\n      inDevMode: this.inDevMode(),\n      resourcePath: this.getLoadSettings().resourcePath,\n    });\n\n    const sourceMapCache = {};\n\n    // https://developer.mozilla.org/en-US/docs/Web/API/GlobalEventHandlers/onerror\n    window.onerror = (message, url, line, column, originalError) => {\n      if (!this.inDevMode()) {\n        return this.reportError(originalError, {url, line, column});\n      }\n      const {line: newLine, column: newColumn} = mapSourcePosition({source: url, line, column});\n      originalError.stack = convertStackTrace(originalError.stack, sourceMapCache);\n      return this.reportError(originalError, {url, line: newLine, column: newColumn});\n    };\n\n    process.on('uncaughtException', e => this.reportError(e));\n\n    // Based on testing, there are some unhandled rejections that don't get\n    // caught by `process.on('unhandledRejection')`, so we listen for unhandled\n    // rejections on the`window` as well\n    window.addEventListener('unhandledrejection', e => {\n      // This event is supposed to look like {reason, promise}, according to\n      // https://developer.mozilla.org/en-US/docs/Web/API/PromiseRejectionEvent\n      // In practice, it can have different shapes, so we try to make our best\n      // guess\n      if (!e) {\n        const error = new Error(`Unknown window.unhandledrejection event.`)\n        this._onUnhandledRejection(error, sourceMapCache)\n        return\n      }\n      if (e instanceof Error) {\n        this._onUnhandledRejection(e, sourceMapCache)\n        return\n      }\n      if (e.reason) {\n        const error = e.reason\n        this._onUnhandledRejection(error, sourceMapCache)\n        return\n      }\n      if (e.detail && e.detail.reason) {\n        const error = e.detail.reason\n        this._onUnhandledRejection(error, sourceMapCache)\n        return\n      }\n      const error = new Error(`Unrecognized event shape in window.unhandledrejection handler. Event keys: ${Object.keys(e)}`)\n      this._onUnhandledRejection(error, sourceMapCache)\n    });\n\n    if (this.inSpecMode() || (this.inDevMode() && !this.inBenchmarkMode())) {\n      return Promise.config({longStackTraces: true});\n    }\n    return null;\n  }\n\n  // Given that we listen to unhandled rejections on both the `window` and the\n  // `process`, more often than not both of those will get called almost\n  // immedaitely with the same error. To prevent double reporting the same\n  // error, we debounce this function with a very small interval\n  _onUnhandledRejection = (error, sourceMapCache) => {\n    if (this.inDevMode()) {\n      error.stack = convertStackTrace(error.stack, sourceMapCache);\n    }\n    this.reportError(error, {\n      rateLimit: {\n        ratePerHour: 30,\n        key: `UnhandledRejection:${error.stack}`,\n      },\n    })\n  }\n\n  _createErrorCallbackEvent(error, extraArgs = {}) {\n    const event = _.extend({}, extraArgs, {\n      message: error.message,\n      originalError: error,\n      defaultPrevented: false,\n    });\n    event.preventDefault = () => { event.defaultPrevented = true; return true };\n    return event;\n  }\n\n  // Public: report an error through the `ErrorLogger`\n  //\n  // Takes an error and an extra object to report. Hooks into the\n  // `onWillThrowError` and `onDidThrowError` callbacks. If someone\n  // registered with `onWillThrowError` calls `preventDefault` on the event\n  // object it's given, then no error will be reported.\n  //\n  // The difference between this and `ErrorLogger.reportError` is that\n  // `NylasEnv.reportError` will hook into the event callbacks and handle\n  // test failures and dev tool popups.\n  reportError(error, extra = {}, {noWindows} = {}) {\n    const event = this._createErrorCallbackEvent(error, extra);\n    this.emitter.emit('will-throw-error', event);\n    if (event.defaultPrevented) { return; }\n\n    this.lastUncaughtError = error;\n\n    extra.pluginIds = this._findPluginsFromError(error);\n\n    if (this.inSpecMode()) {\n      jasmine.getEnv().currentSpec.fail(error);\n    } else if (this.inDevMode() && !noWindows) {\n      if (!this.isDevToolsOpened()) {\n        this.openDevTools();\n        this.executeJavaScriptInDevTools(\"DevToolsAPI.showPanel('console')\");\n      }\n    }\n\n    this.errorLogger.reportError(error, extra);\n\n    this.emitter.emit('did-throw-error', event);\n  }\n\n  _findPluginsFromError(error) {\n    if (!error.stack) { return []; }\n    const left = error.stack.match(/((?:\\/[\\w-_]+)+)/g);\n    const stackPaths = left || [];\n    const stackTokens = _.uniq(_.flatten(stackPaths.map(p => p.split(\"/\"))));\n    const pluginIdsByPathBase = this.packages.getPluginIdsByPathBase();\n    const tokens = _.intersection(Object.keys(pluginIdsByPathBase), stackTokens);\n    return tokens.map(tok => pluginIdsByPathBase[tok]);\n  }\n\n  /*\n  Section: Event Subscription\n  */\n\n  // Extended: Invoke the given callback whenever {::beep} is called.\n  //\n  // * `callback` {Function} to be called whenever {::beep} is called.\n  //\n  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidBeep(callback) {\n    return this.emitter.on('did-beep', callback);\n  }\n\n  // Extended: Invoke the given callback when there is an unhandled error, but\n  // before the devtools pop open\n  //\n  // * `callback` {Function} to be called whenever there is an unhandled error\n  //   * `event` {Object}\n  //     * `originalError` {Object} the original error object\n  //     * `message` {String} the original error object\n  //     * `url` {String} Url to the file where the error originated.\n  //     * `line` {Number}\n  //     * `column` {Number}\n  //     * `preventDefault` {Function} call this to avoid popping up the dev tools.\n  //\n  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onWillThrowError(callback) {\n    return this.emitter.on('will-throw-error', callback);\n  }\n\n  // Extended: Invoke the given callback whenever there is an unhandled error.\n  //\n  // * `callback` {Function} to be called whenever there is an unhandled error\n  //   * `event` {Object}\n  //     * `originalError` {Object} the original error object\n  //     * `message` {String} the original error object\n  //     * `url` {String} Url to the file where the error originated.\n  //     * `line` {Number}\n  //     * `column` {Number}\n  //\n  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidThrowError(callback) {\n    return this.emitter.on('did-throw-error', callback);\n  }\n\n  // Extended: Run the Chromium content-tracing module for five seconds, and save\n  // the output to a file which is printed to the command-line output of the app.\n  // You can take the file exported by this function and load it into Chrome's\n  // content trace visualizer (chrome://tracing). It's like Chromium Developer\n  // Tools Profiler, but for all processes and threads.\n  trace() {\n    const tracing = remote.contentTracing;\n    const opts = {\n      categoryFilter: '*',\n      traceOptions: 'record-until-full,enable-sampling,enable-systrace',\n    };\n    return tracing.startRecording(opts, () => {\n      console.log('Tracing started');\n      return setTimeout(() =>\n        tracing.stopRecording('', p => console.log(`Tracing data recorded to ${p}`))\n\n      , 5000);\n    });\n  }\n\n  isMainWindow() {\n    return !!this.getLoadSettings().mainWindow;\n  }\n\n  isEmptyWindow() {\n    return this.getWindowType() === 'emptyWindow';\n  }\n\n  isWorkWindow() {\n    return this.getWindowType() === 'work';\n  }\n\n  isComposerWindow() {\n    return [\"composer\", \"composer-preload\"].includes(this.getWindowType());\n  }\n\n  isThreadWindow() {\n    return this.getWindowType() === 'thread-popout';\n  }\n\n  getWindowType() {\n    return this.getLoadSettings().windowType;\n  }\n\n  // Public: Is the current window in development mode?\n  inDevMode() {\n    return this.getLoadSettings().devMode;\n  }\n\n  inBenchmarkMode() {\n    return this.getLoadSettings().benchmarkMode;\n  }\n\n  // Public: Is the current window in safe mode?\n  inSafeMode() {\n    return this.getLoadSettings().safeMode;\n  }\n\n  // Public: Is the current window running specs?\n  inSpecMode() {\n    return this.getLoadSettings().isSpec;\n  }\n\n  // Public: Get the version of Nylas Mail.\n  //\n  // Returns the version text {String}.\n  getVersion() {\n    return this.appVersion != null ? this.appVersion : (this.appVersion = this.getLoadSettings().appVersion);\n  }\n\n  // Public: Determine whether the current version is an official release.\n  isReleasedVersion() {\n    return !/\\w{7}/.test(this.getVersion()); // Check if the release is a 7-character SHA prefix\n  }\n\n  // Public: Get the directory path to Nylas Mail's configuration area.\n  getConfigDirPath() { return this.getLoadSettings().configDirPath; }\n\n  // Public: Get the time taken to completely load the current window.\n  //\n  // This time include things like loading and activating packages, creating\n  // DOM elements for the editor, and reading the config.\n  //\n  // Returns the {Number} of milliseconds taken to load the window or null\n  // if the window hasn't finished loading yet.\n  getWindowLoadTime() {\n    return this.loadTime;\n  }\n\n  // Public: Get the load settings for the current window.\n  //\n  // Returns an {Object} containing all the load setting key/value pairs.\n  getLoadSettings() {\n    return this.constructor.getLoadSettings();\n  }\n\n  /*\n  Section: Managing The Nylas Window\n  */\n\n  // Essential: Close the current window.\n  close() {\n    return this.getCurrentWindow().close();\n  }\n\n  quit() {\n    return remote.app.quit();\n  }\n\n  // Essential: Get the size of current window.\n  //\n  // Returns an {Object} in the format `{width: 1000, height: 700}`\n  getSize() {\n    const [width, height] = Array.from(this.getCurrentWindow().getSize());\n    return {width, height};\n  }\n\n  // Essential: Set the size of current window.\n  //\n  // * `width` The {Number} of pixels.\n  // * `height` The {Number} of pixels.\n  setSize(width, height) {\n    return this.getCurrentWindow().setSize(\n      ensureInteger(width, 100),\n      ensureInteger(height, 100));\n  }\n\n  // Essential: Transition and set the size of the current window.\n  //\n  // * `width` The {Number} of pixels.\n  // * `height` The {Number} of pixels.\n  // * `duration` The {Number} of pixels.\n  setSizeAnimated(width, height, duration = 400) {\n    // On Windows, the native window resizing code isn't fast enough to \"animate\"\n    // by resizing over and over again. Just turn off animation for now.\n    let animDuration = duration;\n    if (process.platform === 'win32') {\n      animDuration = 1;\n    }\n\n    // Avoid divide by zero errors below\n    animDuration = Math.max(1, duration);\n\n    // Keep track of the number of times this method has been invoked, and ensure\n    // that we only `tick` for the last invocation. This prevents two resizes from\n    // running at the same time.\n    if (this._setSizeAnimatedCallCount == null) {\n      this._setSizeAnimatedCallCount = 0;\n    }\n    this._setSizeAnimatedCallCount += 1;\n    const call = this._setSizeAnimatedCallCount;\n\n    const cubicInOut = (t) => {\n      if (t < 0.5) {\n        return 4 * (t ** 3);\n      }\n      return (t - 1) * (((2 * t) - 2) ** 2) + 1;\n    };\n    const win = this.getCurrentWindow();\n    const animWidth = Math.round(width);\n    const animHeight = Math.round(height);\n\n    const startBounds = win.getBounds();\n    const startTime = Date.now() - 1; // - 1 so that if animDuration is 1, t = 1 on the first frame\n\n    const boundsForI = i =>\n      // It's very important this function never return undefined for any of the\n      // keys which blows up setBounds.\n      ({\n        x: ensureInteger(startBounds.x + ((animWidth - startBounds.animWidth) * -0.5 * i), 0),\n        y: ensureInteger(startBounds.y + ((animHeight - startBounds.animHeight) * -0.5 * i), 0),\n        width: ensureInteger(startBounds.animWidth + ((animWidth - startBounds.animWidth) * i), animWidth),\n        height: ensureInteger(startBounds.animHeight + ((animHeight - startBounds.animHeight) * i), animHeight),\n      })\n    ;\n\n    const tick = () => {\n      if (call !== this._setSizeAnimatedCallCount) { return; }\n      const t = Math.min(1, (Date.now() - startTime) / (animDuration));\n      const i = cubicInOut(t);\n      win.setBounds(boundsForI(i));\n      if (t !== 1) {\n        _.defer(tick);\n      }\n    };\n    tick();\n  }\n\n  setMinimumWidth(minWidth) {\n    const win = this.getCurrentWindow();\n    const minHeight = win.getMinimumSize()[1];\n    win.setMinimumSize(ensureInteger(minWidth, 0), minHeight);\n\n    const [currWidth, currHeight] = Array.from(win.getSize());\n    if (minWidth > currWidth) {\n      win.setSize(minWidth, currHeight);\n    }\n  }\n\n  // Essential: Get the position of current window.\n  //\n  // Returns an {Object} in the format `{x: 10, y: 20}`\n  getPosition() {\n    const [x, y] = Array.from(this.getCurrentWindow().getPosition());\n    return {x, y};\n  }\n\n  // Essential: Set the position of current window.\n  //\n  // * `x` The {Number} of pixels.\n  // * `y` The {Number} of pixels.\n  setPosition(x, y) {\n    return ipcRenderer.send('call-window-method', 'setPosition',\n      ensureInteger(x, 0),\n      ensureInteger(y, 0));\n  }\n\n  // Extended: Get the current window\n  getCurrentWindow() {\n    return this.constructor.getCurrentWindow();\n  }\n\n  // Extended: Move current window to the center of the screen.\n  center() {\n    return ipcRenderer.send('call-window-method', 'center');\n  }\n\n  // Extended: Focus the current window. Note: this will not open the window\n  // if it is hidden.\n  focus() {\n    ipcRenderer.send('call-window-method', 'focus');\n    return window.focus();\n  }\n\n  // Extended: Show the current window.\n  show() {\n    return ipcRenderer.send('call-window-method', 'show');\n  }\n\n  isVisible() {\n    return this.getCurrentWindow().isVisible();\n  }\n\n  // Extended: Hide the current window.\n  hide() {\n    return ipcRenderer.send('call-window-method', 'hide');\n  }\n\n  // Extended: Reload the current window.\n  reload() {\n    this.isReloading = true;\n    return ipcRenderer.send('call-webcontents-method', 'reload');\n  }\n\n  // Public: The windowProps passed when creating the window via `newWindow`.\n  //\n  getWindowProps() {\n    return this.getLoadSettings().windowProps || {};\n  }\n\n  // Public: If your package declares hot-loaded window types, `onWindowPropsReceived`\n  // fires when your hot-loaded window is about to be shown so you can update\n  // components to reflect the new window props.\n  //\n  // - callback: A function to call when window props are received, just before\n  //   the hot window is shown. The first parameter is the new windowProps.\n  //\n  onWindowPropsReceived(callback) {\n    return this.emitter.on('window-props-received', callback);\n  }\n\n  // Extended: Is the current window maximized?\n  isMaximixed() {\n    return this.getCurrentWindow().isMaximized();\n  }\n\n  maximize() {\n    return ipcRenderer.send('call-window-method', 'maximize');\n  }\n\n  minimize() {\n    return ipcRenderer.send('call-window-method', 'minimize');\n  }\n\n  // Extended: Is the current window in full screen mode?\n  isFullScreen() {\n    return this.getCurrentWindow().isFullScreen();\n  }\n\n  // Extended: Set the full screen state of the current window.\n  setFullScreen(fullScreen = false) {\n    ipcRenderer.send('call-window-method', 'setFullScreen', fullScreen);\n    if (fullScreen) {\n      return document.body.classList.add(\"fullscreen\");\n    }\n    return document.body.classList.remove(\"fullscreen\");\n  }\n\n  // Extended: Toggle the full screen state of the current window.\n  toggleFullScreen() {\n    return this.setFullScreen(!this.isFullScreen());\n  }\n\n  getAllWindowDimensions() {\n    return remote.getGlobal('application').getAllWindowDimensions();\n  }\n\n  // Get the dimensions of this window.\n  //\n  // Returns an {Object} with the following keys:\n  //   * `x`      The window's x-position {Number}.\n  //   * `y`      The window's y-position {Number}.\n  //   * `width`  The window's width {Number}.\n  //   * `height` The window's height {Number}.\n  getWindowDimensions() {\n    const browserWindow = this.getCurrentWindow();\n    const {x, y, width, height} = browserWindow.getBounds();\n    const maximized = browserWindow.isMaximized();\n    const fullScreen = browserWindow.isFullScreen();\n    return {x, y, width, height, maximized, fullScreen};\n  }\n\n  // Set the dimensions of the window.\n  //\n  // The window will be centered if either the x or y coordinate is not set\n  // in the dimensions parameter. If x or y are omitted the window will be\n  // centered. If height or width are omitted only the position will be changed.\n  //\n  // * `dimensions` An {Object} with the following keys:\n  //   * `x` The new x coordinate.\n  //   * `y` The new y coordinate.\n  //   * `width` The new width.\n  //   * `height` The new height.\n  setWindowDimensions({x, y, width, height}) {\n    if ((x != null) && (y != null) && (width != null) && (height != null)) {\n      return this.getCurrentWindow().setBounds({x, y, width, height});\n    } else if ((width != null) && (height != null)) {\n      return this.setSize(width, height);\n    } else if ((x != null) && (y != null)) {\n      return this.setPosition(x, y);\n    }\n    return this.center();\n  }\n\n  // Returns true if the dimensions are useable, false if they should be ignored.\n  // Work around for https://github.com/atom/electron/issues/473\n  isValidDimensions({x, y, width, height} = {}) {\n    return (width > 0) && (height > 0) && ((x + width) > 0) && ((y + height) > 0);\n  }\n\n  getDefaultWindowDimensions() {\n    let {width, height} = remote.screen.getPrimaryDisplay().workAreaSize;\n    let x = 0;\n    let y = 0;\n\n    const MAX_WIDTH = 1440;\n    if (width > MAX_WIDTH) {\n      x = Math.floor((width - MAX_WIDTH) / 2);\n      width = MAX_WIDTH;\n    }\n\n    const MAX_HEIGHT = 900;\n    if (height > MAX_HEIGHT) {\n      y = Math.floor((height - MAX_HEIGHT) / 2);\n      height = MAX_HEIGHT;\n    }\n\n    return {x, y, width, height};\n  }\n\n  restoreWindowDimensions() {\n    let dimensions = this.savedState.windowDimensions;\n    if (!this.isValidDimensions(dimensions)) {\n      dimensions = this.getDefaultWindowDimensions();\n    }\n    this.setWindowDimensions(dimensions);\n    if (dimensions.maximized && (process.platform !== 'darwin')) {\n      this.maximize();\n    }\n    if (dimensions.fullScreen) {\n      this.setFullScreen(true);\n    }\n  }\n\n  storeWindowDimensions() {\n    const dimensions = this.getWindowDimensions();\n    if (this.isValidDimensions(dimensions)) {\n      this.savedState.windowDimensions = dimensions;\n    }\n  }\n\n  storeColumnWidth({id, width}) {\n    if (this.savedState.columnWidths == null) {\n      this.savedState.columnWidths = {};\n    }\n    this.savedState.columnWidths[id] = width;\n  }\n\n  getColumnWidth(id) {\n    if (this.savedState.columnWidths == null) {\n      this.savedState.columnWidths = {};\n    }\n    return this.savedState.columnWidths[id];\n  }\n\n  startWindow() {\n    this.loadConfig();\n    const {packageLoadingDeferred, windowType} = this.getLoadSettings();\n    return StoreRegistry.activateAllStores().then(() => {\n      this.keymaps.loadKeymaps();\n      this.themes.loadBaseStylesheets();\n      if (!packageLoadingDeferred) { this.packages.loadPackages(windowType); }\n      if (!packageLoadingDeferred) { this.deserializePackageStates(); }\n      this.initializeReactRoot();\n      if (!packageLoadingDeferred) { this.packages.activate(); }\n      return this.menu.update();\n    }\n    );\n  }\n\n  // Call this method when establishing a real application window.\n  startRootWindow() {\n    const {safeMode, initializeInBackground} = this.getLoadSettings();\n\n    // Temporary. It takes five paint cycles for all the CSS in index.html to\n    // be applied. Remove if https://github.com/atom/brightray/issues/196 fixed!\n    return window.requestAnimationFrame(() => {\n      return window.requestAnimationFrame(() => {\n        return window.requestAnimationFrame(() => {\n          return window.requestAnimationFrame(() => {\n            return window.requestAnimationFrame(() => {\n              if (!initializeInBackground) { this.displayWindow(); }\n              return this.startWindow().then(() => {\n                // These don't need to wait for the window's stores and\n                // such to fully activate:\n                if (!safeMode) { this.requireUserInitScript(); }\n                this.showMainWindow();\n                return ipcRenderer.send('window-command', 'window:loaded');\n              });\n            });\n          });\n        });\n      });\n    });\n  }\n\n  // Initializes a secondary window.\n  // NOTE: If the `packageLoadingDeferred` option is set (which is true for\n  // hot windows), the packages won't be loaded until `populateHotWindow`\n  // gets fired.\n  startSecondaryWindow() {\n    const elt = document.getElementById(\"application-loading-cover\");\n    if (elt) elt.remove();\n\n    return this.startWindow().then(() => {\n      this.initializeBasicSheet();\n      ipcRenderer.on(\"load-settings-changed\", this.populateHotWindow);\n      return ipcRenderer.send('window-command', 'window:loaded');\n    }\n    );\n  }\n\n  // We setup the initial Sheet for hot windows. This is the default title\n  // bar, stoplights, etc. This saves ~100ms when populating the hot\n  // windows.\n  initializeBasicSheet() {\n    const WorkspaceStore = require('../src/flux/stores/workspace-store');\n    if (!WorkspaceStore.Sheet.Main) {\n      WorkspaceStore.defineSheet('Main', {root: true}, {\n        popout: ['Center'],\n      });\n    }\n  }\n\n  showMainWindow() {\n    document.getElementById(\"application-loading-cover\").remove();\n    document.body.classList.add(\"window-loaded\");\n    this.restoreWindowDimensions();\n    return this.getCurrentWindow().setMinimumSize(875, 250);\n  }\n\n  // Updates the window load settings - called when the app is ready to\n  // display a hot-loaded window. Causes listeners registered with\n  // `onWindowPropsReceived` to receive new window props.\n  //\n  // This also means that the windowType has changed and a different set of\n  // plugins needs to be loaded.\n  populateHotWindow(event, loadSettings) {\n    if (/composer/.test(loadSettings.windowType)) {\n      NylasEnv.timer.split('open-composer-window');\n    }\n    this.loadSettings = loadSettings;\n    this.constructor.loadSettings = loadSettings;\n\n    this.packages.loadPackages(loadSettings.windowType);\n    this.deserializePackageStates();\n    this.packages.activate();\n\n    this.emitter.emit('window-props-received',\n      loadSettings.windowProps != null ? loadSettings.windowProps : {});\n\n    const browserWindow = this.getCurrentWindow();\n    if (browserWindow.isResizable() !== loadSettings.resizable) {\n      browserWindow.setResizable(loadSettings.resizable);\n    }\n\n    if (!loadSettings.hidden) {\n      this.displayWindow();\n    }\n  }\n\n  // We extend nylas observables with our own methods. This happens on\n  // require of nylas-observables\n  extendRxObservables() {\n    return require('nylas-observables');\n  }\n\n  // Launches a new window via the browser/WindowLauncher.\n  //\n  // If you pass a `windowKey` in the options, and that windowKey already\n  // exists, it'll show that window instead of spawing a new one. This is\n  // useful for places like popout composer windows where you want to\n  // simply display the draft instead of spawning a whole new window for\n  // the same draft.\n  //\n  // `options` are documented in browser/WindowLauncher\n  newWindow(options = {}) {\n    return ipcRenderer.send('new-window', options);\n  }\n\n  saveStateAndUnloadWindow() {\n    this.packages.deactivatePackages();\n    this.savedState.packageStates = this.packages.packageStates;\n    this.saveSync();\n    this.windowState = null;\n  }\n\n  /*\n  Section: Messaging the User\n  */\n\n  displayWindow({maximize} = {}) {\n    if (this.inSpecMode()) { return; }\n    this.show();\n    this.focus();\n    if (maximize) this.maximize();\n  }\n\n  // Essential: Visually and audibly trigger a beep.\n  beep() {\n    if (this.config.get('core.audioBeep')) { shell.beep(); }\n    return this.emitter.emit('did-beep');\n  }\n\n  // Essential: A flexible way to open a dialog akin to an alert dialog.\n  //\n  // ## Examples\n  //\n  // ```coffee\n  // NylasEnv.confirm\n  //   message: 'How you feeling?'\n  //   detailedMessage: 'Be honest.'\n  //   buttons:\n  //     Good: -> window.alert('good to hear')\n  //     Bad: -> window.alert('bummer')\n  // ```\n  //\n  // * `options` An {Object} with the following keys:\n  //   * `message` The {String} message to display.\n  //   * `detailedMessage` (optional) The {String} detailed message to display.\n  //   * `buttons` (optional) Either an array of strings or an object where keys are\n  //     button names and the values are callbacks to invoke when clicked.\n  //\n  // Returns the chosen button index {Number} if the buttons option was an array.\n  confirm({message, detailedMessage, buttons} = {}) {\n    let buttonLabels;\n    if (_.isArray(buttons)) {\n      buttonLabels = buttons;\n    } else {\n      buttonLabels = Object.keys(buttons || {});\n    }\n\n    const chosen = remote.dialog.showMessageBox(this.getCurrentWindow(), {\n      type: 'info',\n      message,\n      detail: detailedMessage,\n      buttons: buttonLabels,\n    }\n    );\n\n    if (_.isArray(buttons)) {\n      return chosen;\n    }\n    const callback = buttons[buttonLabels[chosen]];\n    return callback ? callback() : undefined;\n  }\n\n  /*\n  Section: Managing the Dev Tools\n  */\n\n  // Extended: Open the dev tools for the current window.\n  openDevTools() {\n    return ipcRenderer.send('call-webcontents-method', 'openDevTools');\n  }\n\n  isDevToolsOpened() {\n    return this.getCurrentWindow().webContents.isDevToolsOpened()\n  }\n\n  // Extended: Toggle the visibility of the dev tools for the current window.\n  toggleDevTools() {\n    return ipcRenderer.send('call-webcontents-method', 'toggleDevTools');\n  }\n\n  // Extended: Execute code in dev tools.\n  executeJavaScriptInDevTools(code) {\n    return ipcRenderer.send('call-devtools-webcontents-method', 'executeJavaScript', code);\n  }\n\n  /*\n  Section: Private\n  */\n\n  initializeReactRoot() {\n    // Put state back into sheet-container? Restore app state here\n    this.item = document.createElement(\"nylas-workspace\");\n    this.item.setAttribute(\"id\", \"sheet-container\");\n    this.item.setAttribute(\"class\", \"sheet-container\");\n    this.item.setAttribute(\"tabIndex\", \"-1\");\n\n    const React = require(\"react\");\n    const ReactDOM = require(\"react-dom\");\n    const SheetContainer = require('./sheet-container');\n    ReactDOM.render(React.createElement(SheetContainer), this.item);\n    return document.querySelector(this.workspaceViewParentSelector).appendChild(this.item);\n  }\n\n  deserializePackageStates() {\n    this.packages.packageStates = this.savedState.packageStates || {};\n    return delete this.savedState.packageStates;\n  }\n\n  loadConfig() {\n    this.config.setSchema(null, {type: 'object', properties: _.clone(require('./config-schema').default)});\n    return this.config.load();\n  }\n\n  watchThemes() {\n    return this.themes.onDidChangeActiveThemes(() => {\n      // Only reload stylesheets from non-theme packages\n      for (const pack of Array.from(this.packages.getActivePackages())) {\n        if (pack.getType() !== 'theme') {\n          if (typeof pack.reloadStylesheets === 'function') {\n            pack.reloadStylesheets();\n          }\n        }\n      }\n      return null;\n    }\n    );\n  }\n\n  exit(status) {\n    const { app } = remote;\n    app.emit('will-exit');\n    return remote.process.exit(status);\n  }\n\n  showOpenDialog(options, callback) {\n    return callback(remote.dialog.showOpenDialog(this.getCurrentWindow(), options));\n  }\n\n  showSaveDialog(options, callback) {\n    if (options.title == null) { options.title = 'Save File'; }\n    return callback(remote.dialog.showSaveDialog(this.getCurrentWindow(), options));\n  }\n\n  showErrorDialog(messageData, {showInMainWindow, detail} = {}) {\n    let message;\n    let title;\n    if (_.isString(messageData) || _.isNumber(messageData)) {\n      message = messageData;\n      title = \"Error\";\n    } else if (_.isObject(messageData)) {\n      ({ message } = messageData);\n      ({ title } = messageData);\n    } else {\n      throw new Error(\"Must pass a valid message to show dialog\", message);\n    }\n\n    let winToShow = null;\n    if (showInMainWindow) {\n      winToShow = remote.getGlobal('application').getMainWindow();\n    }\n\n    if (!detail) {\n      return remote.dialog.showMessageBox(winToShow, {\n        type: 'warning',\n        buttons: ['Okay'],\n        message: title,\n        detail: message,\n      });\n    }\n    return remote.dialog.showMessageBox(winToShow, {\n      type: 'warning',\n      buttons: ['Okay', 'Show Details'],\n      message: title,\n      detail: message,\n    }, (buttonIndex) => {\n      if (buttonIndex === 1) {\n        const {Actions} = require('nylas-exports');\n        const {CodeSnippet} = require('nylas-component-kit');\n        Actions.openModal({\n          component: CodeSnippet({intro: message, code: detail, className: 'error-details'}),\n          height: 600,\n          width: 800,\n        });\n      }\n    });\n  }\n\n  // Delegate to the browser's process fileListCache\n  fileListCache() {\n    return remote.getGlobal('application').fileListCache;\n  }\n\n  saveSync() {\n    const stateString = JSON.stringify(this.savedState);\n    const statePath = this.constructor.getStatePath();\n    if (statePath) {\n      return fs.writeFileSync(statePath, stateString, 'utf8');\n    }\n    this.getCurrentWindow().loadSettings.windowState = stateString;\n    return stateString;\n  }\n\n  crashMainProcess() {\n    return remote.process.crash();\n  }\n\n  crashRenderProcess() {\n    return process.crash();\n  }\n\n  getUserInitScriptPath() {\n    const initScriptPath = fs.resolve(this.getConfigDirPath(), 'init', ['js', 'coffee']);\n    return initScriptPath != null ? initScriptPath : path.join(this.getConfigDirPath(), 'init.coffee');\n  }\n\n  requireUserInitScript() {\n    const userInitScriptPath = this.getUserInitScriptPath();\n    if (userInitScriptPath) {\n      try {\n        if (fs.isFileSync(userInitScriptPath)) { require(userInitScriptPath); }\n      } catch (error) {\n        console.log(error);\n      }\n    }\n  }\n\n  // Require the module with the given globals.\n  //\n  // The globals will be set on the `window` object and removed after the\n  // require completes.\n  //\n  // * `id` The {String} module name or path.\n  // * `globals` An optinal {Object} to set as globals during require.\n  requireWithGlobals(id, globals = {}) {\n    const existingGlobals = {};\n    for (const key of globals) {\n      const value = globals[key];\n      existingGlobals[key] = window[key];\n      window[key] = value;\n    }\n\n    require(id);\n\n    return (() => {\n      const result = [];\n      for (const key of existingGlobals) {\n        const value = existingGlobals[key];\n        if (value === undefined) {\n          result.push(delete window[key]);\n        } else {\n          result.push(window[key] = value);\n        }\n      }\n      return result;\n    })();\n  }\n\n  // Lets multiple components register beforeUnload callbacks.\n  // The callbacks are expected to return either true or false.\n  //\n  // Note: If you return false to cancel the window close, you /must/ perform\n  // work and then call finishUnload. We do not support cancelling quit!\n  // https://phab.nylas.com/D1932#inline-11722\n  //\n  // Also see logic in browser/NylasWindow::handleEvents where we listen\n  // to the browserWindow.on 'close' event to catch \"unclosable\" windows.\n  onBeforeUnload(callback) {\n    return this.windowEventHandler.addUnloadCallback(callback);\n  }\n\n  removeUnloadCallback(callback) {\n    return this.windowEventHandler.removeUnloadCallback(callback);\n  }\n\n  enhanceEventObject() {\n    const overriddenStop = Event.prototype.stopPropagation;\n    Event.prototype.stopPropagation = function stopPropagation(...args) {\n      this.propagationStopped = true;\n      return overriddenStop.apply(this, args);\n    };\n    Event.prototype.isPropagationStopped = function isPropagationStopped() {\n      return this.propagationStopped;\n    };\n  }\n\n  registerGlobalActions(...args) {\n    if (this.inSpecMode()) { return; }\n    this.actionBridge.registerGlobalActions(...args);\n  }\n}\nNylasEnvConstructor.initClass();\n"
  },
  {
    "path": "packages/client-app/src/package-manager.coffee",
    "content": "path = require 'path'\nurl = require 'url'\n\n_ = require 'underscore'\n{ipcRenderer, remote} = require 'electron'\nEmitterMixin = require('emissary').Emitter\n{Emitter} = require 'event-kit'\nfs = require 'fs-plus'\nQ = require 'q'\n\nActions = require('./flux/actions').default\nPackage = require './package'\nThemePackage = require './theme-package'\nDatabaseStore = require('./flux/stores/database-store').default\nAPMWrapper = require './apm-wrapper'\n\nbasePackagePaths = null\n\n# Extended: Package manager for coordinating the lifecycle of Nylas Mail packages.\n#\n# An instance of this class is always available as the `NylasEnv.packages` global.\n#\n# Packages can be loaded, activated, and deactivated, and unloaded:\n#  * Loading a package reads and parses the package's metadata and resources\n#    such as keymaps, menus, stylesheets, etc.\n#  * Activating a package registers the loaded resources and calls `activate()`\n#    on the package's main module.\n#  * Deactivating a package unregisters the package's resources  and calls\n#    `deactivate()` on the package's main module.\n#  * Unloading a package removes it completely from the package manager.\n#\n# Packages can be enabled/disabled via the `core.disabledPackages` config\n# settings and also by calling `enablePackage()/disablePackage()`.\n#\n# Section: NylasEnv\nmodule.exports =\nclass PackageManager\n  EmitterMixin.includeInto(this)\n\n  constructor: ({configDirPath, @devMode, safeMode, @resourcePath, @specMode}) ->\n    @emitter = new Emitter\n    @onPluginsChanged = _.debounce(@_onPluginsChanged, 200)\n    @packageDirPaths = []\n    if @specMode\n      @packageDirPaths.push(path.join(@resourcePath, \"spec\", \"fixtures\", \"packages\"))\n    else\n      @packageDirPaths.push(path.join(@resourcePath, \"internal_packages\"))\n      if not safeMode\n        if @devMode\n          @packageDirPaths.push(path.join(configDirPath, \"dev\", \"packages\"))\n        @packageDirPaths.push(path.join(configDirPath, \"packages\"))\n\n    @loadedPackages = {}\n    @cachedPackagePluginIds = {}\n    @packagesWithDatabaseObjects = []\n    @activePackages = {}\n    @packageStates = {}\n\n    @packageActivators = []\n    @registerPackageActivator(this, ['nylas'])\n\n    ipcRenderer.on(\"changePluginStateFromUrl\", @_onChangePluginState)\n\n\n  pluginIdFor: (packageName) =>\n    env = NylasEnv.config.get(\"env\")\n    cacheKey = \"#{packageName}:#{env}\"\n\n    if @cachedPackagePluginIds[cacheKey] is undefined\n      @cachedPackagePluginIds[cacheKey] = @_resolvePluginIdFor(packageName, env)\n    return @cachedPackagePluginIds[cacheKey]\n\n  _onChangePluginState: (event, urlToOpen = \"\") =>\n    {query} = url.parse(urlToOpen, true)\n    disabled = NylasEnv.config.get('core.disabledPackages') ? []\n    turnedOn = []\n    turnedOff = []\n    for name, state of query\n      continue if /-displayName/gi.test(name)\n      displayName = query[\"#{name}-displayName\"] ? name\n      if state is \"off\" and name not in disabled\n        turnedOff.push(displayName)\n        if name not in disabled then disabled.push(name)\n      else if state is \"on\"\n        turnedOn.push(displayName)\n        disabled = _.without(disabled, name)\n    NylasEnv.config.set('core.disabledPackages', disabled)\n    if NylasEnv.isMainWindow() then NylasEnv.focus()\n    if turnedOn.length > 0 then @_notifyPluginsChanged(turnedOn, \"enabled\")\n    if turnedOff.length > 0 then @_notifyPluginsChanged(turnedOff, \"disabled\")\n\n  _notifyPluginsChanged: (names, dir) =>\n    if names.length >= 2\n      last = names[names.length - 1]\n      names[names.length - 1] = \"and #{last}\"\n    has = if names.length is 1 then \"has\" else \"have\"\n    pluginText = if names.length is 1 then \"Plugin\" else \"Plugins\"\n    setTimeout =>\n      remote.dialog.showMessageBox(remote.getCurrentWindow(), {\n        type: 'info',\n        message: \"#{pluginText} #{dir}\",\n        detail: \"#{names.join(\", \")} #{has} been #{dir}\"\n        buttons: ['Thanks'],\n      })\n    , 500\n\n  _resolvePluginIdFor: (packageName, env) =>\n    metadata = @loadedPackages[packageName]?.metadata\n\n    unless metadata\n      packagePath = @resolvePackagePath(packageName)\n      return null unless packagePath\n      metadata = Package.loadMetadata(packagePath)\n\n    return metadata.name if metadata\n    return null\n\n  ###\n  Section: Event Subscription\n  ###\n\n  # Public: Invoke the given callback when all packages have been loaded.\n  #\n  # * `callback` {Function}\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidLoadInitialPackages: (callback) ->\n    @emitter.on 'did-load-initial-packages', callback\n\n  # Public: Invoke the given callback when all packages have been activated.\n  #\n  # * `callback` {Function}\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidActivateInitialPackages: (callback) ->\n    @emitter.on 'did-activate-initial-packages', callback\n\n  # Public: Invoke the given callback when a package is activated.\n  #\n  # * `callback` A {Function} to be invoked when a package is activated.\n  #   * `package` The {Package} that was activated.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidActivatePackage: (callback) ->\n    @emitter.on 'did-activate-package', callback\n\n  # Public: Invoke the given callback when a package is deactivated.\n  #\n  # * `callback` A {Function} to be invoked when a package is deactivated.\n  #   * `package` The {Package} that was deactivated.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidDeactivatePackage: (callback) ->\n    @emitter.on 'did-deactivate-package', callback\n\n  # Public: Invoke the given callback when a package is loaded.\n  #\n  # * `callback` A {Function} to be invoked when a package is loaded.\n  #   * `package` The {Package} that was loaded.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidLoadPackage: (callback) ->\n    @emitter.on 'did-load-package', callback\n\n  # Public: Invoke the given callback when a package is unloaded.\n  #\n  # * `callback` A {Function} to be invoked when a package is unloaded.\n  #   * `package` The {Package} that was unloaded.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidUnloadPackage: (callback) ->\n    @emitter.on 'did-unload-package', callback\n\n  ###\n  Section: Package system data\n  ###\n\n  # Public: Get the path to the apm command.\n  #\n  # Return a {String} file path to apm.\n  getApmPath: ->\n    return @apmPath if @apmPath?\n\n    commandName = 'apm'\n    commandName += '.cmd' if process.platform is 'win32'\n\n    @apmPath = path.join(process.resourcesPath, 'apm', 'bin', commandName)\n    if not fs.isFileSync(@apmPath)\n      @apmPath = path.join(@resourcePath, 'apm', 'bin', commandName)\n    if not fs.isFileSync(@apmPath)\n      @apmPath = path.join(@resourcePath, 'apm', 'node_modules', 'atom-package-manager', 'bin', commandName)\n    @apmPath\n\n  # Public: Get the paths being used to look for packages.\n  #\n  # Returns an {Array} of {String} directory paths.\n  getPackageDirPaths: ->\n    _.clone(@packageDirPaths)\n\n  ###\n  Section: General package data\n  ###\n\n  # Public: Resolve the given package name to a path on disk.\n  #\n  # * `name` - The {String} package name.\n  #\n  # Return a {String} folder path or undefined if it could not be resolved.\n  resolvePackagePath: (name) ->\n    packagePath = fs.resolve(@packageDirPaths..., name)\n    return packagePath if fs.isDirectorySync(packagePath)\n\n    packagePath = path.join(@resourcePath, 'node_modules', name)\n    return packagePath if @hasNylasEngine(packagePath)\n\n  # Public: Is the package with the given name bundled with Nylas?\n  #\n  # * `name` - The {String} package name.\n  #\n  # Returns a {Boolean}.\n  isBundledPackage: (name) ->\n    @getPackageDependencies().hasOwnProperty(name)\n\n  ###\n  Section: Enabling and disabling packages\n  ###\n\n  # Public: Enable the package with the given name.\n  #\n  # Returns the {Package} that was enabled or null if it isn't loaded.\n  enablePackage: (name) ->\n    pack = @loadPackage(name)\n    pack?.enable()\n    pack\n\n  # Public: Disable the package with the given name.\n  #\n  # Returns the {Package} that was disabled or null if it isn't loaded.\n  disablePackage: (name) ->\n    pack = @loadPackage(name)\n    pack?.disable()\n    pack\n\n  # Public: Is the package with the given name disabled?\n  #\n  # * `name` - The {String} package name.\n  #\n  # Returns a {Boolean}.\n  isPackageDisabled: (name) ->\n    _.include(NylasEnv.config.get('core.disabledPackages') ? [], name)\n\n  ###\n  Section: Accessing active packages\n  ###\n\n  # Public: Get an {Array} of all the active {Package}s.\n  getActivePackages: ->\n    _.values(@activePackages)\n\n  # Public: Get the active {Package} with the given name.\n  #\n  # * `name` - The {String} package name.\n  #\n  # Returns a {Package} or undefined.\n  getActivePackage: (name) ->\n    @activePackages[name]\n\n  # Public: Is the {Package} with the given name active?\n  #\n  # * `name` - The {String} package name.\n  #\n  # Returns a {Boolean}.\n  isPackageActive: (name) ->\n    @getActivePackage(name)?\n\n  ###\n  Section: Accessing loaded packages\n  ###\n\n  # Public: Get an {Array} of all the loaded {Package}s\n  getLoadedPackages: ->\n    _.values(@loadedPackages)\n\n  # Get packages for a certain package type\n  #\n  # * `types` an {Array} of {String}s like ['nylas', 'my-package'].\n  getLoadedPackagesForTypes: (types) ->\n    pack for pack in @getLoadedPackages() when pack.getType() in types\n\n  # Public: Get the loaded {Package} with the given name.\n  #\n  # * `name` - The {String} package name.\n  #\n  # Returns a {Package} or undefined.\n  getLoadedPackage: (name) ->\n    @loadedPackages[name]\n\n  # Public: Gets the root paths of all loaded packages.\n  #\n  # Useful when determining if an error originated from a package.\n  getPluginIdsByPathBase: ->\n    pluginIdsByPathBase = {}\n    for name, pack of @loadedPackages\n      pathBase = _.last(pack.path.split(\"/\"))\n\n      if pack.pluginId() and pack.pluginId() isnt name\n        id = \"#{name}-#{pack.pluginId()}\"\n      else\n        id = pack.pluginId()\n\n      pluginIdsByPathBase[pathBase] = id\n    return pluginIdsByPathBase\n\n  # Public: Is the package with the given name loaded?\n  #\n  # * `name` - The {String} package name.\n  #\n  # Returns a {Boolean}.\n  isPackageLoaded: (name) ->\n    @getLoadedPackage(name)?\n\n  ###\n  Section: Accessing available packages\n  ###\n\n  # Public: Get an {Array} of {String}s of all the available package paths.\n  #\n  # If the optional windowType is passed, it will only load packages\n  # that declare that windowType in their package.json\n  getAvailablePackagePaths: (windowType) ->\n    packagePaths = []\n\n    loadPackagesWhenNoTypesSpecified = windowType is 'default'\n\n    basePackagePaths ?= NylasEnv.fileListCache().basePackagePaths ? []\n    if basePackagePaths.length is 0\n      for packageDirPath in @packageDirPaths\n        for packagePath in fs.listSync(packageDirPath)\n          # Ignore files in package directory\n          continue unless fs.isDirectorySync(packagePath)\n          # Ignore .git in package directory\n          continue if path.basename(packagePath)[0] is '.'\n          packagePaths.push(packagePath)\n      basePackagePaths = packagePaths\n      cache = NylasEnv.fileListCache()\n      cache.basePackagePaths = basePackagePaths\n    else\n      packagePaths = basePackagePaths\n\n    if windowType\n      packagePaths = _.filter packagePaths, (packagePath) ->\n        try\n          metadata = Package.loadMetadata(packagePath) ? {}\n\n          if not (metadata.engines?.nylas)\n            console.error(\"INVALID PACKAGE: Your package at #{packagePath} does not have a properly formatted `package.json`. You must include an {'engines': {'nylas': version}} property\")\n\n          {windowTypes} = metadata\n          if windowTypes\n            return windowTypes[windowType]? or windowTypes[\"all\"]?\n          else if loadPackagesWhenNoTypesSpecified\n            return true\n          return false\n        catch\n          return false\n\n    packagesPath = path.join(@resourcePath, 'node_modules')\n    for packageName, packageVersion of @getPackageDependencies()\n      packagePath = path.join(packagesPath, packageName)\n      packagePaths.push(packagePath) if fs.isDirectorySync(packagePath)\n\n    _.uniq(packagePaths)\n\n  # Public: Get an {Array} of {String}s of all the available package names.\n  getAvailablePackageNames: ->\n    _.uniq _.map @getAvailablePackagePaths(), (packagePath) -> path.basename(packagePath)\n\n  # Public: Get an {Array} of {String}s of all the available package metadata.\n  getAvailablePackageMetadata: ->\n    packages = []\n    for packagePath in @getAvailablePackagePaths()\n      name = path.basename(packagePath)\n      metadata = @getLoadedPackage(name)?.metadata ? Package.loadMetadata(packagePath, true)\n      packages.push(metadata)\n    packages\n\n  installPackageFromPath: (packageSourceDir, callback) ->\n    jsonPath = path.join(packageSourceDir, 'package.json')\n    if not fs.existsSync(jsonPath)\n      return callback(new Error(\"The folder you selected doesn't look like a valid N1 plugin. All N1 plugins must have a package.json file in the top level of the folder. Check the contents of #{packageSourceDir} and try again.\"), null)\n\n    try\n      json = JSON.parse(fs.readFileSync(jsonPath))\n    catch e\n      return callback(e, null)\n\n    if not json.name\n      return callback(new Error(\"The package.json file must contain a valid `name` value.\"), null)\n\n    packagesDir = path.join(NylasEnv.getConfigDirPath(), 'packages')\n    packageName = json.name\n    packageTargetDir = path.join(packagesDir, packageName)\n\n    fs.makeTree packagesDir, (err) =>\n      return callback(err, null) if err\n\n      fs.exists packageTargetDir, (packageAlreadyExists) =>\n        if packageAlreadyExists\n          return callback(new Error(\"A package named '#{packageName}' is already installed in ~/.nylas-mail/packages.\"), null)\n\n        fs.copySync(packageSourceDir, packageTargetDir)\n\n        apm = new APMWrapper()\n        apm.installDependenciesInPackageDirectory packageTargetDir, (err) =>\n          return callback(err, packageTargetDir) if err\n          @enablePackage(packageTargetDir)\n          @activatePackage(packageName)\n          callback(null, packageName)\n\n  ###\n  Section: Private\n  ###\n\n  getPackageState: (name) ->\n    @packageStates[name]\n\n  setPackageState: (name, state) ->\n    @packageStates[name] = state\n\n  getPackageDependencies: ->\n    unless @packageDependencies?\n      try\n        metadataPath = path.join(@resourcePath, 'package.json')\n        {@packageDependencies} = JSON.parse(fs.readFileSync(metadataPath)) ? {}\n      @packageDependencies ?= {}\n\n    @packageDependencies\n\n  hasNylasEngine: (packagePath) ->\n    metadata = Package.loadMetadata(packagePath, true)\n    metadata?.engines?.nylas?\n\n  unobserveDisabledPackages: ->\n    @disabledPackagesSubscription?.dispose()\n    @disabledPackagesSubscription = null\n\n  observeDisabledPackages: ->\n    @disabledPackagesSubscription ?= NylasEnv.config.onDidChange 'core.disabledPackages', ({newValue, oldValue}) =>\n      packagesToEnable = _.difference(oldValue, newValue)\n      packagesToDisable = _.difference(newValue, oldValue)\n\n      @deactivatePackage(packageName) for packageName in packagesToDisable when @getActivePackage(packageName)\n\n      for packageName in packagesToEnable\n        @loadPackage(packageName)\n\n      @refreshDatabaseSchema()\n\n      for packageName in packagesToEnable\n        @activatePackage(packageName)\n\n      null\n\n  # This lets us report the active plugins in the main window (since plugins\n  # are window-specific). Useful for letting the worker window know what\n  # plugins are installed in the main window.\n  _onPluginsChanged: =>\n    return unless NylasEnv.isMainWindow()\n    # All active plugins, core optional, core required, and 3rd party\n    activePluginNames = @getActivePackages().map((p) -> p.name)\n\n    # Only active 3rd party plugins\n    activeThirdPartyPluginNames = @getActivePackages().filter((p) ->\n      (p.path?.indexOf('internal_packages') is -1 and\n      p.path?.indexOf('nylas-private') is -1)\n    ).map((p) -> p.name)\n\n    # Only active core optional, and core required plugins\n    activeCorePluginNames = _.difference(activePluginNames, activeThirdPartyPluginNames)\n\n    # All plugins (3rd party and core optional) that have the {optional: true}\n    # flag.  If it's an internal_packages core package, it'll show up in\n    # preferences.\n    optionalPluginNames = @getAvailablePackageMetadata()\n      .filter(({isOptional}) -> isOptional)\n      .map((p) -> p.name)\n\n    activeCoreOptionalPluginNames = _.intersection(activeCorePluginNames, optionalPluginNames)\n\n    Actions.notifyPluginsChanged({\n      allActivePluginNames: activePluginNames\n      coreActivePluginNames: activeCoreOptionalPluginNames\n      thirdPartyActivePluginNames: activeThirdPartyPluginNames\n    })\n\n  # If a windowType is passed, we'll only load packages who declare that\n  # windowType as `true` in their package.json file.\n  loadPackages: (windowType) ->\n    packagePaths = @getAvailablePackagePaths(windowType)\n\n    packagePaths = packagePaths.filter (packagePath) => not @isPackageDisabled(path.basename(packagePath))\n    packagePaths = _.uniq packagePaths, (packagePath) -> path.basename(packagePath)\n    @loadPackage(packagePath) for packagePath in packagePaths\n    @emit 'loaded'\n    @emitter.emit 'did-load-initial-packages'\n\n  loadPackage: (nameOrPath) ->\n    return pack if pack = @getLoadedPackage(nameOrPath)\n\n    if packagePath = @resolvePackagePath(nameOrPath)\n      name = path.basename(nameOrPath)\n      return pack if pack = @getLoadedPackage(name)\n\n      try\n        metadata = Package.loadMetadata(packagePath) ? {}\n        if metadata.theme\n          pack = new ThemePackage(packagePath, metadata)\n        else\n          pack = new Package(packagePath, metadata)\n\n        if metadata.supportedEnvs && !metadata.supportedEnvs.includes(NylasEnv.config.get('env'))\n          return null\n\n        pack.load()\n        if pack.declaresNewDatabaseObjects\n          @packagesWithDatabaseObjects.push pack\n        @loadedPackages[pack.name] = pack\n        @emitter.emit 'did-load-package', pack\n        @onPluginsChanged()\n        return pack\n      catch error\n        console.warn \"Failed to load package.json '#{path.basename(packagePath)}'\"\n        console.warn error.stack ? error\n    else\n      console.warn \"Could not resolve '#{nameOrPath}' to a package path\"\n    null\n\n  unloadPackages: ->\n    @unloadPackage(name) for name in Object.keys(@loadedPackages)\n    null\n\n  unloadPackage: (name) ->\n    if @isPackageActive(name)\n      throw new Error(\"Tried to unload active package '#{name}'\")\n\n    if pack = @getLoadedPackage(name)\n      delete @loadedPackages[pack.name]\n      @emitter.emit 'did-unload-package', pack\n      @onPluginsChanged()\n    else\n      throw new Error(\"No loaded package for name '#{name}'\")\n\n  # Activate all the packages that should be activated.\n  activate: ->\n    promises = []\n    for [activator, types] in @packageActivators\n      packages = @getLoadedPackagesForTypes(types)\n      promises = promises.concat(activator.activatePackages(packages))\n    Q.all(promises).then =>\n      @emit 'activated'\n      @emitter.emit 'did-activate-initial-packages'\n\n  # another type of package manager can handle other package types.\n  # See ThemeManager\n  registerPackageActivator: (activator, types) ->\n    @packageActivators.push([activator, types])\n\n  activatePackages: (packages) ->\n    promises = []\n    NylasEnv.config.transact =>\n      for pack in packages\n        @loadPackage(pack.name)\n\n      setupPromise = @refreshDatabaseSchema()\n\n      for pack in packages\n        promise = @activatePackage(pack.name, setupPromise)\n        promises.push(promise)\n    @observeDisabledPackages()\n    promises\n\n  # When packages load they can declare new DatabaseObjects that need to\n  # be setup in the Database. It's important that the Database starts\n  # getting setup before packages activate so any DB queries in the\n  # `activate` methods get properly queued then executed.\n  #\n  # When a package with database-altering changes loads, it will put an\n  # entry in `packagesWithDatabaseObjects`.\n  refreshDatabaseSchema: ->\n    if @packagesWithDatabaseObjects.length > 0\n      return DatabaseStore.refreshDatabaseSchema().then =>\n        @packagesWithDatabaseObjects = []\n    return Promise.resolve()\n\n  # Activate a single package by name\n  activatePackage: (name, setupPromise = Promise.resolve()) ->\n    if pack = @getActivePackage(name)\n      Q(pack)\n    else if pack = @loadPackage(name)\n      setupPromise.then =>\n        pack.activate().then =>\n          @activePackages[pack.name] = pack\n          @emitter.emit 'did-activate-package', pack\n          @onPluginsChanged()\n          pack\n    else\n      Q.reject(new Error(\"Failed to load package '#{name}'\"))\n\n  # Deactivate all packages\n  deactivatePackages: ->\n    NylasEnv.config.transact =>\n      @deactivatePackage(pack.name) for pack in @getLoadedPackages()\n    @unobserveDisabledPackages()\n\n  # Deactivate the package with the given name\n  deactivatePackage: (name) ->\n    pack = @getLoadedPackage(name)\n    if @isPackageActive(name)\n      @setPackageState(pack.name, state) if state = pack.serialize?()\n    pack.deactivate()\n    delete @activePackages[pack.name]\n    @emitter.emit 'did-deactivate-package', pack\n    @onPluginsChanged()\n"
  },
  {
    "path": "packages/client-app/src/package.coffee",
    "content": "path = require 'path'\n\n_ = require 'underscore'\nasync = require 'async'\nfs = require 'fs-plus'\nEmitterMixin = require('emissary').Emitter\n{Emitter, CompositeDisposable} = require 'event-kit'\nQ = require 'q'\n\nModuleCache = require './module-cache'\n\nTaskRegistry = require('./registries/task-registry').default\nDatabaseObjectRegistry = require('./registries/database-object-registry').default\n\ntry\n  packagesCache = require('../package.json')?._N1Packages ? {}\ncatch error\n  packagesCache = {}\n\n# Loads and activates a package's main module and resources such as\n# stylesheets, keymaps, and menus.\nmodule.exports =\nclass Package\n  EmitterMixin.includeInto(this)\n\n  @isBundledPackagePath: (packagePath) ->\n    if NylasEnv.packages.devMode\n      return false unless NylasEnv.packages.resourcePath.startsWith(\"#{process.resourcesPath}#{path.sep}\")\n\n    @resourcePathWithTrailingSlash ?= \"#{NylasEnv.packages.resourcePath}#{path.sep}\"\n    packagePath?.startsWith(@resourcePathWithTrailingSlash)\n\n  @loadMetadata: (packagePath, ignoreErrors=false) ->\n    packageName = path.basename(packagePath)\n    if @isBundledPackagePath(packagePath)\n      metadata = packagesCache[packageName]?.metadata\n    unless metadata?\n      metadataPath = fs.resolve(path.join(packagePath, 'package.json'))\n      if fs.existsSync(metadataPath)\n        try\n          metadata = JSON.parse(fs.readFileSync(metadataPath))\n        catch error\n          throw error unless ignoreErrors\n    metadata ?= {}\n    metadata.name = packageName\n\n    if metadata.stylesheets?\n      metadata.styleSheets = metadata.stylesheets\n\n    metadata\n\n  keymaps: null\n  menus: null\n  stylesheets: null\n  stylesheetDisposables: null\n  mainModulePath: null\n  resolvedMainModulePath: false\n  mainModule: null\n\n  ###\n  Section: Construction\n  ###\n\n  constructor: (@path, @metadata) ->\n    @emitter = new Emitter\n    @metadata ?= Package.loadMetadata(@path)\n    @bundledPackage = Package.isBundledPackagePath(@path)\n    @name = @metadata?.name ? path.basename(@path)\n    @pluginAppId = @name\n\n    @displayName = @metadata?.displayName || @name\n    ModuleCache.add(@path, @metadata)\n    @reset()\n    @declaresNewDatabaseObjects = false\n\n  # TODO FIXME: Use a unique pluginID instead of just the \"name\"\n  # This needs to be included here to prevent a circular dependency error\n  pluginId: -> return @pluginAppId ? @name\n\n  ###\n  Section: Event Subscription\n  ###\n\n  # Essential: Invoke the given callback when all packages have been activated.\n  #\n  # * `callback` {Function}\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.\n  onDidDeactivate: (callback) ->\n    @emitter.on 'did-deactivate', callback\n\n  ###\n  Section: Instance Methods\n  ###\n\n  enable: ->\n    NylasEnv.config.removeAtKeyPath('core.disabledPackages', @name)\n\n  disable: ->\n    NylasEnv.config.pushAtKeyPath('core.disabledPackages', @name)\n\n  isTheme: ->\n    @metadata?.theme?\n\n  measure: (key, fn) ->\n    startTime = Date.now()\n    value = fn()\n    @[key] = Date.now() - startTime\n    value\n\n  getType: -> 'nylas'\n\n  getStyleSheetPriority: -> 0\n\n  load: ->\n    @measure 'loadTime', =>\n      try\n        @declaresNewDatabaseObjects = false\n        @loadKeymaps()\n        @loadMenus()\n        @loadStylesheets()\n        mainModule = @requireMainModule()\n        return unless mainModule\n        @registerModelConstructors(mainModule.modelConstructors)\n        @registerTaskConstructors(mainModule.taskConstructors)\n\n      catch error\n        console.warn \"Failed to load package named '#{@name}'\"\n        console.warn error.stack ? error\n        console.error(error.message, error)\n    this\n\n  registerModelConstructors: (constructors=[]) ->\n    if constructors.length > 0\n      @declaresNewDatabaseObjects = true\n\n      _.each constructors, (constructor) ->\n        constructorFactory = -> constructor\n        DatabaseObjectRegistry.register(constructor.name, constructorFactory)\n\n  registerTaskConstructors: (constructors=[]) ->\n    _.each constructors, (constructor) ->\n      constructorFactory = -> constructor\n      TaskRegistry.register(constructor.name, constructorFactory)\n\n  reset: ->\n    @stylesheets = []\n    @keymaps = []\n    @menus = []\n\n  activate: ->\n    unless @activationDeferred?\n      @activationDeferred = Q.defer()\n      @measure 'activateTime', =>\n        @activateResources()\n        @activateNow()\n\n    Q.all([@activationDeferred.promise])\n\n  activateNow: ->\n    try\n      @activateConfig()\n      @activateStylesheets()\n      if @requireMainModule()\n        localState = NylasEnv.packages.getPackageState(@name) ? {}\n        @mainModule.activate(localState)\n        @mainActivated = true\n    catch e\n      console.error e.message\n      console.error e.stack\n      console.warn \"Failed to activate package named '#{@name}'\", e.stack\n\n    @activationDeferred?.resolve()\n\n  activateConfig: ->\n    return if @configActivated\n\n    @requireMainModule()\n    if @mainModule?\n      if @mainModule.config? and typeof @mainModule.config is 'object'\n        NylasEnv.config.setSchema @name, {type: 'object', properties: @mainModule.config}\n      else if @mainModule.configDefaults? and typeof @mainModule.configDefaults is 'object'\n        NylasEnv.config.setDefaults(@name, @mainModule.configDefaults)\n      @mainModule.activateConfig?()\n    @configActivated = true\n\n  activateStylesheets: ->\n    return if @stylesheetsActivated\n\n    @stylesheetDisposables = new CompositeDisposable\n\n    priority = @getStyleSheetPriority()\n    for [sourcePath, source] in @stylesheets\n      if match = path.basename(sourcePath).match(/[^.]*\\.([^.]*)\\./)\n        context = match[1]\n      else\n        context = undefined\n\n      @stylesheetDisposables.add(NylasEnv.styles.addStyleSheet(source, {sourcePath, priority, context}))\n    @stylesheetsActivated = true\n\n  activateResources: ->\n    @activationDisposables = new CompositeDisposable\n    @activationDisposables.add(NylasEnv.keymaps.loadKeymap(keymapPath, map)) for [keymapPath, map] in @keymaps\n    @activationDisposables.add(NylasEnv.menu.add(map['menu'])) for [menuPath, map] in @menus when map['menu']?\n\n  loadKeymaps: ->\n    try\n      if @bundledPackage and packagesCache[@name]?\n        @keymaps = ([\"#{NylasEnv.packages.resourcePath}#{path.sep}#{keymapPath}\", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)\n      else\n        @keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, JSON.parse(fs.readFileSync(keymapPath)) ? {}]\n    catch e\n      console.error \"Error reading keymaps for package '#{@name}': #{e.message}\", e.stack\n\n  loadMenus: ->\n    try\n      if @bundledPackage and packagesCache[@name]?\n        @menus = ([\"#{NylasEnv.packages.resourcePath}#{path.sep}#{menuPath}\", menuObject] for menuPath, menuObject of packagesCache[@name].menus)\n      else\n        @menus = @getMenuPaths().map (menuPath) -> [menuPath, JSON.parse(fs.readFileSync(menuPath)) ? {}]\n    catch e\n      console.error \"Error reading menus for package '#{@name}': #{e.message}\", e.stack\n\n    for [menuPath, menuObj] in @menus\n      menuItem.isOptional = @metadata.isOptional for menuItem in menuObj.menu\n\n  getKeymapPaths: ->\n    keymapsDirPath = path.join(@path, 'keymaps')\n    if @metadata.keymaps\n      @metadata.keymaps.map (name) -> fs.resolve(keymapsDirPath, name, ['json', ''])\n    else\n      fs.listSync(keymapsDirPath, ['json'])\n\n  getMenuPaths: ->\n    menusDirPath = path.join(@path, 'menus')\n    if @metadata.menus\n      @metadata.menus.map (name) -> fs.resolve(menusDirPath, name, ['json', ''])\n    else\n      fs.listSync(menusDirPath, ['json'])\n\n  loadStylesheets: ->\n    @stylesheets = @getStylesheetPaths().map (stylesheetPath) ->\n      [stylesheetPath, NylasEnv.themes.loadStylesheet(stylesheetPath, true)]\n\n  getStylesheetsPath: ->\n    if fs.isDirectorySync(path.join(@path, 'stylesheets'))\n      path.join(@path, 'stylesheets')\n    else\n      path.join(@path, 'styles')\n\n  getStylesheetPaths: ->\n    stylesheetDirPath = @getStylesheetsPath()\n    if @metadata.mainStyleSheet\n      [fs.resolve(@path, @metadata.mainStyleSheet)]\n    else if @metadata.styleSheets\n      @metadata.styleSheets.map (name) -> fs.resolve(stylesheetDirPath, name, ['css', 'less', ''])\n    else if indexStylesheet = fs.resolve(@path, 'index', ['css', 'less'])\n      [indexStylesheet]\n    else\n      _.filter fs.listSync(stylesheetDirPath, ['css', 'less']), (file) ->\n        path.basename(file)[0] isnt '.'\n\n  serialize: ->\n    if @mainActivated\n      try\n        @mainModule?.serialize?()\n      catch e\n        console.error \"Error serializing package '#{@name}'\", e.stack\n\n  deactivate: ->\n    @activationDeferred?.reject()\n    @activationDeferred = null\n    @deactivateResources()\n    @deactivateConfig()\n    if @mainActivated\n      try\n        @mainModule?.deactivate?()\n      catch e\n        console.error \"Error deactivating package '#{@name}'\", e.stack\n    @emit 'deactivated'\n    @emitter.emit 'did-deactivate'\n\n  deactivateConfig: ->\n    @mainModule?.deactivateConfig?()\n    @configActivated = false\n\n  deactivateResources: ->\n    @stylesheetDisposables?.dispose()\n    @activationDisposables?.dispose()\n    @stylesheetsActivated = false\n\n  reloadStylesheets: ->\n    oldSheets = _.clone(@stylesheets)\n    @loadStylesheets()\n    @stylesheetDisposables?.dispose()\n    @stylesheetDisposables = new CompositeDisposable\n    @stylesheetsActivated = false\n    @activateStylesheets()\n\n  requireMainModule: ->\n    return @mainModule if @mainModule?\n    unless @isCompatible()\n      console.warn \"\"\"\n        Failed to require the main module of '#{@name}' because it requires an incompatible native module.\n        Run `apm rebuild` in the package directory to resolve.\n      \"\"\"\n      return\n    mainModulePath = @getMainModulePath()\n    if fs.isFileSync(mainModulePath)\n      @mainModule = require(mainModulePath)\n    return @mainModule\n\n  getMainModulePath: ->\n    return @mainModulePath if @resolvedMainModulePath\n    @resolvedMainModulePath = true\n\n    if @bundledPackage and packagesCache[@name]?\n      if packagesCache[@name].main\n        @mainModulePath = \"#{NylasEnv.packages.resourcePath}#{path.sep}#{packagesCache[@name].main}\"\n        @mainModulePath = fs.resolveExtension(@mainModulePath, [\"\", Object.keys(require.extensions)...])\n      else\n        @mainModulePath = null\n    else\n      mainModulePath =\n        if @metadata.main\n          path.join(@path, @metadata.main)\n        else\n          path.join(@path, 'index')\n      @mainModulePath = fs.resolveExtension(mainModulePath, [\"\", Object.keys(require.extensions)...])\n\n  isNativeModule: (modulePath) ->\n    try\n      fs.listSync(path.join(modulePath, 'build', 'Release'), ['.node']).length > 0\n    catch error\n      false\n\n  # Get an array of all the native modules that this package depends on.\n  # This will recurse through all dependencies.\n  getNativeModuleDependencyPaths: ->\n    nativeModulePaths = []\n\n    traversePath = (nodeModulesPath) =>\n      try\n        for modulePath in fs.listSync(nodeModulesPath)\n          nativeModulePaths.push(modulePath) if @isNativeModule(modulePath)\n          traversePath(path.join(modulePath, 'node_modules'))\n\n    traversePath(path.join(@path, 'node_modules'))\n    nativeModulePaths\n\n  # Get the incompatible native modules that this package depends on.\n  # This recurses through all dependencies and requires all modules that\n  # contain a `.node` file.\n  #\n  # This information is cached in local storage on a per package/version basis\n  # to minimize the impact on startup time.\n  getIncompatibleNativeModules: ->\n    localStorageKey = \"installed-packages:#{@name}:#{@metadata.version}\"\n    unless NylasEnv.inDevMode()\n      try\n        {incompatibleNativeModules} = JSON.parse(global.localStorage.getItem(localStorageKey)) ? {}\n      return incompatibleNativeModules if incompatibleNativeModules?\n\n    incompatibleNativeModules = []\n    for nativeModulePath in @getNativeModuleDependencyPaths()\n      try\n        require(nativeModulePath)\n      catch error\n        try\n          version = require(\"#{nativeModulePath}/package.json\").version\n        incompatibleNativeModules.push\n          path: nativeModulePath\n          name: path.basename(nativeModulePath)\n          version: version\n          error: error.message\n\n    global.localStorage.setItem(localStorageKey, JSON.stringify({incompatibleNativeModules}))\n    incompatibleNativeModules\n\n  # Public: Is this package compatible with this version of N1?\n  #\n  # Incompatible packages cannot be activated. This will include packages\n  # installed to ~/.nylas-mail/packages that were built against node 0.11.10 but\n  # now need to be upgrade to node 0.11.13.\n  #\n  # Returns a {Boolean}, true if compatible, false if incompatible.\n  isCompatible: ->\n    return @compatible if @compatible?\n\n    if @path.indexOf(path.join(NylasEnv.packages.resourcePath, 'node_modules') + path.sep) is 0\n      # Bundled packages are always considered compatible\n      @compatible = true\n    else if packageMain = @getMainModulePath()\n      @incompatibleModules = @getIncompatibleNativeModules()\n      @compatible = @incompatibleModules.length is 0\n    else\n      @compatible = true\n"
  },
  {
    "path": "packages/client-app/src/priority-ui-coordinator.es6",
    "content": "import { generateTempId } from './flux/models/utils';\n\n// A small object that keeps track of the current animation state of the\n// application. You can use it to defer work until animations have finished.\n// Integrated with our fork of ReactCSSTransitionGroup\n//\n//  PriorityUICoordinator.settle.then ->\n//   # Do something expensive\n//\nclass PriorityUICoordinator {\n  constructor() {\n    this.tasks = {};\n    this.settle = Promise.resolve();\n    setInterval(() => this.detectOrphanedTasks(), 1000);\n  }\n\n  beginPriorityTask() {\n    if (Object.keys(this.tasks).length === 0) {\n      this.settle = new Promise((resolve) => {\n        this.settlePromiseResolve = resolve;\n      });\n    }\n\n    const id = generateTempId();\n    this.tasks[id] = Date.now();\n    return id;\n  }\n\n  endPriorityTask(id) {\n    if (!id) {\n      throw new Error(\"You must provide a task id to endPriorityTask\");\n    }\n    delete this.tasks[id];\n    if (Object.keys(this.tasks).length === 0) {\n      if (this.settlePromiseResolve) {\n        this.settlePromiseResolve();\n      }\n      this.settlePromiseResolve = null;\n    }\n  }\n\n  detectOrphanedTasks() {\n    const now = Date.now();\n    const threshold = 15000; // milliseconds\n\n    for (const id of Object.keys(this.tasks)) {\n      const timestamp = this.tasks[id];\n      if (now - timestamp > threshold) {\n        console.log(`PriorityUICoordinator detected oprhaned priority task lasting ${threshold}ms. Ending.`);\n        this.endPriorityTask(id);\n      }\n    }\n  }\n\n  busy() {\n    return Object.keys(this.tasks).length > 0;\n  }\n}\n\nexport default new PriorityUICoordinator();\n"
  },
  {
    "path": "packages/client-app/src/regexp-utils.coffee",
    "content": "_ = require('underscore')\nEmojiData = require('emoji-data')\n\nUnicodeEmailChars = '\\u0080-\\u00FF\\u0100-\\u017F\\u0180-\\u024F\\u0250-\\u02AF\\u0300-\\u036F\\u0370-\\u03FF\\u0400-\\u04FF\\u0500-\\u052F\\u0530-\\u058F\\u0590-\\u05FF\\u0600-\\u06FF\\u0700-\\u074F\\u0750-\\u077F\\u0780-\\u07BF\\u07C0-\\u07FF\\u0900-\\u097F\\u0980-\\u09FF\\u0A00-\\u0A7F\\u0A80-\\u0AFF\\u0B00-\\u0B7F\\u0B80-\\u0BFF\\u0C00-\\u0C7F\\u0C80-\\u0CFF\\u0D00-\\u0D7F\\u0D80-\\u0DFF\\u0E00-\\u0E7F\\u0E80-\\u0EFF\\u0F00-\\u0FFF\\u1000-\\u109F\\u10A0-\\u10FF\\u1100-\\u11FF\\u1200-\\u137F\\u1380-\\u139F\\u13A0-\\u13FF\\u1400-\\u167F\\u1680-\\u169F\\u16A0-\\u16FF\\u1700-\\u171F\\u1720-\\u173F\\u1740-\\u175F\\u1760-\\u177F\\u1780-\\u17FF\\u1800-\\u18AF\\u1900-\\u194F\\u1950-\\u197F\\u1980-\\u19DF\\u19E0-\\u19FF\\u1A00-\\u1A1F\\u1B00-\\u1B7F\\u1D00-\\u1D7F\\u1D80-\\u1DBF\\u1DC0-\\u1DFF\\u1E00-\\u1EFF\\u1F00-\\u1FFF\\u20D0-\\u20FF\\u2100-\\u214F\\u2C00-\\u2C5F\\u2C60-\\u2C7F\\u2C80-\\u2CFF\\u2D00-\\u2D2F\\u2D30-\\u2D7F\\u2D80-\\u2DDF\\u2F00-\\u2FDF\\u2FF0-\\u2FFF\\u3040-\\u309F\\u30A0-\\u30FF\\u3100-\\u312F\\u3130-\\u318F\\u3190-\\u319F\\u31C0-\\u31EF\\u31F0-\\u31FF\\u3200-\\u32FF\\u3300-\\u33FF\\u3400-\\u4DBF\\u4DC0-\\u4DFF\\u4E00-\\u9FFF\\uA000-\\uA48F\\uA490-\\uA4CF\\uA700-\\uA71F\\uA800-\\uA82F\\uA840-\\uA87F\\uAC00-\\uD7AF\\uF900-\\uFAFF'\n\nRegExpUtils =\n\n  # It's important that the regex be wrapped in parens, otherwise\n  # javascript's RegExp::exec method won't find anything even when the\n  # regex matches!\n  #\n  # It's also imporant we return a fresh copy of the RegExp every time. A\n  # javascript regex is stateful and multiple functions using this method\n  # will cause unexpected behavior!\n  #\n  # See http://tools.ietf.org/html/rfc5322#section-3.4 and\n  # https://tools.ietf.org/html/rfc6531 and\n  # https://en.wikipedia.org/wiki/Email_address#Local_part\n  emailRegex: -> new RegExp(\"([a-z.A-Z#{UnicodeEmailChars}0-9!#$%&\\\\'*+\\\\-/=?^_`{|}~;]+@[A-Za-z#{UnicodeEmailChars}0-9.-]+\\\\.[A-Za-z]{2,63})\", 'g')\n\n  # http://stackoverflow.com/questions/16631571/javascript-regular-expression-detect-all-the-phone-number-from-the-page-source\n  # http://www.regexpal.com/?fam=94521\n  # NOTE: This is not exhaustive, and balances what is technically a phone number\n  # with what would be annoying to linkify. eg: 12223334444 does not match.\n  phoneRegex: -> new RegExp(/([\\+\\(]+|\\b)(?:(\\d{1,3}[- ()]*)?)(\\d{3})[- )]+(\\d{3})[- ]+(\\d{4})(?: *x(\\d+))?\\b/g)\n\n  # http://stackoverflow.com/a/16463966\n  # http://www.regexpal.com/?fam=93928\n  # NOTE: This does not match full urls with `http` protocol components.\n  domainRegex: -> new RegExp(\"^(?!:\\\\/\\\\/)([a-zA-Z#{UnicodeEmailChars}0-9-_]+\\\\.)*[a-zA-Z#{UnicodeEmailChars}0-9][a-zA-Z#{UnicodeEmailChars}0-9-_]+\\\\.[a-zA-Z]{2,11}?\", 'i')\n\n  # http://www.regexpal.com/?fam=95875\n  hashtagOrMentionRegex: -> new RegExp(/\\s([@#])([\\w_-]+)/i)\n\n  # https://www.safaribooksonline.com/library/view/regular-expressions-cookbook/9780596802837/ch07s16.html\n  ipAddressRegex: -> new RegExp(/^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}$/i)\n\n  nylasCommandRegex: -> new RegExp(/nylas:\\S+/i)\n\n  # Test cases: https://regex101.com/r/pD7iS5/3\n  urlRegex: ({matchEntireString} = {}) ->\n    commonTlds = ['com', 'org', 'edu', 'gov', 'uk', 'net', 'ca', 'de', 'jp', 'fr', 'au', 'us', 'ru', 'ch', 'it', 'nl', 'se', 'no', 'es', 'mil', 'ly']\n\n    parts = [\n      '('\n        # one of:\n        '('\n          # This OR block matches any TLD if the URL includes a scheme, and only\n          # the top ten TLDs if the scheme is omitted.\n          # YES - https://nylas.ai\n          # YES - https://10.2.3.1\n          # YES - nylas.com\n          # NO  - nylas.ai\n          '('\n            # scheme, ala https:// (mandatory)\n            '([A-Za-z]{3,9}:(?:\\\\/\\\\/))'\n\n            # username:password (optional)\n            '(?:[\\\\-;:&=\\\\+\\\\$,\\\\w]+@)?'\n\n            # one of:\n            '('\n              # domain with any tld\n              '([a-zA-Z0-9-_]+\\\\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\\\\.[a-zA-Z]{2,11}'\n\n              '|'\n\n              # ip address\n              '(?:[0-9]{1,3}\\\\.){3}[0-9]{1,3}'\n            ')'\n\n            '|'\n\n            # scheme, ala https:// (optional)\n            '([A-Za-z]{3,9}:(?:\\\\/\\\\/))?'\n\n            # username:password (optional)\n            '(?:[\\\\-;:&=\\\\+\\\\$,\\\\w]+@)?'\n\n            # one of:\n            '('\n              # domain with common tld\n              '([a-zA-Z0-9-_]+\\\\.)*[a-zA-Z0-9][a-zA-Z0-9-_]+\\\\.(?:' + commonTlds.join('|') + ')'\n\n              '|'\n\n              # ip address\n              '(?:[0-9]{1,3}\\\\.){3}[0-9]{1,3}'\n            ')'\n          ')'\n\n          # :port (optional)\n          '(?::\\d*)?'\n\n          '|'\n\n          # mailto:username@password.com\n          'mailto:\\\\/*(?:\\\\w+\\\\.|[\\\\-;:&=\\\\+\\\\$.,\\\\w]+@)[A-Za-z0-9\\\\.\\\\-]+'\n        ')'\n\n        # optionally followed by:\n        '('\n          # URL components\n          # (last character must not be puncation, hence two groups)\n          '(?:[\\\\+~%\\\\/\\\\.\\\\w\\\\-_@]*[\\\\+~%\\\\/\\\\w\\\\-_]+)?'\n\n          # optionally followed by: a query string and/or a #location\n          # (last character must not be puncation, hence two groups)\n          '(?:(\\\\?[\\\\-\\\\+=&;%@\\\\.\\\\w_\\\\#]*[\\\\#\\\\-\\\\+=&;%@\\\\w_\\\\/]+)?#?(?:[\\'\\\\$\\\\&\\\\(\\\\)\\\\*\\\\+,;=\\\\.\\\\!\\\\/\\\\\\\\\\\\w%-]*[\\\\/\\\\\\\\\\\\w]+)?)?'\n        ')?'\n      ')'\n    ]\n    if matchEntireString\n      parts.unshift('^')\n\n    return new RegExp(parts.join(''), 'gi')\n\n  # Test cases: https://regex101.com/r/jD5zC7/2\n  # Returns the following capturing groups:\n  # 1. start of the opening a tag to href=\"\n  # 2. The contents of the href without quotes\n  # 3. the rest of the opening a tag\n  # 4. the contents of the a tag\n  # 5. the closing tag\n  linkTagRegex: -> new RegExp(/(<a.*?href\\s*?=\\s*?['\"])(.*?)(['\"].*?>)([\\s\\S]*?)(<\\/a>)/gim)\n\n  # Test cases: https://regex101.com/r/cK0zD8/4\n  # Catches link tags containing which are:\n  # - Non empty\n  # - Not a mailto: link\n  # Returns the following capturing groups:\n  # 1. start of the opening a tag to href=\"\n  # 2. The contents of the href without quotes\n  # 3. the rest of the opening a tag\n  # 4. the contents of the a tag\n  # 5. the closing tag\n  urlLinkTagRegex: -> new RegExp(/(<a.*?href\\s*?=\\s*?['\"])((?!mailto).+?)(['\"].*?>)([\\s\\S]*?)(<\\/a>)/gim)\n\n  # https://regex101.com/r/zG7aW4/3\n  imageTagRegex: -> /<img\\s+[^>]*src=\"([^\"]*)\"[^>]*>/g\n\n  # Regex that matches our link tracking urls, surrounded by quotes\n  # (\"link.nylas.com...?redirect=\")\n  # Test cases: https://regex101.com/r/rB4fO4/3\n  # Returns the following capturing groups\n  # 1.The redirect url: the actual url you want to visit by clicking a url\n  # that matches this regex\n  trackedLinkRegex: -> /[\\\"|\\']https:\\/\\/link\\.nylas\\.com\\/link\\/.*?\\?.*?redirect=([^&\\\"\\']*).*?[\\\"|\\']/g\n\n  punctuation: ({exclude}={}) ->\n    exclude ?= []\n    punctuation = [ '.', ',', '\\\\/', '#', '!', '$', '%', '^', '&', '*',\n      ';', ':', '{', '}', '=', '\\\\-', '_', '`', '~', '(', ')', '@', '+',\n      '?', '>', '<', '\\\\[', '\\\\]', '+' ]\n    punctuation = _.difference(punctuation, exclude).join('')\n    return new RegExp(\"[#{punctuation}]\", 'g')\n\n  # This tests for valid schemes as per RFC 3986\n  # We need both http: https: and mailto: and a variety of other schemes.\n  # This does not check for invalid usage of the http: scheme. For\n  # example, http:bad.com would pass. We do not check for\n  # protocol-relative uri's.\n  #\n  # Regex explanation here: https://regex101.com/r/nR2yL6/2\n  # See RFC here: https://tools.ietf.org/html/rfc3986#section-3.1\n  # SO discussion: http://stackoverflow.com/questions/10687099/how-to-test-if-a-url-string-is-absolute-or-relative/31991870#31991870\n  hasValidSchemeRegex: -> new RegExp('^[a-z][a-z0-9+.-]*:', 'i')\n\n  emojiRegex: -> FBS_REGEXP = new RegExp(\n    \"(?:#{EmojiData.chars({include_variants: true}).join(\"|\")})\",\n    \"g\")\n\n  looseStyleTag: -> /<style/gim\n\n  # Regular expression matching javasript function arguments:\n  # https://regex101.com/r/pZ6zF0/2\n  functionArgs: -> /(?:\\(\\s*([^)]+?)\\s*\\)|(\\w+)\\s?=>)/\n\n  illegalPathCharactersRegexp: ->\n    #https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx\n    /[\\\\\\/:|?*><\"#]/g\n\n  # https://regex101.com/r/nC0qL2/2\n  signatureRegex: ->\n    new RegExp(/(<br\\/>){0,2}<signature>[^]*<\\/signature>/)\n\n  # Finds the start of a quoted text region as inserted by N1. This is not\n  # a general-purpose quote detection scheme and only works for\n  # N1-composed emails.\n  n1QuoteStartRegex: ->\n    new RegExp(/<\\w+[^>]*gmail_quote/i)\n\n  # https://regex101.com/r/jK8cC2/1\n  subcategorySplitRegex: ->\n    /[./\\\\]/g\n\nmodule.exports = RegExpUtils\n"
  },
  {
    "path": "packages/client-app/src/registries/command-registry.es6",
    "content": "import { Emitter, Disposable, CompositeDisposable } from 'event-kit';\n\nexport default class CommandRegistry {\n  constructor() {\n    this.emitter = new Emitter();\n    this.listenerCounts = {};\n    this.listenerCountChanges = {};\n  }\n\n  add(target, commandName, callback) {\n    if (typeof commandName === 'object') {\n      const commands = commandName;\n      const disposable = new CompositeDisposable();\n      for (const subcommandName of Object.keys(commands)) {\n        const subCallback = commands[subcommandName];\n        disposable.add(this.add(target, subcommandName, subCallback));\n      }\n      return disposable;\n    }\n\n    if (typeof callback !== 'function') {\n      throw new Error(\"Can't register a command with non-function callback.\");\n    }\n\n    if (typeof target === 'string') {\n      throw new Error(\"Commands can no longer be registered to CSS selectors. Consider using KeyCommandRegion instead.\");\n    }\n\n    target.addEventListener(commandName, callback);\n    this.listenerCountChanges[commandName] = (this.listenerCountChanges[commandName] || 0) + 1;\n    this.flushChangesSoon();\n\n    return new Disposable(() => {\n      target.removeEventListener(commandName, callback);\n      this.listenerCountChanges[commandName] = (this.listenerCountChanges[commandName] || 0) - 1;\n      this.flushChangesSoon();\n    });\n  }\n\n  listenerCountForCommand(commandName) {\n    return (this.listenerCounts[commandName] || 0) + (this.listenerCountChanges[commandName] || 0);\n  }\n\n  // Public: Simulate the dispatch of a command on a DOM node.\n  //\n  // This can be useful for testing when you want to simulate the invocation of a\n  // command on a detached DOM node. Otherwise, the DOM node in question needs to\n  // be attached to the document so the event bubbles up to the root node to be\n  // processed.\n  //\n  // * `target` The DOM node at which to start bubbling the command event.\n  // * `commandName` {String} indicating the name of the command to dispatch.\n  dispatch(commandName, detail) {\n    const event = new CustomEvent(commandName, {bubbles: true, detail});\n    return document.activeElement.dispatchEvent(event);\n  }\n\n  flushChangesSoon = () => {\n    if (this.pendingEmit) { return; }\n    this.pendingEmit = true;\n\n    setTimeout(() => {\n      this.pendingEmit = false;\n\n      let changed = false;\n      for (const commandName of Object.keys(this.listenerCountChanges)) {\n        const val = this.listenerCountChanges[commandName];\n        this.listenerCounts[commandName] = (this.listenerCounts[commandName] || 0) + val;\n        if (val !== 0) { changed = true; }\n      }\n      this.listenerCountChanges = {};\n      if (changed) {\n        this.emitter.emit('commands-changed');\n      }\n    }, 100);\n  }\n\n  onRegistedCommandsChanged(callback) {\n    return this.emitter.on('commands-changed', callback);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/registries/component-registry.coffee",
    "content": "_ = require 'underscore'\n\n{Listener, Publisher} = require '../flux/modules/reflux-coffee'\nCoffeeHelpers = require '../flux/coffee-helpers'\n\nDeprecatedRoles = {\n  'thread:BulkAction': 'ThreadActionsToolbarButton',\n  'draft:BulkAction': 'DraftActionsToolbarButton',\n  'message:Toolbar': 'ThreadActionsToolbarButton',\n  'thread:Toolbar': 'ThreadActionsToolbarButton',\n}\n\n###\nPublic: The ComponentRegistry maintains an index of React components registered\nby Nylas packages. Components can use {InjectedComponent} and {InjectedComponentSet}\nto dynamically render components registered with the ComponentRegistry.\n\nSection: Stores\n###\nclass ComponentRegistry\n  @include: CoffeeHelpers.includeModule\n\n  @include Publisher\n  @include Listener\n\n  constructor: ->\n    @_registry = {}\n    @_cache = {}\n    @_showComponentRegions = false\n\n\n  # Public: Register a new component with the Component Registry.\n  # Typically, packages call this method from their main `activate` method\n  # to extend the Nylas user interface, and call the corresponding `unregister`\n  # method in `deactivate`.\n  #\n  # * `component` {Object} A React Component with a `displayName`\n  # * `options` {Object}:\n  #\n  #   * `role`: (optional) {String} If you want to display your component in a location\n  #      desigated by a role, pass the role identifier.\n  #\n  #   * `modes`: (optional) {Array} If your component should only be displayed\n  #      in particular Workspace Modes, pass an array of supported modes.\n  #      ('list', 'split', etc.)\n  #\n  #   * `location`: (optional) {Object} If your component should be displayed in a\n  #      column or toolbar, pass the fully qualified location object, such as:\n  #      `WorkspaceStore.Location.ThreadList`\n  #\n  #   Note that for advanced use cases, you can also pass (`modes`, `roles`, `locations`)\n  #   with arrays instead of single values.\n  #\n  # This method is chainable.\n  #\n  register: (component, options) =>\n    if component.view?\n      return console.warn(\"Ignoring component trying to register with old CommandRegistry.register syntax\")\n\n    throw new Error(\"ComponentRegistry.register() requires `options` that describe the component\") unless options\n    throw new Error(\"ComponentRegistry.register() requires `component`, a React component\") unless component\n    throw new Error(\"ComponentRegistry.register() requires that your React Component defines a `displayName`\") unless component.displayName\n\n    {locations, modes, roles} = @_pluralizeDescriptor(options)\n\n    throw new Error(\"ComponentRegistry.register() requires `role` or `location`\") if not roles and not locations\n\n    if @_registry[component.displayName] and @_registry[component.displayName].component isnt component\n      throw new Error(\"ComponentRegistry.register(): A different component was already registered with the name #{component.displayName}\")\n\n    roles = @_removeDeprecatedRoles(component.displayName, roles) if roles\n\n    @_cache = {}\n    @_registry[component.displayName] = {component, locations, modes, roles}\n\n    # Trigger listeners. It's very important the component registry is debounced.\n    # During app launch packages register tons of components and if we re-rendered\n    # the entire UI after each registration it takes forever to load the UI.\n    @triggerDebounced()\n\n    # Return `this` for chaining\n    @\n\n  unregister: (component) =>\n    if _.isString(component)\n      throw new Error(\"ComponentRegistry.unregister() must be called with a component.\")\n    @_cache = {}\n    delete @_registry[component.displayName]\n    @triggerDebounced()\n\n  # Public: Retrieve the registry entry for a given name.\n  #\n  # - `name`: The {String} name of the registered component to retrieve.\n  #\n  # Returns a {React.Component}\n  #\n  findComponentByName: (name) =>\n    @_registry[name]?.component\n\n  ###\n  Public: Retrieve all of the registry entries matching a given descriptor.\n\n  ```coffee\n    ComponentRegistry.findComponentsMatching({\n      role: 'Composer:ActionButton'\n    })\n\n    ComponentRegistry.findComponentsMatching({\n      location: WorkspaceStore.Location.RootSidebar.Toolbar\n    })\n  ```\n\n  - `descriptor`: An {Object} that specifies set of components using the\n    available keys below.\n\n    * `mode`: (optional) {String} Components that specifically list modes\n       will only be returned if they include this mode.\n\n    * `role`: (optional) {String} Only return components that have registered\n       for this role.\n\n    * `location`: (optional) {Object} Only return components that have registered\n       for this location.\n\n    Note that for advanced use cases, you can also pass (`modes`, `roles`, `locations`)\n    with arrays instead of single values.\n\n  Returns an {Array} of {React.Component} objects\n  ###\n  findComponentsMatching: (descriptor) =>\n    if not descriptor?\n      throw new Error(\"ComponentRegistry.findComponentsMatching called without descriptor\")\n\n    {locations, modes, roles} = @_pluralizeDescriptor(descriptor)\n\n    if not locations and not modes and not roles\n      throw new Error(\"ComponentRegistry.findComponentsMatching called with an empty descriptor\")\n\n    cacheKey = JSON.stringify({locations, modes, roles})\n    return [].concat(@_cache[cacheKey]) if @_cache[cacheKey]\n\n    # Made into a convenience function because default\n    # values (`[]`) are necessary and it was getting messy.\n    overlaps = (entry = [], search = []) ->\n      _.intersection(entry, search).length > 0\n\n    entries = _.values @_registry\n    entries = _.filter entries, (entry) ->\n      if modes and entry.modes and not overlaps(modes, entry.modes)\n        return false\n      if locations and not overlaps(locations, entry.locations)\n        return false\n      if roles and not overlaps(roles, entry.roles)\n        return false\n      return true\n\n    results = _.map entries, (entry) -> entry.component\n    @_cache[cacheKey] = results\n\n    return [].concat(results)\n\n  # We debounce because a single plugin may activate many components in\n  # their `activate` methods. Furthermore, when the window loads several\n  # plugins may load in sequence. Plugin loading takes a while (dozens of\n  # ms) since javascript is being read and `require` trees are being\n  # traversed.\n  #\n  # Triggering the ComponentRegistry is fairly expensive since many very\n  # high-level components (like the <Sheet />) listen and re-render when\n  # this triggers.\n  #\n  # We set the debouce interval to 1 \"frame\" (16ms) to balance\n  # responsiveness and efficient batching.\n  triggerDebounced: _.debounce(( -> @trigger(@)), 16)\n\n  _removeDeprecatedRoles: (displayName, roles) ->\n    newRoles = _.clone(roles)\n    roles.forEach (role, idx) ->\n      if role of DeprecatedRoles\n        instead = DeprecatedRoles[role]\n        console.warn(\"Deprecation warning! The role `#{role}` has been deprecated.\n        Register `#{displayName}` for the role `#{instead}` instead.\")\n        newRoles.splice(idx, 1, instead)\n    return newRoles\n\n  _pluralizeDescriptor: (descriptor) ->\n    {locations, modes, roles} = descriptor\n    modes = [descriptor.mode] if descriptor.mode\n    roles = [descriptor.role] if descriptor.role\n    locations = [descriptor.location] if descriptor.location\n    {locations, modes, roles}\n\n  _clear: =>\n    @_cache = {}\n    @_registry = {}\n\n  # Showing Component Regions\n\n  toggleComponentRegions: ->\n    @_showComponentRegions = !@_showComponentRegions\n    @trigger(@)\n\n  showComponentRegions: =>\n    @_showComponentRegions\n\n\nmodule.exports = new ComponentRegistry()\n"
  },
  {
    "path": "packages/client-app/src/registries/database-object-registry.es6",
    "content": "import SerializableRegistry from './serializable-registry'\n\nclass DatabaseObjectRegistry extends SerializableRegistry { }\n\nconst registry = new DatabaseObjectRegistry()\nexport default registry\n"
  },
  {
    "path": "packages/client-app/src/registries/extension-registry.es6",
    "content": "import _ from 'underscore';\nimport {Listener, Publisher} from '../flux/modules/reflux-coffee';\nimport {includeModule} from '../flux/coffee-helpers';\n\nexport class Registry {\n\n  static include = includeModule;\n\n  constructor(name, deprecationAdapter = (ext) => ext) {\n    this.name = name;\n    this._deprecationAdapter = deprecationAdapter;\n    this._registry = new Map();\n  }\n\n  register(ext, {priority = 0} = {}) {\n    this.validateExtension(ext, 'register');\n    const extension = this._deprecationAdapter(ext)\n    this._registry.set(ext.name, {extension, priority});\n    this.triggerDebounced();\n    return this;\n  }\n\n  unregister(extension) {\n    this.validateExtension(extension, 'unregister');\n    this._registry.delete(extension.name);\n    this.triggerDebounced();\n  }\n\n  extensions() {\n    return _.pluck(_.sortBy(Array.from(this._registry.values()), \"priority\"), \"extension\").reverse()\n  }\n\n  clear() {\n    this._registry = new Map();\n  }\n\n  triggerDebounced = _.debounce(::this.trigger, 1);\n\n  validateExtension(extension, method) {\n    if (!extension || Array.isArray(extension) || !_.isObject(extension)) {\n      throw new Error(`ExtensionRegistry.${this.name}.${method} requires a valid \\\\\n                      extension object that implements one of the functions defined by ${this.name}Extension`);\n    }\n    if (!extension.name) {\n      throw new Error(`ExtensionRegistry.${this.name}.${method} requires a \\\\\n                      \\`name\\` property defined on the extension object`);\n    }\n  }\n}\n\nRegistry.include(Publisher);\nRegistry.include(Listener);\n\nexport const Composer = new Registry('Composer');\n\nexport const MessageView = new Registry('MessageView');\n\nexport const ThreadList = new Registry('ThreadList');\n\nexport const AccountSidebar = new Registry('AccountSidebar');\n"
  },
  {
    "path": "packages/client-app/src/registries/serializable-registry.es6",
    "content": "/**\n * Public: This keeps track of constructors so we know how to inflate\n * serialized objects.\n *\n * We map constructor string names with factory functions that will return\n * the actual constructor itself.\n *\n * The reason we have an extra function call to return a constructor is so\n * we don't need to `require` all constructors at once on load. We are\n * wasting a very large amount of time on bootup requiring files that may\n * never be used or only used way down the line.\n *\n * If 3rd party packages want to register new inflatable models, they can\n * use `register` and pass the constructor generator along with the name.\n *\n * Note that there is one registry per window.\n */\nexport default class SerializableRegistry {\n  constructor() {\n    this._constructorFactories = {}\n  }\n\n  get(name) {\n    return this._constructorFactories[name].call(null)\n  }\n\n  getAllConstructors() {\n    const constructors = []\n    for (const name of Object.keys(this._constructorFactories)) {\n      constructors.push(this.get(name))\n    }\n    return constructors;\n  }\n\n  isInRegistry(name) {\n    return !!this._constructorFactories[name]\n  }\n\n  deserialize(name, dataJSON) {\n    let data = dataJSON;\n    if (typeof data === \"string\") {\n      data = JSON.parse(dataJSON)\n    }\n\n    const constructor = this.get(name)\n\n    if (typeof constructor !== \"function\") {\n      throw new Error(`Unsure of how to inflate ${JSON.stringify(data)}. \\\nYour constructor factory must return a class constructor.`);\n    }\n\n    const object = new constructor()\n    object.fromJSON(data)\n\n    return object\n  }\n\n  register(name, constructorFactory) {\n    this._constructorFactories[name] = constructorFactory\n  }\n\n  unregister(name) {\n    delete this._constructorFactories[name]\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/registries/service-registry.es6",
    "content": "class ServiceRegistry {\n  constructor() {\n    this._waitingForServices = {};\n    this._services = {};\n  }\n\n  withService(name, callback) {\n    if (this._services[name]) {\n      setTimeout(() => callback(this._services[name]), 0);\n    } else {\n      this._waitingForServices[name] = this._waitingForServices[name] || [];\n      this._waitingForServices[name].push(callback);\n    }\n  }\n\n  registerService(name, obj) {\n    this._services[name] = obj;\n    if (this._waitingForServices[name]) {\n      for (const callback of this._waitingForServices[name]) {\n        callback(obj);\n      }\n      delete this._waitingForServices[name];\n    }\n  }\n\n  unregisterService(name) {\n    delete this._services[name];\n  }\n}\n\nexport default new ServiceRegistry()\n"
  },
  {
    "path": "packages/client-app/src/registries/sound-registry.coffee",
    "content": "_ = require 'underscore'\npath = require 'path'\n\nclass SoundRegistry\n  constructor: ->\n    @_sounds = {}\n\n  playSound: (name) ->\n    return if NylasEnv.inSpecMode()\n    src = @_sounds[name]\n    return unless src\n\n    a = new Audio()\n\n    if _.isString src\n      if src.indexOf(\"nylas://\") is 0\n        a.src = src\n      else\n        a.src = path.join(resourcePath, 'static', 'sounds', src)\n    else if _.isArray src\n      {resourcePath} = NylasEnv.getLoadSettings()\n      args = [resourcePath].concat(src)\n      a.src = path.join.apply(@, args)\n\n    a.autoplay = true\n    a.play()\n\n  register: (name, path) ->\n    if _.isObject(name)\n      @_sounds[key] = path for key, path of name\n    else if _.isString(name)\n      @_sounds[name] = path\n\n  unregister: (name) ->\n    if _.isArray(name)\n      delete @_sounds[key] for key in name\n    else if _.isString(name)\n      delete @_sounds[name]\n\nmodule.exports = new SoundRegistry()\n"
  },
  {
    "path": "packages/client-app/src/registries/store-registry.es6",
    "content": "import SerializableRegistry from './serializable-registry'\n\nclass StoreRegistry extends SerializableRegistry {\n  /**\n   * Most of the core Flux stores construct themselves on require. That\n   * construction initialize the stores, sets up listeners, and may access\n   * the database.\n   *\n   * It also kicks off a fairly large tree of require statements that\n   * takes considerable time to process.\n   */\n  async activateAllStores() {\n    for (const name of Object.keys(this._constructorFactories)) {\n      // All we need to do is hit `require` on the store. This will\n      // construct the object an initialize the require cache. The\n      // stores are now available in nylas-exports or from the node\n      // require cache.\n      const store = this.get(name);\n\n      /**\n       * Some stores may have extra activation work to do. This work may\n       * be asynchronous. We detect that here and call the store's\n       * activate methods.\n       */\n      if (store.activate) {\n        await store.activate()\n      }\n    }\n  }\n}\n\nconst registry = new StoreRegistry()\nexport default registry\n"
  },
  {
    "path": "packages/client-app/src/registries/task-registry.es6",
    "content": "import SerializableRegistry from './serializable-registry'\n\nclass TaskRegistry extends SerializableRegistry { }\n\nconst registry = new TaskRegistry()\nexport default registry\n"
  },
  {
    "path": "packages/client-app/src/searchable-components/iframe-searcher.es6",
    "content": "import RealDOMParser from './real-dom-parser'\n\nexport default class IFrameSearcher {\n  /**\n   * An imperative renderer for iframes\n   */\n  static highlightSearchInDocument(regionId, searchTerm, doc, searchIndex) {\n    const parser = new RealDOMParser(regionId)\n    if (parser.matchesSearch(doc, searchTerm)) {\n      parser.removeMatchesAndNormalize(doc)\n      const matchNodeMap = parser.getElementsWithNewMatchNodes(doc, searchTerm, searchIndex)\n      parser.highlightSearch(doc, matchNodeMap)\n    } else {\n      parser.removeMatchesAndNormalize(doc)\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/searchable-components/real-dom-parser.es6",
    "content": "import _ from 'underscore'\nimport {DOMUtils} from 'nylas-exports'\nimport UnifiedDOMParser from './unified-dom-parser'\n\nexport default class RealDOMParser extends UnifiedDOMParser {\n  * _pruningDOMWalker({node, pruneFn, filterFn}) {\n    if (filterFn(node)) {\n      yield node;\n    }\n    if (node && !pruneFn(node) && node.childNodes.length > 0) {\n      for (let i = 0; i < node.childNodes.length; i++) {\n        yield* this._pruningDOMWalker({node: node.childNodes[i], pruneFn, filterFn});\n      }\n    }\n    return;\n  }\n\n  getWalker(dom) {\n    const filterFn = (node) => {\n      return node.nodeType === Node.TEXT_NODE\n    }\n    const pruneFn = (node) => {\n      return node.nodeName === \"STYLE\"\n    }\n    return this._pruningDOMWalker({node: dom, pruneFn, filterFn});\n  }\n\n  isTextNode(node) {\n    return node.nodeType === Node.TEXT_NODE\n  }\n\n  textNodeLength(textNode) {\n    return (textNode.data || \"\").length\n  }\n\n  textNodeContents(textNode) {\n    return (textNode.data)\n  }\n\n  looksLikeBlockElement(node) {\n    return DOMUtils.looksLikeBlockElement(node)\n  }\n\n  getRawFullString(fullString) {\n    return _.pluck(fullString, \"data\").join('');\n  }\n\n  removeMatchesAndNormalize(element) {\n    const matches = element.querySelectorAll('search-match');\n    if (matches.length === 0) { return null }\n    for (let i = 0; i < matches.length; i++) {\n      const match = matches[i];\n      DOMUtils.unwrapNode(match)\n    }\n    element.normalize();\n    return element\n  }\n\n  createTextNode({rawText}) {\n    return document.createTextNode(rawText);\n  }\n  createMatchNode({matchText, regionId, isCurrentMatch, renderIndex}) {\n    const text = document.createTextNode(matchText);\n    const newNode = document.createElement('search-match');\n    const className = isCurrentMatch ? \"current-match\" : \"\";\n    newNode.setAttribute('data-region-id', regionId)\n    newNode.setAttribute('data-render-index', renderIndex)\n    newNode.setAttribute('class', className)\n    newNode.appendChild(text);\n    return newNode\n  }\n  textNodeKey(textElement) {\n    return textElement;\n  }\n\n  highlightSearch(element, matchNodeMap) {\n    const walker = this.getWalker(element);\n    // We have to expand the whole generator because we're mutating in\n    // place\n    const textNodes = [...walker]\n    for (const textNode of textNodes) {\n      if (matchNodeMap.has(textNode)) {\n        const {originalTextNode, newTextNodes} = matchNodeMap.get(textNode);\n        const frag = document.createDocumentFragment();\n        for (const newNode of newTextNodes) {\n          frag.appendChild(newNode);\n        }\n        textNode.parentNode.replaceChild(frag, originalTextNode)\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/searchable-components/search-constants.es6",
    "content": "export const MAX_MATCHES = 999;\nexport const CHAR_THRESHOLD = 2;\n"
  },
  {
    "path": "packages/client-app/src/searchable-components/search-match.jsx",
    "content": "import React from 'react'\n\nconst SearchMatch = (props) => {\n  return (\n    <span\n      data-region-id={props.regionId}\n      data-render-index={props.renderIndex}\n      className={`search-match ${props.className}`}\n    >\n      {props.children}\n    </span>\n  )\n}\n\nSearchMatch.propTypes = {\n  regionId: React.PropTypes.string,\n  className: React.PropTypes.string,\n  renderIndex: React.PropTypes.number,\n};\n\nexport default SearchMatch;\n"
  },
  {
    "path": "packages/client-app/src/searchable-components/searchable-component-maker.jsx",
    "content": "import _ from 'underscore'\nimport ReactDOM from 'react-dom'\nimport Utils from '../flux/models/utils'\nimport VirtualDOMParser from './virtual-dom-parser'\nimport SearchableComponentStore from '../flux/stores/searchable-component-store'\n\nclass SearchableComponent {\n  componentDidMount(superMethod, ...args) {\n    if (superMethod) superMethod.apply(this, args);\n    this.__regionId = Utils.generateTempId();\n    this._searchableListener = SearchableComponentStore.listen(() => { this._onSearchableComponentStoreChange() })\n    SearchableComponentStore.registerSearchRegion(this.__regionId, ReactDOM.findDOMNode(this))\n  }\n\n  _onSearchableComponentStoreChange() {\n    const searchIndex = SearchableComponentStore.getCurrentRegionIndex(this.__regionId);\n    const {searchTerm} = SearchableComponentStore.getCurrentSearchData()\n    this.setState({\n      __searchTerm: searchTerm,\n      __searchIndex: searchIndex,\n    })\n  }\n\n  shouldComponentUpdate(superMethod, nextProps, nextState) {\n    let shouldUpdate = true;\n    if (superMethod) {\n      shouldUpdate = superMethod.apply(this, [nextProps, nextState]);\n    }\n    if (shouldUpdate && (this.__searchTerm || (this.__searchIndex !== null && this.__searchIndex !== undefined))) {\n      shouldUpdate = this.__searchTerm !== nextState.__searchTerm || this.__searchIndex !== nextState.__searchIndex\n    }\n    return shouldUpdate\n  }\n\n  componentWillUnmount(superMethod, ...args) {\n    if (superMethod) superMethod.apply(this, args);\n    this._searchableListener()\n    SearchableComponentStore.unregisterSearchRegion(this.__regionId)\n  }\n\n  componentDidUpdate(superMethod, ...args) {\n    if (superMethod) superMethod.apply(this, args);\n    SearchableComponentStore.registerSearchRegion(this.__regionId, ReactDOM.findDOMNode(this))\n  }\n\n  render(superMethod, ...args) {\n    if (superMethod) {\n      const vDOM = superMethod.apply(this, args);\n      const parser = new VirtualDOMParser(this.__regionId);\n      const searchTerm = this.state.__searchTerm\n      if (parser.matchesSearch(vDOM, searchTerm)) {\n        const normalizedDOM = parser.removeMatchesAndNormalize(vDOM)\n        const matchNodeMap = parser.getElementsWithNewMatchNodes(normalizedDOM, searchTerm, this.state.__searchIndex);\n        return parser.highlightSearch(normalizedDOM, matchNodeMap)\n      }\n      return vDOM;\n    }\n    return null;\n  }\n}\n\n/**\n * Takes a React component and makes it searchable\n */\nexport default class SearchableComponentMaker {\n  static extend(component) {\n    const proto = SearchableComponent.prototype;\n    for (const propName of Object.getOwnPropertyNames(proto)) {\n      const origMethod = component.prototype[propName]\n      if (origMethod) {\n        if (propName === \"constructor\") { continue }\n        component.prototype[propName] = _.partial(proto[propName], origMethod)\n      } else {\n        component.prototype[propName] = proto[propName]\n      }\n    }\n    return component\n  }\n\n  static searchInIframe(contentDocument) {\n    return contentDocument;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/searchable-components/unified-dom-parser.es6",
    "content": "import {Utils} from 'nylas-exports'\nimport {MAX_MATCHES, CHAR_THRESHOLD} from './search-constants'\n\nexport default class UnifiedDOMParser {\n  constructor(regionId) {\n    this.regionId = regionId\n    this.matchRenderIndex = 0\n  }\n\n  matchesSearch(dom, searchTerm) {\n    if ((searchTerm || \"\").trim().length < CHAR_THRESHOLD) { return false; }\n    const fullStrings = this.buildNormalizedText(dom)\n    // For each match, we return an array of new elements.\n    for (const fullString of fullStrings) {\n      const matches = this.matchesFromFullString(fullString, searchTerm);\n      if (matches.length > 0) {\n        return true;\n      }\n    }\n    return false;\n  }\n\n  buildNormalizedText(dom) {\n    const walker = this.getWalker(dom);\n\n    const fullStrings = [];\n    let textElementAccumulator = [];\n    let stringIndex = 0;\n\n    for (const node of walker) {\n      if (this.isTextNode(node)) {\n        node.fullStringIndex = stringIndex\n        textElementAccumulator.push(node);\n        stringIndex += this.textNodeLength(node);\n      } else if (this.looksLikeBlockElement(node)) {\n        if (textElementAccumulator.length > 0) {\n          fullStrings.push(textElementAccumulator);\n          textElementAccumulator = [];\n          stringIndex = 0;\n        }\n      }\n      // else continue for inline elements\n    }\n    if (textElementAccumulator.length > 0) {\n      fullStrings.push(textElementAccumulator);\n    }\n    return fullStrings\n  }\n  // OVERRIDE ME\n  getWalker() { }\n  isTextNode() { }\n  textNodeLength() { }\n  looksLikeBlockElement() { }\n  textNodeContents() {}\n\n  matchesFromFullString(fullString, searchTerm) {\n    const re = this.searchRE(searchTerm);\n    if (!re) { return [] }\n    const rawString = this.getRawFullString(fullString)\n    const matches = []\n    let matchCount = 0;\n    let match = re.exec(rawString);\n    while (match && matchCount <= MAX_MATCHES) {\n      const matchStart = match.index;\n      const matchEnd = match.index + match[0].length;\n      matches.push([matchStart, matchEnd])\n      match = re.exec(rawString)\n      matchCount += 1;\n    }\n    return matches;\n  }\n  getRawFullString() { }\n\n  searchRE(searchTerm) {\n    let re;\n    const regexRe = /^\\/(.+)\\/(.*)$/\n    try {\n      if (regexRe.test(searchTerm)) {\n        // Looks like regex\n        const matches = searchTerm.match(regexRe);\n        const reText = matches[1];\n        re = new RegExp(reText, \"ig\");\n      } else {\n        re = new RegExp(Utils.escapeRegExp(searchTerm), \"ig\");\n      }\n    } catch (e) {\n      return null\n    }\n    return re\n  }\n\n  // OVERRIDE ME\n  removeMatchesAndNormalize() { }\n\n  getElementsWithNewMatchNodes(rootNode, searchTerm, currentMatchRenderIndex) {\n    const fullStrings = this.buildNormalizedText(rootNode)\n\n    const modifiedElements = new Map()\n    // For each match, we return an array of new elements.\n    for (const fullString of fullStrings) {\n      const matches = this.matchesFromFullString(fullString, searchTerm);\n\n      if (matches.length === 0) {\n        continue;\n      }\n\n      for (const textNode of fullString) {\n        const slicePoints = this.slicePointsForMatches(textNode,\n            matches);\n        if (slicePoints.length > 0) {\n          const {key, originalTextNode, newTextNodes} = this.slicedTextElement(textNode, slicePoints, currentMatchRenderIndex);\n          modifiedElements.set(key, {originalTextNode, newTextNodes})\n        }\n      }\n    }\n\n    return modifiedElements;\n  }\n\n  slicePointsForMatches(textElement, matches) {\n    const textElStart = textElement.fullStringIndex;\n    const textLength = this.textNodeLength(textElement);\n    const textElEnd = textElement.fullStringIndex + textLength;\n\n    const slicePoints = [];\n\n    for (const [matchStart, matchEnd] of matches) {\n      if (matchStart < textElStart && matchEnd >= textElEnd) {\n        // textEl is completely inside of match\n        slicePoints.push([0, textLength])\n      } else if (matchStart >= textElStart && matchEnd < textElEnd) {\n        // match is completely inside of textEl\n        slicePoints.push([matchStart - textElStart, matchEnd - textElStart])\n      } else if (matchEnd >= textElStart && matchEnd < textElEnd) {\n        // match started in a previous el but ends in this one\n        slicePoints.push([0, matchEnd - textElStart])\n      } else if (matchStart >= textElStart && matchStart < textElEnd) {\n        // match starts in this el but ends in a future one\n        slicePoints.push([matchStart - textElStart, textLength])\n      } else {\n        // match is not in this element\n        continue;\n      }\n    }\n    return slicePoints;\n  }\n\n  /**\n   * Given some text element and a slice point, it will split that text\n   * element at the slice points and return the new nodes as a value,\n   * keyed by a way to find that insertion point in the DOM.\n   */\n  slicedTextElement(textNode, slicePoints, currentMatchRenderIndex) {\n    const key = this.textNodeKey(textNode)\n    const text = this.textNodeContents(textNode)\n    const newTextNodes = [];\n    let sliceOffset = 0;\n    let remainingText = text;\n    for (let [sliceStart, sliceEnd] of slicePoints) {\n      sliceStart -= sliceOffset;\n      sliceEnd -= sliceOffset;\n      const before = remainingText.slice(0, sliceStart);\n      if (before.length > 0) {\n        newTextNodes.push(this.createTextNode({rawText: before}))\n      }\n\n      const matchText = remainingText.slice(sliceStart, sliceEnd);\n      if (matchText.length > 0) {\n        let isCurrentMatch = false;\n        if (this.matchRenderIndex === currentMatchRenderIndex) {\n          isCurrentMatch = true;\n        }\n        newTextNodes.push(this.createMatchNode({regionId: this.regionId, renderIndex: this.matchRenderIndex, matchText, isCurrentMatch}));\n        this.matchRenderIndex += 1\n      }\n\n      remainingText = remainingText.slice(sliceEnd, remainingText.length)\n      sliceOffset += sliceEnd\n    }\n    newTextNodes.push(this.createTextNode({rawText: remainingText}));\n    return {\n      key: key,\n      originalTextNode: textNode,\n      newTextNodes: newTextNodes,\n    };\n  }\n  // OVERRIDE ME\n  createTextNode() {}\n  createMatchNode() {}\n  textNodeKey() {}\n\n  // OVERRIDE ME\n  highlightSearch() { }\n}\n"
  },
  {
    "path": "packages/client-app/src/searchable-components/virtual-dom-parser.es6",
    "content": "import _ from 'underscore'\nimport React from 'react'\nimport {VirtualDOMUtils} from 'nylas-exports'\n\nimport SearchMatch from './search-match'\nimport UnifiedDOMParser from './unified-dom-parser'\n\nexport default class VirtualDOMParser extends UnifiedDOMParser {\n  getWalker(dom) {\n    const pruneFn = (node) => {\n      return node.type === \"style\";\n    }\n    return VirtualDOMUtils.walk({element: dom, pruneFn});\n  }\n\n  isTextNode({element}) {\n    return (typeof element === \"string\")\n  }\n\n  textNodeLength({element}) {\n    return element.length\n  }\n\n  textNodeContents(textNode) {\n    return textNode.element\n  }\n\n  looksLikeBlockElement({element}) {\n    if (!element) { return false; }\n    const blockTypes = [\"br\", \"p\", \"blockquote\", \"div\", \"table\", \"iframe\"]\n    if (_.isFunction(element.type)) {\n      return true\n    } else if (blockTypes.indexOf(element.type) >= 0) {\n      return true\n    }\n    return false\n  }\n\n  getRawFullString(fullString) {\n    return _.pluck(fullString, \"element\").join('');\n  }\n\n  removeMatchesAndNormalize(element) {\n    let newChildren = [];\n    let strAccumulator = [];\n\n    const resetAccumulator = () => {\n      if (strAccumulator.length > 0) {\n        newChildren.push(strAccumulator.join(''));\n        strAccumulator = [];\n      }\n    }\n\n    if (React.isValidElement(element) || _.isArray(element)) {\n      let children;\n\n      if (_.isArray(element)) {\n        children = element;\n      } else {\n        children = element.props.children;\n      }\n\n      if (!children) {\n        newChildren = null\n      } else if (React.isValidElement(children)) {\n        newChildren = children\n      } else if (typeof children === \"string\") {\n        strAccumulator.push(children)\n      } else if (children.length > 0) {\n        for (let i = 0; i < children.length; i++) {\n          const child = children[i];\n          if (typeof child === \"string\") {\n            strAccumulator.push(child)\n          } else if (this._isSearchElement(child)) {\n            resetAccumulator();\n            newChildren.push(child.props.children);\n          } else {\n            resetAccumulator();\n            newChildren.push(this.removeMatchesAndNormalize(child));\n          }\n        }\n      } else {\n        newChildren = children\n      }\n\n      resetAccumulator();\n\n      if (_.isArray(element)) {\n        return newChildren;\n      }\n      return React.cloneElement(element, {}, newChildren)\n    }\n    return element;\n  }\n  _isSearchElement(element) {\n    return element.type === SearchMatch\n  }\n\n  createTextNode({rawText}) {\n    return rawText\n  }\n  createMatchNode({matchText, regionId, isCurrentMatch, renderIndex}) {\n    const className = isCurrentMatch ? \"current-match\" : \"\"\n    return React.createElement(SearchMatch, {className, regionId, renderIndex}, matchText);\n  }\n  textNodeKey(textElement) {\n    return textElement.parentNode\n  }\n\n  highlightSearch(element, matchNodeMap) {\n    if (React.isValidElement(element) || _.isArray(element)) {\n      let newChildren = []\n      let children;\n\n      if (_.isArray(element)) {\n        children = element;\n      } else {\n        children = element.props.children;\n      }\n\n      const matchNode = matchNodeMap.get(element);\n      let originalTextNode = null;\n      let newTextNodes = [];\n      if (matchNode) {\n        originalTextNode = matchNode.originalTextNode;\n        newTextNodes = matchNode.newTextNodes;\n      }\n\n      if (!children) {\n        newChildren = null\n      } else if (React.isValidElement(children)) {\n        if (originalTextNode && originalTextNode.childOffset === 0) {\n          newChildren = newTextNodes\n        } else {\n          newChildren = this.highlightSearch(children, matchNodeMap)\n        }\n      } else if (!_.isString(children) && children.length > 0) {\n        for (let i = 0; i < children.length; i++) {\n          const child = children[i];\n          if (originalTextNode && originalTextNode.childOffset === i) {\n            newChildren.push(newTextNodes)\n          } else {\n            newChildren.push(this.highlightSearch(child, matchNodeMap))\n          }\n        }\n      } else {\n        if (originalTextNode && originalTextNode.childOffset === 0) {\n          newChildren = newTextNodes\n        } else {\n          newChildren = children\n        }\n      }\n\n      if (_.isArray(element)) {\n        return newChildren;\n      }\n      return React.cloneElement(element, {}, newChildren)\n    }\n    return element;\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/src/secondary-window-bootstrap.es6",
    "content": "/* eslint import/first: 0 */\n\n// Effectively all secondary windows are empty hot windows. We spawn the\n// window and pre-load all of the basic javascript libraries (which takes a\n// full second or so).\n// #\n// Eventually when `WindowManager::newWindow` gets called, instead of\n// actually spawning a new window, we'll call\n// `NylasWindow::setLoadSettings` on the window instead. This will replace\n// the window options, adjust params as necessary, and then re-load the\n// plugins. Once `NylasWindow::setLoadSettings` fires, the main NylasEnv in\n// the window will be notified via the `load-settings-changed` config\n//\n// Swap out Node's native Promise for Bluebird, which allows us to\n// do fancy things like handle exceptions inside promise blocks\nglobal.Promise = require('bluebird');\nconst timeout = global.setTimeout;\nPromise.setScheduler((fn) => timeout(fn, 0));\n\nimport './window';\n\nimport NylasEnvConstructor from './nylas-env';\nwindow.NylasEnv = window.atom = NylasEnvConstructor.loadOrCreate();\n\nNylasEnv.initialize();\nNylasEnv.startSecondaryWindow();\n\n// Workaround for focus getting cleared upon window creation\nconst windowFocused = () => {\n  window.removeEventListener('focus', windowFocused);\n  return setTimeout((() => document.querySelector('body').focus()), 0);\n};\nwindow.addEventListener('focus', windowFocused);\n"
  },
  {
    "path": "packages/client-app/src/services/battery-status-manager.es6",
    "content": "import moment from 'moment-timezone'\nimport Actions from '../flux/actions'\n\nclass BatteryStatusManager {\n  constructor() {\n    this._callbacks = [];\n    this._battery = null;\n    this._lastChangeTime = Date.now();\n  }\n\n  async activate() {\n    if (this._battery) {\n      return;\n    }\n    this._battery = await navigator.getBattery();\n    this._battery.addEventListener('chargingchange', this._onChargingChange);\n  }\n\n  deactivate() {\n    if (!this._battery) {\n      return;\n    }\n    this._battery.removeEventListener('chargingchange', this._onChargingChange);\n    this._battery = null;\n  }\n\n  _onChargingChange = () => {\n    const changeTime = Date.now();\n    Actions.recordUserEvent(\"Battery State Changed\", {\n      oldState: this.isBatteryCharging() ? 'battery' : 'ac',\n      oldStateDuration: Math.min(changeTime - this._lastChangeTime, moment.duration(12, 'hours').asMilliseconds()),\n    });\n    this._lastChangeTime = changeTime;\n    this._callbacks.forEach(cb => cb());\n  }\n\n  onChange(callback) {\n    this._callbacks.push(callback);\n  }\n\n  isBatteryCharging() {\n    if (!this._battery) {\n      return false;\n    }\n    return this._battery.charging;\n  }\n}\n\nconst manager = new BatteryStatusManager();\nmanager.activate();\nexport default manager;\n"
  },
  {
    "path": "packages/client-app/src/services/delta-processor.es6",
    "content": "import _ from 'underscore';\nimport Actions from '../flux/actions'\nimport Thread from '../flux/models/thread'\nimport Message from '../flux/models/message'\nimport DatabaseStore from '../flux/stores/database-store'\nimport * as NylasAPIHelpers from '../flux/nylas-api-helpers'\n\n/**\n * This injests deltas from multiple sources. One is from local-sync, the\n * other is from n1-cloud. Both sources use\n * isomorphic-core/src/delta-stream-builder to generate the delta stream.\n *\n * In both cases we are given the JSON serialized form of a `Transaction`\n * model. An example Thread delta would look like:\n *\n *   modelDelta = {\n *     id: 518,\n *     event: \"modify\",\n *     object: \"thread\",\n *     objectId: 2887,\n *     changedFields: [\"subject\", \"unread\"],\n *     attributes: {\n *       id: 2887,\n *       object: 'thread',\n *       account_id: 2,\n *       subject: \"Hello World\",\n *       unread: true,\n *       ...\n *     }\n *   }\n *\n * An example Metadata delta would look like:\n *\n *   metadataDelta = {\n *     id: 519,\n *     event: \"create\",\n *     object: \"metadata\",\n *     objectId: 8876,\n *     changedFields: [\"version\", \"object\"],\n *     attributes: {\n *       id: 8876,\n *       value: {link_clicks: 1},\n *       object: \"metadata\",\n *       version: 2,\n *       plugin_id: \"link-tracking\",\n *       object_id: 2887,\n *       object_type: \"thread\"\n *       account_id: 2,\n *     }\n *   }\n *\n * The `object` may be \"thread\", \"message\", \"metadata\", or any other model\n * type we support\n */\nclass DeltaProcessor {\n  constructor() {\n    this.activationTime = Date.now()\n  }\n\n  async process(rawDeltas = [], {source} = {}) {\n    try {\n      const deltas = await this._decorateDeltas(rawDeltas);\n      if (source === \"n1Cloud\") {\n        Actions.longPollReceivedRawDeltas(deltas);\n      }\n\n      const {\n        modelDeltas,\n        accountDeltas,\n        metadataDeltas,\n      } = this._extractDeltaTypes(deltas);\n      this._handleAccountDeltas(accountDeltas);\n\n      const models = await this._saveModels(modelDeltas);\n      await this._saveMetadata(metadataDeltas);\n      await this._notifyOfNewMessages(models.created);\n      this._notifyOfSyncbackRequestDeltas(models)\n    } catch (err) {\n      console.error(rawDeltas)\n      console.error(\"DeltaProcessor: Process failed.\", err)\n      NylasEnv.reportError(err);\n    } finally {\n      Actions.longPollProcessedDeltas()\n    }\n  }\n\n  /**\n   * Create a (non-enumerable) reference from the attributes which we\n   * carry forward back to their original deltas. This allows us to\n   * mark the deltas that the app ignores later in the process.\n   */\n  _decorateDeltas(rawDeltas) {\n    rawDeltas.forEach((delta) => {\n      if (!delta.attributes) return;\n      Object.defineProperty(delta.attributes, '_delta', {\n        configurable: true,\n        get() { return delta; },\n      });\n    })\n    return rawDeltas\n  }\n\n  _extractDeltaTypes(rawDeltas) {\n    const modelDeltas = []\n    const accountDeltas = []\n    const metadataDeltas = []\n    rawDeltas.forEach((delta) => {\n      if (delta.object === \"metadata\") {\n        metadataDeltas.push(delta)\n      } else if (delta.object === \"account\") {\n        accountDeltas.push(delta)\n      } else {\n        modelDeltas.push(delta)\n      }\n    })\n    return {modelDeltas, metadataDeltas, accountDeltas}\n  }\n\n  _handleAccountDeltas = (accountDeltas) => {\n    const {modify} = this._clusterDeltas(accountDeltas);\n    if (!modify.account) return;\n    for (const accountJSON of _.values(modify.account)) {\n      Actions.updateAccount(accountJSON.account_id, {syncState: accountJSON.sync_state});\n      if (accountJSON.sync_state !== \"running\") {\n        Actions.recordUserEvent('Account Sync Errored', {\n          accountId: accountJSON.account_id,\n          syncState: accountJSON.sync_state,\n        });\n      }\n    }\n  }\n\n  _notifyOfSyncbackRequestDeltas({created, updated} = {}) {\n    const createdRequests = created.syncbackRequest || []\n    const updatedRequests = updated.syncbackRequest || []\n    const syncbackRequests = createdRequests.concat(updatedRequests)\n    if (syncbackRequests.length === 0) { return }\n\n    Actions.didReceiveSyncbackRequestDeltas(syncbackRequests)\n  }\n\n  async _saveModels(modelDeltas) {\n    const {create, modify, destroy} = this._clusterDeltas(modelDeltas);\n\n    const created = await Promise.props(_.mapObject(create, (val) =>\n      NylasAPIHelpers.handleModelResponse(_.values(val))\n    ))\n\n    const updated = await Promise.props(_.mapObject(modify, (val) =>\n      NylasAPIHelpers.handleModelResponse(_.values(val))\n    ));\n\n    await Promise.map(destroy, this._handleDestroyDelta);\n\n    return {created, updated};\n  }\n\n  async _saveMetadata(deltas) {\n    const all = {};\n\n    for (const delta of deltas.filter(d => d.event === 'create')) {\n      all[delta.attributes.object_id] = delta.attributes;\n    }\n    for (const delta of deltas.filter(d => d.event === 'modify')) {\n      all[delta.attributes.object_id] = delta.attributes;\n    }\n    const allByObjectType = _.groupBy(_.values(all), \"object_type\")\n\n    return Promise.map(Object.keys(allByObjectType), (objType) => {\n      const jsons = allByObjectType[objType]\n      const Klass = NylasAPIHelpers.apiObjectToClassMap[objType];\n      const objectIds = jsons.map(j => j.object_id)\n\n      return DatabaseStore.inTransaction((t) => {\n        return this._findOrCreateModelsForMetadata(t, Klass, objectIds).then((modelsByObjectId) => {\n          const models = [];\n          Object.keys(modelsByObjectId).forEach((objectId) => {\n            const model = modelsByObjectId[objectId];\n            const metadataJSON = all[objectId];\n            const modelWithMetadata = model.applyPluginMetadata(metadataJSON.plugin_id, metadataJSON.value);\n            const localMetadatum = modelWithMetadata.metadataObjectForPluginId(metadataJSON.plugin_id);\n            localMetadatum.version = metadataJSON.version;\n            models.push(modelWithMetadata);\n          })\n          return t.persistModels(models)\n        });\n      });\n    })\n  }\n\n\n  /**\n  @param ids An array of metadata object_ids that correspond to threads\n  @returns A map of the object_ids to threads in the database, resolving the\n  IDs as necessary. If the thread does not yet exist, we create a ghost thread that\n  contains only the serverId and the metadata.\n  */\n  async _findOrCreateThreadsForMetadata(t, ids) {\n    // Since threads can have different ids, we need to find the equivalent threads\n    // through their messages. First, find the messages that correspond to the thread\n    // ids (which are of the format `t:${messageId}`)\n    const messageIds = ids.map(i => i.substring(2))\n    const messages = await t.findAll(Message, {id: messageIds})\n\n    // Create a map of which local thread ids are equivalent to the ids from the server\n    const localIdToRemoteId = {}\n    messages.forEach(msg => { localIdToRemoteId[msg.threadId] = `t:${msg.id}` })\n\n    // Then map the actual thread models to the server ids\n    const threads = await t.findAll(Thread, {id: Object.keys(localIdToRemoteId)})\n    const map = {};\n    for (const thread of threads) {\n      const pluginObjectId = localIdToRemoteId[thread.id]\n      map[pluginObjectId] = thread;\n    }\n\n    // Create ghost models for threads that we haven't synced yet\n    const missingIds = ids.filter(id => !map[id])\n    const newThreads = [];\n    const newMessages = [];\n    missingIds.forEach(id => {\n      // Build both the thread and corresponding message. We won't be able to find\n      // the ghost thread if the message with the corresponding id doesn't exist.\n      const thread = new Thread();\n      thread.serverId = id;\n      thread.categories = [];\n      const message = new Message();\n      message.serverId = id.substring(2);\n      message.threadId = id;\n\n      map[id] = thread;\n      newThreads.push(thread);\n      newMessages.push(message);\n    })\n\n    if (newThreads.length > 0) {\n      await t.persistModels(newThreads);\n      await t.persistModels(newMessages);\n    }\n    return map;\n  }\n\n  /**\n  @param ids An array of metadata object_ids\n  @returns A map of the object_ids to models in the database, resolving the\n  IDs as necessary. If the model does not yet exist, we create a ghost model that\n  contains only the serverId and the metadata.\n  */\n  async _findOrCreateModelsForMetadata(t, Klass, ids) {\n    if (Klass === Thread) {\n      return this._findOrCreateThreadsForMetadata(t, ids)\n    }\n\n    const models = await t.findAll(Klass, {id: ids})\n    const map = {};\n    for (const model of models) {\n      const pluginObjectId = model.id;\n      map[pluginObjectId] = model;\n    }\n\n    // Build ghost models for objects we haven't synced yet\n    const missingIds = ids.filter(id => !map[id])\n    const instances = []\n    missingIds.forEach(id => {\n      const klass = new Klass({serverId: id})\n      map[id] = klass\n      instances.push(klass)\n    })\n\n    if (instances.length > 0) {\n      await t.persistModels(instances)\n    }\n    return map;\n  }\n\n  /**\n   * Group deltas by object type so we can mutate the cache efficiently.\n   * NOTE: This code must not just accumulate creates, modifies and\n   * destroys but also de-dupe them. We cannot call\n   * \"persistModels(itemA, itemA, itemB)\" or it will throw an exception\n   */\n  _clusterDeltas(deltas) {\n    const create = {};\n    const modify = {};\n    const destroy = [];\n    for (const delta of deltas) {\n      if (delta.event === 'create') {\n        if (!create[delta.object]) { create[delta.object] = {}; }\n        create[delta.object][delta.attributes.id] = delta.attributes;\n      } else if (delta.event === 'modify') {\n        if (!modify[delta.object]) { modify[delta.object] = {}; }\n        modify[delta.object][delta.attributes.id] = delta.attributes;\n      } else if (delta.event === 'delete') {\n        destroy.push(delta);\n      }\n    }\n\n    return {create, modify, destroy};\n  }\n\n  async _notifyOfNewMessages(created) {\n    const incomingMessages = created.message || [];\n\n    // Filter for new messages that are not sent by the current user\n    const newUnread = incomingMessages.filter((msg) => {\n      const isUnread = msg.unread === true;\n      const isNew = msg.date && msg.date.valueOf() >= this.activationTime;\n      const isFromMe = msg.isFromMe();\n      return isUnread && isNew && !isFromMe;\n    });\n\n    if (newUnread.length === 0) {\n      return;\n    }\n\n    Actions.onNewMailDeltas(created)\n  }\n\n  _handleDestroyDelta(delta) {\n    const klass = NylasAPIHelpers.apiObjectToClassMap[delta.object];\n    if (!klass) { return Promise.resolve(); }\n\n    return DatabaseStore.inTransaction(t => {\n      return t.find(klass, delta.objectId).then((model) => {\n        if (!model) { return Promise.resolve(); }\n        return t.unpersistModel(model);\n      });\n    });\n  }\n}\n\nexport default new DeltaProcessor()\n"
  },
  {
    "path": "packages/client-app/src/services/delta-streaming-connection.es6",
    "content": "import _ from 'underscore'\nimport {ExponentialBackoffScheduler} from 'isomorphic-core'\nimport N1CloudAPI from '../n1-cloud-api'\nimport Actions from '../flux/actions'\nimport {APIError} from '../flux/errors'\nimport Account from '../flux/models/account'\nimport DeltaProcessor from './delta-processor'\nimport DatabaseStore from '../flux/stores/database-store'\nimport IdentityStore from '../flux/stores/identity-store'\nimport OnlineStatusStore from '../flux/stores/online-status-store'\nimport NylasLongConnection from '../flux/nylas-long-connection'\n\n\nconst MAX_RETRY_DELAY = 10 * 60 * 1000;\nconst BASE_RETRY_DELAY = 1000;\n\nclass DeltaStreamingConnection {\n  constructor(account) {\n    this._account = account\n    this._state = null\n    this._longConnection = null\n    this._retryTimeout = null\n    this._unsubscribers = []\n    this._writeStateDebounced = _.debounce(this._writeState, 100)\n    this._backoffScheduler = new ExponentialBackoffScheduler({\n      baseDelay: BASE_RETRY_DELAY,\n      maxDelay: MAX_RETRY_DELAY,\n    })\n\n    this._setupListeners()\n    NylasEnv.onBeforeUnload = (readyToUnload) => {\n      this._writeState().finally(readyToUnload)\n    }\n  }\n\n  async start() {\n    try {\n      if (!IdentityStore.identity()) {\n        console.warn(`Can't start DeltaStreamingConnection without a Nylas Identity`)\n        return\n      }\n      if (!this._state) {\n        this._state = await this._loadState()\n      }\n      const cursor = this._state.cursor || 0\n      this._clearRetryTimeout()\n      this._longConnection = new NylasLongConnection({\n        api: N1CloudAPI,\n        accountId: this._account.id,\n        path: `/delta/streaming?cursor=${cursor}`,\n        throttleResultsInterval: 1000,\n        closeIfDataStopsInterval: 15 * 1000,\n        onError: this._onError,\n        onResults: this._onResults,\n        onStatusChanged: this._onStatusChanged,\n      })\n      this._longConnection.start()\n    } catch (err) {\n      this._onError(err)\n    }\n  }\n\n  restart() {\n    try {\n      this._restarting = true\n      this.close();\n      this._disposeListeners()\n      this._setupListeners()\n      this.start();\n    } finally {\n      this._restarting = false\n    }\n  }\n\n  close() {\n    this._clearRetryTimeout()\n    this._disposeListeners()\n    if (this._longConnection) {\n      this._longConnection.close()\n    }\n  }\n\n  end() {\n    this._clearRetryTimeout()\n    this._disposeListeners()\n    if (this._longConnection) {\n      this._longConnection.end()\n    }\n  }\n\n  _setupListeners() {\n    this._unsubscribers = [\n      Actions.retryDeltaConnection.listen(this.restart, this),\n      OnlineStatusStore.listen(this._onOnlineStatusChanged, this),\n      IdentityStore.listen(this._onIdentityChanged, this),\n    ]\n  }\n\n  _disposeListeners() {\n    this._unsubscribers.forEach(usub => usub())\n    this._unsubscribers = []\n  }\n\n  _clearRetryTimeout() {\n    clearTimeout(this._retryTimeout)\n    this._retryTimeout = null\n  }\n\n  _onOnlineStatusChanged = () => {\n    if (OnlineStatusStore.isOnline()) {\n      this.restart()\n    }\n  }\n\n  _onIdentityChanged = () => {\n    if (IdentityStore.identity()) {\n      this.restart()\n    }\n  }\n\n  _onStatusChanged = (status) => {\n    if (this._restarting) { return; }\n    this._state.status = status;\n    this._writeStateDebounced();\n    const {Closed, Connected} = NylasLongConnection.Status\n    if (status === Connected) {\n      Actions.updateAccount(this._account.id, {\n        n1CloudState: Account.N1_CLOUD_STATE_RUNNING,\n      })\n    }\n    if (status === Closed) {\n      if (this._retryTimeout) { return }\n      this._clearRetryTimeout()\n      this._retryTimeout = setTimeout(() => this.restart(), this._backoffScheduler.nextDelay());\n    }\n  }\n\n  _onResults = (deltas = []) => {\n    this._backoffScheduler.reset()\n\n    const last = _.last(deltas);\n    if (last && last.cursor) {\n      this._setCursor(last.cursor)\n    }\n    DeltaProcessor.process(deltas, {source: 'n1Cloud'})\n  }\n\n  _onError = (err = {}) => {\n    if (err.message && err.message.includes('Invalid cursor')) {\n      // TODO is this still necessary?\n      const error = new Error('DeltaStreamingConnection: Cursor is invalid. Need to blow away local cache.');\n      NylasEnv.reportError(error)\n      this._setCursor(0)\n      const app = require('electron').remote.getGlobal('application') // eslint-disable-line\n      app.rebuildDatabase({showErrorDialog: false})\n      return\n    }\n\n    err.message = `Error connecting to delta stream: ${err.message}`\n    if (!(err instanceof APIError)) {\n      NylasEnv.reportError(err)\n      return\n    }\n\n    if (err.shouldReportError()) {\n      // TODO move this check into NylasEnv.reportError()?\n      NylasEnv.reportError(err)\n    }\n\n    if (err.statusCode === 401) {\n      Actions.updateAccount(this._account.id, {\n        n1CloudState: Account.N1_CLOUD_STATE_AUTH_FAILED,\n      })\n    }\n  }\n\n  _setCursor = (cursor) => {\n    this._state.cursor = cursor;\n    this._writeStateDebounced();\n  }\n\n  async _loadState() {\n    const json = await DatabaseStore.findJSONBlob(`DeltaStreamingConnectionStatus:${this._account.id}`)\n    if (json) {\n      return {\n        cursor: json.cursor || undefined,\n        status: json.status || undefined,\n      }\n    }\n\n    // Migrate from old storage key\n    const oldState = await DatabaseStore.findJSONBlob(`NylasSyncWorker:${this._account.id}`)\n    if (!oldState) {\n      return {\n        cursor: undefined,\n        status: undefined,\n      };\n    }\n\n    const {deltaCursors = {}, deltaStatus = {}} = oldState\n    return {\n      cursor: deltaCursors.n1Cloud,\n      status: deltaStatus.n1Cloud,\n    }\n  }\n\n  async _writeState() {\n    if (!this._state) { return }\n    await DatabaseStore.inTransaction(t =>\n      t.persistJSONBlob(`DeltaStreamingConnectionStatus:${this._account.id}`, this._state)\n    );\n  }\n}\n\nexport default DeltaStreamingConnection\n"
  },
  {
    "path": "packages/client-app/src/services/inline-style-transformer.es6",
    "content": "/* eslint global-require: 0 */\nimport {ipcRenderer} from \"electron\";\nimport crypto from 'crypto';\nimport _ from 'underscore';\n\nimport RegExpUtils from '../regexp-utils';\n\nlet userAgentDefault = null;\n\nclass InlineStyleTransformer {\n  constructor() {\n    this.run = this.run.bind(this);\n    this._onInlineStylesResult = this._onInlineStylesResult.bind(this);\n    this._inlineStylePromises = {};\n    this._inlineStyleResolvers = {};\n    ipcRenderer.on('inline-styles-result', this._onInlineStylesResult);\n  }\n\n  run(html) {\n    if (!html || !_.isString(html) || html.length <= 0) {\n      return Promise.resolve(html);\n    }\n    if (!RegExpUtils.looseStyleTag().test(html)) {\n      return Promise.resolve(html);\n    }\n\n    const key = crypto.createHash('md5').update(html).digest('hex');\n\n    // http://stackoverflow.com/questions/8695031/why-is-there-often-a-inside-the-style-tag\n    // https://regex101.com/r/bZ5tX4/1\n    let styled = html.replace(/<style[^>]*>[\\n\\r]*<!--([^</]*)-->[\\n\\r]*<\\/style/g, (full, content) =>\n      `<style>${content}</style`\n    );\n\n    styled = this._injectUserAgentStyles(styled);\n\n    if (this._inlineStylePromises[key] == null) {\n      this._inlineStylePromises[key] = new Promise((resolve) => {\n        this._inlineStyleResolvers[key] = resolve;\n        ipcRenderer.send('inline-style-parse', {html: styled, key: key});\n      });\n    }\n    return this._inlineStylePromises[key];\n  }\n\n  // This will prepend the user agent stylesheet so we can apply it to the\n  // styles properly.\n  _injectUserAgentStyles(body) {\n    // No DOM parsing! Just find the first <style> tag and prepend there.\n    const i = body.search(RegExpUtils.looseStyleTag());\n    if (i === -1) { return body; }\n\n    if (typeof userAgentDefault === 'undefined' || userAgentDefault === null) {\n      userAgentDefault = require('../chrome-user-agent-stylesheet-string');\n    }\n    return `${body.slice(0, i)}<style>${userAgentDefault}</style>${body.slice(i)}`;\n  }\n\n  _onInlineStylesResult(event, {html, key}) {\n    delete this._inlineStylePromises[key];\n    this._inlineStyleResolvers[key](html);\n    delete this._inlineStyleResolvers[key];\n  }\n}\n\nexport default new InlineStyleTransformer();\n"
  },
  {
    "path": "packages/client-app/src/services/model-search-indexer.es6",
    "content": "import DatabaseStore from '../flux/stores/database-store'\nimport SearchIndexScheduler from './search-index-scheduler'\n\nconst INDEXING_PAGE_SIZE = 1000;\nconst INDEXING_PAGE_DELAY = 1000;\n\nexport default class ModelSearchIndexer {\n  constructor() {\n    this.unsubscribers = []\n    this.indexer = SearchIndexScheduler;\n  }\n\n  get MaxIndexSize() {\n    throw new Error(\"Override me and return a number\")\n  }\n\n  get ConfigKey() {\n    throw new Error(\"Override me and return a string config key\")\n  }\n\n  get IndexVersion() {\n    throw new Error(\"Override me and return an IndexVersion\")\n  }\n\n  get ModelClass() {\n    throw new Error(\"Override me and return a class constructor\")\n  }\n\n  getIndexDataForModel() {\n    throw new Error(\"Override me and return a hash with a `content` array\")\n  }\n\n  activate() {\n    this.indexer.registerSearchableModel({\n      modelClass: this.ModelClass,\n      indexSize: this.MaxIndexSize,\n      indexCallback: (model) => this._indexModel(model),\n      unindexCallback: (model) => this._unindexModel(model),\n    });\n\n    this._initializeIndex();\n    this.unsubscribers = [\n      // TODO listen for changes in AccountStore\n      DatabaseStore.listen(this._onDataChanged),\n      () => this.indexer.unregisterSearchableModel(this.ModelClass),\n    ];\n  }\n\n  deactivate() {\n    this.unsubscribers.forEach(unsub => unsub())\n  }\n\n  _initializeIndex() {\n    if (NylasEnv.config.get(this.ConfigKey) !== this.IndexVersion) {\n      return DatabaseStore.dropSearchIndex(this.ModelClass)\n      .then(() => DatabaseStore.createSearchIndex(this.ModelClass))\n      .then(() => this._buildIndex())\n    }\n    return Promise.resolve()\n  }\n\n  _buildIndex(offset = 0) {\n    const {ModelClass, IndexVersion, ConfigKey} = this\n    return DatabaseStore.findAll(ModelClass)\n    .limit(INDEXING_PAGE_SIZE)\n    .offset(offset)\n    .background()\n    .then((models) => {\n      if (models.length === 0) {\n        NylasEnv.config.set(ConfigKey, IndexVersion)\n        return;\n      }\n      Promise.each(models, (model) => {\n        return DatabaseStore.indexModel(model, this.getIndexDataForModel(model))\n      })\n      .then(() => {\n        setTimeout(() => {\n          this._buildIndex(offset + models.length);\n        }, INDEXING_PAGE_DELAY);\n      });\n    });\n  }\n\n  _indexModel(model) {\n    DatabaseStore.indexModel(model, this.getIndexDataForModel(model))\n  }\n\n  _unindexModel(model) {\n    DatabaseStore.unindexModel(model)\n  }\n\n  /**\n   * When a model gets updated we will update the search index with the\n   * data from that model if the account it belongs to is not being\n   * currently synced.\n   */\n  _onDataChanged = (change) => {\n    if (change.objectClass !== this.ModelClass.name) {\n      return;\n    }\n\n    change.objects.forEach((model) => {\n      if (change.type === 'persist') {\n        this.indexer.notifyHasIndexingToDo();\n      } else {\n        this._unindexModel(model);\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/services/quote-string-detector.es6",
    "content": "import DOMWalkers from '../dom-walkers'\n\n/*\n * There are semi-common cases where immediately before a blockquote, we\n * encounter a string like: \"On Thu … so and so … wrote:\". This should be part\n * of the blockquote but was usually left as a collection of nodes. To help\n * with false-positives, we only look for strings like that that immediately\n * preceeded the blockquoted section. By the time the function gets here, the\n * last blockquote has been removed and the text we want will be at the end of\n * the document.\n *\n * This is in its own file to make use of ES6 generators\n *\n * See email_19 as a test case for this.\n */\nexport default function quoteStringDetector(doc) {\n  const quoteNodesToRemove = [];\n  let seenInitialQuoteEnd = false;\n  for (const node of DOMWalkers.walkBackwards(doc)) {\n    if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim().length > 0) {\n      if (!seenInitialQuoteEnd) {\n        if (/wrote:\\s*$/gim.test(node.nodeValue)) {\n          seenInitialQuoteEnd = true;\n          quoteNodesToRemove.push(node);\n          if (/On \\S/gim.test(node.nodeValue)) {\n            // The beginning of the quoted string may be in the same node\n            return quoteNodesToRemove;\n          }\n        } else {\n          // This means there's some text in between the end of the content\n          // (adjacent to the blockquote) and the quote string. We shouldn't be\n          // killing any text in this case.\n          return quoteNodesToRemove;\n        }\n      } else {\n        quoteNodesToRemove.push(node)\n        if (/On \\S/gim.test(node.nodeValue)) {\n          // This means we've reached the beginning of the quoted string.\n          return quoteNodesToRemove;\n        }\n      }\n    } else {\n      if (seenInitialQuoteEnd) {\n        quoteNodesToRemove.push(node)\n      }\n    }\n  }\n  return quoteNodesToRemove;\n}\n"
  },
  {
    "path": "packages/client-app/src/services/quoted-html-transformer.es6",
    "content": "import _ from 'underscore';\nimport DOMUtils from '../dom-utils';\nimport quoteStringDetector from './quote-string-detector';\nimport unwrappedSignatureDetector from './unwrapped-signature-detector';\n\nclass QuotedHTMLTransformer {\n\n  annotationClass = \"nylas-quoted-text-segment\";\n\n  // Given an html string, it will add the `annotationClass` to the DOM\n  // element\n  hideQuotedHTML(html, {keepIfWholeBodyIsQuote} = {}) {\n    const doc = this._parseHTML(html);\n    const quoteElements = this._findQuoteLikeElements(doc);\n    if (!keepIfWholeBodyIsQuote || !this._wholeBodyIsQuote(doc, quoteElements)) {\n      this._annotateElements(quoteElements);\n    }\n    return this._outputHTMLFor(doc, {initialHTML: html});\n  }\n\n  hasQuotedHTML(html) {\n    const doc = this._parseHTML(html);\n    const quoteElements = this._findQuoteLikeElements(doc);\n    return quoteElements.length > 0;\n  }\n\n  // Public: Removes quoted text from an HTML string\n  //\n  // If we find a quoted text region that is \"inline\" with the root level\n  // message, meaning it has non quoted text before and after it, then we\n  // leave it in the message. If you set the `includeInline` option to true,\n  // then all inline blocks will also be removed.\n  //\n  // - `html` The string full of quoted text areas\n  // - `options`\n  //   - `includeInline` Defaults false. If true, inline quotes are removed\n  //   too\n  //   - `keepIfWholeBodyIsQuote` Defaults false. If true, then it will\n  //   check to see if the whole html body is a giant quote. If so, it will\n  //   preserve it.\n  //\n  // Returns HTML without quoted text\n  removeQuotedHTML(html, options = {keepIfWholeBodyIsQuote: true}) {\n    const doc = this._parseHTML(html);\n    const quoteElements = this._findQuoteLikeElements(doc, options);\n    const asDOM = !!options.asDOM\n\n    if (options.keepIfWholeBodyIsQuote && this._wholeBodyIsQuote(doc, quoteElements)) {\n      return this._outputHTMLFor(this._parseHTML(html), {initialHTML: html, asDOM});\n    }\n\n    DOMUtils.Mutating.removeElements(quoteElements, options);\n\n    // It's possible that the entire body was quoted text anyway and we've\n    // removed everything.\n    if (options.keepIfWholeBodyIsQuote && (!doc.body || !doc.children[0])) {\n      return this._outputHTMLFor(this._parseHTML(html), {initialHTML: html, asDOM});\n    }\n\n    if (!doc.body) {\n      return this._outputHTMLFor(this._parseHTML(\"\"), {initialHTML: html, asDOM});\n    }\n\n    this.removeTrailingBr(doc);\n    DOMUtils.Mutating.removeElements(quoteStringDetector(doc));\n    if (options.keepIfWholeBodyIsQuote && (!doc.children[0] || this._wholeNylasPlaintextBodyIsQuote(doc))) {\n      return this._outputHTMLFor(this._parseHTML(html), {initialHTML: html, asDOM});\n    }\n\n    return this._outputHTMLFor(doc, {initialHTML: html, asDOM});\n  }\n\n  // Finds any trailing BR tags and removes them in place\n  removeTrailingBr(doc) {\n    const { childNodes } = doc.body;\n    const extraTailBrTags = [];\n    for (let i = childNodes.length - 1; i >= 0; i--) {\n      const curr = childNodes[i];\n      const next = childNodes[i - 1];\n      if (curr && curr.nodeName === 'BR' && next && next.nodeName === 'BR') {\n        extraTailBrTags.push(curr);\n      } else {\n        break;\n      }\n    }\n    return DOMUtils.Mutating.removeElements(extraTailBrTags);\n  }\n\n  appendQuotedHTML(htmlWithoutQuotes, originalHTML) {\n    let doc = this._parseHTML(originalHTML);\n    const quoteElements = this._findQuoteLikeElements(doc);\n    doc = this._parseHTML(htmlWithoutQuotes);\n    for (let i = 0; i < quoteElements.length; i++) {\n      const node = quoteElements[i];\n      doc.body.appendChild(node);\n    }\n    return this._outputHTMLFor(doc, {initialHTML: originalHTML});\n  }\n\n  restoreAnnotatedHTML(html) {\n    const doc = this._parseHTML(html);\n    const quoteElements = this._findAnnotatedElements(doc);\n    this._removeAnnotation(quoteElements);\n    return this._outputHTMLFor(doc, {initialHTML: html});\n  }\n\n  _parseHTML(text) {\n    const domParser = new DOMParser();\n    let doc;\n    try {\n      doc = domParser.parseFromString(text, \"text/html\");\n    } catch (error) {\n      const errText = `HTML Parser Error: ${error.toString()}`;\n      doc = domParser.parseFromString(errText, \"text/html\");\n      NylasEnv.reportError(error);\n    }\n\n    // As far as we can tell, when this succeeds, doc /always/ has at least\n    // one child: an <html> node.\n    return doc;\n  }\n\n  _outputHTMLFor(doc, {initialHTML, asDOM} = {}) {\n    if (asDOM) return doc\n    if (/<\\s?head\\s?>/i.test(initialHTML) || /<\\s?body[\\s>]/i.test(initialHTML)) {\n      return doc.children[0].innerHTML;\n    }\n    return doc.body.innerHTML;\n  }\n\n  _wholeNylasPlaintextBodyIsQuote(doc) {\n    const preElement = doc.body.children[0];\n    return (preElement && preElement.tagName === 'PRE' && !preElement.children[0]);\n  }\n\n  _wholeBodyIsQuote(doc, quoteElements) {\n    const nonBlankChildElements = [];\n    for (let i = 0; i < doc.body.childNodes.length; i++) {\n      const child = doc.body.childNodes[i];\n      if (child.textContent.trim() === \"\") {\n        continue;\n      } else { nonBlankChildElements.push(child); }\n    }\n\n    if (nonBlankChildElements.length === 1) {\n      return Array.from(quoteElements).includes(nonBlankChildElements[0])\n    }\n    return false;\n  }\n\n    // We used to have a scheme where we cached the `doc` object, keyed by\n    // the md5 of the text. Unfortunately we can't do this because the\n    // `doc` is mutated in place. Returning clones of the DOM is just as\n    // bad as re-parsing from string, which is very fast anyway.\n\n  _findQuoteLikeElements(doc, {includeInline} = {}) {\n    const parsers = [\n      this._findGmailQuotes,\n      this._findOffice365Quotes,\n      this._findBlockquoteQuotes,\n    ];\n\n    let quoteElements = [];\n    for (const parser of parsers) {\n      quoteElements = quoteElements.concat(parser(doc) || []);\n    }\n\n    /**\n     * At this point we've pulled out of the DOM all elements that happen\n     * to look like quote blocks via CSS selectors and other patterns.\n     * They are not necessarily ordered nor should all be eliminated\n     * (because people can type inline around quoted text blocks).\n     *\n     * The `unwrappedSignatureDetector` looks for a case when signatures\n     * look almost exactly like someone replying inline at the end of the\n     * message. We detect this case (by looking for signature text\n     * repetition) and add it to the set of flagged quote candidates.\n     */\n    const unwrappedSignatureNodes = unwrappedSignatureDetector(doc, quoteElements)\n    quoteElements = quoteElements.concat(unwrappedSignatureNodes)\n\n    if (!includeInline && quoteElements.length > 0) {\n      const trailingQuotes = this._findTrailingQuotes(doc, Array.from(quoteElements));\n\n      // Only keep the trailing quotes so we can delete them.\n      /**\n       * The _findTrailingQuotes method will return an array of the quote\n       * elements we should remove. If there was no trailing text, it\n       * should include all of the existing VISIBLE quoteElements. If\n       * there was trailing text, it will only include the quote elements\n       * up to that trailling text. The intersection below will only\n       * mark the quote elements below trailing text ot be deleted.\n       */\n      quoteElements = _.intersection(quoteElements, trailingQuotes);\n\n      /**\n       * The _findTraillingQuotes method only preserves VISIBLE elements.\n       * It's possible that the unwrappedSignatureDetector discovered a\n       * collection of nodes with both visible and not visible (like br)\n       * content. If we're going to get rid of trailing signatures we\n       * need to also remove those trailling <br/>s, or we can get a bunch\n       * of blank space at the end of the text. First make sure that some\n       * of our unwrappedSignatureNodes were marked for deletion, and then\n       * make sure we include all of them.\n       */\n      if (_.intersection(quoteElements, unwrappedSignatureNodes).length > 0) {\n        quoteElements = _.uniq(quoteElements.concat(unwrappedSignatureNodes))\n      }\n    }\n\n    return _.compact(_.uniq(quoteElements));\n  }\n\n  /**\n   * Now that we have a set of quoted text candidates, we need to figure\n   * out which ones to remove. The main thing preventing us from removing\n   * all of them is the fact users can type text after quoted text as an\n   * inline reply.\n   *\n   * To detect this, we recursively move through the dom backwards, from\n   * bottom to top, and keep going until we find visible text that's not a\n   * quote candidate. If we find some visible text, we assume that is\n   * unique text that a user wrote. We return at that point assuming that\n   * everything at the text and above should be visible, even if it's a\n   * quoted text candidate.\n   *\n   * See email_18 and email_23 and unwrapped-signature-detector\n   */\n  _findTrailingQuotes(scopeElement, quoteElements = []) {\n    let trailingQuotes = [];\n\n    // We need to find only the child nodes that have content in them. We\n    // determine if it's an inline quote based on if there's VISIBLE\n    // content after a piece of quoted text\n    const nodesWithContent = DOMUtils.nodesWithContent(scopeElement);\n\n    // There may be multiple quote blocks that are sibilings of each\n    // other at the end of the message. We want to include all of these\n    // trailing quote elements.\n    for (let i = nodesWithContent.length - 1; i >= 0; i--) {\n      const nodeWithContent = nodesWithContent[i];\n      if (quoteElements.includes(nodeWithContent)) {\n        // This is a valid quote. Let's keep it!\n        //\n        // This quote block may have many more quote blocks inside of it.\n        // Luckily we don't need to explicitly find all of those because\n        // one this block gets removed from the DOM, we'll delete all\n        // sub-quotes as well.\n        trailingQuotes.push(nodeWithContent);\n        continue;\n      } else {\n        const moreTrailing = this._findTrailingQuotes(nodeWithContent, quoteElements);\n        trailingQuotes = trailingQuotes.concat(moreTrailing);\n        break;\n      }\n    }\n\n    return trailingQuotes;\n  }\n\n  _contains(node, quoteElement) {\n    return node === quoteElement || node.contains(quoteElement);\n  }\n\n  _findAnnotatedElements(doc) {\n    return Array.from(doc.getElementsByClassName(this.annotationClass));\n  }\n\n  _annotateElements(elements = []) {\n    let originalDisplay;\n    return elements.forEach((el) => {\n      el.classList.add(this.annotationClass)\n      originalDisplay = el.style.display\n      el.style.display = \"none\"\n      el.setAttribute(\"data-nylas-quoted-text-original-display\", originalDisplay);\n    });\n  }\n\n  _removeAnnotation(elements = []) {\n    let originalDisplay;\n    return elements.forEach((el) => {\n      el.classList.remove(this.annotationClass)\n      originalDisplay = el.getAttribute(\"data-nylas-quoted-text-original-display\")\n      el.style.display = originalDisplay\n      el.removeAttribute(\"data-nylas-quoted-text-original-display\");\n    })\n  }\n\n  _findGmailQuotes(doc) {\n    // Gmail creates both div.gmail_quote and blockquote.gmail_quote. The div\n    // version marks text but does not cause indentation, but both should be\n    // considered quoted text.\n    return Array.from(doc.querySelectorAll('.gmail_quote'));\n  }\n\n  _findOffice365Quotes(doc) {\n    let elements = doc.querySelectorAll('#divRplyFwdMsg, #OLK_SRC_BODY_SECTION');\n    elements = Array.from(elements);\n\n    const weirdEl = doc.getElementById('3D\"divRplyFwdMsg\"');\n    if (weirdEl) { elements.push(weirdEl); }\n\n    elements = elements.map((el) => {\n      /**\n       * When Office 365 wraps quotes in a '#divRplyFwdMsg' id, it usually\n       * preceedes it with an <hr> tag and then wraps the entire section\n       * in an anonymous div one level up.\n       */\n      if (el.previousElementSibling && el.previousElementSibling.nodeName === \"HR\") {\n        if (el.parentElement && el.parentElement.nodeName !== \"BODY\") {\n          return el.parentElement;\n        }\n        const quoteNodes = [el.previousElementSibling, el]\n        let node = el.nextSibling;\n        while (node) {\n          quoteNodes.push(node);\n          node = node.nextSibling;\n        }\n        return quoteNodes\n      }\n      return el\n    });\n    return _.flatten(elements);\n  }\n\n  _findBlockquoteQuotes(doc) {\n    return Array.from(doc.querySelectorAll('blockquote'));\n  }\n}\n\nexport default new QuotedHTMLTransformer();\n"
  },
  {
    "path": "packages/client-app/src/services/quoted-plain-text-transformer.coffee",
    "content": "_ = require 'underscore'\n_str = require 'underscore.string'\n\n# Parses plain text emails to find quoted text and signatures.\n#\n# For plain text emails we look for lines that look like they're quoted\n# text based on common conventions:\n#\n# For HTML emails use QuotedHTMLTransformer\n#\n# This is modied from https://github.com/mko/emailreplyparser, which is a\n# JS port of GitHub's Ruby https://github.com/github/email_reply_parser\nQuotedPlainTextParser =\n  parse: (text) ->\n    parsedEmail = new ParsedEmail\n    parsedEmail.parse text\n\n  visibleText: (text, {showQuoted, showSignature}={}) ->\n    showQuoted ?= false\n    showSignature ?= false\n    @parse(text).visibleText({showQuoted, showSignature})\n\n  hiddenText: (text, {showQuoted, showSignature}={}) ->\n    showQuoted ?= false\n    showSignature ?= false\n    @parse(text).hiddenText({showQuoted, showSignature})\n\n  hasQuotedHTML: (text) ->\n    return @parse(text).hasQuotedHTML()\n\nchomp = ->\n  @replace /(\\n|\\r)+$/, ''\n\n# An ParsedEmail instance contains various `Fragment`s that indicate if we\n# think a section of text is quoted or is a signature\nclass ParsedEmail\n  constructor: ->\n    @fragments = []\n    @currentFragment = null\n    return\n\n  fragments: []\n\n  hasQuotedHTML: ->\n    for fragment in @fragments\n      return true if fragment.quoted\n    return false\n\n  visibleText: ({showSignature, showQuoted}={}) ->\n    @_setHiddenState({showSignature, showQuoted})\n    return _.reject(@fragments, (f) -> f.hidden).map((f) -> f.to_s()).join('\\n')\n\n  hiddenText: ({showSignature, showQuoted}={}) ->\n    @_setHiddenState({showSignature, showQuoted})\n    return _.filter(@fragments, (f) -> f.hidden).map((f) -> f.to_s()).join('\\n')\n\n  # We set a hidden state just so we can test the expected output in our\n  # specs. The hidden state is determined by the requested view parameters\n  # and the `quoted` flag on each `fragment`\n  _setHiddenState: ({showSignature, showQuoted}={}) ->\n    fragments = _.reject @fragments, (f) ->\n      if f.to_s().trim() is \"\"\n        f.hidden = true\n        return true\n      else return false\n\n    for fragment, i in fragments\n      fragment.hidden = true\n      if fragment.quoted\n        if showQuoted or (fragments[i+1]? and not (fragments[i+1].quoted or fragments[i+1].signature))\n          fragment.hidden = false\n          continue\n        else continue\n\n      if fragment.signature\n        if showSignature\n          fragment.hidden = false\n          continue\n        else continue\n\n      fragment.hidden = false\n\n  parse: (text) ->\n\n    # This instance variable points to the current Fragment.  If the matched\n    # line fits, it should be added to this Fragment.  Otherwise, finish it\n    # and start a new Fragment.\n    @currentFragment = null\n    @_parsePlain(text)\n\n  _parsePlain: (text) ->\n    # Check for multi-line reply headers. Some clients break up\n    # the \"On DATE, NAME <EMAIL> wrote:\" line into multiple lines.\n    patt = /^(On\\s(\\n|.)*wrote:)$/m\n    doubleOnPatt = /^(On\\s(\\n|.)*(^(> )?On\\s)((\\n|.)*)wrote:)$/m\n    if patt.test(text) and !doubleOnPatt.test(text)\n      replyHeader = patt.exec(text)[0]\n      # Remove all new lines from the reply header.\n      text = text.replace(replyHeader, replyHeader.replace(/\\n/g, ' '))\n\n    # The text is reversed initially due to the way we check for hidden\n    # fragments.\n    text = _str.reverse(text)\n\n    # Use the StringScanner to pull out each line of the email content.\n    lines = text.split('\\n')\n\n    for i of lines\n      @_scanPlainLine lines[i]\n\n    # Finish up the final fragment.  Finishing a fragment will detect any\n    # attributes (hidden, signature, reply), and join each line into a\n    # string.\n    @_finishFragment()\n\n    # Now that parsing is done, reverse the order.\n    @fragments.reverse()\n\n    return @\n\n  _signatureRE:\n    /(--|__|^-\\w)|(^sent from my (\\s*\\w+){1,3}$)/i\n\n  # NOTE: Plain lines are scanned bottom to top. We reverse the text in\n  # `_parsePlain`\n  _scanPlainLine: (line) ->\n    line = chomp.apply(line)\n\n    if !new RegExp(@_signatureRE).test(_str.reverse(line))\n      line = _str.ltrim(line)\n\n    # Mark the current Fragment as a signature if the current line is ''\n    # and the Fragment starts with a common signature indicator.\n    if @currentFragment != null and line == ''\n      if new RegExp(@_signatureRE).test(_str.reverse(@currentFragment.lines[@currentFragment.lines.length - 1]))\n        @currentFragment.signature = true\n        @_finishFragment()\n\n    # We're looking for leading `>`'s to see if this line is part of a\n    # quoted Fragment.\n    isQuoted = new RegExp('(>+)$').test(line)\n\n    # If the line matches the current fragment, add it.  Note that a common\n    # reply header also counts as part of the quoted Fragment, even though\n    # it doesn't start with `>`.\n    if @currentFragment != null and (@currentFragment.quoted == isQuoted or @currentFragment.quoted and (@_quoteHeader(line) or line == ''))\n      @currentFragment.lines.push line\n    else\n      @_finishFragment()\n      @currentFragment = new Fragment(isQuoted, line, \"plain\")\n    return\n\n  _quoteHeader: (line) ->\n    new RegExp('^:etorw.*nO$').test line\n\n  _finishFragment: ->\n    if @currentFragment != null\n      @currentFragment.finish()\n      @fragments.push @currentFragment\n      @currentFragment = null\n    return\n\n# Represents a group of paragraphs in the email sharing common attributes.\n# Paragraphs should get their own fragment if they are a quoted area or a\n# signature.\nclass Fragment\n  constructor: (@quoted, firstLine) ->\n    @signature = false\n    @hidden = false\n    @lines = [ firstLine ]\n    @content = null\n    @lines = @lines.filter(->\n      true\n    )\n    return\n\n  content: null\n\n  finish: ->\n    @content = @lines.join(\"\\n\")\n    @lines = []\n\n    @content = _str.reverse(@content)\n\n    return\n\n  to_s: ->\n    @content.toString().trim()\n\nmodule.exports = QuotedPlainTextParser\n"
  },
  {
    "path": "packages/client-app/src/services/sanitize-transformer.es6",
    "content": "import sanitizeHtml from 'sanitize-html';\n\nconst Preset = {\n  Strict: {\n    allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike', 'table'],\n    allowedAttributes: {\n      a: ['href', 'name'],\n      img: ['src', 'alt'],\n    },\n    transformTags: {\n      h1: \"p\",\n      h2: \"p\",\n      h3: \"p\",\n      h4: \"p\",\n      h5: \"p\",\n      h6: \"p\",\n      div: \"p\",\n      pre: \"p\",\n      blockquote: \"p\",\n    },\n  },\n\n  Permissive: {\n    allowedTags: ['p', 'b', 'i', 'em', 'strong', 'a', 'br', 'img', 'ul', 'ol', 'li', 'strike', 'table', 'tr', 'td', 'th', 'col', 'colgroup', 'div'],\n    allowedSchemesByTag: {\n      img: [\"data\"],\n    },\n    allowedAttributes: ['abbr', 'accept', 'acceptcharset', 'accesskey', 'action', 'align', 'alt', 'async', 'autocomplete', 'axis', 'border', 'bgcolor', 'cellpadding', 'cellspacing', 'char', 'charoff', 'charset', 'checked', 'class', 'classid', 'classname', 'colspan', 'cols', 'content', 'contextmenu', 'controls', 'coords', 'data-overlay-id', 'data-component-props', 'data-component-key', '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'],\n  },\n\n  UnsafeOnly: {\n    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\"],\n    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'],\n    allowedSchemes: ['http', 'https', 'ftp', 'mailto', 'data'],\n    allowedClasses: {\n      pre: ['nylas-plaintext'],\n    },\n  },\n};\n\n\nclass SanitizeTransformer {\n  Preset = Preset;\n\n  run(body, settings = Preset.Strict) {\n    if (settings.allowedAttributes instanceof Array) {\n      const attrMap = {};\n      for (const tag of settings.allowedTags) {\n        attrMap[tag] = settings.allowedAttributes;\n      }\n      settings.allowedAttributes = attrMap;\n    }\n\n    return Promise.resolve(sanitizeHtml(body, settings));\n  }\n}\n\nexport default new SanitizeTransformer();\n"
  },
  {
    "path": "packages/client-app/src/services/search/search-query-ast.es6",
    "content": "\nclass SearchQueryExpressionVisitor {\n  constructor() {\n    this._result = null;\n  }\n\n  visitAndGetResult(node) {\n    node.accept(this);\n    const result = this._result;\n    this._result = null;\n    return result;\n  }\n\n  visitAnd(node) { throw new Error('Abstract function not implemented!', node); }\n  visitOr(node) { throw new Error('Abstract function not implemented!', node); }\n  visitFrom(node) { throw new Error('Abstract function not implemented!', node); }\n  visitTo(node) { throw new Error('Abstract function not implemented!', node); }\n  visitSubject(node) { throw new Error('Abstract function not implemented!', node); }\n  visitGeneric(node) { throw new Error('Abstract function not implemented!', node); }\n  visitText(node) { throw new Error('Abstract function not implemented!', node); }\n  visitUnread(node) { throw new Error('Abstract function not implemented!', node); }\n  visitStarred(node) { throw new Error('Abstract function not implemented!', node); }\n  visitMatch(node) { throw new Error('Abstract function not implemented!', node); }\n  visitIn(node) { throw new Error('Abstract function not implemented!', node); }\n  visitHasAttachment(node) { throw new Error('Abstract function not implemented!', node); }\n}\n\nclass QueryExpression {\n  constructor() {\n    this._isMatchCompatible = null;\n  }\n\n  accept(visitor) {\n    throw new Error('Abstract function not implemented!', visitor);\n  }\n\n  isMatchCompatible() {\n    if (this._isMatchCompatible === null) {\n      this._isMatchCompatible = this._computeIsMatchCompatible();\n    }\n    return this._isMatchCompatible;\n  }\n\n  _computeIsMatchCompatible() {\n    throw new Error('Abstract function not implemented!');\n  }\n\n  equals(other) {\n    throw new Error('Abstract function not implemented!', other);\n  }\n}\n\nclass AndQueryExpression extends QueryExpression {\n  constructor(e1, e2) {\n    super();\n    this.e1 = e1;\n    this.e2 = e2;\n  }\n\n  accept(visitor) {\n    visitor.visitAnd(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return this.e1.isMatchCompatible() && this.e2.isMatchCompatible();\n  }\n\n  equals(other) {\n    if (!(other instanceof AndQueryExpression)) {\n      return false;\n    }\n    return this.e1.equals(other.e1) && this.e2.equals(other.e2);\n  }\n}\n\nclass OrQueryExpression extends QueryExpression {\n  constructor(e1, e2) {\n    super();\n    this.e1 = e1;\n    this.e2 = e2;\n  }\n\n  accept(visitor) {\n    visitor.visitOr(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return this.e1.isMatchCompatible() && this.e2.isMatchCompatible();\n  }\n\n  equals(other) {\n    if (!(other instanceof OrQueryExpression)) {\n      return false;\n    }\n    return this.e1.equals(other.e1) && this.e2.equals(other.e2);\n  }\n}\n\nclass FromQueryExpression extends QueryExpression {\n  constructor(text) {\n    super();\n    this.text = text;\n  }\n\n  accept(visitor) {\n    visitor.visitFrom(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return true;\n  }\n\n  equals(other) {\n    if (!(other instanceof FromQueryExpression)) {\n      return false;\n    }\n    return this.text.equals(other.text);\n  }\n}\n\nclass ToQueryExpression extends QueryExpression {\n  constructor(text) {\n    super();\n    this.text = text;\n  }\n\n  accept(visitor) {\n    visitor.visitTo(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return true;\n  }\n\n  equals(other) {\n    if (!(other instanceof ToQueryExpression)) {\n      return false;\n    }\n    return this.text.equals(other.text);\n  }\n}\n\nclass SubjectQueryExpression extends QueryExpression {\n  constructor(text) {\n    super();\n    this.text = text;\n  }\n\n  accept(visitor) {\n    visitor.visitSubject(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return true;\n  }\n\n  equals(other) {\n    if (!(other instanceof SubjectQueryExpression)) {\n      return false;\n    }\n    return this.text.equals(other.text);\n  }\n}\n\nclass UnreadStatusQueryExpression extends QueryExpression {\n  constructor(status) {\n    super();\n    this.status = status;\n  }\n\n\n  accept(visitor) {\n    visitor.visitUnread(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return false;\n  }\n\n  equals(other) {\n    if (!(other instanceof UnreadStatusQueryExpression)) {\n      return false;\n    }\n    return this.status === other.status;\n  }\n}\n\nclass StarredStatusQueryExpression extends QueryExpression {\n  constructor(status) {\n    super();\n    this.status = status;\n  }\n\n  accept(visitor) {\n    visitor.visitStarred(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return false;\n  }\n\n  equals(other) {\n    if (!(other instanceof StarredStatusQueryExpression)) {\n      return false;\n    }\n    return this.status === other.status;\n  }\n}\n\nclass GenericQueryExpression extends QueryExpression {\n  constructor(text) {\n    super();\n    this.text = text;\n  }\n\n  accept(visitor) {\n    visitor.visitGeneric(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return true;\n  }\n\n  equals(other) {\n    if (!(other instanceof GenericQueryExpression)) {\n      return false;\n    }\n    return this.text.equals(other.text);\n  }\n}\n\nclass TextQueryExpression extends QueryExpression {\n  constructor(text) {\n    super();\n    this.token = text;\n  }\n\n  accept(visitor) {\n    visitor.visitText(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return true;\n  }\n\n  equals(other) {\n    if (!(other instanceof TextQueryExpression)) {\n      return false;\n    }\n    return this.token.equals(other.token);\n  }\n}\n\nclass InQueryExpression extends QueryExpression {\n  constructor(text) {\n    super();\n    this.text = text;\n  }\n\n  accept(visitor) {\n    visitor.visitIn(this);\n  }\n\n  _computeIsMatchCompatible() {\n    return true;\n  }\n\n  equals(other) {\n    if (!(other instanceof InQueryExpression)) {\n      return false;\n    }\n    return this.text.equals(other.text);\n  }\n}\n\nclass HasAttachmentQueryExpression extends QueryExpression {\n  accept(visitor) {\n    visitor.visitHasAttachment(this);\n  }\n\n  equals(other) {\n    return (other instanceof HasAttachmentQueryExpression);\n  }\n}\n\n/*\n * Intermediate representation for multiple match-compatible nodes. Used when\n * translating the initial query AST into the proper SQL-compatible query.\n */\nclass MatchQueryExpression extends QueryExpression {\n  constructor(rawMatchQuery) {\n    super();\n    this.rawQuery = rawMatchQuery;\n  }\n\n  accept(visitor) {\n    visitor.visitMatch(this);\n  }\n\n  _computeIsMatchCompatible() {\n    /*\n     * We should never call this for match nodes b/c we generate match nodes\n     * after checking if other nodes are match-compatible.\n     */\n    throw new Error('Invalid node');\n  }\n\n  equals(other) {\n    if (!(other instanceof MatchQueryExpression)) {\n      return false;\n    }\n    return this.rawQuery === other.rawQuery;\n  }\n}\n\nclass SearchQueryToken {\n  constructor(s) {\n    this.s = s;\n  }\n\n  equals(other) {\n    if (!(other instanceof SearchQueryToken)) {\n      return false;\n    }\n    return this.s === other.s;\n  }\n}\n\nmodule.exports = {\n  SearchQueryExpressionVisitor,\n  SearchQueryToken,\n  OrQueryExpression,\n  AndQueryExpression,\n  FromQueryExpression,\n  ToQueryExpression,\n  SubjectQueryExpression,\n  GenericQueryExpression,\n  TextQueryExpression,\n  UnreadStatusQueryExpression,\n  StarredStatusQueryExpression,\n  MatchQueryExpression,\n  InQueryExpression,\n  HasAttachmentQueryExpression,\n};\n"
  },
  {
    "path": "packages/client-app/src/services/search/search-query-backend-imap.es6",
    "content": "import _ from 'underscore'\nimport {\n  AndQueryExpression,\n  SearchQueryExpressionVisitor,\n} from './search-query-ast';\n\nconst TOP = 'top';\n\nclass IMAPSearchQueryFolderFinderVisitor extends SearchQueryExpressionVisitor {\n  visit(root) {\n    const result = this.visitAndGetResult(root);\n    if (result === TOP) {\n      return 'all';\n    }\n    return result;\n  }\n\n  visitAnd(node) {\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    if (lhs === TOP) {\n      this._result = rhs;\n      return;\n    }\n    if (rhs === TOP) {\n      this._result = lhs;\n      return;\n    }\n    this._result = _.intersection(lhs, rhs);\n  }\n\n  visitOr(node) {\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    if (lhs === TOP || rhs === TOP) {\n      this._result = TOP;\n      return;\n    }\n    this._result = _.union(lhs, rhs);\n  }\n\n  visitIn(node) {\n    const folderName = this.visitAndGetResult(node.text);\n    this._result = [folderName];\n  }\n\n  visitFrom(/* node */) {\n    this._result = TOP;\n  }\n\n  visitTo(/* node */) {\n    this._result = TOP;\n  }\n\n  visitSubject(/* node */) {\n    this._result = TOP;\n  }\n\n  visitGeneric(/* node */) {\n    this._result = TOP;\n  }\n\n  visitText(node) {\n    this._result = node.token.s;\n  }\n\n  visitUnread(/* node */) {\n    this._result = TOP;\n  }\n\n  visitStarred(/* node */) {\n    this._result = TOP;\n  }\n\n  visitHasAttachment(/* node */) {\n    this._result = TOP;\n  }\n}\n\nclass IMAPSearchQueryExpressionVisitor extends SearchQueryExpressionVisitor {\n  constructor(folder) {\n    super();\n    this._folder = folder;\n  }\n\n  visit(root) {\n    const result = this.visitAndGetResult(root);\n    if (root instanceof AndQueryExpression) {\n      return result;\n    }\n    return [result];\n  }\n\n  visitAnd(node) {\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = [];\n    if (node.e1 instanceof AndQueryExpression) {\n      this._result = this._result.concat(lhs);\n    } else {\n      this._result.push(lhs);\n    }\n\n    if (node.e2 instanceof AndQueryExpression) {\n      this._result = this._result.concat(rhs);\n    } else {\n      this._result.push(rhs);\n    }\n  }\n\n  visitOr(node) {\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = ['OR', lhs, rhs];\n  }\n\n  visitFrom(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = ['FROM', text];\n  }\n\n  visitTo(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = ['TO', text];\n  }\n\n  visitSubject(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = ['SUBJECT', text];\n  }\n\n  visitGeneric(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = ['TEXT', text];\n  }\n\n  visitText(node) {\n    this._result = node.token.s;\n  }\n\n  visitUnread(node) {\n    this._result = node.status ? 'UNSEEN' : 'SEEN';\n  }\n\n  visitStarred(node) {\n    this._result = node.status ? 'FLAGGED' : 'UNFLAGGED';\n  }\n\n  visitIn(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = text === this._folder.name ? 'ALL' : '!ALL';\n  }\n\n  visitHasAttachment(/* node */) {\n    this._result = ['OR',\n      ['HEADER', 'Content-Type', 'multipart/mixed'],\n      ['HEADER', 'Content-Type', 'multipart/related'],\n    ];\n  }\n}\n\n\nexport default class IMAPSearchQueryBackend {\n  static ALL_FOLDERS() {\n    return 'all';\n  }\n\n  static compile(ast, folder) {\n    return (new IMAPSearchQueryBackend()).compile(ast, folder);\n  }\n\n  static folderNamesForQuery(ast) {\n    return (new IMAPSearchQueryFolderFinderVisitor()).visit(ast);\n  }\n\n  compile(ast, folder) {\n    return (new IMAPSearchQueryExpressionVisitor(folder)).visit(ast);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/services/search/search-query-backend-local.es6",
    "content": "import {\n  SearchQueryExpressionVisitor,\n  OrQueryExpression,\n  AndQueryExpression,\n  UnreadStatusQueryExpression,\n  StarredStatusQueryExpression,\n  HasAttachmentQueryExpression,\n  MatchQueryExpression,\n} from './search-query-ast'\n\n/*\n * This class visits a match-compatible subtree and condenses it into a single\n * MatchQueryExpression.\n */\nclass MatchQueryExpressionVisitor extends SearchQueryExpressionVisitor {\n  visit(root) {\n    const result = this.visitAndGetResult(root);\n    return new MatchQueryExpression(`${result}`);\n  }\n\n  _assertIsMatchCompatible(node) {\n    if (!node.isMatchCompatible()) {\n      throw new Error(`Expected ${node} to be match compatible`);\n    }\n  }\n\n  visitAnd(node) {\n    this._assertIsMatchCompatible(node);\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = `(${lhs} AND ${rhs})`;\n  }\n\n  visitOr(node) {\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = `(${lhs} OR ${rhs})`;\n  }\n\n  visitFrom(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = `(from_ : \"${text}\"*)`;\n  }\n\n  visitTo(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = `(to_ : \"${text}\"*)`;\n  }\n\n  visitSubject(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = `(subject : \"${text}\"*)`;\n  }\n\n  visitGeneric(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = `(\"${text}\"*)`\n  }\n\n  visitText(node) {\n    // TODO: Should we do anything about possible SQL injection attacks?\n    this._result = node.token.s;\n  }\n\n  visitUnread(node) {\n    this._assertIsMatchCompatible(node);\n  }\n\n  visitStarred(node) {\n    this._assertIsMatchCompatible(node);\n  }\n\n  visitIn(node) {\n    const text = this.visitAndGetResult(node.text);\n    this._result = `(categories : \"${text}*\")`;\n  }\n\n  visitHasAttachment(node) {\n    this._assertIsMatchCompatible(node);\n  }\n}\n\n/*\n * This class creates a new AST by converting match-compatible subtrees into\n * MatchQueryExpressions.\n */\nclass MatchCompatibleQueryCondenser extends SearchQueryExpressionVisitor {\n  constructor() {\n    super();\n    this._matchVisitor = new MatchQueryExpressionVisitor();\n  }\n\n  visit(root) {\n    return this.visitAndGetResult(root);\n  }\n\n  visitAnd(node) {\n    if (node.isMatchCompatible()) {\n      this._result = this._matchVisitor.visit(node);\n      return;\n    }\n\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = new AndQueryExpression(lhs, rhs);\n  }\n\n  visitOr(node) {\n    if (node.isMatchCompatible()) {\n      this._result = this._matchVisitor.visit(node);\n      return;\n    }\n\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = new OrQueryExpression(lhs, rhs);\n  }\n\n  visitFrom(node) {\n    this._result = this._matchVisitor.visit(node);\n  }\n\n  visitTo(node) {\n    this._result = this._matchVisitor.visit(node);\n  }\n\n  visitSubject(node) {\n    this._result = this._matchVisitor.visit(node);\n  }\n\n  visitGeneric(node) {\n    this._result = this._matchVisitor.visit(node);\n  }\n\n  visitText(node) {\n    this._result = this._matchVisitor.visit(node);\n  }\n\n  visitIn(node) {\n    this._result = this._matchVisitor.visit(node);\n  }\n\n  visitUnread(node) {\n    this._result = new UnreadStatusQueryExpression(node.status);\n  }\n\n  visitStarred(node) {\n    this._result = new StarredStatusQueryExpression(node.status);\n  }\n\n  visitHasAttachment(/* node */) {\n    this._result = new HasAttachmentQueryExpression();\n  }\n}\n\n/*\n * Converts a search query into the appropriate where clause. It does this by\n * converting match-compatible subtrees into the appropriate subquery that\n * uses a MATCH clause.\n */\nclass StructuredSearchQueryVisitor extends SearchQueryExpressionVisitor {\n  constructor(className) {\n    super();\n    this._className = className;\n  }\n\n  visit(root) {\n    return this.visitAndGetResult(root);\n  }\n\n  visitAnd(node) {\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = `(${lhs} AND ${rhs})`;\n  }\n\n  visitOr(node) {\n    const lhs = this.visitAndGetResult(node.e1);\n    const rhs = this.visitAndGetResult(node.e2);\n    this._result = `(${lhs} OR ${rhs})`;\n  }\n\n  visitFrom(node) {\n    throw new Error('Unreachable', node);\n  }\n\n  visitTo(node) {\n    throw new Error('Unreachable', node);\n  }\n\n  visitSubject(node) {\n    throw new Error('Unreachable', node);\n  }\n\n  visitGeneric(node) {\n    throw new Error('Unreachable', node);\n  }\n\n  visitText(node) {\n    throw new Error('Unreachable', node);\n  }\n\n  visitIn(node) {\n    throw new Error('Unreachable', node);\n  }\n\n  visitUnread(node) {\n    const unread = node.status ? 1 : 0;\n    this._result = `(\\`${this._className}\\`.\\`unread\\` = ${unread})`;\n  }\n\n  visitStarred(node) {\n    const starred = node.status ? 1 : 0;\n    this._result = `(\\`${this._className}\\`.\\`starred\\` = ${starred})`;\n  }\n\n  visitHasAttachment(/* node */) {\n    this._result = `(\\`${this._className}\\`.\\`data\\` LIKE '%\"has_attachments\":true%')`;\n  }\n\n  visitMatch(node) {\n    const searchTable = `${this._className}Search`;\n    this._result = `(\\`${this._className}\\`.\\`id\\` IN (SELECT \\`content_id\\` FROM \\`${searchTable}\\` WHERE \\`${searchTable}\\` MATCH '${node.rawQuery}' LIMIT 1000))`;\n  }\n}\n\nexport default class LocalSearchQueryBackend {\n  constructor(modelClassName) {\n    this._modelClassName = modelClassName;\n  }\n\n  compile(ast) {\n    const condenser = new MatchCompatibleQueryCondenser();\n    const intermediateAST = condenser.visit(ast);\n\n    const codegen = new StructuredSearchQueryVisitor(`${this._modelClassName}`);\n    return codegen.visit(intermediateAST);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/services/search/search-query-parser.es6",
    "content": "import {\n  SearchQueryToken,\n  OrQueryExpression,\n  AndQueryExpression,\n  FromQueryExpression,\n  ToQueryExpression,\n  SubjectQueryExpression,\n  GenericQueryExpression,\n  TextQueryExpression,\n  UnreadStatusQueryExpression,\n  StarredStatusQueryExpression,\n  InQueryExpression,\n  HasAttachmentQueryExpression,\n} from './search-query-ast';\n\nconst nextStringToken = (text) => {\n  if (text[0] !== '\"') {\n    throw new Error('Expected string token to begin with double quote (\")');\n  }\n  if (text.length < 2) {\n    throw new Error('Expected string but ran out of input');\n  }\n  let pos = 1;\n  while (pos < text.length) {\n    const c = text[pos];\n    if (c === '\"') {\n      return [new SearchQueryToken(text.substring(1, pos)), text.substring(pos + 1)];\n    }\n    pos += 1;\n  }\n  throw new Error('Expected string but ran out of input');\n};\n\nconst isWhitespace = (c) => {\n  switch (c) {\n    case ' ':\n    case '\\t':\n    case '\\n': return true;\n    default: return false;\n  }\n};\n\nconst consumeWhitespace = (text) => {\n  let pos = 0;\n  while (pos < text.length && isWhitespace(text[pos])) {\n    pos += 1;\n  }\n  return text.substring(pos);\n};\n\nconst reserved = [\n  '(',\n  ')',\n  ':',\n  'is',\n  'read',\n  'unread',\n  'starred',\n  'and',\n  'or',\n  'from',\n  'to',\n  'subject',\n  'in',\n  'has',\n  'attachment',\n];\n\nconst mightBeReserved = (text) => {\n  for (const r of reserved) {\n    if (r.startsWith(text) || r.toUpperCase().startsWith(text)) {\n      return true;\n    }\n  }\n  return false;\n};\n\nconst isValidNonStringChar = (c) => {\n  switch (c) {\n    case '(':\n    case ')':\n    case ':': return false;\n    default: return !isWhitespace(c);\n  }\n};\n\nconst isValidNonStringText = (text) => {\n  if (text.length < 1) {\n    return false;\n  }\n\n  for (const c of text) {\n    if (!isValidNonStringChar(c)) {\n      return false;\n    }\n  }\n  return true;\n};\n\nconst nextToken = (text) => {\n  const newText = consumeWhitespace(text);\n  if (newText.length === 0) {\n    return [null, newText];\n  }\n\n  if (newText[0] === '\"') {\n    return nextStringToken(newText);\n  }\n\n  let isReserved = true;\n  let pos = 0;\n  while (pos < newText.length) {\n    if (isWhitespace(newText[pos])) {\n      return [new SearchQueryToken(newText.substring(0, pos)), newText.substring(pos)];\n    }\n\n    const curr = newText.substring(0, pos + 1);\n    if (isReserved) {\n      // We no longer have a reserved keyword.\n      if (!mightBeReserved(curr)) {\n        // We became an invalid non-reserved token so return the previous pos.\n        if (!isValidNonStringText(curr)) {\n          return [new SearchQueryToken(newText.substring(0, pos)), newText.substring(pos)];\n        }\n        // We're still a valid token but we're no longer reserved.\n        isReserved = false;\n      }\n    } else {\n      // We're not reserved and we become invalid so go back.\n      if (!isReserved && !isValidNonStringText(curr)) {\n        return [new SearchQueryToken(newText.substring(0, pos)), newText.substring(pos)];\n      }\n    }\n    pos += 1;\n  }\n  return [new SearchQueryToken(newText.substring(0, pos + 1)), newText.substring(pos + 1)];\n};\n\n/*\n * query: and_query+\n *\n * and_query: or_query [and_query_rest]\n * and_query_rest: AND and_query\n *\n * or_query: simple_query [or_query_rest]\n * or_query_rest: OR or_query\n *\n * simple_query: TEXT\n *             | from_query\n *             | to_query\n *             | subject_query\n *             | paren_query\n *             | is_query\n *             | has_query\n *\n * from_query: FROM COLON TEXT\n * to_query: TO COLON TEXT\n * subject_query: SUBJECT COLON TEXT\n * paren_query: LPAREN query RPAREN\n * is_query: IS COLON is_query_rest\n * is_query_rest: read_cond\n *              | starred_cond\n * has_query: HAS COLON ATTACHMENT\n * read_cond: READ | UNREAD\n * starred_cond: STARRED | UNSTARRED\n * in_query: IN COLON TEXT\n *\n * TEXT: STRING\n *     | [^\\s]+\n * STRING: DQUOTE [^\"]* DQUOTE\n */\nconst consumeExpectedToken = (text, token) => {\n  const [tok, afterTok] = nextToken(text);\n  if (tok.s !== token) {\n    throw new Error(`Expected '${token}', got '${tok.s}'`);\n  }\n  return afterTok;\n};\n\nconst parseText = (text) => {\n  const [tok, afterTok] = nextToken(text);\n  if (tok === null) {\n    throw new Error('Expected text but none available');\n  }\n  return [new TextQueryExpression(tok), afterTok];\n};\n\nconst parseIsQuery = (text) => {\n  const afterColon = consumeExpectedToken(text, ':');\n  const [tok, afterTok] = nextToken(afterColon);\n  if (tok === null) {\n    return null;\n  }\n  const tokText = tok.s.toUpperCase();\n  switch (tokText) {\n    case 'READ':\n    case 'UNREAD': {\n      return [new UnreadStatusQueryExpression(tokText === 'UNREAD'), afterTok];\n    }\n    case 'STARRED':\n    case 'UNSTARRED': {\n      return [new StarredStatusQueryExpression(tokText === 'STARRED'), afterTok];\n    }\n    default: break;\n  }\n  return null;\n};\n\nconst parseHasQuery = (text) => {\n  const afterColon = consumeExpectedToken(text, ':');\n  const [tok, afterTok] = nextToken(afterColon);\n  if (tok === null) {\n    return null;\n  }\n  const tokText = tok.s.toUpperCase();\n  switch (tokText) {\n    case 'ATTACHMENT': {\n      return [new HasAttachmentQueryExpression(), afterTok];\n    }\n    default: break;\n  }\n  return null;\n};\n\nlet parseQuery = null; // Satisfy our robot overlords.\nconst parseSimpleQuery = (text) => {\n  const [tok, afterTok] = nextToken(text);\n  if (tok === null) {\n    return [null, afterTok];\n  }\n\n  if (tok.s === ')') {\n    return [null, text];\n  }\n\n  if (tok.s === '(') {\n    const [exp, afterExp] = parseQuery(afterTok);\n    const afterRparen = consumeExpectedToken(afterExp, ')');\n    return [exp, afterRparen];\n  }\n\n  if (tok.s.toUpperCase() === 'TO') {\n    const afterColon = consumeExpectedToken(afterTok, ':');\n    const [txt, afterTxt] = parseText(afterColon);\n    return [new ToQueryExpression(txt), afterTxt];\n  }\n\n  if (tok.s.toUpperCase() === 'FROM') {\n    const afterColon = consumeExpectedToken(afterTok, ':');\n    const [txt, afterTxt] = parseText(afterColon);\n    return [new FromQueryExpression(txt), afterTxt];\n  }\n\n  if (tok.s.toUpperCase() === 'SUBJECT') {\n    const afterColon = consumeExpectedToken(afterTok, ':');\n    const [txt, afterTxt] = parseText(afterColon);\n    return [new SubjectQueryExpression(txt), afterTxt];\n  }\n\n  if (tok.s.toUpperCase() === 'IS') {\n    const result = parseIsQuery(afterTok);\n    if (result !== null) {\n      return result;\n    }\n  }\n\n  if (tok.s.toUpperCase() === 'HAS') {\n    const result = parseHasQuery(afterTok);\n    if (result !== null) {\n      return result;\n    }\n  }\n\n  if (tok.s.toUpperCase() === 'IN') {\n    const afterColon = consumeExpectedToken(afterTok, ':');\n    const [txt, afterTxt] = parseText(afterColon);\n    return [new InQueryExpression(txt), afterTxt];\n  }\n\n  const [txt, afterTxt] = parseText(text);\n  return [new GenericQueryExpression(txt), afterTxt];\n};\n\nconst parseOrQuery = (text) => {\n  const [lhs, afterLhs] = parseSimpleQuery(text);\n  const [tok, afterOr] = nextToken(afterLhs);\n  if (tok === null) {\n    return [lhs, afterLhs];\n  }\n  if (tok.s.toUpperCase() !== 'OR') {\n    return [lhs, afterLhs];\n  }\n  const [rhs, afterRhs] = parseOrQuery(afterOr);\n  return [new OrQueryExpression(lhs, rhs), afterRhs];\n};\n\nconst parseAndQuery = (text) => {\n  const [lhs, afterLhs] = parseOrQuery(text);\n  const [tok, afterAnd] = nextToken(afterLhs);\n  if (tok === null) {\n    return [lhs, afterLhs];\n  }\n  if (tok.s.toUpperCase() !== 'AND') {\n    return [lhs, afterLhs];\n  }\n  const [rhs, afterRhs] = parseAndQuery(afterAnd);\n  return [new AndQueryExpression(lhs, rhs), afterRhs];\n};\n\nparseQuery = (text) => {\n  let currText = text;\n  const exps = [];\n  while (currText.length > 0) {\n    const [result, leftover] = parseAndQuery(currText);\n    if (result === null) {\n      break;\n    }\n    exps.push(result);\n    currText = leftover;\n  }\n\n  if (exps.length === 0) {\n    throw new Error('Unable to parse query');\n  }\n\n  let result = null;\n  for (let i = exps.length - 1; i >= 0; --i) {\n    if (result === null) {\n      result = exps[i];\n    } else {\n      result = new AndQueryExpression(exps[i], result);\n    }\n  }\n  return [result, currText];\n}\n\nconst parseQueryWrapper = (text) => {\n  const [result, leftover] = parseQuery(text);\n  const leftoverNoWhitespace = consumeWhitespace(leftover);\n  if (leftoverNoWhitespace.length > 0) {\n    throw new Error('Unable to parse query: expected end of stream');\n  }\n  return result;\n};\n\nexport default class SearchQueryParser {\n  static parse(query) {\n    return parseQueryWrapper(query);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/services/search-index-scheduler.es6",
    "content": "import _ from 'underscore';\nimport {\n  DatabaseStore,\n} from 'nylas-exports'\n\nconst CHUNK_SIZE = 10;\nconst FRACTION_CPU_AVAILABLE = 0.05;\nconst MIN_TIMEOUT = 1000;\nconst MAX_TIMEOUT = 5 * 60 * 1000; // 5 minutes\n\nclass SearchIndexScheduler {\n  constructor() {\n    this._searchableModels = {};\n    this._hasIndexingToDo = false;\n    this._lastTimeStart = null;\n    this._lastTimeStop = null;\n  }\n\n  registerSearchableModel({modelClass, indexSize, indexCallback, unindexCallback}) {\n    this._searchableModels[modelClass.name] = {modelClass, indexSize, indexCallback, unindexCallback};\n  }\n\n  unregisterSearchableModel(modelClass) {\n    delete this._searchableModels[modelClass.name];\n  }\n\n  async _getIndexCutoff(modelClass, indexSize) {\n    const query = DatabaseStore.findAll(modelClass)\n      .order(modelClass.naturalSortOrder())\n      .offset(indexSize)\n      .limit(1)\n      .silenceQueryPlanDebugOutput()\n    // console.info('SearchIndexScheduler: _getIndexCutoff query', query.sql());\n    const models = await query;\n    return models[0];\n  }\n\n  _getNewUnindexed(modelClass, indexSize, cutoff) {\n    const whereConds = [modelClass.attributes.isSearchIndexed.equal(false)];\n    if (cutoff) {\n      whereConds.push(modelClass.sortOrderAttribute().greaterThan(cutoff[modelClass.sortOrderAttribute().modelKey]));\n    }\n    const query = DatabaseStore.findAll(modelClass)\n      .where(whereConds)\n      .limit(CHUNK_SIZE)\n      .order(modelClass.naturalSortOrder())\n    // console.info('SearchIndexScheduler: _getNewUnindexed query', query.sql());\n    return query;\n  }\n\n  _getOldIndexed(modelClass, cutoff) {\n    // If there's no cutoff then that means we haven't reached the max index size yet.\n    if (!cutoff) {\n      return Promise.resolve([]);\n    }\n    const whereConds = [\n      modelClass.attributes.isSearchIndexed.equal(true),\n      modelClass.sortOrderAttribute().lessThanOrEqualTo(cutoff[modelClass.sortOrderAttribute().modelKey]),\n    ];\n    const query = DatabaseStore.findAll(modelClass)\n      .where(whereConds)\n      .limit(CHUNK_SIZE)\n      .order(modelClass.naturalSortOrder())\n    // console.info('SearchIndexScheduler: _getOldIndexed query', query.sql());\n    return query;\n  }\n\n  async _getIndexDiff() {\n    const results = await Promise.all(Object.keys(this._searchableModels).map(async (modelName) => {\n      const {modelClass, indexSize} = this._searchableModels[modelName];\n      const cutoff = await this._getIndexCutoff(modelClass, indexSize);\n      const [toIndex, toUnindex] = await Promise.all([\n        this._getNewUnindexed(modelClass, indexSize, cutoff),\n        this._getOldIndexed(modelClass, cutoff),\n      ]);\n      // console.info('SearchIndexScheduler: ', modelClass.name);\n      // console.info('SearchIndexScheduler: _getIndexCutoff cutoff', cutoff);\n      // console.info('SearchIndexScheduler: _getIndexDiff toIndex', toIndex.map((model) => [model.isSearchIndexed, model.subject]));\n      // console.info('SearchIndexScheduler: _getIndexDiff toUnindex', toUnindex.map((model) => [model.isSearchIndexed, model.subject]));\n      return [toIndex, toUnindex];\n    }));\n    const [toIndex, toUnindex] = _.unzip(results).map((l) => _.flatten(l))\n    return {toIndex, toUnindex};\n  }\n\n  _indexItems(items) {\n    return Promise.all([items.map((item) => this._searchableModels[item.constructor.name].indexCallback(item))]);\n  }\n\n  _unindexItems(items) {\n    return Promise.all([items.map((item) => this._searchableModels[item.constructor.name].unindexCallback(item))]);\n  }\n\n  notifyHasIndexingToDo() {\n    if (this._hasIndexingToDo) {\n      return;\n    }\n    this._hasIndexingToDo = true;\n    this._scheduleRun();\n  }\n\n  _computeNextTimeout() {\n    if (!this._lastTimeStop || !this._lastTimeStart) {\n      return MIN_TIMEOUT;\n    }\n    const spanMillis = this._lastTimeStop.getTime() - this._lastTimeStart.getTime();\n    const multiplier = 1.0 / FRACTION_CPU_AVAILABLE;\n    return Math.min(Math.max(spanMillis * multiplier, MIN_TIMEOUT), MAX_TIMEOUT);\n  }\n\n  _scheduleRun() {\n    // console.info(`SearchIndexScheduler: setting timeout for ${this._computeNextTimeout()} ms`);\n    setTimeout(() => this.run(), this._computeNextTimeout());\n  }\n\n  async run() {\n    if (!this._hasIndexingToDo) {\n      return;\n    }\n\n    const start = new Date();\n    const {toIndex, toUnindex} = await this._getIndexDiff();\n    if (toIndex.length !== 0 || toUnindex.length !== 0) {\n      await Promise.all([\n        this._indexItems(toIndex),\n        this._unindexItems(toUnindex),\n      ]);\n      this._lastTimeStart = start;\n      this._lastTimeStop = new Date();\n      // console.info(`SearchIndexScheduler: ${toIndex.length} items indexed, ${toUnindex.length} items unindexed, took ${this._lastTimeStop.getTime() - this._lastTimeStart.getTime()} ms`);\n      this._scheduleRun();\n    } else {\n      // const stop = new Date();\n      // console.info(`SearchIndexScheduler: No changes to index, took ${stop.getTime() - start.getTime()} ms`);\n      this._hasIndexingToDo = false;\n    }\n  }\n}\n\nexport default new SearchIndexScheduler()\n"
  },
  {
    "path": "packages/client-app/src/services/unwrapped-signature-detector.es6",
    "content": "import DOMWalkers from '../dom-walkers'\nimport Utils from '../flux/models/utils'\n\nfunction textAndNodesAfterNode(node) {\n  let text = \"\";\n  let curNode = node;\n  const nodes = []\n  while (curNode) {\n    let sibling = curNode.nextSibling;\n    while (sibling) {\n      text += sibling.textContent;\n      nodes.push(sibling);\n      sibling = sibling.nextSibling;\n    }\n    curNode = curNode.parentNode;\n  }\n  return {text, nodes}\n}\n\n/**\n * Sometimes the last signature of an email will not be placed in a quote\n * block. This will cause out quote detector to not strip anything since\n * it looks very similar to someone writing inline regular text after some\n * quoted text (which is allowed).\n *\n * See email_18, email_20, email_21, and email_23 test cases for this.\n */\nexport default function unwrappedSignatureDetector(doc, quoteElements) {\n  // Find the last quoteBlock\n  for (const node of DOMWalkers.walkBackwards(doc)) {\n    let textAndNodes;\n    let focusNode = node;\n    if (node && quoteElements.includes(node)) {\n      textAndNodes = textAndNodesAfterNode(node);\n    } else if (node.previousSibling && quoteElements.includes(node.previousSibling)) {\n      focusNode = node.previousSibling;\n      textAndNodes = textAndNodesAfterNode(node.previousSibling);\n    } else {\n      continue;\n    }\n\n    const {text, nodes} = textAndNodes;\n    const maybeSig = text.replace(/\\s/g, \"\");\n    if (maybeSig.length > 0) {\n      if ((focusNode.textContent || \"\").replace(/\\s/g, \"\").search(Utils.escapeRegExp(maybeSig)) >= 0) {\n        return nodes;\n      }\n    }\n    break;\n  }\n  return []\n}\n"
  },
  {
    "path": "packages/client-app/src/sheet-container.cjsx",
    "content": "React = require 'react'\nReactCSSTransitionGroup = require 'react-addons-css-transition-group'\nSheet = require './sheet'\nToolbar = require './sheet-toolbar'\nFlexbox = require('./components/flexbox').default\nRetinaImg = require('./components/retina-img').default\nInjectedComponentSet = require './components/injected-component-set'\n_ = require 'underscore'\n\n{Actions,\n ComponentRegistry,\n WorkspaceStore} = require \"nylas-exports\"\n\nclass SheetContainer extends React.Component\n  displayName = 'SheetContainer'\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @unsubscribe = WorkspaceStore.listen @_onStoreChange\n\n  componentWillUnmount: =>\n    @unsubscribe() if @unsubscribe\n\n  render: =>\n    totalSheets = @state.stack.length\n    topSheet = @state.stack[totalSheets - 1]\n\n    return <div></div> unless topSheet\n\n    sheetElements = @_sheetElements()\n\n    <Flexbox direction=\"column\" className=\"layout-mode-#{@state.mode}\" style={overflow: 'hidden'}>\n      {@_toolbarContainerElement()}\n\n      <div name=\"Header\" style={order:1, zIndex: 2}>\n        <InjectedComponentSet matching={locations: [topSheet.Header, WorkspaceStore.Sheet.Global.Header]}\n                              direction=\"column\"\n                              id={topSheet.id}/>\n      </div>\n\n      <div name=\"Center\" style={order:2, flex: 1, position:'relative', zIndex: 1}>\n        {sheetElements[0]}\n        <ReactCSSTransitionGroup transitionLeaveTimeout={125}\n                                transitionEnterTimeout={125}\n                                transitionName=\"sheet-stack\">\n          {sheetElements[1..-1]}\n        </ReactCSSTransitionGroup>\n      </div>\n\n      <div name=\"Footer\" style={order:3, zIndex: 4}>\n        <InjectedComponentSet matching={locations: [topSheet.Footer, WorkspaceStore.Sheet.Global.Footer]}\n                              direction=\"column\"\n                              id={topSheet.id}/>\n\n      </div>\n    </Flexbox>\n\n  _toolbarContainerElement: =>\n    {toolbar} = NylasEnv.getLoadSettings()\n    return [] unless toolbar\n\n    toolbarElements = @_toolbarElements()\n    <div name=\"Toolbar\" style={order:0, zIndex: 3} className=\"sheet-toolbar\">\n      {toolbarElements[0]}\n      <ReactCSSTransitionGroup  transitionLeaveTimeout={125}\n                               transitionEnterTimeout={125}\n                               transitionName=\"opacity-125ms\">\n        {toolbarElements[1..-1]}\n      </ReactCSSTransitionGroup>\n    </div>\n\n  _toolbarElements: =>\n    @state.stack.map (sheet, index) ->\n      <Toolbar data={sheet}\n               ref={\"toolbar-#{index}\"}\n               key={\"#{index}:#{sheet.id}:toolbar\"}\n               depth={index} />\n\n  _sheetElements: =>\n    @state.stack.map (sheet, index) =>\n      <Sheet data={sheet}\n             depth={index}\n             key={\"#{index}:#{sheet.id}\"}\n             onColumnSizeChanged={@_onColumnSizeChanged} />\n\n  _onColumnSizeChanged: (sheet) =>\n    @refs[\"toolbar-#{sheet.props.depth}\"]?.recomputeLayout()\n    window.dispatchEvent(new Event('resize'))\n\n  _onStoreChange: =>\n    @setState(@_getStateFromStores())\n\n  _getStateFromStores: =>\n    stack: WorkspaceStore.sheetStack()\n    mode: WorkspaceStore.layoutMode()\n\n\nmodule.exports = SheetContainer\n"
  },
  {
    "path": "packages/client-app/src/sheet-toolbar.cjsx",
    "content": "React = require 'react'\nReactDOM = require 'react-dom'\nSheet = require './sheet'\nFlexbox = require('./components/flexbox').default\nRetinaImg = require('./components/retina-img').default\nUtils = require './flux/models/utils'\n{remote} = require 'electron'\n_str = require 'underscore.string'\n_ = require 'underscore'\n\n{Actions,\n ComponentRegistry,\n WorkspaceStore} = require \"nylas-exports\"\n\nclass ToolbarSpacer extends React.Component\n  @displayName: 'ToolbarSpacer'\n  @propTypes:\n    order: React.PropTypes.number\n\n  render: =>\n    <div className=\"item-spacer\" style={flex: 1, order:@props.order ? 0}></div>\n\nclass WindowTitle extends React.Component\n  @displayName: \"WindowTitle\"\n\n  constructor: (@props) ->\n    @state = NylasEnv.getLoadSettings()\n\n  componentDidMount: ->\n    @unlisten = NylasEnv.onWindowPropsReceived (windowProps) =>\n      @setState NylasEnv.getLoadSettings()\n\n  componentWillUnmount: ->\n    @unlisten?()\n\n  render: ->\n    <div className=\"window-title\">{@state.title}</div>\n\nCategory = null\nFocusedPerspectiveStore = null\nclass ToolbarBack extends React.Component\n  @displayName: 'ToolbarBack'\n\n  # These stores are only required when this Toolbar is actually needed.\n  # This is because loading these stores has database side effects.\n  constructor: (@props) ->\n    Category ?= require('./flux/models/category').default\n    FocusedPerspectiveStore ?= require('./flux/stores/focused-perspective-store').default\n    @state =\n      categoryName: FocusedPerspectiveStore.current().name\n\n  componentDidMount: =>\n    @_unsubscriber = FocusedPerspectiveStore.listen =>\n      @setState(categoryName: FocusedPerspectiveStore.current().name)\n\n  componentWillUnmount: =>\n    @_unsubscriber() if @_unsubscriber\n\n  render: =>\n    if @state.categoryName is Category.AllMailName\n      title = 'All Mail'\n    else if @state.categoryName\n      title = _str.titleize(@state.categoryName)\n    else\n      title = \"Back\"\n\n    <div className=\"item-back\" onClick={@_onClick} title=\"Return to #{title}\">\n      <RetinaImg name=\"sheet-back.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      <div className=\"item-back-title\">{title}</div>\n    </div>\n\n  _onClick: =>\n    Actions.popSheet()\n\nclass ToolbarWindowControls extends React.Component\n  @displayName: 'ToolbarWindowControls'\n  constructor: (@props) ->\n    @state = {alt: false}\n\n  componentDidMount: =>\n    if process.platform is 'darwin'\n      window.addEventListener('keydown', @_onAlt)\n      window.addEventListener('keyup', @_onAlt)\n\n  componentWillUnmount: =>\n    if process.platform is 'darwin'\n      window.removeEventListener('keydown', @_onAlt)\n      window.removeEventListener('keyup', @_onAlt)\n\n  render: =>\n    <div name=\"ToolbarWindowControls\" className=\"toolbar-window-controls alt-#{@state.alt}\">\n      <button tabIndex={-1} className=\"close\" onClick={ -> NylasEnv.close()}></button>\n      <button tabIndex={-1} className=\"minimize\" onClick={ -> NylasEnv.minimize()}></button>\n      <button tabIndex={-1} className=\"maximize\" onClick={@_onMaximize}></button>\n    </div>\n\n  _onAlt: (event) =>\n    @setState(alt: event.altKey) if @state.alt isnt event.altKey\n\n  _onMaximize: (event) =>\n    if process.platform is 'darwin' and not event.altKey\n      NylasEnv.setFullScreen(!NylasEnv.isFullScreen())\n    else\n      NylasEnv.maximize()\n\nclass ToolbarMenuControl extends React.Component\n  @displayName: 'ToolbarMenuControl'\n  render: =>\n    <div className=\"toolbar-menu-control\">\n      <button tabIndex={-1} className=\"btn btn-toolbar\" onClick={@_openMenu}>\n        <RetinaImg name=\"windows-menu-icon.png\" mode={RetinaImg.Mode.ContentIsMask} />\n      </button>\n    </div>\n\n  _openMenu: =>\n    applicationMenu = remote.getGlobal('application').applicationMenu\n    applicationMenu.menu.popup(NylasEnv.getCurrentWindow())\n\nComponentRegistry.register ToolbarWindowControls,\n  location: WorkspaceStore.Sheet.Global.Toolbar.Left\n\nComponentRegistry.register ToolbarMenuControl,\n  location: WorkspaceStore.Sheet.Global.Toolbar.Right\n\nclass Toolbar extends React.Component\n  @displayName: 'Toolbar'\n\n  @propTypes:\n    data: React.PropTypes.object\n    depth: React.PropTypes.number\n\n  @childContextTypes:\n    sheetDepth: React.PropTypes.number\n  getChildContext: =>\n    sheetDepth: @props.depth\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @mounted = true\n    @unlisteners = []\n    @unlisteners.push WorkspaceStore.listen (event) =>\n      @setState(@_getStateFromStores())\n    @unlisteners.push ComponentRegistry.listen (event) =>\n      @setState(@_getStateFromStores())\n    window.addEventListener(\"resize\", @_onWindowResize)\n    window.requestAnimationFrame => @recomputeLayout()\n\n  componentWillUnmount: =>\n    @mounted = false\n    window.removeEventListener(\"resize\", @_onWindowResize)\n    unlistener() for unlistener in @unlisteners\n\n  componentWillReceiveProps: (props) =>\n    @setState(@_getStateFromStores(props))\n\n  componentDidUpdate: =>\n    # Wait for other components that are dirty (the actual columns in the sheet)\n    window.requestAnimationFrame => @recomputeLayout()\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    # This is very important. Because toolbar uses ReactCSSTransitionGroup,\n    # repetitive unnecessary updates can break animations and cause performance issues.\n    not Utils.isEqualReact(nextProps, @props) or not Utils.isEqualReact(nextState, @state)\n\n  render: =>\n    style =\n      position:'absolute'\n      width:'100%'\n      height:'100%'\n      zIndex: 1\n\n    toolbars = @state.columns.map (components, idx) =>\n      <div style={position: 'absolute', top:0, display:'none'}\n           className=\"toolbar-#{@state.columnNames[idx]}\"\n           data-column={idx}\n           key={idx}>\n        {@_flexboxForComponents(components)}\n      </div>\n\n    <div\n      style={style}\n      className={\"sheet-toolbar-container mode-#{@state.mode}\"}\n      data-id={@props.data.id}>\n      {toolbars}\n    </div>\n\n  _flexboxForComponents: (components) =>\n    elements = components.map (Component) =>\n      <Component key={Component.displayName} {...@props} />\n\n    <Flexbox className=\"item-container\" direction=\"row\">\n      {elements}\n      <ToolbarSpacer key=\"spacer-50\" order={-50}/>\n      <ToolbarSpacer key=\"spacer+50\" order={50}/>\n    </Flexbox>\n\n  recomputeLayout: =>\n    # Yes this really happens - do not remove!\n    return unless @mounted\n\n    # Find our item containers that are tied to specific columns\n    el = ReactDOM.findDOMNode(@)\n    columnToolbarEls = el.querySelectorAll('[data-column]')\n\n    # Find the top sheet in the stack\n    sheet = document.querySelectorAll(\"[name='Sheet']\")[@props.depth]\n    return unless sheet\n\n    # Position item containers so they have the position and width\n    # as their respective columns in the top sheet\n    for columnToolbarEl in columnToolbarEls\n      column = columnToolbarEl.dataset.column\n      columnEl = sheet.querySelector(\"[data-column='#{column}']\")\n      continue unless columnEl\n\n      columnToolbarEl.style.display = 'inherit'\n      columnToolbarEl.style.left = \"#{columnEl.offsetLeft}px\"\n      columnToolbarEl.style.width = \"#{columnEl.offsetWidth}px\"\n\n    # Record our overall height for sheets\n    remote.getCurrentWindow().setSheetOffset(el.clientHeight)\n\n  _onWindowResize: =>\n    @recomputeLayout()\n\n  _getStateFromStores: (props) =>\n    props ?= @props\n    state =\n      mode: WorkspaceStore.layoutMode()\n      columns: []\n      columnNames: []\n\n    # Add items registered to Regions in the current sheet\n    if @props.data?.columns[state.mode]?\n      for loc in @props.data.columns[state.mode]\n        continue if WorkspaceStore.isLocationHidden(loc)\n        entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar, mode: state.mode})\n        state.columns.push(entries)\n        state.columnNames.push(loc.Toolbar.id.split(\":\")[0]) if entries\n\n    # Add left items registered to the Sheet instead of to a Region\n    for loc in [WorkspaceStore.Sheet.Global, @props.data]\n      entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Left, mode: state.mode})\n      state.columns[0]?.push(entries...)\n    if @props.depth > 0\n      state.columns[0]?.push(ToolbarBack)\n\n    # Add right items registered to the Sheet instead of to a Region\n    for loc in [WorkspaceStore.Sheet.Global, @props.data]\n      entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Right, mode: state.mode})\n      state.columns[state.columns.length - 1]?.push(entries...)\n    if state.mode is \"popout\"\n      state.columns[0]?.push(WindowTitle)\n\n    state\n\nmodule.exports = Toolbar\n"
  },
  {
    "path": "packages/client-app/src/sheet.cjsx",
    "content": "React = require 'react'\n_ = require 'underscore'\n{Actions,ComponentRegistry, WorkspaceStore} = require \"nylas-exports\"\nRetinaImg = require('./components/retina-img').default\nFlexbox = require('./components/flexbox').default\nInjectedComponentSet = require './components/injected-component-set'\nResizableRegion = require './components/resizable-region'\n\nFLEX = 10000\n\nclass Sheet extends React.Component\n  @displayName = 'Sheet'\n\n  @propTypes =\n    data: React.PropTypes.object.isRequired\n    depth: React.PropTypes.number.isRequired\n    onColumnSizeChanged: React.PropTypes.func\n\n  @defaultProps:\n    onColumnSizeChanged: ->\n\n  @childContextTypes:\n    sheetDepth: React.PropTypes.number\n\n  getChildContext: =>\n    sheetDepth: @props.depth\n\n  constructor: (@props) ->\n    @state = @_getStateFromStores()\n\n  componentDidMount: =>\n    @unlisteners ?= []\n    @unlisteners.push ComponentRegistry.listen (event) =>\n      @setState(@_getStateFromStores())\n    @unlisteners.push WorkspaceStore.listen (event) =>\n      @setState(@_getStateFromStores())\n\n  componentDidUpdate: =>\n    @props.onColumnSizeChanged(@)\n    minWidth = 0\n    minWidth += col.minWidth for col in @state.columns\n    NylasEnv.setMinimumWidth(minWidth)\n\n  shouldComponentUpdate: (nextProps, nextState) =>\n    not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)\n\n  componentWillUnmount: =>\n    unlisten() for unlisten in @unlisteners\n\n  render: =>\n    style =\n      position:'absolute'\n      width:'100%'\n      height:'100%'\n      zIndex: 1\n\n    # Note - setting the z-index of the sheet is important, even though it's\n    # always 1. Assigning a z-index creates a \"stacking context\" in the browser,\n    # so z-indexes inside the sheet are relative to each other, but something in\n    # one sheet cannot be on top of something in another sheet.\n    # http://philipwalton.com/articles/what-no-one-told-you-about-z-index/\n\n    <div name={\"Sheet\"}\n         style={style}\n         className={\"sheet mode-#{@state.mode}\"}\n         data-id={@props.data.id}>\n      <Flexbox direction=\"row\" style={overflow: 'hidden'}>\n        {@_columnFlexboxElements()}\n      </Flexbox>\n    </div>\n\n  _columnFlexboxElements: =>\n    @state.columns.map (column, idx) =>\n      {maxWidth, minWidth, handle, location, width} = column\n      if minWidth != maxWidth and maxWidth < FLEX\n        <ResizableRegion\n          key={\"#{@props.data.id}:#{idx}\"}\n          name={\"#{@props.data.id}:#{idx}\"}\n          className={\"column-#{location.id}\"}\n          style={height:'100%'}\n          data-column={idx}\n          onResize={@_onColumnResize.bind(@, column)}\n          initialWidth={width}\n          minWidth={minWidth}\n          maxWidth={maxWidth}\n          handle={handle}>\n          <InjectedComponentSet direction=\"column\" matching={location: location, mode: @state.mode}/>\n        </ResizableRegion>\n      else\n        style =\n          height: '100%'\n          minWidth: minWidth\n          overflow: 'hidden'\n        if maxWidth < FLEX\n          style.width = maxWidth\n        else\n          style.flex = 1\n        <InjectedComponentSet direction=\"column\"\n                              key={\"#{@props.data.id}:#{idx}\"}\n                              name={\"#{@props.data.id}:#{idx}\"}\n                              className={\"column-#{location.id}\"}\n                              data-column={idx}\n                              style={style}\n                              matching={location: location, mode: @state.mode}/>\n\n  _onColumnResize: (column, width) =>\n    NylasEnv.storeColumnWidth(id: column.location.id, width: width)\n    @props.onColumnSizeChanged(@)\n\n  _getStateFromStores: =>\n    state =\n      mode: WorkspaceStore.layoutMode()\n      columns: []\n\n    widest = -1\n    widestWidth = -1\n\n    if @props.data?.columns[state.mode]?\n      for location, idx in @props.data.columns[state.mode]\n        continue if WorkspaceStore.isLocationHidden(location)\n        entries = ComponentRegistry.findComponentsMatching({location: location, mode: state.mode})\n        maxWidth = _.reduce entries, ((m,component) -> Math.min(component.containerStyles?.maxWidth ? 10000, m)), 10000\n        minWidth = _.reduce entries, ((m,component) -> Math.max(component.containerStyles?.minWidth ? 0, m)), 0\n        width = NylasEnv.getColumnWidth(location.id)\n        col = {maxWidth, minWidth, location, width}\n        state.columns.push(col)\n\n        if maxWidth > widestWidth\n          widestWidth = maxWidth\n          widest = idx\n\n    if state.columns.length > 0\n      # Once we've accumulated all the React components for the columns,\n      # ensure that at least one column has a huge max-width so that the columns\n      # expand to fill the window. This may make items in the column unhappy, but\n      # we pick the column with the highest max-width so the effect is minimal.\n      state.columns[widest].maxWidth = FLEX\n\n      # Assign flexible edges based on whether items are to the left or right\n      # of the flexible column (which has no edges)\n      state.columns[i].handle = ResizableRegion.Handle.Right for i in [0..widest-1] by 1\n      state.columns[i].handle = ResizableRegion.Handle.Left  for i in [widest..state.columns.length-1] by 1\n    state\n\n  _pop: =>\n    Actions.popSheet()\n\nmodule.exports = Sheet\n"
  },
  {
    "path": "packages/client-app/src/spellchecker.es6",
    "content": "import {remote} from 'electron';\nimport {SpellCheckHandler} from 'electron-spellchecker';\nimport fs from 'fs';\nimport path from 'path';\n\nconst MenuItem = remote.MenuItem;\nconst customDictFilePath = path.join(NylasEnv.getConfigDirPath(), 'custom-dict.json')\n\nclass Spellchecker {\n  constructor() {\n    this.handler = new SpellCheckHandler();\n    this.handler.switchLanguage('en-US'); // Start with US English\n    this.handler.attachToInput();\n    this.isMisspelledCache = {};\n\n    this._customDictLoaded = false;\n    this._saveOnLoad = false;\n    this._savingCustomDict = false;\n    this._saveAgain = false;\n\n    this._customDict = {};\n    this._loadCustomDict();\n  }\n\n  _loadCustomDict = () => {\n    fs.readFile(customDictFilePath, (err, data) => {\n      let fileData = data;\n      if (err) {\n        if (err.code === \"ENOENT\") { // File doesn't exist, we haven't saved any words yet\n          fileData = \"{}\";\n        } else {\n          NylasEnv.reportError(err);\n          return;\n        }\n      }\n      const loadedDict = JSON.parse(fileData);\n      this._customDict = Object.assign(loadedDict, this._customDict);\n      this._customDictLoaded = true;\n      if (this._saveOnLoad) {\n        this._saveCustomDict();\n        this._saveOnLoad = false;\n      }\n    })\n  }\n\n  _saveCustomDict = () => {\n    // If we haven't loaded the dict yet, saving could overwrite all the things.\n    // Wait until the loaded dict is merged with our working copy before saving\n    if (this._customDictLoaded) {\n      // Don't perform two writes at the same time, as this results in an overlaid\n      // version of the data. (This may or may not happen in practice, but was\n      // an issue with the tests)\n      if (this._savingCustomDict) {\n        this._saveAgain = true;\n      } else {\n        this._savingCustomDict = true;\n        fs.writeFile(customDictFilePath, JSON.stringify(this._customDict), (err) => {\n          if (err) {\n            NylasEnv.reportError(err);\n          }\n          this._savingCustomDict = false;\n          if (this._saveAgain) {\n            this._saveAgain = false;\n            this._saveCustomDict();\n          }\n        })\n      }\n    } else {\n      this._saveOnLoad = true;\n    }\n  }\n\n  isMisspelled = (word) => {\n    if ({}.hasOwnProperty.call(this._customDict, word)) {\n      return false\n    }\n    if ({}.hasOwnProperty.call(this.isMisspelledCache, word)) {\n      return this.isMisspelledCache[word]\n    }\n    const misspelled = !this.handler.handleElectronSpellCheck(word);\n    this.isMisspelledCache[word] = misspelled;\n    return misspelled;\n  }\n\n  learnWord = (word) => {\n    this._customDict[word] = \"\";\n    this._saveCustomDict();\n  }\n\n  unlearnWord = (word) => {\n    if (word in this._customDict) {\n      delete this._customDict[word];\n      this._saveCustomDict();\n    }\n  }\n\n  appendSpellingItemsToMenu = ({menu, word, onCorrect, onDidLearn}) => {\n    if (this.isMisspelled(word)) {\n      const corrections = this.handler.currentSpellchecker.getCorrectionsForMisspelling(word)\n      if (corrections.length > 0) {\n        corrections.forEach((correction) => {\n          menu.append(new MenuItem({\n            label: correction,\n            click: () => onCorrect(correction),\n          }))\n        });\n      } else {\n        menu.append(new MenuItem({ label: 'No Guesses Found', enabled: false}))\n      }\n      menu.append(new MenuItem({ type: 'separator' }))\n\n      menu.append(new MenuItem({\n        label: 'Learn Spelling',\n        click: () => {\n          this.learnWord(word);\n          if (onDidLearn) {\n            onDidLearn(word);\n          }\n        },\n      }))\n      menu.append(new MenuItem({ type: 'separator' }))\n    }\n  }\n}\n\nexport default new Spellchecker();\n"
  },
  {
    "path": "packages/client-app/src/style-manager.coffee",
    "content": "fs = require 'fs-plus'\npath = require 'path'\n{Emitter, Disposable} = require 'event-kit'\n\n# Extended: A singleton instance of this class available via `NylasEnv.styles`,\n# which you can use to globally query and observe the set of active style\n# sheets. The `StyleManager` doesn't add any style elements to the DOM on its\n# own, but is instead subscribed to by individual `<nylas-styles>` elements,\n# which clone and attach style elements in different contexts.\nmodule.exports =\nclass StyleManager\n  constructor: ->\n    @emitter = new Emitter\n    @styleElements = []\n    @styleElementsBySourcePath = {}\n\n  ###\n  Section: Event Subscription\n  ###\n\n  # Extended: Invoke `callback` for all current and future style elements.\n  #\n  # * `callback` {Function} that is called with style elements.\n  #   * `styleElement` An `HTMLStyleElement` instance. The `.sheet` property\n  #     will be null because this element isn't attached to the DOM. If you want\n  #     to attach this element to the DOM, be sure to clone it first by calling\n  #     `.cloneNode(true)` on it. The style element will also have the following\n  #     non-standard properties:\n  #     * `sourcePath` A {String} containing the path from which the style\n  #       element was loaded.\n  #     * `context` A {String} indicating the target context of the style\n  #       element.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to cancel the\n  # subscription.\n  observeStyleElements: (callback) ->\n    callback(styleElement) for styleElement in @getStyleElements()\n    @onDidAddStyleElement(callback)\n\n  # Extended: Invoke `callback` when a style element is added.\n  #\n  # * `callback` {Function} that is called with style elements.\n  #   * `styleElement` An `HTMLStyleElement` instance. The `.sheet` property\n  #     will be null because this element isn't attached to the DOM. If you want\n  #     to attach this element to the DOM, be sure to clone it first by calling\n  #     `.cloneNode(true)` on it. The style element will also have the following\n  #     non-standard properties:\n  #     * `sourcePath` A {String} containing the path from which the style\n  #       element was loaded.\n  #     * `context` A {String} indicating the target context of the style\n  #       element.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to cancel the\n  # subscription.\n  onDidAddStyleElement: (callback) ->\n    @emitter.on 'did-add-style-element', callback\n\n  # Extended: Invoke `callback` when a style element is removed.\n  #\n  # * `callback` {Function} that is called with style elements.\n  #   * `styleElement` An `HTMLStyleElement` instance.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to cancel the\n  # subscription.\n  onDidRemoveStyleElement: (callback) ->\n    @emitter.on 'did-remove-style-element', callback\n\n  # Extended: Invoke `callback` when an existing style element is updated.\n  #\n  # * `callback` {Function} that is called with style elements.\n  #   * `styleElement` An `HTMLStyleElement` instance. The `.sheet` property\n  #      will be null because this element isn't attached to the DOM. The style\n  #      element will also have the following non-standard properties:\n  #     * `sourcePath` A {String} containing the path from which the style\n  #       element was loaded.\n  #     * `context` A {String} indicating the target context of the style\n  #       element.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to cancel the\n  # subscription.\n  onDidUpdateStyleElement: (callback) ->\n    @emitter.on 'did-update-style-element', callback\n\n  ###\n  Section: Reading Style Elements\n  ###\n\n  # Extended: Get all loaded style elements.\n  getStyleElements: ->\n    @styleElements.slice()\n\n  addStyleSheet: (source, params) ->\n    sourcePath = params?.sourcePath\n    context = params?.context\n    priority = params?.priority\n\n    if sourcePath? and styleElement = @styleElementsBySourcePath[sourcePath]\n      updated = true\n    else\n      styleElement = document.createElement('style')\n      if sourcePath?\n        styleElement.sourcePath = sourcePath\n        styleElement.setAttribute('source-path', sourcePath)\n\n      if context?\n        styleElement.context = context\n        styleElement.setAttribute('context', context)\n\n      if priority?\n        styleElement.priority = priority\n        styleElement.setAttribute('priority', priority)\n\n    styleElement.textContent = source\n\n    if updated\n      @emitter.emit 'did-update-style-element', styleElement\n    else\n      @addStyleElement(styleElement)\n\n    new Disposable => @removeStyleElement(styleElement)\n\n  addStyleElement: (styleElement) ->\n    {sourcePath, priority} = styleElement\n\n    if priority?\n      for existingElement, index in @styleElements\n        if existingElement.priority > priority\n          insertIndex = index\n          break\n\n    insertIndex ?= @styleElements.length\n\n    @styleElements.splice(insertIndex, 0, styleElement)\n    @styleElementsBySourcePath[sourcePath] ?= styleElement if sourcePath?\n    @emitter.emit 'did-add-style-element', styleElement\n\n  removeStyleElement: (styleElement) ->\n    index = @styleElements.indexOf(styleElement)\n    unless index is -1\n      @styleElements.splice(index, 1)\n      delete @styleElementsBySourcePath[styleElement.sourcePath] if styleElement.sourcePath?\n      @emitter.emit 'did-remove-style-element', styleElement\n\n  getSnapshot: ->\n    @styleElements.slice()\n\n  restoreSnapshot: (styleElementsToRestore) ->\n    for styleElement in @getStyleElements()\n      @removeStyleElement(styleElement) unless styleElement in styleElementsToRestore\n\n    existingStyleElements = @getStyleElements()\n    for styleElement in styleElementsToRestore\n      @addStyleElement(styleElement) unless styleElement in existingStyleElements\n"
  },
  {
    "path": "packages/client-app/src/styles-element.coffee",
    "content": "{Emitter, CompositeDisposable} = require 'event-kit'\n\nclass StylesElement extends HTMLElement\n  subscriptions: null\n  context: null\n\n  onDidAddStyleElement: (callback) ->\n    @emitter.on 'did-add-style-element', callback\n\n  onDidRemoveStyleElement: (callback) ->\n    @emitter.on 'did-remove-style-element', callback\n\n  onDidUpdateStyleElement: (callback) ->\n    @emitter.on 'did-update-style-element', callback\n\n  createdCallback: ->\n    @emitter = new Emitter\n    @styleElementClonesByOriginalElement = new WeakMap\n\n  attachedCallback: ->\n    @initialize()\n\n  detachedCallback: ->\n    @subscriptions.dispose()\n    @subscriptions = null\n\n  attributeChangedCallback: (attrName, oldVal, newVal) ->\n    @contextChanged() if attrName is 'context'\n\n  initialize: ->\n    return if @subscriptions?\n\n    @subscriptions = new CompositeDisposable\n    @context = @getAttribute('context') ? undefined\n\n    @subscriptions.add NylasEnv.styles.observeStyleElements(@styleElementAdded.bind(this))\n    @subscriptions.add NylasEnv.styles.onDidRemoveStyleElement(@styleElementRemoved.bind(this))\n    @subscriptions.add NylasEnv.styles.onDidUpdateStyleElement(@styleElementUpdated.bind(this))\n\n  contextChanged: ->\n    return unless @subscriptions?\n\n    @styleElementRemoved(child) for child in Array::slice.call(@children)\n    @context = @getAttribute('context')\n    @styleElementAdded(styleElement) for styleElement in NylasEnv.styles.getStyleElements()\n\n  styleElementAdded: (styleElement) ->\n    return unless @styleElementMatchesContext(styleElement)\n\n    styleElementClone = styleElement.cloneNode(true)\n    styleElementClone.sourcePath = styleElement.sourcePath\n    styleElementClone.context = styleElement.context\n    styleElementClone.priority = styleElement.priority\n    @styleElementClonesByOriginalElement.set(styleElement, styleElementClone)\n\n    priority = styleElement.priority\n    if priority?\n      for child in @children\n        if child.priority > priority\n          insertBefore = child\n          break\n\n    @insertBefore(styleElementClone, insertBefore)\n    @emitter.emit 'did-add-style-element', styleElementClone\n\n  styleElementRemoved: (styleElement) ->\n    return unless @styleElementMatchesContext(styleElement)\n\n    styleElementClone = @styleElementClonesByOriginalElement.get(styleElement) ? styleElement\n    styleElementClone.remove()\n    @emitter.emit 'did-remove-style-element', styleElementClone\n\n  styleElementUpdated: (styleElement) ->\n    return unless @styleElementMatchesContext(styleElement)\n\n    styleElementClone = @styleElementClonesByOriginalElement.get(styleElement)\n    styleElementClone.textContent = styleElement.textContent\n    @emitter.emit 'did-update-style-element', styleElementClone\n\n  styleElementMatchesContext: (styleElement) ->\n    not @context? or styleElement.context is @context\n\nmodule.exports = StylesElement = document.registerElement 'nylas-styles', prototype: StylesElement.prototype\n"
  },
  {
    "path": "packages/client-app/src/system-start-service.es6",
    "content": "import path from 'path'\nimport fs from 'fs'\nimport os from 'os'\nimport {exec} from 'child_process'\nimport ws from 'windows-shortcuts'\n\nclass SystemStartServiceBase {\n  checkAvailability() {\n    return Promise.resolve(false);\n  }\n\n  doesLaunchOnSystemStart() {\n    throw new Error(\"doesLaunchOnSystemStart is not available\");\n  }\n\n  configureToLaunchOnSystemStart() {\n    throw new Error(\"configureToLaunchOnSystemStart is not available\")\n  }\n\n  dontLaunchOnSystemStart() {\n    throw new Error(\"dontLaunchOnSystemStart is not available\")\n  }\n}\n\nclass SystemStartServiceDarwin extends SystemStartServiceBase {\n  checkAvailability() {\n    return new Promise((resolve) => {\n      fs.access(this._launcherPath(), fs.R_OK | fs.W_OK, (err) => {\n        if (err) { resolve(false) } else { resolve(true) }\n      });\n    });\n  }\n\n  doesLaunchOnSystemStart() {\n    return new Promise((resolve) => {\n      fs.access(this._plistPath(), fs.R_OK | fs.W_OK, (err) => {\n        if (err) { resolve(false) } else { resolve(true) }\n      });\n    });\n  }\n\n  configureToLaunchOnSystemStart() {\n    fs.writeFile(this._plistPath(), JSON.stringify(this._launchdPlist()), (err) => {\n      if (!err) {\n        exec(`plutil -convert xml1 ${this._plistPath()}`)\n      }\n    })\n  }\n\n  dontLaunchOnSystemStart() {\n    return fs.unlink(this._plistPath())\n  }\n\n  _launcherPath() {\n    return path.join(\"/\", \"Applications\", \"Nylas Mail.app\", \"Contents\",\n                     \"MacOS\", \"Nylas\")\n  }\n\n  _plistPath() {\n    return path.join(process.env.HOME, \"Library\",\n                     \"LaunchAgents\", \"com.nylas.plist\");\n  }\n\n  _launchdPlist() {\n    return {\n      Label: \"com.nylas.n1\",\n      Program: this._launcherPath(),\n      ProgramArguments: [\"--background\"],\n      RunAtLoad: true,\n    }\n  }\n}\n\nclass SystemStartServiceWin32 extends SystemStartServiceBase {\n  checkAvailability() {\n    return new Promise((resolve) => {\n      fs.access(this._launcherPath(), fs.R_OK | fs.W_OK, (err) => {\n        if (err) { resolve(false) } else { resolve(true) }\n      });\n    });\n  }\n\n  doesLaunchOnSystemStart() {\n    return new Promise((resolve) => {\n      fs.access(this._shortcutPath(), fs.R_OK | fs.W_OK, (err) => {\n        if (err) { resolve(false) } else { resolve(true) }\n      });\n    });\n  }\n\n  configureToLaunchOnSystemStart() {\n    ws.create(this._shortcutPath(), {\n      target: this._launcherPath(),\n      args: \"--processStart=nylas.exe --process-start-args=--background\",\n      runStyle: ws.MIN,\n      desc: \"An extensible, open-source mail client built on the modern web.\",\n    }, (err) => {\n      if (err) NylasEnv.reportError(err)\n    });\n  }\n\n  dontLaunchOnSystemStart() {\n    return fs.unlink(this._shortcutPath())\n  }\n\n  _launcherPath() {\n    return path.join(process.env.LOCALAPPDATA, \"nylas\", \"Update.exe\")\n  }\n\n  _shortcutPath() {\n    return path.join(process.env.APPDATA, \"Microsoft\", \"Windows\",\n                     \"Start Menu\", \"Programs\", \"Startup\", \"Nylas.lnk\")\n  }\n}\n\nclass SystemStartServiceLinux extends SystemStartServiceBase {\n  checkAvailability() {\n    return new Promise((resolve) => {\n      fs.access(this._launcherPath(), fs.R_OK, (err) => {\n        if (err) { resolve(false) } else { resolve(true) }\n      });\n    });\n  }\n\n  doesLaunchOnSystemStart() {\n    return new Promise((resolve) => {\n      fs.access(this._shortcutPath(), fs.R_OK | fs.W_OK, (err) => {\n        if (err) { resolve(false) } else { resolve(true) }\n      });\n    });\n  }\n\n  configureToLaunchOnSystemStart() {\n    fs.readFile(this._launcherPath(), 'utf8', (error, data) => {\n      // Append the --background flag before the Exec key\n      const parsedData = data.replace('%U', '--background %U');\n\n      fs.writeFile(this._shortcutPath(), parsedData, () => {});\n    });\n  }\n\n  dontLaunchOnSystemStart() {\n    return fs.unlink(this._shortcutPath())\n  }\n\n  _launcherPath() {\n    return path.join('/', 'usr', 'share', 'applications', 'nylas.desktop');\n  }\n\n  _shortcutPath() {\n    const configDir = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), '.config');\n    return path.join(configDir, 'autostart', 'nylas-mail.desktop');\n  }\n}\n\n/* eslint import/no-mutable-exports: 0*/\nlet SystemStartService;\nif (process.platform === \"darwin\") {\n  SystemStartService = SystemStartServiceDarwin;\n} else if (process.platform === \"linux\") {\n  SystemStartService = SystemStartServiceLinux;\n} else if (process.platform === \"win32\") {\n  SystemStartService = SystemStartServiceWin32;\n} else {\n  SystemStartService = SystemStartServiceBase;\n}\n\nexport default SystemStartService\n"
  },
  {
    "path": "packages/client-app/src/task-bootstrap.coffee",
    "content": "{userAgent, taskPath} = process.env\nhandler = null\n\nsetupGlobals = ->\n  global.attachEvent = ->\n  console =\n    warn: -> emit 'task:warn', arguments...\n    log: -> emit 'task:log', arguments...\n    error: -> emit 'task:error', arguments...\n    trace: ->\n  global.__defineGetter__ 'console', -> console\n\n  global.document =\n    createElement: ->\n      setAttribute: ->\n      getElementsByTagName: -> []\n      appendChild: ->\n    documentElement:\n      insertBefore: ->\n      removeChild: ->\n    getElementById: -> {}\n    createComment: -> {}\n    createDocumentFragment: -> {}\n\n  global.emit = (event, args...) ->\n    process.send({event, args})\n  global.navigator = {userAgent}\n  global.window = global\n\nhandleEvents = ->\n  process.on 'unhandledRejection', (reason, promise) ->\n    console.error(reason.stack, promise)\n  process.on 'uncaughtException', (error) ->\n    console.error(error.message, error.stack)\n  process.on 'message', ({event, args}={}) ->\n    return unless event is 'start'\n\n    isAsync = false\n    async = ->\n      isAsync = true\n      (result) ->\n        emit('task:completed', result)\n    result = handler.bind({async})(args...)\n    emit('task:completed', result) unless isAsync\n\nsetupGlobals()\nhandleEvents()\nhandler = require(taskPath)\n"
  },
  {
    "path": "packages/client-app/src/task.coffee",
    "content": "_ = require 'underscore'\nChildProcess = require 'child_process'\n{Emitter} = require 'event-kit'\n\n# Extended: Run a node script in a separate process.\n#\n# Used by the fuzzy-finder and [find in project](https://github.com/atom/atom/blob/master/src/scan-handler.coffee).\n#\n# For a real-world example, see the [scan-handler](https://github.com/atom/atom/blob/master/src/scan-handler.coffee)\n# and the [instantiation of the task](https://github.com/atom/atom/blob/4a20f13162f65afc816b512ad7201e528c3443d7/src/project.coffee#L245).\n#\n# ## Examples\n#\n# In your package code:\n#\n# ```coffee\n# {Task} = require './task'\n#\n# task = Task.once '/path/to/task-file.coffee', parameter1, parameter2, ->\n#   console.log 'task has finished'\n#\n# task.on 'some-event-from-the-task', (data) =>\n#   console.log data.someString # prints 'yep this is it'\n# ```\n#\n# In `'/path/to/task-file.coffee'`:\n#\n# ```coffee\n# module.exports = (parameter1, parameter2) ->\n#   # Indicates that this task will be async.\n#   # Call the `callback` to finish the task\n#   callback = @async()\n#\n#   emit('some-event-from-the-task', {someString: 'yep this is it'})\n#\n#   callback()\n# ```\nmodule.exports =\nclass Task\n  # Public: A helper method to easily launch and run a task once.\n  #\n  # * `taskPath` The {String} path to the CoffeeScript/JavaScript file which\n  #   exports a single {Function} to execute.\n  # * `args` The arguments to pass to the exported function.\n  #\n  # Returns the created {Task}.\n  @once: (taskPath, args...) ->\n    task = new Task(taskPath)\n    task.once 'task:completed', -> task.terminate()\n    task.start(args...)\n    task\n\n  # Called upon task completion.\n  #\n  # It receives the same arguments that were passed to the task.\n  #\n  # If subclassed, this is intended to be overridden. However if {::start}\n  # receives a completion callback, this is overridden.\n  callback: null\n\n  # Public: Creates a task. You should probably use {.once}\n  #\n  # * `taskPath` The {String} path to the CoffeeScript/JavaScript file that\n  #   exports a single {Function} to execute.\n  constructor: (taskPath) ->\n    @emitter = new Emitter\n\n    compileCacheRequire = \"require('#{require.resolve('./compile-cache')}')\"\n    compileCachePath = require('./compile-cache').getCacheDirectory()\n    taskBootstrapRequire = \"require('#{require.resolve('./task-bootstrap')}');\"\n    bootstrap = \"\"\"\n      #{compileCacheRequire}.setCacheDirectory('#{compileCachePath}');\n      #{taskBootstrapRequire}\n    \"\"\"\n    bootstrap = bootstrap.replace(/\\\\/g, \"\\\\\\\\\")\n\n    taskPath = require.resolve(taskPath)\n    taskPath = taskPath.replace(/\\\\/g, \"\\\\\\\\\")\n\n    env = _.extend({}, process.env, {taskPath, userAgent: 'NylasMail'})\n    @childProcess = ChildProcess.fork '--eval', [bootstrap], {env, silent: true}\n\n    @on \"task:log\", -> console.log(arguments...)\n    @on \"task:warn\", -> console.warn(arguments...)\n    @on \"task:error\", -> console.error(arguments...)\n    @on \"task:completed\", (args...) => @callback?(args...)\n\n    @handleEvents()\n\n  # Routes messages from the child to the appropriate event.\n  handleEvents: ->\n    @childProcess.removeAllListeners()\n    @childProcess.on 'message', ({event, args}) =>\n      @emitter.emit(event, args) if @childProcess?\n\n    # Catch the errors that happened before task-bootstrap.\n    if @childProcess.stdout?\n      @childProcess.stdout.removeAllListeners()\n      @childProcess.stdout.on 'data', (data) -> console.log data.toString()\n\n    if @childProcess.stderr?\n      @childProcess.stderr.removeAllListeners()\n      @childProcess.stderr.on 'data', (data) -> console.error data.toString()\n\n  # Public: Starts the task.\n  #\n  # Throws an error if this task has already been terminated or if sending a\n  # message to the child process fails.\n  #\n  # * `args` The arguments to pass to the function exported by this task's script.\n  # * `callback` (optional) A {Function} to call when the task completes.\n  start: (args..., callback) ->\n    throw new Error('Cannot start terminated process') unless @childProcess?\n\n    @handleEvents()\n    if _.isFunction(callback)\n      @callback = callback\n    else\n      args.push(callback)\n    @send({event: 'start', args})\n    undefined\n\n  # Public: Send message to the task.\n  #\n  # Throws an error if this task has already been terminated or if sending a\n  # message to the child process fails.\n  #\n  # * `message` The message to send to the task.\n  send: (message) ->\n    if @childProcess?\n      @childProcess.send(message)\n    else\n      throw new Error('Cannot send message to terminated process')\n    undefined\n\n  # Public: Call a function when an event is emitted by the child process\n  #\n  # * `eventName` The {String} name of the event to handle.\n  # * `callback` The {Function} to call when the event is emitted.\n  #\n  # Returns a {Disposable} that can be used to stop listening for the event.\n  on: (eventName, callback) -> @emitter.on eventName, (args) -> callback(args...)\n\n  once: (eventName, callback) ->\n    disposable = @on eventName, (args...) ->\n      disposable.dispose()\n      callback(args...)\n\n  # Public: Forcefully stop the running task.\n  #\n  # No more events are emitted once this method is called.\n  terminate: ->\n    return false unless @childProcess?\n\n    @childProcess.removeAllListeners()\n    @childProcess.stdout?.removeAllListeners()\n    @childProcess.stderr?.removeAllListeners()\n    @childProcess.kill()\n    @childProcess = null\n\n    true\n\n  cancel: ->\n    didForcefullyTerminate = @terminate()\n    if didForcefullyTerminate\n      @emitter.emit('task:canceled')\n    didForcefullyTerminate\n"
  },
  {
    "path": "packages/client-app/src/theme-manager.coffee",
    "content": "path = require 'path'\n\n_ = require 'underscore'\nEmitterMixin = require('emissary').Emitter\n{Emitter, Disposable, CompositeDisposable} = require 'event-kit'\n{File} = require 'pathwatcher'\nfs = require 'fs-plus'\nQ = require 'q'\n\nPackage = require './package'\n\n# Extended: Handles loading and activating available themes.\n#\n# An instance of this class is always available as the `NylasEnv.themes` global.\nmodule.exports =\nclass ThemeManager\n  EmitterMixin.includeInto(this)\n\n  constructor: ({@packageManager, @resourcePath, @configDirPath, @safeMode}) ->\n    @emitter = new Emitter\n    @styleSheetDisposablesBySourcePath = {}\n    @lessCache = null\n    @initialLoadComplete = false\n    @packageManager.registerPackageActivator(this, ['theme'])\n    @sheetsByStyleElement = new WeakMap\n\n    stylesElement = document.head.querySelector('nylas-styles')\n    stylesElement.onDidAddStyleElement @styleElementAdded.bind(this)\n    stylesElement.onDidRemoveStyleElement @styleElementRemoved.bind(this)\n    stylesElement.onDidUpdateStyleElement @styleElementUpdated.bind(this)\n\n  baseThemeName: -> 'ui-light'\n\n  watchCoreStyles: ->\n    console.log('Watching /static and /internal_packages for LESS changes')\n    watchStylesIn = (folder) =>\n      stylePaths = fs.listTreeSync(folder)\n      PathWatcher = require 'pathwatcher'\n      for stylePath in stylePaths\n        continue unless path.extname(stylePath) is '.less'\n        PathWatcher.watch stylePath, =>\n          @activateThemes()\n    watchStylesIn(\"#{@resourcePath}/static\")\n    watchStylesIn(\"#{@resourcePath}/internal_packages\")\n\n  styleElementAdded: (styleElement) ->\n    {sheet} = styleElement\n    @sheetsByStyleElement.set(styleElement, sheet)\n    @emitter.emit 'did-add-stylesheet', sheet\n    @emitter.emit 'did-change-stylesheets'\n\n  styleElementRemoved: (styleElement) ->\n    sheet = @sheetsByStyleElement.get(styleElement)\n    @emitter.emit 'did-remove-stylesheet', sheet\n    @emitter.emit 'did-change-stylesheets'\n\n  styleElementUpdated: ({sheet}) ->\n    @emitter.emit 'did-remove-stylesheet', sheet\n    @emitter.emit 'did-add-stylesheet', sheet\n    @emitter.emit 'did-change-stylesheets'\n\n  ###\n  Section: Event Subscription\n  ###\n\n  # Essential: Invoke `callback` when style sheet changes associated with\n  # updating the list of active themes have completed.\n  #\n  # * `callback` {Function}\n  onDidChangeActiveThemes: (callback) ->\n    @emitter.on 'did-change-active-themes', callback\n\n  ###\n  Section: Accessing Available Themes\n  ###\n\n  getAvailableNames: ->\n    # TODO: Maybe should change to list all the available themes out there?\n    @getLoadedNames()\n\n  ###\n  Section: Accessing Loaded Themes\n  ###\n\n  # Public: Get an array of all the loaded theme names.\n  getLoadedThemeNames: ->\n    theme.name for theme in @getLoadedThemes()\n\n  # Public: Get an array of all the loaded themes.\n  getLoadedThemes: ->\n    pack for pack in @packageManager.getLoadedPackages() when pack.isTheme()\n\n  ###\n  Section: Accessing Active Themes\n  ###\n\n  # Public: Get an array of all the active theme names.\n  getActiveThemeNames: ->\n    theme.name for theme in @getActiveThemes()\n\n  # Public: Get an array of all the active themes.\n  getActiveThemes: ->\n    pack for pack in @packageManager.getActivePackages() when pack.isTheme()\n\n  getActiveTheme: ->\n    # The first element in the array returned by `getActiveNames` themes will\n    # actually be the active theme\n    @getActiveThemes()[0]\n\n  activatePackages: -> @activateThemes()\n\n  ###\n  Section: Managing Enabled Themes\n  ###\n\n  # Public: Get the enabled theme names from the config.\n  #\n  # Returns an array of theme names in the order that they should be activated.\n  getEnabledThemeNames: ->\n    themeNames = NylasEnv.config.get('core.themes') ? []\n    themeNames = [themeNames] unless _.isArray(themeNames)\n    themeNames = themeNames.filter (themeName) ->\n      if themeName and typeof themeName is 'string'\n        return true if NylasEnv.packages.resolvePackagePath(themeName)\n        console.warn(\"Enabled theme '#{themeName}' is not installed.\")\n      false\n\n    # Use a built-in theme any time the configured themes are not\n    # available.\n    if themeNames.length is 0\n      builtInThemeNames = [\n        'ui-light', 'ui-dark'\n      ]\n      themeNames = _.intersection(themeNames, builtInThemeNames)\n      if themeNames.length is 0\n        themeNames = ['ui-light']\n\n    # Reverse so the first (top) theme is loaded after the others. We want\n    # the first/top theme to override later themes in the stack.\n    themeNames.reverse()\n\n  # Set the active theme.\n  # Because of how theme-manager works, we always need to set the\n  # base theme first, and the newly activated theme after it to override the\n  # styles. We don't want to have more than 1 theme active at a time, so the\n  # array of active themes should always be of size 2.\n  #\n  # * `theme` {string} - the theme to activate\n  setActiveTheme: (theme) ->\n    base = @baseThemeName()\n    NylasEnv.config.set('core.themes', _.uniq [base, theme])\n\n  ###\n  Section: Private\n  ###\n\n  # Resolve and apply the stylesheet specified by the path.\n  #\n  # This supports both CSS and Less stylsheets.\n  #\n  # * `stylesheetPath` A {String} path to the stylesheet that can be an absolute\n  #   path or a relative path that will be resolved against the load path.\n  #\n  # Returns a {Disposable} on which `.dispose()` can be called to remove the\n  # required stylesheet.\n  requireStylesheet: (stylesheetPath) ->\n    if fullPath = @resolveStylesheet(stylesheetPath)\n      content = @loadStylesheet(fullPath)\n      @applyStylesheet(fullPath, content)\n    else\n      throw new Error(\"Could not find a file at path '#{stylesheetPath}'\")\n\n  loadBaseStylesheets: ->\n    @reloadBaseStylesheets()\n\n  reloadBaseStylesheets: ->\n    @requireStylesheet('../static/index')\n    @requireStylesheet('../static/email-frame')\n    if nativeStylesheetPath = fs.resolveOnLoadPath(process.platform, ['css', 'less'])\n      @requireStylesheet(nativeStylesheetPath)\n\n  stylesheetElementForId: (id) ->\n    document.head.querySelector(\"nylas-styles style[source-path=\\\"#{id}\\\"]\")\n\n  resolveStylesheet: (stylesheetPath) ->\n    if path.extname(stylesheetPath).length > 0\n      fs.resolveOnLoadPath(stylesheetPath)\n    else\n      fs.resolveOnLoadPath(stylesheetPath, ['css', 'less'])\n\n  loadStylesheet: (stylesheetPath, importFallbackVariables) ->\n    if path.extname(stylesheetPath) is '.less'\n      @loadLessStylesheet(stylesheetPath, importFallbackVariables)\n    else\n      fs.readFileSync(stylesheetPath, 'utf8')\n\n  loadLessStylesheet: (lessStylesheetPath, importFallbackVariables=false) ->\n    unless @lessCache?\n      LessCompileCache = require('./less-compile-cache').default\n      @lessCache = new LessCompileCache({@configDirPath, @resourcePath, importPaths: @getImportPaths()})\n\n    try\n      if importFallbackVariables\n        baseVarImports = \"\"\"\n        @import \"variables/ui-variables\";\n        \"\"\"\n        less = fs.readFileSync(lessStylesheetPath, 'utf8')\n        @lessCache.cssForFile(lessStylesheetPath, [baseVarImports, less].join('\\n'))\n      else\n        @lessCache.read(lessStylesheetPath)\n    catch error\n      if error.line?\n        message = \"Error compiling Less stylesheet: `#{lessStylesheetPath}`\"\n        detail = \"\"\"\n          Line number: #{error.line}\n          #{error.message}\n        \"\"\"\n      else\n        message = \"Error loading Less stylesheet: `#{lessStylesheetPath}`\"\n        detail = error.message\n\n      console.error(message, {detail, dismissable: true})\n      console.error(detail)\n      throw error\n\n  removeStylesheet: (stylesheetPath) ->\n    @styleSheetDisposablesBySourcePath[stylesheetPath]?.dispose()\n\n  applyStylesheet: (path, text) ->\n    @styleSheetDisposablesBySourcePath[path] = NylasEnv.styles.addStyleSheet(text, sourcePath: path)\n\n  stringToId: (string) ->\n    string.replace(/\\\\/g, '/')\n\n  activateThemes: ->\n    deferred = Q.defer()\n\n    # NylasEnv.config.observe runs the callback once, then on subsequent changes.\n    NylasEnv.config.observe 'core.themes', =>\n      @deactivateThemes()\n\n      # Refreshing the less cache is very expensive (hundreds of ms). It\n      # will be refreshed once the promise resolves after packages are\n      # activated.\n\n      promises = []\n      for themeName in @getEnabledThemeNames()\n        if @packageManager.resolvePackagePath(themeName)\n          promises.push(@packageManager.activatePackage(themeName))\n        else\n          console.warn(\"Failed to activate theme '#{themeName}' because it isn't installed.\")\n\n      Q.all(promises).then =>\n        @addActiveThemeClasses()\n        @refreshLessCache() # Update cache again now that @getActiveThemes() is populated\n        @reloadBaseStylesheets()\n        @initialLoadComplete = true\n        @emitter.emit 'did-change-active-themes'\n        deferred.resolve()\n\n    deferred.promise\n\n  deactivateThemes: ->\n    @removeActiveThemeClasses()\n    @packageManager.deactivatePackage(pack.name) for pack in @getActiveThemes()\n    null\n\n  isInitialLoadComplete: -> @initialLoadComplete\n\n  addActiveThemeClasses: ->\n    for pack in @getActiveThemes()\n      document.body.classList.add(\"theme-#{pack.name}\")\n    return\n\n  removeActiveThemeClasses: ->\n    for pack in @getActiveThemes()\n      document.body.classList.remove(\"theme-#{pack.name}\")\n    return\n\n  refreshLessCache: ->\n    @lessCache?.setImportPaths(@getImportPaths())\n\n  getImportPaths: ->\n    activeThemes = @getActiveThemes()\n    if activeThemes.length > 0\n      themePaths = (theme.getStylesheetsPath() for theme in activeThemes when theme)\n    else\n      themePaths = []\n      for themeName in @getEnabledThemeNames()\n        if themePath = @packageManager.resolvePackagePath(themeName)\n          deprecatedPath = path.join(themePath, 'stylesheets')\n          if fs.isDirectorySync(deprecatedPath)\n            themePaths.push(deprecatedPath)\n          else\n            themePaths.push(path.join(themePath, 'styles'))\n\n    themePaths.filter (themePath) -> fs.isDirectorySync(themePath)\n"
  },
  {
    "path": "packages/client-app/src/theme-package.coffee",
    "content": "Q = require 'q'\nPackage = require './package'\n\nmodule.exports =\nclass ThemePackage extends Package\n  getType: -> 'theme'\n\n  getStyleSheetPriority: -> 1\n\n  enable: ->\n    NylasEnv.themes.setActiveTheme(@name)\n\n  disable: ->\n    NylasEnv.config.removeAtKeyPath('core.themes', @name)\n\n  load: ->\n    @measure 'loadTime', =>\n      try\n        @metadata ?= Package.loadMetadata(@path)\n      catch error\n        console.warn \"Failed to load theme named '#{@name}'\", error.stack ? error\n    this\n\n  activate: ->\n    return @activationDeferred.promise if @activationDeferred?\n\n    @activationDeferred = Q.defer()\n    @measure 'activateTime', =>\n      @loadStylesheets()\n      @activateNow()\n\n    @activationDeferred.promise\n"
  },
  {
    "path": "packages/client-app/src/undo-stack.es6",
    "content": "import _ from 'underscore';\n\nexport default class UndoStack {\n  constructor(options) {\n    this._options = options;\n    this._stack = []\n    this._redoStack = []\n    this._MAX_STACK_SIZE = 1000\n    this._accumulated = {};\n  }\n\n  current() {\n    return _.last(this._stack) || null;\n  }\n\n  undo() {\n    if (this._stack.length <= 1) { return null; }\n    const item = this._stack.pop();\n    this._redoStack.push(item);\n    return this.current();\n  }\n\n  redo() {\n    const item = this._redoStack.pop();\n    if (!item) { return null; }\n    this._stack.push(item);\n    return this.current();\n  }\n\n  accumulate = (state) => {\n    Object.assign(this._accumulated, state);\n    const shouldSnapshot = this._options.shouldSnapshot && this._options.shouldSnapshot(this.current(), this._accumulated);\n    if (!this.current() || shouldSnapshot) {\n      this.save(this._accumulated);\n      this._accumulated = {};\n    }\n  }\n\n  save = (historyItem) => {\n    if (_.isEqual(this.current(), historyItem)) {\n      return;\n    }\n\n    this._redoStack = [];\n    this._stack.push(historyItem);\n    while (this._stack.length > this._MAX_STACK_SIZE) {\n      this._stack.shift();\n    }\n  }\n\n  saveAndUndo = (currentItem) => {\n    const top = this._stack.length - 1;\n    const snapshot = this._stack[top];\n    if (!snapshot) {\n      return null;\n    }\n    this._stack[top] = currentItem;\n    this.undo();\n\n    return snapshot;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/src/virtual-dom-utils.es6",
    "content": "import _ from 'underscore'\nimport React from 'react'\n\nconst VirtualDOMUtils = {\n  * walk({element, parentNode, childOffset, pruneFn = () => {}}) {\n    yield {element, parentNode, childOffset};\n    if (React.isValidElement(element) && !pruneFn(element)) {\n      const children = element.props.children;\n      if (!children) {\n        return\n      } else if (_.isString(children)) {\n        yield {element: children, parentNode: element, childOffset: 0}\n      } else if (children.length > 0) {\n        for (let i = 0; i < children.length; i++) {\n          yield* this.walk({element: children[i], parentNode: element, childOffset: i, pruneFn})\n        }\n      } else {\n        yield* this.walk({element: children, parentNode: element, childOffset: 0, pruneFn})\n      }\n    } else if (_.isArray(element)) {\n      for (let i = 0; i < element.length; i++) {\n        yield* this.walk({element: element[i], parentNode: element, childOffset: i})\n      }\n    }\n    return\n  },\n}\nexport default VirtualDOMUtils\n"
  },
  {
    "path": "packages/client-app/src/window-bootstrap.es6",
    "content": "/* eslint import/first: 0 */\n\n// Swap out Node's native Promise for Bluebird, which allows us to\n// do fancy things like handle exceptions inside promise blocks\nglobal.Promise = require('bluebird');\nconst timeout = global.setTimeout;\nPromise.setScheduler((fn) => timeout(fn, 0));\n\n// Like sands through the hourglass, so are the days of our lives.\nimport './window';\n\nimport NylasEnvConstructor from './nylas-env';\nwindow.NylasEnv = NylasEnvConstructor.loadOrCreate();\nNylasEnv.initialize();\nNylasEnv.startRootWindow();\n\n\n// Workaround for focus getting cleared upon window creation\nconst windowFocused = () => {\n  window.removeEventListener('focus', windowFocused);\n  return setTimeout((() => {\n    const elt = document.getElementById('sheet-container');\n    if (elt) elt.focus();\n  }), 0);\n}\nwindow.addEventListener('focus', windowFocused);\n"
  },
  {
    "path": "packages/client-app/src/window-bridge.coffee",
    "content": "_ = require 'underscore'\n{ipcRenderer} = require 'electron'\nUtils = require './flux/models/utils'\n\nclass WindowBridge\n  constructor: ->\n    @_tasks = {}\n    ipcRenderer.on(\"remote-run-results\", @_onResults)\n    ipcRenderer.on(\"run-in-window\", @_onRunInWindow)\n\n  runInWindow: (window, objectName, methodName, args) ->\n    taskId = Utils.generateTempId()\n    new Promise (resolve, reject) =>\n      @_tasks[taskId] = {resolve, reject}\n      args = JSON.stringify(args, Utils.registeredObjectReplacer)\n      params = {window, objectName, methodName, args, taskId}\n      ipcRenderer.send(\"run-in-window\", params)\n\n  runInMainWindow: (args...) ->\n    @runInWindow(\"main\", args...)\n\n  runInWorkWindow: (args...) ->\n    @runInWindow(\"work\", args...)\n\n  _onResults: (event, {returnValue, taskId}={}) =>\n    returnValue = JSON.parse(returnValue, Utils.registeredObjectReviver)\n    @_tasks[taskId].resolve(returnValue)\n    delete @_tasks[taskId]\n\n  _onRunInWindow: (event, {objectName, methodName, args, taskId}={}) =>\n    args = JSON.parse(args, Utils.registeredObjectReviver)\n    exports = require 'nylas-exports'\n    result = exports[objectName][methodName].apply(null, args)\n    if _.isFunction(result.then)\n      result.then (returnValue) ->\n        returnValue = JSON.stringify(returnValue, Utils.registeredObjectReplacer)\n        ipcRenderer.send('remote-run-results', {returnValue, taskId})\n    else\n      returnValue = result\n      returnValue = JSON.stringify(returnValue, Utils.registeredObjectReplacer)\n      ipcRenderer.send('remote-run-results', {returnValue, taskId})\n\nmodule.exports = new WindowBridge\n"
  },
  {
    "path": "packages/client-app/src/window-event-handler.coffee",
    "content": "path = require 'path'\n_ = require 'underscore'\n{Disposable} = require 'event-kit'\n{shell, ipcRenderer, remote} = require 'electron'\nfs = require 'fs-plus'\nurl = require 'url'\n\n# Handles low-level events related to the window.\nmodule.exports =\nclass WindowEventHandler\n  constructor: ->\n    @unloadCallbacks = []\n\n    _.defer =>\n      @showDevModeMessages()\n\n    ipcRenderer.on 'browser-window-focus', ->\n      document.body.classList.remove('is-blurred')\n      window.dispatchEvent(new Event('browser-window-focus'))\n\n    ipcRenderer.on 'browser-window-blur', ->\n      document.body.classList.add('is-blurred')\n      window.dispatchEvent(new Event('browser-window-blur'))\n\n    ipcRenderer.on 'command', (event, command, args...) ->\n      NylasEnv.commands.dispatch(command, args[0])\n\n    ipcRenderer.on 'scroll-touch-begin', ->\n      window.dispatchEvent(new Event('scroll-touch-begin'))\n\n    ipcRenderer.on 'scroll-touch-end', ->\n      window.dispatchEvent(new Event('scroll-touch-end'))\n\n    window.onbeforeunload = =>\n      if NylasEnv.inSpecMode() then return undefined\n      # Don't hide the window here if we don't want the renderer process to be\n      # throttled in case more work needs to be done before closing\n\n      # In Electron, returning any value other than undefined cancels the close.\n      if @runUnloadCallbacks()\n        # Good to go! Window will be closing...\n        NylasEnv.storeWindowDimensions()\n        NylasEnv.saveStateAndUnloadWindow()\n        return undefined\n      return false\n\n    NylasEnv.commands.add document.body, 'window:toggle-full-screen', ->\n      NylasEnv.toggleFullScreen()\n\n    NylasEnv.commands.add document.body, 'window:close', ->\n      NylasEnv.close()\n\n    NylasEnv.commands.add document.body, 'window:reload', =>\n      NylasEnv.reload()\n\n    NylasEnv.commands.add document.body, 'window:toggle-dev-tools', ->\n      NylasEnv.toggleDevTools()\n\n    NylasEnv.commands.add document.body, 'window:open-errorlogger-logs', ->\n      NylasEnv.errorLogger.openLogs()\n\n    NylasEnv.commands.add document.body, 'window:toggle-component-regions', ->\n      ComponentRegistry = require './registries/component-registry'\n      ComponentRegistry.toggleComponentRegions()\n\n    webContents = NylasEnv.getCurrentWindow().webContents\n    NylasEnv.commands.add(document.body, 'core:copy', => webContents.copy())\n    NylasEnv.commands.add(document.body, 'core:cut', => webContents.cut())\n    NylasEnv.commands.add(document.body, 'core:paste', => webContents.paste())\n    NylasEnv.commands.add(document.body, 'core:paste-and-match-style', => webContents.pasteAndMatchStyle())\n    NylasEnv.commands.add(document.body, 'core:undo', => webContents.undo())\n    NylasEnv.commands.add(document.body, 'core:redo', => webContents.redo())\n    NylasEnv.commands.add(document.body, 'core:select-all', => webContents.selectAll())\n\n    # \"Pinch to zoom\" on the Mac gets translated by the system into a\n    # \"scroll with ctrl key down\". To prevent the page from zooming in,\n    # prevent default when the ctrlKey is detected.\n    document.addEventListener 'mousewheel', ->\n      if event.ctrlKey\n        event.preventDefault()\n\n    document.addEventListener 'drop', @onDrop\n\n    document.addEventListener 'dragover', @onDragOver\n\n    document.addEventListener 'click', (event) =>\n      if event.target.closest('[href]')\n        @openLink(event)\n\n    document.addEventListener 'contextmenu', (event) =>\n      if event.target.nodeName is 'INPUT'\n        @openContextualMenuForInput(event)\n\n    # Prevent form submits from changing the current window's URL\n    document.addEventListener 'submit', (event) =>\n      if event.target.nodeName is 'FORM'\n        event.preventDefault()\n        @openContextualMenuForInput(event)\n\n  addUnloadCallback: (callback) ->\n    @unloadCallbacks.push(callback)\n\n  removeUnloadCallback: (callback) ->\n    @unloadCallbacks = @unloadCallbacks.filter (cb) -> cb isnt callback\n\n  runUnloadCallbacks: ->\n    hasReturned = false\n\n    unloadCallbacksRunning = 0\n    unloadCallbackComplete = =>\n      unloadCallbacksRunning -= 1\n      if unloadCallbacksRunning is 0 and hasReturned\n        @runUnloadFinished()\n\n    for callback in @unloadCallbacks\n      returnValue = callback(unloadCallbackComplete)\n      if returnValue is false\n        unloadCallbacksRunning += 1\n      else if returnValue isnt true\n        console.warn \"You registered an `onBeforeUnload` callback that does not return either exactly `true` or `false`. It returned #{returnValue}\", callback\n\n    # In Electron, returning false cancels the close.\n    hasReturned = true\n    return (unloadCallbacksRunning is 0)\n\n  runUnloadFinished: ->\n    {remote} = require('electron')\n    _.defer ->\n      if remote.getGlobal('application').isQuitting()\n        remote.app.quit()\n      else if NylasEnv.isReloading\n        NylasEnv.isReloading = false\n        NylasEnv.reload()\n      else\n        NylasEnv.close()\n\n  # Important: even though we don't do anything here, we need to catch the\n  # drop event to prevent the browser from navigating the to the \"url\" of the\n  # file and completely leaving the app.\n  onDrop: (event) ->\n    event.preventDefault()\n    event.stopPropagation()\n\n  onDragOver: (event) ->\n    event.preventDefault()\n    event.stopPropagation()\n\n  resolveHref: (el) ->\n    return null unless el\n    closestHrefEl = el.closest('[href]')\n    return closestHrefEl.getAttribute('href') if closestHrefEl\n    return null\n\n  openLink: ({href, target, currentTarget, metaKey}) ->\n    if not href\n      href = @resolveHref(target || currentTarget)\n    return unless href\n\n    return if target?.closest('.no-open-link-events')\n\n    {protocol} = url.parse(href)\n    return unless protocol\n\n    if protocol in ['mailto:', 'nylas:']\n      # We sometimes get mailto URIs that are not escaped properly, or have been only partially escaped.\n      # (T1927) Be sure to escape them once, and completely, before we try to open them. This logic\n      # *might* apply to http/https as well but it's unclear.\n      href = encodeURI(decodeURI(href))\n      remote.getGlobal('application').openUrl(href)\n    else if protocol in ['http:', 'https:', 'tel:']\n      shell.openExternal(href, activate: !metaKey)\n\n    return\n\n  openContextualMenuForInput: (event) ->\n    event.preventDefault()\n\n    return unless event.target.type in ['text', 'password', 'email', 'number', 'range', 'search', 'tel', 'url']\n    hasSelectedText = event.target.selectionStart isnt event.target.selectionEnd\n\n    if hasSelectedText\n      wordStart = event.target.selectionStart\n      wordEnd = event.target.selectionEnd\n    else\n      wordStart = event.target.value.lastIndexOf(\" \", event.target.selectionStart)\n      wordStart = 0 if wordStart is -1\n      wordEnd = event.target.value.indexOf(\" \", event.target.selectionStart)\n      wordEnd = event.target.value.length if wordEnd is -1\n    word = event.target.value.substr(wordStart, wordEnd - wordStart)\n\n    {remote} = require('electron')\n    {Menu, MenuItem} = remote\n    menu = new Menu()\n\n    Spellchecker = require('./spellchecker').default\n    Spellchecker.appendSpellingItemsToMenu\n      menu: menu,\n      word: word,\n      onCorrect: (correction) =>\n        insertionPoint = wordStart + correction.length\n        event.target.value = event.target.value.replace(word, correction)\n        event.target.setSelectionRange(insertionPoint, insertionPoint)\n\n    menu.append(new MenuItem({\n      label: 'Cut'\n      enabled: hasSelectedText\n      click: => document.execCommand('cut')\n    }))\n    menu.append(new MenuItem({\n      label: 'Copy'\n      enabled: hasSelectedText\n      click: => document.execCommand('copy')\n    }))\n    menu.append(new MenuItem({\n      label: 'Paste',\n      click: => document.execCommand('paste')\n    }))\n    menu.popup(remote.getCurrentWindow())\n\n  showDevModeMessages: ->\n    return unless NylasEnv.isMainWindow()\n\n    if !NylasEnv.inDevMode()\n      console.log(\"%c Welcome to Nylas Mail! If you're exploring the source or building a\n                   plugin, you should enable debug flags. It's slower, but\n                   gives you better exceptions, the debug version of React,\n                   and more. Choose %c Developer > Run with Debug Flags %c\n                   from the menu. Also, check out https://nylas.github.io/N1/docs/\n                   for documentation and sample code!\",\n                   \"background-color: antiquewhite;\",\n                   \"background-color: antiquewhite; font-weight:bold;\",\n                   \"background-color: antiquewhite; font-weight:normal;\")\n"
  },
  {
    "path": "packages/client-app/src/window.coffee",
    "content": "# Public: Measure how long a function takes to run.\n#\n# description - A {String} description that will be logged to the console when\n#               the function completes.\n# fn - A {Function} to measure the duration of.\n#\n# Returns the value returned by the given function.\nwindow.measure = (description, fn) ->\n  start = Date.now()\n  value = fn()\n  result = Date.now() - start\n  console.log description, result\n  value\n\n# Public: Create a dev tools profile for a function.\n#\n# description - A {String} description that will be available in the Profiles\n#               tab of the dev tools.\n# fn - A {Function} to profile.\n#\n# Returns the value returned by the given function.\nwindow.profile = (description, fn) ->\n  measure description, ->\n    console.profile(description)\n    value = fn()\n    console.profileEnd(description)\n    value\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/airstrip/airstrip.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1,IE=edge\" />\n\t<title>Airstrip</title>\n\t<style>\n\t\thtml {\n\t\t\theight:100%;\n\t\t}\n\t\tbody {\n\t\t\tmargin:0;\n\t\t\theight:100%;\n\t\t}\n\t</style>\n\t<!-- copy these lines to your document head: -->\n\n\t<meta name=\"viewport\" content=\"user-scalable=yes, width=600\" />\n\n\t<!-- end copy -->\n  </head>\n  <body>\n\t<!-- copy these lines to your document: -->\n\n\t<div id=\"airstrip_hype_container\" style=\"margin:auto;position:relative;width:600px;height:500px;overflow:hidden;\" aria-live=\"polite\">\n\t\t<script type=\"text/javascript\" charset=\"utf-8\" src=\"airstrip.hyperesources/airstrip_hype_generated_script.js?2725\"></script>\n\t</div>\n\n\t<!-- end copy -->\n\n\n\n\t<!-- text content for search engines: -->\n\n\t<div style=\"display:none\">\n\n\t\t<div></div>\n\n\t</div>\n\n\t<!-- end text content: -->\n\n  </body>\n</html>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/airstrip/airstrip.hyperesources/PIE.htc",
    "content": "<!--\nPIE: CSS3 rendering for IE\nVersion 1.0.0\nhttp://css3pie.com\nDual-licensed for use under the Apache License Version 2.0 or the General Public License (GPL) Version 2.\n-->\n<PUBLIC:COMPONENT lightWeight=\"true\">\n<!-- saved from url=(0014)about:internet -->\n<PUBLIC:ATTACH EVENT=\"oncontentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondocumentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondetach\" FOR=\"element\" ONEVENT=\"cleanup()\" />\n\n<script type=\"text/javascript\">\nvar doc = element.document;var f=window.PIE;\nif(!f){f=window.PIE={F:\"-pie-\",nb:\"Pie\",La:\"pie_\",Ac:{TD:1,TH:1},cc:{TABLE:1,THEAD:1,TBODY:1,TFOOT:1,TR:1,INPUT:1,TEXTAREA:1,SELECT:1,OPTION:1,IMG:1,HR:1},fc:{A:1,INPUT:1,TEXTAREA:1,SELECT:1,BUTTON:1},Gd:{submit:1,button:1,reset:1},aa:function(){}};try{doc.execCommand(\"BackgroundImageCache\",false,true)}catch(aa){}for(var ba=4,Z=doc.createElement(\"div\"),ca=Z.getElementsByTagName(\"i\"),ga;Z.innerHTML=\"<!--[if gt IE \"+ ++ba+\"]><i></i><![endif]--\\>\",ca[0];);f.O=ba;if(ba===6)f.F=f.F.replace(/^-/,\"\");f.ja=\ndoc.documentMode||f.O;Z.innerHTML='<v:shape adj=\"1\"/>';ga=Z.firstChild;ga.style.behavior=\"url(#default#VML)\";f.zc=typeof ga.adj===\"object\";(function(){var a,b=0,c={};f.p={Za:function(d){if(!a){a=doc.createDocumentFragment();a.namespaces.add(\"css3vml\",\"urn:schemas-microsoft-com:vml\")}return a.createElement(\"css3vml:\"+d)},Ba:function(d){return d&&d._pieId||(d._pieId=\"_\"+ ++b)},Eb:function(d){var e,g,j,i,h=arguments;e=1;for(g=h.length;e<g;e++){i=h[e];for(j in i)if(i.hasOwnProperty(j))d[j]=i[j]}return d},\nRb:function(d,e,g){var j=c[d],i,h;if(j)Object.prototype.toString.call(j)===\"[object Array]\"?j.push([e,g]):e.call(g,j);else{h=c[d]=[[e,g]];i=new Image;i.onload=function(){j=c[d]={h:i.width,f:i.height};for(var k=0,n=h.length;k<n;k++)h[k][0].call(h[k][1],j);i.onload=null};i.src=d}}}})();f.Na={gc:function(a,b,c,d){function e(){k=j>=90&&j<270?b:0;n=j<180?c:0;m=b-k;p=c-n}function g(){for(;j<0;)j+=360;j%=360}var j=d.sa;d=d.zb;var i,h,k,n,m,p,r,t;if(d){d=d.coords(a,b,c);i=d.x;h=d.y}if(j){j=j.jd();g();e();\nif(!d){i=k;h=n}d=f.Na.tc(i,h,j,m,p);a=d[0];d=d[1]}else if(d){a=b-i;d=c-h}else{i=h=a=0;d=c}r=a-i;t=d-h;if(j===void 0){j=!r?t<0?90:270:!t?r<0?180:0:-Math.atan2(t,r)/Math.PI*180;g();e()}return{sa:j,xc:i,yc:h,td:a,ud:d,Wd:k,Xd:n,rd:m,sd:p,kd:r,ld:t,rc:f.Na.dc(i,h,a,d)}},tc:function(a,b,c,d,e){if(c===0||c===180)return[d,b];else if(c===90||c===270)return[a,e];else{c=Math.tan(-c*Math.PI/180);a=c*a-b;b=-1/c;d=b*d-e;e=b-c;return[(d-a)/e,(c*d-b*a)/e]}},dc:function(a,b,c,d){a=c-a;b=d-b;return Math.abs(a===0?\nb:b===0?a:Math.sqrt(a*a+b*b))}};f.ea=function(){this.Gb=[];this.oc={}};f.ea.prototype={ba:function(a){var b=f.p.Ba(a),c=this.oc,d=this.Gb;if(!(b in c)){c[b]=d.length;d.push(a)}},Ha:function(a){a=f.p.Ba(a);var b=this.oc;if(a&&a in b){delete this.Gb[b[a]];delete b[a]}},xa:function(){for(var a=this.Gb,b=a.length;b--;)a[b]&&a[b]()}};f.Oa=new f.ea;f.Oa.Rd=function(){var a=this,b;if(!a.Sd){b=doc.documentElement.currentStyle.getAttribute(f.F+\"poll-interval\")||250;(function c(){a.xa();setTimeout(c,b)})();\na.Sd=1}};(function(){function a(){f.L.xa();window.detachEvent(\"onunload\",a);window.PIE=null}f.L=new f.ea;window.attachEvent(\"onunload\",a);f.L.ta=function(b,c,d){b.attachEvent(c,d);this.ba(function(){b.detachEvent(c,d)})}})();f.Qa=new f.ea;f.L.ta(window,\"onresize\",function(){f.Qa.xa()});(function(){function a(){f.mb.xa()}f.mb=new f.ea;f.L.ta(window,\"onscroll\",a);f.Qa.ba(a)})();(function(){function a(){c=f.kb.md()}function b(){if(c){for(var d=0,e=c.length;d<e;d++)f.attach(c[d]);c=0}}var c;if(f.ja<9){f.L.ta(window,\n\"onbeforeprint\",a);f.L.ta(window,\"onafterprint\",b)}})();f.lb=new f.ea;f.L.ta(doc,\"onmouseup\",function(){f.lb.xa()});f.he=function(){function a(h){this.Y=h}var b=doc.createElement(\"length-calc\"),c=doc.body||doc.documentElement,d=b.style,e={},g=[\"mm\",\"cm\",\"in\",\"pt\",\"pc\"],j=g.length,i={};d.position=\"absolute\";d.top=d.left=\"-9999px\";for(c.appendChild(b);j--;){d.width=\"100\"+g[j];e[g[j]]=b.offsetWidth/100}c.removeChild(b);d.width=\"1em\";a.prototype={Kb:/(px|em|ex|mm|cm|in|pt|pc|%)$/,ic:function(){var h=\nthis.Jd;if(h===void 0)h=this.Jd=parseFloat(this.Y);return h},yb:function(){var h=this.ae;if(!h)h=this.ae=(h=this.Y.match(this.Kb))&&h[0]||\"px\";return h},a:function(h,k){var n=this.ic(),m=this.yb();switch(m){case \"px\":return n;case \"%\":return n*(typeof k===\"function\"?k():k)/100;case \"em\":return n*this.xb(h);case \"ex\":return n*this.xb(h)/2;default:return n*e[m]}},xb:function(h){var k=h.currentStyle.fontSize,n,m;if(k.indexOf(\"px\")>0)return parseFloat(k);else if(h.tagName in f.cc){m=this;n=h.parentNode;\nreturn f.n(k).a(n,function(){return m.xb(n)})}else{h.appendChild(b);k=b.offsetWidth;b.parentNode===h&&h.removeChild(b);return k}}};f.n=function(h){return i[h]||(i[h]=new a(h))};return a}();f.Ja=function(){function a(e){this.X=e}var b=f.n(\"50%\"),c={top:1,center:1,bottom:1},d={left:1,center:1,right:1};a.prototype={zd:function(){if(!this.ac){var e=this.X,g=e.length,j=f.v,i=j.qa,h=f.n(\"0\");i=i.na;h=[\"left\",h,\"top\",h];if(g===1){e.push(new j.ob(i,\"center\"));g++}if(g===2){i&(e[0].k|e[1].k)&&e[0].d in c&&\ne[1].d in d&&e.push(e.shift());if(e[0].k&i)if(e[0].d===\"center\")h[1]=b;else h[0]=e[0].d;else if(e[0].W())h[1]=f.n(e[0].d);if(e[1].k&i)if(e[1].d===\"center\")h[3]=b;else h[2]=e[1].d;else if(e[1].W())h[3]=f.n(e[1].d)}this.ac=h}return this.ac},coords:function(e,g,j){var i=this.zd(),h=i[1].a(e,g);e=i[3].a(e,j);return{x:i[0]===\"right\"?g-h:h,y:i[2]===\"bottom\"?j-e:e}}};return a}();f.Ka=function(){function a(b,c){this.h=b;this.f=c}a.prototype={a:function(b,c,d,e,g){var j=this.h,i=this.f,h=c/d;e=e/g;if(j===\n\"contain\"){j=e>h?c:d*e;i=e>h?c/e:d}else if(j===\"cover\"){j=e<h?c:d*e;i=e<h?c/e:d}else if(j===\"auto\"){i=i===\"auto\"?g:i.a(b,d);j=i*e}else{j=j.a(b,c);i=i===\"auto\"?j/e:i.a(b,d)}return{h:j,f:i}}};a.Kc=new a(\"auto\",\"auto\");return a}();f.Ec=function(){function a(b){this.Y=b}a.prototype={Kb:/[a-z]+$/i,yb:function(){return this.ad||(this.ad=this.Y.match(this.Kb)[0].toLowerCase())},jd:function(){var b=this.Vc,c;if(b===undefined){b=this.yb();c=parseFloat(this.Y,10);b=this.Vc=b===\"deg\"?c:b===\"rad\"?c/Math.PI*180:\nb===\"grad\"?c/400*360:b===\"turn\"?c*360:0}return b}};return a}();f.Jc=function(){function a(c){this.Y=c}var b={};a.Qd=/\\s*rgba\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d+|\\d*\\.\\d+)\\s*\\)\\s*/;a.Fb={aliceblue:\"F0F8FF\",antiquewhite:\"FAEBD7\",aqua:\"0FF\",aquamarine:\"7FFFD4\",azure:\"F0FFFF\",beige:\"F5F5DC\",bisque:\"FFE4C4\",black:\"000\",blanchedalmond:\"FFEBCD\",blue:\"00F\",blueviolet:\"8A2BE2\",brown:\"A52A2A\",burlywood:\"DEB887\",cadetblue:\"5F9EA0\",chartreuse:\"7FFF00\",chocolate:\"D2691E\",coral:\"FF7F50\",cornflowerblue:\"6495ED\",\ncornsilk:\"FFF8DC\",crimson:\"DC143C\",cyan:\"0FF\",darkblue:\"00008B\",darkcyan:\"008B8B\",darkgoldenrod:\"B8860B\",darkgray:\"A9A9A9\",darkgreen:\"006400\",darkkhaki:\"BDB76B\",darkmagenta:\"8B008B\",darkolivegreen:\"556B2F\",darkorange:\"FF8C00\",darkorchid:\"9932CC\",darkred:\"8B0000\",darksalmon:\"E9967A\",darkseagreen:\"8FBC8F\",darkslateblue:\"483D8B\",darkslategray:\"2F4F4F\",darkturquoise:\"00CED1\",darkviolet:\"9400D3\",deeppink:\"FF1493\",deepskyblue:\"00BFFF\",dimgray:\"696969\",dodgerblue:\"1E90FF\",firebrick:\"B22222\",floralwhite:\"FFFAF0\",\nforestgreen:\"228B22\",fuchsia:\"F0F\",gainsboro:\"DCDCDC\",ghostwhite:\"F8F8FF\",gold:\"FFD700\",goldenrod:\"DAA520\",gray:\"808080\",green:\"008000\",greenyellow:\"ADFF2F\",honeydew:\"F0FFF0\",hotpink:\"FF69B4\",indianred:\"CD5C5C\",indigo:\"4B0082\",ivory:\"FFFFF0\",khaki:\"F0E68C\",lavender:\"E6E6FA\",lavenderblush:\"FFF0F5\",lawngreen:\"7CFC00\",lemonchiffon:\"FFFACD\",lightblue:\"ADD8E6\",lightcoral:\"F08080\",lightcyan:\"E0FFFF\",lightgoldenrodyellow:\"FAFAD2\",lightgreen:\"90EE90\",lightgrey:\"D3D3D3\",lightpink:\"FFB6C1\",lightsalmon:\"FFA07A\",\nlightseagreen:\"20B2AA\",lightskyblue:\"87CEFA\",lightslategray:\"789\",lightsteelblue:\"B0C4DE\",lightyellow:\"FFFFE0\",lime:\"0F0\",limegreen:\"32CD32\",linen:\"FAF0E6\",magenta:\"F0F\",maroon:\"800000\",mediumauqamarine:\"66CDAA\",mediumblue:\"0000CD\",mediumorchid:\"BA55D3\",mediumpurple:\"9370D8\",mediumseagreen:\"3CB371\",mediumslateblue:\"7B68EE\",mediumspringgreen:\"00FA9A\",mediumturquoise:\"48D1CC\",mediumvioletred:\"C71585\",midnightblue:\"191970\",mintcream:\"F5FFFA\",mistyrose:\"FFE4E1\",moccasin:\"FFE4B5\",navajowhite:\"FFDEAD\",\nnavy:\"000080\",oldlace:\"FDF5E6\",olive:\"808000\",olivedrab:\"688E23\",orange:\"FFA500\",orangered:\"FF4500\",orchid:\"DA70D6\",palegoldenrod:\"EEE8AA\",palegreen:\"98FB98\",paleturquoise:\"AFEEEE\",palevioletred:\"D87093\",papayawhip:\"FFEFD5\",peachpuff:\"FFDAB9\",peru:\"CD853F\",pink:\"FFC0CB\",plum:\"DDA0DD\",powderblue:\"B0E0E6\",purple:\"800080\",red:\"F00\",rosybrown:\"BC8F8F\",royalblue:\"4169E1\",saddlebrown:\"8B4513\",salmon:\"FA8072\",sandybrown:\"F4A460\",seagreen:\"2E8B57\",seashell:\"FFF5EE\",sienna:\"A0522D\",silver:\"C0C0C0\",skyblue:\"87CEEB\",\nslateblue:\"6A5ACD\",slategray:\"708090\",snow:\"FFFAFA\",springgreen:\"00FF7F\",steelblue:\"4682B4\",tan:\"D2B48C\",teal:\"008080\",thistle:\"D8BFD8\",tomato:\"FF6347\",turquoise:\"40E0D0\",violet:\"EE82EE\",wheat:\"F5DEB3\",white:\"FFF\",whitesmoke:\"F5F5F5\",yellow:\"FF0\",yellowgreen:\"9ACD32\"};a.prototype={parse:function(){if(!this.Ua){var c=this.Y,d;if(d=c.match(a.Qd)){this.Ua=\"rgb(\"+d[1]+\",\"+d[2]+\",\"+d[3]+\")\";this.Yb=parseFloat(d[4])}else{if((d=c.toLowerCase())in a.Fb)c=\"#\"+a.Fb[d];this.Ua=c;this.Yb=c===\"transparent\"?0:\n1}}},U:function(c){this.parse();return this.Ua===\"currentColor\"?c.currentStyle.color:this.Ua},fa:function(){this.parse();return this.Yb}};f.ha=function(c){return b[c]||(b[c]=new a(c))};return a}();f.v=function(){function a(c){this.$a=c;this.ch=0;this.X=[];this.Ga=0}var b=a.qa={Ia:1,Wb:2,z:4,Lc:8,Xb:16,na:32,K:64,oa:128,pa:256,Ra:512,Tc:1024,URL:2048};a.ob=function(c,d){this.k=c;this.d=d};a.ob.prototype={Ca:function(){return this.k&b.K||this.k&b.oa&&this.d===\"0\"},W:function(){return this.Ca()||this.k&\nb.Ra}};a.prototype={de:/\\s/,Kd:/^[\\+\\-]?(\\d*\\.)?\\d+/,url:/^url\\(\\s*(\"([^\"]*)\"|'([^']*)'|([!#$%&*-~]*))\\s*\\)/i,nc:/^\\-?[_a-z][\\w-]*/i,Yd:/^(\"([^\"]*)\"|'([^']*)')/,Bd:/^#([\\da-f]{6}|[\\da-f]{3})/i,be:{px:b.K,em:b.K,ex:b.K,mm:b.K,cm:b.K,\"in\":b.K,pt:b.K,pc:b.K,deg:b.Ia,rad:b.Ia,grad:b.Ia},fd:{rgb:1,rgba:1,hsl:1,hsla:1},next:function(c){function d(p,r){p=new a.ob(p,r);if(!c){k.X.push(p);k.Ga++}return p}function e(){k.Ga++;return null}var g,j,i,h,k=this;if(this.Ga<this.X.length)return this.X[this.Ga++];for(;this.de.test(this.$a.charAt(this.ch));)this.ch++;\nif(this.ch>=this.$a.length)return e();j=this.ch;g=this.$a.substring(this.ch);i=g.charAt(0);switch(i){case \"#\":if(h=g.match(this.Bd)){this.ch+=h[0].length;return d(b.z,h[0])}break;case '\"':case \"'\":if(h=g.match(this.Yd)){this.ch+=h[0].length;return d(b.Tc,h[2]||h[3]||\"\")}break;case \"/\":case \",\":this.ch++;return d(b.pa,i);case \"u\":if(h=g.match(this.url)){this.ch+=h[0].length;return d(b.URL,h[2]||h[3]||h[4]||\"\")}}if(h=g.match(this.Kd)){i=h[0];this.ch+=i.length;if(g.charAt(i.length)===\"%\"){this.ch++;\nreturn d(b.Ra,i+\"%\")}if(h=g.substring(i.length).match(this.nc)){i+=h[0];this.ch+=h[0].length;return d(this.be[h[0].toLowerCase()]||b.Lc,i)}return d(b.oa,i)}if(h=g.match(this.nc)){i=h[0];this.ch+=i.length;if(i.toLowerCase()in f.Jc.Fb||i===\"currentColor\"||i===\"transparent\")return d(b.z,i);if(g.charAt(i.length)===\"(\"){this.ch++;if(i.toLowerCase()in this.fd){g=function(p){return p&&p.k&b.oa};h=function(p){return p&&p.k&(b.oa|b.Ra)};var n=function(p,r){return p&&p.d===r},m=function(){return k.next(1)};\nif((i.charAt(0)===\"r\"?h(m()):g(m()))&&n(m(),\",\")&&h(m())&&n(m(),\",\")&&h(m())&&(i===\"rgb\"||i===\"hsa\"||n(m(),\",\")&&g(m()))&&n(m(),\")\"))return d(b.z,this.$a.substring(j,this.ch));return e()}return d(b.Xb,i)}return d(b.na,i)}this.ch++;return d(b.Wb,i)},D:function(){return this.X[this.Ga-- -2]},all:function(){for(;this.next(););return this.X},ma:function(c,d){for(var e=[],g,j;g=this.next();){if(c(g)){j=true;this.D();break}e.push(g)}return d&&!j?null:e}};return a}();var ha=function(a){this.e=a};ha.prototype=\n{Z:0,Od:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.x!==b.x||a.y!==b.y)},Td:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.h!==b.h||a.f!==b.f)},hc:function(){var a=this.e,b=a.getBoundingClientRect(),c=f.ja===9,d=f.O===7,e=b.right-b.left;return{x:b.left,y:b.top,h:c||d?a.offsetWidth:e,f:c||d?a.offsetHeight:b.bottom-b.top,Hd:d&&e?a.offsetWidth/e:1}},o:function(){return this.Z?this.Va||(this.Va=this.hc()):this.hc()},Ad:function(){return!!this.qb},cb:function(){++this.Z},hb:function(){if(!--this.Z){if(this.Va)this.qb=\nthis.Va;this.Va=null}}};(function(){function a(b){var c=f.p.Ba(b);return function(){if(this.Z){var d=this.$b||(this.$b={});return c in d?d[c]:(d[c]=b.call(this))}else return b.call(this)}}f.B={Z:0,ka:function(b){function c(d){this.e=d;this.Zb=this.ia()}f.p.Eb(c.prototype,f.B,b);c.$c={};return c},j:function(){var b=this.ia(),c=this.constructor.$c;return b?b in c?c[b]:(c[b]=this.la(b)):null},ia:a(function(){var b=this.e,c=this.constructor,d=b.style;b=b.currentStyle;var e=this.wa,g=this.Fa,j=c.Yc||(c.Yc=\nf.F+e);c=c.Zc||(c.Zc=f.nb+g.charAt(0).toUpperCase()+g.substring(1));return d[c]||b.getAttribute(j)||d[g]||b.getAttribute(e)}),i:a(function(){return!!this.j()}),H:a(function(){var b=this.ia(),c=b!==this.Zb;this.Zb=b;return c}),va:a,cb:function(){++this.Z},hb:function(){--this.Z||delete this.$b}}})();f.Sb=f.B.ka({wa:f.F+\"background\",Fa:f.nb+\"Background\",cd:{scroll:1,fixed:1,local:1},fb:{\"repeat-x\":1,\"repeat-y\":1,repeat:1,\"no-repeat\":1},sc:{\"padding-box\":1,\"border-box\":1,\"content-box\":1},Pd:{top:1,right:1,\nbottom:1,left:1,center:1},Ud:{contain:1,cover:1},eb:{Ma:\"backgroundClip\",z:\"backgroundColor\",da:\"backgroundImage\",Pa:\"backgroundOrigin\",S:\"backgroundPosition\",T:\"backgroundRepeat\",Sa:\"backgroundSize\"},la:function(a){function b(s){return s&&s.W()||s.k&k&&s.d in t}function c(s){return s&&(s.W()&&f.n(s.d)||s.d===\"auto\"&&\"auto\")}var d=this.e.currentStyle,e,g,j,i=f.v.qa,h=i.pa,k=i.na,n=i.z,m,p,r=0,t=this.Pd,v,l,q={M:[]};if(this.wb()){e=new f.v(a);for(j={};g=e.next();){m=g.k;p=g.d;if(!j.P&&m&i.Xb&&p===\n\"linear-gradient\"){v={ca:[],P:p};for(l={};g=e.next();){m=g.k;p=g.d;if(m&i.Wb&&p===\")\"){l.color&&v.ca.push(l);v.ca.length>1&&f.p.Eb(j,v);break}if(m&n){if(v.sa||v.zb){g=e.D();if(g.k!==h)break;e.next()}l={color:f.ha(p)};g=e.next();if(g.W())l.db=f.n(g.d);else e.D()}else if(m&i.Ia&&!v.sa&&!l.color&&!v.ca.length)v.sa=new f.Ec(g.d);else if(b(g)&&!v.zb&&!l.color&&!v.ca.length){e.D();v.zb=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&h&&p===\",\"){if(l.color){v.ca.push(l);l={}}}else break}}else if(!j.P&&\nm&i.URL){j.Ab=p;j.P=\"image\"}else if(b(g)&&!j.$){e.D();j.$=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&k)if(p in this.fb&&!j.bb)j.bb=p;else if(p in this.sc&&!j.Wa){j.Wa=p;if((g=e.next())&&g.k&k&&g.d in this.sc)j.ub=g.d;else{j.ub=p;e.D()}}else if(p in this.cd&&!j.bc)j.bc=p;else return null;else if(m&n&&!q.color)q.color=f.ha(p);else if(m&h&&p===\"/\"&&!j.Xa&&j.$){g=e.next();if(g.k&k&&g.d in this.Ud)j.Xa=new f.Ka(g.d);else if(g=c(g)){m=c(e.next());if(!m){m=g;e.D()}j.Xa=new f.Ka(g,m)}else return null}else if(m&\nh&&p===\",\"&&j.P){j.Hb=a.substring(r,e.ch-1);r=e.ch;q.M.push(j);j={}}else return null}if(j.P){j.Hb=a.substring(r);q.M.push(j)}}else this.Bc(f.ja<9?function(){var s=this.eb,o=d[s.S+\"X\"],u=d[s.S+\"Y\"],x=d[s.da],y=d[s.z];if(y!==\"transparent\")q.color=f.ha(y);if(x!==\"none\")q.M=[{P:\"image\",Ab:(new f.v(x)).next().d,bb:d[s.T],$:new f.Ja((new f.v(o+\" \"+u)).all())}]}:function(){var s=this.eb,o=/\\s*,\\s*/,u=d[s.da].split(o),x=d[s.z],y,z,B,E,D,C;if(x!==\"transparent\")q.color=f.ha(x);if((E=u.length)&&u[0]!==\"none\"){x=\nd[s.T].split(o);y=d[s.S].split(o);z=d[s.Pa].split(o);B=d[s.Ma].split(o);s=d[s.Sa].split(o);q.M=[];for(o=0;o<E;o++)if((D=u[o])&&D!==\"none\"){C=s[o].split(\" \");q.M.push({Hb:D+\" \"+x[o]+\" \"+y[o]+\" / \"+s[o]+\" \"+z[o]+\" \"+B[o],P:\"image\",Ab:(new f.v(D)).next().d,bb:x[o],$:new f.Ja((new f.v(y[o])).all()),Wa:z[o],ub:B[o],Xa:new f.Ka(C[0],C[1])})}}});return q.color||q.M[0]?q:null},Bc:function(a){var b=f.ja>8,c=this.eb,d=this.e.runtimeStyle,e=d[c.da],g=d[c.z],j=d[c.T],i,h,k,n;if(e)d[c.da]=\"\";if(g)d[c.z]=\"\";if(j)d[c.T]=\n\"\";if(b){i=d[c.Ma];h=d[c.Pa];n=d[c.S];k=d[c.Sa];if(i)d[c.Ma]=\"\";if(h)d[c.Pa]=\"\";if(n)d[c.S]=\"\";if(k)d[c.Sa]=\"\"}a=a.call(this);if(e)d[c.da]=e;if(g)d[c.z]=g;if(j)d[c.T]=j;if(b){if(i)d[c.Ma]=i;if(h)d[c.Pa]=h;if(n)d[c.S]=n;if(k)d[c.Sa]=k}return a},ia:f.B.va(function(){return this.wb()||this.Bc(function(){var a=this.e.currentStyle,b=this.eb;return a[b.z]+\" \"+a[b.da]+\" \"+a[b.T]+\" \"+a[b.S+\"X\"]+\" \"+a[b.S+\"Y\"]})}),wb:f.B.va(function(){var a=this.e;return a.style[this.Fa]||a.currentStyle.getAttribute(this.wa)}),\nqc:function(){var a=0;if(f.O<7){a=this.e;a=\"\"+(a.style[f.nb+\"PngFix\"]||a.currentStyle.getAttribute(f.F+\"png-fix\"))===\"true\"}return a},i:f.B.va(function(){return(this.wb()||this.qc())&&!!this.j()})});f.Vb=f.B.ka({wc:[\"Top\",\"Right\",\"Bottom\",\"Left\"],Id:{thin:\"1px\",medium:\"3px\",thick:\"5px\"},la:function(){var a={},b={},c={},d=false,e=true,g=true,j=true;this.Cc(function(){for(var i=this.e.currentStyle,h=0,k,n,m,p,r,t,v;h<4;h++){m=this.wc[h];v=m.charAt(0).toLowerCase();k=b[v]=i[\"border\"+m+\"Style\"];n=i[\"border\"+\nm+\"Color\"];m=i[\"border\"+m+\"Width\"];if(h>0){if(k!==p)g=false;if(n!==r)e=false;if(m!==t)j=false}p=k;r=n;t=m;c[v]=f.ha(n);m=a[v]=f.n(b[v]===\"none\"?\"0\":this.Id[m]||m);if(m.a(this.e)>0)d=true}});return d?{J:a,Zd:b,gd:c,ee:j,hd:e,$d:g}:null},ia:f.B.va(function(){var a=this.e,b=a.currentStyle,c;a.tagName in f.Ac&&a.offsetParent.currentStyle.borderCollapse===\"collapse\"||this.Cc(function(){c=b.borderWidth+\"|\"+b.borderStyle+\"|\"+b.borderColor});return c}),Cc:function(a){var b=this.e.runtimeStyle,c=b.borderWidth,\nd=b.borderColor;if(c)b.borderWidth=\"\";if(d)b.borderColor=\"\";a=a.call(this);if(c)b.borderWidth=c;if(d)b.borderColor=d;return a}});(function(){f.jb=f.B.ka({wa:\"border-radius\",Fa:\"borderRadius\",la:function(b){var c=null,d,e,g,j,i=false;if(b){e=new f.v(b);var h=function(){for(var k=[],n;(g=e.next())&&g.W();){j=f.n(g.d);n=j.ic();if(n<0)return null;if(n>0)i=true;k.push(j)}return k.length>0&&k.length<5?{tl:k[0],tr:k[1]||k[0],br:k[2]||k[0],bl:k[3]||k[1]||k[0]}:null};if(b=h()){if(g){if(g.k&f.v.qa.pa&&g.d===\n\"/\")d=h()}else d=b;if(i&&b&&d)c={x:b,y:d}}}return c}});var a=f.n(\"0\");a={tl:a,tr:a,br:a,bl:a};f.jb.Dc={x:a,y:a}})();f.Ub=f.B.ka({wa:\"border-image\",Fa:\"borderImage\",fb:{stretch:1,round:1,repeat:1,space:1},la:function(a){var b=null,c,d,e,g,j,i,h=0,k=f.v.qa,n=k.na,m=k.oa,p=k.Ra;if(a){c=new f.v(a);b={};for(var r=function(l){return l&&l.k&k.pa&&l.d===\"/\"},t=function(l){return l&&l.k&n&&l.d===\"fill\"},v=function(){g=c.ma(function(l){return!(l.k&(m|p))});if(t(c.next())&&!b.fill)b.fill=true;else c.D();if(r(c.next())){h++;\nj=c.ma(function(l){return!l.W()&&!(l.k&n&&l.d===\"auto\")});if(r(c.next())){h++;i=c.ma(function(l){return!l.Ca()})}}else c.D()};a=c.next();){d=a.k;e=a.d;if(d&(m|p)&&!g){c.D();v()}else if(t(a)&&!b.fill){b.fill=true;v()}else if(d&n&&this.fb[e]&&!b.repeat){b.repeat={f:e};if(a=c.next())if(a.k&n&&this.fb[a.d])b.repeat.Ob=a.d;else c.D()}else if(d&k.URL&&!b.src)b.src=e;else return null}if(!b.src||!g||g.length<1||g.length>4||j&&j.length>4||h===1&&j.length<1||i&&i.length>4||h===2&&i.length<1)return null;if(!b.repeat)b.repeat=\n{f:\"stretch\"};if(!b.repeat.Ob)b.repeat.Ob=b.repeat.f;a=function(l,q){return{t:q(l[0]),r:q(l[1]||l[0]),b:q(l[2]||l[0]),l:q(l[3]||l[1]||l[0])}};b.slice=a(g,function(l){return f.n(l.k&m?l.d+\"px\":l.d)});if(j&&j[0])b.J=a(j,function(l){return l.W()?f.n(l.d):l.d});if(i&&i[0])b.Da=a(i,function(l){return l.Ca()?f.n(l.d):l.d})}return b}});f.Ic=f.B.ka({wa:\"box-shadow\",Fa:\"boxShadow\",la:function(a){var b,c=f.n,d=f.v.qa,e;if(a){e=new f.v(a);b={Da:[],Bb:[]};for(a=function(){for(var g,j,i,h,k,n;g=e.next();){i=g.d;\nj=g.k;if(j&d.pa&&i===\",\")break;else if(g.Ca()&&!k){e.D();k=e.ma(function(m){return!m.Ca()})}else if(j&d.z&&!h)h=i;else if(j&d.na&&i===\"inset\"&&!n)n=true;else return false}g=k&&k.length;if(g>1&&g<5){(n?b.Bb:b.Da).push({fe:c(k[0].d),ge:c(k[1].d),blur:c(k[2]?k[2].d:\"0\"),Vd:c(k[3]?k[3].d:\"0\"),color:f.ha(h||\"currentColor\")});return true}return false};a(););}return b&&(b.Bb.length||b.Da.length)?b:null}});f.Uc=f.B.ka({ia:f.B.va(function(){var a=this.e.currentStyle;return a.visibility+\"|\"+a.display}),la:function(){var a=\nthis.e,b=a.runtimeStyle;a=a.currentStyle;var c=b.visibility,d;b.visibility=\"\";d=a.visibility;b.visibility=c;return{ce:d!==\"hidden\",nd:a.display!==\"none\"}},i:function(){return false}});f.u={R:function(a){function b(c,d,e,g){this.e=c;this.s=d;this.g=e;this.parent=g}f.p.Eb(b.prototype,f.u,a);return b},Cb:false,Q:function(){return false},Ea:f.aa,Lb:function(){this.m();this.i()&&this.V()},ib:function(){this.Cb=true},Mb:function(){this.i()?this.V():this.m()},sb:function(a,b){this.vc(a);for(var c=this.ra||\n(this.ra=[]),d=a+1,e=c.length,g;d<e;d++)if(g=c[d])break;c[a]=b;this.I().insertBefore(b,g||null)},za:function(a){var b=this.ra;return b&&b[a]||null},vc:function(a){var b=this.za(a),c=this.Ta;if(b&&c){c.removeChild(b);this.ra[a]=null}},Aa:function(a,b,c,d){var e=this.rb||(this.rb={}),g=e[a];if(!g){g=e[a]=f.p.Za(\"shape\");if(b)g.appendChild(g[b]=f.p.Za(b));if(d){c=this.za(d);if(!c){this.sb(d,doc.createElement(\"group\"+d));c=this.za(d)}}c.appendChild(g);a=g.style;a.position=\"absolute\";a.left=a.top=0;a.behavior=\n\"url(#default#VML)\"}return g},vb:function(a){var b=this.rb,c=b&&b[a];if(c){c.parentNode.removeChild(c);delete b[a]}return!!c},kc:function(a){var b=this.e,c=this.s.o(),d=c.h,e=c.f,g,j,i,h,k,n;c=a.x.tl.a(b,d);g=a.y.tl.a(b,e);j=a.x.tr.a(b,d);i=a.y.tr.a(b,e);h=a.x.br.a(b,d);k=a.y.br.a(b,e);n=a.x.bl.a(b,d);a=a.y.bl.a(b,e);d=Math.min(d/(c+j),e/(i+k),d/(n+h),e/(g+a));if(d<1){c*=d;g*=d;j*=d;i*=d;h*=d;k*=d;n*=d;a*=d}return{x:{tl:c,tr:j,br:h,bl:n},y:{tl:g,tr:i,br:k,bl:a}}},ya:function(a,b,c){b=b||1;var d,e,\ng=this.s.o();e=g.h*b;g=g.f*b;var j=this.g.G,i=Math.floor,h=Math.ceil,k=a?a.Jb*b:0,n=a?a.Ib*b:0,m=a?a.tb*b:0;a=a?a.Db*b:0;var p,r,t,v,l;if(c||j.i()){d=this.kc(c||j.j());c=d.x.tl*b;j=d.y.tl*b;p=d.x.tr*b;r=d.y.tr*b;t=d.x.br*b;v=d.y.br*b;l=d.x.bl*b;b=d.y.bl*b;e=\"m\"+i(a)+\",\"+i(j)+\"qy\"+i(c)+\",\"+i(k)+\"l\"+h(e-p)+\",\"+i(k)+\"qx\"+h(e-n)+\",\"+i(r)+\"l\"+h(e-n)+\",\"+h(g-v)+\"qy\"+h(e-t)+\",\"+h(g-m)+\"l\"+i(l)+\",\"+h(g-m)+\"qx\"+i(a)+\",\"+h(g-b)+\" x e\"}else e=\"m\"+i(a)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+h(g-m)+\"l\"+i(a)+\n\",\"+h(g-m)+\"xe\";return e},I:function(){var a=this.parent.za(this.N),b;if(!a){a=doc.createElement(this.Ya);b=a.style;b.position=\"absolute\";b.top=b.left=0;this.parent.sb(this.N,a)}return a},mc:function(){var a=this.e,b=a.currentStyle,c=a.runtimeStyle,d=a.tagName,e=f.O===6,g;if(e&&(d in f.cc||d===\"FIELDSET\")||d===\"BUTTON\"||d===\"INPUT\"&&a.type in f.Gd){c.borderWidth=\"\";d=this.g.w.wc;for(g=d.length;g--;){e=d[g];c[\"padding\"+e]=\"\";c[\"padding\"+e]=f.n(b[\"padding\"+e]).a(a)+f.n(b[\"border\"+e+\"Width\"]).a(a)+(f.O!==\n8&&g%2?1:0)}c.borderWidth=0}else if(e){if(a.childNodes.length!==1||a.firstChild.tagName!==\"ie6-mask\"){b=doc.createElement(\"ie6-mask\");d=b.style;d.visibility=\"visible\";for(d.zoom=1;d=a.firstChild;)b.appendChild(d);a.appendChild(b);c.visibility=\"hidden\"}}else c.borderColor=\"transparent\"},ie:function(){},m:function(){this.parent.vc(this.N);delete this.rb;delete this.ra}};f.Rc=f.u.R({i:function(){var a=this.ed;for(var b in a)if(a.hasOwnProperty(b)&&a[b].i())return true;return false},Q:function(){return this.g.Pb.H()},\nib:function(){if(this.i()){var a=this.jc(),b=a,c;a=a.currentStyle;var d=a.position,e=this.I().style,g=0,j=0;j=this.s.o();var i=j.Hd;if(d===\"fixed\"&&f.O>6){g=j.x*i;j=j.y*i;b=d}else{do b=b.offsetParent;while(b&&b.currentStyle.position===\"static\");if(b){c=b.getBoundingClientRect();b=b.currentStyle;g=(j.x-c.left)*i-(parseFloat(b.borderLeftWidth)||0);j=(j.y-c.top)*i-(parseFloat(b.borderTopWidth)||0)}else{b=doc.documentElement;g=(j.x+b.scrollLeft-b.clientLeft)*i;j=(j.y+b.scrollTop-b.clientTop)*i}b=\"absolute\"}e.position=\nb;e.left=g;e.top=j;e.zIndex=d===\"static\"?-1:a.zIndex;this.Cb=true}},Mb:f.aa,Nb:function(){var a=this.g.Pb.j();this.I().style.display=a.ce&&a.nd?\"\":\"none\"},Lb:function(){this.i()?this.Nb():this.m()},jc:function(){var a=this.e;return a.tagName in f.Ac?a.offsetParent:a},I:function(){var a=this.Ta,b;if(!a){b=this.jc();a=this.Ta=doc.createElement(\"css3-container\");a.style.direction=\"ltr\";this.Nb();b.parentNode.insertBefore(a,b)}return a},ab:f.aa,m:function(){var a=this.Ta,b;if(a&&(b=a.parentNode))b.removeChild(a);\ndelete this.Ta;delete this.ra}});f.Fc=f.u.R({N:2,Ya:\"background\",Q:function(){var a=this.g;return a.C.H()||a.G.H()},i:function(){var a=this.g;return a.q.i()||a.G.i()||a.C.i()||a.ga.i()&&a.ga.j().Bb},V:function(){var a=this.s.o();if(a.h&&a.f){this.od();this.pd()}},od:function(){var a=this.g.C.j(),b=this.s.o(),c=this.e,d=a&&a.color,e,g;if(d&&d.fa()>0){this.lc();a=this.Aa(\"bgColor\",\"fill\",this.I(),1);e=b.h;b=b.f;a.stroked=false;a.coordsize=e*2+\",\"+b*2;a.coordorigin=\"1,1\";a.path=this.ya(null,2);g=a.style;\ng.width=e;g.height=b;a.fill.color=d.U(c);c=d.fa();if(c<1)a.fill.opacity=c}else this.vb(\"bgColor\")},pd:function(){var a=this.g.C.j(),b=this.s.o();a=a&&a.M;var c,d,e,g,j;if(a){this.lc();d=b.h;e=b.f;for(j=a.length;j--;){b=a[j];c=this.Aa(\"bgImage\"+j,\"fill\",this.I(),2);c.stroked=false;c.fill.type=\"tile\";c.fillcolor=\"none\";c.coordsize=d*2+\",\"+e*2;c.coordorigin=\"1,1\";c.path=this.ya(0,2);g=c.style;g.width=d;g.height=e;if(b.P===\"linear-gradient\")this.bd(c,b);else{c.fill.src=b.Ab;this.Nd(c,j)}}}for(j=a?a.length:\n0;this.vb(\"bgImage\"+j++););},Nd:function(a,b){var c=this;f.p.Rb(a.fill.src,function(d){var e=c.e,g=c.s.o(),j=g.h;g=g.f;if(j&&g){var i=a.fill,h=c.g,k=h.w.j(),n=k&&k.J;k=n?n.t.a(e):0;var m=n?n.r.a(e):0,p=n?n.b.a(e):0;n=n?n.l.a(e):0;h=h.C.j().M[b];e=h.$?h.$.coords(e,j-d.h-n-m,g-d.f-k-p):{x:0,y:0};h=h.bb;p=m=0;var r=j+1,t=g+1,v=f.O===8?0:1;n=Math.round(e.x)+n+0.5;k=Math.round(e.y)+k+0.5;i.position=n/j+\",\"+k/g;i.size.x=1;i.size=d.h+\"px,\"+d.f+\"px\";if(h&&h!==\"repeat\"){if(h===\"repeat-x\"||h===\"no-repeat\"){m=\nk+1;t=k+d.f+v}if(h===\"repeat-y\"||h===\"no-repeat\"){p=n+1;r=n+d.h+v}a.style.clip=\"rect(\"+m+\"px,\"+r+\"px,\"+t+\"px,\"+p+\"px)\"}}})},bd:function(a,b){var c=this.e,d=this.s.o(),e=d.h,g=d.f;a=a.fill;d=b.ca;var j=d.length,i=Math.PI,h=f.Na,k=h.tc,n=h.dc;b=h.gc(c,e,g,b);h=b.sa;var m=b.xc,p=b.yc,r=b.Wd,t=b.Xd,v=b.rd,l=b.sd,q=b.kd,s=b.ld;b=b.rc;e=h%90?Math.atan2(q*e/g,s)/i*180:h+90;e+=180;e%=360;v=k(r,t,h,v,l);g=n(r,t,v[0],v[1]);i=[];v=k(m,p,h,r,t);n=n(m,p,v[0],v[1])/g*100;k=[];for(h=0;h<j;h++)k.push(d[h].db?d[h].db.a(c,\nb):h===0?0:h===j-1?b:null);for(h=1;h<j;h++){if(k[h]===null){m=k[h-1];b=h;do p=k[++b];while(p===null);k[h]=m+(p-m)/(b-h+1)}k[h]=Math.max(k[h],k[h-1])}for(h=0;h<j;h++)i.push(n+k[h]/g*100+\"% \"+d[h].color.U(c));a.angle=e;a.type=\"gradient\";a.method=\"sigma\";a.color=d[0].color.U(c);a.color2=d[j-1].color.U(c);if(a.colors)a.colors.value=i.join(\",\");else a.colors=i.join(\",\")},lc:function(){var a=this.e.runtimeStyle;a.backgroundImage=\"url(about:blank)\";a.backgroundColor=\"transparent\"},m:function(){f.u.m.call(this);\nvar a=this.e.runtimeStyle;a.backgroundImage=a.backgroundColor=\"\"}});f.Gc=f.u.R({N:4,Ya:\"border\",Q:function(){var a=this.g;return a.w.H()||a.G.H()},i:function(){var a=this.g;return a.G.i()&&!a.q.i()&&a.w.i()},V:function(){var a=this.e,b=this.g.w.j(),c=this.s.o(),d=c.h;c=c.f;var e,g,j,i,h;if(b){this.mc();b=this.wd(2);i=0;for(h=b.length;i<h;i++){j=b[i];e=this.Aa(\"borderPiece\"+i,j.stroke?\"stroke\":\"fill\",this.I());e.coordsize=d*2+\",\"+c*2;e.coordorigin=\"1,1\";e.path=j.path;g=e.style;g.width=d;g.height=c;\ne.filled=!!j.fill;e.stroked=!!j.stroke;if(j.stroke){e=e.stroke;e.weight=j.Qb+\"px\";e.color=j.color.U(a);e.dashstyle=j.stroke===\"dashed\"?\"2 2\":j.stroke===\"dotted\"?\"1 1\":\"solid\";e.linestyle=j.stroke===\"double\"&&j.Qb>2?\"ThinThin\":\"Single\"}else e.fill.color=j.fill.U(a)}for(;this.vb(\"borderPiece\"+i++););}},wd:function(a){var b=this.e,c,d,e,g=this.g.w,j=[],i,h,k,n,m=Math.round,p,r,t;if(g.i()){c=g.j();g=c.J;r=c.Zd;t=c.gd;if(c.ee&&c.$d&&c.hd){if(t.t.fa()>0){c=g.t.a(b);k=c/2;j.push({path:this.ya({Jb:k,Ib:k,\ntb:k,Db:k},a),stroke:r.t,color:t.t,Qb:c})}}else{a=a||1;c=this.s.o();d=c.h;e=c.f;c=m(g.t.a(b));k=m(g.r.a(b));n=m(g.b.a(b));b=m(g.l.a(b));var v={t:c,r:k,b:n,l:b};b=this.g.G;if(b.i())p=this.kc(b.j());i=Math.floor;h=Math.ceil;var l=function(o,u){return p?p[o][u]:0},q=function(o,u,x,y,z,B){var E=l(\"x\",o),D=l(\"y\",o),C=o.charAt(1)===\"r\";o=o.charAt(0)===\"b\";return E>0&&D>0?(B?\"al\":\"ae\")+(C?h(d-E):i(E))*a+\",\"+(o?h(e-D):i(D))*a+\",\"+(i(E)-u)*a+\",\"+(i(D)-x)*a+\",\"+y*65535+\",\"+2949075*(z?1:-1):(B?\"m\":\"l\")+(C?d-\nu:u)*a+\",\"+(o?e-x:x)*a},s=function(o,u,x,y){var z=o===\"t\"?i(l(\"x\",\"tl\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+i(l(\"y\",\"tr\"))*a:o===\"b\"?h(d-l(\"x\",\"br\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+h(e-l(\"y\",\"bl\"))*a;o=o===\"t\"?h(d-l(\"x\",\"tr\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+h(e-l(\"y\",\"br\"))*a:o===\"b\"?i(l(\"x\",\"bl\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+i(l(\"y\",\"tl\"))*a;return x?(y?\"m\"+o:\"\")+\"l\"+z:(y?\"m\"+z:\"\")+\"l\"+o};b=function(o,u,x,y,z,B){var E=o===\"l\"||o===\"r\",D=v[o],C,F;if(D>0&&r[o]!==\"none\"&&t[o].fa()>0){C=v[E?o:u];u=v[E?u:\no];F=v[E?o:x];x=v[E?x:o];if(r[o]===\"dashed\"||r[o]===\"dotted\"){j.push({path:q(y,C,u,B+45,0,1)+q(y,0,0,B,1,0),fill:t[o]});j.push({path:s(o,D/2,0,1),stroke:r[o],Qb:D,color:t[o]});j.push({path:q(z,F,x,B,0,1)+q(z,0,0,B-45,1,0),fill:t[o]})}else j.push({path:q(y,C,u,B+45,0,1)+s(o,D,0,0)+q(z,F,x,B,0,0)+(r[o]===\"double\"&&D>2?q(z,F-i(F/3),x-i(x/3),B-45,1,0)+s(o,h(D/3*2),1,0)+q(y,C-i(C/3),u-i(u/3),B,1,0)+\"x \"+q(y,i(C/3),i(u/3),B+45,0,1)+s(o,i(D/3),1,0)+q(z,i(F/3),i(x/3),B,0,0):\"\")+q(z,0,0,B-45,1,0)+s(o,0,1,\n0)+q(y,0,0,B,1,0),fill:t[o]})}};b(\"t\",\"l\",\"r\",\"tl\",\"tr\",90);b(\"r\",\"t\",\"b\",\"tr\",\"br\",0);b(\"b\",\"r\",\"l\",\"br\",\"bl\",-90);b(\"l\",\"b\",\"t\",\"bl\",\"tl\",-180)}}return j},m:function(){if(this.ec||!this.g.q.i())this.e.runtimeStyle.borderColor=\"\";f.u.m.call(this)}});f.Tb=f.u.R({N:5,Md:[\"t\",\"tr\",\"r\",\"br\",\"b\",\"bl\",\"l\",\"tl\",\"c\"],Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){this.I();var a=this.g.q.j(),b=this.g.w.j(),c=this.s.o(),d=this.e,e=this.uc;f.p.Rb(a.src,function(g){function j(s,\no,u,x,y){s=e[s].style;var z=Math.max;s.width=z(o,0);s.height=z(u,0);s.left=x;s.top=y}function i(s,o,u){for(var x=0,y=s.length;x<y;x++)e[s[x]].imagedata[o]=u}var h=c.h,k=c.f,n=f.n(\"0\"),m=a.J||(b?b.J:{t:n,r:n,b:n,l:n});n=m.t.a(d);var p=m.r.a(d),r=m.b.a(d);m=m.l.a(d);var t=a.slice,v=t.t.a(d),l=t.r.a(d),q=t.b.a(d);t=t.l.a(d);j(\"tl\",m,n,0,0);j(\"t\",h-m-p,n,m,0);j(\"tr\",p,n,h-p,0);j(\"r\",p,k-n-r,h-p,n);j(\"br\",p,r,h-p,k-r);j(\"b\",h-m-p,r,m,k-r);j(\"bl\",m,r,0,k-r);j(\"l\",m,k-n-r,0,n);j(\"c\",h-m-p,k-n-r,m,n);i([\"tl\",\n\"t\",\"tr\"],\"cropBottom\",(g.f-v)/g.f);i([\"tl\",\"l\",\"bl\"],\"cropRight\",(g.h-t)/g.h);i([\"bl\",\"b\",\"br\"],\"cropTop\",(g.f-q)/g.f);i([\"tr\",\"r\",\"br\"],\"cropLeft\",(g.h-l)/g.h);i([\"l\",\"r\",\"c\"],\"cropTop\",v/g.f);i([\"l\",\"r\",\"c\"],\"cropBottom\",q/g.f);i([\"t\",\"b\",\"c\"],\"cropLeft\",t/g.h);i([\"t\",\"b\",\"c\"],\"cropRight\",l/g.h);e.c.style.display=a.fill?\"\":\"none\"},this)},I:function(){var a=this.parent.za(this.N),b,c,d,e=this.Md,g=e.length;if(!a){a=doc.createElement(\"border-image\");b=a.style;b.position=\"absolute\";this.uc={};for(d=\n0;d<g;d++){c=this.uc[e[d]]=f.p.Za(\"rect\");c.appendChild(f.p.Za(\"imagedata\"));b=c.style;b.behavior=\"url(#default#VML)\";b.position=\"absolute\";b.top=b.left=0;c.imagedata.src=this.g.q.j().src;c.stroked=false;c.filled=false;a.appendChild(c)}this.parent.sb(this.N,a)}return a},Ea:function(){if(this.i()){var a=this.e,b=a.runtimeStyle,c=this.g.q.j().J;b.borderStyle=\"solid\";if(c){b.borderTopWidth=c.t.a(a)+\"px\";b.borderRightWidth=c.r.a(a)+\"px\";b.borderBottomWidth=c.b.a(a)+\"px\";b.borderLeftWidth=c.l.a(a)+\"px\"}this.mc()}},\nm:function(){var a=this.e.runtimeStyle;a.borderStyle=\"\";if(this.ec||!this.g.w.i())a.borderColor=a.borderWidth=\"\";f.u.m.call(this)}});f.Hc=f.u.R({N:1,Ya:\"outset-box-shadow\",Q:function(){var a=this.g;return a.ga.H()||a.G.H()},i:function(){var a=this.g.ga;return a.i()&&a.j().Da[0]},V:function(){function a(C,F,O,H,M,P,I){C=b.Aa(\"shadow\"+C+F,\"fill\",d,j-C);F=C.fill;C.coordsize=n*2+\",\"+m*2;C.coordorigin=\"1,1\";C.stroked=false;C.filled=true;F.color=M.U(c);if(P){F.type=\"gradienttitle\";F.color2=F.color;F.opacity=\n0}C.path=I;l=C.style;l.left=O;l.top=H;l.width=n;l.height=m;return C}var b=this,c=this.e,d=this.I(),e=this.g,g=e.ga.j().Da;e=e.G.j();var j=g.length,i=j,h,k=this.s.o(),n=k.h,m=k.f;k=f.O===8?1:0;for(var p=[\"tl\",\"tr\",\"br\",\"bl\"],r,t,v,l,q,s,o,u,x,y,z,B,E,D;i--;){t=g[i];q=t.fe.a(c);s=t.ge.a(c);h=t.Vd.a(c);o=t.blur.a(c);t=t.color;u=-h-o;if(!e&&o)e=f.jb.Dc;u=this.ya({Jb:u,Ib:u,tb:u,Db:u},2,e);if(o){x=(h+o)*2+n;y=(h+o)*2+m;z=x?o*2/x:0;B=y?o*2/y:0;if(o-h>n/2||o-h>m/2)for(h=4;h--;){r=p[h];E=r.charAt(0)===\"b\";\nD=r.charAt(1)===\"r\";r=a(i,r,q,s,t,o,u);v=r.fill;v.focusposition=(D?1-z:z)+\",\"+(E?1-B:B);v.focussize=\"0,0\";r.style.clip=\"rect(\"+((E?y/2:0)+k)+\"px,\"+(D?x:x/2)+\"px,\"+(E?y:y/2)+\"px,\"+((D?x/2:0)+k)+\"px)\"}else{r=a(i,\"\",q,s,t,o,u);v=r.fill;v.focusposition=z+\",\"+B;v.focussize=1-z*2+\",\"+(1-B*2)}}else{r=a(i,\"\",q,s,t,o,u);q=t.fa();if(q<1)r.fill.opacity=q}}}});f.Pc=f.u.R({N:6,Ya:\"imgEl\",Q:function(){var a=this.g;return this.e.src!==this.Xc||a.G.H()},i:function(){var a=this.g;return a.G.i()||a.C.qc()},V:function(){this.Xc=\nj;this.Cd();var a=this.Aa(\"img\",\"fill\",this.I()),b=a.fill,c=this.s.o(),d=c.h;c=c.f;var e=this.g.w.j(),g=e&&e.J;e=this.e;var j=e.src,i=Math.round,h=e.currentStyle,k=f.n;if(!g||f.O<7){g=f.n(\"0\");g={t:g,r:g,b:g,l:g}}a.stroked=false;b.type=\"frame\";b.src=j;b.position=(d?0.5/d:0)+\",\"+(c?0.5/c:0);a.coordsize=d*2+\",\"+c*2;a.coordorigin=\"1,1\";a.path=this.ya({Jb:i(g.t.a(e)+k(h.paddingTop).a(e)),Ib:i(g.r.a(e)+k(h.paddingRight).a(e)),tb:i(g.b.a(e)+k(h.paddingBottom).a(e)),Db:i(g.l.a(e)+k(h.paddingLeft).a(e))},\n2);a=a.style;a.width=d;a.height=c},Cd:function(){this.e.runtimeStyle.filter=\"alpha(opacity=0)\"},m:function(){f.u.m.call(this);this.e.runtimeStyle.filter=\"\"}});f.Oc=f.u.R({ib:f.aa,Mb:f.aa,Nb:f.aa,Lb:f.aa,Ld:/^,+|,+$/g,Fd:/,+/g,gb:function(a,b){(this.pb||(this.pb=[]))[a]=b||void 0},ab:function(){var a=this.pb,b;if(a&&(b=a.join(\",\").replace(this.Ld,\"\").replace(this.Fd,\",\"))!==this.Wc)this.Wc=this.e.runtimeStyle.background=b},m:function(){this.e.runtimeStyle.background=\"\";delete this.pb}});f.Mc=f.u.R({ua:1,\nQ:function(){return this.g.C.H()},i:function(){var a=this.g;return a.C.i()||a.q.i()},V:function(){var a=this.g.C.j(),b,c,d=0,e,g;if(a){b=[];if(c=a.M)for(;e=c[d++];)if(e.P===\"linear-gradient\"){g=this.vd(e.Wa);g=(e.Xa||f.Ka.Kc).a(this.e,g.h,g.f,g.h,g.f);b.push(\"url(data:image/svg+xml,\"+escape(this.xd(e,g.h,g.f))+\") \"+this.dd(e.$)+\" / \"+g.h+\"px \"+g.f+\"px \"+(e.bc||\"\")+\" \"+(e.Wa||\"\")+\" \"+(e.ub||\"\"))}else b.push(e.Hb);a.color&&b.push(a.color.Y);this.parent.gb(this.ua,b.join(\",\"))}},dd:function(a){return a?\na.X.map(function(b){return b.d}).join(\" \"):\"0 0\"},vd:function(a){var b=this.e,c=this.s.o(),d=c.h;c=c.f;var e;if(a!==\"border-box\")if((e=this.g.w.j())&&(e=e.J)){d-=e.l.a(b)+e.l.a(b);c-=e.t.a(b)+e.b.a(b)}if(a===\"content-box\"){a=f.n;e=b.currentStyle;d-=a(e.paddingLeft).a(b)+a(e.paddingRight).a(b);c-=a(e.paddingTop).a(b)+a(e.paddingBottom).a(b)}return{h:d,f:c}},xd:function(a,b,c){var d=this.e,e=a.ca,g=e.length,j=f.Na.gc(d,b,c,a);a=j.xc;var i=j.yc,h=j.td,k=j.ud;j=j.rc;var n,m,p,r,t;n=[];for(m=0;m<g;m++)n.push(e[m].db?\ne[m].db.a(d,j):m===0?0:m===g-1?j:null);for(m=1;m<g;m++)if(n[m]===null){r=n[m-1];p=m;do t=n[++p];while(t===null);n[m]=r+(t-r)/(p-m+1)}b=['<svg width=\"'+b+'\" height=\"'+c+'\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"g\" gradientUnits=\"userSpaceOnUse\" x1=\"'+a/b*100+'%\" y1=\"'+i/c*100+'%\" x2=\"'+h/b*100+'%\" y2=\"'+k/c*100+'%\">'];for(m=0;m<g;m++)b.push('<stop offset=\"'+n[m]/j+'\" stop-color=\"'+e[m].color.U(d)+'\" stop-opacity=\"'+e[m].color.fa()+'\"/>');b.push('</linearGradient></defs><rect width=\"100%\" height=\"100%\" fill=\"url(#g)\"/></svg>');\nreturn b.join(\"\")},m:function(){this.parent.gb(this.ua)}});f.Nc=f.u.R({T:\"repeat\",Sc:\"stretch\",Qc:\"round\",ua:0,Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){var a=this,b=a.g.q.j(),c=a.g.w.j(),d=a.s.o(),e=b.repeat,g=e.f,j=e.Ob,i=a.e,h=0;f.p.Rb(b.src,function(k){function n(Q,R,U,V,W,Y,X,S,w,A){K.push('<pattern patternUnits=\"userSpaceOnUse\" id=\"pattern'+G+'\" x=\"'+(g===l?Q+U/2-w/2:Q)+'\" y=\"'+(j===l?R+V/2-A/2:R)+'\" width=\"'+w+'\" height=\"'+A+'\"><svg width=\"'+w+'\" height=\"'+\nA+'\" viewBox=\"'+W+\" \"+Y+\" \"+X+\" \"+S+'\" preserveAspectRatio=\"none\"><image xlink:href=\"'+v+'\" x=\"0\" y=\"0\" width=\"'+r+'\" height=\"'+t+'\" /></svg></pattern>');J.push('<rect x=\"'+Q+'\" y=\"'+R+'\" width=\"'+U+'\" height=\"'+V+'\" fill=\"url(#pattern'+G+')\" />');G++}var m=d.h,p=d.f,r=k.h,t=k.f,v=a.Dd(b.src,r,t),l=a.T,q=a.Sc;k=a.Qc;var s=Math.ceil,o=f.n(\"0\"),u=b.J||(c?c.J:{t:o,r:o,b:o,l:o});o=u.t.a(i);var x=u.r.a(i),y=u.b.a(i);u=u.l.a(i);var z=b.slice,B=z.t.a(i),E=z.r.a(i),D=z.b.a(i);z=z.l.a(i);var C=m-u-x,F=p-o-\ny,O=r-z-E,H=t-B-D,M=g===q?C:O*o/B,P=j===q?F:H*x/E,I=g===q?C:O*y/D;q=j===q?F:H*u/z;var K=[],J=[],G=0;if(g===k){M-=(M-(C%M||M))/s(C/M);I-=(I-(C%I||I))/s(C/I)}if(j===k){P-=(P-(F%P||P))/s(F/P);q-=(q-(F%q||q))/s(F/q)}k=['<svg width=\"'+m+'\" height=\"'+p+'\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">'];n(0,0,u,o,0,0,z,B,u,o);n(u,0,C,o,z,0,O,B,M,o);n(m-x,0,x,o,r-E,0,E,B,x,o);n(0,o,u,F,0,B,z,H,u,q);if(b.fill)n(u,o,C,F,z,B,O,H,M||I||O,q||P||H);n(m-x,o,x,F,r-E,B,E,H,x,P);n(0,\np-y,u,y,0,t-D,z,D,u,y);n(u,p-y,C,y,z,t-D,O,D,I,y);n(m-x,p-y,x,y,r-E,t-D,E,D,x,y);k.push(\"<defs>\"+K.join(\"\\n\")+\"</defs>\"+J.join(\"\\n\")+\"</svg>\");a.parent.gb(a.ua,\"url(data:image/svg+xml,\"+escape(k.join(\"\"))+\") no-repeat border-box border-box\");h&&a.parent.ab()},a);h=1},Dd:function(){var a={};return function(b,c,d){var e=a[b],g;if(!e){e=new Image;g=doc.createElement(\"canvas\");e.src=b;g.width=c;g.height=d;g.getContext(\"2d\").drawImage(e,0,0);e=a[b]=g.toDataURL()}return e}}(),Ea:f.Tb.prototype.Ea,m:function(){var a=\nthis.e.runtimeStyle;this.parent.gb(this.ua);a.borderColor=a.borderStyle=a.borderWidth=\"\"}});f.kb=function(){function a(l,q){l.className+=\" \"+q}function b(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;)a(l,q[s])},0)}function c(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;){var o=q[s];o=t[o]||(t[o]=new RegExp(\"\\\\b\"+o+\"\\\\b\",\"g\"));l.className=l.className.replace(o,\"\")}},0)}function d(l){function q(){if(!U){var w,A,L=f.ja,T=l.currentStyle,\nN=T.getAttribute(g)===\"true\",da=T.getAttribute(i)!==\"false\",ea=T.getAttribute(h)!==\"false\";S=T.getAttribute(j);S=L>7?S!==\"false\":S===\"true\";if(!R){R=1;l.runtimeStyle.zoom=1;T=l;for(var fa=1;T=T.previousSibling;)if(T.nodeType===1){fa=0;break}fa&&a(l,p)}J.cb();if(N&&(A=J.o())&&(w=doc.documentElement||doc.body)&&(A.y>w.clientHeight||A.x>w.clientWidth||A.y+A.f<0||A.x+A.h<0)){if(!Y){Y=1;f.mb.ba(q)}}else{U=1;Y=R=0;f.mb.Ha(q);if(L===9){G={C:new f.Sb(l),q:new f.Ub(l),w:new f.Vb(l)};Q=[G.C,G.q];K=new f.Oc(l,\nJ,G);w=[new f.Mc(l,J,G,K),new f.Nc(l,J,G,K)]}else{G={C:new f.Sb(l),w:new f.Vb(l),q:new f.Ub(l),G:new f.jb(l),ga:new f.Ic(l),Pb:new f.Uc(l)};Q=[G.C,G.w,G.q,G.G,G.ga,G.Pb];K=new f.Rc(l,J,G);w=[new f.Hc(l,J,G,K),new f.Fc(l,J,G,K),new f.Gc(l,J,G,K),new f.Tb(l,J,G,K)];l.tagName===\"IMG\"&&w.push(new f.Pc(l,J,G,K));K.ed=w}I=[K].concat(w);if(w=l.currentStyle.getAttribute(f.F+\"watch-ancestors\")){w=parseInt(w,10);A=0;for(N=l.parentNode;N&&(w===\"NaN\"||A++<w);){H(N,\"onpropertychange\",C);H(N,\"onmouseenter\",x);\nH(N,\"onmouseleave\",y);H(N,\"onmousedown\",z);if(N.tagName in f.fc){H(N,\"onfocus\",E);H(N,\"onblur\",D)}N=N.parentNode}}if(S){f.Oa.ba(o);f.Oa.Rd()}o(1)}if(!V){V=1;L<9&&H(l,\"onmove\",s);H(l,\"onresize\",s);H(l,\"onpropertychange\",u);ea&&H(l,\"onmouseenter\",x);if(ea||da)H(l,\"onmouseleave\",y);da&&H(l,\"onmousedown\",z);if(l.tagName in f.fc){H(l,\"onfocus\",E);H(l,\"onblur\",D)}f.Qa.ba(s);f.L.ba(M)}J.hb()}}function s(){J&&J.Ad()&&o()}function o(w){if(!X)if(U){var A,L=I.length;F();for(A=0;A<L;A++)I[A].Ea();if(w||J.Od())for(A=\n0;A<L;A++)I[A].ib();if(w||J.Td())for(A=0;A<L;A++)I[A].Mb();K.ab();O()}else R||q()}function u(){var w,A=I.length,L;w=event;if(!X&&!(w&&w.propertyName in r))if(U){F();for(w=0;w<A;w++)I[w].Ea();for(w=0;w<A;w++){L=I[w];L.Cb||L.ib();L.Q()&&L.Lb()}K.ab();O()}else R||q()}function x(){b(l,k)}function y(){c(l,k,n)}function z(){b(l,n);f.lb.ba(B)}function B(){c(l,n);f.lb.Ha(B)}function E(){b(l,m)}function D(){c(l,m)}function C(){var w=event.propertyName;if(w===\"className\"||w===\"id\")u()}function F(){J.cb();for(var w=\nQ.length;w--;)Q[w].cb()}function O(){for(var w=Q.length;w--;)Q[w].hb();J.hb()}function H(w,A,L){w.attachEvent(A,L);W.push([w,A,L])}function M(){if(V){for(var w=W.length,A;w--;){A=W[w];A[0].detachEvent(A[1],A[2])}f.L.Ha(M);V=0;W=[]}}function P(){if(!X){var w,A;M();X=1;if(I){w=0;for(A=I.length;w<A;w++){I[w].ec=1;I[w].m()}}S&&f.Oa.Ha(o);f.Qa.Ha(o);I=J=G=Q=l=null}}var I,K,J=new ha(l),G,Q,R,U,V,W=[],Y,X,S;this.Ed=q;this.update=o;this.m=P;this.qd=l}var e={},g=f.F+\"lazy-init\",j=f.F+\"poll\",i=f.F+\"track-active\",\nh=f.F+\"track-hover\",k=f.La+\"hover\",n=f.La+\"active\",m=f.La+\"focus\",p=f.La+\"first-child\",r={background:1,bgColor:1,display:1},t={},v=[];d.yd=function(l){var q=f.p.Ba(l);return e[q]||(e[q]=new d(l))};d.m=function(l){l=f.p.Ba(l);var q=e[l];if(q){q.m();delete e[l]}};d.md=function(){var l=[],q;if(e){for(var s in e)if(e.hasOwnProperty(s)){q=e[s];l.push(q.qd);q.m()}e={}}return l};return d}();f.supportsVML=f.zc;f.attach=function(a){f.ja<10&&f.zc&&f.kb.yd(a).Ed()};f.detach=function(a){f.kb.m(a)}};\nvar $=element;function init(){if(doc.media!==\"print\"){var a=window.PIE;a&&a.attach($)}}function cleanup(){if(doc.media!==\"print\"){var a=window.PIE;if(a){a.detach($);$=0}}}$.readyState===\"complete\"&&init();\n</script>\n</PUBLIC:COMPONENT>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/airstrip/airstrip.hyperesources/airstrip_hype_generated_script.js",
    "content": "//\tHYPE.documents[\"Airstrip\"]\n\n(function(){(function k(){function l(a,b,d){var c=!1;null==window[a]&&(null==window[b]?(window[b]=[],window[b].push(k),a=document.getElementsByTagName(\"head\")[0],b=document.createElement(\"script\"),c=h,false==!0&&(c=\"\"),b.type=\"text/javascript\",b.src=c+\"/\"+d,a.appendChild(b)):window[b].push(k),c=!0);return c}var h=\"Airstrip.hyperesources\",c=\"Airstrip\",e=\"airstrip_hype_container\";if(false==!1)try{for(var f=document.getElementsByTagName(\"script\"),\na=0;a<f.length;a++){var b=f[a].src;if(null!=b&&-1!=b.indexOf(\"airstrip_hype_generated_script.js\")){h=b.substr(0,b.lastIndexOf(\"/\"));break}}}catch(n){}if(false==!1&&(a=navigator.userAgent.match(/MSIE (\\d+\\.\\d+)/),a=parseFloat(a&&a[1])||null,a=l(\"HYPE_526\",\"HYPE_dtl_526\",!0==(null!=a&&10>a||false==!0)?\"HYPE-526.full.min.js\":\"HYPE-526.thin.min.js\"),false==!0&&(a=a||l(\"HYPE_w_526\",\"HYPE_wdtl_526\",\"HYPE-526.waypoints.min.js\")),a))return;\nf=window.HYPE.documents;if(null!=f[c]){b=1;a=c;do c=\"\"+a+\"-\"+b++;while(null!=f[c]);for(var d=document.getElementsByTagName(\"div\"),b=!1,a=0;a<d.length;a++)if(d[a].id==e&&null==d[a].getAttribute(\"HYP_dn\")){var b=1,g=e;do e=\"\"+g+\"-\"+b++;while(null!=document.getElementById(e));d[a].id=e;b=!0;break}if(!1==b)return}b=[];b=[];d={};g={};for(a=0;a<b.length;a++)try{g[b[a].identifier]=b[a].name,d[b[a].name]=eval(\"(function(){return \"+b[a].source+\"})();\")}catch(m){window.console&&window.console.log(m),\nd[b[a].name]=function(){}}a=new HYPE_526(c,e,{\"10\":{p:1,n:\"Stoutpine.svg\",g:\"568\",t:\"image/svg+xml\"},\"2\":{p:1,n:\"plane2.svg\",g:\"542\",t:\"image/svg+xml\"},\"3\":{p:1,n:\"planeshadow.svg\",g:\"544\",t:\"image/svg+xml\"},\"11\":{p:1,n:\"grassbits.svg\",g:\"561\",t:\"image/svg+xml\"},\"4\":{p:1,n:\"counterstripe.svg\",g:\"546\",t:\"image/svg+xml\"},\"5\":{p:1,n:\"stripe.svg\",g:\"549\",t:\"image/svg+xml\"},\"12\":{p:1,n:\"grass.svg\",g:\"536\",t:\"image/svg+xml\"},\"6\":{p:1,n:\"endstripes.svg\",g:\"554\",t:\"image/svg+xml\"},\"7\":{p:1,n:\"pole1.svg\",g:\"556\",t:\"image/svg+xml\"},\"0\":{p:1,n:\"runway.svg\",g:\"538\",t:\"image/svg+xml\"},\"8\":{p:1,n:\"pole2.svg\",g:\"559\",t:\"image/svg+xml\"},\"1\":{p:1,n:\"airbag.svg\",g:\"540\",t:\"image/svg+xml\"},\"9\":{p:1,n:\"Largepine.svg\",g:\"563\",t:\"image/svg+xml\"}},h,[],d,[{n:\"Airstrip\",o:\"533\",X:[0]}],[{o:\"535\",p:\"600px\",x:0,cA:false,Z:500,Y:600,c:\"#FFFFFF\",L:[],bY:1,d:600,U:{},T:{kTimelineDefaultIdentifier:{i:\"kTimelineDefaultIdentifier\",n:\"Main Timeline\",z:4.19,b:[],a:[{f:\"c\",y:0,z:1.02,i:\"e\",e:1,s:0,o:\"943\"},{f:\"c\",y:0.09,z:0.18,i:\"e\",e:1,s:0,o:\"946\"},{f:\"c\",y:0.11,z:1,i:\"e\",e:1,s:0,o:\"955\"},{f:\"g\",y:0.2,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"952\"},{f:\"g\",y:0.2,z:0.07,i:\"cR\",e:0.60000000000000009,s:1,o:\"952\"},{f:\"g\",y:0.27,z:0.2,i:\"cR\",e:1,s:0.60000000000000009,o:\"952\"},{y:0.27,i:\"e\",s:1,z:0,o:\"946\",f:\"c\"},{f:\"g\",y:0.29,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"937\"},{f:\"g\",y:0.29,z:0.07,i:\"cR\",e:0.60000000000000009,s:1,o:\"937\"},{f:\"g\",y:0.29,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"945\"},{f:\"g\",y:0.29,z:0.07,i:\"cR\",e:0.60000000000000009,s:1,o:\"945\"},{y:1.02,i:\"e\",s:1,z:0,o:\"943\",f:\"c\"},{f:\"g\",y:1.03,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"950\"},{f:\"g\",y:1.03,z:0.05,i:\"cR\",e:0.60000000000000009,s:1,o:\"950\"},{f:\"g\",y:1.03,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"944\"},{f:\"g\",y:1.03,z:0.08,i:\"cR\",e:0.60000000000000009,s:1,o:\"944\"},{f:\"c\",y:1.04,z:0.08,i:\"e\",e:1,s:0,o:\"939\"},{f:\"c\",y:1.05,z:0.19,i:\"f\",e:0,s:-66,o:\"939\"},{f:\"c\",y:1.05,z:0.19,i:\"f\",e:0,s:-90,o:\"940\"},{f:\"f\",y:1.05,z:0.09,i:\"e\",e:1,s:0,o:\"953\"},{f:\"c\",y:1.05,z:0.07,i:\"e\",e:1,s:0,o:\"940\"},{f:\"g\",y:1.06,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"932\"},{f:\"g\",y:1.06,z:0.08,i:\"cR\",e:0.60000000000000009,s:1,o:\"932\"},{f:\"g\",y:1.06,z:0.2,i:\"cR\",e:1,s:0.60000000000000009,o:\"937\"},{f:\"g\",y:1.06,z:0.2,i:\"cR\",e:1,s:0.60000000000000009,o:\"945\"},{f:\"g\",y:1.08,z:0.22,i:\"cR\",e:1,s:0.60000000000000009,o:\"950\"},{f:\"g\",y:1.09,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"935\"},{f:\"g\",y:1.09,z:0.27,i:\"cQ\",e:1,s:0.0000000000,o:\"933\"},{f:\"g\",y:1.09,z:0.06,i:\"cR\",e:0.60000000000000009,s:1,o:\"935\"},{f:\"g\",y:1.09,z:0.06,i:\"cR\",e:0.60000000000000009,s:1,o:\"933\"},{f:\"f\",y:1.11,z:0.09,i:\"e\",e:1,s:0,o:\"942\"},{y:1.11,i:\"e\",s:1,z:0,o:\"955\",f:\"c\"},{f:\"g\",y:1.11,z:0.19,i:\"cR\",e:1,s:0.60000000000000009,o:\"944\"},{y:1.12,i:\"e\",s:1,z:0,o:\"939\",f:\"c\"},{y:1.12,i:\"e\",s:1,z:0,o:\"940\",f:\"c\"},{f:\"c\",y:1.13,z:0.11,i:\"f\",e:0,s:-30,o:\"941\"},{f:\"g\",y:1.13,z:0.22,i:\"cQ\",e:1,s:0.0000000000,o:\"941\"},{f:\"f\",y:1.13,z:0.09,i:\"e\",e:1,s:0,o:\"936\"},{f:\"g\",y:1.14,z:0.19,i:\"cR\",e:1,s:0.60000000000000009,o:\"932\"},{y:1.14,i:\"e\",s:1,z:0,o:\"953\",f:\"c\"},{f:\"f\",y:1.15,z:0.09,i:\"e\",e:1,s:0,o:\"956\"},{f:\"g\",y:1.15,z:0.21,i:\"cR\",e:1,s:0.60000000000000009,o:\"935\"},{f:\"g\",y:1.15,z:0.21,i:\"cR\",e:1,s:0.60000000000000009,o:\"933\"},{y:1.17,i:\"cQ\",s:1,z:0,o:\"952\",f:\"c\"},{y:1.17,i:\"cR\",s:1,z:0,o:\"952\",f:\"c\"},{f:\"f\",y:1.18,z:0.09,i:\"e\",e:1,s:0,o:\"951\"},{y:1.2,i:\"e\",s:1,z:0,o:\"942\",f:\"c\"},{f:\"f\",y:1.21,z:0.09,i:\"e\",e:1,s:0,o:\"934\"},{y:1.22,i:\"e\",s:1,z:0,o:\"936\",f:\"c\"},{y:1.24,i:\"f\",s:0,z:0,o:\"941\",f:\"c\"},{y:1.24,i:\"f\",s:0,z:0,o:\"939\",f:\"c\"},{y:1.24,i:\"f\",s:0,z:0,o:\"940\",f:\"c\"},{f:\"f\",y:1.24,z:0.09,i:\"e\",e:1,s:0,o:\"954\"},{y:1.24,i:\"e\",s:1,z:0,o:\"956\",f:\"c\"},{y:1.26,i:\"cQ\",s:1,z:0,o:\"937\",f:\"c\"},{y:1.26,i:\"cQ\",s:1,z:0,o:\"945\",f:\"c\"},{y:1.26,i:\"cR\",s:1,z:0,o:\"937\",f:\"c\"},{y:1.26,i:\"cR\",s:1,z:0,o:\"945\",f:\"c\"},{y:1.27,i:\"e\",s:1,z:0,o:\"951\",f:\"c\"},{y:2,i:\"cQ\",s:1,z:0,o:\"950\",f:\"c\"},{y:2,i:\"cQ\",s:1,z:0,o:\"944\",f:\"c\"},{y:2,i:\"cR\",s:1,z:0,o:\"950\",f:\"c\"},{y:2,i:\"cR\",s:1,z:0,o:\"944\",f:\"c\"},{y:2,i:\"e\",s:1,z:0,o:\"934\",f:\"c\"},{y:2.03,i:\"cQ\",s:1,z:0,o:\"932\",f:\"c\"},{y:2.03,i:\"cR\",s:1,z:0,o:\"932\",f:\"c\"},{y:2.03,i:\"e\",s:1,z:0,o:\"954\",f:\"c\"},{y:2.05,i:\"cQ\",s:1,z:0,o:\"941\",f:\"c\"},{y:2.06,i:\"cQ\",s:1,z:0,o:\"935\",f:\"c\"},{y:2.06,i:\"cQ\",s:1,z:0,o:\"933\",f:\"c\"},{y:2.06,i:\"cR\",s:1,z:0,o:\"935\",f:\"c\"},{y:2.06,i:\"cR\",s:1,z:0,o:\"933\",f:\"c\"},{f:\"709\",y:2.25,z:1.24,i:\"b\",e:23,s:-350,o:\"948\"},{f:\"709\",y:2.25,z:1.24,i:\"a\",e:-19,s:328,o:\"948\"},{f:\"684\",y:2.26,z:1.23,i:\"a\",e:-15,s:325,o:\"949\"},{f:\"684\",y:2.26,z:1.23,i:\"b\",e:30,s:-314,o:\"949\"},{y:4.19,i:\"a\",s:-19,z:0,o:\"948\",f:\"c\"},{y:4.19,i:\"b\",s:30,z:0,o:\"949\",f:\"c\"},{y:4.19,i:\"a\",s:-15,z:0,o:\"949\",f:\"c\"},{y:4.19,i:\"b\",s:23,z:0,o:\"948\",f:\"c\"}],f:30}},bZ:180,O:[\"948\",\"949\",\"947\",\"945\",\"935\",\"932\",\"952\",\"950\",\"944\",\"937\",\"933\",\"946\",\"953\",\"942\",\"936\",\"956\",\"951\",\"934\",\"954\",\"939\",\"940\",\"941\",\"938\",\"955\",\"943\",\"931\"],v:{\"950\":{h:\"563\",p:\"no-repeat\",x:\"visible\",a:433,q:\"100% 100%\",b:231,j:\"absolute\",bF:\"931\",z:15,k:\"div\",c:43,d:90,r:\"inline\",cQ:0.0000000000,cR:1},\"942\":{h:\"549\",p:\"no-repeat\",x:\"visible\",a:357,q:\"100% 100%\",b:228,j:\"absolute\",bF:\"931\",z:9,k:\"div\",c:21,d:21,r:\"inline\",e:0},\"934\":{h:\"546\",p:\"no-repeat\",x:\"visible\",a:226,q:\"100% 100%\",b:310,j:\"absolute\",bF:\"931\",z:5,k:\"div\",c:21,d:21,r:\"inline\",e:0},\"955\":{h:\"538\",p:\"no-repeat\",x:\"visible\",a:157,q:\"100% 100%\",b:187,j:\"absolute\",bF:\"931\",z:2,k:\"div\",c:263,d:194,r:\"inline\",e:0},\"947\":{k:\"div\",x:\"visible\",c:101,d:102,z:20,a:263,j:\"absolute\",bF:\"931\",b:239},\"939\":{h:\"559\",p:\"no-repeat\",x:\"visible\",a:0,q:\"100% 100%\",b:0,j:\"absolute\",bF:\"938\",z:3,k:\"div\",c:7,d:91,r:\"inline\",e:0,f:-66},\"951\":{h:\"549\",p:\"no-repeat\",x:\"visible\",a:263,q:\"100% 100%\",b:321,j:\"absolute\",bF:\"931\",z:6,k:\"div\",c:21,d:21,r:\"inline\",e:0},\"943\":{h:\"536\",p:\"no-repeat\",x:\"visible\",a:30,q:\"100% 100%\",b:109,j:\"absolute\",bF:\"931\",z:1,k:\"div\",c:514,d:349,r:\"inline\",e:0},\"935\":{h:\"568\",p:\"no-repeat\",x:\"visible\",a:501,q:\"100% 100%\",b:289,j:\"absolute\",bF:\"931\",z:18,k:\"div\",c:37,d:41,r:\"inline\",cQ:0.0000000000,cR:1},\"956\":{h:\"549\",p:\"no-repeat\",x:\"visible\",a:295,q:\"100% 100%\",b:289,j:\"absolute\",bF:\"931\",z:7,k:\"div\",c:21,d:21,r:\"inline\",e:0},\"948\":{h:\"542\",p:\"no-repeat\",x:\"visible\",a:328,q:\"100% 100%\",b:-350,j:\"absolute\",bF:\"947\",c:98,k:\"div\",z:2,d:96,r:\"inline\"},\"931\":{k:\"div\",x:\"visible\",c:561,d:460,z:1,a:12,j:\"absolute\",b:-21},\"952\":{h:\"563\",p:\"no-repeat\",x:\"visible\",a:133,q:\"100% 100%\",b:325,j:\"absolute\",bF:\"931\",z:16,k:\"div\",c:24,d:50,r:\"inline\",cQ:0.0000000000,cR:1},\"944\":{h:\"563\",p:\"no-repeat\",x:\"visible\",a:486,q:\"100% 100%\",b:130,j:\"absolute\",bF:\"931\",z:14,k:\"div\",c:34,d:71,r:\"inline\",cQ:0.0000000000,cR:1},\"936\":{h:\"549\",p:\"no-repeat\",x:\"visible\",a:327,q:\"100% 100%\",b:257,j:\"absolute\",bF:\"931\",z:8,k:\"div\",c:21,d:21,r:\"inline\",e:0},\"949\":{h:\"544\",p:\"no-repeat\",x:\"visible\",a:325,q:\"100% 100%\",b:-314,j:\"absolute\",bF:\"947\",c:97,k:\"div\",z:1,d:95,r:\"inline\"},\"940\":{h:\"556\",p:\"no-repeat\",x:\"visible\",a:2,q:\"100% 100%\",b:0,j:\"absolute\",bF:\"938\",z:2,k:\"div\",c:7,d:90,r:\"inline\",e:0,f:-90},\"932\":{h:\"563\",p:\"no-repeat\",x:\"visible\",a:73,q:\"100% 100%\",b:200,j:\"absolute\",bF:\"931\",z:17,k:\"div\",c:24,d:50,r:\"inline\",cQ:0.0000000000,cR:1},\"953\":{h:\"554\",p:\"no-repeat\",x:\"visible\",a:377,q:\"100% 100%\",b:198,j:\"absolute\",bF:\"931\",z:10,k:\"div\",c:31,d:31,r:\"inline\",e:0},\"945\":{h:\"568\",p:\"no-repeat\",x:\"visible\",a:235,q:\"100% 100%\",b:406,j:\"absolute\",bF:\"931\",z:19,k:\"div\",c:37,d:41,r:\"inline\",cQ:0.0000000000,cR:1},\"937\":{h:\"563\",p:\"no-repeat\",x:\"visible\",a:101,q:\"100% 100%\",b:166,j:\"absolute\",bF:\"931\",z:13,k:\"div\",c:39,d:82,r:\"inline\",cQ:0.0000000000,cR:1},\"941\":{h:\"540\",p:\"no-repeat\",x:\"visible\",a:17,q:\"100% 100%\",b:1,j:\"absolute\",bF:\"938\",z:1,k:\"div\",c:46,d:26,r:\"inline\",cQ:0.0000000000,f:-30},\"933\":{h:\"563\",p:\"no-repeat\",x:\"visible\",a:176,q:\"100% 100%\",b:44,j:\"absolute\",bF:\"931\",z:12,k:\"div\",c:39,d:82,r:\"inline\",cQ:0.0000000000,cR:1},\"954\":{h:\"546\",p:\"no-repeat\",x:\"visible\",a:193,q:\"100% 100%\",b:277,j:\"absolute\",bF:\"931\",z:4,k:\"div\",c:21,d:21,r:\"inline\",e:0},\"946\":{h:\"561\",p:\"no-repeat\",x:\"visible\",a:73,q:\"100% 100%\",b:98,j:\"absolute\",bF:\"931\",z:11,k:\"div\",c:458,d:356,r:\"inline\",e:0},\"938\":{k:\"div\",x:\"visible\",c:63,d:91,z:3,a:214,j:\"absolute\",bF:\"931\",b:153}}}],{},g,{\"709\":[[0,0,0,0,0.03923,1,1,1]],g:[[0,0,0.0425,0.22,0.089,1.373,0.169,1.373],[0.169,1.373,0.223,1.373,0.2656,0.868,0.356,0.868],[0.356,0.868,0.4085,0.868,0.457,1.047,0.544,1.047],[0.544,1.047,0.5976,1.047,0.637,0.984,0.731,0.984],[0.731,0.984,0.794,0.984,0.829,1.006,0.919,1.006],[0.919,1.006,0.953,1.006,1,1,1,1]],\"684\":[[0,0,0,0,-0.008064,1,1,1]],f:[[0,0,0.1971,0,0.3391,0.8944,0.3636,1],[0.3636,1,0.3636,1,0.4425,0.75,0.5455,0.75],[0.5455,0.75,0.6519,0.75,0.7273,1,0.7273,1],[0.7273,1,0.7273,1,0.7718,0.9375,0.8182,0.9375],[0.8182,0.9375,0.8646,0.9375,0.9091,1,0.9091,1],[0.9091,1,0.9091,1,0.9294,0.9844,0.9546,0.9844],[0.9546,0.9844,0.9798,0.9844,1,1,1,1]]},null,false,false,-1,true,true,false,true);f[c]=a.API;document.getElementById(e).setAttribute(\"HYP_dn\",\nc);a.z_o(this.body)})();})();\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/galaxy/galaxy.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1,IE=edge\" />\n\t<title>Galaxy</title>\n\t<style>\n\t\thtml {\n\t\t\theight:100%;\n\t\t}\n\t\tbody {\n\t\t\tmargin:0;\n\t\t\theight:100%;\n\t\t}\n\t</style>\n\t<!-- copy these lines to your document head: -->\n\n\t<meta name=\"viewport\" content=\"user-scalable=yes, width=600\" />\n\n\t<!-- end copy -->\n  </head>\n  <body>\n\t<!-- copy these lines to your document: -->\n\n\t<div id=\"galaxy_hype_container\" style=\"margin:auto;position:relative;width:600px;height:500px;overflow:hidden;\" aria-live=\"polite\">\n\t\t<script type=\"text/javascript\" charset=\"utf-8\" src=\"galaxy.hyperesources/galaxy_hype_generated_script.js?18759\"></script>\n\t</div>\n\n\t<!-- end copy -->\n\n\n\n\t<!-- text content for search engines: -->\n\n\t<div style=\"display:none\">\n\n\t\t<div></div>\n\n\t</div>\n\n\t<!-- end text content: -->\n\n  </body>\n</html>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/galaxy/galaxy.hyperesources/PIE.htc",
    "content": "<!--\nPIE: CSS3 rendering for IE\nVersion 1.0.0\nhttp://css3pie.com\nDual-licensed for use under the Apache License Version 2.0 or the General Public License (GPL) Version 2.\n-->\n<PUBLIC:COMPONENT lightWeight=\"true\">\n<!-- saved from url=(0014)about:internet -->\n<PUBLIC:ATTACH EVENT=\"oncontentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondocumentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondetach\" FOR=\"element\" ONEVENT=\"cleanup()\" />\n\n<script type=\"text/javascript\">\nvar doc = element.document;var f=window.PIE;\nif(!f){f=window.PIE={F:\"-pie-\",nb:\"Pie\",La:\"pie_\",Ac:{TD:1,TH:1},cc:{TABLE:1,THEAD:1,TBODY:1,TFOOT:1,TR:1,INPUT:1,TEXTAREA:1,SELECT:1,OPTION:1,IMG:1,HR:1},fc:{A:1,INPUT:1,TEXTAREA:1,SELECT:1,BUTTON:1},Gd:{submit:1,button:1,reset:1},aa:function(){}};try{doc.execCommand(\"BackgroundImageCache\",false,true)}catch(aa){}for(var ba=4,Z=doc.createElement(\"div\"),ca=Z.getElementsByTagName(\"i\"),ga;Z.innerHTML=\"<!--[if gt IE \"+ ++ba+\"]><i></i><![endif]--\\>\",ca[0];);f.O=ba;if(ba===6)f.F=f.F.replace(/^-/,\"\");f.ja=\ndoc.documentMode||f.O;Z.innerHTML='<v:shape adj=\"1\"/>';ga=Z.firstChild;ga.style.behavior=\"url(#default#VML)\";f.zc=typeof ga.adj===\"object\";(function(){var a,b=0,c={};f.p={Za:function(d){if(!a){a=doc.createDocumentFragment();a.namespaces.add(\"css3vml\",\"urn:schemas-microsoft-com:vml\")}return a.createElement(\"css3vml:\"+d)},Ba:function(d){return d&&d._pieId||(d._pieId=\"_\"+ ++b)},Eb:function(d){var e,g,j,i,h=arguments;e=1;for(g=h.length;e<g;e++){i=h[e];for(j in i)if(i.hasOwnProperty(j))d[j]=i[j]}return d},\nRb:function(d,e,g){var j=c[d],i,h;if(j)Object.prototype.toString.call(j)===\"[object Array]\"?j.push([e,g]):e.call(g,j);else{h=c[d]=[[e,g]];i=new Image;i.onload=function(){j=c[d]={h:i.width,f:i.height};for(var k=0,n=h.length;k<n;k++)h[k][0].call(h[k][1],j);i.onload=null};i.src=d}}}})();f.Na={gc:function(a,b,c,d){function e(){k=j>=90&&j<270?b:0;n=j<180?c:0;m=b-k;p=c-n}function g(){for(;j<0;)j+=360;j%=360}var j=d.sa;d=d.zb;var i,h,k,n,m,p,r,t;if(d){d=d.coords(a,b,c);i=d.x;h=d.y}if(j){j=j.jd();g();e();\nif(!d){i=k;h=n}d=f.Na.tc(i,h,j,m,p);a=d[0];d=d[1]}else if(d){a=b-i;d=c-h}else{i=h=a=0;d=c}r=a-i;t=d-h;if(j===void 0){j=!r?t<0?90:270:!t?r<0?180:0:-Math.atan2(t,r)/Math.PI*180;g();e()}return{sa:j,xc:i,yc:h,td:a,ud:d,Wd:k,Xd:n,rd:m,sd:p,kd:r,ld:t,rc:f.Na.dc(i,h,a,d)}},tc:function(a,b,c,d,e){if(c===0||c===180)return[d,b];else if(c===90||c===270)return[a,e];else{c=Math.tan(-c*Math.PI/180);a=c*a-b;b=-1/c;d=b*d-e;e=b-c;return[(d-a)/e,(c*d-b*a)/e]}},dc:function(a,b,c,d){a=c-a;b=d-b;return Math.abs(a===0?\nb:b===0?a:Math.sqrt(a*a+b*b))}};f.ea=function(){this.Gb=[];this.oc={}};f.ea.prototype={ba:function(a){var b=f.p.Ba(a),c=this.oc,d=this.Gb;if(!(b in c)){c[b]=d.length;d.push(a)}},Ha:function(a){a=f.p.Ba(a);var b=this.oc;if(a&&a in b){delete this.Gb[b[a]];delete b[a]}},xa:function(){for(var a=this.Gb,b=a.length;b--;)a[b]&&a[b]()}};f.Oa=new f.ea;f.Oa.Rd=function(){var a=this,b;if(!a.Sd){b=doc.documentElement.currentStyle.getAttribute(f.F+\"poll-interval\")||250;(function c(){a.xa();setTimeout(c,b)})();\na.Sd=1}};(function(){function a(){f.L.xa();window.detachEvent(\"onunload\",a);window.PIE=null}f.L=new f.ea;window.attachEvent(\"onunload\",a);f.L.ta=function(b,c,d){b.attachEvent(c,d);this.ba(function(){b.detachEvent(c,d)})}})();f.Qa=new f.ea;f.L.ta(window,\"onresize\",function(){f.Qa.xa()});(function(){function a(){f.mb.xa()}f.mb=new f.ea;f.L.ta(window,\"onscroll\",a);f.Qa.ba(a)})();(function(){function a(){c=f.kb.md()}function b(){if(c){for(var d=0,e=c.length;d<e;d++)f.attach(c[d]);c=0}}var c;if(f.ja<9){f.L.ta(window,\n\"onbeforeprint\",a);f.L.ta(window,\"onafterprint\",b)}})();f.lb=new f.ea;f.L.ta(doc,\"onmouseup\",function(){f.lb.xa()});f.he=function(){function a(h){this.Y=h}var b=doc.createElement(\"length-calc\"),c=doc.body||doc.documentElement,d=b.style,e={},g=[\"mm\",\"cm\",\"in\",\"pt\",\"pc\"],j=g.length,i={};d.position=\"absolute\";d.top=d.left=\"-9999px\";for(c.appendChild(b);j--;){d.width=\"100\"+g[j];e[g[j]]=b.offsetWidth/100}c.removeChild(b);d.width=\"1em\";a.prototype={Kb:/(px|em|ex|mm|cm|in|pt|pc|%)$/,ic:function(){var h=\nthis.Jd;if(h===void 0)h=this.Jd=parseFloat(this.Y);return h},yb:function(){var h=this.ae;if(!h)h=this.ae=(h=this.Y.match(this.Kb))&&h[0]||\"px\";return h},a:function(h,k){var n=this.ic(),m=this.yb();switch(m){case \"px\":return n;case \"%\":return n*(typeof k===\"function\"?k():k)/100;case \"em\":return n*this.xb(h);case \"ex\":return n*this.xb(h)/2;default:return n*e[m]}},xb:function(h){var k=h.currentStyle.fontSize,n,m;if(k.indexOf(\"px\")>0)return parseFloat(k);else if(h.tagName in f.cc){m=this;n=h.parentNode;\nreturn f.n(k).a(n,function(){return m.xb(n)})}else{h.appendChild(b);k=b.offsetWidth;b.parentNode===h&&h.removeChild(b);return k}}};f.n=function(h){return i[h]||(i[h]=new a(h))};return a}();f.Ja=function(){function a(e){this.X=e}var b=f.n(\"50%\"),c={top:1,center:1,bottom:1},d={left:1,center:1,right:1};a.prototype={zd:function(){if(!this.ac){var e=this.X,g=e.length,j=f.v,i=j.qa,h=f.n(\"0\");i=i.na;h=[\"left\",h,\"top\",h];if(g===1){e.push(new j.ob(i,\"center\"));g++}if(g===2){i&(e[0].k|e[1].k)&&e[0].d in c&&\ne[1].d in d&&e.push(e.shift());if(e[0].k&i)if(e[0].d===\"center\")h[1]=b;else h[0]=e[0].d;else if(e[0].W())h[1]=f.n(e[0].d);if(e[1].k&i)if(e[1].d===\"center\")h[3]=b;else h[2]=e[1].d;else if(e[1].W())h[3]=f.n(e[1].d)}this.ac=h}return this.ac},coords:function(e,g,j){var i=this.zd(),h=i[1].a(e,g);e=i[3].a(e,j);return{x:i[0]===\"right\"?g-h:h,y:i[2]===\"bottom\"?j-e:e}}};return a}();f.Ka=function(){function a(b,c){this.h=b;this.f=c}a.prototype={a:function(b,c,d,e,g){var j=this.h,i=this.f,h=c/d;e=e/g;if(j===\n\"contain\"){j=e>h?c:d*e;i=e>h?c/e:d}else if(j===\"cover\"){j=e<h?c:d*e;i=e<h?c/e:d}else if(j===\"auto\"){i=i===\"auto\"?g:i.a(b,d);j=i*e}else{j=j.a(b,c);i=i===\"auto\"?j/e:i.a(b,d)}return{h:j,f:i}}};a.Kc=new a(\"auto\",\"auto\");return a}();f.Ec=function(){function a(b){this.Y=b}a.prototype={Kb:/[a-z]+$/i,yb:function(){return this.ad||(this.ad=this.Y.match(this.Kb)[0].toLowerCase())},jd:function(){var b=this.Vc,c;if(b===undefined){b=this.yb();c=parseFloat(this.Y,10);b=this.Vc=b===\"deg\"?c:b===\"rad\"?c/Math.PI*180:\nb===\"grad\"?c/400*360:b===\"turn\"?c*360:0}return b}};return a}();f.Jc=function(){function a(c){this.Y=c}var b={};a.Qd=/\\s*rgba\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d+|\\d*\\.\\d+)\\s*\\)\\s*/;a.Fb={aliceblue:\"F0F8FF\",antiquewhite:\"FAEBD7\",aqua:\"0FF\",aquamarine:\"7FFFD4\",azure:\"F0FFFF\",beige:\"F5F5DC\",bisque:\"FFE4C4\",black:\"000\",blanchedalmond:\"FFEBCD\",blue:\"00F\",blueviolet:\"8A2BE2\",brown:\"A52A2A\",burlywood:\"DEB887\",cadetblue:\"5F9EA0\",chartreuse:\"7FFF00\",chocolate:\"D2691E\",coral:\"FF7F50\",cornflowerblue:\"6495ED\",\ncornsilk:\"FFF8DC\",crimson:\"DC143C\",cyan:\"0FF\",darkblue:\"00008B\",darkcyan:\"008B8B\",darkgoldenrod:\"B8860B\",darkgray:\"A9A9A9\",darkgreen:\"006400\",darkkhaki:\"BDB76B\",darkmagenta:\"8B008B\",darkolivegreen:\"556B2F\",darkorange:\"FF8C00\",darkorchid:\"9932CC\",darkred:\"8B0000\",darksalmon:\"E9967A\",darkseagreen:\"8FBC8F\",darkslateblue:\"483D8B\",darkslategray:\"2F4F4F\",darkturquoise:\"00CED1\",darkviolet:\"9400D3\",deeppink:\"FF1493\",deepskyblue:\"00BFFF\",dimgray:\"696969\",dodgerblue:\"1E90FF\",firebrick:\"B22222\",floralwhite:\"FFFAF0\",\nforestgreen:\"228B22\",fuchsia:\"F0F\",gainsboro:\"DCDCDC\",ghostwhite:\"F8F8FF\",gold:\"FFD700\",goldenrod:\"DAA520\",gray:\"808080\",green:\"008000\",greenyellow:\"ADFF2F\",honeydew:\"F0FFF0\",hotpink:\"FF69B4\",indianred:\"CD5C5C\",indigo:\"4B0082\",ivory:\"FFFFF0\",khaki:\"F0E68C\",lavender:\"E6E6FA\",lavenderblush:\"FFF0F5\",lawngreen:\"7CFC00\",lemonchiffon:\"FFFACD\",lightblue:\"ADD8E6\",lightcoral:\"F08080\",lightcyan:\"E0FFFF\",lightgoldenrodyellow:\"FAFAD2\",lightgreen:\"90EE90\",lightgrey:\"D3D3D3\",lightpink:\"FFB6C1\",lightsalmon:\"FFA07A\",\nlightseagreen:\"20B2AA\",lightskyblue:\"87CEFA\",lightslategray:\"789\",lightsteelblue:\"B0C4DE\",lightyellow:\"FFFFE0\",lime:\"0F0\",limegreen:\"32CD32\",linen:\"FAF0E6\",magenta:\"F0F\",maroon:\"800000\",mediumauqamarine:\"66CDAA\",mediumblue:\"0000CD\",mediumorchid:\"BA55D3\",mediumpurple:\"9370D8\",mediumseagreen:\"3CB371\",mediumslateblue:\"7B68EE\",mediumspringgreen:\"00FA9A\",mediumturquoise:\"48D1CC\",mediumvioletred:\"C71585\",midnightblue:\"191970\",mintcream:\"F5FFFA\",mistyrose:\"FFE4E1\",moccasin:\"FFE4B5\",navajowhite:\"FFDEAD\",\nnavy:\"000080\",oldlace:\"FDF5E6\",olive:\"808000\",olivedrab:\"688E23\",orange:\"FFA500\",orangered:\"FF4500\",orchid:\"DA70D6\",palegoldenrod:\"EEE8AA\",palegreen:\"98FB98\",paleturquoise:\"AFEEEE\",palevioletred:\"D87093\",papayawhip:\"FFEFD5\",peachpuff:\"FFDAB9\",peru:\"CD853F\",pink:\"FFC0CB\",plum:\"DDA0DD\",powderblue:\"B0E0E6\",purple:\"800080\",red:\"F00\",rosybrown:\"BC8F8F\",royalblue:\"4169E1\",saddlebrown:\"8B4513\",salmon:\"FA8072\",sandybrown:\"F4A460\",seagreen:\"2E8B57\",seashell:\"FFF5EE\",sienna:\"A0522D\",silver:\"C0C0C0\",skyblue:\"87CEEB\",\nslateblue:\"6A5ACD\",slategray:\"708090\",snow:\"FFFAFA\",springgreen:\"00FF7F\",steelblue:\"4682B4\",tan:\"D2B48C\",teal:\"008080\",thistle:\"D8BFD8\",tomato:\"FF6347\",turquoise:\"40E0D0\",violet:\"EE82EE\",wheat:\"F5DEB3\",white:\"FFF\",whitesmoke:\"F5F5F5\",yellow:\"FF0\",yellowgreen:\"9ACD32\"};a.prototype={parse:function(){if(!this.Ua){var c=this.Y,d;if(d=c.match(a.Qd)){this.Ua=\"rgb(\"+d[1]+\",\"+d[2]+\",\"+d[3]+\")\";this.Yb=parseFloat(d[4])}else{if((d=c.toLowerCase())in a.Fb)c=\"#\"+a.Fb[d];this.Ua=c;this.Yb=c===\"transparent\"?0:\n1}}},U:function(c){this.parse();return this.Ua===\"currentColor\"?c.currentStyle.color:this.Ua},fa:function(){this.parse();return this.Yb}};f.ha=function(c){return b[c]||(b[c]=new a(c))};return a}();f.v=function(){function a(c){this.$a=c;this.ch=0;this.X=[];this.Ga=0}var b=a.qa={Ia:1,Wb:2,z:4,Lc:8,Xb:16,na:32,K:64,oa:128,pa:256,Ra:512,Tc:1024,URL:2048};a.ob=function(c,d){this.k=c;this.d=d};a.ob.prototype={Ca:function(){return this.k&b.K||this.k&b.oa&&this.d===\"0\"},W:function(){return this.Ca()||this.k&\nb.Ra}};a.prototype={de:/\\s/,Kd:/^[\\+\\-]?(\\d*\\.)?\\d+/,url:/^url\\(\\s*(\"([^\"]*)\"|'([^']*)'|([!#$%&*-~]*))\\s*\\)/i,nc:/^\\-?[_a-z][\\w-]*/i,Yd:/^(\"([^\"]*)\"|'([^']*)')/,Bd:/^#([\\da-f]{6}|[\\da-f]{3})/i,be:{px:b.K,em:b.K,ex:b.K,mm:b.K,cm:b.K,\"in\":b.K,pt:b.K,pc:b.K,deg:b.Ia,rad:b.Ia,grad:b.Ia},fd:{rgb:1,rgba:1,hsl:1,hsla:1},next:function(c){function d(p,r){p=new a.ob(p,r);if(!c){k.X.push(p);k.Ga++}return p}function e(){k.Ga++;return null}var g,j,i,h,k=this;if(this.Ga<this.X.length)return this.X[this.Ga++];for(;this.de.test(this.$a.charAt(this.ch));)this.ch++;\nif(this.ch>=this.$a.length)return e();j=this.ch;g=this.$a.substring(this.ch);i=g.charAt(0);switch(i){case \"#\":if(h=g.match(this.Bd)){this.ch+=h[0].length;return d(b.z,h[0])}break;case '\"':case \"'\":if(h=g.match(this.Yd)){this.ch+=h[0].length;return d(b.Tc,h[2]||h[3]||\"\")}break;case \"/\":case \",\":this.ch++;return d(b.pa,i);case \"u\":if(h=g.match(this.url)){this.ch+=h[0].length;return d(b.URL,h[2]||h[3]||h[4]||\"\")}}if(h=g.match(this.Kd)){i=h[0];this.ch+=i.length;if(g.charAt(i.length)===\"%\"){this.ch++;\nreturn d(b.Ra,i+\"%\")}if(h=g.substring(i.length).match(this.nc)){i+=h[0];this.ch+=h[0].length;return d(this.be[h[0].toLowerCase()]||b.Lc,i)}return d(b.oa,i)}if(h=g.match(this.nc)){i=h[0];this.ch+=i.length;if(i.toLowerCase()in f.Jc.Fb||i===\"currentColor\"||i===\"transparent\")return d(b.z,i);if(g.charAt(i.length)===\"(\"){this.ch++;if(i.toLowerCase()in this.fd){g=function(p){return p&&p.k&b.oa};h=function(p){return p&&p.k&(b.oa|b.Ra)};var n=function(p,r){return p&&p.d===r},m=function(){return k.next(1)};\nif((i.charAt(0)===\"r\"?h(m()):g(m()))&&n(m(),\",\")&&h(m())&&n(m(),\",\")&&h(m())&&(i===\"rgb\"||i===\"hsa\"||n(m(),\",\")&&g(m()))&&n(m(),\")\"))return d(b.z,this.$a.substring(j,this.ch));return e()}return d(b.Xb,i)}return d(b.na,i)}this.ch++;return d(b.Wb,i)},D:function(){return this.X[this.Ga-- -2]},all:function(){for(;this.next(););return this.X},ma:function(c,d){for(var e=[],g,j;g=this.next();){if(c(g)){j=true;this.D();break}e.push(g)}return d&&!j?null:e}};return a}();var ha=function(a){this.e=a};ha.prototype=\n{Z:0,Od:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.x!==b.x||a.y!==b.y)},Td:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.h!==b.h||a.f!==b.f)},hc:function(){var a=this.e,b=a.getBoundingClientRect(),c=f.ja===9,d=f.O===7,e=b.right-b.left;return{x:b.left,y:b.top,h:c||d?a.offsetWidth:e,f:c||d?a.offsetHeight:b.bottom-b.top,Hd:d&&e?a.offsetWidth/e:1}},o:function(){return this.Z?this.Va||(this.Va=this.hc()):this.hc()},Ad:function(){return!!this.qb},cb:function(){++this.Z},hb:function(){if(!--this.Z){if(this.Va)this.qb=\nthis.Va;this.Va=null}}};(function(){function a(b){var c=f.p.Ba(b);return function(){if(this.Z){var d=this.$b||(this.$b={});return c in d?d[c]:(d[c]=b.call(this))}else return b.call(this)}}f.B={Z:0,ka:function(b){function c(d){this.e=d;this.Zb=this.ia()}f.p.Eb(c.prototype,f.B,b);c.$c={};return c},j:function(){var b=this.ia(),c=this.constructor.$c;return b?b in c?c[b]:(c[b]=this.la(b)):null},ia:a(function(){var b=this.e,c=this.constructor,d=b.style;b=b.currentStyle;var e=this.wa,g=this.Fa,j=c.Yc||(c.Yc=\nf.F+e);c=c.Zc||(c.Zc=f.nb+g.charAt(0).toUpperCase()+g.substring(1));return d[c]||b.getAttribute(j)||d[g]||b.getAttribute(e)}),i:a(function(){return!!this.j()}),H:a(function(){var b=this.ia(),c=b!==this.Zb;this.Zb=b;return c}),va:a,cb:function(){++this.Z},hb:function(){--this.Z||delete this.$b}}})();f.Sb=f.B.ka({wa:f.F+\"background\",Fa:f.nb+\"Background\",cd:{scroll:1,fixed:1,local:1},fb:{\"repeat-x\":1,\"repeat-y\":1,repeat:1,\"no-repeat\":1},sc:{\"padding-box\":1,\"border-box\":1,\"content-box\":1},Pd:{top:1,right:1,\nbottom:1,left:1,center:1},Ud:{contain:1,cover:1},eb:{Ma:\"backgroundClip\",z:\"backgroundColor\",da:\"backgroundImage\",Pa:\"backgroundOrigin\",S:\"backgroundPosition\",T:\"backgroundRepeat\",Sa:\"backgroundSize\"},la:function(a){function b(s){return s&&s.W()||s.k&k&&s.d in t}function c(s){return s&&(s.W()&&f.n(s.d)||s.d===\"auto\"&&\"auto\")}var d=this.e.currentStyle,e,g,j,i=f.v.qa,h=i.pa,k=i.na,n=i.z,m,p,r=0,t=this.Pd,v,l,q={M:[]};if(this.wb()){e=new f.v(a);for(j={};g=e.next();){m=g.k;p=g.d;if(!j.P&&m&i.Xb&&p===\n\"linear-gradient\"){v={ca:[],P:p};for(l={};g=e.next();){m=g.k;p=g.d;if(m&i.Wb&&p===\")\"){l.color&&v.ca.push(l);v.ca.length>1&&f.p.Eb(j,v);break}if(m&n){if(v.sa||v.zb){g=e.D();if(g.k!==h)break;e.next()}l={color:f.ha(p)};g=e.next();if(g.W())l.db=f.n(g.d);else e.D()}else if(m&i.Ia&&!v.sa&&!l.color&&!v.ca.length)v.sa=new f.Ec(g.d);else if(b(g)&&!v.zb&&!l.color&&!v.ca.length){e.D();v.zb=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&h&&p===\",\"){if(l.color){v.ca.push(l);l={}}}else break}}else if(!j.P&&\nm&i.URL){j.Ab=p;j.P=\"image\"}else if(b(g)&&!j.$){e.D();j.$=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&k)if(p in this.fb&&!j.bb)j.bb=p;else if(p in this.sc&&!j.Wa){j.Wa=p;if((g=e.next())&&g.k&k&&g.d in this.sc)j.ub=g.d;else{j.ub=p;e.D()}}else if(p in this.cd&&!j.bc)j.bc=p;else return null;else if(m&n&&!q.color)q.color=f.ha(p);else if(m&h&&p===\"/\"&&!j.Xa&&j.$){g=e.next();if(g.k&k&&g.d in this.Ud)j.Xa=new f.Ka(g.d);else if(g=c(g)){m=c(e.next());if(!m){m=g;e.D()}j.Xa=new f.Ka(g,m)}else return null}else if(m&\nh&&p===\",\"&&j.P){j.Hb=a.substring(r,e.ch-1);r=e.ch;q.M.push(j);j={}}else return null}if(j.P){j.Hb=a.substring(r);q.M.push(j)}}else this.Bc(f.ja<9?function(){var s=this.eb,o=d[s.S+\"X\"],u=d[s.S+\"Y\"],x=d[s.da],y=d[s.z];if(y!==\"transparent\")q.color=f.ha(y);if(x!==\"none\")q.M=[{P:\"image\",Ab:(new f.v(x)).next().d,bb:d[s.T],$:new f.Ja((new f.v(o+\" \"+u)).all())}]}:function(){var s=this.eb,o=/\\s*,\\s*/,u=d[s.da].split(o),x=d[s.z],y,z,B,E,D,C;if(x!==\"transparent\")q.color=f.ha(x);if((E=u.length)&&u[0]!==\"none\"){x=\nd[s.T].split(o);y=d[s.S].split(o);z=d[s.Pa].split(o);B=d[s.Ma].split(o);s=d[s.Sa].split(o);q.M=[];for(o=0;o<E;o++)if((D=u[o])&&D!==\"none\"){C=s[o].split(\" \");q.M.push({Hb:D+\" \"+x[o]+\" \"+y[o]+\" / \"+s[o]+\" \"+z[o]+\" \"+B[o],P:\"image\",Ab:(new f.v(D)).next().d,bb:x[o],$:new f.Ja((new f.v(y[o])).all()),Wa:z[o],ub:B[o],Xa:new f.Ka(C[0],C[1])})}}});return q.color||q.M[0]?q:null},Bc:function(a){var b=f.ja>8,c=this.eb,d=this.e.runtimeStyle,e=d[c.da],g=d[c.z],j=d[c.T],i,h,k,n;if(e)d[c.da]=\"\";if(g)d[c.z]=\"\";if(j)d[c.T]=\n\"\";if(b){i=d[c.Ma];h=d[c.Pa];n=d[c.S];k=d[c.Sa];if(i)d[c.Ma]=\"\";if(h)d[c.Pa]=\"\";if(n)d[c.S]=\"\";if(k)d[c.Sa]=\"\"}a=a.call(this);if(e)d[c.da]=e;if(g)d[c.z]=g;if(j)d[c.T]=j;if(b){if(i)d[c.Ma]=i;if(h)d[c.Pa]=h;if(n)d[c.S]=n;if(k)d[c.Sa]=k}return a},ia:f.B.va(function(){return this.wb()||this.Bc(function(){var a=this.e.currentStyle,b=this.eb;return a[b.z]+\" \"+a[b.da]+\" \"+a[b.T]+\" \"+a[b.S+\"X\"]+\" \"+a[b.S+\"Y\"]})}),wb:f.B.va(function(){var a=this.e;return a.style[this.Fa]||a.currentStyle.getAttribute(this.wa)}),\nqc:function(){var a=0;if(f.O<7){a=this.e;a=\"\"+(a.style[f.nb+\"PngFix\"]||a.currentStyle.getAttribute(f.F+\"png-fix\"))===\"true\"}return a},i:f.B.va(function(){return(this.wb()||this.qc())&&!!this.j()})});f.Vb=f.B.ka({wc:[\"Top\",\"Right\",\"Bottom\",\"Left\"],Id:{thin:\"1px\",medium:\"3px\",thick:\"5px\"},la:function(){var a={},b={},c={},d=false,e=true,g=true,j=true;this.Cc(function(){for(var i=this.e.currentStyle,h=0,k,n,m,p,r,t,v;h<4;h++){m=this.wc[h];v=m.charAt(0).toLowerCase();k=b[v]=i[\"border\"+m+\"Style\"];n=i[\"border\"+\nm+\"Color\"];m=i[\"border\"+m+\"Width\"];if(h>0){if(k!==p)g=false;if(n!==r)e=false;if(m!==t)j=false}p=k;r=n;t=m;c[v]=f.ha(n);m=a[v]=f.n(b[v]===\"none\"?\"0\":this.Id[m]||m);if(m.a(this.e)>0)d=true}});return d?{J:a,Zd:b,gd:c,ee:j,hd:e,$d:g}:null},ia:f.B.va(function(){var a=this.e,b=a.currentStyle,c;a.tagName in f.Ac&&a.offsetParent.currentStyle.borderCollapse===\"collapse\"||this.Cc(function(){c=b.borderWidth+\"|\"+b.borderStyle+\"|\"+b.borderColor});return c}),Cc:function(a){var b=this.e.runtimeStyle,c=b.borderWidth,\nd=b.borderColor;if(c)b.borderWidth=\"\";if(d)b.borderColor=\"\";a=a.call(this);if(c)b.borderWidth=c;if(d)b.borderColor=d;return a}});(function(){f.jb=f.B.ka({wa:\"border-radius\",Fa:\"borderRadius\",la:function(b){var c=null,d,e,g,j,i=false;if(b){e=new f.v(b);var h=function(){for(var k=[],n;(g=e.next())&&g.W();){j=f.n(g.d);n=j.ic();if(n<0)return null;if(n>0)i=true;k.push(j)}return k.length>0&&k.length<5?{tl:k[0],tr:k[1]||k[0],br:k[2]||k[0],bl:k[3]||k[1]||k[0]}:null};if(b=h()){if(g){if(g.k&f.v.qa.pa&&g.d===\n\"/\")d=h()}else d=b;if(i&&b&&d)c={x:b,y:d}}}return c}});var a=f.n(\"0\");a={tl:a,tr:a,br:a,bl:a};f.jb.Dc={x:a,y:a}})();f.Ub=f.B.ka({wa:\"border-image\",Fa:\"borderImage\",fb:{stretch:1,round:1,repeat:1,space:1},la:function(a){var b=null,c,d,e,g,j,i,h=0,k=f.v.qa,n=k.na,m=k.oa,p=k.Ra;if(a){c=new f.v(a);b={};for(var r=function(l){return l&&l.k&k.pa&&l.d===\"/\"},t=function(l){return l&&l.k&n&&l.d===\"fill\"},v=function(){g=c.ma(function(l){return!(l.k&(m|p))});if(t(c.next())&&!b.fill)b.fill=true;else c.D();if(r(c.next())){h++;\nj=c.ma(function(l){return!l.W()&&!(l.k&n&&l.d===\"auto\")});if(r(c.next())){h++;i=c.ma(function(l){return!l.Ca()})}}else c.D()};a=c.next();){d=a.k;e=a.d;if(d&(m|p)&&!g){c.D();v()}else if(t(a)&&!b.fill){b.fill=true;v()}else if(d&n&&this.fb[e]&&!b.repeat){b.repeat={f:e};if(a=c.next())if(a.k&n&&this.fb[a.d])b.repeat.Ob=a.d;else c.D()}else if(d&k.URL&&!b.src)b.src=e;else return null}if(!b.src||!g||g.length<1||g.length>4||j&&j.length>4||h===1&&j.length<1||i&&i.length>4||h===2&&i.length<1)return null;if(!b.repeat)b.repeat=\n{f:\"stretch\"};if(!b.repeat.Ob)b.repeat.Ob=b.repeat.f;a=function(l,q){return{t:q(l[0]),r:q(l[1]||l[0]),b:q(l[2]||l[0]),l:q(l[3]||l[1]||l[0])}};b.slice=a(g,function(l){return f.n(l.k&m?l.d+\"px\":l.d)});if(j&&j[0])b.J=a(j,function(l){return l.W()?f.n(l.d):l.d});if(i&&i[0])b.Da=a(i,function(l){return l.Ca()?f.n(l.d):l.d})}return b}});f.Ic=f.B.ka({wa:\"box-shadow\",Fa:\"boxShadow\",la:function(a){var b,c=f.n,d=f.v.qa,e;if(a){e=new f.v(a);b={Da:[],Bb:[]};for(a=function(){for(var g,j,i,h,k,n;g=e.next();){i=g.d;\nj=g.k;if(j&d.pa&&i===\",\")break;else if(g.Ca()&&!k){e.D();k=e.ma(function(m){return!m.Ca()})}else if(j&d.z&&!h)h=i;else if(j&d.na&&i===\"inset\"&&!n)n=true;else return false}g=k&&k.length;if(g>1&&g<5){(n?b.Bb:b.Da).push({fe:c(k[0].d),ge:c(k[1].d),blur:c(k[2]?k[2].d:\"0\"),Vd:c(k[3]?k[3].d:\"0\"),color:f.ha(h||\"currentColor\")});return true}return false};a(););}return b&&(b.Bb.length||b.Da.length)?b:null}});f.Uc=f.B.ka({ia:f.B.va(function(){var a=this.e.currentStyle;return a.visibility+\"|\"+a.display}),la:function(){var a=\nthis.e,b=a.runtimeStyle;a=a.currentStyle;var c=b.visibility,d;b.visibility=\"\";d=a.visibility;b.visibility=c;return{ce:d!==\"hidden\",nd:a.display!==\"none\"}},i:function(){return false}});f.u={R:function(a){function b(c,d,e,g){this.e=c;this.s=d;this.g=e;this.parent=g}f.p.Eb(b.prototype,f.u,a);return b},Cb:false,Q:function(){return false},Ea:f.aa,Lb:function(){this.m();this.i()&&this.V()},ib:function(){this.Cb=true},Mb:function(){this.i()?this.V():this.m()},sb:function(a,b){this.vc(a);for(var c=this.ra||\n(this.ra=[]),d=a+1,e=c.length,g;d<e;d++)if(g=c[d])break;c[a]=b;this.I().insertBefore(b,g||null)},za:function(a){var b=this.ra;return b&&b[a]||null},vc:function(a){var b=this.za(a),c=this.Ta;if(b&&c){c.removeChild(b);this.ra[a]=null}},Aa:function(a,b,c,d){var e=this.rb||(this.rb={}),g=e[a];if(!g){g=e[a]=f.p.Za(\"shape\");if(b)g.appendChild(g[b]=f.p.Za(b));if(d){c=this.za(d);if(!c){this.sb(d,doc.createElement(\"group\"+d));c=this.za(d)}}c.appendChild(g);a=g.style;a.position=\"absolute\";a.left=a.top=0;a.behavior=\n\"url(#default#VML)\"}return g},vb:function(a){var b=this.rb,c=b&&b[a];if(c){c.parentNode.removeChild(c);delete b[a]}return!!c},kc:function(a){var b=this.e,c=this.s.o(),d=c.h,e=c.f,g,j,i,h,k,n;c=a.x.tl.a(b,d);g=a.y.tl.a(b,e);j=a.x.tr.a(b,d);i=a.y.tr.a(b,e);h=a.x.br.a(b,d);k=a.y.br.a(b,e);n=a.x.bl.a(b,d);a=a.y.bl.a(b,e);d=Math.min(d/(c+j),e/(i+k),d/(n+h),e/(g+a));if(d<1){c*=d;g*=d;j*=d;i*=d;h*=d;k*=d;n*=d;a*=d}return{x:{tl:c,tr:j,br:h,bl:n},y:{tl:g,tr:i,br:k,bl:a}}},ya:function(a,b,c){b=b||1;var d,e,\ng=this.s.o();e=g.h*b;g=g.f*b;var j=this.g.G,i=Math.floor,h=Math.ceil,k=a?a.Jb*b:0,n=a?a.Ib*b:0,m=a?a.tb*b:0;a=a?a.Db*b:0;var p,r,t,v,l;if(c||j.i()){d=this.kc(c||j.j());c=d.x.tl*b;j=d.y.tl*b;p=d.x.tr*b;r=d.y.tr*b;t=d.x.br*b;v=d.y.br*b;l=d.x.bl*b;b=d.y.bl*b;e=\"m\"+i(a)+\",\"+i(j)+\"qy\"+i(c)+\",\"+i(k)+\"l\"+h(e-p)+\",\"+i(k)+\"qx\"+h(e-n)+\",\"+i(r)+\"l\"+h(e-n)+\",\"+h(g-v)+\"qy\"+h(e-t)+\",\"+h(g-m)+\"l\"+i(l)+\",\"+h(g-m)+\"qx\"+i(a)+\",\"+h(g-b)+\" x e\"}else e=\"m\"+i(a)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+h(g-m)+\"l\"+i(a)+\n\",\"+h(g-m)+\"xe\";return e},I:function(){var a=this.parent.za(this.N),b;if(!a){a=doc.createElement(this.Ya);b=a.style;b.position=\"absolute\";b.top=b.left=0;this.parent.sb(this.N,a)}return a},mc:function(){var a=this.e,b=a.currentStyle,c=a.runtimeStyle,d=a.tagName,e=f.O===6,g;if(e&&(d in f.cc||d===\"FIELDSET\")||d===\"BUTTON\"||d===\"INPUT\"&&a.type in f.Gd){c.borderWidth=\"\";d=this.g.w.wc;for(g=d.length;g--;){e=d[g];c[\"padding\"+e]=\"\";c[\"padding\"+e]=f.n(b[\"padding\"+e]).a(a)+f.n(b[\"border\"+e+\"Width\"]).a(a)+(f.O!==\n8&&g%2?1:0)}c.borderWidth=0}else if(e){if(a.childNodes.length!==1||a.firstChild.tagName!==\"ie6-mask\"){b=doc.createElement(\"ie6-mask\");d=b.style;d.visibility=\"visible\";for(d.zoom=1;d=a.firstChild;)b.appendChild(d);a.appendChild(b);c.visibility=\"hidden\"}}else c.borderColor=\"transparent\"},ie:function(){},m:function(){this.parent.vc(this.N);delete this.rb;delete this.ra}};f.Rc=f.u.R({i:function(){var a=this.ed;for(var b in a)if(a.hasOwnProperty(b)&&a[b].i())return true;return false},Q:function(){return this.g.Pb.H()},\nib:function(){if(this.i()){var a=this.jc(),b=a,c;a=a.currentStyle;var d=a.position,e=this.I().style,g=0,j=0;j=this.s.o();var i=j.Hd;if(d===\"fixed\"&&f.O>6){g=j.x*i;j=j.y*i;b=d}else{do b=b.offsetParent;while(b&&b.currentStyle.position===\"static\");if(b){c=b.getBoundingClientRect();b=b.currentStyle;g=(j.x-c.left)*i-(parseFloat(b.borderLeftWidth)||0);j=(j.y-c.top)*i-(parseFloat(b.borderTopWidth)||0)}else{b=doc.documentElement;g=(j.x+b.scrollLeft-b.clientLeft)*i;j=(j.y+b.scrollTop-b.clientTop)*i}b=\"absolute\"}e.position=\nb;e.left=g;e.top=j;e.zIndex=d===\"static\"?-1:a.zIndex;this.Cb=true}},Mb:f.aa,Nb:function(){var a=this.g.Pb.j();this.I().style.display=a.ce&&a.nd?\"\":\"none\"},Lb:function(){this.i()?this.Nb():this.m()},jc:function(){var a=this.e;return a.tagName in f.Ac?a.offsetParent:a},I:function(){var a=this.Ta,b;if(!a){b=this.jc();a=this.Ta=doc.createElement(\"css3-container\");a.style.direction=\"ltr\";this.Nb();b.parentNode.insertBefore(a,b)}return a},ab:f.aa,m:function(){var a=this.Ta,b;if(a&&(b=a.parentNode))b.removeChild(a);\ndelete this.Ta;delete this.ra}});f.Fc=f.u.R({N:2,Ya:\"background\",Q:function(){var a=this.g;return a.C.H()||a.G.H()},i:function(){var a=this.g;return a.q.i()||a.G.i()||a.C.i()||a.ga.i()&&a.ga.j().Bb},V:function(){var a=this.s.o();if(a.h&&a.f){this.od();this.pd()}},od:function(){var a=this.g.C.j(),b=this.s.o(),c=this.e,d=a&&a.color,e,g;if(d&&d.fa()>0){this.lc();a=this.Aa(\"bgColor\",\"fill\",this.I(),1);e=b.h;b=b.f;a.stroked=false;a.coordsize=e*2+\",\"+b*2;a.coordorigin=\"1,1\";a.path=this.ya(null,2);g=a.style;\ng.width=e;g.height=b;a.fill.color=d.U(c);c=d.fa();if(c<1)a.fill.opacity=c}else this.vb(\"bgColor\")},pd:function(){var a=this.g.C.j(),b=this.s.o();a=a&&a.M;var c,d,e,g,j;if(a){this.lc();d=b.h;e=b.f;for(j=a.length;j--;){b=a[j];c=this.Aa(\"bgImage\"+j,\"fill\",this.I(),2);c.stroked=false;c.fill.type=\"tile\";c.fillcolor=\"none\";c.coordsize=d*2+\",\"+e*2;c.coordorigin=\"1,1\";c.path=this.ya(0,2);g=c.style;g.width=d;g.height=e;if(b.P===\"linear-gradient\")this.bd(c,b);else{c.fill.src=b.Ab;this.Nd(c,j)}}}for(j=a?a.length:\n0;this.vb(\"bgImage\"+j++););},Nd:function(a,b){var c=this;f.p.Rb(a.fill.src,function(d){var e=c.e,g=c.s.o(),j=g.h;g=g.f;if(j&&g){var i=a.fill,h=c.g,k=h.w.j(),n=k&&k.J;k=n?n.t.a(e):0;var m=n?n.r.a(e):0,p=n?n.b.a(e):0;n=n?n.l.a(e):0;h=h.C.j().M[b];e=h.$?h.$.coords(e,j-d.h-n-m,g-d.f-k-p):{x:0,y:0};h=h.bb;p=m=0;var r=j+1,t=g+1,v=f.O===8?0:1;n=Math.round(e.x)+n+0.5;k=Math.round(e.y)+k+0.5;i.position=n/j+\",\"+k/g;i.size.x=1;i.size=d.h+\"px,\"+d.f+\"px\";if(h&&h!==\"repeat\"){if(h===\"repeat-x\"||h===\"no-repeat\"){m=\nk+1;t=k+d.f+v}if(h===\"repeat-y\"||h===\"no-repeat\"){p=n+1;r=n+d.h+v}a.style.clip=\"rect(\"+m+\"px,\"+r+\"px,\"+t+\"px,\"+p+\"px)\"}}})},bd:function(a,b){var c=this.e,d=this.s.o(),e=d.h,g=d.f;a=a.fill;d=b.ca;var j=d.length,i=Math.PI,h=f.Na,k=h.tc,n=h.dc;b=h.gc(c,e,g,b);h=b.sa;var m=b.xc,p=b.yc,r=b.Wd,t=b.Xd,v=b.rd,l=b.sd,q=b.kd,s=b.ld;b=b.rc;e=h%90?Math.atan2(q*e/g,s)/i*180:h+90;e+=180;e%=360;v=k(r,t,h,v,l);g=n(r,t,v[0],v[1]);i=[];v=k(m,p,h,r,t);n=n(m,p,v[0],v[1])/g*100;k=[];for(h=0;h<j;h++)k.push(d[h].db?d[h].db.a(c,\nb):h===0?0:h===j-1?b:null);for(h=1;h<j;h++){if(k[h]===null){m=k[h-1];b=h;do p=k[++b];while(p===null);k[h]=m+(p-m)/(b-h+1)}k[h]=Math.max(k[h],k[h-1])}for(h=0;h<j;h++)i.push(n+k[h]/g*100+\"% \"+d[h].color.U(c));a.angle=e;a.type=\"gradient\";a.method=\"sigma\";a.color=d[0].color.U(c);a.color2=d[j-1].color.U(c);if(a.colors)a.colors.value=i.join(\",\");else a.colors=i.join(\",\")},lc:function(){var a=this.e.runtimeStyle;a.backgroundImage=\"url(about:blank)\";a.backgroundColor=\"transparent\"},m:function(){f.u.m.call(this);\nvar a=this.e.runtimeStyle;a.backgroundImage=a.backgroundColor=\"\"}});f.Gc=f.u.R({N:4,Ya:\"border\",Q:function(){var a=this.g;return a.w.H()||a.G.H()},i:function(){var a=this.g;return a.G.i()&&!a.q.i()&&a.w.i()},V:function(){var a=this.e,b=this.g.w.j(),c=this.s.o(),d=c.h;c=c.f;var e,g,j,i,h;if(b){this.mc();b=this.wd(2);i=0;for(h=b.length;i<h;i++){j=b[i];e=this.Aa(\"borderPiece\"+i,j.stroke?\"stroke\":\"fill\",this.I());e.coordsize=d*2+\",\"+c*2;e.coordorigin=\"1,1\";e.path=j.path;g=e.style;g.width=d;g.height=c;\ne.filled=!!j.fill;e.stroked=!!j.stroke;if(j.stroke){e=e.stroke;e.weight=j.Qb+\"px\";e.color=j.color.U(a);e.dashstyle=j.stroke===\"dashed\"?\"2 2\":j.stroke===\"dotted\"?\"1 1\":\"solid\";e.linestyle=j.stroke===\"double\"&&j.Qb>2?\"ThinThin\":\"Single\"}else e.fill.color=j.fill.U(a)}for(;this.vb(\"borderPiece\"+i++););}},wd:function(a){var b=this.e,c,d,e,g=this.g.w,j=[],i,h,k,n,m=Math.round,p,r,t;if(g.i()){c=g.j();g=c.J;r=c.Zd;t=c.gd;if(c.ee&&c.$d&&c.hd){if(t.t.fa()>0){c=g.t.a(b);k=c/2;j.push({path:this.ya({Jb:k,Ib:k,\ntb:k,Db:k},a),stroke:r.t,color:t.t,Qb:c})}}else{a=a||1;c=this.s.o();d=c.h;e=c.f;c=m(g.t.a(b));k=m(g.r.a(b));n=m(g.b.a(b));b=m(g.l.a(b));var v={t:c,r:k,b:n,l:b};b=this.g.G;if(b.i())p=this.kc(b.j());i=Math.floor;h=Math.ceil;var l=function(o,u){return p?p[o][u]:0},q=function(o,u,x,y,z,B){var E=l(\"x\",o),D=l(\"y\",o),C=o.charAt(1)===\"r\";o=o.charAt(0)===\"b\";return E>0&&D>0?(B?\"al\":\"ae\")+(C?h(d-E):i(E))*a+\",\"+(o?h(e-D):i(D))*a+\",\"+(i(E)-u)*a+\",\"+(i(D)-x)*a+\",\"+y*65535+\",\"+2949075*(z?1:-1):(B?\"m\":\"l\")+(C?d-\nu:u)*a+\",\"+(o?e-x:x)*a},s=function(o,u,x,y){var z=o===\"t\"?i(l(\"x\",\"tl\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+i(l(\"y\",\"tr\"))*a:o===\"b\"?h(d-l(\"x\",\"br\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+h(e-l(\"y\",\"bl\"))*a;o=o===\"t\"?h(d-l(\"x\",\"tr\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+h(e-l(\"y\",\"br\"))*a:o===\"b\"?i(l(\"x\",\"bl\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+i(l(\"y\",\"tl\"))*a;return x?(y?\"m\"+o:\"\")+\"l\"+z:(y?\"m\"+z:\"\")+\"l\"+o};b=function(o,u,x,y,z,B){var E=o===\"l\"||o===\"r\",D=v[o],C,F;if(D>0&&r[o]!==\"none\"&&t[o].fa()>0){C=v[E?o:u];u=v[E?u:\no];F=v[E?o:x];x=v[E?x:o];if(r[o]===\"dashed\"||r[o]===\"dotted\"){j.push({path:q(y,C,u,B+45,0,1)+q(y,0,0,B,1,0),fill:t[o]});j.push({path:s(o,D/2,0,1),stroke:r[o],Qb:D,color:t[o]});j.push({path:q(z,F,x,B,0,1)+q(z,0,0,B-45,1,0),fill:t[o]})}else j.push({path:q(y,C,u,B+45,0,1)+s(o,D,0,0)+q(z,F,x,B,0,0)+(r[o]===\"double\"&&D>2?q(z,F-i(F/3),x-i(x/3),B-45,1,0)+s(o,h(D/3*2),1,0)+q(y,C-i(C/3),u-i(u/3),B,1,0)+\"x \"+q(y,i(C/3),i(u/3),B+45,0,1)+s(o,i(D/3),1,0)+q(z,i(F/3),i(x/3),B,0,0):\"\")+q(z,0,0,B-45,1,0)+s(o,0,1,\n0)+q(y,0,0,B,1,0),fill:t[o]})}};b(\"t\",\"l\",\"r\",\"tl\",\"tr\",90);b(\"r\",\"t\",\"b\",\"tr\",\"br\",0);b(\"b\",\"r\",\"l\",\"br\",\"bl\",-90);b(\"l\",\"b\",\"t\",\"bl\",\"tl\",-180)}}return j},m:function(){if(this.ec||!this.g.q.i())this.e.runtimeStyle.borderColor=\"\";f.u.m.call(this)}});f.Tb=f.u.R({N:5,Md:[\"t\",\"tr\",\"r\",\"br\",\"b\",\"bl\",\"l\",\"tl\",\"c\"],Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){this.I();var a=this.g.q.j(),b=this.g.w.j(),c=this.s.o(),d=this.e,e=this.uc;f.p.Rb(a.src,function(g){function j(s,\no,u,x,y){s=e[s].style;var z=Math.max;s.width=z(o,0);s.height=z(u,0);s.left=x;s.top=y}function i(s,o,u){for(var x=0,y=s.length;x<y;x++)e[s[x]].imagedata[o]=u}var h=c.h,k=c.f,n=f.n(\"0\"),m=a.J||(b?b.J:{t:n,r:n,b:n,l:n});n=m.t.a(d);var p=m.r.a(d),r=m.b.a(d);m=m.l.a(d);var t=a.slice,v=t.t.a(d),l=t.r.a(d),q=t.b.a(d);t=t.l.a(d);j(\"tl\",m,n,0,0);j(\"t\",h-m-p,n,m,0);j(\"tr\",p,n,h-p,0);j(\"r\",p,k-n-r,h-p,n);j(\"br\",p,r,h-p,k-r);j(\"b\",h-m-p,r,m,k-r);j(\"bl\",m,r,0,k-r);j(\"l\",m,k-n-r,0,n);j(\"c\",h-m-p,k-n-r,m,n);i([\"tl\",\n\"t\",\"tr\"],\"cropBottom\",(g.f-v)/g.f);i([\"tl\",\"l\",\"bl\"],\"cropRight\",(g.h-t)/g.h);i([\"bl\",\"b\",\"br\"],\"cropTop\",(g.f-q)/g.f);i([\"tr\",\"r\",\"br\"],\"cropLeft\",(g.h-l)/g.h);i([\"l\",\"r\",\"c\"],\"cropTop\",v/g.f);i([\"l\",\"r\",\"c\"],\"cropBottom\",q/g.f);i([\"t\",\"b\",\"c\"],\"cropLeft\",t/g.h);i([\"t\",\"b\",\"c\"],\"cropRight\",l/g.h);e.c.style.display=a.fill?\"\":\"none\"},this)},I:function(){var a=this.parent.za(this.N),b,c,d,e=this.Md,g=e.length;if(!a){a=doc.createElement(\"border-image\");b=a.style;b.position=\"absolute\";this.uc={};for(d=\n0;d<g;d++){c=this.uc[e[d]]=f.p.Za(\"rect\");c.appendChild(f.p.Za(\"imagedata\"));b=c.style;b.behavior=\"url(#default#VML)\";b.position=\"absolute\";b.top=b.left=0;c.imagedata.src=this.g.q.j().src;c.stroked=false;c.filled=false;a.appendChild(c)}this.parent.sb(this.N,a)}return a},Ea:function(){if(this.i()){var a=this.e,b=a.runtimeStyle,c=this.g.q.j().J;b.borderStyle=\"solid\";if(c){b.borderTopWidth=c.t.a(a)+\"px\";b.borderRightWidth=c.r.a(a)+\"px\";b.borderBottomWidth=c.b.a(a)+\"px\";b.borderLeftWidth=c.l.a(a)+\"px\"}this.mc()}},\nm:function(){var a=this.e.runtimeStyle;a.borderStyle=\"\";if(this.ec||!this.g.w.i())a.borderColor=a.borderWidth=\"\";f.u.m.call(this)}});f.Hc=f.u.R({N:1,Ya:\"outset-box-shadow\",Q:function(){var a=this.g;return a.ga.H()||a.G.H()},i:function(){var a=this.g.ga;return a.i()&&a.j().Da[0]},V:function(){function a(C,F,O,H,M,P,I){C=b.Aa(\"shadow\"+C+F,\"fill\",d,j-C);F=C.fill;C.coordsize=n*2+\",\"+m*2;C.coordorigin=\"1,1\";C.stroked=false;C.filled=true;F.color=M.U(c);if(P){F.type=\"gradienttitle\";F.color2=F.color;F.opacity=\n0}C.path=I;l=C.style;l.left=O;l.top=H;l.width=n;l.height=m;return C}var b=this,c=this.e,d=this.I(),e=this.g,g=e.ga.j().Da;e=e.G.j();var j=g.length,i=j,h,k=this.s.o(),n=k.h,m=k.f;k=f.O===8?1:0;for(var p=[\"tl\",\"tr\",\"br\",\"bl\"],r,t,v,l,q,s,o,u,x,y,z,B,E,D;i--;){t=g[i];q=t.fe.a(c);s=t.ge.a(c);h=t.Vd.a(c);o=t.blur.a(c);t=t.color;u=-h-o;if(!e&&o)e=f.jb.Dc;u=this.ya({Jb:u,Ib:u,tb:u,Db:u},2,e);if(o){x=(h+o)*2+n;y=(h+o)*2+m;z=x?o*2/x:0;B=y?o*2/y:0;if(o-h>n/2||o-h>m/2)for(h=4;h--;){r=p[h];E=r.charAt(0)===\"b\";\nD=r.charAt(1)===\"r\";r=a(i,r,q,s,t,o,u);v=r.fill;v.focusposition=(D?1-z:z)+\",\"+(E?1-B:B);v.focussize=\"0,0\";r.style.clip=\"rect(\"+((E?y/2:0)+k)+\"px,\"+(D?x:x/2)+\"px,\"+(E?y:y/2)+\"px,\"+((D?x/2:0)+k)+\"px)\"}else{r=a(i,\"\",q,s,t,o,u);v=r.fill;v.focusposition=z+\",\"+B;v.focussize=1-z*2+\",\"+(1-B*2)}}else{r=a(i,\"\",q,s,t,o,u);q=t.fa();if(q<1)r.fill.opacity=q}}}});f.Pc=f.u.R({N:6,Ya:\"imgEl\",Q:function(){var a=this.g;return this.e.src!==this.Xc||a.G.H()},i:function(){var a=this.g;return a.G.i()||a.C.qc()},V:function(){this.Xc=\nj;this.Cd();var a=this.Aa(\"img\",\"fill\",this.I()),b=a.fill,c=this.s.o(),d=c.h;c=c.f;var e=this.g.w.j(),g=e&&e.J;e=this.e;var j=e.src,i=Math.round,h=e.currentStyle,k=f.n;if(!g||f.O<7){g=f.n(\"0\");g={t:g,r:g,b:g,l:g}}a.stroked=false;b.type=\"frame\";b.src=j;b.position=(d?0.5/d:0)+\",\"+(c?0.5/c:0);a.coordsize=d*2+\",\"+c*2;a.coordorigin=\"1,1\";a.path=this.ya({Jb:i(g.t.a(e)+k(h.paddingTop).a(e)),Ib:i(g.r.a(e)+k(h.paddingRight).a(e)),tb:i(g.b.a(e)+k(h.paddingBottom).a(e)),Db:i(g.l.a(e)+k(h.paddingLeft).a(e))},\n2);a=a.style;a.width=d;a.height=c},Cd:function(){this.e.runtimeStyle.filter=\"alpha(opacity=0)\"},m:function(){f.u.m.call(this);this.e.runtimeStyle.filter=\"\"}});f.Oc=f.u.R({ib:f.aa,Mb:f.aa,Nb:f.aa,Lb:f.aa,Ld:/^,+|,+$/g,Fd:/,+/g,gb:function(a,b){(this.pb||(this.pb=[]))[a]=b||void 0},ab:function(){var a=this.pb,b;if(a&&(b=a.join(\",\").replace(this.Ld,\"\").replace(this.Fd,\",\"))!==this.Wc)this.Wc=this.e.runtimeStyle.background=b},m:function(){this.e.runtimeStyle.background=\"\";delete this.pb}});f.Mc=f.u.R({ua:1,\nQ:function(){return this.g.C.H()},i:function(){var a=this.g;return a.C.i()||a.q.i()},V:function(){var a=this.g.C.j(),b,c,d=0,e,g;if(a){b=[];if(c=a.M)for(;e=c[d++];)if(e.P===\"linear-gradient\"){g=this.vd(e.Wa);g=(e.Xa||f.Ka.Kc).a(this.e,g.h,g.f,g.h,g.f);b.push(\"url(data:image/svg+xml,\"+escape(this.xd(e,g.h,g.f))+\") \"+this.dd(e.$)+\" / \"+g.h+\"px \"+g.f+\"px \"+(e.bc||\"\")+\" \"+(e.Wa||\"\")+\" \"+(e.ub||\"\"))}else b.push(e.Hb);a.color&&b.push(a.color.Y);this.parent.gb(this.ua,b.join(\",\"))}},dd:function(a){return a?\na.X.map(function(b){return b.d}).join(\" \"):\"0 0\"},vd:function(a){var b=this.e,c=this.s.o(),d=c.h;c=c.f;var e;if(a!==\"border-box\")if((e=this.g.w.j())&&(e=e.J)){d-=e.l.a(b)+e.l.a(b);c-=e.t.a(b)+e.b.a(b)}if(a===\"content-box\"){a=f.n;e=b.currentStyle;d-=a(e.paddingLeft).a(b)+a(e.paddingRight).a(b);c-=a(e.paddingTop).a(b)+a(e.paddingBottom).a(b)}return{h:d,f:c}},xd:function(a,b,c){var d=this.e,e=a.ca,g=e.length,j=f.Na.gc(d,b,c,a);a=j.xc;var i=j.yc,h=j.td,k=j.ud;j=j.rc;var n,m,p,r,t;n=[];for(m=0;m<g;m++)n.push(e[m].db?\ne[m].db.a(d,j):m===0?0:m===g-1?j:null);for(m=1;m<g;m++)if(n[m]===null){r=n[m-1];p=m;do t=n[++p];while(t===null);n[m]=r+(t-r)/(p-m+1)}b=['<svg width=\"'+b+'\" height=\"'+c+'\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"g\" gradientUnits=\"userSpaceOnUse\" x1=\"'+a/b*100+'%\" y1=\"'+i/c*100+'%\" x2=\"'+h/b*100+'%\" y2=\"'+k/c*100+'%\">'];for(m=0;m<g;m++)b.push('<stop offset=\"'+n[m]/j+'\" stop-color=\"'+e[m].color.U(d)+'\" stop-opacity=\"'+e[m].color.fa()+'\"/>');b.push('</linearGradient></defs><rect width=\"100%\" height=\"100%\" fill=\"url(#g)\"/></svg>');\nreturn b.join(\"\")},m:function(){this.parent.gb(this.ua)}});f.Nc=f.u.R({T:\"repeat\",Sc:\"stretch\",Qc:\"round\",ua:0,Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){var a=this,b=a.g.q.j(),c=a.g.w.j(),d=a.s.o(),e=b.repeat,g=e.f,j=e.Ob,i=a.e,h=0;f.p.Rb(b.src,function(k){function n(Q,R,U,V,W,Y,X,S,w,A){K.push('<pattern patternUnits=\"userSpaceOnUse\" id=\"pattern'+G+'\" x=\"'+(g===l?Q+U/2-w/2:Q)+'\" y=\"'+(j===l?R+V/2-A/2:R)+'\" width=\"'+w+'\" height=\"'+A+'\"><svg width=\"'+w+'\" height=\"'+\nA+'\" viewBox=\"'+W+\" \"+Y+\" \"+X+\" \"+S+'\" preserveAspectRatio=\"none\"><image xlink:href=\"'+v+'\" x=\"0\" y=\"0\" width=\"'+r+'\" height=\"'+t+'\" /></svg></pattern>');J.push('<rect x=\"'+Q+'\" y=\"'+R+'\" width=\"'+U+'\" height=\"'+V+'\" fill=\"url(#pattern'+G+')\" />');G++}var m=d.h,p=d.f,r=k.h,t=k.f,v=a.Dd(b.src,r,t),l=a.T,q=a.Sc;k=a.Qc;var s=Math.ceil,o=f.n(\"0\"),u=b.J||(c?c.J:{t:o,r:o,b:o,l:o});o=u.t.a(i);var x=u.r.a(i),y=u.b.a(i);u=u.l.a(i);var z=b.slice,B=z.t.a(i),E=z.r.a(i),D=z.b.a(i);z=z.l.a(i);var C=m-u-x,F=p-o-\ny,O=r-z-E,H=t-B-D,M=g===q?C:O*o/B,P=j===q?F:H*x/E,I=g===q?C:O*y/D;q=j===q?F:H*u/z;var K=[],J=[],G=0;if(g===k){M-=(M-(C%M||M))/s(C/M);I-=(I-(C%I||I))/s(C/I)}if(j===k){P-=(P-(F%P||P))/s(F/P);q-=(q-(F%q||q))/s(F/q)}k=['<svg width=\"'+m+'\" height=\"'+p+'\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">'];n(0,0,u,o,0,0,z,B,u,o);n(u,0,C,o,z,0,O,B,M,o);n(m-x,0,x,o,r-E,0,E,B,x,o);n(0,o,u,F,0,B,z,H,u,q);if(b.fill)n(u,o,C,F,z,B,O,H,M||I||O,q||P||H);n(m-x,o,x,F,r-E,B,E,H,x,P);n(0,\np-y,u,y,0,t-D,z,D,u,y);n(u,p-y,C,y,z,t-D,O,D,I,y);n(m-x,p-y,x,y,r-E,t-D,E,D,x,y);k.push(\"<defs>\"+K.join(\"\\n\")+\"</defs>\"+J.join(\"\\n\")+\"</svg>\");a.parent.gb(a.ua,\"url(data:image/svg+xml,\"+escape(k.join(\"\"))+\") no-repeat border-box border-box\");h&&a.parent.ab()},a);h=1},Dd:function(){var a={};return function(b,c,d){var e=a[b],g;if(!e){e=new Image;g=doc.createElement(\"canvas\");e.src=b;g.width=c;g.height=d;g.getContext(\"2d\").drawImage(e,0,0);e=a[b]=g.toDataURL()}return e}}(),Ea:f.Tb.prototype.Ea,m:function(){var a=\nthis.e.runtimeStyle;this.parent.gb(this.ua);a.borderColor=a.borderStyle=a.borderWidth=\"\"}});f.kb=function(){function a(l,q){l.className+=\" \"+q}function b(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;)a(l,q[s])},0)}function c(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;){var o=q[s];o=t[o]||(t[o]=new RegExp(\"\\\\b\"+o+\"\\\\b\",\"g\"));l.className=l.className.replace(o,\"\")}},0)}function d(l){function q(){if(!U){var w,A,L=f.ja,T=l.currentStyle,\nN=T.getAttribute(g)===\"true\",da=T.getAttribute(i)!==\"false\",ea=T.getAttribute(h)!==\"false\";S=T.getAttribute(j);S=L>7?S!==\"false\":S===\"true\";if(!R){R=1;l.runtimeStyle.zoom=1;T=l;for(var fa=1;T=T.previousSibling;)if(T.nodeType===1){fa=0;break}fa&&a(l,p)}J.cb();if(N&&(A=J.o())&&(w=doc.documentElement||doc.body)&&(A.y>w.clientHeight||A.x>w.clientWidth||A.y+A.f<0||A.x+A.h<0)){if(!Y){Y=1;f.mb.ba(q)}}else{U=1;Y=R=0;f.mb.Ha(q);if(L===9){G={C:new f.Sb(l),q:new f.Ub(l),w:new f.Vb(l)};Q=[G.C,G.q];K=new f.Oc(l,\nJ,G);w=[new f.Mc(l,J,G,K),new f.Nc(l,J,G,K)]}else{G={C:new f.Sb(l),w:new f.Vb(l),q:new f.Ub(l),G:new f.jb(l),ga:new f.Ic(l),Pb:new f.Uc(l)};Q=[G.C,G.w,G.q,G.G,G.ga,G.Pb];K=new f.Rc(l,J,G);w=[new f.Hc(l,J,G,K),new f.Fc(l,J,G,K),new f.Gc(l,J,G,K),new f.Tb(l,J,G,K)];l.tagName===\"IMG\"&&w.push(new f.Pc(l,J,G,K));K.ed=w}I=[K].concat(w);if(w=l.currentStyle.getAttribute(f.F+\"watch-ancestors\")){w=parseInt(w,10);A=0;for(N=l.parentNode;N&&(w===\"NaN\"||A++<w);){H(N,\"onpropertychange\",C);H(N,\"onmouseenter\",x);\nH(N,\"onmouseleave\",y);H(N,\"onmousedown\",z);if(N.tagName in f.fc){H(N,\"onfocus\",E);H(N,\"onblur\",D)}N=N.parentNode}}if(S){f.Oa.ba(o);f.Oa.Rd()}o(1)}if(!V){V=1;L<9&&H(l,\"onmove\",s);H(l,\"onresize\",s);H(l,\"onpropertychange\",u);ea&&H(l,\"onmouseenter\",x);if(ea||da)H(l,\"onmouseleave\",y);da&&H(l,\"onmousedown\",z);if(l.tagName in f.fc){H(l,\"onfocus\",E);H(l,\"onblur\",D)}f.Qa.ba(s);f.L.ba(M)}J.hb()}}function s(){J&&J.Ad()&&o()}function o(w){if(!X)if(U){var A,L=I.length;F();for(A=0;A<L;A++)I[A].Ea();if(w||J.Od())for(A=\n0;A<L;A++)I[A].ib();if(w||J.Td())for(A=0;A<L;A++)I[A].Mb();K.ab();O()}else R||q()}function u(){var w,A=I.length,L;w=event;if(!X&&!(w&&w.propertyName in r))if(U){F();for(w=0;w<A;w++)I[w].Ea();for(w=0;w<A;w++){L=I[w];L.Cb||L.ib();L.Q()&&L.Lb()}K.ab();O()}else R||q()}function x(){b(l,k)}function y(){c(l,k,n)}function z(){b(l,n);f.lb.ba(B)}function B(){c(l,n);f.lb.Ha(B)}function E(){b(l,m)}function D(){c(l,m)}function C(){var w=event.propertyName;if(w===\"className\"||w===\"id\")u()}function F(){J.cb();for(var w=\nQ.length;w--;)Q[w].cb()}function O(){for(var w=Q.length;w--;)Q[w].hb();J.hb()}function H(w,A,L){w.attachEvent(A,L);W.push([w,A,L])}function M(){if(V){for(var w=W.length,A;w--;){A=W[w];A[0].detachEvent(A[1],A[2])}f.L.Ha(M);V=0;W=[]}}function P(){if(!X){var w,A;M();X=1;if(I){w=0;for(A=I.length;w<A;w++){I[w].ec=1;I[w].m()}}S&&f.Oa.Ha(o);f.Qa.Ha(o);I=J=G=Q=l=null}}var I,K,J=new ha(l),G,Q,R,U,V,W=[],Y,X,S;this.Ed=q;this.update=o;this.m=P;this.qd=l}var e={},g=f.F+\"lazy-init\",j=f.F+\"poll\",i=f.F+\"track-active\",\nh=f.F+\"track-hover\",k=f.La+\"hover\",n=f.La+\"active\",m=f.La+\"focus\",p=f.La+\"first-child\",r={background:1,bgColor:1,display:1},t={},v=[];d.yd=function(l){var q=f.p.Ba(l);return e[q]||(e[q]=new d(l))};d.m=function(l){l=f.p.Ba(l);var q=e[l];if(q){q.m();delete e[l]}};d.md=function(){var l=[],q;if(e){for(var s in e)if(e.hasOwnProperty(s)){q=e[s];l.push(q.qd);q.m()}e={}}return l};return d}();f.supportsVML=f.zc;f.attach=function(a){f.ja<10&&f.zc&&f.kb.yd(a).Ed()};f.detach=function(a){f.kb.m(a)}};\nvar $=element;function init(){if(doc.media!==\"print\"){var a=window.PIE;a&&a.attach($)}}function cleanup(){if(doc.media!==\"print\"){var a=window.PIE;if(a){a.detach($);$=0}}}$.readyState===\"complete\"&&init();\n</script>\n</PUBLIC:COMPONENT>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/galaxy/galaxy.hyperesources/galaxy_hype_generated_script.js",
    "content": "//\tHYPE.documents[\"Galaxy\"]\n\n(function(){(function k(){function l(a,b,d){var c=!1;null==window[a]&&(null==window[b]?(window[b]=[],window[b].push(k),a=document.getElementsByTagName(\"head\")[0],b=document.createElement(\"script\"),c=h,false==!0&&(c=\"\"),b.type=\"text/javascript\",b.src=c+\"/\"+d,a.appendChild(b)):window[b].push(k),c=!0);return c}var h=\"Galaxy.hyperesources\",c=\"Galaxy\",e=\"galaxy_hype_container\";if(false==!1)try{for(var f=document.getElementsByTagName(\"script\"),\na=0;a<f.length;a++){var b=f[a].src;if(null!=b&&-1!=b.indexOf(\"galaxy_hype_generated_script.js\")){h=b.substr(0,b.lastIndexOf(\"/\"));break}}}catch(n){}if(false==!1&&(a=navigator.userAgent.match(/MSIE (\\d+\\.\\d+)/),a=parseFloat(a&&a[1])||null,a=l(\"HYPE_526\",\"HYPE_dtl_526\",!0==(null!=a&&10>a||false==!0)?\"HYPE-526.full.min.js\":\"HYPE-526.thin.min.js\"),false==!0&&(a=a||l(\"HYPE_w_526\",\"HYPE_wdtl_526\",\"HYPE-526.waypoints.min.js\")),a))return;\nf=window.HYPE.documents;if(null!=f[c]){b=1;a=c;do c=\"\"+a+\"-\"+b++;while(null!=f[c]);for(var d=document.getElementsByTagName(\"div\"),b=!1,a=0;a<d.length;a++)if(d[a].id==e&&null==d[a].getAttribute(\"HYP_dn\")){var b=1,g=e;do e=\"\"+g+\"-\"+b++;while(null!=document.getElementById(e));d[a].id=e;b=!0;break}if(!1==b)return}b=[];b=[];d={};g={};for(a=0;a<b.length;a++)try{g[b[a].identifier]=b[a].name,d[b[a].name]=eval(\"(function(){return \"+b[a].source+\"})();\")}catch(m){window.console&&window.console.log(m),\nd[b[a].name]=function(){}}a=new HYPE_526(c,e,{\"18\":{p:1,n:\"Ray%204.svg\",g:\"763\",t:\"image/svg+xml\"},\"10\":{p:1,n:\"Star%201%20Copy.svg\",g:\"744\",t:\"image/svg+xml\"},\"19\":{p:1,n:\"Ray%203.svg\",g:\"765\",t:\"image/svg+xml\"},\"11\":{p:1,n:\"Star%201%20Copy%209.svg\",g:\"746\",t:\"image/svg+xml\"},\"0\":{p:1,n:\"Combined%20Shape-1.svg\",g:\"722\",t:\"image/svg+xml\"},\"12\":{p:1,n:\"Star%201%20Copy%208.svg\",g:\"748\",t:\"image/svg+xml\"},\"1\":{p:1,n:\"Checkmark%20body-1.svg\",g:\"724\",t:\"image/svg+xml\"},\"20\":{p:1,n:\"Ray%202.svg\",g:\"767\",t:\"image/svg+xml\"},\"2\":{p:1,n:\"Path%207-1.svg\",g:\"726\",t:\"image/svg+xml\"},\"13\":{p:1,n:\"Star%201%20Copy%207.svg\",g:\"750\",t:\"image/svg+xml\"},\"3\":{p:1,n:\"Path%204-1.svg\",g:\"728\",t:\"image/svg+xml\"},\"21\":{p:1,n:\"Ray%201.svg\",g:\"769\",t:\"image/svg+xml\"},\"14\":{p:1,n:\"Star%201%20Copy%205.svg\",g:\"752\",t:\"image/svg+xml\"},\"4\":{p:1,n:\"Path%203-1.svg\",g:\"730\",t:\"image/svg+xml\"},\"5\":{p:1,n:\"Planet%201.png\",g:\"738\",o:true,t:\"@1x\"},\"15\":{p:1,n:\"Star%201%20Copy%202.svg\",g:\"754\",t:\"image/svg+xml\"},\"22\":{p:1,n:\"comet.png\",g:\"900\",o:true,t:\"@1x\"},\"6\":{p:1,n:\"Planet%201_2x.png\",g:\"738\",o:true,t:\"@2x\"},\"23\":{p:1,n:\"comet_2x.png\",g:\"900\",o:true,t:\"@2x\"},\"16\":{p:1,n:\"Ray%206.svg\",g:\"759\",t:\"image/svg+xml\"},\"7\":{p:1,n:\"Planet%202.png\",g:\"740\",o:true,t:\"@1x\"},\"8\":{p:1,n:\"Planet%202_2x.png\",g:\"740\",o:true,t:\"@2x\"},\"17\":{p:1,n:\"Ray%205.svg\",g:\"761\",t:\"image/svg+xml\"},\"9\":{p:1,n:\"Star.svg\",g:\"742\",t:\"image/svg+xml\"}},h,[],d,[{n:\"Galaxy\",o:\"719\",X:[0]}],[{o:\"721\",p:\"600px\",x:0,cA:false,Z:500,Y:600,c:\"#FFFFFF\",L:[],bY:1,d:600,U:{},T:{kTimelineDefaultIdentifier:{f:30,z:19.25,i:\"kTimelineDefaultIdentifier\",n:\"Main Timeline\",j:{\"0\":[[472,175,472,175,125,277,125,277]]},a:[{f:\"c\",y:0,z:5.23,i:\"a\",e:313,s:313,o:\"939\"},{f:\"c\",y:0,z:1.02,i:\"e\",e:1,s:0,o:\"938\"},{f:\"c\",y:0,z:1.02,i:\"e\",e:1,s:0,o:\"952\"},{f:\"c\",y:0,z:1.02,i:\"e\",e:1,s:0,o:\"954\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"931\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"936\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"933\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"949\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"948\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"955\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"947\"},{f:\"c\",y:0.11,z:1.02,i:\"e\",e:1,s:0,o:\"950\"},{f:\"c\",y:0.24,z:1.02,i:\"e\",e:1,s:0,o:\"951\"},{f:\"c\",y:0.28,z:1.02,i:\"e\",e:1,s:0,o:\"935\"},{f:\"c\",y:0.28,z:1.02,i:\"e\",e:1,s:0,o:\"937\"},{f:\"c\",y:1.01,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:1.01,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:1.01,z:4.03,i:\"b\",e:247,s:239,o:\"935\"},{y:1.01,i:\"a\",s:157,z:0,o:\"951\",f:\"a\"},{f:\"c\",y:1.01,z:4.03,i:\"b\",e:205,s:212,o:\"951\"},{f:\"c\",y:1.01,z:4.03,i:\"b\",e:189,s:176,o:\"937\"},{y:1.02,i:\"e\",s:1,z:0,o:\"938\",f:\"c\"},{y:1.02,i:\"e\",s:1,z:0,o:\"952\",f:\"c\"},{y:1.02,i:\"e\",s:1,z:0,o:\"954\",f:\"c\"},{f:\"c\",y:1.1,z:1.02,i:\"e\",e:1,s:0,o:\"939\"},{f:\"c\",y:1.1,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"950\"},{f:\"c\",y:1.1,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"950\"},{f:\"c\",y:1.13,z:1.02,i:\"e\",e:1,s:0,o:\"932\"},{f:\"c\",y:1.13,z:1.02,i:\"e\",e:1,s:0,o:\"953\"},{y:1.13,i:\"e\",s:1,z:0,o:\"931\",f:\"c\"},{y:1.13,i:\"e\",s:1,z:0,o:\"936\",f:\"c\"},{y:1.13,i:\"e\",s:1,z:0,o:\"933\",f:\"c\"},{y:1.13,i:\"e\",s:1,z:0,o:\"949\",f:\"c\"},{y:1.13,i:\"e\",s:1,z:0,o:\"948\",f:\"c\"},{y:1.13,i:\"e\",s:1,z:0,o:\"955\",f:\"c\"},{y:1.13,i:\"e\",s:1,z:0,o:\"947\",f:\"c\"},{y:1.13,i:\"e\",s:1,z:0,o:\"950\",f:\"c\"},{f:\"c\",y:1.17,z:4.01,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:1.17,z:4.01,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:1.2,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:1.2,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"e\",y:1.2,z:9,i:\"f\",e:360,s:0,o:\"942\"},{f:\"e\",y:1.2,z:18,i:\"f\",e:2160,s:0,o:\"945\"},{f:\"c\",y:1.2,z:4.03,i:\"b\",e:212,s:237,o:\"939\"},{f:\"e\",y:1.2,z:18,i:\"f\",e:360,s:0,o:\"946\"},{f:\"e\",y:1.2,z:9,i:\"f\",e:360,s:0,o:\"944\"},{f:\"c\",y:1.2,z:4.25,i:\"a\",e:251,s:251,o:\"953\"},{f:\"882\",y:1.2,z:1.06,i:\"b\",e:324,s:182,o:\"953\"},{f:\"e\",y:1.2,z:18,i:\"f\",e:360,s:0,o:\"943\"},{f:\"c\",y:1.2,z:2.3,i:\"f\",e:0,s:7,o:\"932\"},{f:\"894\",y:1.2,z:2.3,i:\"b\",e:278,s:306,o:\"932\"},{f:\"c\",y:1.22,z:1.02,i:\"e\",e:1,s:0,o:\"934\"},{f:\"e\",y:1.22,z:17.28,i:\"f\",e:2160,s:0,o:\"940\"},{y:1.26,i:\"e\",s:1,z:0,o:\"951\",f:\"c\"},{f:\"c\",y:1.29,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"952\"},{f:\"c\",y:1.29,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"952\"},{y:2,i:\"e\",s:1,z:0,o:\"935\",f:\"c\"},{y:2,i:\"e\",s:1,z:0,o:\"937\",f:\"c\"},{f:\"c\",y:2.06,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:2.06,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"952\"},{y:2.12,i:\"e\",s:1,z:0,o:\"939\",f:\"c\"},{f:\"c\",y:2.15,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:2.15,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:2.15,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:2.15,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"931\"},{y:2.15,i:\"e\",s:1,z:0,o:\"932\",f:\"c\"},{y:2.15,i:\"e\",s:1,z:0,o:\"953\",f:\"c\"},{f:\"c\",y:2.2,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:2.2,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"947\"},{y:2.24,i:\"e\",s:1,z:0,o:\"934\",f:\"c\"},{f:\"c\",y:2.24,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:2.24,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:2.24,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"931\"},{f:\"c\",y:2.24,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"931\"},{f:\"c\",y:2.26,z:2.1,i:\"b\",e:319,s:324,o:\"953\"},{f:\"c\",y:2.29,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"947\"},{f:\"c\",y:2.29,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"947\"},{f:\"c\",y:3.01,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:3.01,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:3.01,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:3.01,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:3.06,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:3.06,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:3.12,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:3.12,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:3.21,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"955\"},{f:\"c\",y:3.21,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"955\"},{f:\"c\",y:3.28,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:3.28,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:4.05,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:4.05,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:4.12,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:4.12,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:4.14,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"933\"},{f:\"c\",y:4.14,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"933\"},{y:4.2,i:\"f\",s:0,z:0,o:\"932\",f:\"c\"},{f:\"c\",y:4.2,z:3,i:\"b\",e:273,s:278,o:\"932\"},{f:\"c\",y:4.21,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"949\"},{f:\"c\",y:4.21,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"949\"},{f:\"c\",y:4.21,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:4.21,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:4.28,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:4.28,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:4.28,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:4.28,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"949\"},{y:5.04,i:\"b\",s:247,z:0,o:\"935\",f:\"c\"},{y:5.04,i:\"b\",s:205,z:0,o:\"951\",f:\"c\"},{y:5.04,i:\"b\",s:189,z:0,o:\"937\",f:\"c\"},{f:\"c\",y:5.06,z:3.01,i:\"b\",e:324,s:319,o:\"953\"},{f:\"c\",y:5.07,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:5.07,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:5.14,z:3.12,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:5.14,z:3.12,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:5.18,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:5.18,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:5.18,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:5.18,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"950\"},{y:5.23,i:\"b\",s:212,z:0,o:\"939\",f:\"c\"},{y:5.23,i:\"a\",s:313,z:0,o:\"939\",f:\"c\"},{f:\"c\",y:5.27,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"952\"},{f:\"c\",y:5.27,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"952\"},{f:\"c\",y:5.27,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"950\"},{f:\"c\",y:5.27,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"950\"},{f:\"c\",y:6.04,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:6.04,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:6.04,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:6.04,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:6.13,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:6.13,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:6.13,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:6.13,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:6.15,z:0.1,i:\"a\",e:251,s:251,o:\"953\"},{f:\"c\",y:6.18,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:6.18,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:6.22,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:6.22,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:6.22,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"931\"},{f:\"c\",y:6.22,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"931\"},{y:6.25,i:\"a\",s:251,z:0,o:\"953\",f:\"c\"},{f:\"c\",y:6.27,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"947\"},{f:\"c\",y:6.27,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"947\"},{f:\"c\",y:6.29,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:6.29,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:6.29,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:6.29,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:7.04,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:7.04,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:7.1,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:7.1,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"955\"},{o:\"934\",y:7.16,z:0.22,i:\"b\",e:274.5,a:\"0\",f:\"c\",s:172.5},{o:\"934\",y:7.16,z:0.22,i:\"a\",e:103.5,a:\"0\",f:\"c\",s:450.5},{f:\"c\",y:7.19,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"955\"},{f:\"c\",y:7.19,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"955\"},{f:\"c\",y:7.2,z:7.25,i:\"b\",e:282,s:273,o:\"932\"},{f:\"c\",y:7.26,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:7.26,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:8.03,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:8.03,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:8.07,z:6.06,i:\"b\",e:329,s:324,o:\"953\"},{y:8.08,i:\"a\",s:103.5,z:0,o:\"934\",f:\"b\"},{y:8.08,i:\"b\",s:274.5,z:0,o:\"934\",f:\"b\"},{f:\"c\",y:8.1,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:8.1,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:8.12,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"933\"},{f:\"c\",y:8.12,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"933\"},{f:\"c\",y:8.19,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"949\"},{f:\"c\",y:8.19,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"949\"},{f:\"c\",y:8.19,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:8.19,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:8.26,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:8.26,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:8.26,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:8.26,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:9.05,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:9.05,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:9.12,z:3.26,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:9.12,z:3.26,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:10,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:10,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:10,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:10,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:10.09,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"952\"},{f:\"c\",y:10.09,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"952\"},{f:\"c\",y:10.09,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"950\"},{f:\"c\",y:10.09,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"950\"},{f:\"c\",y:10.16,z:3.18,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:10.16,z:3.18,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:10.16,z:3.18,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:10.16,z:3.18,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"950\"},{f:\"e\",y:10.2,z:8.3,i:\"f\",e:720,s:360,o:\"942\"},{f:\"e\",y:10.2,z:8.3,i:\"f\",e:720,s:360,o:\"944\"},{f:\"c\",y:10.25,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:10.25,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:10.25,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:10.25,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:11,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:11,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:11.04,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:11.04,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:11.04,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"931\"},{f:\"c\",y:11.04,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"931\"},{f:\"c\",y:11.09,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"947\"},{f:\"c\",y:11.09,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"947\"},{f:\"c\",y:11.11,z:5.27,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:11.11,z:5.27,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:11.11,z:3.18,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:11.11,z:3.18,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:11.16,z:5.27,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:11.16,z:5.27,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:11.22,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:11.22,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:12.01,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"955\"},{f:\"c\",y:12.01,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"955\"},{f:\"c\",y:12.08,z:5.27,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:12.08,z:5.27,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:12.15,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:12.15,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:12.22,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:12.22,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:12.24,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"933\"},{f:\"c\",y:12.24,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"933\"},{f:\"c\",y:13.01,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"949\"},{f:\"c\",y:13.01,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"949\"},{f:\"c\",y:13.01,z:3.18,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:13.01,z:3.18,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:13.08,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:13.08,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:13.08,z:5.27,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:13.08,z:5.27,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:13.17,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:13.17,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:13.24,z:3.18,i:\"cR\",e:0.80000000000000004,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:13.24,z:3.18,i:\"cQ\",e:0.80000000000000004,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:14.04,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:14.04,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"952\"},{f:\"c\",y:14.04,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:14.04,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"950\"},{f:\"c\",y:14.13,z:5.12,i:\"b\",e:324,s:329,o:\"953\"},{f:\"c\",y:14.13,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"952\"},{f:\"c\",y:14.13,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"952\"},{f:\"c\",y:14.13,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"950\"},{f:\"c\",y:14.13,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"950\"},{y:14.2,i:\"cR\",s:0.80000000000000004,z:0,o:\"952\",f:\"c\"},{y:14.2,i:\"cQ\",s:0.80000000000000004,z:0,o:\"952\",f:\"c\"},{y:14.2,i:\"cR\",s:0.80000000000000004,z:0,o:\"950\",f:\"c\"},{y:14.2,i:\"cQ\",s:0.80000000000000004,z:0,o:\"950\",f:\"c\"},{f:\"c\",y:14.29,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:14.29,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"931\"},{f:\"c\",y:15.08,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"931\"},{f:\"c\",y:15.08,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"931\"},{f:\"c\",y:15.15,z:4.1,i:\"b\",e:278,s:282,o:\"932\"},{y:15.15,i:\"cR\",s:0.80000000000000004,z:0,o:\"931\",f:\"c\"},{y:15.15,i:\"cQ\",s:0.80000000000000004,z:0,o:\"931\",f:\"c\"},{f:\"c\",y:16.19,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:16.19,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"933\"},{f:\"c\",y:16.28,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"933\"},{f:\"c\",y:16.28,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"933\"},{y:17.05,i:\"cQ\",s:0.80000000000000004,z:0,o:\"933\",f:\"c\"},{y:17.05,i:\"cR\",s:0.80000000000000004,z:0,o:\"933\",f:\"c\"},{f:\"c\",y:17.08,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:17.08,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"936\"},{f:\"c\",y:17.12,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:17.12,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"948\"},{f:\"c\",y:17.13,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:17.13,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"947\"},{f:\"c\",y:17.17,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:17.17,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"936\"},{f:\"c\",y:17.21,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:17.21,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"948\"},{f:\"c\",y:17.22,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"947\"},{f:\"c\",y:17.22,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"947\"},{y:17.24,i:\"cR\",s:0.80000000000000004,z:0,o:\"936\",f:\"c\"},{y:17.24,i:\"cQ\",s:0.80000000000000004,z:0,o:\"936\",f:\"c\"},{y:17.28,i:\"cR\",s:0.80000000000000004,z:0,o:\"948\",f:\"c\"},{y:17.28,i:\"cQ\",s:0.80000000000000004,z:0,o:\"948\",f:\"c\"},{y:17.29,i:\"cR\",s:0.80000000000000004,z:0,o:\"947\",f:\"c\"},{y:17.29,i:\"cQ\",s:0.80000000000000004,z:0,o:\"947\",f:\"c\"},{f:\"c\",y:18.05,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:18.05,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"955\"},{f:\"c\",y:18.14,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"955\"},{f:\"c\",y:18.14,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"955\"},{y:18.21,i:\"cQ\",s:0.80000000000000004,z:0,o:\"955\",f:\"c\"},{y:18.21,i:\"cR\",s:0.80000000000000004,z:0,o:\"955\",f:\"c\"},{f:\"c\",y:19.05,z:0.09,i:\"cR\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:19.05,z:0.09,i:\"cQ\",e:1,s:0.80000000000000004,o:\"949\"},{f:\"c\",y:19.14,z:0.07,i:\"cR\",e:0.80000000000000004,s:1,o:\"949\"},{f:\"c\",y:19.14,z:0.07,i:\"cQ\",e:0.80000000000000004,s:1,o:\"949\"},{y:19.2,i:\"f\",s:2160,z:0,o:\"945\",f:\"c\"},{y:19.2,i:\"f\",s:360,z:0,o:\"946\",f:\"c\"},{y:19.2,i:\"f\",s:2160,z:0,o:\"940\",f:\"c\"},{y:19.2,i:\"f\",s:360,z:0,o:\"943\",f:\"c\"},{y:19.2,i:\"f\",s:720,z:0,o:\"942\",f:\"c\"},{y:19.2,i:\"f\",s:720,z:0,o:\"944\",f:\"c\"},{y:19.21,i:\"cR\",s:0.80000000000000004,z:0,o:\"949\",f:\"c\"},{y:19.21,i:\"cQ\",s:0.80000000000000004,z:0,o:\"949\",f:\"c\"},{y:19.25,i:\"b\",s:278,z:0,o:\"932\",f:\"c\"},{y:19.25,i:\"b\",s:324,z:0,o:\"953\",f:\"c\"}],b:[]}},bZ:180,O:[\"938\",\"952\",\"936\",\"955\",\"947\",\"949\",\"950\",\"933\",\"948\",\"931\",\"934\",\"945\",\"940\",\"944\",\"942\",\"946\",\"943\",\"941\",\"939\",\"953\",\"932\",\"937\",\"951\",\"935\",\"954\"],v:{\"950\":{w:\"\",h:\"744\",p:\"no-repeat\",x:\"visible\",a:160,q:\"100% 100%\",b:260,j:\"absolute\",r:\"inline\",z:21,k:\"div\",c:4,d:4,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"942\":{h:\"763\",p:\"no-repeat\",x:\"visible\",a:17,q:\"100% 100%\",b:17,j:\"absolute\",bF:\"939\",z:4,k:\"div\",c:75,d:75,r:\"inline\",f:0},\"934\":{h:\"900\",p:\"no-repeat\",x:\"visible\",tY:0.5,a:450.5,q:\"100% 100%\",j:\"absolute\",b:172.5,z:10,k:\"div\",c:43,d:5,r:\"inline\",e:0,f:-17,tX:0.5},\"955\":{h:\"754\",p:\"no-repeat\",x:\"visible\",a:421,q:\"100% 100%\",b:178,j:\"absolute\",r:\"inline\",z:24,k:\"div\",c:9,d:9,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"947\":{h:\"752\",p:\"no-repeat\",x:\"visible\",a:146,q:\"100% 100%\",b:235,j:\"absolute\",r:\"inline\",z:23,k:\"div\",c:11,d:11,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"939\":{x:\"visible\",k:\"div\",c:109,d:109,z:9,e:0,a:313,j:\"absolute\",b:237},\"951\":{h:\"728\",p:\"no-repeat\",x:\"visible\",a:157,q:\"100% 100%\",b:212,j:\"absolute\",r:\"inline\",c:318,k:\"div\",z:5,d:164,e:0},\"943\":{h:\"759\",p:\"no-repeat\",x:\"visible\",a:8,q:\"100% 100%\",b:8,j:\"absolute\",bF:\"939\",z:2,k:\"div\",c:94,d:94,r:\"inline\",f:0},\"935\":{h:\"726\",p:\"no-repeat\",x:\"visible\",a:105,q:\"100% 100%\",b:239,j:\"absolute\",r:\"inline\",c:194,k:\"div\",z:3,d:72,e:0},\"948\":{h:\"748\",p:\"no-repeat\",x:\"visible\",a:161,q:\"100% 100%\",b:220,j:\"absolute\",r:\"inline\",z:19,k:\"div\",c:6,d:6,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"931\":{w:\"\",h:\"746\",p:\"no-repeat\",x:\"visible\",a:401,q:\"100% 100%\",b:151,j:\"absolute\",r:\"inline\",z:18,k:\"div\",c:12,d:12,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"952\":{h:\"754\",p:\"no-repeat\",x:\"visible\",a:289,q:\"100% 100%\",b:279,j:\"absolute\",r:\"inline\",z:26,k:\"div\",c:5,d:5,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"944\":{h:\"765\",p:\"no-repeat\",x:\"visible\",a:19,q:\"100% 100%\",b:20,j:\"absolute\",bF:\"939\",z:5,k:\"div\",c:69,d:70,r:\"inline\",f:0},\"936\":{h:\"754\",p:\"no-repeat\",x:\"visible\",a:383,q:\"100% 100%\",b:173,j:\"absolute\",r:\"inline\",z:25,k:\"div\",c:5,d:5,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"949\":{h:\"752\",p:\"no-repeat\",x:\"visible\",a:276,q:\"100% 100%\",b:301,j:\"absolute\",r:\"inline\",z:22,k:\"div\",c:11,d:11,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"940\":{h:\"767\",p:\"no-repeat\",x:\"visible\",a:21,q:\"100% 100%\",b:22,j:\"absolute\",bF:\"939\",z:6,k:\"div\",c:65,d:65,r:\"inline\",f:0},\"932\":{h:\"738\",p:\"no-repeat\",x:\"visible\",a:148,q:\"100% 100%\",b:306,j:\"absolute\",r:\"inline\",z:7,k:\"div\",c:108,d:79,e:0,f:7},\"953\":{h:\"740\",p:\"no-repeat\",x:\"visible\",a:251,q:\"100% 100%\",b:182,j:\"absolute\",r:\"inline\",c:30,k:\"div\",z:8,d:31,e:0},\"945\":{h:\"769\",p:\"no-repeat\",x:\"visible\",a:24,q:\"100% 100%\",b:25,j:\"absolute\",bF:\"939\",z:7,k:\"div\",c:59,d:59,r:\"inline\",f:0},\"937\":{h:\"730\",p:\"no-repeat\",x:\"visible\",a:102,q:\"100% 100%\",b:176,j:\"absolute\",r:\"inline\",c:172,k:\"div\",z:6,d:83,e:0},\"941\":{h:\"742\",p:\"no-repeat\",x:\"visible\",a:0,q:\"100% 100%\",b:0,j:\"absolute\",bF:\"939\",c:109,k:\"div\",z:1,d:109,r:\"inline\"},\"933\":{h:\"750\",p:\"no-repeat\",x:\"visible\",a:197,q:\"100% 100%\",b:264,j:\"absolute\",r:\"inline\",z:20,k:\"div\",c:9,d:9,cQ:0.80000000000000004,e:0,cR:0.80000000000000004},\"954\":{h:\"724\",p:\"no-repeat\",x:\"visible\",a:123,q:\"100% 100%\",b:138,j:\"absolute\",r:\"inline\",c:327,k:\"div\",z:1,d:239,e:0},\"946\":{h:\"761\",p:\"no-repeat\",x:\"visible\",a:12,q:\"100% 100%\",b:12,j:\"absolute\",bF:\"939\",z:3,k:\"div\",c:86,d:86,r:\"inline\",f:0},\"938\":{w:\"\",h:\"722\",p:\"no-repeat\",x:\"visible\",a:32,q:\"100% 100%\",b:74,j:\"absolute\",r:\"inline\",z:27,k:\"div\",c:532,d:374,e:0}}}],{},g,{\"882\":[[0,0,0.1322,0.6072,0.1353,0.8972,1,1]],\"894\":[[0,0,0.1781,0.311,0.5242,1.1509,1,1]]},null,false,false,-1,true,true,false,true);f[c]=a.API;document.getElementById(e).setAttribute(\"HYP_dn\",\nc);a.z_o(this.body)})();})();\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/gem/gem.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1,IE=edge\" />\n\t<title>GEM Slower</title>\n\t<style>\n\t\thtml {\n\t\t\theight:100%;\n\t\t}\n\t\tbody {\n\t\t\tmargin:0;\n\t\t\theight:100%;\n\t\t}\n\t</style>\n\t<!-- copy these lines to your document head: -->\n\n\t<meta name=\"viewport\" content=\"user-scalable=yes, width=600\" />\n\n\t<!-- end copy -->\n  </head>\n  <body>\n\t<!-- copy these lines to your document: -->\n\n\t<div id=\"gemslower_hype_container\" style=\"margin:auto;position:relative;width:600px;height:500px;overflow:hidden;\" aria-live=\"polite\">\n\t\t<script type=\"text/javascript\" charset=\"utf-8\" src=\"gem.hyperesources/gemslower_hype_generated_script.js?77203\"></script>\n\t</div>\n\n\t<!-- end copy -->\n\n\n\n\t<!-- text content for search engines: -->\n\n\t<div style=\"display:none\">\n\n\t\t<div></div>\n\n\t</div>\n\n\t<!-- end text content: -->\n\n  </body>\n</html>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/gem/gem.hyperesources/gemslower_hype_generated_script.js",
    "content": "//\tHYPE.documents[\"GEM Slower\"]\n\n(function(){(function k(){function l(a,b,d){var c=!1;null==window[a]&&(null==window[b]?(window[b]=[],window[b].push(k),a=document.getElementsByTagName(\"head\")[0],b=document.createElement(\"script\"),c=h,false==!0&&(c=\"\"),b.type=\"text/javascript\",b.src=c+\"/\"+d,a.appendChild(b)):window[b].push(k),c=!0);return c}var h=\"GEM%20Slower.hyperesources\",c=\"GEM Slower\",e=\"gemslower_hype_container\";if(false==!1)try{for(var f=document.getElementsByTagName(\"script\"),\na=0;a<f.length;a++){var b=f[a].src;if(null!=b&&-1!=b.indexOf(\"gemslower_hype_generated_script.js\")){h=b.substr(0,b.lastIndexOf(\"/\"));break}}}catch(n){}if(false==!1&&(a=navigator.userAgent.match(/MSIE (\\d+\\.\\d+)/),a=parseFloat(a&&a[1])||null,a=l(\"HYPE_526\",\"HYPE_dtl_526\",!0==(null!=a&&10>a||true==!0)?\"HYPE-526.full.min.js\":\"HYPE-526.thin.min.js\"),false==!0&&(a=a||l(\"HYPE_w_526\",\"HYPE_wdtl_526\",\"HYPE-526.waypoints.min.js\")),a))return;\nf=window.HYPE.documents;if(null!=f[c]){b=1;a=c;do c=\"\"+a+\"-\"+b++;while(null!=f[c]);for(var d=document.getElementsByTagName(\"div\"),b=!1,a=0;a<d.length;a++)if(d[a].id==e&&null==d[a].getAttribute(\"HYP_dn\")){var b=1,g=e;do e=\"\"+g+\"-\"+b++;while(null!=document.getElementById(e));d[a].id=e;b=!0;break}if(!1==b)return}b=[];b=[];d={};g={};for(a=0;a<b.length;a++)try{g[b[a].identifier]=b[a].name,d[b[a].name]=eval(\"(function(){return \"+b[a].source+\"})();\")}catch(m){window.console&&window.console.log(m),\nd[b[a].name]=function(){}}a=new HYPE_526(c,e,{\"10\":{p:1,n:\"Layer%204.png\",g:\"44\",t:\"@1x\"},\"2\":{p:1,n:\"Layer%2014.png\",g:\"24\",t:\"@1x\"},\"15\":{p:1,n:\"Layer%201.png\",g:\"57\",t:\"@1x\"},\"3\":{p:1,n:\"Layer%2010.png\",g:\"32\",t:\"@1x\"},\"11\":{p:1,n:\"Layer%203.png\",g:\"46\",t:\"@1x\"},\"4\":{p:1,n:\"Layer%209.png\",g:\"34\",o:true,t:\"@1x\"},\"16\":{p:1,n:\"Layer%2015.png\",g:\"59\",o:true,t:\"@1x\"},\"5\":{p:1,n:\"Layer%209_2x.png\",g:\"34\",o:true,t:\"@2x\"},\"12\":{p:1,n:\"Layer%2011.png\",g:\"30\",o:true,t:\"@1x\"},\"17\":{p:1,n:\"Layer%2015_2x.png\",g:\"59\",o:true,t:\"@2x\"},\"6\":{p:1,n:\"Layer%208.png\",g:\"36\",t:\"@1x\"},\"13\":{p:1,n:\"Layer%2011_2x.png\",g:\"30\",o:true,t:\"@2x\"},\"7\":{p:1,n:\"Layer%207.png\",g:\"38\",o:true,t:\"@1x\"},\"18\":{p:1,n:\"Layer%206%20copy.png\",g:\"66\",t:\"@1x\"},\"0\":{p:1,n:\"Layer%2012.png\",g:\"28\",t:\"@1x\"},\"8\":{p:1,n:\"Layer%207_2x.png\",g:\"38\",o:true,t:\"@2x\"},\"14\":{p:1,n:\"Layer%202.png\",g:\"48\",t:\"@1x\"},\"1\":{p:1,n:\"Layer%2013.png\",g:\"26\",t:\"@1x\"},\"9\":{p:1,n:\"Layer%205.png\",g:\"42\",t:\"@1x\"}},h,[],d,[{n:\"GEM Slower\",o:\"335\",X:[0]}],[{o:\"383\",p:\"600px\",x:0,cA:false,Z:500,Y:600,c:\"#FFFFFF\",L:[],bY:1,d:600,U:{},T:{kTimelineDefaultIdentifier:{i:\"kTimelineDefaultIdentifier\",n:\"Main Timeline\",z:1.14,b:[],a:[{f:\"c\",y:0,z:0.21,i:\"e\",e:1,s:0,o:\"944\"},{f:\"c\",y:0,z:1,i:\"a\",e:31,s:29,o:\"941\"},{f:\"c\",y:0.03,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"964\"},{f:\"c\",y:0.03,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"964\"},{f:\"c\",y:0.03,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"967\"},{f:\"c\",y:0.03,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"967\"},{f:\"c\",y:0.05,z:0.05,i:\"b\",e:172,s:172,o:\"964\"},{f:\"85\",y:0.05,z:0.05,i:\"f\",e:0,s:-15,o:\"964\"},{f:\"c\",y:0.05,z:0.09,i:\"bM\",e:\"1\",s:\"0\",o:\"967\"},{y:0.05,i:\"w\",s:\"\",z:0.06,o:\"967\",f:\"c\"},{f:\"85\",y:0.05,z:0.05,i:\"a\",e:207,s:193,o:\"964\"},{f:\"c\",y:0.05,z:0.06,i:\"b\",e:193,s:193,o:\"967\"},{f:\"c\",y:0.05,z:0.06,i:\"f\",e:0,s:20,o:\"967\"},{f:\"c\",y:0.05,z:0.06,i:\"a\",e:149,s:169,o:\"967\"},{f:\"c\",y:0.06,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"932\"},{f:\"c\",y:0.07,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"964\"},{f:\"c\",y:0.07,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"964\"},{f:\"c\",y:0.07,z:1,i:\"a\",e:31,s:29,o:\"974\"},{f:\"c\",y:0.07,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"967\"},{f:\"c\",y:0.07,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"967\"},{f:\"c\",y:0.08,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"932\"},{f:\"c\",y:0.08,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"932\"},{y:0.09,i:\"cQ\",s:1,z:0,o:\"964\",f:\"c\"},{y:0.09,i:\"cR\",s:1,z:0,o:\"964\",f:\"c\"},{f:\"c\",y:0.09,z:0.05,i:\"cQ\",e:1,s:1,o:\"967\"},{f:\"c\",y:0.09,z:0,i:\"cR\",e:1,s:1,o:\"967\"},{y:0.09,i:\"cR\",s:1,z:0,o:\"967\",f:\"c\"},{y:0.1,i:\"b\",s:172,z:0,o:\"964\",f:\"90\"},{y:0.1,i:\"f\",s:0,z:0,o:\"964\",f:\"90\"},{y:0.1,i:\"a\",s:207,z:0,o:\"964\",f:\"90\"},{f:\"c\",y:0.11,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"968\"},{y:0.11,i:\"b\",s:193,z:0,o:\"967\",f:\"c\"},{y:0.11,i:\"f\",s:0,z:0,o:\"967\",f:\"c\"},{y:0.11,i:\"a\",s:149,z:0,o:\"967\",f:\"c\"},{f:\"c\",y:0.11,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"968\"},{f:\"c\",y:0.11,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"968\"},{f:\"c\",y:0.12,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"963\"},{f:\"c\",y:0.12,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"932\"},{f:\"c\",y:0.12,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"932\"},{f:\"c\",y:0.13,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"935\"},{y:0.13,i:\"bM\",s:\"1\",z:0,o:\"932\",f:\"c\"},{f:\"c\",y:0.13,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"935\"},{f:\"c\",y:0.13,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"935\"},{f:\"c\",y:0.14,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"966\"},{y:0.14,i:\"bM\",s:\"1\",z:0,o:\"967\",f:\"c\"},{f:\"c\",y:0.14,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"966\"},{f:\"c\",y:0.14,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"966\"},{y:0.14,i:\"cQ\",s:1,z:0,o:\"932\",f:\"c\"},{y:0.14,i:\"cR\",s:1,z:0,o:\"932\",f:\"c\"},{y:0.14,i:\"cQ\",s:1,z:0,o:\"967\",f:\"c\"},{f:\"c\",y:0.15,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"977\"},{f:\"c\",y:0.15,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"977\"},{f:\"c\",y:0.15,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"977\"},{f:\"c\",y:0.15,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"968\"},{f:\"c\",y:0.15,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"968\"},{f:\"c\",y:0.17,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"933\"},{f:\"c\",y:0.17,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"933\"},{f:\"c\",y:0.17,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"933\"},{f:\"c\",y:0.17,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"935\"},{f:\"c\",y:0.17,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"935\"},{y:0.17,i:\"cQ\",s:1,z:0,o:\"968\",f:\"c\"},{y:0.17,i:\"cR\",s:1,z:0,o:\"968\",f:\"c\"},{f:\"c\",y:0.17,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"963\"},{f:\"c\",y:0.17,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"963\"},{y:0.18,i:\"bM\",s:\"1\",z:0,o:\"968\",f:\"c\"},{f:\"c\",y:0.18,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"966\"},{f:\"c\",y:0.18,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"966\"},{f:\"c\",y:0.19,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"969\"},{f:\"85\",y:0.19,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"934\"},{y:0.19,i:\"bM\",s:\"1\",z:0,o:\"963\",f:\"c\"},{f:\"c\",y:0.19,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"969\"},{f:\"c\",y:0.19,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"969\"},{f:\"c\",y:0.19,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"977\"},{f:\"c\",y:0.19,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"977\"},{y:0.19,i:\"cR\",s:1,z:0,o:\"935\",f:\"c\"},{y:0.19,i:\"cQ\",s:1,z:0,o:\"935\",f:\"c\"},{f:\"85\",y:0.19,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"934\"},{f:\"85\",y:0.19,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"934\"},{y:0.2,i:\"bM\",s:\"1\",z:0,o:\"935\",f:\"c\"},{y:0.2,i:\"cR\",s:1,z:0,o:\"966\",f:\"c\"},{y:0.2,i:\"cQ\",s:1,z:0,o:\"966\",f:\"c\"},{y:0.21,i:\"e\",s:1,z:0,o:\"944\",f:\"c\"},{f:\"c\",y:0.21,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"976\"},{y:0.21,i:\"bM\",s:\"1\",z:0,o:\"966\",f:\"c\"},{f:\"c\",y:0.21,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"976\"},{f:\"c\",y:0.21,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"976\"},{f:\"c\",y:0.21,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"933\"},{f:\"c\",y:0.21,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"933\"},{y:0.21,i:\"cR\",s:1,z:0,o:\"977\",f:\"c\"},{y:0.21,i:\"cQ\",s:1,z:0,o:\"977\",f:\"c\"},{f:\"c\",y:0.21,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"963\"},{f:\"c\",y:0.21,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"963\"},{f:\"c\",y:0.22,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"936\"},{y:0.22,i:\"bM\",s:\"1\",z:0,o:\"977\",f:\"c\"},{f:\"c\",y:0.22,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"936\"},{f:\"c\",y:0.22,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"936\"},{f:\"c\",y:0.23,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"969\"},{f:\"c\",y:0.23,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"969\"},{y:0.23,i:\"cR\",s:1,z:0,o:\"933\",f:\"c\"},{y:0.23,i:\"cQ\",s:1,z:0,o:\"933\",f:\"c\"},{f:\"85\",y:0.23,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"934\"},{f:\"85\",y:0.23,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"934\"},{y:0.23,i:\"cQ\",s:1,z:0,o:\"963\",f:\"c\"},{y:0.23,i:\"cR\",s:1,z:0,o:\"963\",f:\"c\"},{f:\"c\",y:0.24,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"937\"},{y:0.24,i:\"bM\",s:\"1\",z:0,o:\"933\",f:\"c\"},{f:\"c\",y:0.24,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"937\"},{f:\"c\",y:0.24,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"937\"},{f:\"c\",y:0.25,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"976\"},{f:\"c\",y:0.25,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"976\"},{y:0.25,i:\"cQ\",s:1,z:0,o:\"969\",f:\"c\"},{y:0.25,i:\"cR\",s:1,z:0,o:\"969\",f:\"c\"},{y:0.25,i:\"cR\",s:1,z:0,o:\"934\",f:\"c\"},{y:0.25,i:\"cQ\",s:1,z:0,o:\"934\",f:\"c\"},{f:\"c\",y:0.26,z:0.04,i:\"c\",e:12,s:0,o:\"941\"},{f:\"c\",y:0.26,z:0.04,i:\"d\",e:3,s:2,o:\"941\"},{y:0.26,i:\"bM\",s:\"1\",z:0,o:\"969\",f:\"c\"},{y:0.26,i:\"bM\",s:\"1\",z:0,o:\"934\",f:\"c\"},{f:\"c\",y:0.26,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"936\"},{f:\"c\",y:0.26,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"936\"},{f:\"c\",y:0.27,z:0.07,i:\"bM\",e:\"1\",s:\"0\",o:\"965\"},{f:\"c\",y:0.27,z:0.04,i:\"cQ\",e:1.1000000000000001,s:0.0000000000,o:\"965\"},{f:\"c\",y:0.27,z:0.04,i:\"cR\",e:1.1000000000000001,s:0.0000000000,o:\"965\"},{y:0.27,i:\"cQ\",s:1,z:0,o:\"976\",f:\"c\"},{y:0.27,i:\"cR\",s:1,z:0,o:\"976\",f:\"c\"},{y:0.28,i:\"bM\",s:\"1\",z:0,o:\"976\",f:\"c\"},{f:\"c\",y:0.28,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"937\"},{f:\"c\",y:0.28,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"937\"},{y:0.28,i:\"cR\",s:1,z:0,o:\"936\",f:\"c\"},{y:0.28,i:\"cQ\",s:1,z:0,o:\"936\",f:\"c\"},{f:\"c\",y:0.29,z:0.02,i:\"c\",e:6,s:0,o:\"943\"},{f:\"c\",y:0.29,z:0.06,i:\"b\",e:9,s:9,o:\"939\"},{f:\"c\",y:0.29,z:0.13,i:\"b\",e:9,s:9,o:\"973\"},{f:\"c\",y:0.29,z:0.02,i:\"a\",e:-9,s:-3,o:\"943\"},{y:0.29,i:\"bM\",s:\"1\",z:0,o:\"936\",f:\"c\"},{f:\"c\",y:1,z:0.04,i:\"d\",e:2,s:3,o:\"941\"},{f:\"c\",y:1,z:0.04,i:\"c\",e:-1,s:12,o:\"941\"},{f:\"c\",y:1,z:0.04,i:\"a\",e:44,s:31,o:\"941\"},{y:1,i:\"cR\",s:1,z:0,o:\"937\",f:\"c\"},{y:1,i:\"cQ\",s:1,z:0,o:\"937\",f:\"c\"},{f:\"c\",y:1.01,z:0.02,i:\"c\",e:0,s:6,o:\"943\"},{y:1.01,i:\"a\",s:-9,z:0,o:\"943\",f:\"c\"},{y:1.01,i:\"bM\",s:\"1\",z:0,o:\"937\",f:\"c\"},{f:\"c\",y:1.01,z:0.02,i:\"cQ\",e:1,s:1.1000000000000001,o:\"965\"},{f:\"c\",y:1.01,z:0.02,i:\"cR\",e:1,s:1.1000000000000001,o:\"965\"},{f:\"c\",y:1.03,z:0.04,i:\"d\",e:3,s:2,o:\"974\"},{f:\"c\",y:1.03,z:0.02,i:\"e\",e:1,s:0,o:\"938\"},{f:\"c\",y:1.03,z:0.02,i:\"d\",e:6,s:0,o:\"940\"},{f:\"c\",y:1.03,z:0.02,i:\"e\",e:1,s:0,o:\"970\"},{f:\"c\",y:1.03,z:0.04,i:\"c\",e:12,s:0,o:\"974\"},{f:\"c\",y:1.03,z:0.02,i:\"d\",e:6,s:-1,o:\"939\"},{f:\"c\",y:1.03,z:0.02,i:\"d\",e:1,s:1,o:\"942\"},{f:\"c\",y:1.03,z:0.02,i:\"c\",e:6,s:0,o:\"942\"},{f:\"c\",y:1.03,z:0.02,i:\"b\",e:-4,s:2,o:\"940\"},{y:1.03,i:\"c\",s:0,z:0,o:\"943\",f:\"c\"},{f:\"c\",y:1.03,z:0.04,i:\"a\",e:8,s:2,o:\"942\"},{y:1.03,i:\"cQ\",s:1,z:0,o:\"965\",f:\"c\"},{y:1.03,i:\"cR\",s:1,z:0,o:\"965\",f:\"c\"},{y:1.04,i:\"d\",s:2,z:0,o:\"941\",f:\"c\"},{y:1.04,i:\"c\",s:-1,z:0,o:\"941\",f:\"c\"},{y:1.04,i:\"a\",s:44,z:0,o:\"941\",f:\"c\"},{y:1.04,i:\"bM\",s:\"1\",z:0,o:\"965\",f:\"c\"},{f:\"c\",y:1.05,z:0.02,i:\"e\",e:0,s:1,o:\"938\"},{f:\"c\",y:1.05,z:0.02,i:\"e\",e:0,s:1,o:\"970\"},{y:1.05,i:\"b\",s:-4,z:0,o:\"940\",f:\"c\"},{f:\"c\",y:1.05,z:0.02,i:\"d\",e:0,s:6,o:\"940\"},{f:\"c\",y:1.05,z:0.02,i:\"d\",e:-1,s:6,o:\"939\"},{f:\"c\",y:1.05,z:0.02,i:\"b\",e:16,s:9,o:\"939\"},{f:\"c\",y:1.05,z:0.02,i:\"c\",e:0,s:6,o:\"942\"},{y:1.05,i:\"d\",s:1,z:0,o:\"942\",f:\"c\"},{f:\"c\",y:1.06,z:0.02,i:\"c\",e:6,s:0,o:\"972\"},{f:\"c\",y:1.06,z:0.02,i:\"a\",e:-9,s:-3,o:\"972\"},{y:1.07,i:\"e\",s:0,z:0,o:\"970\",f:\"c\"},{y:1.07,i:\"e\",s:0,z:0,o:\"938\",f:\"c\"},{y:1.07,i:\"b\",s:16,z:0,o:\"939\",f:\"c\"},{f:\"c\",y:1.07,z:0.04,i:\"d\",e:2,s:3,o:\"974\"},{f:\"c\",y:1.07,z:0.04,i:\"c\",e:-1,s:12,o:\"974\"},{y:1.07,i:\"d\",s:0,z:0,o:\"940\",f:\"c\"},{y:1.07,i:\"c\",s:0,z:0,o:\"942\",f:\"c\"},{f:\"c\",y:1.07,z:0.04,i:\"a\",e:44,s:31,o:\"974\"},{y:1.07,i:\"d\",s:-1,z:0,o:\"939\",f:\"c\"},{y:1.07,i:\"a\",s:8,z:0,o:\"942\",f:\"c\"},{f:\"c\",y:1.08,z:0.02,i:\"c\",e:0,s:6,o:\"972\"},{y:1.08,i:\"a\",s:-9,z:0,o:\"972\",f:\"c\"},{f:\"c\",y:1.1,z:0.02,i:\"b\",e:-4,s:2,o:\"975\"},{f:\"c\",y:1.1,z:0.02,i:\"c\",e:6,s:0,o:\"971\"},{f:\"c\",y:1.1,z:0.02,i:\"d\",e:1,s:1,o:\"971\"},{f:\"c\",y:1.1,z:0.02,i:\"d\",e:6,s:-1,o:\"973\"},{y:1.1,i:\"c\",s:0,z:0,o:\"972\",f:\"c\"},{f:\"c\",y:1.1,z:0.04,i:\"a\",e:8,s:2,o:\"971\"},{f:\"c\",y:1.1,z:0.02,i:\"d\",e:6,s:0,o:\"975\"},{y:1.11,i:\"c\",s:-1,z:0,o:\"974\",f:\"c\"},{y:1.11,i:\"d\",s:2,z:0,o:\"974\",f:\"c\"},{y:1.11,i:\"a\",s:44,z:0,o:\"974\",f:\"c\"},{f:\"c\",y:1.12,z:0.02,i:\"b\",e:16,s:9,o:\"973\"},{y:1.12,i:\"b\",s:-4,z:0,o:\"975\",f:\"c\"},{f:\"c\",y:1.12,z:0.02,i:\"c\",e:0,s:6,o:\"971\"},{y:1.12,i:\"d\",s:1,z:0,o:\"971\",f:\"c\"},{f:\"c\",y:1.12,z:0.02,i:\"d\",e:-1,s:6,o:\"973\"},{f:\"c\",y:1.12,z:0.02,i:\"d\",e:0,s:6,o:\"975\"},{y:1.14,i:\"b\",s:16,z:0,o:\"973\",f:\"c\"},{y:1.14,i:\"a\",s:8,z:0,o:\"971\",f:\"c\"},{y:1.14,i:\"c\",s:0,z:0,o:\"971\",f:\"c\"},{y:1.14,i:\"d\",s:-1,z:0,o:\"973\",f:\"c\"},{y:1.14,i:\"d\",s:0,z:0,o:\"975\",f:\"c\"}],f:30}},bZ:180,O:[\"971\",\"975\",\"973\",\"974\",\"972\",\"970\",\"942\",\"940\",\"939\",\"941\",\"943\",\"938\",\"964\",\"967\",\"934\",\"963\",\"966\",\"968\",\"932\",\"977\",\"933\",\"935\",\"969\",\"976\",\"936\",\"965\",\"937\",\"950\",\"949\",\"958\",\"946\",\"957\",\"956\",\"952\",\"948\",\"951\",\"961\",\"959\",\"945\",\"955\",\"960\",\"947\",\"954\",\"953\",\"962\",\"944\",\"931\"],v:{\"951\":{c:151,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:10,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:62,aL:56,b:239},\"959\":{c:93,d:23,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:8,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:114,aL:56,b:319},\"970\":{x:\"visible\",k:\"div\",c:31,r:\"none\",d:10,z:18,e:0,a:242,j:\"absolute\",bF:\"931\",b:215},\"946\":{c:157,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:15,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:297,aL:56,b:205},\"965\":{h:\"26\",p:\"no-repeat\",x:\"visible\",a:393,q:\"100% 100%\",b:64,j:\"absolute\",bF:\"931\",z:3,k:\"div\",c:34,d:35,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"933\":{h:\"36\",p:\"no-repeat\",x:\"visible\",a:262,q:\"100% 100%\",b:124,j:\"absolute\",bF:\"931\",z:8,k:\"div\",c:38,d:38,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"952\":{c:247,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:12,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:129,aL:56,b:167},\"971\":{c:0,d:1,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:5,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"970\",P:1,a:2,aL:10,b:5},\"947\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:4,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:34,aL:56,b:153},\"966\":{w:\"\",h:\"46\",p:\"no-repeat\",x:\"visible\",a:80,q:\"100% 100%\",b:134,j:\"absolute\",bF:\"931\",z:12,k:\"div\",c:39,d:38,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"934\":{h:\"48\",p:\"no-repeat\",x:\"visible\",a:106,q:\"100% 100%\",b:104,j:\"absolute\",bF:\"931\",z:14,k:\"div\",c:29,d:29,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"953\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:2,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:122,aL:56,b:112},\"972\":{c:0,d:1,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:1,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"970\",P:1,a:-3,aL:10,b:5},\"940\":{c:1,d:0,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:4,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"938\",P:1,a:0,aL:10,b:2},\"948\":{c:151,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:11,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:236,aL:56,b:258},\"967\":{w:\"\",h:\"66\",p:\"no-repeat\",x:\"visible\",a:169,q:\"100% 100%\",b:193,j:\"absolute\",bF:\"931\",z:15,k:\"div\",c:68,d:98,r:\"inline\",cQ:0.0000000000,f:20,cR:0.0000000000,bM:\"0\"},\"935\":{h:\"34\",p:\"no-repeat\",x:\"visible\",a:314,q:\"100% 100%\",b:177,j:\"absolute\",bF:\"931\",z:7,k:\"div\",c:16,d:15,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"954\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:3,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:81,aL:56,b:125},\"973\":{c:1,d:-1,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:3,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"970\",P:1,a:0,aL:10,b:9},\"941\":{c:0,d:2,I:\"Solid\",r:\"none\",J:\"Solid\",f:0,K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",O:1,j:\"absolute\",aJ:10,k:\"div\",C:\"#FFFFFF\",z:2,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"938\",P:1,a:29,aL:10,b:0},\"949\":{c:157,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:17,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:207,aL:56,b:26},\"960\":{c:211,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:5,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:184,aL:56,b:132},\"968\":{w:\"\",h:\"44\",p:\"no-repeat\",x:\"visible\",a:105,q:\"100% 100%\",b:171,j:\"absolute\",bF:\"931\",z:11,k:\"div\",c:37,d:37,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"936\":{h:\"28\",p:\"no-repeat\",x:\"visible\",a:328,q:\"100% 100%\",b:69,j:\"absolute\",bF:\"931\",z:4,k:\"div\",c:34,d:34,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"955\":{c:211,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:6,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:138,aL:56,b:228},\"974\":{c:0,d:2,I:\"Solid\",r:\"none\",J:\"Solid\",f:0,K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",O:1,j:\"absolute\",aJ:10,k:\"div\",C:\"#FFFFFF\",z:2,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"970\",P:1,a:29,aL:10,b:0},\"942\":{c:0,d:1,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:5,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"938\",P:1,a:2,aL:10,b:5},\"961\":{c:151,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:9,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:122,aL:56,b:239},\"969\":{h:\"32\",p:\"no-repeat\",x:\"visible\",a:314,q:\"100% 100%\",b:107,j:\"absolute\",bF:\"931\",z:6,k:\"div\",c:53,d:63,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"937\":{h:\"24\",p:\"no-repeat\",x:\"visible\",a:366,q:\"100% 100%\",b:33,j:\"absolute\",bF:\"931\",z:2,k:\"div\",c:23,d:85,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"956\":{c:247,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:13,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:34,aL:56,b:193},\"975\":{c:1,d:0,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:4,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"970\",P:1,a:0,aL:10,b:2},\"943\":{c:0,d:1,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:1,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"938\",P:1,a:-3,aL:10,b:5},\"962\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:1,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:236,aL:56,b:97},\"938\":{x:\"visible\",k:\"div\",c:31,r:\"none\",d:10,z:17,e:0,a:114,j:\"absolute\",bF:\"931\",b:180},\"957\":{c:248,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:14,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:98,aL:56,b:214},\"976\":{w:\"\",h:\"30\",p:\"no-repeat\",x:\"visible\",a:301,q:\"100% 100%\",b:93,j:\"absolute\",bF:\"931\",z:5,k:\"div\",c:23,d:28,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"944\":{x:\"visible\",a:0,b:0,j:\"absolute\",bF:\"931\",c:454,k:\"div\",z:1,d:342,cQ:1.2,e:0,cR:1.2},\"963\":{h:\"59\",p:\"no-repeat\",x:\"visible\",a:125,q:\"100% 100%\",b:130,j:\"absolute\",bF:\"931\",z:13,k:\"div\",c:22,d:33,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"931\":{k:\"div\",x:\"visible\",c:454,d:342,z:1,a:65,j:\"absolute\",b:92},\"939\":{c:1,d:-1,I:\"Solid\",J:\"Solid\",K:\"Solid\",g:\"#E8EBED\",L:\"Solid\",M:1,N:1,aI:10,A:\"#FFFFFF\",x:\"visible\",j:\"absolute\",O:1,aJ:10,k:\"div\",C:\"#FFFFFF\",z:3,B:\"#FFFFFF\",D:\"#FFFFFF\",aK:10,bF:\"938\",P:1,a:0,aL:10,b:9},\"950\":{c:93,d:23,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:18,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:98,aL:56,b:66},\"958\":{c:93,d:29,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:16,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:290,aL:56,b:51},\"977\":{w:\"\",h:\"38\",p:\"no-repeat\",x:\"visible\",a:228,q:\"100% 100%\",b:169,j:\"absolute\",bF:\"931\",z:9,k:\"div\",c:30,d:27,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"},\"945\":{c:113,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F5F2E3\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:7,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"944\",P:0,a:39,aL:56,b:266},\"964\":{h:\"57\",p:\"no-repeat\",x:\"visible\",a:193,q:\"100% 100%\",b:172,j:\"absolute\",bF:\"931\",z:16,k:\"div\",c:100,d:119,r:\"inline\",cQ:0.0000000000,f:-15,cR:0.0000000000,bM:\"0\"},\"932\":{w:\"\",h:\"42\",p:\"no-repeat\",x:\"visible\",a:154,q:\"100% 100%\",b:148,j:\"absolute\",bF:\"931\",z:10,k:\"div\",c:39,d:56,r:\"inline\",cQ:0.0000000000,cR:0.0000000000,bM:\"0\"}}}],{},g,{\"85\":[[0,0,1.2815,0,0.6806,1.4487,1,1]],\"90\":[[0,0,1.3672,0,0.09444,1,1,1]]},null,true,false,-1,true,true,false,true);f[c]=a.API;document.getElementById(e).setAttribute(\"HYP_dn\",\nc);a.z_o(this.body)})();})();\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/oasis/oasis.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1,IE=edge\" />\n\t<title>Oasis</title>\n\t<style>\n\t\thtml {\n\t\t\theight:100%;\n\t\t}\n\t\tbody {\n\t\t\tmargin:0;\n\t\t\theight:100%;\n\t\t}\n\t</style>\n\t<!-- copy these lines to your document head: -->\n\n\t<meta name=\"viewport\" content=\"user-scalable=yes, width=600\" />\n\n\t<!-- end copy -->\n  </head>\n  <body>\n\t<!-- copy these lines to your document: -->\n\n\t<div id=\"oasis_hype_container\" style=\"margin:auto;position:relative;width:600px;height:500px;overflow:hidden;\" aria-live=\"polite\">\n\t\t<script type=\"text/javascript\" charset=\"utf-8\" src=\"oasis.hyperesources/oasis_hype_generated_script.js?69631\"></script>\n\t</div>\n\n\t<!-- end copy -->\n\n\n\n\t<!-- text content for search engines: -->\n\n\t<div style=\"display:none\">\n\n\t\t<div></div>\n\n\t</div>\n\n\t<!-- end text content: -->\n\n  </body>\n</html>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/oasis/oasis.hyperesources/PIE.htc",
    "content": "<!--\nPIE: CSS3 rendering for IE\nVersion 1.0.0\nhttp://css3pie.com\nDual-licensed for use under the Apache License Version 2.0 or the General Public License (GPL) Version 2.\n-->\n<PUBLIC:COMPONENT lightWeight=\"true\">\n<!-- saved from url=(0014)about:internet -->\n<PUBLIC:ATTACH EVENT=\"oncontentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondocumentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondetach\" FOR=\"element\" ONEVENT=\"cleanup()\" />\n\n<script type=\"text/javascript\">\nvar doc = element.document;var f=window.PIE;\nif(!f){f=window.PIE={F:\"-pie-\",nb:\"Pie\",La:\"pie_\",Ac:{TD:1,TH:1},cc:{TABLE:1,THEAD:1,TBODY:1,TFOOT:1,TR:1,INPUT:1,TEXTAREA:1,SELECT:1,OPTION:1,IMG:1,HR:1},fc:{A:1,INPUT:1,TEXTAREA:1,SELECT:1,BUTTON:1},Gd:{submit:1,button:1,reset:1},aa:function(){}};try{doc.execCommand(\"BackgroundImageCache\",false,true)}catch(aa){}for(var ba=4,Z=doc.createElement(\"div\"),ca=Z.getElementsByTagName(\"i\"),ga;Z.innerHTML=\"<!--[if gt IE \"+ ++ba+\"]><i></i><![endif]--\\>\",ca[0];);f.O=ba;if(ba===6)f.F=f.F.replace(/^-/,\"\");f.ja=\ndoc.documentMode||f.O;Z.innerHTML='<v:shape adj=\"1\"/>';ga=Z.firstChild;ga.style.behavior=\"url(#default#VML)\";f.zc=typeof ga.adj===\"object\";(function(){var a,b=0,c={};f.p={Za:function(d){if(!a){a=doc.createDocumentFragment();a.namespaces.add(\"css3vml\",\"urn:schemas-microsoft-com:vml\")}return a.createElement(\"css3vml:\"+d)},Ba:function(d){return d&&d._pieId||(d._pieId=\"_\"+ ++b)},Eb:function(d){var e,g,j,i,h=arguments;e=1;for(g=h.length;e<g;e++){i=h[e];for(j in i)if(i.hasOwnProperty(j))d[j]=i[j]}return d},\nRb:function(d,e,g){var j=c[d],i,h;if(j)Object.prototype.toString.call(j)===\"[object Array]\"?j.push([e,g]):e.call(g,j);else{h=c[d]=[[e,g]];i=new Image;i.onload=function(){j=c[d]={h:i.width,f:i.height};for(var k=0,n=h.length;k<n;k++)h[k][0].call(h[k][1],j);i.onload=null};i.src=d}}}})();f.Na={gc:function(a,b,c,d){function e(){k=j>=90&&j<270?b:0;n=j<180?c:0;m=b-k;p=c-n}function g(){for(;j<0;)j+=360;j%=360}var j=d.sa;d=d.zb;var i,h,k,n,m,p,r,t;if(d){d=d.coords(a,b,c);i=d.x;h=d.y}if(j){j=j.jd();g();e();\nif(!d){i=k;h=n}d=f.Na.tc(i,h,j,m,p);a=d[0];d=d[1]}else if(d){a=b-i;d=c-h}else{i=h=a=0;d=c}r=a-i;t=d-h;if(j===void 0){j=!r?t<0?90:270:!t?r<0?180:0:-Math.atan2(t,r)/Math.PI*180;g();e()}return{sa:j,xc:i,yc:h,td:a,ud:d,Wd:k,Xd:n,rd:m,sd:p,kd:r,ld:t,rc:f.Na.dc(i,h,a,d)}},tc:function(a,b,c,d,e){if(c===0||c===180)return[d,b];else if(c===90||c===270)return[a,e];else{c=Math.tan(-c*Math.PI/180);a=c*a-b;b=-1/c;d=b*d-e;e=b-c;return[(d-a)/e,(c*d-b*a)/e]}},dc:function(a,b,c,d){a=c-a;b=d-b;return Math.abs(a===0?\nb:b===0?a:Math.sqrt(a*a+b*b))}};f.ea=function(){this.Gb=[];this.oc={}};f.ea.prototype={ba:function(a){var b=f.p.Ba(a),c=this.oc,d=this.Gb;if(!(b in c)){c[b]=d.length;d.push(a)}},Ha:function(a){a=f.p.Ba(a);var b=this.oc;if(a&&a in b){delete this.Gb[b[a]];delete b[a]}},xa:function(){for(var a=this.Gb,b=a.length;b--;)a[b]&&a[b]()}};f.Oa=new f.ea;f.Oa.Rd=function(){var a=this,b;if(!a.Sd){b=doc.documentElement.currentStyle.getAttribute(f.F+\"poll-interval\")||250;(function c(){a.xa();setTimeout(c,b)})();\na.Sd=1}};(function(){function a(){f.L.xa();window.detachEvent(\"onunload\",a);window.PIE=null}f.L=new f.ea;window.attachEvent(\"onunload\",a);f.L.ta=function(b,c,d){b.attachEvent(c,d);this.ba(function(){b.detachEvent(c,d)})}})();f.Qa=new f.ea;f.L.ta(window,\"onresize\",function(){f.Qa.xa()});(function(){function a(){f.mb.xa()}f.mb=new f.ea;f.L.ta(window,\"onscroll\",a);f.Qa.ba(a)})();(function(){function a(){c=f.kb.md()}function b(){if(c){for(var d=0,e=c.length;d<e;d++)f.attach(c[d]);c=0}}var c;if(f.ja<9){f.L.ta(window,\n\"onbeforeprint\",a);f.L.ta(window,\"onafterprint\",b)}})();f.lb=new f.ea;f.L.ta(doc,\"onmouseup\",function(){f.lb.xa()});f.he=function(){function a(h){this.Y=h}var b=doc.createElement(\"length-calc\"),c=doc.body||doc.documentElement,d=b.style,e={},g=[\"mm\",\"cm\",\"in\",\"pt\",\"pc\"],j=g.length,i={};d.position=\"absolute\";d.top=d.left=\"-9999px\";for(c.appendChild(b);j--;){d.width=\"100\"+g[j];e[g[j]]=b.offsetWidth/100}c.removeChild(b);d.width=\"1em\";a.prototype={Kb:/(px|em|ex|mm|cm|in|pt|pc|%)$/,ic:function(){var h=\nthis.Jd;if(h===void 0)h=this.Jd=parseFloat(this.Y);return h},yb:function(){var h=this.ae;if(!h)h=this.ae=(h=this.Y.match(this.Kb))&&h[0]||\"px\";return h},a:function(h,k){var n=this.ic(),m=this.yb();switch(m){case \"px\":return n;case \"%\":return n*(typeof k===\"function\"?k():k)/100;case \"em\":return n*this.xb(h);case \"ex\":return n*this.xb(h)/2;default:return n*e[m]}},xb:function(h){var k=h.currentStyle.fontSize,n,m;if(k.indexOf(\"px\")>0)return parseFloat(k);else if(h.tagName in f.cc){m=this;n=h.parentNode;\nreturn f.n(k).a(n,function(){return m.xb(n)})}else{h.appendChild(b);k=b.offsetWidth;b.parentNode===h&&h.removeChild(b);return k}}};f.n=function(h){return i[h]||(i[h]=new a(h))};return a}();f.Ja=function(){function a(e){this.X=e}var b=f.n(\"50%\"),c={top:1,center:1,bottom:1},d={left:1,center:1,right:1};a.prototype={zd:function(){if(!this.ac){var e=this.X,g=e.length,j=f.v,i=j.qa,h=f.n(\"0\");i=i.na;h=[\"left\",h,\"top\",h];if(g===1){e.push(new j.ob(i,\"center\"));g++}if(g===2){i&(e[0].k|e[1].k)&&e[0].d in c&&\ne[1].d in d&&e.push(e.shift());if(e[0].k&i)if(e[0].d===\"center\")h[1]=b;else h[0]=e[0].d;else if(e[0].W())h[1]=f.n(e[0].d);if(e[1].k&i)if(e[1].d===\"center\")h[3]=b;else h[2]=e[1].d;else if(e[1].W())h[3]=f.n(e[1].d)}this.ac=h}return this.ac},coords:function(e,g,j){var i=this.zd(),h=i[1].a(e,g);e=i[3].a(e,j);return{x:i[0]===\"right\"?g-h:h,y:i[2]===\"bottom\"?j-e:e}}};return a}();f.Ka=function(){function a(b,c){this.h=b;this.f=c}a.prototype={a:function(b,c,d,e,g){var j=this.h,i=this.f,h=c/d;e=e/g;if(j===\n\"contain\"){j=e>h?c:d*e;i=e>h?c/e:d}else if(j===\"cover\"){j=e<h?c:d*e;i=e<h?c/e:d}else if(j===\"auto\"){i=i===\"auto\"?g:i.a(b,d);j=i*e}else{j=j.a(b,c);i=i===\"auto\"?j/e:i.a(b,d)}return{h:j,f:i}}};a.Kc=new a(\"auto\",\"auto\");return a}();f.Ec=function(){function a(b){this.Y=b}a.prototype={Kb:/[a-z]+$/i,yb:function(){return this.ad||(this.ad=this.Y.match(this.Kb)[0].toLowerCase())},jd:function(){var b=this.Vc,c;if(b===undefined){b=this.yb();c=parseFloat(this.Y,10);b=this.Vc=b===\"deg\"?c:b===\"rad\"?c/Math.PI*180:\nb===\"grad\"?c/400*360:b===\"turn\"?c*360:0}return b}};return a}();f.Jc=function(){function a(c){this.Y=c}var b={};a.Qd=/\\s*rgba\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d+|\\d*\\.\\d+)\\s*\\)\\s*/;a.Fb={aliceblue:\"F0F8FF\",antiquewhite:\"FAEBD7\",aqua:\"0FF\",aquamarine:\"7FFFD4\",azure:\"F0FFFF\",beige:\"F5F5DC\",bisque:\"FFE4C4\",black:\"000\",blanchedalmond:\"FFEBCD\",blue:\"00F\",blueviolet:\"8A2BE2\",brown:\"A52A2A\",burlywood:\"DEB887\",cadetblue:\"5F9EA0\",chartreuse:\"7FFF00\",chocolate:\"D2691E\",coral:\"FF7F50\",cornflowerblue:\"6495ED\",\ncornsilk:\"FFF8DC\",crimson:\"DC143C\",cyan:\"0FF\",darkblue:\"00008B\",darkcyan:\"008B8B\",darkgoldenrod:\"B8860B\",darkgray:\"A9A9A9\",darkgreen:\"006400\",darkkhaki:\"BDB76B\",darkmagenta:\"8B008B\",darkolivegreen:\"556B2F\",darkorange:\"FF8C00\",darkorchid:\"9932CC\",darkred:\"8B0000\",darksalmon:\"E9967A\",darkseagreen:\"8FBC8F\",darkslateblue:\"483D8B\",darkslategray:\"2F4F4F\",darkturquoise:\"00CED1\",darkviolet:\"9400D3\",deeppink:\"FF1493\",deepskyblue:\"00BFFF\",dimgray:\"696969\",dodgerblue:\"1E90FF\",firebrick:\"B22222\",floralwhite:\"FFFAF0\",\nforestgreen:\"228B22\",fuchsia:\"F0F\",gainsboro:\"DCDCDC\",ghostwhite:\"F8F8FF\",gold:\"FFD700\",goldenrod:\"DAA520\",gray:\"808080\",green:\"008000\",greenyellow:\"ADFF2F\",honeydew:\"F0FFF0\",hotpink:\"FF69B4\",indianred:\"CD5C5C\",indigo:\"4B0082\",ivory:\"FFFFF0\",khaki:\"F0E68C\",lavender:\"E6E6FA\",lavenderblush:\"FFF0F5\",lawngreen:\"7CFC00\",lemonchiffon:\"FFFACD\",lightblue:\"ADD8E6\",lightcoral:\"F08080\",lightcyan:\"E0FFFF\",lightgoldenrodyellow:\"FAFAD2\",lightgreen:\"90EE90\",lightgrey:\"D3D3D3\",lightpink:\"FFB6C1\",lightsalmon:\"FFA07A\",\nlightseagreen:\"20B2AA\",lightskyblue:\"87CEFA\",lightslategray:\"789\",lightsteelblue:\"B0C4DE\",lightyellow:\"FFFFE0\",lime:\"0F0\",limegreen:\"32CD32\",linen:\"FAF0E6\",magenta:\"F0F\",maroon:\"800000\",mediumauqamarine:\"66CDAA\",mediumblue:\"0000CD\",mediumorchid:\"BA55D3\",mediumpurple:\"9370D8\",mediumseagreen:\"3CB371\",mediumslateblue:\"7B68EE\",mediumspringgreen:\"00FA9A\",mediumturquoise:\"48D1CC\",mediumvioletred:\"C71585\",midnightblue:\"191970\",mintcream:\"F5FFFA\",mistyrose:\"FFE4E1\",moccasin:\"FFE4B5\",navajowhite:\"FFDEAD\",\nnavy:\"000080\",oldlace:\"FDF5E6\",olive:\"808000\",olivedrab:\"688E23\",orange:\"FFA500\",orangered:\"FF4500\",orchid:\"DA70D6\",palegoldenrod:\"EEE8AA\",palegreen:\"98FB98\",paleturquoise:\"AFEEEE\",palevioletred:\"D87093\",papayawhip:\"FFEFD5\",peachpuff:\"FFDAB9\",peru:\"CD853F\",pink:\"FFC0CB\",plum:\"DDA0DD\",powderblue:\"B0E0E6\",purple:\"800080\",red:\"F00\",rosybrown:\"BC8F8F\",royalblue:\"4169E1\",saddlebrown:\"8B4513\",salmon:\"FA8072\",sandybrown:\"F4A460\",seagreen:\"2E8B57\",seashell:\"FFF5EE\",sienna:\"A0522D\",silver:\"C0C0C0\",skyblue:\"87CEEB\",\nslateblue:\"6A5ACD\",slategray:\"708090\",snow:\"FFFAFA\",springgreen:\"00FF7F\",steelblue:\"4682B4\",tan:\"D2B48C\",teal:\"008080\",thistle:\"D8BFD8\",tomato:\"FF6347\",turquoise:\"40E0D0\",violet:\"EE82EE\",wheat:\"F5DEB3\",white:\"FFF\",whitesmoke:\"F5F5F5\",yellow:\"FF0\",yellowgreen:\"9ACD32\"};a.prototype={parse:function(){if(!this.Ua){var c=this.Y,d;if(d=c.match(a.Qd)){this.Ua=\"rgb(\"+d[1]+\",\"+d[2]+\",\"+d[3]+\")\";this.Yb=parseFloat(d[4])}else{if((d=c.toLowerCase())in a.Fb)c=\"#\"+a.Fb[d];this.Ua=c;this.Yb=c===\"transparent\"?0:\n1}}},U:function(c){this.parse();return this.Ua===\"currentColor\"?c.currentStyle.color:this.Ua},fa:function(){this.parse();return this.Yb}};f.ha=function(c){return b[c]||(b[c]=new a(c))};return a}();f.v=function(){function a(c){this.$a=c;this.ch=0;this.X=[];this.Ga=0}var b=a.qa={Ia:1,Wb:2,z:4,Lc:8,Xb:16,na:32,K:64,oa:128,pa:256,Ra:512,Tc:1024,URL:2048};a.ob=function(c,d){this.k=c;this.d=d};a.ob.prototype={Ca:function(){return this.k&b.K||this.k&b.oa&&this.d===\"0\"},W:function(){return this.Ca()||this.k&\nb.Ra}};a.prototype={de:/\\s/,Kd:/^[\\+\\-]?(\\d*\\.)?\\d+/,url:/^url\\(\\s*(\"([^\"]*)\"|'([^']*)'|([!#$%&*-~]*))\\s*\\)/i,nc:/^\\-?[_a-z][\\w-]*/i,Yd:/^(\"([^\"]*)\"|'([^']*)')/,Bd:/^#([\\da-f]{6}|[\\da-f]{3})/i,be:{px:b.K,em:b.K,ex:b.K,mm:b.K,cm:b.K,\"in\":b.K,pt:b.K,pc:b.K,deg:b.Ia,rad:b.Ia,grad:b.Ia},fd:{rgb:1,rgba:1,hsl:1,hsla:1},next:function(c){function d(p,r){p=new a.ob(p,r);if(!c){k.X.push(p);k.Ga++}return p}function e(){k.Ga++;return null}var g,j,i,h,k=this;if(this.Ga<this.X.length)return this.X[this.Ga++];for(;this.de.test(this.$a.charAt(this.ch));)this.ch++;\nif(this.ch>=this.$a.length)return e();j=this.ch;g=this.$a.substring(this.ch);i=g.charAt(0);switch(i){case \"#\":if(h=g.match(this.Bd)){this.ch+=h[0].length;return d(b.z,h[0])}break;case '\"':case \"'\":if(h=g.match(this.Yd)){this.ch+=h[0].length;return d(b.Tc,h[2]||h[3]||\"\")}break;case \"/\":case \",\":this.ch++;return d(b.pa,i);case \"u\":if(h=g.match(this.url)){this.ch+=h[0].length;return d(b.URL,h[2]||h[3]||h[4]||\"\")}}if(h=g.match(this.Kd)){i=h[0];this.ch+=i.length;if(g.charAt(i.length)===\"%\"){this.ch++;\nreturn d(b.Ra,i+\"%\")}if(h=g.substring(i.length).match(this.nc)){i+=h[0];this.ch+=h[0].length;return d(this.be[h[0].toLowerCase()]||b.Lc,i)}return d(b.oa,i)}if(h=g.match(this.nc)){i=h[0];this.ch+=i.length;if(i.toLowerCase()in f.Jc.Fb||i===\"currentColor\"||i===\"transparent\")return d(b.z,i);if(g.charAt(i.length)===\"(\"){this.ch++;if(i.toLowerCase()in this.fd){g=function(p){return p&&p.k&b.oa};h=function(p){return p&&p.k&(b.oa|b.Ra)};var n=function(p,r){return p&&p.d===r},m=function(){return k.next(1)};\nif((i.charAt(0)===\"r\"?h(m()):g(m()))&&n(m(),\",\")&&h(m())&&n(m(),\",\")&&h(m())&&(i===\"rgb\"||i===\"hsa\"||n(m(),\",\")&&g(m()))&&n(m(),\")\"))return d(b.z,this.$a.substring(j,this.ch));return e()}return d(b.Xb,i)}return d(b.na,i)}this.ch++;return d(b.Wb,i)},D:function(){return this.X[this.Ga-- -2]},all:function(){for(;this.next(););return this.X},ma:function(c,d){for(var e=[],g,j;g=this.next();){if(c(g)){j=true;this.D();break}e.push(g)}return d&&!j?null:e}};return a}();var ha=function(a){this.e=a};ha.prototype=\n{Z:0,Od:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.x!==b.x||a.y!==b.y)},Td:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.h!==b.h||a.f!==b.f)},hc:function(){var a=this.e,b=a.getBoundingClientRect(),c=f.ja===9,d=f.O===7,e=b.right-b.left;return{x:b.left,y:b.top,h:c||d?a.offsetWidth:e,f:c||d?a.offsetHeight:b.bottom-b.top,Hd:d&&e?a.offsetWidth/e:1}},o:function(){return this.Z?this.Va||(this.Va=this.hc()):this.hc()},Ad:function(){return!!this.qb},cb:function(){++this.Z},hb:function(){if(!--this.Z){if(this.Va)this.qb=\nthis.Va;this.Va=null}}};(function(){function a(b){var c=f.p.Ba(b);return function(){if(this.Z){var d=this.$b||(this.$b={});return c in d?d[c]:(d[c]=b.call(this))}else return b.call(this)}}f.B={Z:0,ka:function(b){function c(d){this.e=d;this.Zb=this.ia()}f.p.Eb(c.prototype,f.B,b);c.$c={};return c},j:function(){var b=this.ia(),c=this.constructor.$c;return b?b in c?c[b]:(c[b]=this.la(b)):null},ia:a(function(){var b=this.e,c=this.constructor,d=b.style;b=b.currentStyle;var e=this.wa,g=this.Fa,j=c.Yc||(c.Yc=\nf.F+e);c=c.Zc||(c.Zc=f.nb+g.charAt(0).toUpperCase()+g.substring(1));return d[c]||b.getAttribute(j)||d[g]||b.getAttribute(e)}),i:a(function(){return!!this.j()}),H:a(function(){var b=this.ia(),c=b!==this.Zb;this.Zb=b;return c}),va:a,cb:function(){++this.Z},hb:function(){--this.Z||delete this.$b}}})();f.Sb=f.B.ka({wa:f.F+\"background\",Fa:f.nb+\"Background\",cd:{scroll:1,fixed:1,local:1},fb:{\"repeat-x\":1,\"repeat-y\":1,repeat:1,\"no-repeat\":1},sc:{\"padding-box\":1,\"border-box\":1,\"content-box\":1},Pd:{top:1,right:1,\nbottom:1,left:1,center:1},Ud:{contain:1,cover:1},eb:{Ma:\"backgroundClip\",z:\"backgroundColor\",da:\"backgroundImage\",Pa:\"backgroundOrigin\",S:\"backgroundPosition\",T:\"backgroundRepeat\",Sa:\"backgroundSize\"},la:function(a){function b(s){return s&&s.W()||s.k&k&&s.d in t}function c(s){return s&&(s.W()&&f.n(s.d)||s.d===\"auto\"&&\"auto\")}var d=this.e.currentStyle,e,g,j,i=f.v.qa,h=i.pa,k=i.na,n=i.z,m,p,r=0,t=this.Pd,v,l,q={M:[]};if(this.wb()){e=new f.v(a);for(j={};g=e.next();){m=g.k;p=g.d;if(!j.P&&m&i.Xb&&p===\n\"linear-gradient\"){v={ca:[],P:p};for(l={};g=e.next();){m=g.k;p=g.d;if(m&i.Wb&&p===\")\"){l.color&&v.ca.push(l);v.ca.length>1&&f.p.Eb(j,v);break}if(m&n){if(v.sa||v.zb){g=e.D();if(g.k!==h)break;e.next()}l={color:f.ha(p)};g=e.next();if(g.W())l.db=f.n(g.d);else e.D()}else if(m&i.Ia&&!v.sa&&!l.color&&!v.ca.length)v.sa=new f.Ec(g.d);else if(b(g)&&!v.zb&&!l.color&&!v.ca.length){e.D();v.zb=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&h&&p===\",\"){if(l.color){v.ca.push(l);l={}}}else break}}else if(!j.P&&\nm&i.URL){j.Ab=p;j.P=\"image\"}else if(b(g)&&!j.$){e.D();j.$=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&k)if(p in this.fb&&!j.bb)j.bb=p;else if(p in this.sc&&!j.Wa){j.Wa=p;if((g=e.next())&&g.k&k&&g.d in this.sc)j.ub=g.d;else{j.ub=p;e.D()}}else if(p in this.cd&&!j.bc)j.bc=p;else return null;else if(m&n&&!q.color)q.color=f.ha(p);else if(m&h&&p===\"/\"&&!j.Xa&&j.$){g=e.next();if(g.k&k&&g.d in this.Ud)j.Xa=new f.Ka(g.d);else if(g=c(g)){m=c(e.next());if(!m){m=g;e.D()}j.Xa=new f.Ka(g,m)}else return null}else if(m&\nh&&p===\",\"&&j.P){j.Hb=a.substring(r,e.ch-1);r=e.ch;q.M.push(j);j={}}else return null}if(j.P){j.Hb=a.substring(r);q.M.push(j)}}else this.Bc(f.ja<9?function(){var s=this.eb,o=d[s.S+\"X\"],u=d[s.S+\"Y\"],x=d[s.da],y=d[s.z];if(y!==\"transparent\")q.color=f.ha(y);if(x!==\"none\")q.M=[{P:\"image\",Ab:(new f.v(x)).next().d,bb:d[s.T],$:new f.Ja((new f.v(o+\" \"+u)).all())}]}:function(){var s=this.eb,o=/\\s*,\\s*/,u=d[s.da].split(o),x=d[s.z],y,z,B,E,D,C;if(x!==\"transparent\")q.color=f.ha(x);if((E=u.length)&&u[0]!==\"none\"){x=\nd[s.T].split(o);y=d[s.S].split(o);z=d[s.Pa].split(o);B=d[s.Ma].split(o);s=d[s.Sa].split(o);q.M=[];for(o=0;o<E;o++)if((D=u[o])&&D!==\"none\"){C=s[o].split(\" \");q.M.push({Hb:D+\" \"+x[o]+\" \"+y[o]+\" / \"+s[o]+\" \"+z[o]+\" \"+B[o],P:\"image\",Ab:(new f.v(D)).next().d,bb:x[o],$:new f.Ja((new f.v(y[o])).all()),Wa:z[o],ub:B[o],Xa:new f.Ka(C[0],C[1])})}}});return q.color||q.M[0]?q:null},Bc:function(a){var b=f.ja>8,c=this.eb,d=this.e.runtimeStyle,e=d[c.da],g=d[c.z],j=d[c.T],i,h,k,n;if(e)d[c.da]=\"\";if(g)d[c.z]=\"\";if(j)d[c.T]=\n\"\";if(b){i=d[c.Ma];h=d[c.Pa];n=d[c.S];k=d[c.Sa];if(i)d[c.Ma]=\"\";if(h)d[c.Pa]=\"\";if(n)d[c.S]=\"\";if(k)d[c.Sa]=\"\"}a=a.call(this);if(e)d[c.da]=e;if(g)d[c.z]=g;if(j)d[c.T]=j;if(b){if(i)d[c.Ma]=i;if(h)d[c.Pa]=h;if(n)d[c.S]=n;if(k)d[c.Sa]=k}return a},ia:f.B.va(function(){return this.wb()||this.Bc(function(){var a=this.e.currentStyle,b=this.eb;return a[b.z]+\" \"+a[b.da]+\" \"+a[b.T]+\" \"+a[b.S+\"X\"]+\" \"+a[b.S+\"Y\"]})}),wb:f.B.va(function(){var a=this.e;return a.style[this.Fa]||a.currentStyle.getAttribute(this.wa)}),\nqc:function(){var a=0;if(f.O<7){a=this.e;a=\"\"+(a.style[f.nb+\"PngFix\"]||a.currentStyle.getAttribute(f.F+\"png-fix\"))===\"true\"}return a},i:f.B.va(function(){return(this.wb()||this.qc())&&!!this.j()})});f.Vb=f.B.ka({wc:[\"Top\",\"Right\",\"Bottom\",\"Left\"],Id:{thin:\"1px\",medium:\"3px\",thick:\"5px\"},la:function(){var a={},b={},c={},d=false,e=true,g=true,j=true;this.Cc(function(){for(var i=this.e.currentStyle,h=0,k,n,m,p,r,t,v;h<4;h++){m=this.wc[h];v=m.charAt(0).toLowerCase();k=b[v]=i[\"border\"+m+\"Style\"];n=i[\"border\"+\nm+\"Color\"];m=i[\"border\"+m+\"Width\"];if(h>0){if(k!==p)g=false;if(n!==r)e=false;if(m!==t)j=false}p=k;r=n;t=m;c[v]=f.ha(n);m=a[v]=f.n(b[v]===\"none\"?\"0\":this.Id[m]||m);if(m.a(this.e)>0)d=true}});return d?{J:a,Zd:b,gd:c,ee:j,hd:e,$d:g}:null},ia:f.B.va(function(){var a=this.e,b=a.currentStyle,c;a.tagName in f.Ac&&a.offsetParent.currentStyle.borderCollapse===\"collapse\"||this.Cc(function(){c=b.borderWidth+\"|\"+b.borderStyle+\"|\"+b.borderColor});return c}),Cc:function(a){var b=this.e.runtimeStyle,c=b.borderWidth,\nd=b.borderColor;if(c)b.borderWidth=\"\";if(d)b.borderColor=\"\";a=a.call(this);if(c)b.borderWidth=c;if(d)b.borderColor=d;return a}});(function(){f.jb=f.B.ka({wa:\"border-radius\",Fa:\"borderRadius\",la:function(b){var c=null,d,e,g,j,i=false;if(b){e=new f.v(b);var h=function(){for(var k=[],n;(g=e.next())&&g.W();){j=f.n(g.d);n=j.ic();if(n<0)return null;if(n>0)i=true;k.push(j)}return k.length>0&&k.length<5?{tl:k[0],tr:k[1]||k[0],br:k[2]||k[0],bl:k[3]||k[1]||k[0]}:null};if(b=h()){if(g){if(g.k&f.v.qa.pa&&g.d===\n\"/\")d=h()}else d=b;if(i&&b&&d)c={x:b,y:d}}}return c}});var a=f.n(\"0\");a={tl:a,tr:a,br:a,bl:a};f.jb.Dc={x:a,y:a}})();f.Ub=f.B.ka({wa:\"border-image\",Fa:\"borderImage\",fb:{stretch:1,round:1,repeat:1,space:1},la:function(a){var b=null,c,d,e,g,j,i,h=0,k=f.v.qa,n=k.na,m=k.oa,p=k.Ra;if(a){c=new f.v(a);b={};for(var r=function(l){return l&&l.k&k.pa&&l.d===\"/\"},t=function(l){return l&&l.k&n&&l.d===\"fill\"},v=function(){g=c.ma(function(l){return!(l.k&(m|p))});if(t(c.next())&&!b.fill)b.fill=true;else c.D();if(r(c.next())){h++;\nj=c.ma(function(l){return!l.W()&&!(l.k&n&&l.d===\"auto\")});if(r(c.next())){h++;i=c.ma(function(l){return!l.Ca()})}}else c.D()};a=c.next();){d=a.k;e=a.d;if(d&(m|p)&&!g){c.D();v()}else if(t(a)&&!b.fill){b.fill=true;v()}else if(d&n&&this.fb[e]&&!b.repeat){b.repeat={f:e};if(a=c.next())if(a.k&n&&this.fb[a.d])b.repeat.Ob=a.d;else c.D()}else if(d&k.URL&&!b.src)b.src=e;else return null}if(!b.src||!g||g.length<1||g.length>4||j&&j.length>4||h===1&&j.length<1||i&&i.length>4||h===2&&i.length<1)return null;if(!b.repeat)b.repeat=\n{f:\"stretch\"};if(!b.repeat.Ob)b.repeat.Ob=b.repeat.f;a=function(l,q){return{t:q(l[0]),r:q(l[1]||l[0]),b:q(l[2]||l[0]),l:q(l[3]||l[1]||l[0])}};b.slice=a(g,function(l){return f.n(l.k&m?l.d+\"px\":l.d)});if(j&&j[0])b.J=a(j,function(l){return l.W()?f.n(l.d):l.d});if(i&&i[0])b.Da=a(i,function(l){return l.Ca()?f.n(l.d):l.d})}return b}});f.Ic=f.B.ka({wa:\"box-shadow\",Fa:\"boxShadow\",la:function(a){var b,c=f.n,d=f.v.qa,e;if(a){e=new f.v(a);b={Da:[],Bb:[]};for(a=function(){for(var g,j,i,h,k,n;g=e.next();){i=g.d;\nj=g.k;if(j&d.pa&&i===\",\")break;else if(g.Ca()&&!k){e.D();k=e.ma(function(m){return!m.Ca()})}else if(j&d.z&&!h)h=i;else if(j&d.na&&i===\"inset\"&&!n)n=true;else return false}g=k&&k.length;if(g>1&&g<5){(n?b.Bb:b.Da).push({fe:c(k[0].d),ge:c(k[1].d),blur:c(k[2]?k[2].d:\"0\"),Vd:c(k[3]?k[3].d:\"0\"),color:f.ha(h||\"currentColor\")});return true}return false};a(););}return b&&(b.Bb.length||b.Da.length)?b:null}});f.Uc=f.B.ka({ia:f.B.va(function(){var a=this.e.currentStyle;return a.visibility+\"|\"+a.display}),la:function(){var a=\nthis.e,b=a.runtimeStyle;a=a.currentStyle;var c=b.visibility,d;b.visibility=\"\";d=a.visibility;b.visibility=c;return{ce:d!==\"hidden\",nd:a.display!==\"none\"}},i:function(){return false}});f.u={R:function(a){function b(c,d,e,g){this.e=c;this.s=d;this.g=e;this.parent=g}f.p.Eb(b.prototype,f.u,a);return b},Cb:false,Q:function(){return false},Ea:f.aa,Lb:function(){this.m();this.i()&&this.V()},ib:function(){this.Cb=true},Mb:function(){this.i()?this.V():this.m()},sb:function(a,b){this.vc(a);for(var c=this.ra||\n(this.ra=[]),d=a+1,e=c.length,g;d<e;d++)if(g=c[d])break;c[a]=b;this.I().insertBefore(b,g||null)},za:function(a){var b=this.ra;return b&&b[a]||null},vc:function(a){var b=this.za(a),c=this.Ta;if(b&&c){c.removeChild(b);this.ra[a]=null}},Aa:function(a,b,c,d){var e=this.rb||(this.rb={}),g=e[a];if(!g){g=e[a]=f.p.Za(\"shape\");if(b)g.appendChild(g[b]=f.p.Za(b));if(d){c=this.za(d);if(!c){this.sb(d,doc.createElement(\"group\"+d));c=this.za(d)}}c.appendChild(g);a=g.style;a.position=\"absolute\";a.left=a.top=0;a.behavior=\n\"url(#default#VML)\"}return g},vb:function(a){var b=this.rb,c=b&&b[a];if(c){c.parentNode.removeChild(c);delete b[a]}return!!c},kc:function(a){var b=this.e,c=this.s.o(),d=c.h,e=c.f,g,j,i,h,k,n;c=a.x.tl.a(b,d);g=a.y.tl.a(b,e);j=a.x.tr.a(b,d);i=a.y.tr.a(b,e);h=a.x.br.a(b,d);k=a.y.br.a(b,e);n=a.x.bl.a(b,d);a=a.y.bl.a(b,e);d=Math.min(d/(c+j),e/(i+k),d/(n+h),e/(g+a));if(d<1){c*=d;g*=d;j*=d;i*=d;h*=d;k*=d;n*=d;a*=d}return{x:{tl:c,tr:j,br:h,bl:n},y:{tl:g,tr:i,br:k,bl:a}}},ya:function(a,b,c){b=b||1;var d,e,\ng=this.s.o();e=g.h*b;g=g.f*b;var j=this.g.G,i=Math.floor,h=Math.ceil,k=a?a.Jb*b:0,n=a?a.Ib*b:0,m=a?a.tb*b:0;a=a?a.Db*b:0;var p,r,t,v,l;if(c||j.i()){d=this.kc(c||j.j());c=d.x.tl*b;j=d.y.tl*b;p=d.x.tr*b;r=d.y.tr*b;t=d.x.br*b;v=d.y.br*b;l=d.x.bl*b;b=d.y.bl*b;e=\"m\"+i(a)+\",\"+i(j)+\"qy\"+i(c)+\",\"+i(k)+\"l\"+h(e-p)+\",\"+i(k)+\"qx\"+h(e-n)+\",\"+i(r)+\"l\"+h(e-n)+\",\"+h(g-v)+\"qy\"+h(e-t)+\",\"+h(g-m)+\"l\"+i(l)+\",\"+h(g-m)+\"qx\"+i(a)+\",\"+h(g-b)+\" x e\"}else e=\"m\"+i(a)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+h(g-m)+\"l\"+i(a)+\n\",\"+h(g-m)+\"xe\";return e},I:function(){var a=this.parent.za(this.N),b;if(!a){a=doc.createElement(this.Ya);b=a.style;b.position=\"absolute\";b.top=b.left=0;this.parent.sb(this.N,a)}return a},mc:function(){var a=this.e,b=a.currentStyle,c=a.runtimeStyle,d=a.tagName,e=f.O===6,g;if(e&&(d in f.cc||d===\"FIELDSET\")||d===\"BUTTON\"||d===\"INPUT\"&&a.type in f.Gd){c.borderWidth=\"\";d=this.g.w.wc;for(g=d.length;g--;){e=d[g];c[\"padding\"+e]=\"\";c[\"padding\"+e]=f.n(b[\"padding\"+e]).a(a)+f.n(b[\"border\"+e+\"Width\"]).a(a)+(f.O!==\n8&&g%2?1:0)}c.borderWidth=0}else if(e){if(a.childNodes.length!==1||a.firstChild.tagName!==\"ie6-mask\"){b=doc.createElement(\"ie6-mask\");d=b.style;d.visibility=\"visible\";for(d.zoom=1;d=a.firstChild;)b.appendChild(d);a.appendChild(b);c.visibility=\"hidden\"}}else c.borderColor=\"transparent\"},ie:function(){},m:function(){this.parent.vc(this.N);delete this.rb;delete this.ra}};f.Rc=f.u.R({i:function(){var a=this.ed;for(var b in a)if(a.hasOwnProperty(b)&&a[b].i())return true;return false},Q:function(){return this.g.Pb.H()},\nib:function(){if(this.i()){var a=this.jc(),b=a,c;a=a.currentStyle;var d=a.position,e=this.I().style,g=0,j=0;j=this.s.o();var i=j.Hd;if(d===\"fixed\"&&f.O>6){g=j.x*i;j=j.y*i;b=d}else{do b=b.offsetParent;while(b&&b.currentStyle.position===\"static\");if(b){c=b.getBoundingClientRect();b=b.currentStyle;g=(j.x-c.left)*i-(parseFloat(b.borderLeftWidth)||0);j=(j.y-c.top)*i-(parseFloat(b.borderTopWidth)||0)}else{b=doc.documentElement;g=(j.x+b.scrollLeft-b.clientLeft)*i;j=(j.y+b.scrollTop-b.clientTop)*i}b=\"absolute\"}e.position=\nb;e.left=g;e.top=j;e.zIndex=d===\"static\"?-1:a.zIndex;this.Cb=true}},Mb:f.aa,Nb:function(){var a=this.g.Pb.j();this.I().style.display=a.ce&&a.nd?\"\":\"none\"},Lb:function(){this.i()?this.Nb():this.m()},jc:function(){var a=this.e;return a.tagName in f.Ac?a.offsetParent:a},I:function(){var a=this.Ta,b;if(!a){b=this.jc();a=this.Ta=doc.createElement(\"css3-container\");a.style.direction=\"ltr\";this.Nb();b.parentNode.insertBefore(a,b)}return a},ab:f.aa,m:function(){var a=this.Ta,b;if(a&&(b=a.parentNode))b.removeChild(a);\ndelete this.Ta;delete this.ra}});f.Fc=f.u.R({N:2,Ya:\"background\",Q:function(){var a=this.g;return a.C.H()||a.G.H()},i:function(){var a=this.g;return a.q.i()||a.G.i()||a.C.i()||a.ga.i()&&a.ga.j().Bb},V:function(){var a=this.s.o();if(a.h&&a.f){this.od();this.pd()}},od:function(){var a=this.g.C.j(),b=this.s.o(),c=this.e,d=a&&a.color,e,g;if(d&&d.fa()>0){this.lc();a=this.Aa(\"bgColor\",\"fill\",this.I(),1);e=b.h;b=b.f;a.stroked=false;a.coordsize=e*2+\",\"+b*2;a.coordorigin=\"1,1\";a.path=this.ya(null,2);g=a.style;\ng.width=e;g.height=b;a.fill.color=d.U(c);c=d.fa();if(c<1)a.fill.opacity=c}else this.vb(\"bgColor\")},pd:function(){var a=this.g.C.j(),b=this.s.o();a=a&&a.M;var c,d,e,g,j;if(a){this.lc();d=b.h;e=b.f;for(j=a.length;j--;){b=a[j];c=this.Aa(\"bgImage\"+j,\"fill\",this.I(),2);c.stroked=false;c.fill.type=\"tile\";c.fillcolor=\"none\";c.coordsize=d*2+\",\"+e*2;c.coordorigin=\"1,1\";c.path=this.ya(0,2);g=c.style;g.width=d;g.height=e;if(b.P===\"linear-gradient\")this.bd(c,b);else{c.fill.src=b.Ab;this.Nd(c,j)}}}for(j=a?a.length:\n0;this.vb(\"bgImage\"+j++););},Nd:function(a,b){var c=this;f.p.Rb(a.fill.src,function(d){var e=c.e,g=c.s.o(),j=g.h;g=g.f;if(j&&g){var i=a.fill,h=c.g,k=h.w.j(),n=k&&k.J;k=n?n.t.a(e):0;var m=n?n.r.a(e):0,p=n?n.b.a(e):0;n=n?n.l.a(e):0;h=h.C.j().M[b];e=h.$?h.$.coords(e,j-d.h-n-m,g-d.f-k-p):{x:0,y:0};h=h.bb;p=m=0;var r=j+1,t=g+1,v=f.O===8?0:1;n=Math.round(e.x)+n+0.5;k=Math.round(e.y)+k+0.5;i.position=n/j+\",\"+k/g;i.size.x=1;i.size=d.h+\"px,\"+d.f+\"px\";if(h&&h!==\"repeat\"){if(h===\"repeat-x\"||h===\"no-repeat\"){m=\nk+1;t=k+d.f+v}if(h===\"repeat-y\"||h===\"no-repeat\"){p=n+1;r=n+d.h+v}a.style.clip=\"rect(\"+m+\"px,\"+r+\"px,\"+t+\"px,\"+p+\"px)\"}}})},bd:function(a,b){var c=this.e,d=this.s.o(),e=d.h,g=d.f;a=a.fill;d=b.ca;var j=d.length,i=Math.PI,h=f.Na,k=h.tc,n=h.dc;b=h.gc(c,e,g,b);h=b.sa;var m=b.xc,p=b.yc,r=b.Wd,t=b.Xd,v=b.rd,l=b.sd,q=b.kd,s=b.ld;b=b.rc;e=h%90?Math.atan2(q*e/g,s)/i*180:h+90;e+=180;e%=360;v=k(r,t,h,v,l);g=n(r,t,v[0],v[1]);i=[];v=k(m,p,h,r,t);n=n(m,p,v[0],v[1])/g*100;k=[];for(h=0;h<j;h++)k.push(d[h].db?d[h].db.a(c,\nb):h===0?0:h===j-1?b:null);for(h=1;h<j;h++){if(k[h]===null){m=k[h-1];b=h;do p=k[++b];while(p===null);k[h]=m+(p-m)/(b-h+1)}k[h]=Math.max(k[h],k[h-1])}for(h=0;h<j;h++)i.push(n+k[h]/g*100+\"% \"+d[h].color.U(c));a.angle=e;a.type=\"gradient\";a.method=\"sigma\";a.color=d[0].color.U(c);a.color2=d[j-1].color.U(c);if(a.colors)a.colors.value=i.join(\",\");else a.colors=i.join(\",\")},lc:function(){var a=this.e.runtimeStyle;a.backgroundImage=\"url(about:blank)\";a.backgroundColor=\"transparent\"},m:function(){f.u.m.call(this);\nvar a=this.e.runtimeStyle;a.backgroundImage=a.backgroundColor=\"\"}});f.Gc=f.u.R({N:4,Ya:\"border\",Q:function(){var a=this.g;return a.w.H()||a.G.H()},i:function(){var a=this.g;return a.G.i()&&!a.q.i()&&a.w.i()},V:function(){var a=this.e,b=this.g.w.j(),c=this.s.o(),d=c.h;c=c.f;var e,g,j,i,h;if(b){this.mc();b=this.wd(2);i=0;for(h=b.length;i<h;i++){j=b[i];e=this.Aa(\"borderPiece\"+i,j.stroke?\"stroke\":\"fill\",this.I());e.coordsize=d*2+\",\"+c*2;e.coordorigin=\"1,1\";e.path=j.path;g=e.style;g.width=d;g.height=c;\ne.filled=!!j.fill;e.stroked=!!j.stroke;if(j.stroke){e=e.stroke;e.weight=j.Qb+\"px\";e.color=j.color.U(a);e.dashstyle=j.stroke===\"dashed\"?\"2 2\":j.stroke===\"dotted\"?\"1 1\":\"solid\";e.linestyle=j.stroke===\"double\"&&j.Qb>2?\"ThinThin\":\"Single\"}else e.fill.color=j.fill.U(a)}for(;this.vb(\"borderPiece\"+i++););}},wd:function(a){var b=this.e,c,d,e,g=this.g.w,j=[],i,h,k,n,m=Math.round,p,r,t;if(g.i()){c=g.j();g=c.J;r=c.Zd;t=c.gd;if(c.ee&&c.$d&&c.hd){if(t.t.fa()>0){c=g.t.a(b);k=c/2;j.push({path:this.ya({Jb:k,Ib:k,\ntb:k,Db:k},a),stroke:r.t,color:t.t,Qb:c})}}else{a=a||1;c=this.s.o();d=c.h;e=c.f;c=m(g.t.a(b));k=m(g.r.a(b));n=m(g.b.a(b));b=m(g.l.a(b));var v={t:c,r:k,b:n,l:b};b=this.g.G;if(b.i())p=this.kc(b.j());i=Math.floor;h=Math.ceil;var l=function(o,u){return p?p[o][u]:0},q=function(o,u,x,y,z,B){var E=l(\"x\",o),D=l(\"y\",o),C=o.charAt(1)===\"r\";o=o.charAt(0)===\"b\";return E>0&&D>0?(B?\"al\":\"ae\")+(C?h(d-E):i(E))*a+\",\"+(o?h(e-D):i(D))*a+\",\"+(i(E)-u)*a+\",\"+(i(D)-x)*a+\",\"+y*65535+\",\"+2949075*(z?1:-1):(B?\"m\":\"l\")+(C?d-\nu:u)*a+\",\"+(o?e-x:x)*a},s=function(o,u,x,y){var z=o===\"t\"?i(l(\"x\",\"tl\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+i(l(\"y\",\"tr\"))*a:o===\"b\"?h(d-l(\"x\",\"br\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+h(e-l(\"y\",\"bl\"))*a;o=o===\"t\"?h(d-l(\"x\",\"tr\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+h(e-l(\"y\",\"br\"))*a:o===\"b\"?i(l(\"x\",\"bl\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+i(l(\"y\",\"tl\"))*a;return x?(y?\"m\"+o:\"\")+\"l\"+z:(y?\"m\"+z:\"\")+\"l\"+o};b=function(o,u,x,y,z,B){var E=o===\"l\"||o===\"r\",D=v[o],C,F;if(D>0&&r[o]!==\"none\"&&t[o].fa()>0){C=v[E?o:u];u=v[E?u:\no];F=v[E?o:x];x=v[E?x:o];if(r[o]===\"dashed\"||r[o]===\"dotted\"){j.push({path:q(y,C,u,B+45,0,1)+q(y,0,0,B,1,0),fill:t[o]});j.push({path:s(o,D/2,0,1),stroke:r[o],Qb:D,color:t[o]});j.push({path:q(z,F,x,B,0,1)+q(z,0,0,B-45,1,0),fill:t[o]})}else j.push({path:q(y,C,u,B+45,0,1)+s(o,D,0,0)+q(z,F,x,B,0,0)+(r[o]===\"double\"&&D>2?q(z,F-i(F/3),x-i(x/3),B-45,1,0)+s(o,h(D/3*2),1,0)+q(y,C-i(C/3),u-i(u/3),B,1,0)+\"x \"+q(y,i(C/3),i(u/3),B+45,0,1)+s(o,i(D/3),1,0)+q(z,i(F/3),i(x/3),B,0,0):\"\")+q(z,0,0,B-45,1,0)+s(o,0,1,\n0)+q(y,0,0,B,1,0),fill:t[o]})}};b(\"t\",\"l\",\"r\",\"tl\",\"tr\",90);b(\"r\",\"t\",\"b\",\"tr\",\"br\",0);b(\"b\",\"r\",\"l\",\"br\",\"bl\",-90);b(\"l\",\"b\",\"t\",\"bl\",\"tl\",-180)}}return j},m:function(){if(this.ec||!this.g.q.i())this.e.runtimeStyle.borderColor=\"\";f.u.m.call(this)}});f.Tb=f.u.R({N:5,Md:[\"t\",\"tr\",\"r\",\"br\",\"b\",\"bl\",\"l\",\"tl\",\"c\"],Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){this.I();var a=this.g.q.j(),b=this.g.w.j(),c=this.s.o(),d=this.e,e=this.uc;f.p.Rb(a.src,function(g){function j(s,\no,u,x,y){s=e[s].style;var z=Math.max;s.width=z(o,0);s.height=z(u,0);s.left=x;s.top=y}function i(s,o,u){for(var x=0,y=s.length;x<y;x++)e[s[x]].imagedata[o]=u}var h=c.h,k=c.f,n=f.n(\"0\"),m=a.J||(b?b.J:{t:n,r:n,b:n,l:n});n=m.t.a(d);var p=m.r.a(d),r=m.b.a(d);m=m.l.a(d);var t=a.slice,v=t.t.a(d),l=t.r.a(d),q=t.b.a(d);t=t.l.a(d);j(\"tl\",m,n,0,0);j(\"t\",h-m-p,n,m,0);j(\"tr\",p,n,h-p,0);j(\"r\",p,k-n-r,h-p,n);j(\"br\",p,r,h-p,k-r);j(\"b\",h-m-p,r,m,k-r);j(\"bl\",m,r,0,k-r);j(\"l\",m,k-n-r,0,n);j(\"c\",h-m-p,k-n-r,m,n);i([\"tl\",\n\"t\",\"tr\"],\"cropBottom\",(g.f-v)/g.f);i([\"tl\",\"l\",\"bl\"],\"cropRight\",(g.h-t)/g.h);i([\"bl\",\"b\",\"br\"],\"cropTop\",(g.f-q)/g.f);i([\"tr\",\"r\",\"br\"],\"cropLeft\",(g.h-l)/g.h);i([\"l\",\"r\",\"c\"],\"cropTop\",v/g.f);i([\"l\",\"r\",\"c\"],\"cropBottom\",q/g.f);i([\"t\",\"b\",\"c\"],\"cropLeft\",t/g.h);i([\"t\",\"b\",\"c\"],\"cropRight\",l/g.h);e.c.style.display=a.fill?\"\":\"none\"},this)},I:function(){var a=this.parent.za(this.N),b,c,d,e=this.Md,g=e.length;if(!a){a=doc.createElement(\"border-image\");b=a.style;b.position=\"absolute\";this.uc={};for(d=\n0;d<g;d++){c=this.uc[e[d]]=f.p.Za(\"rect\");c.appendChild(f.p.Za(\"imagedata\"));b=c.style;b.behavior=\"url(#default#VML)\";b.position=\"absolute\";b.top=b.left=0;c.imagedata.src=this.g.q.j().src;c.stroked=false;c.filled=false;a.appendChild(c)}this.parent.sb(this.N,a)}return a},Ea:function(){if(this.i()){var a=this.e,b=a.runtimeStyle,c=this.g.q.j().J;b.borderStyle=\"solid\";if(c){b.borderTopWidth=c.t.a(a)+\"px\";b.borderRightWidth=c.r.a(a)+\"px\";b.borderBottomWidth=c.b.a(a)+\"px\";b.borderLeftWidth=c.l.a(a)+\"px\"}this.mc()}},\nm:function(){var a=this.e.runtimeStyle;a.borderStyle=\"\";if(this.ec||!this.g.w.i())a.borderColor=a.borderWidth=\"\";f.u.m.call(this)}});f.Hc=f.u.R({N:1,Ya:\"outset-box-shadow\",Q:function(){var a=this.g;return a.ga.H()||a.G.H()},i:function(){var a=this.g.ga;return a.i()&&a.j().Da[0]},V:function(){function a(C,F,O,H,M,P,I){C=b.Aa(\"shadow\"+C+F,\"fill\",d,j-C);F=C.fill;C.coordsize=n*2+\",\"+m*2;C.coordorigin=\"1,1\";C.stroked=false;C.filled=true;F.color=M.U(c);if(P){F.type=\"gradienttitle\";F.color2=F.color;F.opacity=\n0}C.path=I;l=C.style;l.left=O;l.top=H;l.width=n;l.height=m;return C}var b=this,c=this.e,d=this.I(),e=this.g,g=e.ga.j().Da;e=e.G.j();var j=g.length,i=j,h,k=this.s.o(),n=k.h,m=k.f;k=f.O===8?1:0;for(var p=[\"tl\",\"tr\",\"br\",\"bl\"],r,t,v,l,q,s,o,u,x,y,z,B,E,D;i--;){t=g[i];q=t.fe.a(c);s=t.ge.a(c);h=t.Vd.a(c);o=t.blur.a(c);t=t.color;u=-h-o;if(!e&&o)e=f.jb.Dc;u=this.ya({Jb:u,Ib:u,tb:u,Db:u},2,e);if(o){x=(h+o)*2+n;y=(h+o)*2+m;z=x?o*2/x:0;B=y?o*2/y:0;if(o-h>n/2||o-h>m/2)for(h=4;h--;){r=p[h];E=r.charAt(0)===\"b\";\nD=r.charAt(1)===\"r\";r=a(i,r,q,s,t,o,u);v=r.fill;v.focusposition=(D?1-z:z)+\",\"+(E?1-B:B);v.focussize=\"0,0\";r.style.clip=\"rect(\"+((E?y/2:0)+k)+\"px,\"+(D?x:x/2)+\"px,\"+(E?y:y/2)+\"px,\"+((D?x/2:0)+k)+\"px)\"}else{r=a(i,\"\",q,s,t,o,u);v=r.fill;v.focusposition=z+\",\"+B;v.focussize=1-z*2+\",\"+(1-B*2)}}else{r=a(i,\"\",q,s,t,o,u);q=t.fa();if(q<1)r.fill.opacity=q}}}});f.Pc=f.u.R({N:6,Ya:\"imgEl\",Q:function(){var a=this.g;return this.e.src!==this.Xc||a.G.H()},i:function(){var a=this.g;return a.G.i()||a.C.qc()},V:function(){this.Xc=\nj;this.Cd();var a=this.Aa(\"img\",\"fill\",this.I()),b=a.fill,c=this.s.o(),d=c.h;c=c.f;var e=this.g.w.j(),g=e&&e.J;e=this.e;var j=e.src,i=Math.round,h=e.currentStyle,k=f.n;if(!g||f.O<7){g=f.n(\"0\");g={t:g,r:g,b:g,l:g}}a.stroked=false;b.type=\"frame\";b.src=j;b.position=(d?0.5/d:0)+\",\"+(c?0.5/c:0);a.coordsize=d*2+\",\"+c*2;a.coordorigin=\"1,1\";a.path=this.ya({Jb:i(g.t.a(e)+k(h.paddingTop).a(e)),Ib:i(g.r.a(e)+k(h.paddingRight).a(e)),tb:i(g.b.a(e)+k(h.paddingBottom).a(e)),Db:i(g.l.a(e)+k(h.paddingLeft).a(e))},\n2);a=a.style;a.width=d;a.height=c},Cd:function(){this.e.runtimeStyle.filter=\"alpha(opacity=0)\"},m:function(){f.u.m.call(this);this.e.runtimeStyle.filter=\"\"}});f.Oc=f.u.R({ib:f.aa,Mb:f.aa,Nb:f.aa,Lb:f.aa,Ld:/^,+|,+$/g,Fd:/,+/g,gb:function(a,b){(this.pb||(this.pb=[]))[a]=b||void 0},ab:function(){var a=this.pb,b;if(a&&(b=a.join(\",\").replace(this.Ld,\"\").replace(this.Fd,\",\"))!==this.Wc)this.Wc=this.e.runtimeStyle.background=b},m:function(){this.e.runtimeStyle.background=\"\";delete this.pb}});f.Mc=f.u.R({ua:1,\nQ:function(){return this.g.C.H()},i:function(){var a=this.g;return a.C.i()||a.q.i()},V:function(){var a=this.g.C.j(),b,c,d=0,e,g;if(a){b=[];if(c=a.M)for(;e=c[d++];)if(e.P===\"linear-gradient\"){g=this.vd(e.Wa);g=(e.Xa||f.Ka.Kc).a(this.e,g.h,g.f,g.h,g.f);b.push(\"url(data:image/svg+xml,\"+escape(this.xd(e,g.h,g.f))+\") \"+this.dd(e.$)+\" / \"+g.h+\"px \"+g.f+\"px \"+(e.bc||\"\")+\" \"+(e.Wa||\"\")+\" \"+(e.ub||\"\"))}else b.push(e.Hb);a.color&&b.push(a.color.Y);this.parent.gb(this.ua,b.join(\",\"))}},dd:function(a){return a?\na.X.map(function(b){return b.d}).join(\" \"):\"0 0\"},vd:function(a){var b=this.e,c=this.s.o(),d=c.h;c=c.f;var e;if(a!==\"border-box\")if((e=this.g.w.j())&&(e=e.J)){d-=e.l.a(b)+e.l.a(b);c-=e.t.a(b)+e.b.a(b)}if(a===\"content-box\"){a=f.n;e=b.currentStyle;d-=a(e.paddingLeft).a(b)+a(e.paddingRight).a(b);c-=a(e.paddingTop).a(b)+a(e.paddingBottom).a(b)}return{h:d,f:c}},xd:function(a,b,c){var d=this.e,e=a.ca,g=e.length,j=f.Na.gc(d,b,c,a);a=j.xc;var i=j.yc,h=j.td,k=j.ud;j=j.rc;var n,m,p,r,t;n=[];for(m=0;m<g;m++)n.push(e[m].db?\ne[m].db.a(d,j):m===0?0:m===g-1?j:null);for(m=1;m<g;m++)if(n[m]===null){r=n[m-1];p=m;do t=n[++p];while(t===null);n[m]=r+(t-r)/(p-m+1)}b=['<svg width=\"'+b+'\" height=\"'+c+'\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"g\" gradientUnits=\"userSpaceOnUse\" x1=\"'+a/b*100+'%\" y1=\"'+i/c*100+'%\" x2=\"'+h/b*100+'%\" y2=\"'+k/c*100+'%\">'];for(m=0;m<g;m++)b.push('<stop offset=\"'+n[m]/j+'\" stop-color=\"'+e[m].color.U(d)+'\" stop-opacity=\"'+e[m].color.fa()+'\"/>');b.push('</linearGradient></defs><rect width=\"100%\" height=\"100%\" fill=\"url(#g)\"/></svg>');\nreturn b.join(\"\")},m:function(){this.parent.gb(this.ua)}});f.Nc=f.u.R({T:\"repeat\",Sc:\"stretch\",Qc:\"round\",ua:0,Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){var a=this,b=a.g.q.j(),c=a.g.w.j(),d=a.s.o(),e=b.repeat,g=e.f,j=e.Ob,i=a.e,h=0;f.p.Rb(b.src,function(k){function n(Q,R,U,V,W,Y,X,S,w,A){K.push('<pattern patternUnits=\"userSpaceOnUse\" id=\"pattern'+G+'\" x=\"'+(g===l?Q+U/2-w/2:Q)+'\" y=\"'+(j===l?R+V/2-A/2:R)+'\" width=\"'+w+'\" height=\"'+A+'\"><svg width=\"'+w+'\" height=\"'+\nA+'\" viewBox=\"'+W+\" \"+Y+\" \"+X+\" \"+S+'\" preserveAspectRatio=\"none\"><image xlink:href=\"'+v+'\" x=\"0\" y=\"0\" width=\"'+r+'\" height=\"'+t+'\" /></svg></pattern>');J.push('<rect x=\"'+Q+'\" y=\"'+R+'\" width=\"'+U+'\" height=\"'+V+'\" fill=\"url(#pattern'+G+')\" />');G++}var m=d.h,p=d.f,r=k.h,t=k.f,v=a.Dd(b.src,r,t),l=a.T,q=a.Sc;k=a.Qc;var s=Math.ceil,o=f.n(\"0\"),u=b.J||(c?c.J:{t:o,r:o,b:o,l:o});o=u.t.a(i);var x=u.r.a(i),y=u.b.a(i);u=u.l.a(i);var z=b.slice,B=z.t.a(i),E=z.r.a(i),D=z.b.a(i);z=z.l.a(i);var C=m-u-x,F=p-o-\ny,O=r-z-E,H=t-B-D,M=g===q?C:O*o/B,P=j===q?F:H*x/E,I=g===q?C:O*y/D;q=j===q?F:H*u/z;var K=[],J=[],G=0;if(g===k){M-=(M-(C%M||M))/s(C/M);I-=(I-(C%I||I))/s(C/I)}if(j===k){P-=(P-(F%P||P))/s(F/P);q-=(q-(F%q||q))/s(F/q)}k=['<svg width=\"'+m+'\" height=\"'+p+'\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">'];n(0,0,u,o,0,0,z,B,u,o);n(u,0,C,o,z,0,O,B,M,o);n(m-x,0,x,o,r-E,0,E,B,x,o);n(0,o,u,F,0,B,z,H,u,q);if(b.fill)n(u,o,C,F,z,B,O,H,M||I||O,q||P||H);n(m-x,o,x,F,r-E,B,E,H,x,P);n(0,\np-y,u,y,0,t-D,z,D,u,y);n(u,p-y,C,y,z,t-D,O,D,I,y);n(m-x,p-y,x,y,r-E,t-D,E,D,x,y);k.push(\"<defs>\"+K.join(\"\\n\")+\"</defs>\"+J.join(\"\\n\")+\"</svg>\");a.parent.gb(a.ua,\"url(data:image/svg+xml,\"+escape(k.join(\"\"))+\") no-repeat border-box border-box\");h&&a.parent.ab()},a);h=1},Dd:function(){var a={};return function(b,c,d){var e=a[b],g;if(!e){e=new Image;g=doc.createElement(\"canvas\");e.src=b;g.width=c;g.height=d;g.getContext(\"2d\").drawImage(e,0,0);e=a[b]=g.toDataURL()}return e}}(),Ea:f.Tb.prototype.Ea,m:function(){var a=\nthis.e.runtimeStyle;this.parent.gb(this.ua);a.borderColor=a.borderStyle=a.borderWidth=\"\"}});f.kb=function(){function a(l,q){l.className+=\" \"+q}function b(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;)a(l,q[s])},0)}function c(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;){var o=q[s];o=t[o]||(t[o]=new RegExp(\"\\\\b\"+o+\"\\\\b\",\"g\"));l.className=l.className.replace(o,\"\")}},0)}function d(l){function q(){if(!U){var w,A,L=f.ja,T=l.currentStyle,\nN=T.getAttribute(g)===\"true\",da=T.getAttribute(i)!==\"false\",ea=T.getAttribute(h)!==\"false\";S=T.getAttribute(j);S=L>7?S!==\"false\":S===\"true\";if(!R){R=1;l.runtimeStyle.zoom=1;T=l;for(var fa=1;T=T.previousSibling;)if(T.nodeType===1){fa=0;break}fa&&a(l,p)}J.cb();if(N&&(A=J.o())&&(w=doc.documentElement||doc.body)&&(A.y>w.clientHeight||A.x>w.clientWidth||A.y+A.f<0||A.x+A.h<0)){if(!Y){Y=1;f.mb.ba(q)}}else{U=1;Y=R=0;f.mb.Ha(q);if(L===9){G={C:new f.Sb(l),q:new f.Ub(l),w:new f.Vb(l)};Q=[G.C,G.q];K=new f.Oc(l,\nJ,G);w=[new f.Mc(l,J,G,K),new f.Nc(l,J,G,K)]}else{G={C:new f.Sb(l),w:new f.Vb(l),q:new f.Ub(l),G:new f.jb(l),ga:new f.Ic(l),Pb:new f.Uc(l)};Q=[G.C,G.w,G.q,G.G,G.ga,G.Pb];K=new f.Rc(l,J,G);w=[new f.Hc(l,J,G,K),new f.Fc(l,J,G,K),new f.Gc(l,J,G,K),new f.Tb(l,J,G,K)];l.tagName===\"IMG\"&&w.push(new f.Pc(l,J,G,K));K.ed=w}I=[K].concat(w);if(w=l.currentStyle.getAttribute(f.F+\"watch-ancestors\")){w=parseInt(w,10);A=0;for(N=l.parentNode;N&&(w===\"NaN\"||A++<w);){H(N,\"onpropertychange\",C);H(N,\"onmouseenter\",x);\nH(N,\"onmouseleave\",y);H(N,\"onmousedown\",z);if(N.tagName in f.fc){H(N,\"onfocus\",E);H(N,\"onblur\",D)}N=N.parentNode}}if(S){f.Oa.ba(o);f.Oa.Rd()}o(1)}if(!V){V=1;L<9&&H(l,\"onmove\",s);H(l,\"onresize\",s);H(l,\"onpropertychange\",u);ea&&H(l,\"onmouseenter\",x);if(ea||da)H(l,\"onmouseleave\",y);da&&H(l,\"onmousedown\",z);if(l.tagName in f.fc){H(l,\"onfocus\",E);H(l,\"onblur\",D)}f.Qa.ba(s);f.L.ba(M)}J.hb()}}function s(){J&&J.Ad()&&o()}function o(w){if(!X)if(U){var A,L=I.length;F();for(A=0;A<L;A++)I[A].Ea();if(w||J.Od())for(A=\n0;A<L;A++)I[A].ib();if(w||J.Td())for(A=0;A<L;A++)I[A].Mb();K.ab();O()}else R||q()}function u(){var w,A=I.length,L;w=event;if(!X&&!(w&&w.propertyName in r))if(U){F();for(w=0;w<A;w++)I[w].Ea();for(w=0;w<A;w++){L=I[w];L.Cb||L.ib();L.Q()&&L.Lb()}K.ab();O()}else R||q()}function x(){b(l,k)}function y(){c(l,k,n)}function z(){b(l,n);f.lb.ba(B)}function B(){c(l,n);f.lb.Ha(B)}function E(){b(l,m)}function D(){c(l,m)}function C(){var w=event.propertyName;if(w===\"className\"||w===\"id\")u()}function F(){J.cb();for(var w=\nQ.length;w--;)Q[w].cb()}function O(){for(var w=Q.length;w--;)Q[w].hb();J.hb()}function H(w,A,L){w.attachEvent(A,L);W.push([w,A,L])}function M(){if(V){for(var w=W.length,A;w--;){A=W[w];A[0].detachEvent(A[1],A[2])}f.L.Ha(M);V=0;W=[]}}function P(){if(!X){var w,A;M();X=1;if(I){w=0;for(A=I.length;w<A;w++){I[w].ec=1;I[w].m()}}S&&f.Oa.Ha(o);f.Qa.Ha(o);I=J=G=Q=l=null}}var I,K,J=new ha(l),G,Q,R,U,V,W=[],Y,X,S;this.Ed=q;this.update=o;this.m=P;this.qd=l}var e={},g=f.F+\"lazy-init\",j=f.F+\"poll\",i=f.F+\"track-active\",\nh=f.F+\"track-hover\",k=f.La+\"hover\",n=f.La+\"active\",m=f.La+\"focus\",p=f.La+\"first-child\",r={background:1,bgColor:1,display:1},t={},v=[];d.yd=function(l){var q=f.p.Ba(l);return e[q]||(e[q]=new d(l))};d.m=function(l){l=f.p.Ba(l);var q=e[l];if(q){q.m();delete e[l]}};d.md=function(){var l=[],q;if(e){for(var s in e)if(e.hasOwnProperty(s)){q=e[s];l.push(q.qd);q.m()}e={}}return l};return d}();f.supportsVML=f.zc;f.attach=function(a){f.ja<10&&f.zc&&f.kb.yd(a).Ed()};f.detach=function(a){f.kb.m(a)}};\nvar $=element;function init(){if(doc.media!==\"print\"){var a=window.PIE;a&&a.attach($)}}function cleanup(){if(doc.media!==\"print\"){var a=window.PIE;if(a){a.detach($);$=0}}}$.readyState===\"complete\"&&init();\n</script>\n</PUBLIC:COMPONENT>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/oasis/oasis.hyperesources/oasis_hype_generated_script.js",
    "content": "//\tHYPE.documents[\"Oasis\"]\n\n(function(){(function k(){function l(a,b,d){var c=!1;null==window[a]&&(null==window[b]?(window[b]=[],window[b].push(k),a=document.getElementsByTagName(\"head\")[0],b=document.createElement(\"script\"),c=h,false==!0&&(c=\"\"),b.type=\"text/javascript\",b.src=c+\"/\"+d,a.appendChild(b)):window[b].push(k),c=!0);return c}var h=\"Oasis.hyperesources\",c=\"Oasis\",e=\"oasis_hype_container\";if(false==!1)try{for(var f=document.getElementsByTagName(\"script\"),\na=0;a<f.length;a++){var b=f[a].src;if(null!=b&&-1!=b.indexOf(\"oasis_hype_generated_script.js\")){h=b.substr(0,b.lastIndexOf(\"/\"));break}}}catch(n){}if(false==!1&&(a=navigator.userAgent.match(/MSIE (\\d+\\.\\d+)/),a=parseFloat(a&&a[1])||null,a=l(\"HYPE_526\",\"HYPE_dtl_526\",!0==(null!=a&&10>a||false==!0)?\"HYPE-526.full.min.js\":\"HYPE-526.thin.min.js\"),false==!0&&(a=a||l(\"HYPE_w_526\",\"HYPE_wdtl_526\",\"HYPE-526.waypoints.min.js\")),a))return;\nf=window.HYPE.documents;if(null!=f[c]){b=1;a=c;do c=\"\"+a+\"-\"+b++;while(null!=f[c]);for(var d=document.getElementsByTagName(\"div\"),b=!1,a=0;a<d.length;a++)if(d[a].id==e&&null==d[a].getAttribute(\"HYP_dn\")){var b=1,g=e;do e=\"\"+g+\"-\"+b++;while(null!=document.getElementById(e));d[a].id=e;b=!0;break}if(!1==b)return}b=[];b=[];d={};g={};for(a=0;a<b.length;a++)try{g[b[a].identifier]=b[a].name,d[b[a].name]=eval(\"(function(){return \"+b[a].source+\"})();\")}catch(m){window.console&&window.console.log(m),\nd[b[a].name]=function(){}}a=new HYPE_526(c,e,{\"18\":{p:1,n:\"topright-tree-topright.svg\",g:\"469\",t:\"image/svg+xml\"},\"10\":{p:1,n:\"top-tree-topleft.svg\",g:\"451\",t:\"image/svg+xml\"},\"19\":{p:1,n:\"topright-tree-topleft.svg\",g:\"471\",t:\"image/svg+xml\"},\"11\":{p:1,n:\"tree-bottom-trunk.svg\",g:\"454\",t:\"image/svg+xml\"},\"0\":{p:1,n:\"desertfill.svg\",g:\"433\",t:\"image/svg+xml\"},\"12\":{p:1,n:\"bottom-tree-left.svg\",g:\"457\",t:\"image/svg+xml\"},\"1\":{p:1,n:\"check-lake.png\",g:\"435\",o:true,t:\"@1x\"},\"20\":{p:1,n:\"topright-tree-left.svg\",g:\"473\",t:\"image/svg+xml\"},\"2\":{p:1,n:\"check-lake_2x.png\",g:\"435\",o:true,t:\"@2x\"},\"13\":{p:1,n:\"bottom-tree-left-topleft.svg\",g:\"459\",t:\"image/svg+xml\"},\"3\":{p:1,n:\"desertfreckles.svg\",g:\"437\",t:\"image/svg+xml\"},\"21\":{p:1,n:\"topright-tree-right.svg\",g:\"475\",t:\"image/svg+xml\"},\"14\":{p:1,n:\"bottom-tree-left-topright.svg\",g:\"461\",t:\"image/svg+xml\"},\"4\":{p:1,n:\"sanddunerdige.svg\",g:\"439\",t:\"image/svg+xml\"},\"5\":{p:1,n:\"treetrunk-top.svg\",g:\"441\",t:\"image/svg+xml\"},\"15\":{p:1,n:\"bottom-tree-left-right.svg\",g:\"463\",t:\"image/svg+xml\"},\"6\":{p:1,n:\"nuts-cluster.svg\",g:\"443\",t:\"image/svg+xml\"},\"16\":{p:1,n:\"top-right-trunk.svg\",g:\"465\",t:\"image/svg+xml\"},\"7\":{p:1,n:\"top-tree--topright.svg\",g:\"445\",t:\"image/svg+xml\"},\"8\":{p:1,n:\"top-tree-right.svg\",g:\"447\",t:\"image/svg+xml\"},\"17\":{p:1,n:\"smallnuts-cluster.svg\",g:\"467\",t:\"image/svg+xml\"},\"9\":{p:1,n:\"top-tree-left.svg\",g:\"449\",t:\"image/svg+xml\"}},h,[],d,[{n:\"Oasis\",o:\"384\",X:[0]}],[{o:\"432\",p:\"600px\",x:0,cA:false,Z:500,Y:600,c:\"#FFFFFF\",L:[],bY:1,d:600,U:{},T:{kTimelineDefaultIdentifier:{i:\"kTimelineDefaultIdentifier\",n:\"Main Timeline\",z:1.2,b:[],a:[{f:\"c\",y:0,z:0.1,i:\"e\",e:1,s:0,o:\"934\"},{f:\"c\",y:0.02,z:0.06,i:\"e\",e:1,s:0,o:\"933\"},{f:\"c\",y:0.03,z:0.08,i:\"e\",e:1,s:0,o:\"943\"},{y:0.08,i:\"e\",s:1,z:0,o:\"933\",f:\"f\"},{f:\"g\",y:0.09,z:0.11,i:\"cR\",e:0.99999999999999989,s:0,o:\"940\"},{f:\"g\",y:0.09,z:0.11,i:\"cQ\",e:0.99999999999999989,s:0,o:\"940\"},{f:\"f\",y:0.1,z:0.19,i:\"a\",e:51,s:29,o:\"939\"},{f:\"f\",y:0.1,z:0.19,i:\"b\",e:19,s:25,o:\"939\"},{f:\"f\",y:0.1,z:0.19,i:\"f\",e:0,s:-60,o:\"939\"},{f:\"f\",y:0.1,z:0.19,i:\"cQ\",e:1,s:0.0000000000,o:\"939\"},{f:\"f\",y:0.1,z:0.19,i:\"cR\",e:1,s:0.0000000000,o:\"939\"},{f:\"f\",y:0.1,z:0.2,i:\"a\",e:0,s:23,o:\"942\"},{f:\"f\",y:0.1,z:0.2,i:\"b\",e:17,s:23,o:\"942\"},{f:\"f\",y:0.1,z:0.2,i:\"f\",e:0,s:27,o:\"942\"},{f:\"f\",y:0.1,z:0.2,i:\"cQ\",e:1,s:0.0000000000,o:\"942\"},{f:\"f\",y:0.1,z:0.2,i:\"cR\",e:1,s:0.0000000000,o:\"942\"},{f:\"f\",y:0.1,z:0.17,i:\"a\",e:23,s:33,o:\"941\"},{f:\"f\",y:0.1,z:0.17,i:\"b\",e:0,s:15,o:\"941\"},{f:\"f\",y:0.1,z:0.17,i:\"f\",e:0,s:27,o:\"941\"},{f:\"f\",y:0.1,z:0.17,i:\"cQ\",e:1,s:0.0000000000,o:\"941\"},{f:\"f\",y:0.1,z:0.17,i:\"cR\",e:1,s:0.0000000000,o:\"941\"},{f:\"f\",y:0.1,z:0.21,i:\"a\",e:50,s:25,o:\"937\"},{f:\"f\",y:0.1,z:0.21,i:\"b\",e:0,s:15,o:\"937\"},{f:\"f\",y:0.1,z:0.21,i:\"f\",e:0,s:-40,o:\"937\"},{f:\"f\",y:0.1,z:0.21,i:\"cQ\",e:1,s:0.0000000000,o:\"937\"},{f:\"f\",y:0.1,z:0.21,i:\"cR\",e:1,s:0.0000000000,o:\"937\"},{f:\"g\",y:0.1,z:0.13,i:\"b\",e:30,s:35,o:\"938\"},{y:0.1,i:\"e\",s:1,z:0,o:\"934\",f:\"c\"},{f:\"g\",y:0.1,z:0.13,i:\"cR\",e:1,s:0.0000000000,o:\"938\"},{f:\"c\",y:0.11,z:0.17,i:\"e\",e:1,s:0,o:\"944\"},{y:0.11,i:\"e\",s:1,z:0,o:\"943\",f:\"c\"},{f:\"c\",y:0.13,z:0.06,i:\"e\",e:1,s:0,o:\"932\"},{y:0.19,i:\"e\",s:1,z:0,o:\"932\",f:\"c\"},{y:0.2,i:\"cQ\",s:0.99999999999999989,z:0,o:\"940\",f:\"c\"},{y:0.2,i:\"cR\",s:0.99999999999999989,z:0,o:\"940\",f:\"c\"},{f:\"c\",y:0.21,z:0.02,i:\"a\",e:39,s:39,o:\"938\"},{f:\"g\",y:0.21,z:0.02,i:\"cQ\",e:1,s:1,o:\"938\"},{f:\"f\",y:0.22,z:0.11,i:\"d\",e:28,s:4,o:\"946\"},{f:\"f\",y:0.22,z:0.11,i:\"c\",e:28,s:8,o:\"946\"},{f:\"f\",y:0.22,z:0.11,i:\"f\",e:0,s:-60,o:\"946\"},{f:\"f\",y:0.22,z:0.11,i:\"b\",e:-1,s:21,o:\"946\"},{f:\"g\",y:0.22,z:0.17,i:\"a\",e:43,s:46,o:\"948\"},{f:\"g\",y:0.22,z:0.17,i:\"b\",e:30,s:49,o:\"948\"},{f:\"g\",y:0.22,z:0.17,i:\"cQ\",e:1,s:0.0000000000,o:\"948\"},{f:\"g\",y:0.22,z:0.17,i:\"cR\",e:1,s:0.0000000000,o:\"948\"},{f:\"c\",y:0.22,z:0.02,i:\"e\",e:1,s:0,o:\"946\"},{f:\"f\",y:0.23,z:0.13,i:\"f\",e:0,s:45,o:\"950\"},{f:\"f\",y:0.23,z:0.13,i:\"a\",e:-2,s:19,o:\"950\"},{f:\"f\",y:0.23,z:0.13,i:\"b\",e:0,s:13,o:\"950\"},{y:0.23,i:\"a\",s:39,z:0,o:\"938\",f:\"g\"},{f:\"g\",y:0.23,z:0.02,i:\"cQ\",e:1,s:0.0048388955345148033,o:\"949\"},{f:\"g\",y:0.23,z:0.02,i:\"cR\",e:1,s:0.0048388955345148033,o:\"949\"},{y:0.23,i:\"b\",s:30,z:0,o:\"938\",f:\"c\"},{f:\"g\",y:0.23,z:0.13,i:\"cR\",e:1,s:0.0000000000,o:\"951\"},{f:\"g\",y:0.23,z:0.13,i:\"cQ\",e:1,s:0.0000000000,o:\"951\"},{f:\"f\",y:0.23,z:0.13,i:\"cQ\",e:1,s:0.0000000000,o:\"950\"},{f:\"f\",y:0.23,z:0.13,i:\"cR\",e:1,s:0.0000000000,o:\"950\"},{y:0.23,i:\"cQ\",s:1,z:0,o:\"938\",f:\"g\"},{y:0.23,i:\"cR\",s:1,z:0,o:\"938\",f:\"c\"},{f:\"g\",y:0.24,z:0.15,i:\"f\",e:0,s:38,o:\"947\"},{f:\"g\",y:0.24,z:0.13,i:\"a\",e:3,s:18,o:\"947\"},{f:\"g\",y:0.24,z:0.13,i:\"b\",e:20,s:26,o:\"947\"},{y:0.24,i:\"e\",s:1,z:0,o:\"946\",f:\"c\"},{f:\"g\",y:0.24,z:0.13,i:\"cQ\",e:1,s:0.0000000000,o:\"947\"},{f:\"g\",y:0.24,z:0.13,i:\"cR\",e:1,s:0.0000000000,o:\"947\"},{f:\"g\",y:0.25,z:0.12,i:\"d\",e:12,s:4,o:\"949\"},{f:\"g\",y:0.25,z:0.12,i:\"c\",e:40,s:12,o:\"949\"},{f:\"g\",y:0.25,z:0.12,i:\"f\",e:0,s:-50,o:\"949\"},{f:\"g\",y:0.25,z:0.12,i:\"b\",e:20,s:28,o:\"949\"},{y:0.25,i:\"cQ\",s:1,z:0,o:\"949\",f:\"c\"},{y:0.25,i:\"cR\",s:1,z:0,o:\"949\",f:\"c\"},{y:0.27,i:\"f\",s:0,z:0,o:\"941\",f:\"c\"},{y:0.27,i:\"a\",s:23,z:0,o:\"941\",f:\"c\"},{y:0.27,i:\"b\",s:0,z:0,o:\"941\",f:\"c\"},{y:0.27,i:\"cQ\",s:1,z:0,o:\"941\",f:\"c\"},{y:0.27,i:\"cR\",s:1,z:0,o:\"941\",f:\"c\"},{y:0.28,i:\"e\",s:1,z:0,o:\"944\",f:\"c\"},{y:0.29,i:\"f\",s:0,z:0,o:\"939\",f:\"c\"},{y:0.29,i:\"a\",s:51,z:0,o:\"939\",f:\"c\"},{y:0.29,i:\"b\",s:19,z:0,o:\"939\",f:\"c\"},{y:0.29,i:\"cQ\",s:1,z:0,o:\"939\",f:\"c\"},{y:0.29,i:\"cR\",s:1,z:0,o:\"939\",f:\"c\"},{y:1,i:\"f\",s:0,z:0,o:\"942\",f:\"c\"},{y:1,i:\"a\",s:0,z:0,o:\"942\",f:\"c\"},{y:1,i:\"b\",s:17,z:0,o:\"942\",f:\"c\"},{y:1,i:\"cQ\",s:1,z:0,o:\"942\",f:\"c\"},{y:1,i:\"cR\",s:1,z:0,o:\"942\",f:\"c\"},{y:1.01,i:\"f\",s:0,z:0,o:\"937\",f:\"c\"},{y:1.01,i:\"a\",s:50,z:0,o:\"937\",f:\"c\"},{y:1.01,i:\"b\",s:0,z:0,o:\"937\",f:\"c\"},{y:1.01,i:\"cQ\",s:1,z:0,o:\"937\",f:\"c\"},{y:1.01,i:\"cR\",s:1,z:0,o:\"937\",f:\"c\"},{f:\"c\",y:1.02,z:0.03,i:\"e\",e:1,s:0,o:\"935\"},{f:\"g\",y:1.02,z:0.12,i:\"a\",e:10,s:7,o:\"958\"},{f:\"g\",y:1.02,z:0.12,i:\"b\",e:28,s:41,o:\"958\"},{f:\"g\",y:1.02,z:0.12,i:\"cR\",e:0.99999999999999989,s:0,o:\"958\"},{f:\"g\",y:1.02,z:0.12,i:\"cQ\",e:0.99999999999999989,s:0,o:\"958\"},{y:1.03,i:\"d\",s:28,z:0,o:\"946\",f:\"c\"},{y:1.03,i:\"c\",s:28,z:0,o:\"946\",f:\"c\"},{f:\"f\",y:1.03,z:0.16,i:\"f\",e:0,s:29,o:\"957\"},{f:\"f\",y:1.03,z:0.14,i:\"f\",e:0,s:-27,o:\"954\"},{f:\"f\",y:1.03,z:0.16,i:\"a\",e:0,s:12,o:\"957\"},{y:1.03,i:\"f\",s:0,z:0,o:\"946\",f:\"c\"},{f:\"f\",y:1.03,z:0.14,i:\"a\",e:36,s:23,o:\"954\"},{f:\"f\",y:1.03,z:0.16,i:\"b\",e:20,s:24,o:\"957\"},{y:1.03,i:\"a\",s:48,z:0,o:\"946\",f:\"f\"},{f:\"f\",y:1.03,z:0.14,i:\"b\",e:21,s:25,o:\"954\"},{y:1.03,i:\"b\",s:-1,z:0,o:\"946\",f:\"c\"},{f:\"g\",y:1.03,z:0.12,i:\"cQ\",e:1,s:0.0000000000,o:\"955\"},{f:\"g\",y:1.03,z:0.12,i:\"cR\",e:1,s:0.0000000000,o:\"955\"},{f:\"f\",y:1.03,z:0.16,i:\"cR\",e:1,s:0.0000000000,o:\"957\"},{f:\"f\",y:1.03,z:0.16,i:\"cQ\",e:1,s:0.0000000000,o:\"957\"},{f:\"f\",y:1.03,z:0.14,i:\"cR\",e:1,s:0.0000000000,o:\"954\"},{f:\"f\",y:1.03,z:0.14,i:\"cQ\",e:1,s:0.0000000000,o:\"954\"},{f:\"f\",y:1.04,z:0.15,i:\"f\",e:0,s:-33,o:\"953\"},{f:\"f\",y:1.04,z:0.16,i:\"f\",e:0,s:40,o:\"956\"},{f:\"f\",y:1.04,z:0.15,i:\"a\",e:34,s:21,o:\"953\"},{f:\"f\",y:1.04,z:0.16,i:\"a\",e:6,s:16,o:\"956\"},{f:\"f\",y:1.04,z:0.15,i:\"b\",e:4,s:13,o:\"953\"},{f:\"f\",y:1.04,z:0.16,i:\"b\",e:2,s:10,o:\"956\"},{f:\"f\",y:1.04,z:0.15,i:\"cR\",e:1,s:0.0000000000,o:\"953\"},{f:\"f\",y:1.04,z:0.15,i:\"cQ\",e:1,s:0.0000000000,o:\"953\"},{f:\"f\",y:1.04,z:0.16,i:\"cR\",e:1,s:0,o:\"956\"},{f:\"f\",y:1.04,z:0.16,i:\"cQ\",e:0.99999999999999989,s:0,o:\"956\"},{y:1.05,i:\"e\",s:1,z:0,o:\"935\",f:\"c\"},{y:1.06,i:\"f\",s:0,z:0,o:\"950\",f:\"c\"},{y:1.06,i:\"a\",s:-2,z:0,o:\"950\",f:\"c\"},{y:1.06,i:\"b\",s:0,z:0,o:\"950\",f:\"c\"},{y:1.06,i:\"cR\",s:1,z:0,o:\"951\",f:\"c\"},{y:1.06,i:\"cQ\",s:1,z:0,o:\"951\",f:\"c\"},{y:1.06,i:\"cQ\",s:1,z:0,o:\"950\",f:\"c\"},{y:1.06,i:\"cR\",s:1,z:0,o:\"950\",f:\"c\"},{y:1.07,i:\"d\",s:12,z:0,o:\"949\",f:\"c\"},{y:1.07,i:\"c\",s:40,z:0,o:\"949\",f:\"c\"},{y:1.07,i:\"f\",s:0,z:0,o:\"949\",f:\"c\"},{y:1.07,i:\"a\",s:3,z:0,o:\"947\",f:\"c\"},{y:1.07,i:\"b\",s:20,z:0,o:\"949\",f:\"c\"},{y:1.07,i:\"b\",s:20,z:0,o:\"947\",f:\"c\"},{y:1.07,i:\"cQ\",s:1,z:0,o:\"947\",f:\"c\"},{y:1.07,i:\"cR\",s:1,z:0,o:\"947\",f:\"c\"},{y:1.09,i:\"f\",s:0,z:0,o:\"947\",f:\"c\"},{y:1.09,i:\"a\",s:43,z:0,o:\"948\",f:\"c\"},{y:1.09,i:\"b\",s:30,z:0,o:\"948\",f:\"c\"},{y:1.09,i:\"cQ\",s:1,z:0,o:\"948\",f:\"c\"},{y:1.09,i:\"cR\",s:1,z:0,o:\"948\",f:\"c\"},{y:1.14,i:\"a\",s:10,z:0,o:\"958\",f:\"c\"},{y:1.14,i:\"b\",s:28,z:0,o:\"958\",f:\"c\"},{y:1.14,i:\"cR\",s:0.99999999999999989,z:0,o:\"958\",f:\"c\"},{y:1.14,i:\"cQ\",s:0.99999999999999989,z:0,o:\"958\",f:\"c\"},{y:1.15,i:\"cQ\",s:1,z:0,o:\"955\",f:\"c\"},{y:1.15,i:\"cR\",s:1,z:0,o:\"955\",f:\"c\"},{y:1.17,i:\"f\",s:0,z:0,o:\"954\",f:\"c\"},{y:1.17,i:\"a\",s:36,z:0,o:\"954\",f:\"c\"},{y:1.17,i:\"b\",s:21,z:0,o:\"954\",f:\"c\"},{y:1.17,i:\"cR\",s:1,z:0,o:\"954\",f:\"c\"},{y:1.17,i:\"cQ\",s:1,z:0,o:\"954\",f:\"c\"},{y:1.19,i:\"f\",s:0,z:0,o:\"953\",f:\"c\"},{y:1.19,i:\"f\",s:0,z:0,o:\"957\",f:\"c\"},{y:1.19,i:\"a\",s:34,z:0,o:\"953\",f:\"c\"},{y:1.19,i:\"a\",s:0,z:0,o:\"957\",f:\"c\"},{y:1.19,i:\"b\",s:4,z:0,o:\"953\",f:\"c\"},{y:1.19,i:\"b\",s:20,z:0,o:\"957\",f:\"c\"},{y:1.19,i:\"cR\",s:1,z:0,o:\"953\",f:\"c\"},{y:1.19,i:\"cQ\",s:1,z:0,o:\"953\",f:\"c\"},{y:1.19,i:\"cR\",s:1,z:0,o:\"957\",f:\"c\"},{y:1.19,i:\"cQ\",s:1,z:0,o:\"957\",f:\"c\"},{y:1.2,i:\"f\",s:0,z:0,o:\"956\",f:\"c\"},{y:1.2,i:\"a\",s:6,z:0,o:\"956\",f:\"c\"},{y:1.2,i:\"b\",s:2,z:0,o:\"956\",f:\"c\"},{y:1.2,i:\"cR\",s:1,z:0,o:\"956\",f:\"c\"},{y:1.2,i:\"cQ\",s:0.99999999999999989,z:0,o:\"956\",f:\"c\"}],f:30}},bZ:180,O:[\"939\",\"937\",\"941\",\"942\",\"940\",\"938\",\"936\",\"950\",\"947\",\"949\",\"946\",\"951\",\"948\",\"945\",\"944\",\"954\",\"957\",\"956\",\"953\",\"955\",\"958\",\"952\",\"935\",\"932\",\"933\",\"943\",\"934\",\"931\"],v:{\"950\":{h:\"451\",p:\"no-repeat\",x:\"visible\",a:19,q:\"100% 100%\",b:13,j:\"absolute\",bF:\"945\",z:6,k:\"div\",c:48,d:32,r:\"inline\",cQ:0.0000000000,f:45,cR:0.0000000000},\"942\":{h:\"457\",p:\"no-repeat\",x:\"visible\",a:23,q:\"100% 100%\",b:23,j:\"absolute\",bF:\"936\",z:3,k:\"div\",c:48,d:14,r:\"inline\",cQ:0.0000000000,f:27,cR:0.0000000000},\"934\":{h:\"433\",p:\"no-repeat\",x:\"visible\",a:0,q:\"100% 100%\",b:0,j:\"absolute\",bF:\"931\",z:1,k:\"div\",c:484,d:435,r:\"inline\",e:0},\"955\":{h:\"467\",p:\"no-repeat\",x:\"visible\",a:28,q:\"100% 100%\",b:24,j:\"absolute\",bF:\"952\",z:2,k:\"div\",c:12,d:12,r:\"inline\",cQ:0.0000000000,cR:0.0000000000},\"947\":{h:\"449\",p:\"no-repeat\",x:\"visible\",a:18,q:\"100% 100%\",b:26,j:\"absolute\",bF:\"945\",z:5,k:\"div\",c:44,d:13,r:\"inline\",cQ:0.0000000000,f:38,cR:0.0000000000},\"939\":{h:\"463\",p:\"no-repeat\",x:\"visible\",a:29,q:\"100% 100%\",b:25,j:\"absolute\",bF:\"936\",z:6,k:\"div\",c:42,d:13,r:\"inline\",cQ:0.0000000000,f:-60,cR:0.0000000000},\"951\":{h:\"443\",p:\"no-repeat\",x:\"visible\",a:40,q:\"100% 100%\",b:24,j:\"absolute\",bF:\"945\",z:2,k:\"div\",c:17,d:16,r:\"inline\",cQ:0.0000000000,cR:0.0000000000},\"943\":{h:\"435\",p:\"no-repeat\",x:\"visible\",a:123,q:\"100% 100%\",b:134,j:\"absolute\",bF:\"931\",z:2,k:\"div\",c:264,d:183,r:\"inline\",e:0},\"935\":{c:39,d:12,I:\"None\",e:0,J:\"None\",K:\"None\",g:\"rgba(234, 222, 190, 0.630)\",L:\"None\",M:0,N:0,aI:\"50%\",A:\"#D8DDE4\",x:\"visible\",O:0,j:\"absolute\",aJ:\"50%\",k:\"div\",C:\"#D8DDE4\",z:5,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:\"50%\",bF:\"931\",P:0,a:370,aL:\"50%\",b:60},\"956\":{h:\"471\",p:\"no-repeat\",x:\"visible\",a:16,q:\"100% 100%\",b:10,j:\"absolute\",bF:\"952\",z:4,k:\"div\",c:28,d:28,r:\"inline\",cQ:0,f:40,cR:0},\"948\":{h:\"441\",p:\"no-repeat\",x:\"visible\",a:46,q:\"100% 100%\",b:49,j:\"absolute\",bF:\"945\",z:1,k:\"div\",c:40,d:47,r:\"inline\",cQ:0.0000000000,cR:0.0000000000},\"931\":{k:\"div\",x:\"visible\",c:484,d:435,z:1,a:46,j:\"absolute\",b:31},\"952\":{k:\"div\",x:\"visible\",c:68,d:62,z:6,a:365,j:\"absolute\",bF:\"931\",b:5},\"944\":{h:\"439\",p:\"no-repeat\",x:\"visible\",a:102,q:\"100% 100%\",b:154,j:\"absolute\",bF:\"931\",z:7,k:\"div\",c:74,d:17,r:\"inline\",e:0},\"936\":{k:\"div\",x:\"visible\",c:98,d:79.966200000000001,z:9,a:271,j:\"absolute\",bF:\"931\",b:220},\"957\":{h:\"473\",p:\"no-repeat\",x:\"visible\",a:12,q:\"100% 100%\",b:24,j:\"absolute\",bF:\"952\",z:5,k:\"div\",c:34,d:10,r:\"inline\",cQ:0.0000000000,f:29,cR:0.0000000000},\"949\":{h:\"447\",p:\"no-repeat\",x:\"visible\",a:50,q:\"100% 100%\",b:28,j:\"absolute\",bF:\"945\",z:4,k:\"div\",c:12,d:4,r:\"inline\",cQ:0.0048388955345148033,f:-50,cR:0.0048388955345148033},\"940\":{h:\"443\",p:\"no-repeat\",x:\"visible\",a:42,q:\"100% 100%\",b:25,j:\"absolute\",bF:\"936\",z:2,k:\"div\",c:15,d:14,r:\"inline\",cQ:0,cR:0},\"932\":{c:46,d:13,I:\"None\",e:0,J:\"None\",K:\"None\",g:\"rgba(229, 217, 185, 0.500)\",L:\"None\",M:0,N:0,aI:\"50%\",A:\"#D8DDE4\",x:\"visible\",O:0,j:\"absolute\",aJ:\"50%\",k:\"div\",C:\"#D8DDE4\",z:4,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:\"50%\",bF:\"931\",P:0,a:297,aL:\"50%\",b:285},\"953\":{h:\"469\",p:\"no-repeat\",x:\"visible\",a:21,q:\"100% 100%\",b:13,j:\"absolute\",bF:\"952\",z:3,k:\"div\",c:34,d:23,r:\"inline\",cQ:0.0000000000,f:-33,cR:0.0000000000},\"945\":{k:\"div\",x:\"visible\",c:98,d:77,z:8,a:73,j:\"absolute\",bF:\"931\",b:79},\"937\":{h:\"461\",p:\"no-repeat\",x:\"visible\",a:25,q:\"100% 100%\",b:15,j:\"absolute\",bF:\"936\",z:5,k:\"div\",c:48,d:32,r:\"inline\",cQ:0.0000000000,f:-40,cR:0.0000000000},\"958\":{h:\"465\",p:\"no-repeat\",x:\"visible\",a:7,q:\"100% 100%\",b:41,j:\"absolute\",bF:\"952\",z:1,k:\"div\",c:29,d:34,r:\"inline\",cQ:0,cR:0},\"941\":{h:\"459\",p:\"no-repeat\",x:\"visible\",a:33,q:\"100% 100%\",b:15,j:\"absolute\",bF:\"936\",z:4,k:\"div\",c:27,d:27,r:\"inline\",cQ:0.0000000000,f:27,cR:0.0000000000},\"933\":{h:\"437\",p:\"no-repeat\",x:\"visible\",a:56,q:\"100% 100%\",b:131,j:\"absolute\",bF:\"931\",z:3,k:\"div\",c:389,d:194,r:\"inline\",e:0},\"954\":{h:\"475\",p:\"no-repeat\",x:\"visible\",a:23,q:\"100% 100%\",b:25,j:\"absolute\",bF:\"952\",z:6,k:\"div\",c:31,d:10,r:\"inline\",cQ:0.0000000000,f:-27,cR:0.0000000000},\"946\":{h:\"445\",p:\"no-repeat\",x:\"visible\",a:48,q:\"100% 100%\",b:21,j:\"absolute\",bF:\"945\",z:3,k:\"div\",c:8,d:4,r:\"inline\",cQ:1,e:0,f:-60,cR:1},\"938\":{h:\"454\",p:\"no-repeat\",x:\"visible\",a:39,q:\"100% 100%\",b:35,j:\"absolute\",bF:\"936\",z:1,k:\"div\",c:19,d:45,r:\"inline\",cQ:1,cR:0.0000000000}}}],{},g,{f:[[0,0,0.1971,0,0.3391,0.8944,0.3636,1],[0.3636,1,0.3636,1,0.4425,0.75,0.5455,0.75],[0.5455,0.75,0.6519,0.75,0.7273,1,0.7273,1],[0.7273,1,0.7273,1,0.7718,0.9375,0.8182,0.9375],[0.8182,0.9375,0.8646,0.9375,0.9091,1,0.9091,1],[0.9091,1,0.9091,1,0.9294,0.9844,0.9546,0.9844],[0.9546,0.9844,0.9798,0.9844,1,1,1,1]],g:[[0,0,0.0425,0.22,0.089,1.373,0.169,1.373],[0.169,1.373,0.223,1.373,0.2656,0.868,0.356,0.868],[0.356,0.868,0.4085,0.868,0.457,1.047,0.544,1.047],[0.544,1.047,0.5976,1.047,0.637,0.984,0.731,0.984],[0.731,0.984,0.794,0.984,0.829,1.006,0.919,1.006],[0.919,1.006,0.953,1.006,1,1,1,1]]},null,false,false,-1,true,true,false,true);f[c]=a.API;document.getElementById(e).setAttribute(\"HYP_dn\",\nc);a.z_o(this.body)})();})();\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/tron/tron.html",
    "content": "<!DOCTYPE html>\n<html>\n  <head>\n\t<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\n\t<meta http-equiv=\"X-UA-Compatible\" content=\"chrome=1,IE=edge\" />\n\t<title>TRON</title>\n\t<style>\n\t\thtml {\n\t\t\theight:100%;\n\t\t}\n\t\tbody {\n\t\t\tmargin:0;\n\t\t\theight:100%;\n\t\t}\n\t</style>\n\t<!-- copy these lines to your document head: -->\n\n\t<meta name=\"viewport\" content=\"user-scalable=yes, width=600\" />\n\n\t<!-- end copy -->\n  </head>\n  <body>\n\t<!-- copy these lines to your document: -->\n\n\t<div id=\"tron_hype_container\" style=\"margin:auto;position:relative;width:600px;height:500px;overflow:hidden;\" aria-live=\"polite\">\n\t\t<script type=\"text/javascript\" charset=\"utf-8\" src=\"tron.hyperesources/tron_hype_generated_script.js?46699\"></script>\n\t</div>\n\n\t<!-- end copy -->\n\n\n\n\t<!-- text content for search engines: -->\n\n\t<div style=\"display:none\">\n\n\t\t<div></div>\n\n\t</div>\n\n\t<!-- end text content: -->\n\n  </body>\n</html>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/tron/tron.hyperesources/PIE.htc",
    "content": "<!--\nPIE: CSS3 rendering for IE\nVersion 1.0.0\nhttp://css3pie.com\nDual-licensed for use under the Apache License Version 2.0 or the General Public License (GPL) Version 2.\n-->\n<PUBLIC:COMPONENT lightWeight=\"true\">\n<!-- saved from url=(0014)about:internet -->\n<PUBLIC:ATTACH EVENT=\"oncontentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondocumentready\" FOR=\"element\" ONEVENT=\"init()\" />\n<PUBLIC:ATTACH EVENT=\"ondetach\" FOR=\"element\" ONEVENT=\"cleanup()\" />\n\n<script type=\"text/javascript\">\nvar doc = element.document;var f=window.PIE;\nif(!f){f=window.PIE={F:\"-pie-\",nb:\"Pie\",La:\"pie_\",Ac:{TD:1,TH:1},cc:{TABLE:1,THEAD:1,TBODY:1,TFOOT:1,TR:1,INPUT:1,TEXTAREA:1,SELECT:1,OPTION:1,IMG:1,HR:1},fc:{A:1,INPUT:1,TEXTAREA:1,SELECT:1,BUTTON:1},Gd:{submit:1,button:1,reset:1},aa:function(){}};try{doc.execCommand(\"BackgroundImageCache\",false,true)}catch(aa){}for(var ba=4,Z=doc.createElement(\"div\"),ca=Z.getElementsByTagName(\"i\"),ga;Z.innerHTML=\"<!--[if gt IE \"+ ++ba+\"]><i></i><![endif]--\\>\",ca[0];);f.O=ba;if(ba===6)f.F=f.F.replace(/^-/,\"\");f.ja=\ndoc.documentMode||f.O;Z.innerHTML='<v:shape adj=\"1\"/>';ga=Z.firstChild;ga.style.behavior=\"url(#default#VML)\";f.zc=typeof ga.adj===\"object\";(function(){var a,b=0,c={};f.p={Za:function(d){if(!a){a=doc.createDocumentFragment();a.namespaces.add(\"css3vml\",\"urn:schemas-microsoft-com:vml\")}return a.createElement(\"css3vml:\"+d)},Ba:function(d){return d&&d._pieId||(d._pieId=\"_\"+ ++b)},Eb:function(d){var e,g,j,i,h=arguments;e=1;for(g=h.length;e<g;e++){i=h[e];for(j in i)if(i.hasOwnProperty(j))d[j]=i[j]}return d},\nRb:function(d,e,g){var j=c[d],i,h;if(j)Object.prototype.toString.call(j)===\"[object Array]\"?j.push([e,g]):e.call(g,j);else{h=c[d]=[[e,g]];i=new Image;i.onload=function(){j=c[d]={h:i.width,f:i.height};for(var k=0,n=h.length;k<n;k++)h[k][0].call(h[k][1],j);i.onload=null};i.src=d}}}})();f.Na={gc:function(a,b,c,d){function e(){k=j>=90&&j<270?b:0;n=j<180?c:0;m=b-k;p=c-n}function g(){for(;j<0;)j+=360;j%=360}var j=d.sa;d=d.zb;var i,h,k,n,m,p,r,t;if(d){d=d.coords(a,b,c);i=d.x;h=d.y}if(j){j=j.jd();g();e();\nif(!d){i=k;h=n}d=f.Na.tc(i,h,j,m,p);a=d[0];d=d[1]}else if(d){a=b-i;d=c-h}else{i=h=a=0;d=c}r=a-i;t=d-h;if(j===void 0){j=!r?t<0?90:270:!t?r<0?180:0:-Math.atan2(t,r)/Math.PI*180;g();e()}return{sa:j,xc:i,yc:h,td:a,ud:d,Wd:k,Xd:n,rd:m,sd:p,kd:r,ld:t,rc:f.Na.dc(i,h,a,d)}},tc:function(a,b,c,d,e){if(c===0||c===180)return[d,b];else if(c===90||c===270)return[a,e];else{c=Math.tan(-c*Math.PI/180);a=c*a-b;b=-1/c;d=b*d-e;e=b-c;return[(d-a)/e,(c*d-b*a)/e]}},dc:function(a,b,c,d){a=c-a;b=d-b;return Math.abs(a===0?\nb:b===0?a:Math.sqrt(a*a+b*b))}};f.ea=function(){this.Gb=[];this.oc={}};f.ea.prototype={ba:function(a){var b=f.p.Ba(a),c=this.oc,d=this.Gb;if(!(b in c)){c[b]=d.length;d.push(a)}},Ha:function(a){a=f.p.Ba(a);var b=this.oc;if(a&&a in b){delete this.Gb[b[a]];delete b[a]}},xa:function(){for(var a=this.Gb,b=a.length;b--;)a[b]&&a[b]()}};f.Oa=new f.ea;f.Oa.Rd=function(){var a=this,b;if(!a.Sd){b=doc.documentElement.currentStyle.getAttribute(f.F+\"poll-interval\")||250;(function c(){a.xa();setTimeout(c,b)})();\na.Sd=1}};(function(){function a(){f.L.xa();window.detachEvent(\"onunload\",a);window.PIE=null}f.L=new f.ea;window.attachEvent(\"onunload\",a);f.L.ta=function(b,c,d){b.attachEvent(c,d);this.ba(function(){b.detachEvent(c,d)})}})();f.Qa=new f.ea;f.L.ta(window,\"onresize\",function(){f.Qa.xa()});(function(){function a(){f.mb.xa()}f.mb=new f.ea;f.L.ta(window,\"onscroll\",a);f.Qa.ba(a)})();(function(){function a(){c=f.kb.md()}function b(){if(c){for(var d=0,e=c.length;d<e;d++)f.attach(c[d]);c=0}}var c;if(f.ja<9){f.L.ta(window,\n\"onbeforeprint\",a);f.L.ta(window,\"onafterprint\",b)}})();f.lb=new f.ea;f.L.ta(doc,\"onmouseup\",function(){f.lb.xa()});f.he=function(){function a(h){this.Y=h}var b=doc.createElement(\"length-calc\"),c=doc.body||doc.documentElement,d=b.style,e={},g=[\"mm\",\"cm\",\"in\",\"pt\",\"pc\"],j=g.length,i={};d.position=\"absolute\";d.top=d.left=\"-9999px\";for(c.appendChild(b);j--;){d.width=\"100\"+g[j];e[g[j]]=b.offsetWidth/100}c.removeChild(b);d.width=\"1em\";a.prototype={Kb:/(px|em|ex|mm|cm|in|pt|pc|%)$/,ic:function(){var h=\nthis.Jd;if(h===void 0)h=this.Jd=parseFloat(this.Y);return h},yb:function(){var h=this.ae;if(!h)h=this.ae=(h=this.Y.match(this.Kb))&&h[0]||\"px\";return h},a:function(h,k){var n=this.ic(),m=this.yb();switch(m){case \"px\":return n;case \"%\":return n*(typeof k===\"function\"?k():k)/100;case \"em\":return n*this.xb(h);case \"ex\":return n*this.xb(h)/2;default:return n*e[m]}},xb:function(h){var k=h.currentStyle.fontSize,n,m;if(k.indexOf(\"px\")>0)return parseFloat(k);else if(h.tagName in f.cc){m=this;n=h.parentNode;\nreturn f.n(k).a(n,function(){return m.xb(n)})}else{h.appendChild(b);k=b.offsetWidth;b.parentNode===h&&h.removeChild(b);return k}}};f.n=function(h){return i[h]||(i[h]=new a(h))};return a}();f.Ja=function(){function a(e){this.X=e}var b=f.n(\"50%\"),c={top:1,center:1,bottom:1},d={left:1,center:1,right:1};a.prototype={zd:function(){if(!this.ac){var e=this.X,g=e.length,j=f.v,i=j.qa,h=f.n(\"0\");i=i.na;h=[\"left\",h,\"top\",h];if(g===1){e.push(new j.ob(i,\"center\"));g++}if(g===2){i&(e[0].k|e[1].k)&&e[0].d in c&&\ne[1].d in d&&e.push(e.shift());if(e[0].k&i)if(e[0].d===\"center\")h[1]=b;else h[0]=e[0].d;else if(e[0].W())h[1]=f.n(e[0].d);if(e[1].k&i)if(e[1].d===\"center\")h[3]=b;else h[2]=e[1].d;else if(e[1].W())h[3]=f.n(e[1].d)}this.ac=h}return this.ac},coords:function(e,g,j){var i=this.zd(),h=i[1].a(e,g);e=i[3].a(e,j);return{x:i[0]===\"right\"?g-h:h,y:i[2]===\"bottom\"?j-e:e}}};return a}();f.Ka=function(){function a(b,c){this.h=b;this.f=c}a.prototype={a:function(b,c,d,e,g){var j=this.h,i=this.f,h=c/d;e=e/g;if(j===\n\"contain\"){j=e>h?c:d*e;i=e>h?c/e:d}else if(j===\"cover\"){j=e<h?c:d*e;i=e<h?c/e:d}else if(j===\"auto\"){i=i===\"auto\"?g:i.a(b,d);j=i*e}else{j=j.a(b,c);i=i===\"auto\"?j/e:i.a(b,d)}return{h:j,f:i}}};a.Kc=new a(\"auto\",\"auto\");return a}();f.Ec=function(){function a(b){this.Y=b}a.prototype={Kb:/[a-z]+$/i,yb:function(){return this.ad||(this.ad=this.Y.match(this.Kb)[0].toLowerCase())},jd:function(){var b=this.Vc,c;if(b===undefined){b=this.yb();c=parseFloat(this.Y,10);b=this.Vc=b===\"deg\"?c:b===\"rad\"?c/Math.PI*180:\nb===\"grad\"?c/400*360:b===\"turn\"?c*360:0}return b}};return a}();f.Jc=function(){function a(c){this.Y=c}var b={};a.Qd=/\\s*rgba\\(\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d{1,3})\\s*,\\s*(\\d+|\\d*\\.\\d+)\\s*\\)\\s*/;a.Fb={aliceblue:\"F0F8FF\",antiquewhite:\"FAEBD7\",aqua:\"0FF\",aquamarine:\"7FFFD4\",azure:\"F0FFFF\",beige:\"F5F5DC\",bisque:\"FFE4C4\",black:\"000\",blanchedalmond:\"FFEBCD\",blue:\"00F\",blueviolet:\"8A2BE2\",brown:\"A52A2A\",burlywood:\"DEB887\",cadetblue:\"5F9EA0\",chartreuse:\"7FFF00\",chocolate:\"D2691E\",coral:\"FF7F50\",cornflowerblue:\"6495ED\",\ncornsilk:\"FFF8DC\",crimson:\"DC143C\",cyan:\"0FF\",darkblue:\"00008B\",darkcyan:\"008B8B\",darkgoldenrod:\"B8860B\",darkgray:\"A9A9A9\",darkgreen:\"006400\",darkkhaki:\"BDB76B\",darkmagenta:\"8B008B\",darkolivegreen:\"556B2F\",darkorange:\"FF8C00\",darkorchid:\"9932CC\",darkred:\"8B0000\",darksalmon:\"E9967A\",darkseagreen:\"8FBC8F\",darkslateblue:\"483D8B\",darkslategray:\"2F4F4F\",darkturquoise:\"00CED1\",darkviolet:\"9400D3\",deeppink:\"FF1493\",deepskyblue:\"00BFFF\",dimgray:\"696969\",dodgerblue:\"1E90FF\",firebrick:\"B22222\",floralwhite:\"FFFAF0\",\nforestgreen:\"228B22\",fuchsia:\"F0F\",gainsboro:\"DCDCDC\",ghostwhite:\"F8F8FF\",gold:\"FFD700\",goldenrod:\"DAA520\",gray:\"808080\",green:\"008000\",greenyellow:\"ADFF2F\",honeydew:\"F0FFF0\",hotpink:\"FF69B4\",indianred:\"CD5C5C\",indigo:\"4B0082\",ivory:\"FFFFF0\",khaki:\"F0E68C\",lavender:\"E6E6FA\",lavenderblush:\"FFF0F5\",lawngreen:\"7CFC00\",lemonchiffon:\"FFFACD\",lightblue:\"ADD8E6\",lightcoral:\"F08080\",lightcyan:\"E0FFFF\",lightgoldenrodyellow:\"FAFAD2\",lightgreen:\"90EE90\",lightgrey:\"D3D3D3\",lightpink:\"FFB6C1\",lightsalmon:\"FFA07A\",\nlightseagreen:\"20B2AA\",lightskyblue:\"87CEFA\",lightslategray:\"789\",lightsteelblue:\"B0C4DE\",lightyellow:\"FFFFE0\",lime:\"0F0\",limegreen:\"32CD32\",linen:\"FAF0E6\",magenta:\"F0F\",maroon:\"800000\",mediumauqamarine:\"66CDAA\",mediumblue:\"0000CD\",mediumorchid:\"BA55D3\",mediumpurple:\"9370D8\",mediumseagreen:\"3CB371\",mediumslateblue:\"7B68EE\",mediumspringgreen:\"00FA9A\",mediumturquoise:\"48D1CC\",mediumvioletred:\"C71585\",midnightblue:\"191970\",mintcream:\"F5FFFA\",mistyrose:\"FFE4E1\",moccasin:\"FFE4B5\",navajowhite:\"FFDEAD\",\nnavy:\"000080\",oldlace:\"FDF5E6\",olive:\"808000\",olivedrab:\"688E23\",orange:\"FFA500\",orangered:\"FF4500\",orchid:\"DA70D6\",palegoldenrod:\"EEE8AA\",palegreen:\"98FB98\",paleturquoise:\"AFEEEE\",palevioletred:\"D87093\",papayawhip:\"FFEFD5\",peachpuff:\"FFDAB9\",peru:\"CD853F\",pink:\"FFC0CB\",plum:\"DDA0DD\",powderblue:\"B0E0E6\",purple:\"800080\",red:\"F00\",rosybrown:\"BC8F8F\",royalblue:\"4169E1\",saddlebrown:\"8B4513\",salmon:\"FA8072\",sandybrown:\"F4A460\",seagreen:\"2E8B57\",seashell:\"FFF5EE\",sienna:\"A0522D\",silver:\"C0C0C0\",skyblue:\"87CEEB\",\nslateblue:\"6A5ACD\",slategray:\"708090\",snow:\"FFFAFA\",springgreen:\"00FF7F\",steelblue:\"4682B4\",tan:\"D2B48C\",teal:\"008080\",thistle:\"D8BFD8\",tomato:\"FF6347\",turquoise:\"40E0D0\",violet:\"EE82EE\",wheat:\"F5DEB3\",white:\"FFF\",whitesmoke:\"F5F5F5\",yellow:\"FF0\",yellowgreen:\"9ACD32\"};a.prototype={parse:function(){if(!this.Ua){var c=this.Y,d;if(d=c.match(a.Qd)){this.Ua=\"rgb(\"+d[1]+\",\"+d[2]+\",\"+d[3]+\")\";this.Yb=parseFloat(d[4])}else{if((d=c.toLowerCase())in a.Fb)c=\"#\"+a.Fb[d];this.Ua=c;this.Yb=c===\"transparent\"?0:\n1}}},U:function(c){this.parse();return this.Ua===\"currentColor\"?c.currentStyle.color:this.Ua},fa:function(){this.parse();return this.Yb}};f.ha=function(c){return b[c]||(b[c]=new a(c))};return a}();f.v=function(){function a(c){this.$a=c;this.ch=0;this.X=[];this.Ga=0}var b=a.qa={Ia:1,Wb:2,z:4,Lc:8,Xb:16,na:32,K:64,oa:128,pa:256,Ra:512,Tc:1024,URL:2048};a.ob=function(c,d){this.k=c;this.d=d};a.ob.prototype={Ca:function(){return this.k&b.K||this.k&b.oa&&this.d===\"0\"},W:function(){return this.Ca()||this.k&\nb.Ra}};a.prototype={de:/\\s/,Kd:/^[\\+\\-]?(\\d*\\.)?\\d+/,url:/^url\\(\\s*(\"([^\"]*)\"|'([^']*)'|([!#$%&*-~]*))\\s*\\)/i,nc:/^\\-?[_a-z][\\w-]*/i,Yd:/^(\"([^\"]*)\"|'([^']*)')/,Bd:/^#([\\da-f]{6}|[\\da-f]{3})/i,be:{px:b.K,em:b.K,ex:b.K,mm:b.K,cm:b.K,\"in\":b.K,pt:b.K,pc:b.K,deg:b.Ia,rad:b.Ia,grad:b.Ia},fd:{rgb:1,rgba:1,hsl:1,hsla:1},next:function(c){function d(p,r){p=new a.ob(p,r);if(!c){k.X.push(p);k.Ga++}return p}function e(){k.Ga++;return null}var g,j,i,h,k=this;if(this.Ga<this.X.length)return this.X[this.Ga++];for(;this.de.test(this.$a.charAt(this.ch));)this.ch++;\nif(this.ch>=this.$a.length)return e();j=this.ch;g=this.$a.substring(this.ch);i=g.charAt(0);switch(i){case \"#\":if(h=g.match(this.Bd)){this.ch+=h[0].length;return d(b.z,h[0])}break;case '\"':case \"'\":if(h=g.match(this.Yd)){this.ch+=h[0].length;return d(b.Tc,h[2]||h[3]||\"\")}break;case \"/\":case \",\":this.ch++;return d(b.pa,i);case \"u\":if(h=g.match(this.url)){this.ch+=h[0].length;return d(b.URL,h[2]||h[3]||h[4]||\"\")}}if(h=g.match(this.Kd)){i=h[0];this.ch+=i.length;if(g.charAt(i.length)===\"%\"){this.ch++;\nreturn d(b.Ra,i+\"%\")}if(h=g.substring(i.length).match(this.nc)){i+=h[0];this.ch+=h[0].length;return d(this.be[h[0].toLowerCase()]||b.Lc,i)}return d(b.oa,i)}if(h=g.match(this.nc)){i=h[0];this.ch+=i.length;if(i.toLowerCase()in f.Jc.Fb||i===\"currentColor\"||i===\"transparent\")return d(b.z,i);if(g.charAt(i.length)===\"(\"){this.ch++;if(i.toLowerCase()in this.fd){g=function(p){return p&&p.k&b.oa};h=function(p){return p&&p.k&(b.oa|b.Ra)};var n=function(p,r){return p&&p.d===r},m=function(){return k.next(1)};\nif((i.charAt(0)===\"r\"?h(m()):g(m()))&&n(m(),\",\")&&h(m())&&n(m(),\",\")&&h(m())&&(i===\"rgb\"||i===\"hsa\"||n(m(),\",\")&&g(m()))&&n(m(),\")\"))return d(b.z,this.$a.substring(j,this.ch));return e()}return d(b.Xb,i)}return d(b.na,i)}this.ch++;return d(b.Wb,i)},D:function(){return this.X[this.Ga-- -2]},all:function(){for(;this.next(););return this.X},ma:function(c,d){for(var e=[],g,j;g=this.next();){if(c(g)){j=true;this.D();break}e.push(g)}return d&&!j?null:e}};return a}();var ha=function(a){this.e=a};ha.prototype=\n{Z:0,Od:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.x!==b.x||a.y!==b.y)},Td:function(){var a=this.qb,b;return!a||(b=this.o())&&(a.h!==b.h||a.f!==b.f)},hc:function(){var a=this.e,b=a.getBoundingClientRect(),c=f.ja===9,d=f.O===7,e=b.right-b.left;return{x:b.left,y:b.top,h:c||d?a.offsetWidth:e,f:c||d?a.offsetHeight:b.bottom-b.top,Hd:d&&e?a.offsetWidth/e:1}},o:function(){return this.Z?this.Va||(this.Va=this.hc()):this.hc()},Ad:function(){return!!this.qb},cb:function(){++this.Z},hb:function(){if(!--this.Z){if(this.Va)this.qb=\nthis.Va;this.Va=null}}};(function(){function a(b){var c=f.p.Ba(b);return function(){if(this.Z){var d=this.$b||(this.$b={});return c in d?d[c]:(d[c]=b.call(this))}else return b.call(this)}}f.B={Z:0,ka:function(b){function c(d){this.e=d;this.Zb=this.ia()}f.p.Eb(c.prototype,f.B,b);c.$c={};return c},j:function(){var b=this.ia(),c=this.constructor.$c;return b?b in c?c[b]:(c[b]=this.la(b)):null},ia:a(function(){var b=this.e,c=this.constructor,d=b.style;b=b.currentStyle;var e=this.wa,g=this.Fa,j=c.Yc||(c.Yc=\nf.F+e);c=c.Zc||(c.Zc=f.nb+g.charAt(0).toUpperCase()+g.substring(1));return d[c]||b.getAttribute(j)||d[g]||b.getAttribute(e)}),i:a(function(){return!!this.j()}),H:a(function(){var b=this.ia(),c=b!==this.Zb;this.Zb=b;return c}),va:a,cb:function(){++this.Z},hb:function(){--this.Z||delete this.$b}}})();f.Sb=f.B.ka({wa:f.F+\"background\",Fa:f.nb+\"Background\",cd:{scroll:1,fixed:1,local:1},fb:{\"repeat-x\":1,\"repeat-y\":1,repeat:1,\"no-repeat\":1},sc:{\"padding-box\":1,\"border-box\":1,\"content-box\":1},Pd:{top:1,right:1,\nbottom:1,left:1,center:1},Ud:{contain:1,cover:1},eb:{Ma:\"backgroundClip\",z:\"backgroundColor\",da:\"backgroundImage\",Pa:\"backgroundOrigin\",S:\"backgroundPosition\",T:\"backgroundRepeat\",Sa:\"backgroundSize\"},la:function(a){function b(s){return s&&s.W()||s.k&k&&s.d in t}function c(s){return s&&(s.W()&&f.n(s.d)||s.d===\"auto\"&&\"auto\")}var d=this.e.currentStyle,e,g,j,i=f.v.qa,h=i.pa,k=i.na,n=i.z,m,p,r=0,t=this.Pd,v,l,q={M:[]};if(this.wb()){e=new f.v(a);for(j={};g=e.next();){m=g.k;p=g.d;if(!j.P&&m&i.Xb&&p===\n\"linear-gradient\"){v={ca:[],P:p};for(l={};g=e.next();){m=g.k;p=g.d;if(m&i.Wb&&p===\")\"){l.color&&v.ca.push(l);v.ca.length>1&&f.p.Eb(j,v);break}if(m&n){if(v.sa||v.zb){g=e.D();if(g.k!==h)break;e.next()}l={color:f.ha(p)};g=e.next();if(g.W())l.db=f.n(g.d);else e.D()}else if(m&i.Ia&&!v.sa&&!l.color&&!v.ca.length)v.sa=new f.Ec(g.d);else if(b(g)&&!v.zb&&!l.color&&!v.ca.length){e.D();v.zb=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&h&&p===\",\"){if(l.color){v.ca.push(l);l={}}}else break}}else if(!j.P&&\nm&i.URL){j.Ab=p;j.P=\"image\"}else if(b(g)&&!j.$){e.D();j.$=new f.Ja(e.ma(function(s){return!b(s)},false))}else if(m&k)if(p in this.fb&&!j.bb)j.bb=p;else if(p in this.sc&&!j.Wa){j.Wa=p;if((g=e.next())&&g.k&k&&g.d in this.sc)j.ub=g.d;else{j.ub=p;e.D()}}else if(p in this.cd&&!j.bc)j.bc=p;else return null;else if(m&n&&!q.color)q.color=f.ha(p);else if(m&h&&p===\"/\"&&!j.Xa&&j.$){g=e.next();if(g.k&k&&g.d in this.Ud)j.Xa=new f.Ka(g.d);else if(g=c(g)){m=c(e.next());if(!m){m=g;e.D()}j.Xa=new f.Ka(g,m)}else return null}else if(m&\nh&&p===\",\"&&j.P){j.Hb=a.substring(r,e.ch-1);r=e.ch;q.M.push(j);j={}}else return null}if(j.P){j.Hb=a.substring(r);q.M.push(j)}}else this.Bc(f.ja<9?function(){var s=this.eb,o=d[s.S+\"X\"],u=d[s.S+\"Y\"],x=d[s.da],y=d[s.z];if(y!==\"transparent\")q.color=f.ha(y);if(x!==\"none\")q.M=[{P:\"image\",Ab:(new f.v(x)).next().d,bb:d[s.T],$:new f.Ja((new f.v(o+\" \"+u)).all())}]}:function(){var s=this.eb,o=/\\s*,\\s*/,u=d[s.da].split(o),x=d[s.z],y,z,B,E,D,C;if(x!==\"transparent\")q.color=f.ha(x);if((E=u.length)&&u[0]!==\"none\"){x=\nd[s.T].split(o);y=d[s.S].split(o);z=d[s.Pa].split(o);B=d[s.Ma].split(o);s=d[s.Sa].split(o);q.M=[];for(o=0;o<E;o++)if((D=u[o])&&D!==\"none\"){C=s[o].split(\" \");q.M.push({Hb:D+\" \"+x[o]+\" \"+y[o]+\" / \"+s[o]+\" \"+z[o]+\" \"+B[o],P:\"image\",Ab:(new f.v(D)).next().d,bb:x[o],$:new f.Ja((new f.v(y[o])).all()),Wa:z[o],ub:B[o],Xa:new f.Ka(C[0],C[1])})}}});return q.color||q.M[0]?q:null},Bc:function(a){var b=f.ja>8,c=this.eb,d=this.e.runtimeStyle,e=d[c.da],g=d[c.z],j=d[c.T],i,h,k,n;if(e)d[c.da]=\"\";if(g)d[c.z]=\"\";if(j)d[c.T]=\n\"\";if(b){i=d[c.Ma];h=d[c.Pa];n=d[c.S];k=d[c.Sa];if(i)d[c.Ma]=\"\";if(h)d[c.Pa]=\"\";if(n)d[c.S]=\"\";if(k)d[c.Sa]=\"\"}a=a.call(this);if(e)d[c.da]=e;if(g)d[c.z]=g;if(j)d[c.T]=j;if(b){if(i)d[c.Ma]=i;if(h)d[c.Pa]=h;if(n)d[c.S]=n;if(k)d[c.Sa]=k}return a},ia:f.B.va(function(){return this.wb()||this.Bc(function(){var a=this.e.currentStyle,b=this.eb;return a[b.z]+\" \"+a[b.da]+\" \"+a[b.T]+\" \"+a[b.S+\"X\"]+\" \"+a[b.S+\"Y\"]})}),wb:f.B.va(function(){var a=this.e;return a.style[this.Fa]||a.currentStyle.getAttribute(this.wa)}),\nqc:function(){var a=0;if(f.O<7){a=this.e;a=\"\"+(a.style[f.nb+\"PngFix\"]||a.currentStyle.getAttribute(f.F+\"png-fix\"))===\"true\"}return a},i:f.B.va(function(){return(this.wb()||this.qc())&&!!this.j()})});f.Vb=f.B.ka({wc:[\"Top\",\"Right\",\"Bottom\",\"Left\"],Id:{thin:\"1px\",medium:\"3px\",thick:\"5px\"},la:function(){var a={},b={},c={},d=false,e=true,g=true,j=true;this.Cc(function(){for(var i=this.e.currentStyle,h=0,k,n,m,p,r,t,v;h<4;h++){m=this.wc[h];v=m.charAt(0).toLowerCase();k=b[v]=i[\"border\"+m+\"Style\"];n=i[\"border\"+\nm+\"Color\"];m=i[\"border\"+m+\"Width\"];if(h>0){if(k!==p)g=false;if(n!==r)e=false;if(m!==t)j=false}p=k;r=n;t=m;c[v]=f.ha(n);m=a[v]=f.n(b[v]===\"none\"?\"0\":this.Id[m]||m);if(m.a(this.e)>0)d=true}});return d?{J:a,Zd:b,gd:c,ee:j,hd:e,$d:g}:null},ia:f.B.va(function(){var a=this.e,b=a.currentStyle,c;a.tagName in f.Ac&&a.offsetParent.currentStyle.borderCollapse===\"collapse\"||this.Cc(function(){c=b.borderWidth+\"|\"+b.borderStyle+\"|\"+b.borderColor});return c}),Cc:function(a){var b=this.e.runtimeStyle,c=b.borderWidth,\nd=b.borderColor;if(c)b.borderWidth=\"\";if(d)b.borderColor=\"\";a=a.call(this);if(c)b.borderWidth=c;if(d)b.borderColor=d;return a}});(function(){f.jb=f.B.ka({wa:\"border-radius\",Fa:\"borderRadius\",la:function(b){var c=null,d,e,g,j,i=false;if(b){e=new f.v(b);var h=function(){for(var k=[],n;(g=e.next())&&g.W();){j=f.n(g.d);n=j.ic();if(n<0)return null;if(n>0)i=true;k.push(j)}return k.length>0&&k.length<5?{tl:k[0],tr:k[1]||k[0],br:k[2]||k[0],bl:k[3]||k[1]||k[0]}:null};if(b=h()){if(g){if(g.k&f.v.qa.pa&&g.d===\n\"/\")d=h()}else d=b;if(i&&b&&d)c={x:b,y:d}}}return c}});var a=f.n(\"0\");a={tl:a,tr:a,br:a,bl:a};f.jb.Dc={x:a,y:a}})();f.Ub=f.B.ka({wa:\"border-image\",Fa:\"borderImage\",fb:{stretch:1,round:1,repeat:1,space:1},la:function(a){var b=null,c,d,e,g,j,i,h=0,k=f.v.qa,n=k.na,m=k.oa,p=k.Ra;if(a){c=new f.v(a);b={};for(var r=function(l){return l&&l.k&k.pa&&l.d===\"/\"},t=function(l){return l&&l.k&n&&l.d===\"fill\"},v=function(){g=c.ma(function(l){return!(l.k&(m|p))});if(t(c.next())&&!b.fill)b.fill=true;else c.D();if(r(c.next())){h++;\nj=c.ma(function(l){return!l.W()&&!(l.k&n&&l.d===\"auto\")});if(r(c.next())){h++;i=c.ma(function(l){return!l.Ca()})}}else c.D()};a=c.next();){d=a.k;e=a.d;if(d&(m|p)&&!g){c.D();v()}else if(t(a)&&!b.fill){b.fill=true;v()}else if(d&n&&this.fb[e]&&!b.repeat){b.repeat={f:e};if(a=c.next())if(a.k&n&&this.fb[a.d])b.repeat.Ob=a.d;else c.D()}else if(d&k.URL&&!b.src)b.src=e;else return null}if(!b.src||!g||g.length<1||g.length>4||j&&j.length>4||h===1&&j.length<1||i&&i.length>4||h===2&&i.length<1)return null;if(!b.repeat)b.repeat=\n{f:\"stretch\"};if(!b.repeat.Ob)b.repeat.Ob=b.repeat.f;a=function(l,q){return{t:q(l[0]),r:q(l[1]||l[0]),b:q(l[2]||l[0]),l:q(l[3]||l[1]||l[0])}};b.slice=a(g,function(l){return f.n(l.k&m?l.d+\"px\":l.d)});if(j&&j[0])b.J=a(j,function(l){return l.W()?f.n(l.d):l.d});if(i&&i[0])b.Da=a(i,function(l){return l.Ca()?f.n(l.d):l.d})}return b}});f.Ic=f.B.ka({wa:\"box-shadow\",Fa:\"boxShadow\",la:function(a){var b,c=f.n,d=f.v.qa,e;if(a){e=new f.v(a);b={Da:[],Bb:[]};for(a=function(){for(var g,j,i,h,k,n;g=e.next();){i=g.d;\nj=g.k;if(j&d.pa&&i===\",\")break;else if(g.Ca()&&!k){e.D();k=e.ma(function(m){return!m.Ca()})}else if(j&d.z&&!h)h=i;else if(j&d.na&&i===\"inset\"&&!n)n=true;else return false}g=k&&k.length;if(g>1&&g<5){(n?b.Bb:b.Da).push({fe:c(k[0].d),ge:c(k[1].d),blur:c(k[2]?k[2].d:\"0\"),Vd:c(k[3]?k[3].d:\"0\"),color:f.ha(h||\"currentColor\")});return true}return false};a(););}return b&&(b.Bb.length||b.Da.length)?b:null}});f.Uc=f.B.ka({ia:f.B.va(function(){var a=this.e.currentStyle;return a.visibility+\"|\"+a.display}),la:function(){var a=\nthis.e,b=a.runtimeStyle;a=a.currentStyle;var c=b.visibility,d;b.visibility=\"\";d=a.visibility;b.visibility=c;return{ce:d!==\"hidden\",nd:a.display!==\"none\"}},i:function(){return false}});f.u={R:function(a){function b(c,d,e,g){this.e=c;this.s=d;this.g=e;this.parent=g}f.p.Eb(b.prototype,f.u,a);return b},Cb:false,Q:function(){return false},Ea:f.aa,Lb:function(){this.m();this.i()&&this.V()},ib:function(){this.Cb=true},Mb:function(){this.i()?this.V():this.m()},sb:function(a,b){this.vc(a);for(var c=this.ra||\n(this.ra=[]),d=a+1,e=c.length,g;d<e;d++)if(g=c[d])break;c[a]=b;this.I().insertBefore(b,g||null)},za:function(a){var b=this.ra;return b&&b[a]||null},vc:function(a){var b=this.za(a),c=this.Ta;if(b&&c){c.removeChild(b);this.ra[a]=null}},Aa:function(a,b,c,d){var e=this.rb||(this.rb={}),g=e[a];if(!g){g=e[a]=f.p.Za(\"shape\");if(b)g.appendChild(g[b]=f.p.Za(b));if(d){c=this.za(d);if(!c){this.sb(d,doc.createElement(\"group\"+d));c=this.za(d)}}c.appendChild(g);a=g.style;a.position=\"absolute\";a.left=a.top=0;a.behavior=\n\"url(#default#VML)\"}return g},vb:function(a){var b=this.rb,c=b&&b[a];if(c){c.parentNode.removeChild(c);delete b[a]}return!!c},kc:function(a){var b=this.e,c=this.s.o(),d=c.h,e=c.f,g,j,i,h,k,n;c=a.x.tl.a(b,d);g=a.y.tl.a(b,e);j=a.x.tr.a(b,d);i=a.y.tr.a(b,e);h=a.x.br.a(b,d);k=a.y.br.a(b,e);n=a.x.bl.a(b,d);a=a.y.bl.a(b,e);d=Math.min(d/(c+j),e/(i+k),d/(n+h),e/(g+a));if(d<1){c*=d;g*=d;j*=d;i*=d;h*=d;k*=d;n*=d;a*=d}return{x:{tl:c,tr:j,br:h,bl:n},y:{tl:g,tr:i,br:k,bl:a}}},ya:function(a,b,c){b=b||1;var d,e,\ng=this.s.o();e=g.h*b;g=g.f*b;var j=this.g.G,i=Math.floor,h=Math.ceil,k=a?a.Jb*b:0,n=a?a.Ib*b:0,m=a?a.tb*b:0;a=a?a.Db*b:0;var p,r,t,v,l;if(c||j.i()){d=this.kc(c||j.j());c=d.x.tl*b;j=d.y.tl*b;p=d.x.tr*b;r=d.y.tr*b;t=d.x.br*b;v=d.y.br*b;l=d.x.bl*b;b=d.y.bl*b;e=\"m\"+i(a)+\",\"+i(j)+\"qy\"+i(c)+\",\"+i(k)+\"l\"+h(e-p)+\",\"+i(k)+\"qx\"+h(e-n)+\",\"+i(r)+\"l\"+h(e-n)+\",\"+h(g-v)+\"qy\"+h(e-t)+\",\"+h(g-m)+\"l\"+i(l)+\",\"+h(g-m)+\"qx\"+i(a)+\",\"+h(g-b)+\" x e\"}else e=\"m\"+i(a)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+i(k)+\"l\"+h(e-n)+\",\"+h(g-m)+\"l\"+i(a)+\n\",\"+h(g-m)+\"xe\";return e},I:function(){var a=this.parent.za(this.N),b;if(!a){a=doc.createElement(this.Ya);b=a.style;b.position=\"absolute\";b.top=b.left=0;this.parent.sb(this.N,a)}return a},mc:function(){var a=this.e,b=a.currentStyle,c=a.runtimeStyle,d=a.tagName,e=f.O===6,g;if(e&&(d in f.cc||d===\"FIELDSET\")||d===\"BUTTON\"||d===\"INPUT\"&&a.type in f.Gd){c.borderWidth=\"\";d=this.g.w.wc;for(g=d.length;g--;){e=d[g];c[\"padding\"+e]=\"\";c[\"padding\"+e]=f.n(b[\"padding\"+e]).a(a)+f.n(b[\"border\"+e+\"Width\"]).a(a)+(f.O!==\n8&&g%2?1:0)}c.borderWidth=0}else if(e){if(a.childNodes.length!==1||a.firstChild.tagName!==\"ie6-mask\"){b=doc.createElement(\"ie6-mask\");d=b.style;d.visibility=\"visible\";for(d.zoom=1;d=a.firstChild;)b.appendChild(d);a.appendChild(b);c.visibility=\"hidden\"}}else c.borderColor=\"transparent\"},ie:function(){},m:function(){this.parent.vc(this.N);delete this.rb;delete this.ra}};f.Rc=f.u.R({i:function(){var a=this.ed;for(var b in a)if(a.hasOwnProperty(b)&&a[b].i())return true;return false},Q:function(){return this.g.Pb.H()},\nib:function(){if(this.i()){var a=this.jc(),b=a,c;a=a.currentStyle;var d=a.position,e=this.I().style,g=0,j=0;j=this.s.o();var i=j.Hd;if(d===\"fixed\"&&f.O>6){g=j.x*i;j=j.y*i;b=d}else{do b=b.offsetParent;while(b&&b.currentStyle.position===\"static\");if(b){c=b.getBoundingClientRect();b=b.currentStyle;g=(j.x-c.left)*i-(parseFloat(b.borderLeftWidth)||0);j=(j.y-c.top)*i-(parseFloat(b.borderTopWidth)||0)}else{b=doc.documentElement;g=(j.x+b.scrollLeft-b.clientLeft)*i;j=(j.y+b.scrollTop-b.clientTop)*i}b=\"absolute\"}e.position=\nb;e.left=g;e.top=j;e.zIndex=d===\"static\"?-1:a.zIndex;this.Cb=true}},Mb:f.aa,Nb:function(){var a=this.g.Pb.j();this.I().style.display=a.ce&&a.nd?\"\":\"none\"},Lb:function(){this.i()?this.Nb():this.m()},jc:function(){var a=this.e;return a.tagName in f.Ac?a.offsetParent:a},I:function(){var a=this.Ta,b;if(!a){b=this.jc();a=this.Ta=doc.createElement(\"css3-container\");a.style.direction=\"ltr\";this.Nb();b.parentNode.insertBefore(a,b)}return a},ab:f.aa,m:function(){var a=this.Ta,b;if(a&&(b=a.parentNode))b.removeChild(a);\ndelete this.Ta;delete this.ra}});f.Fc=f.u.R({N:2,Ya:\"background\",Q:function(){var a=this.g;return a.C.H()||a.G.H()},i:function(){var a=this.g;return a.q.i()||a.G.i()||a.C.i()||a.ga.i()&&a.ga.j().Bb},V:function(){var a=this.s.o();if(a.h&&a.f){this.od();this.pd()}},od:function(){var a=this.g.C.j(),b=this.s.o(),c=this.e,d=a&&a.color,e,g;if(d&&d.fa()>0){this.lc();a=this.Aa(\"bgColor\",\"fill\",this.I(),1);e=b.h;b=b.f;a.stroked=false;a.coordsize=e*2+\",\"+b*2;a.coordorigin=\"1,1\";a.path=this.ya(null,2);g=a.style;\ng.width=e;g.height=b;a.fill.color=d.U(c);c=d.fa();if(c<1)a.fill.opacity=c}else this.vb(\"bgColor\")},pd:function(){var a=this.g.C.j(),b=this.s.o();a=a&&a.M;var c,d,e,g,j;if(a){this.lc();d=b.h;e=b.f;for(j=a.length;j--;){b=a[j];c=this.Aa(\"bgImage\"+j,\"fill\",this.I(),2);c.stroked=false;c.fill.type=\"tile\";c.fillcolor=\"none\";c.coordsize=d*2+\",\"+e*2;c.coordorigin=\"1,1\";c.path=this.ya(0,2);g=c.style;g.width=d;g.height=e;if(b.P===\"linear-gradient\")this.bd(c,b);else{c.fill.src=b.Ab;this.Nd(c,j)}}}for(j=a?a.length:\n0;this.vb(\"bgImage\"+j++););},Nd:function(a,b){var c=this;f.p.Rb(a.fill.src,function(d){var e=c.e,g=c.s.o(),j=g.h;g=g.f;if(j&&g){var i=a.fill,h=c.g,k=h.w.j(),n=k&&k.J;k=n?n.t.a(e):0;var m=n?n.r.a(e):0,p=n?n.b.a(e):0;n=n?n.l.a(e):0;h=h.C.j().M[b];e=h.$?h.$.coords(e,j-d.h-n-m,g-d.f-k-p):{x:0,y:0};h=h.bb;p=m=0;var r=j+1,t=g+1,v=f.O===8?0:1;n=Math.round(e.x)+n+0.5;k=Math.round(e.y)+k+0.5;i.position=n/j+\",\"+k/g;i.size.x=1;i.size=d.h+\"px,\"+d.f+\"px\";if(h&&h!==\"repeat\"){if(h===\"repeat-x\"||h===\"no-repeat\"){m=\nk+1;t=k+d.f+v}if(h===\"repeat-y\"||h===\"no-repeat\"){p=n+1;r=n+d.h+v}a.style.clip=\"rect(\"+m+\"px,\"+r+\"px,\"+t+\"px,\"+p+\"px)\"}}})},bd:function(a,b){var c=this.e,d=this.s.o(),e=d.h,g=d.f;a=a.fill;d=b.ca;var j=d.length,i=Math.PI,h=f.Na,k=h.tc,n=h.dc;b=h.gc(c,e,g,b);h=b.sa;var m=b.xc,p=b.yc,r=b.Wd,t=b.Xd,v=b.rd,l=b.sd,q=b.kd,s=b.ld;b=b.rc;e=h%90?Math.atan2(q*e/g,s)/i*180:h+90;e+=180;e%=360;v=k(r,t,h,v,l);g=n(r,t,v[0],v[1]);i=[];v=k(m,p,h,r,t);n=n(m,p,v[0],v[1])/g*100;k=[];for(h=0;h<j;h++)k.push(d[h].db?d[h].db.a(c,\nb):h===0?0:h===j-1?b:null);for(h=1;h<j;h++){if(k[h]===null){m=k[h-1];b=h;do p=k[++b];while(p===null);k[h]=m+(p-m)/(b-h+1)}k[h]=Math.max(k[h],k[h-1])}for(h=0;h<j;h++)i.push(n+k[h]/g*100+\"% \"+d[h].color.U(c));a.angle=e;a.type=\"gradient\";a.method=\"sigma\";a.color=d[0].color.U(c);a.color2=d[j-1].color.U(c);if(a.colors)a.colors.value=i.join(\",\");else a.colors=i.join(\",\")},lc:function(){var a=this.e.runtimeStyle;a.backgroundImage=\"url(about:blank)\";a.backgroundColor=\"transparent\"},m:function(){f.u.m.call(this);\nvar a=this.e.runtimeStyle;a.backgroundImage=a.backgroundColor=\"\"}});f.Gc=f.u.R({N:4,Ya:\"border\",Q:function(){var a=this.g;return a.w.H()||a.G.H()},i:function(){var a=this.g;return a.G.i()&&!a.q.i()&&a.w.i()},V:function(){var a=this.e,b=this.g.w.j(),c=this.s.o(),d=c.h;c=c.f;var e,g,j,i,h;if(b){this.mc();b=this.wd(2);i=0;for(h=b.length;i<h;i++){j=b[i];e=this.Aa(\"borderPiece\"+i,j.stroke?\"stroke\":\"fill\",this.I());e.coordsize=d*2+\",\"+c*2;e.coordorigin=\"1,1\";e.path=j.path;g=e.style;g.width=d;g.height=c;\ne.filled=!!j.fill;e.stroked=!!j.stroke;if(j.stroke){e=e.stroke;e.weight=j.Qb+\"px\";e.color=j.color.U(a);e.dashstyle=j.stroke===\"dashed\"?\"2 2\":j.stroke===\"dotted\"?\"1 1\":\"solid\";e.linestyle=j.stroke===\"double\"&&j.Qb>2?\"ThinThin\":\"Single\"}else e.fill.color=j.fill.U(a)}for(;this.vb(\"borderPiece\"+i++););}},wd:function(a){var b=this.e,c,d,e,g=this.g.w,j=[],i,h,k,n,m=Math.round,p,r,t;if(g.i()){c=g.j();g=c.J;r=c.Zd;t=c.gd;if(c.ee&&c.$d&&c.hd){if(t.t.fa()>0){c=g.t.a(b);k=c/2;j.push({path:this.ya({Jb:k,Ib:k,\ntb:k,Db:k},a),stroke:r.t,color:t.t,Qb:c})}}else{a=a||1;c=this.s.o();d=c.h;e=c.f;c=m(g.t.a(b));k=m(g.r.a(b));n=m(g.b.a(b));b=m(g.l.a(b));var v={t:c,r:k,b:n,l:b};b=this.g.G;if(b.i())p=this.kc(b.j());i=Math.floor;h=Math.ceil;var l=function(o,u){return p?p[o][u]:0},q=function(o,u,x,y,z,B){var E=l(\"x\",o),D=l(\"y\",o),C=o.charAt(1)===\"r\";o=o.charAt(0)===\"b\";return E>0&&D>0?(B?\"al\":\"ae\")+(C?h(d-E):i(E))*a+\",\"+(o?h(e-D):i(D))*a+\",\"+(i(E)-u)*a+\",\"+(i(D)-x)*a+\",\"+y*65535+\",\"+2949075*(z?1:-1):(B?\"m\":\"l\")+(C?d-\nu:u)*a+\",\"+(o?e-x:x)*a},s=function(o,u,x,y){var z=o===\"t\"?i(l(\"x\",\"tl\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+i(l(\"y\",\"tr\"))*a:o===\"b\"?h(d-l(\"x\",\"br\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+h(e-l(\"y\",\"bl\"))*a;o=o===\"t\"?h(d-l(\"x\",\"tr\"))*a+\",\"+h(u)*a:o===\"r\"?h(d-u)*a+\",\"+h(e-l(\"y\",\"br\"))*a:o===\"b\"?i(l(\"x\",\"bl\"))*a+\",\"+i(e-u)*a:i(u)*a+\",\"+i(l(\"y\",\"tl\"))*a;return x?(y?\"m\"+o:\"\")+\"l\"+z:(y?\"m\"+z:\"\")+\"l\"+o};b=function(o,u,x,y,z,B){var E=o===\"l\"||o===\"r\",D=v[o],C,F;if(D>0&&r[o]!==\"none\"&&t[o].fa()>0){C=v[E?o:u];u=v[E?u:\no];F=v[E?o:x];x=v[E?x:o];if(r[o]===\"dashed\"||r[o]===\"dotted\"){j.push({path:q(y,C,u,B+45,0,1)+q(y,0,0,B,1,0),fill:t[o]});j.push({path:s(o,D/2,0,1),stroke:r[o],Qb:D,color:t[o]});j.push({path:q(z,F,x,B,0,1)+q(z,0,0,B-45,1,0),fill:t[o]})}else j.push({path:q(y,C,u,B+45,0,1)+s(o,D,0,0)+q(z,F,x,B,0,0)+(r[o]===\"double\"&&D>2?q(z,F-i(F/3),x-i(x/3),B-45,1,0)+s(o,h(D/3*2),1,0)+q(y,C-i(C/3),u-i(u/3),B,1,0)+\"x \"+q(y,i(C/3),i(u/3),B+45,0,1)+s(o,i(D/3),1,0)+q(z,i(F/3),i(x/3),B,0,0):\"\")+q(z,0,0,B-45,1,0)+s(o,0,1,\n0)+q(y,0,0,B,1,0),fill:t[o]})}};b(\"t\",\"l\",\"r\",\"tl\",\"tr\",90);b(\"r\",\"t\",\"b\",\"tr\",\"br\",0);b(\"b\",\"r\",\"l\",\"br\",\"bl\",-90);b(\"l\",\"b\",\"t\",\"bl\",\"tl\",-180)}}return j},m:function(){if(this.ec||!this.g.q.i())this.e.runtimeStyle.borderColor=\"\";f.u.m.call(this)}});f.Tb=f.u.R({N:5,Md:[\"t\",\"tr\",\"r\",\"br\",\"b\",\"bl\",\"l\",\"tl\",\"c\"],Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){this.I();var a=this.g.q.j(),b=this.g.w.j(),c=this.s.o(),d=this.e,e=this.uc;f.p.Rb(a.src,function(g){function j(s,\no,u,x,y){s=e[s].style;var z=Math.max;s.width=z(o,0);s.height=z(u,0);s.left=x;s.top=y}function i(s,o,u){for(var x=0,y=s.length;x<y;x++)e[s[x]].imagedata[o]=u}var h=c.h,k=c.f,n=f.n(\"0\"),m=a.J||(b?b.J:{t:n,r:n,b:n,l:n});n=m.t.a(d);var p=m.r.a(d),r=m.b.a(d);m=m.l.a(d);var t=a.slice,v=t.t.a(d),l=t.r.a(d),q=t.b.a(d);t=t.l.a(d);j(\"tl\",m,n,0,0);j(\"t\",h-m-p,n,m,0);j(\"tr\",p,n,h-p,0);j(\"r\",p,k-n-r,h-p,n);j(\"br\",p,r,h-p,k-r);j(\"b\",h-m-p,r,m,k-r);j(\"bl\",m,r,0,k-r);j(\"l\",m,k-n-r,0,n);j(\"c\",h-m-p,k-n-r,m,n);i([\"tl\",\n\"t\",\"tr\"],\"cropBottom\",(g.f-v)/g.f);i([\"tl\",\"l\",\"bl\"],\"cropRight\",(g.h-t)/g.h);i([\"bl\",\"b\",\"br\"],\"cropTop\",(g.f-q)/g.f);i([\"tr\",\"r\",\"br\"],\"cropLeft\",(g.h-l)/g.h);i([\"l\",\"r\",\"c\"],\"cropTop\",v/g.f);i([\"l\",\"r\",\"c\"],\"cropBottom\",q/g.f);i([\"t\",\"b\",\"c\"],\"cropLeft\",t/g.h);i([\"t\",\"b\",\"c\"],\"cropRight\",l/g.h);e.c.style.display=a.fill?\"\":\"none\"},this)},I:function(){var a=this.parent.za(this.N),b,c,d,e=this.Md,g=e.length;if(!a){a=doc.createElement(\"border-image\");b=a.style;b.position=\"absolute\";this.uc={};for(d=\n0;d<g;d++){c=this.uc[e[d]]=f.p.Za(\"rect\");c.appendChild(f.p.Za(\"imagedata\"));b=c.style;b.behavior=\"url(#default#VML)\";b.position=\"absolute\";b.top=b.left=0;c.imagedata.src=this.g.q.j().src;c.stroked=false;c.filled=false;a.appendChild(c)}this.parent.sb(this.N,a)}return a},Ea:function(){if(this.i()){var a=this.e,b=a.runtimeStyle,c=this.g.q.j().J;b.borderStyle=\"solid\";if(c){b.borderTopWidth=c.t.a(a)+\"px\";b.borderRightWidth=c.r.a(a)+\"px\";b.borderBottomWidth=c.b.a(a)+\"px\";b.borderLeftWidth=c.l.a(a)+\"px\"}this.mc()}},\nm:function(){var a=this.e.runtimeStyle;a.borderStyle=\"\";if(this.ec||!this.g.w.i())a.borderColor=a.borderWidth=\"\";f.u.m.call(this)}});f.Hc=f.u.R({N:1,Ya:\"outset-box-shadow\",Q:function(){var a=this.g;return a.ga.H()||a.G.H()},i:function(){var a=this.g.ga;return a.i()&&a.j().Da[0]},V:function(){function a(C,F,O,H,M,P,I){C=b.Aa(\"shadow\"+C+F,\"fill\",d,j-C);F=C.fill;C.coordsize=n*2+\",\"+m*2;C.coordorigin=\"1,1\";C.stroked=false;C.filled=true;F.color=M.U(c);if(P){F.type=\"gradienttitle\";F.color2=F.color;F.opacity=\n0}C.path=I;l=C.style;l.left=O;l.top=H;l.width=n;l.height=m;return C}var b=this,c=this.e,d=this.I(),e=this.g,g=e.ga.j().Da;e=e.G.j();var j=g.length,i=j,h,k=this.s.o(),n=k.h,m=k.f;k=f.O===8?1:0;for(var p=[\"tl\",\"tr\",\"br\",\"bl\"],r,t,v,l,q,s,o,u,x,y,z,B,E,D;i--;){t=g[i];q=t.fe.a(c);s=t.ge.a(c);h=t.Vd.a(c);o=t.blur.a(c);t=t.color;u=-h-o;if(!e&&o)e=f.jb.Dc;u=this.ya({Jb:u,Ib:u,tb:u,Db:u},2,e);if(o){x=(h+o)*2+n;y=(h+o)*2+m;z=x?o*2/x:0;B=y?o*2/y:0;if(o-h>n/2||o-h>m/2)for(h=4;h--;){r=p[h];E=r.charAt(0)===\"b\";\nD=r.charAt(1)===\"r\";r=a(i,r,q,s,t,o,u);v=r.fill;v.focusposition=(D?1-z:z)+\",\"+(E?1-B:B);v.focussize=\"0,0\";r.style.clip=\"rect(\"+((E?y/2:0)+k)+\"px,\"+(D?x:x/2)+\"px,\"+(E?y:y/2)+\"px,\"+((D?x/2:0)+k)+\"px)\"}else{r=a(i,\"\",q,s,t,o,u);v=r.fill;v.focusposition=z+\",\"+B;v.focussize=1-z*2+\",\"+(1-B*2)}}else{r=a(i,\"\",q,s,t,o,u);q=t.fa();if(q<1)r.fill.opacity=q}}}});f.Pc=f.u.R({N:6,Ya:\"imgEl\",Q:function(){var a=this.g;return this.e.src!==this.Xc||a.G.H()},i:function(){var a=this.g;return a.G.i()||a.C.qc()},V:function(){this.Xc=\nj;this.Cd();var a=this.Aa(\"img\",\"fill\",this.I()),b=a.fill,c=this.s.o(),d=c.h;c=c.f;var e=this.g.w.j(),g=e&&e.J;e=this.e;var j=e.src,i=Math.round,h=e.currentStyle,k=f.n;if(!g||f.O<7){g=f.n(\"0\");g={t:g,r:g,b:g,l:g}}a.stroked=false;b.type=\"frame\";b.src=j;b.position=(d?0.5/d:0)+\",\"+(c?0.5/c:0);a.coordsize=d*2+\",\"+c*2;a.coordorigin=\"1,1\";a.path=this.ya({Jb:i(g.t.a(e)+k(h.paddingTop).a(e)),Ib:i(g.r.a(e)+k(h.paddingRight).a(e)),tb:i(g.b.a(e)+k(h.paddingBottom).a(e)),Db:i(g.l.a(e)+k(h.paddingLeft).a(e))},\n2);a=a.style;a.width=d;a.height=c},Cd:function(){this.e.runtimeStyle.filter=\"alpha(opacity=0)\"},m:function(){f.u.m.call(this);this.e.runtimeStyle.filter=\"\"}});f.Oc=f.u.R({ib:f.aa,Mb:f.aa,Nb:f.aa,Lb:f.aa,Ld:/^,+|,+$/g,Fd:/,+/g,gb:function(a,b){(this.pb||(this.pb=[]))[a]=b||void 0},ab:function(){var a=this.pb,b;if(a&&(b=a.join(\",\").replace(this.Ld,\"\").replace(this.Fd,\",\"))!==this.Wc)this.Wc=this.e.runtimeStyle.background=b},m:function(){this.e.runtimeStyle.background=\"\";delete this.pb}});f.Mc=f.u.R({ua:1,\nQ:function(){return this.g.C.H()},i:function(){var a=this.g;return a.C.i()||a.q.i()},V:function(){var a=this.g.C.j(),b,c,d=0,e,g;if(a){b=[];if(c=a.M)for(;e=c[d++];)if(e.P===\"linear-gradient\"){g=this.vd(e.Wa);g=(e.Xa||f.Ka.Kc).a(this.e,g.h,g.f,g.h,g.f);b.push(\"url(data:image/svg+xml,\"+escape(this.xd(e,g.h,g.f))+\") \"+this.dd(e.$)+\" / \"+g.h+\"px \"+g.f+\"px \"+(e.bc||\"\")+\" \"+(e.Wa||\"\")+\" \"+(e.ub||\"\"))}else b.push(e.Hb);a.color&&b.push(a.color.Y);this.parent.gb(this.ua,b.join(\",\"))}},dd:function(a){return a?\na.X.map(function(b){return b.d}).join(\" \"):\"0 0\"},vd:function(a){var b=this.e,c=this.s.o(),d=c.h;c=c.f;var e;if(a!==\"border-box\")if((e=this.g.w.j())&&(e=e.J)){d-=e.l.a(b)+e.l.a(b);c-=e.t.a(b)+e.b.a(b)}if(a===\"content-box\"){a=f.n;e=b.currentStyle;d-=a(e.paddingLeft).a(b)+a(e.paddingRight).a(b);c-=a(e.paddingTop).a(b)+a(e.paddingBottom).a(b)}return{h:d,f:c}},xd:function(a,b,c){var d=this.e,e=a.ca,g=e.length,j=f.Na.gc(d,b,c,a);a=j.xc;var i=j.yc,h=j.td,k=j.ud;j=j.rc;var n,m,p,r,t;n=[];for(m=0;m<g;m++)n.push(e[m].db?\ne[m].db.a(d,j):m===0?0:m===g-1?j:null);for(m=1;m<g;m++)if(n[m]===null){r=n[m-1];p=m;do t=n[++p];while(t===null);n[m]=r+(t-r)/(p-m+1)}b=['<svg width=\"'+b+'\" height=\"'+c+'\" xmlns=\"http://www.w3.org/2000/svg\"><defs><linearGradient id=\"g\" gradientUnits=\"userSpaceOnUse\" x1=\"'+a/b*100+'%\" y1=\"'+i/c*100+'%\" x2=\"'+h/b*100+'%\" y2=\"'+k/c*100+'%\">'];for(m=0;m<g;m++)b.push('<stop offset=\"'+n[m]/j+'\" stop-color=\"'+e[m].color.U(d)+'\" stop-opacity=\"'+e[m].color.fa()+'\"/>');b.push('</linearGradient></defs><rect width=\"100%\" height=\"100%\" fill=\"url(#g)\"/></svg>');\nreturn b.join(\"\")},m:function(){this.parent.gb(this.ua)}});f.Nc=f.u.R({T:\"repeat\",Sc:\"stretch\",Qc:\"round\",ua:0,Q:function(){return this.g.q.H()},i:function(){return this.g.q.i()},V:function(){var a=this,b=a.g.q.j(),c=a.g.w.j(),d=a.s.o(),e=b.repeat,g=e.f,j=e.Ob,i=a.e,h=0;f.p.Rb(b.src,function(k){function n(Q,R,U,V,W,Y,X,S,w,A){K.push('<pattern patternUnits=\"userSpaceOnUse\" id=\"pattern'+G+'\" x=\"'+(g===l?Q+U/2-w/2:Q)+'\" y=\"'+(j===l?R+V/2-A/2:R)+'\" width=\"'+w+'\" height=\"'+A+'\"><svg width=\"'+w+'\" height=\"'+\nA+'\" viewBox=\"'+W+\" \"+Y+\" \"+X+\" \"+S+'\" preserveAspectRatio=\"none\"><image xlink:href=\"'+v+'\" x=\"0\" y=\"0\" width=\"'+r+'\" height=\"'+t+'\" /></svg></pattern>');J.push('<rect x=\"'+Q+'\" y=\"'+R+'\" width=\"'+U+'\" height=\"'+V+'\" fill=\"url(#pattern'+G+')\" />');G++}var m=d.h,p=d.f,r=k.h,t=k.f,v=a.Dd(b.src,r,t),l=a.T,q=a.Sc;k=a.Qc;var s=Math.ceil,o=f.n(\"0\"),u=b.J||(c?c.J:{t:o,r:o,b:o,l:o});o=u.t.a(i);var x=u.r.a(i),y=u.b.a(i);u=u.l.a(i);var z=b.slice,B=z.t.a(i),E=z.r.a(i),D=z.b.a(i);z=z.l.a(i);var C=m-u-x,F=p-o-\ny,O=r-z-E,H=t-B-D,M=g===q?C:O*o/B,P=j===q?F:H*x/E,I=g===q?C:O*y/D;q=j===q?F:H*u/z;var K=[],J=[],G=0;if(g===k){M-=(M-(C%M||M))/s(C/M);I-=(I-(C%I||I))/s(C/I)}if(j===k){P-=(P-(F%P||P))/s(F/P);q-=(q-(F%q||q))/s(F/q)}k=['<svg width=\"'+m+'\" height=\"'+p+'\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">'];n(0,0,u,o,0,0,z,B,u,o);n(u,0,C,o,z,0,O,B,M,o);n(m-x,0,x,o,r-E,0,E,B,x,o);n(0,o,u,F,0,B,z,H,u,q);if(b.fill)n(u,o,C,F,z,B,O,H,M||I||O,q||P||H);n(m-x,o,x,F,r-E,B,E,H,x,P);n(0,\np-y,u,y,0,t-D,z,D,u,y);n(u,p-y,C,y,z,t-D,O,D,I,y);n(m-x,p-y,x,y,r-E,t-D,E,D,x,y);k.push(\"<defs>\"+K.join(\"\\n\")+\"</defs>\"+J.join(\"\\n\")+\"</svg>\");a.parent.gb(a.ua,\"url(data:image/svg+xml,\"+escape(k.join(\"\"))+\") no-repeat border-box border-box\");h&&a.parent.ab()},a);h=1},Dd:function(){var a={};return function(b,c,d){var e=a[b],g;if(!e){e=new Image;g=doc.createElement(\"canvas\");e.src=b;g.width=c;g.height=d;g.getContext(\"2d\").drawImage(e,0,0);e=a[b]=g.toDataURL()}return e}}(),Ea:f.Tb.prototype.Ea,m:function(){var a=\nthis.e.runtimeStyle;this.parent.gb(this.ua);a.borderColor=a.borderStyle=a.borderWidth=\"\"}});f.kb=function(){function a(l,q){l.className+=\" \"+q}function b(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;)a(l,q[s])},0)}function c(l){var q=v.slice.call(arguments,1),s=q.length;setTimeout(function(){if(l)for(;s--;){var o=q[s];o=t[o]||(t[o]=new RegExp(\"\\\\b\"+o+\"\\\\b\",\"g\"));l.className=l.className.replace(o,\"\")}},0)}function d(l){function q(){if(!U){var w,A,L=f.ja,T=l.currentStyle,\nN=T.getAttribute(g)===\"true\",da=T.getAttribute(i)!==\"false\",ea=T.getAttribute(h)!==\"false\";S=T.getAttribute(j);S=L>7?S!==\"false\":S===\"true\";if(!R){R=1;l.runtimeStyle.zoom=1;T=l;for(var fa=1;T=T.previousSibling;)if(T.nodeType===1){fa=0;break}fa&&a(l,p)}J.cb();if(N&&(A=J.o())&&(w=doc.documentElement||doc.body)&&(A.y>w.clientHeight||A.x>w.clientWidth||A.y+A.f<0||A.x+A.h<0)){if(!Y){Y=1;f.mb.ba(q)}}else{U=1;Y=R=0;f.mb.Ha(q);if(L===9){G={C:new f.Sb(l),q:new f.Ub(l),w:new f.Vb(l)};Q=[G.C,G.q];K=new f.Oc(l,\nJ,G);w=[new f.Mc(l,J,G,K),new f.Nc(l,J,G,K)]}else{G={C:new f.Sb(l),w:new f.Vb(l),q:new f.Ub(l),G:new f.jb(l),ga:new f.Ic(l),Pb:new f.Uc(l)};Q=[G.C,G.w,G.q,G.G,G.ga,G.Pb];K=new f.Rc(l,J,G);w=[new f.Hc(l,J,G,K),new f.Fc(l,J,G,K),new f.Gc(l,J,G,K),new f.Tb(l,J,G,K)];l.tagName===\"IMG\"&&w.push(new f.Pc(l,J,G,K));K.ed=w}I=[K].concat(w);if(w=l.currentStyle.getAttribute(f.F+\"watch-ancestors\")){w=parseInt(w,10);A=0;for(N=l.parentNode;N&&(w===\"NaN\"||A++<w);){H(N,\"onpropertychange\",C);H(N,\"onmouseenter\",x);\nH(N,\"onmouseleave\",y);H(N,\"onmousedown\",z);if(N.tagName in f.fc){H(N,\"onfocus\",E);H(N,\"onblur\",D)}N=N.parentNode}}if(S){f.Oa.ba(o);f.Oa.Rd()}o(1)}if(!V){V=1;L<9&&H(l,\"onmove\",s);H(l,\"onresize\",s);H(l,\"onpropertychange\",u);ea&&H(l,\"onmouseenter\",x);if(ea||da)H(l,\"onmouseleave\",y);da&&H(l,\"onmousedown\",z);if(l.tagName in f.fc){H(l,\"onfocus\",E);H(l,\"onblur\",D)}f.Qa.ba(s);f.L.ba(M)}J.hb()}}function s(){J&&J.Ad()&&o()}function o(w){if(!X)if(U){var A,L=I.length;F();for(A=0;A<L;A++)I[A].Ea();if(w||J.Od())for(A=\n0;A<L;A++)I[A].ib();if(w||J.Td())for(A=0;A<L;A++)I[A].Mb();K.ab();O()}else R||q()}function u(){var w,A=I.length,L;w=event;if(!X&&!(w&&w.propertyName in r))if(U){F();for(w=0;w<A;w++)I[w].Ea();for(w=0;w<A;w++){L=I[w];L.Cb||L.ib();L.Q()&&L.Lb()}K.ab();O()}else R||q()}function x(){b(l,k)}function y(){c(l,k,n)}function z(){b(l,n);f.lb.ba(B)}function B(){c(l,n);f.lb.Ha(B)}function E(){b(l,m)}function D(){c(l,m)}function C(){var w=event.propertyName;if(w===\"className\"||w===\"id\")u()}function F(){J.cb();for(var w=\nQ.length;w--;)Q[w].cb()}function O(){for(var w=Q.length;w--;)Q[w].hb();J.hb()}function H(w,A,L){w.attachEvent(A,L);W.push([w,A,L])}function M(){if(V){for(var w=W.length,A;w--;){A=W[w];A[0].detachEvent(A[1],A[2])}f.L.Ha(M);V=0;W=[]}}function P(){if(!X){var w,A;M();X=1;if(I){w=0;for(A=I.length;w<A;w++){I[w].ec=1;I[w].m()}}S&&f.Oa.Ha(o);f.Qa.Ha(o);I=J=G=Q=l=null}}var I,K,J=new ha(l),G,Q,R,U,V,W=[],Y,X,S;this.Ed=q;this.update=o;this.m=P;this.qd=l}var e={},g=f.F+\"lazy-init\",j=f.F+\"poll\",i=f.F+\"track-active\",\nh=f.F+\"track-hover\",k=f.La+\"hover\",n=f.La+\"active\",m=f.La+\"focus\",p=f.La+\"first-child\",r={background:1,bgColor:1,display:1},t={},v=[];d.yd=function(l){var q=f.p.Ba(l);return e[q]||(e[q]=new d(l))};d.m=function(l){l=f.p.Ba(l);var q=e[l];if(q){q.m();delete e[l]}};d.md=function(){var l=[],q;if(e){for(var s in e)if(e.hasOwnProperty(s)){q=e[s];l.push(q.qd);q.m()}e={}}return l};return d}();f.supportsVML=f.zc;f.attach=function(a){f.ja<10&&f.zc&&f.kb.yd(a).Ed()};f.detach=function(a){f.kb.m(a)}};\nvar $=element;function init(){if(doc.media!==\"print\"){var a=window.PIE;a&&a.attach($)}}function cleanup(){if(doc.media!==\"print\"){var a=window.PIE;if(a){a.detach($);$=0}}}$.readyState===\"complete\"&&init();\n</script>\n</PUBLIC:COMPONENT>\n"
  },
  {
    "path": "packages/client-app/static/animations/inbox-zero/tron/tron.hyperesources/tron_hype_generated_script.js",
    "content": "//\tHYPE.documents[\"TRON\"]\n\n(function(){(function k(){function l(a,b,d){var c=!1;null==window[a]&&(null==window[b]?(window[b]=[],window[b].push(k),a=document.getElementsByTagName(\"head\")[0],b=document.createElement(\"script\"),c=h,false==!0&&(c=\"\"),b.type=\"text/javascript\",b.src=c+\"/\"+d,a.appendChild(b)):window[b].push(k),c=!0);return c}var h=\"TRON.hyperesources\",c=\"TRON\",e=\"tron_hype_container\";if(false==!1)try{for(var f=document.getElementsByTagName(\"script\"),\na=0;a<f.length;a++){var b=f[a].src;if(null!=b&&-1!=b.indexOf(\"tron_hype_generated_script.js\")){h=b.substr(0,b.lastIndexOf(\"/\"));break}}}catch(n){}if(false==!1&&(a=navigator.userAgent.match(/MSIE (\\d+\\.\\d+)/),a=parseFloat(a&&a[1])||null,a=l(\"HYPE_526\",\"HYPE_dtl_526\",!0==(null!=a&&10>a||false==!0)?\"HYPE-526.full.min.js\":\"HYPE-526.thin.min.js\"),false==!0&&(a=a||l(\"HYPE_w_526\",\"HYPE_wdtl_526\",\"HYPE-526.waypoints.min.js\")),a))return;\nf=window.HYPE.documents;if(null!=f[c]){b=1;a=c;do c=\"\"+a+\"-\"+b++;while(null!=f[c]);for(var d=document.getElementsByTagName(\"div\"),b=!1,a=0;a<d.length;a++)if(d[a].id==e&&null==d[a].getAttribute(\"HYP_dn\")){var b=1,g=e;do e=\"\"+g+\"-\"+b++;while(null!=document.getElementById(e));d[a].id=e;b=!0;break}if(!1==b)return}b=[];b=[];d={};g={};for(a=0;a<b.length;a++)try{g[b[a].identifier]=b[a].name,d[b[a].name]=eval(\"(function(){return \"+b[a].source+\"})();\")}catch(m){window.console&&window.console.log(m),\nd[b[a].name]=function(){}}a=new HYPE_526(c,e,{\"10\":{p:1,n:\"Combined%20Shape.svg\",g:\"234\",t:\"image/svg+xml\"},\"2\":{p:1,n:\"Path%207.svg\",g:\"218\",t:\"image/svg+xml\"},\"15\":{p:1,n:\"ACtGlowy%20right.svg\",g:\"294\",t:\"image/svg+xml\"},\"3\":{p:1,n:\"Path%206.svg\",g:\"220\",t:\"image/svg+xml\"},\"11\":{p:1,n:\"Checkmark%20body.svg\",g:\"236\",t:\"image/svg+xml\"},\"4\":{p:1,n:\"Path%205.svg\",g:\"222\",t:\"image/svg+xml\"},\"5\":{p:1,n:\"Path%204.svg\",g:\"224\",t:\"image/svg+xml\"},\"12\":{p:1,n:\"Component%20top.svg\",g:\"238\",t:\"image/svg+xml\"},\"6\":{p:1,n:\"Path%203.svg\",g:\"226\",t:\"image/svg+xml\"},\"13\":{p:1,n:\"ActGlowy%20Top%20Right.svg\",g:\"290\",t:\"image/svg+xml\"},\"7\":{p:1,n:\"Glowy%20Top%20Right.svg\",g:\"228\",t:\"image/svg+xml\"},\"0\":{p:1,n:\"Path%2012.svg\",g:\"214\",t:\"image/svg+xml\"},\"8\":{p:1,n:\"Glowy%20Top%20Left.svg\",g:\"230\",t:\"image/svg+xml\"},\"14\":{p:1,n:\"ActGlowy%20Top%20Left.svg\",g:\"292\",t:\"image/svg+xml\"},\"1\":{p:1,n:\"Path%2010.svg\",g:\"216\",t:\"image/svg+xml\"},\"9\":{p:1,n:\"Glowy%20right.svg\",g:\"232\",t:\"image/svg+xml\"}},h,[],d,[{n:\"TRON\",o:\"130\",X:[0]}],[{o:\"132\",p:\"600px\",x:0,cA:false,Z:500,Y:600,c:\"#FFFFFF\",L:[],bY:1,d:600,U:{},T:{kTimelineDefaultIdentifier:{i:\"kTimelineDefaultIdentifier\",n:\"Main Timeline\",z:3,b:[],a:[{f:\"c\",y:0,z:0.21,i:\"e\",e:1,s:0,o:\"948\"},{f:\"c\",y:0,z:0.29,i:\"a\",e:6,s:6,o:\"933\"},{f:\"c\",y:0,z:1.17,i:\"S\",e:0,s:0,o:\"947\"},{f:\"c\",y:0,z:1.17,i:\"T\",e:0,s:0,o:\"947\"},{f:\"c\",y:0,z:1.17,i:\"Q\",e:0,s:0,o:\"947\"},{f:\"c\",y:0,z:0.22,i:\"a\",e:138,s:138,o:\"937\"},{f:\"c\",y:0,z:1.17,i:\"S\",e:0,s:0,o:\"943\"},{f:\"c\",y:0,z:1.17,i:\"T\",e:0,s:0,o:\"943\"},{f:\"c\",y:0,z:1.17,i:\"Q\",e:0,s:0,o:\"943\"},{f:\"c\",y:0,z:0.1,i:\"e\",e:1,s:0,o:\"931\"},{f:\"c\",y:0,z:1.29,i:\"S\",e:0,s:0,o:\"939\"},{f:\"c\",y:0,z:1.29,i:\"T\",e:0,s:0,o:\"939\"},{f:\"c\",y:0,z:1.29,i:\"Q\",e:0,s:0,o:\"939\"},{f:\"c\",y:0.1,z:0.12,i:\"b\",e:188,s:188,o:\"936\"},{y:0.1,i:\"e\",s:1,z:0,o:\"931\",f:\"c\"},{f:\"c\",y:0.13,z:1.04,i:\"R\",e:\"#3FCAE8\",s:\"#000000\",o:\"947\"},{f:\"c\",y:0.13,z:1.04,i:\"R\",e:\"#3FCAE8\",s:\"#000000\",o:\"943\"},{f:\"g\",y:0.15,z:0.07,i:\"b\",e:128,s:139,o:\"937\"},{f:\"g\",y:0.15,z:0.1,i:\"a\",e:84,s:89,o:\"940\"},{f:\"g\",y:0.15,z:0.07,i:\"a\",e:138,s:125,o:\"936\"},{f:\"g\",y:0.2,z:0.07,i:\"b\",e:38,s:42,o:\"945\"},{f:\"g\",y:0.2,z:0.07,i:\"b\",e:121,s:120,o:\"941\"},{f:\"g\",y:0.2,z:0.07,i:\"a\",e:164,s:168,o:\"945\"},{f:\"g\",y:0.2,z:0.07,i:\"a\",e:-12,s:0,o:\"941\"},{y:0.21,i:\"e\",s:1,z:0,o:\"948\",f:\"c\"},{f:\"g\",y:0.22,z:0.07,i:\"b\",e:71,s:77,o:\"933\"},{f:\"g\",y:0.22,z:0.07,i:\"b\",e:-4,s:0,o:\"934\"},{f:\"g\",y:0.22,z:0.07,i:\"a\",e:257,s:261,o:\"934\"},{y:0.22,i:\"b\",s:128,z:0,o:\"937\",f:\"c\"},{y:0.22,i:\"a\",s:138,z:0,o:\"937\",f:\"g\"},{y:0.22,i:\"b\",s:188,z:0,o:\"936\",f:\"g\"},{y:0.22,i:\"a\",s:138,z:0,o:\"936\",f:\"c\"},{f:\"c\",y:0.25,z:1.04,i:\"R\",e:\"#3FCAE8\",s:\"#000000\",o:\"939\"},{f:\"g\",y:0.25,z:0.07,i:\"b\",e:120,s:114,o:\"942\"},{f:\"g\",y:0.25,z:0.07,i:\"a\",e:234,s:227,o:\"942\"},{y:0.25,i:\"a\",s:84,z:0,o:\"940\",f:\"c\"},{f:\"g\",y:0.27,z:0.03,i:\"b\",e:30,s:38,o:\"945\"},{y:0.27,i:\"b\",s:121,z:0,o:\"941\",f:\"c\"},{f:\"g\",y:0.27,z:0.03,i:\"a\",e:165,s:164,o:\"945\"},{y:0.27,i:\"a\",s:-12,z:0,o:\"941\",f:\"c\"},{f:\"g\",y:0.28,z:0.08,i:\"b\",e:43,s:37,o:\"946\"},{f:\"g\",y:0.28,z:0.08,i:\"a\",e:271,s:263,o:\"946\"},{f:\"g\",y:0.29,z:0.03,i:\"b\",e:68,s:71,o:\"933\"},{f:\"g\",y:0.29,z:0.03,i:\"b\",e:-12,s:-4,o:\"934\"},{f:\"g\",y:0.29,z:0.03,i:\"a\",e:10,s:6,o:\"933\"},{f:\"g\",y:0.29,z:0.03,i:\"a\",e:257,s:257,o:\"934\"},{y:1,i:\"b\",s:30,z:0,o:\"945\",f:\"c\"},{y:1,i:\"a\",s:165,z:0,o:\"945\",f:\"c\"},{y:1.02,i:\"b\",s:120,z:0,o:\"942\",f:\"c\"},{y:1.02,i:\"b\",s:68,z:0,o:\"933\",f:\"c\"},{y:1.02,i:\"a\",s:234,z:0,o:\"942\",f:\"c\"},{y:1.02,i:\"b\",s:-12,z:0,o:\"934\",f:\"c\"},{y:1.02,i:\"a\",s:10,z:0,o:\"933\",f:\"c\"},{y:1.02,i:\"a\",s:257,z:0,o:\"934\",f:\"c\"},{f:\"85\",y:1.04,z:0.1,i:\"e\",e:1,s:0,o:\"943\"},{f:\"g\",y:1.06,z:0.05,i:\"b\",e:43,s:43,o:\"946\"},{f:\"g\",y:1.06,z:0.05,i:\"a\",e:274,s:271,o:\"946\"},{f:\"85\",y:1.07,z:0.1,i:\"e\",e:1,s:0,o:\"947\"},{f:\"85\",y:1.11,z:0.1,i:\"e\",e:1,s:0,o:\"939\"},{y:1.11,i:\"b\",s:43,z:0,o:\"946\",f:\"f\"},{y:1.11,i:\"a\",s:274,z:0,o:\"946\",f:\"f\"},{f:\"85\",y:1.14,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"943\"},{y:1.17,i:\"R\",s:\"#3FCAE8\",z:0,o:\"947\",f:\"c\"},{y:1.17,i:\"Q\",s:0,z:0,o:\"947\",f:\"c\"},{y:1.17,i:\"T\",s:0,z:0,o:\"947\",f:\"c\"},{y:1.17,i:\"S\",s:0,z:0,o:\"947\",f:\"c\"},{y:1.17,i:\"R\",s:\"#3FCAE8\",z:0,o:\"943\",f:\"c\"},{y:1.17,i:\"Q\",s:0,z:0,o:\"943\",f:\"c\"},{y:1.17,i:\"T\",s:0,z:0,o:\"943\",f:\"c\"},{y:1.17,i:\"S\",s:0,z:0,o:\"943\",f:\"c\"},{f:\"85\",y:1.17,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"947\"},{f:\"85\",y:1.21,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"939\"},{f:\"85\",y:1.21,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"943\"},{f:\"85\",y:1.24,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"947\"},{f:\"85\",y:1.27,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"943\"},{f:\"85\",y:1.28,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"939\"},{y:1.29,i:\"R\",s:\"#3FCAE8\",z:0,o:\"939\",f:\"c\"},{y:1.29,i:\"Q\",s:0,z:0,o:\"939\",f:\"c\"},{y:1.29,i:\"T\",s:0,z:0,o:\"939\",f:\"c\"},{y:1.29,i:\"S\",s:0,z:0,o:\"939\",f:\"c\"},{f:\"85\",y:2,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"947\"},{f:\"85\",y:2.04,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"939\"},{f:\"85\",y:2.04,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"943\"},{f:\"85\",y:2.07,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"947\"},{f:\"85\",y:2.1,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"943\"},{f:\"85\",y:2.11,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"939\"},{f:\"85\",y:2.13,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"947\"},{f:\"85\",y:2.17,z:0.07,i:\"e\",e:0.72483188291139244,s:1,o:\"939\"},{f:\"85\",y:2.17,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"943\"},{f:\"85\",y:2.2,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"947\"},{y:2.23,i:\"e\",s:1,z:0,o:\"943\",f:\"c\"},{f:\"85\",y:2.24,z:0.06,i:\"e\",e:1,s:0.72483188291139244,o:\"939\"},{y:2.26,i:\"e\",s:1,z:0,o:\"947\",f:\"c\"},{y:3,i:\"e\",s:1,z:0,o:\"939\",f:\"c\"}],f:30}},bZ:180,O:[\"933\",\"940\",\"934\",\"946\",\"942\",\"937\",\"945\",\"941\",\"936\",\"947\",\"943\",\"939\",\"935\",\"932\",\"938\",\"944\",\"931\",\"951\",\"966\",\"963\",\"960\",\"955\",\"953\",\"949\",\"964\",\"961\",\"958\",\"957\",\"956\",\"954\",\"952\",\"950\",\"965\",\"962\",\"959\",\"948\"],v:{\"951\":{c:93,d:23,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:18,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:98,aL:56,b:66},\"959\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:1,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:236,aL:56,b:97},\"946\":{h:\"224\",p:\"no-repeat\",x:\"visible\",a:263,q:\"100% 100%\",b:37,j:\"absolute\",bF:\"931\",c:99,k:\"div\",z:13,d:91,r:\"inline\"},\"965\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:3,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:81,aL:56,b:125},\"933\":{h:\"238\",p:\"no-repeat\",x:\"visible\",a:6,q:\"100% 100%\",b:77,j:\"absolute\",bF:\"931\",c:115,k:\"div\",z:16,d:58,r:\"inline\"},\"952\":{c:211,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:5,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:184,aL:56,b:132},\"947\":{w:\"\",Q:0,h:\"294\",x:\"visible\",R:\"#000000\",a:181,b:49,S:0,q:\"100% 100%\",z:7,T:0,j:\"absolute\",d:175,k:\"div\",p:\"no-repeat\",e:0,bF:\"931\",c:79,r:\"inline\"},\"966\":{c:157,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:17,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:207,aL:56,b:26},\"934\":{h:\"226\",p:\"no-repeat\",x:\"visible\",a:261,q:\"100% 100%\",b:0,j:\"absolute\",bF:\"931\",c:75,k:\"div\",z:14,d:49,r:\"inline\"},\"953\":{c:247,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:13,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:34,aL:56,b:193},\"940\":{h:\"234\",p:\"no-repeat\",x:\"visible\",a:89,q:\"100% 100%\",b:151,j:\"absolute\",bF:\"931\",c:47,k:\"div\",z:15,d:114,r:\"inline\"},\"948\":{x:\"visible\",k:\"div\",c:454,cR:1.2,d:342,z:1,e:0,a:65,j:\"absolute\",cQ:1.2,b:82},\"935\":{w:\"\",h:\"232\",p:\"no-repeat\",x:\"visible\",a:181,q:\"100% 100%\",b:49,j:\"absolute\",bF:\"931\",z:4,k:\"div\",c:79,d:178,r:\"inline\"},\"954\":{c:211,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:6,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:138,aL:56,b:228},\"941\":{h:\"216\",p:\"no-repeat\",x:\"visible\",a:0,q:\"100% 100%\",b:120,j:\"absolute\",bF:\"931\",c:75,k:\"div\",z:9,d:75,r:\"inline\"},\"949\":{c:247,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:12,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:129,aL:56,b:167},\"960\":{c:157,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:15,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:297,aL:56,b:205},\"936\":{h:\"214\",p:\"no-repeat\",x:\"visible\",a:125,q:\"100% 100%\",b:188,j:\"absolute\",bF:\"931\",c:83,k:\"div\",z:8,d:77,r:\"inline\"},\"955\":{c:248,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:14,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:98,aL:56,b:214},\"942\":{h:\"222\",p:\"no-repeat\",x:\"visible\",a:227,q:\"100% 100%\",b:114,j:\"absolute\",bF:\"931\",c:54,k:\"div\",z:12,d:62,r:\"inline\"},\"961\":{c:151,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:10,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:62,aL:56,b:239},\"937\":{w:\"\",h:\"220\",p:\"no-repeat\",x:\"visible\",a:138,q:\"100% 100%\",b:139,j:\"absolute\",bF:\"931\",z:11,k:\"div\",c:40,d:33,r:\"inline\"},\"956\":{c:113,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:7,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:39,aL:56,b:266},\"943\":{Q:0,h:\"292\",x:\"visible\",R:\"#000000\",a:12,q:\"100% 100%\",S:0,b:102,z:6,T:0,j:\"absolute\",d:76,k:\"div\",p:\"no-repeat\",e:0,bF:\"931\",c:92,r:\"inline\"},\"962\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:2,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:122,aL:56,b:112},\"938\":{h:\"228\",p:\"no-repeat\",x:\"visible\",a:273,q:\"100% 100%\",b:30,j:\"absolute\",bF:\"931\",c:52,k:\"div\",z:2,d:83,r:\"inline\"},\"957\":{c:93,d:23,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:8,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:114,aL:56,b:319},\"944\":{h:\"236\",p:\"no-repeat\",x:\"visible\",a:2,q:\"100% 100%\",b:3,j:\"absolute\",bF:\"931\",c:352,k:\"div\",z:1,d:257,r:\"inline\"},\"963\":{c:93,d:29,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:16,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:290,aL:56,b:51},\"931\":{x:\"visible\",k:\"div\",c:362,d:265,z:2,e:0,a:122,j:\"absolute\",b:137},\"939\":{Q:0,h:\"290\",x:\"visible\",R:\"#000000\",a:273,q:\"100% 100%\",S:0,b:29,z:5,T:0,j:\"absolute\",d:84,k:\"div\",p:\"no-repeat\",e:0,bF:\"931\",c:52,r:\"inline\"},\"950\":{c:190,d:57,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:4,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:34,aL:56,b:153},\"958\":{c:151,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:9,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:122,aL:56,b:239},\"945\":{h:\"218\",p:\"no-repeat\",x:\"visible\",a:168,q:\"100% 100%\",b:42,j:\"absolute\",bF:\"931\",c:96,k:\"div\",z:10,d:103,r:\"inline\"},\"964\":{c:151,d:37,I:\"None\",J:\"None\",K:\"None\",g:\"#F2F2F2\",L:\"None\",M:0,N:0,aI:56,A:\"#D8DDE4\",x:\"visible\",j:\"absolute\",O:0,aJ:56,k:\"div\",C:\"#D8DDE4\",z:11,B:\"#D8DDE4\",D:\"#D8DDE4\",aK:56,bF:\"948\",P:0,a:236,aL:56,b:258},\"932\":{h:\"230\",p:\"no-repeat\",x:\"visible\",a:11,q:\"100% 100%\",b:103,j:\"absolute\",bF:\"931\",z:3,k:\"div\",c:92,d:76,r:\"inline\",g:\"\"}}}],{},g,{f:[[0,0,0.1971,0,0.3391,0.8944,0.3636,1],[0.3636,1,0.3636,1,0.4425,0.75,0.5455,0.75],[0.5455,0.75,0.6519,0.75,0.7273,1,0.7273,1],[0.7273,1,0.7273,1,0.7718,0.9375,0.8182,0.9375],[0.8182,0.9375,0.8646,0.9375,0.9091,1,0.9091,1],[0.9091,1,0.9091,1,0.9294,0.9844,0.9546,0.9844],[0.9546,0.9844,0.9798,0.9844,1,1,1,1]],g:[[0,0,0.0425,0.22,0.089,1.373,0.169,1.373],[0.169,1.373,0.223,1.373,0.2656,0.868,0.356,0.868],[0.356,0.868,0.4085,0.868,0.457,1.047,0.544,1.047],[0.544,1.047,0.5976,1.047,0.637,0.984,0.731,0.984],[0.731,0.984,0.794,0.984,0.829,1.006,0.919,1.006],[0.919,1.006,0.953,1.006,1,1,1,1]],\"85\":[[0,0,1.2815,0,0.6806,1.4487,1,1]]},null,false,false,-1,true,true,false,true);f[c]=a.API;document.getElementById(e).setAttribute(\"HYP_dn\",\nc);a.z_o(this.body)})();})();\n"
  },
  {
    "path": "packages/client-app/static/buttons.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\nbutton, html input[type=\"button\"] {\n  cursor: default;\n  &:hover, &:active {\n    cursor: default;\n  }\n}\n\nbody.platform-win32 {\n  .btn {\n    .windows-btn-bg;\n    .windows-btn-border;\n\n    &.btn-action {\n      border: 2px solid darken(@btn-action-bg-color, 10%);\n      &:hover {\n        background: @btn-action-bg-color;\n      }\n    }\n\n    &.btn-emphasis {\n      background: @btn-emphasis-bg-color;\n      border: 0;\n\n      &:hover {\n        border-radius: 0;\n        background: darken(@btn-emphasis-bg-color, 10%);\n      }\n    }\n\n    &.btn-emphasis:active {\n      background: @btn-emphasis-bg-color;\n      box-shadow: 0 0 0;\n    }\n\n    &.btn-danger, .btn-destructive {\n      background: @btn-danger-bg-color;\n      border: 0;\n      &:hover {\n        border-radius: 0;\n        background: darken(@btn-danger-bg-color, 10%);\n      }\n    }\n  }\n}\n\n.btn {\n  padding: 0 0.8em;\n  border-radius: @border-radius-base;\n  border: 0;\n  cursor: default;\n  display:inline-block;\n  color: @btn-default-text-color;\n  background: @background-primary;\n\n  img.content-mask { background-color: @btn-default-text-color; }\n\n  // Use 4 box shadows to create a 0.5px hairline around the button, and another\n  // for the actual shadow. Pending https://code.google.com/p/chromium/issues/detail?id=236371\n  // Yes, 1px border looks really bad on retina.\n  box-shadow: 0 0.5px 0 rgba(0,0,0,0.15), 0 -0.5px 0 rgba(0,0,0,0.15), 0.5px 0 0 rgba(0,0,0,0.15), -0.5px 0 0 rgba(0,0,0,0.15), 0 0.5px 1px rgba(0, 0, 0, 0.15);\n\n  height: 1.9em;\n  line-height: 1.9em;\n\n  .text {\n    margin-left: 6px;\n  }\n\n  &:active {\n    cursor: default;\n    background: darken(@btn-default-bg-color, 9%);\n  }\n  &:focus {\n    outline: none\n  }\n\n  font-size: @font-size-small;\n\n  &.btn-small {\n    font-size: @font-size-smaller;\n  }\n  &.btn-large {\n    font-size: @font-size-base;\n    padding: 0 1.3em;\n    line-height: 2.2em;\n    height: 2.3em;\n  }\n  &.btn-larger {\n    font-size: @font-size-large;\n    padding: 0 1.6em;\n  }\n\n  &.btn-action {\n    color:      @btn-action-text-color;\n    background: @btn-action-bg-color;\n    img.content-mask { background-color:@btn-action-text-color; }\n  }\n\n  &.btn-disabled {\n    color:      fadeout(@btn-default-text-color, 40%);\n    background: fadeout(@btn-default-bg-color, 15%);\n    &:active {\n      background: fadeout(@btn-default-bg-color, 15%);\n    }\n  }\n\n  &.btn-emphasis {\n    position: relative;\n    color: @btn-emphasis-text-color;\n    font-weight: @font-weight-medium;\n\n    img.content-mask { background-color:@btn-emphasis-text-color; }\n\n    background: linear-gradient(to bottom, #6bb1f9 0%, #0a80ff 100%);\n    box-shadow: none;\n    border: 1px solid darken(@btn-emphasis-bg-color, 7%);\n\n    &.btn-disabled {\n      opacity: 0.4;\n    }\n\n    &:before {\n      content: ' ';\n      width: calc(~\"100% + 2px\");\n      height: calc(~\"100% + 2px\");\n      border-radius: @border-radius-base + 1;\n      top: -1px;\n      left: -1px;\n      position: absolute;\n      z-index: -1;\n      background: linear-gradient(to bottom, #4ca2f9 0%, #015cff 100%);\n    }\n    &:active {\n      background: -webkit-gradient(linear, left top, left bottom, from(darken(@btn-emphasis-bg-color,10%)), to(darken(@btn-emphasis-bg-color, 4%)));\n    }\n  }\n\n  &.btn-danger, .btn-destructive {\n    color:      @btn-danger-text-color;\n    background: @btn-danger-bg-color;\n    img.content-mask { background-color:@btn-danger-text-color; }\n  }\n}\n\n.btn-toolbar {\n  .btn-gradient;\n\n  padding: 0 13px;\n  &.narrow {\n    padding: 0 9px;\n  }\n\n}\n\n.btn-gradient {\n  background: -webkit-gradient(linear, left top, left bottom, from(@btn-default-bg-color), to(darken(@btn-default-bg-color, 4.8%)));\n}\n.btn-gradient:active {\n  background: -webkit-gradient(linear, left top, left bottom, from(darken(@btn-default-bg-color, 9%)), to(darken(@btn-default-bg-color, 13.5%)));\n}\n\n.btn-icon {\n  background: transparent;\n  border: 0;\n  box-shadow: none;\n  color: @text-color-subtle;\n  img.content-mask { background-color:@text-color-subtle; }\n  margin-right: 10px;\n  outline: none !important;\n  font-size: 20px;\n\n  &:last-child {\n    margin-right: 0;\n  }\n\n  &.inverse {\n    color: @text-color-inverse;\n    img.content-mask { background-color:@text-color-inverse; }\n\n    &:hover {\n      color: white;\n    }\n\n    &:active {\n      color: @text-color-inverse;\n      img.content-mask { background-color:@text-color-inverse; }\n    }\n  }\n\n  &:hover {\n    cursor: default;\n    color: @text-color-link;\n    img.content-mask { background-color:@text-color-link; }\n    box-shadow: none;\n  }\n  &:active {\n    color: @text-color-link-active;\n    img.content-mask { background-color:@text-color-link-active; }\n    box-shadow: none;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/attachment-items.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n@file-icon-color: #c7c7c7;\n@attachment-border-color: rgba(0, 0, 0, 0.09);\n\n\n.nylas-attachment-item {\n  cursor: default;\n  display: inline-block;\n  position: relative;\n  font-size: @font-size-small;\n  margin: 0 0 @spacing-standard @spacing-standard;\n  width: calc(~\"50% - 23px\");\n  min-width: 320px;\n\n  &.file-attachment-item:focus {\n    .file-info-wrap,.file-action-icon,.file-thumbnail-preview {\n      border-color: lighten(@component-active-color, 10%);\n    }\n    .file-action-icon {\n      border-left-color: @attachment-border-color;\n    }\n  }\n\n  .file-thumbnail-preview {\n    width: 100%;\n    max-height: 64px;\n    text-align: left;\n    overflow: hidden;\n    border: solid 1px @attachment-border-color;\n    border-bottom: none;\n    border-top-left-radius: 8px;\n    border-top-right-radius: 8px;\n  }\n\n  &.file-attachment-item.has-preview {\n    &:focus {\n      .file-info-wrap,.file-action-icon {\n        border-top-color: @attachment-border-color;\n      }\n    }\n    .file-info-wrap {\n      border-top-left-radius: 0;\n    }\n    .file-action-icon {\n      border-top-right-radius: 0;\n    }\n  }\n\n  .inner {\n    border-radius: 2px;\n    color: @text-color;\n    background: @background-off-primary;\n    height: 37px;\n  }\n\n  .file-info-wrap {\n    display: flex;\n    align-items: center;\n    flex-grow: 2;\n    min-width: 85%;\n    padding: 10px;\n    height: 105%;\n    border: solid 1px @attachment-border-color;\n    border-top-left-radius: 8px;\n    border-bottom-left-radius: 8px;\n    border-right: none;\n\n    .file-icon {\n      margin-right: 10px;\n      flex-shrink:0;\n    }\n    .file-name {\n      font-weight: @font-weight-medium;\n      flex: 1;\n      overflow: hidden;\n      text-overflow: ellipsis;\n      white-space: nowrap;\n      max-width: 235px;\n    }\n    .file-size {\n      @file-size-color: #b8b8b8;\n\n      margin-left: auto;\n      margin-right: @spacing-three-quarters;\n      color: @file-size-color;\n    }\n    img.quicklook-icon {\n      background-color: @file-icon-color;\n\n      &:hover {\n        background-color: darken(@file-icon-color, 20%);\n      }\n      &:active {\n        background-color: darken(@file-icon-color, 40%);\n      }\n    }\n  }\n\n  .file-action-icon {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    flex-shrink: 0;\n    flex-grow: 0;\n    flex-basis: 38px;\n    margin-left: auto;\n    padding: 10px;\n    height: 105%;\n    width: 38px;\n    border: solid 1px @attachment-border-color;\n    border-top-right-radius: 8px;\n    border-bottom-right-radius: 8px;\n    // box-shadow: inset 0 0 0 1px @attachment-border-color;\n\n    img {\n      background-color: @file-icon-color;\n    }\n\n    &:hover img {\n      background-color: darken(@file-icon-color, 20%);\n    }\n    &:active {\n      background-color: darken(@btn-default-bg-color, 5%);\n    }\n  }\n\n  .progress-bar-wrap {\n    display: none;\n\n    &.state-downloading, &.state-started, &.state-progress {\n      display: block;\n    }\n\n    &.state-completed, &.state-success {\n      display: block;\n      .progress-foreground { background: @background-color-success; }\n    }\n\n    &.state-aborted, &.state-failed {\n      display: block;\n      .progress-foreground { background: @background-color-error; }\n    }\n\n    .progress-foreground {\n      position: absolute;\n      left: 4px;\n      bottom: 0;\n      height: 2px;\n      width: 0; // Changed by React\n      z-index: 3;\n      display: block;\n      background: @blue-light;\n      border-bottom-left-radius:4px;\n      transition: width .3s linear;\n\n    }\n    .progress-background {\n      position: absolute;\n      left: 4px;\n      bottom: 0;\n      height: 2px;\n      width: 97.5%;\n      z-index: 2;\n      display: block;\n      background: @background-color-pending;\n      border-bottom-left-radius:4px;\n      border-bottom-right-radius:4px;\n    }\n  }\n}\n\nbody.platform-win32 {\n  .nylas-attachment-item {\n    .inner {\n      border-radius: 0;\n    }\n  }\n}\n\n.nylas-attachment-item.image-attachment-item {\n  position: relative;\n  text-align: center;\n  display:inline-block;\n  vertical-align: top;\n  margin-bottom: @spacing-standard;\n  margin-right: @spacing-standard;\n  margin-left: @spacing-standard;\n  width: initial;\n  max-width: calc(~\"100% - 30px\");\n\n  .progress-foreground,\n  .progress-foreground {\n    bottom: -2px;\n  }\n  .progress-background {\n    bottom: -2px;\n  }\n\n  .file-action-icon, .file-name-container, .file-name {\n    display: none;\n  }\n  .file-action-icon:active {\n    background: none;\n    -webkit-filter: brightness(95%);\n  }\n\n  &:hover {\n    .file-action-icon, .file-name-container, .file-name {\n      display: block;\n    }\n  }\n\n  .file-action-icon {\n    position: absolute;\n    z-index: 2;\n    right: -8px;\n    top: -8px;\n    width: 26px;\n    border-radius: 0 0 0 3px;\n    border: none;\n    img {\n      background: none;\n    }\n  }\n\n  .file-preview {\n    position: relative;\n    z-index: 1;\n    overflow: hidden;\n\n    .file-name-container {\n      cursor: default;\n      position: absolute;\n      bottom: 0;\n      top: 0;\n      z-index: 2;\n      width: 100%;\n      height:100%;\n      min-height:300px;\n      background: linear-gradient(to top, rgba(0,0,0,0.75) 0%,rgba(0,0,0,0) 23%);\n      vertical-align:bottom;\n\n      // Important! file-name-container is on top of the image and prevents you from dragging it.\n      pointer-events: none;\n\n      .file-name {\n        color: @white;\n        left: @spacing-standard;\n        right: @spacing-standard;\n        bottom: @spacing-standard;\n        text-align:left;\n        position: absolute;\n        z-index: 3;\n      }\n    }\n\n    img {\n      position: relative;\n      z-index: 1;\n      max-width: 100%;\n      background: url(../static/images/attachments/transparency-background.png) top left repeat;\n      background-size: 8px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/billing-modal.less",
    "content": "@import \"ui-variables\";\n\n.billing-modal {\n  width: 100%;\n  height: 100%;\n  top: 0;\n  position: absolute;\n  display: flex;\n  cursor: default;\n\n  .webview-cover {\n    background: linear-gradient( -10deg, #fff, #f5f6fd );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/button-dropdown.less",
    "content": "@import \"../buttons\";\n\n.button-dropdown {\n  position: relative;\n  display: inline-block;\n\n  &.open {\n    .secondary-items {\n      visibility: inherit;\n    }\n  }\n\n  &.open.open-up {\n    .secondary-items {\n      border-radius: @border-radius-base @border-radius-base 0 @border-radius-base;\n      box-shadow: 0 0.5px 0 @standard-shadow-color, 0 -0.5px 0 @standard-shadow-color, 0.5px 0 0 @standard-shadow-color, -0.5px 0 0 @standard-shadow-color, 0 -3px 12px @standard-shadow-color;\n      top: -100%;\n      transform:translate(0, -6px);\n    }\n    .secondary-picker {\n      border-top-right-radius: 0;\n    }\n  }\n  &.open.open-down {\n    .secondary-items {\n      border-radius: @border-radius-base 0 @border-radius-base @border-radius-base;\n      box-shadow: 0 0.5px 0 @standard-shadow-color, 0 -0.5px 0 @standard-shadow-color, 0.5px 0 0 @standard-shadow-color, -0.5px 0 0 @standard-shadow-color, 0 5px 12px @standard-shadow-color;\n      transform:translate(0, 1.5px);\n    }\n    .secondary-picker {\n      border-bottom-right-radius: 0;\n    }\n  }\n\n\n  &.btn-emphasis {\n    .primary-item,\n    .secondary-picker,\n    .only-item {\n      .btn.btn-emphasis();\n    }\n    .primary-item {\n      border-right:0;\n    }\n  }\n\n  .primary-item,\n  .only-item {\n    .btn();\n    cursor: default;\n    color: @btn-default-text-color;\n  }\n  .primary-item {\n    border-radius: @border-radius-base 0 0 @border-radius-base;\n  }\n\n  .secondary-picker {\n    .btn();\n    box-shadow: @standard-shadow-color 0 0.5px 0, @standard-shadow-color 0 -0.5px 0, @standard-shadow-color 0.5px 0 0, @standard-shadow-color 0 0.5px 1px;\n    border-radius: 0 @border-radius-base @border-radius-base 0;\n    border-left: 0;\n    padding: 0 6px;\n  }\n  .secondary-items {\n    &:hover {\n      cursor: default;\n    }\n    &.left {\n      width:auto;\n      left:-1px;\n      right:auto;\n      white-space:nowrap;\n    }\n    z-index: 2;\n    background-color: @background-secondary;\n    position: absolute;\n    right: 0;\n    white-space:nowrap;\n    visibility:hidden;\n\n    .menu {\n      .footer-container,\n      .header-container {\n        display:none;\n      }\n      .content-container {\n        background: transparent;\n        margin-top:0;\n      }\n      .item {\n        font-size: 13px;\n        padding: 4px 11px;\n        &:first-child {\n          padding-top: 6px;\n        }\n        &:last-child {\n          padding-bottom: 6px;\n        }\n        img {\n          margin-right:4px;\n          vertical-align: text-bottom;\n        }\n        &.selected {\n          background-color: inherit;\n          color:@text-color;\n        }\n      }\n      .item:first-child {\n        border-top-left-radius: @border-radius-base;\n      }\n      .item:last-child {\n        border-bottom-left-radius: @border-radius-base;\n        border-bottom-right-radius: @border-radius-base;\n      }\n      .item:hover {\n        background-color: darken(@background-secondary, 3%);\n        color:inherit;\n      }\n    }\n  }\n  img {\n    background-color: @text-color;\n  }\n}\n\nbody.platform-win32 {\n  .button-dropdown {\n    .primary-item {\n      .windows-btn-bg;\n      .windows-btn-border;\n      border-radius: 0;\n      position: relative;\n      left: -1px;\n    }\n    .secondary-picker {\n      .windows-btn-bg;\n      .windows-btn-border;\n      position: relative;\n      right: -1px;\n    }\n    .secondary-picker,\n    .secondary-items {\n      border-radius: 0;\n    }\n    &.btn-emphasis {\n      .primary-item,\n      .secondary-picker,\n      .only-item {\n        border: 0;\n        background: @btn-emphasis-bg-color;\n        &:hover {\n          border-radius: 0;\n          background: darken(@btn-emphasis-bg-color, 10%);\n        }\n        &:active {\n          background: @btn-emphasis-bg-color;\n        }\n      }\n      .primary-item {\n        box-shadow: -1px 0 0 2px @btn-emphasis-bg-color;\n        &:hover { box-shadow: -1px 0 0 2px darken(@btn-emphasis-bg-color, 10%); }\n      }\n      .secondary-picker {\n        box-shadow: 1px 0 0 2px @btn-emphasis-bg-color;\n        &:hover { box-shadow: 1px 0 0 2px darken(@btn-emphasis-bg-color, 10%); }\n      }\n      .only-item {\n        box-shadow: 0 0 0 2px @btn-emphasis-bg-color;\n        &:hover { box-shadow: 0 0 0 2px darken(@btn-emphasis-bg-color, 10%); }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/code-snippet.less",
    "content": ".error-details {\n  height: 550px;\n  width: 750px;\n  margin: 5px 25px;\n  display: flex;\n  flex-direction: column;\n\n  textarea {\n    resize: none;\n    width: 100%;\n    height: 100%;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/contenteditable.less",
    "content": "@import \"ui-variables\";\n\n.contenteditable-container {\n  flex: 1;\n  display: flex;\n  line-height: 1.4;\n  position: relative;\n  color: @text-color;\n  font-size: @font-size;\n  padding-bottom: 15px;\n\n  div[contenteditable], .contenteditable {\n    flex: 1;\n\n    // Subtle: overflow is required to make the composer wrap\n    // instead of scroll when you type a very long \"word\" / url.\n    overflow-x: hidden;\n    a:hover {\n      cursor: text;\n    }\n  }\n\n  img.emoji {\n    -webkit-user-drag: none;\n  }\n  spelling.misspelled {\n    background: linear-gradient(45deg, transparent, transparent 49%, red 49%, transparent 51%);\n    background-size: 2px 2px;\n    background-position: bottom;\n    background-repeat-y: no-repeat;\n  }\n\n  .floating-toolbar {\n    z-index: 10;\n    position: absolute;\n\n    background: #fff;\n    box-shadow: 0 10px 20px rgba(0,0,0,0.19), inset 0 0 1px rgba(0,0,0,0.5);\n    border-radius: @border-radius-large;\n    color: @text-color;\n\n    transition-duration: .15s;\n    transition-property: opacity, margin;\n    margin-top: 0;\n\n    &.toolbar-visible {\n      opacity: 1;\n      visibility: visible;\n      margin-top: 0;\n    }\n\n    .toolbar-pointer-container {\n      position: absolute;\n      width: 23px;\n      height: 10px;\n\n      div {\n        -webkit-mask-image: url('images/tooltip/tooltip-bg-pointer@2x.png');\n        background-color: #fff;\n        position: absolute;\n        zoom: 0.5;\n        width: 45px;\n        height: 20px;\n        &.shadow {\n          -webkit-mask-image: url('images/tooltip/tooltip-bg-pointer-shadow@2x.png');\n          background-color: fade(@black, 22%);\n          transform: translateY(0.5px);\n        }\n      }\n    }\n\n    &.above {\n      .toolbar-pointer-container {\n        transform: translateX(-11px) rotate(0deg);\n        bottom: -9px;\n      }\n    }\n    &.below {\n      .toolbar-pointer-container {\n        transform: translateX(-11px) rotate(180deg);\n        top: -9px;\n      }\n    }\n\n    .floating-toolbar-input {\n      border: 0;\n      display: inline;\n      color: @text-color;\n      &:focus {\n        border: 0;\n        box-shadow: 0 0 0;\n      }\n    }\n\n    @padding: 0.5em;\n    .btn {\n      background: transparent;\n      font-size: 16px;\n      height: auto;\n      border-radius: 0;\n      padding: @padding*0.25 @padding;\n      margin: 0;\n      color: @text-color;\n      box-shadow: none;\n      &:first-child {\n        padding-left: 1.5*@padding;\n      }\n      &:last-child {\n        padding-right: 1.5*@padding;\n      }\n      &:hover, &:active {\n        color: lighten(@text-color-link, 10%);\n        background: transparent;\n      }\n    }\n\n    .preview-btn-icon {\n      position: relative;\n      top: 1px;\n      padding: 0 @padding*0.25 0 @padding*1.5;\n    }\n\n    button.btn.toolbar-btn {\n      @padding-top: 4px;\n      @padding-left: 8px;\n\n      width: 12.5px + 2*@padding-left;\n      height: 12.5px + 2*@padding-top;\n      margin: 7.5px 0;\n      box-shadow: none;\n      border: 0;\n      border-right: 1px solid @border-color-divider;\n      &:last-child { border-right: 0 }\n\n      background: no-repeat;\n      background-size: 12.5px 12.5px;\n      background-position: @padding-left @padding-top;\n      &.btn-bold { background-image: url(\"images/composer/tooltip-bold-black@2x.png\") }\n      &.btn-italic { background-image: url(\"images/composer/tooltip-italic-black@2x.png\") }\n      &.btn-underline { background-image: url(\"images/composer/tooltip-underline-black@2x.png\") }\n      &.btn-link { background-image: url(\"images/composer/tooltip-link-black@2x.png\") }\n      &:hover {\n        cursor: pointer;\n        background: no-repeat;\n        background-size: 12.5px 12.5px;\n        background-position: @padding-left @padding-top;\n        &.btn-bold { background-image: url(\"images/composer/tooltip-bold-blue@2x.png\") }\n        &.btn-italic { background-image: url(\"images/composer/tooltip-italic-blue@2x.png\") }\n        &.btn-underline { background-image: url(\"images/composer/tooltip-underline-blue@2x.png\") }\n        &.btn-link { background-image: url(\"images/composer/tooltip-link-blue@2x.png\") }\n      }\n    }\n  }\n}\n\nbody.platform-win32 {\n  .contenteditable-container {\n    .floating-toolbar {\n      border-radius: 0;\n      input, input:focus {\n        box-shadow: none;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/date-input.less",
    "content": "@import \"ui-variables\";\n\n.nylas-date-input {\n  text-align: center;\n\n  .date-interpretation {\n    color: @text-color-subtle;\n    font-size: @font-size-small;\n    opacity: 0.6;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/date-picker-popover.less",
    "content": "@import \"ui-variables\";\n\n.date-picker-popover {\n  .menu .item {\n    .time {\n      display: none;\n      float: right;\n      padding-right: @padding-base-horizontal;\n    }\n    &.selected,\n    &:hover {\n      .time {\n        display: inline-block;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/date-picker.less",
    "content": "@import 'ui-variables';\n\n.date-picker {\n  display: inline-block;\n  position: relative;\n  .day-text {\n    &:hover {\n      color: @text-color-link-hover;\n    }\n    &.focused {\n      color: @text-color-link-hover;\n    }\n    color: @text-color-link;\n  }\n  .mini-month-view-wrap {\n    position: absolute;\n    top: 20px;\n    left: 0;\n    width: 225px;\n    height: 225px;\n    z-index: 999;\n  }\n}\n\n.fixed-popover-container {\n  .date-picker-popover {\n    .menu {\n      width: 250px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/disclosure-triangle.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.disclosure-triangle {\n  flex-shrink: 0;\n  padding: 7px;\n  padding-top: 10px;\n  width: 20px;\n  visibility: hidden;\n\n  div {\n    transform:rotate(90deg);\n    transition: transform 90ms linear;\n    border-top: 4px solid transparent;\n    border-bottom: 4px solid transparent;\n    border-left: 7px solid @text-color-very-subtle;\n  }\n\n  &.visible {\n    visibility: visible;\n  }\n  &.collapsed {\n    div {\n      transform:rotate(0deg);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/editable-list.less",
    "content": "@import \"ui-variables\";\n\n.nylas-editable-list {\n\n  .items-wrapper .selected.editing,\n  &:focus .items-wrapper .selected {\n    color: @component-active-bg;\n    background-color: @component-active-color;\n  }\n\n  .items-wrapper {\n    display: flex;\n    flex-direction: column;\n    border: 1px solid @border-color-secondary;\n    background-color: @background-primary;\n    height: 90px;\n    font-size: 0.9em;\n\n    .selected {\n      background-color: fade(desaturate(@component-active-color, 100%), 70%);\n      color: @component-active-bg;\n    }\n\n    .insertion-point {\n      display:block;\n      width:100%;\n      height: 0;\n      position: relative;\n      pointer-events: none;\n\n      div {\n        height:2px;\n        width: 100%;\n        position: absolute;\n        top: -1px;\n        background-color: @component-active-color;\n        box-shadow: 0 0 1px fade(@component-active-color, 50%);\n      }\n    }\n\n    .edit-icon {\n      display: none;\n      cursor: pointer;\n    }\n\n    .editable-item {\n      padding: (@padding-small-vertical - 1) @padding-small-horizontal;\n      cursor: default;\n      border-bottom: 1px solid @border-color-divider;\n      flex-shrink: 0;\n\n      &.selected.with-edit-icon {\n        display: flex;\n        align-items: center;\n        padding-right: 20px;\n\n        img.edit-icon {\n          display: inline;\n          background-color: @component-active-bg;\n          margin-left: auto;\n        }\n      }\n\n      &>input {\n        border: none;\n        padding: 0;\n        color: @component-active-bg;\n        background: transparent;\n        font-size: inherit;\n        line-height: 1.5;\n        border-radius: 0;\n        &:focus {\n          box-shadow: none;\n        }\n      }\n      ::-webkit-input-placeholder {\n        color: @text-color-inverse-very-subtle;\n      }\n    }\n\n    .create-item-input {\n      &>input {\n        padding: (@padding-small-vertical - 1) @padding-small-horizontal;\n        border: none;\n        border-bottom: 1px solid @border-color-divider;\n        font-size: inherit;\n        border-radius: 0;\n        &:focus {\n          box-shadow: none;\n        }\n      }\n      ::-webkit-input-placeholder {\n        font-style: italic;\n      }\n    }\n  }\n\n  .buttons-wrapper {\n    display: flex;\n    border: 1px solid @border-color-secondary;\n    border-top: none;\n    background-color: @background-secondary;\n\n    .btn-editable-list {\n      display: flex;\n      justify-content: center;\n      height: 25px;\n      width: 25px;\n      line-height: 25px;\n      border-right: 1px solid @border-color-secondary;\n      font-size: 1em;\n      cursor: default;\n      color: @text-color-subtle;\n\n      &.btn-disabled {\n        background-color: transparent;\n        opacity: 0.4;\n      }\n      &:active {\n        cursor: default;\n        background-color: darken(@btn-default-bg-color, 9%);\n        box-shadow: inset 0 1px 0.5px rgba(0, 0, 0, 0.21);\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/editable-table.less",
    "content": "@import \"ui-variables\";\n\n.editable-table-container {\n  display: flex;\n  align-items: flex-start;\n  justify-content: flex-start;\n  height: 100%;\n  padding-bottom: 5px;\n\n  .column-actions {\n    display: flex;\n    margin-left: @padding-small-horizontal;\n    margin-top: @padding-base-vertical - 1;\n\n    .btn.btn-small {\n      padding: 0;\n      border-radius: 100%;\n      text-align: center;\n      margin-left: @padding-large-vertical - 2;\n      margin-top: 1px;\n      width: 24px;\n      line-height: 22px;\n      height: 24px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/empty-list-state.less",
    "content": "@import \"ui-variables\";\n\n@duration: 1s;\n\n.empty-state {\n  position:absolute;\n  left: 0;\n  top: 0;\n  right: 0;\n  bottom: 0;\n  overflow:hidden;\n  z-index: 10;\n\n  > div {\n    opacity: 0;\n    transition: opacity @duration ease-in;\n    width: 100%;\n    height: 100%;\n  }\n\n  .perspective-empty-state {\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n\n    img {\n      background-color: @text-color-subtle;\n    }\n    .message {\n      color: @text-color-very-subtle;\n      margin: 1em;\n      margin-bottom: 0;\n      font-size: 2em;\n      font-weight: @font-weight-thin;\n      text-align: center;\n    }\n  }\n\n  .inbox-zero-animation {\n    // The animation inside the iframe already contains a fade effect, so we\n    // don't want to animate opacity for this component\n    opacity: 1;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n\n    .animation-wrapper {\n      text-align: center;\n      transform: scale(0.8);\n      transition: transform 100ms ease-in;\n\n      iframe {\n        width: 600px;\n        height: 500px;\n        border: none;\n      }\n      .message {\n        opacity: 0;\n        transition: opacity @duration ease-in;\n        font-size: 3.5em;\n        color: @text-color-subtle;\n        font-weight: @font-weight-thin;\n      }\n    }\n  }\n\n  &.active {\n    > div {\n      opacity: 1;\n\n      .message {\n        opacity: 1 !important;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/extra.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.quoted-text-control {\n  color: @text-color-very-subtle;\n  display: inline-block;\n  border: 1px solid fade(@text-color-very-subtle, 15%);\n  border-radius: 3px;\n  line-height: 10px;\n  padding: 1px 5px;\n  font-weight: 600;\n  font-size: @font-size-smaller;\n  margin: 5px 0 3px 0;\n\n  &:hover {\n    cursor: pointer;\n    color: @text-color-subtle;\n    border: 1px solid fade(@text-color-subtle, 35%);\n  }\n\n  .dots {\n    display: inline-block;\n    font-size: @font-size-smaller * 0.8;\n    top:-1px;\n    position:relative;\n  }\n}\n\nbody.platform-win32 {\n  .quoted-text-control {\n    .windows-btn-bg;\n    .windows-btn-border;\n    &:hover {\n      color: @text-color-very-subtle;\n      .windows-btn-border;\n    }\n  }\n  .mail-label {\n    border-radius: 0;\n  }\n}\n\n.mail-label {\n  font-size: 0.9em;\n  padding: 1px 8px;\n  margin-right: 6px;\n  flex-shrink: 0;\n  border-radius: 3px;\n  display: inline-block;\n  cursor:default;\n  line-height: 22px;\n  -webkit-user-select: none;\n\n  &:last-child {\n    margin-right: 0;\n  }\n}\n.mail-label.removable {\n  padding-left:12px;\n  padding-right:4px;\n  .inner {\n    position: relative;\n    left:0;\n    transition: left 0.05s linear;\n  }\n  .x {\n    opacity: 0;\n    left:-4px;\n    top:-3px;\n    position: relative;\n    display:inline-block;\n    transition: opacity 0.05s ease-in;\n  }\n}\n.mail-label.removable:hover {\n  .inner {\n    left:-6px;\n  }\n  .x {\n    opacity: 1;\n  }\n}\n\n\n.mail-important-icon {\n  width: 25px;\n  height: 25px;\n  display:inline-block;\n  background-repeat: no-repeat;\n  background-position:center;\n  background-image:url(../static/images/important/Icon-Important-Hover@2x.png);\n  background-size: 16px;\n}\n.mail-important-icon.enabled.active {\n  background-image:url(../static/images/important/Icon-Important-Active@2x.png);\n  background-size: 16px;\n}\n.mail-important-icon.enabled:hover {\n  background-image:url(../static/images/important/Icon-Important-HoverActive@2x.png);\n  background-size: 16px;\n}\n.mail-important-icon.enabled.active:hover {\n  background-image:url(../static/images/important/Icon-Important-Active@2x.png);\n  background-size: 16px;\n  -webkit-filter: brightness(90%);\n}\n"
  },
  {
    "path": "packages/client-app/static/components/feature-used-up-modal.less",
    "content": "@import \"ui-variables\";\n\n.feature-usage-modal {\n  position: absolute;\n  top: 0;\n  width: 100%;\n  height: 100%;\n\n  .feature-header {\n    text-align: center;\n    padding-top: 32px;\n    padding-bottom: 30px;\n    color: @white;\n    border-radius: 5px 5px 0 0;\n  }\n  .header-text {\n    color: @white;\n    margin-top: 24px;\n    margin-bottom: 11px;\n  }\n  .recharge-text {\n    margin: 0;\n    opacity: 0.67;\n  }\n  .feature-cta {\n    text-align: center;\n    padding-bottom: 26px;\n    h2 {\n      margin-top: 26px;\n      margin-bottom: 26px;\n    }\n  }\n  .pro-description {\n    width: 275px;\n    margin: 0 auto;\n    padding-bottom: 28px;\n    border-top: 1px solid rgba(0,0,0,0.1);\n    border-bottom: 1px solid rgba(0,0,0,0.1);\n\n    ul {\n      color: rgba(51, 51, 51, 0.67);\n      text-align: left;\n      list-style: none;\n      line-height: 24px;\n      margin-bottom: 0;\n    }\n    li {\n      &:before {\n        content: '✓';\n        margin-right: 11px;\n      }\n    }\n    h3 {\n      margin-top: 23px;\n      margin-bottom: 18px;\n    }\n    p {\n      color: rgba(51, 51, 51, 0.67);\n      margin: 0;\n      text-align: left;\n      padding-left: 62px;\n      line-height: 24px;\n    }\n  }\n\n  .btn {\n    margin-top: 21px;\n    padding: 0 33px 25px 33px;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/fixed-popover.less",
    "content": "@import \"ui-variables\";\n@header-color: #afafaf;\n\n// TODO\n// Most of these styles are duplicated from the original popover.less\n// Eventually, we will get rid of the original popover and switch to this\n// implementation\n.fixed-popover-blur-trap {\n  position: absolute;\n  z-index: 40;\n}\n\n.fixed-popover-container {\n  visibility: hidden;\n  opacity: 0;\n  position: absolute;\n  z-index: 40;\n\n  .fixed-popover {\n    position: absolute;\n    background-color: @background-secondary;\n    border-radius: @border-radius-base;\n    box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 4px 7px rgba(0,0,0,0.15);\n\n    .menu {\n      z-index:1;\n      position: relative;\n\n      .content-container {\n        background: none;\n      }\n      .header-container {\n        border-top-left-radius: @border-radius-base;\n        border-top-right-radius: @border-radius-base;\n        background: none;\n        color: @header-color;\n        font-weight: bold;\n        border-bottom: none;\n        overflow: hidden;\n        padding: @padding-base-vertical * 1.5  @padding-base-horizontal;\n      }\n      .footer-container {\n        border-bottom-left-radius: @border-radius-base;\n        border-bottom-right-radius: @border-radius-base;\n        background: none;\n\n        .item:last-child:hover {\n            border-bottom-left-radius: @border-radius-base;\n            border-bottom-right-radius: @border-radius-base;\n        }\n      }\n    }\n\n    .section {\n      padding: @padding-base-vertical * 1.5  @padding-base-horizontal;\n    }\n\n    .divider {\n      border-top: 1px solid @border-color-divider;\n    }\n\n    input[type=text] {\n      border: 1px solid darken(@background-secondary, 10%);\n      border-radius: 3px;\n      background-color: @background-primary;\n      box-shadow: inset 0 1px 0 rgba(0,0,0,0.05), 0 1px 0 rgba(0,0,0,0.05);\n      color: @text-color;\n\n      &.search {\n        padding-left: 0;\n        background-repeat: no-repeat;\n        background-image: url(\"../static/images/search/searchloupe@2x.png\");\n        background-size: 15px 15px;\n        background-position: 7px 4px;\n        text-indent: 31px;\n      }\n    }\n  }\n\n  .fixed-popover-pointer,.fixed-popover-pointer.shadow {\n    position: absolute;\n    height: 20px;\n    width: 45px;\n  }\n  .fixed-popover-pointer {\n    -webkit-mask-image: url('images/tooltip/tooltip-bg-pointer@2x.png');\n    background-color: @background-secondary;\n  }\n  .fixed-popover-pointer.shadow {\n    -webkit-mask-image: url('images/tooltip/tooltip-bg-pointer-shadow@2x.png');\n    background-color: fade(@black, 22%);\n  }\n\n  &.popout  {\n    visibility: visible;\n    opacity: 1;\n    animation: popout-animation 240ms cubic-bezier(.56,.25,.25,1.56);\n  }\n  @keyframes popout-animation {\n    from {\n      visibility: hidden;\n      opacity: 0;\n      transform: scale(0.87);\n    }\n    to {\n      visibility: visible;\n      opacity: 1;\n      transform: scale(1);\n    }\n  }\n}\n\nbody.platform-win32 {\n  .fixed-popover {\n    border-radius: 0;\n\n    .menu {\n      .header-container,\n      .footer-container {\n        border-radius: 0;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/generated-form.less",
    "content": "@import \"ui-variables\";\n\n@spacing-form: 22px;\n@form-margin: 15px;\n\nbody.platform-win32 {\n  .generated-form {\n    .tokenizing-field {\n      border-radius: 0;\n      border: 0;\n      border-radius: 0;\n      box-shadow: 0 0 0 2px @input-border-color;\n\n      &.focused {\n        border: 0;\n        border-radius: 0;\n        box-shadow: 0 0 0 2px darken(@input-border-color, 20%);\n      }\n    }\n  }\n}\n\nbody.is-blurred, body.platform-win32.is-blurred {\n  .generated-form {\n    .tokenizing-field {\n      &.focused {\n        border: 1px solid @input-border-color;\n        box-shadow: none;\n      }\n    }\n    .form-item .input-area select:focus {\n      box-shadow: none;\n    }\n    .btn:focus {\n      box-shadow: none;\n    }\n  }\n}\n\n.generated-form {\n  position: relative;\n\n  fieldset {\n    legend {\n      color: fade(@black, 40%);\n      font-size: 1.4em;\n    }\n    margin: 0;\n    padding: 22px @spacing-form 22px @spacing-form;\n    border: 0;\n    border-top: 1px solid rgba(0,0,0,0.08);\n    border-bottom: 1px solid rgba(0,0,0,0.03);\n    &:first-child {\n      border-top: 0;\n    }\n  }\n\n  .prefilled-message {\n    text-align: center;\n    padding: @padding-base-vertical @padding-base-horizontal;\n    background: @background-secondary;\n    margin: 20px auto 0;\n    width: 340px;\n    border-radius: @border-radius-large;\n\n    .highlighted {\n      color: @color-success;\n    }\n  }\n\n  .tokenizing-field {\n    border: 1px solid @input-border-color;\n    border-radius: @border-radius-base;\n\n    &.focused {\n      border: 1px solid @accent-primary;\n      box-shadow: 0 0 1.5px @accent-primary;\n    }\n  }\n\n  .form-item {\n    display: flex;\n    margin-top: @form-margin;\n    &:first-child { margin-top: 0; }\n    flex-direction: column;\n\n    &.invalid {\n      .label-area {\n        color: @color-error;\n      }\n    }\n\n    .label-area {\n    }\n\n    .required {\n      color: @color-danger;\n    }\n\n    &.prefilled .input-area input {\n      border: 1px solid @color-success;\n    }\n\n    .input-area {\n      position: relative;\n      margin-top: 0.3em;\n\n      select {\n        margin-top: 2px;\n        &:focus {\n          border: 1px solid @accent-primary;\n          box-shadow: 0 0 1.5px @accent-primary;\n        }\n      }\n    }\n\n    .form-error {\n      color: @color-error;\n      font-size: @font-size-smaller;\n      margin-top: 0.3em;\n    }\n\n    input, select[multiple], textarea {\n      padding: 5px 8px;\n    }\n    .tokenizing-field-input {\n      overflow: hidden;\n      padding: 0 5px 5px 5px;\n      margin-left: 0;\n      .placeholder {\n        font-size: 14px;\n      }\n\n      .token {\n        padding: 0 20px 0 28px;\n        margin: 5px 5px 0 0;\n\n        &.selected {\n          border: 0;\n        }\n      }\n\n      input {\n        margin: 5px 0 0 0;\n        padding: 0;\n        line-height: 26px;\n      }\n    }\n\n    textarea {\n      width: 100%;\n      border: 1px solid @input-border-color;\n    }\n  }\n\n  .menu .item {\n    padding-left: 8px;\n    padding-right: 8px;\n  }\n\n  .fieldset-form-items {\n    .row {\n      position: relative;\n      display: flex;\n      flex-direction: row;\n      margin-top: @form-margin;\n    }\n    .column {\n      flex: 1;\n      &:last-child {\n        margin-right: 0;\n      }\n    }\n    .column-spacer {\n      width: 22px;\n    }\n  }\n\n  .form-footer {\n    background: @background-off-primary;\n    border-top: 1px solid darken(@background-off-primary, 7%);\n    padding: 9px 22px;\n  }\n\n  .btn {\n    &:focus {\n      box-shadow: 0 0 6px @btn-emphasis-bg-color;\n    }\n  }\n\n  .form-header-error {\n    background: @color-error;\n    color: @text-color-inverse;\n    text-align: center;\n    font-weight: @font-weight-semi-bold;\n    margin-bottom: 5px;\n    padding: 0.5em;\n  }\n\n  .last-fieldset {\n    .content-container {\n      position: relative;\n    }\n  }\n\n  /* NOTE: We intentionally don't use the HTML :valid :invalid CSS pseudo\n   * selectors since we can't set them declaratively with React.\n  input, textarea {\n    &:valid { }\n    &:invalid { }\n  }\n  */\n}\n"
  },
  {
    "path": "packages/client-app/static/components/key-commands-region.less",
    "content": ".key-commands-region {\n  position: relative;\n  height: 100%;\n  width: 100%;\n}\n"
  },
  {
    "path": "packages/client-app/static/components/list-tabular.less",
    "content": "@import \"ui-variables\";\n\n.selection-bar-absolute-enter {\n  opacity: 0;\n  .inner {\n    top: -100%;\n  }\n}\n\n.selection-bar-absolute-enter.selection-bar-absolute-enter-active {\n  opacity: 1;\n  .inner {\n    top:0;\n  }\n}\n\n.selection-bar-absolute-leave {\n  opacity: 1;\n  .inner {\n    top:0;\n  }\n}\n\n.selection-bar-absolute-leave.selection-bar-absolute-leave-active {\n  opacity: 0;\n  .inner {\n    top: -100%;\n  }\n}\n\n.sheet-toolbar .selection-bar {\n  // This item sits in the toolbar and takes up all the remaining\n  // space from the toolbar-spacer divs, but flex-shrink means that\n  // it shrinks before any other element when not enough space is available.\n\n  // This is important because the spacers will prevent items from being clickable,\n  // (webkit-app-region:drag) even if we're covering them up. We need to make them\n  // 0px wide!\n\n  width: 100%;\n  flex-shrink:100;\n  height:38px;\n  z-index: 10000;\n  -webkit-app-region: drag;\n\n  .absolute {\n    position: absolute;\n    left: -1px;\n    right:-1px;\n    top: 0;\n    height:37px;\n    border-left:1px solid @border-color-divider;\n    border-right:1px solid @border-color-divider;\n    background-color: @background-primary;\n    transition: opacity 0.2s ease-in-out;\n\n    .inner {\n      position: absolute;\n      width: 100%;\n      display:flex;\n      -webkit-app-region: no-drag;\n      transition: top 0.2s ease-in-out;\n\n      .centered {\n        flex: 1;\n        cursor:default;\n        text-align: center;\n        color:@text-color-subtle;\n        line-height: 38px;\n        text-overflow: ellipsis;\n        overflow: hidden;\n        white-space: nowrap;\n        -webkit-app-region: drag;\n      }\n    }\n  }\n}\n\n\n.list-container {\n  .list-rows > div {\n    // Note: This allows rows to be animated in and out!\n    transition: top ease-out 120ms;\n  }\n  .list-item {\n    font-size: @font-size-base;\n    color: @text-color;\n    background: @list-bg;\n\n    &:hover {\n      background: darken(@list-bg, 5%);\n    }\n\n    &.selected {\n      background: @list-selected-bg;\n      color: @list-selected-color;\n      border-bottom: 1px solid @list-selected-border;\n    }\n\n    &.next-is-selected {\n      border-bottom: 1px solid @list-selected-border;\n    }\n\n    &.focused {\n      background: @list-focused-bg;\n      color: @list-focused-color;\n      border-bottom: 1px solid @list-focused-border;\n    }\n  }\n}\n\nbody.is-blurred {\n  .list-container {\n    .list-item {\n      &.selected {\n        background: fadeout(desaturate(@list-selected-bg, 100%), 65%);\n        border-bottom: 1px solid fadeout(desaturate(@list-selected-border, 100%), 65%);\n        color: @text-color;\n      }\n\n      &.focused {\n        background: fadeout(desaturate(@list-focused-bg, 100%), 65%);\n        border-bottom: 1px solid fadeout(desaturate(@list-focused-border, 100%), 65%);\n        color: @text-color;\n      }\n    }\n  }\n}\n\n.list-tabular {\n  flex: 1;\n  width: 100%;\n  height: 100%;\n\n  .list-tabular-item {\n    position: relative;\n    width: 100%;\n    display: flex;\n    align-items: center;\n    border-bottom: 1px solid @list-border;\n    border-left:4px solid transparent;\n\n    &:hover {\n      cursor: default;\n    }\n\n    &.keyboard-cursor {\n      border-left:4px solid @list-focused-bg;\n    }\n\n    &.selected {\n      .checkmark .inner {\n        background-color: @accent-primary;\n        background-image: url(images/thread-list/checkbox-checkmark@2x.png);\n        border:none;\n        border-radius: 2px;\n      }\n    }\n\n    &.selected.focused {\n      .checkmark .inner {\n        background-color: @list-bg;\n        background-image: url(images/thread-list/checkbox-checkmark-activerow@2x.png);\n        border:none;\n        border-radius: 2px;\n      }\n    }\n    &.focused {\n      .checkmark .inner {\n        border:1px solid @accent-primary;\n      }\n    }\n\n    .checkmark {\n      padding: 11px;\n      position: absolute;\n      top: 0;\n      left: 0;\n      .inner {\n        width:14px;\n        height:14px;\n        border:1px solid @list-border;\n        border-radius: 2px;\n        background: transparent;\n        background-size: 12px 9px;\n        background-repeat: no-repeat;\n        background-position: center;\n      }\n    }\n  }\n\n  .list-column {\n    // The width is set by React.\n    display: inherit;\n    padding: 0 @padding-base-horizontal;\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    align-items: center;\n\n    &:first-child {\n      padding-left: @padding-base-horizontal - 4;\n    }\n    &:last-child {\n      text-align: right;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/menu.less",
    "content": "@import \"ui-variables\";\n\n.menu {\n  flex: 1;\n  display: flex;\n  flex-direction: column;\n\n  .header-container {\n    flex-shrink: 0; // Don't squish the header! There may be a search box here\n    background-color: @background-secondary;\n    border-bottom: 1px solid @border-color-secondary;\n    padding: 3px;\n    position: relative;\n  }\n\n  .footer-container {\n    position: relative;\n  }\n\n  .content-container {\n    background: @background-secondary;\n    width: 100%;\n    margin-top: -1px;\n    overflow: auto;\n  }\n\n  .item {\n    display: block;\n    padding-left: @padding-base-horizontal;\n    padding-top: @padding-base-vertical;\n    padding-bottom: @padding-base-vertical;\n    cursor: default;\n    color: @text-color;\n    width: 100%;\n    overflow: hidden;\n\n    .primary {\n      max-width: 100%;\n      overflow: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n    }\n    .secondary {\n      max-width: 100%;\n      overflow: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n      padding-left:5px;\n      color:@text-color-very-subtle;\n    }\n  }\n\n  .item.divider {\n    font-weight:@headings-font-weight;\n    color: @border-color-divider;\n    font-size: @font-size-small;\n    text-transform: uppercase;\n    margin-top: 10px;\n    pointer-events: none;\n  }\n\n  .item.checked {\n    background-image:url(./images/menu/checked@2x.png);\n    background-size: 16px;\n    background-position: right;\n    background-repeat: no-repeat;\n    background-position-x: 97%;\n    margin-right: 20px;\n    padding-right: 10%;\n  }\n\n  .item.selected, .item:active {\n    text-decoration: none;\n    background-color: @accent-primary;\n    color: @text-color-inverse;\n    .primary {\n      color: @text-color-inverse;\n    }\n    .secondary {\n      color: @text-color-inverse-very-subtle;\n    }\n  }\n\n  .item:not(.active):not(.selected):hover {\n    text-decoration: none;\n    background-color: fade(@black, 3%);\n  }\n\n}\nbody.platform-win32 {\n  .menu {\n    .header-container {\n      input.search {\n        box-shadow: none;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/modal.less",
    "content": "@import \"ui-variables\";\n\n.nylas-modal-container {\n  position: absolute;\n  z-index: 40;\n\n  .modal-close {\n    margin: 6px;\n  }\n\n  .modal {\n    position: absolute;\n    background-color: @background-primary;\n    border-radius: @border-radius-base;\n    box-shadow: 0 10px 20px rgba(0, 0, 0, 0.20), 0 0 1px rgba(0, 0, 0, 0.7);\n  }\n}\n\n@media (-webkit-min-device-pixel-ratio: 2) {\n  .modal-close {\n    margin: 12px;\n  }\n}\n\nbody.platform-win32 {\n  .modal {\n    border-radius: 0;\n  }\n}"
  },
  {
    "path": "packages/client-app/static/components/multiselect-dropdown.less",
    "content": "@import \"ui-variables\";\n\n.nylas-multiselect-dropdown {\n  display: inline-block;\n\n  .button-dropdown, .menu, .secondary-items{\n    .content-container {\n      background: @dropdown-default-bg-color;\n      border-radius: 3px;\n      .item {\n        padding: 5px 10px 5px 20px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/nylas-calendar.less",
    "content": "@import 'ui-variables';\nbody.platform-win32 {\n  .calendar-toggles {\n    .colored-checkbox {\n      border-radius: 0;\n      .bg-color {\n        border-radius: 0;\n      }\n    }\n  }\n}\n\n.nylas-calendar {\n\n  height: 100%;\n  display: flex;\n\n  .calendar-toggles {\n    display: flex;\n    background-color: @source-list-bg;\n    border-right: 1px solid @border-color-divider;\n    color: @text-color-subtle;\n    .calendar-toggles-wrap {\n      padding: 20px;\n      padding-top: 6px;\n    }\n    .colored-checkbox {\n      position: relative;\n      top: 1px;\n      margin-left: 1px;\n      display: inline-block;\n      border-radius: 3px;\n      width: 12px;\n      height: 12px;\n      box-shadow: 0 0 0.5px rgba(0,0,0,0.4), inset 0 1px 0 rgba(0,0,0,0.13);\n      background: white;\n      .bg-color {\n        width: 100%;\n        height: 100%;\n        border-radius: 3px;\n      }\n    }\n    label {\n      padding-left: 0.4em;\n    }\n    .toggle-wrap, .account-label {\n      overflow: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n      margin-top: 2px;\n    }\n    .account-calendars-wrap {\n      &:first-child {\n        margin-top: 0;\n      }\n    }\n    .account-label {\n      cursor: default;\n      color: @text-color-very-subtle;\n      font-weight: @font-weight-semi-bold;\n      font-size: @font-size-small * 0.9;\n      margin-top: @padding-large-vertical;\n      letter-spacing: -0.2px;\n    }\n  }\n\n  background: @background-off-primary;\n  .calendar-view {\n    height: 100%;\n    flex: 1;\n    position: relative;\n  }\n\n  .calendar-event {\n    @interval-height: 21px;\n    position: absolute;\n    font-size: 11px;\n    line-height: 11px;\n    font-weight: 500;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    min-height: @interval-height;\n\n    border-top: 0;\n    border-bottom: 2px solid @background-primary;\n    border-left: 0;\n\n    display: flex;\n    cursor: default;\n    .default-header {\n      overflow: hidden;\n      text-overflow: ellipsis;\n      margin: 4px 5px 5px 6px;\n      cursor: default;\n      opacity: 0.7;\n      flex: 1;\n    }\n\n    &:before {\n      content: \"\";\n      position: absolute;\n      width: 2px;\n      height: 100%;\n      left: 0;\n      top: 0;\n      background: rgba(0,0,0,0.1);\n    }\n\n    &.horizontal {\n      white-space: nowrap;\n    }\n    &.selected {\n      background-color: @accent-primary !important;\n    }\n  }\n\n  .calendar-mouse-handler {\n    height: 100%;\n    display: flex;\n    flex-direction: column;\n  }\n  .week-view {\n    height: 100%;\n\n    @legend-width: 75px;\n\n    .calendar-body-wrap {\n      display: flex;\n      flex-direction: row;\n      position: relative;\n      height: 100%;\n\n      .calendar-area-wrap {\n        &::-webkit-scrollbar {\n          display: none;\n        }\n        flex: 1;\n        display: flex;\n        flex-direction: column;\n        overflow-x: auto;\n        overflow-y: hidden;\n        position: relative;\n      }\n\n      .calendar-legend {\n        width: @legend-width;\n        box-shadow: 1px 0 0 rgba(177,177,177,0.15);\n        z-index: 2;\n      }\n    }\n\n    .week-header {\n      position: relative;\n      padding-top: 75px;\n      border-bottom: 1px solid @border-color-divider;\n      flex-shrink: 0;\n    }\n\n    .date-labels {\n      position: absolute;\n      width: 100%;\n      top: 0;\n      left: 0;\n      height: 100%;\n      display: flex;\n    }\n\n    .all-day-events {\n      position: relative;\n      width: auto;\n      z-index: 2;\n      overflow: hidden;\n    }\n\n    .all-day-legend {\n      width: @legend-width;\n      position: relative;\n    }\n\n    .legend-text {\n      font-size: 11px;\n      position: absolute;\n      color: #bfbfbf;\n      right: 10px;\n    }\n\n    .date-label-legend {\n      width: @legend-width;\n      position: relative;\n      border-bottom: 1px solid #dddddd;\n      box-shadow: 0 1px 2.5px rgba(0, 0, 0, 0.15);\n      z-index: 3;\n      .legend-text {\n        bottom: 7px;\n      }\n    }\n\n    .day-label-wrap {\n      padding: 15px;\n      text-align: center;\n      flex: 1;\n      box-shadow: inset 1px 0 0 rgba(177,177,177,0.15);\n      &.is-today {\n        .date-label { color: @accent-primary; }\n        .weekday-label {\n          color: @accent-primary;\n        }\n      }\n    }\n    .date-label {\n      display: block;\n      font-size: 16px;\n      font-weight: 300;\n      color: #808080;\n    }\n    .weekday-label {\n      display: block;\n      font-weight: 500;\n      text-transform: uppercase;\n      margin-top: 3px;\n      font-size: 12px;\n      color: #ccd4d8;\n    }\n\n    .event-grid-wrap {\n      flex: 1;\n      overflow: auto;\n      background: @background-primary;\n      box-shadow: inset 0 1px 2.5px rgba(0,0,0,0.15);\n      height: 100%;\n    }\n    .event-grid {\n      display: flex;\n      position: relative;\n    }\n    .event-grid-legend-wrap {\n      overflow: hidden;\n      box-shadow: 1px 0 0 rgba(177,177,177,0.15);\n    }\n    .event-grid-legend {\n      position: relative;\n      background: @background-primary;\n      z-index: 2;\n    }\n    .event-grid-bg-wrap {\n      .event-grid-bg {\n        position: absolute;\n        top: 0;\n        left: 0;\n      }\n      .cursor {\n        background: rgba(0,0,0,0.04);\n        position: absolute;\n      }\n      position: absolute;\n      top: 0;\n      left: 0;\n      right: 0;\n      bottom: 0;\n      z-index: 0;\n    }\n\n    .event-column {\n      flex: 1;\n      position: relative;\n      height: 100%;\n      z-index: 1;\n      overflow: hidden;\n      box-shadow: 1px 0 0 rgba(177,177,177,0.15);\n      &.weekend {\n        background: rgba(0,0,0,0.02);\n      }\n    }\n  }\n\n  .month-view {\n\n  }\n\n  .current-time-indicator {\n    position: absolute;\n    width: 100%;\n    height: 1px;\n    opacity: 0;\n    border-top: 1px solid rgb(255,100,100);\n    z-index: 2;\n    transition: opacity ease-in-out 300ms;\n\n    &.visible {\n      opacity: 1;\n    }\n    div {\n      position: absolute;\n      width:11px;\n      height:11px;\n      border-radius: 6px;\n      background-color: rgb(255,100,100);\n      transform: translate3d(-75%, -50%, 0);\n      border: 1px solid @background-primary;\n    }\n  }\n\n  .top-banner {\n    color: rgba(33,99,146,0.6);\n    background: #e0eff6;\n    font-size: 12px;\n    line-height: 25px;\n    text-align: center;\n    box-shadow: inset 0 -1px 1px rgba(0,0,0,0.07);\n  }\n\n  .header-controls {\n    padding: 10px;\n    display: flex;\n    color: #808080;\n    border-bottom: 1px solid @border-color-divider;\n    box-shadow: inset 0 -1px 1px rgba(191,191,191,0.12);\n    flex-shrink: 0;\n\n    .title {\n      display: inline-block;\n      width: 275px;\n    }\n    .title, .btn-icon {\n      font-size: 15px;\n      line-height: 25px;\n    }\n\n    .btn-icon {\n      margin: 0;\n      height: auto;\n      img {\n        vertical-align: baseline;\n      }\n    }\n  }\n\n  .footer-controls {\n    padding: 10px;\n    min-height: 45px;\n    display: flex;\n    color: #808080;\n    background: @background-primary;\n    border-top: @border-color-divider;\n    box-shadow: 0 -3px 16px rgba(0,0,0,0.11);\n    z-index: 2;\n  }\n\n  .center-controls {\n    text-align: center;\n    flex: 1;\n    order: 0;\n  }\n}\n\n.mini-month-view {\n  width: 100%;\n  height: 100%;\n  min-width: 200px;\n  min-height: 200px;\n  text-align: center;\n  display: flex;\n  flex-direction: column;\n  background: @background-primary;\n  border: 1px solid @border-color-divider;\n  border-radius: @border-radius-base;\n\n  .header {\n    display: flex;\n    background: @background-secondary;\n    padding: 4px 0 3px 0;\n    .month-title {\n      padding-top: 3px;\n      color: @text-color;\n      flex: 1;\n    }\n    border-bottom: 1px solid @border-color-divider;\n    .btn.btn-icon {\n      line-height: 27px;\n      height: 27px;\n      margin-top: -1px;\n      margin-right: 0;\n      &:active {\n        background: transparent;\n      }\n    }\n  }\n\n  .legend {\n    display: flex;\n    .weekday {\n      flex: 1;\n    }\n    padding: 3px 0;\n    border-bottom: 1px solid @border-color-divider;\n  }\n\n  .day-grid {\n    display: flex;\n    flex-direction: column;\n    flex: 1;\n    .week {\n      flex: 1;\n      display: flex;\n      min-height: 28px;\n    }\n    .day {\n      display: flex;\n      flex-direction: column;\n      justify-content: center;\n      flex: 1;\n      min-height: 28px;\n      color: @text-color-very-subtle;\n      &.cur-month {\n        color: @text-color;\n      }\n      &:hover {\n        background: rgba(0,0,0,0.05);\n        cursor: pointer;\n      }\n      &.today {\n        border: 1px solid @accent-primary;\n      }\n      &.cur-day {\n        background: @accent-primary;\n        color: @text-color-inverse;\n        &:hover {\n          background: darken(@accent-primary, 5%);\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/outline-view.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n@count-color: fadeout(@text-color-subtle, 40%);\n\n.nylas-outline-view {\n  order: 1;\n\n  section {\n    margin-bottom: @padding-base-vertical;\n  }\n\n  section.item-children {\n    padding-left: @padding-base-horizontal;\n    margin-bottom: 0;\n  }\n\n  .heading {\n    display: flex;\n    align-items: center;\n    cursor: default;\n    color: @text-color-very-subtle;\n    font-weight: @font-weight-semi-bold;\n    font-size: @font-size-small * 0.9;\n    padding-left:@padding-small-horizontal;\n    padding-top:@padding-small-horizontal;\n    padding-right: @padding-xs-horizontal;\n    letter-spacing: -0.2px;\n\n    span:first-child {\n      text-overflow: ellipsis;\n      overflow: hidden;\n      white-space: nowrap;\n    }\n\n    .add-item-button {\n      margin-left: @padding-small-horizontal;\n      margin-right: @padding-small-horizontal;\n      cursor: pointer;\n      img {background: @text-color-very-subtle; }\n      display: none;\n    }\n\n    .collapse-button {\n      cursor: pointer;\n      display: none;\n      min-width: 30px;\n      margin-left: auto;\n      margin-right: 8px;\n    }\n  }\n\n  &:hover {\n    .heading {\n      .collapse-button,.add-item-button {\n        display: inherit;\n      }\n    }\n  }\n\n  .item-container {\n    display:flex;\n    &.dropping {\n      background-color: lighten(@source-list-bg, 20%);\n      .item {\n        color: @source-list-active-color;\n        img.content-mask { background-color: @source-list-active-color; }\n      }\n    }\n  }\n\n  .item {\n    color: @text-color-subtle;\n    flex: 1;\n    display: flex;\n    align-items: baseline;\n    font-size: @font-size-base;\n    font-weight: 400;\n    padding-right: @spacing-standard;\n    line-height: @line-height-large;\n    clear: both;\n\n    // The overflow needs to be `hidden` in order for the name to flex\n    // fit within the contained space.\n    overflow: hidden;\n\n    img.content-mask {\n      background-color: @text-color-subtle;\n      vertical-align: text-bottom;\n    }\n\n    .icon {\n      flex-shrink: 0;\n      order: 1;\n    }\n    .name {\n      order: 2;\n      padding-left: @padding-small-horizontal * 0.85;\n      overflow: hidden;\n      line-height: @line-height-small;\n      text-overflow: ellipsis;\n    }\n    .item-count-box {\n      order: 3;\n      flex-shrink: 0;\n      font-weight: @font-weight-semi-bold;\n      color: @count-color;\n      margin-left: @padding-small-horizontal * 0.8;\n      box-shadow: 0 0.5px 0 @count-color, 0 -0.5px 0 @count-color, 0.5px 0 0 @count-color, -0.5px 0 0 @count-color;\n    }\n    .item-count-box.alt-count {\n      color: @source-list-active-bg;\n      background: @source-list-active-color;\n      box-shadow: none;\n    }\n    .item-count-box.archive,\n    .item-count-box.all,\n    .item-count-box.spam {\n      display: none;\n    }\n\n    &.selected {\n      background: @source-list-active-bg;\n      color: @source-list-active-color;\n      img.content-mask { background-color: @source-list-active-color; }\n    }\n    &.dropping {\n      background-color: lighten(@source-list-bg, 20%);\n      color: @source-list-active-color;\n      img.content-mask { background-color: @source-list-active-color; }\n    }\n    &.editing {\n      align-items: center;\n\n      .item-input {\n        order: 2;\n        font-size: @font-size-small;\n        margin-left: @padding-small-horizontal * 0.3;\n        height: 22px;\n        padding-left: 0;\n        text-indent: @padding-small-horizontal * 0.55;\n        width: 85%;\n      }\n    }\n    &:hover {\n      cursor: default;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/scroll-region.less",
    "content": "@import \"ui-variables\";\n\n@tooltipBorderColor: rgba(54, 56, 57, 0.9);\n@tooltipBackground: -webkit-gradient(linear, left top, left bottom, from(rgba(99, 102, 103, 0.9)), to(rgba(82, 85, 86, 0.9)));\n\n.scroll-tooltip {\n  background: @tooltipBackground;\n  color: white;\n  box-shadow: 0 2px 7px rgba(0, 0, 0, 0.25);\n  padding: @padding-base-vertical @padding-base-horizontal @padding-base-vertical @padding-base-horizontal;\n  border: 1px solid @tooltipBorderColor;\n  border-radius: @border-radius-base;\n  transform: translate(-15px, 0);\n  position: relative;\n  white-space:nowrap;\n  pointer-events: none;\n}\n.scroll-tooltip:after, .scroll-tooltip:before {\n  left: 100%;\n  top: 50%;\n  border: solid transparent;\n  content: \" \";\n  height: 0;\n  width: 0;\n  position: absolute;\n  pointer-events: none;\n}\n.scroll-tooltip:after {\n  border-color: transparent;\n  border-left-color: lighten(rgba(99, 102, 103, 1), 3%);\n  border-width: 8px;\n  margin-top: -8px;\n}\n.scroll-tooltip:before {\n  border-color: transparent;\n  border-left-color: darken(@tooltipBorderColor, 20%);\n  border-width: 9px;\n  margin-top: -9px;\n}\n\n\n.scroll-region {\n  ::-webkit-scrollbar {\n    display: none;\n  }\n\n  position:relative;\n\n  .scroll-region-content {\n    position: absolute;\n    height: 100%;\n    width: 100%;\n    overflow-y: scroll;\n    overflow-x: hidden;\n    z-index: 1; // Important so that content does not repaint with container\n  }\n\n  .scroll-region-content-inner {\n    transform:translate3d(0,0,0);\n  }\n}\n\n.scroll-region.scrolling {\n  .scroll-region-content-inner {\n    pointer-events: none;\n  }\n}\n\n.scrollbar-track {\n  position: relative;\n  opacity: 0;\n  transition: opacity 0.3s;\n  transition-delay: 0.5s;\n  padding:3px;\n  width:17px;\n  background: @list-bg;\n  border-left: 1px solid @border-color-divider;\n\n  &:hover {\n    opacity: 1;\n    transition-delay: 0s;\n  }\n\n  &.scrolling {\n    opacity: 1;\n    transition-delay: 0s;\n  }\n\n  &.with-ticks {\n    opacity: 1;\n    transition-delay: 0s;\n  }\n\n  &.dragging {\n    opacity: 1;\n    .scrollbar-handle {\n      cursor: default;\n      background-color: lighten(@gray, 30%);\n      border:1px solid lighten(@gray, 20%);\n      .tooltip {\n        opacity: 1;\n        display:block;\n      }\n    }\n  }\n\n  /* Used to read the track height with padding applied. */\n  .scrollbar-track-inner {\n    height:100%;\n  }\n\n  .scrollbar-handle {\n    background-color: lighten(@gray, 40%);\n    border:1px solid lighten(@gray, 30%);\n    border-radius:8px;\n    .tooltip {\n      opacity: 0;\n      display:none;\n      transition: opacity 0.3s;\n      top: 50%;\n      transform: translate(-100%, -50%);\n      position: absolute;\n      pointer-events: none;\n    }\n  }\n\n  .scrollbar-ticks {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    z-index: 2;\n    .t {\n      &.match {\n        background: @text-color-search-current-match;\n        z-index: 2;\n      }\n      position: absolute;\n      width: 90%;\n      left: 5%;\n      height: 2px;\n      background: @text-color-search-match;\n      box-shadow: 0 0.5px 0.5px rgba(0,0,0,0.25);\n    }\n  }\n}\nbody.platform-win32 {\n  .scroll-tooltip {\n    border-radius: 0;\n  }\n  .scrollbar-track {\n    .scrollbar-handle {\n      border-radius: 0;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/search-bar.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.nylas-search-bar {\n  height: initial;\n\n  .menu .header-container {\n    padding: 0;\n    height: 23px;\n    background-color: transparent;\n    border: none;\n\n    input {\n      padding-left: 26px;\n      padding-right: 26px;\n      width: 100%;\n      height: 23px;\n      border: 1px solid transparent;\n      box-shadow: @shadow-border;\n    }\n    input.empty {\n      text-align: left;\n    }\n    input.empty:focus {\n      text-align: left;\n    }\n  }\n\n  .menu .footer-container {\n    border:none;\n  }\n  .menu .content-container {\n    position: absolute;\n    top: 23px;\n    z-index: 2;\n    width: 100%;\n\n    box-shadow: @standard-shadow;\n\n    .item {\n      overflow: hidden;\n      white-space: nowrap;\n      text-overflow: ellipsis;\n    }\n  }\n\n  .search-container {\n    position: relative;\n\n    &.showing-suggestions {\n      .suggestions { display: inherit; }\n      .clear {\n        color: @text-color-subtle;\n        display: inherit;\n      }\n    }\n    &.showing-query {\n      .clear { display: inherit; }\n    }\n  }\n\n  .search-accessory {\n\n    &.search {\n      position: absolute;\n      top: 8px;\n      left: @padding-base-horizontal;\n\n      &.loading {\n        top: 4px;\n        left: 7px;\n        width: 15px;\n        height: 15px;\n      }\n    }\n\n    &.clear {\n      position: absolute;\n      top: 3px;\n      right: 4px;\n      color: @text-color-subtle;\n      display: none;\n    }\n  }\n}\n\nbody.is-blurred {\n  .search-bar .menu .header-container input {\n    background: none;\n    box-shadow: @shadow-border;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/spinner.less",
    "content": "@import \"ui-variables\";\n\n.spinner {\n  margin: 0;\n  width: 94px;\n  text-align: center;\n  opacity: 1;\n  transition: opacity 0.2s linear;\n  pointer-events: none;\n}\n\n.spinner.hidden {\n  opacity: 0;\n}\n.spinner.paused {\n  > div {\n    // important. animating with opacity 0 chews up about 5% cpu\n    animation-play-state: paused;\n  }\n}\n\n.spinner-cover {\n  &.hidden {\n    opacity: 0;\n  }\n}\n\n.spinner > div {\n  width: 14px;\n  height: 14px;\n\n  border-radius: 100%;\n  border: 1px solid @gray-light;\n  display: inline-block;\n  animation: bouncedelay 1.1s infinite cubic-bezier(0.45, 0.05, 0.55, 0.95);\n  /* Prevent first frame from flickering when animation starts */\n  animation-fill-mode: both;\n  margin-right:4px;\n  margin-left:4px;\n}\n\n.spinner .bounce1 {\n  animation-delay: -0.34s;\n}\n.spinner .bounce2 {\n  animation-delay: -0.22s;\n}\n.spinner .bounce3 {\n  animation-delay: -0.12s;\n}\n.spinner .bounce4 {\n  animation-delay: 0s;\n}\n\n@keyframes bouncedelay {\n  0%, 80%, 100% {\n    transform: scale(0.0);\n  } 40% {\n    transform: scale(1.0);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/switch.less",
    "content": "@import \"ui-variables\";\n\n.slide-switch {\n  border-radius: 12px;\n  box-shadow: inset 0 1px 1.5px rgba(0,0,0,0.3);\n  background-color: @gray-light;\n  position: relative;\n  display: inline-block;\n  vertical-align: middle;\n  height: 21px;\n  width: 40px;\n\n  .handle {\n    border-radius: 14px;\n    width: 25px;\n    height: 25px;\n    position: absolute;\n    top: -2px;\n    left: -3px;\n    background-color: @white;\n    box-shadow: 0 0.5px 3px rgba(0,0,0,0.4);\n    transition: all 150ms cubic-bezier(0.22, 0.61, 0.36, 1);\n  }\n  &.active {\n    background-color: #69ba55;\n    .handle {\n      left: 18px;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/syncing-list-state.less",
    "content": "@import 'ui-variables';\n\n.syncing-list-state {\n  font-size: 105%;\n  color: @text-color-very-subtle;\n  padding: 10px 0;\n\n  a {\n    cursor: pointer;\n    font-size: 75%;\n    vertical-align: top;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/table.less",
    "content": "@import 'ui-variables';\n\n@row-highlight-color: #e3effc;\n@row-highlight-text-color: #3a6fa7;\n@row-highlight-border-color: #c9dcf0;\n\n.nylas-table {\n  height: 100%;\n  width: 100%;\n  overflow-y: hidden;\n  overflow-x: scroll;\n\n  thead, tbody {\n    display: block;\n  }\n\n  .table-row {\n\n    .table-cell {\n      border: 1px solid lighten(@border-color-secondary, 5%);\n      color: @text-color-very-subtle;\n\n      &>div {\n        min-height: 20px;\n        min-width: 100px;\n      }\n\n      input {\n        color: inherit;\n        border: none;\n        &:focus {\n          border: none;\n          outline: none;\n          box-shadow: none;\n        }\n      }\n\n      &.selected {\n        // TODO\n      }\n\n      &.numbered-cell {\n        width: 20px;\n        min-width: 20px;\n        text-align: center;\n      }\n    }\n\n    &.selected {\n      background: @row-highlight-color;\n\n      .table-cell {\n        color: @row-highlight-text-color;\n        border: 1px double @row-highlight-border-color;\n        input {\n          color: @row-highlight-text-color;\n        }\n      }\n    }\n  }\n\n}\n"
  },
  {
    "path": "packages/client-app/static/components/time-picker.less",
    "content": "@import \"ui-variables\";\n\n.time-picker-wrap {\n  display: inline-block;\n  width: 5.5em;\n  position: relative;\n  input {\n    width: 5.5em;\n    &.invalid {\n      background: fade(@color-error, 10%);\n    }\n  }\n\n  .time-options {\n    max-height: 158px;\n    overflow: auto;\n    border: 1px solid #eee;\n    position: absolute;\n    width: 100%;\n    text-align: center;\n    background: @background-primary;\n    border-top: 0;\n    border-radius: 0 0 @border-radius-base @border-radius-base;\n    box-shadow: 0 0 3px rgba(0,0,0,0.1);\n\n    &.relative-to {\n      width: 120px;\n      text-align: left;\n\n      .option {\n        padding-left: 12px;\n      }\n    }\n\n    .option {\n      line-height: 1.75;\n      &.selected {\n        color: @text-color;\n        &:hover {\n          background: transparent;\n        }\n      }\n      &.focused, &:hover {\n        background: rgba(0,0,0,0.05);\n      }\n      .rel-text {\n        font-size: 0.8em;\n        margin-left: 4px;\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/toast.less",
    "content": "@import \"ui-variables\";\n\n.nylas-toast {\n  position: absolute;\n  bottom: 10px;\n  z-index: 100;\n  width:100%;\n  text-align: center;\n  pointer-events: none;\n\n  .nylas-toast-wrap {\n    height: 45px;\n    display:inline-block;\n    padding-top: 12px;\n    opacity: 0.9;\n    border-radius: @border-radius-base;\n    background: @gray-darker;\n    white-space:nowrap;\n    cursor:default;\n    pointer-events: initial;\n    display: inline-flex;\n    flex-direction:row;\n    max-width:90%;\n  }\n}\n\nbody.platform-win32 {\n  .nylas-toast-wrap {\n    border-radius: 0;\n  }\n}\n\n.nylas-toast-item-enter {\n  opacity: 0.01;\n  transform: translate3d(0, 10px, 0);\n  transition: all .15s ease-out;\n}\n\n.nylas-toast-item-enter.nylas-toast-item-enter-active {\n  opacity: 1;\n  transform: translate3d(0, 0, 0);\n}\n\n.nylas-toast-item-leave {\n  opacity: 1;\n  transform: translate3d(0, 0, 0);\n  transition: all .15s ease-in;\n}\n\n.nylas-toast-item-leave.nylas-toast-item-leave-active {\n  transform: translate3d(0, 10px, 0);\n  opacity: 0.01;\n}\n"
  },
  {
    "path": "packages/client-app/static/components/tokenizing-text-field.less",
    "content": "@import \"ui-variables\";\n\n@token-top:lighten(@background-secondary,0.6%);\n@token-bottom:darken(@background-secondary, 2.5%);\n\n@token-hover-top: mix(@token-top, @component-active-color, 92%);\n@token-hover-bottom: mix(@token-bottom, @component-active-color, 90%);\n\n@token-selected-top: mix(@token-top, @component-active-color, 15%);\n@token-selected-bottom: mix(@token-bottom, @component-active-color, 0%);\n\n@token-invalid-selected-top: mix(@token-top, red, 60%);\n@token-invalid-selected-bottom: mix(@token-bottom, red, 55%);\n\n@base-box-shadow: 0 0.5px 0 rgba(0,0,0,0.17), 0 -0.5px 0 rgba(0,0,0,0.17), 0.5px 0 0 rgba(0,0,0,0.17), -0.5px 0 0 rgba(0,0,0,0.17), 0 1px 1px rgba(0, 0, 0, 0.1);\n\n.tokenizing-field {\n  display: block;\n  margin: 0;\n  padding: 0;\n  position: relative;\n\n  &.disabled {\n    background: rgba(0,0,0,0.02);\n    opacity: 0.7;\n    .token {\n      background: @token-top;\n      box-shadow: @base-box-shadow;\n      &:hover {\n        background: @token-top;\n        cursor: default;\n        box-shadow: @base-box-shadow;\n      }\n    }\n    .tokenizing-field-input {\n      &:hover {\n        cursor: default;\n      }\n    }\n    &:hover {\n      cursor: default;\n    }\n  }\n\n  .content-container {\n    border: 1px solid @border-color-secondary;\n    box-shadow: 0 2px 8px rgba(0,0,0,0.2);\n    border-radius: @border-radius-small;\n    background-color: @background-primary;\n    position: absolute;\n    z-index: 2;\n  }\n\n  .content-container.empty {\n    border: 0;\n    box-shadow: none;\n  }\n\n  .header-container, .footer-container {\n    background-color: transparent;\n    padding:0;\n    margin:0;\n    border:0;\n  }\n\n  .token-editing-input {\n    max-width: 100%;\n    font-size: 15px;\n    line-height: 17px;\n    padding: 2em;//0.5em @spacing-three-quarters 0.5em @spacing-three-quarters;\n    padding-right: 1.5em;\n    margin: 3px 6px 6px 1px;\n  }\n\n  .token {\n    display: inline-block;\n    position: relative;\n    color: @text-color;\n    padding: 0 @spacing-three-quarters;\n    padding-right: 1.5em;\n    margin: 1px 5px 1px 1px;\n    border-radius: @border-radius-base * 0.8;\n    max-width: 100%;\n    line-height: 1.9em;\n\n    background: linear-gradient(to bottom, @token-top 0%, @token-bottom 100%);\n    box-shadow: @base-box-shadow;\n    vertical-align: middle;\n\n    .action {\n      position:absolute;\n      padding: 0;\n      border: 0;\n      margin: 0;\n      right: 7px;\n      background-color: transparent;\n      color: @text-color-very-subtle;\n      img { background-color: @text-color-very-subtle; }\n      font-size: 10px;\n    }\n    &:hover {\n      background: linear-gradient(to bottom, @token-hover-top 0%, @token-hover-bottom 100%);\n      box-shadow: 0 0.5px 0 darken(@token-hover-bottom, 35%), 0 -0.5px 0 darken(@token-hover-top, 25%), 0.5px 0 0 darken(@token-hover-bottom, 25%), -0.5px 0 0 darken(@token-hover-bottom, 25%), 0 1px 1px rgba(0, 0, 0, 0.07);\n      cursor: default;\n    }\n    &.invalid {\n      border-bottom:1px dashed red;\n      margin-bottom: -1px;\n      background: transparent;\n      &:hover {\n        box-shadow: 0 -0.5px 0 @token-invalid-selected-top, 0.5px 0 0 @token-invalid-selected-bottom, -0.5px 0 0 @token-invalid-selected-bottom, 0 1px 1px rgba(0, 0, 0, 0.07);\n      }\n    }\n    &.invalid.selected, &.invalid.dragging {\n      background: linear-gradient(to bottom, @token-invalid-selected-top 0%, @token-invalid-selected-bottom 100%);\n      box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.3), 0 1px 1px rgba(0, 0, 0, 0.1);\n      border: 1px solid darken(@token-invalid-selected-bottom, 8%);\n      border-top: 1px solid darken(@token-invalid-selected-top, 10%);\n    }\n\n    &.selected,\n    &.dragging {\n      background: linear-gradient(to bottom, @token-selected-top 0%, @token-selected-bottom 100%);\n      box-shadow: inset 0 1.5px 0 rgba(255,255,255,0.3), 0 1px 1px rgba(0, 0, 0, 0.1);\n      border: 1px solid darken(@token-selected-bottom, 8%);\n      border-top: 1px solid darken(@token-selected-top, 10%);\n      border-radius: @border-radius-base;\n\n      // Note: we switch from 0.5px borders with box shadows to a real border,\n      // because the 0.5px shadows can't be as dark as we want. This means\n      // margins / border radius change by 1px.\n      margin: 0 4px 0 0;\n\n      color: @text-color-inverse;\n      .action {\n        color: @text-color-inverse-subtle;\n        img { background-color: @text-color-inverse-subtle; }\n      }\n      .secondary,\n      .participant-secondary {\n        color: @text-color-inverse-subtle;\n      }\n    }\n    &.dragging {\n      cursor: -webkit-grabbing;\n    }\n  }\n\n  .tokenizing-field-label {\n    color: @text-color-very-subtle;\n    float: left;\n    text-transform: capitalize;\n    padding-top: 12px;\n    display: block;\n    &:hover {\n      cursor: default;\n    }\n  }\n\n  .tokenizing-field-input {\n    position: relative;\n    margin-left: 2.8em;\n    padding-top: 9px;\n    padding-bottom: 3px;\n\n    &:hover {\n      cursor: text;\n    }\n    &.at-max-tokens {\n      cursor: default;\n      input {\n        &:hover { cursor: default; }\n        opacity: 0;\n      }\n    }\n\n    input {\n      display: inline-block;\n      width: initial;\n      vertical-align:top;\n      border: none;\n      min-width: 0.5em;\n      background-color:transparent;\n\n      // NOTE: padding-top and padding-bottom need to match that of\n      // `.token`. to ensure they have the same baseline.\n      //\n      // The padding-left and padding-right must be 0 so we can manually\n      // set the width properly to always match the size of the input\n      // test.\n      padding: 0.3em 0 0.3em 0;\n\n      &.noop-input {\n        position: absolute;\n        min-width: 0;\n        padding-left: 0;\n        margin-right: 0;\n        box-shadow: none;\n        &:focus {\n          box-shadow: none;\n        }\n      }\n    }\n\n    input:focus {\n      box-shadow: none;\n    }\n  }\n\n  .placeholder {\n    color: fade(@text-color, 50%);\n    position: absolute;\n    top: 50%;\n    margin-top: -0.75em;\n    left: 8px;\n  }\n}\n\nbody.is-blurred .tokenizing-field .token:not(.invalid) {\n  background: @background-secondary;\n  color: @text-color;\n  &.selected .action img { background-color: @text-color-very-subtle; }\n}\n\nbody.platform-win32 {\n  .tokenizing-field {\n    .content-container {\n      border-radius: 0;\n    }\n    .tokenizing-field-input {\n      input, input:focus {\n        border: 0;\n        box-shadow: 0 0 0;\n      }\n    }\n  }\n  .token {\n    border-radius: 0;\n    background: @token-bottom;\n    &.selected,\n    &.dragging {\n      border-radius: 0;\n      background: @token-selected-top;\n    }\n    &:hover {\n      background: @token-hover-bottom;\n    }\n    &.invalid.selected, &.invalid.dragging {\n      background: @token-invalid-selected-top;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/tutorial-overlay.less",
    "content": "tutorial-tip-background {\n  position: absolute;\n  top: 0;\n  left: 0;\n  right: 0;\n  bottom: 0;\n  z-index: 20;\n  pointer-events: none;\n  opacity: 0;\n  transition: opacity ease-in-out 250ms;\n  background-color: rgba(0,0,0,0.25);\n  -webkit-mask-image: linear-gradient(to top, #fff 0%, #aaa 100%), url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAMAAABrrFhUAAAAt1BMVEUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABa5Z9DAAAAPHRSTlMABgsQLSgUGDMcOCPy7TxlQCD6dkilTET2zYJgqplQxFhUv1xpcW3kkHrohuDRutmu1ZS2oMiyfordnY2UR3hBAAAS+UlEQVR42uyW2Y4TMRRE2QkgQSI2se8IhECIXfD//4VMOT4uX5sWCt10gOJlZh6Qzqm6Tk78z//8gZzUP3Ji/+/vjqgn8rdqcMh/S0Nb+6lx/kIJHfLp/CUSaL5hPz3KwMJxOoB9DD4t4kiHQPc99nPj9Cwc4Q4ifCQ/36bjAQnHtAOnhx3uiWDBJBzJDHr0jn7h53EN7mDtMzhp+AEe9N0oaDAJOFj1CiI98E5+aZTiAQvBwVpXAD70NbyTn+3FPVQScLDaGTi+0wse8MlgoeNgjSvQ7Xfwga/Zz5AYLNQSooJVjQD8QA98IN94ggckmIP1rUD4lO/0wDv3RdCbv0QJ7kAzWM0KBvjQA19ISUwziNrBChWk4/fx5/KhB97QL/diGpCAg/RfS8FK3gLVDz7lQw98Db7tBRFIwIFmUBT8+RFQf4NvywfeyW/GuAck2C2sSMEYH3qDN/BrMSYCCTgYK1jeAPUP8aGHHfAbMYjAAg76ChYewbh+8Cl/X/0e3sDvx5iIvYT9EJgBCv7cCGL9jr8Bv4I38EcxJqKSgIKNKWAEixrg8Vf9Ed/pgTfwhzEmAgnmICrQCJY0QP2+fvADveAN/EmMiZCE4AAF3MHSIwj1j/GpXuyA3+kFEbLAELoKFh8Br7/Xz/rBz+UXerFDfqUXPMhCcZBngALuwEawxKcB86d+w6d86AXv5E/buAdJwAEzQIGPQGcwr4CTxq/6x/jQC97An8eYCEnAwUgBI1jAgM8/1A++yoc+w9fkz2LwkC2YgzQDFMQRcAYomIuf+Tt+4gff6cUO+dUYPMiCOTAFlxsFnMGsBuAP9af1g38/40MveCN/2QYP2QIOioL7UlDuwEfgBhbip37HV/nQZ3bIP7fBQ7aAA83AFTCCRQxE/lH9wlf50Au+gN/up4iQBBxoBlIwGMHMBuA/Z/zU38OHXvCgv+oFDZKAg44CRuAGzs1joD9/6nd8yhe94Gv0L73UGiRBDpiBK2AE/TNYhJ/6waf8RE/zBf3bj7yooz8VDSwhOWAGKGAEsxpg/8bP/L1+8Clf9Jm9Bn9XpxaRLcgBM0ABI7AzMAO6ghn4OX/NP9QvfMoXveBr8HueWoQkyAEzkIIwAp0BD4EZmInf56/6Iz70Bd7BvxIXUSTgICrQCOwMZjDA9/8+v9Wv9Tt+oRc84B/aSESRgANXoDuwEfQM/L5nYIq/X7/wVb7oBQ/59TZ4kAQ50AykoDuCKQNz8m/FT/0B3+jFDvndOniQBXMQFDACGdhOGJiJX+fP4+/4Kl/0ufkI/jYlitAS5EAzaBTwcaCHYC4DfAEY8Jf5q/50/Ibf0oOtfErhVyTgwBWkpyCPoJzBwIB/HTj8CxD8CR9+5q/6M762v19+gRd2yfsUfsVCdlAUvMgK8gg4AwwkBRg48AsRHwADfs6f+Qs/8dM+9IIXdsmbFH4tFmoHrCAZkALOgIegawABBz8Au92I3+sHP5UveuCFrTx+U37Qj1KBBDlIM0CBj2BkYLezZ+C38m9afl6/VH9aP/h0n+EFPIosSAI7QEG6gzQC3sLWwGZg4PAPgD4/86f+Bh/6iv1BL5UFHDQKGAFn0DNw+EcBDwAfAHr/Mz/PX64/8Wv9uv2MD72zf/S4BRxkBXoLdAfJQB4BT6EM6LOAj4IDngF7AMf85fy9fvCN3shv1TEP5gAFPoLyEEQDBz6E8QCcfxv40/x1/azf8IGHvBuXYAq4g/QS6AyCga0ZOOgI/AEc83P+1A++ys/0sL/uBwtykGeAAkbAQzA24A/hL/L7AzDNr/mr/rR+x8/0sH9nzgxyo4iCGHoBEIgFmhXKKiCEEiVKFqC5/7mQ8DSvq6otaxi6lb6B/e1XnuRp68MFeVAt+NMDhUA1yA5kDOQCAAANAO0/8V/6qT/PX+SjfqX9cX4rF/CgWEAIAIEc0C3QJtQcAAOmBOECbAHA66f+PL/k6/EX9WvtP+u3dmHxQDGQBYQAEFgHtjFwSwE4AF0/9a/PL/Tx+FK/kv6jfisb5AExEA5rCABBd4BTkEuQC7Cl/0vUX+RX9UX61+UrNhQPigXZgS/NAVeCbMAoAADU/t3QX55f3Uc+4hE+vrUJWCAWlBBsOKBVDAhbCbwBuQAAAP0frP4pX29fxH/f+ooJysGwwDvwAQfAwL+VwBVAALy7G/qf0c/zF/l6+6L9ff2KC8pBsYAQ4MDzcODuTiB0JbipAABQ+9/o5/lX8pe3H9K3bFhygAWrEBgH9LsAEN5QAgIwCgAAtX//6v+F/vL8F/moR/y7/mHCxQMsIATFgV9/HdAqBoSUIEUgB6AVQAC8v9f+V/+lX/VHv9An+VX9O/9VD2SBcCgHAIEcEAf0u+D+XiCsJTARyCeQ3wAUoAEA/qFf8dfzV/lN/Kl/zYRqgUKgGuAAJGwYoAT8JiAC2QBfAPRr/zf9PL/S3+QP7c6FaoF6QAi6A/pdgAO2BEQgn0AISAEAAAD0+vX8yK/iP82vmoAFCoF3ABCCAUoAB+0pzAHQBFIBtICyfj1/ld+1WxeqBQpBdkB7SCXQHEoRyAHwBdjUr/rz/F0+6s/zw4NmASEQCLYd8CVoEbguABCQAqD/+XL/0V+fv8nv2q0LxYIWAhxY9sAzDlACODgikDfADIAmkC6gCiAAWv08f338c/5qDAiBdUAgVAl0CzWHSgTsIcgBEAEpgBaQCvDysqVf8ef5jfrsASFQDbYceHlRCbSHVAI4GCIQAgABmUANANp/Vj/hb/KzBRTBOaBN2DHAHIKDIQI5AJ8JwKUAAKDpfyz6o/xsQXPgsTkABi4lIAKfQwSuCwAEXArAAXh4KPrX9Y/yswUrEFQHHh44BZcSNA7mCOQAcAIhYCnAov91yX+Lf5CfLGg1WFrwenGglgAOcgpNBKwBPgAUgAuwAFD7B/3m+W8LAQ5oES0g5BJQAhOBZAAjaAaADVwKAACM/vNtn3EADJQSsIhHBPoYyg1gAxAA/QZqBQj6d3NglkC/iogAW6B3ICBwnoB+AikAANT+QT/yb7YAB7SIACElqKdwHoKOwYzAzQAwgSiAAGj07+OAQEgJmEMbEWgYTAaAQEbgDMAsgPZv1b+DA1rFswQzAsxBMDg7kBvABkA/E6jr/x703+rA9+4AcwgH2AKxA/w7sDWAG9gD8I0A1AIM/bs50EpABL4RgXYJRwfyCCAAMmAEgAIAgJ304wAYoASDg4sBRMBNgdyAicAWABUAAKJ/DwcEQkpQItAweFUHSgMmAtkAMwAUYNF/Qv9/deB0cYASjAiwBSYGawdyA64PAAU47/FRgqsjkDuAARsjgJ+BGoEEQAQ0+nd3QBwkAtoC/CgcU6AYkBvACjQBgIAUYB8AgAFKAAdNBFiDsQMYkBoQApABcDsGUgRCBzDAN2AY4AlAAEwB9ioBEfAUqAb4DnAEWUFqgDWAX0EEYNcCUIIeAX4TWQPUAbYQhzAgoDeAG3h8AHIEuIStAx4C/odQboANwHnfb0YgYNBDIBzBj9YAG4DTQQacXAS8AR/tIcQAEBAbwA3MAdg3AlzC2AEggAEZAaygdQAWAzQCIcCuBISDUEBzcDFgFQG2UIBAXgG5ASkA+0UgdyBDIO/gsYIGAtMJ2OkQVAzOLZTX8DTAIUC/A7gBswEH6ccBOjDvgH4PTAhEAzICaMChCMwYpAMZAhjgGOgRwApSA3YPQI6AOsAW8hAYFAwMzAh4ehMGPAUIZApigGdgQ8DBNyDfgQYBT0FvAEfAMNAh4Gj9OGAgYCg4z0A6AjCQFfAGEGAgMJYAFLRnwA1hw8CJgAMakDswIWAoyBj2BiQGgoDXioBj9ONAg8ArEAgUrAbw15B8BQsDj0RAhsCkYLyD+pvINVdwMvAIBGQIGApecQejAToCb4iBgYKcgWsNmH8NuVxBhvCbYOBv9s7ohGIYhoHZpvtv+Hj0wxQRXFpoAncrHCGSHVuZ3IJphk8djJ5IB+BqA6oSCACL7sC8BQtAVANhBAJA44OiEkgAXx2APAIBoKkGwgkFgN4GlAouFIGUgdLB3gj0AKY2YBcVnOtgGoEeQDrh7W3AUyOQXvg1gGM5gEMAXwEoFdwWwJ+AAAQgAAGoAgK4C0AnaC1gNWg/wI6QPUG7wr4L0F+G8G+Dvg6z5wPwEyLOCDkl5pwgflLUWWH2tDh+X8CNEXeG6Ftjg743iN8cdXcYvz2Ozw8wQcIMEXqKDD5HyCQpfJYYPk2OnidoouQY9ExRfKqsucL4ZGmzxfHp8vj/Bfxh4sfeGau2EQVR1CGQwo1x48KNCbgzuEmX//+woJz39uhmNCxovUtAmj84Z+7clap382+M3F+Zufl3hu4vTT3c+ltjN//a3P29wZt/cfLh4dbfHL35V2fv7w7fX56++bfH76/Pt0dwbuC9N3BJATFQQh3hx/Iv4ff87+f8Gw4AAe0RWIQXDUQIuAMVkAMkMAEuPPQTn/TH+i/yW4DNASBgwxFYhOsGUkE4QIIaRAde+n/xV/ktwA0H4LNTHkHUwGUDVuGi4HPcwdtQgAMkoIGZ6MBDP/HfRvo/F3zr7zK/BeABRANecQTWQG/Aj0GEgBQYAyRgIQZ24F0+24/1W/8tvwVQD2DrEViEGuA34XIGhkAFOkACFpzBDrz04rv+Jf78/pPfAtxwAE4ewZoBi8AQFAU40EKM7NAXfNfv+a/xX3MACsgaaA3wmzCLIBRQhzrAQpnJLj3VF/hx/vz+bfmzABBwzRFYA34KqgGLwBCogBgogSnkwkMf+K7f809+PwBZAN+v5M8a6A08TQOcAU0QCsIBEvQQ5MBLn/hcP/Gf/E8d/7YCyCOwCPkYLgZ+TgMWgSFQgQ6QgAVH9gEvvfiu3/OHn/6Hnw+gBZgHsK0GGgP8L5hnQBOkAh0gAQtlYAde+sTn+mf8+f3f8LcFsN3AYzVAERiCUEAMTg6QgIUysJ/goWf5ge/6Of/K//i1/KUIGwN5BiqgC3CwSMACIn4NbshhB37Sc/viZ/wb/q0FmBHoDPCbcFZhhoA7GAp0gAQsMGLDPuClH/ikP9c/64/ffw2/ArYa8FMwvwUa4AwIwelzgAJTgAMkYGGO2LADv9C7/RM+5c/6ib/8s//9AGznzxpoDHgGdmEqCAdIcM6whQ/6xLf9jH/D/+Nr+FcMUASeQSqgC3DALWBBEQkOO8kf9Nx+4ht/zn+df5cMWIWcQaMgHJCE8JDkbD7oG3zib/3tx79mgCKYIZh3gIJxCThAAhbwwCzksAMPPdkf+DP9c/2c/378KuBj2BigCGoIVKADJSjit+DCSy9+XT/nX/n9ACJgFwP8L6AIDEFRoAMkDAuKCHDYB7z0Bd/1c/78/t+Nvxp4HgbOzwAFGEDBjAEOpgQsKEJw2Cc89MvywYcf/LP4T/7nXfgtQg2UMzAEKjAGOHj9wAIahgjBQYf94xV6ly++6y/xl98C2N9AhCAUGINxC1hAQxnQYR/Jd/mBH+vfmV8DfguyCAxBKDAGOEACFtBQ53WyAw+9yxc/1p/nb//Dv4OBrEJC0CvQgRLQEDPRhZe+wXf9WX/y72rAM6ghQAGXoAMkYAERzgCHHXjpyT74Zf3Gv/AfYcAQVAXEQAdIwAIeHMgHO/DQu3zxy/qP4K8G4gwIgQpepgId/JWABTw4ksMOvPQT/0V81h/x358/DRiCuINQEA6QgAU8OIMcduCDPvBNf6x/f34/h55BhkAF4xJ0gAQs4MGRHHbgpZ/ZFz/Xb/zL5+/L+dMAIWgVhAMkYAERMYDDDnzQt/isP/l3NuAZGIJUQBekAyRgAQ+O5LAP+KTn9hOf9R8V/1oENQQqMAY4QAIW9OBADjvw0Lv8wC/rl/8wA4agU0AMyAESsIAIR3DYgWf3LL/Fd/1H7b8PQSgoDpCABUQ4gsMOfKEP/Gb9x8y3EoKiIB0oAQuIcASHXfikr/jHr7+egXegAmOAAyRMC4hwBId9gYfe5Ytv+o/l75sgFBgDc4AELCDCERx24Nl9LF/849ffG+gUpAMtIMIJcNmDvsWXf93AkQp0oAQtIMIRHHbhpV/BP3Q00CgwBjhAAhb04EgOO/DQx/Ib/KPXX0OQCoiBDlKCIhzBE156lp/4R69/PQQoIAY6UIIayoie8NC7fPD/k/X/6b6OcRiGYSAI/v/XKa4YkIqUdKZ8CNLPWhbgYwINRJDBDB2e/hv/8ccvQXkRWgMRyrrb4Fd9Dv8ofkuwNhBBBzNy+K6fyd8mSAMRVDBjh49+xx9UIJ8HEtQGiZAK2RYee/BNjz/i7jucAg1EUEEHI4+94OlnHn6HIAlag0RQQYcy8tjhq37e4S+TQAMRZDBDr/jowx/79JcEGoggw27o8PR38FuDRFDBNm724C/Ua1AjGKiRr/j79G5EDVQ4jb3rJ198mwRpoIKd4ez36k0EFQ5jv/LgG37+y37J4e9+9BbJX4N/jb5J/pC/zG6d1dXvphdhfvVr8iH5BwNfr46hwrnWAAAAAElFTkSuQmCC\");\n  -webkit-mask-composite: xor;\n  -webkit-mask-repeat: no-repeat;\n  -webkit-mask-size: 100% 100%, 128px 128px;\n}\n\ntutorial-tip-background.visible {\n  pointer-events: inherit;\n  opacity: 1;\n}\n\n.tutorial-tip {\n  position: absolute;\n  width: 13px;\n  height: 13px;\n  border-radius: 50%;\n  display: inline-block;\n  border: 2px solid white;\n  cursor: pointer;\n  pointer-events: none;\n  z-index: 100;\n  transform: translate3d(-50%,-50%,0);\n  background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(241, 170, 211)), to(rgb(185, 59, 255)));\n  opacity: 0;\n  transition: opacity ease-out 100ms;\n  box-shadow: 0 1px 2px rgba(0,0,0,0.2);\n}\n\n.tutorial-tip.visible {\n  opacity: 1;\n}\n\nbody {\n  background: #000;\n}\n\n.tutorial-tip.visible:after {\n  pointer-events: none;\n  position: absolute;\n  width: 100%;\n  height: 100%;\n  border-radius: 50%;\n  content: '';\n\n  border: 1px solid rgb(185, 59, 255);\n  transform-origin: 0.4 0.4;\n\n  animation: sonarEffect 3s ease-out 75ms;\n  animation-iteration-count: infinite;\n  animation-delay: 1s;\n}\n\n@-webkit-keyframes sonarEffect {\n  0% {\n    opacity: 0;\n  }\n  60% {\n    opacity: 0;\n  }\n  65% {\n    opacity: 0.5;\n    transform: scale3d(1, 1, 1);\n  }\n  80% {\n    opacity: 0.4;\n    transform: scale3d(2.6, 2.6, 2.6);\n  }\n  100% {\n    opacity: 0;\n    transform: scale3d(3, 3, 3);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/undo-toast.less",
    "content": "@import \"ui-variables\";\n\n.nylas-undo-toast {\n\n  .undo-message-wrapper {\n    flex: 1;\n    flex-shrink: 1;\n    margin-left: 16px;\n    margin-right: 30px;\n    color: @background-primary;\n    overflow:hidden;\n    text-overflow: ellipsis;\n    text-align: left;\n  }\n\n  .undo-action-wrapper {\n    flex-shrink: 0;\n    margin-right: 15px;\n    white-space:nowrap;\n\n    img {\n      background-color: @background-primary;\n    }\n    .undo-action-text {\n      margin-left: 5px;\n      color: @background-primary;\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/unsafe.less",
    "content": "@import \"ui-variables\";\n\n.unsafe-component-exception {\n  padding: @padding-base-vertical @padding-base-horizontal;\n\n  background-color: rgba(255, 0, 0, 0.1);\n  color: rgba(160,50,50, 1);\n\n  .message {\n    font-weight: @font-weight-semi-bold;\n    -webkit-user-select:text;\n  }\n  .trace {\n    font-size:0.9em;\n    white-space:pre;\n    max-height:100px;\n    overflow:hidden;\n    text-overflow: ellipsis;\n    -webkit-user-select:text;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/components/webview.less",
    "content": ".webview-wrap {\n  flex: 1;\n  display: flex;\n\n  webview {\n    display: flex;\n    flex: 1;\n  }\n\n  .webview-loading-spinner {\n    position: absolute;\n    right: 17px;\n    top: 17px;\n    opacity: 0;\n    transition: opacity 200ms ease-in-out;\n    transition-delay: 200ms;\n    &.loading-true {\n      opacity: 1;\n    }\n  }\n\n  .webview-cover {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background-color: #F3F3F3;\n    opacity: 1;\n    transition: opacity 200ms ease-out;\n    display: flex;\n    flex-direction: column;\n    align-items: center;\n    .message {\n      color: #444;\n      opacity: 0;\n      margin-top: 20px;\n      transition: opacity 200ms ease-out;\n    }\n    .try-again {\n      opacity: 0;\n      transition: opacity 200ms ease-out;\n    }\n  }\n  .webview-cover.slow,\n  .webview-cover.error {\n    .message {\n      opacity: 1;\n      max-width: 400px;\n    }\n  }\n  .webview-cover.error {\n    .spinner { visibility: hidden;}\n    .try-again {\n      opacity: 1;\n    }\n  }\n  .webview-cover.ready {\n    pointer-events: none;\n    opacity: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/dropdowns.less",
    "content": "@import \"ui-variables\";\n\nselect {\n  // Set `border` property to allow styling for `select` controls\n  border: 1px solid @dropdown-default-border-color;\n  color: @dropdown-default-text-color;\n  background: @dropdown-default-bg-color;\n}\n"
  },
  {
    "path": "packages/client-app/static/email-frame.less",
    "content": "@import 'variables/ui-variables';\n@import 'ui-variables';\n\n.ignore-in-parent-frame {\n  // ----- Font Families -----\n  @font-face {\n    font-family: 'Nylas-Pro';\n    font-style: normal;\n    font-weight: 200;\n    src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Thin.otf');\n  }\n\n  @font-face {\n    font-family: 'Nylas-Pro';\n    font-style: normal;\n    font-weight: 300;\n    src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Blond.otf');\n  }\n\n  @font-face {\n    font-family: 'Nylas-Pro';\n    font-style: normal;\n    font-weight: 400;\n    src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Normal.otf');\n  }\n\n  @font-face {\n    font-family: 'Nylas-Pro';\n    font-style: normal;\n    font-weight: 500;\n    src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Medium.otf');\n  }\n\n  @font-face {\n    font-family: 'Nylas-Pro';\n    font-style: normal;\n    font-weight: 600;\n    src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-SemiBold.otf');\n  }\n\n  // Pro-SemiBold doesn't render emoji properly. Override the emjoi unicode\n  // block so that it uses the \"Normal\" weight even at font-weight:600.\n  @font-face {\n    font-family: 'Nylas-Pro';\n    font-style: normal;\n    font-weight: 600;\n    src: url('nylas://nylas-private-fonts/fonts/Nylas-Pro-Normal.otf'), Helvetica, sans-serif;\n    unicode-range: U+1F300-1F5FF, U+1F600-1F64F, U+1F680-1F6FF, U+2600-26FF;\n  }\n\n  html, body {\n    font-family: \"Nylas-Pro\", \"Helvetica\", \"Lucidia Grande\", sans-serif;\n    font-size: 14.5px;\n    line-height: 1.5;\n    color: @text-color;\n    background-color: transparent !important;\n    border: 0;\n    margin: 0;\n    padding: 0;\n    overflow-x: auto;\n\n    -webkit-text-size-adjust: auto;\n    word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space;\n  }\n\n  strong, b, .bold {\n    font-weight: 600;\n  }\n\n  body {\n    padding: 0;\n    margin: auto;\n    max-width: 840px;\n    overflow-y: hidden;\n    word-break: break-word;\n    -webkit-font-smoothing: antialiased;\n  }\n\n  a {\n    color: @text-color-link;\n  }\n\n  a:hover {\n    color: @text-color-link-hover;\n  }\n\n  a:visited {\n    color: darken(@text-color-link, 10%);\n  }\n  a img {\n    border-bottom: 0;\n  }\n\n  body.heightDetermined {\n    overflow-y: hidden;\n  }\n\n  div,pre {\n    max-width: 100%;\n  }\n\n  pre.nylas-plaintext {\n    // The default font for <pre> tags is the system monospace font. We want\n    // themes to be able to override with monospace, but not have that be the\n    // default for normal people.\n    font-family: \"Nylas-Pro\", \"Helvetica\", \"Lucidia Grande\", sans-serif;\n    white-space: pre-wrap;\n    word-wrap: break-word;\n  }\n\n  img {\n    border: 0;\n  }\n\n  search-match, .search-match {\n    background: @text-color-search-match;\n    border-radius: @border-radius-base;\n    box-shadow: 0 0.5px 0.5px rgba(0,0,0,0.25);\n    &.current-match {\n      background: @text-color-search-current-match;\n    }\n  }\n\n  .inline-download-prompt {\n    border: solid 1px rgba(0, 0, 0, 0.09);\n    border-radius: 2px;\n    color: @text-color;\n    background: @background-off-primary;\n    margin: 4px 0;\n    padding: 4px 8px;\n    cursor: pointer;\n    display: inline-block;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    overflow: hidden;\n  }\n  .inline-download-prompt:hover {\n    cursor: pointer;\n    color: @text-color;\n    border: solid 1px rgba(0, 0, 0, 0.16);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/index.html",
    "content": "<!DOCTYPE html>\n<html style=\"background: #fff\">\n<head>\n  <title>Nylas Mail</title>\n\n  <meta http-equiv=\"Content-Security-Policy\" content=\"default-src * nylas:; script-src 'self' 'unsafe-eval' chrome-extension://react-developer-tools; style-src * 'unsafe-inline' nylas:; img-src * data: nylas: file:;\">\n\n  <style>\n  .application-loading-cover {\n    position: fixed;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    z-index: 100000;\n    background: #F6F6F6;\n    text-align: center;\n  }\n  </style>\n\n  <script src=\"index.js\"></script>\n</head>\n<body>\n  <div class=\"application-loading-cover\" id=\"application-loading-cover\">\n    <div class=\"application-loading-cover-centered\">\n      <img style=\"width: 150px; height: 150px; margin-top: 90px;\" src=\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAALUAAAC7CAMAAAAUhy/rAAABX1BMVEUAAAACAgIBAQEBAQEAAAAAAAAAAAAAAAAAAAD///8BAQEDAwP09PQFBQUBAQECAgIDAwP///8LCwv+/v4BAQH////////////+/v7////+/v7////////+/v7////////////////9/f3////////////////9/f3////////5+fn////////////////////39/f///////////8SEhL///////////////9oaGj////////////////29vb////////q6uozMzPh4eHZ2dn////////////09PSBgYFNTU3///9ISEjw8PD////MzMz///+Tk5O0tLSXl5d5eXkzMzPt7e3R0dHy8vL////s7Oz///+bm5v///+xsbGrq6vp6enh4eHj4+PV1dX///////9fX1/////Gxsbi4uK8vLy8vLz///////+6urrKysrS0tLu7u709PTHx8f///8ncRFIAAAAdXRSTlMAEg8MCBAGDQoCHBgmFSEfGiICBBYGCg54e3YncgwcHxBVfBkza08YOXl2bmBBKhRzaDxIEXBbEi4jZFJ6Nm9iXV0dVFAsWE1sKSAWFWdLRDAqKSIcGWVHamdhPzBZODNZWU5JRkQcQz9HPjJmRTs0PB5dC4IqRDTyAAAN4ElEQVR42tWceXvbRBDGvWvJK0sokh3lJkdJQwq0ENpAytXSQoFCudKU+ygpLaScbfn+D1pJyUsyu9Sasf/oJHEcxdn5PSO9M7O7ijuTNjXsPHk2PZUFnSfO1rJ4KplsuIddpZQOxulkJVZpNmU6k7Khjqcai7tjG3UQK6XiqbzXmYiZErZIU6XSNInJxSijtuGehCqH+VScqiNL46liODZqa8kEVDnMMjCrJjrdcVLTQMih40wRw8Uop0Ygxmi3CHTjZXuM1ONWZTCVKqflU1lvfNSNKscE3b+KkWm406GQmgRiPKp8lCm/FVNZdzzUUCUCIbB7NNRyVYLaEYgxqHLmw8TDi8tES6gnkp7mrhItUlXGPTb1ZNLTJhlXokpKPZn09OCx1E1FjnjUk0lPqyNRqxR9sphanp76T2Fchiq51FClnFquSkotCYScGn0yg5oZCDk1+mQeNV+Vcmr0yXJqqDKaIDWiY8TU/vQkp5ZrH9QyVcqpoUo5NVWlnFquSlDLVCmnRp/MoJY1DaB+LWFho0+WU0OVvVGpL7+bg5uhSga1XJXr7y9ufZegx2Z4YVHLOtjZwd3XF37ChIbhhUEtVuXMSxffeOX5+0nBgIYXNjV3XtmfXTl/dv6L62xVZhGDWl7FptdefH/jmTt8VSZDBrW8is2sv/X94iWJKhnU8irWnxv88fXCT1dJuGUVeXAdgRD1ln5Vfnbq6ef/HK8qV88iPcl6S78qV789+9wXpFZKTupLF5CeGHqJR1XlC899mRBu9jR+ZuVbkp4mkLzXz7y6+MHngpPKSE/ylmp6bunCywvvQJWykypPT+60OiRePjpdqvIHfhJUjvT0+4JElZpSk4TeX149d/nNK3xVTjvSE2kaZGl1dgrax8V47dkX5j9lqjJee0x6kquy/yB2JfTZlTNvz3/F0378oO9rGr5kqhKLgpiBNZtqVJXPvMtRZfxU35eeoEpmSwVqcqzxslmqcuvHJBVTIxA/f430xGqpQO3V/vLg3OsLV6B9NjVUeZrXNGADDdTeacn0zsVnX5n/NUnE1FDlubPPfcNWpVUgqL2LBeWE4e35j68nbOqAqvJZNA286ttQQ6kuVX6y8YydxrOo+0hPwqYBquwfo/aq8q33oMqW1MsZSU/ipmEO1I76CVXaafztDxMG9UqMQEibBpQDUHvqJ6bxi6jIo1MPYqQn0jS0UyWGBzWpn9xpPKVGepI3DaB2LxYw+2RK7U9PUKWYGosF3D6ZUiM9UVXapoFDHYYqVKr6st9CqNLXJ4d4XfW34dEf2rEIdfUKryq3SlU2I4ZHY2Hw5hmltq6sHT2p/iyxqqR9clWR8Vr7rSY/NEJ9OCD2/GkrX9M13mv2w5HDEwZql9noqKGvTw6JuakxujsQ0zu1KgvvcKNSw0uWBZ5pfNGGGoHwNQ22lRdTw4uvIn+XqPbU1WWSeVt5ITWsUSXtk98sK3I7agQicbTyddMgpYab/H8q8ujUME/T8EvVysupqSppRWZQQ5WklYcqhdR+VUL7Lagfq8qyVsqoqSppRYaXUamhysjXwQqoW2ifQR26m4ZN6EVM7VflZlORW1JDld4OVkqN6FRTezqNfxPab0Htr5X1vFJETVVJtf9COY0vGNQeVTarPUpODS/+aXwLaoxYVKqkVcz2llJqqko6jb+aMKihSrpFdB96cVBrrUMdhtp+t2Z/rH+yH0dHm1+p3D+NV9Uox6ntMPazHrAeo/mon1u9aNcarD2BNag+8q41qFtZqGyfTCtyNY3XlPrxdqhKOuN7NynIq3nU8EKn8b8mKYNah03TQGd89gQKqeHFo8r6pI5ODbOqDFzrMLalklKDO81c0/hP7DS+JTUCUQwdJ9C2VCmlzgsWtspdFdlO46+3o8aIUCWd6JygXrpSJnQet7NP/uP3VznU0AtNq7bZKY5Rd1ZO2YSuOeaZxp9ZZ1FDL3SiY1sq9V/q5WvVplqqGRY6p/Fzs52W1LCqg6UnsG6pQI3FglAzTKXQPoxHDb34Wqq0oT5K6OUxbrihSik1qhhVZbMwcESNTTW5KuXUflVeKDfQPkxAjU01zTGVZ3lPQk1HjLN06FwYuJ/kDTU21X4oFM9Lo0pKbVMWa0TbW9INNKvAmhrHrFJ5XkKPKgcP7zMDESZZ4mypXl3qHz9WKbVIWV5UjlQLW3+jDA433HHmaqnON+UASq031YqQ66VLtL9kdxgY6cl3AvtzO7Mdz/I900tCVdmkJ65ecAKJ0eV7viqz7aF7z59/mRgXJ13Cs4sFfFXGgXvPn6/KvEehPYsFn/JVOXTv+fNVSdPqo8y7fM9UZbxsx6B7/s9w0xOdgvQ3Y+VdLLjF8pK77iCaEaWnk1OQ/iDXYZ4lnsUCzsWIkkvSk0iVaHbsDKw+5lssSI3W2pSf9qv+qE3r5rF+gX2ofwtq5w5DoZrB7GuBZeox6keMWr2q+l1qVQnqynma0WnJTrVYkCrTzkDtWTFNDcfsJZEOQY1j7sWCVJMhONTYYbCBYBiqr6XGMbpQ+GKtSjLCqNSePf9WgQC2ti3VITWOeRYLvktDA2tDnbn3/PdJIEYP9/CIGsdcd+LYPX/iZSTq2SxLPHv+3MskftRQwxql0ntjF+/DSwvqzRiBIHv+vHDnDypqolTfnn+qW1MPcmd6mt5hqBLDW2p66cSB+06cQjGokZ5o0wBVSqmNbbOce/5QZRtqpCeqSpueeNRJ13SrT3P0aMpwG/e9sbfSbmmmeXG3+nbiMTlGbQ8brW0g3Hv+adit7GjECuLomKmhYElD7TCdxknP/Q81tRe/naRu/Idx7Nnzv5d22xioqRmdN9MS2ien7agRiNxz+26qhNQwqJLcG5uqdtQIhPLs+6VaSg0viUP7601Fbk/drfQSeVp5JafGxeiuyKUqFYO6a6BK0sqnWkgN00XsrchhO2oEwrhbeahSRA1VeioygxqqdOz7pUpODVX6KzKDGqp0tvJianjxV2TTkhrpydPKKzE13OSxU5Xw4qP2jK6TOHWr8jelhdQwU8S+u/CUZlAjEI5aqQTU1Iv2eeFQd42radi5CL3IqFEict80PmxHjUBsO/XywefKyKnhxaP9m6olNWpl4KtiUmpY+D8VmUENVdLVnj9VKKaGKt3aX7wEVbaghipJFStnfEpATb14pvH3FIMaqqRrsHegShd1FEXlgz1UPWme1j/WT6Po6PdGOScMVpVK26GKk9RRafUgeP7fwU0ZCGdvaU9gFJ2gKyrqImplljuptU9vr9pT3ZPUI42o8rznnPHdvqei4wbq1tw6zz3/UJMqBnUEvbhOoJQaXlKHl/Uz5eLarmpBDYMqyQks06qD2kQMc7cmT9mbHq+2osaIaVxQVTYt1UnqpTv2GIdbeabxDwk1Vy+Y6Ch9nHr17TKh45hclefurrSnhioDX7PzX+rO2plysWAP4eZ6wUndXO4wqKFKz/1xKgT14WKBPQYTeUGkQS3QC6YgqttQY1PNHguiKKi/Amv1N/vKIKqe42BzoE6CxMsJ6noYDG4freHHCEPbE5j03C3VDVW+oqbGsTDgWOkl7Xmpi7bDWW6nKm1L9ZsyQWqp0WZVxwKeFz2UU8NQxcgG2p46pMZiwZ4KWF5Ce1Ld1MxANFWM3h93B0UMiwVK87wUsRq6qPeZgYhMkpuOq6W6DWq8+0ZZP7leIkq98n2ZnjSL26fKC6t9x2LB/jhVOXe6Tk8tx8IJpMl7Za7jXCyQqLIDk6enyDhUOY1Qk/rJ8EFOqjw9+dOqa/l+8e9tppc03x66U1YY8EYscndadS7f32R6MUkydK9lbvNUGenEnVbdiwWK6SV5VI1C0tMHN8Oo3VA4gcoTbrpYsLUbsrSfbvruxNm/oQOWdRN/s0PffeMe5zJByaX/VfgbLxBBNIoqsXwfGhE1/a/CyakS777BUSWoaXoSqjIY+U00D5QWUdP2nq1Klaejv4nmrrYXY89+NY/WeuU3+9l82IfqM1Aeavz7S6XKXj1Y84eHPzcHe9YOfw1/3SQxo7+J5g3dG90Itatp0N0exwKdtHgTzb/gRUSNpmFPs7BRDkZ6E014EVFjh+FvZrhRDkZ6E81tgz+VUEOVOui1NzVwD++7N3YXXiTUaBr2t+kJlA9P+2QtG5buLupoMtTok6FK+bBoGvQEqdEna/GwSE9Q5WSocW/sl/AiGxb7fnqC1OiTdSAZli5QH2ybCVKjTzaiYentu0hPE6BGn6wjwbD0f30Xbo/cNNxaakeNPhkVWUSNpmF0VUY3B+2ZcRcevAio0TSMqkr91yqPGhVZQE2bhtFUGR6sM6FRkY2cGk3DKKoM9n7eYTKjIsOLhBr7fo9tGvTB6Vk2MyoyVCmkRisf+KG7N99bnRZAoyKbSEJNb999uGd80MH2w2/nZNCYxhsRNb19947xpCf9jiDUVPtdETW9fXfXOCeN+6+fXpZDYxpvAhk1bRpIuLs3rmyc32GPTafxtiIbGTVt5Y/rJTC7lzbObUquD1qR4UVIjVb+i11jgkNks/tw4fL5j+TQdBpvBNR0yXtjYetg11i7sXtwaeGFNy6uCaD90/gD3RVTQ5VvnXp5/p9/5re27OPGqTMrs5Jx/dN4q0pQSwOxvHLx7qnLL29svHz21N3PVubGF2j6Jpo3uqAWWn9mbX3w4rVrS4P1tcfEWa59Qi0LxsxMiyDztb9fUj9ZZqfxz64+adR2wjBY6zx5No3rcFL2LxbFEOJzdAcFAAAAAElFTkSuQmCC\"/><br>\n      <img style=\"width: 214px; height: 26px; margin-top: 2em;\" src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAa0AAAA0CAYAAAA5SGcIAAABS2lUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxMzggNzkuMTU5ODI0LCAyMDE2LzA5LzE0LTAxOjA5OjAxICAgICAgICAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIi8+CiA8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJyIj8+IEmuOgAAIABJREFUeJztfXtwncV592933/e85yKdo3N0tyxZliUjWzK2sWVjXS3JVo0NBptLgdqeDpROgWBSmoRMmyltmUkhExpS2mnppHSAZDJfO2mh7WcgaeLgW7/E9oADOLZBAXy3bMm669ze3e+P3T06ko+kc3SzHM5v5oyko/fdy7PP7rP73JYIIXA9QQiZlXpqa2sdAKz47w4dOtQ3K5WncUMhVV6ZKd66EXn2RmzzXENtbW3mqK9Chw4dCo/x7JToPdX1f6rr92TqN6ZU4w0EQkghIaSRECIAQAgxAOA/rnOz0piDIIRUE0Kq4niF1NbW/p+xFo6Z4q1U2zEXkJ5nUweldAMhxAPIMRdC/ALAqUTP3og8MlV8YYQWpRSUUlsPLuecX+82pTE3QSkVo3iFjrejnCneSrUdcwHpeTZ1UEo5pdQGpBDinI95HLkReWSqMACgvr5+CyEkoL8UQnQdOHDg/0618Lq6ugWU0sa4cokQ4j8OHjw4q+oCIQRpa2uDbds2pVQAgG3bthCC6MH+oqOurm4LISQQv2MTQnQdPHhwynwwTp3VhJCV8XUCAOd8z6FDhzpnqt7xIIQgGzduFEIIm1JqCyEo55zn5+cnXAlmirdSbcdcQHqeTR1CCNLS0mIzxmJCi1KKRDS8EXlkOkABKdkZY7ZhGFHDMKKMsazGxsaVUy2cMRbWZeuPZuZZBlu7dq3HMAzdDp6ZmckAuK5DW+YkRo+T5oP6+vrCGaxz0eg6VTuu5wJnlpaWOjWfaJ59+OGH3VqojsJM8Vaq7ZgLSM+zqcOVmZnJ9Jgzxuzq6mo3EmvFbkQemTIMAGCMccMwok6nMwiADA4OOjnnlY2NjRf37dt3YbKFU0oFYywat+ui09TuVGGWl5dnHT58WDDGuBCCeL1eA0AmgMHr1KY5Bc0Dbrc7aFlWtLu722PbNrVtewmASfPAWGhoaChhjLkNw4j6/f5+ANB1RqPR6a4uFTg3bdrkbm9vZw6HwwEAvb294YULF/oB9AAYLVBnirdSbcdcQHqeTR2ZXq/X6Ozs5IQQYds2rays9ANwAIiMevZG5JEpwwBgZWZmsnA4bDscDtvpdEYjkYipFo7VjY2N7+zbt28yRj22bt0699GjRzmllAMAIUTcd999bgADmF2C0oyMDCchROgjtmmaDJIRvvAQQlhbt25l4XDYNk0zallWJDs7u6+rqytDCJHX2NgY2LdvX9d01ulwOMoJIbZlWRGHwxEFAK0SaWlpcb/77rvdhJDrYQ8x7r77bgNAKYAs9d3nGNv+O1O8lWo75gLS82zqcJimSQkhUU1Hj8fjBMASPHsj8siUQQFk5uXlWZRSYRgGN03Tzs3N7VHqITeldM0kyzZramqylbqHM8Y4pVQsX748oOpNY+5gNA9wy7Ki6vRlOxyO8umsrLGxMQAglzFm+3y+AYfDYTscDlvzyIoVKwJIPEnTSCONLzgoAMvtdhvKY4UbhsGFEMTn8w0qHen8xsbGySxaNCMjw6V3C2rnJTwejwtpoTXXEOMBQoiglArbtonP5xuilHLO+cLGxsaM6arMNM2lWg8fDocN27ap4hEQQnhWVtb15BEOIAypzhpQnyEA9he0HWnMXXwheeSaY2RnZ6dHCAHOObUsK6K8yJYr+1Z/CmULcb0jl9OYFDo7Oz2AtEEquySnlJYDeH+qZTc2NmYIIYoIIVFKqYhEIqy3t5fNIVYJA7gIwATgVt91AUiF93+b2pHG3MUXkkdG7GZt22YAsHjxYichRCh3S84YMyil9deniWnMJiKRiAEAFRUVTkopdzgcEeVIU9bQ0DBl24TD4SjXakBCiLj77ruzKyoqnFNu+PQhBOAMgPcA/D/1+RBAN2bXDjtX2pHGLCMjI2mlxheSR0YIrVAoZADAnXfemd/W1uYlhAht12CM+ZqamqbsBp/G3IUQgkQiEQYADzzwQBEggxWVI41hWdaCqZSvhF6ZCojkALBp06ai+FPW9XbVJYTYhJABQsgVQsgl9blKCAnNZqzRXGlHGnMXX1QeiakHlRqQaJXezp07Fx09evTDy5cv68A1KDf4C/v27bs4nY2ora1dAMDSkdxCCBw6dChh2pIJylk86qvPpjOdSW1tbaFKU5MBIEP9DAsh+gH0CyG6hBCTrrOurm4BgEJCiC47Q5Wry79w8ODBz6erP4mgeSAQCDh37txZ8Oqrr3bozUs0Gq0E8PFky1ZCzyCERAGgpqbGk5ubm9HZ2WlPRVhN97io3G8j4tMmw49TxVTaMRd4abKora11qPEsBBBQ7XdgeEy7hBAXhBAXpjK/Z3o+x9WTSQhZQAgpiBuLsBCiXwjRBeDzgwcPTiqsJBUeSfBsH+LCWerr6wsJIRUYpgXiaHHxwIEDU5oDiejQ2Ng4gg4HDhyItaeurq4QMlxC48LBgwf7rrFpcc4p5xxer9f66le/Wva1r33tYyGEUE4aNoCaZN3g/X5/Ui7LlNIMQshKDB9pSV1d3YVUMmfU1dUVKk9HAYAIISJTJXJc2Zkqc0OxchaIufRCBk66hBB5QohFQoildXV176WyIKjMIVUA/Fptpv5lx5fPOV/U0NCwknN+/ODBgzO2iHLO6dDQUKS+vj5PCy1CCCeEuDZu3LjgJz/5yaQWOyFEpWEYXG9O7r777kIAIc45VcIypfJmalwIIYFRvDSAMXK/zSQm0465xkupor6+vkrl0jNHjacN6VHqE0JkqTGN1NfXf3TgwIGPUqljpuezRm1trYNSWk0IWTKqnhF9UWNxkRCyHwAyMzOT3sClwiNxz2p8DuBCQ0NDBiGkkVI6mmeAYXoXNzY2LuWc/yJesEw3HRobGy8KIfbv378/zBgrB7BA941z/ksAfSPUg/q0FQwGQwAGlyxZkrVr1648QogOFLan6AafEISQj1VGBFtH01NKi1Mpg1JaEh8Zzhg7OR1tq6+vX0Ap3WoYRpFhGLZpmtGxPipMwMUYq29oaGhIsvwqxlg9Y8ynyrDHq0eVX1NfX3/LdPRvNDQPRCKRYH5+vtna2poFxHKccc75ksmU+zu/8zuFANxK+Iny8nLn0qVLnQAuc85FqietmRyXOF6PZSWYTJ+nilTbMdd4KRXU1dU5GhoaNlNKVzDGmGEYsXY6HI6I/owaU0YpXdHQ0LC5rq4uKXvrTM/nuP5kMsY2M8YqdT3x/RjVF5sxlkcp/Z329vaUQj1S4ZG4Z2MZS9avX5/BGNukeWY0zRPQoqWhoaFiJunAGNvU1NTkiOuTzvbBgVE2LaUCJP39/f0AzgIY2Lp16/zy8nJLe5FN0Q3+Gggh2L59+5jH4+kwDMO2LCuiYoNKUimHMTZfE8UwDLu5ufmyEMKcitqpvr4+g1K6Jn7yG4YRzcrK6qmoqLhQW1v7+U033XQxLy+vizFmm6YZe4ZSWtzQ0DCuDaihoWEBY2y5Sp9lm6ZpOxyOoaqqqtPNzc3tv//7v3+iubm5fenSpWdM0wzGl88Yq0yFeZKFEAJCCAwNDfUAuLp169YCAPG7I5+Ks0oJ0Wh0CWPM1qesu+66KxfSYHwuEomkdMqayXERQpDCwkKDMcbjJjEXQsxq3Fiq7ZiLvJQs6urqHJTS2/TCqRcy3ZfMzMyhoqKinszMzCE15tH45xhjPkrpbRMJrpmez3H1OCilmxhjrvh61KLMA4FAf0FBQbdlWWFVj160Xc8++2xZZmYmT8YmlQqPJHrW6/USy7JqDMNgcULDzsrK6i0oKOgeTW9Fa5sxtryhoWFCb5HJ0EHV57Isq8br9ZK4enleXp4BgBjA8IKkPQb7+/sHIL1SYFlW6dNPP13xyCOPfEApFU6n01YLzM0TuMELIbM8xo6bYwyEBaDg4Ycf5v/0T/9kK/sJbNv2bt26NfM///M/J1QRtrW1LeCcM7Uoinnz5kWfeeaZbMj4hW5V92g1wIRMwRirNwyDUEqjisiR3bt3R9auXesE4Id0NeUAgidPnhz69re/Tc+dO2dSCRGNRlc3NDRc2L9/f0JVKmNsNWNMMwK/+eabh5577jkAKIJUU5iQqVuG+vr6Bv7sz/4s8sknnzgJIbr8mzEFG1M8EvBAL4DQwoULK9asWeM5fPhwv0rNEyWELAVwINmym5qaAoSQHEpplFLKA4GAuW7dOi+kGuOyECJ/VBvGLW+Gx8W45ZZbMn72s59FdSoit9ttAPAA6B2DdinzVhJIqR1zhZcmOc+q1C4+ahiGTSkVbrebP/7443ZNTY3b4/F4IdVI9sDAwODevXsHX331VTY4OEhVbCEjhLhs266C9KQbq54Znc9x9axgjFHGWEQnVnC5XOLJJ5+kt9xyi9vtdmepeobOnTvX+93vfpd/8MEHHgDo6OjwhUIhm1IqhBAgKm5yjKpS4ZERzxJCEIlEcgkh1DTNCAA8+OCDndu2bfNkZGT4oe7ounTpUvDv/u7vgu+99x4RQnBCCFde5iswwRowWTqog0YBgIhpmlEhBGzbpkuXLvUAMBMGcEYikSjkSetjAFcKCgqcf/EXf1FGCAHnnKgGGIyxunHaLGzbTkatQgD4t23bVpqdnU115yilNuc8qd2fYRgLKKWxjAr33HNPDqQudNKu1I2NjQHGmE+rLRlj/Lnnnstfu3btzQCWAagEsBjATQCqbrrppuXPP//8Ap/Ph7hjLaOUJjwxNjU1BZiEzRjj8+bNw3PPPVcByQxVqtxyXX5mZuaK559/fpHP54NhGLHym5qapi3oV0MIQUKhUAjAeQBXN2/enAsgNoGEEPNSqVelbOJ68t1+++0By7L0ab6np6cn6bbN9LgAMEtKSnxKHSoopUIlffUk3cjpQdLtmMu8NBHUeN6kd/OMMV5VVUVefvnl+evXr1/m8XiWqrYvAlDu8XiW3n777cu+853vzFuwYAH0yUGdGG8a6wQwC3wTq4dSWqbrMQzDrq6uZi+//PLC+vr6arfbXan7AmBJUVHR8m9961uLdu/ebetxHhgY4Mls3pAar8aeZYxxv98/YFmWbRiG7XK57Jdeegk7d+5clpGRsULRogJARX5+/pJnn322esuWLW6dfELRoqipqWlMjctU6MCUx3qcrOGUUlFaWurFaKGld0e9vb0E0rPkN+rTu2rVqtyWlpYsQkj8IPrWr1+/YiLKToAwZBS3efvttweA2H0yIhqNFk30clNTkyMajWbHGxBvvfVWvyo3PFnXT8uyYimoKKVi69at3uLi4lIAPkivyzBkEF8QUvBmBAKBojvvvDOgnVZUnFPCSeTxeEaU39raGoDcXbgBRCFPiFfUzygAt8vlKmxpackChk8lHo9n3mT6Nx6EEOjs7OQALgO4tHr16gy/3+/QQotSyk3TTEo93NTU5OCcl+rxEfI6hWzVt4tIMZHqTI8LAGJZlhl36hOUUoLZz+eWdDvmMi9NBErpai1sKaUiOzubPvvss1WBQKAIgBfSa1BAjqtQf3vnz59f/M1vfnOp9myNW5NqEtUzC3wzoj9aMALA17/+9aWBQCAfcjwo5BhEVb1eAEW33Xbbkvvvv5/q+ZWTk5NMgHAqvBr/LCzLinLOQQgRTz/9tKu8vHwJpGehR/U/GkfvjO3bty9Uvg2ahtyyrDGF1jTRQeTm5vbp07vL5XIAYONNRBtAJ+RpywfA+eijj5YfO3bs/StXrujjKDjnlU1NTRfffffdybrBRwFcBXCloaFh0fe//33trSYikYhz8+bN2Xv27BnzbqXMzMzCUCjEqLo0rbW1NdPv9w8BuASZ1mRS8Hg8XsZYODs727Asy6ivr8+DVK9chVwAetTfOot1DoBAeXl5pvo/GGPc4XBkiwR34fj9/qwrV67EsmH39/cLyI3CJfXpU+WbkAOaD6Dg3nvvLW5tbc0Nh8NXL1y40H7+/Pm+PXv2TLabY0JdPNcNqSYueuihhwpeeOGF01pwqdROxyfyIjVNc6k6ZXEA2LBhg9fv94ehTnEYTvQJYEwVcgwzPS43IuY6L42FpqamAsZYlhZahBBx//3351uW5YIUIn2QgmQwrv0eSJfxzKysLM+uXbvyXn/99Q7ddyFEbqIEz7PBN/H9UZs0PProo0UqLVlI9acPcl2yITVBGZBj4rn33nsXvvPOO+39/f18cHDQMZO82dvb6ySEiAULFhhr164tgxRO3YoOA5DrskO1LSs/P9+9evXqjCNHjvQ7HI4o55yaplkI4JMZpEM0ER20TSu2WIx6IALpx38KQKZlWfOfeeaZiieeeOI4pdQ2DEN7fa1qamr6ybvvvnvNAqbKi5XNRt2VpHbffQAuFBUVlSj7SR9jjHPOuWEYJZDCcyws0gxPCBFtbW0B1eZOSMaP1aPbor0hxypQCEFVGSWQR2UP5A7scwAn1f80wSmkzaAAwOKenp5MVR+nlIIx5lF0jsSX/7d/+7eRt99+O5b9/uc//3nvli1buubNm3cCcpKEMewSakEuNEuysrIqsrKysgB4KisrBwB88nu/93tTXnzjeCDerhSEXPQ61q5duzgnJ+f8lStXIowxcM4Ny7LKARyfoNyFeicMyMB1yBPceUXD2G5f1z1WWTM9LnFtTppXJvN8skim3LnGS6nQwuFwFBNCbMMwbEIIz8nJMVpaWgogx/A8pJanA8NjyiAXtwIAZQAKN27cOO+11167RAgRhmFACAFKaQFkOqMYjTALfONwOPLj+5OdnW20tLTkj+qP3kxzSKGQpdpV6nQ6s/7gD/4g/8UXXzwXDAaNCexZKdM7/kQWjUYpAOzatWueous5AO2qnf2KFg4AAQALASwsKipy6GtnGGO20+m0hBCUjLqNYabpkPCkpY6YgBQ2gwA+g9x9uEtLS3Mff/zxon/4h384B0AYhmELIdxCiBoAB8cj8DgYgmTOq42Njf7Dhw/3q3bwcDhchDGMqw8++KDV0dGRbZpmlBAiAoGAuWjRIqbK6htNzBRgQu60tIqFQ07+XwM4Mbrs3bt3Ox0Oh+H1erM++uijEAA4nc7o0NCQw7IsCrlQxDO52dbWZrzzzjsxgX716lX7kUceoZRSs6enh+/fvz9mSFUbgz7IY3sWhg3rWZC7ExNxAno6oJxtOJSHn8vlKtq8eXP2a6+9dlHrxW3bLsM4Qmvjxo3lABilVAcTZ5SVlVFItWAn5G4uFe/OmR6XGxFznpfGAmPMTwjhLpcrFAwGzeXLl2dalhWGXDyPAzgNtYBq1TLkmnUJki+rsrKySisqKpzt7e1BwzAiKvlyPkby5azwDWMsnxDC3W53MBQKORYuXOiyLCsC4FPI9EqJ+mNBbuL6AFSVlJR4hRDnZ1oDIIQgtm2T4uJiN6TQ/hUk3XsxrBokkHO1BwCys7MDABCJRJhSMTohaRuaLB3i6kmaDgmFFpHbbL2Y6IXrE0g1oXvTpk3Fv/zlL3u0RxmXgTZF69evL//5z38eOy4GAoGkMh0QQmwhRA/kjr5AHb85pVSEw2Hnpk2bCt5+++1r1I89PT0l2v4FALfddlvA5XL1q85P5dI5B6SQdqj+D0DuRM60tbW5g8FgUUNDQyYhxA8g49ixY6bW9SpjsrAsKxoMBk3GmGbyeB21o7Ky0rl58+aMt99+uxeQMRSRSITatr2EMVbV1NQUVZHiVxsbGzudTufACy+80Lls2bLPoLLzQzIYxQxc4zE4OGhA8sAAJFNf3rBhQ8lrr712CYCglHLbtp1tbW0lP/7xj08nKoMQUhZvUFYOHd2Kln2EEHH69GkzhQk60+NyI2LO81IiCCHo5s2b3aqtDADy8/MdkIvkccj1ZiieN9TvESHEVUjBSgB45s+f7/z444+Dai3gDofDOeoEMON8o/tDCBHhcNjgnKOiosINuYH+NaRAGEzQn6AQ4gLkAm4VFhZWcc4JkVJtxlKaKVUq9fl8IcjT5seQwil+oy8gT6TnAfj8fr8XMoBZAECcAA/FlZsSHVRRup6k6BAvtLStQjDGgJE7YBtSEJyCFFxlu3fvXvzUU08du3LlStTpdIaDwaAJoFrZt+IHNFn3134AF10u18Jt27b533zzzSsANOMsgGTmESCEzFe2EiGEQE1NTQbkqbALo3ZCauHUzgATtUVAMvYZAJ2vvvpq6Gc/+xk/ffp0IwAYhqHVnfrIHdEGVI/HY4fDYREKhRgAYZomgdyNxIMCMB5//PGCaDRKf/rTn15Vx2nt6EA55wSAl3PuA1AaiUTIE088QXJzc7ubmppOfulLX9JG4yuYvqsIYqoGw9CaY2ILIToBnPP7/fnbtm3LfuONNy7rSc05r4TcOY1Ac3NzvhDCyxiLKhWBuXr16gxIlcxljNzNx6sGr+e4QL+P5Hkl5eeTRZLlzileSoEWTs45ZYxF9aKUk5MDSF46h1ECa1QdXMj0QmcA5Ofm5uYTQgTnnKqfFkaeAGaDbyxl5xndn89UvYPj9EfPsd+4XK78QCBgdHd3a8eTaeM9/ax+Jjs7m7nd7g7Vvl6MFFjxCAHoDIfDQ0r9KgBwp9Op1c3TQgcM+1GMS4dU7iwKQzLTSQAdWVlZ7PHHHy8FAM659vgzGGO1KZQ5uvwrADrXrl3r0x0mhPBQKFQw+uEdO3ZYoVAoW+toKyoqrEWLFglI4dY7xeN1CMBnf//3f39y8+bNA6+++io9d+6cS3nD6ODNqGmaEfU3d7lcV9etW3f24YcfFsrzkRFpw0vkzROFXCTsJ598cskzzzyzcMGCBYZSu40I5nM4HBEd4GcYRrSrq8v7ox/9qGDDhg253/zmN7XtbqZVXHpn2rl58+Z8bYhWi4d3/fr1+aNfME1zifbUAoCHHnqoEHIndxYJxifJ8ZrpcbkRcaPxEgDg2LFjXjLKpdswjCDiTuHjva/+3wPgXE5ODhdCkGg0SuPG1hX3+IzzzXvvvZeoPyGMwe8JEIFc/85nZmbSVILtU4VOIJCZmalV9d0Yf7PCAQRDoVAw/kt16hxBi9mgQyqTVkCehj6F1H27Vq1albN9+/buf//3f7+ivHdsIYS3tbX15p/+9KcnMIFhPR5qAewFcKm6uro4Ozvb7OrqClN5IaGxefPmgj179sROW319fRVaNSiEIBs2bAhAegJdhpzEkwYhJNLY2OiklC7TRse4WIiI1+vtCgQC4YqKiqH6+vrI6tWrCaT6If/YsWM5qg3xdBt9zA9DTrh+ANmrVq0qWLVqle/06dNnDh06dOm9996LfP7552Z/f79DCAHGmC2GExoT27Yp59z74x//uPbtt99+Z//+/dPK4aOZDnJh7ARwsaCgIL+mpibjyJEjfdq2RQhZAmlnAACsX78+QwiRQ9SV4UIIsnbt2ixIFcQlSBsmhBDkzJkzqeRZm+lxuRExp3lpLCxfvpxCrQ+UUti2TT777LMhyAU0WcEZBtDzwQcfDAGAZVn20NCQ4fP5tI0EwOzwzcqVKxni1jshBLly5coQ5Almwv6oeTIIoOs3v/mNWzuVzBTU2ANyzRxK4pVIJBKJCCFGy4wRtJgqHTDsRzEmHWIZMfTOjHNOBgbG9BTnkJ2MucE/8MADC48fP9536tSpoDpew7btioceeujKV7/6Va6ZA0AyXlWDkItaz+/+7u/m/uM//uM5bTNzOp0jVIS2bc+L38mrDMZnMM6uQbcl7oibEE1NTQ5K6SoWF6HvcrmGtm7dennXrl2aobMgPZlckC6bFgBXNBrVNjWM482jhcAZRUc/AFdJScnCkpKS/Pvvv1+7+Q6eOnWq/6OPPgodOXIEp06dcgaDQcoYo7Ztc0qpMxqNLgFwbAK6TgjNA1pFF68iJsMenucAlNx7770FR48e7dMegZzz7Obm5oy9e/f2A4BlWUuUiooTQsTOnTvzLcsaVO93x9kaSGdnpxHPI2KcmToL4xJDsrwy2eeTRRLlzjleSpIWps/nQzAYFA6Hw7Zt2zxz5kwUKcRWKr4MnTt3LkopFQ6HIxoKhVhWVtYIFd4s8U2sP0p4mr/+9a8jAFK5JiT8wQcfhEfVM6EHYSq8F/+smodDkDw0EexIJBKllDL9fgIzEjALdJiMeiQCKVhOAfBalrXgscceK//yl7/8obJzAAA6OjqWdXV1XU6wax8POmarY8WKFbHgVUopHxwcjKmgtm/fnjEwMJBhmmbsigu/36/dswem6nlDCMlnKl+WYRi22+0WL730kicvL28+pLusI+6jHRbCAIZOnjzZK4SAYRicc57QqK0mWzek55IBGRnux7Drpw9ygxBdvHhxePHixaFt27ZFurq6+p5//vnOEydO2HE7mbyp9DVB2wAAfX19FCMZMgxpTL2wZMmSZeXl5a729vZBrfu3LKsSwJGWlhYHgEItAIUQpL6+PgBpzNWuxfH1pXLSmtFxuRExl3lpLCjVP6uurjaPHDkS0Sr+c+fOcSS3gMYjcv78eQEAQ0NDJgBUVlYaiDN9zDTf6P6UlJSwU6dORQx5G4ap+pOKupUfOHDA0OOR4tqZMmzbFpD9TMbLWoy3oVQPzAodqP5SSbXY7xMgCLmz+xhAZ2lpacZjjz02nxACwzC4aZo2IcT5L//yL7nqNBQrd7yyVSP7AFwqLCyM1tTUZGhpSwhht99+u878XsIYE7rcTZs25UA6X1zBGKrB+PonaofT6cxQyTJtxpjYsGFDRl5eXjVkvEIu5G7MhlTL6LiDXwP4lXLXJ263OzpePYQQHWD7PoCjkIvOWchd8yCG4xd0oGNhIBAof/TRRxcxxoSms2marunwMpqIPnE2hLMAuh988MF5emwMw+BCiAUbN250WJZVRillSm2IjRs3+goKCgSUTYwQcs0pONmxmY1xSaU9k30+WSRb7lzipRRoQVeuXOkAQMLhsEEIwdmzZ9lLL72UOdYLifCXf/mXuYODgyCEQNlasXLlSi14AMwa39CamhoLABkaGnJQSsWZM2fYv/7rvyadSo4QIo4cOeKjlAqmEv85AAAUW0lEQVTTNMV0816C5wSG3c6TbeNEdU2ZDgDGpcN4J63xqCUgdZS/gYr1aGtrKzl8+HCPVhtRStHZ2WlSmYpj4NKlS8nmNQtCxWxt2rQp98iRI/1xi2MxgDOc8yLlNQgAWLZsmaXe6SGTj82SHROC3H333YFQKCRM07Q558Tv9+vUJp2QJ0FtQ4iP2A/+8z//s//UqVOLTdNEfPsSoampaev69etNQMbFfe973/tlWVlZJuQu2QepqnBDqiksyIllBQKBTBGXSJMQwl577TUPZsd1OwRJ547q6urFgUDA7OrqiqixsTnnFYyx+QBiats77rijEHJDcc0pCwChwzGB42K2xuVGxA3KS3zZsmX6FoZY0Ou77767GCpZdzI4evRoGVU58TjnJBAImNXV1YA6Pcwi3/Dq6uqYSlLT9L/+678WIoHncyLccccdgfPnz/sdDkc0MzMz2N3d7Zr4rSljug1nU6bD1q1bA5FIZEw6JPQeVAvJRIuJDXmyOQW5qws98cQTi7Ozsw1CiDBNk3u93lB+fn6/mhDJtBdqJ94D6ZBhaU81KgON8++55x5/JBLx6KPjnXfeme10OgcgVYNTic3SYOXl5aCU6rQw5PTp00OQrtqHAewH8L8AjgD4ANKb8tOWlpYr//Zv/1auJ1+cfSghKKVBxphhGAYjhLDHHntsCHKH/B6A/wdgH4C9qr7DkEF5Zy5fvtwb51kp8vLy+K5du6bdCy6R2k5tCLoBnLUsq/++++4rUH3hKsnlIkKIpW1d5eXlrtLSUgNSYF3BtaofQghJ1oN1VsblRsRc56XRUPSPlpSU8J07d+ZprROllPf392e1trYmdZdea2trZSQSsagM1SFCCDz66KPz3G53BMN27RnnG/W9vWTJEtxyyy2xZLWUUn758uV5LS0t/mT6E4lElml1u8PhuC53uE0Fs0UHqgqN7XSIdOtMtp1aLfExgA6v12t85Stf0bYo4XQ6o6ZpcsbYiCtKksAAgEtOp3Ng+/btftUmbppm1LbtVToJIyFE3HrrrV7IHVMXxonkj+/fBG1x1NbWMjKcGkXs3bu3/3/+538+gWTqzwghFwkhVwkhA4SQSEtLSx5jrJFSSnXb4mmaqBLDMK7EX7oWjUarWlpaBCGkhxByiRByBvIkewJS7fMLAEdef/31s/H9qa6uZpC76ClBt1elXhHBYHC0TUtjCFIIdaxfvz5bv+vz+YYcDoedlZU1pMfmwQcfLITcyY7lxkyuXr1qzKVxAVLilUk9nyySLXcu8VIKtAgD6LvjjjvycnJyDKUK0glW17S0tNw8Xj2tra2rDcOo0olsCSFizZo1GTU1NZmQJya9FswW30QADP7hH/5hCZTTgNrI2YyxhpaWlkVj9WX9+vUZra2tGwBkq7u7RFdXl0v3awKVX9K8N1U+JcNxbDFBnuCxSdOhubk5Y8OGDRPSYaq7Kh209xlUtoybbropsH379pw333yzo7u72yooKEjVsApIhusEcKWxsXH+G2+80RlHZEsf1f1+v7l06VID0xObpeG45ZZbHK+88gqA4YH+9re/nf/cc8/1cM47mpqaBgA4CCF+Smk+pbRIX6C2YcMGtm/fvmhvb+/ooLsYhBDWD3/4w54f/OAHsXQuhBBvJBJpbG5uPrl3794zqm4OqZILbdmyxXS5XJ7BwcGoaZq6HNLY2OgC4NAn0mnofzwSnbZsITMSnLMsa96uXbsKvv/9718IBoNGQUFB38DAgAkAOTk5xtKlS92Qqp4OTD010IyPy42IG4iXRiMI4KplWZHHHnus9Nlnn/0YAEzTtFUc0eLW1tZ8zvlZzvlVSLVdhhrXPMaYNz7RrhACDz300CL1XLwb92zxTRBAd0FBAdm2bVvuG2+8cVlttm1DRuqvbGlpyeecX1LzJ0wI8RNCApTSUsMwDMaY7fV6xdKlS13vv/9+P27MkIyU6LB+/foYHRhjSdEhljBXOTXEnBtSwDVu8Pfcc8+ijz76qP/TTz8duHz5sic3N3cglXKJjNnqAXBx4cKFxTk5OWZ3d3fY4/EETdPkPT09Ls452bJlSzakKrEDE8QakDhnE6XHH+tRo7i42PzGN75R9Pzzz58BYhHzNqV0sW3blSrDQExfS2XyYF5SUhLesWNH6f79+z8TQpBx+ux94IEHSgsKCpwvvfTSAABEo1FBCPHatn3rhg0bVnHOY5mtCSFmJBJxc871TlQQQrBz5868NWvWAKkFiY9JnxR4YBAqf+C6desKfvCDH1zQ9A0GgyalFPfee2+BUtuexQS2Rl03IAPVx6h7NsYl1p4keWVSzyeLJMudU7yUAi2GoLKrr1y5smT79u3db7755mUhBFc2JNi27bVtu1oJMaJ323rnrscXAL7xjW9UqDRQXZAu6jobxmzxje5P/9atW0vOnTsXPHr0aJ/aYNuq3YWc86IE9cX687WvfS37ww8/5O+///4ASYKRUuG96eDTJN6fcTqMYFB1I+VkoN3gTwI4b1kW/+M//uNK27apEAIdHR2TuUBPJ9Htueeee/IBYGhoyOF2u6OA3BnW1dX5IBlU3xM0HbABRGtqanw7duwoBGL3e+nLzKLxqhh9RXRpaenAt771rVxKaZa2E4zjiWUCCDQ3N1f80R/90Twi1XI8rnxmmqbXNM1s9dOpI/f1JL3rrrtyt23btgByck5X32M8oI7+Y7VfxwadKygoEC0tLX4VROjWtoU1a9YEoO7jwjgbCkppsrv62RiXGxFzlpcmQARyfp8B0Ldjx47FTz311AIAMQ9kwzCiDocjEj+mKqtHrP2BQIC98MIL1StWrNB5LT+H5E1tC5ktvtFOShd9Pp/4+te/vnzHjh35ZNijWpedsD5Kqf3II48Yy5YtKwYwo3kHZxgzTgetHoy54Ash0N8/Kech7QbvBZCRm5ub/9RTTy38zne+8xtdR4rlxWK21qxZU/m9731PEEJEX1+fSQgRFRUVTqV6vIQk0r5oaOk+TsiBvscnfNdddy2qra3N/eEPf3hq3759QaWfJfHvZmdn9959991i8+bNxQDm9fb2Etu2CVUBeGPUE4KKEG9raytdu3at70c/+tEn//3f/x2ONxjr3UycHhlNTU3s/vvvL8vPz8+FVIX0YJw8bSlgBA+o3U3CiaPUMTrYeMGWLVvm7d27t0sJLHLnnXfmZmVlhXBtMPE1RfX19Rm6r6rusYTYbIzLCCTBK1N6PllMUO5c5KUJaaG0KVchM+zkAXCvW7eu+JVXXsl666232t96662+vr6+Mdufn5+P++67L3vdunULVbbxQcj15zOMXA9mhW9Uf7pU/fkArDvvvLNixYoVvu9+97ufnDlzxuacc5X6KFYOIQQLFy4M7t6921daWloGwGHbNoS8qxBAUkkZUuK9yfKpboee50iwpk8nHeLiwgQhBF6vV16/I4QoAtAAec+MBTnA7wP4JdTlZymAqYbeAnnNtw8jF74zAN6F9DiM6MaOBSGEW5Wz7q//+q873n///R5CZGLM3bt3l9bX1w8AOABpTB3T20YI4YG8Unsd5ASJQp4K3yXSSB3/rAl5V8869VNf1XCpvb39848//rg3GAxGS0tLeVlZmdPr9WZD3vjph1xABlW/M8eqRwjhhLzaew3kHTIMQN/g4ODpTz/99OyvfvWrgfb2dvT391OXy2WXlpby0tJS9+rVqws8Hk+ZqktAOsEcBnCcEDLpCy/H4wFCSEIeUEybo/qwXPVXIwJp+D8E4FNCSMLAQiGEpepsAKBvqT4H6dF1ghASnz16xsdF1ZM0r0zm+WSRbLlziZcmQTsCOSbLAKxU7zD1XveJEyfOfPrppz0XL14Mdnd3IxAIkPz8fGdVVZW/uLi4CDLchkHy2xlID8CThJDBuDpmhW9UXVT1YRWAasgNPAEQuXDhwvkTJ05cam9vHxgYGIi4XK5oaWkpra2tzcjIyJgHuW4akJsHnaXDAbkG/xLAMUJI76j6UlnXxnwWyYUYeCHn+RrIeR+CdOzZTwg5NxN0ePHFF88ePHiwDwCi0Sj7kz/5k8H6+vrD0+3eqrP0nlIN1TEhk4XOPH0VStoqISeWLVvmgtyl9YwnsCaBKOTx9jeQfciDZJ7CRYsW+RYtWjSonqHqe5f6OQCpmugHUI6Ri/ho6OTDp9T7eQAy3W53eVVVVX5VVVUPpEpNX0pnqfJ86nkBqbtvh2S4KeVanAxGnbZKIDMLaHWzvuxN35k1HZiNcbkRccPyUhwPtWOYf7IhF61AZWVlRmVl5RDkAqkvgXRi+A4wov53ETIY+HSC9s8a36hTRjekfd+CFJJZAByFhYXFhYWFuc3NzUOQYyZUH3R/oPpxHsNj45iYinMP00WHTz/9NHaSo5SKrKwsDoztPZhMnNZY0G7wJyEHev449YwL7ZBx9uzZy4cPHzYMwwCllLS0tAR8Pp9O2zStgZBqIvVCEtwB2Z8cSIJ61UeDQ06Sy5AM/jGky3BpEv3qhlxoGOSkyIGcuLmQE9dW5RP1DFN/D0EuMjpqP2GWiVmCTu10CbLNHsh2X4W6ATUJVVNSvDYb43Ij4kbnJSK9Ua9Axo6FINNQ6fZbkIvZaOj0Q/2QC9wpyD5co4q+DnwTghT+EUjBtwhyHCzVp9H2fT0OHZAnly7Ik/MNG0vY3Nzs2L59u9nQ0NBVW1v768rKykGfz1eGFOhw4sSJvtOnT89XVyTB7/ez6urqCABhYNhwpm+hHIJcdCZ7RYHO0vs5JGMMYFiC6quzU8lKMPDyyy9HoHKCcc5x6623ZmE4bVMyrtQ2pN7/DIZvzLyEUTduasRNpF+peoohJ78LwwLYhmTwLsid7llIQ3Ce+lsHOCashwzfofMB5CQpgTwaeyDHgUHu/rgqJwpJ1w5V13kAV8dSvaWI0TwwiAR3kiXog/bybFfvedU7p5Gcm7vmlXMY3iFfgRyja3hkNsYFKfLKJJ5PFkmXO4d4aVK0IISEhRAXIdeey5Ab3XwMq8gYpMDVaYeCkGvURdX+S5AbpITryizxja5LABgSQpyFpPFVSKGXAzm/dIopW5Wpx+FzVadWmXrU8z3qk0hjkQq9p8qnUdUOffeWdqS55n1K6aK+vr6b9+zZI/bs2YOCgoLLr7/++pFU6PDKK6+UA8Pmo6qqKku1gRuQ9osPIXcqVBUygKlll9CZEz5UjdBpPYK49nbMcXHfffeZQ0NDmTo5biAQMFesWOGB3Bn2jmPkj0cE6j4XyEmgF8uesV4ghEQUo/dDMm0GhhkJGNZ36/QvQ6pfHZC2AedE9ag6OtX7lzDMrDrTtAE5UCH10cbyXsisydOVkmg0D0SRPA8Mqfc6IWmr+SeZuLkoZKDyIIZVISHIPibc8c/CuKTKKynzVpJIqdw5wkuTpgUhJKoM+P2QPOGL64MWvDaGHSu6VT0DAKIT8dpszOcE9V3G8M3ffshNnRPKvhNXlh6HIOQ4nYSko7bX9SKxcEmF3lPl0xDkWn4Vcjy4er9v9IOU0n7GWCwRc0dHRyYkDZOiw44dO1w9PT0FlmXFBPW6des8UEJrhOfM9cB4jhgbN250GIbRbJqmZZomJ4SIp556asG6detsAAcBnCIyWeiMQxkXDYzcmUkiToO3lTJKx9ehM1XH744j01XfbwtmelxuRPw28JIaVweGBZY+aem2R6YiaGeTb1RdJoazyANyLCKQQti+XuMw1fV/9PothCBPP/20dfz48dv0+AghaDQa/fytt956HxPQoa2tzTQMo940zQyHw2FTSrnf73e8+OKLhU6n8wCAU3NWaG3cuDGfMbZSCawoY0xUVFQ4//zP/7zSsqyTkPnCLszVSZdGGmmkMdcxA0LLBJD1V3/1V4uPHTuWBQCccxqNRlkkErnAOT/1k5/85Gqi+tva2spUai6q4rY4APzpn/5p+fLly/sgc2h+NmeEVmtraz6ASkqpSQhxqwSgdnwQ5AsvvFBdXFwcgXQB/RUh5JqjaRpppJFGGslhBoSWB9KRZOnf/M3fBH/xi18MCiFIJBJh0WiURaNRxjnvFvKW5gHIU5ebUuqjlJp6zVfXW4kdO3bMu/3224sgzRf/C+DijGd0ThaMMQ+lNFsnWGSMRdRPTggRTz755MLi4mIPgE8gDbDTkdE9jTTSSCON6YN2Yc95/PHHczjnx44cOdKvUnMJSinnnGcKITJ1tgui0jgxxiL6ZmnGGH/kkUfmNzc3L4J0zroMtebPGaFF1WWC8VmVKaW8qKiIfvnLX15cXFycD2nsP4vr6+adRhpppJFGYoShnGQsy8r5yle+csvJkyc/f/XVV0+3t7fbSmjR+PRMOsuJzqS/ZMkS9vDDDy8uLi4uUOWNSAc3J9SDQgj6pS99qeLChQtlDoeD+/3+8JIlS8iqVauyV61atciyLC+k98opSNXg6Wly9U4jjTTS+MJiJhwxAAQArIDMoJENFcB+4sSJ9r1793Z98sknvKenxwiFQgYAZGZmhsvKysJVVVUZy5cvL5w3b94CyPg4ASms3oNUD/YAENddaCk4IWMnyiA7rAP/fJDBaDakm6q+SrwfN3DwXRpppJHGbzFMyFRYKyHTtHkh1+shyFNYN+QaHob0atXrvXaHNzGcXek4ZPxhh/puzqgHDUiJXI7h/GM6JVAIUtoeh0zCOIC0wEojjTTSmKvQt34cV38vgEzj5FafPAwHaxNIIWVCrvlauF2BzEhyQv0eMwfNFaGlYyZ09HsUUgoPQErYTyAF1lWklk0jjTTSSCON2UcYMnuGTs1UDJnGyYPhOC0d5BzF8MlLZ0X5DDKzzjXZmeaKetANqRq8CTJSPQgZaX0VUmLryPK0wEojjTTSuHHAINV/fkihFcBwJhIDUmgFIdf3AciMHV0Yvn362nRuc0RoMUhhlYGRJ60ghi+mmxMNTSONNNJII2XoGwacGNaoMYzMchJWnwjGOaD8fzJJDyk5L1MqAAAAAElFTkSuQmCC' />\n    </div>\n  </div>\n</body>\n</html>\n"
  },
  {
    "path": "packages/client-app/static/index.js",
    "content": "window.eval = global.eval = function() {\n  throw new Error(\"Sorry, N1 does not support window.eval() for security reasons.\");\n}\n\nvar util = require('util')\nvar path = require('path');\nvar electron = require('electron');\nvar remote = electron.remote;\nvar ipcRenderer = electron.ipcRenderer;\n\nconsole.inspect = function consoleInspect(val) {\n  console.log(util.inspect(val, true, depth=7, colorize=true));\n}\n\nfunction setLoadTime (loadTime) {\n  if (global.NylasEnv) {\n    global.NylasEnv.loadTime = loadTime;\n    if (NylasEnv.inSpecMode()) return;\n    console.log('Window load time: ' + global.NylasEnv.getWindowLoadTime() + 'ms')\n  }\n}\n\nfunction handleSetupError (error) {\n  var errorJSON = \"{}\";\n  try {\n    errorJSON = JSON.stringify(error);\n  } catch (err) {\n    var recoveredError = new Error();\n    recoveredError.stack = error.stack;\n    recoveredError.message = `Recovered Error: ${error.message}`;\n    errorJSON = JSON.stringify(recoveredError)\n  }\n  console.error(error.stack || error)\n  ipcRenderer.sendSync(\"report-error\", {errorJSON: errorJSON})\n  var message = `We encountered an unexpected problem starting up Nylas Mail. Please try again.`\n  ipcRenderer.send(\"quit-with-error-message\", message)\n}\n\nfunction copyEnvFromMainProcess() {\n  var _ = require('underscore');\n  var remote = require('electron').remote;\n  var newEnv = _.extend({}, process.env, remote.process.env);\n  process.env = newEnv;\n}\n\nfunction setupWindow (loadSettings) {\n  if (process.platform === 'linux') {\n    // This will properly inherit process.env from the main process, which it\n    // doesn't do by default on Linux. See:\n    // https://github.com/atom/electron/issues/3306\n    copyEnvFromMainProcess();\n  }\n\n  var CompileCache = require('../src/compile-cache')\n  CompileCache.setHomeDirectory(loadSettings.configDirPath)\n\n  var ModuleCache = require('../src/module-cache')\n  ModuleCache.register(loadSettings)\n  ModuleCache.add(loadSettings.resourcePath)\n\n  // Start the crash reporter before anything else.\n  // require('crash-reporter').start({\n  //   productName: 'N1',\n  //   companyName: 'Nylas',\n  //   // By explicitly passing the app version here, we could save the call\n  //   // of \"require('electron').remote.app.getVersion()\".\n  //   extra: {_version: loadSettings.appVersion}\n  // })\n\n  setupVmCompatibility()\n\n  require(loadSettings.bootstrapScript)\n}\n\nfunction setupVmCompatibility () {\n  var vm = require('vm')\n  if (!vm.Script.createContext) {\n    vm.Script.createContext = vm.createContext\n  }\n}\n\n\nwindow.onload = function() {\n  try {\n    var startTime = Date.now();\n\n    var fs = require('fs');\n    var path = require('path');\n\n    // Skip \"?loadSettings=\".\n    var rawLoadSettings = decodeURIComponent(location.search.substr(14));\n    var loadSettings;\n    try {\n      loadSettings = JSON.parse(rawLoadSettings);\n    } catch (error) {\n      console.error(\"Failed to parse load settings: \" + rawLoadSettings);\n      throw error;\n    }\n\n    // Normalize to make sure drive letter case is consistent on Windows\n    process.resourcesPath = path.normalize(process.resourcesPath);\n\n    setupWindow(loadSettings)\n    setLoadTime(Date.now() - startTime)\n  }\n  catch (error) {\n    handleSetupError(error)\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/index.less",
    "content": "@import \"variables/ui-variables\";\n@import \"variables/ui-mixins\";\n\n@import (css) \"font-awesome.min.css\";\n\n@import \"normalize\";\n\n@import \"type\";\n@import \"inputs\";\n@import \"buttons\";\n@import \"dropdowns\";\n@import \"workspace\";\n@import \"resizable\";\n@import \"selection\";\n@import \"utilities\";\n\n@import \"components/menu\";\n@import \"components/switch\";\n@import \"components/tokenizing-text-field\";\n@import \"components/extra\";\n@import \"components/list-tabular\";\n@import \"components/disclosure-triangle\";\n@import \"components/button-dropdown\";\n@import \"components/scroll-region\";\n@import \"components/spinner\";\n@import \"components/generated-form\";\n@import \"components/unsafe\";\n@import \"components/key-commands-region\";\n@import \"components/contenteditable\";\n@import \"components/editable-list\";\n@import \"components/outline-view\";\n@import \"components/fixed-popover\";\n@import \"components/date-picker-popover\";\n@import \"components/modal\";\n@import \"components/date-input\";\n@import \"components/nylas-calendar\";\n@import \"components/empty-list-state\";\n@import \"components/syncing-list-state\";\n@import \"components/date-picker\";\n@import \"components/time-picker\";\n@import \"components/table\";\n@import \"components/editable-table\";\n@import \"components/multiselect-dropdown\";\n@import \"components/tutorial-overlay\";\n@import \"components/toast\";\n@import \"components/undo-toast\";\n@import \"components/attachment-items\";\n@import \"components/search-bar\";\n@import \"components/code-snippet\";\n@import \"components/feature-used-up-modal\";\n@import \"components/webview\";\n@import \"components/billing-modal\";\n"
  },
  {
    "path": "packages/client-app/static/inputs.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\ninput[type=\"text\"],\ninput[type=\"email\"],\ninput[type=\"date\"],\ninput[type=\"datetime\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"],\ninput[type=\"number\"],\ninput[type=\"password\"],\ninput[type=\"range\"],\ninput[type=\"search\"],\ninput[type=\"tel\"],\ninput[type=\"time\"],\ninput[type=\"url\"] {\n  width: 100%;\n  padding-left: @padding-xs-horizontal;\n  padding-right: @padding-xs-horizontal;\n  line-height: @line-height-computed;\n  font-weight:400;\n  background: @input-bg;\n  color: @text-color;\n\n  border-radius: @border-radius-base;\n  border: 1px solid @input-border-color;\n\n  &:disabled {\n    background: rgba(0,0,0,0.02);\n    opacity: 0.7;\n  }\n}\n\n.search-bar .menu .header-container input,\ntextarea,\ninput[type=\"text\"],\ninput[type=\"email\"],\ninput[type=\"date\"],\ninput[type=\"datetime\"],\ninput[type=\"datetime-local\"],\ninput[type=\"month\"],\ninput[type=\"number\"],\ninput[type=\"password\"],\ninput[type=\"range\"],\ninput[type=\"search\"],\ninput[type=\"tel\"],\ninput[type=\"time\"],\ninput[type=\"url\"] {\n  &:focus {\n    border: 1px solid @accent-primary;\n    box-shadow: 0 0 1.5px @accent-primary;\n  }\n}\n\nbody.platform-win32 {\n  input[type=\"text\"],\n  input[type=\"email\"],\n  input[type=\"date\"],\n  input[type=\"datetime\"],\n  input[type=\"datetime-local\"],\n  input[type=\"month\"],\n  input[type=\"number\"],\n  input[type=\"password\"],\n  input[type=\"range\"],\n  input[type=\"search\"],\n  input[type=\"tel\"],\n  input[type=\"time\"],\n  input[type=\"url\"] {\n    border: 0;\n    border-radius: 0;\n    box-shadow: 0 0 0 2px @input-border-color;\n\n    &:focus {\n      border: 0;\n      border-radius: 0;\n      box-shadow: 0 0 0 2px darken(@input-border-color, 20%);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/jasmine.less",
    "content": "\n#jasmine_content {\n  position: fixed;\n  right: 100%;\n}\n\nbody {\n  background-color: #fff;\n  padding: 0;\n}\n\n.spec-reporter {\n  font-size: 11px;\n  line-height: 1.6em;\n  color: #333;\n\n  .plain-text-output {\n    display: none;\n  }\n\n  .list-unstyled {\n    list-style: none;\n  }\n\n  .reload-button {\n    color: #333;\n    background-color: #fff;\n    border: 1px solid #ccc;\n\n    &:hover {\n      background-color: #ddd;\n      color: #222;\n    }\n  }\n\n  .symbol-header {\n    font-size: 18px;\n    font-weight: bold;\n    padding-bottom: 10px;\n  }\n\n  .symbol-area {\n    padding: 10px;\n  }\n\n  .symbol-summary {\n    overflow: hidden;\n    margin: 0;\n\n    li {\n      font-family: Monaco, Consolas, monospace;\n      float: left;\n      line-height: 10px;\n      height: 10px;\n      width: 10px;\n      font-size: 10px;\n\n      &.passed {\n        color: #5cb85c;\n      }\n\n      &.failed {\n        color: #d9534f;\n      }\n\n      &.skipped {\n        color: #f0ad4e;\n      }\n\n      &.pending {\n        color: #eee;\n      }\n\n      &:before {\n        content: \"\\02022\";\n      }\n    }\n  }\n\n  .status {\n    font-size: 20px;\n    line-height: 2em;\n    padding: 5px;\n    border-radius: 0;\n    text-align: center;\n\n    .spec-count {\n      float: left;\n    }\n\n    .time {\n      float: right;\n    }\n  }\n\n  .results {\n    padding: 10px;\n\n    .description {\n      font-size: 16px;\n      padding: 5px 0 5px 0;\n    }\n\n    > .suite {\n      > .description {\n        font-size: 18px;\n        font-weight: bold;\n      }\n\n      margin-bottom: 20px;\n    }\n\n    .spec {\n      margin-top: 5px;\n      padding: 0 10px 10px 10px;\n      border-left: 3px solid #d9534f;\n\n      .spec-toggle {\n        // .octicon(fold);\n        float: right;\n        cursor: pointer;\n        opacity: 0;\n        color: #999;\n\n        &.folded {\n          // .octicon(unfold);\n        }\n      }\n\n      .spec-toggle:hover {\n        color: #333;\n      }\n\n      &:hover .spec-toggle {\n        opacity: 1;\n      }\n    }\n\n    .suite > .suite,\n    .suite > .spec {\n      margin-left: 10px;\n    }\n  }\n\n  .result-message {\n    font-size: 16px;\n    font-weight: bold;\n    color: #d9534f;\n    padding: 5px 0 5px 0;\n    -webkit-user-select: text;\n    user-select: text;\n  }\n\n  .result-message.deprecation-message {\n    font-weight: normal;\n    color: darken(#f0ad4e, 20%);\n    line-height: 1.4;\n\n    a {\n      color: darken(#f0ad4e, 15%);\n    }\n\n    code {\n      color: darken(#f0ad4e, 20%);\n      background: lighten(#f0ad4e, 35%);\n    }\n  }\n\n  .stack-trace {\n    font-size: 12px;\n    margin: 5px 0 0 0;\n    border-radius: 2px;\n    line-height: 18px;\n    color: #666;\n    border: 1px solid #ddd;\n    overflow: auto;\n  }\n\n  .tooltip {\n    .tooltip-inner {\n      border: 1px solid #ccc;\n      background: #fff;\n      color: #666;\n      max-width: 400px;\n    }\n\n    &.in {\n      opacity: 1;\n    }\n\n    .tooltip-arrow {\n      visibility: hidden;\n    }\n  }\n\n  .result-message.fail, .stack-trace.padded {\n    text-overflow: ellipsis;\n    display: -webkit-box;\n    -webkit-box-orient: vertical;\n    -webkit-line-clamp: 10;\n    overflow: hidden;\n\n    &.expanded {\n      -webkit-line-clamp: inherit;\n      // overflow: hidden;\n    }\n  }\n\n  .deprecation-toggle {\n    // .octicon(fold);\n    float: right;\n    cursor: pointer;\n    opacity: 0;\n    color: #999;\n\n    &.folded {\n      // .octicon(unfold);\n    }\n  }\n\n  .deprecation-toggle:hover {\n    color: #333;\n  }\n\n  &:hover .deprecation-toggle {\n    opacity: 1;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/linux.less",
    "content": "::-webkit-scrollbar-corner {\n  background-color: transparent;\n}\n\n::-webkit-scrollbar {\n  width: 8px;\n  height: 8px;\n}\n\n::-webkit-scrollbar-thumb {\n  -webkit-border-radius: 2px;\n  background: rgba(150, 150, 150, .33);\n}\n"
  },
  {
    "path": "packages/client-app/static/mixins/background-variant.less",
    "content": "// Contextual backgrounds\n\n.bg-variant(@color) {\n  background-color: @color;\n  a&:hover {\n    background-color: darken(@color, 10%);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/mixins/common-ui-elements.less",
    "content": "@import \"ui-variables\";\n\n// A Mixin holding common UI elements.\n\n// A box to hold counts of things (like number of items in a tag, or\n// number of messages in a thread)\n.item-count-box {\n  min-width: 15px;\n  height: 15px;\n  text-align: center;\n  display: inline;\n  font-size: @font-size-tiny;\n  padding: 0 0.286em 0 0.286em;\n  line-height: 15px;\n  border-radius: @border-radius-small;\n  align-self: center;\n}\nbody.platform-win32 {\n  .item-count-box {\n    border-radius: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/mixins/text-emphasis.less",
    "content": "// Typography\n\n.text-emphasis-variant(@color) {\n  color: @color;\n  a&:hover {\n    color: darken(@color, 10%);\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/mixins/windows.less",
    "content": "@import \"ui-variables\";\n\n.windows-btn-bg {\n  transition: background 300ms, box-shadow 300ms;\n  background: transparent;\n  border-radius: 0;\n  &:hover {\n    background: fade(@border-color-divider, 80%);\n  }\n}\n.windows-btn-border {\n  border: 0;\n  box-shadow: 0 0 0 2px fade(@border-color-divider, 80%);\n}\n"
  },
  {
    "path": "packages/client-app/static/normalize.less",
    "content": "//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n  box-sizing: border-box;\n}\n*:before,\n*:after {\n  box-sizing: border-box;\n}\n\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n  font-family: inherit;\n  font-size: inherit;\n  line-height: inherit;\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n  margin: 0;\n}\n\n\n// Images\n\nimg {\n  vertical-align: middle;\n  border: 0;\n}\n\n/*! normalize.css v3.0.1 | MIT License | git.io/normalize */\n\n//  * 1. Set default font family to sans-serif.\n//  * 2. Prevent iOS text size adjust after orientation change, without disabling user zoom.\nhtml {\n  font-family: sans-serif;\n  -ms-text-size-adjust: 100%;\n  -webkit-text-size-adjust: 100%;\n}\n\n//  Remove default margin.\nbody { margin: 0 }\n\n// HTML5 display definitions ==========================================================================\n//  * Correct `block` display not defined for any HTML5 element in IE 8/9.\n//  * Correct `block` display not defined for `details` or `summary` in IE 10/11 and Firefox.\n//  * Correct `block` display not defined for `main` in IE 11.\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nnav,\nsection,\nsummary {\n  display: block;\n}\n\n//  * 1. Correct `inline-block` display not defined in IE 8/9.\n//  * 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\naudio,\ncanvas,\nprogress,\nvideo {\n  display: inline-block;\n  vertical-align: baseline;\n}\n\n//  * Prevent modern browsers from displaying `audio` without controls.\n//  * Remove excess height in iOS 5 devices.\naudio:not([controls]) {\n  display: none;\n  height: 0;\n}\n\n\n// Links ==========================================================================\n//  * Remove the gray background color from active links in IE 10.\na {\n  background: transparent;\n  //  * Improve readability when focused and also mouse hovered in all browsers.\n  &:active,\n  &:hover {\n    outline: 0;\n  }\n}\n\n// Text-level semantics ==========================================================================\n\n//  * Address styling not present in IE 8/9/10/11, Safari, and Chrome.\nabbr[title] { border-bottom: 1px dotted; }\n\n//  * Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\nb,\nstrong {\n  font-weight: bold;\n}\n\n//  * Address styling not present in Safari and Chrome.\ndfn { font-style: italic; }\n\n//  * Address variable `h1` font-size and margin within `section` and `article` contexts in Firefox 4+, Safari, and Chrome.\nh1 {\n  font-size: 2em;\n  margin: 0.67em 0;\n}\n\n//  * Address styling not present in IE 8/9.\nmark {\n  background: #ff0;\n  color: #000;\n}\n\n//  * Address inconsistent and variable font size in all browsers.\nsmall { font-size: 80%; }\n\n//  * Prevent `sub` and `sup` affecting `line-height` in all browsers.\nsub,\nsup {\n  font-size: 75%;\n  line-height: 0;\n  position: relative;\n  vertical-align: baseline;\n}\n\nsup { top: -0.5em; }\nsub { bottom: -0.25em; }\n\n//  * Correct overflow not hidden in IE 9/10/11.\nsvg:not(:root) { overflow: hidden; }\n\n//  * Address differences between Firefox and other browsers.\nhr {\n  -moz-box-sizing: content-box;\n  box-sizing: content-box;\n  height: 0;\n}\n\n//  * Contain overflow in all browsers.\npre { overflow: auto; }\n\n//  * Address odd `em`-unit font size rendering in all browsers.\ncode,\nkbd,\npre,\nsamp {\n  font-family: monospace, monospace;\n  font-size: 1em;\n}\n\n// Forms ==========================================================================\n//  * Known limitation: by default, Chrome and Safari on OS X allow very limited\n//  * styling of `select`, unless a `border` property is set.\n\n//  * 1. Correct color not being inherited.\n//  *    Known issue: affects color of disabled elements.\n//  * 2. Correct font properties not being inherited.\n//  * 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n  color: inherit;\n  font: inherit;\n  margin: 0;\n}\n\n//  * Address `overflow` set to `hidden` in IE 8/9/10/11.\nbutton { overflow: visible;}\n\n//  * Address inconsistent `text-transform` inheritance for `button` and `select`.\n//  * All other form control elements do not inherit `text-transform` values.\n//  * Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n//  * Correct `select` style inheritance in Firefox.\nbutton,\nselect {\n  text-transform: none;\n}\n\n//  * 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio` and `video` controls.\n//  * 2. Correct inability to style clickable `input` types in iOS.\n//  * 3. Improve usability and consistency of cursor style between image-type `input` and others.\nbutton,\nhtml input[type=\"button\"] {\n  -webkit-appearance: button;\n  cursor: pointer;\n}\n\n//  * Re-set default cursor for disabled elements.\nbutton[disabled],\nhtml input[disabled] {\n  cursor: default;\n}\n\n//  * Remove inner padding and border in Firefox 4+.\nbutton\ninput {\n  &::-moz-focus-inner {\n    border: 0;\n    padding: 0;\n  }\n}\n\n//  * Address Firefox 4+ setting `line-height` on `input` using `!important` in the UA stylesheet.\ninput {\n  line-height: normal;\n  &[type=\"reset\"],\n  &[type=\"submit\"] {\n    -webkit-appearance: button;\n    cursor: pointer;\n  }\n\n  // * It's recommended that you don't attempt to style these elements.\n  // * Firefox's implementation doesn't respect box-sizing, padding, or width.\n  // * 1. Address box sizing set to `content-box` in IE 8/9/10.\n  // * 2. Remove excess padding in IE 8/9/10.\n  &[type=\"checkbox\"],\n  &[type=\"radio\"] {\n    box-sizing: border-box;\n    padding: 0;\n  }\n\n  //  * Fix the cursor style for Chrome's increment/decrement buttons. For certain\n  //  * `font-size` values of the `input`, it causes the cursor style of the\n  //  * decrement button to change from `default` to `text`.\n  &[type=\"number\"] {\n    &::-webkit-inner-spin-button,\n    &::-webkit-outer-spin-button {\n      height: auto;\n    }\n  }\n\n  // * 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n  // * 2. Address `box-sizing` set to `border-box` in Safari and Chrome (include `-moz` to future-proof).\n  &[type=\"search\"] {\n    -webkit-appearance: textfield;\n    -moz-box-sizing: content-box;\n    -webkit-box-sizing: content-box;\n    box-sizing: content-box;\n\n  //  * Remove inner padding and search cancel button in Safari and Chrome on OS X.\n  //  * Safari (but not Chrome) clips the cancel button when the search input has\n  //  * padding (and `textfield` appearance).\n    &::-webkit-search-cancel-button,\n    &::-webkit-search-decoration {\n      -webkit-appearance: none;\n    }\n  }\n\n}\n\n//  * Define consistent border, margin, and padding.\nfieldset {\n  border: 1px solid #c0c0c0;\n  margin: 0 2px;\n  padding: 0.35em 0.625em 0.75em;\n}\n\n//  * 1. Correct `color` not being inherited in IE 8/9/10/11.\n//  * 2. Remove padding so people aren't caught out if they zero out fieldsets.\nlegend {\n  border: 0;\n  padding: 0;\n}\n\n// * Remove default vertical scrollbar in IE 8/9/10/11.\ntextarea { overflow: auto; }\n\n//  * Don't inherit the `font-weight` (applied by a rule above).\n//  * NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\noptgroup { font-weight: bold; }\n\n// Tables ==========================================================================\n//  * Remove most spacing between table cells.\ntable {\n  border-collapse: collapse;\n  border-spacing: 0;\n}\n\ntd,\nth {\n  padding: 0;\n}\n"
  },
  {
    "path": "packages/client-app/static/package-template/README.md",
    "content": "\n## My Package\n\nA sample package for N1. It demonstrates how to add components to the composer's action bar and the message sidebar. Enjoy!\n"
  },
  {
    "path": "packages/client-app/static/package-template/lib/main.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports';\n\nimport MyComposerButton from './my-composer-button';\nimport MyMessageSidebar from './my-message-sidebar';\n\n// Activate is called when the package is loaded. If your package previously\n// saved state using `serialize` it is provided.\n//\nexport function activate() {\n  ComponentRegistry.register(MyComposerButton, {\n    role: 'Composer:ActionButton',\n  });\n  ComponentRegistry.register(MyMessageSidebar, {\n    role: 'MessageListSidebar:ContactCard',\n  });\n}\n\n// Serialize is called when your package is about to be unmounted.\n// You can return a state object that will be passed back to your package\n// when it is re-activated.\n//\nexport function serialize() {\n}\n\n// This **optional** method is called when the window is shutting down,\n// or when your package is being updated or disabled. If your package is\n// watching any files, holding external resources, providing commands or\n// subscribing to events, release them here.\n//\nexport function deactivate() {\n  ComponentRegistry.unregister(MyComposerButton);\n  ComponentRegistry.unregister(MyMessageSidebar);\n}\n"
  },
  {
    "path": "packages/client-app/static/package-template/lib/my-composer-button.jsx",
    "content": "import {React} from 'nylas-exports';\n\nexport default class MyComposerButton extends React.Component {\n\n  // Note: You should assign a new displayName to avoid naming\n  // conflicts when injecting your item\n  static displayName = 'MyComposerButton';\n\n  // When you register as a composer button, you receive a\n  // reference to the draft, and you can look it up to perform\n  // actions and retrieve data.\n  static propTypes = {\n    draft: React.PropTypes.object.isRequired,\n    session: React.PropTypes.object.isRequired,\n  };\n\n  shouldComponentUpdate(nextProps) {\n    // Our render method doesn't use the provided `draft`, and the draft changes\n    // constantly (on every keystroke!) `shouldComponentUpdate` helps keep N1 fast.\n    return nextProps.session !== this.props.session;\n  }\n\n  _onClick = () => {\n    const {session, draft} = this.props;\n\n    // To retrieve information about the draft, we fetch the current editing\n    // session from the draft store. We can access attributes of the draft\n    // and add changes to the session which will be appear immediately.\n    const newSubject = `${draft.subject} - It Worked!`;\n\n    const dialog = this._getDialog();\n    dialog.showMessageBox({\n      title: 'Here we go...',\n      detail: `Adjusting the subject line To \"${newSubject}\"`,\n      buttons: ['OK'],\n      type: 'info',\n    });\n\n    session.changes.add({subject: newSubject});\n  }\n\n  _getDialog() {\n    return require('electron').remote.dialog;\n  }\n\n  render() {\n    return (\n      <div className=\"my-package\">\n        <button className=\"btn btn-toolbar\" onClick={() => this._onClick()} ref=\"button\">\n          Hello World\n        </button>\n      </div>\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/package-template/lib/my-message-sidebar.jsx",
    "content": "import {\n  React,\n  FocusedContactsStore,\n} from 'nylas-exports';\n\nexport default class MyMessageSidebar extends React.Component {\n  static displayName = 'MyMessageSidebar';\n\n  // This sidebar component listens to the FocusedContactStore,\n  // which gives us access to the Contact object of the currently\n  // selected person in the conversation. If you wanted to take\n  // the contact and fetch your own data, you'd want to create\n  // your own store, so the flow of data would be:\n\n  // FocusedContactStore => Your Store => Your Component\n  constructor(props) {\n    super(props);\n    this.state = this._getStateFromStores();\n  }\n\n  componentDidMount() {\n    this.unsubscribe = FocusedContactsStore.listen(this._onChange);\n  }\n\n  componentWillUnmount() {\n    this.unsubscribe();\n  }\n\n  _onChange = () => {\n    this.setState(this._getStateFromStores());\n  }\n\n  _getStateFromStores = () => {\n    return {\n      contact: FocusedContactsStore.focusedContact(),\n    };\n  }\n\n  _renderContent() {\n    // Want to include images or other static assets in your components?\n    // Reference them using the nylas:// URL scheme:\n    //\n    // <RetinaImg\n    //    url=\"nylas://<<package.name>>/assets/checkmark_templatethis.2x.png\"\n    //    mode={RetinaImg.Mode.ContentIsMask}/>\n    //\n    return (\n      <div className=\"header\">\n        <h1>{this.state.contact.displayName()} is the focused contact.</h1>\n      </div>\n    );\n  }\n\n  _renderPlaceholder() {\n    return (\n      <div> No Data Available </div>\n    );\n  }\n\n  render() {\n    const content = (this.state.contact) ? this._renderContent() : this._renderPlaceholder();\n    return (\n      <div className=\"my-message-sidebar\">\n        {content}\n      </div>\n    );\n  }\n}\n\n\n// Providing container styles tells the app how to constrain\n// the column your component is being rendered in. The min and\n// max size of the column are chosen automatically based on\n// these values.\nMyMessageSidebar.containerStyles = {\n  order: 1,\n  flexShrink: 0,\n};\n"
  },
  {
    "path": "packages/client-app/static/package-template/spec/main-spec.es6",
    "content": "import {ComponentRegistry} from 'nylas-exports';\nimport {activate, deactivate} from '../lib/main';\n\nimport MyMessageSidebar from '../lib/my-message-sidebar';\nimport MyComposerButton from '../lib/my-composer-button';\n\ndescribe(\"activate\", () => {\n  it(\"should register the composer button and sidebar\", () => {\n    spyOn(ComponentRegistry, 'register');\n    activate();\n    expect(ComponentRegistry.register).toHaveBeenCalledWith(MyComposerButton, {role: 'Composer:ActionButton'});\n    expect(ComponentRegistry.register).toHaveBeenCalledWith(MyMessageSidebar, {role: 'MessageListSidebar:ContactCard'});\n  });\n});\n\ndescribe(\"deactivate\", () => {\n  it(\"should unregister the composer button and sidebar\", () => {\n    spyOn(ComponentRegistry, 'unregister');\n    deactivate();\n    expect(ComponentRegistry.unregister).toHaveBeenCalledWith(MyComposerButton);\n    expect(ComponentRegistry.unregister).toHaveBeenCalledWith(MyMessageSidebar);\n  });\n});\n"
  },
  {
    "path": "packages/client-app/static/package-template/spec/my-composer-button-spec.jsx",
    "content": "import {React, ReactDOM} from 'nylas-exports';\nconst ReactTestUtils = require('react-addons-test-utils')\n\nimport MyComposerButton from '../lib/my-composer-button';\n\ndescribe(\"MyComposerButton\", () => {\n  beforeEach(() => {\n    this.component = ReactTestUtils.renderIntoDocument(\n      <MyComposerButton draftClientId=\"test\" />\n    );\n  });\n\n  it(\"should render into the page\", () => {\n    expect(this.component).toBeDefined();\n  });\n\n  it(\"should have a displayName\", () => {\n    expect(MyComposerButton.displayName).toBe('MyComposerButton');\n  });\n\n  it(\"should show a dialog box when clicked\", () => {\n    spyOn(this.component, '_onClick');\n    const buttonNode = ReactDOM.findDOMNode(this.component.refs.button);\n    ReactTestUtils.Simulate.click(buttonNode);\n    expect(this.component._onClick).toHaveBeenCalled();\n  });\n});\n"
  },
  {
    "path": "packages/client-app/static/package-template/stylesheets/main.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n.my-package .btn {\n  \n}\n"
  },
  {
    "path": "packages/client-app/static/resizable.less",
    "content": "@resizableBorder: 1px solid #bbb;\n\n.resizable {\n  position: relative;\n}\n\n.resizable .resizeBar {\n  box-sizing: border-box;\n  display: block;\n  position: absolute;\n\n  &.top, &.bottom {\n    height: 5px;\n    width: 100%;\n    cursor: ns-resize;\n  }\n\n  &.left, &.right {\n    width: 5px;\n    height: 100%;\n    cursor: ew-resize;\n  }\n\n  &.top {\n    border-top: @resizableBorder;\n    top: 0;\n  }\n\n  &.bottom {\n    border-bottom: @resizableBorder;\n    bottom: 0;\n  }\n\n  &.left {\n    border-left: @resizableBorder;\n    left: 0;\n  }\n\n  &.right {\n    border-right: @resizableBorder;\n    right: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/selection.less",
    "content": "* {\n  -webkit-user-select: inherit;\n  user-select: inherit;\n}\nbody {\n  -webkit-user-select: none;\n  user-select: none;\n}\ninput, textarea, div[contenteditable], {\n  -webkit-user-select: auto;\n  user-select: auto;\n}\n.selectable {\n  &:hover {\n    cursor: text;\n  }\n  -webkit-user-select: auto;\n  user-select: auto;\n}\n"
  },
  {
    "path": "packages/client-app/static/type.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6 {\n  font-family: @font-family-heading;\n  line-height: @line-height-heading;\n  color: @text-color-heading;\n\n  small,\n  .small {\n    line-height: 1;\n  }\n}\n\nh1 {\n  font-size:   @font-size-h1;\n  font-weight: @font-weight-semi-bold;\n}\nh2 {\n  font-size:   @font-size-h2;\n  font-weight: @font-weight-blond;\n}\nh3 {\n  font-size:   @font-size-h3;\n  font-weight: @font-weight-blond;\n}\nh4 { font-size: @font-size-h4; }\nh5 { font-size: @font-size-h5; }\nh6 { font-size: @font-size-h6; }\n\nh1, h2, h3{\n  margin-top: @line-height-computed;\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 65%;\n  }\n}\nh4, h5, h6 {\n  margin-top: (@line-height-computed / 2);\n  margin-bottom: (@line-height-computed / 2);\n\n  small,\n  .small {\n    font-size: 75%;\n  }\n}\n\n\n// Body text\n// -------------------------\n\na { color: @text-color-link; }\na:hover { color: @text-color-link; cursor: default; }\na:active { color: @text-color-link-active; }\na:visisted { color: @text-color-link-active; }\n\np {\n  margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n  margin-bottom: @line-height-computed;\n  font-size: floor((@font-size-base * 1.15));\n  font-weight: 300;\n  line-height: 1.4;\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n  font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n  background-color: #fcf8e3;\n  padding: .2em;\n}\n\n// Alignment\n.text-left           { text-align: left; }\n.text-right          { text-align: right; }\n.text-center         { text-align: center; }\n.text-justify        { text-align: justify; }\n.text-nowrap         { white-space: nowrap; }\n\n// Transformation\n.text-lowercase      { text-transform: lowercase; }\n.text-uppercase      { text-transform: uppercase; }\n.text-capitalize     { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n  color: @text-color-subtle;\n}\n.text-primary {\n  .text-emphasis-variant(@accent-primary);\n}\n.text-success {\n  .text-emphasis-variant(#3c763d);\n}\n.text-info {\n  .text-emphasis-variant(#31708f);\n}\n.text-warning {\n  .text-emphasis-variant(#8a6d3b);\n}\n.text-danger {\n  .text-emphasis-variant(#a94442);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n  // Given the contrast here, this is the only class to have its color inverted\n  // automatically.\n  color: #fff;\n  .bg-variant(@accent-primary);\n}\n.bg-success {\n  .bg-variant(#dff0d8);\n}\n.bg-info {\n  .bg-variant(#d9edf7);\n}\n.bg-warning {\n  .bg-variant(#fcf8e3);\n}\n.bg-danger {\n  .bg-variant(#f2dede);\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n  margin-top: 0;\n  margin-bottom: (@line-height-computed / 2);\n  ul,\n  ol {\n    margin-bottom: 0;\n  }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n  padding-left: 0;\n  list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n  .list-unstyled();\n  margin-left: -5px;\n\n  > li {\n    display: inline-block;\n    padding-left: 5px;\n    padding-right: 5px;\n  }\n}\n\n// Description Lists\ndl {\n  margin-top: 0; // Remove browser default\n  margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n  line-height: @line-height-base;\n}\ndt {\n  font-weight: bold;\n}\ndd {\n  margin-left: 0; // Undo browser default\n}\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n  cursor: help;\n  border-bottom: 1px dotted @gray-light;\n}\n.initialism {\n  font-size: 90%;\n  text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n  padding: (@line-height-computed / 2) @line-height-computed;\n  margin: 0 0 @line-height-computed;\n  border-left: 5px solid @gray-lighter;\n\n  p,\n  ul,\n  ol {\n    &:last-child {\n      margin-bottom: 0;\n    }\n  }\n\n  // Note: Deprecated small and .small as of v3.1.0\n  // Context: https://github.com/twbs/bootstrap/issues/11660\n  footer,\n  small,\n  .small {\n    display: block;\n    font-size: 80%; // back to default font-size\n    line-height: @line-height-base;\n    color: @gray-light;\n\n    &:before {\n      content: '\\2014 \\00A0'; // em dash, nbsp\n    }\n  }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n  padding-right: 15px;\n  padding-left: 0;\n  border-right: 5px solid @gray-lighter;\n  border-left: 0;\n  text-align: right;\n\n  // Account for citation\n  footer,\n  small,\n  .small {\n    &:before { content: ''; }\n    &:after {\n      content: '\\00A0 \\2014'; // nbsp, em dash\n    }\n  }\n}\n\n// Addresses\naddress {\n  margin-bottom: @line-height-computed;\n  font-style: normal;\n  line-height: @line-height-base;\n}\n"
  },
  {
    "path": "packages/client-app/static/utilities.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\n:focus {\n  outline: none;\n}\n\n.pull-center {\n  margin-left: auto;\n  margin-right: auto;\n}\n\n.padded {\n  padding: @component-padding;\n}\n\n// Blocks\n\n// Must be div.block so as not to affect syntax highlighting.\nul.block,\ndiv.block {\n  margin-bottom: @component-padding;\n}\ndiv > ul.block:last-child,\ndiv > div.block:last-child {\n  margin-bottom: 0;\n}\n\n// Inline Blocks\n\n.inline-block,\n.inline-block-tight {\n  display: inline-block;\n  vertical-align: middle;\n}\n.inline-block {\n  margin-right: @component-padding;\n}\n.inline-block-tight {\n  margin-right: @component-padding/2;\n}\ndiv > .inline-block:last-child,\ndiv > .inline-block-tight:last-child {\n  margin-right: 0;\n}\n\n.inline-block .inline-block {\n  vertical-align: top;\n}\n\n// Use left margin when it's in a float: right element.\n// Sets the margin correctly when inline blocks are hidden and shown.\n.pull-right {\n  .inline-block {\n    margin-right: 0;\n    margin-left: @component-padding;\n  }\n  .inline-block-tight {\n    margin-right: 0;\n    margin-left: @component-padding/2;\n  }\n\n  > .inline-block:first-child,\n  > .inline-block-tight:first-child {\n    margin-left: 0;\n  }\n}\n"
  },
  {
    "path": "packages/client-app/static/variables/ui-mixins.less",
    "content": "@import \"ui-variables\";\n@import \"../mixins/common-ui-elements\";\n@import \"../mixins/text-emphasis\";\n@import \"../mixins/background-variant\";\n@import \"../mixins/windows\";\n"
  },
  {
    "path": "packages/client-app/static/variables/ui-variables.less",
    "content": "// Color abstraction hierarchy:\n// 1. Hex code (#428bca)\n// 2. Common color name (@blue)\n// 3. Generic color descriptor (@accent-primary, @background-primary)\n// --------\n// 4. Generic usage descriptor (@input-background, @button-background)\n// 5. Application-specific usage (@unread-label-background)\n\n// Typography abstraction hierarchy\n// 1. Font-face (Nylas-Pro)\n// 2. Common name (@bold, @italic)\n// 3. Generic font descriptor mixins (.bold, .italic, .h1, .h2)\n// --------\n// 4. Generic usage descriptor (.btn-text, .p-body)\n// 5. Application-specific usage (.message-list-h1)\n\n\n//=============================== Colors ===============================//\n//== Color Definitions\n@black:        #231f20;\n@gray-base:    #0a0b0c;\n@gray-darker:  lighten(@gray-base, 13.5%); // #222\n@gray-dark:    lighten(@gray-base, 20%);   // #333\n@gray:         lighten(@gray-base, 33.5%); // #555\n@gray-light:   lighten(@gray-base, 46.7%); // #777\n@gray-lighter: lighten(@gray-base, 92.5%); // #eee\n@white:        #ffffff;\n\n@blue-dark:    #3187e1;\n@blue:         #419bf9;\n@blue-light:   #009ec4;\n\n//== Color Descriptors\n@accent-primary:      @blue;\n@accent-primary-dark: @blue-dark;\n\n@background-primary:     @white;\n@background-off-primary: #fdfdfd;\n@background-secondary:   #f6f6f6;\n@background-tertiary:    #6d7987;\n\n@color-info:    @blue-dark;\n@color-success: #5CB346;\n@color-warning: #f0ad4e;\n@color-error:   #d9534f;\n@color-danger:  #d9534f;\n\n@component-active-color: @accent-primary-dark;\n@component-active-bg:    @background-primary;\n\n@background-gradient: linear-gradient(to top, rgba(241,241,241,0.75) 0%,\nrgba(253,253,253,0.75) 100%);\n\n@border-color-primary: darken(@background-primary, 10%);\n@border-color-secondary: darken(@background-secondary, 10%);\n@border-color-tertiary: darken(@background-tertiary, 10%);\n@border-color-divider: @border-color-secondary;\n\n\n//============================= Typography =============================//\n\n// ----- Colors -----\n@text-color:                     @black;\n@text-color-subtle:              fadeout(@text-color, 20%);\n@text-color-very-subtle:         fadeout(@text-color, 50%);\n@text-color-inverse:             @white;\n@text-color-inverse-subtle:      fadeout(@text-color-inverse, 20%);\n@text-color-inverse-very-subtle: fadeout(@text-color-inverse, 50%);\n\n@text-color-heading: #434648;\n\n@text-color-link:        @blue;\n@text-color-link-hover:  @blue-dark;\n@text-color-link-active: @blue-dark;\n\n@text-color-selected:  @text-color-inverse;\n\n@text-color-search-match: #fff000;\n@text-color-search-current-match: #ff8b1a;\n\n@font-family-sans-serif:  \"Nylas-Pro\", \"Helvetica\", sans-serif;\n@font-family-serif:       Georgia, \"Times New Roman\", Times, serif;\n@font-family-monospace:   Menlo, Monaco, Consolas, \"Courier New\", monospace;\n\n@font-family:             @font-family-sans-serif;\n@font-family-heading:     @font-family-sans-serif;\n\n// ----- Font Weights -----\n@font-weight-thin:        200;\n@font-weight-blond:       300;\n@font-weight-normal:      400;\n@font-weight-medium:      500;\n@font-weight-semi-bold:   600;\n@headings-font-weight:    600;\n\n// ----- Font Sizes -----\n@font-size-base:          14px;\n\n@font-size-tiny:          @font-size-base * 0.75; // 10.5px\n@font-size-smaller:       @font-size-base * 0.86; // 12px\n@font-size-small:         @font-size-base * 0.93; // 13px\n@font-size:               @font-size-base;        // 14px\n@font-size-large:         @font-size-base * 1.14; // 16px\n@font-size-larger:        @font-size-base * 1.29; // 18px\n\n@font-size-h1:            @font-size-base * 1.71;  // 24px\n@font-size-h2:            @font-size-base * 1.71;  // 24px\n@font-size-h3:            @font-size-base * 1.43;  // 20px\n@font-size-h4:            @font-size-base * 1.29;  // 18px\n@font-size-h5:            @font-size-base;\n@font-size-h6:            @font-size-base * 0.86;  // 12px\n\n// ----- Line Height -----\n@line-height-base:         1.5; // 22.5/15\n@line-height-computed:     floor((@font-size-base * @line-height-base)); // ~20px\n@line-height-heading:      1.1;\n\n\n//============================== Spacing ===============================//\n// Define common padding and border radius sizes and more. Values based on\n// 14px text and 1.428 line-height (~20px to start).\n\n@spacing-standard:          @font-size-base;\n\n@spacing-quarter:           @spacing-standard * 0.25;\n@spacing-half:              @spacing-standard * 0.5;\n@spacing-three-quarters:    @spacing-standard * 0.75;\n@spacing-double:            @spacing-standard * 2;\n\n@padding-base-vertical:     5px;\n@padding-base-horizontal:   12px;\n\n@padding-large-vertical:    9px;\n@padding-large-horizontal:  16px;\n\n@padding-small-vertical:    4px;\n@padding-small-horizontal:  10px;\n\n@padding-xs-vertical:       1px;\n@padding-xs-horizontal:     5px;\n\n@line-height-large:         @line-height-computed * 1.3;\n@line-height-small:         @line-height-computed * 0.95;\n\n@border-radius-base:        3px;\n@border-radius-large:       5px;\n@border-radius-small:       2px;\n\n\n//============================== Shadows ===============================//\n\n@standard-shadow-color: rgba(0, 0, 0, 0.15);\n@standard-shadow: 0 1px 4px 0 @standard-shadow-color;\n@standard-shadow-up: 0 -1px 4px 0 @standard-shadow-color;\n@shadow-border: 0 0.5px 0 @standard-shadow-color, 0 -0.5px 0 @standard-shadow-color,\n0.5px 0 0 @standard-shadow-color, -0.5px 0 0 @standard-shadow-color;\n\n\n//=============================== Buttons ==============================//\n\n@btn-shadow: @standard-shadow;\n\n@btn-default-bg-color:    darken(@background-primary, 0.5%);\n@btn-default-text-color:  @text-color;\n\n@btn-icon-color: #919191;\n\n@btn-action-bg-color:     @color-success;\n@btn-action-text-color:   @text-color;\n\n@btn-emphasis-bg-color:   #5b90fb;\n@btn-emphasis-text-color: @text-color-inverse;\n\n@btn-danger-bg-color:     @color-danger;\n@btn-danger-text-color:   @text-color-inverse;\n\n\n//=============================== Dropdowns ============================//\n\n@dropdown-default-bg-color:     @background-primary;\n@dropdown-default-text-color:   @text-color;\n@dropdown-default-border-color: fadeout(@border-color-primary, 10%);\n\n\n//=============================== Inputs ===============================//\n\n@input-bg:                       @white;\n@input-bg-disabled:              @gray-lighter;\n\n@input-border-color:         fadeout(@border-color-primary, 10%);\n@input-border-color-blurred: desaturate(@input-border-color, 100%);\n\n@input-font-size: 14px;\n\n\n//=============================== Components ===========================//\n\n//== Toolbar\n@toolbar-background-color: darken(@white, 17.5%);\n\n//== Account Sidebar\n@panel-background-color:         @gray-lighter;\n@source-list-bg:                 @panel-background-color;\n@source-list-active-bg:          @panel-background-color;\n@source-list-active-color:       @component-active-color;\n\n//== Thread List (e.g, `.list-group-item`, `.list-item`)\n@list-bg:                  @white;\n@list-border:              #ddd;\n@list-hover-bg:            darken(@list-bg, 4%);\n@list-focused-color:       @list-bg;\n@list-focused-bg:          @component-active-color;\n@list-focused-border:      @list-focused-bg;\n@list-selected-color:      inherit;\n@list-selected-bg:         mix(@component-active-color, @list-bg, 17%);\n@list-selected-border:     mix(@component-active-color, @list-bg, 50%);\n\n//== Notifications\n@background-color-info:    @blue-light;\n@background-color-success: #00ac6f;\n@background-color-warning: #ff4800;\n@background-color-error:   #ca2541;\n@background-color-pending: #b4babd;\n\n//== Menus\n@menu-item-color-hover:    fade(@blue-light, 10%);\n@menu-item-color-selected: @blue-light;\n@menu-text-color-selected: @text-color-inverse;\n\n//== Sizes\n@component-padding:       10px;\n@component-icon-padding:  5px;\n@component-icon-size:     16px;\n@component-line-height:   25px;\n@component-border-radius: 2px;\n\n\n// Helpers for Specs - Do Not Remove\n@spec-test-variable: rgb(152,123,0);\n"
  },
  {
    "path": "packages/client-app/static/workspace.less",
    "content": "@import \"ui-variables\";\n@import \"ui-mixins\";\n\nhtml,\nbody {\n  width: 100%;\n  height: 100%;\n  overflow: hidden;\n  font-family: @font-family;\n  font-size: @font-size;\n  line-height: @line-height-base;\n  -webkit-font-smoothing: antialiased;\n}\n\nnylas-workspace {\n  display: block;\n  height: 100%;\n  overflow: hidden;\n  position: relative;\n  font-family: @font-family;\n\n  // Important: This attribute is used in the theme-manager-specs to check that\n  // themes load and override each other correctly. Do not remove!\n  background-color: @background-primary;\n\n  nylas-workspace-axis.horizontal {\n    display: -webkit-flex;\n    height: 100%;\n  }\n\n  nylas-workspace-axis.vertical {\n    display: -webkit-flex;\n    -webkit-flex: 1;\n    -webkit-flex-flow: column;\n  }\n}\n\n.sheet-container {\n  height:100%;\n}\n\n.sheet {\n  background-color: @background-primary;\n}\n\n.sheet-stack-enter {\n  left:30px;\n  opacity: 0;\n  transition: all .125s ease-out;\n}\n\n.sheet-stack-enter.sheet-stack-enter-active {\n  left:0;\n  opacity: 1;\n}\n\n.sheet-stack-leave {\n  left:0;\n  opacity: 1;\n  transition: all .125s ease-in;\n}\n\n.sheet-stack-leave.sheet-stack-leave-active {\n  left:30px;\n  opacity: 0;\n}\n\n.toolbar-menu-control {\n  display:none;\n}\n\n.toolbar-window-controls {\n  margin-top:9px;\n  margin-left:@spacing-half;\n  order: -1000;\n  min-width: 72px;\n  width: 72px;\n  flex-grow: 0;\n  flex-shrink: 0;\n\n  &:hover {\n    button {\n      background-position: 0 -12px;\n    }\n  }\n\n  button {\n    -webkit-app-region: no-drag;\n    display:inline-block;\n    padding:0;\n    width:12px;\n    height:12px;\n    margin:4px;\n    float:left;\n    background-color: transparent;\n    background-repeat: no-repeat;\n    background-position: 0 0;\n    background-size: 12px 48px;\n    border: 0;\n    &:active {\n      background-position: 0 -24px;\n    }\n  }\n  .close {\n    background-image: url(\"images/application-frame/close@1x.png\");\n  }\n  .minimize {\n    background-image: url(\"images/application-frame/minimize@1x.png\");\n  }\n  .maximize {\n    background-image: url(\"images/application-frame/fullscreen@1x.png\");\n  }\n  &.alt-true {\n    .maximize {\n      background-image: url(\"images/application-frame/maximize@1x.png\");\n    }\n  }\n}\n\n@media (-webkit-min-device-pixel-ratio: 2) {\n  .toolbar-window-controls {\n    .close {\n      background-image: url(\"images/application-frame/close@2x.png\");\n    }\n    .minimize {\n      background-image: url(\"images/application-frame/minimize@2x.png\");\n    }\n    .maximize {\n      background-image: url(\"images/application-frame/fullscreen@2x.png\");\n    }\n    &.alt-true {\n      .maximize {\n        background-image: url(\"images/application-frame/maximize@2x.png\");\n      }\n    }\n  }\n}\n\nbody.is-blurred {\n  .toolbar-window-controls {\n    button {\n      background-position: 0 -36px;\n    }\n  }\n  .sheet-toolbar-container {\n    background-image: -webkit-linear-gradient(top, lighten(@toolbar-background-color, 14%), lighten(@toolbar-background-color, 14%));\n\n    .btn.btn-toolbar {\n      background: none;\n      box-shadow: 0 0.5px 0 rgba(0,0,0,0.15), 0 -0.5px 0 rgba(0,0,0,0.15), 0.5px 0 0 rgba(0,0,0,0.15), -0.5px 0 0 rgba(0,0,0,0.15);\n      img { opacity:0.5; }\n    }\n    .item-container {\n      .window-title {\n        opacity: 0.5;\n      }\n    }\n  }\n}\n\n.sheet-toolbar-container {\n  background-image: -webkit-linear-gradient(lighten(@toolbar-background-color, 10%) 0%, lighten(@toolbar-background-color, 10%) 1.9%, lighten(@toolbar-background-color, 9%) 2%, @toolbar-background-color 100%);\n  color:@text-color-heading;\n}\n\n.layout-mode-popout {\n  .sheet-toolbar {\n    background: @background-primary;\n    height: 35px;\n    min-height: 35px;\n    max-height: 35px;\n\n    .btn-toolbar {\n      margin-top: 6px;\n    }\n  }\n  .toolbar-window-controls {\n    margin-top: 7px;\n  }\n}\n\n.sheet-toolbar {\n  position: relative;\n  -webkit-app-region: drag;\n  border-bottom: 1px solid darken(@toolbar-background-color, 9%);\n  width:  100%;\n  height: 38px;\n\n  // prevent flexbox from ever, ever resizing toolbars, no matter\n  // how much it thinks other content is being squished\n  min-height: 38px;\n  max-height: 38px;\n\n  // cover up the vertical resizing separators, so the toolbar appears\n  // to be one continuous bar.\n  z-index: 10;\n\n  .item-container > * {\n    -webkit-app-region: no-drag;\n  }\n  .item-spacer {\n    -webkit-app-region: drag;\n  }\n\n  .item-compose {\n    order: 101;\n  }\n\n  .item-container {\n    .window-title {\n      position: absolute;\n      text-align: center;\n      left: 50%;\n      transform: translateX(-50%);\n      -webkit-app-region: drag;\n      line-height: 36px;\n      &:hover {\n        cursor: default;\n      }\n    }\n  }\n\n  .item-back {\n    order:-999;\n    img.content-mask { background-color: @text-color-heading; }\n    flex-grow: 0;\n    flex-shrink: 0;\n\n    .item-back-title {\n      cursor: default;\n      color:@text-color-heading;\n      margin:0;\n      font-size: @font-size-h4;\n      font-weight: @font-weight-normal;\n      vertical-align: middle;\n      display:inline-block;\n    }\n    &:active {\n      .item-back-title { color: mix(@text-color-heading, @black, 30%); }\n      img.content-mask { background-color: mix(@text-color-heading, @black, 30%); }\n    }\n  }\n\n  .btn-toolbar {\n    margin-top: 8px;\n    margin-left: @spacing-three-quarters;\n    margin-right: 0;\n    flex-shrink: 0;\n    line-height: 1.75em;\n    height: 1.75em;\n    box-shadow: 0 0.5px 0.05px rgba(0,0,0,0.4), 0 -0.5px 0 rgba(0,0,0,0.12);\n    img.content-mask { background-color: fade(@text-color-heading, 80%); }\n  }\n  .btn-toolbar:active {\n    img.content-mask { background-color: fade(@text-color-heading, 90%); }\n  }\n  .btn-toolbar:only-of-type {\n    margin-right: @spacing-three-quarters;\n  }\n}\n\n.opacity-125ms-enter {\n  opacity:0;\n  transition: opacity .125s ease-out;\n}\n\n.opacity-125ms-enter.opacity-125ms-enter-active {\n  opacity:1;\n}\n\n.opacity-125ms-leave {\n  opacity:1;\n  transition: opacity .125s ease-in;\n}\n\n.opacity-125ms-leave.opacity-125ms-leave-active {\n  opacity:0;\n}\n\n.flexbox-handle-horizontal {\n  width: 8px;\n  top: 0;\n  bottom: 0;\n  z-index: 2;\n  position: absolute;\n  cursor: col-resize;\n  div {\n    height:100%;\n    box-shadow: 0.5px 0 0 @border-color-divider;\n  }\n\n  &.flexbox-handle-right {\n    right:-4px;\n    padding-right:4px;\n  }\n  &.flexbox-handle-left {\n    left:-4px;\n    padding-right:4px;\n  }\n}\n\n.flexbox-handle-vertical {\n  width:100%;\n  height:6px;\n  left:0;\n  right:0;\n  z-index:2;\n  position:absolute;\n  cursor: row-resize;\n  div {\n    width:100%;\n  }\n  &.flexbox-handle-top {\n    top:-3px;\n    padding-right:3px;\n  }\n  &.flexbox-handle-bottom {\n    bottom: 0;\n    padding-right:2px;\n  }\n}\n\n.registered-region-visible {\n  border: 1px dashed rgba(255,0,0,0.5);\n  margin: 2px;\n  position:relative;\n  min-height:1.5em;\n  > .name {\n    background-color: rgba(255,180,180,0.9);\n    position: absolute;\n    color: black;\n    font-size: 13px;\n    top:50%;\n    left:50%;\n    white-space: nowrap;\n    z-index:100;\n    -webkit-transform: translate(-50%, -50%);\n    -webkit-user-select:text;\n  }\n  &:hover {\n    border: 1px dashed rgba(255,0,0,1);\n  }\n}\n\n// WINDOWS\n\nbody.platform-win32 {\n  &.window-type-default {\n    .toolbar-menu-control {\n      display:inherit;\n      order:10000;\n      .btn-toolbar {\n        margin-left: 0;\n        padding: 0 17px;\n        line-height: 36px;\n        img.content-mask {\n          vertical-align: middle;\n          background-color: @text-color-heading;\n        }\n      }\n    }\n\n  }\n\n  .item-compose {\n    order: -101;\n  }\n\n  .btn {\n    border-radius: 0;\n  }\n\n  .flexbox-handle-vertical {\n    cursor:ns-resize;\n  }\n  .flexbox-handle-horizontal {\n    cursor:ew-resize;\n  }\n\n  .toolbar-window-controls {\n    display:none;\n  }\n\n  .sheet-toolbar-container {\n    background-image: none;\n    background: @background-primary;\n    .btn-toolbar {\n      transition: background 300ms;\n      margin: 0 0 0 1px;\n      height: 37px;\n      padding: 0 14px;\n      padding-bottom: 2px;\n      border: 0;\n      box-shadow: none;\n      background: none;\n      &:hover {\n        background: darken(@list-hover-bg, 5%);\n      }\n    }\n  }\n  .btn-feedback {\n    background: @blue;\n    &:hover { background: lighten(@blue, 5%); }\n    &:active { background: lighten(@blue, 20%); }\n  }\n}\n\nbody.platform-win32.is-blurred {\n  .sheet-toolbar-container {\n    background-image: none;\n    background: @background-primary;\n    .btn-toolbar {\n      box-shadow: none;\n    }\n  }\n}\n\n// LINUX\n\nbody.platform-linux {\n  .toolbar-window-controls {\n    display:none;\n  }\n  .item-compose {\n    order: -101;\n  }\n\n}\n"
  },
  {
    "path": "packages/client-sync/README.md",
    "content": "# Client Sync\n\nThis is the mail sync engine that runs within the Nylas Mail client\n\nIt is symlinked in as an `internal_package` of Nylas Mail via the `postinstall`\nscript of the root repo.\n\n## Important Usage Notes:\n\nSince this is symlinked in as an `internal_package` of Nylas Mail, there are a\nhandulf of considerations when developing in client-sync. Some common gotchas:\n\n- You MAY use `NylasEnv`, `NylasExports` and other injected libraries in the\n  Nylas Mail client environment.\n- You MAY use any 3rd party library declared in `client-app/package.json`.\n  Since this gets added as a plugin of the Nylas Mail client, you'll have\n  access to all libraries. This works because the `client-app/node_modules` was\n  added to the global require paths. That lets us access client-app plugins\n  without being a file directory decendent of client-app (client-sync is now a\n  sibling of client-app)\n- You may NOT add \"dependencies\" to the `client-sync/package.json`. If you need\n  a 3rd party library, add it to the main `client-app/package.json`. All Nylas\n  Mail plugins (those inside of `internal_packages`), may no longer declare\n  their own dependencies.\n- You should be aggressive at moving generic mail methods to `isomorphic-core`.\n  We may eventually want to make large chunks of client-sync work in a cloud\n  environment as well.\n"
  },
  {
    "path": "packages/client-sync/main.es6",
    "content": "/* eslint global-require: 0 */\nimport Sequelize from 'sequelize'; // eslint-disable-line\nimport {ComponentRegistry} from 'nylas-exports'\nimport {createLogger} from './src/shared/logger'\nimport shimSequelize from './src/shared/shim-sequelize'\nimport {removeDuplicateAccountsWithOldSettings} from './src/shared/dedupe-accounts'\nimport SendTaskManager from './src/local-sync-worker/send-task-manager'\n\nexport async function activate() {\n  shimSequelize(Sequelize);\n  global.Logger = createLogger()\n  require('./src/local-api');\n\n  // NOTE: See https://phab.nylas.com/D4425 for explanation of why this check\n  // is necessary\n  // TODO remove this check after it no longer affects users\n  await removeDuplicateAccountsWithOldSettings()\n\n  require('./src/local-sync-worker');\n  SendTaskManager.activate();\n  const Root = require('./src/local-sync-dashboard/root').default;\n  ComponentRegistry.register(Root, {role: 'Developer:LocalSyncUI'});\n}\n\nexport function deactivate() {\n  SendTaskManager.deactivate()\n}\n"
  },
  {
    "path": "packages/client-sync/package.json",
    "content": "{\n  \"name\": \"client-sync\",\n  \"version\": \"0.0.1\",\n  \"description\": \"The local sync engine for Nylas Mail\",\n  \"main\": \"./main\",\n  \"author\": \"Nylas\",\n  \"license\": \"proprietary\",\n  \"engines\": {\n    \"nylas\": \"*\"\n  },\n  \"windowTypes\": {\n    \"work\": true\n  }\n}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/FetchFolderList/gmail-bengotow.json",
    "content": "{\n\t\"boxes\": {\n\t\t\"GitHub\": {\n\t\t\t\"attribs\": [\"\\\\HasChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": {\n\t\t\t\t\"Electron\": {\n\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\"\n\t\t\t\t},\n\t\t\t\t\"N1\": {\n\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"INBOX\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Junk (Gmail)\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"N1-Snoozed\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Notes\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Receipts\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Recruiters\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Sentry\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"[Gmail]\": {\n\t\t\t\"attribs\": [\"\\\\HasChildren\", \"\\\\Noselect\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": {\n\t\t\t\t\"All Mail\": {\n\t\t\t\t\t\"attribs\": [\"\\\\All\", \"\\\\HasNoChildren\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\",\n\t\t\t\t\t\"special_use_attrib\": \"\\\\All\"\n\t\t\t\t},\n\t\t\t\t\"Drafts\": {\n\t\t\t\t\t\"attribs\": [\"\\\\Drafts\", \"\\\\HasNoChildren\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\",\n\t\t\t\t\t\"special_use_attrib\": \"\\\\Drafts\"\n\t\t\t\t},\n\t\t\t\t\"Important\": {\n\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Important\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\",\n\t\t\t\t\t\"special_use_attrib\": \"\\\\Important\"\n\t\t\t\t},\n\t\t\t\t\"Sent Mail\": {\n\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Sent\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\",\n\t\t\t\t\t\"special_use_attrib\": \"\\\\Sent\"\n\t\t\t\t},\n\t\t\t\t\"Spam\": {\n\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Junk\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\",\n\t\t\t\t\t\"special_use_attrib\": \"\\\\Junk\"\n\t\t\t\t},\n\t\t\t\t\"Starred\": {\n\t\t\t\t\t\"attribs\": [\"\\\\Flagged\", \"\\\\HasNoChildren\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\",\n\t\t\t\t\t\"special_use_attrib\": \"\\\\Flagged\"\n\t\t\t\t},\n\t\t\t\t\"Trash\": {\n\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Trash\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\",\n\t\t\t\t\t\"special_use_attrib\": \"\\\\Trash\"\n\t\t\t\t}\n\t\t\t},\n\t\t\t\"parent\": null\n\t\t}\n\t},\n\t\"expectedFolders\": [{\n\t\t\"role\": \"trash\",\n\t\t\"name\": \"[Gmail]/Trash\"\n\t}, {\n\t\t\"role\": \"spam\",\n\t\t\"name\": \"[Gmail]/Spam\"\n\t}, {\n\t\t\"role\": \"all\",\n\t\t\"name\": \"[Gmail]/All Mail\"\n\t}],\n\t\"expectedLabels\": [{\n\t\t\"role\": \"starred\",\n\t\t\"name\": \"[Gmail]/Starred\"\n\t}, {\n\t\t\"role\": \"sent\",\n\t\t\"name\": \"[Gmail]/Sent Mail\"\n\t}, {\n\t\t\"role\": \"important\",\n\t\t\"name\": \"[Gmail]/Important\"\n\t}, {\n\t\t\"role\": \"drafts\",\n\t\t\"name\": \"[Gmail]/Drafts\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"Sentry\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"Recruiters\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"Receipts\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"Notes\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"N1-Snoozed\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"Junk (Gmail)\"\n\t}, {\n\t\t\"role\": \"inbox\",\n\t\t\"name\": \"INBOX\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"GitHub\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"GitHub/N1\"\n\t}, {\n\t\t\"role\": null,\n\t\t\"name\": \"GitHub/Electron\"\n\t}]\n}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/FetchFolderList/imap-inboxapptest1.json",
    "content": "{\n\t\"boxes\": {\n\t\t\"2016\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"INBOX\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Archive\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Archive\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null,\n\t\t\t\"special_use_attrib\": \"\\\\Archive\"\n\t\t},\n\t\t\"Arts\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Boîte de réception\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Drafts\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Drafts\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null,\n\t\t\t\"special_use_attrib\": \"\\\\Drafts\"\n\t\t},\n\t\t\"Fondue\": {\n\t\t\t\"children\": {\n\t\t\t\t\"Savoyarde\": {\n\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\"parent\": \"[Circular]\"\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"Housse\": {\n\t\t\t\"children\": {\n\t\t\t\t\"De\": {\n\t\t\t\t\t\"children\": {\n\t\t\t\t\t\t\"Bateau\": {\n\t\t\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\t\t\"parent\": \"[Circular]\"\n\t\t\t\t\t\t},\n\t\t\t\t\t\t\"Rateau\": {\n\t\t\t\t\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\t\t\t\t\"delimiter\": \"/\",\n\t\t\t\t\t\t\t\"children\": null,\n\t\t\t\t\t\t\t\"parent\": \"[Circular]\"\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t},\n\t\t\"JJJJJJJ JJJJJJJJ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"N1-Snoozed\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Over the top\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Sent\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Sent\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null,\n\t\t\t\"special_use_attrib\": \"\\\\Sent\"\n\t\t},\n\t\t\"Spam\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Junk\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null,\n\t\t\t\"special_use_attrib\": \"\\\\Junk\"\n\t\t},\n\t\t\"Taxes\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null\n\t\t},\n\t\t\"Trash\": {\n\t\t\t\"attribs\": [\"\\\\HasNoChildren\", \"\\\\Trash\"],\n\t\t\t\"delimiter\": \"/\",\n\t\t\t\"children\": null,\n\t\t\t\"parent\": null,\n\t\t\t\"special_use_attrib\": \"\\\\Trash\"\n\t\t}\n\t},\n\t\"expectedFolders\": [{\n\t\t\"name\": \"Trash\",\n\t\t\"role\": \"trash\"\n\t}, {\n\t\t\"name\": \"Taxes\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"Spam\",\n\t\t\"role\": \"spam\"\n\t}, {\n\t\t\"name\": \"Sent\",\n\t\t\"role\": \"sent\"\n\t}, {\n\t\t\"name\": \"Over the top\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"N1-Snoozed\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"JJJJJJJ JJJJJJJJ 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"Housse/De/Rateau\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"Housse/De/Bateau\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"Fondue/Savoyarde\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"Drafts\",\n\t\t\"role\": \"drafts\"\n\t}, {\n\t\t\"name\": \"Boîte de réception\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"Arts\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"Archive\",\n\t\t\"role\": null\n\t}, {\n\t\t\"name\": \"INBOX\",\n\t\t\"role\": \"inbox\"\n\t}, {\n\t\t\"name\": \"2016\",\n\t\t\"role\": null\n\t}],\n\t\"expectedLabels\": []\n}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseFromImap/crypto-gram-ascii-plaintext.json",
    "content": "{\"imapMessage\":{\"attributes\":{\"struct\":[{\"partID\":\"1\",\"type\":\"text\",\"subtype\":\"plain\",\"params\":{\"charset\":\"us-ascii\",\"format\":\"flowed\"},\"id\":null,\"description\":null,\"encoding\":\"7BIT\",\"size\":42161,\"lines\":820,\"md5\":null,\"disposition\":null,\"language\":null}],\"date\":\"2016-11-15T07:50:26.000Z\",\"flags\":[\"\\\\Seen\"],\"uid\":345982,\"modseq\":\"8120006\",\"x-gm-labels\":[\"\\\\Inbox\"],\"x-gm-msgid\":\"1551049662245032910\",\"x-gm-thrid\":\"1551049662245032910\"},\"headers\":\"Delivered-To: christine@spang.cc\\r\\nReceived: by 10.31.185.141 with SMTP id j135csp15122vkf; Mon, 14 Nov 2016\\r\\n 23:50:26 -0800 (PST)\\r\\nX-Received: by 10.37.220.66 with SMTP id y63mr6697075ybe.190.1479196226438;\\r\\n Mon, 14 Nov 2016 23:50:26 -0800 (PST)\\r\\nReturn-Path: <crypto-gram-bounces@lists.schneier.com>\\r\\nReceived: from schneier.modwest.com (schneier.modwest.com. [204.11.247.92]) by\\r\\n mx.google.com with ESMTPS id i126si6507480ybb.7.2016.11.14.23.50.26 for\\r\\n <christine@spang.cc> (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256\\r\\n bits=128/128); Mon, 14 Nov 2016 23:50:26 -0800 (PST)\\r\\nReceived-SPF: pass (google.com: domain of\\r\\n crypto-gram-bounces@lists.schneier.com designates 204.11.247.92 as permitted\\r\\n sender) client-ip=204.11.247.92;\\r\\nAuthentication-Results: mx.google.com; spf=pass (google.com: domain of\\r\\n crypto-gram-bounces@lists.schneier.com designates 204.11.247.92 as permitted\\r\\n sender) smtp.mailfrom=crypto-gram-bounces@lists.schneier.com\\r\\nReceived: from schneier.modwest.com (localhost [127.0.0.1]) by\\r\\n schneier.modwest.com (Postfix) with ESMTP id A57D33A66E for\\r\\n <christine@spang.cc>; Tue, 15 Nov 2016 00:48:53 -0700 (MST)\\r\\nX-Original-To: crypto-gram@lists.schneier.com\\r\\nDelivered-To: crypto-gram@lists.schneier.com\\r\\nReceived: from webmail.schneier.com (localhost [127.0.0.1]) by\\r\\n schneier.modwest.com (Postfix) with ESMTPA id 735B038F18; Tue, 15 Nov 2016\\r\\n 00:27:10 -0700 (MST)\\r\\nMIME-Version: 1.0\\r\\nDate: Tue, 15 Nov 2016 01:27:10 -0600\\r\\nFrom: Bruce Schneier <schneier@schneier.com>\\r\\nSubject: CRYPTO-GRAM, November 15, 2016\\r\\nMessage-ID: <76bcad7045e1f498eb00e27fc969ee53@schneier.com>\\r\\nX-Sender: schneier@schneier.com\\r\\nUser-Agent: Roundcube Webmail/0.9.5\\r\\nX-Mailman-Approved-At: Tue, 15 Nov 2016 00:45:13 -0700\\r\\nX-BeenThere: crypto-gram@lists.schneier.com\\r\\nX-Mailman-Version: 2.1.15\\r\\nPrecedence: list\\r\\nCc: Crypto-Gram Mailing List <crypto-gram@lists.schneier.com>\\r\\nList-Id: Crypto-Gram Mailing List <crypto-gram.lists.schneier.com>\\r\\nList-Unsubscribe: <https://lists.schneier.com/cgi-bin/mailman/options/crypto-gram>,\\r\\n <mailto:crypto-gram-request@lists.schneier.com?subject=unsubscribe>\\r\\nList-Post: <mailto:crypto-gram@lists.schneier.com>\\r\\nList-Help: <mailto:crypto-gram-request@lists.schneier.com?subject=help>\\r\\nList-Subscribe: <https://lists.schneier.com/cgi-bin/mailman/listinfo/crypto-gram>,\\r\\n <mailto:crypto-gram-request@lists.schneier.com?subject=subscribe>\\r\\nContent-Transfer-Encoding: 7bit\\r\\nContent-Type: text/plain; charset=\\\"us-ascii\\\"; Format=\\\"flowed\\\"\\r\\nTo: christine@spang.cc\\r\\nErrors-To: crypto-gram-bounces@lists.schneier.com\\r\\nSender: \\\"Crypto-Gram\\\" <crypto-gram-bounces@lists.schneier.com>\\r\\n\\r\\n\",\"parts\":{\"1\":\"\\r\\n             CRYPTO-GRAM\\r\\n\\r\\n          November 15, 2016\\r\\n\\r\\n          by Bruce Schneier\\r\\n    CTO, Resilient, an IBM Company\\r\\n        schneier@schneier.com\\r\\n       https://www.schneier.com\\r\\n\\r\\n\\r\\nA free monthly newsletter providing summaries, analyses, insights, and \\r\\ncommentaries on security: computer and otherwise.\\r\\n\\r\\nFor back issues, or to subscribe, visit \\r\\n<https://www.schneier.com/crypto-gram.html>.\\r\\n\\r\\nYou can read this issue on the web at \\r\\n<https://www.schneier.com/crypto-gram/archives/2016/1115.html>. These \\r\\nsame essays and news items appear in the \\\"Schneier on Security\\\" blog at \\r\\n<http://www.schneier.com/blog>, along with a lively and intelligent \\r\\ncomment section. An RSS feed is available.\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\nIn this issue:\\r\\n      Election Security\\r\\n      News\\r\\n      Lessons From the Dyn DDoS Attack\\r\\n      Regulation of the Internet of Things\\r\\n      Schneier News\\r\\n      Virtual Kidnapping\\r\\n      Intelligence Oversight and How It Can Fail\\r\\n      Whistleblower Investigative Report on NSA Suite B Cryptography\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Election Security\\r\\n\\r\\n\\r\\n\\r\\nIt's over. The voting went smoothly. As of the time of writing, there \\r\\nare no serious fraud allegations, nor credible evidence that anyone \\r\\ntampered with voting rolls or voting machines. And most important, the \\r\\nresults are not in doubt.\\r\\n\\r\\nWhile we may breathe a collective sigh of relief about that, we can't \\r\\nignore the issue until the next election. The risks remain.\\r\\n\\r\\nAs computer security experts have been saying for years, our newly \\r\\ncomputerized voting systems are vulnerable to attack by both individual \\r\\nhackers and government-sponsored cyberwarriors. It is only a matter of \\r\\ntime before such an attack happens.\\r\\n\\r\\nElectronic voting machines can be hacked, and those machines that do not \\r\\ninclude a paper ballot that can verify each voter's choice can be hacked \\r\\nundetectably. Voting rolls are also vulnerable; they are all \\r\\ncomputerized databases whose entries can be deleted or changed to sow \\r\\nchaos on Election Day.\\r\\n\\r\\nThe largely ad hoc system in states for collecting and tabulating \\r\\nindividual voting results is vulnerable as well. While the difference \\r\\nbetween theoretical if demonstrable vulnerabilities and an actual attack \\r\\non Election Day is considerable, we got lucky this year. Not just \\r\\npresidential elections are at risk, but state and local elections, too.\\r\\n\\r\\nTo be very clear, this is not about voter fraud. The risks of ineligible \\r\\npeople voting, or people voting twice, have been repeatedly shown to be \\r\\nvirtually nonexistent, and \\\"solutions\\\" to this problem are largely \\r\\nvoter-suppression measures. Election fraud, however, is both far more \\r\\nfeasible and much more worrisome.\\r\\n\\r\\nHere's my worry. On the day after an election, someone claims that a \\r\\nresult was hacked. Maybe one of the candidates points to a wide \\r\\ndiscrepancy between the most recent polls and the actual results. Maybe \\r\\nan anonymous person announces that he hacked a particular brand of \\r\\nvoting machine, describing in detail how. Or maybe it's a system failure \\r\\nduring Election Day: voting machines recording significantly fewer votes \\r\\nthan there were voters, or zero votes for one candidate or another. \\r\\n(These are not theoretical occurrences; they have both happened in the \\r\\nUnited States before, though because of error, not malice.)\\r\\n\\r\\nWe have no procedures for how to proceed if any of these things happen. \\r\\nThere's no manual, no national panel of experts, no regulatory body to \\r\\nsteer us through this crisis. How do we figure out if someone hacked the \\r\\nvote? Can we recover the true votes, or are they lost? What do we do \\r\\nthen?\\r\\n\\r\\nFirst, we need to do more to secure our elections system. We should \\r\\ndeclare our voting systems to be critical national infrastructure. This \\r\\nis largely symbolic, but it demonstrates a commitment to secure \\r\\nelections and makes funding and other resources available to states.\\r\\n\\r\\nWe need national security standards for voting machines, and funding for \\r\\nstates to procure machines that comply with those standards. \\r\\nVoting-security experts can deal with the technical details, but such \\r\\nmachines must include a paper ballot that provides a record verifiable \\r\\nby voters. The simplest and most reliable way to do that is already \\r\\npracticed in 37 states: optical-scan paper ballots, marked by the \\r\\nvoters, counted by computer but recountable by hand. And we need a \\r\\nsystem of pre-election and postelection security audits to increase \\r\\nconfidence in the system.\\r\\n\\r\\nSecond, election tampering, either by a foreign power or by a domestic \\r\\nactor, is inevitable, so we need detailed procedures to follow -- both \\r\\ntechnical procedures to figure out what happened, and legal procedures \\r\\nto figure out what to do -- that will efficiently get us to a fair and \\r\\nequitable election resolution. There should be a board of independent \\r\\ncomputer-security experts to unravel what happened, and a board of \\r\\nindependent election officials, either at the Federal Election \\r\\nCommission or elsewhere, empowered to determine and put in place an \\r\\nappropriate response.\\r\\n\\r\\nIn the absence of such impartial measures, people rush to defend their \\r\\ncandidate and their party. Florida in 2000 was a perfect example. What \\r\\ncould have been a purely technical issue of determining the intent of \\r\\nevery voter became a battle for who would win the presidency. The \\r\\ndebates about hanging chads and spoiled ballots and how broad the \\r\\nrecount should be were contested by people angling for a particular \\r\\noutcome. In the same way, after a hacked election, partisan politics \\r\\nwill place tremendous pressure on officials to make decisions that \\r\\noverride fairness and accuracy.\\r\\n\\r\\nThat is why we need to agree on policies to deal with future election \\r\\nfraud. We need procedures to evaluate claims of voting-machine hacking. \\r\\nWe need a fair and robust vote-auditing process. And we need all of this \\r\\nin place before an election is hacked and battle lines are drawn.\\r\\n\\r\\nIn response to Florida, the Help America Vote Act of 2002 required each \\r\\nstate to publish its own guidelines on what constitutes a vote. Some \\r\\nstates -- Indiana, in particular -- set up a \\\"war room\\\" of public and \\r\\nprivate cybersecurity experts ready to help if anything did occur. While \\r\\nthe Department of Homeland Security is assisting some states with \\r\\nelection security, and the F.B.I. and the Justice Department made some \\r\\npreparations this year, the approach is too piecemeal.\\r\\n\\r\\nElections serve two purposes. First, and most obvious, they are how we \\r\\nchoose a winner. But second, and equally important, they convince the \\r\\nloser -- and all the supporters -- that he or she lost. To achieve the \\r\\nfirst purpose, the voting system must be fair and accurate. To achieve \\r\\nthe second one, it must be *shown* to be fair and accurate.\\r\\n\\r\\nWe need to have these conversations before something happens, when \\r\\neveryone can be calm and rational about the issues. The integrity of our \\r\\nelections is at stake, which means our democracy is at stake.\\r\\n\\r\\nThis essay previously appeared in the New York Times.\\r\\nhttp://www.nytimes.com/2016/11/09/opinion/american-elections-will-be-hacked.html\\r\\n\\r\\nElection-machine vulnerabilities:\\r\\nhttps://www.washingtonpost.com/posteverything/wp/2016/07/27/by-november-russian-hackers-could-target-voting-machines/\\r\\n\\r\\nElections are hard to rig:\\r\\nhttps://www.washingtonpost.com/news/the-fix/wp/2016/08/03/one-reason-to-doubt-the-presidential-election-will-be-rigged-its-a-lot-harder-than-it-seems/\\r\\n\\r\\nVoting systems as critical infrastructure:\\r\\nhttps://papers.ssrn.com/sol3/papers.cfm?abstract_id=2852461\\r\\n\\r\\nVoting machine security:\\r\\nhttps://www.verifiedvoting.org/\\r\\nhttp://votingmachines.procon.org/view.answers.php?questionID=000291\\r\\nhttp://votingmachines.procon.org/view.answers.php?questionID=000291\\r\\n\\r\\nElection-defense preparations for 2016:\\r\\nhttp://www.usatoday.com/story/tech/news/2016/11/05/election-2016-cyber-hack-issues-homeland-security-indiana-pennsylvania-election-protection-verified-voter/93262960/\\r\\nhttp://www.nbcnews.com/storyline/2016-election-day/all-hands-deck-protect-election-hack-say-officials-n679271\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      News\\r\\n\\r\\n\\r\\n\\r\\nLance Spitzner looks at the safety features of a power saw and tries to \\r\\napply them to Internet security.\\r\\nhttps://securingthehuman.sans.org/blog/2016/10/18/what-iot-and-security-needs-to-learn-from-the-dewalt-mitre-saw\\r\\n\\r\\nResearchers discover a clever attack that bypasses the address space \\r\\nlayout randomization (ALSR) on Intel's CPUs.\\r\\nhttp://arstechnica.com/security/2016/10/flaw-in-intel-chips-could-make-malware-attacks-more-potent/\\r\\nhttp://www.cs.ucr.edu/~nael/pubs/micro16.pdf\\r\\n\\r\\nIn an interviw in Wired, President Obama talks about AI risk, \\r\\ncybersecurity, and more.\\r\\nhttps://www.wired.com/2016/10/president-obama-mit-joi-ito-interview/\\r\\n\\r\\nPrivacy makes workers more productive. Interesting research.\\r\\nhttps://www.psychologytoday.com/blog/the-outsourced-mind/201604/want-people-behave-better-give-them-more-privacy\\r\\n\\r\\nNews about the DDOS attacks against Dyn.\\r\\nhttps://motherboard.vice.com/read/twitter-reddit-spotify-were-collateral-damage-in-major-internet-attack\\r\\nhttps://krebsonsecurity.com/2016/10/ddos-on-dyn-impacts-twitter-spotify-reddit/\\r\\nhttps://motherboard.vice.com/read/blame-the-internet-of-things-for-destroying-the-internet-today\\r\\n\\r\\nJosephine Wolff examines different Internet governance stakeholders and \\r\\nhow they frame security debates.\\r\\nhttps://policyreview.info/articles/analysis/what-we-talk-about-when-we-talk-about-cybersecurity-security-internet-governance\\r\\n\\r\\nThe UK is admitting \\\"offensive cyber\\\" operations against ISIS/Daesh. I \\r\\nthink this might be the first time it has been openly acknowledged.\\r\\nhttps://www.theguardian.com/politics/blog/live/2016/oct/20/philip-green-knighthood-commons-set-to-debate-stripping-philip-green-of-his-knighthood-politics-live\\r\\n\\r\\nIt's not hard to imagine the criminal possibilities of automation, \\r\\nautonomy, and artificial intelligence. But the imaginings are becoming \\r\\nmainstream -- and the future isn't too far off.\\r\\nhttp://www.nytimes.com/2016/10/24/technology/artificial-intelligence-evolves-with-its-criminal-potential.html\\r\\n\\r\\nAlong similar lines, computers are able to predict court verdicts. My \\r\\nguess is that the real use here isn't to predict actual court verdicts, \\r\\nbut for well-paid defense teams to test various defensive tactics.\\r\\nhttp://www.telegraph.co.uk/science/2016/10/23/artifically-intelligent-judge-developed-which-can-predict-court/\\r\\n\\r\\nGood long article on the 2015 attack against the US Office of Personnel \\r\\nManagement.\\r\\nhttps://www.wired.com/2016/10/inside-cyberattack-shocked-us-government/\\r\\n\\r\\nHow Powell's and Podesta's e-mail accounts were hacked. It was phishing.\\r\\nhttps://motherboard.vice.com/read/how-hackers-broke-into-john-podesta-and-colin-powells-gmail-accounts\\r\\n\\r\\nA year and a half ago, I wrote about hardware bit-flipping attacks, \\r\\nwhich were then largely theoretical. Now, they can be used to root \\r\\nAndroid phones.\\r\\nhttp://arstechnica.com/security/2016/10/using-rowhammer-bitflips-to-root-android-phones-is-now-a-thing/\\r\\nhttps://vvdveen.com/publications/drammer.pdf\\r\\nhttps://www.vusec.net/projects/drammer/\\r\\n\\r\\nEavesdropping on typing while connected over VoIP.\\r\\nhttps://arxiv.org/pdf/1609.09359.pdf\\r\\nhttps://news.uci.edu/research/typing-while-skyping-could-compromise-privacy/\\r\\n\\r\\nAn impressive Chinese device that automatically reads marked cards in \\r\\norder to cheat at poker and other card games.\\r\\nhttps://www.elie.net/blog/security/fuller-house-exposing-high-end-poker-cheating-devices\\r\\n\\r\\nA useful guide on how to avoid kidnapping children on Halloween.\\r\\nhttp://reductress.com/post/how-to-not-kidnap-any-kids-on-halloween-not-even-one/\\r\\n\\r\\nA card game based on the iterated prisoner's dilemma.\\r\\nhttps://opinionatedgamers.com/2016/10/26/h-m-s-dolores-game-review-by-chris-wray/\\r\\n\\r\\nThere's another leak of NSA hacking tools and data from the Shadow \\r\\nBrokers. This one includes a list of hacked sites. The data is old, but \\r\\nyou can see if you've been hacked.\\r\\nhttp://arstechnica.co.uk/security/2016/10/new-leak-may-show-if-you-were-hacked-by-the-nsa/\\r\\nHonestly, I am surprised by this release. I thought that the original \\r\\nShadow Brokers dump was everything. Now that we know they held things \\r\\nback, there could easily be more releases.\\r\\nhttp://www.networkworld.com/article/3137065/security/shadow-brokers-leak-list-of-nsa-targets-and-compromised-servers.html\\r\\nNote that the Hague-based Organization for the Prohibition of Chemical \\r\\nWeapons is on the list, hacked in 2000.\\r\\nhttps://boingboing.net/2016/11/06/in-2000-the-nsa-hacked-the-ha.html\\r\\n\\r\\nFree cybersecurity MOOC from F-Secure and the University of Finland.\\r\\nhttp://mooc.fi/courses/2016/cybersecurity/\\r\\n\\r\\nResearchers have trained a neural network to encrypt its communications. \\r\\nThis story is more about AI and neural networks than it is about \\r\\ncryptography. The algorithm isn't any good, but is a perfect example of \\r\\nwhat I've heard called \\\"Schneier's Law\\\": Anyone can design a cipher that \\r\\nthey themselves cannot break.\\r\\nhttps://www.newscientist.com/article/2110522-googles-neural-networks-invent-their-own-encryption/\\r\\nhttp://arstechnica.com/information-technology/2016/10/google-ai-neural-network-cryptography/\\r\\nhttps://www.engadget.com/2016/10/28/google-ai-created-its-own-form-of-encryption/\\r\\nhttps://arxiv.org/pdf/1610.06918v1.pdf\\r\\nSchneier's Law:\\r\\nhttps://www.schneier.com/blog/archives/2011/04/schneiers_law.html\\r\\n\\r\\nGoogle now links anonymous browser tracking with identifiable tracking. \\r\\nThe article also explains how to opt out.\\r\\nhttps://www.propublica.org/article/google-has-quietly-dropped-ban-on-personally-identifiable-web-tracking\\r\\n\\r\\nNew Atlas has a great three-part feature on the history of hacking as \\r\\nportrayed in films, including video clips. The 1980s. The 1990s. The \\r\\n2000s.\\r\\nhttp://newatlas.com/history-hollywood-hacking-1980s/45482/\\r\\nhttp://newatlas.com/hollywood-hacking-movies-1990s/45623/\\r\\nhttp://newatlas.com/hollywood-hacking-2000s/45965\\r\\n\\r\\nFor years, the DMCA has been used to stifle legitimate research into the \\r\\nsecurity of embedded systems. Finally, the research exemption to the \\r\\nDMCA is in effect (for two years, but we can hope it'll be extended \\r\\nforever).\\r\\nhttps://www.wired.com/2016/10/hacking-car-pacemaker-toaster-just-became-legal/\\r\\nhttps://www.eff.org/deeplinks/2016/10/why-did-we-have-wait-year-fix-our-cars\\r\\n\\r\\nFirefox is removing the battery status API, citing privacy concerns.\\r\\nhttps://www.fxsitecompat.com/en-CA/docs/2016/battery-status-api-has-been-removed/\\r\\nhttps://eprint.iacr.org/2015/616.pdf\\r\\nW3C is updating the spec.\\r\\nhttps://www.w3.org/TR/battery-status/#acknowledgements\\r\\nHere's a battery tracker found in the wild.\\r\\nhttp://randomwalker.info/publications/OpenWPM_1_million_site_tracking_measurement.pdf\\r\\n\\r\\nElection-day humor from 2004, but still relevent.\\r\\nhttp://www.ganssle.com/tem/tem316.html#article2\\r\\n\\r\\nA self-propagating smart light bulb worm.\\r\\nhttp://iotworm.eyalro.net/\\r\\nhttps://boingboing.net/2016/11/09/a-lightbulb-worm-could-take-ov.html\\r\\nhttps://tech.slashdot.org/story/16/11/09/0041201/researchers-hack-philips-hue-smart-bulbs-using-a-drone\\r\\nThis is exactly the sort of Internet-of-Things attack that has me \\r\\nworried.\\r\\n\\r\\nAd networks are surreptitiously using ultrasonic communications to jump \\r\\nfrom device to device. It should come as no surprise that this \\r\\ncommunications channel can be used to hack devices as well.\\r\\nhttps://www.newscientist.com/article/2110762-your-homes-online-gadgets-could-be-hacked-by-ultrasound/\\r\\nhttps://www.schneier.com/blog/archives/2015/11/ads_surreptitio.html\\r\\n\\r\\nThis is some interesting research. You can fool facial recognition \\r\\nsystems by wearing glasses printed with elements of other peoples' \\r\\nfaces.\\r\\nhttps://www.cs.cmu.edu/~sbhagava/papers/face-rec-ccs16.pdf\\r\\nhttp://qz.com/823820/carnegie-mellon-made-a-special-pair-of-glasses-that-lets-you-steal-a-digital-identity/\\r\\nhttps://boingboing.net/2016/11/02/researchers-trick-facial-recog.html\\r\\n\\r\\nInteresting research: \\\"Using Artificial Intelligence to Identify State \\r\\nSecrets,\\\" https://arxiv.org/abs/1611.00356\\r\\n\\r\\nThere's a Kickstarter for a sticker that you can stick on a glove and \\r\\nthen register with a biometric access system like an iPhone. It's an \\r\\ninteresting security trade-off: swapping something you are (the \\r\\nbiometric) with something you have (the glove).\\r\\nhttps://www.kickstarter.com/projects/nanotips/taps-touchscreen-sticker-w-touch-id-ships-before-x?token=5b586aa6\\r\\nhttps://gizmodo.com/these-fake-fingerprint-stickers-let-you-access-a-protec-1788710313\\r\\n\\r\\nJulian Oliver has designed and built a cellular eavesdropping device \\r\\nthat's disguised as an old HP printer. It's more of a conceptual art \\r\\npiece than an actual piece of eavesdropping equipment, but it still \\r\\nmakes the point.\\r\\nhttps://julianoliver.com/output/stealth-cell-tower\\r\\nhttps://www.wired.com/2016/11/evil-office-printer-hijacks-cellphone-connection/\\r\\nhttps://boingboing.net/2016/11/03/a-fake-hp-printer-thats-actu.html\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Lessons From the Dyn DDoS Attack\\r\\n\\r\\n\\r\\n\\r\\nA week ago Friday, someone took down numerous popular websites in a \\r\\nmassive distributed denial-of-service (DDoS) attack against the domain \\r\\nname provider Dyn. DDoS attacks are neither new nor sophisticated. The \\r\\nattacker sends a massive amount of traffic, causing the victim's system \\r\\nto slow to a crawl and eventually crash. There are more or less clever \\r\\nvariants, but basically, it's a datapipe-size battle between attacker \\r\\nand victim. If the defender has a larger capacity to receive and process \\r\\ndata, he or she will win. If the attacker can throw more data than the \\r\\nvictim can process, he or she will win.\\r\\n\\r\\nThe attacker can build a giant data cannon, but that's expensive. It is \\r\\nmuch smarter to recruit millions of innocent computers on the internet. \\r\\nThis is the \\\"distributed\\\" part of the DDoS attack, and pretty much how \\r\\nit's worked for decades. Cybercriminals infect innocent computers around \\r\\nthe internet and recruit them into a botnet. They then target that \\r\\nbotnet against a single victim.\\r\\n\\r\\nYou can imagine how it might work in the real world. If I can trick tens \\r\\nof thousands of others to order pizzas to be delivered to your house at \\r\\nthe same time, I can clog up your street and prevent any legitimate \\r\\ntraffic from getting through. If I can trick many millions, I might be \\r\\nable to crush your house from the weight. That's a DDoS attack -- it's \\r\\nsimple brute force.\\r\\n\\r\\nAs you'd expect, DDoSers have various motives. The attacks started out \\r\\nas a way to show off, then quickly transitioned to a method of \\r\\nintimidation -- or a way of just getting back at someone you didn't \\r\\nlike. More recently, they've become vehicles of protest. In 2013, the \\r\\nhacker group Anonymous petitioned the White House to recognize DDoS \\r\\nattacks as a legitimate form of protest. Criminals have used these \\r\\nattacks as a means of extortion, although one group found that just the \\r\\nfear of attack was enough. Military agencies are also thinking about \\r\\nDDoS as a tool in their cyberwar arsenals. A 2007 DDoS attack against \\r\\nEstonia was blamed on Russia and widely called an act of cyberwar.\\r\\n\\r\\nThe DDoS attack against Dyn two weeks ago was nothing new, but it \\r\\nillustrated several important trends in computer security.\\r\\n\\r\\nThese attack techniques are broadly available. Fully capable DDoS attack \\r\\ntools are available for free download. Criminal groups offer DDoS \\r\\nservices for hire. The particular attack technique used against Dyn was \\r\\nfirst used a month earlier. It's called Mirai, and since the source code \\r\\nwas released four weeks ago, over a dozen botnets have incorporated the \\r\\ncode.\\r\\n\\r\\nThe Dyn attacks were probably not originated by a government. The \\r\\nperpetrators were most likely hackers mad at Dyn for helping Brian Krebs \\r\\nidentify -- and the FBI arrest -- two Israeli hackers who were running a \\r\\nDDoS-for-hire ring. Recently I have written about probing DDoS attacks \\r\\nagainst internet infrastructure companies that appear to be perpetrated \\r\\nby a nation-state. But, honestly, we don't know for sure.\\r\\n\\r\\nThis is important. Software spreads capabilities. The smartest attacker \\r\\nneeds to figure out the attack and write the software. After that, \\r\\nanyone can use it. There's not even much of a difference between \\r\\ngovernment and criminal attacks. In December 2014, there was a \\r\\nlegitimate debate in the security community as to whether the massive \\r\\nattack against Sony had been perpetrated by a nation-state with a $20 \\r\\nbillion military budget or a couple of guys in a basement somewhere. The \\r\\ninternet is the only place where we can't tell the difference. Everyone \\r\\nuses the same tools, the same techniques and the same tactics.\\r\\n\\r\\nThese attacks are getting larger. The Dyn DDoS attack set a record at \\r\\n1.2 Tbps. The previous record holder was the attack against \\r\\ncybersecurity journalist Brian Krebs a month prior at 620 Gbps. This is \\r\\nmuch larger than required to knock the typical website offline. A year \\r\\nago, it was unheard of. Now it occurs regularly.\\r\\n\\r\\nThe botnets attacking Dyn and Brian Krebs consisted largely of unsecure \\r\\nInternet of Things (IoT) devices -- webcams, digital video recorders, \\r\\nrouters and so on. This isn't new, either. We've already seen \\r\\ninternet-enabled refrigerators and TVs used in DDoS botnets. But again, \\r\\nthe scale is bigger now. In 2014, the news was hundreds of thousands of \\r\\nIoT devices -- the Dyn attack used millions. Analysts expect the IoT to \\r\\nincrease the number of things on the internet by a factor of 10 or more. \\r\\nExpect these attacks to similarly increase.\\r\\n\\r\\nThe problem is that these IoT devices are unsecure and likely to remain \\r\\nthat way. The economics of internet security don't trickle down to the \\r\\nIoT. Commenting on the Krebs attack last month, I wrote:\\r\\n\\r\\n     The market can't fix this because neither the buyer nor the\\r\\n     seller cares. Think of all the CCTV cameras and DVRs used in\\r\\n     the attack against Brian Krebs. The owners of those devices\\r\\n     don't care. Their devices were cheap to buy, they still work,\\r\\n     and they don't even know Brian. The sellers of those devices\\r\\n     don't care: They're now selling newer and better models, and\\r\\n     the original buyers only cared about price and features. There\\r\\n     is no market solution because the insecurity is what economists\\r\\n     call an externality: It's an effect of the purchasing decision\\r\\n     that affects other people. Think of it kind of like invisible\\r\\n     pollution.\\r\\n\\r\\nTo be fair, one company that made some of the unsecure things used in \\r\\nthese attacks recalled its unsecure webcams. But this is more of a \\r\\npublicity stunt than anything else. I would be surprised if the company \\r\\ngot many devices back. We already know that the reputational damage from \\r\\nhaving your unsecure software made public isn't large and doesn't last. \\r\\nAt this point, the market still largely rewards sacrificing security in \\r\\nfavor of price and time-to-market.\\r\\n\\r\\nDDoS prevention works best deep in the network, where the pipes are the \\r\\nlargest and the capability to identify and block the attacks is the most \\r\\nevident. But the backbone providers have no incentive to do this. They \\r\\ndon't feel the pain when the attacks occur and they have no way of \\r\\nbilling for the service when they provide it. So they let the attacks \\r\\nthrough and force the victims to defend themselves. In many ways, this \\r\\nis similar to the spam problem. It, too, is best dealt with in the \\r\\nbackbone, but similar economics dump the problem onto the endpoints.\\r\\n\\r\\nWe're unlikely to get any regulation forcing backbone companies to clean \\r\\nup either DDoS attacks or spam, just as we are unlikely to get any \\r\\nregulations forcing IoT manufacturers to make their systems secure. This \\r\\nis me again:\\r\\n\\r\\n     What this all means is that the IoT will remain insecure unless\\r\\n     government steps in and fixes the problem. When we have market\\r\\n     failures, government is the only solution. The government could\\r\\n     impose security regulations on IoT manufacturers, forcing them\\r\\n     to make their devices secure even though their customers don't\\r\\n     care. They could impose liabilities on manufacturers, allowing\\r\\n     people like Brian Krebs to sue them. Any of these would raise\\r\\n     the cost of insecurity and give companies incentives to spend\\r\\n     money making their devices secure.\\r\\n\\r\\nThat leaves the victims to pay. This is where we are in much of computer \\r\\nsecurity. Because the hardware, software and networks we use are so \\r\\nunsecure, we have to pay an entire industry to provide after-the-fact \\r\\nsecurity.\\r\\n\\r\\nThere are solutions you can buy. Many companies offer DDoS protection, \\r\\nalthough they're generally calibrated to the older, smaller attacks. We \\r\\ncan safely assume that they'll up their offerings, although the cost \\r\\nmight be prohibitive for many users. Understand your risks. Buy \\r\\nmitigation if you need it, but understand its limitations. Know the \\r\\nattacks are possible and will succeed if large enough. And the attacks \\r\\nare getting larger all the time. Prepare for that.\\r\\n\\r\\nThis essay previously appeared on the SecurityIntelligence website.\\r\\nhttps://securityintelligence.com/lessons-from-the-dyn-ddos-attack/\\r\\n\\r\\nhttps://securityintelligence.com/news/multi-phased-ddos-attack-causes-hours-long-outages/\\r\\nhttp://arstechnica.com/information-technology/2016/10/inside-the-machine-uprising-how-cameras-dvrs-took-down-parts-of-the-internet/\\r\\nhttps://www.theguardian.com/technology/2016/oct/26/ddos-attack-dyn-mirai-botnet\\r\\nhttp://searchsecurity.techtarget.com/news/450401962/Details-emerging-on-Dyn-DNS-DDoS-attack-Mirai-IoT-botnet\\r\\nhttp://hub.dyn.com/static/hub.dyn.com/dyn-blog/dyn-statement-on-10-21-2016-ddos-attack.html\\r\\n\\r\\nDDoS petition:\\r\\nhttp://www.huffingtonpost.com/2013/01/12/anonymous-ddos-petition-white-house_n_2463009.html\\r\\n\\r\\nDDoS extortion:\\r\\nhttps://securityintelligence.com/ddos-extortion-easy-and-lucrative/\\r\\nhttp://www.computerworld.com/article/3061813/security/empty-ddos-threats-deliver-100k-to-extortion-group.html\\r\\n\\r\\nDDoS against Estonia:\\r\\nhttp://www.iar-gwu.org/node/65\\r\\n\\r\\nDDoS for hire:\\r\\nhttp://www.forbes.com/sites/thomasbrewster/2016/10/23/massive-ddos-iot-botnet-for-hire-twitter-dyn-amazon/#11f82518c915\\r\\n\\r\\nMirai:\\r\\nhttps://www.arbornetworks.com/blog/asert/mirai-iot-botnet-description-ddos-attack-mitigation/\\r\\nhttps://krebsonsecurity.com/2016/10/source-code-for-iot-botnet-mirai-released/\\r\\nhttps://threatpost.com/mirai-bots-more-than-double-since-source-code-release/121368/\\r\\n\\r\\nKrebs:\\r\\nhttp://krebsonsecurity.com/2016/09/israeli-online-attack-service-vdos-earned-600000-in-two-years/\\r\\nhttp://www.theverge.com/2016/9/11/12878692/vdos-israeli-teens-ddos-cyberattack-service-arrested\\r\\nhttps://krebsonsecurity.com/2016/09/krebsonsecurity-hit-with-record-ddos/\\r\\nhttp://www.businessinsider.com/akamai-brian-krebs-ddos-attack-2016-9\\r\\n\\r\\nNation-state DDoS Attacks:\\r\\nhttps://www.schneier.com/blog/archives/2016/09/someone_is_lear.html\\r\\n\\r\\nNorth Korea and Sony:\\r\\nhttps://www.theatlantic.com/international/archive/2014/12/did-north-korea-really-attack-sony/383973/\\r\\n\\r\\nInternet of Things (IoT) security:\\r\\nhttps://securityintelligence.com/will-internet-things-leveraged-ruin-companys-day-understanding-iot-security/\\r\\nhttps://thehackernews.com/2014/01/100000-refrigerators-and-other-home.html\\r\\n\\r\\nEver larger DDoS Attacks:\\r\\nhttp://www.ibtimes.co.uk/biggest-ever-terabit-scale-ddos-attack-yet-could-be-horizon-experts-warn-1588364\\r\\n\\r\\nMy previous essay on this:\\r\\nhttps://www.schneier.com/essays/archives/2016/10/we_need_to_save_the_.html\\r\\n\\r\\nrecalled:\\r\\nhttp://www.zdnet.com/article/chinese-tech-giant-recalls-webcams-used-in-dyn-cyberattack/\\r\\n\\r\\nidentify and block the attacks:\\r\\nhttp://www.ibm.com/security/threat-protection/\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Regulation of the Internet of Things\\r\\n\\r\\n\\r\\n\\r\\nLate last month, popular websites like Twitter, Pinterest, Reddit and \\r\\nPayPal went down for most of a day. The distributed denial-of-service \\r\\nattack that caused the outages, and the vulnerabilities that made the \\r\\nattack possible, was as much a failure of market and policy as it was of \\r\\ntechnology. If we want to secure our increasingly computerized and \\r\\nconnected world, we need more government involvement in the security of \\r\\nthe \\\"Internet of Things\\\" and increased regulation of what are now \\r\\ncritical and life-threatening technologies. It's no longer a question of \\r\\nif, it's a question of when.\\r\\n\\r\\nFirst, the facts. Those websites went down because their domain name \\r\\nprovider -- a company named Dyn -- was forced offline. We don't know who \\r\\nperpetrated that attack, but it could have easily been a lone hacker. \\r\\nWhoever it was launched a distributed denial-of-service attack against \\r\\nDyn by exploiting a vulnerability in large numbers -- possibly millions \\r\\n-- of Internet-of-Things devices like webcams and digital video \\r\\nrecorders, then recruiting them all into a single botnet. The botnet \\r\\nbombarded Dyn with traffic, so much that it went down. And when it went \\r\\ndown, so did dozens of websites.\\r\\n\\r\\nYour security on the Internet depends on the security of millions of \\r\\nInternet-enabled devices, designed and sold by companies you've never \\r\\nheard of to consumers who don't care about your security.\\r\\n\\r\\nThe technical reason these devices are insecure is complicated, but \\r\\nthere is a market failure at work. The Internet of Things is bringing \\r\\ncomputerization and connectivity to many tens of millions of devices \\r\\nworldwide. These devices will affect every aspect of our lives, because \\r\\nthey're things like cars, home appliances, thermostats, lightbulbs, \\r\\nfitness trackers, medical devices, smart streetlights and sidewalk \\r\\nsquares. Many of these devices are low-cost, designed and built \\r\\noffshore, then rebranded and resold. The teams building these devices \\r\\ndon't have the security expertise we've come to expect from the major \\r\\ncomputer and smartphone manufacturers, simply because the market won't \\r\\nstand for the additional costs that would require. These devices don't \\r\\nget security updates like our more expensive computers, and many don't \\r\\neven have a way to be patched. And, unlike our computers and phones, \\r\\nthey stay around for years and decades.\\r\\n\\r\\nAn additional market failure illustrated by the Dyn attack is that \\r\\nneither the seller nor the buyer of those devices cares about fixing the \\r\\nvulnerability. The owners of those devices don't care. They wanted a \\r\\nwebcam -- or thermostat, or refrigerator -- with nice features at a good \\r\\nprice. Even after they were recruited into this botnet, they still work \\r\\nfine -- you can't even tell they were used in the attack. The sellers of \\r\\nthose devices don't care: They've already moved on to selling newer and \\r\\nbetter models. There is no market solution because the insecurity \\r\\nprimarily affects other people. It's a form of invisible pollution.\\r\\n\\r\\nAnd, like pollution, the only solution is to regulate. The government \\r\\ncould impose minimum security standards on IoT manufacturers, forcing \\r\\nthem to make their devices secure even though their customers don't \\r\\ncare. They could impose liabilities on manufacturers, allowing companies \\r\\nlike Dyn to sue them if their devices are used in DDoS attacks. The \\r\\ndetails would need to be carefully scoped, but either of these options \\r\\nwould raise the cost of insecurity and give companies incentives to \\r\\nspend money making their devices secure.\\r\\n\\r\\nIt's true that this is a domestic solution to an international problem \\r\\nand that there's no U.S. regulation that will affect, say, an Asian-made \\r\\nproduct sold in South America, even though that product could still be \\r\\nused to take down U.S. websites. But the main costs in making software \\r\\ncome from development. If the United States and perhaps a few other \\r\\nmajor markets implement strong Internet-security regulations on IoT \\r\\ndevices, manufacturers will be forced to upgrade their security if they \\r\\nwant to sell to those markets. And any improvements they make in their \\r\\nsoftware will be available in their products wherever they are sold, \\r\\nsimply because it makes no sense to maintain two different versions of \\r\\nthe software. This is truly an area where the actions of a few countries \\r\\ncan drive worldwide change.\\r\\n\\r\\nRegardless of what you think about regulation vs. market solutions, I \\r\\nbelieve there is no choice. Governments will get involved in the IoT, \\r\\nbecause the risks are too great and the stakes are too high. Computers \\r\\nare now able to affect our world in a direct and physical manner.\\r\\n\\r\\nSecurity researchers have demonstrated the ability to remotely take \\r\\ncontrol of Internet-enabled cars. They've demonstrated ransomware \\r\\nagainst home thermostats and exposed vulnerabilities in implanted \\r\\nmedical devices. They've hacked voting machines and power plants. In one \\r\\nrecent paper, researchers showed how a vulnerability in smart lightbulbs \\r\\ncould be used to start a chain reaction, resulting in them *all* being \\r\\ncontrolled by the attackers -- that's every one in a city. Security \\r\\nflaws in these things could mean people dying and property being \\r\\ndestroyed.\\r\\n\\r\\nNothing motivates the U.S. government like fear. Remember 2001? A \\r\\nsmall-government Republican president created the Department of Homeland \\r\\nSecurity in the wake of the Sept. 11 terrorist attacks: a rushed and \\r\\nill-thought-out decision that we've been trying to fix for more than a \\r\\ndecade. A fatal IoT disaster will similarly spur our government into \\r\\naction, and it's unlikely to be well-considered and thoughtful action. \\r\\nOur choice isn't between government involvement and no government \\r\\ninvolvement. Our choice is between smarter government involvement and \\r\\nstupider government involvement. We have to start thinking about this \\r\\nnow. Regulations are necessary, important and complex -- and they're \\r\\ncoming. We can't afford to ignore these issues until it's too late.\\r\\n\\r\\nIn general, the software market demands that products be fast and cheap \\r\\nand that security be a secondary consideration. That was okay when \\r\\nsoftware didn't matter -- it was okay that your spreadsheet crashed once \\r\\nin a while. But a software bug that literally crashes your car is \\r\\nanother thing altogether. The security vulnerabilities in the Internet \\r\\nof Things are deep and pervasive, and they won't get fixed if the market \\r\\nis left to sort it out for itself. We need to proactively discuss good \\r\\nregulatory solutions; otherwise, a disaster will impose bad ones on us.\\r\\n\\r\\nThis essay previously appeared in the Washington Post.\\r\\nhttps://www.washingtonpost.com/posteverything/wp/2016/11/03/your-wifi-connected-thermostat-can-take-down-the-whole-internet-we-need-new-regulations/\\r\\n\\r\\n\\r\\nDDoS:\\r\\nhttps://www.washingtonpost.com/news/the-switch/wp/2016/10/21/someone-attacked-a-major-part-of-the-internets-infrastructure/\\r\\n\\r\\nIoT and DDoS:\\r\\nhttps://krebsonsecurity.com/2016/10/hacked-cameras-dvrs-powered-todays-massive-internet-outage/\\r\\n\\r\\nThe IoT market failure and regulation:\\r\\nhttps://www.schneier.com/essays/archives/2016/10/we_need_to_save_the_.html\\r\\nhttps://www.wired.com/2014/01/theres-no-good-way-to-patch-the-internet-of-things-and-thats-a-huge-problem/\\r\\nhttp://www.computerworld.com/article/3136650/security/after-ddos-attack-senator-seeks-industry-led-security-standards-for-iot-devices.html\\r\\n\\r\\nIoT ransomware:\\r\\nhttps://motherboard.vice.com/read/internet-of-things-ransomware-smart-thermostat\\r\\nmedical:\\r\\n\\r\\nHacking medical devices:\\r\\nhttp://motherboard.vice.com/read/hackers-killed-a-simulated-human-by-turning-off-its-pacemaker\\r\\nhttp://abcnews.go.com/US/vice-president-dick-cheney-feared-pacemaker-hacking/story?id=20621434\\r\\n\\r\\nHacking voting machines:\\r\\nhttp://www.politico.com/magazine/story/2016/08/2016-elections-russia-hack-how-to-hack-an-election-in-seven-minutes-214144\\r\\n\\r\\nHacking power plants:\\r\\nhttps://www.wired.com/2016/01/everything-we-know-about-ukraines-power-plant-hack/\\r\\n\\r\\nHacking light bulbs:\\r\\nhttp://iotworm.eyalro.net\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Schneier News\\r\\n\\r\\n\\r\\nI am speaking in Cambridge, MA on November 15 at the Harvard Big-Data \\r\\nClub.\\r\\nhttp://harvardbigdata.com/event/keynote-lecture-bruce-schneier\\r\\n\\r\\nI am speaking in Palm Springs, CA on November 30 at the TEDMED \\r\\nConference.\\r\\nhttp://www.tedmed.com/speakers/show?id=627300\\r\\n\\r\\nI am participating in the Resilient end-of-year webinar on December 8.\\r\\nhttp://info.resilientsystems.com/webinar-eoy-cybersecurity-2016-review-2017-predictions\\r\\n\\r\\nI am speaking on December 14 in Accra at the University of Ghana.\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Virtual Kidnapping\\r\\n\\r\\n\\r\\n\\r\\nThis is a harrowing story of a scam artist that convinced a mother that \\r\\nher daughter had been kidnapped. It's unclear if these virtual \\r\\nkidnappers use data about their victims, or just call people at random \\r\\nand hope to get lucky. Still, it's a new criminal use of smartphones and \\r\\nubiquitous information. Reminds me of the scammers who call low-wage \\r\\nworkers at retail establishments late at night and convince them to do \\r\\noutlandish and occasionally dangerous things.\\r\\nhttps://www.washingtonpost.com/local/we-have-your-daughter-a-virtual-kidnapping-and-a-mothers-five-hours-of-hell/2016/10/03/8f082690-8963-11e6-875e-2c1bfe943b66_story.html\\r\\nMore stories are here.\\r\\nhttp://www.nbcwashington.com/investigations/Several-Virtual-Kidnapping-Attempts-in-Maryland-Recently-375792991.html\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Intelligence Oversight and How It Can Fail\\r\\n\\r\\n\\r\\n\\r\\nFormer NSA attorneys John DeLong and Susan Hennessay have written a \\r\\nfascinating article describing a particular incident of oversight \\r\\nfailure inside the NSA. Technically, the story hinges on a definitional \\r\\ndifference between the NSA and the FISA court meaning of the word \\r\\n\\\"archived.\\\" (For the record, I would have defaulted to the NSA's \\r\\ninterpretation, which feels more accurate technically.) But while the \\r\\nstory is worth reading, what's especially interesting are the broader \\r\\nissues about how a nontechnical judiciary can provide oversight over a \\r\\nvery technical data collection-and-analysis organization -- especially \\r\\nif the oversight must largely be conducted in secret.\\r\\n\\r\\nIn many places I have separated different kinds of oversight: are we \\r\\ndoing things right versus are we doing the right things? This is very \\r\\nmuch about the first: is the NSA complying with the rules the courts \\r\\nimpose on them? I believe that the NSA tries very hard to follow the \\r\\nrules it's given, while at the same time being very aggressive about how \\r\\nit interprets any kind of ambiguities and using its nonadversarial \\r\\nrelationship with its overseers to its advantage.\\r\\n\\r\\nThe only possible solution I can see to all of this is more public \\r\\nscrutiny. Secrecy is toxic here.\\r\\n\\r\\nhttps://www.lawfareblog.com/understanding-footnote-14-nsa-lawyering-oversight-and-compliance\\r\\n\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Whistleblower Investigative Report on NSA Suite B Cryptography\\r\\n\\r\\n\\r\\n\\r\\nThe NSA has been abandoning secret and proprietary cryptographic \\r\\nalgorithms in favor of commercial public algorithms, generally known as \\r\\n\\\"Suite B.\\\" In 2010, an NSA employee filed some sort of whistleblower \\r\\ncomplaint, alleging that this move is both insecure and wasteful.  The \\r\\nUS DoD Inspector General investigated and wrote a report in 2011.\\r\\n\\r\\nThe report -- slightly redacted and declassified -- found that there was \\r\\nno wrongdoing. But the report is an interesting window into the NSA's \\r\\nsystem of algorithm selection and testing (pages 5 and 6), as well as \\r\\nhow they investigate whistleblower complaints.\\r\\n\\r\\nhttp://www.dodig.mil/FOIA/err/11-INTEL-06%20(Redacted).pdf\\r\\n\\r\\nSuite B Cryptography:\\r\\nhttp://csrc.nist.gov/groups/SMA/ispab/documents/minutes/2006-03/E_Barker-March2006-ISPAB.pdf\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\nSince 1998, CRYPTO-GRAM has been a free monthly newsletter providing \\r\\nsummaries, analyses, insights, and commentaries on security: computer \\r\\nand otherwise. You can subscribe, unsubscribe, or change your address on \\r\\nthe Web at <https://www.schneier.com/crypto-gram.html>. Back issues are \\r\\nalso available at that URL.\\r\\n\\r\\nPlease feel free to forward CRYPTO-GRAM, in whole or in part, to \\r\\ncolleagues and friends who will find it valuable. Permission is also \\r\\ngranted to reprint CRYPTO-GRAM, as long as it is reprinted in its \\r\\nentirety.\\r\\n\\r\\nCRYPTO-GRAM is written by Bruce Schneier. Bruce Schneier is an \\r\\ninternationally renowned security technologist, called a \\\"security guru\\\" \\r\\nby The Economist. He is the author of 13 books -- including his latest, \\r\\n\\\"Data and Goliath\\\" -- as well as hundreds of articles, essays, and \\r\\nacademic papers. His influential newsletter \\\"Crypto-Gram\\\" and his blog \\r\\n\\\"Schneier on Security\\\" are read by over 250,000 people. He has testified \\r\\nbefore Congress, is a frequent guest on television and radio, has served \\r\\non several government committees, and is regularly quoted in the press. \\r\\nSchneier is a fellow at the Berkman Center for Internet and Society at \\r\\nHarvard Law School, a program fellow at the New America Foundation's \\r\\nOpen Technology Institute, a board member of the Electronic Frontier \\r\\nFoundation, an Advisory Board Member of the Electronic Privacy \\r\\nInformation Center, and the Chief Technology Officer at Resilient, an \\r\\nIBM Company.  See <https://www.schneier.com>.\\r\\n\\r\\nCrypto-Gram is a personal newsletter. Opinions expressed are not \\r\\nnecessarily those of Resilient, an IBM Company.\\r\\n\\r\\nCopyright (c) 2016 by Bruce Schneier.\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\nTo unsubscribe from Crypto-Gram, click this link:\\r\\n\\r\\nhttps://lists.schneier.com/cgi-bin/mailman/options/crypto-gram/christine%40spang.cc?login-unsub=Unsubscribe\\r\\n\\r\\nYou will be e-mailed a confirmation message.  Follow the instructions in that message to confirm your removal from the list.\\r\\n\"}},\"desiredParts\":[{\"id\":\"1\",\"encoding\":\"7BIT\",\"mimetype\":\"text/plain\"}],\"result\":{\"to\":[{\"name\":\"\",\"email\":\"christine@spang.cc\"}],\"cc\":[{\"name\":\"Crypto-Gram Mailing List\",\"email\":\"crypto-gram@lists.schneier.com\"}],\"bcc\":[],\"from\":[{\"name\":\"Bruce Schneier\",\"email\":\"schneier@schneier.com\"}],\"replyTo\":[],\"accountId\":\"test-account-id\",\"body\":\"<pre class=\\\"nylas-plaintext\\\">\\r\\n             CRYPTO-GRAM\\r\\n\\r\\n          November 15, 2016\\r\\n\\r\\n          by Bruce Schneier\\r\\n    CTO, Resilient, an IBM Company\\r\\n        schneier@schneier.com\\r\\n       https://www.schneier.com\\r\\n\\r\\n\\r\\nA free monthly newsletter providing summaries, analyses, insights, and \\r\\ncommentaries on security: computer and otherwise.\\r\\n\\r\\nFor back issues, or to subscribe, visit \\r\\n&lt;https://www.schneier.com/crypto-gram.html&gt;.\\r\\n\\r\\nYou can read this issue on the web at \\r\\n&lt;https://www.schneier.com/crypto-gram/archives/2016/1115.html&gt;. These \\r\\nsame essays and news items appear in the &quot;Schneier on Security&quot; blog at \\r\\n&lt;http://www.schneier.com/blog&gt;, along with a lively and intelligent \\r\\ncomment section. An RSS feed is available.\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\nIn this issue:\\r\\n      Election Security\\r\\n      News\\r\\n      Lessons From the Dyn DDoS Attack\\r\\n      Regulation of the Internet of Things\\r\\n      Schneier News\\r\\n      Virtual Kidnapping\\r\\n      Intelligence Oversight and How It Can Fail\\r\\n      Whistleblower Investigative Report on NSA Suite B Cryptography\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Election Security\\r\\n\\r\\n\\r\\n\\r\\nIt&#x27;s over. The voting went smoothly. As of the time of writing, there \\r\\nare no serious fraud allegations, nor credible evidence that anyone \\r\\ntampered with voting rolls or voting machines. And most important, the \\r\\nresults are not in doubt.\\r\\n\\r\\nWhile we may breathe a collective sigh of relief about that, we can&#x27;t \\r\\nignore the issue until the next election. The risks remain.\\r\\n\\r\\nAs computer security experts have been saying for years, our newly \\r\\ncomputerized voting systems are vulnerable to attack by both individual \\r\\nhackers and government-sponsored cyberwarriors. It is only a matter of \\r\\ntime before such an attack happens.\\r\\n\\r\\nElectronic voting machines can be hacked, and those machines that do not \\r\\ninclude a paper ballot that can verify each voter&#x27;s choice can be hacked \\r\\nundetectably. Voting rolls are also vulnerable; they are all \\r\\ncomputerized databases whose entries can be deleted or changed to sow \\r\\nchaos on Election Day.\\r\\n\\r\\nThe largely ad hoc system in states for collecting and tabulating \\r\\nindividual voting results is vulnerable as well. While the difference \\r\\nbetween theoretical if demonstrable vulnerabilities and an actual attack \\r\\non Election Day is considerable, we got lucky this year. Not just \\r\\npresidential elections are at risk, but state and local elections, too.\\r\\n\\r\\nTo be very clear, this is not about voter fraud. The risks of ineligible \\r\\npeople voting, or people voting twice, have been repeatedly shown to be \\r\\nvirtually nonexistent, and &quot;solutions&quot; to this problem are largely \\r\\nvoter-suppression measures. Election fraud, however, is both far more \\r\\nfeasible and much more worrisome.\\r\\n\\r\\nHere&#x27;s my worry. On the day after an election, someone claims that a \\r\\nresult was hacked. Maybe one of the candidates points to a wide \\r\\ndiscrepancy between the most recent polls and the actual results. Maybe \\r\\nan anonymous person announces that he hacked a particular brand of \\r\\nvoting machine, describing in detail how. Or maybe it&#x27;s a system failure \\r\\nduring Election Day: voting machines recording significantly fewer votes \\r\\nthan there were voters, or zero votes for one candidate or another. \\r\\n(These are not theoretical occurrences; they have both happened in the \\r\\nUnited States before, though because of error, not malice.)\\r\\n\\r\\nWe have no procedures for how to proceed if any of these things happen. \\r\\nThere&#x27;s no manual, no national panel of experts, no regulatory body to \\r\\nsteer us through this crisis. How do we figure out if someone hacked the \\r\\nvote? Can we recover the true votes, or are they lost? What do we do \\r\\nthen?\\r\\n\\r\\nFirst, we need to do more to secure our elections system. We should \\r\\ndeclare our voting systems to be critical national infrastructure. This \\r\\nis largely symbolic, but it demonstrates a commitment to secure \\r\\nelections and makes funding and other resources available to states.\\r\\n\\r\\nWe need national security standards for voting machines, and funding for \\r\\nstates to procure machines that comply with those standards. \\r\\nVoting-security experts can deal with the technical details, but such \\r\\nmachines must include a paper ballot that provides a record verifiable \\r\\nby voters. The simplest and most reliable way to do that is already \\r\\npracticed in 37 states: optical-scan paper ballots, marked by the \\r\\nvoters, counted by computer but recountable by hand. And we need a \\r\\nsystem of pre-election and postelection security audits to increase \\r\\nconfidence in the system.\\r\\n\\r\\nSecond, election tampering, either by a foreign power or by a domestic \\r\\nactor, is inevitable, so we need detailed procedures to follow -- both \\r\\ntechnical procedures to figure out what happened, and legal procedures \\r\\nto figure out what to do -- that will efficiently get us to a fair and \\r\\nequitable election resolution. There should be a board of independent \\r\\ncomputer-security experts to unravel what happened, and a board of \\r\\nindependent election officials, either at the Federal Election \\r\\nCommission or elsewhere, empowered to determine and put in place an \\r\\nappropriate response.\\r\\n\\r\\nIn the absence of such impartial measures, people rush to defend their \\r\\ncandidate and their party. Florida in 2000 was a perfect example. What \\r\\ncould have been a purely technical issue of determining the intent of \\r\\nevery voter became a battle for who would win the presidency. The \\r\\ndebates about hanging chads and spoiled ballots and how broad the \\r\\nrecount should be were contested by people angling for a particular \\r\\noutcome. In the same way, after a hacked election, partisan politics \\r\\nwill place tremendous pressure on officials to make decisions that \\r\\noverride fairness and accuracy.\\r\\n\\r\\nThat is why we need to agree on policies to deal with future election \\r\\nfraud. We need procedures to evaluate claims of voting-machine hacking. \\r\\nWe need a fair and robust vote-auditing process. And we need all of this \\r\\nin place before an election is hacked and battle lines are drawn.\\r\\n\\r\\nIn response to Florida, the Help America Vote Act of 2002 required each \\r\\nstate to publish its own guidelines on what constitutes a vote. Some \\r\\nstates -- Indiana, in particular -- set up a &quot;war room&quot; of public and \\r\\nprivate cybersecurity experts ready to help if anything did occur. While \\r\\nthe Department of Homeland Security is assisting some states with \\r\\nelection security, and the F.B.I. and the Justice Department made some \\r\\npreparations this year, the approach is too piecemeal.\\r\\n\\r\\nElections serve two purposes. First, and most obvious, they are how we \\r\\nchoose a winner. But second, and equally important, they convince the \\r\\nloser -- and all the supporters -- that he or she lost. To achieve the \\r\\nfirst purpose, the voting system must be fair and accurate. To achieve \\r\\nthe second one, it must be *shown* to be fair and accurate.\\r\\n\\r\\nWe need to have these conversations before something happens, when \\r\\neveryone can be calm and rational about the issues. The integrity of our \\r\\nelections is at stake, which means our democracy is at stake.\\r\\n\\r\\nThis essay previously appeared in the New York Times.\\r\\nhttp://www.nytimes.com/2016/11/09/opinion/american-elections-will-be-hacked.html\\r\\n\\r\\nElection-machine vulnerabilities:\\r\\nhttps://www.washingtonpost.com/posteverything/wp/2016/07/27/by-november-russian-hackers-could-target-voting-machines/\\r\\n\\r\\nElections are hard to rig:\\r\\nhttps://www.washingtonpost.com/news/the-fix/wp/2016/08/03/one-reason-to-doubt-the-presidential-election-will-be-rigged-its-a-lot-harder-than-it-seems/\\r\\n\\r\\nVoting systems as critical infrastructure:\\r\\nhttps://papers.ssrn.com/sol3/papers.cfm?abstract_id=2852461\\r\\n\\r\\nVoting machine security:\\r\\nhttps://www.verifiedvoting.org/\\r\\nhttp://votingmachines.procon.org/view.answers.php?questionID=000291\\r\\nhttp://votingmachines.procon.org/view.answers.php?questionID=000291\\r\\n\\r\\nElection-defense preparations for 2016:\\r\\nhttp://www.usatoday.com/story/tech/news/2016/11/05/election-2016-cyber-hack-issues-homeland-security-indiana-pennsylvania-election-protection-verified-voter/93262960/\\r\\nhttp://www.nbcnews.com/storyline/2016-election-day/all-hands-deck-protect-election-hack-say-officials-n679271\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      News\\r\\n\\r\\n\\r\\n\\r\\nLance Spitzner looks at the safety features of a power saw and tries to \\r\\napply them to Internet security.\\r\\nhttps://securingthehuman.sans.org/blog/2016/10/18/what-iot-and-security-needs-to-learn-from-the-dewalt-mitre-saw\\r\\n\\r\\nResearchers discover a clever attack that bypasses the address space \\r\\nlayout randomization (ALSR) on Intel&#x27;s CPUs.\\r\\nhttp://arstechnica.com/security/2016/10/flaw-in-intel-chips-could-make-malware-attacks-more-potent/\\r\\nhttp://www.cs.ucr.edu/~nael/pubs/micro16.pdf\\r\\n\\r\\nIn an interviw in Wired, President Obama talks about AI risk, \\r\\ncybersecurity, and more.\\r\\nhttps://www.wired.com/2016/10/president-obama-mit-joi-ito-interview/\\r\\n\\r\\nPrivacy makes workers more productive. Interesting research.\\r\\nhttps://www.psychologytoday.com/blog/the-outsourced-mind/201604/want-people-behave-better-give-them-more-privacy\\r\\n\\r\\nNews about the DDOS attacks against Dyn.\\r\\nhttps://motherboard.vice.com/read/twitter-reddit-spotify-were-collateral-damage-in-major-internet-attack\\r\\nhttps://krebsonsecurity.com/2016/10/ddos-on-dyn-impacts-twitter-spotify-reddit/\\r\\nhttps://motherboard.vice.com/read/blame-the-internet-of-things-for-destroying-the-internet-today\\r\\n\\r\\nJosephine Wolff examines different Internet governance stakeholders and \\r\\nhow they frame security debates.\\r\\nhttps://policyreview.info/articles/analysis/what-we-talk-about-when-we-talk-about-cybersecurity-security-internet-governance\\r\\n\\r\\nThe UK is admitting &quot;offensive cyber&quot; operations against ISIS/Daesh. I \\r\\nthink this might be the first time it has been openly acknowledged.\\r\\nhttps://www.theguardian.com/politics/blog/live/2016/oct/20/philip-green-knighthood-commons-set-to-debate-stripping-philip-green-of-his-knighthood-politics-live\\r\\n\\r\\nIt&#x27;s not hard to imagine the criminal possibilities of automation, \\r\\nautonomy, and artificial intelligence. But the imaginings are becoming \\r\\nmainstream -- and the future isn&#x27;t too far off.\\r\\nhttp://www.nytimes.com/2016/10/24/technology/artificial-intelligence-evolves-with-its-criminal-potential.html\\r\\n\\r\\nAlong similar lines, computers are able to predict court verdicts. My \\r\\nguess is that the real use here isn&#x27;t to predict actual court verdicts, \\r\\nbut for well-paid defense teams to test various defensive tactics.\\r\\nhttp://www.telegraph.co.uk/science/2016/10/23/artifically-intelligent-judge-developed-which-can-predict-court/\\r\\n\\r\\nGood long article on the 2015 attack against the US Office of Personnel \\r\\nManagement.\\r\\nhttps://www.wired.com/2016/10/inside-cyberattack-shocked-us-government/\\r\\n\\r\\nHow Powell&#x27;s and Podesta&#x27;s e-mail accounts were hacked. It was phishing.\\r\\nhttps://motherboard.vice.com/read/how-hackers-broke-into-john-podesta-and-colin-powells-gmail-accounts\\r\\n\\r\\nA year and a half ago, I wrote about hardware bit-flipping attacks, \\r\\nwhich were then largely theoretical. Now, they can be used to root \\r\\nAndroid phones.\\r\\nhttp://arstechnica.com/security/2016/10/using-rowhammer-bitflips-to-root-android-phones-is-now-a-thing/\\r\\nhttps://vvdveen.com/publications/drammer.pdf\\r\\nhttps://www.vusec.net/projects/drammer/\\r\\n\\r\\nEavesdropping on typing while connected over VoIP.\\r\\nhttps://arxiv.org/pdf/1609.09359.pdf\\r\\nhttps://news.uci.edu/research/typing-while-skyping-could-compromise-privacy/\\r\\n\\r\\nAn impressive Chinese device that automatically reads marked cards in \\r\\norder to cheat at poker and other card games.\\r\\nhttps://www.elie.net/blog/security/fuller-house-exposing-high-end-poker-cheating-devices\\r\\n\\r\\nA useful guide on how to avoid kidnapping children on Halloween.\\r\\nhttp://reductress.com/post/how-to-not-kidnap-any-kids-on-halloween-not-even-one/\\r\\n\\r\\nA card game based on the iterated prisoner&#x27;s dilemma.\\r\\nhttps://opinionatedgamers.com/2016/10/26/h-m-s-dolores-game-review-by-chris-wray/\\r\\n\\r\\nThere&#x27;s another leak of NSA hacking tools and data from the Shadow \\r\\nBrokers. This one includes a list of hacked sites. The data is old, but \\r\\nyou can see if you&#x27;ve been hacked.\\r\\nhttp://arstechnica.co.uk/security/2016/10/new-leak-may-show-if-you-were-hacked-by-the-nsa/\\r\\nHonestly, I am surprised by this release. I thought that the original \\r\\nShadow Brokers dump was everything. Now that we know they held things \\r\\nback, there could easily be more releases.\\r\\nhttp://www.networkworld.com/article/3137065/security/shadow-brokers-leak-list-of-nsa-targets-and-compromised-servers.html\\r\\nNote that the Hague-based Organization for the Prohibition of Chemical \\r\\nWeapons is on the list, hacked in 2000.\\r\\nhttps://boingboing.net/2016/11/06/in-2000-the-nsa-hacked-the-ha.html\\r\\n\\r\\nFree cybersecurity MOOC from F-Secure and the University of Finland.\\r\\nhttp://mooc.fi/courses/2016/cybersecurity/\\r\\n\\r\\nResearchers have trained a neural network to encrypt its communications. \\r\\nThis story is more about AI and neural networks than it is about \\r\\ncryptography. The algorithm isn&#x27;t any good, but is a perfect example of \\r\\nwhat I&#x27;ve heard called &quot;Schneier&#x27;s Law&quot;: Anyone can design a cipher that \\r\\nthey themselves cannot break.\\r\\nhttps://www.newscientist.com/article/2110522-googles-neural-networks-invent-their-own-encryption/\\r\\nhttp://arstechnica.com/information-technology/2016/10/google-ai-neural-network-cryptography/\\r\\nhttps://www.engadget.com/2016/10/28/google-ai-created-its-own-form-of-encryption/\\r\\nhttps://arxiv.org/pdf/1610.06918v1.pdf\\r\\nSchneier&#x27;s Law:\\r\\nhttps://www.schneier.com/blog/archives/2011/04/schneiers_law.html\\r\\n\\r\\nGoogle now links anonymous browser tracking with identifiable tracking. \\r\\nThe article also explains how to opt out.\\r\\nhttps://www.propublica.org/article/google-has-quietly-dropped-ban-on-personally-identifiable-web-tracking\\r\\n\\r\\nNew Atlas has a great three-part feature on the history of hacking as \\r\\nportrayed in films, including video clips. The 1980s. The 1990s. The \\r\\n2000s.\\r\\nhttp://newatlas.com/history-hollywood-hacking-1980s/45482/\\r\\nhttp://newatlas.com/hollywood-hacking-movies-1990s/45623/\\r\\nhttp://newatlas.com/hollywood-hacking-2000s/45965\\r\\n\\r\\nFor years, the DMCA has been used to stifle legitimate research into the \\r\\nsecurity of embedded systems. Finally, the research exemption to the \\r\\nDMCA is in effect (for two years, but we can hope it&#x27;ll be extended \\r\\nforever).\\r\\nhttps://www.wired.com/2016/10/hacking-car-pacemaker-toaster-just-became-legal/\\r\\nhttps://www.eff.org/deeplinks/2016/10/why-did-we-have-wait-year-fix-our-cars\\r\\n\\r\\nFirefox is removing the battery status API, citing privacy concerns.\\r\\nhttps://www.fxsitecompat.com/en-CA/docs/2016/battery-status-api-has-been-removed/\\r\\nhttps://eprint.iacr.org/2015/616.pdf\\r\\nW3C is updating the spec.\\r\\nhttps://www.w3.org/TR/battery-status/#acknowledgements\\r\\nHere&#x27;s a battery tracker found in the wild.\\r\\nhttp://randomwalker.info/publications/OpenWPM_1_million_site_tracking_measurement.pdf\\r\\n\\r\\nElection-day humor from 2004, but still relevent.\\r\\nhttp://www.ganssle.com/tem/tem316.html#article2\\r\\n\\r\\nA self-propagating smart light bulb worm.\\r\\nhttp://iotworm.eyalro.net/\\r\\nhttps://boingboing.net/2016/11/09/a-lightbulb-worm-could-take-ov.html\\r\\nhttps://tech.slashdot.org/story/16/11/09/0041201/researchers-hack-philips-hue-smart-bulbs-using-a-drone\\r\\nThis is exactly the sort of Internet-of-Things attack that has me \\r\\nworried.\\r\\n\\r\\nAd networks are surreptitiously using ultrasonic communications to jump \\r\\nfrom device to device. It should come as no surprise that this \\r\\ncommunications channel can be used to hack devices as well.\\r\\nhttps://www.newscientist.com/article/2110762-your-homes-online-gadgets-could-be-hacked-by-ultrasound/\\r\\nhttps://www.schneier.com/blog/archives/2015/11/ads_surreptitio.html\\r\\n\\r\\nThis is some interesting research. You can fool facial recognition \\r\\nsystems by wearing glasses printed with elements of other peoples&#x27; \\r\\nfaces.\\r\\nhttps://www.cs.cmu.edu/~sbhagava/papers/face-rec-ccs16.pdf\\r\\nhttp://qz.com/823820/carnegie-mellon-made-a-special-pair-of-glasses-that-lets-you-steal-a-digital-identity/\\r\\nhttps://boingboing.net/2016/11/02/researchers-trick-facial-recog.html\\r\\n\\r\\nInteresting research: &quot;Using Artificial Intelligence to Identify State \\r\\nSecrets,&quot; https://arxiv.org/abs/1611.00356\\r\\n\\r\\nThere&#x27;s a Kickstarter for a sticker that you can stick on a glove and \\r\\nthen register with a biometric access system like an iPhone. It&#x27;s an \\r\\ninteresting security trade-off: swapping something you are (the \\r\\nbiometric) with something you have (the glove).\\r\\nhttps://www.kickstarter.com/projects/nanotips/taps-touchscreen-sticker-w-touch-id-ships-before-x?token=5b586aa6\\r\\nhttps://gizmodo.com/these-fake-fingerprint-stickers-let-you-access-a-protec-1788710313\\r\\n\\r\\nJulian Oliver has designed and built a cellular eavesdropping device \\r\\nthat&#x27;s disguised as an old HP printer. It&#x27;s more of a conceptual art \\r\\npiece than an actual piece of eavesdropping equipment, but it still \\r\\nmakes the point.\\r\\nhttps://julianoliver.com/output/stealth-cell-tower\\r\\nhttps://www.wired.com/2016/11/evil-office-printer-hijacks-cellphone-connection/\\r\\nhttps://boingboing.net/2016/11/03/a-fake-hp-printer-thats-actu.html\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Lessons From the Dyn DDoS Attack\\r\\n\\r\\n\\r\\n\\r\\nA week ago Friday, someone took down numerous popular websites in a \\r\\nmassive distributed denial-of-service (DDoS) attack against the domain \\r\\nname provider Dyn. DDoS attacks are neither new nor sophisticated. The \\r\\nattacker sends a massive amount of traffic, causing the victim&#x27;s system \\r\\nto slow to a crawl and eventually crash. There are more or less clever \\r\\nvariants, but basically, it&#x27;s a datapipe-size battle between attacker \\r\\nand victim. If the defender has a larger capacity to receive and process \\r\\ndata, he or she will win. If the attacker can throw more data than the \\r\\nvictim can process, he or she will win.\\r\\n\\r\\nThe attacker can build a giant data cannon, but that&#x27;s expensive. It is \\r\\nmuch smarter to recruit millions of innocent computers on the internet. \\r\\nThis is the &quot;distributed&quot; part of the DDoS attack, and pretty much how \\r\\nit&#x27;s worked for decades. Cybercriminals infect innocent computers around \\r\\nthe internet and recruit them into a botnet. They then target that \\r\\nbotnet against a single victim.\\r\\n\\r\\nYou can imagine how it might work in the real world. If I can trick tens \\r\\nof thousands of others to order pizzas to be delivered to your house at \\r\\nthe same time, I can clog up your street and prevent any legitimate \\r\\ntraffic from getting through. If I can trick many millions, I might be \\r\\nable to crush your house from the weight. That&#x27;s a DDoS attack -- it&#x27;s \\r\\nsimple brute force.\\r\\n\\r\\nAs you&#x27;d expect, DDoSers have various motives. The attacks started out \\r\\nas a way to show off, then quickly transitioned to a method of \\r\\nintimidation -- or a way of just getting back at someone you didn&#x27;t \\r\\nlike. More recently, they&#x27;ve become vehicles of protest. In 2013, the \\r\\nhacker group Anonymous petitioned the White House to recognize DDoS \\r\\nattacks as a legitimate form of protest. Criminals have used these \\r\\nattacks as a means of extortion, although one group found that just the \\r\\nfear of attack was enough. Military agencies are also thinking about \\r\\nDDoS as a tool in their cyberwar arsenals. A 2007 DDoS attack against \\r\\nEstonia was blamed on Russia and widely called an act of cyberwar.\\r\\n\\r\\nThe DDoS attack against Dyn two weeks ago was nothing new, but it \\r\\nillustrated several important trends in computer security.\\r\\n\\r\\nThese attack techniques are broadly available. Fully capable DDoS attack \\r\\ntools are available for free download. Criminal groups offer DDoS \\r\\nservices for hire. The particular attack technique used against Dyn was \\r\\nfirst used a month earlier. It&#x27;s called Mirai, and since the source code \\r\\nwas released four weeks ago, over a dozen botnets have incorporated the \\r\\ncode.\\r\\n\\r\\nThe Dyn attacks were probably not originated by a government. The \\r\\nperpetrators were most likely hackers mad at Dyn for helping Brian Krebs \\r\\nidentify -- and the FBI arrest -- two Israeli hackers who were running a \\r\\nDDoS-for-hire ring. Recently I have written about probing DDoS attacks \\r\\nagainst internet infrastructure companies that appear to be perpetrated \\r\\nby a nation-state. But, honestly, we don&#x27;t know for sure.\\r\\n\\r\\nThis is important. Software spreads capabilities. The smartest attacker \\r\\nneeds to figure out the attack and write the software. After that, \\r\\nanyone can use it. There&#x27;s not even much of a difference between \\r\\ngovernment and criminal attacks. In December 2014, there was a \\r\\nlegitimate debate in the security community as to whether the massive \\r\\nattack against Sony had been perpetrated by a nation-state with a $20 \\r\\nbillion military budget or a couple of guys in a basement somewhere. The \\r\\ninternet is the only place where we can&#x27;t tell the difference. Everyone \\r\\nuses the same tools, the same techniques and the same tactics.\\r\\n\\r\\nThese attacks are getting larger. The Dyn DDoS attack set a record at \\r\\n1.2 Tbps. The previous record holder was the attack against \\r\\ncybersecurity journalist Brian Krebs a month prior at 620 Gbps. This is \\r\\nmuch larger than required to knock the typical website offline. A year \\r\\nago, it was unheard of. Now it occurs regularly.\\r\\n\\r\\nThe botnets attacking Dyn and Brian Krebs consisted largely of unsecure \\r\\nInternet of Things (IoT) devices -- webcams, digital video recorders, \\r\\nrouters and so on. This isn&#x27;t new, either. We&#x27;ve already seen \\r\\ninternet-enabled refrigerators and TVs used in DDoS botnets. But again, \\r\\nthe scale is bigger now. In 2014, the news was hundreds of thousands of \\r\\nIoT devices -- the Dyn attack used millions. Analysts expect the IoT to \\r\\nincrease the number of things on the internet by a factor of 10 or more. \\r\\nExpect these attacks to similarly increase.\\r\\n\\r\\nThe problem is that these IoT devices are unsecure and likely to remain \\r\\nthat way. The economics of internet security don&#x27;t trickle down to the \\r\\nIoT. Commenting on the Krebs attack last month, I wrote:\\r\\n\\r\\n     The market can&#x27;t fix this because neither the buyer nor the\\r\\n     seller cares. Think of all the CCTV cameras and DVRs used in\\r\\n     the attack against Brian Krebs. The owners of those devices\\r\\n     don&#x27;t care. Their devices were cheap to buy, they still work,\\r\\n     and they don&#x27;t even know Brian. The sellers of those devices\\r\\n     don&#x27;t care: They&#x27;re now selling newer and better models, and\\r\\n     the original buyers only cared about price and features. There\\r\\n     is no market solution because the insecurity is what economists\\r\\n     call an externality: It&#x27;s an effect of the purchasing decision\\r\\n     that affects other people. Think of it kind of like invisible\\r\\n     pollution.\\r\\n\\r\\nTo be fair, one company that made some of the unsecure things used in \\r\\nthese attacks recalled its unsecure webcams. But this is more of a \\r\\npublicity stunt than anything else. I would be surprised if the company \\r\\ngot many devices back. We already know that the reputational damage from \\r\\nhaving your unsecure software made public isn&#x27;t large and doesn&#x27;t last. \\r\\nAt this point, the market still largely rewards sacrificing security in \\r\\nfavor of price and time-to-market.\\r\\n\\r\\nDDoS prevention works best deep in the network, where the pipes are the \\r\\nlargest and the capability to identify and block the attacks is the most \\r\\nevident. But the backbone providers have no incentive to do this. They \\r\\ndon&#x27;t feel the pain when the attacks occur and they have no way of \\r\\nbilling for the service when they provide it. So they let the attacks \\r\\nthrough and force the victims to defend themselves. In many ways, this \\r\\nis similar to the spam problem. It, too, is best dealt with in the \\r\\nbackbone, but similar economics dump the problem onto the endpoints.\\r\\n\\r\\nWe&#x27;re unlikely to get any regulation forcing backbone companies to clean \\r\\nup either DDoS attacks or spam, just as we are unlikely to get any \\r\\nregulations forcing IoT manufacturers to make their systems secure. This \\r\\nis me again:\\r\\n\\r\\n     What this all means is that the IoT will remain insecure unless\\r\\n     government steps in and fixes the problem. When we have market\\r\\n     failures, government is the only solution. The government could\\r\\n     impose security regulations on IoT manufacturers, forcing them\\r\\n     to make their devices secure even though their customers don&#x27;t\\r\\n     care. They could impose liabilities on manufacturers, allowing\\r\\n     people like Brian Krebs to sue them. Any of these would raise\\r\\n     the cost of insecurity and give companies incentives to spend\\r\\n     money making their devices secure.\\r\\n\\r\\nThat leaves the victims to pay. This is where we are in much of computer \\r\\nsecurity. Because the hardware, software and networks we use are so \\r\\nunsecure, we have to pay an entire industry to provide after-the-fact \\r\\nsecurity.\\r\\n\\r\\nThere are solutions you can buy. Many companies offer DDoS protection, \\r\\nalthough they&#x27;re generally calibrated to the older, smaller attacks. We \\r\\ncan safely assume that they&#x27;ll up their offerings, although the cost \\r\\nmight be prohibitive for many users. Understand your risks. Buy \\r\\nmitigation if you need it, but understand its limitations. Know the \\r\\nattacks are possible and will succeed if large enough. And the attacks \\r\\nare getting larger all the time. Prepare for that.\\r\\n\\r\\nThis essay previously appeared on the SecurityIntelligence website.\\r\\nhttps://securityintelligence.com/lessons-from-the-dyn-ddos-attack/\\r\\n\\r\\nhttps://securityintelligence.com/news/multi-phased-ddos-attack-causes-hours-long-outages/\\r\\nhttp://arstechnica.com/information-technology/2016/10/inside-the-machine-uprising-how-cameras-dvrs-took-down-parts-of-the-internet/\\r\\nhttps://www.theguardian.com/technology/2016/oct/26/ddos-attack-dyn-mirai-botnet\\r\\nhttp://searchsecurity.techtarget.com/news/450401962/Details-emerging-on-Dyn-DNS-DDoS-attack-Mirai-IoT-botnet\\r\\nhttp://hub.dyn.com/static/hub.dyn.com/dyn-blog/dyn-statement-on-10-21-2016-ddos-attack.html\\r\\n\\r\\nDDoS petition:\\r\\nhttp://www.huffingtonpost.com/2013/01/12/anonymous-ddos-petition-white-house_n_2463009.html\\r\\n\\r\\nDDoS extortion:\\r\\nhttps://securityintelligence.com/ddos-extortion-easy-and-lucrative/\\r\\nhttp://www.computerworld.com/article/3061813/security/empty-ddos-threats-deliver-100k-to-extortion-group.html\\r\\n\\r\\nDDoS against Estonia:\\r\\nhttp://www.iar-gwu.org/node/65\\r\\n\\r\\nDDoS for hire:\\r\\nhttp://www.forbes.com/sites/thomasbrewster/2016/10/23/massive-ddos-iot-botnet-for-hire-twitter-dyn-amazon/#11f82518c915\\r\\n\\r\\nMirai:\\r\\nhttps://www.arbornetworks.com/blog/asert/mirai-iot-botnet-description-ddos-attack-mitigation/\\r\\nhttps://krebsonsecurity.com/2016/10/source-code-for-iot-botnet-mirai-released/\\r\\nhttps://threatpost.com/mirai-bots-more-than-double-since-source-code-release/121368/\\r\\n\\r\\nKrebs:\\r\\nhttp://krebsonsecurity.com/2016/09/israeli-online-attack-service-vdos-earned-600000-in-two-years/\\r\\nhttp://www.theverge.com/2016/9/11/12878692/vdos-israeli-teens-ddos-cyberattack-service-arrested\\r\\nhttps://krebsonsecurity.com/2016/09/krebsonsecurity-hit-with-record-ddos/\\r\\nhttp://www.businessinsider.com/akamai-brian-krebs-ddos-attack-2016-9\\r\\n\\r\\nNation-state DDoS Attacks:\\r\\nhttps://www.schneier.com/blog/archives/2016/09/someone_is_lear.html\\r\\n\\r\\nNorth Korea and Sony:\\r\\nhttps://www.theatlantic.com/international/archive/2014/12/did-north-korea-really-attack-sony/383973/\\r\\n\\r\\nInternet of Things (IoT) security:\\r\\nhttps://securityintelligence.com/will-internet-things-leveraged-ruin-companys-day-understanding-iot-security/\\r\\nhttps://thehackernews.com/2014/01/100000-refrigerators-and-other-home.html\\r\\n\\r\\nEver larger DDoS Attacks:\\r\\nhttp://www.ibtimes.co.uk/biggest-ever-terabit-scale-ddos-attack-yet-could-be-horizon-experts-warn-1588364\\r\\n\\r\\nMy previous essay on this:\\r\\nhttps://www.schneier.com/essays/archives/2016/10/we_need_to_save_the_.html\\r\\n\\r\\nrecalled:\\r\\nhttp://www.zdnet.com/article/chinese-tech-giant-recalls-webcams-used-in-dyn-cyberattack/\\r\\n\\r\\nidentify and block the attacks:\\r\\nhttp://www.ibm.com/security/threat-protection/\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Regulation of the Internet of Things\\r\\n\\r\\n\\r\\n\\r\\nLate last month, popular websites like Twitter, Pinterest, Reddit and \\r\\nPayPal went down for most of a day. The distributed denial-of-service \\r\\nattack that caused the outages, and the vulnerabilities that made the \\r\\nattack possible, was as much a failure of market and policy as it was of \\r\\ntechnology. If we want to secure our increasingly computerized and \\r\\nconnected world, we need more government involvement in the security of \\r\\nthe &quot;Internet of Things&quot; and increased regulation of what are now \\r\\ncritical and life-threatening technologies. It&#x27;s no longer a question of \\r\\nif, it&#x27;s a question of when.\\r\\n\\r\\nFirst, the facts. Those websites went down because their domain name \\r\\nprovider -- a company named Dyn -- was forced offline. We don&#x27;t know who \\r\\nperpetrated that attack, but it could have easily been a lone hacker. \\r\\nWhoever it was launched a distributed denial-of-service attack against \\r\\nDyn by exploiting a vulnerability in large numbers -- possibly millions \\r\\n-- of Internet-of-Things devices like webcams and digital video \\r\\nrecorders, then recruiting them all into a single botnet. The botnet \\r\\nbombarded Dyn with traffic, so much that it went down. And when it went \\r\\ndown, so did dozens of websites.\\r\\n\\r\\nYour security on the Internet depends on the security of millions of \\r\\nInternet-enabled devices, designed and sold by companies you&#x27;ve never \\r\\nheard of to consumers who don&#x27;t care about your security.\\r\\n\\r\\nThe technical reason these devices are insecure is complicated, but \\r\\nthere is a market failure at work. The Internet of Things is bringing \\r\\ncomputerization and connectivity to many tens of millions of devices \\r\\nworldwide. These devices will affect every aspect of our lives, because \\r\\nthey&#x27;re things like cars, home appliances, thermostats, lightbulbs, \\r\\nfitness trackers, medical devices, smart streetlights and sidewalk \\r\\nsquares. Many of these devices are low-cost, designed and built \\r\\noffshore, then rebranded and resold. The teams building these devices \\r\\ndon&#x27;t have the security expertise we&#x27;ve come to expect from the major \\r\\ncomputer and smartphone manufacturers, simply because the market won&#x27;t \\r\\nstand for the additional costs that would require. These devices don&#x27;t \\r\\nget security updates like our more expensive computers, and many don&#x27;t \\r\\neven have a way to be patched. And, unlike our computers and phones, \\r\\nthey stay around for years and decades.\\r\\n\\r\\nAn additional market failure illustrated by the Dyn attack is that \\r\\nneither the seller nor the buyer of those devices cares about fixing the \\r\\nvulnerability. The owners of those devices don&#x27;t care. They wanted a \\r\\nwebcam -- or thermostat, or refrigerator -- with nice features at a good \\r\\nprice. Even after they were recruited into this botnet, they still work \\r\\nfine -- you can&#x27;t even tell they were used in the attack. The sellers of \\r\\nthose devices don&#x27;t care: They&#x27;ve already moved on to selling newer and \\r\\nbetter models. There is no market solution because the insecurity \\r\\nprimarily affects other people. It&#x27;s a form of invisible pollution.\\r\\n\\r\\nAnd, like pollution, the only solution is to regulate. The government \\r\\ncould impose minimum security standards on IoT manufacturers, forcing \\r\\nthem to make their devices secure even though their customers don&#x27;t \\r\\ncare. They could impose liabilities on manufacturers, allowing companies \\r\\nlike Dyn to sue them if their devices are used in DDoS attacks. The \\r\\ndetails would need to be carefully scoped, but either of these options \\r\\nwould raise the cost of insecurity and give companies incentives to \\r\\nspend money making their devices secure.\\r\\n\\r\\nIt&#x27;s true that this is a domestic solution to an international problem \\r\\nand that there&#x27;s no U.S. regulation that will affect, say, an Asian-made \\r\\nproduct sold in South America, even though that product could still be \\r\\nused to take down U.S. websites. But the main costs in making software \\r\\ncome from development. If the United States and perhaps a few other \\r\\nmajor markets implement strong Internet-security regulations on IoT \\r\\ndevices, manufacturers will be forced to upgrade their security if they \\r\\nwant to sell to those markets. And any improvements they make in their \\r\\nsoftware will be available in their products wherever they are sold, \\r\\nsimply because it makes no sense to maintain two different versions of \\r\\nthe software. This is truly an area where the actions of a few countries \\r\\ncan drive worldwide change.\\r\\n\\r\\nRegardless of what you think about regulation vs. market solutions, I \\r\\nbelieve there is no choice. Governments will get involved in the IoT, \\r\\nbecause the risks are too great and the stakes are too high. Computers \\r\\nare now able to affect our world in a direct and physical manner.\\r\\n\\r\\nSecurity researchers have demonstrated the ability to remotely take \\r\\ncontrol of Internet-enabled cars. They&#x27;ve demonstrated ransomware \\r\\nagainst home thermostats and exposed vulnerabilities in implanted \\r\\nmedical devices. They&#x27;ve hacked voting machines and power plants. In one \\r\\nrecent paper, researchers showed how a vulnerability in smart lightbulbs \\r\\ncould be used to start a chain reaction, resulting in them *all* being \\r\\ncontrolled by the attackers -- that&#x27;s every one in a city. Security \\r\\nflaws in these things could mean people dying and property being \\r\\ndestroyed.\\r\\n\\r\\nNothing motivates the U.S. government like fear. Remember 2001? A \\r\\nsmall-government Republican president created the Department of Homeland \\r\\nSecurity in the wake of the Sept. 11 terrorist attacks: a rushed and \\r\\nill-thought-out decision that we&#x27;ve been trying to fix for more than a \\r\\ndecade. A fatal IoT disaster will similarly spur our government into \\r\\naction, and it&#x27;s unlikely to be well-considered and thoughtful action. \\r\\nOur choice isn&#x27;t between government involvement and no government \\r\\ninvolvement. Our choice is between smarter government involvement and \\r\\nstupider government involvement. We have to start thinking about this \\r\\nnow. Regulations are necessary, important and complex -- and they&#x27;re \\r\\ncoming. We can&#x27;t afford to ignore these issues until it&#x27;s too late.\\r\\n\\r\\nIn general, the software market demands that products be fast and cheap \\r\\nand that security be a secondary consideration. That was okay when \\r\\nsoftware didn&#x27;t matter -- it was okay that your spreadsheet crashed once \\r\\nin a while. But a software bug that literally crashes your car is \\r\\nanother thing altogether. The security vulnerabilities in the Internet \\r\\nof Things are deep and pervasive, and they won&#x27;t get fixed if the market \\r\\nis left to sort it out for itself. We need to proactively discuss good \\r\\nregulatory solutions; otherwise, a disaster will impose bad ones on us.\\r\\n\\r\\nThis essay previously appeared in the Washington Post.\\r\\nhttps://www.washingtonpost.com/posteverything/wp/2016/11/03/your-wifi-connected-thermostat-can-take-down-the-whole-internet-we-need-new-regulations/\\r\\n\\r\\n\\r\\nDDoS:\\r\\nhttps://www.washingtonpost.com/news/the-switch/wp/2016/10/21/someone-attacked-a-major-part-of-the-internets-infrastructure/\\r\\n\\r\\nIoT and DDoS:\\r\\nhttps://krebsonsecurity.com/2016/10/hacked-cameras-dvrs-powered-todays-massive-internet-outage/\\r\\n\\r\\nThe IoT market failure and regulation:\\r\\nhttps://www.schneier.com/essays/archives/2016/10/we_need_to_save_the_.html\\r\\nhttps://www.wired.com/2014/01/theres-no-good-way-to-patch-the-internet-of-things-and-thats-a-huge-problem/\\r\\nhttp://www.computerworld.com/article/3136650/security/after-ddos-attack-senator-seeks-industry-led-security-standards-for-iot-devices.html\\r\\n\\r\\nIoT ransomware:\\r\\nhttps://motherboard.vice.com/read/internet-of-things-ransomware-smart-thermostat\\r\\nmedical:\\r\\n\\r\\nHacking medical devices:\\r\\nhttp://motherboard.vice.com/read/hackers-killed-a-simulated-human-by-turning-off-its-pacemaker\\r\\nhttp://abcnews.go.com/US/vice-president-dick-cheney-feared-pacemaker-hacking/story?id=20621434\\r\\n\\r\\nHacking voting machines:\\r\\nhttp://www.politico.com/magazine/story/2016/08/2016-elections-russia-hack-how-to-hack-an-election-in-seven-minutes-214144\\r\\n\\r\\nHacking power plants:\\r\\nhttps://www.wired.com/2016/01/everything-we-know-about-ukraines-power-plant-hack/\\r\\n\\r\\nHacking light bulbs:\\r\\nhttp://iotworm.eyalro.net\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Schneier News\\r\\n\\r\\n\\r\\nI am speaking in Cambridge, MA on November 15 at the Harvard Big-Data \\r\\nClub.\\r\\nhttp://harvardbigdata.com/event/keynote-lecture-bruce-schneier\\r\\n\\r\\nI am speaking in Palm Springs, CA on November 30 at the TEDMED \\r\\nConference.\\r\\nhttp://www.tedmed.com/speakers/show?id=627300\\r\\n\\r\\nI am participating in the Resilient end-of-year webinar on December 8.\\r\\nhttp://info.resilientsystems.com/webinar-eoy-cybersecurity-2016-review-2017-predictions\\r\\n\\r\\nI am speaking on December 14 in Accra at the University of Ghana.\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Virtual Kidnapping\\r\\n\\r\\n\\r\\n\\r\\nThis is a harrowing story of a scam artist that convinced a mother that \\r\\nher daughter had been kidnapped. It&#x27;s unclear if these virtual \\r\\nkidnappers use data about their victims, or just call people at random \\r\\nand hope to get lucky. Still, it&#x27;s a new criminal use of smartphones and \\r\\nubiquitous information. Reminds me of the scammers who call low-wage \\r\\nworkers at retail establishments late at night and convince them to do \\r\\noutlandish and occasionally dangerous things.\\r\\nhttps://www.washingtonpost.com/local/we-have-your-daughter-a-virtual-kidnapping-and-a-mothers-five-hours-of-hell/2016/10/03/8f082690-8963-11e6-875e-2c1bfe943b66_story.html\\r\\nMore stories are here.\\r\\nhttp://www.nbcwashington.com/investigations/Several-Virtual-Kidnapping-Attempts-in-Maryland-Recently-375792991.html\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Intelligence Oversight and How It Can Fail\\r\\n\\r\\n\\r\\n\\r\\nFormer NSA attorneys John DeLong and Susan Hennessay have written a \\r\\nfascinating article describing a particular incident of oversight \\r\\nfailure inside the NSA. Technically, the story hinges on a definitional \\r\\ndifference between the NSA and the FISA court meaning of the word \\r\\n&quot;archived.&quot; (For the record, I would have defaulted to the NSA&#x27;s \\r\\ninterpretation, which feels more accurate technically.) But while the \\r\\nstory is worth reading, what&#x27;s especially interesting are the broader \\r\\nissues about how a nontechnical judiciary can provide oversight over a \\r\\nvery technical data collection-and-analysis organization -- especially \\r\\nif the oversight must largely be conducted in secret.\\r\\n\\r\\nIn many places I have separated different kinds of oversight: are we \\r\\ndoing things right versus are we doing the right things? This is very \\r\\nmuch about the first: is the NSA complying with the rules the courts \\r\\nimpose on them? I believe that the NSA tries very hard to follow the \\r\\nrules it&#x27;s given, while at the same time being very aggressive about how \\r\\nit interprets any kind of ambiguities and using its nonadversarial \\r\\nrelationship with its overseers to its advantage.\\r\\n\\r\\nThe only possible solution I can see to all of this is more public \\r\\nscrutiny. Secrecy is toxic here.\\r\\n\\r\\nhttps://www.lawfareblog.com/understanding-footnote-14-nsa-lawyering-oversight-and-compliance\\r\\n\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n      Whistleblower Investigative Report on NSA Suite B Cryptography\\r\\n\\r\\n\\r\\n\\r\\nThe NSA has been abandoning secret and proprietary cryptographic \\r\\nalgorithms in favor of commercial public algorithms, generally known as \\r\\n&quot;Suite B.&quot; In 2010, an NSA employee filed some sort of whistleblower \\r\\ncomplaint, alleging that this move is both insecure and wasteful.  The \\r\\nUS DoD Inspector General investigated and wrote a report in 2011.\\r\\n\\r\\nThe report -- slightly redacted and declassified -- found that there was \\r\\nno wrongdoing. But the report is an interesting window into the NSA&#x27;s \\r\\nsystem of algorithm selection and testing (pages 5 and 6), as well as \\r\\nhow they investigate whistleblower complaints.\\r\\n\\r\\nhttp://www.dodig.mil/FOIA/err/11-INTEL-06%20(Redacted).pdf\\r\\n\\r\\nSuite B Cryptography:\\r\\nhttp://csrc.nist.gov/groups/SMA/ispab/documents/minutes/2006-03/E_Barker-March2006-ISPAB.pdf\\r\\n\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\nSince 1998, CRYPTO-GRAM has been a free monthly newsletter providing \\r\\nsummaries, analyses, insights, and commentaries on security: computer \\r\\nand otherwise. You can subscribe, unsubscribe, or change your address on \\r\\nthe Web at &lt;https://www.schneier.com/crypto-gram.html&gt;. Back issues are \\r\\nalso available at that URL.\\r\\n\\r\\nPlease feel free to forward CRYPTO-GRAM, in whole or in part, to \\r\\ncolleagues and friends who will find it valuable. Permission is also \\r\\ngranted to reprint CRYPTO-GRAM, as long as it is reprinted in its \\r\\nentirety.\\r\\n\\r\\nCRYPTO-GRAM is written by Bruce Schneier. Bruce Schneier is an \\r\\ninternationally renowned security technologist, called a &quot;security guru&quot; \\r\\nby The Economist. He is the author of 13 books -- including his latest, \\r\\n&quot;Data and Goliath&quot; -- as well as hundreds of articles, essays, and \\r\\nacademic papers. His influential newsletter &quot;Crypto-Gram&quot; and his blog \\r\\n&quot;Schneier on Security&quot; are read by over 250,000 people. He has testified \\r\\nbefore Congress, is a frequent guest on television and radio, has served \\r\\non several government committees, and is regularly quoted in the press. \\r\\nSchneier is a fellow at the Berkman Center for Internet and Society at \\r\\nHarvard Law School, a program fellow at the New America Foundation&#x27;s \\r\\nOpen Technology Institute, a board member of the Electronic Frontier \\r\\nFoundation, an Advisory Board Member of the Electronic Privacy \\r\\nInformation Center, and the Chief Technology Officer at Resilient, an \\r\\nIBM Company.  See &lt;https://www.schneier.com&gt;.\\r\\n\\r\\nCrypto-Gram is a personal newsletter. Opinions expressed are not \\r\\nnecessarily those of Resilient, an IBM Company.\\r\\n\\r\\nCopyright (c) 2016 by Bruce Schneier.\\r\\n\\r\\n** *** ***** ******* *********** *************\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\nTo unsubscribe from Crypto-Gram, click this link:\\r\\n\\r\\nhttps://lists.schneier.com/cgi-bin/mailman/options/crypto-gram/christine%40spang.cc?login-unsub=Unsubscribe\\r\\n\\r\\nYou will be e-mailed a confirmation message.  Follow the instructions in that message to confirm your removal from the list.\\r\\n</pre>\",\"snippet\":\"CRYPTO-GRAM November 15, 2016 by Bruce Schneier CTO, Resilient, an IBM Company schneier@schneier.com\",\"unread\":false,\"starred\":false,\"date\":\"Tue, 15 Nov 2016 01:27:10 -0600\",\"folderImapUID\":345982,\"folderId\":\"test-folder-id\",\"folder\":{\"id\":\"test-folder-id\",\"account_id\":\"test-account-id\",\"object\":\"folder\",\"name\":null,\"display_name\":\"Test Folder\",\"sync_state\":{}},\"labels\":[],\"headers\":{\"delivered-to\":[\"christine@spang.cc\",\"crypto-gram@lists.schneier.com\"],\"received\":[\"by 10.31.185.141 with SMTP id j135csp15122vkf; Mon, 14 Nov 2016 23:50:26 -0800 (PST)\",\"from schneier.modwest.com (schneier.modwest.com. [204.11.247.92]) by mx.google.com with ESMTPS id i126si6507480ybb.7.2016.11.14.23.50.26 for <christine@spang.cc> (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Mon, 14 Nov 2016 23:50:26 -0800 (PST)\",\"from schneier.modwest.com (localhost [127.0.0.1]) by schneier.modwest.com (Postfix) with ESMTP id A57D33A66E for <christine@spang.cc>; Tue, 15 Nov 2016 00:48:53 -0700 (MST)\",\"from webmail.schneier.com (localhost [127.0.0.1]) by schneier.modwest.com (Postfix) with ESMTPA id 735B038F18; Tue, 15 Nov 2016 00:27:10 -0700 (MST)\"],\"x-received\":[\"by 10.37.220.66 with SMTP id y63mr6697075ybe.190.1479196226438; Mon, 14 Nov 2016 23:50:26 -0800 (PST)\"],\"return-path\":[\"<crypto-gram-bounces@lists.schneier.com>\"],\"received-spf\":[\"pass (google.com: domain of crypto-gram-bounces@lists.schneier.com designates 204.11.247.92 as permitted sender) client-ip=204.11.247.92;\"],\"authentication-results\":[\"mx.google.com; spf=pass (google.com: domain of crypto-gram-bounces@lists.schneier.com designates 204.11.247.92 as permitted sender) smtp.mailfrom=crypto-gram-bounces@lists.schneier.com\"],\"x-original-to\":[\"crypto-gram@lists.schneier.com\"],\"mime-version\":[\"1.0\"],\"date\":[\"Tue, 15 Nov 2016 01:27:10 -0600\"],\"from\":[\"Bruce Schneier <schneier@schneier.com>\"],\"subject\":[\"CRYPTO-GRAM, November 15, 2016\"],\"message-id\":[\"<76bcad7045e1f498eb00e27fc969ee53@schneier.com>\"],\"x-sender\":[\"schneier@schneier.com\"],\"user-agent\":[\"Roundcube Webmail/0.9.5\"],\"x-mailman-approved-at\":[\"Tue, 15 Nov 2016 00:45:13 -0700\"],\"x-beenthere\":[\"crypto-gram@lists.schneier.com\"],\"x-mailman-version\":[\"2.1.15\"],\"precedence\":[\"list\"],\"cc\":[\"Crypto-Gram Mailing List <crypto-gram@lists.schneier.com>\"],\"list-id\":[\"Crypto-Gram Mailing List <crypto-gram.lists.schneier.com>\"],\"list-unsubscribe\":[\"<https://lists.schneier.com/cgi-bin/mailman/options/crypto-gram>, <mailto:crypto-gram-request@lists.schneier.com?subject=unsubscribe>\"],\"list-post\":[\"<mailto:crypto-gram@lists.schneier.com>\"],\"list-help\":[\"<mailto:crypto-gram-request@lists.schneier.com?subject=help>\"],\"list-subscribe\":[\"<https://lists.schneier.com/cgi-bin/mailman/listinfo/crypto-gram>, <mailto:crypto-gram-request@lists.schneier.com?subject=subscribe>\"],\"content-transfer-encoding\":[\"7bit\"],\"content-type\":[\"text/plain; charset=\\\"us-ascii\\\"; Format=\\\"flowed\\\"\"],\"to\":[\"christine@spang.cc\"],\"errors-to\":[\"crypto-gram-bounces@lists.schneier.com\"],\"sender\":[\"\\\"Crypto-Gram\\\" <crypto-gram-bounces@lists.schneier.com>\"],\"x-gm-thrid\":\"1551049662245032910\",\"x-gm-msgid\":\"1551049662245032910\",\"x-gm-labels\":[\"\\\\Inbox\"]},\"headerMessageId\":\"<76bcad7045e1f498eb00e27fc969ee53@schneier.com>\",\"gMsgId\":\"1551049662245032910\",\"subject\":\"CRYPTO-GRAM, November 15, 2016\",\"id\":\"0b0d7b384a7be99a0bb0d892694b53e9df0117185e731398328ee0bc2823e245\",\"folderImapXGMLabels\":\"[\\\"\\\\\\\\Inbox\\\"]\"}}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseFromImap/eff-plaintext-no-mime.json",
    "content": "{\"imapMessage\":{\"attributes\":{\"struct\":[{\"partID\":\"1\",\"type\":\"text\",\"subtype\":\"plain\",\"params\":null,\"id\":null,\"description\":null,\"encoding\":\"7BIT\",\"size\":3050,\"lines\":67,\"md5\":null,\"disposition\":null,\"language\":null}],\"date\":\"2016-12-01T01:34:44.000Z\",\"flags\":[\"\\\\Seen\"],\"uid\":348040,\"modseq\":\"8228548\",\"x-gm-labels\":[\"\\\\Inbox\"],\"x-gm-msgid\":\"1552475576878158784\",\"x-gm-thrid\":\"1552475576878158784\"},\"headers\":\"Delivered-To: christine@spang.cc\\r\\nReceived: by 10.140.100.181 with SMTP id s50csp492441qge; Wed, 30 Nov 2016\\r\\n 17:34:44 -0800 (PST)\\r\\nX-Received: by 10.129.120.215 with SMTP id t206mr41825274ywc.39.1480556084610;\\r\\n Wed, 30 Nov 2016 17:34:44 -0800 (PST)\\r\\nReturn-Path: <www-data@web5.eff.org>\\r\\nReceived: from mail2.eff.org (mail2.eff.org. [173.239.79.204]) by\\r\\n mx.google.com with ESMTPS id c198si18454640ywb.136.2016.11.30.17.34.44 for\\r\\n <christine@spang.cc> (version=TLS1_2 cipher=AES128-SHA bits=128/128); Wed, 30\\r\\n Nov 2016 17:34:44 -0800 (PST)\\r\\nReceived-SPF: pass (google.com: best guess record for domain of\\r\\n www-data@web5.eff.org designates 173.239.79.204 as permitted sender)\\r\\n client-ip=173.239.79.204;\\r\\nAuthentication-Results: mx.google.com; dkim=pass header.i=@eff.org; spf=pass\\r\\n (google.com: best guess record for domain of www-data@web5.eff.org designates\\r\\n 173.239.79.204 as permitted sender) smtp.mailfrom=www-data@web5.eff.org;\\r\\n dmarc=pass (p=NONE dis=NONE) header.from=eff.org\\r\\nDKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=eff.org;\\r\\n s=mail2; h=Date:Message-Id:Sender:Reply-To:From:Subject:To;\\r\\n bh=85sXKrmP+EL3X9i986SKLxXpb0v60xG0c09b/uRaP10=;\\r\\n b=IByfHFGQGSbxCZfsWU5gd3ek92bd4yhEReZ8qDGPo/CWDCeUO3QnB6yY3aMpuJdD9TUUUKM6rcmfpz4zTmCtwkakMW/uIay2CBXUWuAsowRwUtofpIDmn4aDOhkMUHvyMZe9cZhpgWr7EC1JqEI+3J/kvhR/HTgi7r0dVnz7FBk=;\\r\\nReceived: from static-69.50.232.52.nephosdns.com ([69.50.232.52]:57896\\r\\n helo=web5.eff.org) by mail2.eff.org with esmtp (Exim 4.80) (envelope-from\\r\\n <www-data@web5.eff.org>) id 1cCGGo-000366-QC for christine@spang.cc; Wed, 30\\r\\n Nov 2016 17:34:42 -0800\\r\\nReceived: by web5.eff.org (Postfix, from userid 33) id BFBE132AD8C; Wed, 30\\r\\n Nov 2016 17:34:42 -0800 (PST)\\r\\nTo: christine@spang.cc\\r\\nSubject: EFF Membership Benefits\\r\\nFrom: Member Services <membership@eff.org>\\r\\nReply-To: membership@eff.org\\r\\nSender: membership@eff.org\\r\\nMessage-Id: <20161201013442.BFBE132AD8C@web5.eff.org>\\r\\nDate: Wed, 30 Nov 2016 17:34:42 -0800 (PST)\\r\\nReceived-SPF: skipped for local relay\\r\\n\\r\\n\",\"parts\":{\"1\":\"Thank you for being a member of the Electronic Frontier Foundation! Your\\r\\nEFF membership lasts for 12 months and you have the opportunity to\\r\\nselect a free gift every time you renew! If you are a monthly donor,\\r\\nyour membership lasts as long as you like, and you are eligible for a\\r\\nmember gift every 12 months. Contributions are tax deductible as allowed\\r\\nby law. EFF is a U.S. 501(c)(3) organization, and our federal tax ID\\r\\nnumber is 04-3091431.\\r\\n\\r\\nYour contribution makes a significant difference in our ability to\\r\\ndefend your rights in the digital world whether that means standing up\\r\\nfor users in the courts, educating the public about dangerous laws, or\\r\\ncreating privacy-enhancing technologies. We are pleased to offer you a\\r\\nfew modest perks as a token of our thanks. This is a one-time email\\r\\nnotice and EFF will never share your information with partnering or\\r\\nsupporting organizations. Contact us at membership@eff.org if you have\\r\\nany questions!\\r\\n\\r\\nEFF MEMBER BENEFITS:\\r\\n\\r\\n-EFF Online Rights bumper sticker (via print mail).\\r\\n\\r\\n-EFF Member Card (via print mail).\\r\\n\\r\\n-Digital Badges: Show your support on your blog, website, or in an HTML\\r\\nemail signature.\\r\\nhttps://www.eff.org/files/2016/11/01/2017mb.png\\r\\n\\r\\n-Discounts on Events: Receive discounts on General Admission to EFF\\r\\nevents! Choose the member price when purchasing tickets or show your EFF\\r\\nMember card at the door. Watch for specific details in event invitations.\\r\\n\\r\\n-Speakeasy Invitations: Get notified about informal members-only meetups\\r\\nand drink ups with EFF lawyers, activists, and technologists in your area!\\r\\n\\r\\n-EFF Online Shop Discount: Get 10% off purchases (Gift Memberships\\r\\nexcluded) at EFF's online store by using the discount code ALIQUID16 at\\r\\nhttps://www.eff.org/shop.\\r\\n\\r\\nPARTNER DISCOUNTS FOR EFF MEMBERS:\\r\\n\\r\\n-LeanPub: Get a copy of Uncensored, an ever expanding collection of\\r\\nessays on Internet freedom from many of its original innovators. Follow\\r\\nthis promo url: http://leanpub.com/uncensored/c/effdonor.\\r\\n\\r\\n-Borderlands Books and Cafe: Borderlands is San Francisco's home for\\r\\nscience fiction, fantasy, and horror books. Present your EFF Member Card\\r\\nand get a 10% discount on in-store purchases. Borderlands Cafe also\\r\\noffers occasional EFF Member specials so be sure to stop by for some\\r\\nreading time and a treat! http://www.borderlands-books.com\\r\\n\\r\\n-Magnatune: Get an $8 gift card from Magnatune, the music download\\r\\nservice that isn't evil. All the music is DRM free and licensed under\\r\\nCreative Commons. Musicians get 50%, you get to choose the price you\\r\\npay, and you're encouraged to share your purchased music with 3 friends.\\r\\nGo to http://magnatune.com and use the gift code: 111801248707\\r\\n\\r\\n-No Starch Press: Get a 30% discount on geektastic books that entertain\\r\\nand/or instruct by using the code EFFMEMBER at http://www.nostarch.com.\\r\\n\\r\\n-Take Control Ebooks: Get a 30% off by following this EFF member portal.\\r\\nhttp://www.takecontrolbooks.com/?pt=EFF&cp=CPN80131EFF\\r\\n\\r\\n-- \\r\\nAaron Jue\\r\\nEFF Senior Membership Advocate\\r\\nmembership@eff.org\\r\\n\\r\\n\"}},\"desiredParts\":[{\"id\":\"1\",\"encoding\":\"7BIT\",\"mimetype\":\"text/plain\"}],\"result\":{\"to\":[{\"name\":\"\",\"email\":\"christine@spang.cc\"}],\"cc\":[],\"bcc\":[],\"from\":[{\"name\":\"Member Services\",\"email\":\"membership@eff.org\"}],\"replyTo\":[{\"name\":\"\",\"email\":\"membership@eff.org\"}],\"accountId\":\"test-account-id\",\"body\":\"<pre class=\\\"nylas-plaintext\\\">Thank you for being a member of the Electronic Frontier Foundation! Your\\r\\nEFF membership lasts for 12 months and you have the opportunity to\\r\\nselect a free gift every time you renew! If you are a monthly donor,\\r\\nyour membership lasts as long as you like, and you are eligible for a\\r\\nmember gift every 12 months. Contributions are tax deductible as allowed\\r\\nby law. EFF is a U.S. 501(c)(3) organization, and our federal tax ID\\r\\nnumber is 04-3091431.\\r\\n\\r\\nYour contribution makes a significant difference in our ability to\\r\\ndefend your rights in the digital world whether that means standing up\\r\\nfor users in the courts, educating the public about dangerous laws, or\\r\\ncreating privacy-enhancing technologies. We are pleased to offer you a\\r\\nfew modest perks as a token of our thanks. This is a one-time email\\r\\nnotice and EFF will never share your information with partnering or\\r\\nsupporting organizations. Contact us at membership@eff.org if you have\\r\\nany questions!\\r\\n\\r\\nEFF MEMBER BENEFITS:\\r\\n\\r\\n-EFF Online Rights bumper sticker (via print mail).\\r\\n\\r\\n-EFF Member Card (via print mail).\\r\\n\\r\\n-Digital Badges: Show your support on your blog, website, or in an HTML\\r\\nemail signature.\\r\\nhttps://www.eff.org/files/2016/11/01/2017mb.png\\r\\n\\r\\n-Discounts on Events: Receive discounts on General Admission to EFF\\r\\nevents! Choose the member price when purchasing tickets or show your EFF\\r\\nMember card at the door. Watch for specific details in event invitations.\\r\\n\\r\\n-Speakeasy Invitations: Get notified about informal members-only meetups\\r\\nand drink ups with EFF lawyers, activists, and technologists in your area!\\r\\n\\r\\n-EFF Online Shop Discount: Get 10% off purchases (Gift Memberships\\r\\nexcluded) at EFF&#x27;s online store by using the discount code ALIQUID16 at\\r\\nhttps://www.eff.org/shop.\\r\\n\\r\\nPARTNER DISCOUNTS FOR EFF MEMBERS:\\r\\n\\r\\n-LeanPub: Get a copy of Uncensored, an ever expanding collection of\\r\\nessays on Internet freedom from many of its original innovators. Follow\\r\\nthis promo url: http://leanpub.com/uncensored/c/effdonor.\\r\\n\\r\\n-Borderlands Books and Cafe: Borderlands is San Francisco&#x27;s home for\\r\\nscience fiction, fantasy, and horror books. Present your EFF Member Card\\r\\nand get a 10% discount on in-store purchases. Borderlands Cafe also\\r\\noffers occasional EFF Member specials so be sure to stop by for some\\r\\nreading time and a treat! http://www.borderlands-books.com\\r\\n\\r\\n-Magnatune: Get an $8 gift card from Magnatune, the music download\\r\\nservice that isn&#x27;t evil. All the music is DRM free and licensed under\\r\\nCreative Commons. Musicians get 50%, you get to choose the price you\\r\\npay, and you&#x27;re encouraged to share your purchased music with 3 friends.\\r\\nGo to http://magnatune.com and use the gift code: 111801248707\\r\\n\\r\\n-No Starch Press: Get a 30% discount on geektastic books that entertain\\r\\nand/or instruct by using the code EFFMEMBER at http://www.nostarch.com.\\r\\n\\r\\n-Take Control Ebooks: Get a 30% off by following this EFF member portal.\\r\\nhttp://www.takecontrolbooks.com/?pt=EFF&amp;cp=CPN80131EFF\\r\\n\\r\\n-- \\r\\nAaron Jue\\r\\nEFF Senior Membership Advocate\\r\\nmembership@eff.org\\r\\n\\r\\n</pre>\",\"snippet\":\"Thank you for being a member of the Electronic Frontier Foundation! Your EFF membership lasts for 12\",\"unread\":false,\"starred\":false,\"date\":\"Wed, 30 Nov 2016 17:34:42 -0800 (PST)\",\"folderImapUID\":348040,\"folderId\":\"test-folder-id\",\"folder\":{\"id\":\"test-folder-id\",\"account_id\":\"test-account-id\",\"object\":\"folder\",\"name\":null,\"display_name\":\"Test Folder\",\"sync_state\":{}},\"labels\":[],\"headers\":{\"delivered-to\":[\"christine@spang.cc\"],\"received\":[\"by 10.140.100.181 with SMTP id s50csp492441qge; Wed, 30 Nov 2016 17:34:44 -0800 (PST)\",\"from mail2.eff.org (mail2.eff.org. [173.239.79.204]) by mx.google.com with ESMTPS id c198si18454640ywb.136.2016.11.30.17.34.44 for <christine@spang.cc> (version=TLS1_2 cipher=AES128-SHA bits=128/128); Wed, 30 Nov 2016 17:34:44 -0800 (PST)\",\"from static-69.50.232.52.nephosdns.com ([69.50.232.52]:57896 helo=web5.eff.org) by mail2.eff.org with esmtp (Exim 4.80) (envelope-from <www-data@web5.eff.org>) id 1cCGGo-000366-QC for christine@spang.cc; Wed, 30 Nov 2016 17:34:42 -0800\",\"by web5.eff.org (Postfix, from userid 33) id BFBE132AD8C; Wed, 30 Nov 2016 17:34:42 -0800 (PST)\"],\"x-received\":[\"by 10.129.120.215 with SMTP id t206mr41825274ywc.39.1480556084610; Wed, 30 Nov 2016 17:34:44 -0800 (PST)\"],\"return-path\":[\"<www-data@web5.eff.org>\"],\"received-spf\":[\"pass (google.com: best guess record for domain of www-data@web5.eff.org designates 173.239.79.204 as permitted sender) client-ip=173.239.79.204;\",\"skipped for local relay\"],\"authentication-results\":[\"mx.google.com; dkim=pass header.i=@eff.org; spf=pass (google.com: best guess record for domain of www-data@web5.eff.org designates 173.239.79.204 as permitted sender) smtp.mailfrom=www-data@web5.eff.org; dmarc=pass (p=NONE dis=NONE) header.from=eff.org\"],\"dkim-signature\":[\"v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=eff.org; s=mail2; h=Date:Message-Id:Sender:Reply-To:From:Subject:To; bh=85sXKrmP+EL3X9i986SKLxXpb0v60xG0c09b/uRaP10=; b=IByfHFGQGSbxCZfsWU5gd3ek92bd4yhEReZ8qDGPo/CWDCeUO3QnB6yY3aMpuJdD9TUUUKM6rcmfpz4zTmCtwkakMW/uIay2CBXUWuAsowRwUtofpIDmn4aDOhkMUHvyMZe9cZhpgWr7EC1JqEI+3J/kvhR/HTgi7r0dVnz7FBk=;\"],\"to\":[\"christine@spang.cc\"],\"subject\":[\"EFF Membership Benefits\"],\"from\":[\"Member Services <membership@eff.org>\"],\"reply-to\":[\"membership@eff.org\"],\"sender\":[\"membership@eff.org\"],\"message-id\":[\"<20161201013442.BFBE132AD8C@web5.eff.org>\"],\"date\":[\"Wed, 30 Nov 2016 17:34:42 -0800 (PST)\"],\"x-gm-thrid\":\"1552475576878158784\",\"x-gm-msgid\":\"1552475576878158784\",\"x-gm-labels\":[\"\\\\Inbox\"]},\"headerMessageId\":\"<20161201013442.BFBE132AD8C@web5.eff.org>\",\"gMsgId\":\"1552475576878158784\",\"subject\":\"EFF Membership Benefits\",\"id\":\"c5b2ade0b3fe9cff0acdca503aed76c27e211cf255c97e5c513436b47edd8d24\",\"folderImapXGMLabels\":\"[\\\"\\\\\\\\Inbox\\\"]\"}}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseFromImap/hacker-newsletter-multipart-alternative.json",
    "content": "{\"imapMessage\":{\"attributes\":{\"struct\":[{\"type\":\"alternative\",\"params\":{\"boundary\":\"_----------=_MCPart_776050397\"},\"disposition\":null,\"language\":null},[{\"partID\":\"1\",\"type\":\"text\",\"subtype\":\"plain\",\"params\":{\"charset\":\"utf-8\",\"format\":\"fixed\"},\"id\":null,\"description\":null,\"encoding\":\"QUOTED-PRINTABLE\",\"size\":17854,\"lines\":362,\"md5\":null,\"disposition\":null,\"language\":null}],[{\"partID\":\"2\",\"type\":\"text\",\"subtype\":\"html\",\"params\":{\"charset\":\"utf-8\"},\"id\":null,\"description\":null,\"encoding\":\"QUOTED-PRINTABLE\",\"size\":66229,\"lines\":1172,\"md5\":null,\"disposition\":null,\"language\":null}]],\"date\":\"2016-11-04T13:33:12.000Z\",\"flags\":[],\"uid\":344200,\"modseq\":\"8118301\",\"x-gm-labels\":[\"\\\\Important\"],\"x-gm-msgid\":\"1550074660376052354\",\"x-gm-thrid\":\"1550074660376052354\"},\"headers\":\"Delivered-To: christine@spang.cc\\r\\nReceived: by 10.31.236.3 with SMTP id k3csp257576vkh; Fri, 4 Nov 2016 06:33:12\\r\\n -0700 (PDT)\\r\\nX-Received: by 10.55.207.210 with SMTP id v79mr12608992qkl.199.1478266392152;\\r\\n Fri, 04 Nov 2016 06:33:12 -0700 (PDT)\\r\\nReturn-Path: <bounce-mc.us1_1832689.2559073-Christine=spang.cc@mail250.atl61.mcsv.net>\\r\\nReceived: from mail250.atl61.mcsv.net (mail250.atl61.mcsv.net.\\r\\n [205.201.135.250]) by mx.google.com with ESMTP id\\r\\n f11si7594937qte.84.2016.11.04.06.33.10 for <Christine@spang.cc>; Fri, 04 Nov\\r\\n 2016 06:33:12 -0700 (PDT)\\r\\nReceived-SPF: pass (google.com: domain of\\r\\n bounce-mc.us1_1832689.2559073-christine=spang.cc@mail250.atl61.mcsv.net\\r\\n designates 205.201.135.250 as permitted sender) client-ip=205.201.135.250;\\r\\nAuthentication-Results: mx.google.com; dkim=pass\\r\\n header.i=@hackernewsletter.com; spf=pass (google.com: domain of\\r\\n bounce-mc.us1_1832689.2559073-christine=spang.cc@mail250.atl61.mcsv.net\\r\\n designates 205.201.135.250 as permitted sender)\\r\\n smtp.mailfrom=bounce-mc.us1_1832689.2559073-Christine=spang.cc@mail250.atl61.mcsv.net\\r\\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=k1;\\r\\n d=hackernewsletter.com;\\r\\n h=Subject:From:Reply-To:To:Date:Message-ID:List-ID:List-Unsubscribe:Content-Type:MIME-Version;\\r\\n i=kale@hackernewsletter.com; bh=vZB3PnpQdFA+LVdXRQIdK2lMkvI=;\\r\\n b=RuL/qDIhe22oioO4rRO81v3j4nGp4VXHbJILx7VRSkXQ6yFLcyEPISC1+l3WaAEyBeyBU5lTNvvr\\r\\n 6KdJ0UzAYt854dXg0fefITQUDJmKEZS+wGWsJtvSK380WU3M5V6rQXWodFYBmvZmJXJH0oqh5TMo\\r\\n Iwel9pTPpzKCVGtG3L4=\\r\\nReceived: from (127.0.0.1) by mail250.atl61.mcsv.net id h3i71e174acc for\\r\\n <Christine@spang.cc>; Fri, 4 Nov 2016 13:33:01 +0000 (envelope-from\\r\\n <bounce-mc.us1_1832689.2559073-Christine=spang.cc@mail250.atl61.mcsv.net>)\\r\\nSubject: =?utf-8?Q?Hacker=20Newsletter=20#325?=\\r\\nFrom: =?utf-8?Q?Hacker=20Newsletter?= <kale@hackernewsletter.com>\\r\\nReply-To: =?utf-8?Q?Hacker=20Newsletter?= <kale@hackernewsletter.com>\\r\\nTo: <Christine@spang.cc>\\r\\nDate: Fri, 4 Nov 2016 13:33:01 +0000\\r\\nMessage-ID: <faa8eb4ef3a111cef92c4f3d4765272fcdd.20161104133210@mail250.atl61.mcsv.net>\\r\\nX-Mailer: MailChimp Mailer - **CIDa018682c80765272fcdd**\\r\\nX-Campaign: mailchimpfaa8eb4ef3a111cef92c4f3d4.a018682c80\\r\\nX-campaignid: mailchimpfaa8eb4ef3a111cef92c4f3d4.a018682c80\\r\\nX-Report-Abuse: Please report abuse for this campaign here:\\r\\n http://www.mailchimp.com/abuse/abuse.phtml?u=faa8eb4ef3a111cef92c4f3d4&id=a018682c80&e=765272fcdd\\r\\nX-MC-User: faa8eb4ef3a111cef92c4f3d4\\r\\nX-Feedback-ID: 1832689:1832689.2559073:us1:mc\\r\\nList-ID: faa8eb4ef3a111cef92c4f3d4mc list\\r\\n <faa8eb4ef3a111cef92c4f3d4.583821.list-id.mcsv.net>\\r\\nX-Accounttype: pr\\r\\nList-Unsubscribe: <mailto:unsubscribe-mc.us1_faa8eb4ef3a111cef92c4f3d4.a018682c80-765272fcdd@mailin1.us2.mcsv.net?subject=unsubscribe>,\\r\\n <http://hackernewsletter.us1.list-manage.com/unsubscribe?u=faa8eb4ef3a111cef92c4f3d4&id=e505c88a2e&e=765272fcdd&c=a018682c80>\\r\\nx-mcda: FALSE\\r\\nContent-Type: multipart/alternative; boundary=\\\"_----------=_MCPart_776050397\\\"\\r\\nMIME-Version: 1.0\\r\\n\\r\\n\",\"parts\":{\"1\":\"The only thing more expensive than writing software is writing bad softwar=\\r\\ne. //Alan Cooper\\r\\n\\r\\n\\r\\n** hackernewsletter (http://hackernewsletter.us1.list-manage2.com/track/cl=\\r\\nick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Db9331dfc08&e=3D765272fcdd)\\r\\n------------------------------------------------------------\\r\\nIssue #325 // November 04=2C 2016 // View in your browser (http://us1.camp=\\r\\naign-archive2.com/?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da018682c80&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\n\\r\\n** #Sponsor\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nhttp://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a11=\\r\\n1cef92c4f3d4&id=3D49346392ee&e=3D765272fcdd Hired - The End of Job H=\\r\\nunting As You Know It (http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dc2bf580f8d&e=3D765272fcdd)\\r\\nHired brings job offers to you=2C so you can cut to the chase and focus on=\\r\\n finding the right fit=2C (not applying). Get multiple job offers and upfr=\\r\\nont compensation information=2C with just one application. Get Hired (http=\\r\\n://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef9=\\r\\n2c4f3d4&id=3Dec31429e96&e=3D765272fcdd)\\r\\n\\r\\n\\r\\n** #Favorites\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nEve: Programming designed for humans (http://hackernewsletter.us1.list-man=\\r\\nage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De08dd4413c&e=3D=\\r\\n765272fcdd)\\r\\n//witheve comments=E2=86=92 (http://hackernewsletter.us1.list-manage.com/t=\\r\\nrack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd9b62e9185&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nTotal Nightmare: USB-C and Thunderbolt 3 (http://hackernewsletter.us1.list=\\r\\n-manage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D0e51dee03d&e=\\r\\n=3D765272fcdd)\\r\\n//fosketts comments=E2=86=92 (http://hackernewsletter.us1.list-manage.com/=\\r\\ntrack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D64456f4c47&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nSigns that a startup is focused on stuff that doesn=E2=80=99t matter (http=\\r\\n://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef9=\\r\\n2c4f3d4&id=3Daae701c750&e=3D765272fcdd)\\r\\n//groovehq comments=E2=86=92 (http://hackernewsletter.us1.list-manage.com/=\\r\\ntrack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D98188f8267&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nNew MacBook Pro Is Not a Laptop for Developers Anymore (http://hackernewsl=\\r\\netter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9=\\r\\n79a461b49&e=3D765272fcdd)\\r\\n//blog comments=E2=86=92 (http://hackernewsletter.us1.list-manage2.com/tra=\\r\\nck/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd0e2535fa5&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nWeb fonts=2C boy=2C I don't know (http://hackernewsletter.us1.list-manage.=\\r\\ncom/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D4c37a6c08d&e=3D=\\r\\n765272fcdd)\\r\\n//meowni comments=E2=86=92 (http://hackernewsletter.us1.list-manage.com/tr=\\r\\nack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D86f87b9395&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nNo One Saw Tesla=E2=80=99s Solar Roof Coming (http://hackernewsletter.us1.=\\r\\nlist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd506272321=\\r\\n&e=3D765272fcdd)\\r\\n//bloomberg comments=E2=86=92 (http://hackernewsletter.us1.list-manage.com=\\r\\n/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D46898a2195&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nWays Data Projects Fail (http://hackernewsletter.us1.list-manage.com/track=\\r\\n/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D155328bf94&e=3D765272fcdd)\\r\\n//martingoodson comments=E2=86=92 (http://hackernewsletter.us1.list-manage=\\r\\n=2Ecom/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dcf6b7316d0&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nA very valuable vulnerability (http://hackernewsletter.us1.list-manage.com=\\r\\n/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D741d7004e5&e=3D=\\r\\n765272fcdd)\\r\\n//daemonology comments=E2=86=92 (http://hackernewsletter.us1.list-manage1.=\\r\\ncom/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D87d1dd6ee7&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nMaking what people want isn=E2=80=99t enough=2C you have to share it (http=\\r\\n://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef=\\r\\n92c4f3d4&id=3D1b614a6e79&e=3D765272fcdd)\\r\\n//oldgeekjobs comments=E2=86=92 (http://hackernewsletter.us1.list-manage.c=\\r\\nom/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D38d624ea5f&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nHow ReadMe Went from SaaS to On-Premises in Less Than One Week (http://hac=\\r\\nkernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d=\\r\\n4&id=3Df4c054b8a4&e=3D765272fcdd)\\r\\n//stackshare comments=E2=86=92 (http://hackernewsletter.us1.list-manage1.c=\\r\\nom/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da04633cd33&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\n\\r\\n** #Ask HN\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nAny other blind devs interested in working on dev tools for the blind? (ht=\\r\\ntp://hackernewsletter.us1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111c=\\r\\nef92c4f3d4&id=3Da4a9a08427&e=3D765272fcdd)\\r\\n\\r\\nHow to make a career working remotely? (http://hackernewsletter.us1.list-m=\\r\\nanage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D517f7a0c8b&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\n\\r\\n** #Show HN\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nMusic for Programming (http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df72d025a71&e=3D765272fcdd) /=\\r\\n/musicforprogramming comments=E2=86=92 (http://hackernewsletter.us1.list-m=\\r\\nanage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D793628fd74&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nPowerwall 2 and Integrated Solar (http://hackernewsletter.us1.list-manage.=\\r\\ncom/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dba1ef6be0e&e=3D=\\r\\n765272fcdd) //tesla comments=E2=86=92 (http://hackernewsletter.us1.list-mana=\\r\\nge1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D242de3029e&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nSonder E-Ink Keyboard (http://hackernewsletter.us1.list-manage2.com/track/=\\r\\nclick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dca8a503ec9&e=3D765272fcdd)=\\r\\n //sonderdesign comments=E2=86=92 (http://hackernewsletter.us1.list-manage=\\r\\n2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D88ec65b546&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nMicrosoft Teams=2C the new chat-based workspace in Office 365 (http://hack=\\r\\nernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4=\\r\\n&id=3D2a1be32eea&e=3D765272fcdd) //office comments=E2=86=92 (http://=\\r\\nhackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4=\\r\\nf3d4&id=3D0414a41b94&e=3D765272fcdd)\\r\\n\\r\\nFlow =E2=80=93 Create automated workflows between your favorite apps (http=\\r\\n://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef9=\\r\\n2c4f3d4&id=3D93b5aafe0d&e=3D765272fcdd) //microsoft comments=E2=86=\\r\\n=92 (http://hackernewsletter.us1.list-manage2.com/track/click?u=3Dfaa8eb4e=\\r\\nf3a111cef92c4f3d4&id=3D9a0e4a6a60&e=3D765272fcdd)\\r\\n\\r\\nPortier =E2=80=93 An email-based=2C passwordless authentication service (h=\\r\\nttp://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111=\\r\\ncef92c4f3d4&id=3D1359a45752&e=3D765272fcdd) //github comments=E2=86=\\r\\n=92 (http://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4e=\\r\\nf3a111cef92c4f3d4&id=3D29564a0bdd&e=3D765272fcdd)\\r\\n\\r\\nNewly Redesigned Boston.gov Just Went Open Source (http://hackernewsletter=\\r\\n=2Eus1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd8f82=\\r\\n851ff&e=3D765272fcdd) //routefifty comments=E2=86=92 (http://hackern=\\r\\newsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&i=\\r\\nd=3D815b94b7ed&e=3D765272fcdd)\\r\\n\\r\\nSodaphonic =E2=80=93 record and edit audio in the browser (http://hackerne=\\r\\nwsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=\\r\\n=3Dbab7541aac&e=3D765272fcdd) //sodaphonic comments=E2=86=92 (http:/=\\r\\n/hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c=\\r\\n4f3d4&id=3Ded91ce0da4&e=3D765272fcdd)\\r\\n\\r\\n\\r\\n** #Featured\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nDear Matt Mullenweg: An Open Letter from Wix.com=E2=80=99s CEO Avishai Abr=\\r\\nahami (http://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb=\\r\\n4ef3a111cef92c4f3d4&id=3D6585f01a53&e=3D765272fcdd) //wix comments=\\r\\n=E2=86=92 (http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa=\\r\\n8eb4ef3a111cef92c4f3d4&id=3D604030a8b7&e=3D765272fcdd)\\r\\n\\r\\nOpen Letter to Tim Cook (http://hackernewsletter.us1.list-manage2.com/trac=\\r\\nk/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D34d547c50d&e=3D765272fcdd=\\r\\n) //petersphilo comments=E2=86=92 (http://hackernewsletter.us1.list-manage=\\r\\n1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D08b7caf574&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nDear Microsoft (http://hackernewsletter.us1.list-manage.com/track/click?u=\\r\\n=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dec227e8c1f&e=3D765272fcdd) //slack=\\r\\nhq comments=E2=86=92 (http://hackernewsletter.us1.list-manage.com/track/cl=\\r\\nick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9b0a2aaca5&e=3D765272fcdd)\\r\\n\\r\\n=E2=9D=97=EF=B8=8FSlack may regret its letter to Microsoft (http://hackern=\\r\\newsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=\\r\\n=3D9ddcd67f11&e=3D765272fcdd) //theverge comments=E2=86=92 (http://h=\\r\\nackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f=\\r\\n3d4&id=3D338bd7f466&e=3D765272fcdd)\\r\\n\\r\\n\\r\\n** #Code\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nDarling =E2=80=93 MacOS translation layer for Linux (http://hackernewslett=\\r\\ner.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D094c=\\r\\n969b13&e=3D765272fcdd) //darlinghq comments=E2=86=92 (http://hackern=\\r\\newsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&i=\\r\\nd=3D56990dcc67&e=3D765272fcdd)\\r\\n\\r\\nI don't understand Python's Asyncio (http://hackernewsletter.us1.list-mana=\\r\\nge.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D4a3a08a849&e=3D=\\r\\n765272fcdd) //pocoo comments=E2=86=92 (http://hackernewsletter.us1.list-m=\\r\\nanage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D3419a482f1&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nStep-by-step tutorial to build a modern JavaScript stack from scratch (htt=\\r\\np://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef=\\r\\n92c4f3d4&id=3Dea6c97f36d&e=3D765272fcdd) //github comments=E2=86=92=\\r\\n (http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a1=\\r\\n11cef92c4f3d4&id=3D45059d8ac3&e=3D765272fcdd)\\r\\n\\r\\nA fork of sudo with Touch ID support (http://hackernewsletter.us1.list-man=\\r\\nage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dc52784337f&e=3D=\\r\\n765272fcdd) //github comments=E2=86=92 (http://hackernewsletter.us1.lis=\\r\\nt-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D00e5c49e67&e=\\r\\n=3D765272fcdd)\\r\\n\\r\\nWriting more legible SQL (http://hackernewsletter.us1.list-manage1.com/tra=\\r\\nck/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Def47d7c8d3&e=3D=\\r\\n765272fcdd) //craigkerstiens comments=E2=86=92 (http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D505c66dcf1&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nLighthouse =E2=80=93 Auditing and Performance Metrics for Progressive Web=\\r\\n Apps (http://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb=\\r\\n4ef3a111cef92c4f3d4&id=3D3705b0e290&e=3D765272fcdd) //github comment=\\r\\ns=E2=86=92 (http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfa=\\r\\na8eb4ef3a111cef92c4f3d4&id=3D563534ac47&e=3D765272fcdd)\\r\\n\\r\\nBashcached =E2=80=93 memcached built on bash and ncat (http://hackernewsle=\\r\\ntter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd3=\\r\\n8c37782d&e=3D765272fcdd) //github comments=E2=86=92 (http://hackerne=\\r\\nwsletter.us1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=\\r\\n=3D759a4124b3&e=3D765272fcdd)\\r\\n\\r\\n\\r\\n** #Design\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nDon=E2=80=99t go to art school (2013) (http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D19aaf7ce5b&e=3D=\\r\\n765272fcdd) //medium comments=E2=86=92 (http://hackernewsletter.us1.lis=\\r\\nt-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd211d367da&e=\\r\\n=3D765272fcdd)\\r\\n\\r\\nUX Myths (2014) (http://hackernewsletter.us1.list-manage1.com/track/click?=\\r\\nu=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D6ffa88b963&e=3D765272fcdd) //uxmy=\\r\\nths comments=E2=86=92 (http://hackernewsletter.us1.list-manage1.com/track/=\\r\\nclick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D00e93d7214&e=3D765272fcdd)\\r\\n\\r\\n\\r\\n** #Watching\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nThomas Piketty=E2=80=99s Capital in the 21st Century=2C in 20 minutes (htt=\\r\\np://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef=\\r\\n92c4f3d4&id=3Dab43dfa0d6&e=3D765272fcdd) //boingboing comments=E2=86=\\r\\n=92 (http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef=\\r\\n3a111cef92c4f3d4&id=3D28fd789f6f&e=3D765272fcdd)\\r\\n\\r\\nKeep Ruby Weird Again (http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D98946ca1d4&e=3D765272fcdd) /=\\r\\n/testdouble comments=E2=86=92 (http://hackernewsletter.us1.list-manage.com=\\r\\n/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D86d3c6925c&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nJapan=E2=80=99s Disposable Workers: Dumping Ground (http://hackernewslette=\\r\\nr.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D97347=\\r\\n9829c&e=3D765272fcdd) //vimeo comments=E2=86=92 (http://hackernewsle=\\r\\ntter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D3d=\\r\\nc2036d48&e=3D765272fcdd)\\r\\n\\r\\n\\r\\n** #Working\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\n=E2=9D=97=EF=B8=8FAsk HN: Who is firing? (http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd2c73aae27&e=3D=\\r\\n765272fcdd) //ycombinator\\r\\n\\r\\nGuide to Remote Work (http://hackernewsletter.us1.list-manage1.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D18afc5989a&e=3D765272fcdd) /=\\r\\n/zapier comments=E2=86=92 (http://hackernewsletter.us1.list-manage1.com/tr=\\r\\nack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D0167b4f792&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nAsk HN: Who is hiring? (http://hackernewsletter.us1.list-manage1.com/track=\\r\\n/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dc604af7c93&e=3D765272fcdd)=\\r\\n //ycombinator\\r\\n\\r\\nAsk HN: Who wants to be hired? (http://hackernewsletter.us1.list-manage.co=\\r\\nm/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De46ca7ced5&e=3D=\\r\\n765272fcdd) //ycombinator\\r\\n\\r\\nAsk HN: Freelancer? Seeking freelancer? (http://hackernewsletter.us1.list-=\\r\\nmanage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Ddbcca91439&e=3D=\\r\\n765272fcdd) //ycombinator\\r\\n\\r\\n\\r\\n** #Fun\\r\\n------------------------------------------------------------\\r\\n------------------------------------------------------------\\r\\n\\r\\nStealth Cell Tower Disguised as Printer (http://hackernewsletter.us1.list-=\\r\\nmanage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dae17e93e7f&e=3D=\\r\\n765272fcdd) //julianoliver comments=E2=86=92 (http://hackernewsletter=\\r\\n=2Eus1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Ddd0291=\\r\\n23dd&e=3D765272fcdd)\\r\\n\\r\\nMuseu de la T=C3=A8cnica (http://hackernewsletter.us1.list-manage2.com/tra=\\r\\nck/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D093d9205aa&e=3D=\\r\\n765272fcdd) //twitter comments=E2=86=92 (http://hackernewsletter.us1.list-manage.co=\\r\\nm/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D39549aa682&e=3D=\\r\\n765272fcdd)\\r\\n\\r\\nBenjamin Button Reviews the New MacBook Pro (http://hackernewsletter.us1.l=\\r\\nist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dce263de0f0&=\\r\\ne=3D765272fcdd) //pinboard comments=E2=86=92 (http://hackernewslette=\\r\\nr.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Ddb15b=\\r\\n6b38b&e=3D765272fcdd)\\r\\n\\r\\nA Gamer Spent 200 Hours Building an Incredibly Detailed Digital San Franci=\\r\\nsco (http://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4e=\\r\\nf3a111cef92c4f3d4&id=3Df6289432f1&e=3D765272fcdd) //citylab comments=\\r\\n=E2=86=92 (http://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfa=\\r\\na8eb4ef3a111cef92c4f3d4&id=3D5deab9201e&e=3D765272fcdd)\\r\\n\\r\\nWhy html thinks 'chucknorris' is a color (http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dba55670176&e=\\r\\n=3D765272fcdd) //stackoverflow comments=E2=86=92 (http://hackernewsl=\\r\\netter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D=\\r\\ne27a284d73&e=3D765272fcdd)\\r\\n__END__\\r\\n\\r\\nYou're among 38=2C633 others who received this email because you wanted a=\\r\\n weekly recap of the best articles from Hacker News. Published by Curpress=\\r\\n (http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a1=\\r\\n11cef92c4f3d4&id=3D2620a45995&e=3D765272fcdd) from a smallish metal=\\r\\n box at PO BOX 2621 Decatur=2C Georgia 30031. Hacker Newsletter is not aff=\\r\\niliated with Y Combinator in any way.\\r\\n\\r\\nYou can update your email (http://hackernewsletter.us1.list-manage.com/pro=\\r\\nfile?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De505c88a2e&e=3D765272fcdd) or unsubs=\\r\\ncribe (http://hackernewsletter.us1.list-manage.com/unsubscribe?u=3Dfaa8eb4=\\r\\nef3a111cef92c4f3d4&id=3De505c88a2e&e=3D765272fcdd&c=3Da018682c80) .\\r\\n\\r\\nNot a subscriber? Subscribe at http://hackernewsletter.us1.list-manage.com=\\r\\n/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Daba6892cb9&e=3D=\\r\\n765272fcdd.\\r\\n\\r\\nEmail Marketing Powered by MailChimp\\r\\nhttp://www.mailchimp.com/monkey-rewards/?utm_source=3Dfreemium_newsletter&=\\r\\nutm_medium=3Demail&utm_campaign=3Dmonkey_rewards&aid=3Dfaa8eb4ef3a111cef92=\\r\\nc4f3d4&afl=3D1\",\"2\":\"<!DOCTYPE html>\\r\\n<html xmlns=3D\\\"http://www.w3.org/1999/xhtml\\\">\\r\\n<head><meta name=3D\\\"twitter:image:src\\\" content=3D\\\"https://gallery.mailchim=\\r\\np.com/faa8eb4ef3a111cef92c4f3d4/images/9d6a9334-af30-482f-a4e3-a6e52121c84=\\r\\n2.png\\\"><meta name=3D\\\"twitter:description\\\" content=3D\\\"A weekly newsletter o=\\r\\nf the best articles on startups=2C technology=2C programming=2C and more.=\\r\\n All links are curated by hand from the popular Hacker News site.\\\"><meta n=\\r\\name=3D\\\"twitter:title\\\" content=3D\\\"Hacker Newsletter #325\\\"><meta name=3D\\\"twi=\\r\\ntter:card\\\" content=3D\\\"summary_large_image\\\"><meta property=3D\\\"og:type\\\" cont=\\r\\nent=3D\\\"article\\\"><meta property=3D\\\"og:description\\\" content=3D\\\"A weekly news=\\r\\nletter of the best articles on startups=2C technology=2C programming=2C an=\\r\\nd more. All links are curated by hand from the popular Hacker News site.\\\">=\\r\\n<meta property=3D\\\"og:image\\\" content=3D\\\"https://gallery.mailchimp.com/faa8e=\\r\\nb4ef3a111cef92c4f3d4/images/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"><met=\\r\\na property=3D\\\"og:title\\\" content=3D\\\"Hacker Newsletter #325\\\"><meta property=\\r\\n=3D\\\"og:url\\\" content=3D\\\"http://eepurl.com/cncb2H\\\"><meta name=3D\\\"twitter:ima=\\r\\nge:src\\\" content=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111cef92c4f3d4=\\r\\n/images/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"><meta name=3D\\\"twitter:de=\\r\\nscription\\\" content=3D\\\"A weekly newsletter of the best articles on startups=\\r\\n=2C technology=2C programming=2C and more. All links are curated by hand f=\\r\\nrom the popular Hacker News site.\\\"><meta name=3D\\\"twitter:title\\\" content=3D=\\r\\n\\\"Hacker Newsletter #325\\\"><meta name=3D\\\"twitter:card\\\" content=3D\\\"summary_la=\\r\\nrge_image\\\"><meta property=3D\\\"og:type\\\" content=3D\\\"article\\\"><meta property=\\r\\n=3D\\\"og:description\\\" content=3D\\\"A weekly newsletter of the best articles on=\\r\\n startups=2C technology=2C programming=2C and more. All links are curated=\\r\\n by hand from the popular Hacker News site.\\\"><meta property=3D\\\"og:image\\\" c=\\r\\nontent=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111cef92c4f3d4/images/9=\\r\\nd6a9334-af30-482f-a4e3-a6e52121c842.png\\\"><meta property=3D\\\"og:title\\\" conte=\\r\\nnt=3D\\\"Hacker Newsletter #325\\\"><meta property=3D\\\"og:url\\\" content=3D\\\"http://=\\r\\neepurl.com/cncb2H\\\">\\r\\n  <title>Hacker Newsletter #325</title>\\r\\n  <meta http-equiv=3D\\\"Content-Type\\\" content=3D\\\"text/html; charset=3Dutf-8\\\"=\\r\\n>\\r\\n  <meta name=3D\\\"viewport\\\" id=3D\\\"viewport\\\" content=3D\\\"width=3Ddevice-width=\\r\\n=2Cminimum-scale=3D1.0=2Cmaximum-scale=3D10.0=2Cinitial-scale=3D1.0\\\">\\r\\n  <meta name=3D\\\"viewport\\\" id=3D\\\"viewport-iphone5\\\" content=3D\\\"initial-scale=\\r\\n=3D1.0=2Cuser-scalable=3Dno=2Cmaximum-scale=3D1\\\" media=3D\\\"(device-height:=\\r\\n 568px)\\\">\\r\\n  <style type=3D\\\"text/css\\\">\\r\\n    body { background: #FFF; }\\r\\n    body=2C h1=2C h2=2C h3=2C p=2C table=2C tr=2C td {\\r\\n      color: #333;\\r\\n      font-family: ubuntu=2C Helvetica=2C \\\"Lucida Grande\\\"=2C Arial=2C sans=\\r\\n-serif;\\r\\n      margin: 0;\\r\\n      padding: 0;\\r\\n    }\\r\\n    .main=2C table { background-color: #FFFFFF; }\\r\\n    a { color: #1786C1; text-decoration: underline; }\\r\\n    a:hover { text-decoration: none; }\\r\\n    h1 {\\r\\n      font-size: 35px;\\r\\n      margin-bottom: 5px;\\r\\n      padding-top: 10px;\\r\\n      width: 100%;\\r\\n    }\\r\\n    h1 a { color: #333; text-decoration: none; }\\r\\n    h1 a:hover { color: #666; text-decoration: underline; }\\r\\n    h2 {\\r\\n      color: #cc0000;\\r\\n      font-size: 16px;\\r\\n      font-weight: bold;\\r\\n      margin: 20px 0 10px;\\r\\n    }\\r\\n    h3 {\\r\\n      border-bottom: 1px solid #333;\\r\\n      border-top: 3px solid #333;\\r\\n      font-size: 16px;\\r\\n      font-weight: normal;\\r\\n      line-height: 22px;\\r\\n      margin: 20px 0;\\r\\n      padding: 20px 0;\\r\\n    }\\r\\n    p {\\r\\n      color: #333;\\r\\n      font-size: 16px;\\r\\n      line-height: 24px;\\r\\n      width: 500px;\\r\\n    }\\r\\n    p {\\r\\n      font-size: 16px;\\r\\n      line-height: 22px;\\r\\n      margin: 0 0 10px 0;\\r\\n      width: 100%;\\r\\n    }\\r\\n    table=2C .main=2C #header=2C #footer { max-width: 600px; width: 100%;=\\r\\n }\\r\\n    #header { margin-bottom: 20px; }\\r\\n    #issue {\\r\\n      color: #333;\\r\\n      font-family: ubuntu=2C Helvetica=2C \\\"Lucida Grande\\\"=2C Arial=2C sans=\\r\\n-serif;\\r\\n      font-size: 12px;\\r\\n      text-align: right;\\r\\n    }\\r\\n    #issue i { color: #999; font-style: normal; }\\r\\n\\r\\n    @media only screen and (max-device-width: 480px) {\\r\\n      table=2C #header=2C #content=2C #footer { padding: 0 20px !important=\\r\\n; }\\r\\n      h1 { font-size: 30px !important; }\\r\\n    }\\r\\n\\r\\n    @media only screen and (max-device-width: 679px)=2C only screen and (m=\\r\\nax-width: 679px) {\\r\\n      .main { margin: 20px auto; width: 100%; }\\r\\n      table=2C #header=2C #content=2C #footer { padding: 0 30px; width: 10=\\r\\n0%; }\\r\\n\\r\\n      #details { padding-right: 10px; }\\r\\n    }\\r\\n\\r\\n    @media only screen and (min-width: 680px) {\\r\\n      body { background: #999 url('http://hackernewsletter.com/images/debu=\\r\\nt_dark.png') 0 0 fixed repeat !important; }\\r\\n      #details { width: 496px; }\\r\\n      .main=2C table=2C #header=2C #content=2C #footer { background: #FFF=\\r\\n url('') 0 0 repeat; width: 600px; }\\r\\n\\r\\n      .main {\\r\\n                box-shadow: 0 0 12px rgba(0=2C 0=2C 0=2C .6);\\r\\n           -moz-box-shadow: 0 0 12px rgba(0=2C 0=2C 0=2C .6);\\r\\n        -webkit-box-shadow: 0 0 12px rgba(0=2C 0=2C 0=2C .6);\\r\\n        margin: 10px auto 40px;\\r\\n        padding: 30px 40px 10px;\\r\\n      }\\r\\n    }\\r\\n  </style>\\r\\n</head>\\r\\n<body style=3D\\\"-webkit-text-size-adjust: none;width: 100% !important;backg=\\r\\nround-color: #FFF;background-image: none;background-repeat: repeat;backgro=\\r\\nund-position: top left;background-attachment: scroll;color: #333;font-fami=\\r\\nly: ubuntu=2C Helvetica=2C 'Lucida Grande'=2C Arial=2C sans-serif;margin-t=\\r\\nop: 0;margin-bottom: 0;margin-right: 0;margin-left: 0;padding-top: 0;paddi=\\r\\nng-bottom: 0;padding-right: 0;padding-left: 0;background: #FFF;margin: 0;p=\\r\\nadding: 0;\\\"><div itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/EmailMessage=\\r\\n\\\"><div itemprop=3D\\\"publisher\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org=\\r\\n/Organization\\\"><meta itemprop=3D\\\"name\\\" content=3D\\\"Hacker Newsletter\\\"><link=\\r\\n itemprop=3D\\\"url\\\" content=3D\\\"http://www.hackernewsletter.com\\\"></div><div i=\\r\\ntemprop=3D\\\"about\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/Offer\\\"><lin=\\r\\nk itemprop=3D\\\"image\\\" href=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111c=\\r\\nef92c4f3d4/images/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"></div></div><d=\\r\\niv itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/EmailMessage\\\"><div itempro=\\r\\np=3D\\\"publisher\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/Organization\\\"=\\r\\n><meta itemprop=3D\\\"name\\\" content=3D\\\"Hacker Newsletter\\\"><link itemprop=3D\\\"u=\\r\\nrl\\\" content=3D\\\"http://www.hackernewsletter.com\\\"></div><div itemprop=3D\\\"abo=\\r\\nut\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/Offer\\\"><link itemprop=3D\\\"=\\r\\nimage\\\" href=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111cef92c4f3d4/ima=\\r\\nges/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"></div></div>\\r\\n  <div class=3D\\\"main\\\" style=3D\\\"background-color:#FFFFFF;max-width:600px;wi=\\r\\ndth:100%;\\\">\\r\\n\\r\\n    <table id=3D\\\"header\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\"=\\r\\n style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C &quot;Lucida Gra=\\r\\nnde&quot;=2C Arial=2C sans-serif;margin: 0;padding: 0;background-color: #F=\\r\\nFFFFF;max-width: 600px;width: 100%;margin-bottom: 20px;\\\">\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C &quot;L=\\r\\nucida Grande&quot;=2C Arial=2C sans-serif;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C &quot=\\r\\n;Lucida Grande&quot;=2C Arial=2C sans-serif;margin: 0;padding: 0;\\\">\\r\\n          <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Grande'=\\r\\n=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-right: 0;mar=\\r\\ngin-left: 0;padding-top: 0;padding-right: 0;padding-left: 0;color: #7B7B7B=\\r\\n;font-size: 12px;font-weight: normal;line-height: 22px;padding-bottom: 20p=\\r\\nx;margin: 0 0 10px 0;padding: 0;width: 100%;\\\">\\r\\n            The only thing more expensive than writing software is writing=\\r\\n bad software. //Alan Cooper\\r\\n          </p>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </table>\\r\\n\\r\\n    <table id=3D\\\"content\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\"=\\r\\n style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida Grande'=\\r\\n=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-right: 0;mar=\\r\\ngin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padding-left=\\r\\n: 0;background-color: #FFFFFF;max-width: 600px;width: 100%;margin: 0;paddi=\\r\\nng: 0;\\\">\\r\\n\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Luci=\\r\\nda Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-ri=\\r\\nght: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;pa=\\r\\ndding-left: 0;margin: 0;padding: 0;\\\">\\r\\n\\r\\n          <h1 style=3D\\\"color: #333;font-family: Helvetica=2C 'Lucida Grand=\\r\\ne'=2C Arial=2C sans-serif;margin-top: 0;margin-right: 0;margin-left: 0;pad=\\r\\nding-bottom: 0;padding-right: 0;padding-left: 0;margin-bottom: 0px;padding=\\r\\n-top: 20px;width: 100%;margin: 0;padding: 0;font-size: 35px;\\\">\\r\\n            <a href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D6277d2945c&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color: #333;text-decoration: none;\\\">hacker<span style=3D\\\"color:#ff3300=\\r\\n\\\">news</span>letter</a>\\r\\n          </h1>\\r\\n        </td>\\r\\n      </tr>\\r\\n\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td id=3D\\\"details\\\" style=3D\\\"color: #333;font-family: ubuntu=2C Hel=\\r\\nvetica=2C 'Lucida Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bott=\\r\\nom: 0;margin-right: 0;margin-left: 0;padding-top: 0;padding-bottom: 10px;p=\\r\\nadding-right: 0;padding-left: 0;margin: 0;padding: 0;\\\">\\r\\n          <span id=3D\\\"issue\\\" style=3D\\\"color:#333;font-family:ubuntu=2C Hel=\\r\\nvetica=2C 'Lucida Grande'=2C Arial=2C sans-serif;font-size:12px;text-align=\\r\\n:right;\\\">Issue #325 <i style=3D\\\"color:#999;font-style:normal;\\\">//</i> Nove=\\r\\nmber 04=2C 2016 <i style=3D\\\"color:#999;font-style:normal;\\\">//</i> <a href=\\r\\n=3D\\\"http://us1.campaign-archive2.com/?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da=\\r\\n018682c80&e=3D765272fcdd\\\" style=3D\\\"color:#3B6B9B;text-decoration:underline;\\\"=\\r\\n>View in your browser</a></span>\\r\\n        </td>\\r\\n      </tr>\\r\\n\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Luci=\\r\\nda Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-ri=\\r\\nght: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;pa=\\r\\ndding-left: 0;margin: 0;padding: 0;\\\">\\r\\n          <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=2C=\\r\\n sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;padding-l=\\r\\neft: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20px;m=\\r\\nargin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: uppercase=\\r\\n;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Spons=\\r\\nor</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n          <p style=3D\\\"padding-left: 10px;color: #333;font-family: ubuntu=\\r\\n=2C Helvetica=2C &quot;Lucida Grande&quot;=2C Arial=2C sans-serif;margin:=\\r\\n 0 0 10px 0;padding: 0;font-size: 16px;line-height: 22px;width: 100%;\\\">\\r\\n            <a href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/=\\r\\nclick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D223d78539f&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color: #1786C1;text-decoration: underline;\\\"><img style=3D\\\"width: 120px=\\r\\n; float: left; margin-right: 14px; margin-bottom: 16px; padding: 2px; bord=\\r\\ner: none; height: auto; line-height: 100%; outline: none; text-decoration:=\\r\\n none; display: inline;\\\" src=3D\\\"http://hackernewsletter.com/img/hired125.p=\\r\\nng\\\"></a>\\r\\n            <a href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df34c194567&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color: #3B6B9B; font-weight: bold; text-decoration: underline;\\\">Hired=\\r\\n - The End of Job Hunting As You Know It</a><br>Hired brings job offers to=\\r\\n you=2C so you can cut to the chase and focus on finding the right fit=2C=\\r\\n (not applying). Get multiple job offers and upfront compensation informat=\\r\\nion=2C with just one application. <a href=3D\\\"http://hackernewsletter.us1.l=\\r\\nist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D19969ea9d1&=\\r\\ne=3D765272fcdd\\\" style=3D\\\"color: #3B6B9B; font-weight: normal; text-decora=\\r\\ntion: underline;\\\">Get Hired</a>\\r\\n          </p>\\r\\n          <p style=3D\\\"clear: both;color: #333;font-family: ubuntu=2C Helve=\\r\\ntica=2C &quot;Lucida Grande&quot;=2C Arial=2C sans-serif;margin: 0 0 10px=\\r\\n 0;padding: 0;font-size: 16px;line-height: 22px;width: 100%;\\\"></p>\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Fa=\\r\\nvorites</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dbeb20e52d7&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 1066 Comments: 80\\\">Eve: Programming designed for hum=\\r\\nans</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"fon=\\r\\nt-size:11px;padding-right:1px;\\\">//</span>witheve <a style=3D\\\"text-decorati=\\r\\non: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage=\\r\\n2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D80976fd42f&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D5a32e61edc&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 803 Comments: 65\\\">Total Nightmare: USB-C and Thun=\\r\\nderbolt 3</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=\\r\\n=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>fosketts <a style=3D\\\"text-=\\r\\ndecoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.lis=\\r\\nt-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2978edafd1&e=\\r\\n=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></=\\r\\nspan></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D5604b993eb&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 548 Comments: 59\\\">Signs that a startup is focused=\\r\\n on stuff that doesn=E2=80=99t matter</a><br><span style=3D\\\"font-size: 13p=\\r\\nx; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span=\\r\\n>groovehq <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http=\\r\\n://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef9=\\r\\n2c4f3d4&id=3D1a0fd3ad47&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3=\\r\\n300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd56067cc6f&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 516 Comments: 152\\\">New MacBook Pro Is Not a Lapto=\\r\\np for Developers Anymore</a><br><span style=3D\\\"font-size: 13px; color: #77=\\r\\n7\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>blog <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9825=\\r\\n502db7&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</spa=\\r\\nn></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9fbe914f3f&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 473 Comments: 41\\\">Web fonts=2C boy=2C I don't kno=\\r\\nw</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-=\\r\\nsize:11px;padding-right:1px;\\\">//</span>meowni <a style=3D\\\"text-decoration:=\\r\\n none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.co=\\r\\nm/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De201ba68bb&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D70fe5044af&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 459 Comments: 38\\\">No One Saw Tesla=E2=80=99s Solar R=\\r\\noof Coming</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=\\r\\n=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>bloomberg <a style=3D\\\"text=\\r\\n-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.li=\\r\\nst-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df251daf9b8&e=\\r\\n=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></=\\r\\nspan></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D74433e7684&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 356 Comments: 15\\\">Ways Data Projects Fail</a><br><sp=\\r\\nan style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;pa=\\r\\ndding-right:1px;\\\">//</span>martingoodson <a style=3D\\\"text-decoration: none=\\r\\n; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/tra=\\r\\nck/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D08f9d94a7f&e=3D765272fcdd\\\">co=\\r\\nmments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D14431ecb68&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 310 Comments: 6\\\">A very valuable vulnerability</a><b=\\r\\nr><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11=\\r\\npx;padding-right:1px;\\\">//</span>daemonology <a style=3D\\\"text-decoration: n=\\r\\none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/=\\r\\ntrack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D0888f32e26&e=3D765272fcdd\\\"=\\r\\n>comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da1165dabad&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 264 Comments: 27\\\">Making what people want isn=E2=80=\\r\\n=99t enough=2C you have to share it</a><br><span style=3D\\\"font-size: 13px;=\\r\\n color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>o=\\r\\nldgeekjobs <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"htt=\\r\\np://hackernewsletter.us1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111ce=\\r\\nf92c4f3d4&id=3D8597e2e648&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#f=\\r\\nf3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dbaa66ea75c&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 140 Comments: 10\\\">How ReadMe Went from SaaS to On=\\r\\n-Premises in Less Than One Week</a><br><span style=3D\\\"font-size: 13px; col=\\r\\nor: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>stack=\\r\\nshare <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://h=\\r\\nackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f=\\r\\n3d4&id=3D96eccec5b3&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;=\\r\\n\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>As=\\r\\nk HN</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D542f549d4b&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 520 Comments: 59\\\">Any other blind devs interested=\\r\\n in working on dev tools for the blind?</a></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9f4d11d0bf&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 412 Comments: 37\\\">How to make a career working remot=\\r\\nely?</a></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Sh=\\r\\now HN</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D1fb70805ff&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 715 Comments: 136\\\">Music for Programming</a> <span style=3D\\\"font-size:=\\r\\n 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</=\\r\\nspan>musicforprogramming <a style=3D\\\"text-decoration: none; color: #336699=\\r\\n;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa=\\r\\n8eb4ef3a111cef92c4f3d4&id=3D3638b3dd13&e=3D765272fcdd\\\">comments<span styl=\\r\\ne=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dad530e4493&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 520 Comments: 44\\\">Powerwall 2 and Integrated Solar</a> <span style=3D\\\"=\\r\\nfont-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:=\\r\\n1px;\\\">//</span>tesla <a style=3D\\\"text-decoration: none; color: #336699;\\\" h=\\r\\nref=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4=\\r\\nef3a111cef92c4f3d4&id=3D2817f25e9a&e=3D765272fcdd\\\">comments<span style=3D=\\r\\n\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D89cf4973c3&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 354 Comments: 47\\\">Sonder E-Ink Keyboard</a> <span style=3D\\\"font-size: 1=\\r\\n3px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</sp=\\r\\nan>sonderdesign <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=\\r\\n=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3=\\r\\na111cef92c4f3d4&id=3D50f5bcc6e3&e=3D765272fcdd\\\">comments<span style=3D\\\"co=\\r\\nlor:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D565afeb0a6&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 296 Comments: 38\\\">Microsoft Teams=2C the new chat-based workspace in Of=\\r\\nfice 365</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"f=\\r\\nont-size:11px;padding-right:1px;\\\">//</span>office <a style=3D\\\"text-decorat=\\r\\nion: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manag=\\r\\ne.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D8b9ff7b22e&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D0ea8f9dd98&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 210 Comments: 28\\\">Flow =E2=80=93 Create automated workflows between yo=\\r\\nur favorite apps</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span st=\\r\\nyle=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>microsoft <a style=3D\\\"t=\\r\\next-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1=\\r\\n=2Elist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D60766b118=\\r\\n5&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a=\\r\\n></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da62f6fa376&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 198 Comments: 15\\\">Portier =E2=80=93 An email-based=2C passwordless aut=\\r\\nhentication service</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span=\\r\\n style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>github <a style=3D\\\"t=\\r\\next-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1=\\r\\n=2Elist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Ddd5396574=\\r\\nf&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a=\\r\\n></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D986fb86342&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 103 Comments: 8\\\">Newly Redesigned Boston.gov Just Went Open Source</a>=\\r\\n <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11p=\\r\\nx;padding-right:1px;\\\">//</span>routefifty <a style=3D\\\"text-decoration: non=\\r\\ne; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/tr=\\r\\nack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2b0346293a&e=3D765272fcdd\\\">c=\\r\\nomments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd179182492&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 50 Comments: 8\\\">Sodaphonic =E2=80=93 record and edit audio in the brow=\\r\\nser</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-s=\\r\\nize:11px;padding-right:1px;\\\">//</span>sodaphonic <a style=3D\\\"text-decorati=\\r\\non: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage=\\r\\n2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2c7d5c0186&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Fe=\\r\\natured</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De3e6c607b2&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 209 Comments: 31\\\">Dear Matt Mullenweg: An Open Letter from Wix.com=E2=\\r\\n=80=99s CEO Avishai Abrahami</a> <span style=3D\\\"font-size: 13px; color: #7=\\r\\n77\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>wix <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D947=\\r\\ndb727e8&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</sp=\\r\\nan></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9581130dbe&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 208 Comments: 24\\\">Open Letter to Tim Cook</a> <span style=3D\\\"font-size=\\r\\n: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//<=\\r\\n/span>petersphilo <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=\\r\\n=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef=\\r\\n3a111cef92c4f3d4&id=3Dcfe7f2db39&e=3D765272fcdd\\\">comments<span style=3D\\\"c=\\r\\nolor:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D95d0c443e9&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 104 Comments: 45\\\">Dear Microsoft</a> <span style=3D\\\"font-size: 13px; c=\\r\\nolor: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>sla=\\r\\nckhq <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://ha=\\r\\nckernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3=\\r\\nd4&id=3D09fc5ae3ef&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\"=\\r\\n>&rarr;</span></a></span></p>\\r\\n\\r\\n                 <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida G=\\r\\nrande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-rig=\\r\\nht: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin=\\r\\n-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;mar=\\r\\ngin: 0 0 10px 0;padding: 0;\\\">=E2=9D=97=EF=B8=8F<a href=3D\\\"http://hackernew=\\r\\nsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=\\r\\n=3Dc558014b74&e=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:unde=\\r\\nrline;\\\" title=3D\\\"Votes: 283 Comments: 45\\\">Slack may regret its letter to M=\\r\\nicrosoft</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"f=\\r\\nont-size:11px;padding-right:1px;\\\">//</span>theverge <a style=3D\\\"text-decor=\\r\\nation: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-man=\\r\\nage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df5d14c77d0&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span><=\\r\\n/p>\\r\\n\\r\\n\\r\\n\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Co=\\r\\nde</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D7d3d6ba2f8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 611 Comments: 29\\\">Darling =E2=80=93 MacOS translation layer for Linux</=\\r\\na> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:1=\\r\\n1px;padding-right:1px;\\\">//</span>darlinghq <a style=3D\\\"text-decoration: no=\\r\\nne; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/t=\\r\\nrack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D997fe00b69&e=3D765272fcdd\\\">=\\r\\ncomments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D8b2df1b5cf&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 372 Comments: 37\\\">I don't understand Python's Asyncio</a> <span style=\\r\\n=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-ri=\\r\\nght:1px;\\\">//</span>pocoo <a style=3D\\\"text-decoration: none; color: #336699=\\r\\n;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa=\\r\\n8eb4ef3a111cef92c4f3d4&id=3Dcfa183b0b8&e=3D765272fcdd\\\">comments<span styl=\\r\\ne=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D92c0e76c15&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 363 Comments: 33\\\">Step-by-step tutorial to build a modern JavaScript st=\\r\\nack from scratch</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span st=\\r\\nyle=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>github <a style=3D\\\"text=\\r\\n-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.li=\\r\\nst-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D26b1cc8b9f&e=\\r\\n=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></=\\r\\nspan></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dcbe3a418d9&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 346 Comments: 18\\\">A fork of sudo with Touch ID support</a> <span style=\\r\\n=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-ri=\\r\\nght:1px;\\\">//</span>github <a style=3D\\\"text-decoration: none; color: #33669=\\r\\n9;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfa=\\r\\na8eb4ef3a111cef92c4f3d4&id=3D596564d3f7&e=3D765272fcdd\\\">comments<span sty=\\r\\nle=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dec79b70642&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 184 Comments: 44\\\">Writing more legible SQL</a> <span style=3D\\\"font-size=\\r\\n: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//<=\\r\\n/span>craigkerstiens <a style=3D\\\"text-decoration: none; color: #336699;\\\" h=\\r\\nref=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4=\\r\\nef3a111cef92c4f3d4&id=3D925e39bb0a&e=3D765272fcdd\\\">comments<span style=3D=\\r\\n\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd0faac4e0c&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 138 Comments: 1\\\">Lighthouse =E2=80=93 Auditing and Performance Metrics=\\r\\n for Progressive Web Apps</a> <span style=3D\\\"font-size: 13px; color: #777\\\"=\\r\\n><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>github <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D947a=\\r\\n25757f&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</spa=\\r\\nn></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D166fa21546&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 82 Comments: 8\\\">Bashcached =E2=80=93 memcached built on bash and ncat</=\\r\\na> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:1=\\r\\n1px;padding-right:1px;\\\">//</span>github <a style=3D\\\"text-decoration: none;=\\r\\n color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage2.com/tra=\\r\\nck/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D43e8ba70fc&e=3D765272fcdd\\\">co=\\r\\nmments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>De=\\r\\nsign</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D1648a4f0e2&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 345 Comments: 38\\\">Don=E2=80=99t go to art school (2013)</a> <span style=\\r\\n=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-ri=\\r\\nght:1px;\\\">//</span>medium <a style=3D\\\"text-decoration: none; color: #33669=\\r\\n9;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/click?u=3Df=\\r\\naa8eb4ef3a111cef92c4f3d4&id=3Dde6fa5997c&e=3D765272fcdd\\\">comments<span st=\\r\\nyle=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D4f708d14b8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 232 Comments: 12\\\">UX Myths (2014)</a> <span style=3D\\\"font-size: 13px; c=\\r\\nolor: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>uxm=\\r\\nyths <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://ha=\\r\\nckernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f=\\r\\n3d4&id=3D1076cd06b1&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;=\\r\\n\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Wa=\\r\\ntching</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df339137326&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 274 Comments: 16\\\">Thomas Piketty=E2=80=99s Capital in the 21st Century=\\r\\n=2C in 20 minutes</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span s=\\r\\ntyle=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>boingboing <a style=3D=\\r\\n\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.u=\\r\\ns1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D38cad1c=\\r\\n8ed&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span><=\\r\\n/a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D1ad42efbec&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 190 Comments: 12\\\">Keep Ruby Weird Again</a> <span style=3D\\\"font-size: 1=\\r\\n3px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</sp=\\r\\nan>testdouble <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"=\\r\\nhttp://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111=\\r\\ncef92c4f3d4&id=3D75c2f8a66b&e=3D765272fcdd\\\">comments<span style=3D\\\"color:=\\r\\n#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dca25e48358&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 69 Comments: 9\\\">Japan=E2=80=99s Disposable Workers: Dumping Ground</a>=\\r\\n <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11p=\\r\\nx;padding-right:1px;\\\">//</span>vimeo <a style=3D\\\"text-decoration: none; co=\\r\\nlor: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Db2b2a1a5fa&e=3D765272fcdd\\\">commen=\\r\\nts<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Wo=\\r\\nrking</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\">=E2=9D=97=EF=B8=8F<a href=3D\\\"http://hackernews=\\r\\nletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D=\\r\\n6b31dbf058&e=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underli=\\r\\nne;\\\" title=3D\\\"Votes: 1047 Comments: 50\\\">Ask HN: Who is firing?</a> <span s=\\r\\ntyle=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;paddin=\\r\\ng-right:1px;\\\">//</span>ycombinator</span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D74bcea07ef&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 730 Comments: 22\\\">Guide to Remote Work</a> <span style=3D\\\"font-size: 13=\\r\\npx; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</spa=\\r\\nn>zapier <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http:=\\r\\n//hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef9=\\r\\n2c4f3d4&id=3D3016a20bad&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3=\\r\\n300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D65aa059ce8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 683 Comments: 821\\\">Ask HN: Who is hiring?</a> <span style=3D\\\"font-size:=\\r\\n 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</=\\r\\nspan>ycombinator</span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D31e85384f4&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 124 Comments: 221\\\">Ask HN: Who wants to be hired?</a> <span style=3D\\\"fo=\\r\\nnt-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1p=\\r\\nx;\\\">//</span>ycombinator</span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dcc1f34da72&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 76 Comments: 115\\\">Ask HN: Freelancer? Seeking freelancer?</a> <span sty=\\r\\nle=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-=\\r\\nright:1px;\\\">//</span>ycombinator</span></p>\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Fu=\\r\\nn</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D89e4b080ef&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 510 Comments: 15\\\">Stealth Cell Tower Disguised as Printer</a> <span sty=\\r\\nle=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-=\\r\\nright:1px;\\\">//</span>julianoliver <a style=3D\\\"text-decoration: none; color=\\r\\n: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/cli=\\r\\nck?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D5a59366f95&e=3D765272fcdd\\\">comments=\\r\\n<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dba8d170a4d&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 443 Comments: 39\\\">Museu de la T=C3=A8cnica</a> <span style=3D\\\"font-size=\\r\\n: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//<=\\r\\n/span>twitter <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"=\\r\\nhttp://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111=\\r\\ncef92c4f3d4&id=3Dd2f921f1eb&e=3D765272fcdd\\\">comments<span style=3D\\\"color:=\\r\\n#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd576da7fa8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 249 Comments: 14\\\">Benjamin Button Reviews the New MacBook Pro</a> <span=\\r\\n style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padd=\\r\\ning-right:1px;\\\">//</span>pinboard <a style=3D\\\"text-decoration: none; color=\\r\\n: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/clic=\\r\\nk?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dcef41b1362&e=3D765272fcdd\\\">comments<=\\r\\nspan style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D34e4f4cbc8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 150 Comments: 16\\\">A Gamer Spent 200 Hours Building an Incredibly Detail=\\r\\ned Digital San Francisco</a> <span style=3D\\\"font-size: 13px; color: #777\\\">=\\r\\n<span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>citylab <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2210=\\r\\n663ceb&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</spa=\\r\\nn></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D10a776509b&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 10 Comments: 2\\\">Why html thinks 'chucknorris' is a color</a> <span styl=\\r\\ne=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-r=\\r\\night:1px;\\\">//</span>stackoverflow <a style=3D\\\"text-decoration: none; color=\\r\\n: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/clic=\\r\\nk?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D24fa902082&e=3D765272fcdd\\\">comments<=\\r\\nspan style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n\\r\\n=09\\r\\n=09</td>\\r\\n      </tr>\\r\\n    </table>\\r\\n\\r\\n    <table id=3D\\\"footer\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\"=\\r\\n style=3D\\\"color: #7A7A7A;font-family: ubuntu=2C Helvetica=2C 'Lucida Grand=\\r\\ne'=2C Arial=2C sans-serif;margin-top: 30px;margin-bottom: 0;margin-right:=\\r\\n 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;background-color: #FFFFFF;max-width: 600px;width: 100%;margin: 0=\\r\\n;padding: 0;\\\">\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Luci=\\r\\nda Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-ri=\\r\\nght: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;pa=\\r\\ndding-left: 0;margin: 0;padding: 0;\\\">\\r\\n          <h5 style=3D\\\"font-family:Helvetica=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;margin-top:30px;margin-right:0;margin-left:0;padding-top:30=\\r\\npx;padding-bottom:0;padding-right:0;padding-left:0;color:#999;font-size:20=\\r\\npx;margin-bottom:5px;width:100%;\\\">__END__</h5>\\r\\n\\r\\n          <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Grande'=\\r\\n=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-right: 0;mar=\\r\\ngin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padding-left=\\r\\n: 0;color: #999;font-size: 14px;font-weight: normal;line-height: 22px;marg=\\r\\nin: 0 0 10px 0;padding: 0;width: 100%;\\\">\\r\\n            You're among 38=2C633 others who received this email because y=\\r\\nou wanted a weekly recap of the best articles from Hacker News. Published=\\r\\n by <a href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/click?u=\\r\\n=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D4ca9cd5366&e=3D765272fcdd\\\" style=3D\\\"col=\\r\\nor: #1786C1;text-decoration: underline;\\\">Curpress</a> from a smallish meta=\\r\\nl box at PO BOX 2621 Decatur=2C Georgia 30031. Hacker Newsletter is not af=\\r\\nfiliated with Y Combinator in any way.\\r\\n            <br><br>\\r\\n            You can <a href=3D\\\"http://hackernewsletter.us1.list-manage.com=\\r\\n/profile?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De505c88a2e&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color:#3B6B9B;text-decoration:underline;\\\">update your email</a> or <a=\\r\\n href=3D\\\"http://hackernewsletter.us1.list-manage.com/unsubscribe?u=3Dfaa8e=\\r\\nb4ef3a111cef92c4f3d4&id=3De505c88a2e&e=3D765272fcdd&c=3Da018682c80\\\" style=3D=\\r\\n\\\"color:#3B6B9B;text-decoration:underline;\\\">unsubscribe</a>.\\r\\n            <br><br>\\r\\n            Not a subscriber? Subscribe at <a style=3D\\\"color:#8793AF;text-=\\r\\ndecoration:underline;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com=\\r\\n/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D783f8c1fbc&e=3D765272fcdd=\\r\\n\\\">http://hackernewsletter.com</a>.\\r\\n            <br><br>\\r\\n            <a href=3D\\\"http://www.mailchimp.com/monkey-rewards/?utm_source=\\r\\n=3Dfreemium_newsletter&utm_medium=3Demail&utm_campaign=3Dmonkey_rewards&ai=\\r\\nd=3Dfaa8eb4ef3a111cef92c4f3d4&afl=3D1\\\"><img src=3D\\\"http://cdn-images.mailc=\\r\\nhimp.com/monkey_rewards/MC_MonkeyReward_07.png\\\" border=3D\\\"0\\\" alt=3D\\\"Email=\\r\\n Marketing Powered by MailChimp\\\" title=3D\\\"MailChimp Email Marketing\\\" width=\\r\\n=3D\\\"172\\\" height=3D\\\"56\\\"></a>\\r\\n          </p>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </table>\\r\\n  </div>\\r\\n<img src=3D\\\"http://hackernewsletter.us1.list-manage.com/track/open.php?u=\\r\\n=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da018682c80&e=3D765272fcdd\\\" height=3D\\\"1\\\"=\\r\\n width=3D\\\"1\\\"></body>\\r\\n</html>\"}},\"desiredParts\":[{\"id\":\"1\",\"encoding\":\"QUOTED-PRINTABLE\",\"mimetype\":\"text/plain\"},{\"id\":\"2\",\"encoding\":\"QUOTED-PRINTABLE\",\"mimetype\":\"text/html\"}],\"result\":{\"to\":[{\"name\":\"\",\"email\":\"Christine@spang.cc\"}],\"cc\":[],\"bcc\":[],\"from\":[{\"name\":\"Hacker Newsletter\",\"email\":\"kale@hackernewsletter.com\"}],\"replyTo\":[{\"name\":\"Hacker Newsletter\",\"email\":\"kale@hackernewsletter.com\"}],\"accountId\":\"test-account-id\",\"body\":\"<!DOCTYPE html>\\r\\n<html xmlns=3D\\\"http://www.w3.org/1999/xhtml\\\">\\r\\n<head><meta name=3D\\\"twitter:image:src\\\" content=3D\\\"https://gallery.mailchim=\\r\\np.com/faa8eb4ef3a111cef92c4f3d4/images/9d6a9334-af30-482f-a4e3-a6e52121c84=\\r\\n2.png\\\"><meta name=3D\\\"twitter:description\\\" content=3D\\\"A weekly newsletter o=\\r\\nf the best articles on startups=2C technology=2C programming=2C and more.=\\r\\n All links are curated by hand from the popular Hacker News site.\\\"><meta n=\\r\\name=3D\\\"twitter:title\\\" content=3D\\\"Hacker Newsletter #325\\\"><meta name=3D\\\"twi=\\r\\ntter:card\\\" content=3D\\\"summary_large_image\\\"><meta property=3D\\\"og:type\\\" cont=\\r\\nent=3D\\\"article\\\"><meta property=3D\\\"og:description\\\" content=3D\\\"A weekly news=\\r\\nletter of the best articles on startups=2C technology=2C programming=2C an=\\r\\nd more. All links are curated by hand from the popular Hacker News site.\\\">=\\r\\n<meta property=3D\\\"og:image\\\" content=3D\\\"https://gallery.mailchimp.com/faa8e=\\r\\nb4ef3a111cef92c4f3d4/images/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"><met=\\r\\na property=3D\\\"og:title\\\" content=3D\\\"Hacker Newsletter #325\\\"><meta property=\\r\\n=3D\\\"og:url\\\" content=3D\\\"http://eepurl.com/cncb2H\\\"><meta name=3D\\\"twitter:ima=\\r\\nge:src\\\" content=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111cef92c4f3d4=\\r\\n/images/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"><meta name=3D\\\"twitter:de=\\r\\nscription\\\" content=3D\\\"A weekly newsletter of the best articles on startups=\\r\\n=2C technology=2C programming=2C and more. All links are curated by hand f=\\r\\nrom the popular Hacker News site.\\\"><meta name=3D\\\"twitter:title\\\" content=3D=\\r\\n\\\"Hacker Newsletter #325\\\"><meta name=3D\\\"twitter:card\\\" content=3D\\\"summary_la=\\r\\nrge_image\\\"><meta property=3D\\\"og:type\\\" content=3D\\\"article\\\"><meta property=\\r\\n=3D\\\"og:description\\\" content=3D\\\"A weekly newsletter of the best articles on=\\r\\n startups=2C technology=2C programming=2C and more. All links are curated=\\r\\n by hand from the popular Hacker News site.\\\"><meta property=3D\\\"og:image\\\" c=\\r\\nontent=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111cef92c4f3d4/images/9=\\r\\nd6a9334-af30-482f-a4e3-a6e52121c842.png\\\"><meta property=3D\\\"og:title\\\" conte=\\r\\nnt=3D\\\"Hacker Newsletter #325\\\"><meta property=3D\\\"og:url\\\" content=3D\\\"http://=\\r\\neepurl.com/cncb2H\\\">\\r\\n  <title>Hacker Newsletter #325</title>\\r\\n  <meta http-equiv=3D\\\"Content-Type\\\" content=3D\\\"text/html; charset=3Dutf-8\\\"=\\r\\n>\\r\\n  <meta name=3D\\\"viewport\\\" id=3D\\\"viewport\\\" content=3D\\\"width=3Ddevice-width=\\r\\n=2Cminimum-scale=3D1.0=2Cmaximum-scale=3D10.0=2Cinitial-scale=3D1.0\\\">\\r\\n  <meta name=3D\\\"viewport\\\" id=3D\\\"viewport-iphone5\\\" content=3D\\\"initial-scale=\\r\\n=3D1.0=2Cuser-scalable=3Dno=2Cmaximum-scale=3D1\\\" media=3D\\\"(device-height:=\\r\\n 568px)\\\">\\r\\n  <style type=3D\\\"text/css\\\">\\r\\n    body { background: #FFF; }\\r\\n    body=2C h1=2C h2=2C h3=2C p=2C table=2C tr=2C td {\\r\\n      color: #333;\\r\\n      font-family: ubuntu=2C Helvetica=2C \\\"Lucida Grande\\\"=2C Arial=2C sans=\\r\\n-serif;\\r\\n      margin: 0;\\r\\n      padding: 0;\\r\\n    }\\r\\n    .main=2C table { background-color: #FFFFFF; }\\r\\n    a { color: #1786C1; text-decoration: underline; }\\r\\n    a:hover { text-decoration: none; }\\r\\n    h1 {\\r\\n      font-size: 35px;\\r\\n      margin-bottom: 5px;\\r\\n      padding-top: 10px;\\r\\n      width: 100%;\\r\\n    }\\r\\n    h1 a { color: #333; text-decoration: none; }\\r\\n    h1 a:hover { color: #666; text-decoration: underline; }\\r\\n    h2 {\\r\\n      color: #cc0000;\\r\\n      font-size: 16px;\\r\\n      font-weight: bold;\\r\\n      margin: 20px 0 10px;\\r\\n    }\\r\\n    h3 {\\r\\n      border-bottom: 1px solid #333;\\r\\n      border-top: 3px solid #333;\\r\\n      font-size: 16px;\\r\\n      font-weight: normal;\\r\\n      line-height: 22px;\\r\\n      margin: 20px 0;\\r\\n      padding: 20px 0;\\r\\n    }\\r\\n    p {\\r\\n      color: #333;\\r\\n      font-size: 16px;\\r\\n      line-height: 24px;\\r\\n      width: 500px;\\r\\n    }\\r\\n    p {\\r\\n      font-size: 16px;\\r\\n      line-height: 22px;\\r\\n      margin: 0 0 10px 0;\\r\\n      width: 100%;\\r\\n    }\\r\\n    table=2C .main=2C #header=2C #footer { max-width: 600px; width: 100%;=\\r\\n }\\r\\n    #header { margin-bottom: 20px; }\\r\\n    #issue {\\r\\n      color: #333;\\r\\n      font-family: ubuntu=2C Helvetica=2C \\\"Lucida Grande\\\"=2C Arial=2C sans=\\r\\n-serif;\\r\\n      font-size: 12px;\\r\\n      text-align: right;\\r\\n    }\\r\\n    #issue i { color: #999; font-style: normal; }\\r\\n\\r\\n    @media only screen and (max-device-width: 480px) {\\r\\n      table=2C #header=2C #content=2C #footer { padding: 0 20px !important=\\r\\n; }\\r\\n      h1 { font-size: 30px !important; }\\r\\n    }\\r\\n\\r\\n    @media only screen and (max-device-width: 679px)=2C only screen and (m=\\r\\nax-width: 679px) {\\r\\n      .main { margin: 20px auto; width: 100%; }\\r\\n      table=2C #header=2C #content=2C #footer { padding: 0 30px; width: 10=\\r\\n0%; }\\r\\n\\r\\n      #details { padding-right: 10px; }\\r\\n    }\\r\\n\\r\\n    @media only screen and (min-width: 680px) {\\r\\n      body { background: #999 url('http://hackernewsletter.com/images/debu=\\r\\nt_dark.png') 0 0 fixed repeat !important; }\\r\\n      #details { width: 496px; }\\r\\n      .main=2C table=2C #header=2C #content=2C #footer { background: #FFF=\\r\\n url('') 0 0 repeat; width: 600px; }\\r\\n\\r\\n      .main {\\r\\n                box-shadow: 0 0 12px rgba(0=2C 0=2C 0=2C .6);\\r\\n           -moz-box-shadow: 0 0 12px rgba(0=2C 0=2C 0=2C .6);\\r\\n        -webkit-box-shadow: 0 0 12px rgba(0=2C 0=2C 0=2C .6);\\r\\n        margin: 10px auto 40px;\\r\\n        padding: 30px 40px 10px;\\r\\n      }\\r\\n    }\\r\\n  </style>\\r\\n</head>\\r\\n<body style=3D\\\"-webkit-text-size-adjust: none;width: 100% !important;backg=\\r\\nround-color: #FFF;background-image: none;background-repeat: repeat;backgro=\\r\\nund-position: top left;background-attachment: scroll;color: #333;font-fami=\\r\\nly: ubuntu=2C Helvetica=2C 'Lucida Grande'=2C Arial=2C sans-serif;margin-t=\\r\\nop: 0;margin-bottom: 0;margin-right: 0;margin-left: 0;padding-top: 0;paddi=\\r\\nng-bottom: 0;padding-right: 0;padding-left: 0;background: #FFF;margin: 0;p=\\r\\nadding: 0;\\\"><div itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/EmailMessage=\\r\\n\\\"><div itemprop=3D\\\"publisher\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org=\\r\\n/Organization\\\"><meta itemprop=3D\\\"name\\\" content=3D\\\"Hacker Newsletter\\\"><link=\\r\\n itemprop=3D\\\"url\\\" content=3D\\\"http://www.hackernewsletter.com\\\"></div><div i=\\r\\ntemprop=3D\\\"about\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/Offer\\\"><lin=\\r\\nk itemprop=3D\\\"image\\\" href=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111c=\\r\\nef92c4f3d4/images/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"></div></div><d=\\r\\niv itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/EmailMessage\\\"><div itempro=\\r\\np=3D\\\"publisher\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/Organization\\\"=\\r\\n><meta itemprop=3D\\\"name\\\" content=3D\\\"Hacker Newsletter\\\"><link itemprop=3D\\\"u=\\r\\nrl\\\" content=3D\\\"http://www.hackernewsletter.com\\\"></div><div itemprop=3D\\\"abo=\\r\\nut\\\" itemscope=3D\\\"\\\" itemtype=3D\\\"http://schema.org/Offer\\\"><link itemprop=3D\\\"=\\r\\nimage\\\" href=3D\\\"https://gallery.mailchimp.com/faa8eb4ef3a111cef92c4f3d4/ima=\\r\\nges/9d6a9334-af30-482f-a4e3-a6e52121c842.png\\\"></div></div>\\r\\n  <div class=3D\\\"main\\\" style=3D\\\"background-color:#FFFFFF;max-width:600px;wi=\\r\\ndth:100%;\\\">\\r\\n\\r\\n    <table id=3D\\\"header\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\"=\\r\\n style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C &quot;Lucida Gra=\\r\\nnde&quot;=2C Arial=2C sans-serif;margin: 0;padding: 0;background-color: #F=\\r\\nFFFFF;max-width: 600px;width: 100%;margin-bottom: 20px;\\\">\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C &quot;L=\\r\\nucida Grande&quot;=2C Arial=2C sans-serif;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C &quot=\\r\\n;Lucida Grande&quot;=2C Arial=2C sans-serif;margin: 0;padding: 0;\\\">\\r\\n          <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Grande'=\\r\\n=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-right: 0;mar=\\r\\ngin-left: 0;padding-top: 0;padding-right: 0;padding-left: 0;color: #7B7B7B=\\r\\n;font-size: 12px;font-weight: normal;line-height: 22px;padding-bottom: 20p=\\r\\nx;margin: 0 0 10px 0;padding: 0;width: 100%;\\\">\\r\\n            The only thing more expensive than writing software is writing=\\r\\n bad software. //Alan Cooper\\r\\n          </p>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </table>\\r\\n\\r\\n    <table id=3D\\\"content\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\"=\\r\\n style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida Grande'=\\r\\n=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-right: 0;mar=\\r\\ngin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padding-left=\\r\\n: 0;background-color: #FFFFFF;max-width: 600px;width: 100%;margin: 0;paddi=\\r\\nng: 0;\\\">\\r\\n\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Luci=\\r\\nda Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-ri=\\r\\nght: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;pa=\\r\\ndding-left: 0;margin: 0;padding: 0;\\\">\\r\\n\\r\\n          <h1 style=3D\\\"color: #333;font-family: Helvetica=2C 'Lucida Grand=\\r\\ne'=2C Arial=2C sans-serif;margin-top: 0;margin-right: 0;margin-left: 0;pad=\\r\\nding-bottom: 0;padding-right: 0;padding-left: 0;margin-bottom: 0px;padding=\\r\\n-top: 20px;width: 100%;margin: 0;padding: 0;font-size: 35px;\\\">\\r\\n            <a href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D6277d2945c&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color: #333;text-decoration: none;\\\">hacker<span style=3D\\\"color:#ff3300=\\r\\n\\\">news</span>letter</a>\\r\\n          </h1>\\r\\n        </td>\\r\\n      </tr>\\r\\n\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td id=3D\\\"details\\\" style=3D\\\"color: #333;font-family: ubuntu=2C Hel=\\r\\nvetica=2C 'Lucida Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bott=\\r\\nom: 0;margin-right: 0;margin-left: 0;padding-top: 0;padding-bottom: 10px;p=\\r\\nadding-right: 0;padding-left: 0;margin: 0;padding: 0;\\\">\\r\\n          <span id=3D\\\"issue\\\" style=3D\\\"color:#333;font-family:ubuntu=2C Hel=\\r\\nvetica=2C 'Lucida Grande'=2C Arial=2C sans-serif;font-size:12px;text-align=\\r\\n:right;\\\">Issue #325 <i style=3D\\\"color:#999;font-style:normal;\\\">//</i> Nove=\\r\\nmber 04=2C 2016 <i style=3D\\\"color:#999;font-style:normal;\\\">//</i> <a href=\\r\\n=3D\\\"http://us1.campaign-archive2.com/?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da=\\r\\n018682c80&e=3D765272fcdd\\\" style=3D\\\"color:#3B6B9B;text-decoration:underline;\\\"=\\r\\n>View in your browser</a></span>\\r\\n        </td>\\r\\n      </tr>\\r\\n\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Luci=\\r\\nda Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-ri=\\r\\nght: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;pa=\\r\\ndding-left: 0;margin: 0;padding: 0;\\\">\\r\\n          <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=2C=\\r\\n sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;padding-l=\\r\\neft: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20px;m=\\r\\nargin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: uppercase=\\r\\n;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Spons=\\r\\nor</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n          <p style=3D\\\"padding-left: 10px;color: #333;font-family: ubuntu=\\r\\n=2C Helvetica=2C &quot;Lucida Grande&quot;=2C Arial=2C sans-serif;margin:=\\r\\n 0 0 10px 0;padding: 0;font-size: 16px;line-height: 22px;width: 100%;\\\">\\r\\n            <a href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/=\\r\\nclick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D223d78539f&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color: #1786C1;text-decoration: underline;\\\"><img style=3D\\\"width: 120px=\\r\\n; float: left; margin-right: 14px; margin-bottom: 16px; padding: 2px; bord=\\r\\ner: none; height: auto; line-height: 100%; outline: none; text-decoration:=\\r\\n none; display: inline;\\\" src=3D\\\"http://hackernewsletter.com/img/hired125.p=\\r\\nng\\\"></a>\\r\\n            <a href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df34c194567&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color: #3B6B9B; font-weight: bold; text-decoration: underline;\\\">Hired=\\r\\n - The End of Job Hunting As You Know It</a><br>Hired brings job offers to=\\r\\n you=2C so you can cut to the chase and focus on finding the right fit=2C=\\r\\n (not applying). Get multiple job offers and upfront compensation informat=\\r\\nion=2C with just one application. <a href=3D\\\"http://hackernewsletter.us1.l=\\r\\nist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D19969ea9d1&=\\r\\ne=3D765272fcdd\\\" style=3D\\\"color: #3B6B9B; font-weight: normal; text-decora=\\r\\ntion: underline;\\\">Get Hired</a>\\r\\n          </p>\\r\\n          <p style=3D\\\"clear: both;color: #333;font-family: ubuntu=2C Helve=\\r\\ntica=2C &quot;Lucida Grande&quot;=2C Arial=2C sans-serif;margin: 0 0 10px=\\r\\n 0;padding: 0;font-size: 16px;line-height: 22px;width: 100%;\\\"></p>\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Fa=\\r\\nvorites</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dbeb20e52d7&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 1066 Comments: 80\\\">Eve: Programming designed for hum=\\r\\nans</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"fon=\\r\\nt-size:11px;padding-right:1px;\\\">//</span>witheve <a style=3D\\\"text-decorati=\\r\\non: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage=\\r\\n2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D80976fd42f&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D5a32e61edc&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 803 Comments: 65\\\">Total Nightmare: USB-C and Thun=\\r\\nderbolt 3</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=\\r\\n=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>fosketts <a style=3D\\\"text-=\\r\\ndecoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.lis=\\r\\nt-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2978edafd1&e=\\r\\n=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></=\\r\\nspan></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D5604b993eb&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 548 Comments: 59\\\">Signs that a startup is focused=\\r\\n on stuff that doesn=E2=80=99t matter</a><br><span style=3D\\\"font-size: 13p=\\r\\nx; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span=\\r\\n>groovehq <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http=\\r\\n://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef9=\\r\\n2c4f3d4&id=3D1a0fd3ad47&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3=\\r\\n300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd56067cc6f&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 516 Comments: 152\\\">New MacBook Pro Is Not a Lapto=\\r\\np for Developers Anymore</a><br><span style=3D\\\"font-size: 13px; color: #77=\\r\\n7\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>blog <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9825=\\r\\n502db7&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</spa=\\r\\nn></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9fbe914f3f&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 473 Comments: 41\\\">Web fonts=2C boy=2C I don't kno=\\r\\nw</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-=\\r\\nsize:11px;padding-right:1px;\\\">//</span>meowni <a style=3D\\\"text-decoration:=\\r\\n none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.co=\\r\\nm/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De201ba68bb&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D70fe5044af&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 459 Comments: 38\\\">No One Saw Tesla=E2=80=99s Solar R=\\r\\noof Coming</a><br><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=\\r\\n=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>bloomberg <a style=3D\\\"text=\\r\\n-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.li=\\r\\nst-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df251daf9b8&e=\\r\\n=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></=\\r\\nspan></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D74433e7684&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 356 Comments: 15\\\">Ways Data Projects Fail</a><br><sp=\\r\\nan style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;pa=\\r\\ndding-right:1px;\\\">//</span>martingoodson <a style=3D\\\"text-decoration: none=\\r\\n; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/tra=\\r\\nck/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D08f9d94a7f&e=3D765272fcdd\\\">co=\\r\\nmments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D14431ecb68&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 310 Comments: 6\\\">A very valuable vulnerability</a><b=\\r\\nr><span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11=\\r\\npx;padding-right:1px;\\\">//</span>daemonology <a style=3D\\\"text-decoration: n=\\r\\none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/=\\r\\ntrack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D0888f32e26&e=3D765272fcdd\\\"=\\r\\n>comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da1165dabad&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 264 Comments: 27\\\">Making what people want isn=E2=80=\\r\\n=99t enough=2C you have to share it</a><br><span style=3D\\\"font-size: 13px;=\\r\\n color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>o=\\r\\nldgeekjobs <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"htt=\\r\\np://hackernewsletter.us1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111ce=\\r\\nf92c4f3d4&id=3D8597e2e648&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#f=\\r\\nf3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dbaa66ea75c&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 140 Comments: 10\\\">How ReadMe Went from SaaS to On=\\r\\n-Premises in Less Than One Week</a><br><span style=3D\\\"font-size: 13px; col=\\r\\nor: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>stack=\\r\\nshare <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://h=\\r\\nackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f=\\r\\n3d4&id=3D96eccec5b3&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;=\\r\\n\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>As=\\r\\nk HN</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D542f549d4b&e=\\r\\n=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-siz=\\r\\ne:17px;\\\" title=3D\\\"Votes: 520 Comments: 59\\\">Any other blind devs interested=\\r\\n in working on dev tools for the blind?</a></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #363636;font-size: 16px;line-height: 22px;marg=\\r\\nin-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;m=\\r\\nargin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list=\\r\\n-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9f4d11d0bf&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline; font-size:1=\\r\\n7px;\\\" title=3D\\\"Votes: 412 Comments: 37\\\">How to make a career working remot=\\r\\nely?</a></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Sh=\\r\\now HN</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D1fb70805ff&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 715 Comments: 136\\\">Music for Programming</a> <span style=3D\\\"font-size:=\\r\\n 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</=\\r\\nspan>musicforprogramming <a style=3D\\\"text-decoration: none; color: #336699=\\r\\n;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa=\\r\\n8eb4ef3a111cef92c4f3d4&id=3D3638b3dd13&e=3D765272fcdd\\\">comments<span styl=\\r\\ne=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dad530e4493&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 520 Comments: 44\\\">Powerwall 2 and Integrated Solar</a> <span style=3D\\\"=\\r\\nfont-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:=\\r\\n1px;\\\">//</span>tesla <a style=3D\\\"text-decoration: none; color: #336699;\\\" h=\\r\\nref=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4=\\r\\nef3a111cef92c4f3d4&id=3D2817f25e9a&e=3D765272fcdd\\\">comments<span style=3D=\\r\\n\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D89cf4973c3&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 354 Comments: 47\\\">Sonder E-Ink Keyboard</a> <span style=3D\\\"font-size: 1=\\r\\n3px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</sp=\\r\\nan>sonderdesign <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=\\r\\n=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3=\\r\\na111cef92c4f3d4&id=3D50f5bcc6e3&e=3D765272fcdd\\\">comments<span style=3D\\\"co=\\r\\nlor:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D565afeb0a6&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 296 Comments: 38\\\">Microsoft Teams=2C the new chat-based workspace in Of=\\r\\nfice 365</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"f=\\r\\nont-size:11px;padding-right:1px;\\\">//</span>office <a style=3D\\\"text-decorat=\\r\\nion: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manag=\\r\\ne.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D8b9ff7b22e&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D0ea8f9dd98&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 210 Comments: 28\\\">Flow =E2=80=93 Create automated workflows between yo=\\r\\nur favorite apps</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span st=\\r\\nyle=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>microsoft <a style=3D\\\"t=\\r\\next-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1=\\r\\n=2Elist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D60766b118=\\r\\n5&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a=\\r\\n></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da62f6fa376&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 198 Comments: 15\\\">Portier =E2=80=93 An email-based=2C passwordless aut=\\r\\nhentication service</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span=\\r\\n style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>github <a style=3D\\\"t=\\r\\next-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1=\\r\\n=2Elist-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Ddd5396574=\\r\\nf&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a=\\r\\n></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D986fb86342&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 103 Comments: 8\\\">Newly Redesigned Boston.gov Just Went Open Source</a>=\\r\\n <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11p=\\r\\nx;padding-right:1px;\\\">//</span>routefifty <a style=3D\\\"text-decoration: non=\\r\\ne; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/tr=\\r\\nack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2b0346293a&e=3D765272fcdd\\\">c=\\r\\nomments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd179182492&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 50 Comments: 8\\\">Sodaphonic =E2=80=93 record and edit audio in the brow=\\r\\nser</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-s=\\r\\nize:11px;padding-right:1px;\\\">//</span>sodaphonic <a style=3D\\\"text-decorati=\\r\\non: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage=\\r\\n2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2c7d5c0186&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Fe=\\r\\natured</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De3e6c607b2&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 209 Comments: 31\\\">Dear Matt Mullenweg: An Open Letter from Wix.com=E2=\\r\\n=80=99s CEO Avishai Abrahami</a> <span style=3D\\\"font-size: 13px; color: #7=\\r\\n77\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>wix <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D947=\\r\\ndb727e8&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</sp=\\r\\nan></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D9581130dbe&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 208 Comments: 24\\\">Open Letter to Tim Cook</a> <span style=3D\\\"font-size=\\r\\n: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//<=\\r\\n/span>petersphilo <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=\\r\\n=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef=\\r\\n3a111cef92c4f3d4&id=3Dcfe7f2db39&e=3D765272fcdd\\\">comments<span style=3D\\\"c=\\r\\nolor:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D95d0c443e9&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 104 Comments: 45\\\">Dear Microsoft</a> <span style=3D\\\"font-size: 13px; c=\\r\\nolor: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>sla=\\r\\nckhq <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://ha=\\r\\nckernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3=\\r\\nd4&id=3D09fc5ae3ef&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\"=\\r\\n>&rarr;</span></a></span></p>\\r\\n\\r\\n                 <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida G=\\r\\nrande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-rig=\\r\\nht: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin=\\r\\n-top: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;mar=\\r\\ngin: 0 0 10px 0;padding: 0;\\\">=E2=9D=97=EF=B8=8F<a href=3D\\\"http://hackernew=\\r\\nsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=\\r\\n=3Dc558014b74&e=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:unde=\\r\\nrline;\\\" title=3D\\\"Votes: 283 Comments: 45\\\">Slack may regret its letter to M=\\r\\nicrosoft</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"f=\\r\\nont-size:11px;padding-right:1px;\\\">//</span>theverge <a style=3D\\\"text-decor=\\r\\nation: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-man=\\r\\nage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df5d14c77d0&e=3D=\\r\\n765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span><=\\r\\n/p>\\r\\n\\r\\n\\r\\n\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Co=\\r\\nde</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D7d3d6ba2f8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 611 Comments: 29\\\">Darling =E2=80=93 MacOS translation layer for Linux</=\\r\\na> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:1=\\r\\n1px;padding-right:1px;\\\">//</span>darlinghq <a style=3D\\\"text-decoration: no=\\r\\nne; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/t=\\r\\nrack/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D997fe00b69&e=3D765272fcdd\\\">=\\r\\ncomments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D8b2df1b5cf&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 372 Comments: 37\\\">I don't understand Python's Asyncio</a> <span style=\\r\\n=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-ri=\\r\\nght:1px;\\\">//</span>pocoo <a style=3D\\\"text-decoration: none; color: #336699=\\r\\n;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa=\\r\\n8eb4ef3a111cef92c4f3d4&id=3Dcfa183b0b8&e=3D765272fcdd\\\">comments<span styl=\\r\\ne=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D92c0e76c15&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 363 Comments: 33\\\">Step-by-step tutorial to build a modern JavaScript st=\\r\\nack from scratch</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span st=\\r\\nyle=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>github <a style=3D\\\"text=\\r\\n-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.li=\\r\\nst-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D26b1cc8b9f&e=\\r\\n=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></=\\r\\nspan></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dcbe3a418d9&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 346 Comments: 18\\\">A fork of sudo with Touch ID support</a> <span style=\\r\\n=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-ri=\\r\\nght:1px;\\\">//</span>github <a style=3D\\\"text-decoration: none; color: #33669=\\r\\n9;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfa=\\r\\na8eb4ef3a111cef92c4f3d4&id=3D596564d3f7&e=3D765272fcdd\\\">comments<span sty=\\r\\nle=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dec79b70642&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 184 Comments: 44\\\">Writing more legible SQL</a> <span style=3D\\\"font-size=\\r\\n: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//<=\\r\\n/span>craigkerstiens <a style=3D\\\"text-decoration: none; color: #336699;\\\" h=\\r\\nref=3D\\\"http://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4=\\r\\nef3a111cef92c4f3d4&id=3D925e39bb0a&e=3D765272fcdd\\\">comments<span style=3D=\\r\\n\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd0faac4e0c&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 138 Comments: 1\\\">Lighthouse =E2=80=93 Auditing and Performance Metrics=\\r\\n for Progressive Web Apps</a> <span style=3D\\\"font-size: 13px; color: #777\\\"=\\r\\n><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>github <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D947a=\\r\\n25757f&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</spa=\\r\\nn></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D166fa21546&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 82 Comments: 8\\\">Bashcached =E2=80=93 memcached built on bash and ncat</=\\r\\na> <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:1=\\r\\n1px;padding-right:1px;\\\">//</span>github <a style=3D\\\"text-decoration: none;=\\r\\n color: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage2.com/tra=\\r\\nck/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D43e8ba70fc&e=3D765272fcdd\\\">co=\\r\\nmments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>De=\\r\\nsign</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D1648a4f0e2&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 345 Comments: 38\\\">Don=E2=80=99t go to art school (2013)</a> <span style=\\r\\n=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-ri=\\r\\nght:1px;\\\">//</span>medium <a style=3D\\\"text-decoration: none; color: #33669=\\r\\n9;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/click?u=3Df=\\r\\naa8eb4ef3a111cef92c4f3d4&id=3Dde6fa5997c&e=3D765272fcdd\\\">comments<span st=\\r\\nyle=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D4f708d14b8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 232 Comments: 12\\\">UX Myths (2014)</a> <span style=3D\\\"font-size: 13px; c=\\r\\nolor: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>uxm=\\r\\nyths <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://ha=\\r\\nckernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f=\\r\\n3d4&id=3D1076cd06b1&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;=\\r\\n\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Wa=\\r\\ntching</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Df339137326&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 274 Comments: 16\\\">Thomas Piketty=E2=80=99s Capital in the 21st Century=\\r\\n=2C in 20 minutes</a> <span style=3D\\\"font-size: 13px; color: #777\\\"><span s=\\r\\ntyle=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>boingboing <a style=3D=\\r\\n\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewsletter.u=\\r\\ns1.list-manage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D38cad1c=\\r\\n8ed&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</span><=\\r\\n/a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D1ad42efbec&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 190 Comments: 12\\\">Keep Ruby Weird Again</a> <span style=3D\\\"font-size: 1=\\r\\n3px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</sp=\\r\\nan>testdouble <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"=\\r\\nhttp://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111=\\r\\ncef92c4f3d4&id=3D75c2f8a66b&e=3D765272fcdd\\\">comments<span style=3D\\\"color:=\\r\\n#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage2.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dca25e48358&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vot=\\r\\nes: 69 Comments: 9\\\">Japan=E2=80=99s Disposable Workers: Dumping Ground</a>=\\r\\n <span style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11p=\\r\\nx;padding-right:1px;\\\">//</span>vimeo <a style=3D\\\"text-decoration: none; co=\\r\\nlor: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/c=\\r\\nlick?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Db2b2a1a5fa&e=3D765272fcdd\\\">commen=\\r\\nts<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Wo=\\r\\nrking</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\">=E2=9D=97=EF=B8=8F<a href=3D\\\"http://hackernews=\\r\\nletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D=\\r\\n6b31dbf058&e=3D765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underli=\\r\\nne;\\\" title=3D\\\"Votes: 1047 Comments: 50\\\">Ask HN: Who is firing?</a> <span s=\\r\\ntyle=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;paddin=\\r\\ng-right:1px;\\\">//</span>ycombinator</span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D74bcea07ef&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 730 Comments: 22\\\">Guide to Remote Work</a> <span style=3D\\\"font-size: 13=\\r\\npx; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</spa=\\r\\nn>zapier <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http:=\\r\\n//hackernewsletter.us1.list-manage1.com/track/click?u=3Dfaa8eb4ef3a111cef9=\\r\\n2c4f3d4&id=3D3016a20bad&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3=\\r\\n300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D65aa059ce8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 683 Comments: 821\\\">Ask HN: Who is hiring?</a> <span style=3D\\\"font-size:=\\r\\n 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</=\\r\\nspan>ycombinator</span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D31e85384f4&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 124 Comments: 221\\\">Ask HN: Who wants to be hired?</a> <span style=3D\\\"fo=\\r\\nnt-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1p=\\r\\nx;\\\">//</span>ycombinator</span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dcc1f34da72&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 76 Comments: 115\\\">Ask HN: Freelancer? Seeking freelancer?</a> <span sty=\\r\\nle=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-=\\r\\nright:1px;\\\">//</span>ycombinator</span></p>\\r\\n\\r\\n\\r\\n=09\\r\\n            <h2 style=3D\\\"font-family: ubuntu=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;padding-top: 20px;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;color: #ff3300;font-size: 16px;font-weight: bold;margin-top: 20p=\\r\\nx;margin-bottom: 3px;margin-right: 0;margin-left: 0;text-transform: upperc=\\r\\nase;margin: 20px 0 10px;padding: 0;\\\"><span style=3D\\\"color:#333\\\">#</span>Fu=\\r\\nn</h2>\\r\\n\\r\\n<hr style=3D\\\"border-style:none;margin-top:0px;margin-bottom:5px;margin-rig=\\r\\nht:0;margin-left:0;border-top-width:1px;border-top-style:solid;border-top-=\\r\\ncolor:#9b9b9b;/\\\">\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D89e4b080ef&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 510 Comments: 15\\\">Stealth Cell Tower Disguised as Printer</a> <span sty=\\r\\nle=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-=\\r\\nright:1px;\\\">//</span>julianoliver <a style=3D\\\"text-decoration: none; color=\\r\\n: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/cli=\\r\\nck?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D5a59366f95&e=3D765272fcdd\\\">comments=\\r\\n<span style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dba8d170a4d&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 443 Comments: 39\\\">Museu de la T=C3=A8cnica</a> <span style=3D\\\"font-size=\\r\\n: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-right:1px;\\\">//<=\\r\\n/span>twitter <a style=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"=\\r\\nhttp://hackernewsletter.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111=\\r\\ncef92c4f3d4&id=3Dd2f921f1eb&e=3D765272fcdd\\\">comments<span style=3D\\\"color:=\\r\\n#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dd576da7fa8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 249 Comments: 14\\\">Benjamin Button Reviews the New MacBook Pro</a> <span=\\r\\n style=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padd=\\r\\ning-right:1px;\\\">//</span>pinboard <a style=3D\\\"text-decoration: none; color=\\r\\n: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/clic=\\r\\nk?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Dcef41b1362&e=3D765272fcdd\\\">comments<=\\r\\nspan style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D34e4f4cbc8&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 150 Comments: 16\\\">A Gamer Spent 200 Hours Building an Incredibly Detail=\\r\\ned Digital San Francisco</a> <span style=3D\\\"font-size: 13px; color: #777\\\">=\\r\\n<span style=3D\\\"font-size:11px;padding-right:1px;\\\">//</span>citylab <a styl=\\r\\ne=3D\\\"text-decoration: none; color: #336699;\\\" href=3D\\\"http://hackernewslett=\\r\\ner.us1.list-manage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D2210=\\r\\n663ceb&e=3D765272fcdd\\\">comments<span style=3D\\\"color:#ff3300;\\\">&rarr;</spa=\\r\\nn></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n                <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Gr=\\r\\nande'=2C Arial=2C sans-serif;padding-top: 0;padding-bottom: 0;padding-righ=\\r\\nt: 0;padding-left: 0;color: #333;font-size: 16px;line-height: 22px;margin-=\\r\\ntop: 0;margin-bottom: 10px;margin-right: 0;margin-left: 0;width: 100%;marg=\\r\\nin: 0 0 10px 0;padding: 0;\\\"><a href=3D\\\"http://hackernewsletter.us1.list-ma=\\r\\nnage.com/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D10a776509b&e=3D=\\r\\n765272fcdd\\\" style=3D\\\"color:#0446AB;text-decoration:underline;\\\" title=3D\\\"Vote=\\r\\ns: 10 Comments: 2\\\">Why html thinks 'chucknorris' is a color</a> <span styl=\\r\\ne=3D\\\"font-size: 13px; color: #777\\\"><span style=3D\\\"font-size:11px;padding-r=\\r\\night:1px;\\\">//</span>stackoverflow <a style=3D\\\"text-decoration: none; color=\\r\\n: #336699;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com/track/clic=\\r\\nk?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D24fa902082&e=3D765272fcdd\\\">comments<=\\r\\nspan style=3D\\\"color:#ff3300;\\\">&rarr;</span></a></span></p>\\r\\n\\r\\n\\r\\n\\r\\n=09\\r\\n\\r\\n=09\\r\\n=09</td>\\r\\n      </tr>\\r\\n    </table>\\r\\n\\r\\n    <table id=3D\\\"footer\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\"=\\r\\n style=3D\\\"color: #7A7A7A;font-family: ubuntu=2C Helvetica=2C 'Lucida Grand=\\r\\ne'=2C Arial=2C sans-serif;margin-top: 30px;margin-bottom: 0;margin-right:=\\r\\n 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;paddin=\\r\\ng-left: 0;background-color: #FFFFFF;max-width: 600px;width: 100%;margin: 0=\\r\\n;padding: 0;\\\">\\r\\n      <tr style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Lucida=\\r\\n Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-righ=\\r\\nt: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padd=\\r\\ning-left: 0;margin: 0;padding: 0;\\\">\\r\\n        <td style=3D\\\"color: #333;font-family: ubuntu=2C Helvetica=2C 'Luci=\\r\\nda Grande'=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-ri=\\r\\nght: 0;margin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;pa=\\r\\ndding-left: 0;margin: 0;padding: 0;\\\">\\r\\n          <h5 style=3D\\\"font-family:Helvetica=2C 'Lucida Grande'=2C Arial=\\r\\n=2C sans-serif;margin-top:30px;margin-right:0;margin-left:0;padding-top:30=\\r\\npx;padding-bottom:0;padding-right:0;padding-left:0;color:#999;font-size:20=\\r\\npx;margin-bottom:5px;width:100%;\\\">__END__</h5>\\r\\n\\r\\n          <p style=3D\\\"font-family: ubuntu=2C Helvetica=2C 'Lucida Grande'=\\r\\n=2C Arial=2C sans-serif;margin-top: 0;margin-bottom: 0;margin-right: 0;mar=\\r\\ngin-left: 0;padding-top: 0;padding-bottom: 0;padding-right: 0;padding-left=\\r\\n: 0;color: #999;font-size: 14px;font-weight: normal;line-height: 22px;marg=\\r\\nin: 0 0 10px 0;padding: 0;width: 100%;\\\">\\r\\n            You're among 38=2C633 others who received this email because y=\\r\\nou wanted a weekly recap of the best articles from Hacker News. Published=\\r\\n by <a href=3D\\\"http://hackernewsletter.us1.list-manage1.com/track/click?u=\\r\\n=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D4ca9cd5366&e=3D765272fcdd\\\" style=3D\\\"col=\\r\\nor: #1786C1;text-decoration: underline;\\\">Curpress</a> from a smallish meta=\\r\\nl box at PO BOX 2621 Decatur=2C Georgia 30031. Hacker Newsletter is not af=\\r\\nfiliated with Y Combinator in any way.\\r\\n            <br><br>\\r\\n            You can <a href=3D\\\"http://hackernewsletter.us1.list-manage.com=\\r\\n/profile?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3De505c88a2e&e=3D765272fcdd\\\" style=\\r\\n=3D\\\"color:#3B6B9B;text-decoration:underline;\\\">update your email</a> or <a=\\r\\n href=3D\\\"http://hackernewsletter.us1.list-manage.com/unsubscribe?u=3Dfaa8e=\\r\\nb4ef3a111cef92c4f3d4&id=3De505c88a2e&e=3D765272fcdd&c=3Da018682c80\\\" style=3D=\\r\\n\\\"color:#3B6B9B;text-decoration:underline;\\\">unsubscribe</a>.\\r\\n            <br><br>\\r\\n            Not a subscriber? Subscribe at <a style=3D\\\"color:#8793AF;text-=\\r\\ndecoration:underline;\\\" href=3D\\\"http://hackernewsletter.us1.list-manage.com=\\r\\n/track/click?u=3Dfaa8eb4ef3a111cef92c4f3d4&id=3D783f8c1fbc&e=3D765272fcdd=\\r\\n\\\">http://hackernewsletter.com</a>.\\r\\n            <br><br>\\r\\n            <a href=3D\\\"http://www.mailchimp.com/monkey-rewards/?utm_source=\\r\\n=3Dfreemium_newsletter&utm_medium=3Demail&utm_campaign=3Dmonkey_rewards&ai=\\r\\nd=3Dfaa8eb4ef3a111cef92c4f3d4&afl=3D1\\\"><img src=3D\\\"http://cdn-images.mailc=\\r\\nhimp.com/monkey_rewards/MC_MonkeyReward_07.png\\\" border=3D\\\"0\\\" alt=3D\\\"Email=\\r\\n Marketing Powered by MailChimp\\\" title=3D\\\"MailChimp Email Marketing\\\" width=\\r\\n=3D\\\"172\\\" height=3D\\\"56\\\"></a>\\r\\n          </p>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </table>\\r\\n  </div>\\r\\n<img src=3D\\\"http://hackernewsletter.us1.list-manage.com/track/open.php?u=\\r\\n=3Dfaa8eb4ef3a111cef92c4f3d4&id=3Da018682c80&e=3D765272fcdd\\\" height=3D\\\"1\\\"=\\r\\n width=3D\\\"1\\\"></body>\\r\\n</html>\",\"snippet\":\"= The only thing more expensive than writing software is writing= bad software. //Alan Cooper hacker\",\"unread\":true,\"starred\":false,\"date\":\"Fri, 4 Nov 2016 13:33:01 +0000\",\"folderImapUID\":344200,\"folderId\":\"test-folder-id\",\"folder\":{\"id\":\"test-folder-id\",\"account_id\":\"test-account-id\",\"object\":\"folder\",\"name\":null,\"display_name\":\"Test Folder\",\"sync_state\":{}},\"labels\":[],\"headers\":{\"delivered-to\":[\"christine@spang.cc\"],\"received\":[\"by 10.31.236.3 with SMTP id k3csp257576vkh; Fri, 4 Nov 2016 06:33:12 -0700 (PDT)\",\"from mail250.atl61.mcsv.net (mail250.atl61.mcsv.net. [205.201.135.250]) by mx.google.com with ESMTP id f11si7594937qte.84.2016.11.04.06.33.10 for <Christine@spang.cc>; Fri, 04 Nov 2016 06:33:12 -0700 (PDT)\",\"from (127.0.0.1) by mail250.atl61.mcsv.net id h3i71e174acc for <Christine@spang.cc>; Fri, 4 Nov 2016 13:33:01 +0000 (envelope-from <bounce-mc.us1_1832689.2559073-Christine=spang.cc@mail250.atl61.mcsv.net>)\"],\"x-received\":[\"by 10.55.207.210 with SMTP id v79mr12608992qkl.199.1478266392152; Fri, 04 Nov 2016 06:33:12 -0700 (PDT)\"],\"return-path\":[\"<bounce-mc.us1_1832689.2559073-Christine=spang.cc@mail250.atl61.mcsv.net>\"],\"received-spf\":[\"pass (google.com: domain of bounce-mc.us1_1832689.2559073-christine=spang.cc@mail250.atl61.mcsv.net designates 205.201.135.250 as permitted sender) client-ip=205.201.135.250;\"],\"authentication-results\":[\"mx.google.com; dkim=pass header.i=@hackernewsletter.com; spf=pass (google.com: domain of bounce-mc.us1_1832689.2559073-christine=spang.cc@mail250.atl61.mcsv.net designates 205.201.135.250 as permitted sender) smtp.mailfrom=bounce-mc.us1_1832689.2559073-Christine=spang.cc@mail250.atl61.mcsv.net\"],\"dkim-signature\":[\"v=1; a=rsa-sha1; c=relaxed/relaxed; s=k1; d=hackernewsletter.com; h=Subject:From:Reply-To:To:Date:Message-ID:List-ID:List-Unsubscribe:Content-Type:MIME-Version; i=kale@hackernewsletter.com; bh=vZB3PnpQdFA+LVdXRQIdK2lMkvI=; b=RuL/qDIhe22oioO4rRO81v3j4nGp4VXHbJILx7VRSkXQ6yFLcyEPISC1+l3WaAEyBeyBU5lTNvvr 6KdJ0UzAYt854dXg0fefITQUDJmKEZS+wGWsJtvSK380WU3M5V6rQXWodFYBmvZmJXJH0oqh5TMo Iwel9pTPpzKCVGtG3L4=\"],\"subject\":[\"Hacker Newsletter #325\"],\"from\":[\"Hacker Newsletter <kale@hackernewsletter.com>\"],\"reply-to\":[\"Hacker Newsletter <kale@hackernewsletter.com>\"],\"to\":[\"<Christine@spang.cc>\"],\"date\":[\"Fri, 4 Nov 2016 13:33:01 +0000\"],\"message-id\":[\"<faa8eb4ef3a111cef92c4f3d4765272fcdd.20161104133210@mail250.atl61.mcsv.net>\"],\"x-mailer\":[\"MailChimp Mailer - **CIDa018682c80765272fcdd**\"],\"x-campaign\":[\"mailchimpfaa8eb4ef3a111cef92c4f3d4.a018682c80\"],\"x-campaignid\":[\"mailchimpfaa8eb4ef3a111cef92c4f3d4.a018682c80\"],\"x-report-abuse\":[\"Please report abuse for this campaign here: http://www.mailchimp.com/abuse/abuse.phtml?u=faa8eb4ef3a111cef92c4f3d4&id=a018682c80&e=765272fcdd\"],\"x-mc-user\":[\"faa8eb4ef3a111cef92c4f3d4\"],\"x-feedback-id\":[\"1832689:1832689.2559073:us1:mc\"],\"list-id\":[\"faa8eb4ef3a111cef92c4f3d4mc list <faa8eb4ef3a111cef92c4f3d4.583821.list-id.mcsv.net>\"],\"x-accounttype\":[\"pr\"],\"list-unsubscribe\":[\"<mailto:unsubscribe-mc.us1_faa8eb4ef3a111cef92c4f3d4.a018682c80-765272fcdd@mailin1.us2.mcsv.net?subject=unsubscribe>, <http://hackernewsletter.us1.list-manage.com/unsubscribe?u=faa8eb4ef3a111cef92c4f3d4&id=e505c88a2e&e=765272fcdd&c=a018682c80>\"],\"x-mcda\":[\"FALSE\"],\"content-type\":[\"multipart/alternative; boundary=\\\"_----------=_MCPart_776050397\\\"\"],\"mime-version\":[\"1.0\"],\"x-gm-thrid\":\"1550074660376052354\",\"x-gm-msgid\":\"1550074660376052354\",\"x-gm-labels\":[\"\\\\Important\"]},\"headerMessageId\":\"<faa8eb4ef3a111cef92c4f3d4765272fcdd.20161104133210@mail250.atl61.mcsv.net>\",\"gMsgId\":\"1550074660376052354\",\"subject\":\"Hacker Newsletter #325\",\"id\":\"e2bfb681b46a82c882f5ba94580c13240df5b9223bcc5853f6b71986ac1bad2c\",\"folderImapXGMLabels\":\"[\\\"\\\\\\\\Important\\\"]\"}}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseFromImap/mileageplus-mime-html-only.json",
    "content": "{\"imapMessage\":{\"attributes\":{\"struct\":[{\"partID\":\"1\",\"type\":\"text\",\"subtype\":\"html\",\"params\":{\"charset\":\"UTF-8\"},\"id\":null,\"description\":null,\"encoding\":\"QUOTED-PRINTABLE\",\"size\":63607,\"lines\":1555,\"md5\":null,\"disposition\":null,\"language\":null}],\"date\":\"2016-11-16T20:22:55.000Z\",\"flags\":[],\"uid\":346225,\"modseq\":\"8197177\",\"x-gm-labels\":[],\"x-gm-msgid\":\"1551187601250126809\",\"x-gm-thrid\":\"1551187601250126809\"},\"headers\":\"Delivered-To: christine@spang.cc\\r\\nReceived: by 10.31.185.141 with SMTP id j135csp32042vkf; Wed, 16 Nov 2016\\r\\n 12:22:55 -0800 (PST)\\r\\nX-Received: by 10.129.160.81 with SMTP id x78mr4860400ywg.273.1479327775749;\\r\\n Wed, 16 Nov 2016 12:22:55 -0800 (PST)\\r\\nReturn-Path: <united.5765@envfrm.rsys2.com>\\r\\nReceived: from omp.news.united.com (omp.news.united.com. [12.130.136.195]) by\\r\\n mx.google.com with ESMTP id e80si8263532ywa.331.2016.11.16.12.22.55 for\\r\\n <christine@spang.cc>; Wed, 16 Nov 2016 12:22:55 -0800 (PST)\\r\\nReceived-SPF: pass (google.com: domain of united.5765@envfrm.rsys2.com\\r\\n designates 12.130.136.195 as permitted sender) client-ip=12.130.136.195;\\r\\nAuthentication-Results: mx.google.com; dkim=pass header.i=@news.united.com;\\r\\n spf=pass (google.com: domain of united.5765@envfrm.rsys2.com designates\\r\\n 12.130.136.195 as permitted sender)\\r\\n smtp.mailfrom=united.5765@envfrm.rsys2.com; dmarc=pass (p=REJECT dis=NONE)\\r\\n header.from=news.united.com\\r\\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=united;\\r\\n d=news.united.com;\\r\\n h=MIME-Version:Content-Type:Content-Transfer-Encoding:Date:To:From:Reply-To:Subject:List-Unsubscribe:Message-ID;\\r\\n i=MileagePlus_Partner@news.united.com; bh=oiP9wNJbkuGtDmX9JXmAjTpQYe4=;\\r\\n b=lWhKDlwoeUSLppBUyzjcmkSvlgQys/kL+1R6BJEllHgaawrH/c2sBjY0NAAsZ4GPPUB/rF4h58NO\\r\\n FHBElr/V0H/k4rkQmSrzudpLfIElGb0WN2etlGZeO0qhMmtNvwmbhw7QO5uZu+x6sKMVutOFxmpa\\r\\n 6oPStO1uVojaiyQhVTA=\\r\\nDomainKey-Signature: a=rsa-sha1; c=nofws; q=dns; s=united; d=news.united.com;\\r\\n b=ZkKrC8ZdJvofP8CEVdEeIkv3UmDibivko/0dxilZkSYfk8sPe/o2YR+zo0VqA9kr1o07ORe3dcxv\\r\\n Nz0E0TUCcv4YapXs9qxlN8Bm/Zz8PY8D572GBMV0T34PZ6+5v3ai57LtfUBpPy93fjcRTgNHJqQl\\r\\n HmQTBlZ3wUHv1TBGOqI=;\\r\\nReceived: by omp.news.united.com id h5j01k161o48 for <christine@spang.cc>;\\r\\n Wed, 16 Nov 2016 12:22:49 -0800 (envelope-from\\r\\n <united.5765@envfrm.rsys2.com>)\\r\\nX-CSA-Complaints: whitelist-complaints@eco.de\\r\\nReceived: by omp.news.united.com id h5j01i161o4e for <christine@spang.cc>;\\r\\n Wed, 16 Nov 2016 12:22:48 -0800 (envelope-from\\r\\n <united.5765@envfrm.rsys2.com>)\\r\\nX-CSA-Complaints: whitelist-complaints@eco.de\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/html; charset=\\\"UTF-8\\\"\\r\\nContent-Transfer-Encoding: quoted-printable\\r\\nDate: Wed, 16 Nov 2016 12:22:53 -0800\\r\\nTo: christine@spang.cc\\r\\nFrom: \\\"MileagePlus Explorer Card\\\" <MileagePlus_Partner@news.united.com>\\r\\nReply-To: \\\"MileagePlus Explorer Card\\\" <MileagePlus_NoReply@united.com>\\r\\nSubject: Free checked bag for you and one companion when you use your Card\\r\\nFeedback-ID: 5765:1192222:oraclersys\\r\\nList-Unsubscribe: <https://news.united.com/pub/optout/UnsubscribeOneStepConfirmAction?YES=true&_ri_=X0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCof&_ei_=Ejaf7_rRKzx0ctn6G-vCRQxQFpOo1jnhz_D7Qj8uilahE5HqTLtnYXpmd53bnyOwa5G4ok2G4SqEqZyK_i5MlB4glRW1lFv87L9Bq3zhUkhE6W-I9Lls8knlgu8Rrb4Dorss_mjDo7FeWQ-JARWOws8jiyPdFbxa>,\\r\\n <mailto:unsubscribe-YQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCof@imh.rsys2.com?subject=List-Unsubscribe>\\r\\nX-sgxh1: JojpklpgLxkiHgnQJJ\\r\\nX-rext: 5.interact2.EonqtlMeFYuUCgf5wg26BVPnIWv1eCI0ufmo8-nj-fqdCJCz5Wz8hM\\r\\nX-cid: united.3098382\\r\\nMessage-ID: <0.0.AC.822.1D2404732E8AF0C.0@omp.news.united.com>\\r\\n\\r\\n\",\"parts\":{\"1\":\"<html>\\r\\n<head>\\r\\n<meta charset=3D=22UTF-8=22>\\r\\n<meta name=3D=22viewport=22 content=3D=22width=3Ddevice-width, minimum-scal=\\r\\ne=3D1=2E0, maximum-scale=3D1=2E0, initial-scale=3D1=2E0=22>\\r\\n<title>United Airlines - United MileagePlus</title>\\r\\n<style type=3D=22text/css=22>\\r\\nhtml =7B\\r\\n\\t-webkit-text-size-adjust: none;\\r\\n=7D\\r\\n=2EReadMsgBody =7B\\r\\n\\twidth: 100%;\\r\\n=7D\\r\\n=2EExternalClass =7B\\r\\n\\twidth: 100%;\\r\\n=7D\\r\\nbody =7B\\r\\n\\tpadding: 0;\\r\\n\\tmargin: 0;\\r\\n\\twidth: 100%;\\r\\n\\tmargin: 0 auto;\\r\\n\\tbackground-color: =23e6e6e6;\\r\\n=7D\\r\\n/* CSS for hiding content in desktop/webmail clients */\\r\\n=2Eimghide =7B\\r\\n\\tmax-height: 0px;\\r\\n\\tfont-size: 0;\\r\\n\\tdisplay: none;\\r\\n\\toverflow: hidden;\\r\\n=7D\\r\\n\\r\\n=40media only screen and (max-width: 480px) =7B\\r\\n/* Media query for displaying content in mobile email clients */\\r\\n=2Eimghide =7B\\r\\n\\tmax-height: none =21important;\\r\\n\\tfont-size: 12px =21important;\\r\\n\\tdisplay: block =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Efl =7B\\r\\n\\tfloat: left =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eclear =7B\\r\\n\\tclear: both;\\r\\n=21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Efr =7B\\r\\n\\tfloat: right =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewrap =7B\\r\\n\\tdisplay: block =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehide =7B\\r\\n\\tdisplay: none =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehideflow =7B\\r\\n\\toverflow-x: hidden =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22collapse10=22=5D =7B\\r\\n\\twidth: 10px =21important;\\r\\n\\theight: 15px =21important;\\r\\n=7D\\r\\n/* WIDTHS */\\r\\n*=5Bclass=5D=2Ewidth100pc =7B\\r\\n\\twidth: 100% =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth0 =7B\\r\\n\\twidth: 0px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth1 =7B\\r\\n\\twidth: 1px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth2 =7B\\r\\n\\twidth: 2px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth3 =7B\\r\\n\\twidth: 3px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth4 =7B\\r\\n\\twidth: 4px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth5 =7B\\r\\n\\twidth: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth6 =7B\\r\\n\\twidth: 6px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth7 =7B\\r\\n\\twidth: 7px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth8 =7B\\r\\n\\twidth: 8px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth9 =7B\\r\\n\\twidth: 9px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth10 =7B\\r\\n\\twidth: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth15 =7B\\r\\n\\twidth: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth19 =7B\\r\\n\\twidth: 19px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth20 =7B\\r\\n\\twidth: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth25 =7B\\r\\n\\twidth: 25px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth42 =7B\\r\\n\\twidth: 42px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth48 =7B\\r\\n\\twidth: 48px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth58 =7B\\r\\n\\twidth: 58px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth63 =7B\\r\\n\\twidth: 63px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth64 =7B\\r\\n\\twidth: 64px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth82 =7B\\r\\n\\twidth: 82px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth90px =7B\\r\\n\\twidth: 90px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth95px =7B\\r\\n\\twidth: 95px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth100px =7B\\r\\n\\twidth: 100px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth104 =7B\\r\\n\\twidth: 104px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth119 =7B\\r\\n\\twidth: 119px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth120 =7B\\r\\n\\twidth: 120px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth154 =7B\\r\\n\\twidth: 154px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth181 =7B\\r\\n\\twidth: 181px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth185 =7B\\r\\n\\twidth: 185px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth216 =7B\\r\\n\\twidth: 100px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth248 =7B\\r\\n\\twidth: 248px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth260 =7B\\r\\n\\twidth: 260px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth261 =7B\\r\\n\\twidth: 261px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth275 =7B\\r\\n\\twidth: 275px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth278 =7B\\r\\n\\twidth: 278px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth280 =7B\\r\\n\\twidth: 280px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth290 =7B\\r\\n\\twidth: 290px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth295 =7B\\r\\n\\twidth: 295px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth300 =7B\\r\\n\\twidth: 300px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth302 =7B\\r\\n\\twidth: 302px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth320 =7B\\r\\n\\twidth: 320px =21important;\\r\\n=7D\\r\\n/* HEIGHTS */\\r\\n*=5Bclass=5D=2Eheight2 =7B\\r\\n\\theight: 2px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight4 =7B\\r\\n\\theight: 4px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight5 =7B\\r\\n\\theight: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight9 =7B\\r\\n\\theight: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight10 =7B\\r\\n\\theight: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight15 =7B\\r\\n\\theight: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight30 =7B\\r\\n\\theight: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight36 =7B\\r\\n\\theight: 36px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight37 =7B\\r\\n\\theight: 37px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight42 =7B\\r\\n\\theight: 42px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight43 =7B\\r\\n\\theight: 43px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight48 =7B\\r\\n\\theight: 48px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight50 =7B\\r\\n\\theight: 50px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight52 =7B\\r\\n\\theight: 52px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight58 =7B\\r\\n\\theight: 58px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight60 =7B\\r\\n\\theight: 60px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight65 =7B\\r\\n\\theight: 65px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight73 =7B\\r\\n\\theight: 73px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight76 =7B\\r\\n\\theight: 76px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight84 =7B\\r\\n\\theight: 84px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight89 =7B\\r\\n\\theight: 89px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight90 =7B\\r\\n\\theight: 90px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight93 =7B\\r\\n\\theight: 93px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight95 =7B\\r\\n\\theight: 95px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight100 =7B\\r\\n\\theight: 100px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight104 =7B\\r\\n\\theight: 104px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight107 =7B\\r\\n\\theight: 107px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight110 =7B\\r\\n\\theight: 110px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight112 =7B\\r\\n\\theight: 112px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight116 =7B\\r\\n\\theight: 116px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight118 =7B\\r\\n\\theight: 118px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight120 =7B\\r\\n\\theight: 120px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight122 =7B\\r\\n\\theight: 122px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight123 =7B\\r\\n\\theight: 123px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight125 =7B\\r\\n\\theight: 125px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight127 =7B\\r\\n\\theight: 127px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight135 =7B\\r\\n\\theight: 135px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight138 =7B\\r\\n\\theight: 138px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight140 =7B\\r\\n\\theight: 140px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight142 =7B\\r\\n\\theight: 142px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight144 =7B\\r\\n\\theight: 144px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight156 =7B\\r\\n\\theight: 156px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight161 =7B\\r\\n\\theight: 161px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight166 =7B\\r\\n\\theight: 166px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight174 =7B\\r\\n\\theight: 174px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight175 =7B\\r\\n\\theight: 175px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight176 =7B\\r\\n\\theight: 176px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight182 =7B\\r\\n\\theight: 182px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight185 =7B\\r\\n\\theight: 185px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight195 =7B\\r\\n\\theight: 195px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight200 =7B\\r\\n\\theight: 200px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight203 =7B\\r\\n\\theight: 203px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight208 =7B\\r\\n\\theight: 208px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight211 =7B\\r\\n\\theight: 211px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight212 =7B\\r\\n\\theight: 212px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight213 =7B\\r\\n\\theight: 213px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight215 =7B\\r\\n\\theight: 215px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight223 =7B\\r\\n\\theight: 223px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight240 =7B\\r\\n\\theight: 240px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight247 =7B\\r\\n\\theight: 247px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight249 =7B\\r\\n\\theight: 249px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight256 =7B\\r\\n\\theight: 256px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight260 =7B\\r\\n\\theight: 260px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight266 =7B\\r\\n\\theight: 266px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight288 =7B\\r\\n\\theight: 288px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight295 =7B\\r\\n\\theight: 295px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight579 =7B\\r\\n\\theight: 579px =21important;\\r\\n=7D\\r\\n/* MARGINS */\\r\\n*=5Bclass=5D=2Ecenter =7B\\r\\n\\ttext-align: center =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ecentercenter =7B\\r\\n\\tmargin-left: auto=21important;\\r\\n\\tmargin-right: auto =21important;\\r\\n\\tpadding: 0 =21important;\\r\\n\\tfloat: none =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebottom10 =7B\\r\\n\\tmargin-bottom: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ETB10 =7B\\r\\n\\tpadding-top: 10px =21important;\\r\\n\\tpadding-bottom: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELR5 =7B\\r\\n\\tpadding-left: 5px =21important;\\r\\n\\tpadding-right: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad5 =7B\\r\\n\\tpadding-left: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad10 =7B\\r\\n\\tpadding-left: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad15 =7B\\r\\n\\tpadding-left: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad20 =7B\\r\\n\\tpadding-left: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad30 =7B\\r\\n\\tpadding-left: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad0 =7B\\r\\n\\tpadding-left: 0px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad0 =7B\\r\\n\\tpadding-right: 0px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad5 =7B\\r\\n\\tpadding-right: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad10 =7B\\r\\n\\tpadding-right: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad15 =7B\\r\\n\\tpadding-right: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad20 =7B\\r\\n\\tpadding-right: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad30 =7B\\r\\n\\tpadding-right: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad35 =7B\\r\\n\\tpadding-right: 35px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Epaddingtop5 =7B\\r\\n\\tpadding-top: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Epaddingtop10 =7B\\r\\n\\tpadding-top: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etoppad10 =7B\\r\\n\\tpadding-top: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etoppad20 =7B\\r\\n\\tpadding-top: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etoppad30 =7B\\r\\n\\tpadding-top: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebottompad20 =7B\\r\\n\\tpadding-bottom: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebottompad10 =7B\\r\\n\\tpadding-bottom: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftmargin15 =7B\\r\\n\\tmargin-left: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etopmargin10 =7B\\r\\n\\tmargin-top: 10px =21important;\\r\\n=7D\\r\\n/* POSTITIONING */\\r\\n*=5Bclass=5D=2Elefty =7B\\r\\n\\tposition: relative =21important;\\r\\n\\tleft: -320px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etextleft =7B\\r\\n\\ttext-align: left =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Evmiddle =7B\\r\\n\\tvertical-align: middle =21important;\\r\\n=7D\\r\\n/* FONTS */\\r\\n*=5Bclass=5D=2EFS11 =7B\\r\\n\\tfont-size: 11px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS12 =7B\\r\\n\\tfont-size: 12px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS13 =7B\\r\\n\\tfont-size: 13px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS16 =7B\\r\\n\\tfont-size: 16px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS22 =7B\\r\\n\\tfont-size: 22px =21important;\\r\\n=7D\\r\\n/* LINE HEIGHTS */\\r\\n*=5Bclass=5D=2ELH13 =7B\\r\\n\\tline-height: 13px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELH17 =7B\\r\\n\\tline-height: 17px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELH18 =7B\\r\\n\\tline-height: 18px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELH24 =7B\\r\\n\\tline-height: 24px =21important;\\r\\n=7D\\r\\n/* COLOR */\\r\\n*=5Bclass=5D=2EbgFFF =7B\\r\\n\\tbackground-color: =23FFFFFF =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebg888 =7B\\r\\n\\tbackground-color: =23888888 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EfcFFF =7B\\r\\n\\tcolor: =23FFFFFF =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebgedb72b =7B\\r\\n\\tbackground-color: =23edb72b =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EbgAAA =7B\\r\\n\\tbackground-color: =23aaaaaa =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebg039 =7B\\r\\n\\tbackground-color: =23003399 =21important;\\r\\n=7D\\r\\n/* FONT STYLES */\\r\\n*=5Bclass=5D=2EFWbold =7B\\r\\n\\tfont-weight: bold =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etdnone =7B\\r\\n\\ttext-decoration: none =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Enavlink:after =7B\\r\\n\\tcontent: =22 =7C=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eorgin:after =7B\\r\\n\\tcontent: =22/Destination=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehotel:after =7B\\r\\n\\tcontent: =22/Car=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehotel2:after =7B\\r\\n\\tcontent: =22Hotel=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ecar:after =7B\\r\\n\\tcontent: =22Car=22 =21important;\\r\\n=7D\\r\\ntable =7B\\r\\n\\tborder-spacing:0px =21important;\\r\\n\\tborder-collapse:collapse =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dwidth100=5D =7B\\r\\n\\tfloat: none =21important;\\r\\n\\twidth: 100% =21important;\\r\\n\\theight: auto =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dwidth95=5D =7B\\r\\n\\tfloat: none =21important;\\r\\n\\twidth: 95% =21important;\\r\\n\\theight: auto =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dwidth90=5D =7B\\r\\n\\tfloat: none =21important;\\r\\n\\twidth: 90% =21important;\\r\\n\\theight: auto =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dspace25px=5D =7B\\r\\n\\tdisplay: block =21important;\\r\\n\\theight: 25px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22383=22=5D, *=5Bwidth=3D=22343=22=5D, *=5Bwidth=3D=22230=22=\\r\\n=5D =7B\\r\\n\\twidth:300px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22620=22=5D =7B\\r\\n\\twidth:290px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22492=22=5D =7B\\r\\n\\twidth:170px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=2220=22=5D =7B\\r\\n\\tdisplay:block =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22300px_width=22=5D =7B\\r\\n\\twidth:300px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22320px_width=22=5D =7B\\r\\n\\twidth:320px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=228px_height=22=5D =7B\\r\\n\\tfont-size:8px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=2230px_height=22=5D =7B\\r\\n\\tfont-size:30px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22hide=22=5D =7B\\r\\n\\tdisplay:none =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22mp_number_wrapper=22=5D =7B\\r\\n\\twidth:320px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22mp_number=22=5D =7B\\r\\n\\tfont-size:11px =21important;\\r\\n\\twidth:300px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22margin_right_20=22=5D =7B\\r\\n\\tmargin-right:20px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22594=22=5D, *=5Bwidth=3D=22306=22=5D, *=5Bwidth=3D=22320=22=\\r\\n=5D =7B\\r\\n\\twidth:280px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22collapse10=22=5D =7B\\r\\n\\twidth:10px =21important;\\r\\n\\theight:15px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22unHide=22=5D =7B\\r\\n\\tvisibility:visible =21important;\\r\\n\\tdisplay:block =21important;\\r\\n\\twidth:300px =21important;\\r\\n\\theight:auto =21important;\\r\\n\\toverflow:visible =21important;\\r\\n\\tclear:both =21important;\\r\\n\\tvisibility:visible =21important;\\r\\n\\tline-height:normal =21important;\\r\\n\\tfont-size:inherit =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22headline=22=5D =7B\\r\\n\\ttext-align:center =21important;\\r\\n\\twidth:280px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBorder=22=5D =7B\\r\\n\\tvisibility:visible =21important;\\r\\n\\tdisplay:block =21important;\\r\\n\\twidth:10px =21important;\\r\\n\\theight:auto =21important;\\r\\n\\toverflow:visible =21important;\\r\\n\\tclear:both =21important;\\r\\n\\tvisibility:visible =21important;\\r\\n\\tline-height:normal =21important;\\r\\n\\tfont-size:inherit =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBoderColor1=22=5D =7B\\r\\n\\tbackground:=23002859 =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBoderColor2=22=5D =7B\\r\\n\\tbackground:=23394a58 =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBoderColor3=22=5D =7B\\r\\n\\tbackground:=23015282 =21important;\\r\\n=7D\\r\\n=23CTA =7B\\r\\n\\tmargin:auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3DCTA=5D =7B\\r\\n\\twidth: 80% =21important;\\r\\n\\tmax-width: 300px =21important;\\r\\n\\theight: 40px =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22starAlliance=22=5D =7Bfloat: left =21important; height: 40px=\\r\\n =21important; margin-top: 10px =21important; text-align:left =21important;=\\r\\n=7D\\r\\n*=5Bwidth=3D=22445=22=5D =7Bwidth:150px =21important;=7D\\r\\n*=5Bclass=3D=22outlookFix=22=5D =7Bfont-size:1px =21important; line-height:=\\r\\n1px =21important;=7D\\r\\n=7D</style>\\r\\n<meta http-equiv=3D=22Content-Type=22 content=3D=22text/html; charset=3Dutf=\\r\\n-8=22>\\r\\n<=21-- LANGUAGE GLOBAL VARIABLE=20\\r\\nDefault is EN\\r\\nTo pull language code from field in Unica file use setglobalvars(LANGUAGE,l=\\r\\nookup(GENERIC05))\\r\\nTo pull language code from CELL_CODE use setglobalvars(LANGUAGE,cond(eq(loo=\\r\\nkup(CELL_CODE),ET01),CH,cond(eq(lookup(CELL_CODE),ET02),DE,cond(eq(lookup(C=\\r\\nELL_CODE),ET03),EN,cond(eq(lookup(CELL_CODE),ET04),ES,cond(eq(lookup(CELL_C=\\r\\nODE),ET05),FR,cond(eq(lookup(CELL_CODE),ET06),JA,cond(eq(lookup(CELL_CODE),=\\r\\nET07),PT,cond(eq(lookup(CELL_CODE),ET08),KO,cond(eq(lookup(CELL_CODE),ET09)=\\r\\n,TCH,EN))))))))),EN)\\r\\nTo pull language code from Master Contacts List use setglobalvars(LANGUAGE,=\\r\\nlookup(LANG_CD))\\r\\n-->\\r\\n\\r\\n<=21-- OPT GLOBAL VARIABLE=20\\r\\nDefault is OPT_TYPE from Unica file\\r\\nFor MileagePlus ADMN/NONM campaigns use setglobalvars(OPT,MPPR)\\r\\nFor United ADMN/NONM campaigns use setglobalvars(OPT,UADL)\\r\\nYou will need to change the From and Reply To fields on the Campaign Dashbo=\\r\\nard\\r\\n-->\\r\\n\\r\\n<=21-- COUNTRY GLOBAL VARIABLE=20\\r\\nDefault is COUNTRY_ from the Master Contacts List\\r\\nFor ADMN campaigns use setglobalvars(COUNTRY,lookup(ISO_CNTRY_CD))\\r\\nFor NONM campaigns use setglobalvars(COUNTRY,US)\\r\\nor whichever country is appropriate\\r\\n-->\\r\\n\\r\\n<=21-- MEMBER_LEVEL GLOBAL VARIABLE=20\\r\\nDefault is ELITE_LEVEL from the Master Contacts List\\r\\nFor ADMN campaigns use setglobalvars(MEMBER_LEVEL,lookup(MP_PGM_MBRSP_LVL_C=\\r\\nD))\\r\\nFor NONM campaigns use setglobalvars(MEMBER_LEVEL,0)\\r\\nor whichever member level is appropriate\\r\\n-->\\r\\n\\r\\n<=21-- POS GLOBAL VARIABLE=20\\r\\nThis variable is passed to the POS=3D attribute in the link table\\r\\n-->\\r\\n\\r\\n<=21-- SECOND LANGUAGE GLOBAL VARIABLE=20\\r\\nUsed by Language Toggle\\r\\nDefault is EN\\r\\nTo set second language by CELL_CODE use\\r\\nsetglobalvars(SCND_LANG,cond(lookup(LANG_TOGGLE_INDICATOR),select(lookup(CE=\\r\\nLL_CODE),ET01,CH,ET02,DE,ET03,EN,ET04,ES,ET05,FR,ET06,JA,ET07,PT,ET08,KO,ET=\\r\\n09,TCH),nothing()))\\r\\n-->\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n</head>\\r\\n<body style=3D=22margin:0; padding:0;=22 vlink=3D=22=2362a9e3=22>\\r\\n<table width=3D=22100%=22 border=3D=220=22 align=3D=22center=22 cellpadding=\\r\\n=3D=220=22 cellspacing=3D=220=22>\\r\\n  <tr>\\r\\n    <td align=3D=22center=22 valign=3D=22top=22><=21-- Main Container -->\\r\\n     =20\\r\\n      <table width=3D=22670=22 border=3D=220=22 align=3D=22center=22 cellpa=\\r\\ndding=3D=220=22 cellspacing=3D=220=22 class=3D=22width320=22>\\r\\n        <tr class=3D=22hide=22>\\r\\n          <td colspan=3D=225=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=\\r\\n=22 height=3D=221=22 style=3D=22line-height:1px; font-size:0px;=22><img src=\\r\\n=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/uni=\\r\\nted/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 border=3D=220=\\r\\n=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n        <tr>\\r\\n          <td width=3D=221=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=22>=\\r\\n<img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/con=\\r\\ntent/united/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 borde=\\r\\nr=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n          <td width=3D=229=22 class=3D=22hide=22><img src=3D=22https://stat=\\r\\nic=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/united/sp=2Epng=22 wi=\\r\\ndth=3D=229=22 height=3D=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22dis=\\r\\nplay:block;=22 /></td>\\r\\n          <td width=3D=22650=22 align=3D=22center=22 valign=3D=22top=22 bgc=\\r\\nolor=3D=22=23e6e6e6=22 class=3D=22width320=22><=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <=21--/margin -->\\r\\n           =20\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n              <tr>\\r\\n                <td colspan=3D=223=22 align=3D=22center=22 valign=3D=22top=\\r\\n=22><=21-- PREHEADER -->\\r\\n                 =20\\r\\n                  <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=\\r\\n=220=22 cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n                    <tr>\\r\\n                      <td align=3D=22left=22 valign=3D=22top=22 bgcolor=3D=\\r\\n=22=23e6e6e6=22 class=3D=22width320 leftpad20 rightpad10=22 style=3D=22padd=\\r\\ning-left:20px;=22><=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:10=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin -->\\r\\n                       =20\\r\\n                        <div style=3D=22font-family: Arial&=2344;Helvetica&=\\r\\n=2344;sans-serif; color:=23333333; font-size:10px; line-height:12px; font-w=\\r\\neight:normal;=22 class=3D=22centercenter FS11 LH13 hide=22> You and one com=\\r\\npanion get a free checked bag when you purchase United tickets with your Mi=\\r\\nleagePlus Explorer Card=2E </div>\\r\\n                       =20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:10=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin -->\\r\\n                       =20\\r\\n                        <div style=3D=22font-family: Arial, Helvetica, sans=\\r\\n-serif; color:=23333333; font-size:10px; line-height:12px; font-weight:norm=\\r\\nal;=22 class=3D=22FS11 LH13=22>To ensure delivery to your inbox, please add=\\r\\n <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJ=\\r\\nlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWU=\\r\\nWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-c=\\r\\nVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YR=\\r\\nyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22 =\\r\\nstyle=3D=22color:=23333333; font-weight:normal; text-decoration:underline;=\\r\\n=22>MileagePlus_Partner=40news=2Eunited=2Ecom</a> to your address&nbsp;book=\\r\\n=2E</div></td>\\r\\n                      <td align=3D=22right=22 valign=3D=22bottom=22 bgcolor=\\r\\n=3D=22=23e6e6e6=22 width=3D=22130=22 class=3D=22hide=22 style=3D=22padding-=\\r\\nright:10px;=22><div style=3D=22font-family: Arial, Helvetica, sans-serif; c=\\r\\nolor:=23333333; font-size:10px; line-height:12px; font-weight:normal;=22><a=\\r\\n href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQ=\\r\\nGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWUAT&=\\r\\n_ei_=3DEusfb9GiCXrnzcSn25GQITmX9wd2Vl2kuyeVxNalGdM0ReCCx4wn-gYweqXvmG6lKyVz=\\r\\n29dov_gysMf5xaXmMMLwtYXCUXXfpgTGR_D9oRHK79lTz7R2MJ_ORtdrCugqfOLD2CKIqeVvHiC=\\r\\nwQoQU3LL7rs7rFLCHeOe_qdCC7WGcJm4226ZWHuTYTP8nrc7JIMmIYAWPQOKQX-s3p8l1oz_tZK=\\r\\n1BIgIgqJuQVcCsj7ixKFCwoNVE1UGMtOf1eUic_sa8hgmielJyA4aY4GEdwFzRCYhDAaue6lRRt=\\r\\nr9kFYF00sQwKk-FjxR3lrA1PjIPdrtNsuUj7hdf_djk3ifA-g-o4eeAW4qY0Sw5M-Op0FIiI7K_=\\r\\ndYiIx3W-7wS6tN9171PDMMRh5X7DLul43qXzjzY6EfLOMMfYpQbMOmyqOZFrThXP2Yimg5R_aLL=\\r\\n0NnX_-7RqlXpmvLt_XbuCnWUV07scLZDr97F_mssl1JX_EAi1_8L_ijsR2kMUXGwDUfv21j71-I=\\r\\nDtcuUgdqFxoffNzaOtywWZf23a2O9hL3vy0Dpz89Z9nQWr5GnZiBmJjhcgR3Xz8j46Zc_qIqDmj=\\r\\nX5URrxWRrA5pPQRINk=2E=22 style=3D=22color:=23333333; font-weight:normal; te=\\r\\nxt-decoration:underline;=22 target=3D=22_blank=22>View in Web browser</a> <=\\r\\n/div>\\r\\n                       =20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:10=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin --></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                 =20\\r\\n                  <=21--/PREHEADER --></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21-- UNITED MILEAGEPLUS HEADER -->=20\\r\\n           =20\\r\\n            <=21--/UNITED MILEAGEPLUS HEADER -->=20\\r\\n            <=21-- MODULES GO HERE -->=20\\r\\n            <=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/margin -->=20\\r\\n            <=21-- Hero Module -->=20\\r\\n           =20\\r\\n<table width=3D=22660=22 class=3D=22320px_width=22 border=3D=220=22 cellspa=\\r\\ncing=3D=220=22 cellpadding=3D=220=22 bgcolor=3D=22=23ffffff=22>\\r\\n        <tr>\\r\\n          <td width=3D=22660=22 class=3D=22320px_width=22 align=3D=22center=\\r\\n=22><=21-- Begin modules -->\\r\\n            <div style=3D=22font-size:10px; line-height:10px;=22 class=3D=\\r\\n=22hide=22>&nbsp;</div>\\r\\n<table width=3D=22640=22 border=3D=220=22 cellspacing=3D=220=22 cellpadding=\\r\\n=3D=220=22  bgcolor=3D=22=23394a58=22 class=3D=22width100=22>\\r\\n              <tr>\\r\\n                <td width=3D=2223=22 bgcolor=3D=22=23394a58=22 class=3D=22h=\\r\\nide=22>&nbsp;</td>\\r\\n                <td width=3D=22594=22 valign=3D=22bottom=22 bgcolor=3D=22=\\r\\n=23394a58=22><table width=3D=2295%=22 border=3D=220=22 cellspacing=3D=220=\\r\\n=22 cellpadding=3D=220=22 align=3D=22left=22 style=3D=22float:left;=22 clas=\\r\\ns=3D=22width95=22>\\r\\n                    <tr>\\r\\n                      <td align=3D=22left=22 valign=3D=22top=22 bgcolor=3D=\\r\\n=22=23394a58=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-s=\\r\\nize:20px; line-height:24px; font-weight:bold; color:=23ffffff;=22 class=3D=\\r\\n=22headline=22><div style=3D=22font-size:20px; line-height:20px;=22>&nbsp;<=\\r\\n/div>\\r\\n                        Free checked bag for you and one companion\\r\\n                      <div style=3D=22font-size:20px; line-height:20px;=22>=\\r\\n&nbsp;</div></td>\\r\\n                    </tr>\\r\\n                  </table></td>\\r\\n                <td width=3D=2223=22 bgcolor=3D=22=23394a58=22 class=3D=22h=\\r\\nide=22>&nbsp;</td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22width100=22>\\r\\n              <tr>\\r\\n                <td width=3D=2210=22>&nbsp;</td>\\r\\n                <td valign=3D=22top=22><table width=3D=22640=22 border=3D=\\r\\n=220=22 cellspacing=3D=220=22 cellpadding=3D=220=22 class=3D=22width100=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=2223=22 class=3D=22hide=22>&nbsp;</td>\\r\\n                      <td width=3D=22594=22 valign=3D=22top=22 class=3D=22w=\\r\\nidth100=22><table width=3D=22254=22 border=3D=220=22 cellspacing=3D=220=22 =\\r\\ncellpadding=3D=220=22 align=3D=22right=22 class=3D=22width100=22 style=3D=\\r\\n=22float:right;=22>\\r\\n                          <tr>\\r\\n                            <td width=3D=22225=22 height=3D=22146=22 bgcolo=\\r\\nr=3D=22=23ffffff=22 valign=3D=22top=22 class=3D=22width100=22 align=3D=22ce=\\r\\nnter=22><div style=3D=22font-size:16px;=22>&=23160;</div><a href=3D=22https=\\r\\n://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38n=\\r\\nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWCWT&_ei_=3DEqOC4jUOg=\\r\\n4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5Kb=\\r\\ngO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRp=\\r\\nIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22><img border=3D=220=\\r\\n=22 width=3D=22254=22 height=3D=22372=22 alt=3D=22=22 src=3D=22http://lacek=\\r\\n=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Update/UALCHR15104_hero_de=\\r\\nsktop_254x372_R2_ET06=2Ejpg=22 style=3D=22display: block; border:none;=22/>=\\r\\n</a></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <table width=3D=22317=22 border=3D=220=22 cellspaci=\\r\\nng=3D=220=22 cellpadding=3D=220=22 align=3D=22left=22 style=3D=22float:left=\\r\\n;=22 class=3D=22width100=22>\\r\\n                          <tr>\\r\\n                            <td valign=3D=22top=22 align=3D=22left=22 style=\\r\\n=3D=22font-family:Arial, Helvetica, sans-serif; font-size:13px; color:=2333=\\r\\n3333;=22 class=3D=22=22><div style=3D=22font-size:16px;=22>&=23160;</div>\\r\\n                             =20\\r\\n                              <=21-- Body Copy=2E -->Your United MileagePlu=\\r\\ns&reg; Explorer Card provides a free first standard checked bag* for you (t=\\r\\nhe primary Cardmember) and one companion on United&reg;-operated flights &m=\\r\\ndash; a savings of up to =24100 per roundtrip=2E\\r\\n                              <div style=3D=22font-size:8px; line-height:no=\\r\\nrmal;=22>&nbsp;</div>\\r\\n                              As a reminder, to receive this benefit you ne=\\r\\ned to:\\r\\n                              <div style=3D=22font-size:6px; line-height:no=\\r\\nrmal;=22>&nbsp;</div>\\r\\n                             =20\\r\\n                              <=21-- Begin Bulleted List -->\\r\\n                             =20\\r\\n                              <table width=3D=22300=22 border=3D=220=22 cel=\\r\\nlspacing=3D=220=22 cellpadding=3D=220=22 style=3D=22font-family:Arial, Helv=\\r\\netica, sans-serif; font-size:13px; color:=23333333;=22 class=3D=22width90=\\r\\n=22>\\r\\n                                <tr>\\r\\n                                  <td width=3D=2210=22 rowspan=3D=227=22 va=\\r\\nlign=3D=22top=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/spacer=2Egif=22 width=3D=2210=22 height=3D=2210=22 =\\r\\nalt=3D=22=22 style=3D=22display:block;=22 border=3D=220=22 /></td>\\r\\n                                  <td width=3D=228=22 align=3D=22left=22 va=\\r\\nlign=3D=22top=22><strong>&bull;</strong></td>\\r\\n                                  <td width=3D=22299=22 align=3D=22left=22 =\\r\\nvalign=3D=22top=22>Include your MileagePlus number in your reservation\\r\\n                                    <div style=3D=22font-size:4px; line-hei=\\r\\nght:4px;=22>&nbsp;</div></td>\\r\\n                                </tr>\\r\\n                                <tr>\\r\\n                                  <td width=3D=228=22 align=3D=22left=22 va=\\r\\nlign=3D=22top=22><strong>&bull;</strong></td>\\r\\n                                  <td width=3D=22299=22 align=3D=22left=22 =\\r\\nvalign=3D=22top=22>Purchase your ticket(s) with your Explorer Card</td>\\r\\n                                </tr>\\r\\n                              </table>\\r\\n                             =20\\r\\n                              <=21-- Ended Bulleted List -->\\r\\n                             =20\\r\\n                              <div style=3D=22font-size:18px; line-height:n=\\r\\normal;=22>&nbsp;</div>\\r\\n                              <table border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22CTA=22>\\r\\n                                <tr>\\r\\n                                  <td  bgcolor=3D=22=23394a58=22  align=3D=\\r\\n=22center=22 class=3D=22CTA=22><table width=3D=22180=22 border=3D=220=22 ce=\\r\\nllpadding=3D=220=22 cellspacing=3D=220=22>\\r\\n                                      <tr>\\r\\n                                        <td style=3D=22text-align:center;=\\r\\n=22><a target=3D=22_blank=22 style=3D=22font-family:Arial,Helvetica,sans-se=\\r\\nrif;color:=23ffffff;text-decoration:none;font-size:14px;font-weight:bold;=\\r\\n=22 href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJ=\\r\\nlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWC=\\r\\nWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-c=\\r\\nVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YR=\\r\\nyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22>See more details</a></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                    </table></td>\\r\\n                                  <td height=3D=2240=22 class=3D=22hide=22>=\\r\\n<img style=3D=22display:block;=22 src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/=\\r\\no33/UAL/email/2016/16087_FOP_Update/sp=2Epng=22 alt=3D=22=22 width=3D=225=\\r\\n=22 height=3D=2240=22 /></td>\\r\\n                                </tr>\\r\\n                              </table></td>\\r\\n                          </tr>\\r\\n                        </table></td>\\r\\n                      <td width=3D=2223=22 class=3D=22hide=22>&nbsp;</td>\\r\\n                    </tr>\\r\\n                  </table></td>\\r\\n                <td width=3D=2210=22>&nbsp;</td>\\r\\n              </tr>\\r\\n            </table><=21-- End modules --></td>\\r\\n        </tr>\\r\\n        <tr>\\r\\n          <td ><div style=3D=22font-size:24px;=22>&nbsp;</div>\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22  class=3D=22width100=22>\\r\\n              <tr>\\r\\n                <td width=3D=22660=22><table width=3D=22203=22 border=3D=22=\\r\\n0=22 cellspacing=3D=220=22 cellpadding=3D=220=22 align=3D=22left=22 style=\\r\\n=3D=22float:left;=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=2223=22>&nbsp;</td>\\r\\n                      <td width=3D=2261=22 height=3D=2225=22><div style=3D=\\r\\n=22font-size:4px; line-height:4px;=22>&nbsp;</div>\\r\\n                        <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri=\\r\\n_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8=\\r\\nc8FY4FCofVXtpKX%3DUSAYCCT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBt=\\r\\nauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoN=\\r\\naRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22=\\r\\n target=3D=22_blank=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UA=\\r\\nL/email/2016/16087_FOP_Update/fb-like=2Epng=22 alt=3D=22Like=22 width=3D=22=\\r\\n60=22 height=3D=2225=22 border=3D=220=22 /></a><a href=3D=22https://news=2E=\\r\\nunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zey=\\r\\ngb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAYCCT&_ei_=3DEqOC4jUOg4vyD36DYMR=\\r\\nhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iut=\\r\\nZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay=\\r\\n-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22></a></td>\\r\\n                      <td width=3D=2211=22>&nbsp;</td>\\r\\n                      <td width=3D=22108=22 height=3D=2225=22><div style=3D=\\r\\n=22font-size:4px; line-height:4px;=22> &nbsp; </div>\\r\\n                        <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri=\\r\\n_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8=\\r\\nc8FY4FCofVXtpKX%3DUSAYCCT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBt=\\r\\nauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoN=\\r\\naRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22=\\r\\n target=3D=22_blank=22 style=3D=22font-family:Arial, Helvetica, sans-serif;=\\r\\n font-size:12px; font-weight:bold; color:=23333333; text-decoration:none;=\\r\\n=22>MileagePlus Cards by Chase</a></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                  <table width=3D=22288=22 border=3D=220=22 cellspacing=3D=\\r\\n=220=22 cellpadding=3D=220=22 align=3D=22right=22 style=3D=22float:right;=\\r\\n=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=22123=22>&nbsp;</td>\\r\\n                      <td width=3D=22142=22><div style=3D=22font-size:2px; =\\r\\nline-height:2px=22 class=3D=22space25px=22> &nbsp; </div>\\r\\n                        <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri=\\r\\n_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8=\\r\\nc8FY4FCofVXtpKX%3DUSAWWWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBt=\\r\\nauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoN=\\r\\naRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22=\\r\\n target=3D=22_blank=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UA=\\r\\nL/email/2016/16087_FOP_Update/MileagePlus-logo=2Egif=22 width=3D=22141=22 h=\\r\\neight=3D=2237=22 alt=3D=22MileagePlus - United=22 border=3D=220=22 /></a></=\\r\\ntd>\\r\\n                      <td width=3D=2223=22>&nbsp;</td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td colspan=3D=229=22 style=3D=22font-size:1px;=22><i=\\r\\nmg src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Up=\\r\\ndate/spacer=2Egif=22 width=3D=222=22 height=3D=221=22 alt=3D=22=22 style=3D=\\r\\n=22display:block;=22 border=3D=220=22 /></td>\\r\\n                    </tr>\\r\\n                </table></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td height=3D=2215=22 style=3D=22clear:both;=22><img src=3D=\\r\\n=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Update/spac=\\r\\ner=2Egif=22 width=3D=221=22 height=3D=2210=22 alt=3D=22=22 style=3D=22displ=\\r\\nay:block;=22 border=3D=220=22 /></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td bgcolor=3D=22=23f1f1f1=22 style=3D=22font-size:1px;=22>=\\r\\n<img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_=\\r\\nUpdate/spacer=2Egif=22 width=3D=222=22 height=3D=221=22 alt=3D=22=22 style=\\r\\n=3D=22display:block;=22 border=3D=220=22 /></td>\\r\\n              </tr>\\r\\n            </table></td>\\r\\n        </tr>\\r\\n        <tr>\\r\\n          <td ><div style=3D=22font-size:10px;=22>&nbsp;</div>\\r\\n                <=21-- Begin Social Media -->\\r\\n               =20\\r\\n\\r\\n <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22 cellpaddin=\\r\\ng=3D=220=22 class=3D=22320px_width=22>\\r\\n  <tr>\\r\\n    <td align=3D=22right=22 width=3D=22470=22 style=3D=22font-family: Arial=\\r\\n, Helvetica, sans-serif; color:=23333333; font-size:10px; line-height:14px;=\\r\\n font-weight:normal;=22>Stay connected to United</td>\\r\\n    <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=22190=22><table =\\r\\nborder=3D=220=22 cellspacing=3D=220=22 cellpadding=3D=220=22>\\r\\n        <tr>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWWAT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2mobile=2Ejpg=22 width=3D=2225=22 height=3D=\\r\\n=2225=22 alt=3D=22Mobile=22 border=3D=220=22 style=3D=22display:block;=22> =\\r\\n</a></td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWYRT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2hub=2Ejpg=22 width=3D=2225=22 height=3D=222=\\r\\n5=22 alt=3D=22Hub=22 border=3D=220=22 style=3D=22display:block;=22> </a></t=\\r\\nd>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2211=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWYWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2fb=2Ejpg=22 width=3D=2225=22 height=3D=2225=\\r\\n=22 alt=3D=22Facebook=22 border=3D=220=22 style=3D=22display:block;=22> </a=\\r\\n></td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWYCT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2tw=2Ejpg=22 width=3D=2225=22 height=3D=2225=\\r\\n=22 alt=3D=22Twitter=22 border=3D=220=22 style=3D=22display:block;=22> </a>=\\r\\n</td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWATT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2instagram=2Ejpg=22 width=3D=2225=22 height=\\r\\n=3D=2225=22 alt=3D=22Instagram=22 border=3D=220=22 style=3D=22display:block=\\r\\n;=22> </a></td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWAWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2yt=2Ejpg=22 width=3D=2225=22 height=3D=2225=\\r\\n=22 alt=3D=22YouTube=22 border=3D=220=22 style=3D=22display:block;=22> </a>=\\r\\n</td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2212=22><a=\\r\\n href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQ=\\r\\nGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWACT&=\\r\\n_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbf=\\r\\ntB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLm=\\r\\nivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22> <i=\\r\\nmg src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Up=\\r\\ndate/social_2li=2Ejpg=22 width=3D=2225=22 height=3D=2225=22 alt=3D=22Linked=\\r\\nIn=22 border=3D=220=22 style=3D=22display:block;=22></a></td>\\r\\n          <td width=3D=228=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet=\\r\\n/o33/UAL/email/2016/16087_FOP_Update/sp=2Epng=22 width=3D=228=22 height=3D=\\r\\n=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n      </table></td>\\r\\n  </tr>\\r\\n</table>\\r\\n\\r\\n                <=21-- End Social Media -->\\r\\n            <div style=3D=22font-size:10px;=22>&nbsp;</div>\\r\\n            <div style=3D=22font-size:7px; background-color:=23394a58=22>&n=\\r\\nbsp;</div></td>\\r\\n        </tr>\\r\\n      </table>=20\\r\\n            <=21-- /Hero Module -->=20\\r\\n            <=21-- margin -->\\r\\n           =20\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/margin -->=20\\r\\n            <=21-- margin -->\\r\\n           =20\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/margin -->=20\\r\\n            <=21--/MODULES GO HERE -->=20\\r\\n            <=21-- FOOTER -->=20\\r\\n            <=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <=21--/margin -->=20\\r\\n            <=21-- global links -->\\r\\n           =20\\r\\n            <table width=3D=22650=22 border=3D=220=22 cellpadding=3D=220=22=\\r\\n cellspacing=3D=220=22 class=3D=22width320=22>\\r\\n              <tr>\\r\\n                <td width=3D=22650=22 align=3D=22center=22 valign=3D=22midd=\\r\\nle=22 bgcolor=3D=22=23e6e6e6=22 class=3D=22hide=22><=21-- margin -->\\r\\n                  <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=\\r\\n=3D=220=22>\\r\\n                    <tr>\\r\\n                      <td height=3D=2210=22 style=3D=22line-height:15px; fo=\\r\\nnt-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/res=\\r\\nponsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 a=\\r\\nlt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                  <=21--/margin --></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D=22650=22 align=3D=22center=22 valign=3D=22midd=\\r\\nle=22 bgcolor=3D=22=23e6e6e6=22 class=3D=22width320 fl wrap center=22><div =\\r\\nstyle=3D=22font-family: Arial, Helvetica, sans-serif; color:=23010000; font=\\r\\n-size:10px; line-height:12px; font-weight:normal;=22 class=3D=22FS11 LH17 F=\\r\\nWbold fcFFF bg888 LR5 TB10=22> <a href=3D=22https://news=2Eunited=2Ecom/pub=\\r\\n/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqg=\\r\\nrqMzbK8c8FY4FCofVXtpKX%3DUSAWBRT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zx=\\r\\nG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3Uwzd=\\r\\nIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq=\\r\\n0=2E=22 style=3D=22color:=23010000; font-weight:normal; text-decoration:und=\\r\\nerline;=22 class=3D=22fcFFF FWbold=22 target=3D=22_blank=22>united=2Ecom</a=\\r\\n>&nbsp;&nbsp; <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2=\\r\\nX%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofV=\\r\\nXtpKX%3DUSAWBTT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHk=\\r\\nXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j=\\r\\n7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=\\r\\n=22color:=23010000; font-weight:normal; text-decoration:underline;=22 class=\\r\\n=3D=22fcFFF FWbold=22 target=3D=22_blank=22>Partner offers</a>&nbsp;&nbsp; =\\r\\n<a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJl=\\r\\nTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWBW=\\r\\nT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cV=\\r\\nbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRy=\\r\\nLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=22color:=23010=\\r\\n000; font-weight:normal; text-decoration:underline;=22 class=3D=22fcFFF FWb=\\r\\nold=22 target=3D=22_blank=22>Change email address</a>&nbsp;&nbsp; <a href=\\r\\n=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzb=\\r\\nltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWBCT&_ei_=\\r\\n=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9o=\\r\\nrEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBd=\\r\\nhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=22color:=23010000; f=\\r\\nont-weight:normal; text-decoration:underline;=22 class=3D=22fcFFF FWbold=22=\\r\\n target=3D=22_blank=22>Update email preferences</a>&nbsp;&nbsp; <a href=3D=\\r\\n=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltf=\\r\\nLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWCRT&_ei_=3DE=\\r\\nqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhC=\\r\\nWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6=\\r\\nV38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=22color:=23010000; font-=\\r\\nweight:normal; text-decoration:underline;=22 class=3D=22fcFFF FWbold=22 tar=\\r\\nget=3D=22_blank=22>Privacy&nbsp;policy</a> </div></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D=22650=22 align=3D=22center=22 valign=3D=22midd=\\r\\nle=22 bgcolor=3D=22=23e6e6e6=22 class=3D=22hide=22><=21-- margin -->\\r\\n                  <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=\\r\\n=3D=220=22>\\r\\n                    <tr>\\r\\n                      <td height=3D=2210=22 style=3D=22line-height:15px; fo=\\r\\nnt-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/res=\\r\\nponsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 a=\\r\\nlt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                  <=21--/margin --></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/global links -->\\r\\n           =20\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n              <tr>\\r\\n                <td bgcolor=3D=22=23e6e6e6=22>\\r\\n                  <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=\\r\\n=220=22 cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=2230=22 align=3D=22right=22 valign=3D=22=\\r\\ntop=22 class=3D=22width20=22><img src=3D=22https://static=2Ecdn=2Eresponsys=\\r\\n=2Enet/i2/responsysimages/content/united/sp=2Epng=22 width=3D=2210=22 heigh=\\r\\nt=3D=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></=\\r\\ntd>\\r\\n                      <td width=3D=22600=22 align=3D=22left=22 valign=3D=22=\\r\\ntop=22 class=3D=22width290=22><=21-- legal -->=20\\r\\n                       =20\\r\\n<div style=3D=22font-family: Arial, Helvetica, sans-serif; color:=23333333;=\\r\\n font-size:10px; line-height:16px; font-weight:normal;=22><strong>Terms and=\\r\\n conditions:</strong><br />\\r\\n*FREE CHECKED BAG: The primary Cardmember and one traveling companion on th=\\r\\ne same reservation are each eligible to receive their first standard checke=\\r\\nd bag free; authorized users are only eligible if they are on the same rese=\\r\\nrvation as the primary Cardmember=2E To receive first standard checked bag =\\r\\nfree, the primary Cardmember must include their MileagePlus&reg; number in =\\r\\ntheir reservation and use their MileagePlus Explorer Card to purchase their=\\r\\n ticket(s)=2E First standard checked bag free is only available on United&r=\\r\\neg;- and United Express&reg;-operated flights; codeshare partner-operated f=\\r\\nlights are not eligible=2E Service charges for oversized, overweight and ex=\\r\\ntra baggage may apply=2E Cardmembers who are already exempt from other chec=\\r\\nked baggage service charges will not receive an additional free standard ch=\\r\\necked bag=2E Chase is not responsible for the provision of, or failure to p=\\r\\nrovide, the stated benefits=2E Please visit <a href=3D=22http://www=2Eunite=\\r\\nd=2Ecom/chasebag=22 target=3D=22_blank=22 style=3D=22color:=23333333; text-=\\r\\ndecoration:underline;=22>united=2Ecom/chasebag</a> for details=2E<br />\\r\\n<br />\\r\\n<strong>MileagePlus terms and conditions:</strong><br />\\r\\nMiles accrued, awards, and benefits issued are subject to change and are su=\\r\\nbject to the rules of the United MileagePlus program, including without lim=\\r\\nitation the Premier&reg; program (the =22MileagePlus Program=22), which are=\\r\\n expressly incorporated herein=2E Please allow 6&ndash;8 weeks after comple=\\r\\nted qualifying activity for miles to post to your account=2E United may cha=\\r\\nnge the MileagePlus Program including, but not limited to, rules, regulatio=\\r\\nns, travel awards and special offers or terminate the MileagePlus Program a=\\r\\nt any time and without notice=2E United and its subsidiaries, affiliates an=\\r\\nd agents are not responsible for any products or services of other particip=\\r\\nating companies and partners=2E Taxes and fees related to award travel are =\\r\\nthe responsibility of the member=2E Bonus award miles, award miles and any =\\r\\nother miles earned through non-flight activity do not count toward qualific=\\r\\nation for Premier status unless expressly stated otherwise=2E The accumulat=\\r\\nion of mileage or Premier status or any other status does not entitle membe=\\r\\nrs to any vested rights with respect to the MileagePlus Program=2E All calc=\\r\\nulations made in connection with the MileagePlus Program, including without=\\r\\n limitation with respect to the accumulation of mileage and the satisfactio=\\r\\nn of the qualification requirements for Premier status, will be made by Uni=\\r\\nted Airlines and MileagePlus in their discretion and such calculations will=\\r\\n be considered final=2E Information in this communication that relates to t=\\r\\nhe MileagePlus Program does not purport to be complete or comprehensive and=\\r\\n may not include all of the information that a member may believe is import=\\r\\nant, and is qualified in its entirety by reference to all of the informatio=\\r\\nn on the <a href=3D=22http://www=2Eunited=2Ecom=22 target=3D=22_blank=22 st=\\r\\nyle=3D=22color:=23333333; text-decoration:underline;=22>united=2Ecom</a> we=\\r\\nbsite and the MileagePlus Program rules=2E United and MileagePlus are regis=\\r\\ntered service marks=2E For complete details about the MileagePlus Program, =\\r\\ngo to <a href=3D=22http://www=2Eunited=2Ecom=22 target=3D=22_blank=22 style=\\r\\n=3D=22color:=23333333; text-decoration:underline;=22>united=2Ecom</a>=2E<br=\\r\\n />\\r\\n<br />\\r\\n<a href=3D=22https://www=2Eunited=2Ecom/web/en-US/content/mileageplus/rules=\\r\\n/default=2Easpx=22 target=3D=22_blank=22 style=3D=22color:=23333333;=22>See=\\r\\n additional MileagePlus terms and conditions</a><br/>\\r\\n<br/>\\r\\nC000013567 14342 ET06<br />\\r\\n</div>=20\\r\\n                        <=21--/legal -->=20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:15=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin -->=20\\r\\n                        <=21-- footer -->=20\\r\\n                       =20\\r\\n    <=21-- footer -->\\r\\n<div style=3D=22font-family:Arial, Helvetica, sans-serif; font-size:10px; l=\\r\\nine-height:16px; color:=23333333;=22>\\r\\n<a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJl=\\r\\nTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWUW=\\r\\nT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cV=\\r\\nbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRy=\\r\\nLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22 s=\\r\\ntyle=3D=22color:=23333333; font-weight:normal; text-decoration:underline;=\\r\\n=22>Don't miss the latest emails; add =40news=2Eunited=2Ecom</a> <br />\\r\\nYou have received this email because it includes important information rega=\\r\\nrding your MileagePlus Credit Card benefits=2E Your privacy and email prefe=\\r\\nrences are very important to us=2E If you previously opted not to receive p=\\r\\nromotional emails from MileagePlus, we will continue to respect your prefer=\\r\\nences=2E However, we will continue to send you non-promotional, service ema=\\r\\nils concerning your account, the MileagePlus program or United=2E\\r\\n<div style=3D=22font-size:5px; line-height:normal;=22>&nbsp;</div>\\r\\nThis email was sent to <a href=3D=22https://www=2Eunited=2Ecom/CMS/en-US/ac=\\r\\ncount/email/subscription/Pages/emailSubAddressBook=2Easpx=22 target=3D=22_b=\\r\\nlank=22 style=3D=22color:=23333333;=22>christine=40spang=2Ecc</a>\\r\\n<div style=3D=22font-size:5px; line-height:normal;=22>&nbsp;</div>\\r\\nPlease do not reply to this email=2E We cannot accept electronic replies to=\\r\\n this email address=2E<br />\\r\\nEmail <a href=3D=22mailto:mileageplus=40united=2Ecom=22 target=3D=22_blank=\\r\\n=22 style=3D=22color:=23333333;=22>mileageplus=40united=2Ecom</a> with any =\\r\\nquestions about your MileagePlus account or the MileagePlus program=2E <br =\\r\\n/>\\r\\n</div>\\r\\n<table width=3D=22620=22 border=3D=220=22 cellspacing=3D=220=22 cellpadding=\\r\\n=3D=220=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-size:1=\\r\\n0px; color:=23333333;=22>\\r\\n                  <tr>\\r\\n                    <td width=3D=22620=22><table width=3D=22300=22 border=\\r\\n=3D=220=22 cellspacing=3D=220=22 cellpadding=3D=220=22 align=3D=22left=22 s=\\r\\ntyle=3D=22float:left;=22>\\r\\n                      <tr>\\r\\n                        <td width=3D=22300=22 height=3D=2285=22 valign=3D=\\r\\n=22bottom=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-size=\\r\\n:10px; color:=23333333; text-align:left;=22> To contact the sender, write t=\\r\\no: <br />\\r\\n                          United MileagePlus <br />\\r\\n                          900 Grand Plaza Dr=2E<br />\\r\\nHouston, TX 77067<br />\\r\\n                          <br />\\r\\n                          &copy; 2016 United Airlines, Inc=2E All rights re=\\r\\nserved=2E</td>\\r\\n                        </tr>\\r\\n                      </table>\\r\\n                      <table width=3D=22300=22 border=3D=220=22 cellspacing=\\r\\n=3D=220=22 cellpadding=3D=220=22 align=3D=22right=22 style=3D=22float:right=\\r\\n;=22 class=3D=22starAlliance=22>\\r\\n                        <tr>\\r\\n                          <td width=3D=22300=22 height=3D=2285=22 valign=3D=\\r\\n=22bottom=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-size=\\r\\n:10px; color:=23333333; text-align:right;=22 align=3D=22right=22 class=3D=\\r\\n=22starAlliance=22><a href=3D=22https://www=2Eunited=2Ecom/web/en-US/conten=\\r\\nt/company/alliance/star=2Easpx=22_blank=22><img src=3D=22http://lacek=2Evo=\\r\\n=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Update/staralliance_061713=2Egi=\\r\\nf=22 width=3D=22163=22 height=3D=2219=22 border=3D=220=22 alt=3D=22A Star A=\\r\\nlliance Member=22 /></a></td>\\r\\n                          </tr>\\r\\n                        </table></td>\\r\\n                    </tr>\\r\\n  </table>\\r\\n    <=21--/footer -->=20\\r\\n                        <=21--/footer -->=20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:15=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin --></td>\\r\\n                      <td width=3D=2220=22 class=3D=22width10=22><img src=\\r\\n=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/uni=\\r\\nted/sp=2Epng=22 width=3D=2210=22 height=3D=221=22 alt=3D=22=22 border=3D=22=\\r\\n0=22 style=3D=22display:block;=22 /></td>\\r\\n                    </tr>\\r\\n                  </table></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <=21--/margin -->=20\\r\\n            <=21--/FOOTER --></td>\\r\\n          <td width=3D=229=22 class=3D=22hide=22><img src=3D=22https://stat=\\r\\nic=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/united/sp=2Epng=22 wi=\\r\\ndth=3D=229=22 height=3D=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22dis=\\r\\nplay:block;=22 /></td>\\r\\n          <td width=3D=221=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=22>=\\r\\n<img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/con=\\r\\ntent/united/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 borde=\\r\\nr=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n        <tr class=3D=22hide=22>\\r\\n          <td colspan=3D=225=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=\\r\\n=22 height=3D=221=22 style=3D=22line-height:1px; font-size:0px;=22><img src=\\r\\n=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/uni=\\r\\nted/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 border=3D=220=\\r\\n=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n      </table>\\r\\n     =20\\r\\n      <=21-- /Main Container -->=20\\r\\n      <=21-- Retargeting/tracking pixels -->=20\\r\\n      =20\\r\\n      <=21-- /Retargeting/tracking pixels --></td>\\r\\n  </tr>\\r\\n</table><br>\\r\\n<table cellpadding=3D=220=22 cellspacing=3D=220=22 style=3D=22border: 0px; =\\r\\npadding: 0px; margin: 0px; position: absolute; display: none; float: left=\\r\\n=22>\\r\\n<tr>\\r\\n<td height=3D=221=22 style=3D=22font-size: 1px; line-height: 1px; padding: =\\r\\n0px;=22>\\r\\n<br><img height=3D=221=22 width=3D=221=22 style=3D =22padding: 0px 0px; =21=\\r\\nimportant; margin: 0px 0px; font-size: 1px; line-height: 1px;=22 src=3D=22h=\\r\\nttps://news=2Eunited=2Ecom/pub/as?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5=\\r\\nm38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXHkMX%3Dw&_ei_=3DEjaf7_rRKzx=\\r\\n0ctn6G-vCRQxQFpOo1jnhz_D7Qj8uilahE5HqTLtnYXpmd53bnyOwa5G4ok2G4SqEqZyK_i5MlB=\\r\\n4glRW1lFv87L9Bq3zhUkhE6W-I9Lls8knlgu8Rrb4Dorss_mjDo7FeWQ-JARWOws8jiyPdFbxa=\\r\\n=2E=22></img>\\r\\n<br><=21--Start BK pixel--><IMG SRC=3D=22https://tags=2Ebluekai=2Ecom/site/=\\r\\n36540?e_id_s36540=3D9729d1a3a57d2e0b0a83851c6f030a05510060f1ea36db9eb96a9c7=\\r\\n557e258ef&e_id_m36540=3Dff0d6f7a7f24eb386614ee1e768dd100=22 HEIGHT=3D=221=\\r\\n=22 WIDTH=3D=221=22><=21--End BK pixel-->\\r\\n<br><img border=3D=220=22 width=3D=221=22 height=3D=221=22 src=3D=22http://=\\r\\nib=2Eadnxs=2Ecom/getuid?http://a=2Eadrsp=2Enet/dsp/ci/2/E066u9V7s2cpsgXWlW5=\\r\\nW6jSlqYBjE7Ku_fwBQk3_LZpAwGubjwp3fzLJ52fHSFXGe1fCaI_uGKBIh0/%24UID=22></img>\\r\\n</td>\\r\\n</tr>\\r\\n</table>\\r\\n</body>\\r\\n</html>\\r\\n\\r\\n\\r\\n\"}},\"desiredParts\":[{\"id\":\"1\",\"encoding\":\"QUOTED-PRINTABLE\",\"mimetype\":\"text/html\"}],\"result\":{\"to\":[{\"name\":\"\",\"email\":\"christine@spang.cc\"}],\"cc\":[],\"bcc\":[],\"from\":[{\"name\":\"MileagePlus Explorer Card\",\"email\":\"MileagePlus_Partner@news.united.com\"}],\"replyTo\":[{\"name\":\"MileagePlus Explorer Card\",\"email\":\"MileagePlus_NoReply@united.com\"}],\"accountId\":\"test-account-id\",\"body\":\"<html>\\r\\n<head>\\r\\n<meta charset=3D=22UTF-8=22>\\r\\n<meta name=3D=22viewport=22 content=3D=22width=3Ddevice-width, minimum-scal=\\r\\ne=3D1=2E0, maximum-scale=3D1=2E0, initial-scale=3D1=2E0=22>\\r\\n<title>United Airlines - United MileagePlus</title>\\r\\n<style type=3D=22text/css=22>\\r\\nhtml =7B\\r\\n\\t-webkit-text-size-adjust: none;\\r\\n=7D\\r\\n=2EReadMsgBody =7B\\r\\n\\twidth: 100%;\\r\\n=7D\\r\\n=2EExternalClass =7B\\r\\n\\twidth: 100%;\\r\\n=7D\\r\\nbody =7B\\r\\n\\tpadding: 0;\\r\\n\\tmargin: 0;\\r\\n\\twidth: 100%;\\r\\n\\tmargin: 0 auto;\\r\\n\\tbackground-color: =23e6e6e6;\\r\\n=7D\\r\\n/* CSS for hiding content in desktop/webmail clients */\\r\\n=2Eimghide =7B\\r\\n\\tmax-height: 0px;\\r\\n\\tfont-size: 0;\\r\\n\\tdisplay: none;\\r\\n\\toverflow: hidden;\\r\\n=7D\\r\\n\\r\\n=40media only screen and (max-width: 480px) =7B\\r\\n/* Media query for displaying content in mobile email clients */\\r\\n=2Eimghide =7B\\r\\n\\tmax-height: none =21important;\\r\\n\\tfont-size: 12px =21important;\\r\\n\\tdisplay: block =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Efl =7B\\r\\n\\tfloat: left =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eclear =7B\\r\\n\\tclear: both;\\r\\n=21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Efr =7B\\r\\n\\tfloat: right =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewrap =7B\\r\\n\\tdisplay: block =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehide =7B\\r\\n\\tdisplay: none =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehideflow =7B\\r\\n\\toverflow-x: hidden =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22collapse10=22=5D =7B\\r\\n\\twidth: 10px =21important;\\r\\n\\theight: 15px =21important;\\r\\n=7D\\r\\n/* WIDTHS */\\r\\n*=5Bclass=5D=2Ewidth100pc =7B\\r\\n\\twidth: 100% =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth0 =7B\\r\\n\\twidth: 0px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth1 =7B\\r\\n\\twidth: 1px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth2 =7B\\r\\n\\twidth: 2px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth3 =7B\\r\\n\\twidth: 3px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth4 =7B\\r\\n\\twidth: 4px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth5 =7B\\r\\n\\twidth: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth6 =7B\\r\\n\\twidth: 6px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth7 =7B\\r\\n\\twidth: 7px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth8 =7B\\r\\n\\twidth: 8px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth9 =7B\\r\\n\\twidth: 9px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth10 =7B\\r\\n\\twidth: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth15 =7B\\r\\n\\twidth: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth19 =7B\\r\\n\\twidth: 19px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth20 =7B\\r\\n\\twidth: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth25 =7B\\r\\n\\twidth: 25px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth42 =7B\\r\\n\\twidth: 42px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth48 =7B\\r\\n\\twidth: 48px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth58 =7B\\r\\n\\twidth: 58px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth63 =7B\\r\\n\\twidth: 63px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth64 =7B\\r\\n\\twidth: 64px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth82 =7B\\r\\n\\twidth: 82px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth90px =7B\\r\\n\\twidth: 90px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth95px =7B\\r\\n\\twidth: 95px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth100px =7B\\r\\n\\twidth: 100px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth104 =7B\\r\\n\\twidth: 104px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth119 =7B\\r\\n\\twidth: 119px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth120 =7B\\r\\n\\twidth: 120px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth154 =7B\\r\\n\\twidth: 154px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth181 =7B\\r\\n\\twidth: 181px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth185 =7B\\r\\n\\twidth: 185px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth216 =7B\\r\\n\\twidth: 100px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth248 =7B\\r\\n\\twidth: 248px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth260 =7B\\r\\n\\twidth: 260px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth261 =7B\\r\\n\\twidth: 261px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth275 =7B\\r\\n\\twidth: 275px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth278 =7B\\r\\n\\twidth: 278px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth280 =7B\\r\\n\\twidth: 280px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth290 =7B\\r\\n\\twidth: 290px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth295 =7B\\r\\n\\twidth: 295px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth300 =7B\\r\\n\\twidth: 300px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth302 =7B\\r\\n\\twidth: 302px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ewidth320 =7B\\r\\n\\twidth: 320px =21important;\\r\\n=7D\\r\\n/* HEIGHTS */\\r\\n*=5Bclass=5D=2Eheight2 =7B\\r\\n\\theight: 2px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight4 =7B\\r\\n\\theight: 4px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight5 =7B\\r\\n\\theight: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight9 =7B\\r\\n\\theight: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight10 =7B\\r\\n\\theight: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight15 =7B\\r\\n\\theight: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight30 =7B\\r\\n\\theight: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight36 =7B\\r\\n\\theight: 36px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight37 =7B\\r\\n\\theight: 37px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight42 =7B\\r\\n\\theight: 42px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight43 =7B\\r\\n\\theight: 43px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight48 =7B\\r\\n\\theight: 48px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight50 =7B\\r\\n\\theight: 50px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight52 =7B\\r\\n\\theight: 52px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight58 =7B\\r\\n\\theight: 58px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight60 =7B\\r\\n\\theight: 60px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight65 =7B\\r\\n\\theight: 65px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight73 =7B\\r\\n\\theight: 73px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight76 =7B\\r\\n\\theight: 76px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight84 =7B\\r\\n\\theight: 84px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight89 =7B\\r\\n\\theight: 89px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight90 =7B\\r\\n\\theight: 90px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight93 =7B\\r\\n\\theight: 93px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight95 =7B\\r\\n\\theight: 95px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight100 =7B\\r\\n\\theight: 100px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight104 =7B\\r\\n\\theight: 104px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight107 =7B\\r\\n\\theight: 107px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight110 =7B\\r\\n\\theight: 110px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight112 =7B\\r\\n\\theight: 112px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight116 =7B\\r\\n\\theight: 116px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight118 =7B\\r\\n\\theight: 118px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight120 =7B\\r\\n\\theight: 120px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight122 =7B\\r\\n\\theight: 122px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight123 =7B\\r\\n\\theight: 123px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight125 =7B\\r\\n\\theight: 125px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight127 =7B\\r\\n\\theight: 127px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight135 =7B\\r\\n\\theight: 135px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight138 =7B\\r\\n\\theight: 138px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight140 =7B\\r\\n\\theight: 140px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight142 =7B\\r\\n\\theight: 142px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight144 =7B\\r\\n\\theight: 144px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight156 =7B\\r\\n\\theight: 156px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight161 =7B\\r\\n\\theight: 161px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight166 =7B\\r\\n\\theight: 166px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight174 =7B\\r\\n\\theight: 174px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight175 =7B\\r\\n\\theight: 175px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight176 =7B\\r\\n\\theight: 176px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight182 =7B\\r\\n\\theight: 182px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight185 =7B\\r\\n\\theight: 185px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight195 =7B\\r\\n\\theight: 195px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight200 =7B\\r\\n\\theight: 200px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight203 =7B\\r\\n\\theight: 203px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight208 =7B\\r\\n\\theight: 208px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight211 =7B\\r\\n\\theight: 211px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight212 =7B\\r\\n\\theight: 212px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight213 =7B\\r\\n\\theight: 213px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight215 =7B\\r\\n\\theight: 215px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight223 =7B\\r\\n\\theight: 223px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight240 =7B\\r\\n\\theight: 240px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight247 =7B\\r\\n\\theight: 247px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight249 =7B\\r\\n\\theight: 249px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight256 =7B\\r\\n\\theight: 256px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight260 =7B\\r\\n\\theight: 260px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight266 =7B\\r\\n\\theight: 266px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight288 =7B\\r\\n\\theight: 288px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight295 =7B\\r\\n\\theight: 295px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eheight579 =7B\\r\\n\\theight: 579px =21important;\\r\\n=7D\\r\\n/* MARGINS */\\r\\n*=5Bclass=5D=2Ecenter =7B\\r\\n\\ttext-align: center =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ecentercenter =7B\\r\\n\\tmargin-left: auto=21important;\\r\\n\\tmargin-right: auto =21important;\\r\\n\\tpadding: 0 =21important;\\r\\n\\tfloat: none =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebottom10 =7B\\r\\n\\tmargin-bottom: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ETB10 =7B\\r\\n\\tpadding-top: 10px =21important;\\r\\n\\tpadding-bottom: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELR5 =7B\\r\\n\\tpadding-left: 5px =21important;\\r\\n\\tpadding-right: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad5 =7B\\r\\n\\tpadding-left: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad10 =7B\\r\\n\\tpadding-left: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad15 =7B\\r\\n\\tpadding-left: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad20 =7B\\r\\n\\tpadding-left: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad30 =7B\\r\\n\\tpadding-left: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftpad0 =7B\\r\\n\\tpadding-left: 0px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad0 =7B\\r\\n\\tpadding-right: 0px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad5 =7B\\r\\n\\tpadding-right: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad10 =7B\\r\\n\\tpadding-right: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad15 =7B\\r\\n\\tpadding-right: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad20 =7B\\r\\n\\tpadding-right: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad30 =7B\\r\\n\\tpadding-right: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Erightpad35 =7B\\r\\n\\tpadding-right: 35px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Epaddingtop5 =7B\\r\\n\\tpadding-top: 5px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Epaddingtop10 =7B\\r\\n\\tpadding-top: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etoppad10 =7B\\r\\n\\tpadding-top: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etoppad20 =7B\\r\\n\\tpadding-top: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etoppad30 =7B\\r\\n\\tpadding-top: 30px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebottompad20 =7B\\r\\n\\tpadding-bottom: 20px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebottompad10 =7B\\r\\n\\tpadding-bottom: 10px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eleftmargin15 =7B\\r\\n\\tmargin-left: 15px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etopmargin10 =7B\\r\\n\\tmargin-top: 10px =21important;\\r\\n=7D\\r\\n/* POSTITIONING */\\r\\n*=5Bclass=5D=2Elefty =7B\\r\\n\\tposition: relative =21important;\\r\\n\\tleft: -320px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etextleft =7B\\r\\n\\ttext-align: left =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Evmiddle =7B\\r\\n\\tvertical-align: middle =21important;\\r\\n=7D\\r\\n/* FONTS */\\r\\n*=5Bclass=5D=2EFS11 =7B\\r\\n\\tfont-size: 11px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS12 =7B\\r\\n\\tfont-size: 12px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS13 =7B\\r\\n\\tfont-size: 13px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS16 =7B\\r\\n\\tfont-size: 16px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EFS22 =7B\\r\\n\\tfont-size: 22px =21important;\\r\\n=7D\\r\\n/* LINE HEIGHTS */\\r\\n*=5Bclass=5D=2ELH13 =7B\\r\\n\\tline-height: 13px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELH17 =7B\\r\\n\\tline-height: 17px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELH18 =7B\\r\\n\\tline-height: 18px =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2ELH24 =7B\\r\\n\\tline-height: 24px =21important;\\r\\n=7D\\r\\n/* COLOR */\\r\\n*=5Bclass=5D=2EbgFFF =7B\\r\\n\\tbackground-color: =23FFFFFF =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebg888 =7B\\r\\n\\tbackground-color: =23888888 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EfcFFF =7B\\r\\n\\tcolor: =23FFFFFF =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebgedb72b =7B\\r\\n\\tbackground-color: =23edb72b =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2EbgAAA =7B\\r\\n\\tbackground-color: =23aaaaaa =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ebg039 =7B\\r\\n\\tbackground-color: =23003399 =21important;\\r\\n=7D\\r\\n/* FONT STYLES */\\r\\n*=5Bclass=5D=2EFWbold =7B\\r\\n\\tfont-weight: bold =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Etdnone =7B\\r\\n\\ttext-decoration: none =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Enavlink:after =7B\\r\\n\\tcontent: =22 =7C=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Eorgin:after =7B\\r\\n\\tcontent: =22/Destination=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehotel:after =7B\\r\\n\\tcontent: =22/Car=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ehotel2:after =7B\\r\\n\\tcontent: =22Hotel=22 =21important;\\r\\n=7D\\r\\n*=5Bclass=5D=2Ecar:after =7B\\r\\n\\tcontent: =22Car=22 =21important;\\r\\n=7D\\r\\ntable =7B\\r\\n\\tborder-spacing:0px =21important;\\r\\n\\tborder-collapse:collapse =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dwidth100=5D =7B\\r\\n\\tfloat: none =21important;\\r\\n\\twidth: 100% =21important;\\r\\n\\theight: auto =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dwidth95=5D =7B\\r\\n\\tfloat: none =21important;\\r\\n\\twidth: 95% =21important;\\r\\n\\theight: auto =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dwidth90=5D =7B\\r\\n\\tfloat: none =21important;\\r\\n\\twidth: 90% =21important;\\r\\n\\theight: auto =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3Dspace25px=5D =7B\\r\\n\\tdisplay: block =21important;\\r\\n\\theight: 25px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22383=22=5D, *=5Bwidth=3D=22343=22=5D, *=5Bwidth=3D=22230=22=\\r\\n=5D =7B\\r\\n\\twidth:300px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22620=22=5D =7B\\r\\n\\twidth:290px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22492=22=5D =7B\\r\\n\\twidth:170px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=2220=22=5D =7B\\r\\n\\tdisplay:block =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22300px_width=22=5D =7B\\r\\n\\twidth:300px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22320px_width=22=5D =7B\\r\\n\\twidth:320px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=228px_height=22=5D =7B\\r\\n\\tfont-size:8px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=2230px_height=22=5D =7B\\r\\n\\tfont-size:30px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22hide=22=5D =7B\\r\\n\\tdisplay:none =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22mp_number_wrapper=22=5D =7B\\r\\n\\twidth:320px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22mp_number=22=5D =7B\\r\\n\\tfont-size:11px =21important;\\r\\n\\twidth:300px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22margin_right_20=22=5D =7B\\r\\n\\tmargin-right:20px =21important;\\r\\n=7D\\r\\n*=5Bwidth=3D=22594=22=5D, *=5Bwidth=3D=22306=22=5D, *=5Bwidth=3D=22320=22=\\r\\n=5D =7B\\r\\n\\twidth:280px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22collapse10=22=5D =7B\\r\\n\\twidth:10px =21important;\\r\\n\\theight:15px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22unHide=22=5D =7B\\r\\n\\tvisibility:visible =21important;\\r\\n\\tdisplay:block =21important;\\r\\n\\twidth:300px =21important;\\r\\n\\theight:auto =21important;\\r\\n\\toverflow:visible =21important;\\r\\n\\tclear:both =21important;\\r\\n\\tvisibility:visible =21important;\\r\\n\\tline-height:normal =21important;\\r\\n\\tfont-size:inherit =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22headline=22=5D =7B\\r\\n\\ttext-align:center =21important;\\r\\n\\twidth:280px =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBorder=22=5D =7B\\r\\n\\tvisibility:visible =21important;\\r\\n\\tdisplay:block =21important;\\r\\n\\twidth:10px =21important;\\r\\n\\theight:auto =21important;\\r\\n\\toverflow:visible =21important;\\r\\n\\tclear:both =21important;\\r\\n\\tvisibility:visible =21important;\\r\\n\\tline-height:normal =21important;\\r\\n\\tfont-size:inherit =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBoderColor1=22=5D =7B\\r\\n\\tbackground:=23002859 =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBoderColor2=22=5D =7B\\r\\n\\tbackground:=23394a58 =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22heroBoderColor3=22=5D =7B\\r\\n\\tbackground:=23015282 =21important;\\r\\n=7D\\r\\n=23CTA =7B\\r\\n\\tmargin:auto =21important;\\r\\n=7D\\r\\n*=5Bclass=7E=3DCTA=5D =7B\\r\\n\\twidth: 80% =21important;\\r\\n\\tmax-width: 300px =21important;\\r\\n\\theight: 40px =21important;\\r\\n\\tmargin: auto =21important;\\r\\n=7D\\r\\n*=5Bclass=3D=22starAlliance=22=5D =7Bfloat: left =21important; height: 40px=\\r\\n =21important; margin-top: 10px =21important; text-align:left =21important;=\\r\\n=7D\\r\\n*=5Bwidth=3D=22445=22=5D =7Bwidth:150px =21important;=7D\\r\\n*=5Bclass=3D=22outlookFix=22=5D =7Bfont-size:1px =21important; line-height:=\\r\\n1px =21important;=7D\\r\\n=7D</style>\\r\\n<meta http-equiv=3D=22Content-Type=22 content=3D=22text/html; charset=3Dutf=\\r\\n-8=22>\\r\\n<=21-- LANGUAGE GLOBAL VARIABLE=20\\r\\nDefault is EN\\r\\nTo pull language code from field in Unica file use setglobalvars(LANGUAGE,l=\\r\\nookup(GENERIC05))\\r\\nTo pull language code from CELL_CODE use setglobalvars(LANGUAGE,cond(eq(loo=\\r\\nkup(CELL_CODE),ET01),CH,cond(eq(lookup(CELL_CODE),ET02),DE,cond(eq(lookup(C=\\r\\nELL_CODE),ET03),EN,cond(eq(lookup(CELL_CODE),ET04),ES,cond(eq(lookup(CELL_C=\\r\\nODE),ET05),FR,cond(eq(lookup(CELL_CODE),ET06),JA,cond(eq(lookup(CELL_CODE),=\\r\\nET07),PT,cond(eq(lookup(CELL_CODE),ET08),KO,cond(eq(lookup(CELL_CODE),ET09)=\\r\\n,TCH,EN))))))))),EN)\\r\\nTo pull language code from Master Contacts List use setglobalvars(LANGUAGE,=\\r\\nlookup(LANG_CD))\\r\\n-->\\r\\n\\r\\n<=21-- OPT GLOBAL VARIABLE=20\\r\\nDefault is OPT_TYPE from Unica file\\r\\nFor MileagePlus ADMN/NONM campaigns use setglobalvars(OPT,MPPR)\\r\\nFor United ADMN/NONM campaigns use setglobalvars(OPT,UADL)\\r\\nYou will need to change the From and Reply To fields on the Campaign Dashbo=\\r\\nard\\r\\n-->\\r\\n\\r\\n<=21-- COUNTRY GLOBAL VARIABLE=20\\r\\nDefault is COUNTRY_ from the Master Contacts List\\r\\nFor ADMN campaigns use setglobalvars(COUNTRY,lookup(ISO_CNTRY_CD))\\r\\nFor NONM campaigns use setglobalvars(COUNTRY,US)\\r\\nor whichever country is appropriate\\r\\n-->\\r\\n\\r\\n<=21-- MEMBER_LEVEL GLOBAL VARIABLE=20\\r\\nDefault is ELITE_LEVEL from the Master Contacts List\\r\\nFor ADMN campaigns use setglobalvars(MEMBER_LEVEL,lookup(MP_PGM_MBRSP_LVL_C=\\r\\nD))\\r\\nFor NONM campaigns use setglobalvars(MEMBER_LEVEL,0)\\r\\nor whichever member level is appropriate\\r\\n-->\\r\\n\\r\\n<=21-- POS GLOBAL VARIABLE=20\\r\\nThis variable is passed to the POS=3D attribute in the link table\\r\\n-->\\r\\n\\r\\n<=21-- SECOND LANGUAGE GLOBAL VARIABLE=20\\r\\nUsed by Language Toggle\\r\\nDefault is EN\\r\\nTo set second language by CELL_CODE use\\r\\nsetglobalvars(SCND_LANG,cond(lookup(LANG_TOGGLE_INDICATOR),select(lookup(CE=\\r\\nLL_CODE),ET01,CH,ET02,DE,ET03,EN,ET04,ES,ET05,FR,ET06,JA,ET07,PT,ET08,KO,ET=\\r\\n09,TCH),nothing()))\\r\\n-->\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n</head>\\r\\n<body style=3D=22margin:0; padding:0;=22 vlink=3D=22=2362a9e3=22>\\r\\n<table width=3D=22100%=22 border=3D=220=22 align=3D=22center=22 cellpadding=\\r\\n=3D=220=22 cellspacing=3D=220=22>\\r\\n  <tr>\\r\\n    <td align=3D=22center=22 valign=3D=22top=22><=21-- Main Container -->\\r\\n     =20\\r\\n      <table width=3D=22670=22 border=3D=220=22 align=3D=22center=22 cellpa=\\r\\ndding=3D=220=22 cellspacing=3D=220=22 class=3D=22width320=22>\\r\\n        <tr class=3D=22hide=22>\\r\\n          <td colspan=3D=225=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=\\r\\n=22 height=3D=221=22 style=3D=22line-height:1px; font-size:0px;=22><img src=\\r\\n=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/uni=\\r\\nted/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 border=3D=220=\\r\\n=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n        <tr>\\r\\n          <td width=3D=221=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=22>=\\r\\n<img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/con=\\r\\ntent/united/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 borde=\\r\\nr=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n          <td width=3D=229=22 class=3D=22hide=22><img src=3D=22https://stat=\\r\\nic=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/united/sp=2Epng=22 wi=\\r\\ndth=3D=229=22 height=3D=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22dis=\\r\\nplay:block;=22 /></td>\\r\\n          <td width=3D=22650=22 align=3D=22center=22 valign=3D=22top=22 bgc=\\r\\nolor=3D=22=23e6e6e6=22 class=3D=22width320=22><=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <=21--/margin -->\\r\\n           =20\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n              <tr>\\r\\n                <td colspan=3D=223=22 align=3D=22center=22 valign=3D=22top=\\r\\n=22><=21-- PREHEADER -->\\r\\n                 =20\\r\\n                  <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=\\r\\n=220=22 cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n                    <tr>\\r\\n                      <td align=3D=22left=22 valign=3D=22top=22 bgcolor=3D=\\r\\n=22=23e6e6e6=22 class=3D=22width320 leftpad20 rightpad10=22 style=3D=22padd=\\r\\ning-left:20px;=22><=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:10=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin -->\\r\\n                       =20\\r\\n                        <div style=3D=22font-family: Arial&=2344;Helvetica&=\\r\\n=2344;sans-serif; color:=23333333; font-size:10px; line-height:12px; font-w=\\r\\neight:normal;=22 class=3D=22centercenter FS11 LH13 hide=22> You and one com=\\r\\npanion get a free checked bag when you purchase United tickets with your Mi=\\r\\nleagePlus Explorer Card=2E </div>\\r\\n                       =20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:10=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin -->\\r\\n                       =20\\r\\n                        <div style=3D=22font-family: Arial, Helvetica, sans=\\r\\n-serif; color:=23333333; font-size:10px; line-height:12px; font-weight:norm=\\r\\nal;=22 class=3D=22FS11 LH13=22>To ensure delivery to your inbox, please add=\\r\\n <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJ=\\r\\nlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWU=\\r\\nWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-c=\\r\\nVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YR=\\r\\nyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22 =\\r\\nstyle=3D=22color:=23333333; font-weight:normal; text-decoration:underline;=\\r\\n=22>MileagePlus_Partner=40news=2Eunited=2Ecom</a> to your address&nbsp;book=\\r\\n=2E</div></td>\\r\\n                      <td align=3D=22right=22 valign=3D=22bottom=22 bgcolor=\\r\\n=3D=22=23e6e6e6=22 width=3D=22130=22 class=3D=22hide=22 style=3D=22padding-=\\r\\nright:10px;=22><div style=3D=22font-family: Arial, Helvetica, sans-serif; c=\\r\\nolor:=23333333; font-size:10px; line-height:12px; font-weight:normal;=22><a=\\r\\n href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQ=\\r\\nGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWUAT&=\\r\\n_ei_=3DEusfb9GiCXrnzcSn25GQITmX9wd2Vl2kuyeVxNalGdM0ReCCx4wn-gYweqXvmG6lKyVz=\\r\\n29dov_gysMf5xaXmMMLwtYXCUXXfpgTGR_D9oRHK79lTz7R2MJ_ORtdrCugqfOLD2CKIqeVvHiC=\\r\\nwQoQU3LL7rs7rFLCHeOe_qdCC7WGcJm4226ZWHuTYTP8nrc7JIMmIYAWPQOKQX-s3p8l1oz_tZK=\\r\\n1BIgIgqJuQVcCsj7ixKFCwoNVE1UGMtOf1eUic_sa8hgmielJyA4aY4GEdwFzRCYhDAaue6lRRt=\\r\\nr9kFYF00sQwKk-FjxR3lrA1PjIPdrtNsuUj7hdf_djk3ifA-g-o4eeAW4qY0Sw5M-Op0FIiI7K_=\\r\\ndYiIx3W-7wS6tN9171PDMMRh5X7DLul43qXzjzY6EfLOMMfYpQbMOmyqOZFrThXP2Yimg5R_aLL=\\r\\n0NnX_-7RqlXpmvLt_XbuCnWUV07scLZDr97F_mssl1JX_EAi1_8L_ijsR2kMUXGwDUfv21j71-I=\\r\\nDtcuUgdqFxoffNzaOtywWZf23a2O9hL3vy0Dpz89Z9nQWr5GnZiBmJjhcgR3Xz8j46Zc_qIqDmj=\\r\\nX5URrxWRrA5pPQRINk=2E=22 style=3D=22color:=23333333; font-weight:normal; te=\\r\\nxt-decoration:underline;=22 target=3D=22_blank=22>View in Web browser</a> <=\\r\\n/div>\\r\\n                       =20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:10=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin --></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                 =20\\r\\n                  <=21--/PREHEADER --></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21-- UNITED MILEAGEPLUS HEADER -->=20\\r\\n           =20\\r\\n            <=21--/UNITED MILEAGEPLUS HEADER -->=20\\r\\n            <=21-- MODULES GO HERE -->=20\\r\\n            <=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/margin -->=20\\r\\n            <=21-- Hero Module -->=20\\r\\n           =20\\r\\n<table width=3D=22660=22 class=3D=22320px_width=22 border=3D=220=22 cellspa=\\r\\ncing=3D=220=22 cellpadding=3D=220=22 bgcolor=3D=22=23ffffff=22>\\r\\n        <tr>\\r\\n          <td width=3D=22660=22 class=3D=22320px_width=22 align=3D=22center=\\r\\n=22><=21-- Begin modules -->\\r\\n            <div style=3D=22font-size:10px; line-height:10px;=22 class=3D=\\r\\n=22hide=22>&nbsp;</div>\\r\\n<table width=3D=22640=22 border=3D=220=22 cellspacing=3D=220=22 cellpadding=\\r\\n=3D=220=22  bgcolor=3D=22=23394a58=22 class=3D=22width100=22>\\r\\n              <tr>\\r\\n                <td width=3D=2223=22 bgcolor=3D=22=23394a58=22 class=3D=22h=\\r\\nide=22>&nbsp;</td>\\r\\n                <td width=3D=22594=22 valign=3D=22bottom=22 bgcolor=3D=22=\\r\\n=23394a58=22><table width=3D=2295%=22 border=3D=220=22 cellspacing=3D=220=\\r\\n=22 cellpadding=3D=220=22 align=3D=22left=22 style=3D=22float:left;=22 clas=\\r\\ns=3D=22width95=22>\\r\\n                    <tr>\\r\\n                      <td align=3D=22left=22 valign=3D=22top=22 bgcolor=3D=\\r\\n=22=23394a58=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-s=\\r\\nize:20px; line-height:24px; font-weight:bold; color:=23ffffff;=22 class=3D=\\r\\n=22headline=22><div style=3D=22font-size:20px; line-height:20px;=22>&nbsp;<=\\r\\n/div>\\r\\n                        Free checked bag for you and one companion\\r\\n                      <div style=3D=22font-size:20px; line-height:20px;=22>=\\r\\n&nbsp;</div></td>\\r\\n                    </tr>\\r\\n                  </table></td>\\r\\n                <td width=3D=2223=22 bgcolor=3D=22=23394a58=22 class=3D=22h=\\r\\nide=22>&nbsp;</td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22width100=22>\\r\\n              <tr>\\r\\n                <td width=3D=2210=22>&nbsp;</td>\\r\\n                <td valign=3D=22top=22><table width=3D=22640=22 border=3D=\\r\\n=220=22 cellspacing=3D=220=22 cellpadding=3D=220=22 class=3D=22width100=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=2223=22 class=3D=22hide=22>&nbsp;</td>\\r\\n                      <td width=3D=22594=22 valign=3D=22top=22 class=3D=22w=\\r\\nidth100=22><table width=3D=22254=22 border=3D=220=22 cellspacing=3D=220=22 =\\r\\ncellpadding=3D=220=22 align=3D=22right=22 class=3D=22width100=22 style=3D=\\r\\n=22float:right;=22>\\r\\n                          <tr>\\r\\n                            <td width=3D=22225=22 height=3D=22146=22 bgcolo=\\r\\nr=3D=22=23ffffff=22 valign=3D=22top=22 class=3D=22width100=22 align=3D=22ce=\\r\\nnter=22><div style=3D=22font-size:16px;=22>&=23160;</div><a href=3D=22https=\\r\\n://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38n=\\r\\nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWCWT&_ei_=3DEqOC4jUOg=\\r\\n4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5Kb=\\r\\ngO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRp=\\r\\nIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22><img border=3D=220=\\r\\n=22 width=3D=22254=22 height=3D=22372=22 alt=3D=22=22 src=3D=22http://lacek=\\r\\n=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Update/UALCHR15104_hero_de=\\r\\nsktop_254x372_R2_ET06=2Ejpg=22 style=3D=22display: block; border:none;=22/>=\\r\\n</a></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <table width=3D=22317=22 border=3D=220=22 cellspaci=\\r\\nng=3D=220=22 cellpadding=3D=220=22 align=3D=22left=22 style=3D=22float:left=\\r\\n;=22 class=3D=22width100=22>\\r\\n                          <tr>\\r\\n                            <td valign=3D=22top=22 align=3D=22left=22 style=\\r\\n=3D=22font-family:Arial, Helvetica, sans-serif; font-size:13px; color:=2333=\\r\\n3333;=22 class=3D=22=22><div style=3D=22font-size:16px;=22>&=23160;</div>\\r\\n                             =20\\r\\n                              <=21-- Body Copy=2E -->Your United MileagePlu=\\r\\ns&reg; Explorer Card provides a free first standard checked bag* for you (t=\\r\\nhe primary Cardmember) and one companion on United&reg;-operated flights &m=\\r\\ndash; a savings of up to =24100 per roundtrip=2E\\r\\n                              <div style=3D=22font-size:8px; line-height:no=\\r\\nrmal;=22>&nbsp;</div>\\r\\n                              As a reminder, to receive this benefit you ne=\\r\\ned to:\\r\\n                              <div style=3D=22font-size:6px; line-height:no=\\r\\nrmal;=22>&nbsp;</div>\\r\\n                             =20\\r\\n                              <=21-- Begin Bulleted List -->\\r\\n                             =20\\r\\n                              <table width=3D=22300=22 border=3D=220=22 cel=\\r\\nlspacing=3D=220=22 cellpadding=3D=220=22 style=3D=22font-family:Arial, Helv=\\r\\netica, sans-serif; font-size:13px; color:=23333333;=22 class=3D=22width90=\\r\\n=22>\\r\\n                                <tr>\\r\\n                                  <td width=3D=2210=22 rowspan=3D=227=22 va=\\r\\nlign=3D=22top=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/spacer=2Egif=22 width=3D=2210=22 height=3D=2210=22 =\\r\\nalt=3D=22=22 style=3D=22display:block;=22 border=3D=220=22 /></td>\\r\\n                                  <td width=3D=228=22 align=3D=22left=22 va=\\r\\nlign=3D=22top=22><strong>&bull;</strong></td>\\r\\n                                  <td width=3D=22299=22 align=3D=22left=22 =\\r\\nvalign=3D=22top=22>Include your MileagePlus number in your reservation\\r\\n                                    <div style=3D=22font-size:4px; line-hei=\\r\\nght:4px;=22>&nbsp;</div></td>\\r\\n                                </tr>\\r\\n                                <tr>\\r\\n                                  <td width=3D=228=22 align=3D=22left=22 va=\\r\\nlign=3D=22top=22><strong>&bull;</strong></td>\\r\\n                                  <td width=3D=22299=22 align=3D=22left=22 =\\r\\nvalign=3D=22top=22>Purchase your ticket(s) with your Explorer Card</td>\\r\\n                                </tr>\\r\\n                              </table>\\r\\n                             =20\\r\\n                              <=21-- Ended Bulleted List -->\\r\\n                             =20\\r\\n                              <div style=3D=22font-size:18px; line-height:n=\\r\\normal;=22>&nbsp;</div>\\r\\n                              <table border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22CTA=22>\\r\\n                                <tr>\\r\\n                                  <td  bgcolor=3D=22=23394a58=22  align=3D=\\r\\n=22center=22 class=3D=22CTA=22><table width=3D=22180=22 border=3D=220=22 ce=\\r\\nllpadding=3D=220=22 cellspacing=3D=220=22>\\r\\n                                      <tr>\\r\\n                                        <td style=3D=22text-align:center;=\\r\\n=22><a target=3D=22_blank=22 style=3D=22font-family:Arial,Helvetica,sans-se=\\r\\nrif;color:=23ffffff;text-decoration:none;font-size:14px;font-weight:bold;=\\r\\n=22 href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJ=\\r\\nlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWC=\\r\\nWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-c=\\r\\nVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YR=\\r\\nyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22>See more details</a></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                    </table></td>\\r\\n                                  <td height=3D=2240=22 class=3D=22hide=22>=\\r\\n<img style=3D=22display:block;=22 src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/=\\r\\no33/UAL/email/2016/16087_FOP_Update/sp=2Epng=22 alt=3D=22=22 width=3D=225=\\r\\n=22 height=3D=2240=22 /></td>\\r\\n                                </tr>\\r\\n                              </table></td>\\r\\n                          </tr>\\r\\n                        </table></td>\\r\\n                      <td width=3D=2223=22 class=3D=22hide=22>&nbsp;</td>\\r\\n                    </tr>\\r\\n                  </table></td>\\r\\n                <td width=3D=2210=22>&nbsp;</td>\\r\\n              </tr>\\r\\n            </table><=21-- End modules --></td>\\r\\n        </tr>\\r\\n        <tr>\\r\\n          <td ><div style=3D=22font-size:24px;=22>&nbsp;</div>\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22  class=3D=22width100=22>\\r\\n              <tr>\\r\\n                <td width=3D=22660=22><table width=3D=22203=22 border=3D=22=\\r\\n0=22 cellspacing=3D=220=22 cellpadding=3D=220=22 align=3D=22left=22 style=\\r\\n=3D=22float:left;=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=2223=22>&nbsp;</td>\\r\\n                      <td width=3D=2261=22 height=3D=2225=22><div style=3D=\\r\\n=22font-size:4px; line-height:4px;=22>&nbsp;</div>\\r\\n                        <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri=\\r\\n_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8=\\r\\nc8FY4FCofVXtpKX%3DUSAYCCT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBt=\\r\\nauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoN=\\r\\naRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22=\\r\\n target=3D=22_blank=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UA=\\r\\nL/email/2016/16087_FOP_Update/fb-like=2Epng=22 alt=3D=22Like=22 width=3D=22=\\r\\n60=22 height=3D=2225=22 border=3D=220=22 /></a><a href=3D=22https://news=2E=\\r\\nunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zey=\\r\\ngb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAYCCT&_ei_=3DEqOC4jUOg4vyD36DYMR=\\r\\nhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iut=\\r\\nZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay=\\r\\n-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22></a></td>\\r\\n                      <td width=3D=2211=22>&nbsp;</td>\\r\\n                      <td width=3D=22108=22 height=3D=2225=22><div style=3D=\\r\\n=22font-size:4px; line-height:4px;=22> &nbsp; </div>\\r\\n                        <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri=\\r\\n_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8=\\r\\nc8FY4FCofVXtpKX%3DUSAYCCT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBt=\\r\\nauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoN=\\r\\naRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22=\\r\\n target=3D=22_blank=22 style=3D=22font-family:Arial, Helvetica, sans-serif;=\\r\\n font-size:12px; font-weight:bold; color:=23333333; text-decoration:none;=\\r\\n=22>MileagePlus Cards by Chase</a></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                  <table width=3D=22288=22 border=3D=220=22 cellspacing=3D=\\r\\n=220=22 cellpadding=3D=220=22 align=3D=22right=22 style=3D=22float:right;=\\r\\n=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=22123=22>&nbsp;</td>\\r\\n                      <td width=3D=22142=22><div style=3D=22font-size:2px; =\\r\\nline-height:2px=22 class=3D=22space25px=22> &nbsp; </div>\\r\\n                        <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri=\\r\\n_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8=\\r\\nc8FY4FCofVXtpKX%3DUSAWWWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBt=\\r\\nauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoN=\\r\\naRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22=\\r\\n target=3D=22_blank=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UA=\\r\\nL/email/2016/16087_FOP_Update/MileagePlus-logo=2Egif=22 width=3D=22141=22 h=\\r\\neight=3D=2237=22 alt=3D=22MileagePlus - United=22 border=3D=220=22 /></a></=\\r\\ntd>\\r\\n                      <td width=3D=2223=22>&nbsp;</td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td colspan=3D=229=22 style=3D=22font-size:1px;=22><i=\\r\\nmg src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Up=\\r\\ndate/spacer=2Egif=22 width=3D=222=22 height=3D=221=22 alt=3D=22=22 style=3D=\\r\\n=22display:block;=22 border=3D=220=22 /></td>\\r\\n                    </tr>\\r\\n                </table></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td height=3D=2215=22 style=3D=22clear:both;=22><img src=3D=\\r\\n=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Update/spac=\\r\\ner=2Egif=22 width=3D=221=22 height=3D=2210=22 alt=3D=22=22 style=3D=22displ=\\r\\nay:block;=22 border=3D=220=22 /></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td bgcolor=3D=22=23f1f1f1=22 style=3D=22font-size:1px;=22>=\\r\\n<img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_=\\r\\nUpdate/spacer=2Egif=22 width=3D=222=22 height=3D=221=22 alt=3D=22=22 style=\\r\\n=3D=22display:block;=22 border=3D=220=22 /></td>\\r\\n              </tr>\\r\\n            </table></td>\\r\\n        </tr>\\r\\n        <tr>\\r\\n          <td ><div style=3D=22font-size:10px;=22>&nbsp;</div>\\r\\n                <=21-- Begin Social Media -->\\r\\n               =20\\r\\n\\r\\n <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22 cellpaddin=\\r\\ng=3D=220=22 class=3D=22320px_width=22>\\r\\n  <tr>\\r\\n    <td align=3D=22right=22 width=3D=22470=22 style=3D=22font-family: Arial=\\r\\n, Helvetica, sans-serif; color:=23333333; font-size:10px; line-height:14px;=\\r\\n font-weight:normal;=22>Stay connected to United</td>\\r\\n    <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=22190=22><table =\\r\\nborder=3D=220=22 cellspacing=3D=220=22 cellpadding=3D=220=22>\\r\\n        <tr>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWWAT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2mobile=2Ejpg=22 width=3D=2225=22 height=3D=\\r\\n=2225=22 alt=3D=22Mobile=22 border=3D=220=22 style=3D=22display:block;=22> =\\r\\n</a></td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWYRT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2hub=2Ejpg=22 width=3D=2225=22 height=3D=222=\\r\\n5=22 alt=3D=22Hub=22 border=3D=220=22 style=3D=22display:block;=22> </a></t=\\r\\nd>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2211=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWYWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2fb=2Ejpg=22 width=3D=2225=22 height=3D=2225=\\r\\n=22 alt=3D=22Facebook=22 border=3D=220=22 style=3D=22display:block;=22> </a=\\r\\n></td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWYCT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2tw=2Ejpg=22 width=3D=2225=22 height=3D=2225=\\r\\n=22 alt=3D=22Twitter=22 border=3D=220=22 style=3D=22display:block;=22> </a>=\\r\\n</td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWATT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2instagram=2Ejpg=22 width=3D=2225=22 height=\\r\\n=3D=2225=22 alt=3D=22Instagram=22 border=3D=220=22 style=3D=22display:block=\\r\\n;=22> </a></td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2225=22 he=\\r\\night=3D=2225=22><a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gz=\\r\\nc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCo=\\r\\nfVXtpKX%3DUSAWAWT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-E=\\r\\nHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD=\\r\\n4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=\\r\\n=3D=22_blank=22> <img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/emai=\\r\\nl/2016/16087_FOP_Update/social_2yt=2Ejpg=22 width=3D=2225=22 height=3D=2225=\\r\\n=22 alt=3D=22YouTube=22 border=3D=220=22 style=3D=22display:block;=22> </a>=\\r\\n</td>\\r\\n          <td align=3D=22right=22 valign=3D=22bottom=22 width=3D=2212=22><a=\\r\\n href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQ=\\r\\nGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWACT&=\\r\\n_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbf=\\r\\ntB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLm=\\r\\nivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22> <i=\\r\\nmg src=3D=22http://lacek=2Evo=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Up=\\r\\ndate/social_2li=2Ejpg=22 width=3D=2225=22 height=3D=2225=22 alt=3D=22Linked=\\r\\nIn=22 border=3D=220=22 style=3D=22display:block;=22></a></td>\\r\\n          <td width=3D=228=22><img src=3D=22http://lacek=2Evo=2Ellnwd=2Enet=\\r\\n/o33/UAL/email/2016/16087_FOP_Update/sp=2Epng=22 width=3D=228=22 height=3D=\\r\\n=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n      </table></td>\\r\\n  </tr>\\r\\n</table>\\r\\n\\r\\n                <=21-- End Social Media -->\\r\\n            <div style=3D=22font-size:10px;=22>&nbsp;</div>\\r\\n            <div style=3D=22font-size:7px; background-color:=23394a58=22>&n=\\r\\nbsp;</div></td>\\r\\n        </tr>\\r\\n      </table>=20\\r\\n            <=21-- /Hero Module -->=20\\r\\n            <=21-- margin -->\\r\\n           =20\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/margin -->=20\\r\\n            <=21-- margin -->\\r\\n           =20\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/margin -->=20\\r\\n            <=21--/MODULES GO HERE -->=20\\r\\n            <=21-- FOOTER -->=20\\r\\n            <=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <=21--/margin -->=20\\r\\n            <=21-- global links -->\\r\\n           =20\\r\\n            <table width=3D=22650=22 border=3D=220=22 cellpadding=3D=220=22=\\r\\n cellspacing=3D=220=22 class=3D=22width320=22>\\r\\n              <tr>\\r\\n                <td width=3D=22650=22 align=3D=22center=22 valign=3D=22midd=\\r\\nle=22 bgcolor=3D=22=23e6e6e6=22 class=3D=22hide=22><=21-- margin -->\\r\\n                  <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=\\r\\n=3D=220=22>\\r\\n                    <tr>\\r\\n                      <td height=3D=2210=22 style=3D=22line-height:15px; fo=\\r\\nnt-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/res=\\r\\nponsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 a=\\r\\nlt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                  <=21--/margin --></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D=22650=22 align=3D=22center=22 valign=3D=22midd=\\r\\nle=22 bgcolor=3D=22=23e6e6e6=22 class=3D=22width320 fl wrap center=22><div =\\r\\nstyle=3D=22font-family: Arial, Helvetica, sans-serif; color:=23010000; font=\\r\\n-size:10px; line-height:12px; font-weight:normal;=22 class=3D=22FS11 LH17 F=\\r\\nWbold fcFFF bg888 LR5 TB10=22> <a href=3D=22https://news=2Eunited=2Ecom/pub=\\r\\n/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqg=\\r\\nrqMzbK8c8FY4FCofVXtpKX%3DUSAWBRT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zx=\\r\\nG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3Uwzd=\\r\\nIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq=\\r\\n0=2E=22 style=3D=22color:=23010000; font-weight:normal; text-decoration:und=\\r\\nerline;=22 class=3D=22fcFFF FWbold=22 target=3D=22_blank=22>united=2Ecom</a=\\r\\n>&nbsp;&nbsp; <a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2=\\r\\nX%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofV=\\r\\nXtpKX%3DUSAWBTT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHk=\\r\\nXmnwbX0CmK1-cVbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j=\\r\\n7kS1QrfoMc_YRyLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=\\r\\n=22color:=23010000; font-weight:normal; text-decoration:underline;=22 class=\\r\\n=3D=22fcFFF FWbold=22 target=3D=22_blank=22>Partner offers</a>&nbsp;&nbsp; =\\r\\n<a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJl=\\r\\nTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWBW=\\r\\nT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cV=\\r\\nbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRy=\\r\\nLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=22color:=23010=\\r\\n000; font-weight:normal; text-decoration:underline;=22 class=3D=22fcFFF FWb=\\r\\nold=22 target=3D=22_blank=22>Change email address</a>&nbsp;&nbsp; <a href=\\r\\n=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzb=\\r\\nltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWBCT&_ei_=\\r\\n=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9o=\\r\\nrEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBd=\\r\\nhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=22color:=23010000; f=\\r\\nont-weight:normal; text-decoration:underline;=22 class=3D=22fcFFF FWbold=22=\\r\\n target=3D=22_blank=22>Update email preferences</a>&nbsp;&nbsp; <a href=3D=\\r\\n=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltf=\\r\\nLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWCRT&_ei_=3DE=\\r\\nqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cVbftB9orEhC=\\r\\nWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRyLmivBdhyx6=\\r\\nV38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 style=3D=22color:=23010000; font-=\\r\\nweight:normal; text-decoration:underline;=22 class=3D=22fcFFF FWbold=22 tar=\\r\\nget=3D=22_blank=22>Privacy&nbsp;policy</a> </div></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D=22650=22 align=3D=22center=22 valign=3D=22midd=\\r\\nle=22 bgcolor=3D=22=23e6e6e6=22 class=3D=22hide=22><=21-- margin -->\\r\\n                  <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=\\r\\n=3D=220=22>\\r\\n                    <tr>\\r\\n                      <td height=3D=2210=22 style=3D=22line-height:15px; fo=\\r\\nnt-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/res=\\r\\nponsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 a=\\r\\nlt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                    </tr>\\r\\n                  </table>\\r\\n                  <=21--/margin --></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21--/global links -->\\r\\n           =20\\r\\n            <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=220=22=\\r\\n cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n              <tr>\\r\\n                <td bgcolor=3D=22=23e6e6e6=22>\\r\\n                  <table width=3D=22660=22 border=3D=220=22 cellspacing=3D=\\r\\n=220=22 cellpadding=3D=220=22 class=3D=22width320=22>\\r\\n                    <tr>\\r\\n                      <td width=3D=2230=22 align=3D=22right=22 valign=3D=22=\\r\\ntop=22 class=3D=22width20=22><img src=3D=22https://static=2Ecdn=2Eresponsys=\\r\\n=2Enet/i2/responsysimages/content/united/sp=2Epng=22 width=3D=2210=22 heigh=\\r\\nt=3D=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></=\\r\\ntd>\\r\\n                      <td width=3D=22600=22 align=3D=22left=22 valign=3D=22=\\r\\ntop=22 class=3D=22width290=22><=21-- legal -->=20\\r\\n                       =20\\r\\n<div style=3D=22font-family: Arial, Helvetica, sans-serif; color:=23333333;=\\r\\n font-size:10px; line-height:16px; font-weight:normal;=22><strong>Terms and=\\r\\n conditions:</strong><br />\\r\\n*FREE CHECKED BAG: The primary Cardmember and one traveling companion on th=\\r\\ne same reservation are each eligible to receive their first standard checke=\\r\\nd bag free; authorized users are only eligible if they are on the same rese=\\r\\nrvation as the primary Cardmember=2E To receive first standard checked bag =\\r\\nfree, the primary Cardmember must include their MileagePlus&reg; number in =\\r\\ntheir reservation and use their MileagePlus Explorer Card to purchase their=\\r\\n ticket(s)=2E First standard checked bag free is only available on United&r=\\r\\neg;- and United Express&reg;-operated flights; codeshare partner-operated f=\\r\\nlights are not eligible=2E Service charges for oversized, overweight and ex=\\r\\ntra baggage may apply=2E Cardmembers who are already exempt from other chec=\\r\\nked baggage service charges will not receive an additional free standard ch=\\r\\necked bag=2E Chase is not responsible for the provision of, or failure to p=\\r\\nrovide, the stated benefits=2E Please visit <a href=3D=22http://www=2Eunite=\\r\\nd=2Ecom/chasebag=22 target=3D=22_blank=22 style=3D=22color:=23333333; text-=\\r\\ndecoration:underline;=22>united=2Ecom/chasebag</a> for details=2E<br />\\r\\n<br />\\r\\n<strong>MileagePlus terms and conditions:</strong><br />\\r\\nMiles accrued, awards, and benefits issued are subject to change and are su=\\r\\nbject to the rules of the United MileagePlus program, including without lim=\\r\\nitation the Premier&reg; program (the =22MileagePlus Program=22), which are=\\r\\n expressly incorporated herein=2E Please allow 6&ndash;8 weeks after comple=\\r\\nted qualifying activity for miles to post to your account=2E United may cha=\\r\\nnge the MileagePlus Program including, but not limited to, rules, regulatio=\\r\\nns, travel awards and special offers or terminate the MileagePlus Program a=\\r\\nt any time and without notice=2E United and its subsidiaries, affiliates an=\\r\\nd agents are not responsible for any products or services of other particip=\\r\\nating companies and partners=2E Taxes and fees related to award travel are =\\r\\nthe responsibility of the member=2E Bonus award miles, award miles and any =\\r\\nother miles earned through non-flight activity do not count toward qualific=\\r\\nation for Premier status unless expressly stated otherwise=2E The accumulat=\\r\\nion of mileage or Premier status or any other status does not entitle membe=\\r\\nrs to any vested rights with respect to the MileagePlus Program=2E All calc=\\r\\nulations made in connection with the MileagePlus Program, including without=\\r\\n limitation with respect to the accumulation of mileage and the satisfactio=\\r\\nn of the qualification requirements for Premier status, will be made by Uni=\\r\\nted Airlines and MileagePlus in their discretion and such calculations will=\\r\\n be considered final=2E Information in this communication that relates to t=\\r\\nhe MileagePlus Program does not purport to be complete or comprehensive and=\\r\\n may not include all of the information that a member may believe is import=\\r\\nant, and is qualified in its entirety by reference to all of the informatio=\\r\\nn on the <a href=3D=22http://www=2Eunited=2Ecom=22 target=3D=22_blank=22 st=\\r\\nyle=3D=22color:=23333333; text-decoration:underline;=22>united=2Ecom</a> we=\\r\\nbsite and the MileagePlus Program rules=2E United and MileagePlus are regis=\\r\\ntered service marks=2E For complete details about the MileagePlus Program, =\\r\\ngo to <a href=3D=22http://www=2Eunited=2Ecom=22 target=3D=22_blank=22 style=\\r\\n=3D=22color:=23333333; text-decoration:underline;=22>united=2Ecom</a>=2E<br=\\r\\n />\\r\\n<br />\\r\\n<a href=3D=22https://www=2Eunited=2Ecom/web/en-US/content/mileageplus/rules=\\r\\n/default=2Easpx=22 target=3D=22_blank=22 style=3D=22color:=23333333;=22>See=\\r\\n additional MileagePlus terms and conditions</a><br/>\\r\\n<br/>\\r\\nC000013567 14342 ET06<br />\\r\\n</div>=20\\r\\n                        <=21--/legal -->=20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:15=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin -->=20\\r\\n                        <=21-- footer -->=20\\r\\n                       =20\\r\\n    <=21-- footer -->\\r\\n<div style=3D=22font-family:Arial, Helvetica, sans-serif; font-size:10px; l=\\r\\nine-height:16px; color:=23333333;=22>\\r\\n<a href=3D=22https://news=2Eunited=2Ecom/pub/cc?_ri_=3DX0Gzc2X%3DYQpglLjHJl=\\r\\nTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXtpKX%3DUSAWUW=\\r\\nT&_ei_=3DEqOC4jUOg4vyD36DYMRhHem0FcWipqpn2zxG9J2dBtauwFwF-EHkXmnwbX0CmK1-cV=\\r\\nbftB9orEhCWSfeh5KbgO0bpK5iutZGGW4HGD_4e3UwzdIbOrhoNaRPzckqD4j7kS1QrfoMc_YRy=\\r\\nLmivBdhyx6V38d8CRpIai_K_O8ay-CPm1ujvxLAlnfoq0=2E=22 target=3D=22_blank=22 s=\\r\\ntyle=3D=22color:=23333333; font-weight:normal; text-decoration:underline;=\\r\\n=22>Don't miss the latest emails; add =40news=2Eunited=2Ecom</a> <br />\\r\\nYou have received this email because it includes important information rega=\\r\\nrding your MileagePlus Credit Card benefits=2E Your privacy and email prefe=\\r\\nrences are very important to us=2E If you previously opted not to receive p=\\r\\nromotional emails from MileagePlus, we will continue to respect your prefer=\\r\\nences=2E However, we will continue to send you non-promotional, service ema=\\r\\nils concerning your account, the MileagePlus program or United=2E\\r\\n<div style=3D=22font-size:5px; line-height:normal;=22>&nbsp;</div>\\r\\nThis email was sent to <a href=3D=22https://www=2Eunited=2Ecom/CMS/en-US/ac=\\r\\ncount/email/subscription/Pages/emailSubAddressBook=2Easpx=22 target=3D=22_b=\\r\\nlank=22 style=3D=22color:=23333333;=22>christine=40spang=2Ecc</a>\\r\\n<div style=3D=22font-size:5px; line-height:normal;=22>&nbsp;</div>\\r\\nPlease do not reply to this email=2E We cannot accept electronic replies to=\\r\\n this email address=2E<br />\\r\\nEmail <a href=3D=22mailto:mileageplus=40united=2Ecom=22 target=3D=22_blank=\\r\\n=22 style=3D=22color:=23333333;=22>mileageplus=40united=2Ecom</a> with any =\\r\\nquestions about your MileagePlus account or the MileagePlus program=2E <br =\\r\\n/>\\r\\n</div>\\r\\n<table width=3D=22620=22 border=3D=220=22 cellspacing=3D=220=22 cellpadding=\\r\\n=3D=220=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-size:1=\\r\\n0px; color:=23333333;=22>\\r\\n                  <tr>\\r\\n                    <td width=3D=22620=22><table width=3D=22300=22 border=\\r\\n=3D=220=22 cellspacing=3D=220=22 cellpadding=3D=220=22 align=3D=22left=22 s=\\r\\ntyle=3D=22float:left;=22>\\r\\n                      <tr>\\r\\n                        <td width=3D=22300=22 height=3D=2285=22 valign=3D=\\r\\n=22bottom=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-size=\\r\\n:10px; color:=23333333; text-align:left;=22> To contact the sender, write t=\\r\\no: <br />\\r\\n                          United MileagePlus <br />\\r\\n                          900 Grand Plaza Dr=2E<br />\\r\\nHouston, TX 77067<br />\\r\\n                          <br />\\r\\n                          &copy; 2016 United Airlines, Inc=2E All rights re=\\r\\nserved=2E</td>\\r\\n                        </tr>\\r\\n                      </table>\\r\\n                      <table width=3D=22300=22 border=3D=220=22 cellspacing=\\r\\n=3D=220=22 cellpadding=3D=220=22 align=3D=22right=22 style=3D=22float:right=\\r\\n;=22 class=3D=22starAlliance=22>\\r\\n                        <tr>\\r\\n                          <td width=3D=22300=22 height=3D=2285=22 valign=3D=\\r\\n=22bottom=22 style=3D=22font-family:Arial, Helvetica, sans-serif; font-size=\\r\\n:10px; color:=23333333; text-align:right;=22 align=3D=22right=22 class=3D=\\r\\n=22starAlliance=22><a href=3D=22https://www=2Eunited=2Ecom/web/en-US/conten=\\r\\nt/company/alliance/star=2Easpx=22_blank=22><img src=3D=22http://lacek=2Evo=\\r\\n=2Ellnwd=2Enet/o33/UAL/email/2016/16087_FOP_Update/staralliance_061713=2Egi=\\r\\nf=22 width=3D=22163=22 height=3D=2219=22 border=3D=220=22 alt=3D=22A Star A=\\r\\nlliance Member=22 /></a></td>\\r\\n                          </tr>\\r\\n                        </table></td>\\r\\n                    </tr>\\r\\n  </table>\\r\\n    <=21--/footer -->=20\\r\\n                        <=21--/footer -->=20\\r\\n                        <=21-- margin -->\\r\\n                        <table border=3D=220=22 cellpadding=3D=220=22 cells=\\r\\npacing=3D=220=22>\\r\\n                          <tr>\\r\\n                            <td height=3D=2210=22 style=3D=22line-height:15=\\r\\npx; font-size:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/=\\r\\ni2/responsysimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=221=\\r\\n0=22 alt=3D=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n                          </tr>\\r\\n                        </table>\\r\\n                        <=21--/margin --></td>\\r\\n                      <td width=3D=2220=22 class=3D=22width10=22><img src=\\r\\n=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/uni=\\r\\nted/sp=2Epng=22 width=3D=2210=22 height=3D=221=22 alt=3D=22=22 border=3D=22=\\r\\n0=22 style=3D=22display:block;=22 /></td>\\r\\n                    </tr>\\r\\n                  </table></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n           =20\\r\\n            <=21-- margin -->\\r\\n            <table border=3D=220=22 cellpadding=3D=220=22 cellspacing=3D=22=\\r\\n0=22>\\r\\n              <tr>\\r\\n                <td height=3D=2210=22 style=3D=22line-height:10px; font-siz=\\r\\ne:0px;=22><img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsys=\\r\\nimages/content/united/sp=2Epng=22 width=3D=221=22 height=3D=2210=22 alt=3D=\\r\\n=22=22 border=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n              </tr>\\r\\n            </table>\\r\\n            <=21--/margin -->=20\\r\\n            <=21--/FOOTER --></td>\\r\\n          <td width=3D=229=22 class=3D=22hide=22><img src=3D=22https://stat=\\r\\nic=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/united/sp=2Epng=22 wi=\\r\\ndth=3D=229=22 height=3D=221=22 alt=3D=22=22 border=3D=220=22 style=3D=22dis=\\r\\nplay:block;=22 /></td>\\r\\n          <td width=3D=221=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=22>=\\r\\n<img src=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/con=\\r\\ntent/united/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 borde=\\r\\nr=3D=220=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n        <tr class=3D=22hide=22>\\r\\n          <td colspan=3D=225=22 bgcolor=3D=22=237c848a=22 class=3D=22hide=\\r\\n=22 height=3D=221=22 style=3D=22line-height:1px; font-size:0px;=22><img src=\\r\\n=3D=22https://static=2Ecdn=2Eresponsys=2Enet/i2/responsysimages/content/uni=\\r\\nted/sp=2Epng=22 width=3D=221=22 height=3D=221=22 alt=3D=22=22 border=3D=220=\\r\\n=22 style=3D=22display:block;=22 /></td>\\r\\n        </tr>\\r\\n      </table>\\r\\n     =20\\r\\n      <=21-- /Main Container -->=20\\r\\n      <=21-- Retargeting/tracking pixels -->=20\\r\\n      =20\\r\\n      <=21-- /Retargeting/tracking pixels --></td>\\r\\n  </tr>\\r\\n</table><br>\\r\\n<table cellpadding=3D=220=22 cellspacing=3D=220=22 style=3D=22border: 0px; =\\r\\npadding: 0px; margin: 0px; position: absolute; display: none; float: left=\\r\\n=22>\\r\\n<tr>\\r\\n<td height=3D=221=22 style=3D=22font-size: 1px; line-height: 1px; padding: =\\r\\n0px;=22>\\r\\n<br><img height=3D=221=22 width=3D=221=22 style=3D =22padding: 0px 0px; =21=\\r\\nimportant; margin: 0px 0px; font-size: 1px; line-height: 1px;=22 src=3D=22h=\\r\\nttps://news=2Eunited=2Ecom/pub/as?_ri_=3DX0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5=\\r\\nm38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCofVXHkMX%3Dw&_ei_=3DEjaf7_rRKzx=\\r\\n0ctn6G-vCRQxQFpOo1jnhz_D7Qj8uilahE5HqTLtnYXpmd53bnyOwa5G4ok2G4SqEqZyK_i5MlB=\\r\\n4glRW1lFv87L9Bq3zhUkhE6W-I9Lls8knlgu8Rrb4Dorss_mjDo7FeWQ-JARWOws8jiyPdFbxa=\\r\\n=2E=22></img>\\r\\n<br><=21--Start BK pixel--><IMG SRC=3D=22https://tags=2Ebluekai=2Ecom/site/=\\r\\n36540?e_id_s36540=3D9729d1a3a57d2e0b0a83851c6f030a05510060f1ea36db9eb96a9c7=\\r\\n557e258ef&e_id_m36540=3Dff0d6f7a7f24eb386614ee1e768dd100=22 HEIGHT=3D=221=\\r\\n=22 WIDTH=3D=221=22><=21--End BK pixel-->\\r\\n<br><img border=3D=220=22 width=3D=221=22 height=3D=221=22 src=3D=22http://=\\r\\nib=2Eadnxs=2Ecom/getuid?http://a=2Eadrsp=2Enet/dsp/ci/2/E066u9V7s2cpsgXWlW5=\\r\\nW6jSlqYBjE7Ku_fwBQk3_LZpAwGubjwp3fzLJ52fHSFXGe1fCaI_uGKBIh0/%24UID=22></img>\\r\\n</td>\\r\\n</tr>\\r\\n</table>\\r\\n</body>\\r\\n</html>\\r\\n\\r\\n\\r\\n\",\"snippet\":\"<=21-- LANGUAGE GLOBAL VARIABLE=20 Default is EN To pull language code from field in Unica file use setglobalvars(LANGUAGE,l=\",\"unread\":true,\"starred\":false,\"date\":\"Wed, 16 Nov 2016 12:22:53 -0800\",\"folderImapUID\":346225,\"folderId\":\"test-folder-id\",\"folder\":{\"id\":\"test-folder-id\",\"account_id\":\"test-account-id\",\"object\":\"folder\",\"name\":null,\"display_name\":\"Test Folder\",\"sync_state\":{}},\"labels\":[],\"headers\":{\"delivered-to\":[\"christine@spang.cc\"],\"received\":[\"by 10.31.185.141 with SMTP id j135csp32042vkf; Wed, 16 Nov 2016 12:22:55 -0800 (PST)\",\"from omp.news.united.com (omp.news.united.com. [12.130.136.195]) by mx.google.com with ESMTP id e80si8263532ywa.331.2016.11.16.12.22.55 for <christine@spang.cc>; Wed, 16 Nov 2016 12:22:55 -0800 (PST)\",\"by omp.news.united.com id h5j01k161o48 for <christine@spang.cc>; Wed, 16 Nov 2016 12:22:49 -0800 (envelope-from <united.5765@envfrm.rsys2.com>)\",\"by omp.news.united.com id h5j01i161o4e for <christine@spang.cc>; Wed, 16 Nov 2016 12:22:48 -0800 (envelope-from <united.5765@envfrm.rsys2.com>)\"],\"x-received\":[\"by 10.129.160.81 with SMTP id x78mr4860400ywg.273.1479327775749; Wed, 16 Nov 2016 12:22:55 -0800 (PST)\"],\"return-path\":[\"<united.5765@envfrm.rsys2.com>\"],\"received-spf\":[\"pass (google.com: domain of united.5765@envfrm.rsys2.com designates 12.130.136.195 as permitted sender) client-ip=12.130.136.195;\"],\"authentication-results\":[\"mx.google.com; dkim=pass header.i=@news.united.com; spf=pass (google.com: domain of united.5765@envfrm.rsys2.com designates 12.130.136.195 as permitted sender) smtp.mailfrom=united.5765@envfrm.rsys2.com; dmarc=pass (p=REJECT dis=NONE) header.from=news.united.com\"],\"dkim-signature\":[\"v=1; a=rsa-sha1; c=relaxed/relaxed; s=united; d=news.united.com; h=MIME-Version:Content-Type:Content-Transfer-Encoding:Date:To:From:Reply-To:Subject:List-Unsubscribe:Message-ID; i=MileagePlus_Partner@news.united.com; bh=oiP9wNJbkuGtDmX9JXmAjTpQYe4=; b=lWhKDlwoeUSLppBUyzjcmkSvlgQys/kL+1R6BJEllHgaawrH/c2sBjY0NAAsZ4GPPUB/rF4h58NO FHBElr/V0H/k4rkQmSrzudpLfIElGb0WN2etlGZeO0qhMmtNvwmbhw7QO5uZu+x6sKMVutOFxmpa 6oPStO1uVojaiyQhVTA=\"],\"domainkey-signature\":[\"a=rsa-sha1; c=nofws; q=dns; s=united; d=news.united.com; b=ZkKrC8ZdJvofP8CEVdEeIkv3UmDibivko/0dxilZkSYfk8sPe/o2YR+zo0VqA9kr1o07ORe3dcxv Nz0E0TUCcv4YapXs9qxlN8Bm/Zz8PY8D572GBMV0T34PZ6+5v3ai57LtfUBpPy93fjcRTgNHJqQl HmQTBlZ3wUHv1TBGOqI=;\"],\"x-csa-complaints\":[\"whitelist-complaints@eco.de\",\"whitelist-complaints@eco.de\"],\"mime-version\":[\"1.0\"],\"content-type\":[\"text/html; charset=\\\"UTF-8\\\"\"],\"content-transfer-encoding\":[\"quoted-printable\"],\"date\":[\"Wed, 16 Nov 2016 12:22:53 -0800\"],\"to\":[\"christine@spang.cc\"],\"from\":[\"\\\"MileagePlus Explorer Card\\\" <MileagePlus_Partner@news.united.com>\"],\"reply-to\":[\"\\\"MileagePlus Explorer Card\\\" <MileagePlus_NoReply@united.com>\"],\"subject\":[\"Free checked bag for you and one companion when you use your Card\"],\"feedback-id\":[\"5765:1192222:oraclersys\"],\"list-unsubscribe\":[\"<https://news.united.com/pub/optout/UnsubscribeOneStepConfirmAction?YES=true&_ri_=X0Gzc2X%3DYQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCof&_ei_=Ejaf7_rRKzx0ctn6G-vCRQxQFpOo1jnhz_D7Qj8uilahE5HqTLtnYXpmd53bnyOwa5G4ok2G4SqEqZyK_i5MlB4glRW1lFv87L9Bq3zhUkhE6W-I9Lls8knlgu8Rrb4Dorss_mjDo7FeWQ-JARWOws8jiyPdFbxa>, <mailto:unsubscribe-YQpglLjHJlTQGhgzbltfLzg5m38nMYOnTA7zeygb4NSL8bRmMuhCqgrqMzbK8c8FY4FCof@imh.rsys2.com?subject=List-Unsubscribe>\"],\"x-sgxh1\":[\"JojpklpgLxkiHgnQJJ\"],\"x-rext\":[\"5.interact2.EonqtlMeFYuUCgf5wg26BVPnIWv1eCI0ufmo8-nj-fqdCJCz5Wz8hM\"],\"x-cid\":[\"united.3098382\"],\"message-id\":[\"<0.0.AC.822.1D2404732E8AF0C.0@omp.news.united.com>\"],\"x-gm-thrid\":\"1551187601250126809\",\"x-gm-msgid\":\"1551187601250126809\",\"x-gm-labels\":[]},\"headerMessageId\":\"<0.0.AC.822.1D2404732E8AF0C.0@omp.news.united.com>\",\"gMsgId\":\"1551187601250126809\",\"subject\":\"Free checked bag for you and one companion when you use your Card\",\"id\":\"3c8296060ca9bfc2b92bd27633da3ef60f7eb7cfd572cd3ac7610b7c5a3698db\",\"folderImapXGMLabels\":\"[]\"}}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseFromImap/node-streamtest-windows-1252.json",
    "content": "{\"imapMessage\":{\"attributes\":{\"struct\":[{\"partID\":\"1\",\"type\":\"text\",\"subtype\":\"plain\",\"params\":{\"charset\":\"windows-1252\"},\"id\":null,\"description\":null,\"encoding\":\"7BIT\",\"size\":467,\"lines\":14,\"md5\":null,\"disposition\":null,\"language\":null}],\"date\":\"2016-12-05T18:18:35.000Z\",\"flags\":[\"\\\\Seen\"],\"uid\":348641,\"modseq\":\"8252381\",\"x-gm-labels\":[\"debiandevel\"],\"x-gm-msgid\":\"1552901121651491080\",\"x-gm-thrid\":\"1552901121651491080\"},\"headers\":\"Delivered-To: christine@spang.cc\\r\\nReceived: by 10.140.100.181 with SMTP id s50csp1618416qge; Mon, 5 Dec 2016\\r\\n 10:18:35 -0800 (PST)\\r\\nX-Received: by 10.194.138.111 with SMTP id qp15mr33288574wjb.3.1480961915687;\\r\\n Mon, 05 Dec 2016 10:18:35 -0800 (PST)\\r\\nReturn-Path: <bounce-debian-devel=spang=mit.edu@lists.debian.org>\\r\\nReceived: from dmz-mailsec-scanner-7.mit.edu (dmz-mailsec-scanner-7.mit.edu.\\r\\n [18.7.68.36]) by mx.google.com with ESMTPS id\\r\\n d81si1055491wmc.164.2016.12.05.10.18.35 for <christine@spang.cc>\\r\\n (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Mon, 05 Dec\\r\\n 2016 10:18:35 -0800 (PST)\\r\\nReceived-SPF: neutral (google.com: 18.7.68.36 is neither permitted nor denied\\r\\n by manual fallback record for domain of\\r\\n bounce-debian-devel=spang=mit.edu@lists.debian.org) client-ip=18.7.68.36;\\r\\nAuthentication-Results: mx.google.com; dkim=fail header.i=@disroot.org;\\r\\n dkim=fail header.i=@disroot.org; spf=neutral (google.com: 18.7.68.36 is\\r\\n neither permitted nor denied by manual fallback record for domain of\\r\\n bounce-debian-devel=spang=mit.edu@lists.debian.org)\\r\\n smtp.mailfrom=bounce-debian-devel=spang=mit.edu@lists.debian.org\\r\\nReceived: from mailhub-dmz-2.mit.edu ( [18.7.62.37]) (using TLS with cipher\\r\\n DHE-RSA-AES256-SHA (256/256 bits)) (Client did not present a certificate) by \\r\\n (Symantec Messaging Gateway) with SMTP id 4D.F7.26209.97FA5485; Mon,  5 Dec\\r\\n 2016 13:18:33 -0500 (EST)\\r\\nReceived: from dmz-mailsec-scanner-5.mit.edu (dmz-mailsec-scanner-5.mit.edu\\r\\n [18.7.68.34]) by mailhub-dmz-2.mit.edu (8.13.8/8.9.2) with ESMTP id\\r\\n uB5IHhdj021246 for <spang@mit.edu>; Mon, 5 Dec 2016 13:18:33 -0500\\r\\nX-AuditID: 12074424-9b7ff70000006661-5a-5845af79d415\\r\\nReceived: from bendel.debian.org (bendel.debian.org [82.195.75.100]) (using\\r\\n TLS with cipher DHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not\\r\\n present a certificate) by  (Symantec Messaging Gateway) with SMTP id\\r\\n D2.B7.11606.87FA5485; Mon,  5 Dec 2016 13:18:32 -0500 (EST)\\r\\nReceived: from localhost (localhost [127.0.0.1]) by bendel.debian.org\\r\\n (Postfix) with QMQP id 75C2B17F; Mon,  5 Dec 2016 18:18:21 +0000 (UTC)\\r\\nX-Mailbox-Line: From debian-devel-request@lists.debian.org  Mon Dec  5\\r\\n 18:18:21 2016\\r\\nOld-Return-Path: <debbugs@buxtehude.debian.org>\\r\\nX-Spam-Checker-Version: SpamAssassin 3.4.0 (2014-02-07) on bendel.debian.org\\r\\nX-Spam-Level: \\r\\nX-Spam-Status: No, score=-8.0 required=4.0 tests=DKIM_SIGNED,\\r\\n HEADER_FROM_DIFFERENT_DOMAINS,LDO_WHITELIST,RP_MATCHES_RCVD,T_DKIM_INVALID\\r\\n autolearn=unavailable autolearn_force=no version=3.4.0\\r\\nX-Original-To: lists-debian-devel@bendel.debian.org\\r\\nDelivered-To: lists-debian-devel@bendel.debian.org\\r\\nReceived: from localhost (localhost [127.0.0.1]) by bendel.debian.org\\r\\n (Postfix) with ESMTP id 9C7A7151 for <lists-debian-devel@bendel.debian.org>;\\r\\n Mon,  5 Dec 2016 18:18:12 +0000 (UTC)\\r\\nX-Virus-Scanned: at lists.debian.org with policy bank en-ht\\r\\nX-Amavis-Spam-Status: No, score=-7.2 tagged_above=-10000 required=5.3\\r\\n tests=[BAYES_00=-2, DKIM_SIGNED=0.1, HEADER_FROM_DIFFERENT_DOMAINS=0.001,\\r\\n LDO_WHITELIST=-5, RP_MATCHES_RCVD=-0.311, T_DKIM_INVALID=0.01] autolearn=ham\\r\\n autolearn_force=no\\r\\nReceived: from bendel.debian.org ([127.0.0.1]) by localhost (lists.debian.org\\r\\n [127.0.0.1]) (amavisd-new, port 2525) with ESMTP id XjRiqZkpBpLJ for\\r\\n <lists-debian-devel@bendel.debian.org>; Mon,  5 Dec 2016 18:18:07 +0000 (UTC)\\r\\nReceived: from buxtehude.debian.org (buxtehude.debian.org\\r\\n [IPv6:2607:f8f0:614:1::1274:39]) (using TLSv1.2 with cipher\\r\\n ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (Client CN\\r\\n \\\"buxtehude.debian.org\\\", Issuer \\\"Debian SMTP CA\\\" (not verified)) by\\r\\n bendel.debian.org (Postfix) with ESMTPS id 82D7D170; Mon,  5 Dec 2016\\r\\n 18:18:07 +0000 (UTC)\\r\\nReceived: from debbugs by buxtehude.debian.org with local (Exim 4.84_2)\\r\\n (envelope-from <debbugs@buxtehude.debian.org>) id 1cDxq0-0004I6-E6; Mon, 05\\r\\n Dec 2016 18:18:04 +0000\\r\\nX-Loop: owner@bugs.debian.org\\r\\nSubject: Bug#847116: ITP: node-streamtest -- set of utils to test your stream\\r\\n based modules accross various, stream implementations of NodeJS\\r\\nReply-To: Sruthi Chandran <srud@disroot.org>, 847116@bugs.debian.org\\r\\nResent-From: Sruthi Chandran <srud@disroot.org>\\r\\nResent-To: debian-bugs-dist@lists.debian.org\\r\\nResent-CC: debian-devel@lists.debian.org, wnpp@debian.org\\r\\nX-Loop: owner@bugs.debian.org\\r\\nResent-Date: Mon, 05 Dec 2016 18:18:02 +0000\\r\\nResent-Message-ID: <handler.847116.B.148096168015430@bugs.debian.org>\\r\\nX-Debian-PR-Message: report 847116\\r\\nX-Debian-PR-Package: wnpp\\r\\nX-Debian-PR-Keywords: \\r\\nReceived: via spool by submit@bugs.debian.org id=B.148096168015430 (code B);\\r\\n Mon, 05 Dec 2016 18:18:02 +0000\\r\\nReceived: (at submit) by bugs.debian.org; 5 Dec 2016 18:14:40 +0000\\r\\nReceived: from bs-one.disroot.org ([178.21.23.139] helo=disroot.org) by\\r\\n buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256)\\r\\n (Exim 4.84_2) (envelope-from <srud@disroot.org>) id 1cDxmi-00040c-Gx for\\r\\n submit@bugs.debian.org; Mon, 05 Dec 2016 18:14:40 +0000\\r\\nReceived: from localhost (localhost [127.0.0.1]) by disroot.org (Postfix) with\\r\\n ESMTP id 53193239F6 for <submit@bugs.debian.org>; Mon,  5 Dec 2016 19:14:37\\r\\n +0100 (CET)\\r\\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail;\\r\\n t=1480961677; bh=D83w+mcR33/VQkITQI3T+hYFSLlGEhXR0gNcNOKma1M=;\\r\\n h=From:Subject:To:Date:From;\\r\\n b=RGfjrzldXfrn98cwJECU13CLtE6NurUcFar/ymwcIOWcI/iurS1yB77ufr6lvSnIv\\r\\n l7GcLyXyiXlFHJI0gacV3jsigJG3NWGyxOT670K+8/00FSUPPI0oT7GgSQQKrZ0RnX\\r\\n 1ydoD4ZCpm148HmxuCk972HdCYIRlA1YO4ggm35U=\\r\\nReceived: from disroot.org ([127.0.0.1]) by localhost (mail01.disroot.lan\\r\\n [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id z-Tz-PcP0cLK for\\r\\n <submit@bugs.debian.org>; Mon,  5 Dec 2016 19:14:37 +0100 (CET)\\r\\nFrom: Sruthi Chandran <srud@disroot.org>\\r\\nDKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail;\\r\\n t=1480961677; bh=D83w+mcR33/VQkITQI3T+hYFSLlGEhXR0gNcNOKma1M=;\\r\\n h=From:Subject:To:Date:From;\\r\\n b=RGfjrzldXfrn98cwJECU13CLtE6NurUcFar/ymwcIOWcI/iurS1yB77ufr6lvSnIv\\r\\n l7GcLyXyiXlFHJI0gacV3jsigJG3NWGyxOT670K+8/00FSUPPI0oT7GgSQQKrZ0RnX\\r\\n 1ydoD4ZCpm148HmxuCk972HdCYIRlA1YO4ggm35U=\\r\\nTo: submit@bugs.debian.org\\r\\nMessage-ID: <2bee7571-9098-d8d5-b4a4-59fc369c579b@disroot.org>\\r\\nDate: Mon, 5 Dec 2016 23:44:30 +0530\\r\\nUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101\\r\\n Icedove/45.5.0\\r\\nMIME-Version: 1.0\\r\\nContent-Type: text/plain; charset=windows-1252\\r\\nContent-Transfer-Encoding: 7bit\\r\\nDelivered-To: submit@bugs.debian.org\\r\\nX-Rc-Virus: 2007-09-13_01\\r\\nX-Rc-Spam: 2008-11-04_01\\r\\nX-Mailing-List: <debian-devel@lists.debian.org> archive/latest/323341\\r\\nX-Loop: debian-devel@lists.debian.org\\r\\nList-Id: <debian-devel.lists.debian.org>\\r\\nList-URL: <https://lists.debian.org/debian-devel/>\\r\\nList-Post: <mailto:debian-devel@lists.debian.org>\\r\\nList-Help: <mailto:debian-devel-request@lists.debian.org?subject=help>\\r\\nList-Subscribe: <mailto:debian-devel-request@lists.debian.org?subject=subscribe>\\r\\nList-Unsubscribe: <mailto:debian-devel-request@lists.debian.org?subject=unsubscribe>\\r\\nPrecedence: list\\r\\nResent-Sender: debian-devel-request@lists.debian.org\\r\\nList-Archive: https://lists.debian.org/msgid-search/2bee7571-9098-d8d5-b4a4-59fc369c579b@disroot.org\\r\\nAuthentication-Results: symauth.service.identifier\\r\\nX-Brightmail-Tracker: H4sIAAAAAAAAA1VTa0gUYRT1mxl1fEyNY7VXyx5DEURqpVBmSJD2wH5kBUkEObmTu7S72sz6\\r\\n WElaf2zFqmBhWWqskYWKqAmmUUm6lI8eUJm9LItURFFXsswMacZvTfp37jnnnnvv8A1Nck1e\\r\\n wbTeZBYlk2DgvXwpzjtmXailLi5x043La7aNl/zx3on2zkzcIQ6go747tKJBnyFK4TFJvrp7\\r\\n 43eptNdk1k9nEbKiAcKOfGhgI6G85jFSMcfaCLBdSrAjXwX3IDjf30vZET1nsk2RmC8jYMTR\\r\\n TeGiEMG7bocXTjoMb10vSBUvZsOhuc2JML8f7jfOeGO8CF4O/SLUZmCdCIodTyksRECl4/tc\\r\\n A8Vugca+fveEBwhanffcE6KhobcS4e4pBC03O91LDSKwP5xEuHhFQI/tk1upQzDYOeuJzwiB\\r\\n Z++zVT6QLUCQ13OfwHfHwpPuSVL1cMrmfVc5TG8Ex/QYwnQU9BTk/JeCHTWfbpEYx8Hg6xH3\\r\\n ojxUFz5wf+AgeDVR4z5/GTRZq7ywPxNy2y9ShSimRCkZNgA6r/VTKibZMGh++9sT41XQNFpG\\r\\n liOyGoVojdmhRkFvkMXkUDlZMJlEKXRrmFFvDhO16Q1o7jXErm1GefnxbYilEe/PuKxxiZyn\\r\\n kCFbjG0oiCb4pUxRkkItOpGqtegEWXdcSjeIchsCmuSXMEuVh8QxWsGSLUqp89JymuI1jMee\\r\\n H0c4NkUwi6dEMU2U5tUVNM0D86FWaQyQxBQx66TeYF6QCdpHDfdXwqtUDyOnCUZZn4L1LhRB\\r\\n f2ivGCDoR+OVAwRHmVJNYrCGKVWtrGrVpZv+pc2/92GkUY4LZGJUl7/yN/zLG1ZGEcqojvZd\\r\\n 6iizsCAFWxEzuy/rVKV5JnB17MkE576RrMHTZ2s/5yTH11svFOVmTDVm+rWPUd8Otvh/PRNn\\r\\n 01QMdD+iXYbtLYeKg/3q9Xm9rvXr5I/OjQ3TUXe4/D6f/mTNqpWJqWu3PB86FvLmXPSViTF+\\r\\n d+vN8N4jAV9Hb0sdI9c7n33pivSwR0SW0pZalx9PyTph8wZSkoW/az3I+MoDAAA=\\r\\nX-Brightmail-Tracker: H4sIAAAAAAAAA1VSa0wUVxTeMzMsw3YHh0HhuHWjmRATbRcfaNJXWhLEmrZGQzTapo2O7shO\\r\\n 3AeZWZ4NLaWxJRjCWm2sBLIkikF8JSiyVMS6GwWU1ogEfCGJAkGxaKMitYF0hrvY+ufmO+f7\\r\\n zvm+e3NZWrhntrFygV9WvZJbNFuYrMinTkfByczNS89Xr3ynu6vJnA5rSrsu0uvhC8sHTtmt\\r\\n 5Mnqkg+3Wlwtj88wOdfpgvHIPiiBIaocWBb5FbjrBV0OFlbgqykcDfYwpAgA3ugJmsshThdt\\r\\n wL4nf9AGnsUvwVA4AqT/GZ5t+ieW4Hi8NjJBGcPIRwD3B68whEjD+uDT6QGGX45NA4NRh1bA\\r\\n C5GWqMP72HinHsj0C8C2g53RUMOA5eeeASm6Kezd1R9lTgIOd07FkGvYsetmkdFP5CsAd/ee\\r\\n pYy9Ar8KL/U8ow2NoCcf+EUg7bcx+PcYkPa72FtR/NoWojjWX0cTnInD10ejQUVsCLRSBM/F\\r\\n 7r+ORa+fhM0lR8xEn4/fte9hCM7Aq1XPzSRxDYWVdUMxpGgAfNBcCQF4r0oXcnwCdh4YZAxM\\r\\n 86kY6nsZQ/B8bP6zmq4FugHsTk+RwyMpbk3e7tC2S16vrDpWpnoUf6rszG0E/TsIsavEEOyZ\\r\\n +iQMPAuilVvzfeZmIUbK0wo9YZjLUuIcbt9WvRW/zecsdEmaa4ua65a1MCBLi7O5shM6xzml\\r\\n wiJZ9c1Qb7KMmMyZPn6+SeCzJb+8U5ZzZHWGpdjYMMxjWRG5HcZ0gipnywU7FLf//5o447AY\\r\\n Nlbd5si0jZYjeTQlm4guQxp7q/3QEMX+9rh+iBIYr88r25I54bgu5Q2pK9f7auXM1+8Guy2R\\r\\n A5PJJFj1TPpTvM4/hGT9GRK5OsPQqnj9r/we6lEoPUpHe4YRxS/9R9lKIAhfd5R+ef8W90a7\\r\\n qXEi0Ky0Tkobf1ydNjWeFb/QvjsUt25ydR73luP3jq9+yN07ssCXMj/UdGq8yvIrn3Hupu1l\\r\\n 2e3P28y+/fTdbw4v7zmaZLX1/bTWUdOyyC4qabXpvR+F8vd+m5AVmJfy5HRxyoD56Ymstk0d\\r\\n ig/VnyfPLB57NCYymktatphWNelf2asph/UDAAA=\\r\\n\\r\\n\",\"parts\":{\"1\":\"Package: wnpp\\r\\nSeverity: wishlist\\r\\nOwner: Sruthi Chandran <srud@disroot.org>\\r\\nX-Debbugs-CC: debian-devel@lists.debian.org\\r\\n\\r\\n* Package name    : node-streamtest\\r\\n  Version         : 1.2.2\\r\\n  Upstream Author : Nicolas Froidure\\r\\n* URL             : https://github.com/nfroidure/streamtest\\r\\n* License         : Expat\\r\\n  Programming Lang: JavaScript\\r\\n  Description     : set of utils to test your stream based modules\\r\\naccross various stream implementations of NodeJS\\r\\n\\r\\n\"}},\"desiredParts\":[{\"id\":\"1\",\"encoding\":\"7BIT\",\"mimetype\":\"text/plain\"}],\"result\":{\"to\":[{\"name\":\"\",\"email\":\"submit@bugs.debian.org\"}],\"cc\":[],\"bcc\":[],\"from\":[{\"name\":\"Sruthi Chandran\",\"email\":\"srud@disroot.org\"}],\"replyTo\":[{\"name\":\"Sruthi Chandran\",\"email\":\"srud@disroot.org\"},{\"name\":\"\",\"email\":\"847116@bugs.debian.org\"}],\"accountId\":\"test-account-id\",\"body\":\"<pre class=\\\"nylas-plaintext\\\">Package: wnpp\\r\\nSeverity: wishlist\\r\\nOwner: Sruthi Chandran &lt;srud@disroot.org&gt;\\r\\nX-Debbugs-CC: debian-devel@lists.debian.org\\r\\n\\r\\n* Package name    : node-streamtest\\r\\n  Version         : 1.2.2\\r\\n  Upstream Author : Nicolas Froidure\\r\\n* URL             : https://github.com/nfroidure/streamtest\\r\\n* License         : Expat\\r\\n  Programming Lang: JavaScript\\r\\n  Description     : set of utils to test your stream based modules\\r\\naccross various stream implementations of NodeJS\\r\\n\\r\\n</pre>\",\"snippet\":\"Package: wnpp Severity: wishlist Owner: Sruthi Chandran <srud@disroot.org> X-Debbugs-CC: debian-devel@lists.debian.org\",\"unread\":false,\"starred\":false,\"date\":\"Mon, 5 Dec 2016 23:44:30 +0530\",\"folderImapUID\":348641,\"folderId\":\"test-folder-id\",\"folder\":{\"id\":\"test-folder-id\",\"account_id\":\"test-account-id\",\"object\":\"folder\",\"name\":null,\"display_name\":\"Test Folder\",\"sync_state\":{}},\"labels\":[],\"headers\":{\"delivered-to\":[\"christine@spang.cc\",\"lists-debian-devel@bendel.debian.org\",\"submit@bugs.debian.org\"],\"received\":[\"by 10.140.100.181 with SMTP id s50csp1618416qge; Mon, 5 Dec 2016 10:18:35 -0800 (PST)\",\"from dmz-mailsec-scanner-7.mit.edu (dmz-mailsec-scanner-7.mit.edu. [18.7.68.36]) by mx.google.com with ESMTPS id d81si1055491wmc.164.2016.12.05.10.18.35 for <christine@spang.cc> (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Mon, 05 Dec 2016 10:18:35 -0800 (PST)\",\"from mailhub-dmz-2.mit.edu ( [18.7.62.37]) (using TLS with cipher DHE-RSA-AES256-SHA (256/256 bits)) (Client did not present a certificate) by  (Symantec Messaging Gateway) with SMTP id 4D.F7.26209.97FA5485; Mon,  5 Dec 2016 13:18:33 -0500 (EST)\",\"from dmz-mailsec-scanner-5.mit.edu (dmz-mailsec-scanner-5.mit.edu [18.7.68.34]) by mailhub-dmz-2.mit.edu (8.13.8/8.9.2) with ESMTP id uB5IHhdj021246 for <spang@mit.edu>; Mon, 5 Dec 2016 13:18:33 -0500\",\"from bendel.debian.org (bendel.debian.org [82.195.75.100]) (using TLS with cipher DHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (Client did not present a certificate) by  (Symantec Messaging Gateway) with SMTP id D2.B7.11606.87FA5485; Mon,  5 Dec 2016 13:18:32 -0500 (EST)\",\"from localhost (localhost [127.0.0.1]) by bendel.debian.org (Postfix) with QMQP id 75C2B17F; Mon,  5 Dec 2016 18:18:21 +0000 (UTC)\",\"from localhost (localhost [127.0.0.1]) by bendel.debian.org (Postfix) with ESMTP id 9C7A7151 for <lists-debian-devel@bendel.debian.org>; Mon,  5 Dec 2016 18:18:12 +0000 (UTC)\",\"from bendel.debian.org ([127.0.0.1]) by localhost (lists.debian.org [127.0.0.1]) (amavisd-new, port 2525) with ESMTP id XjRiqZkpBpLJ for <lists-debian-devel@bendel.debian.org>; Mon,  5 Dec 2016 18:18:07 +0000 (UTC)\",\"from buxtehude.debian.org (buxtehude.debian.org [IPv6:2607:f8f0:614:1::1274:39]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (Client CN \\\"buxtehude.debian.org\\\", Issuer \\\"Debian SMTP CA\\\" (not verified)) by bendel.debian.org (Postfix) with ESMTPS id 82D7D170; Mon,  5 Dec 2016 18:18:07 +0000 (UTC)\",\"from debbugs by buxtehude.debian.org with local (Exim 4.84_2) (envelope-from <debbugs@buxtehude.debian.org>) id 1cDxq0-0004I6-E6; Mon, 05 Dec 2016 18:18:04 +0000\",\"via spool by submit@bugs.debian.org id=B.148096168015430 (code B); Mon, 05 Dec 2016 18:18:02 +0000\",\"(at submit) by bugs.debian.org; 5 Dec 2016 18:14:40 +0000\",\"from bs-one.disroot.org ([178.21.23.139] helo=disroot.org) by buxtehude.debian.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.84_2) (envelope-from <srud@disroot.org>) id 1cDxmi-00040c-Gx for submit@bugs.debian.org; Mon, 05 Dec 2016 18:14:40 +0000\",\"from localhost (localhost [127.0.0.1]) by disroot.org (Postfix) with ESMTP id 53193239F6 for <submit@bugs.debian.org>; Mon,  5 Dec 2016 19:14:37 +0100 (CET)\",\"from disroot.org ([127.0.0.1]) by localhost (mail01.disroot.lan [127.0.0.1]) (amavisd-new, port 10024) with ESMTP id z-Tz-PcP0cLK for <submit@bugs.debian.org>; Mon,  5 Dec 2016 19:14:37 +0100 (CET)\"],\"x-received\":[\"by 10.194.138.111 with SMTP id qp15mr33288574wjb.3.1480961915687; Mon, 05 Dec 2016 10:18:35 -0800 (PST)\"],\"return-path\":[\"<bounce-debian-devel=spang=mit.edu@lists.debian.org>\"],\"received-spf\":[\"neutral (google.com: 18.7.68.36 is neither permitted nor denied by manual fallback record for domain of bounce-debian-devel=spang=mit.edu@lists.debian.org) client-ip=18.7.68.36;\"],\"authentication-results\":[\"mx.google.com; dkim=fail header.i=@disroot.org; dkim=fail header.i=@disroot.org; spf=neutral (google.com: 18.7.68.36 is neither permitted nor denied by manual fallback record for domain of bounce-debian-devel=spang=mit.edu@lists.debian.org) smtp.mailfrom=bounce-debian-devel=spang=mit.edu@lists.debian.org\",\"symauth.service.identifier\"],\"x-auditid\":[\"12074424-9b7ff70000006661-5a-5845af79d415\"],\"x-mailbox-line\":[\"From debian-devel-request@lists.debian.org  Mon Dec  5 18:18:21 2016\"],\"old-return-path\":[\"<debbugs@buxtehude.debian.org>\"],\"x-spam-checker-version\":[\"SpamAssassin 3.4.0 (2014-02-07) on bendel.debian.org\"],\"x-spam-level\":[\"\"],\"x-spam-status\":[\"No, score=-8.0 required=4.0 tests=DKIM_SIGNED, HEADER_FROM_DIFFERENT_DOMAINS,LDO_WHITELIST,RP_MATCHES_RCVD,T_DKIM_INVALID autolearn=unavailable autolearn_force=no version=3.4.0\"],\"x-original-to\":[\"lists-debian-devel@bendel.debian.org\"],\"x-virus-scanned\":[\"at lists.debian.org with policy bank en-ht\"],\"x-amavis-spam-status\":[\"No, score=-7.2 tagged_above=-10000 required=5.3 tests=[BAYES_00=-2, DKIM_SIGNED=0.1, HEADER_FROM_DIFFERENT_DOMAINS=0.001, LDO_WHITELIST=-5, RP_MATCHES_RCVD=-0.311, T_DKIM_INVALID=0.01] autolearn=ham autolearn_force=no\"],\"x-loop\":[\"owner@bugs.debian.org\",\"owner@bugs.debian.org\",\"debian-devel@lists.debian.org\"],\"subject\":[\"Bug#847116: ITP: node-streamtest -- set of utils to test your stream based modules accross various, stream implementations of NodeJS\"],\"reply-to\":[\"Sruthi Chandran <srud@disroot.org>, 847116@bugs.debian.org\"],\"resent-from\":[\"Sruthi Chandran <srud@disroot.org>\"],\"resent-to\":[\"debian-bugs-dist@lists.debian.org\"],\"resent-cc\":[\"debian-devel@lists.debian.org, wnpp@debian.org\"],\"resent-date\":[\"Mon, 05 Dec 2016 18:18:02 +0000\"],\"resent-message-id\":[\"<handler.847116.B.148096168015430@bugs.debian.org>\"],\"x-debian-pr-message\":[\"report 847116\"],\"x-debian-pr-package\":[\"wnpp\"],\"x-debian-pr-keywords\":[\"\"],\"dkim-signature\":[\"v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail; t=1480961677; bh=D83w+mcR33/VQkITQI3T+hYFSLlGEhXR0gNcNOKma1M=; h=From:Subject:To:Date:From; b=RGfjrzldXfrn98cwJECU13CLtE6NurUcFar/ymwcIOWcI/iurS1yB77ufr6lvSnIv l7GcLyXyiXlFHJI0gacV3jsigJG3NWGyxOT670K+8/00FSUPPI0oT7GgSQQKrZ0RnX 1ydoD4ZCpm148HmxuCk972HdCYIRlA1YO4ggm35U=\",\"v=1; a=rsa-sha256; c=relaxed/simple; d=disroot.org; s=mail; t=1480961677; bh=D83w+mcR33/VQkITQI3T+hYFSLlGEhXR0gNcNOKma1M=; h=From:Subject:To:Date:From; b=RGfjrzldXfrn98cwJECU13CLtE6NurUcFar/ymwcIOWcI/iurS1yB77ufr6lvSnIv l7GcLyXyiXlFHJI0gacV3jsigJG3NWGyxOT670K+8/00FSUPPI0oT7GgSQQKrZ0RnX 1ydoD4ZCpm148HmxuCk972HdCYIRlA1YO4ggm35U=\"],\"from\":[\"Sruthi Chandran <srud@disroot.org>\"],\"to\":[\"submit@bugs.debian.org\"],\"message-id\":[\"<2bee7571-9098-d8d5-b4a4-59fc369c579b@disroot.org>\"],\"date\":[\"Mon, 5 Dec 2016 23:44:30 +0530\"],\"user-agent\":[\"Mozilla/5.0 (X11; Linux x86_64; rv:45.0) Gecko/20100101 Icedove/45.5.0\"],\"mime-version\":[\"1.0\"],\"content-type\":[\"text/plain; charset=windows-1252\"],\"content-transfer-encoding\":[\"7bit\"],\"x-rc-virus\":[\"2007-09-13_01\"],\"x-rc-spam\":[\"2008-11-04_01\"],\"x-mailing-list\":[\"<debian-devel@lists.debian.org> archive/latest/323341\"],\"list-id\":[\"<debian-devel.lists.debian.org>\"],\"list-url\":[\"<https://lists.debian.org/debian-devel/>\"],\"list-post\":[\"<mailto:debian-devel@lists.debian.org>\"],\"list-help\":[\"<mailto:debian-devel-request@lists.debian.org?subject=help>\"],\"list-subscribe\":[\"<mailto:debian-devel-request@lists.debian.org?subject=subscribe>\"],\"list-unsubscribe\":[\"<mailto:debian-devel-request@lists.debian.org?subject=unsubscribe>\"],\"precedence\":[\"list\"],\"resent-sender\":[\"debian-devel-request@lists.debian.org\"],\"list-archive\":[\"https://lists.debian.org/msgid-search/2bee7571-9098-d8d5-b4a4-59fc369c579b@disroot.org\"],\"x-brightmail-tracker\":[\"H4sIAAAAAAAAA1VTa0gUYRT1mxl1fEyNY7VXyx5DEURqpVBmSJD2wH5kBUkEObmTu7S72sz6 WElaf2zFqmBhWWqskYWKqAmmUUm6lI8eUJm9LItURFFXsswMacZvTfp37jnnnnvv8A1Nck1e wbTeZBYlk2DgvXwpzjtmXailLi5x043La7aNl/zx3on2zkzcIQ6go747tKJBnyFK4TFJvrp7 43eptNdk1k9nEbKiAcKOfGhgI6G85jFSMcfaCLBdSrAjXwX3IDjf30vZET1nsk2RmC8jYMTR TeGiEMG7bocXTjoMb10vSBUvZsOhuc2JML8f7jfOeGO8CF4O/SLUZmCdCIodTyksRECl4/tc A8Vugca+fveEBwhanffcE6KhobcS4e4pBC03O91LDSKwP5xEuHhFQI/tk1upQzDYOeuJzwiB Z++zVT6QLUCQ13OfwHfHwpPuSVL1cMrmfVc5TG8Ex/QYwnQU9BTk/JeCHTWfbpEYx8Hg6xH3 ojxUFz5wf+AgeDVR4z5/GTRZq7ywPxNy2y9ShSimRCkZNgA6r/VTKibZMGh++9sT41XQNFpG liOyGoVojdmhRkFvkMXkUDlZMJlEKXRrmFFvDhO16Q1o7jXErm1GefnxbYilEe/PuKxxiZyn kCFbjG0oiCb4pUxRkkItOpGqtegEWXdcSjeIchsCmuSXMEuVh8QxWsGSLUqp89JymuI1jMee H0c4NkUwi6dEMU2U5tUVNM0D86FWaQyQxBQx66TeYF6QCdpHDfdXwqtUDyOnCUZZn4L1LhRB f2ivGCDoR+OVAwRHmVJNYrCGKVWtrGrVpZv+pc2/92GkUY4LZGJUl7/yN/zLG1ZGEcqojvZd 6iizsCAFWxEzuy/rVKV5JnB17MkE576RrMHTZ2s/5yTH11svFOVmTDVm+rWPUd8Otvh/PRNn 01QMdD+iXYbtLYeKg/3q9Xm9rvXr5I/OjQ3TUXe4/D6f/mTNqpWJqWu3PB86FvLmXPSViTF+ d+vN8N4jAV9Hb0sdI9c7n33pivSwR0SW0pZalx9PyTph8wZSkoW/az3I+MoDAAA=\",\"H4sIAAAAAAAAA1VSa0wUVxTeMzMsw3YHh0HhuHWjmRATbRcfaNJXWhLEmrZGQzTapo2O7shO 3AeZWZ4NLaWxJRjCWm2sBLIkikF8JSiyVMS6GwWU1ogEfCGJAkGxaKMitYF0hrvY+ufmO+f7 zvm+e3NZWrhntrFygV9WvZJbNFuYrMinTkfByczNS89Xr3ynu6vJnA5rSrsu0uvhC8sHTtmt 5Mnqkg+3Wlwtj88wOdfpgvHIPiiBIaocWBb5FbjrBV0OFlbgqykcDfYwpAgA3ugJmsshThdt wL4nf9AGnsUvwVA4AqT/GZ5t+ieW4Hi8NjJBGcPIRwD3B68whEjD+uDT6QGGX45NA4NRh1bA C5GWqMP72HinHsj0C8C2g53RUMOA5eeeASm6Kezd1R9lTgIOd07FkGvYsetmkdFP5CsAd/ee pYy9Ar8KL/U8ow2NoCcf+EUg7bcx+PcYkPa72FtR/NoWojjWX0cTnInD10ejQUVsCLRSBM/F 7r+ORa+fhM0lR8xEn4/fte9hCM7Aq1XPzSRxDYWVdUMxpGgAfNBcCQF4r0oXcnwCdh4YZAxM 86kY6nsZQ/B8bP6zmq4FugHsTk+RwyMpbk3e7tC2S16vrDpWpnoUf6rszG0E/TsIsavEEOyZ +iQMPAuilVvzfeZmIUbK0wo9YZjLUuIcbt9WvRW/zecsdEmaa4ua65a1MCBLi7O5shM6xzml wiJZ9c1Qb7KMmMyZPn6+SeCzJb+8U5ZzZHWGpdjYMMxjWRG5HcZ0gipnywU7FLf//5o447AY Nlbd5si0jZYjeTQlm4guQxp7q/3QEMX+9rh+iBIYr88r25I54bgu5Q2pK9f7auXM1+8Guy2R A5PJJFj1TPpTvM4/hGT9GRK5OsPQqnj9r/we6lEoPUpHe4YRxS/9R9lKIAhfd5R+ef8W90a7 qXEi0Ky0Tkobf1ydNjWeFb/QvjsUt25ydR73luP3jq9+yN07ssCXMj/UdGq8yvIrn3Hupu1l 2e3P28y+/fTdbw4v7zmaZLX1/bTWUdOyyC4qabXpvR+F8vd+m5AVmJfy5HRxyoD56Ymstk0d ig/VnyfPLB57NCYymktatphWNelf2asph/UDAAA=\"],\"x-gm-thrid\":\"1552901121651491080\",\"x-gm-msgid\":\"1552901121651491080\",\"x-gm-labels\":[\"debiandevel\"]},\"headerMessageId\":\"<2bee7571-9098-d8d5-b4a4-59fc369c579b@disroot.org>\",\"gMsgId\":\"1552901121651491080\",\"subject\":\"Bug#847116: ITP: node-streamtest -- set of utils to test your stream based modules accross various, stream implementations of NodeJS\",\"id\":\"7718325e52f9da4814108a52eb94beb2361e00369cf24c310890d6725a92e033\",\"folderImapXGMLabels\":\"[\\\"debiandevel\\\"]\"}}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseFromImap/spam-mime-html-base64-encoded.json",
    "content": "{\"imapMessage\":{\"attributes\":{\"struct\":[{\"partID\":\"1\",\"type\":\"text\",\"subtype\":\"html\",\"params\":{\"charset\":\"utf-8\"},\"id\":null,\"description\":null,\"encoding\":\"BASE64\",\"size\":1318,\"lines\":19,\"md5\":null,\"disposition\":null,\"language\":null}],\"date\":\"2016-12-05T22:09:12.000Z\",\"flags\":[],\"uid\":6466,\"modseq\":\"8251529\",\"x-gm-labels\":[],\"x-gm-msgid\":\"1552915630214378399\",\"x-gm-thrid\":\"1552915630214378399\"},\"headers\":\"Delivered-To: christine@spang.cc\\r\\nReceived: by 10.140.100.181 with SMTP id s50csp1710867qge; Mon, 5 Dec 2016\\r\\n 14:09:12 -0800 (PST)\\r\\nX-Received: by 10.129.97.134 with SMTP id v128mr54944350ywb.338.1480975752135;\\r\\n Mon, 05 Dec 2016 14:09:12 -0800 (PST)\\r\\nReturn-Path: <cfvqtub@rapab.com>\\r\\nReceived: from muffat.debian.org (muffat.debian.org.\\r\\n [2607:f8f0:614:1::1274:33]) by mx.google.com with ESMTPS id\\r\\n w195si4900468ywd.160.2016.12.05.14.09.11 for <christine@spang.cc>\\r\\n (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Mon, 05 Dec\\r\\n 2016 14:09:12 -0800 (PST)\\r\\nReceived-SPF: neutral (google.com: 2607:f8f0:614:1::1274:33 is neither\\r\\n permitted nor denied by best guess record for domain of cfvqtub@rapab.com)\\r\\n client-ip=2607:f8f0:614:1::1274:33;\\r\\nAuthentication-Results: mx.google.com; spf=neutral (google.com:\\r\\n 2607:f8f0:614:1::1274:33 is neither permitted nor denied by best guess record\\r\\n for domain of cfvqtub@rapab.com) smtp.mailfrom=cfvqtub@rapab.com\\r\\nMessage-Id: <5845e588.ccd40d0a.ea741.b529SMTPIN_ADDED_MISSING@mx.google.com>\\r\\nReceived: from [180.127.164.182] (helo=rapab.com) by muffat.debian.org with\\r\\n esmtp (Exim 4.84_2) (envelope-from <cfvqtub@rapab.com>) id 1cE1Re-0002e1-KR\\r\\n for christine@spang.cc; Mon, 05 Dec 2016 22:09:11 +0000\\r\\nReceived: from vps5754 ([127.0.0.1]) by localhost via TCP with ESMTPA; Tue, 06\\r\\n Dec 2016 06:08:54 +0800\\r\\nMIME-Version: 1.0\\r\\nFrom: Peggy <yanmusinei55@163.com>\\r\\nSender: Peggy <cfvqtub@rapab.com>\\r\\nTo: christine@debian.org\\r\\nReply-To: Peggy <yanmusinei55@163.com>\\r\\nDate: 6 Dec 2016 06:08:54 +0800\\r\\nSubject: =?utf-8?B?VGhlIEJlc3QgT2ZmZXIgb2YgbGFueWFyZHM=?=\\r\\nContent-Type: text/html; charset=utf-8\\r\\nContent-Transfer-Encoding: base64\\r\\n\\r\\n\",\"parts\":{\"1\":\"PGh0bWw+PGJvZHk+PFA+RGVhciBmcmllbmQsPC9QPg0KPFA+VGhhbmsgeW91IGZvciB5b3Vy\\r\\nIGF0dGVudGlvbi4gPC9QPg0KPFA+T3VyIGNvbXBhbnkgaXMgYSBvbmUtc3RvcCBsYW55YXJk\\r\\ncyBmYWN0b3J5ICwgcHJvdmlkaW5nIHByaW50aW5nIGFuZCBkaXN0cmlidXRpbmcgc2Vydmlj\\r\\nZSB0byBjdXN0b21lcnMgYXJvdW5kIHRoZSB3b3JsZC48L1A+DQo8UD5NYWluIHByb2R1Y3Rz\\r\\nIGFyZSBzaWxrIHByaW50IC9oZWF0IHRyYW5zZmVycmVkIC8gc2F0aW4gbGFjZSBvbiByaWJi\\r\\nb24vIHdvdmVuIGxhbnlhcmRzLCZuYnNwOyBsaWdodCB1cCBsYW55YXJkcywgYm90dGxlIGhv\\r\\nbGRlcnMsIFVTQiB3cmlzdGJhbmRzLCBsYW55YXJkIHdpdGggd2F0ZXJwcm9vZiByYWluIGhh\\r\\ndCwgYm90dGxlIG9wZW5lcnMsa2V5IGNoYWlucywgY2FyYWJpbmVycywgc2hvZXNsYWNlcywg\\r\\nbHVnZ2FnZSBiZWx0cywgSUQgY2FyZCBob2xkZXJzLG1vYmlsZSBwaG9uZSBzdHJhcHMsIG5l\\r\\nY2tzdHJhcHMuZXRjLjwvUD4NCjxQPkFsbCBwcm9kdWN0cyBhcmUgY3VzdG9taXplZC4gTm8g\\r\\ncHJpY2UgbGlzdC4gSWYgeW91IG5lZWQgcXVvdGF0aW9uLCBwbGVhc2UgbGV0IG1lIGtub3cg\\r\\ndGhlIHNwZWNpZmljYXRpb25zLjwvUD4NCjxQPldlIGNhbiBtYWtlIGFzIHBlciB5b3VyIGRl\\r\\nc2lnbiBhbmQgeW91ciBsb2dvLjwvUD4NCjxQPk5vIE1PUSBzZXJ2aWNlPC9QPg0KPFA+RGVz\\r\\naWduZXIgU2VydmljZTwvUD4NCjxQPlBNUyBleHByZXNzIHNlcnZpY2U8L1A+DQo8UD5JdCB3\\r\\naWxsIGJlIGdyZWF0IGlmIHlvdSBjb3VsZCBzZW5kIG1lIGZpbGUgb3IgcGljdHVyZSBmb3Ig\\r\\ncmVmZXJlbmNlLjwvUD4NCjxQPkJlc3QgUmVnYXJkcyw8L1A+DQo8UD5QZWdneTxCUj5Nb2I6\\r\\nODYtMTg4IDI1NTQgNTg0NjxCUj5BREQ6Q2hhbmcgQW4gVG93bixEb25nZ3VhbiBDaXR5IEd1\\r\\nYW5kb25nIFByb3ZpbmNlLCBDaGluYTwvUD48L2JvZHk+PC9odG1sPg==\\r\\n\\r\\n\"}},\"desiredParts\":[{\"id\":\"1\",\"encoding\":\"BASE64\",\"mimetype\":\"text/html\"}],\"result\":{\"to\":[{\"name\":\"\",\"email\":\"christine@debian.org\"}],\"cc\":[],\"bcc\":[],\"from\":[{\"name\":\"Peggy\",\"email\":\"yanmusinei55@163.com\"}],\"replyTo\":[{\"name\":\"Peggy\",\"email\":\"yanmusinei55@163.com\"}],\"accountId\":\"test-account-id\",\"body\":\"PGh0bWw+PGJvZHk+PFA+RGVhciBmcmllbmQsPC9QPg0KPFA+VGhhbmsgeW91IGZvciB5b3Vy\\r\\nIGF0dGVudGlvbi4gPC9QPg0KPFA+T3VyIGNvbXBhbnkgaXMgYSBvbmUtc3RvcCBsYW55YXJk\\r\\ncyBmYWN0b3J5ICwgcHJvdmlkaW5nIHByaW50aW5nIGFuZCBkaXN0cmlidXRpbmcgc2Vydmlj\\r\\nZSB0byBjdXN0b21lcnMgYXJvdW5kIHRoZSB3b3JsZC48L1A+DQo8UD5NYWluIHByb2R1Y3Rz\\r\\nIGFyZSBzaWxrIHByaW50IC9oZWF0IHRyYW5zZmVycmVkIC8gc2F0aW4gbGFjZSBvbiByaWJi\\r\\nb24vIHdvdmVuIGxhbnlhcmRzLCZuYnNwOyBsaWdodCB1cCBsYW55YXJkcywgYm90dGxlIGhv\\r\\nbGRlcnMsIFVTQiB3cmlzdGJhbmRzLCBsYW55YXJkIHdpdGggd2F0ZXJwcm9vZiByYWluIGhh\\r\\ndCwgYm90dGxlIG9wZW5lcnMsa2V5IGNoYWlucywgY2FyYWJpbmVycywgc2hvZXNsYWNlcywg\\r\\nbHVnZ2FnZSBiZWx0cywgSUQgY2FyZCBob2xkZXJzLG1vYmlsZSBwaG9uZSBzdHJhcHMsIG5l\\r\\nY2tzdHJhcHMuZXRjLjwvUD4NCjxQPkFsbCBwcm9kdWN0cyBhcmUgY3VzdG9taXplZC4gTm8g\\r\\ncHJpY2UgbGlzdC4gSWYgeW91IG5lZWQgcXVvdGF0aW9uLCBwbGVhc2UgbGV0IG1lIGtub3cg\\r\\ndGhlIHNwZWNpZmljYXRpb25zLjwvUD4NCjxQPldlIGNhbiBtYWtlIGFzIHBlciB5b3VyIGRl\\r\\nc2lnbiBhbmQgeW91ciBsb2dvLjwvUD4NCjxQPk5vIE1PUSBzZXJ2aWNlPC9QPg0KPFA+RGVz\\r\\naWduZXIgU2VydmljZTwvUD4NCjxQPlBNUyBleHByZXNzIHNlcnZpY2U8L1A+DQo8UD5JdCB3\\r\\naWxsIGJlIGdyZWF0IGlmIHlvdSBjb3VsZCBzZW5kIG1lIGZpbGUgb3IgcGljdHVyZSBmb3Ig\\r\\ncmVmZXJlbmNlLjwvUD4NCjxQPkJlc3QgUmVnYXJkcyw8L1A+DQo8UD5QZWdneTxCUj5Nb2I6\\r\\nODYtMTg4IDI1NTQgNTg0NjxCUj5BREQ6Q2hhbmcgQW4gVG93bixEb25nZ3VhbiBDaXR5IEd1\\r\\nYW5kb25nIFByb3ZpbmNlLCBDaGluYTwvUD48L2JvZHk+PC9odG1sPg==\\r\\n\\r\\n\",\"snippet\":\"PGh0bWw+PGJvZHk+PFA+RGVhciBmcmllbmQsPC9QPg0KPFA+VGhhbmsgeW91IGZvciB5b3Vy IGF0dGVudGlvbi4gPC9QPg0KPFA+T3VyIGNvbXBhbnkgaXMgYSBvbmUtc3RvcCBsYW55YXJk\",\"unread\":true,\"starred\":false,\"date\":\"6 Dec 2016 06:08:54 +0800\",\"folderImapUID\":6466,\"folderId\":\"test-folder-id\",\"folder\":{\"id\":\"test-folder-id\",\"account_id\":\"test-account-id\",\"object\":\"folder\",\"name\":null,\"display_name\":\"Test Folder\",\"sync_state\":{}},\"labels\":[],\"headers\":{\"delivered-to\":[\"christine@spang.cc\"],\"received\":[\"by 10.140.100.181 with SMTP id s50csp1710867qge; Mon, 5 Dec 2016 14:09:12 -0800 (PST)\",\"from muffat.debian.org (muffat.debian.org. [2607:f8f0:614:1::1274:33]) by mx.google.com with ESMTPS id w195si4900468ywd.160.2016.12.05.14.09.11 for <christine@spang.cc> (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Mon, 05 Dec 2016 14:09:12 -0800 (PST)\",\"from [180.127.164.182] (helo=rapab.com) by muffat.debian.org with esmtp (Exim 4.84_2) (envelope-from <cfvqtub@rapab.com>) id 1cE1Re-0002e1-KR for christine@spang.cc; Mon, 05 Dec 2016 22:09:11 +0000\",\"from vps5754 ([127.0.0.1]) by localhost via TCP with ESMTPA; Tue, 06 Dec 2016 06:08:54 +0800\"],\"x-received\":[\"by 10.129.97.134 with SMTP id v128mr54944350ywb.338.1480975752135; Mon, 05 Dec 2016 14:09:12 -0800 (PST)\"],\"return-path\":[\"<cfvqtub@rapab.com>\"],\"received-spf\":[\"neutral (google.com: 2607:f8f0:614:1::1274:33 is neither permitted nor denied by best guess record for domain of cfvqtub@rapab.com) client-ip=2607:f8f0:614:1::1274:33;\"],\"authentication-results\":[\"mx.google.com; spf=neutral (google.com: 2607:f8f0:614:1::1274:33 is neither permitted nor denied by best guess record for domain of cfvqtub@rapab.com) smtp.mailfrom=cfvqtub@rapab.com\"],\"message-id\":[\"<5845e588.ccd40d0a.ea741.b529SMTPIN_ADDED_MISSING@mx.google.com>\"],\"mime-version\":[\"1.0\"],\"from\":[\"Peggy <yanmusinei55@163.com>\"],\"sender\":[\"Peggy <cfvqtub@rapab.com>\"],\"to\":[\"christine@debian.org\"],\"reply-to\":[\"Peggy <yanmusinei55@163.com>\"],\"date\":[\"6 Dec 2016 06:08:54 +0800\"],\"subject\":[\"The Best Offer of lanyards\"],\"content-type\":[\"text/html; charset=utf-8\"],\"content-transfer-encoding\":[\"base64\"],\"x-gm-thrid\":\"1552915630214378399\",\"x-gm-msgid\":\"1552915630214378399\",\"x-gm-labels\":[]},\"headerMessageId\":\"<5845e588.ccd40d0a.ea741.b529SMTPIN_ADDED_MISSING@mx.google.com>\",\"gMsgId\":\"1552915630214378399\",\"subject\":\"The Best Offer of lanyards\",\"id\":\"4eaca06a109e3113f50457360955dd3607c19fdebc2248153229801170bd4be8\",\"folderImapXGMLabels\":\"[]\"}}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseFromImap/theskimm-multipart-alternative-quoted-printable.json",
    "content": "{\"imapMessage\":{\"attributes\":{\"struct\":[{\"type\":\"alternative\",\"params\":{\"boundary\":\"Aw0OfBZoDhFa=_?:\"},\"disposition\":null,\"language\":null},[{\"partID\":\"1\",\"type\":\"text\",\"subtype\":\"plain\",\"params\":{\"charset\":\"us-ascii\"},\"id\":null,\"description\":null,\"encoding\":\"QUOTED-PRINTABLE\",\"size\":16310,\"lines\":365,\"md5\":null,\"disposition\":null,\"language\":null}],[{\"partID\":\"2\",\"type\":\"text\",\"subtype\":\"html\",\"params\":{\"charset\":\"us-ascii\"},\"id\":null,\"description\":null,\"encoding\":\"QUOTED-PRINTABLE\",\"size\":107910,\"lines\":2599,\"md5\":null,\"disposition\":null,\"language\":null}]],\"date\":\"2016-11-02T10:21:52.000Z\",\"flags\":[\"\\\\Seen\"],\"uid\":343848,\"modseq\":\"8022913\",\"x-gm-labels\":[\"\\\\Important\"],\"x-gm-msgid\":\"1549881429115204149\",\"x-gm-thrid\":\"1549881429115204149\"},\"headers\":\"Delivered-To: christine@spang.cc\\r\\nReceived: by 10.31.236.3 with SMTP id k3csp698913vkh; Wed, 2 Nov 2016 03:21:52\\r\\n -0700 (PDT)\\r\\nX-Received: by 10.55.47.193 with SMTP id v184mr2167122qkh.259.1478082112295;\\r\\n Wed, 02 Nov 2016 03:21:52 -0700 (PDT)\\r\\nReturn-Path: <bounce-20_HTML-215009-5489-7208679-430@bounce.morning7.theskimm.com>\\r\\nReceived: from mta.morning7.theskimm.com (mta.morning7.theskimm.com.\\r\\n [136.147.177.13]) by mx.google.com with ESMTPS id\\r\\n j59si882394qtb.16.2016.11.02.03.21.50 for <christine@spang.cc>\\r\\n (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Wed, 02 Nov\\r\\n 2016 03:21:52 -0700 (PDT)\\r\\nReceived-SPF: pass (google.com: domain of\\r\\n bounce-20_html-215009-5489-7208679-430@bounce.morning7.theskimm.com\\r\\n designates 136.147.177.13 as permitted sender) client-ip=136.147.177.13;\\r\\nAuthentication-Results: mx.google.com; dkim=pass\\r\\n header.i=@morning7.theskimm.com; spf=pass (google.com: domain of\\r\\n bounce-20_html-215009-5489-7208679-430@bounce.morning7.theskimm.com\\r\\n designates 136.147.177.13 as permitted sender)\\r\\n smtp.mailfrom=bounce-20_HTML-215009-5489-7208679-430@bounce.morning7.theskimm.com;\\r\\n dmarc=pass (p=NONE dis=NONE) header.from=theskimm.com\\r\\nDKIM-Signature: v=1; a=rsa-sha1; c=relaxed/relaxed; s=200608;\\r\\n d=morning7.theskimm.com;\\r\\n h=From:To:Subject:Date:List-Unsubscribe:MIME-Version:Reply-To:List-ID:Message-ID:Content-Type;\\r\\n i=dailyskimm@morning7.theskimm.com; bh=TfZNUfri1El8i4AUulbXxJgLUCA=;\\r\\n b=H6ZRxb2e2tmobb63CIcQ1ecsfv+X+Ky/G0EW0Kct6kvNmLP/yASdttr9bcgRbUMO45Su7bytynrE\\r\\n U+kbevaHw5nbatKKWQFP7JxGMjzCtQGM6LzOjGV8qCSu7XwdQ0QgUCJo0V4eo7Iu6bafR9h2WJST\\r\\n ct12e4elnzHFZLnF7GM=\\r\\nReceived: by mta.morning7.theskimm.com id h36v3s163hst for\\r\\n <christine@spang.cc>; Wed, 2 Nov 2016 10:21:47 +0000 (envelope-from\\r\\n <bounce-20_HTML-215009-5489-7208679-430@bounce.morning7.theskimm.com>)\\r\\nFrom: \\\"theSkimm\\\" <dailyskimm@morning7.theskimm.com>\\r\\nTo: <christine@spang.cc>\\r\\nSubject: Daily Skimm: Hey batter batter\\r\\nDate: Wed, 02 Nov 2016 04:21:46 -0600\\r\\nList-Unsubscribe: <mailto:leave-fd8916701a3c402029-fece167170640474-ff2511747d6d-fe881372756c027a7c-ff68107375@leave.morning7.theskimm.com>\\r\\nMIME-Version: 1.0\\r\\nReply-To: \\\"theSkimm\\\"\\r\\n <reply-ff2511747d6d-20_HTML-215009-7208679-430@morning7.theskimm.com>\\r\\nList-ID: <7208679_5489.xt.local>\\r\\nX-CSA-Complaints: whitelistcomplaints@eco.de\\r\\nx-job: 7208679_5489\\r\\nMessage-ID: <fdb2713e-91fd-4d20-8d44-c8166229c274@xtgap4s7mta4374.xt.local>\\r\\nContent-Type: multipart/alternative; boundary=\\\"Aw0OfBZoDhFa=_?:\\\"\\r\\n\\r\\n\",\"parts\":{\"1\":\"\\r\\n\\r\\n\\r\\n\\r\\n\\r\\nIs this email not displaying correctly?\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda378a2f7dccb8e2=\\r\\n6b5b302c761b48c753ae37dd56d06342bd7761205726e794f19c \\r\\nView it in your browser=2E \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda37898ee592de28=\\r\\nce7d2b78cb72be88185744d89b55cde5d34a4ad6dba714d942a6 \\r\\nSHARE THIS\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda3721d18ab2f8a5=\\r\\na53c473e99b28bc622950017c4958d9d3d47623dfaa963b2fe2f \\r\\nSHARE THIS\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda370f716da4e7a0=\\r\\nd2fc45cc1fb72979495e447d7a98e5f9d8e8385598d3a36bf093 \\r\\n\\r\\n\\r\\nSkimm for November 2nd\\r\\n\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda3737d8d9ff2a65=\\r\\n6be9041e65e9ad55a4c1c75a705f32854b662fd04c5b65251cda \\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\nSkimm'd while getting up on what's at stake in 2016=2E \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e4ff0b477556=\\r\\nee2011e623a9141618b501e48d7ada8a3ed516d0b3c0f3f9fa1b \\r\\nReady to vote? \\r\\n\\r\\n\\r\\n\\r\\nQUOTE OF THE DAY\\r\\n\\r\\n\\r\\n\\\";Moisture harvested from the clouds\\\"; - \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015187600c0dbcd6d=\\r\\n17ec7331f3cfc3b5f32608d5bedc4d80db9f776591ce730db6c1 \\r\\nA description  of how Sky PA, a new Scottish beer, is made=2E Cloudy with a=\\r\\n chance of hipster=2E\\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015180b3ee957efc7=\\r\\n43770fa87d771b9e197126982da785ce1918153b77f693c3ce8b \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518f2d49f9d3484=\\r\\n562b6c859a38a9b756ddc039572f4aa0f9c5037e50c2d0bae33c \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e7517a647921=\\r\\n45e64a9ed2022fa40ac9e1b913bf4cbf414945e77b598a60d31d \\r\\n\\r\\nGAS PAINS\\r\\n\\r\\nTHE STORY\\r\\nEarlier this week, a major oil pipeline that runs through Alabama \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015180ed06f7e37bd=\\r\\n36a28c9824956812295ae4df7646f389759b34dfd2fda72017a9 \\r\\nexploded =2E\\r\\nWAIT&#8230;BACK UP=2E\\r\\nThe Colonial Pipeline is a system of pipes thousands of miles long that car=\\r\\nries millions of barrels of gas, diesel, and jet fuel aday from Texas to Ne=\\r\\nw Jersey=2E It supplies about a third of the East Coast's gas=2E So it's a =\\r\\nBF gassy D=2E But the pipeline's run into some problems lately=2E Earlier t=\\r\\nhis fall, part of it was \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518813c5513accd=\\r\\n483e8db374227aa0e48b9f59b7e9d29b9b6525b209d347dd27b5 \\r\\nshut down  for over a week after a leak in Alabama spilled hundreds of thou=\\r\\nsands of gallons of gas=2E Cue a \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015185f16c50280b9=\\r\\nde78bd24094b4015e8630991b8156c7bcd4cb37b39824eefd5b3 \\r\\ngas shortage  and prices in southern states going up, up, up=2E\\r\\nSO WHAT'S THE LATEST?\\r\\nOn Monday, there was an explosion along the pipeline in Alabama that caused=\\r\\n a major fire=2E One person was killed and several others were injured=2E Y=\\r\\nesterday, Alabama's governor declared a state of emergencyto help make sure=\\r\\n gas is delivered throughout the state=2EPart of the pipeline's been shut d=\\r\\nown and now the company that operates it says it \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518f1a7a6aa74e5=\\r\\n4eaee3f22c427c587f46e59947aedc70e2ddbba3242f6bfb8b69 \\r\\ncould take days  to get the whole thing up and running again=2E Sofornow it=\\r\\nlooks like gas prices could be back on the up and up=2E\\r\\ntheSKIMM\\r\\nA major pipeline that millions of people depend on for gas every day can't =\\r\\nseem to get off the struggle bus=2E And now this latest incident has fuel c=\\r\\nompanies scrambling to stock up on supply=2E Ifit starts costing more to fi=\\r\\nll up the tank, this is why=2E\\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518ea9e4fa2f243=\\r\\n3c44e74693af3cd9018f5ece13b52fa953ffffb71b777ffa0e25 \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015183b3cc0c3f30e=\\r\\n436433c367ff042d3890808122395e67f4344c779240b0fb96b2 \\r\\n\\r\\nREPEAT AFTER ME=2E=2E=2E\\r\\n\\r\\nWHAT TO SAYTO YOUR FRIEND WHO GREW UP ON DEEP DISH PIZZA&#8230;\\r\\n\\r\\nIn it to win it=2E Last night, the \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518eb92f32ee674=\\r\\nc5ae648f0c76b48d0f861ccc7e253e391b9fc24ffef3c6d321fb \\r\\nChicago Cubs tied up the World Series  by beating the Cleveland Indians in =\\r\\nGame 6=2E They won 9-3=2E Now all eyes are on Game 7 tonight=2E The Cubs ha=\\r\\nven't won the series since 1908=2E The Indians haven't won since 1948=2E So=\\r\\n it's not like there's a lot on the line or anything=2E Break out the peanu=\\r\\nts=2E\\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151855235b31e076=\\r\\n45b8ad0f96b07b7084f725abc8922a18c65775c160f2b08e39a2 \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518bba5a4134f1a=\\r\\nd3263f4b6e4af51516c63ab1f22dacc4f432de7db7b200638eeb \\r\\n\\r\\nWHAT TO SAY WHEN YOU FIND OUT YOURTWO FRIENDS WHO YOU INTRODUCED ARE GETTIN=\\r\\nG TOGETHERWITHOUT YOU=2E=2E=2E\\r\\n\\r\\nWhat's going on here? Earlier this week, \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518335c687702c2=\\r\\na91cdae5c278dd891b638aef063583271a5bb0b6836c0953412f \\r\\na federal judge ordered  the Republican party to explain any deals it mayha=\\r\\nve made with GOP nominee Donald Trump's campaign to monitor the polls durin=\\r\\ng this election season=2E Reminder: for months, Trump has been saying this =\\r\\nelection is\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015181282a946ce17=\\r\\nef3bccb16f5ee0ab70200fc13d574dfa402946fb8f75a3a9f0f7 \\r\\n\\\";rigged\\\"; against him, and encouraging his supporters to be on the lookout=\\r\\n for voter fraud=2E Early voting opened recently in a lot of states=2E And =\\r\\nthere have been reports of Trump supporters allegedly photographing and \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015187d61ecd759d7=\\r\\nb0730c22d96dc27b5bbdf7ca9040c492390b0af040f590c882b5 \\r\\nharassing  people at the polls=2E That's according to a string of different=\\r\\n lawsuits inArizona, Ohio, and Nevada flaggingconcerns that some voters are=\\r\\n being intimidated=2E Last week, the Democratic party filed a separate comp=\\r\\nlaint with a federal court=2E Now, the Republican party has until the end o=\\r\\nf today to turn over any evidence that could be related to collaboratingwit=\\r\\nh the Trump campaign=2EStay tuned=2E\\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518dc0466271971=\\r\\n9d7c96a196b6493a840195c679622ee661714c17971d712ff8e3 \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e34779768729=\\r\\n1180617bc85052596e23d29e50c671041e605e72fc704ed3d004 \\r\\n\\r\\nWHAT TO SAY TO WHEN YOUR AM TRAIN GETS DELAYED FOR THE FIFTH TIME&#8230;\\r\\n\\r\\nI give up=2E Yesterday, Russia put Syrian peace talks on hold=2E \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518f6d20f368728=\\r\\neb2eed5d4f06ed7405a177f0acf26b236762f262bfa4aa7f8044 \\r\\nIndefinitely=2E  For years, Syria has been going through a violent civil wa=\\r\\nr between Syrian President Bashar al-Assad (backed by Russia) and rebel gro=\\r\\nups (some backed by the US) who want him out of power=2E Hundreds of thousa=\\r\\nnds of people have been killed=2E Millions have been forced to leave home, =\\r\\ncausing the EU's worst migrant and refugee crisis since World War II=2E The=\\r\\n two sides have been trying and failing to hash out a peace deal for a whil=\\r\\ne now=2E Earlier this month, Russia agreed to a \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151881e18b7235c5=\\r\\n73a07dea1d6f66d4e48cd2742d456c53c4803082f69bb0806362 \\r\\n\\\";humanitarian pause\\\";  to airstrikes in Aleppo - the key rebel stronghold =\\r\\nwhere a lot of thefighting has been focused=2E Meanwhile, rebel's have been=\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015189ef9643b4194=\\r\\n48cc8bcbcc8372e8c961fd45cc9331da20df64ae665f2298570e \\r\\nfighting back in the city =2E And today, Russia announced it would give reb=\\r\\nels \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518afb1f76fc2f9=\\r\\ne26bd6f8888964170d459663fedc1855df664b5efa241f0c9f2b \\r\\neven more time  to leave Aleppo=2EBut it's still walking away from the peac=\\r\\ne table for the foreseeable future=2E So, Syria's still a peace of work=2E\\r\\n=\\r\\n\\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151842b260af3e1b=\\r\\nc0d9404642d2e41e7037c150074a140f84985b3132dcecd8d9b8 \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151878e0dacd88d3=\\r\\nd84627a4a4c82c3debbbab214d0eab433d4640110ad6d8f2c6e4 \\r\\n\\r\\nWHAT TO SAY WHEN YOUR DATE SAYS HE NEEDS A NOISE MACHINE ON HIGH VOLUME TO =\\r\\nFALL ASLEEP=2E=2E=2E\\r\\n\\r\\nHaving second thoughts=2E That's what Gannett is telling Tronc (the artist =\\r\\nformerly known as Tribune Publishing)=2E For months, Gannett, which owns ab=\\r\\nout a hundred news outlets including USA Today, has been \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518bdef4b851c42=\\r\\n6335e6df0d77034c6d82d93d562a29526e8f2686412ced239f5f \\r\\ntrying to buy Tronc , which owns nameslike the LA Times and Chicago Tribune=\\r\\n=2E For years, print news companies have been struggling to keepad dollars =\\r\\nand readers on board=2E Gannett was hoping that the power of two media comp=\\r\\nanies combined would be more attractive than one, especially to advertisers=\\r\\n=2E The dealwould have created one of the largest media groups in the count=\\r\\nry=2E So earlier this year, Gannett saddled up to Tronc with a buyout offer=\\r\\n of around $400 million=2E Tronc said 'no, thanks, we're not that cheap=2E'=\\r\\n Yesterday, after a lot ofback and forth, Gannett \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518d2bffab0c234=\\r\\nf17b032b9701c23e88c1cd13f48f1fdc3ceff250bcb5f1df018d \\r\\nwalked away from the deal=2E  This mightalso be because Gannett had 'meh' e=\\r\\narnings last quarter, meaning they couldn't get the cash together to write =\\r\\nthe check=2E Either way, both companies are still single AF=2E\\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015182da3184f75be=\\r\\n5fdc54fecb1ac3b84096d5f14c82b9c1885506577545687ffc20 \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151819c57a5144e9=\\r\\n444db885889da1a6f479ee69473d5dc1540bded14eb2254d7a61 \\r\\n\\r\\nWHAT TO SAY TO YOUR FRIEND WHO ONLY DOES SQUATS AT THE GYM =2E=2E=2E\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151874e24bacd196=\\r\\n875a37d50bd496ccbfdb973ca05810b522ba89490afa4a8a9a45 \\r\\nSay goodbye to your fave emoji=2E \\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518989cbf077489=\\r\\n0fc15ee0775e5993d2e6f4301ecd0fb28b902dce8178e03e765a \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015186faa6a631bb0=\\r\\nb3f66a437bac5f5683830b4a05e4edd8e9289820f4606048f5a4 \\r\\n\\r\\nSKIMM THE VOTE\\r\\n\\r\\nIn case you somehow missed it, you have to vote on Tuesday=2E Catch up on w=\\r\\nho's on your ballot and what's at stake=2E Then plan your trip to the polls=\\r\\n=2E \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015188c8432bed622=\\r\\n7d27e00e716a22c0a3cb51beaa8ddacfe063b60ac233b6606f25 \\r\\nQuestions, answered=2E \\r\\n\\r\\nSkimm This\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518d25ca1d7e82b=\\r\\nf2beb04f9ea0fd4fac2a209c9e8fa3162db1c538ebe60b2cb8be \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518fa56ed304e70=\\r\\ndc7806f79721441d3f2dba8a0d21fe6daaa263e2640a05536a61 \\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015181e724ef34cab=\\r\\nd7ed78134c92f94f694fbdb799777126a5594c4ea20eaa22ed06 \\r\\n\\r\\nSKIMM 50\\r\\n\\r\\nThanks to these Skimm'bassadors for sharing like it's hot=2E Want to see yo=\\r\\nur name on this list? \\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518ceda3d8e1ed0=\\r\\nb62e87b8df3fe3ecb8a5f3ad2f674819afcdb1005e18c5f072d3 \\r\\nClick here  to learn more=2E\\r\\n\\r\\nOlivia Simonson, Brittany Berger, Daniela Rivas, Ashley Lawson, Sarah David=\\r\\nson, L Denease Thompson-Mack, Noreen Deutsch, Jessee Fordham, Natasha Shah,=\\r\\n Shari Berga, Aloke Prabhu, Natalie Tenzer, Samantha Panchevre, Ania Arseno=\\r\\nwicz, Ali Wozniak, Reinalyn, Taunya Robinson, Cassie Christensen, Spencer P=\\r\\nhilips, Abby Bilinski, Aner Zhou, Julia Erdenebold, Mehreen Mazhar, Alexand=\\r\\nra Rizzo, Alisa Sutton, Maegan Detlefs, Janina Ancona, Chris Drake, Allan M=\\r\\noss, Jordan Murray, David Benjamin, Leslie Bartula, Hana Muslic, Kelly Wall=\\r\\nace, Leslie Buteyn, Bethany Fitzgerald, Stuart Ferguson, Emily Paulino, All=\\r\\nison Ryan, Niki DeMaio, Ria Conti, Nikki Boyd, Amanda Oliver, Lindsay Barth=\\r\\nel, Shelby Wynn, Brittany Daunno, Emily Rissmiller, Alex Johannes, Andrea B=\\r\\norod, Ashley Montufar, Karli Von Herbulis, Lexie Rindler, Natalie Weaver, A=\\r\\nllison Sotelo, Cynthia Lopez, Isabel Taylor, Jess Tolan, Kasie Heiden, Kels=\\r\\ney Will, Leslie McFayden, Morgan Balavage, Morgan Goracke, Madeline Trainor=\\r\\n, Alexa Parisi, Uma Sudarshan, Brooke Borkowski, Connie Lin, Jess Ridolfino=\\r\\n, Jessica Foster, Mallory Mickus, Sapreen Abbass, Kelli Holland\\r\\n\\r\\nSKIMM SHARE\\r\\n\\r\\nHalfway there=2E Share theSkimm with your work wife whoalways takes a coffe=\\r\\ne break when you need it=2E\\r\\n\\r\\nhttp://www=2Etheskimm=2Ecom/invite/v2/new?email=3Dchristine@spang=2Ecc&utm_=\\r\\nsource=3Demail&utm_medium=3Dinvite&utm_campaign=3Dbottom \\r\\n\\r\\nSKIMM BIRTHDAYS\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015185aea2b21fa15=\\r\\n1bb8b11f09d4ab4aefdd45bee2439742b48454e60e800bcdb09f \\r\\n* indicates Skimm'bassador=2EHammer time=2E\\r\\n\\r\\nLouise Cronin (San Francisco, CA); Faith Greenberg(Longwood, FL);*Hatley Th=\\r\\nompson(Washington, DC);*Monique Ervine(Kindersley, Canada);*Leana Macrito(C=\\r\\nhicago, IL);*AnnMarie Murtaugh(Houston, TX);*Maria Perwerton(Rochester, MI)=\\r\\n;*Lauren Valainis(Washington, DC);*Jaycie Moller(San Francisco, CA);*Erin M=\\r\\nanfull(Iowa City, IA);*Jennifer Rheaume(Boston, MA);*Karishma Tank(New York=\\r\\n, NY);*Neelima Agrawal(Chicago, IL);*Eliza Webb(Seattle, WA );*Conoly Crave=\\r\\nns(Atlanta, GA);*Brent Randall(New York, NY);*Alicia Heiser(Spokane, WA);*N=\\r\\nicole Rodriguez(Sebastian, FL);Sarah Hofschire(Framingham, MA);Aakankhya Pa=\\r\\ntro(College Station, TX);Kalon Taylor(Memphis, TN);Joyce A(Milford, CT);Nor=\\r\\na Delay(Bali, Indonesia);Ashleigh Heaton(Astoria, NY);Angie Teates(Washingt=\\r\\non, DC);Michelle Aclander(Stony Brook, NY);Laura Dominick(Highland Park, NJ=\\r\\n);Polly Minifie Snyder(Washington, DC);Sarah Minifie Wolfgang(Boston, MA);K=\\r\\norrie Nickels(Chicago, IL);Kristyn Gelsomini(Houston, TX);Meaghan Horton(Ne=\\r\\nw York, NY);Dannetta Gibson Ballou(Columbia MD);Suzie Tice(Great Falls, MT)=\\r\\n;Annette Bani(Limerick, PA);Zoe Berman(Armonk, NY);Rebekah Harper(Raleigh, =\\r\\nNC);Alessandra Messineo Long(Greenwich, CT);Anne McArthur(Louisville, KY);J=\\r\\nennifer Wham;Karin Seymour(Fairfield, CT);Lauren DiNicola(New Haven, CT);Lu=\\r\\ncy Jackoboice(Grand Rapids, MI);Zoe Weiss(New York, NY)\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\\r\\nSkimm'd something we missed?\\r\\n\\r\\n\\r\\n\\r\\nEmail \\r\\nmailto:SkimmThis@theSkimm=2Ecom \\r\\nSkimmThis@theSkimm=2Ecom -\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda378a2f7dccb8e2=\\r\\n6b5b302c761b48c753ae37dd56d06342bd7761205726e794f19c \\r\\nRead in browser >> \\r\\n\\r\\n\\r\\nSHARE & FOLLOW US\\r\\n\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda37226dc355defd=\\r\\n764c4a5bacce1c02696337681417286260dfec3f16ecc90018bc \\r\\nFacebook\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda37b28f80f69268=\\r\\n41e373f2063b283fd3960ec2f4f135612ad6b273d8e901e4e7c5 \\r\\nTwitter\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda3784ba9df91bdb=\\r\\n28220027be1a35943845c0ff17cf92fc7b3b89cbe9ce6eee6814 \\r\\nTumblr\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda37254b18b2d0b0=\\r\\n3609f0ba749107959301d96068b56b4cb840c3dafa14865c8eae \\r\\nInstagram\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda3708ba55eaca8d=\\r\\nf450cdc2a1ff1d14b5c3865e1d4fe0745f6c13ceab0948df5210 \\r\\nPinterest\\r\\n\\r\\n\\r\\nCopyright (c) 2016 theSkimm, All rights reserved=2E\\r\\n\\r\\n\\r\\nOur mailing address is: \\r\\n\\r\\ntheSkimm Inc=2E\\r\\n\\r\\n49 W 23rd Street, 10th Floor\\r\\n\\r\\nNew York, NY, 10010, United States\\r\\n\\r\\n\\r\\nhttp://click=2Emorning7=2Etheskimm=2Ecom/profile_center=2Easpx?qs=3Deec37f3=\\r\\n2b3ca83aeebff6369cdb5b630c8bd88a7221b6c466ba89d97456c40da8f9fd278c3d66e231b=\\r\\n08d677113d878806c912d8c8d6295d \\r\\nUpdate Profile \\r\\nhttp://pages=2Emorning7=2Etheskimm=2Ecom/page=2Easpx?QS=3D773ed3059447707d8=\\r\\n211c803e40aaa3b9e56b8b07b4622c1&e=3DY2hyaXN0aW5lQHNwYW5nLmNj \\r\\nUnsubscribe=20\\r\\n\\r\\n\\r\\n\\r\\n\\r\\n\",\"2\":\"\\r\\n\\r\\n<!DOCTYPE html PUBLIC \\\"-//W3C//DTD XHTML 1=2E0 Transitional//EN\\\" \\\"http://ww=\\r\\nw=2Ew3=2Eorg/TR/xhtml1/DTD/xhtml1-transitional=2Edtd\\\">\\r\\n<html>\\r\\n  <head>\\r\\n    <meta http-equiv=3D\\\"Content-Type\\\" content=3D\\\"text/html; charset=3DUTF-8=\\r\\n\\\">\\r\\n    <!-- Responsive Design -->\\r\\n    <!-- Facebook sharing information tags -->\\r\\n    <img src=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/open=2Easpx?ffcb10=\\r\\n-ff2511747d6d-fecc16717d64017e-fe881372756c027a7c-ff9d1670-fece167170640474=\\r\\n-ff68107375\\\" width=3D\\\"1\\\" height=3D\\\"1\\\">\\r\\n    <meta property=3D\\\"og:image\\\" content=3D\\\"http://cdn=2Etheskimm=2Ecom/asse=\\r\\nts/skimm-fb-logo=2Epng\\\">\\r\\n    <meta name=3D\\\"viewport\\\" content=3D\\\"width=3Ddevice-width\\\">\\r\\n\\r\\n      <style type=3D\\\"text/css\\\">\\r\\n    body{\\r\\n      color:#000 !important;\\r\\n    }\\r\\n    p=2Eskimm-p a,a,a:link,a:hover,a:visited{\\r\\n      color:#009f9c!important;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n    a:hover{\\r\\n      text-decoration:underline;\\r\\n    }\\r\\n    p{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:15px;\\r\\n      line-height:20px;\\r\\n      letter-spacing:0em;\\r\\n      color:#000;\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n    }\\r\\n    #logo{\\r\\n      text-decoration:none;\\r\\n      display:block;\\r\\n      padding-top:18px;\\r\\n      margin:0 auto;\\r\\n    }\\r\\n    #missed{\\r\\n      padding:34px 0;\\r\\n    }\\r\\n    #missed p{\\r\\n      text-align:center;\\r\\n      padding:0 0 3px;\\r\\n      font-size:14px;\\r\\n    }\\r\\n    #sharing{\\r\\n      padding:24px 0 32px;\\r\\n      margin:0 auto 35px;\\r\\n      background:#009f9b;\\r\\n      width:100%;\\r\\n    }\\r\\n    #sharing h2{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-weight:bold;\\r\\n      text-transform:uppercase;\\r\\n      text-align:center;\\r\\n      letter-spacing:0=2E28em;\\r\\n      padding:0 0 24px;\\r\\n      margin:0;\\r\\n      font-style:normal;\\r\\n      font-size:13px;\\r\\n      color:#fff;\\r\\n    }\\r\\n    #sharing =2Eshare_icons{\\r\\n      margin:0 auto;\\r\\n      text-align:center;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare{\\r\\n      display:inline-block;\\r\\n      text-transform:uppercase;\\r\\n      color:#fff;\\r\\n      text-decoration:none;\\r\\n      text-align:center;\\r\\n      margin-right:15px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare img{\\r\\n      display:block;\\r\\n      font-size:28px;\\r\\n      margin:0 auto 10px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons span:last-child =2Eshare:last-child{\\r\\n      margin-left:7px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare:last-child img{\\r\\n      margin:0 auto 8px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare span{\\r\\n      display:block;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:11px;\\r\\n      font-weight:bold;\\r\\n      letter-spacing:3px;\\r\\n    }\\r\\n    =2Eimg_el{\\r\\n      margin:0 auto 22px;\\r\\n      display:block;\\r\\n    }\\r\\n    =2Eretinaonlyicon{\\r\\n      width:46px;\\r\\n      height:49px;\\r\\n    }\\r\\n    =2Etheskimm{\\r\\n      text-transform:none !important;\\r\\n    }\\r\\n    #rss-content p,#rss-content h1,#rss-content h2,#rss-content h3,#rss-con=\\r\\ntent img,#rss-content hr{\\r\\n      margin-left:auto;\\r\\n      margin-right:auto;\\r\\n    }\\r\\n    =2Eskimm-birthdays,=2Eskimm-shareus,=2Eskimm-gift,=2Eskimm-life{\\r\\n      padding-bottom:15px !important;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-shareus{\\r\\n      background:url(http://cdn=2Etheskimm=2Ecom/email/3/normal/skimmsend_i=\\r\\ncon=2Epng) no-repeat 50% 0;\\r\\n      text-align:center;\\r\\n      padding-top:50px;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-p{\\r\\n      line-height:23px;\\r\\n      color:#000;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:16px;\\r\\n      padding-bottom:15px !important;\\r\\n    }\\r\\n    =2Eshare-jumpto-links{\\r\\n      margin-top:0px !important;\\r\\n      margin-bottom:12px !important;\\r\\n      margin-left:auto;\\r\\n      margin-right:auto;\\r\\n    }\\r\\n    =2Eskimm-h1{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:22px !important;\\r\\n      font-weight:bold;\\r\\n      color:#000;\\r\\n      border-bottom:3px solid #00A49F;\\r\\n      padding:12px 0 !important;\\r\\n      margin-top:8px;\\r\\n      margin-bottom:24px;\\r\\n      text-rendering:geometricPrecision;\\r\\n      text-align:left;\\r\\n      letter-spacing:0=2E08em;\\r\\n    }\\r\\n    =2Eskimm-h2{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-weight:bold;\\r\\n      font-size:20px;\\r\\n      letter-spacing:0=2E01em;\\r\\n      color:#000;\\r\\n      padding:0 0 0 0;\\r\\n      margin-top:22px;\\r\\n      margin-bottom:12px;\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2Eskimm-h3{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-weight:bold !important;\\r\\n      font-size:15px;\\r\\n      letter-spacing:0=2E15em;\\r\\n      line-height:20px;\\r\\n      color:#000;\\r\\n      padding:0;\\r\\n      margin-bottom:4px;\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-h3-quote{\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-birthdays{\\r\\n      text-align:center;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-life{\\r\\n      text-align:center;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-gift{\\r\\n      text-align:center;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-hr{\\r\\n      border:0;\\r\\n      margin-bottom:0 !important;\\r\\n    }\\r\\n    =2Eskimm-hr=2Eskimm-hr-thick{\\r\\n      border-top:3px solid #009f9c !important;\\r\\n      background:none !important;\\r\\n      margin-bottom:25px !important;\\r\\n    }\\r\\n    =2Eskimm-hr=2Eskimm-hr-thin{\\r\\n      border-top:1px solid #bfbfbf !important;\\r\\n      height:0 !important;\\r\\n      background:#fff !important;\\r\\n      margin-bottom:20px !important;\\r\\n    }\\r\\n    =2Eskimm-hr=2Eskimm-hr-thin=2Eskimm-hr-teal{\\r\\n      border-top:1px solid #009f9c !important;\\r\\n    }\\r\\n    #outlook a{\\r\\n      padding:0;\\r\\n    }\\r\\n    body{\\r\\n      width:100% !important;\\r\\n    }\\r\\n    =2EReadMsgBody{\\r\\n      width:100%;\\r\\n    }\\r\\n    =2EExternalClass{\\r\\n      width:100%;\\r\\n    }\\r\\n    body{\\r\\n      -webkit-text-size-adjust:none;\\r\\n    }\\r\\n    body{\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n    }\\r\\n    img{\\r\\n      border:none;\\r\\n      height:auto;\\r\\n      line-height:100%;\\r\\n      margin:0;\\r\\n      outline:none;\\r\\n      padding:0;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n    #backgroundTable{\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n      width:100% !important;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section background color\\r\\n  @tip Set the background color for your email=2E You may want to choose on=\\r\\ne that matches your company's branding=2E\\r\\n  @theme page\\r\\n  */\\r\\n    body,#backgroundTable{\\r\\n      /*@editable*/background-color:#ffffff;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 1\\r\\n  @tip Set the styling for all first-level headings in your emails=2E These=\\r\\n should be the largest of your headings=2E\\r\\n  @style heading 1\\r\\n  */\\r\\n    h1,=2Eh1{\\r\\n      /*@editable*/color:#202020;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:32px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 2\\r\\n  @tip Set the styling for all second-level headings in your emails=2E\\r\\n  @style heading 2\\r\\n  */\\r\\n    h2,=2Eh2{\\r\\n      /*@editable*/color:#303030;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:26px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      padding-bottom:6px;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 3\\r\\n  @tip Set the styling for all third-level headings in your emails=2E\\r\\n  @style heading 3\\r\\n  */\\r\\n    h3,=2Eh3{\\r\\n      /*@editable*/color:#404040;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:22px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 4\\r\\n  @tip Set the styling for all fourth-level headings in your emails=2E Thes=\\r\\ne should be the smallest of your headings=2E\\r\\n  @style heading 4\\r\\n  */\\r\\n    h4,=2Eh4{\\r\\n      /*@editable*/color:#505050;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:18px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section preheader style\\r\\n  @tip Set the background color for your email's preheader area=2E\\r\\n  @theme page\\r\\n  */\\r\\n    #templatePreheader{\\r\\n      /*@editable*/background-color:#ffffff;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section preheader text\\r\\n  @tip Set the styling for your email's preheader text=2E Choose a size and=\\r\\n color that is easy to read=2E\\r\\n  */\\r\\n    =2EpreheaderContent{\\r\\n      /*@tab Header\\r\\n@section preheader text\\r\\n@tip Set the styling for your email's preheader text=2E Choose a size and c=\\r\\nolor that is easy to read=2E*/border-bottom:3px solid #000;\\r\\n    }\\r\\n    =2EpreheaderContent div{\\r\\n      /*@editable*/color:#505050;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:10px;\\r\\n      /*@editable*/line-height:100%;\\r\\n      /*@editable*/text-align:center;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section preheader link\\r\\n  @tip Set the styling for your email's preheader links=2E Choose a color t=\\r\\nhat helps them stand out from your text=2E\\r\\n  */\\r\\n    =2EpreheaderContent div a:link,=2EpreheaderContent div a:visited,=2Epre=\\r\\nheaderContent div a =2Eyshortcuts{\\r\\n      /*@editable*/color:#009f9c !important;\\r\\n      /*@editable*/font-weight:normal;\\r\\n      /*@editable*/text-decoration:underline;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section header style\\r\\n  @tip Set the background color and border for your email's header area=2E\\r\\n=\\r\\n\\r\\n  @theme header\\r\\n  */\\r\\n    #templateHeader{\\r\\n      /*@editable*/background-color:#ffffff;\\r\\n      /*@editable*/border-bottom:0;\\r\\n      padding:0px;\\r\\n    }\\r\\n  /*\\r\\n  @tab Body\\r\\n  @section body style\\r\\n  @tip Set the background color for your email's body area=2E\\r\\n  */\\r\\n    #templateContainer,=2EbodyContent{\\r\\n      /*@editable*/background-color:#FFFFFF;\\r\\n    }\\r\\n    #sharing-cells{\\r\\n      margin-top:15px;\\r\\n    }\\r\\n    #sharing-cells td{\\r\\n      width:50%;\\r\\n      padding:0 8px;\\r\\n    }\\r\\n    #sharing-cells td:first-child{\\r\\n      padding-left:0;\\r\\n    }\\r\\n    #sharing-cells td:last-child{\\r\\n      padding-right:0;\\r\\n    }\\r\\n    #sharing-cells a:link,#sharing-cells a:visited,#sharing-cells a{\\r\\n      display:block;\\r\\n      height:22px;\\r\\n      line-height:22px;\\r\\n      background:#009f9c;\\r\\n      position:relative;\\r\\n      color:#ffffff;\\r\\n      text-decoration:none;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:13px;\\r\\n      font-weight:bold;\\r\\n    }\\r\\n    #sharing-cells img{\\r\\n      position:relative;\\r\\n      margin-left:10px;\\r\\n    }\\r\\n    #sharing-cells =2Eheader-email-img{\\r\\n      top:1px;\\r\\n    }\\r\\n    #sharing-cells =2Eheader-twitter-img{\\r\\n      top:1px;\\r\\n    }\\r\\n    #sharing-cells =2Eheader-facebook-img{\\r\\n      top:2px;\\r\\n    }\\r\\n  /*\\r\\n  @tab Body\\r\\n  @section body text\\r\\n  @tip Set the styling for your email's main content text=2E Choose a size =\\r\\nand color that is easy to read=2E\\r\\n  @theme main\\r\\n  */\\r\\n    =2EbodyContent{\\r\\n      /*@tab Body\\r\\n@section body text\\r\\n@tip Set the styling for your email's main content text=2E Choose a size an=\\r\\nd color that is easy to read=2E\\r\\n@theme main*/text-align:left;\\r\\n    }\\r\\n    =2EbodyContent p{\\r\\n      margin:0 auto;\\r\\n      padding-bottom:26px;\\r\\n      font-size:14px;\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2EbodyContent p strong{\\r\\n      font-weight:bold;\\r\\n    }\\r\\n    =2EbodyContent hr{\\r\\n      height:3px;\\r\\n      background:#000;\\r\\n      border-top:0;\\r\\n      border-bottom:0;\\r\\n      border-left:0;\\r\\n      border-right:0;\\r\\n      width:145px;\\r\\n      margin-bottom:25px;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section footer style\\r\\n  @tip Set the background color and top border for your email's footer area=\\r\\n=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    #templateFooter{\\r\\n      /*@editable*/background-color:#FFFFFF;\\r\\n      /*@editable*/border-top:0;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section footer text\\r\\n  @tip Set the styling for your email's footer text=2E Choose a size and co=\\r\\nlor that is easy to read=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    =2EfooterContent div{\\r\\n      /*@tab Footer\\r\\n@section footer text\\r\\n@tip Set the styling for your email's footer text=2E Choose a size and colo=\\r\\nr that is easy to read=2E\\r\\n@theme footer*/font-size:12px;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section footer link\\r\\n  @tip Set the styling for your email's footer links=2E Choose a color that=\\r\\n helps them stand out from your text=2E\\r\\n  */\\r\\n    =2EfooterContent div p,=2EfooterContent div a:link,=2EfooterContent div=\\r\\n a:visited,=2EfooterContent div a =2Eyshortcuts{\\r\\n      /*@tab Footer\\r\\n@section footer link\\r\\n@tip Set the styling for your email's footer links=2E Choose a color that h=\\r\\nelps them stand out from your text=2E*/text-align:left;\\r\\n      font-size:12px;\\r\\n    }\\r\\n    =2EfooterContent img{\\r\\n      display:inline;\\r\\n    }\\r\\n    #footerContentLeft a,#monkeyRewards a{\\r\\n      color:#009f9c;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section social bar style\\r\\n  @tip Set the background color and border for your email's footer social b=\\r\\nar=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    #social{\\r\\n      /*@editable*/background-color:#FAFAFA;\\r\\n      /*@editable*/border:0;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section social bar style\\r\\n  @tip Set the background color and border for your email's footer social b=\\r\\nar=2E\\r\\n  */\\r\\n    #social div{\\r\\n      /*@editable*/text-align:center;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section utility bar style\\r\\n  @tip Set the background color and border for your email's footer utility =\\r\\nbar=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    #utility{\\r\\n      /*@editable*/background-color:#FFFFFF;\\r\\n      /*@editable*/border:0;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section utility bar style\\r\\n  @tip Set the background color and border for your email's footer utility =\\r\\nbar=2E\\r\\n  */\\r\\n    #utility div{\\r\\n      /*@editable*/text-align:center;\\r\\n    }\\r\\n    #monkeyRewards img{\\r\\n      max-width:190px;\\r\\n    }\\r\\n    =2EheaderContent a{\\r\\n      color:#000000;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n    =2EheaderContent p{\\r\\n      font-weight:bold;\\r\\n    }\\r\\n  @media only screen and (max-width: 675px),\\r\\n    (-webkit-min-device-pixel-ratio: 1=2E5),\\r\\n  (min-resolution: 144dpi) and (device-width: 1080px) and (orientation: por=\\r\\ntrait),\\r\\n  (-webkit-min-device-pixel-ratio: 3=2E0){\\r\\n    body,table,td,p,a,li,blockquote{\\r\\n      -webkit-text-size-adjust:none !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    table[id=3DtemplatePreheader],table[id=3DtemplateContainer],table[id=3D=\\r\\ntemplateHeader],table[id=3DtemplateBody],table[id=3DtemplateFooter],table[i=\\r\\nd=3DinnerTemplateContainer],=2Eskimm-shareus-bg{\\r\\n      width:100% !important;\\r\\n      margin:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    table[id=3DtemplatePreheader] td,table[id=3DtemplateContainer] td,table=\\r\\n[id=3DtemplateHeader] td,table[id=3DtemplateBody] td,table[id=3DtemplateFoo=\\r\\nter] td,table[id=3DinnerTemplateContainer] td,=2Eskimm-shareus-bg{\\r\\n      padding-left:0 !important;\\r\\n      padding-right:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells{\\r\\n      width:100% !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells a{\\r\\n      height:35px !important;\\r\\n      line-height:35px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells a span{\\r\\n      display:none;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells td{\\r\\n      width:50% !important;\\r\\n      padding:0 2px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells td:first-child{\\r\\n      padding-left:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells td:last-child{\\r\\n      padding-right:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    =2EpreheaderContent div{\\r\\n      font-size:14px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    =2EpreheaderContent a{\\r\\n      display:block;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #rss-content p,#rss-content h1,#rss-content h2,#rss-content h3,#rss-con=\\r\\ntent hr,=2Eskimm-shareus-bg,=2Eshare-jumpto-links{\\r\\n      margin-left:5px !important;\\r\\n      margin-right:5px !important;\\r\\n      width:auto !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #missed p span{\\r\\n      display:block;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing h2{\\r\\n      font-size:13px !important;\\r\\n      padding-bottom:15px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing =2Eshare_icons =2Eshare{\\r\\n      margin-right:3px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing =2Eshare_icons span{\\r\\n      display:block;\\r\\n      margin-bottom:15px;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #footerContentLeft,#monkeyRewards{\\r\\n      width:100% !important;\\r\\n      display:block !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #footerContentLeft{\\r\\n      margin-bottom:15px;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    =2Eskimm-shareus-bg{\\r\\n      padding-left:5px !important;\\r\\n      padding-right:5px !important;\\r\\n    }\\r\\n\\r\\n}   #outlook a{\\r\\n      padding:0;\\r\\n    }\\r\\n    body{\\r\\n      width:100% !important;\\r\\n      -webkit-text-size-adjust:100%;\\r\\n      -ms-text-size-adjust:100%;\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n    }\\r\\n    =2EExternalClass{\\r\\n      width:100%;\\r\\n    }\\r\\n    =2EExternalClass,=2EExternalClass p,=2EExternalClass span,=2EExternalCl=\\r\\nass font,=2EExternalClass td,=2EExternalClass div{\\r\\n      line-height:100%;\\r\\n    }\\r\\n    =2Eecxfacebook span,=2Eecxtwitter span{\\r\\n      padding-right:10px!important;\\r\\n    }\\r\\n    a=2Eecxshare span{\\r\\n      padding:10px 0!important;\\r\\n    }\\r\\n    a=2Eecxshare img{\\r\\n      display:inline-block!important;\\r\\n    }\\r\\n    #backgroundTable{\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n      width:100% !important;\\r\\n      line-height:100% !important;\\r\\n    }\\r\\n    img{\\r\\n      outline:none;\\r\\n      text-decoration:none;\\r\\n      border:none;\\r\\n      -ms-interpolation-mode:bicubic;\\r\\n    }\\r\\n    a img{\\r\\n      border:none;\\r\\n    }\\r\\n    =2Eimage_fix{\\r\\n      display:block;\\r\\n    }\\r\\n    p{\\r\\n      margin:0px 0px !important;\\r\\n    }\\r\\n    table td{\\r\\n      border-collapse:collapse;\\r\\n    }\\r\\n    table{\\r\\n      border-collapse:collapse;\\r\\n      mso-table-lspace:0pt;\\r\\n      mso-table-rspace:0pt;\\r\\n    }\\r\\n    a{\\r\\n      color:#009f9c !important;\\r\\n      text-decoration:none!important;\\r\\n    }\\r\\n    table[class=3Dfull]{\\r\\n      width:100%;\\r\\n      clear:both;\\r\\n    }\\r\\n  @media only screen and (max-width: 640px){\\r\\n    a[href^=3Dtel],a[href^=3Dsms]{\\r\\n      text-decoration:none;\\r\\n      color:#009f9c;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    =2Emobile_link a[href^=3Dtel],=2Emobile_link a[href^=3Dsms]{\\r\\n      text-decoration:default;\\r\\n      color:#009f9c !important;\\r\\n      pointer-events:auto;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Ddevicewidth]{\\r\\n      width:440px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    td[class=3Ddevicewidth]{\\r\\n      width:440px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Ddevicewidth]{\\r\\n      width:440px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Dbanner]{\\r\\n      width:440px!important;\\r\\n      height:147px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Ddevicewidthinner]{\\r\\n      width:420px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Ddevicewidthinner]{\\r\\n      width:420px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Dicontext]{\\r\\n      width:345px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Dcolimg2]{\\r\\n      width:420px!important;\\r\\n      height:243px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Demhide]{\\r\\n      display:none!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Dlogo]{\\r\\n      width:425px!important;\\r\\n      height:198px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    a[href^=3Dtel],a[href^=3Dsms]{\\r\\n      text-decoration:none;\\r\\n      color:#009f9c;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    =2Emobile_link a[href^=3Dtel],=2Emobile_link a[href^=3Dsms]{\\r\\n      text-decoration:default;\\r\\n      color:#009f9c !important;\\r\\n      pointer-events:auto;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Ddevicewidth]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    td[class=3Ddevicewidth]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n      font-size:14px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    p[class=3Dskimm-p]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n      font-size:16px!important;\\r\\n      line-height:22px!important;\\r\\n      padding-bottom:20px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    img[class=3Dbanner]{\\r\\n      width:300px!important;\\r\\n      height:93px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Ddevicewidthinner]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Dicontext]{\\r\\n      width:186px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    img[class=3Dcolimg2]{\\r\\n      width:260px!important;\\r\\n      height:150px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Demhide]{\\r\\n      display:none!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    img[class=3Dlogo]{\\r\\n      width:300px!important;\\r\\n      height:140px!important;\\r\\n      margin:0 auto;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Emobile-block{\\r\\n      display:block !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-column-top{\\r\\n      padding:12px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Ebullet{\\r\\n      display:block !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eemail-title{\\r\\n      text-align:center !important;\\r\\n      min-width:300px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-header{\\r\\n      min-width:300px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-facebook-and-twitter{\\r\\n      text-align:center !important;\\r\\n      min-width:320px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-tumblr-instagram-pinterest{\\r\\n      text-align:center !important;\\r\\n      min-width:320px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[id=3DcanspamBar] td{\\r\\n      font-size:14px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[id=3DcanspamBar] td a{\\r\\n      display:block !important;\\r\\n      margin-top:10px !important;\\r\\n    }\\r\\n\\r\\n}</style></head>\\r\\n<custom type=3D\\\"content\\\" name=3D\\\"feed\\\">\\r\\n<body yahoofix=3D\\\"\\\">\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"2\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" align=3D\\\"center\\\" valign=3D\\\"middle\\\" style=\\r\\n=3D\\\"font-family: Helvetica, arial, sans-serif; font-size: 10px;color:#22222=\\r\\n2;padding:10px;\\\">\\r\\n    <img src=3D\\\"https://knifeopen=2Ecom/open?key=3DtbXNJbs7ktiOLQaM&e=3DY2h=\\r\\nyaXN0aW5lQHNwYW5nLmNj\\\" border=3D\\\"0\\\" width=3D\\\"1\\\" height=3D\\\"1\\\" style=3D\\\"heigh=\\r\\nt:1px !important; width:1px !important; border: 0 !important; margin: 0 !im=\\r\\nportant; padding: 0 !important; overflow:hidden !important\\\">\\r\\n    Is this email not displaying correctly?\\r\\n                  <a href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=\\r\\n=3D580bedd5217eda378a2f7dccb8e26b5b302c761b48c753ae37dd56d06342bd7761205726=\\r\\ne794f19c\\\" target=3D\\\"_blank\\\" style=3D\\\"color: #009f9c !important; text-decora=\\r\\ntion: none\\\" class=3D\\\"mobile-block\\\">View it in your browser=2E</a>\\r\\n                </td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"2\\\"></td>\\r\\n              </tr>\\r\\n              <tr bgcolor=3D\\\"#202020\\\">\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"3\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr valign=3D\\\"middle\\\">\\r\\n                <td width=3D\\\"49%\\\" style=3D\\\"max-width:305px;font-family: 'He=\\r\\nlvetica', 'Arial', sans-serif; font-size: 14px; font-weight: normal; hyphen=\\r\\ns: auto; padding:14px;\\\" bgcolor=3D\\\"#009f9c\\\" valign=3D\\\"middle\\\" class=3D\\\"shar=\\r\\ne-column-top\\\">\\r\\n                  <a class=3D\\\"twitter\\\" href=3D\\\"http://click=2Emorning7=2Eth=\\r\\neskimm=2Ecom/?qs=3D580bedd5217eda37522cd8eba442ef808cdfbce15cef6d2289b67738=\\r\\n60cff7d43a14a4cd720436de\\\" style=3D\\\"color: #fff !important; display: block; =\\r\\nfont-size: 14px; font-weight: bold; text-align: center; text-decoration: no=\\r\\nne\\\"><span style=3D\\\"\\\">SHARE THIS</span><img class=3D\\\"header-twitter-img\\\" src=\\r\\n=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/header_twitter=2Epng\\\" width=\\r\\n=3D\\\"17\\\" height=3D\\\"12\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: non=\\r\\ne; clear: both; display: inline-block; float: none; margin-left: 10px; max-=\\r\\nwidth: 100%; outline: none; position: relative; text-decoration: none;left:=\\r\\n5px;\\\" align=3D\\\"none\\\"></a>\\r\\n                </td>\\r\\n                <td width=3D\\\"2%\\\" style=3D\\\"max-width:10px;\\\"></td>\\r\\n                <td width=3D\\\"49%\\\" style=3D\\\"max-width:305px;font-family: 'He=\\r\\nlvetica', 'Arial', sans-serif; font-size: 14px; font-weight: normal; hyphen=\\r\\ns: auto; padding:14px;\\\" bgcolor=3D\\\"#009f9c\\\" valign=3D\\\"middle\\\" class=3D\\\"shar=\\r\\ne-column-top\\\">\\r\\n                  <a class=3D\\\"facebook\\\" href=3D\\\"http://click=2Emorning7=2Et=\\r\\nheskimm=2Ecom/?qs=3D580bedd5217eda37c0f6dcfc7fe9573401cc2ca5dd8a2ec58771970=\\r\\n36407a2321901ac67b909273e\\\" style=3D\\\"color: #fff !important; display: block;=\\r\\n font-size: 14px; font-weight: bold; text-align: center; text-decoration: n=\\r\\none\\\"><span style=3D\\\"\\\">SHARE THIS</span><img class=3D\\\"header-facebook-img\\\" s=\\r\\nrc=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/header_facebook=2Epng\\\" wid=\\r\\nth=3D\\\"5\\\" height=3D\\\"12\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: no=\\r\\nne; clear: both; display: inline-block; float: none; margin-left: 10px; max=\\r\\n-width: 100%; outline: none; position: relative; text-decoration: none; lef=\\r\\nt:5px;\\\" align=3D\\\"none\\\"></a>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"21\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" align=3D\\\"center\\\">\\r\\n                  <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" align=3D\\\"center\\\" cellspacing=3D\\\"0\\\" cellpadding=3D\\\"0\\\" border=3D\\\"0=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td align=3D\\\"center\\\" class=3D\\\"email-title-logo\\\">\\r\\n                          <a target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorni=\\r\\nng7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda370f716da4e7a0d2fc45cc1fb72979495e=\\r\\n447d7a98e5f9d8e8385598d3a36bf093\\\"><img width=3D\\\"500\\\" border=3D\\\"0\\\" height=3D=\\r\\n\\\"233\\\" alt=3D\\\"\\\" style=3D\\\"display:block; border:none; outline:none; text-deco=\\r\\nration:none;\\\" src=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/preflight/l=\\r\\nogo_google_skimm_20161102=2Epng\\\" class=3D\\\"logo\\\"></a>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td align=3D\\\"center\\\" style=3D\\\"color: #222222;font-f=\\r\\namily: 'Helvetica', 'Arial', sans-serif; font-size: 18px;font-weight: bold;=\\r\\nmin-width: 320px; width: 100%\\\" class=3D\\\"email-title\\\">\\r\\n                          Skimm for November 2nd\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td align=3D\\\"center\\\">\\r\\n                          <a target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorni=\\r\\nng7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda3737d8d9ff2a656be9041e65e9ad55a4c1=\\r\\nc75a705f32854b662fd04c5b65251cda\\\"><img height=3D\\\"40\\\" width=3D\\\"205\\\" border=\\r\\n=3D\\\"0\\\" alt=3D\\\"\\\" style=3D\\\"display:block; border:none; outline:none;text-deco=\\r\\nration:none;\\\" src=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/inviteCTA=\\r\\n=2Epng\\\"></a>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D=\\r\\n\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"30\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                            Skimm&#8217;d w=\\r\\nhile getting up on what&#8217;s at stake in 2016=2E <a href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e4ff0b477556ee2011e623a9=\\r\\n141618b501e48d7ada8a3ed516d0b3c0f3f9fa1b\\\" target=3D\\\"_blank\\\">Ready to vote?<=\\r\\n/a>&#160;\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D=\\r\\n\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"20\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                            <td style=3D\\\"font-family: Helvetica, arial, san=\\r\\ns-serif; font-size: 15px; font-weight:bold; color: #000000; text-align:left=\\r\\n;line-height: 20px; letter-spacing:0=2E15em;\\\" class=3D\\\"title\\\">\\r\\n                              QUOTE OF THE DAY\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                              <p class=3D\\\"skimm-p\\\">&#8220;Moisture harveste=\\r\\nd from the clouds&#8221; - <a style=3D\\\"color:#009f9c!important;text-decorat=\\r\\nion:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e195839=\\r\\n9f015187600c0dbcd6d17ec7331f3cfc3b5f32608d5bedc4d80db9f776591ce730db6c1\\\" ta=\\r\\nrget=3D\\\"_blank\\\">A description</a> of how Sky PA, a new Scottish beer, is ma=\\r\\nde=2E Cloudy with a chance of hipster=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\"><div styl=\\r\\ne=3D\\\"box-sizing:border-box;position:relative;height:45px;\\\">\\r\\n                                <div style=3D\\\"padding:0;overflow:hidden;col=\\r\\nor:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: 12px=\\r\\n;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;text-=\\r\\nalign:left;\\\">\\r\\n                                  <span style=3D\\\"display:inline-block;margi=\\r\\nn-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;vertical-=\\r\\nalign:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://click=2E=\\r\\nmorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015180b3ee957efc743770fa87d771b9=\\r\\ne197126982da785ce1918153b77f693c3ce8b\\\"><img src=3D\\\"http://cdn=2Etheskimm=2E=\\r\\ncom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;vertica=\\r\\nl-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" heig=\\r\\nht=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vertical=\\r\\n-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518f2d49f9d3484562b6c859a38=\\r\\na9b756ddc039572f4aa0f9c5037e50c2d0bae33c\\\"><img src=3D\\\"http://cdn=2Etheskimm=\\r\\n=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;vert=\\r\\nical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D\\\"2=\\r\\n5\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                          <a style=3D\\\"width: 20px; height: 25px; vertical-a=\\r\\nlign: bottom; margin: 5px 5px 0; display: inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e7517a64792145e64a9ed2=\\r\\n022fa40ac9e1b913bf4cbf414945e77b598a60d31d\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/icon_share_instagram=2Epng\\\" style=3D\\\"display: block=\\r\\n; vertical-align: middle; padding-top: 0px; margin: 0 auto !important;\\\" wid=\\r\\nth=3D\\\"20\\\" height=3D\\\"20\\\" alt=3D\\\"Insta This\\\" /></a>\\r\\n                                            </div>\\r\\n                                </div></div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n   <tbody>\\r\\n      <tr>\\r\\n         <td>\\r\\n            <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"=\\r\\n0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n               <tbody>\\r\\n                  <tr>\\r\\n                     <td width=3D\\\"100%\\\">\\r\\n                        <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"=\\r\\nmax-width:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"=\\r\\ncenter\\\" class=3D\\\"devicewidth\\\">\\r\\n                           <tbody>\\r\\n                              <tr>\\r\\n                                 <td>\\r\\n                                    <table width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:600px;\\\" align=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\"=\\r\\n class=3D\\\"devicewidthinner\\\">\\r\\n                                       <tbody>\\r\\n                                          <tr>\\r\\n                                             <td style=3D\\\"font-family: Helv=\\r\\netica, arial, sans-serif; font-size: 24px; font-weight:bold; color: #000000=\\r\\n; text-align:left;line-height: 24px; letter-spacing:2px;\\\">\\r\\n\\r\\n    <h1 class=3D'skimm-h1' id=3D'top-story' style=3D'font-size:24px!importa=\\r\\nnt;font-weight:bold;color:#000;border-bottom:3px solid #00A49F;padding:12px=\\r\\n0!important;margin-top:8px;margin-bottom:24px;text-rendering:geometricPreci=\\r\\nsion;text-align:left;letter-spacing:0=2E08em;'>GAS PAINS</h1>              =\\r\\n                               </td>\\r\\n                                          </tr>\\r\\n                                       </tbody>\\r\\n                                    </table>\\r\\n                                 </td>\\r\\n                              </tr>\\r\\n                           </tbody>\\r\\n                        </table>\\r\\n                     </td>\\r\\n                  </tr>\\r\\n               </tbody>\\r\\n            </table>\\r\\n         </td>\\r\\n      </tr>\\r\\n   </tbody>\\r\\n</table>\\r\\n      <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspaci=\\r\\nng=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-weight:bold;font-size:20px;letter-spacing:0=2E01em;color:#000;p=\\r\\nadding:0000;padding-bottom:12px;text-align:left;\\\">\\r\\n                                                              </td>\\r\\n                            </tr>\\r\\n                            <!--skimmPH:[top-story]-->                     =\\r\\n       <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">THE STORY</h3>\\r\\n<p class=3D\\\"skimm-p\\\">Earlier this week, a major oil pipeline that runs thro=\\r\\nugh Alabama <a style=3D\\\"color:#009f9c!important;text-decoration:none;\\\" href=\\r\\n=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015180ed06f7e=\\r\\n37bd36a28c9824956812295ae4df7646f389759b34dfd2fda72017a9\\\" target=3D\\\"_blank\\\"=\\r\\n>exploded</a>=2E</p>\\r\\n<h3 class=3D\\\"skimm-h3\\\">WAIT&#8230;BACK UP=2E</h3>\\r\\n<p class=3D\\\"skimm-p\\\">The Colonial Pipeline is a system of pipes thousands o=\\r\\nf miles long that carries millions of barrels of gas, diesel, and jet fuel =\\r\\na&#160;day from Texas to New Jersey=2E It supplies about a third of the Eas=\\r\\nt Coast&#8217;s gas=2E So it&#8217;s a BF gassy D=2E But the pipeline&#8217=\\r\\n;s run into some problems lately=2E Earlier this fall, part of it was <a st=\\r\\nyle=3D\\\"color:#009f9c!important;text-decoration:none;\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518813c5513accd483e8db37422=\\r\\n7aa0e48b9f59b7e9d29b9b6525b209d347dd27b5\\\" target=3D\\\"_blank\\\">shut down</a> f=\\r\\nor over a week after a leak in Alabama spilled hundreds of thousands of gal=\\r\\nlons of gas=2E Cue a <a style=3D\\\"color:#009f9c!important;text-decoration:no=\\r\\nne;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151=\\r\\n85f16c50280b9de78bd24094b4015e8630991b8156c7bcd4cb37b39824eefd5b3\\\" target=\\r\\n=3D\\\"_blank\\\">gas shortage</a> and prices in southern states going up, up, up=\\r\\n=2E</p>\\r\\n<h3 class=3D\\\"skimm-h3\\\">SO WHAT&#8217;S THE LATEST?</h3>\\r\\n<p class=3D\\\"skimm-p\\\">On Monday, there was an explosion along the pipeline i=\\r\\nn Alabama that caused a major fire=2E One person was killed and several oth=\\r\\ners were injured=2E Yesterday, Alabama&#8217;s governor declared a state of=\\r\\n emergency&#160;<span style=3D\\\"color: #000000;\\\">to help make sure gas is de=\\r\\nlivered throughout the state=2E&#160;</span>Part of the pipeline&#8217;s be=\\r\\nen shut down and now the company that operates it says it <a style=3D\\\"color=\\r\\n:#009f9c!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=\\r\\n=2Etheskimm=2Ecom/?qs=3D13e1958399f01518f1a7a6aa74e54eaee3f22c427c587f46e59=\\r\\n947aedc70e2ddbba3242f6bfb8b69\\\" target=3D\\\"_blank\\\">could take days</a> to get=\\r\\n the whole thing up and running again=2E So&#160;for&#160;now it&#160;looks=\\r\\n like gas prices could be back on the up and up=2E</p>\\r\\n<h3 class=3D\\\"skimm-h3\\\">theSKIMM</h3>\\r\\n<p class=3D\\\"skimm-p\\\">A major pipeline that millions of people depend on for=\\r\\n gas every day can&#8217;t seem to get off the struggle bus=2E And now this=\\r\\n latest incident has fuel companies scrambling to stock up on supply=2E If&=\\r\\n#160;it starts costing more to fill up the tank, this is why=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518ea9e4fa2f2433c44e74693a=\\r\\nf3cd9018f5ece13b52fa953ffffb71b777ffa0e25\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015183b3cc0c3f30e436433c367=\\r\\nff042d3890808122395e67f4344c779240b0fb96b2\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n   <tbody>\\r\\n      <tr>\\r\\n         <td>\\r\\n            <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"=\\r\\n0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n               <tbody>\\r\\n                  <tr>\\r\\n                     <td width=3D\\\"100%\\\">\\r\\n                        <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"=\\r\\nmax-width:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"=\\r\\ncenter\\\" class=3D\\\"devicewidth\\\">\\r\\n                           <tbody>\\r\\n                              <tr>\\r\\n                                 <td>\\r\\n                                    <table width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:600px;\\\" align=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\"=\\r\\n class=3D\\\"devicewidthinner\\\">\\r\\n                                       <tbody>\\r\\n                                          <tr>\\r\\n                                             <td>\\r\\n\\r\\n    <h1 class=3D'skimm-h1' style=3D'font-size:24px!important;font-weight:bo=\\r\\nld;color:#000;border-bottom:3px solid #00A49F;padding:12px0!important;margi=\\r\\nn-top:8px;margin-bottom:24px;text-rendering:geometricPrecision;text-align:l=\\r\\neft;letter-spacing:0=2E08em;'>REPEAT AFTER ME=2E=2E=2E</h1>                =\\r\\n                             </td>\\r\\n                                          </tr>\\r\\n                                       </tbody>\\r\\n                                    </table>\\r\\n                                 </td>\\r\\n                              </tr>\\r\\n                           </tbody>\\r\\n                        </table>\\r\\n                     </td>\\r\\n                  </tr>\\r\\n               </tbody>\\r\\n            </table>\\r\\n         </td>\\r\\n      </tr>\\r\\n   </tbody>\\r\\n</table>\\r\\n      <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspaci=\\r\\nng=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY&#160;TO =\\r\\nYOUR FRIEND WHO GREW UP ON DEEP DISH PIZZA&#8230;</h3>\\r\\n<h3 class=3D\\\"skimm-h3\\\"></h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">In it to win it=2E Las=\\r\\nt night, the <a style=3D\\\"color:#009f9c!important;text-decoration:none;\\\" hre=\\r\\nf=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518eb92f32=\\r\\nee674c5ae648f0c76b48d0f861ccc7e253e391b9fc24ffef3c6d321fb\\\" target=3D\\\"_blank=\\r\\n\\\">Chicago Cubs tied up the World Series</a> by beating the Cleveland Indian=\\r\\ns in Game 6=2E They won 9-3=2E Now all eyes are on Game 7 tonight=2E The Cu=\\r\\nbs haven&#8217;t won the series since 1908=2E The Indians haven&#8217;t won=\\r\\n since 1948=2E So it&#8217;s not like there&#8217;s a lot on the line or an=\\r\\nything=2E Break out the peanuts=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151855235b31e07645b8ad0f96b=\\r\\n07b7084f725abc8922a18c65775c160f2b08e39a2\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518bba5a4134f1ad3263f4b6e=\\r\\n4af51516c63ab1f22dacc4f432de7db7b200638eeb\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY WHEN YOU=\\r\\n FIND OUT YOUR&#160;TWO FRIENDS WHO YOU INTRODUCED ARE GETTING TOGETHER&#16=\\r\\n0;WITHOUT YOU=2E=2E=2E</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">What&#8217;s going on =\\r\\nhere? Earlier this week, <a style=3D\\\"color:#009f9c!important;text-decoratio=\\r\\nn:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f=\\r\\n01518335c687702c2a91cdae5c278dd891b638aef063583271a5bb0b6836c0953412f\\\" targ=\\r\\net=3D\\\"_blank\\\">a federal judge ordered</a> the Republican party to explain a=\\r\\nny deals it may&#160;have made with GOP nominee Donald Trump&#8217;s campai=\\r\\ngn to monitor the polls during this election season=2E Reminder: for months=\\r\\n, Trump has been saying this election is&#160;<a style=3D\\\"color:#009f9c!imp=\\r\\nortant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2E=\\r\\ncom/?qs=3D13e1958399f015181282a946ce17ef3bccb16f5ee0ab70200fc13d574dfa40294=\\r\\n6fb8f75a3a9f0f7\\\" target=3D\\\"_blank\\\">&#8220;rigged&#8221;</a>&#160;against hi=\\r\\nm, and encouraging his supporters to be on the lookout for voter fraud=2E E=\\r\\narly voting opened recently in a lot of states=2E And there have been repor=\\r\\nts of Trump supporters allegedly photographing and <a style=3D\\\"color:#009f9=\\r\\nc!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheski=\\r\\nmm=2Ecom/?qs=3D13e1958399f015187d61ecd759d7b0730c22d96dc27b5bbdf7ca9040c492=\\r\\n390b0af040f590c882b5\\\" target=3D\\\"_blank\\\">harassing</a> people at the polls=\\r\\n=2E That&#8217;s according to a string of different lawsuits in&#160;Arizon=\\r\\na, Ohio, and Nevada flagging&#160;concerns that some voters are being intim=\\r\\nidated=2E Last week, the Democratic party filed a separate complaint with a=\\r\\n federal court=2E Now, the Republican party has until the end of today to t=\\r\\nurn over any evidence that could be related to collaborating&#160;with the =\\r\\nTrump campaign=2E&#160;Stay tuned=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518dc04662719719d7c96a196b=\\r\\n6493a840195c679622ee661714c17971d712ff8e3\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e347797687291180617bc8=\\r\\n5052596e23d29e50c671041e605e72fc704ed3d004\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY TO WHEN =\\r\\nYOUR AM TRAIN GETS DELAYED FOR THE FIFTH TIME&#8230;</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">I give up=2E Yesterday=\\r\\n, Russia put Syrian peace talks on hold=2E <a style=3D\\\"color:#009f9c!import=\\r\\nant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom=\\r\\n/?qs=3D13e1958399f01518f6d20f368728eb2eed5d4f06ed7405a177f0acf26b236762f262=\\r\\nbfa4aa7f8044\\\" target=3D\\\"_blank\\\">Indefinitely=2E</a> For years, Syria has be=\\r\\nen going through a violent civil war between Syrian President Bashar al-Ass=\\r\\nad (backed by Russia) and rebel groups (some backed by the US) who want him=\\r\\n out of power=2E Hundreds of thousands of people have been killed=2E Millio=\\r\\nns have been forced to leave home, causing the EU&#8217;s worst migrant and=\\r\\n refugee crisis since World War II=2E The two sides have been trying and fa=\\r\\niling to hash out a peace deal for a while now=2E Earlier this month, Russi=\\r\\na agreed to a <a style=3D\\\"color:#009f9c!important;text-decoration:none;\\\" hr=\\r\\nef=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151881e18b=\\r\\n7235c573a07dea1d6f66d4e48cd2742d456c53c4803082f69bb0806362\\\" target=3D\\\"_blan=\\r\\nk\\\">&#8220;humanitarian pause&#8221;</a> to airstrikes in Aleppo - the key r=\\r\\nebel stronghold where a lot of the&#160;fighting has been focused=2E Meanwh=\\r\\nile, rebel&#8217;s have been&#160;<a style=3D\\\"color:#009f9c!important;text-=\\r\\ndecoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13=\\r\\ne1958399f015189ef9643b419448cc8bcbcc8372e8c961fd45cc9331da20df64ae665f22985=\\r\\n70e\\\" target=3D\\\"_blank\\\">fighting back in the city</a>=2E And today, Russia a=\\r\\nnnounced it would give rebels <a style=3D\\\"color:#009f9c!important;text-deco=\\r\\nration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e195=\\r\\n8399f01518afb1f76fc2f9e26bd6f8888964170d459663fedc1855df664b5efa241f0c9f2b\\\"=\\r\\n target=3D\\\"_blank\\\">even more time</a> to leave Aleppo=2E&#160;But it&#8217;=\\r\\ns still walking away from the peace table for the foreseeable future=2E So,=\\r\\n Syria&#8217;s still a peace of work=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151842b260af3e1bc0d9404642d=\\r\\n2e41e7037c150074a140f84985b3132dcecd8d9b8\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151878e0dacd88d3d84627a4a4=\\r\\nc82c3debbbab214d0eab433d4640110ad6d8f2c6e4\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY WHEN YOU=\\r\\nR DATE SAYS HE NEEDS A NOISE MACHINE ON HIGH VOLUME TO FALL ASLEEP=2E=2E=2E=\\r\\n</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">Having second thoughts=\\r\\n=2E That&#8217;s what Gannett is telling Tronc (the artist formerly known a=\\r\\ns Tribune Publishing)=2E For months, Gannett, which owns about a hundred ne=\\r\\nws outlets including USA Today, has been <a style=3D\\\"color:#009f9c!importan=\\r\\nt;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?=\\r\\nqs=3D13e1958399f01518bdef4b851c426335e6df0d77034c6d82d93d562a29526e8f268641=\\r\\n2ced239f5f\\\" target=3D\\\"_blank\\\">trying to buy Tronc</a>, which owns names&#16=\\r\\n0;like the LA Times and Chicago Tribune=2E For years, print news companies =\\r\\nhave been struggling to keep&#160;ad dollars and readers on board=2E Gannet=\\r\\nt was hoping that the power of two media companies combined would be more a=\\r\\nttractive than one, especially to advertisers=2E The deal&#160;would have c=\\r\\nreated one of the largest media groups in the country=2E So earlier this ye=\\r\\nar, Gannett saddled up to Tronc with a buyout offer of around $400 million=\\r\\n=2E Tronc said &#8216;no, thanks, we&#8217;re not that cheap=2E&#8217; Yest=\\r\\nerday, after a lot of&#160;back and forth, Gannett <a style=3D\\\"color:#009f9=\\r\\nc!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheski=\\r\\nmm=2Ecom/?qs=3D13e1958399f01518d2bffab0c234f17b032b9701c23e88c1cd13f48f1fdc=\\r\\n3ceff250bcb5f1df018d\\\" target=3D\\\"_blank\\\">walked away from the deal=2E</a> Th=\\r\\nis might&#160;also be because Gannett had &#8216;meh&#8217; earnings last q=\\r\\nuarter, meaning they couldn&#8217;t get the cash together to write the chec=\\r\\nk=2E Either way, both companies are still single AF=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015182da3184f75be5fdc54fecb1=\\r\\nac3b84096d5f14c82b9c1885506577545687ffc20\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151819c57a5144e9444db88588=\\r\\n9da1a6f479ee69473d5dc1540bded14eb2254d7a61\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-p skimm-h3\\\">WHAT TO SAY =\\r\\nTO YOUR FRIEND WHO ONLY DOES SQUATS AT THE GYM =2E=2E=2E</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\"><a style=3D\\\"color:#009=\\r\\nf9c!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Ethes=\\r\\nkimm=2Ecom/?qs=3D13e1958399f0151874e24bacd196875a37d50bd496ccbfdb973ca05810=\\r\\nb522ba89490afa4a8a9a45\\\" target=3D\\\"_blank\\\">Say goodbye to your fave emoji=2E=\\r\\n</a></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518989cbf0774890fc15ee0775=\\r\\ne5993d2e6f4301ecd0fb28b902dce8178e03e765a\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015186faa6a631bb0b3f66a437b=\\r\\nac5f5683830b4a05e4edd8e9289820f4606048f5a4\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"0\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                                                           =\\r\\n                     <h1 class=3D\\\"skimm-h1\\\" style=3D\\\"font-size:24px!importa=\\r\\nnt;font-weight:bold;color:#000;border-bottom:3px solid #00A49F;padding:12px=\\r\\n0!important;margin-top:8px;margin-bottom:24px;text-rendering:geometricPreci=\\r\\nsion;text-align:left;letter-spacing:0=2E08em;\\\">SKIMM THE VOTE</h1>\\r\\n                                                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <table width=3D\\\"100%\\\" style=3D\\\"text-align:l=\\r\\neft!important;margin:0 auto!important;max-width:595px\\\" cellpadding=3D\\\"0\\\" ce=\\r\\nllspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"left\\\">\\r\\n                                  <tbody>\\r\\n                                    <tr>\\r\\n                                      <td width=3D\\\"100%\\\" align=3D\\\"left\\\" val=\\r\\nign=3D\\\"middle\\\">\\r\\n                                        <img src=3D\\\"http://cdn=2Etheskimm=\\r\\n=2Ecom/email/3/retina/together-with-Google=2Epng\\\" width=3D\\\"300\\\" height=3D\\\"2=\\r\\n8\\\" border=3D\\\"0\\\" style=3D\\\"border:none;min-height:auto;line-height:100%;margi=\\r\\nn:0;outline:none;padding:0;text-decoration:none\\\">\\r\\n                                      </td>\\r\\n                                    </tr>\\r\\n                                    <tr>\\r\\n                                      <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td=\\r\\n>\\r\\n                                    </tr>\\r\\n                                  </tbody>\\r\\n                                </table>\\r\\n                              </td>\\r\\n                            </tr>                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                                <p class=3D=\\r\\n\\\"skimm-p\\\"><span style=3D\\\"color: #000000;\\\">In case you somehow missed it, yo=\\r\\nu have to vote <span class=3D\\\"aBn\\\" data-term=3D\\\"goog_1567168121\\\" tabindex=\\r\\n=3D\\\"0\\\"><span class=3D\\\"aQJ\\\">on Tuesday</span></span>=2E Catch up on who&#821=\\r\\n7;s on your ballot and what&#8217;s at stake=2E Then plan your trip to the =\\r\\npolls=2E </span><span style=3D\\\"color: #000000;\\\"><a style=3D\\\"color:#009f9c!i=\\r\\nmportant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=\\r\\n=2Ecom/?qs=3D13e1958399f015188c8432bed6227d27e00e716a22c0a3cb51beaa8ddacfe0=\\r\\n63b60ac233b6606f25\\\" target=3D\\\"_blank\\\">Questions, answered=2E</a>&#160;</spa=\\r\\nn></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518d25ca1d7e82bf2beb04f9ea=\\r\\n0fd4fac2a209c9e8fa3162db1c538ebe60b2cb8be\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518fa56ed304e70dc7806f797=\\r\\n21441d3f2dba8a0d21fe6daaa263e2640a05536a61\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                          <tr>\\r\\n                                <td style=3D\\\"font-family:Helvetica,Arial,sa=\\r\\nns-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;marg=\\r\\nin:0;padding:0;text-align:center;\\\">\\r\\n                                  <a id=3D\\\"stv_badge\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015181e724ef34cabd7ed78134c92=\\r\\nf94f694fbdb799777126a5594c4ea20eaa22ed06\\\" target=3D\\\"_blank\\\"><img style=3D\\\"m=\\r\\nargin-bottom:20px;\\\" src=3D\\\"https://cdn=2Etheskimm=2Ecom/email/3/retina/Pled=\\r\\nge-To-Vote-R1=2Epng\\\" width=3D\\\"100\\\" height=3D\\\"100\\\" alt=3D\\\"Pledge to Vote\\\" />=\\r\\n</a>\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                                                      </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"0\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                                                           =\\r\\n               <h3 style=3D\\\"text-transform:uppercase;color:#000;display:blo=\\r\\nck;font-family:Helvetica,Arial,sans-serif;font-size:15px;font-weight:bold!i=\\r\\nmportant;line-height:20px;margin-top:20px;margin-right:auto;margin-bottom:4=\\r\\npx;margin-left:auto;text-align:center;letter-spacing:0=2E15em;padding:0;pad=\\r\\nding-bottom:15px!important\\\">SKIMM 50</h3>\\r\\n                                                            </td>\\r\\n                            </tr>\\r\\n                            <!--skimmPH:[skimm-custom-SKIMM 50]-->         =\\r\\n                   <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                                <p class=3D=\\r\\n\\\"skimm-p\\\"><em>Thanks to these Skimm&#8217;bassadors for sharing like it&#82=\\r\\n17;s hot=2E Want to see your name on this list? <a style=3D\\\"color:#009f9c!i=\\r\\nmportant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=\\r\\n=2Ecom/?qs=3D13e1958399f01518ceda3d8e1ed0b62e87b8df3fe3ecb8a5f3ad2f674819af=\\r\\ncdb1005e18c5f072d3\\\" target=3D\\\"_blank\\\">Click here</a> to learn more=2E&#160;=\\r\\n</em></p>\\r\\n<p class=3D\\\"skimm-p\\\"><span style=3D\\\"color: #222222;\\\">Olivia Simonson, Britt=\\r\\nany Berger, Daniela Rivas, Ashley Lawson, Sarah Davidson, L Denease Thompso=\\r\\nn-Mack, Noreen Deutsch, Jessee Fordham, Natasha Shah, Shari Berga, Aloke Pr=\\r\\nabhu, Natalie Tenzer, Samantha Panchevre, Ania Arsenowicz, Ali Wozniak, Rei=\\r\\nnalyn, Taunya Robinson, Cassie Christensen, Spencer Philips, Abby Bilinski,=\\r\\n Aner Zhou, Julia Erdenebold, Mehreen Mazhar, Alexandra Rizzo, Alisa Sutton=\\r\\n, Maegan Detlefs, Janina Ancona, Chris Drake, Allan Moss, Jordan Murray, Da=\\r\\nvid Benjamin, Leslie Bartula, Hana Muslic, Kelly Wallace, Leslie Buteyn, Be=\\r\\nthany Fitzgerald, Stuart Ferguson, Emily Paulino, Allison Ryan, Niki DeMaio=\\r\\n, Ria Conti, Nikki Boyd, Amanda Oliver, Lindsay Barthel, Shelby Wynn, Britt=\\r\\nany Daunno, Emily Rissmiller, Alex Johannes, Andrea Borod, Ashley Montufar,=\\r\\n Karli Von Herbulis, Lexie Rindler, Natalie Weaver, Allison Sotelo, Cynthia=\\r\\n Lopez, Isabel Taylor, Jess Tolan, Kasie Heiden, Kelsey Will, Leslie McFayd=\\r\\nen, Morgan Balavage, Morgan Goracke, Madeline Trainor, Alexa Parisi, Uma Su=\\r\\ndarshan, Brooke Borkowski, Connie Lin, Jess Ridolfino, Jessica Foster, Mall=\\r\\nory Mickus, Sapreen Abbass, Kelli Holland</span></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"20\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td>\\r\\n                              <h3 style=3D\\\"text-transform:uppercase;color:#=\\r\\n000;display:block;font-family:Helvetica,Arial,sans-serif;font-size:15px;fon=\\r\\nt-weight:bold!important;line-height:20px;margin-top:20px;margin-right:auto;=\\r\\nmargin-bottom:4px;margin-left:auto;text-align:center;letter-spacing:0=2E15e=\\r\\nm;padding:0;padding-bottom:15px!important\\\">SKIMM SHARE</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                                           =\\r\\n         <tr>\\r\\n                              <td class=3D\\\"share-button-copy\\\" style=3D\\\"font=\\r\\n-family:Helvetica,Arial,sans-serif;font-size:16px;line-height:20px;letter-s=\\r\\npacing:0em;color:#000;margin:0;padding:0;\\\">\\r\\n                                                                           =\\r\\n                     <p class=3D\\\"skimm-p\\\">Halfway there=2E Share theSkimm w=\\r\\nith your work wife who&#160;always takes a coffee break when you need it=2E=\\r\\n</p>\\r\\n<p class=3D\\\"skimm-p\\\">\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;text-align:center;\\\">\\r\\n                                <a style=3D\\\"color:#009f9c!important;text-de=\\r\\ncoration:none;\\\" href=3D\\\"http://www=2Etheskimm=2Ecom/invite/v2/new?email=3Dc=\\r\\nhristine@spang=2Ecc&utm_source=3Demail&utm_medium=3Dinvite&utm_campaign=3Db=\\r\\nottom\\\" target=3D\\\"_blank\\\"><img src=3D\\\"https://cdn=2Etheskimm=2Ecom/email/3/r=\\r\\netina/badge_share-the-skimm=2Epng\\\" width=3D\\\"100\\\" height=3D\\\"100\\\" alt=3D\\\"Shar=\\r\\ne theSkimm\\\" /></a>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"50\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"0\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                                            <img width=3D\\\"3=\\r\\n6\\\" height=3D\\\"39\\\" src=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/skimmbir=\\r\\nthdays_icon=2Epng\\\" border=3D\\\"0\\\" style=3D\\\"border:none;min-height:auto;line-h=\\r\\neight:100%;margin:0 auto 22px;outline:none;padding:0;text-decoration:none;d=\\r\\nisplay:block;margin-left:auto;margin-right:auto\\\">\\r\\n                                                                           =\\r\\n               <h3 style=3D\\\"text-transform:uppercase;color:#000;display:blo=\\r\\nck;font-family:Helvetica,Arial,sans-serif;font-size:15px;font-weight:bold!i=\\r\\nmportant;line-height:20px;margin-top:20px;margin-right:auto;margin-bottom:4=\\r\\npx;margin-left:auto;text-align:center;letter-spacing:0=2E15em;padding:0;pad=\\r\\nding-bottom:15px!important\\\">SKIMM BIRTHDAYS</h3>\\r\\n                                                            </td>\\r\\n                            </tr>\\r\\n                            <!--skimmPH:[skimm-birthdays-SKIMM BIRTHDAYS]--=\\r\\n>                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                                <p class=3D=\\r\\n\\\"skimm-p\\\"><span style=3D\\\"color: #000000;\\\"><b style=3D\\\"color: #222222;\\\"><a s=\\r\\ntyle=3D\\\"color:#009f9c!important;text-decoration:none;\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015185aea2b21fa151bb8b11f09d4=\\r\\nab4aefdd45bee2439742b48454e60e800bcdb09f\\\" target=3D\\\"_blank\\\" data-saferedire=\\r\\ncturl=3D\\\"https://www=2Egoogle=2Ecom/url?hl=3Den&amp;q=3Dhttp://theskimm=2Ec=\\r\\nom/skimm-guides/skimmbassadors&amp;source=3Dgmail&amp;ust=3D147810774272500=\\r\\n0&amp;usg=3DAFQjCNE7P50gy4ABMYMODv1IPwaf6ivegA\\\" style=3D\\\"color: #1155cc;\\\"><=\\r\\nspan style=3D\\\"color: #000000;\\\">* indicates Skimm&#8217;bassador=2E</span></=\\r\\na>&#160;Hammer time=2E</b></span></p>\\r\\n<p style=3D\\\"color: #222222;\\\" class=3D\\\"skimm-p\\\"><span style=3D\\\"color: #00000=\\r\\n0;\\\"><b>Louise Cronin </b>(San Francisco, CA);<b> Faith Greenberg&#160;</b>(=\\r\\nLongwood, FL);<b>&#160;*Hatley Thompson&#160;</b>(Washington, DC);&#160;<b>=\\r\\n*Monique Ervine&#160;</b>(Kindersley, Canada);&#160;<b>*Leana Macrito&#160;=\\r\\n</b>(Chicago, IL);&#160;<b>*AnnMarie Murtaugh</b>&#160;(Houston, TX);&#160;=\\r\\n<b>*Maria Perwerton&#160;</b>(Rochester, MI);&#160;<b>*Lauren Valainis&#160=\\r\\n;</b>(Washington, DC);&#160;<b>*Jaycie Moller&#160;</b>(San Francisco, CA);=\\r\\n&#160;<b>*Erin Manfull&#160;</b>(Iowa City, IA);&#160;<b>*Jennifer Rheaume&=\\r\\n#160;</b>(Boston, MA);&#160;<b>*Karishma Tank&#160;</b>(New York, NY);&#160=\\r\\n;<b>*Neelima Agrawal&#160;</b>(Chicago, IL);&#160;<b>*Eliza Webb&#160;</b>(=\\r\\nSeattle, WA );&#160;<b>*Conoly Cravens&#160;</b>(Atlanta, GA);&#160;<b>*Bre=\\r\\nnt Randall&#160;</b>(New York, NY);&#160;<b>*Alicia Heiser&#160;</b>(Spokan=\\r\\ne, WA);&#160;<b>*Nicole Rodriguez&#160;</b>(Sebastian, FL);&#160;<b>Sarah H=\\r\\nofschire&#160;</b>(Framingham, MA);&#160;<b>Aakankhya Patro</b>&#160;(Colle=\\r\\nge Station, TX);&#160;<b>Kalon Taylor</b>&#160;(Memphis, TN);&#160;<b>Joyce=\\r\\n A&#160;</b>(Milford, CT);&#160;<b>Nora Delay&#160;</b>(Bali, Indonesia);&#=\\r\\n160;<b>Ashleigh Heaton&#160;</b>(Astoria, NY);&#160;<b>Angie Teates&#160;</=\\r\\nb>(Washington, DC);&#160;<b>Michelle Aclander&#160;</b>(Stony Brook, NY);&#=\\r\\n160;<b>Laura Dominick&#160;</b>(Highland Park, NJ);&#160;<b>Polly Minifie S=\\r\\nnyder&#160;</b>(Washington, DC);&#160;<b>Sarah Minifie Wolfgang&#160;</b>(B=\\r\\noston, MA);&#160;<b>Korrie Nickels&#160;</b>(Chicago, IL);&#160;<b>Kristyn =\\r\\nGelsomini&#160;</b>(Houston, TX);&#160;<b>Meaghan Horton&#160;</b>(New York=\\r\\n, NY);&#160;<b>Dannetta Gibson Ballou&#160;</b>(Columbia MD);&#160;<b>Suzie=\\r\\n Tice&#160;</b>(Great Falls, MT);&#160;<b>Annette Bani</b>&#160;(Limerick, =\\r\\nPA);&#160;<b>Zoe Berman</b>&#160;(Armonk, NY);&#160;<b>Rebekah Harper</b>&#=\\r\\n160;(Raleigh, NC);&#160;<b>Alessandra Messineo Long</b>&#160;(Greenwich, CT=\\r\\n);&#160;<b>Anne McArthur</b>&#160;(Louisville, KY);&#160;<b>Jennifer Wham</=\\r\\nb>;&#160;<b>Karin Seymour</b>&#160;(Fairfield, CT);&#160;<b>Lauren DiNicola=\\r\\n</b>&#160;(New Haven, CT);&#160;<b>Lucy Jackoboice</b>&#160;(Grand Rapids, =\\r\\nMI);&#160;<b>Zoe Weiss</b>&#160;(New York, NY)</span></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"21\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\">\\r\\n                  <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" align=3D\\\"center\\\" cellspacing=3D\\\"0\\\" cellpadding=3D\\\"0\\\" border=3D\\\"0=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td style=3D\\\"color: #000; font-family: 'Helvetica',=\\r\\n 'Arial', sans-serif;font-size: 14px;font-weight: normal;line-height: 20px;=\\r\\n text-align:center;\\\">\\r\\n                          Skimm'd something we missed?\\r\\n                          <br>\\r\\n                          <br>\\r\\n                          Email <a href=3D\\\"mailto:SkimmThis@theSkimm=2Ecom\\\"=\\r\\n target=3D\\\"_blank\\\" style=3D\\\"color: #009f9c !important; text-decoration: non=\\r\\ne\\\">SkimmThis@theSkimm=2Ecom</a>  <span class=3D\\\"bullet\\\">&bull;</span> <a hr=\\r\\nef=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda378a2f7d=\\r\\nccb8e26b5b302c761b48c753ae37dd56d06342bd7761205726e794f19c\\\" target=3D\\\"_blan=\\r\\nk\\\" style=3D\\\"color: #009f9c !important; text-decoration: none\\\">Read in brows=\\r\\ner &raquo;</a>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\">\\r\\n                  <table bgcolor=3D\\\"#009f9c\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td>\\r\\n                          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" a=\\r\\nlign=3D\\\"left\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"dev=\\r\\nicewidthinner\\\">\\r\\n                            <tbody>\\r\\n                              <tr>\\r\\n                                <td style=3D\\\"padding:10px;font-family: 'Hel=\\r\\nvetica', 'Arial', sans-serif; color: #fff !important; display: block; font-=\\r\\nsize: 13px; font-weight: bold; text-align: center; text-decoration: none;le=\\r\\ntter-spacing:3px; min-width: 320px; \\\" class=3D\\\"share-header\\\">\\r\\n                                  SHARE &amp; FOLLOW US\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                            </tbody>\\r\\n                          </table>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                  <table bgcolor=3D\\\"#009f9c\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td>\\r\\n                          <table width=3D\\\"100%\\\" style=3D\\\"\\\" align=3D\\\"left\\\" b=\\r\\norder=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"devicewidthinner\\\">=\\r\\n\\r\\n                            <tbody>\\r\\n                              <tr>\\r\\n                                <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td>\\r\\n                              </tr>\\r\\n                              <tr>\\r\\n                                <td>\\r\\n                                  <table width=3D\\\"100%\\\" style=3D\\\"\\\" align=3D=\\r\\n\\\"center\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"devicewi=\\r\\ndthinner\\\">\\r\\n                                    <tbody>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:center;lin=\\r\\ne-height: 24px;min-width:245px;\\\" class=3D\\\"share-facebook-and-twitter\\\">\\r\\n                                          <a class=3D\\\"share share-facebook\\\"=\\r\\n target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D5=\\r\\n80bedd5217eda37226dc355defd764c4a5bacce1c02696337681417286260dfec3f16ecc900=\\r\\n18bc\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 1=\\r\\n5px; width:90px; white-space:nowrap; text-align: center; text-decoration: n=\\r\\none; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/ema=\\r\\nil/3/retina/footer_facebook=2Epng\\\" width=3D\\\"10\\\" height=3D\\\"23\\\" alt=3D\\\"Like U=\\r\\ns\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both; dis=\\r\\nplay: block; float: none; font-size: 28px; margin: 0 auto 10px; max-width: =\\r\\n100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=3D\\\"=\\r\\ncolor: #fff !important; display: block; font-size: 11px; font-weight: bold;=\\r\\n letter-spacing: 3px; margin: 15px auto\\\">Facebook</span></a>\\r\\n                                          <a class=3D\\\"share share-twitter\\\" =\\r\\ntarget=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D58=\\r\\n0bedd5217eda37b28f80f6926841e373f2063b283fd3960ec2f4f135612ad6b273d8e901e4e=\\r\\n7c5\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 15=\\r\\npx; width:90px; white-space:nowrap; text-align: center; text-decoration: no=\\r\\nne; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/emai=\\r\\nl/3/retina/footer_twitter=2Epng\\\" width=3D\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet wi=\\r\\nth Us\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both;=\\r\\n display: block; float: none; font-size: 28px; margin: 0 auto 10px; max-wid=\\r\\nth: 100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=\\r\\n=3D\\\"color: #fff !important; display: block; font-size: 11px; font-weight: b=\\r\\nold; letter-spacing: 3px; margin: 15px auto\\\">Twitter</span></a>\\r\\n                                          <a class=3D\\\"share share-tumblr\\\" t=\\r\\narget=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580=\\r\\nbedd5217eda3784ba9df91bdb28220027be1a35943845c0ff17cf92fc7b3b89cbe9ce6eee68=\\r\\n14\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 15p=\\r\\nx; width:90px; white-space:nowrap; text-align: center; text-decoration: non=\\r\\ne; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/email=\\r\\n/3/retina/footer_tumblr=2Epng\\\" width=3D\\\"13\\\" height=3D\\\"21\\\" alt=3D\\\"Tumble wit=\\r\\nh Us\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both; =\\r\\ndisplay: block; float: none; font-size: 28px; margin: 0 auto 8px; max-width=\\r\\n: 100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=\\r\\n=3D\\\"color: #fff !important; display: block; font-size: 11px; font-weight: b=\\r\\nold; letter-spacing: 3px; margin: 15px auto\\\">Tumblr</span></a>\\r\\n                                          <a class=3D\\\"share share-instagram=\\r\\n\\\" target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D=\\r\\n580bedd5217eda37254b18b2d0b03609f0ba749107959301d96068b56b4cb840c3dafa14865=\\r\\nc8eae\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 =\\r\\n15px; width:90px; white-space:nowrap; text-align: center; text-decoration: =\\r\\nnone; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/em=\\r\\nail/3/retina/footer_instagram=2Epng\\\" width=3D\\\"21\\\" height=3D\\\"22\\\" alt=3D\\\"Inst=\\r\\nagram Us\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: bo=\\r\\nth; display: block; float: none; font-size: 28px; margin: 0 auto 10px; max-=\\r\\nwidth: 100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span st=\\r\\nyle=3D\\\"color: #fff !important; display: block; font-size: 11px; font-weight=\\r\\n: bold; letter-spacing: 3px; margin: 15px auto\\\">Instagram</span></a>\\r\\n                                          <a class=3D\\\"share share-pinterest=\\r\\n\\\" target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D=\\r\\n580bedd5217eda3708ba55eaca8df450cdc2a1ff1d14b5c3865e1d4fe0745f6c13ceab0948d=\\r\\nf5210\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 =\\r\\n15px; width:90px; white-space:nowrap; text-align: center; text-decoration: =\\r\\nnone; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/em=\\r\\nail/3/retina/footer_pinterest=2Epng\\\" width=3D\\\"24\\\" height=3D\\\"24\\\" alt=3D\\\"Pin =\\r\\nUs\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both; di=\\r\\nsplay: block; float: none; font-size: 28px; margin: 0 auto 8px; max-width: =\\r\\n100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=3D\\\"=\\r\\ncolor: #fff !important; display: block; font-size: 11px; font-weight: bold;=\\r\\n letter-spacing: 3px; margin: 15px auto\\\">Pinterest</span></a>\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                    </tbody>\\r\\n                                  </table>\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                            </tbody>\\r\\n                          </table>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\">\\r\\n                  <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td>\\r\\n                          <table width=3D\\\"100%\\\" style=3D\\\"max-width:290px;\\\" =\\r\\nalign=3D\\\"left\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidth\\\">\\r\\n                            <tbody>\\r\\n                              <tr>\\r\\n                                <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td>\\r\\n                              </tr>\\r\\n                              <tr>\\r\\n                                <td>\\r\\n                                  <table width=3D\\\"100%\\\" style=3D\\\"max-width:=\\r\\n270px;\\\" align=3D\\\"center\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" c=\\r\\nlass=3D\\\"devicewidthinner\\\">\\r\\n                                    <tbody>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:left;line-=\\r\\nheight: 24px;\\\">\\r\\n                                          Copyright &#169; 2016 theSkimm, A=\\r\\nll rights reserved=2E\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:left;line-=\\r\\nheight: 24px;\\\">\\r\\n                                          <b>Our mailing address is: </b><b=\\r\\nr />\\r\\n                                          theSkimm Inc=2E<br />\\r\\n                                          49 W 23rd Street, 10th Floor<br /=\\r\\n>\\r\\n                                          New York, NY, 10010, United State=\\r\\ns\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:left;line-=\\r\\nheight: 24px;\\\">\\r\\n                                          <a href=3D\\\"http://click=2Emorning=\\r\\n7=2Etheskimm=2Ecom/profile_center=2Easpx?qs=3Deec37f32b3ca83aeebff6369cdb5b=\\r\\n630c8bd88a7221b6c466ba89d97456c40da8f9fd278c3d66e231b08d677113d878825b0f05c=\\r\\n429cfd61\\\" >Update Profile</a><br/>\\r\\n            <a href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580be=\\r\\ndd5217eda3795cd2024864be3ff004c57b4a334f836fc8b6174f03069dbbb0ac1024ad66989=\\r\\n\\\" >Unsubscribe</a>\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td width=3D\\\"100%\\\" height=3D\\\"60\\\"></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                    </tbody>\\r\\n                                  </table>\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                              <tr>\\r\\n                                <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td>\\r\\n                              </tr>\\r\\n                            </tbody>\\r\\n                          </table>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n<img src=3D\\\"https://pixel=2Emonitor1=2Ereturnpath=2Enet/pixel=2Egif?r=3D3e9=\\r\\nfa24c7442d95337a14dfcd7b45f193869d154\\\" width=3D\\\"1\\\" height=3D\\\"1\\\" />\\r\\n<IMG SRC=3D\\\"https://ad=2Edoubleclick=2Enet/ddm/trackimp/N5295=2E1915120THES=\\r\\nKIMM/B10126095=2E140587162;dc_trk_aid=3D312596749;dc_trk_cid=3D75394976;ord=\\r\\n=3D78a354ed-3d53-49f8-add5-cc82487a42f8;dc_lat=3D;dc_rdid=3D;tag_for_child_=\\r\\ndirected_treatment=3D?\\\" BORDER=3D\\\"0\\\" HEIGHT=3D\\\"1\\\" WIDTH=3D\\\"1\\\" ALT=3D\\\"Advert=\\r\\nisement\\\">\\r\\n<IMG SRC=3D\\\"https://ad=2Edoubleclick=2Enet/ddm/trackimp/N5295=2E1915120THES=\\r\\nKIMM/B10126095=2E140587164;dc_trk_aid=3D312596749;dc_trk_cid=3D75394976;ord=\\r\\n=3D10ffd6d4-6293-4123-9424-9723c421447e;dc_lat=3D;dc_rdid=3D;tag_for_child_=\\r\\ndirected_treatment=3D?\\\" BORDER=3D\\\"0\\\" HEIGHT=3D\\\"1\\\" WIDTH=3D\\\"1\\\" ALT=3D\\\"Advert=\\r\\nisement\\\">\\t\\t\\r\\n<IMG SRC=3D\\\"https://ad=2Edoubleclick=2Enet/ddm/trackimp/N5295=2E1915120THES=\\r\\nKIMM/B10126095=2E140587170;dc_trk_aid=3D312596749;dc_trk_cid=3D75394976;ord=\\r\\n=3D5717585d-3efb-4821-80f4-232ee95b57c4;dc_lat=3D;dc_rdid=3D;tag_for_child_=\\r\\ndirected_treatment=3D?\\\" BORDER=3D\\\"0\\\" HEIGHT=3D\\\"1\\\" WIDTH=3D\\\"1\\\" ALT=3D\\\"Advert=\\r\\nisement\\\">\\r\\n\\r\\n</body>\\r\\n</html>\\r\\n\"}},\"desiredParts\":[{\"id\":\"1\",\"encoding\":\"QUOTED-PRINTABLE\",\"mimetype\":\"text/plain\"},{\"id\":\"2\",\"encoding\":\"QUOTED-PRINTABLE\",\"mimetype\":\"text/html\"}],\"result\":{\"to\":[{\"name\":\"\",\"email\":\"christine@spang.cc\"}],\"cc\":[],\"bcc\":[],\"from\":[{\"name\":\"theSkimm\",\"email\":\"dailyskimm@morning7.theskimm.com\"}],\"replyTo\":[{\"name\":\"theSkimm\",\"email\":\"reply-ff2511747d6d-20_HTML-215009-7208679-430@morning7.theskimm.com\"}],\"accountId\":\"test-account-id\",\"body\":\"\\r\\n\\r\\n<!DOCTYPE html PUBLIC \\\"-//W3C//DTD XHTML 1=2E0 Transitional//EN\\\" \\\"http://ww=\\r\\nw=2Ew3=2Eorg/TR/xhtml1/DTD/xhtml1-transitional=2Edtd\\\">\\r\\n<html>\\r\\n  <head>\\r\\n    <meta http-equiv=3D\\\"Content-Type\\\" content=3D\\\"text/html; charset=3DUTF-8=\\r\\n\\\">\\r\\n    <!-- Responsive Design -->\\r\\n    <!-- Facebook sharing information tags -->\\r\\n    <img src=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/open=2Easpx?ffcb10=\\r\\n-ff2511747d6d-fecc16717d64017e-fe881372756c027a7c-ff9d1670-fece167170640474=\\r\\n-ff68107375\\\" width=3D\\\"1\\\" height=3D\\\"1\\\">\\r\\n    <meta property=3D\\\"og:image\\\" content=3D\\\"http://cdn=2Etheskimm=2Ecom/asse=\\r\\nts/skimm-fb-logo=2Epng\\\">\\r\\n    <meta name=3D\\\"viewport\\\" content=3D\\\"width=3Ddevice-width\\\">\\r\\n\\r\\n      <style type=3D\\\"text/css\\\">\\r\\n    body{\\r\\n      color:#000 !important;\\r\\n    }\\r\\n    p=2Eskimm-p a,a,a:link,a:hover,a:visited{\\r\\n      color:#009f9c!important;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n    a:hover{\\r\\n      text-decoration:underline;\\r\\n    }\\r\\n    p{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:15px;\\r\\n      line-height:20px;\\r\\n      letter-spacing:0em;\\r\\n      color:#000;\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n    }\\r\\n    #logo{\\r\\n      text-decoration:none;\\r\\n      display:block;\\r\\n      padding-top:18px;\\r\\n      margin:0 auto;\\r\\n    }\\r\\n    #missed{\\r\\n      padding:34px 0;\\r\\n    }\\r\\n    #missed p{\\r\\n      text-align:center;\\r\\n      padding:0 0 3px;\\r\\n      font-size:14px;\\r\\n    }\\r\\n    #sharing{\\r\\n      padding:24px 0 32px;\\r\\n      margin:0 auto 35px;\\r\\n      background:#009f9b;\\r\\n      width:100%;\\r\\n    }\\r\\n    #sharing h2{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-weight:bold;\\r\\n      text-transform:uppercase;\\r\\n      text-align:center;\\r\\n      letter-spacing:0=2E28em;\\r\\n      padding:0 0 24px;\\r\\n      margin:0;\\r\\n      font-style:normal;\\r\\n      font-size:13px;\\r\\n      color:#fff;\\r\\n    }\\r\\n    #sharing =2Eshare_icons{\\r\\n      margin:0 auto;\\r\\n      text-align:center;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare{\\r\\n      display:inline-block;\\r\\n      text-transform:uppercase;\\r\\n      color:#fff;\\r\\n      text-decoration:none;\\r\\n      text-align:center;\\r\\n      margin-right:15px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare img{\\r\\n      display:block;\\r\\n      font-size:28px;\\r\\n      margin:0 auto 10px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons span:last-child =2Eshare:last-child{\\r\\n      margin-left:7px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare:last-child img{\\r\\n      margin:0 auto 8px;\\r\\n    }\\r\\n    #sharing =2Eshare_icons =2Eshare span{\\r\\n      display:block;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:11px;\\r\\n      font-weight:bold;\\r\\n      letter-spacing:3px;\\r\\n    }\\r\\n    =2Eimg_el{\\r\\n      margin:0 auto 22px;\\r\\n      display:block;\\r\\n    }\\r\\n    =2Eretinaonlyicon{\\r\\n      width:46px;\\r\\n      height:49px;\\r\\n    }\\r\\n    =2Etheskimm{\\r\\n      text-transform:none !important;\\r\\n    }\\r\\n    #rss-content p,#rss-content h1,#rss-content h2,#rss-content h3,#rss-con=\\r\\ntent img,#rss-content hr{\\r\\n      margin-left:auto;\\r\\n      margin-right:auto;\\r\\n    }\\r\\n    =2Eskimm-birthdays,=2Eskimm-shareus,=2Eskimm-gift,=2Eskimm-life{\\r\\n      padding-bottom:15px !important;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-shareus{\\r\\n      background:url(http://cdn=2Etheskimm=2Ecom/email/3/normal/skimmsend_i=\\r\\ncon=2Epng) no-repeat 50% 0;\\r\\n      text-align:center;\\r\\n      padding-top:50px;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-p{\\r\\n      line-height:23px;\\r\\n      color:#000;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:16px;\\r\\n      padding-bottom:15px !important;\\r\\n    }\\r\\n    =2Eshare-jumpto-links{\\r\\n      margin-top:0px !important;\\r\\n      margin-bottom:12px !important;\\r\\n      margin-left:auto;\\r\\n      margin-right:auto;\\r\\n    }\\r\\n    =2Eskimm-h1{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:22px !important;\\r\\n      font-weight:bold;\\r\\n      color:#000;\\r\\n      border-bottom:3px solid #00A49F;\\r\\n      padding:12px 0 !important;\\r\\n      margin-top:8px;\\r\\n      margin-bottom:24px;\\r\\n      text-rendering:geometricPrecision;\\r\\n      text-align:left;\\r\\n      letter-spacing:0=2E08em;\\r\\n    }\\r\\n    =2Eskimm-h2{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-weight:bold;\\r\\n      font-size:20px;\\r\\n      letter-spacing:0=2E01em;\\r\\n      color:#000;\\r\\n      padding:0 0 0 0;\\r\\n      margin-top:22px;\\r\\n      margin-bottom:12px;\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2Eskimm-h3{\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-weight:bold !important;\\r\\n      font-size:15px;\\r\\n      letter-spacing:0=2E15em;\\r\\n      line-height:20px;\\r\\n      color:#000;\\r\\n      padding:0;\\r\\n      margin-bottom:4px;\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-h3-quote{\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-birthdays{\\r\\n      text-align:center;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-life{\\r\\n      text-align:center;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-h3=2Eskimm-gift{\\r\\n      text-align:center;\\r\\n      margin-top:20px;\\r\\n    }\\r\\n    =2Eskimm-hr{\\r\\n      border:0;\\r\\n      margin-bottom:0 !important;\\r\\n    }\\r\\n    =2Eskimm-hr=2Eskimm-hr-thick{\\r\\n      border-top:3px solid #009f9c !important;\\r\\n      background:none !important;\\r\\n      margin-bottom:25px !important;\\r\\n    }\\r\\n    =2Eskimm-hr=2Eskimm-hr-thin{\\r\\n      border-top:1px solid #bfbfbf !important;\\r\\n      height:0 !important;\\r\\n      background:#fff !important;\\r\\n      margin-bottom:20px !important;\\r\\n    }\\r\\n    =2Eskimm-hr=2Eskimm-hr-thin=2Eskimm-hr-teal{\\r\\n      border-top:1px solid #009f9c !important;\\r\\n    }\\r\\n    #outlook a{\\r\\n      padding:0;\\r\\n    }\\r\\n    body{\\r\\n      width:100% !important;\\r\\n    }\\r\\n    =2EReadMsgBody{\\r\\n      width:100%;\\r\\n    }\\r\\n    =2EExternalClass{\\r\\n      width:100%;\\r\\n    }\\r\\n    body{\\r\\n      -webkit-text-size-adjust:none;\\r\\n    }\\r\\n    body{\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n    }\\r\\n    img{\\r\\n      border:none;\\r\\n      height:auto;\\r\\n      line-height:100%;\\r\\n      margin:0;\\r\\n      outline:none;\\r\\n      padding:0;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n    #backgroundTable{\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n      width:100% !important;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section background color\\r\\n  @tip Set the background color for your email=2E You may want to choose on=\\r\\ne that matches your company's branding=2E\\r\\n  @theme page\\r\\n  */\\r\\n    body,#backgroundTable{\\r\\n      /*@editable*/background-color:#ffffff;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 1\\r\\n  @tip Set the styling for all first-level headings in your emails=2E These=\\r\\n should be the largest of your headings=2E\\r\\n  @style heading 1\\r\\n  */\\r\\n    h1,=2Eh1{\\r\\n      /*@editable*/color:#202020;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:32px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 2\\r\\n  @tip Set the styling for all second-level headings in your emails=2E\\r\\n  @style heading 2\\r\\n  */\\r\\n    h2,=2Eh2{\\r\\n      /*@editable*/color:#303030;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:26px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      padding-bottom:6px;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 3\\r\\n  @tip Set the styling for all third-level headings in your emails=2E\\r\\n  @style heading 3\\r\\n  */\\r\\n    h3,=2Eh3{\\r\\n      /*@editable*/color:#404040;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:22px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Page\\r\\n  @section heading 4\\r\\n  @tip Set the styling for all fourth-level headings in your emails=2E Thes=\\r\\ne should be the smallest of your headings=2E\\r\\n  @style heading 4\\r\\n  */\\r\\n    h4,=2Eh4{\\r\\n      /*@editable*/color:#505050;\\r\\n      display:block;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:18px;\\r\\n      /*@editable*/font-weight:bold;\\r\\n      /*@editable*/line-height:100%;\\r\\n      margin-top:0;\\r\\n      margin-right:0;\\r\\n      margin-bottom:10px;\\r\\n      margin-left:0;\\r\\n      /*@editable*/text-align:left;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section preheader style\\r\\n  @tip Set the background color for your email's preheader area=2E\\r\\n  @theme page\\r\\n  */\\r\\n    #templatePreheader{\\r\\n      /*@editable*/background-color:#ffffff;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section preheader text\\r\\n  @tip Set the styling for your email's preheader text=2E Choose a size and=\\r\\n color that is easy to read=2E\\r\\n  */\\r\\n    =2EpreheaderContent{\\r\\n      /*@tab Header\\r\\n@section preheader text\\r\\n@tip Set the styling for your email's preheader text=2E Choose a size and c=\\r\\nolor that is easy to read=2E*/border-bottom:3px solid #000;\\r\\n    }\\r\\n    =2EpreheaderContent div{\\r\\n      /*@editable*/color:#505050;\\r\\n      /*@editable*/font-family:Helvetica,Arial,sans-serif;\\r\\n      /*@editable*/font-size:10px;\\r\\n      /*@editable*/line-height:100%;\\r\\n      /*@editable*/text-align:center;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section preheader link\\r\\n  @tip Set the styling for your email's preheader links=2E Choose a color t=\\r\\nhat helps them stand out from your text=2E\\r\\n  */\\r\\n    =2EpreheaderContent div a:link,=2EpreheaderContent div a:visited,=2Epre=\\r\\nheaderContent div a =2Eyshortcuts{\\r\\n      /*@editable*/color:#009f9c !important;\\r\\n      /*@editable*/font-weight:normal;\\r\\n      /*@editable*/text-decoration:underline;\\r\\n    }\\r\\n  /*\\r\\n  @tab Header\\r\\n  @section header style\\r\\n  @tip Set the background color and border for your email's header area=2E\\r\\n=\\r\\n\\r\\n  @theme header\\r\\n  */\\r\\n    #templateHeader{\\r\\n      /*@editable*/background-color:#ffffff;\\r\\n      /*@editable*/border-bottom:0;\\r\\n      padding:0px;\\r\\n    }\\r\\n  /*\\r\\n  @tab Body\\r\\n  @section body style\\r\\n  @tip Set the background color for your email's body area=2E\\r\\n  */\\r\\n    #templateContainer,=2EbodyContent{\\r\\n      /*@editable*/background-color:#FFFFFF;\\r\\n    }\\r\\n    #sharing-cells{\\r\\n      margin-top:15px;\\r\\n    }\\r\\n    #sharing-cells td{\\r\\n      width:50%;\\r\\n      padding:0 8px;\\r\\n    }\\r\\n    #sharing-cells td:first-child{\\r\\n      padding-left:0;\\r\\n    }\\r\\n    #sharing-cells td:last-child{\\r\\n      padding-right:0;\\r\\n    }\\r\\n    #sharing-cells a:link,#sharing-cells a:visited,#sharing-cells a{\\r\\n      display:block;\\r\\n      height:22px;\\r\\n      line-height:22px;\\r\\n      background:#009f9c;\\r\\n      position:relative;\\r\\n      color:#ffffff;\\r\\n      text-decoration:none;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n      font-size:13px;\\r\\n      font-weight:bold;\\r\\n    }\\r\\n    #sharing-cells img{\\r\\n      position:relative;\\r\\n      margin-left:10px;\\r\\n    }\\r\\n    #sharing-cells =2Eheader-email-img{\\r\\n      top:1px;\\r\\n    }\\r\\n    #sharing-cells =2Eheader-twitter-img{\\r\\n      top:1px;\\r\\n    }\\r\\n    #sharing-cells =2Eheader-facebook-img{\\r\\n      top:2px;\\r\\n    }\\r\\n  /*\\r\\n  @tab Body\\r\\n  @section body text\\r\\n  @tip Set the styling for your email's main content text=2E Choose a size =\\r\\nand color that is easy to read=2E\\r\\n  @theme main\\r\\n  */\\r\\n    =2EbodyContent{\\r\\n      /*@tab Body\\r\\n@section body text\\r\\n@tip Set the styling for your email's main content text=2E Choose a size an=\\r\\nd color that is easy to read=2E\\r\\n@theme main*/text-align:left;\\r\\n    }\\r\\n    =2EbodyContent p{\\r\\n      margin:0 auto;\\r\\n      padding-bottom:26px;\\r\\n      font-size:14px;\\r\\n      text-align:left;\\r\\n    }\\r\\n    =2EbodyContent p strong{\\r\\n      font-weight:bold;\\r\\n    }\\r\\n    =2EbodyContent hr{\\r\\n      height:3px;\\r\\n      background:#000;\\r\\n      border-top:0;\\r\\n      border-bottom:0;\\r\\n      border-left:0;\\r\\n      border-right:0;\\r\\n      width:145px;\\r\\n      margin-bottom:25px;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section footer style\\r\\n  @tip Set the background color and top border for your email's footer area=\\r\\n=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    #templateFooter{\\r\\n      /*@editable*/background-color:#FFFFFF;\\r\\n      /*@editable*/border-top:0;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section footer text\\r\\n  @tip Set the styling for your email's footer text=2E Choose a size and co=\\r\\nlor that is easy to read=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    =2EfooterContent div{\\r\\n      /*@tab Footer\\r\\n@section footer text\\r\\n@tip Set the styling for your email's footer text=2E Choose a size and colo=\\r\\nr that is easy to read=2E\\r\\n@theme footer*/font-size:12px;\\r\\n      font-family:Helvetica,Arial,sans-serif;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section footer link\\r\\n  @tip Set the styling for your email's footer links=2E Choose a color that=\\r\\n helps them stand out from your text=2E\\r\\n  */\\r\\n    =2EfooterContent div p,=2EfooterContent div a:link,=2EfooterContent div=\\r\\n a:visited,=2EfooterContent div a =2Eyshortcuts{\\r\\n      /*@tab Footer\\r\\n@section footer link\\r\\n@tip Set the styling for your email's footer links=2E Choose a color that h=\\r\\nelps them stand out from your text=2E*/text-align:left;\\r\\n      font-size:12px;\\r\\n    }\\r\\n    =2EfooterContent img{\\r\\n      display:inline;\\r\\n    }\\r\\n    #footerContentLeft a,#monkeyRewards a{\\r\\n      color:#009f9c;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section social bar style\\r\\n  @tip Set the background color and border for your email's footer social b=\\r\\nar=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    #social{\\r\\n      /*@editable*/background-color:#FAFAFA;\\r\\n      /*@editable*/border:0;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section social bar style\\r\\n  @tip Set the background color and border for your email's footer social b=\\r\\nar=2E\\r\\n  */\\r\\n    #social div{\\r\\n      /*@editable*/text-align:center;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section utility bar style\\r\\n  @tip Set the background color and border for your email's footer utility =\\r\\nbar=2E\\r\\n  @theme footer\\r\\n  */\\r\\n    #utility{\\r\\n      /*@editable*/background-color:#FFFFFF;\\r\\n      /*@editable*/border:0;\\r\\n    }\\r\\n  /*\\r\\n  @tab Footer\\r\\n  @section utility bar style\\r\\n  @tip Set the background color and border for your email's footer utility =\\r\\nbar=2E\\r\\n  */\\r\\n    #utility div{\\r\\n      /*@editable*/text-align:center;\\r\\n    }\\r\\n    #monkeyRewards img{\\r\\n      max-width:190px;\\r\\n    }\\r\\n    =2EheaderContent a{\\r\\n      color:#000000;\\r\\n      text-decoration:none;\\r\\n    }\\r\\n    =2EheaderContent p{\\r\\n      font-weight:bold;\\r\\n    }\\r\\n  @media only screen and (max-width: 675px),\\r\\n    (-webkit-min-device-pixel-ratio: 1=2E5),\\r\\n  (min-resolution: 144dpi) and (device-width: 1080px) and (orientation: por=\\r\\ntrait),\\r\\n  (-webkit-min-device-pixel-ratio: 3=2E0){\\r\\n    body,table,td,p,a,li,blockquote{\\r\\n      -webkit-text-size-adjust:none !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    table[id=3DtemplatePreheader],table[id=3DtemplateContainer],table[id=3D=\\r\\ntemplateHeader],table[id=3DtemplateBody],table[id=3DtemplateFooter],table[i=\\r\\nd=3DinnerTemplateContainer],=2Eskimm-shareus-bg{\\r\\n      width:100% !important;\\r\\n      margin:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    table[id=3DtemplatePreheader] td,table[id=3DtemplateContainer] td,table=\\r\\n[id=3DtemplateHeader] td,table[id=3DtemplateBody] td,table[id=3DtemplateFoo=\\r\\nter] td,table[id=3DinnerTemplateContainer] td,=2Eskimm-shareus-bg{\\r\\n      padding-left:0 !important;\\r\\n      padding-right:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells{\\r\\n      width:100% !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells a{\\r\\n      height:35px !important;\\r\\n      line-height:35px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells a span{\\r\\n      display:none;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells td{\\r\\n      width:50% !important;\\r\\n      padding:0 2px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells td:first-child{\\r\\n      padding-left:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing-cells td:last-child{\\r\\n      padding-right:0 !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    =2EpreheaderContent div{\\r\\n      font-size:14px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    =2EpreheaderContent a{\\r\\n      display:block;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #rss-content p,#rss-content h1,#rss-content h2,#rss-content h3,#rss-con=\\r\\ntent hr,=2Eskimm-shareus-bg,=2Eshare-jumpto-links{\\r\\n      margin-left:5px !important;\\r\\n      margin-right:5px !important;\\r\\n      width:auto !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #missed p span{\\r\\n      display:block;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing h2{\\r\\n      font-size:13px !important;\\r\\n      padding-bottom:15px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing =2Eshare_icons =2Eshare{\\r\\n      margin-right:3px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #sharing =2Eshare_icons span{\\r\\n      display:block;\\r\\n      margin-bottom:15px;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #footerContentLeft,#monkeyRewards{\\r\\n      width:100% !important;\\r\\n      display:block !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    #footerContentLeft{\\r\\n      margin-bottom:15px;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 675px){\\r\\n    =2Eskimm-shareus-bg{\\r\\n      padding-left:5px !important;\\r\\n      padding-right:5px !important;\\r\\n    }\\r\\n\\r\\n}   #outlook a{\\r\\n      padding:0;\\r\\n    }\\r\\n    body{\\r\\n      width:100% !important;\\r\\n      -webkit-text-size-adjust:100%;\\r\\n      -ms-text-size-adjust:100%;\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n    }\\r\\n    =2EExternalClass{\\r\\n      width:100%;\\r\\n    }\\r\\n    =2EExternalClass,=2EExternalClass p,=2EExternalClass span,=2EExternalCl=\\r\\nass font,=2EExternalClass td,=2EExternalClass div{\\r\\n      line-height:100%;\\r\\n    }\\r\\n    =2Eecxfacebook span,=2Eecxtwitter span{\\r\\n      padding-right:10px!important;\\r\\n    }\\r\\n    a=2Eecxshare span{\\r\\n      padding:10px 0!important;\\r\\n    }\\r\\n    a=2Eecxshare img{\\r\\n      display:inline-block!important;\\r\\n    }\\r\\n    #backgroundTable{\\r\\n      margin:0;\\r\\n      padding:0;\\r\\n      width:100% !important;\\r\\n      line-height:100% !important;\\r\\n    }\\r\\n    img{\\r\\n      outline:none;\\r\\n      text-decoration:none;\\r\\n      border:none;\\r\\n      -ms-interpolation-mode:bicubic;\\r\\n    }\\r\\n    a img{\\r\\n      border:none;\\r\\n    }\\r\\n    =2Eimage_fix{\\r\\n      display:block;\\r\\n    }\\r\\n    p{\\r\\n      margin:0px 0px !important;\\r\\n    }\\r\\n    table td{\\r\\n      border-collapse:collapse;\\r\\n    }\\r\\n    table{\\r\\n      border-collapse:collapse;\\r\\n      mso-table-lspace:0pt;\\r\\n      mso-table-rspace:0pt;\\r\\n    }\\r\\n    a{\\r\\n      color:#009f9c !important;\\r\\n      text-decoration:none!important;\\r\\n    }\\r\\n    table[class=3Dfull]{\\r\\n      width:100%;\\r\\n      clear:both;\\r\\n    }\\r\\n  @media only screen and (max-width: 640px){\\r\\n    a[href^=3Dtel],a[href^=3Dsms]{\\r\\n      text-decoration:none;\\r\\n      color:#009f9c;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    =2Emobile_link a[href^=3Dtel],=2Emobile_link a[href^=3Dsms]{\\r\\n      text-decoration:default;\\r\\n      color:#009f9c !important;\\r\\n      pointer-events:auto;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Ddevicewidth]{\\r\\n      width:440px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    td[class=3Ddevicewidth]{\\r\\n      width:440px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Ddevicewidth]{\\r\\n      width:440px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Dbanner]{\\r\\n      width:440px!important;\\r\\n      height:147px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Ddevicewidthinner]{\\r\\n      width:420px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Ddevicewidthinner]{\\r\\n      width:420px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Dicontext]{\\r\\n      width:345px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Dcolimg2]{\\r\\n      width:420px!important;\\r\\n      height:243px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    table[class=3Demhide]{\\r\\n      display:none!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 640px){\\r\\n    img[class=3Dlogo]{\\r\\n      width:425px!important;\\r\\n      height:198px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    a[href^=3Dtel],a[href^=3Dsms]{\\r\\n      text-decoration:none;\\r\\n      color:#009f9c;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    =2Emobile_link a[href^=3Dtel],=2Emobile_link a[href^=3Dsms]{\\r\\n      text-decoration:default;\\r\\n      color:#009f9c !important;\\r\\n      pointer-events:auto;\\r\\n      cursor:default;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Ddevicewidth]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    td[class=3Ddevicewidth]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n      font-size:14px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    p[class=3Dskimm-p]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n      font-size:16px!important;\\r\\n      line-height:22px!important;\\r\\n      padding-bottom:20px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    img[class=3Dbanner]{\\r\\n      width:300px!important;\\r\\n      height:93px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Ddevicewidthinner]{\\r\\n      width:300px!important;\\r\\n      min-width:300px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Dicontext]{\\r\\n      width:186px!important;\\r\\n      text-align:left!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    img[class=3Dcolimg2]{\\r\\n      width:260px!important;\\r\\n      height:150px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[class=3Demhide]{\\r\\n      display:none!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    img[class=3Dlogo]{\\r\\n      width:300px!important;\\r\\n      height:140px!important;\\r\\n      margin:0 auto;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Emobile-block{\\r\\n      display:block !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-column-top{\\r\\n      padding:12px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Ebullet{\\r\\n      display:block !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eemail-title{\\r\\n      text-align:center !important;\\r\\n      min-width:300px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-header{\\r\\n      min-width:300px!important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-facebook-and-twitter{\\r\\n      text-align:center !important;\\r\\n      min-width:320px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    body[yahoofix] =2Eshare-tumblr-instagram-pinterest{\\r\\n      text-align:center !important;\\r\\n      min-width:320px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[id=3DcanspamBar] td{\\r\\n      font-size:14px !important;\\r\\n    }\\r\\n\\r\\n} @media only screen and (max-width: 480px){\\r\\n    table[id=3DcanspamBar] td a{\\r\\n      display:block !important;\\r\\n      margin-top:10px !important;\\r\\n    }\\r\\n\\r\\n}</style></head>\\r\\n<custom type=3D\\\"content\\\" name=3D\\\"feed\\\">\\r\\n<body yahoofix=3D\\\"\\\">\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"2\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" align=3D\\\"center\\\" valign=3D\\\"middle\\\" style=\\r\\n=3D\\\"font-family: Helvetica, arial, sans-serif; font-size: 10px;color:#22222=\\r\\n2;padding:10px;\\\">\\r\\n    <img src=3D\\\"https://knifeopen=2Ecom/open?key=3DtbXNJbs7ktiOLQaM&e=3DY2h=\\r\\nyaXN0aW5lQHNwYW5nLmNj\\\" border=3D\\\"0\\\" width=3D\\\"1\\\" height=3D\\\"1\\\" style=3D\\\"heigh=\\r\\nt:1px !important; width:1px !important; border: 0 !important; margin: 0 !im=\\r\\nportant; padding: 0 !important; overflow:hidden !important\\\">\\r\\n    Is this email not displaying correctly?\\r\\n                  <a href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=\\r\\n=3D580bedd5217eda378a2f7dccb8e26b5b302c761b48c753ae37dd56d06342bd7761205726=\\r\\ne794f19c\\\" target=3D\\\"_blank\\\" style=3D\\\"color: #009f9c !important; text-decora=\\r\\ntion: none\\\" class=3D\\\"mobile-block\\\">View it in your browser=2E</a>\\r\\n                </td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"2\\\"></td>\\r\\n              </tr>\\r\\n              <tr bgcolor=3D\\\"#202020\\\">\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"3\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr valign=3D\\\"middle\\\">\\r\\n                <td width=3D\\\"49%\\\" style=3D\\\"max-width:305px;font-family: 'He=\\r\\nlvetica', 'Arial', sans-serif; font-size: 14px; font-weight: normal; hyphen=\\r\\ns: auto; padding:14px;\\\" bgcolor=3D\\\"#009f9c\\\" valign=3D\\\"middle\\\" class=3D\\\"shar=\\r\\ne-column-top\\\">\\r\\n                  <a class=3D\\\"twitter\\\" href=3D\\\"http://click=2Emorning7=2Eth=\\r\\neskimm=2Ecom/?qs=3D580bedd5217eda37522cd8eba442ef808cdfbce15cef6d2289b67738=\\r\\n60cff7d43a14a4cd720436de\\\" style=3D\\\"color: #fff !important; display: block; =\\r\\nfont-size: 14px; font-weight: bold; text-align: center; text-decoration: no=\\r\\nne\\\"><span style=3D\\\"\\\">SHARE THIS</span><img class=3D\\\"header-twitter-img\\\" src=\\r\\n=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/header_twitter=2Epng\\\" width=\\r\\n=3D\\\"17\\\" height=3D\\\"12\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: non=\\r\\ne; clear: both; display: inline-block; float: none; margin-left: 10px; max-=\\r\\nwidth: 100%; outline: none; position: relative; text-decoration: none;left:=\\r\\n5px;\\\" align=3D\\\"none\\\"></a>\\r\\n                </td>\\r\\n                <td width=3D\\\"2%\\\" style=3D\\\"max-width:10px;\\\"></td>\\r\\n                <td width=3D\\\"49%\\\" style=3D\\\"max-width:305px;font-family: 'He=\\r\\nlvetica', 'Arial', sans-serif; font-size: 14px; font-weight: normal; hyphen=\\r\\ns: auto; padding:14px;\\\" bgcolor=3D\\\"#009f9c\\\" valign=3D\\\"middle\\\" class=3D\\\"shar=\\r\\ne-column-top\\\">\\r\\n                  <a class=3D\\\"facebook\\\" href=3D\\\"http://click=2Emorning7=2Et=\\r\\nheskimm=2Ecom/?qs=3D580bedd5217eda37c0f6dcfc7fe9573401cc2ca5dd8a2ec58771970=\\r\\n36407a2321901ac67b909273e\\\" style=3D\\\"color: #fff !important; display: block;=\\r\\n font-size: 14px; font-weight: bold; text-align: center; text-decoration: n=\\r\\none\\\"><span style=3D\\\"\\\">SHARE THIS</span><img class=3D\\\"header-facebook-img\\\" s=\\r\\nrc=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/header_facebook=2Epng\\\" wid=\\r\\nth=3D\\\"5\\\" height=3D\\\"12\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: no=\\r\\nne; clear: both; display: inline-block; float: none; margin-left: 10px; max=\\r\\n-width: 100%; outline: none; position: relative; text-decoration: none; lef=\\r\\nt:5px;\\\" align=3D\\\"none\\\"></a>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"21\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" align=3D\\\"center\\\">\\r\\n                  <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" align=3D\\\"center\\\" cellspacing=3D\\\"0\\\" cellpadding=3D\\\"0\\\" border=3D\\\"0=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td align=3D\\\"center\\\" class=3D\\\"email-title-logo\\\">\\r\\n                          <a target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorni=\\r\\nng7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda370f716da4e7a0d2fc45cc1fb72979495e=\\r\\n447d7a98e5f9d8e8385598d3a36bf093\\\"><img width=3D\\\"500\\\" border=3D\\\"0\\\" height=3D=\\r\\n\\\"233\\\" alt=3D\\\"\\\" style=3D\\\"display:block; border:none; outline:none; text-deco=\\r\\nration:none;\\\" src=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/preflight/l=\\r\\nogo_google_skimm_20161102=2Epng\\\" class=3D\\\"logo\\\"></a>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td align=3D\\\"center\\\" style=3D\\\"color: #222222;font-f=\\r\\namily: 'Helvetica', 'Arial', sans-serif; font-size: 18px;font-weight: bold;=\\r\\nmin-width: 320px; width: 100%\\\" class=3D\\\"email-title\\\">\\r\\n                          Skimm for November 2nd\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td align=3D\\\"center\\\">\\r\\n                          <a target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorni=\\r\\nng7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda3737d8d9ff2a656be9041e65e9ad55a4c1=\\r\\nc75a705f32854b662fd04c5b65251cda\\\"><img height=3D\\\"40\\\" width=3D\\\"205\\\" border=\\r\\n=3D\\\"0\\\" alt=3D\\\"\\\" style=3D\\\"display:block; border:none; outline:none;text-deco=\\r\\nration:none;\\\" src=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/inviteCTA=\\r\\n=2Epng\\\"></a>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D=\\r\\n\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"30\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                            Skimm&#8217;d w=\\r\\nhile getting up on what&#8217;s at stake in 2016=2E <a href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e4ff0b477556ee2011e623a9=\\r\\n141618b501e48d7ada8a3ed516d0b3c0f3f9fa1b\\\" target=3D\\\"_blank\\\">Ready to vote?<=\\r\\n/a>&#160;\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D=\\r\\n\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"20\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                            <td style=3D\\\"font-family: Helvetica, arial, san=\\r\\ns-serif; font-size: 15px; font-weight:bold; color: #000000; text-align:left=\\r\\n;line-height: 20px; letter-spacing:0=2E15em;\\\" class=3D\\\"title\\\">\\r\\n                              QUOTE OF THE DAY\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                              <p class=3D\\\"skimm-p\\\">&#8220;Moisture harveste=\\r\\nd from the clouds&#8221; - <a style=3D\\\"color:#009f9c!important;text-decorat=\\r\\nion:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e195839=\\r\\n9f015187600c0dbcd6d17ec7331f3cfc3b5f32608d5bedc4d80db9f776591ce730db6c1\\\" ta=\\r\\nrget=3D\\\"_blank\\\">A description</a> of how Sky PA, a new Scottish beer, is ma=\\r\\nde=2E Cloudy with a chance of hipster=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\"><div styl=\\r\\ne=3D\\\"box-sizing:border-box;position:relative;height:45px;\\\">\\r\\n                                <div style=3D\\\"padding:0;overflow:hidden;col=\\r\\nor:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: 12px=\\r\\n;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;text-=\\r\\nalign:left;\\\">\\r\\n                                  <span style=3D\\\"display:inline-block;margi=\\r\\nn-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;vertical-=\\r\\nalign:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://click=2E=\\r\\nmorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015180b3ee957efc743770fa87d771b9=\\r\\ne197126982da785ce1918153b77f693c3ce8b\\\"><img src=3D\\\"http://cdn=2Etheskimm=2E=\\r\\ncom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;vertica=\\r\\nl-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" heig=\\r\\nht=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vertical=\\r\\n-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518f2d49f9d3484562b6c859a38=\\r\\na9b756ddc039572f4aa0f9c5037e50c2d0bae33c\\\"><img src=3D\\\"http://cdn=2Etheskimm=\\r\\n=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;vert=\\r\\nical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D\\\"2=\\r\\n5\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                          <a style=3D\\\"width: 20px; height: 25px; vertical-a=\\r\\nlign: bottom; margin: 5px 5px 0; display: inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e7517a64792145e64a9ed2=\\r\\n022fa40ac9e1b913bf4cbf414945e77b598a60d31d\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/icon_share_instagram=2Epng\\\" style=3D\\\"display: block=\\r\\n; vertical-align: middle; padding-top: 0px; margin: 0 auto !important;\\\" wid=\\r\\nth=3D\\\"20\\\" height=3D\\\"20\\\" alt=3D\\\"Insta This\\\" /></a>\\r\\n                                            </div>\\r\\n                                </div></div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n   <tbody>\\r\\n      <tr>\\r\\n         <td>\\r\\n            <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"=\\r\\n0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n               <tbody>\\r\\n                  <tr>\\r\\n                     <td width=3D\\\"100%\\\">\\r\\n                        <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"=\\r\\nmax-width:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"=\\r\\ncenter\\\" class=3D\\\"devicewidth\\\">\\r\\n                           <tbody>\\r\\n                              <tr>\\r\\n                                 <td>\\r\\n                                    <table width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:600px;\\\" align=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\"=\\r\\n class=3D\\\"devicewidthinner\\\">\\r\\n                                       <tbody>\\r\\n                                          <tr>\\r\\n                                             <td style=3D\\\"font-family: Helv=\\r\\netica, arial, sans-serif; font-size: 24px; font-weight:bold; color: #000000=\\r\\n; text-align:left;line-height: 24px; letter-spacing:2px;\\\">\\r\\n\\r\\n    <h1 class=3D'skimm-h1' id=3D'top-story' style=3D'font-size:24px!importa=\\r\\nnt;font-weight:bold;color:#000;border-bottom:3px solid #00A49F;padding:12px=\\r\\n0!important;margin-top:8px;margin-bottom:24px;text-rendering:geometricPreci=\\r\\nsion;text-align:left;letter-spacing:0=2E08em;'>GAS PAINS</h1>              =\\r\\n                               </td>\\r\\n                                          </tr>\\r\\n                                       </tbody>\\r\\n                                    </table>\\r\\n                                 </td>\\r\\n                              </tr>\\r\\n                           </tbody>\\r\\n                        </table>\\r\\n                     </td>\\r\\n                  </tr>\\r\\n               </tbody>\\r\\n            </table>\\r\\n         </td>\\r\\n      </tr>\\r\\n   </tbody>\\r\\n</table>\\r\\n      <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspaci=\\r\\nng=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-weight:bold;font-size:20px;letter-spacing:0=2E01em;color:#000;p=\\r\\nadding:0000;padding-bottom:12px;text-align:left;\\\">\\r\\n                                                              </td>\\r\\n                            </tr>\\r\\n                            <!--skimmPH:[top-story]-->                     =\\r\\n       <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">THE STORY</h3>\\r\\n<p class=3D\\\"skimm-p\\\">Earlier this week, a major oil pipeline that runs thro=\\r\\nugh Alabama <a style=3D\\\"color:#009f9c!important;text-decoration:none;\\\" href=\\r\\n=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015180ed06f7e=\\r\\n37bd36a28c9824956812295ae4df7646f389759b34dfd2fda72017a9\\\" target=3D\\\"_blank\\\"=\\r\\n>exploded</a>=2E</p>\\r\\n<h3 class=3D\\\"skimm-h3\\\">WAIT&#8230;BACK UP=2E</h3>\\r\\n<p class=3D\\\"skimm-p\\\">The Colonial Pipeline is a system of pipes thousands o=\\r\\nf miles long that carries millions of barrels of gas, diesel, and jet fuel =\\r\\na&#160;day from Texas to New Jersey=2E It supplies about a third of the Eas=\\r\\nt Coast&#8217;s gas=2E So it&#8217;s a BF gassy D=2E But the pipeline&#8217=\\r\\n;s run into some problems lately=2E Earlier this fall, part of it was <a st=\\r\\nyle=3D\\\"color:#009f9c!important;text-decoration:none;\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518813c5513accd483e8db37422=\\r\\n7aa0e48b9f59b7e9d29b9b6525b209d347dd27b5\\\" target=3D\\\"_blank\\\">shut down</a> f=\\r\\nor over a week after a leak in Alabama spilled hundreds of thousands of gal=\\r\\nlons of gas=2E Cue a <a style=3D\\\"color:#009f9c!important;text-decoration:no=\\r\\nne;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151=\\r\\n85f16c50280b9de78bd24094b4015e8630991b8156c7bcd4cb37b39824eefd5b3\\\" target=\\r\\n=3D\\\"_blank\\\">gas shortage</a> and prices in southern states going up, up, up=\\r\\n=2E</p>\\r\\n<h3 class=3D\\\"skimm-h3\\\">SO WHAT&#8217;S THE LATEST?</h3>\\r\\n<p class=3D\\\"skimm-p\\\">On Monday, there was an explosion along the pipeline i=\\r\\nn Alabama that caused a major fire=2E One person was killed and several oth=\\r\\ners were injured=2E Yesterday, Alabama&#8217;s governor declared a state of=\\r\\n emergency&#160;<span style=3D\\\"color: #000000;\\\">to help make sure gas is de=\\r\\nlivered throughout the state=2E&#160;</span>Part of the pipeline&#8217;s be=\\r\\nen shut down and now the company that operates it says it <a style=3D\\\"color=\\r\\n:#009f9c!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=\\r\\n=2Etheskimm=2Ecom/?qs=3D13e1958399f01518f1a7a6aa74e54eaee3f22c427c587f46e59=\\r\\n947aedc70e2ddbba3242f6bfb8b69\\\" target=3D\\\"_blank\\\">could take days</a> to get=\\r\\n the whole thing up and running again=2E So&#160;for&#160;now it&#160;looks=\\r\\n like gas prices could be back on the up and up=2E</p>\\r\\n<h3 class=3D\\\"skimm-h3\\\">theSKIMM</h3>\\r\\n<p class=3D\\\"skimm-p\\\">A major pipeline that millions of people depend on for=\\r\\n gas every day can&#8217;t seem to get off the struggle bus=2E And now this=\\r\\n latest incident has fuel companies scrambling to stock up on supply=2E If&=\\r\\n#160;it starts costing more to fill up the tank, this is why=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518ea9e4fa2f2433c44e74693a=\\r\\nf3cd9018f5ece13b52fa953ffffb71b777ffa0e25\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015183b3cc0c3f30e436433c367=\\r\\nff042d3890808122395e67f4344c779240b0fb96b2\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n   <tbody>\\r\\n      <tr>\\r\\n         <td>\\r\\n            <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"=\\r\\n0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n               <tbody>\\r\\n                  <tr>\\r\\n                     <td width=3D\\\"100%\\\">\\r\\n                        <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"=\\r\\nmax-width:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"=\\r\\ncenter\\\" class=3D\\\"devicewidth\\\">\\r\\n                           <tbody>\\r\\n                              <tr>\\r\\n                                 <td>\\r\\n                                    <table width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:600px;\\\" align=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\"=\\r\\n class=3D\\\"devicewidthinner\\\">\\r\\n                                       <tbody>\\r\\n                                          <tr>\\r\\n                                             <td>\\r\\n\\r\\n    <h1 class=3D'skimm-h1' style=3D'font-size:24px!important;font-weight:bo=\\r\\nld;color:#000;border-bottom:3px solid #00A49F;padding:12px0!important;margi=\\r\\nn-top:8px;margin-bottom:24px;text-rendering:geometricPrecision;text-align:l=\\r\\neft;letter-spacing:0=2E08em;'>REPEAT AFTER ME=2E=2E=2E</h1>                =\\r\\n                             </td>\\r\\n                                          </tr>\\r\\n                                       </tbody>\\r\\n                                    </table>\\r\\n                                 </td>\\r\\n                              </tr>\\r\\n                           </tbody>\\r\\n                        </table>\\r\\n                     </td>\\r\\n                  </tr>\\r\\n               </tbody>\\r\\n            </table>\\r\\n         </td>\\r\\n      </tr>\\r\\n   </tbody>\\r\\n</table>\\r\\n      <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspaci=\\r\\nng=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY&#160;TO =\\r\\nYOUR FRIEND WHO GREW UP ON DEEP DISH PIZZA&#8230;</h3>\\r\\n<h3 class=3D\\\"skimm-h3\\\"></h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">In it to win it=2E Las=\\r\\nt night, the <a style=3D\\\"color:#009f9c!important;text-decoration:none;\\\" hre=\\r\\nf=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518eb92f32=\\r\\nee674c5ae648f0c76b48d0f861ccc7e253e391b9fc24ffef3c6d321fb\\\" target=3D\\\"_blank=\\r\\n\\\">Chicago Cubs tied up the World Series</a> by beating the Cleveland Indian=\\r\\ns in Game 6=2E They won 9-3=2E Now all eyes are on Game 7 tonight=2E The Cu=\\r\\nbs haven&#8217;t won the series since 1908=2E The Indians haven&#8217;t won=\\r\\n since 1948=2E So it&#8217;s not like there&#8217;s a lot on the line or an=\\r\\nything=2E Break out the peanuts=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151855235b31e07645b8ad0f96b=\\r\\n07b7084f725abc8922a18c65775c160f2b08e39a2\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518bba5a4134f1ad3263f4b6e=\\r\\n4af51516c63ab1f22dacc4f432de7db7b200638eeb\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY WHEN YOU=\\r\\n FIND OUT YOUR&#160;TWO FRIENDS WHO YOU INTRODUCED ARE GETTING TOGETHER&#16=\\r\\n0;WITHOUT YOU=2E=2E=2E</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">What&#8217;s going on =\\r\\nhere? Earlier this week, <a style=3D\\\"color:#009f9c!important;text-decoratio=\\r\\nn:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f=\\r\\n01518335c687702c2a91cdae5c278dd891b638aef063583271a5bb0b6836c0953412f\\\" targ=\\r\\net=3D\\\"_blank\\\">a federal judge ordered</a> the Republican party to explain a=\\r\\nny deals it may&#160;have made with GOP nominee Donald Trump&#8217;s campai=\\r\\ngn to monitor the polls during this election season=2E Reminder: for months=\\r\\n, Trump has been saying this election is&#160;<a style=3D\\\"color:#009f9c!imp=\\r\\nortant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2E=\\r\\ncom/?qs=3D13e1958399f015181282a946ce17ef3bccb16f5ee0ab70200fc13d574dfa40294=\\r\\n6fb8f75a3a9f0f7\\\" target=3D\\\"_blank\\\">&#8220;rigged&#8221;</a>&#160;against hi=\\r\\nm, and encouraging his supporters to be on the lookout for voter fraud=2E E=\\r\\narly voting opened recently in a lot of states=2E And there have been repor=\\r\\nts of Trump supporters allegedly photographing and <a style=3D\\\"color:#009f9=\\r\\nc!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheski=\\r\\nmm=2Ecom/?qs=3D13e1958399f015187d61ecd759d7b0730c22d96dc27b5bbdf7ca9040c492=\\r\\n390b0af040f590c882b5\\\" target=3D\\\"_blank\\\">harassing</a> people at the polls=\\r\\n=2E That&#8217;s according to a string of different lawsuits in&#160;Arizon=\\r\\na, Ohio, and Nevada flagging&#160;concerns that some voters are being intim=\\r\\nidated=2E Last week, the Democratic party filed a separate complaint with a=\\r\\n federal court=2E Now, the Republican party has until the end of today to t=\\r\\nurn over any evidence that could be related to collaborating&#160;with the =\\r\\nTrump campaign=2E&#160;Stay tuned=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518dc04662719719d7c96a196b=\\r\\n6493a840195c679622ee661714c17971d712ff8e3\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518e347797687291180617bc8=\\r\\n5052596e23d29e50c671041e605e72fc704ed3d004\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY TO WHEN =\\r\\nYOUR AM TRAIN GETS DELAYED FOR THE FIFTH TIME&#8230;</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">I give up=2E Yesterday=\\r\\n, Russia put Syrian peace talks on hold=2E <a style=3D\\\"color:#009f9c!import=\\r\\nant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom=\\r\\n/?qs=3D13e1958399f01518f6d20f368728eb2eed5d4f06ed7405a177f0acf26b236762f262=\\r\\nbfa4aa7f8044\\\" target=3D\\\"_blank\\\">Indefinitely=2E</a> For years, Syria has be=\\r\\nen going through a violent civil war between Syrian President Bashar al-Ass=\\r\\nad (backed by Russia) and rebel groups (some backed by the US) who want him=\\r\\n out of power=2E Hundreds of thousands of people have been killed=2E Millio=\\r\\nns have been forced to leave home, causing the EU&#8217;s worst migrant and=\\r\\n refugee crisis since World War II=2E The two sides have been trying and fa=\\r\\niling to hash out a peace deal for a while now=2E Earlier this month, Russi=\\r\\na agreed to a <a style=3D\\\"color:#009f9c!important;text-decoration:none;\\\" hr=\\r\\nef=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151881e18b=\\r\\n7235c573a07dea1d6f66d4e48cd2742d456c53c4803082f69bb0806362\\\" target=3D\\\"_blan=\\r\\nk\\\">&#8220;humanitarian pause&#8221;</a> to airstrikes in Aleppo - the key r=\\r\\nebel stronghold where a lot of the&#160;fighting has been focused=2E Meanwh=\\r\\nile, rebel&#8217;s have been&#160;<a style=3D\\\"color:#009f9c!important;text-=\\r\\ndecoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13=\\r\\ne1958399f015189ef9643b419448cc8bcbcc8372e8c961fd45cc9331da20df64ae665f22985=\\r\\n70e\\\" target=3D\\\"_blank\\\">fighting back in the city</a>=2E And today, Russia a=\\r\\nnnounced it would give rebels <a style=3D\\\"color:#009f9c!important;text-deco=\\r\\nration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e195=\\r\\n8399f01518afb1f76fc2f9e26bd6f8888964170d459663fedc1855df664b5efa241f0c9f2b\\\"=\\r\\n target=3D\\\"_blank\\\">even more time</a> to leave Aleppo=2E&#160;But it&#8217;=\\r\\ns still walking away from the peace table for the foreseeable future=2E So,=\\r\\n Syria&#8217;s still a peace of work=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151842b260af3e1bc0d9404642d=\\r\\n2e41e7037c150074a140f84985b3132dcecd8d9b8\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151878e0dacd88d3d84627a4a4=\\r\\nc82c3debbbab214d0eab433d4640110ad6d8f2c6e4\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-h3\\\">WHAT TO SAY WHEN YOU=\\r\\nR DATE SAYS HE NEEDS A NOISE MACHINE ON HIGH VOLUME TO FALL ASLEEP=2E=2E=2E=\\r\\n</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\">Having second thoughts=\\r\\n=2E That&#8217;s what Gannett is telling Tronc (the artist formerly known a=\\r\\ns Tribune Publishing)=2E For months, Gannett, which owns about a hundred ne=\\r\\nws outlets including USA Today, has been <a style=3D\\\"color:#009f9c!importan=\\r\\nt;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?=\\r\\nqs=3D13e1958399f01518bdef4b851c426335e6df0d77034c6d82d93d562a29526e8f268641=\\r\\n2ced239f5f\\\" target=3D\\\"_blank\\\">trying to buy Tronc</a>, which owns names&#16=\\r\\n0;like the LA Times and Chicago Tribune=2E For years, print news companies =\\r\\nhave been struggling to keep&#160;ad dollars and readers on board=2E Gannet=\\r\\nt was hoping that the power of two media companies combined would be more a=\\r\\nttractive than one, especially to advertisers=2E The deal&#160;would have c=\\r\\nreated one of the largest media groups in the country=2E So earlier this ye=\\r\\nar, Gannett saddled up to Tronc with a buyout offer of around $400 million=\\r\\n=2E Tronc said &#8216;no, thanks, we&#8217;re not that cheap=2E&#8217; Yest=\\r\\nerday, after a lot of&#160;back and forth, Gannett <a style=3D\\\"color:#009f9=\\r\\nc!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheski=\\r\\nmm=2Ecom/?qs=3D13e1958399f01518d2bffab0c234f17b032b9701c23e88c1cd13f48f1fdc=\\r\\n3ceff250bcb5f1df018d\\\" target=3D\\\"_blank\\\">walked away from the deal=2E</a> Th=\\r\\nis might&#160;also be because Gannett had &#8216;meh&#8217; earnings last q=\\r\\nuarter, meaning they couldn&#8217;t get the cash together to write the chec=\\r\\nk=2E Either way, both companies are still single AF=2E</p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015182da3184f75be5fdc54fecb1=\\r\\nac3b84096d5f14c82b9c1885506577545687ffc20\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f0151819c57a5144e9444db88588=\\r\\n9da1a6f479ee69473d5dc1540bded14eb2254d7a61\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <h3 class=3D\\\"skimm-p skimm-h3\\\">WHAT TO SAY =\\r\\nTO YOUR FRIEND WHO ONLY DOES SQUATS AT THE GYM =2E=2E=2E</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                <p class=3D\\\"skimm-p\\\"><a style=3D\\\"color:#009=\\r\\nf9c!important;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Ethes=\\r\\nkimm=2Ecom/?qs=3D13e1958399f0151874e24bacd196875a37d50bd496ccbfdb973ca05810=\\r\\nb522ba89490afa4a8a9a45\\\" target=3D\\\"_blank\\\">Say goodbye to your fave emoji=2E=\\r\\n</a></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518989cbf0774890fc15ee0775=\\r\\ne5993d2e6f4301ecd0fb28b902dce8178e03e765a\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015186faa6a631bb0b3f66a437b=\\r\\nac5f5683830b4a05e4edd8e9289820f4606048f5a4\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"0\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                                                           =\\r\\n                     <h1 class=3D\\\"skimm-h1\\\" style=3D\\\"font-size:24px!importa=\\r\\nnt;font-weight:bold;color:#000;border-bottom:3px solid #00A49F;padding:12px=\\r\\n0!important;margin-top:8px;margin-bottom:24px;text-rendering:geometricPreci=\\r\\nsion;text-align:left;letter-spacing:0=2E08em;\\\">SKIMM THE VOTE</h1>\\r\\n                                                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <table width=3D\\\"100%\\\" style=3D\\\"text-align:l=\\r\\neft!important;margin:0 auto!important;max-width:595px\\\" cellpadding=3D\\\"0\\\" ce=\\r\\nllspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"left\\\">\\r\\n                                  <tbody>\\r\\n                                    <tr>\\r\\n                                      <td width=3D\\\"100%\\\" align=3D\\\"left\\\" val=\\r\\nign=3D\\\"middle\\\">\\r\\n                                        <img src=3D\\\"http://cdn=2Etheskimm=\\r\\n=2Ecom/email/3/retina/together-with-Google=2Epng\\\" width=3D\\\"300\\\" height=3D\\\"2=\\r\\n8\\\" border=3D\\\"0\\\" style=3D\\\"border:none;min-height:auto;line-height:100%;margi=\\r\\nn:0;outline:none;padding:0;text-decoration:none\\\">\\r\\n                                      </td>\\r\\n                                    </tr>\\r\\n                                    <tr>\\r\\n                                      <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td=\\r\\n>\\r\\n                                    </tr>\\r\\n                                  </tbody>\\r\\n                                </table>\\r\\n                              </td>\\r\\n                            </tr>                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                                <p class=3D=\\r\\n\\\"skimm-p\\\"><span style=3D\\\"color: #000000;\\\">In case you somehow missed it, yo=\\r\\nu have to vote <span class=3D\\\"aBn\\\" data-term=3D\\\"goog_1567168121\\\" tabindex=\\r\\n=3D\\\"0\\\"><span class=3D\\\"aQJ\\\">on Tuesday</span></span>=2E Catch up on who&#821=\\r\\n7;s on your ballot and what&#8217;s at stake=2E Then plan your trip to the =\\r\\npolls=2E </span><span style=3D\\\"color: #000000;\\\"><a style=3D\\\"color:#009f9c!i=\\r\\nmportant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=\\r\\n=2Ecom/?qs=3D13e1958399f015188c8432bed6227d27e00e716a22c0a3cb51beaa8ddacfe0=\\r\\n63b60ac233b6606f25\\\" target=3D\\\"_blank\\\">Questions, answered=2E</a>&#160;</spa=\\r\\nn></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                <div class=3D\\\"share-jumpto-links\\\" style=3D\\\"=\\r\\nmargin-top:0px!important;margin-bottom:12px!important;margin-left:auto;marg=\\r\\nin-right:auto;\\\">\\r\\n                                  <div style=3D\\\"box-sizing:border-box;posit=\\r\\nion:relative;height:45px;\\\">\\r\\n                                    <div style=3D\\\"padding:0;overflow:hidden=\\r\\n;color:#009f9c;font-family:'Raleway',sans-serif;font-weight:800;font-size: =\\r\\n12px;text-transform: uppercase;line-height: 30px;letter-spacing: 0=2E28em;t=\\r\\next-align:left;\\\">\\r\\n                                      <span style=3D\\\"display:inline-block;m=\\r\\nargin-right:6px;\\\">Skimm This</span><a style=3D\\\"width:20px;height:30px;verti=\\r\\ncal-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://clic=\\r\\nk=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518d25ca1d7e82bf2beb04f9ea=\\r\\n0fd4fac2a209c9e8fa3162db1c538ebe60b2cb8be\\\"><img src=3D\\\"http://cdn=2Etheskim=\\r\\nm=2Ecom/email/3/retina/logo_facebook_teal=2Epng\\\" style=3D\\\"display:block;ver=\\r\\ntical-align:middle;padding-top:0px;margin:0 auto !important;\\\" width=3D\\\"10\\\" =\\r\\nheight=3D\\\"24\\\" alt=3D\\\"Like Us\\\" /></a><a style=3D\\\"width:30px;height:30px;vert=\\r\\nical-align:bottom;margin: 0 5px 0;display:inline-block;\\\" href=3D\\\"http://cli=\\r\\nck=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f01518fa56ed304e70dc7806f797=\\r\\n21441d3f2dba8a0d21fe6daaa263e2640a05536a61\\\"><img src=3D\\\"http://cdn=2Etheski=\\r\\nmm=2Ecom/email/3/retina/logo_twitter_teal=2Epng\\\"  style=3D\\\"display:block;ve=\\r\\nrtical-align:middle;padding-top:0px;margin:4px auto 0 !important;\\\" width=3D=\\r\\n\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet with Us\\\" /></a>\\r\\n                                                  </div>\\r\\n                                  </div>\\r\\n                                </div>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                          <tr>\\r\\n                                <td style=3D\\\"font-family:Helvetica,Arial,sa=\\r\\nns-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;marg=\\r\\nin:0;padding:0;text-align:center;\\\">\\r\\n                                  <a id=3D\\\"stv_badge\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015181e724ef34cabd7ed78134c92=\\r\\nf94f694fbdb799777126a5594c4ea20eaa22ed06\\\" target=3D\\\"_blank\\\"><img style=3D\\\"m=\\r\\nargin-bottom:20px;\\\" src=3D\\\"https://cdn=2Etheskimm=2Ecom/email/3/retina/Pled=\\r\\nge-To-Vote-R1=2Epng\\\" width=3D\\\"100\\\" height=3D\\\"100\\\" alt=3D\\\"Pledge to Vote\\\" />=\\r\\n</a>\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                                                      </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"0\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                                                           =\\r\\n               <h3 style=3D\\\"text-transform:uppercase;color:#000;display:blo=\\r\\nck;font-family:Helvetica,Arial,sans-serif;font-size:15px;font-weight:bold!i=\\r\\nmportant;line-height:20px;margin-top:20px;margin-right:auto;margin-bottom:4=\\r\\npx;margin-left:auto;text-align:center;letter-spacing:0=2E15em;padding:0;pad=\\r\\nding-bottom:15px!important\\\">SKIMM 50</h3>\\r\\n                                                            </td>\\r\\n                            </tr>\\r\\n                            <!--skimmPH:[skimm-custom-SKIMM 50]-->         =\\r\\n                   <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                                <p class=3D=\\r\\n\\\"skimm-p\\\"><em>Thanks to these Skimm&#8217;bassadors for sharing like it&#82=\\r\\n17;s hot=2E Want to see your name on this list? <a style=3D\\\"color:#009f9c!i=\\r\\nmportant;text-decoration:none;\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=\\r\\n=2Ecom/?qs=3D13e1958399f01518ceda3d8e1ed0b62e87b8df3fe3ecb8a5f3ad2f674819af=\\r\\ncdb1005e18c5f072d3\\\" target=3D\\\"_blank\\\">Click here</a> to learn more=2E&#160;=\\r\\n</em></p>\\r\\n<p class=3D\\\"skimm-p\\\"><span style=3D\\\"color: #222222;\\\">Olivia Simonson, Britt=\\r\\nany Berger, Daniela Rivas, Ashley Lawson, Sarah Davidson, L Denease Thompso=\\r\\nn-Mack, Noreen Deutsch, Jessee Fordham, Natasha Shah, Shari Berga, Aloke Pr=\\r\\nabhu, Natalie Tenzer, Samantha Panchevre, Ania Arsenowicz, Ali Wozniak, Rei=\\r\\nnalyn, Taunya Robinson, Cassie Christensen, Spencer Philips, Abby Bilinski,=\\r\\n Aner Zhou, Julia Erdenebold, Mehreen Mazhar, Alexandra Rizzo, Alisa Sutton=\\r\\n, Maegan Detlefs, Janina Ancona, Chris Drake, Allan Moss, Jordan Murray, Da=\\r\\nvid Benjamin, Leslie Bartula, Hana Muslic, Kelly Wallace, Leslie Buteyn, Be=\\r\\nthany Fitzgerald, Stuart Ferguson, Emily Paulino, Allison Ryan, Niki DeMaio=\\r\\n, Ria Conti, Nikki Boyd, Amanda Oliver, Lindsay Barthel, Shelby Wynn, Britt=\\r\\nany Daunno, Emily Rissmiller, Alex Johannes, Andrea Borod, Ashley Montufar,=\\r\\n Karli Von Herbulis, Lexie Rindler, Natalie Weaver, Allison Sotelo, Cynthia=\\r\\n Lopez, Isabel Taylor, Jess Tolan, Kasie Heiden, Kelsey Will, Leslie McFayd=\\r\\nen, Morgan Balavage, Morgan Goracke, Madeline Trainor, Alexa Parisi, Uma Su=\\r\\ndarshan, Brooke Borkowski, Connie Lin, Jess Ridolfino, Jessica Foster, Mall=\\r\\nory Mickus, Sapreen Abbass, Kelli Holland</span></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"20\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td>\\r\\n                              <h3 style=3D\\\"text-transform:uppercase;color:#=\\r\\n000;display:block;font-family:Helvetica,Arial,sans-serif;font-size:15px;fon=\\r\\nt-weight:bold!important;line-height:20px;margin-top:20px;margin-right:auto;=\\r\\nmargin-bottom:4px;margin-left:auto;text-align:center;letter-spacing:0=2E15e=\\r\\nm;padding:0;padding-bottom:15px!important\\\">SKIMM SHARE</h3>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                                           =\\r\\n         <tr>\\r\\n                              <td class=3D\\\"share-button-copy\\\" style=3D\\\"font=\\r\\n-family:Helvetica,Arial,sans-serif;font-size:16px;line-height:20px;letter-s=\\r\\npacing:0em;color:#000;margin:0;padding:0;\\\">\\r\\n                                                                           =\\r\\n                     <p class=3D\\\"skimm-p\\\">Halfway there=2E Share theSkimm w=\\r\\nith your work wife who&#160;always takes a coffee break when you need it=2E=\\r\\n</p>\\r\\n<p class=3D\\\"skimm-p\\\">\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;text-align:center;\\\">\\r\\n                                <a style=3D\\\"color:#009f9c!important;text-de=\\r\\ncoration:none;\\\" href=3D\\\"http://www=2Etheskimm=2Ecom/invite/v2/new?email=3Dc=\\r\\nhristine@spang=2Ecc&utm_source=3Demail&utm_medium=3Dinvite&utm_campaign=3Db=\\r\\nottom\\\" target=3D\\\"_blank\\\"><img src=3D\\\"https://cdn=2Etheskimm=2Ecom/email/3/r=\\r\\netina/badge_share-the-skimm=2Epng\\\" width=3D\\\"100\\\" height=3D\\\"100\\\" alt=3D\\\"Shar=\\r\\ne theSkimm\\\" /></a>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"50\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n<table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"=\\r\\n0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n  <tbody>\\r\\n    <tr>\\r\\n      <td>\\r\\n        <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\" c=\\r\\nellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n          <tbody>\\r\\n            <tr>\\r\\n              <td width=3D\\\"100%\\\">\\r\\n                <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-widt=\\r\\nh:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" =\\r\\nclass=3D\\\"devicewidth\\\">\\r\\n                  <tbody>\\r\\n                    <tr>\\r\\n                      <td width=3D\\\"100%\\\" height=3D\\\"0\\\"></td>\\r\\n                    </tr>\\r\\n                    <tr>\\r\\n                      <td>\\r\\n                        <table width=3D\\\"100%\\\" style=3D\\\"max-width:600px;\\\" al=\\r\\nign=3D\\\"center\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidthinner\\\">\\r\\n                          <tbody>\\r\\n                            <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                            <tr>\\r\\n                              <td>\\r\\n                                                            <img width=3D\\\"3=\\r\\n6\\\" height=3D\\\"39\\\" src=3D\\\"http://cdn=2Etheskimm=2Ecom/email/3/retina/skimmbir=\\r\\nthdays_icon=2Epng\\\" border=3D\\\"0\\\" style=3D\\\"border:none;min-height:auto;line-h=\\r\\neight:100%;margin:0 auto 22px;outline:none;padding:0;text-decoration:none;d=\\r\\nisplay:block;margin-left:auto;margin-right:auto\\\">\\r\\n                                                                           =\\r\\n               <h3 style=3D\\\"text-transform:uppercase;color:#000;display:blo=\\r\\nck;font-family:Helvetica,Arial,sans-serif;font-size:15px;font-weight:bold!i=\\r\\nmportant;line-height:20px;margin-top:20px;margin-right:auto;margin-bottom:4=\\r\\npx;margin-left:auto;text-align:center;letter-spacing:0=2E15em;padding:0;pad=\\r\\nding-bottom:15px!important\\\">SKIMM BIRTHDAYS</h3>\\r\\n                                                            </td>\\r\\n                            </tr>\\r\\n                            <!--skimmPH:[skimm-birthdays-SKIMM BIRTHDAYS]--=\\r\\n>                            <tr>\\r\\n                              <td style=3D\\\"font-family:Helvetica,Arial,sans=\\r\\n-serif;font-size:16px;line-height:20px;letter-spacing:0em;color:#000;margin=\\r\\n:0;padding:0;\\\">\\r\\n                                                                <p class=3D=\\r\\n\\\"skimm-p\\\"><span style=3D\\\"color: #000000;\\\"><b style=3D\\\"color: #222222;\\\"><a s=\\r\\ntyle=3D\\\"color:#009f9c!important;text-decoration:none;\\\" href=3D\\\"http://click=\\r\\n=2Emorning7=2Etheskimm=2Ecom/?qs=3D13e1958399f015185aea2b21fa151bb8b11f09d4=\\r\\nab4aefdd45bee2439742b48454e60e800bcdb09f\\\" target=3D\\\"_blank\\\" data-saferedire=\\r\\ncturl=3D\\\"https://www=2Egoogle=2Ecom/url?hl=3Den&amp;q=3Dhttp://theskimm=2Ec=\\r\\nom/skimm-guides/skimmbassadors&amp;source=3Dgmail&amp;ust=3D147810774272500=\\r\\n0&amp;usg=3DAFQjCNE7P50gy4ABMYMODv1IPwaf6ivegA\\\" style=3D\\\"color: #1155cc;\\\"><=\\r\\nspan style=3D\\\"color: #000000;\\\">* indicates Skimm&#8217;bassador=2E</span></=\\r\\na>&#160;Hammer time=2E</b></span></p>\\r\\n<p style=3D\\\"color: #222222;\\\" class=3D\\\"skimm-p\\\"><span style=3D\\\"color: #00000=\\r\\n0;\\\"><b>Louise Cronin </b>(San Francisco, CA);<b> Faith Greenberg&#160;</b>(=\\r\\nLongwood, FL);<b>&#160;*Hatley Thompson&#160;</b>(Washington, DC);&#160;<b>=\\r\\n*Monique Ervine&#160;</b>(Kindersley, Canada);&#160;<b>*Leana Macrito&#160;=\\r\\n</b>(Chicago, IL);&#160;<b>*AnnMarie Murtaugh</b>&#160;(Houston, TX);&#160;=\\r\\n<b>*Maria Perwerton&#160;</b>(Rochester, MI);&#160;<b>*Lauren Valainis&#160=\\r\\n;</b>(Washington, DC);&#160;<b>*Jaycie Moller&#160;</b>(San Francisco, CA);=\\r\\n&#160;<b>*Erin Manfull&#160;</b>(Iowa City, IA);&#160;<b>*Jennifer Rheaume&=\\r\\n#160;</b>(Boston, MA);&#160;<b>*Karishma Tank&#160;</b>(New York, NY);&#160=\\r\\n;<b>*Neelima Agrawal&#160;</b>(Chicago, IL);&#160;<b>*Eliza Webb&#160;</b>(=\\r\\nSeattle, WA );&#160;<b>*Conoly Cravens&#160;</b>(Atlanta, GA);&#160;<b>*Bre=\\r\\nnt Randall&#160;</b>(New York, NY);&#160;<b>*Alicia Heiser&#160;</b>(Spokan=\\r\\ne, WA);&#160;<b>*Nicole Rodriguez&#160;</b>(Sebastian, FL);&#160;<b>Sarah H=\\r\\nofschire&#160;</b>(Framingham, MA);&#160;<b>Aakankhya Patro</b>&#160;(Colle=\\r\\nge Station, TX);&#160;<b>Kalon Taylor</b>&#160;(Memphis, TN);&#160;<b>Joyce=\\r\\n A&#160;</b>(Milford, CT);&#160;<b>Nora Delay&#160;</b>(Bali, Indonesia);&#=\\r\\n160;<b>Ashleigh Heaton&#160;</b>(Astoria, NY);&#160;<b>Angie Teates&#160;</=\\r\\nb>(Washington, DC);&#160;<b>Michelle Aclander&#160;</b>(Stony Brook, NY);&#=\\r\\n160;<b>Laura Dominick&#160;</b>(Highland Park, NJ);&#160;<b>Polly Minifie S=\\r\\nnyder&#160;</b>(Washington, DC);&#160;<b>Sarah Minifie Wolfgang&#160;</b>(B=\\r\\noston, MA);&#160;<b>Korrie Nickels&#160;</b>(Chicago, IL);&#160;<b>Kristyn =\\r\\nGelsomini&#160;</b>(Houston, TX);&#160;<b>Meaghan Horton&#160;</b>(New York=\\r\\n, NY);&#160;<b>Dannetta Gibson Ballou&#160;</b>(Columbia MD);&#160;<b>Suzie=\\r\\n Tice&#160;</b>(Great Falls, MT);&#160;<b>Annette Bani</b>&#160;(Limerick, =\\r\\nPA);&#160;<b>Zoe Berman</b>&#160;(Armonk, NY);&#160;<b>Rebekah Harper</b>&#=\\r\\n160;(Raleigh, NC);&#160;<b>Alessandra Messineo Long</b>&#160;(Greenwich, CT=\\r\\n);&#160;<b>Anne McArthur</b>&#160;(Louisville, KY);&#160;<b>Jennifer Wham</=\\r\\nb>;&#160;<b>Karin Seymour</b>&#160;(Fairfield, CT);&#160;<b>Lauren DiNicola=\\r\\n</b>&#160;(New Haven, CT);&#160;<b>Lucy Jackoboice</b>&#160;(Grand Rapids, =\\r\\nMI);&#160;<b>Zoe Weiss</b>&#160;(New York, NY)</span></p>\\r\\n                              </td>\\r\\n                            </tr>\\r\\n                                                        <tr>\\r\\n                              <td width=3D\\\"100%\\\" height=3D\\\"15\\\"></td>\\r\\n                            </tr>\\r\\n                          </tbody>\\r\\n                        </table>\\r\\n                      </td>\\r\\n                    </tr>\\r\\n                  </tbody>\\r\\n                </table>\\r\\n              </td>\\r\\n            </tr>\\r\\n          </tbody>\\r\\n        </table>\\r\\n      </td>\\r\\n    </tr>\\r\\n  </tbody>\\r\\n</table>\\r\\n\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\" height=3D\\\"21\\\"></td>\\r\\n              </tr>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\">\\r\\n                  <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" align=3D\\\"center\\\" cellspacing=3D\\\"0\\\" cellpadding=3D\\\"0\\\" border=3D\\\"0=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td style=3D\\\"color: #000; font-family: 'Helvetica',=\\r\\n 'Arial', sans-serif;font-size: 14px;font-weight: normal;line-height: 20px;=\\r\\n text-align:center;\\\">\\r\\n                          Skimm'd something we missed?\\r\\n                          <br>\\r\\n                          <br>\\r\\n                          Email <a href=3D\\\"mailto:SkimmThis@theSkimm=2Ecom\\\"=\\r\\n target=3D\\\"_blank\\\" style=3D\\\"color: #009f9c !important; text-decoration: non=\\r\\ne\\\">SkimmThis@theSkimm=2Ecom</a>  <span class=3D\\\"bullet\\\">&bull;</span> <a hr=\\r\\nef=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580bedd5217eda378a2f7d=\\r\\nccb8e26b5b302c761b48c753ae37dd56d06342bd7761205726e794f19c\\\" target=3D\\\"_blan=\\r\\nk\\\" style=3D\\\"color: #009f9c !important; text-decoration: none\\\">Read in brows=\\r\\ner &raquo;</a>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                      <tr>\\r\\n                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\">\\r\\n                  <table bgcolor=3D\\\"#009f9c\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td>\\r\\n                          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" a=\\r\\nlign=3D\\\"left\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"dev=\\r\\nicewidthinner\\\">\\r\\n                            <tbody>\\r\\n                              <tr>\\r\\n                                <td style=3D\\\"padding:10px;font-family: 'Hel=\\r\\nvetica', 'Arial', sans-serif; color: #fff !important; display: block; font-=\\r\\nsize: 13px; font-weight: bold; text-align: center; text-decoration: none;le=\\r\\ntter-spacing:3px; min-width: 320px; \\\" class=3D\\\"share-header\\\">\\r\\n                                  SHARE &amp; FOLLOW US\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                            </tbody>\\r\\n                          </table>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                  <table bgcolor=3D\\\"#009f9c\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td>\\r\\n                          <table width=3D\\\"100%\\\" style=3D\\\"\\\" align=3D\\\"left\\\" b=\\r\\norder=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"devicewidthinner\\\">=\\r\\n\\r\\n                            <tbody>\\r\\n                              <tr>\\r\\n                                <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td>\\r\\n                              </tr>\\r\\n                              <tr>\\r\\n                                <td>\\r\\n                                  <table width=3D\\\"100%\\\" style=3D\\\"\\\" align=3D=\\r\\n\\\"center\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"devicewi=\\r\\ndthinner\\\">\\r\\n                                    <tbody>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:center;lin=\\r\\ne-height: 24px;min-width:245px;\\\" class=3D\\\"share-facebook-and-twitter\\\">\\r\\n                                          <a class=3D\\\"share share-facebook\\\"=\\r\\n target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D5=\\r\\n80bedd5217eda37226dc355defd764c4a5bacce1c02696337681417286260dfec3f16ecc900=\\r\\n18bc\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 1=\\r\\n5px; width:90px; white-space:nowrap; text-align: center; text-decoration: n=\\r\\none; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/ema=\\r\\nil/3/retina/footer_facebook=2Epng\\\" width=3D\\\"10\\\" height=3D\\\"23\\\" alt=3D\\\"Like U=\\r\\ns\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both; dis=\\r\\nplay: block; float: none; font-size: 28px; margin: 0 auto 10px; max-width: =\\r\\n100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=3D\\\"=\\r\\ncolor: #fff !important; display: block; font-size: 11px; font-weight: bold;=\\r\\n letter-spacing: 3px; margin: 15px auto\\\">Facebook</span></a>\\r\\n                                          <a class=3D\\\"share share-twitter\\\" =\\r\\ntarget=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D58=\\r\\n0bedd5217eda37b28f80f6926841e373f2063b283fd3960ec2f4f135612ad6b273d8e901e4e=\\r\\n7c5\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 15=\\r\\npx; width:90px; white-space:nowrap; text-align: center; text-decoration: no=\\r\\nne; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/emai=\\r\\nl/3/retina/footer_twitter=2Epng\\\" width=3D\\\"25\\\" height=3D\\\"19\\\" alt=3D\\\"Tweet wi=\\r\\nth Us\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both;=\\r\\n display: block; float: none; font-size: 28px; margin: 0 auto 10px; max-wid=\\r\\nth: 100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=\\r\\n=3D\\\"color: #fff !important; display: block; font-size: 11px; font-weight: b=\\r\\nold; letter-spacing: 3px; margin: 15px auto\\\">Twitter</span></a>\\r\\n                                          <a class=3D\\\"share share-tumblr\\\" t=\\r\\narget=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580=\\r\\nbedd5217eda3784ba9df91bdb28220027be1a35943845c0ff17cf92fc7b3b89cbe9ce6eee68=\\r\\n14\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 15p=\\r\\nx; width:90px; white-space:nowrap; text-align: center; text-decoration: non=\\r\\ne; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/email=\\r\\n/3/retina/footer_tumblr=2Epng\\\" width=3D\\\"13\\\" height=3D\\\"21\\\" alt=3D\\\"Tumble wit=\\r\\nh Us\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both; =\\r\\ndisplay: block; float: none; font-size: 28px; margin: 0 auto 8px; max-width=\\r\\n: 100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=\\r\\n=3D\\\"color: #fff !important; display: block; font-size: 11px; font-weight: b=\\r\\nold; letter-spacing: 3px; margin: 15px auto\\\">Tumblr</span></a>\\r\\n                                          <a class=3D\\\"share share-instagram=\\r\\n\\\" target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D=\\r\\n580bedd5217eda37254b18b2d0b03609f0ba749107959301d96068b56b4cb840c3dafa14865=\\r\\nc8eae\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 =\\r\\n15px; width:90px; white-space:nowrap; text-align: center; text-decoration: =\\r\\nnone; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/em=\\r\\nail/3/retina/footer_instagram=2Epng\\\" width=3D\\\"21\\\" height=3D\\\"22\\\" alt=3D\\\"Inst=\\r\\nagram Us\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: bo=\\r\\nth; display: block; float: none; font-size: 28px; margin: 0 auto 10px; max-=\\r\\nwidth: 100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span st=\\r\\nyle=3D\\\"color: #fff !important; display: block; font-size: 11px; font-weight=\\r\\n: bold; letter-spacing: 3px; margin: 15px auto\\\">Instagram</span></a>\\r\\n                                          <a class=3D\\\"share share-pinterest=\\r\\n\\\" target=3D\\\"_blank\\\" href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D=\\r\\n580bedd5217eda3708ba55eaca8df450cdc2a1ff1d14b5c3865e1d4fe0745f6c13ceab0948d=\\r\\nf5210\\\" style=3D\\\"color: #009f9c !important; display: inline-block; margin:0 =\\r\\n15px; width:90px; white-space:nowrap; text-align: center; text-decoration: =\\r\\nnone; text-transform: uppercase\\\"><img src=3D\\\"http://cdn=2Etheskimm=2Ecom/em=\\r\\nail/3/retina/footer_pinterest=2Epng\\\" width=3D\\\"24\\\" height=3D\\\"24\\\" alt=3D\\\"Pin =\\r\\nUs\\\" style=3D\\\"-ms-interpolation-mode: bicubic; border: none; clear: both; di=\\r\\nsplay: block; float: none; font-size: 28px; margin: 0 auto 8px; max-width: =\\r\\n100%; outline: none; text-decoration: none;\\\" align=3D\\\"none\\\"><span style=3D\\\"=\\r\\ncolor: #fff !important; display: block; font-size: 11px; font-weight: bold;=\\r\\n letter-spacing: 3px; margin: 15px auto\\\">Pinterest</span></a>\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                    </tbody>\\r\\n                                  </table>\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                            </tbody>\\r\\n                          </table>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n  <table width=3D\\\"100%\\\" bgcolor=3D\\\"#ffffff\\\" cellpadding=3D\\\"0\\\" cellspacing=\\r\\n=3D\\\"0\\\" border=3D\\\"0\\\" id=3D\\\"backgroundTable\\\">\\r\\n    <tbody>\\r\\n      <tr>\\r\\n        <td>\\r\\n          <table width=3D\\\"100%\\\" style=3D\\\"max-width:620px\\\" cellpadding=3D\\\"0\\\"=\\r\\n cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center\\\" class=3D\\\"devicewidth\\\">\\r\\n            <tbody>\\r\\n              <tr>\\r\\n                <td width=3D\\\"100%\\\">\\r\\n                  <table bgcolor=3D\\\"#ffffff\\\" width=3D\\\"100%\\\" style=3D\\\"max-wi=\\r\\ndth:620px\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" border=3D\\\"0\\\" align=3D\\\"center=\\r\\n\\\" class=3D\\\"devicewidth\\\">\\r\\n                    <tbody>\\r\\n                      <tr>\\r\\n                        <td>\\r\\n                          <table width=3D\\\"100%\\\" style=3D\\\"max-width:290px;\\\" =\\r\\nalign=3D\\\"left\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" class=3D\\\"de=\\r\\nvicewidth\\\">\\r\\n                            <tbody>\\r\\n                              <tr>\\r\\n                                <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td>\\r\\n                              </tr>\\r\\n                              <tr>\\r\\n                                <td>\\r\\n                                  <table width=3D\\\"100%\\\" style=3D\\\"max-width:=\\r\\n270px;\\\" align=3D\\\"center\\\" border=3D\\\"0\\\" cellpadding=3D\\\"0\\\" cellspacing=3D\\\"0\\\" c=\\r\\nlass=3D\\\"devicewidthinner\\\">\\r\\n                                    <tbody>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:left;line-=\\r\\nheight: 24px;\\\">\\r\\n                                          Copyright &#169; 2016 theSkimm, A=\\r\\nll rights reserved=2E\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:left;line-=\\r\\nheight: 24px;\\\">\\r\\n                                          <b>Our mailing address is: </b><b=\\r\\nr />\\r\\n                                          theSkimm Inc=2E<br />\\r\\n                                          49 W 23rd Street, 10th Floor<br /=\\r\\n>\\r\\n                                          New York, NY, 10010, United State=\\r\\ns\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td width=3D\\\"100%\\\" height=3D\\\"22\\\"></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td style=3D\\\"font-family: Helvetica=\\r\\n, arial, sans-serif; font-size: 12px; color: #333333; text-align:left;line-=\\r\\nheight: 24px;\\\">\\r\\n                                          <a href=3D\\\"http://click=2Emorning=\\r\\n7=2Etheskimm=2Ecom/profile_center=2Easpx?qs=3Deec37f32b3ca83aeebff6369cdb5b=\\r\\n630c8bd88a7221b6c466ba89d97456c40da8f9fd278c3d66e231b08d677113d878825b0f05c=\\r\\n429cfd61\\\" >Update Profile</a><br/>\\r\\n            <a href=3D\\\"http://click=2Emorning7=2Etheskimm=2Ecom/?qs=3D580be=\\r\\ndd5217eda3795cd2024864be3ff004c57b4a334f836fc8b6174f03069dbbb0ac1024ad66989=\\r\\n\\\" >Unsubscribe</a>\\r\\n                                        </td>\\r\\n                                      </tr>\\r\\n                                      <tr>\\r\\n                                        <td width=3D\\\"100%\\\" height=3D\\\"60\\\"></=\\r\\ntd>\\r\\n                                      </tr>\\r\\n                                    </tbody>\\r\\n                                  </table>\\r\\n                                </td>\\r\\n                              </tr>\\r\\n                              <tr>\\r\\n                                <td width=3D\\\"100%\\\" height=3D\\\"10\\\"></td>\\r\\n                              </tr>\\r\\n                            </tbody>\\r\\n                          </table>\\r\\n                        </td>\\r\\n                      </tr>\\r\\n                    </tbody>\\r\\n                  </table>\\r\\n                </td>\\r\\n              </tr>\\r\\n            </tbody>\\r\\n          </table>\\r\\n        </td>\\r\\n      </tr>\\r\\n    </tbody>\\r\\n  </table>\\r\\n\\r\\n<img src=3D\\\"https://pixel=2Emonitor1=2Ereturnpath=2Enet/pixel=2Egif?r=3D3e9=\\r\\nfa24c7442d95337a14dfcd7b45f193869d154\\\" width=3D\\\"1\\\" height=3D\\\"1\\\" />\\r\\n<IMG SRC=3D\\\"https://ad=2Edoubleclick=2Enet/ddm/trackimp/N5295=2E1915120THES=\\r\\nKIMM/B10126095=2E140587162;dc_trk_aid=3D312596749;dc_trk_cid=3D75394976;ord=\\r\\n=3D78a354ed-3d53-49f8-add5-cc82487a42f8;dc_lat=3D;dc_rdid=3D;tag_for_child_=\\r\\ndirected_treatment=3D?\\\" BORDER=3D\\\"0\\\" HEIGHT=3D\\\"1\\\" WIDTH=3D\\\"1\\\" ALT=3D\\\"Advert=\\r\\nisement\\\">\\r\\n<IMG SRC=3D\\\"https://ad=2Edoubleclick=2Enet/ddm/trackimp/N5295=2E1915120THES=\\r\\nKIMM/B10126095=2E140587164;dc_trk_aid=3D312596749;dc_trk_cid=3D75394976;ord=\\r\\n=3D10ffd6d4-6293-4123-9424-9723c421447e;dc_lat=3D;dc_rdid=3D;tag_for_child_=\\r\\ndirected_treatment=3D?\\\" BORDER=3D\\\"0\\\" HEIGHT=3D\\\"1\\\" WIDTH=3D\\\"1\\\" ALT=3D\\\"Advert=\\r\\nisement\\\">\\t\\t\\r\\n<IMG SRC=3D\\\"https://ad=2Edoubleclick=2Enet/ddm/trackimp/N5295=2E1915120THES=\\r\\nKIMM/B10126095=2E140587170;dc_trk_aid=3D312596749;dc_trk_cid=3D75394976;ord=\\r\\n=3D5717585d-3efb-4821-80f4-232ee95b57c4;dc_lat=3D;dc_rdid=3D;tag_for_child_=\\r\\ndirected_treatment=3D?\\\" BORDER=3D\\\"0\\\" HEIGHT=3D\\\"1\\\" WIDTH=3D\\\"1\\\" ALT=3D\\\"Advert=\\r\\nisement\\\">\\r\\n\\r\\n</body>\\r\\n</html>\\r\\n\",\"snippet\":\"Is this email not displaying correctly? View it in your browser=2E SHARE THIS SHARE THIS Skimm for November\",\"unread\":false,\"starred\":false,\"date\":\"Wed, 02 Nov 2016 04:21:46 -0600\",\"folderImapUID\":343848,\"folderId\":\"test-folder-id\",\"folder\":{\"id\":\"test-folder-id\",\"account_id\":\"test-account-id\",\"object\":\"folder\",\"name\":null,\"display_name\":\"Test Folder\",\"sync_state\":{}},\"labels\":[],\"headers\":{\"delivered-to\":[\"christine@spang.cc\"],\"received\":[\"by 10.31.236.3 with SMTP id k3csp698913vkh; Wed, 2 Nov 2016 03:21:52 -0700 (PDT)\",\"from mta.morning7.theskimm.com (mta.morning7.theskimm.com. [136.147.177.13]) by mx.google.com with ESMTPS id j59si882394qtb.16.2016.11.02.03.21.50 for <christine@spang.cc> (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128); Wed, 02 Nov 2016 03:21:52 -0700 (PDT)\",\"by mta.morning7.theskimm.com id h36v3s163hst for <christine@spang.cc>; Wed, 2 Nov 2016 10:21:47 +0000 (envelope-from <bounce-20_HTML-215009-5489-7208679-430@bounce.morning7.theskimm.com>)\"],\"x-received\":[\"by 10.55.47.193 with SMTP id v184mr2167122qkh.259.1478082112295; Wed, 02 Nov 2016 03:21:52 -0700 (PDT)\"],\"return-path\":[\"<bounce-20_HTML-215009-5489-7208679-430@bounce.morning7.theskimm.com>\"],\"received-spf\":[\"pass (google.com: domain of bounce-20_html-215009-5489-7208679-430@bounce.morning7.theskimm.com designates 136.147.177.13 as permitted sender) client-ip=136.147.177.13;\"],\"authentication-results\":[\"mx.google.com; dkim=pass header.i=@morning7.theskimm.com; spf=pass (google.com: domain of bounce-20_html-215009-5489-7208679-430@bounce.morning7.theskimm.com designates 136.147.177.13 as permitted sender) smtp.mailfrom=bounce-20_HTML-215009-5489-7208679-430@bounce.morning7.theskimm.com; dmarc=pass (p=NONE dis=NONE) header.from=theskimm.com\"],\"dkim-signature\":[\"v=1; a=rsa-sha1; c=relaxed/relaxed; s=200608; d=morning7.theskimm.com; h=From:To:Subject:Date:List-Unsubscribe:MIME-Version:Reply-To:List-ID:Message-ID:Content-Type; i=dailyskimm@morning7.theskimm.com; bh=TfZNUfri1El8i4AUulbXxJgLUCA=; b=H6ZRxb2e2tmobb63CIcQ1ecsfv+X+Ky/G0EW0Kct6kvNmLP/yASdttr9bcgRbUMO45Su7bytynrE U+kbevaHw5nbatKKWQFP7JxGMjzCtQGM6LzOjGV8qCSu7XwdQ0QgUCJo0V4eo7Iu6bafR9h2WJST ct12e4elnzHFZLnF7GM=\"],\"from\":[\"\\\"theSkimm\\\" <dailyskimm@morning7.theskimm.com>\"],\"to\":[\"<christine@spang.cc>\"],\"subject\":[\"Daily Skimm: Hey batter batter\"],\"date\":[\"Wed, 02 Nov 2016 04:21:46 -0600\"],\"list-unsubscribe\":[\"<mailto:leave-fd8916701a3c402029-fece167170640474-ff2511747d6d-fe881372756c027a7c-ff68107375@leave.morning7.theskimm.com>\"],\"mime-version\":[\"1.0\"],\"reply-to\":[\"\\\"theSkimm\\\" <reply-ff2511747d6d-20_HTML-215009-7208679-430@morning7.theskimm.com>\"],\"list-id\":[\"<7208679_5489.xt.local>\"],\"x-csa-complaints\":[\"whitelistcomplaints@eco.de\"],\"x-job\":[\"7208679_5489\"],\"message-id\":[\"<fdb2713e-91fd-4d20-8d44-c8166229c274@xtgap4s7mta4374.xt.local>\"],\"content-type\":[\"multipart/alternative; boundary=\\\"Aw0OfBZoDhFa=_?:\\\"\"],\"x-gm-thrid\":\"1549881429115204149\",\"x-gm-msgid\":\"1549881429115204149\",\"x-gm-labels\":[\"\\\\Important\"]},\"headerMessageId\":\"<fdb2713e-91fd-4d20-8d44-c8166229c274@xtgap4s7mta4374.xt.local>\",\"gMsgId\":\"1549881429115204149\",\"subject\":\"Daily Skimm: Hey batter batter\",\"id\":\"a77f392abab72c219e51370d4d4c9a5f92c51bb7cc883fd8a86346057989a009\",\"folderImapXGMLabels\":\"[\\\"\\\\\\\\Important\\\"]\"}}\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/finimize.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n<!doctype html>\r\n<html style=\"margin: 0 !important;\">\r\n<head>\r\n<!--\r\n    FINIMIZE 2016.4.3 EMAIL TEMPLATE\r\n    =====================================\r\n -->\r\n<title>🔥 Black gold is on fire</title>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\">\r\n<!--[if !mso]><!-->\r\n<meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\r\n<!--<![endif]-->\r\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\r\n<meta charset=\"utf-8\">\r\n<!--Collapse Border in Old Outlook -->\r\n<!--[if (gte mso 9)|(IE)]>\r\n<style type=\"text/css\">\r\ntable {border-collapse: collapse !important;}\r\n</style>\r\n<![endif]-->\r\n<style type=\"text/css\">\r\n\t\tbody,table,td,a{\r\n\t\t\t-webkit-text-size-adjust:100%;\r\n\t\t\t-ms-text-size-adjust:100%;\r\n\t\t}\r\n\t\ttable,td{\r\n\t\t\tmso-table-lspace:0pt;\r\n\t\t\tmso-table-rspace:0pt;\r\n\t\t}\r\n\t\timg{\r\n\t\t\t-ms-interpolation-mode:bicubic;\r\n\t\t}\r\n\t\thtml{\r\n\t\t\tmargin:0 !important;\r\n\t\t}\r\n\t\ttable{\r\n\t\t\tborder-collapse:separate;\r\n\t\t}\r\n\t\ta,a:link,a:visited{\r\n\t\t\tcolor:#4EB9E7;\r\n\t\t}\r\n\t\ta:hover{\r\n\t\t\ttext-decoration:underline;\r\n\t\t}\r\n\t\th2,h2 a,h2 a:visited,h3,h3 a,h3 a:visited,h4,h5,h6,.t_cht{\r\n\t\t\tcolor:#1E2225;\r\n\t\t}\r\n\t\t.ExternalClass p,.ExternalClass span,.ExternalClass font,.ExternalClass td{\r\n\t\t\tline-height:100%;\r\n\t\t}\r\n\t\t.ExternalClass{\r\n\t\t\twidth:100%;\r\n\t\t}\r\n\t\timg{\r\n\t\t\tborder:0;\r\n\t\t\theight:auto;\r\n\t\t\tline-height:100%;\r\n\t\t\toutline:none;\r\n\t\t\ttext-decoration:none;\r\n\t\t}\r\n\t\tcolor:inherit importanttext-decoration:none importantfont-size:inherit importantfont-family:inherit importantfont-weight:inherit importantline-height:inherit importanta{\r\n\t\t\tcolor:#4EB9E7 !important;\r\n\t\t\tfont-weight:normal;\r\n\t\t\ttext-decoration:underline;\r\n\t\t}\r\n\t\ta.list-recent{\r\n\t\t\tcolor:#4EB9E7 !important;\r\n\t\t\tfont-weight:normal;\r\n\t\t\ttext-decoration:underline;\r\n\t\t}\r\n\t\t#outlook a{\r\n\t\t\tpadding:0;\r\n\t\t\tcolor:#4EB9E7 !important;\r\n\t\t}\r\n\t\t.hoverlink:hover{\r\n\t\t\ttext-decoration:underline !important;\r\n\t\t}\r\n\t\t#footer a{\r\n\t\t\tcolor:#999999 !important;\r\n\t\t}\r\n\t\t#footer a:hover{\r\n\t\t\tcolor:#ffffff !important;\r\n\t\t}\r\n\t\t.greenbutton{\r\n\t\t\ttransition:0.23s linear;\r\n\t\t\tcolor:#ffffff!important;\r\n\t\t\ttext-decoration:none !important;\r\n\t\t}\r\n\t\t.bluebutton{\r\n\t\t\ttransition:0.23s linear;\r\n\t\t\tcolor:#ffffff!important;\r\n\t\t\ttext-decoration:none !important;\r\n\t\t}\r\n\t\t.greenbutton:hover{\r\n\t\t\tbackground-color:#81C96B !important;\r\n\t\t\tbackground:#81C96B !important;\r\n\t\t}\r\n\t\t.bluebutton:hover{\r\n\t\t\tbackground-color:#7BCBED !important;\r\n\t\t\tbackground:#7BCBED !important;\r\n\t\t}\r\n\t\t.qatitle{\r\n\t\t\tcolor:#4EB9E7!important;\r\n\t\t}\r\n\t\t.dbluetext{\r\n\t\t\tcolor:#265C73 !important;\r\n\t\t}\r\n\t\t.whitetext{\r\n\t\t\tcolor:#ffffff !important;\r\n\t\t}\r\n\t@media screen and (max-width: 620px){\r\n\t\t.img-max{\r\n\t\t\twidth:100% !important;\r\n\t\t\tmax-width:100% !important;\r\n\t\t\theight:auto !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 620px){\r\n\t\t.max-width{\r\n\t\t\tmax-width:100% !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 620px){\r\n\t\t.mobile-wrapper{\r\n\t\t\twidth:85% !important;\r\n\t\t\tmax-width:85% !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 620px){\r\n\t\t.mobile-padding{\r\n\t\t\tpadding-left:5% !important;\r\n\t\t\tpadding-right:5% !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 550px){\r\n\t\t.qa_avatar{\r\n\t\t\twidth:37px !important;\r\n\t\t\theight:37px !important;\r\n\t\t\tdisplay:block;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 550px){\r\n\t\t.qa_avatar_holder{\r\n\t\t\twidth:37px !important;\r\n\t\t\tdisplay:block;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 550px){\r\n\t\th2{\r\n\t\t\tfont-size:24px !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 550px){\r\n\t\t.finimizequotetitle{\r\n\t\t\tfont-size:14px !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 550px){\r\n\t\t.whatsappshare{\r\n\t\t\tdisplay:inline-block !important;\r\n\t\t\twidth:32px !important;\r\n\t\t\theight:30px !important;\r\n\t\t\toverflow:visible !important;\r\n\t\t\tfloat:none !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 550px){\r\n\t\t.whatsappshareimage{\r\n\t\t\twidth:32px !important;\r\n\t\t\toverflow:visible !important;\r\n\t\t\tfloat:none !important;\r\n\t\t\tdisplay:block !important;\r\n\t\t\theight:30px !important;\r\n\t\t}\r\n\r\n}\t@media screen and (max-width: 550px){\r\n\t\t.emailshare{\r\n\t\t\tdisplay:none !important;\r\n\t\t}\r\n\r\n}\t@media screen and (min-width: 500px){\r\n\t\t.sharelinebreak{\r\n\t\t\tdisplay:none;\r\n\t\t}\r\n\r\n}\t@media screen and (min-width: 500px){\r\n\t\t.sharethis{\r\n\t\t\tpadding:0px !important;\r\n\t\t}\r\n\r\n}\t@media screen and (min-width: 500px){\r\n\t\t.greenbutton{\r\n\t\t\tfont-size:14px !important;\r\n\t\t\tpadding:14px 30px !important;\r\n\t\t}\r\n\r\n}\t@media screen and (min-width: 700px){\r\n\t\t.qa_avatar_holder{\r\n\t\t\twidth:65px !important;\r\n\t\t\tdisplay:block;\r\n\t\t}\r\n\r\n}\t@media screen and (min-width: 700px){\r\n\t\t.qa_avatar{\r\n\t\t\twidth:65px !important;\r\n\t\t\theight:65px !important;\r\n\t\t\tdisplay:block;\r\n\t\t}\r\n\r\n}\t@media screen{\r\n\t\tfont-familyOpen Sansfont-style:normalfont-weight:400,600,700src:local('Open Sans'),localOpenSans,urlhttpfonts.gstatic.comsopensansv10cJZKeOuBrn4kERxqtaUH3bO3LdcAZYWl9Si6vvxL-qU.woff formatwoffdiv[style*=margin: 16px 0;]{\r\n\t\t\tmargin:0 !important;\r\n\t\t}\r\n\r\n}</style><!--[if !mso]><!--><link href=\"https://fonts.googleapis.com/css?family=Open+Sans:400,400i,700,700i\" rel=\"stylesheet\"><!--<![endif]--></head>\r\n<body style=\"margin: 0 !important;padding: 0;!important background-color: #ffffff;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" bgcolor=\"#ffffff\">\r\n\r\n<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n    <tr>\r\n        <td align=\"center\" valign=\"top\" width=\"100%\" bgcolor=\"#4EB9E7\" style=\"background: #4EB9E7;padding: 10px 15px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\" class=\"mobile-padding\">\r\n            <!--[if (gte mso 9)|(IE)]>\r\n            <table align=\"center\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"600\">\r\n            <tr>\r\n            <td align=\"center\" valign=\"top\" width=\"600\">\r\n            <![endif]-->\r\n<!-- Header -->\r\n            <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"max-width: 600px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                <tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0 0 5px 0;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <a href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=86f1950591&e=e39741c606\" target=\"_blank\" style=\"outline: none;border: ;none: ;text-decoration: none !important;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/f8d67519-4124-409c-ad98-c9cc77b98ed3.png\" alt=\"finimize.\" width=\"200\" height=\"50\" border=\"0\" style=\"display: block;color: #ffffff;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;font-size: 42px;-ms-interpolation-mode: bicubic;border: 0;height: auto;line-height: 100%;outline: none;text-decoration: none;\"></a>\r\n                    </td>\r\n                </tr>\r\n                <tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                          <h1 style=\"color: #ffffff; font-size: 14px; font-weight: 600; line-height: 20px; margin: 0; text-transform: capitalize;\">\r\n                         Finance for our generation<span style=\"display:none;color: #4eb9e7;\">.</span>\r\n                        </h1>\r\n                    </td>\r\n                </tr>\r\n            </table>\r\n            <!--[if (gte mso 9)|(IE)]>\r\n            </td>\r\n            </tr>\r\n            </table>\r\n            <![endif]-->\r\n        </td>\r\n    </tr>\r\n\r\n\r\n\r\n    <tr>\r\n        <td align=\"center\" height=\"100%\" valign=\"top\" width=\"100%\" bgcolor=\"#ffffff\" style=\"padding: 10px 15px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\" class=\"mobile-padding\">\r\n            <!--[if (gte mso 9)|(IE)]>\r\n            <table align=\"center\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"600\">\r\n            <tr>\r\n            <td align=\"center\" valign=\"top\" width=\"600\">\r\n            <![endif]-->\r\n\r\n<!--START RSS    -->\r\n\r\n\r\n\r\n\r\n                <!--Intro--><table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"max-width: 600px !important;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                <tr> <td align=\"center\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 25px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                        <tr>\r\n                                            <td align=\"center\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                                <p style=\"color: #56585B ; font-size: 16px; line-height: 30px; margin: 0;\">Hi Christine, here's the news you need to know for <strong>December 13th</strong>. Reading time&nbsp;is&nbsp;3:17&nbsp;minutes.</p>\r\n                                            </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                </td>\r\n                </tr><!--Cafe Box-->\r\n                <tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0 0 15px 0;font-family: Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                            <tr>\r\n                                <td align=\"center\" bgcolor=\"#ffffff\" style=\"border: 3px solid #4EB9E7;display: block;border-radius: 10px 10px 10px 10px;padding: 15px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                        <tr>\r\n                                            <td align=\"center\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\"><p style=\"color: #4EB9E7; font-size: 16px; line-height: 30px; margin: 0;\">☕&nbsp;&nbsp;Finimize'd over a Pingado at <a style=\"text-decoration: underline !important;color: #4eb9e7!important;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=eb5473c5da&e=e39741c606\">Sampa Coffee</a>, 75 Leather Lane, London, UK.</p></td>\r\n                                        </tr>\r\n                                    </table>\r\n                                    </td>\r\n                    </tr><tr>\r\n                <td mc:edit=\"promosource\" class=\"caferecommendedby\" style=\"padding-top:10px;padding-bottom:0px;padding-right:15px;padding-left:15px;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;mso-table-lspace:0pt;mso-table-rspace:0pt; font-size:12px; color: #999999; line-height: 16px !important; text-align: center; padding-left: 0; padding-right: 0;font-family:'Open Sans', Helvetica, Arial, 'Lucida Grande', sans-serif !important;color:#818488!important;\"></td>\r\n                </tr><tr>\r\n                    <td class=\"articleheader\" align=\"center\" valign=\"top\" style=\"padding: 23px 0;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-bottom: 0px solid #1E2225;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\"><h2 style=\"font-size: 26px; color: #1E2225; font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-weight: 700; margin: 0; padding-bottom: 7px;\">Oil Prices Keep Climbing</h2>                    </td>\r\n                </tr>\r\n                <tr>\r\n                 <td align=\"center\" bgcolor=\"#ffffff\" style=\"border-radius: 0px 0px 0 0;padding: 10px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\"><a href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=60c1f91501&e=e39741c606\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\"> <img src=\"https://www.finimize.com/wp/wp-content/uploads/2016/12/OPECDeal.gif\" border=\"0\" width=\"600\" class=\"img-max mcRssImage\" alt=\"Please Turn On Images\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\"></a></td></tr><tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 0px 0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\"><tr>\r\n                    <td class=\"articlebody\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                          <h3 style=\"font-size: 18px; color: #1E2225; font-weight: 700; margin: 0; padding-bottom: 12px;\">What’s Going On Here?</h3><p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">The price of oil keeps going up after eleven major oil producers announced they would join <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=5477c6b2ab&e=e39741c606\">OPEC</a> (a formal group of oil producing nations) <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=9c438d8171&e=e39741c606\">in cutting oil production</a> in 2017. <span style=\"background: #FFF8A1!important;\">The oil price hit its highest level since the summer of 2015! </span>(<a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=7ee2774224&e=e39741c606\">tweet this</a>)</p> </td>\r\n                    </tr><tr>\r\n                    <td class=\"articlebody\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                          <h3 style=\"font-size: 18px; color: #1E2225; font-weight: 700; margin: 0; padding-bottom: 12px;\">What Does This Mean?</h3><p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">Eleven major oil producers not affiliated with OPEC, including Russia and Mexico, have agreed to cut their collective oil production in 2017 (remember, less supply is positive for the price). Together OPEC and these eleven oil producing nations (in a rare show of cooperation) have agreed to produce 1.8 million fewer barrels of oil every day in 2017, representing almost 2% of the world’s daily oil production! <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=b834419dfc&e=e39741c606\">If they don’t cheat on the agreement</a>, this should be a big deal.</p> </td>\r\n                    </tr><tr>\r\n                    <td class=\"articlebody\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                          <h3 style=\"font-size: 18px; color: #1E2225; font-weight: 700; margin: 0; padding-bottom: 12px;\">Why Should I Care?</h3><p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\"><b>The bigger picture: </b><i>Higher oil prices will likely be a negative for most major economies – although the US could benefit.<br class=\"none\"></i>Britain and Europe have to <i>import </i>oil, so a higher oil price is usually bad news for their economies overall (as it means more money <i>leaves </i>the country). The US, however, is a big oil producer <i>– </i>so the higher price is more likely to give a boost to its economy (especially in oil-producing states like Texas). However, in both Europe and the US, a higher oil price means higher prices for gasoline – leaving less money in people’s pockets to spend on other things.\r\n<br class=\"none\"><br class=\"none\"><b>For markets</b>: <i>Energy companies are lovin’ it.<br class=\"none\"></i>Of course, companies that produce oil are usually pretty happy when the oil price goes up, and the spike in prices has benefitted the likes of <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=4e72f87a99&e=e39741c606\">BP</a> and <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=cc869d53c7&e=e39741c606\">Exxon</a>. The stock prices of US-focused oil drillers, like <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=45714841db&e=e39741c606\">EOG Resources</a> and <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=8a7ec485ea&e=e39741c606\">Anadarko</a>, have moved even higher than more internationally focused companies, since a higher oil price should lead to a significant pickup in US production. That’s because, over the past few weeks, the oil price has jumped above the “break-even” price of production for many US firms.</p> </td>\r\n                    </tr><!--Share Links-->\r\n                                         <tr>\r\n                                             <td style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                               <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\" style=\"max-width: 600px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                                    <tr>\r\n                                          <td class=\"sharelinks\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 20px 0px;width: 100%;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                            <div class=\"share-links\"><div style=\"box-sizing:border-box;position:relative;\">\r\n                                            <div style=\"padding:0;overflow:hidden;color:#56585B;font-family: Avenir Next,sans-serif;font-weight:600;font-size: 18px;text-transform: uppercase; line-height: 28px;text-align:left;\">\r\n                                              <span class=\"sharethis\" style=\"display:inline-block;margin-right:6px; color: #C1C6CD!important;padding-bottom:10px;\">SHARE THIS</span>\r\n                                              <br class=\"sharelinebreak\">\r\n                                                <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 5px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=644c129644&e=e39741c606\">\r\n                                            <img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/5ea3b68e-917d-439b-b1bd-ddb6fd37b6e6.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Twitter\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                                <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 3px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=f1c461d6cb&e=e39741c606\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/2ed1e3ad-2682-4e4d-9c5b-18eaaba264db.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Facebook Share\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                                <a class=\"emailshare\" style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 3px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"mailto:?Subject=Story%20in%20Finimize&Body=Saw%20this%20in%20Finimize%20today:%20Oil Prices Keep Climbing%20-%20https://www.finimize.com/wp/news/oil-prices-keep-climbing/\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/51da9cb0-92fd-48d9-895f-e741876c2f9d.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Email\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                                 <a class=\"whatsappshare\" style=\"vertical-align: bottom;margin: 0 3px 0;width: 0;height: 0;overflow: hidden;float: left;display: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"whatsapp://send?text=Saw this in Finimize: Oil Prices Keep Climbing - https://www.finimize.com/wp/news/oil-prices-keep-climbing/\" data-action=\"share/whatsapp/share\"><img class=\"whatsappshareimage mcRssImage\" src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/8c77567e-6750-45ab-b0c4-fd24bdc10200.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Whatsapp Share\" border=\"0\"></a>\r\n                                                        </div>\r\n                                            </div></div>\r\n                                          </td>\r\n                                        <!--QA Link-->\r\n                                        <td class=\"questionbutton\" align=\"right\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 0px;padding-bottom: 10px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                           <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                                    <tr>\r\n                                                       <td align=\"center\" style=\"font-family: Avenir Next,sans-serif;font-weight: 600;font-size: 16px;color: #ffffff !important;text-decoration: none;border-radius: 99px;background-color: #61BB46;padding: 0px;border: 0px solid #61BB46;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\" bgcolor=\"#61BB46\">\r\n                                                            <a class=\"greenbutton\" href=\"mailto:questions@finimize.com?Subject=Question%20on%20Oil Prices Keep Climbing&Body=Hello%20Finimize%2C%0A%0AI%20read%20your%20story%20today%3A%20https://www.finimize.com/wp/news/oil-prices-keep-climbing/%0A%0AThis%20is%20my%20question%3A%0A%0A%0ANote%3A%20Your%20question%20and%20our%20answer%20may%20be%20featured%20in%20a%20next%20edition.%20We%20like%20to%20share%20the%20love%20%3A%29%20If%20you%20wouldn%27t%20like%20that%2C%20please%20make%20note%20of%20it.\" target=\"_blank\" style=\"font-family: Avenir Next,sans-serif;font-weight: 600;font-size: 12px;color: #ffffff !important;text-decoration: none !important;border-radius: 99px;background-color: #61BB46;padding: 12px 14px;border: 1px solid #61BB46;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;transition: 0.23s linear;\"><span style=\"color:#ffffff;\">ASK&nbsp;US&nbsp;A&nbsp;QUESTION</span>\r\n</a>\r\n                                                 </td>\r\n                                                    </tr>\r\n                                                </table>\r\n                                            </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                        </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                </td>\r\n                            </tr>\r\n                       </table>\r\n                    </td>\r\n                </tr><tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n\r\n                    </td>\r\n                    </tr>\r\n                    <tr>\r\n                    <td class=\"articleheader\" align=\"center\" valign=\"top\" style=\"padding: 23px 0;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-bottom: 0px solid #1E2225;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\"><h2 style=\"font-size: 26px; color: #1E2225; font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-weight: 700; margin: 0; padding-bottom: 7px;\">CBS-Viacom Deal Is A No Go</h2>                    </td>\r\n                </tr>\r\n                <tr>\r\n                 <td align=\"center\" bgcolor=\"#ffffff\" style=\"border-radius: 0px 0px 0 0;padding: 10px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\"><a href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=39a6003b37&e=e39741c606\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\"> <img src=\"https://www.finimize.com/wp/wp-content/uploads/2016/12/Finimize_News_Image_2016_CBS.jpg\" border=\"0\" width=\"600\" class=\"img-max mcRssImage\" alt=\"Please Turn On Images\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\"></a></td></tr><tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 0px 0px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\"><tr>\r\n                    <td class=\"articlebody\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                          <h3 style=\"font-size: 18px; color: #1E2225; font-weight: 700; margin: 0; padding-bottom: 12px;\">What’s Going On Here?</h3><p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\"><a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=3a4ce81683&e=e39741c606\">Viacom</a> and <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=1e797251d0&e=e39741c606\">CBS</a> will <i>not</i> merge together (<a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=1a2d7aa71b&e=e39741c606\">again</a>) to make a media supergiant – and it means that Viacom will have to find its own path to growth as the traditional cable company struggles in the age of <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=62eb3c1c56&e=e39741c606\">Netflix</a> and <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=b73edcb0f2&e=e39741c606\">Hulu</a>.</p> </td>\r\n                    </tr><tr>\r\n                    <td class=\"articlebody\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                          <h3 style=\"font-size: 18px; color: #1E2225; font-weight: 700; margin: 0; padding-bottom: 12px;\">What Does This Mean?</h3><p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">Viacom separated from its then-parent company, CBS, in 2005 with the idea that the high-growth media company, anchored by well-loved channels like <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=178c847282&e=e39741c606\">Comedy Central</a> and <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=b25bd877e2&e=e39741c606\">MTV</a>, would thrive as a separate, more nimble entity. But just the opposite occurred: Viacom has floundered since the spinoff, while CBS has been among the best-performing major US media companies.\r\n<br class=\"none\"><br class=\"none\">Earlier this year, <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=264f10855d&e=e39741c606\">National Amusements</a>, the controlling shareholder in <i>both</i> companies, suggested that Viacom could be turned around by a merger with its former parent CBS. But <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=a9ed19c972&e=e39741c606\">Shari Redstone</a>, the President of National Amusements, backtracked on that plan on Monday, instead putting her hopes in the forward-looking vision of Viacom’s new CEO.</p> </td>\r\n                    </tr><tr>\r\n                    <td class=\"articlebody\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;mso-table-lspace: 0pt;mso-table-rspace: 0pt;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">\r\n                          <h3 style=\"font-size: 18px; color: #1E2225; font-weight: 700; margin: 0; padding-bottom: 12px;\">Why Should I Care?</h3><p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\"><b>For markets: </b><i>CBS didn’t seem enamored with the idea of joining forces with Viacom.<br class=\"none\"></i>Redstone may have given her public backing to Viacom’s new CEO, but markets aren’t really buying it. It seems more likely that CBS’s CEO, the venerable <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=c3fbce1b96&e=e39741c606\">Les Moonves</a>, <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=511fe468c3&e=e39741c606\">balked at letting troubled Viacom back into CBS’s fold</a>. Markets are hinting at the same idea: shares in CBS were flat on Monday, while Viacom’s were down almost 10% – clearly, investors are disappointed that the former golden child won’t be brought back under its parent’s roof.\r\n<br class=\"none\"><br class=\"none\"><b>The bigger picture: </b><i>A “controlling” shareholder can make the successful management of a large corporation difficult.<br class=\"none\"></i>Viacom (and to some extent CBS) have been marred in 2016 by <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=ee629c9981&e=e39741c606\">a public feud</a> between company executives and the company’s most important shareholder, National Amusements. While National Amusements doesn’t own the majority of the companies’ shares, it controls the voting rights (i.e. it is the “controlling shareholder”), which gives it a lot of control over corporate management (such as choosing the CEO). The conflict highlights the downside of shareholders relinquishing their voting rights (which <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=95f0e89c59&e=e39741c606\">recently took place at Facebook</a>).</p> </td>\r\n                    </tr><!--Share Links-->\r\n                                         <tr>\r\n                                             <td style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                               <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\" style=\"max-width: 600px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                                    <tr>\r\n                                          <td class=\"sharelinks\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 20px 0px;width: 100%;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                            <div class=\"share-links\"><div style=\"box-sizing:border-box;position:relative;\">\r\n                                            <div style=\"padding:0;overflow:hidden;color:#56585B;font-family: Avenir Next,sans-serif;font-weight:600;font-size: 18px;text-transform: uppercase; line-height: 28px;text-align:left;\">\r\n                                              <span class=\"sharethis\" style=\"display:inline-block;margin-right:6px; color: #C1C6CD!important;padding-bottom:10px;\">SHARE THIS</span>\r\n                                              <br class=\"sharelinebreak\">\r\n                                                <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 5px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=6cc199f3f3&e=e39741c606\">\r\n                                            <img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/5ea3b68e-917d-439b-b1bd-ddb6fd37b6e6.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Twitter\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                                <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 3px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=f333acb23a&e=e39741c606\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/2ed1e3ad-2682-4e4d-9c5b-18eaaba264db.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Facebook Share\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                                <a class=\"emailshare\" style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 3px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"mailto:?Subject=Story%20in%20Finimize&Body=Saw%20this%20in%20Finimize%20today:%20CBS-Viacom Deal Is A No Go%20-%20https://www.finimize.com/wp/news/cbs-viacom-deal-no-go/\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/51da9cb0-92fd-48d9-895f-e741876c2f9d.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Email\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                                 <a class=\"whatsappshare\" style=\"vertical-align: bottom;margin: 0 3px 0;width: 0;height: 0;overflow: hidden;float: left;display: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"whatsapp://send?text=Saw this in Finimize: CBS-Viacom Deal Is A No Go - https://www.finimize.com/wp/news/cbs-viacom-deal-no-go/\" data-action=\"share/whatsapp/share\"><img class=\"whatsappshareimage mcRssImage\" src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/8c77567e-6750-45ab-b0c4-fd24bdc10200.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Whatsapp Share\" border=\"0\"></a>\r\n                                                        </div>\r\n                                            </div></div>\r\n                                          </td>\r\n                                        <!--QA Link-->\r\n                                        <td class=\"questionbutton\" align=\"right\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 0px;padding-bottom: 10px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                           <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                                    <tr>\r\n                                                       <td align=\"center\" style=\"font-family: Avenir Next,sans-serif;font-weight: 600;font-size: 16px;color: #ffffff !important;text-decoration: none;border-radius: 99px;background-color: #61BB46;padding: 0px;border: 0px solid #61BB46;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\" bgcolor=\"#61BB46\">\r\n                                                            <a class=\"greenbutton\" href=\"mailto:questions@finimize.com?Subject=Question%20on%20CBS-Viacom Deal Is A No Go&Body=Hello%20Finimize%2C%0A%0AI%20read%20your%20story%20today%3A%20https://www.finimize.com/wp/news/cbs-viacom-deal-no-go/%0A%0AThis%20is%20my%20question%3A%0A%0A%0ANote%3A%20Your%20question%20and%20our%20answer%20may%20be%20featured%20in%20a%20next%20edition.%20We%20like%20to%20share%20the%20love%20%3A%29%20If%20you%20wouldn%27t%20like%20that%2C%20please%20make%20note%20of%20it.\" target=\"_blank\" style=\"font-family: Avenir Next,sans-serif;font-weight: 600;font-size: 12px;color: #ffffff !important;text-decoration: none !important;border-radius: 99px;background-color: #61BB46;padding: 12px 14px;border: 1px solid #61BB46;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;transition: 0.23s linear;\"><span style=\"color:#ffffff;\">ASK&nbsp;US&nbsp;A&nbsp;QUESTION</span>\r\n</a>\r\n                                                 </td>\r\n                                                    </tr>\r\n                                                </table>\r\n                                            </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                        </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                </td>\r\n                            </tr>\r\n                       </table>\r\n                    </td>\r\n                </tr> <!--Quote Of The Day-->\r\n                <tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0 0 15px 0;font-family: Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#4EB9E7\" style=\"border: 3px solid #4EB9E7;display: block;border-radius: 10px 10px 10px 10px;padding: 23px;color: #ffffff;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"color: #ffffff !important;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                        <tr>\r\n                                            <td align=\"left\" style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;color: #ffffff;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                                <h2 class=\"finimizequotetitle dbluetext\" style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-size: 16px; color: #265C73 !important; margin: 0; padding-bottom: 10px;\">#FINIMIZEQUOTE</h2>\r\n                                     <p class=\"whitetext\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif; color: #ffffff !important; font-size: 20px; font-weight: 600; line-height: 32px; margin: 0;\">\r\n                                                 “It’s easy to be miserable. Being happy is tougher — and cooler.”</p>\r\n                                              <p class=\"whitetext\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif; color: #ffffff !important; font-size: 16px !important; line-height: 28px; margin: 5px 0;\">- Thom Yorke (an English musician best known as the singer and songwriter of Radiohead)</p></td>\r\n                  </tr>\r\n                  <tr>\r\n                    <td class=\"dbluetext\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 0px 0 0 0;color: #265C73 !important;font-size: 12px;line-height: 20px;margin: 0;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">Want to see your name here? Suggest a quote by tweeting <a href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=cd3d46effe&e=e39741c606\" style=\"color: #265C73 !important;text-decoration: underline;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" target=\"_blank\">@finimize</a>\r\n                    </td>\r\n                  </tr>\r\n                </table>\r\n              </td>\r\n            </tr>\r\n        </table>\r\n    </td>\r\n                </tr><tr>\r\n                      <td align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 20px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <div class=\"share-links\"><div style=\"box-sizing:border-box;position:relative;\">\r\n                        <div style=\"padding:0;overflow:hidden;color:#56585B;font-family: Avenir Next,sans-serif;font-weight:600;font-size: 18px;text-transform: uppercase; line-height: 28px;text-align:left;\">\r\n                          <span style=\"display:inline-block;margin-right:6px; color: #C1C6CD!important;\">SHARE THIS</span>\r\n                            <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 3px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=11ee319ddb&e=e39741c606\">\r\n                        <img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/5ea3b68e-917d-439b-b1bd-ddb6fd37b6e6.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Twitter\" border=\"0\" class=\"mcRssImage\"></a>\r\n                            <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 3px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=5a90f1e643&e=e39741c606\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/2ed1e3ad-2682-4e4d-9c5b-18eaaba264db.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Facebook Share\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                       <a class=\"emailshare\" style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 3px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"mailto:?Subject=Finimize%20Quote&Body=“It’s easy to be miserable. Being happy is tougher — and cooler.”%20-%20Thom Yorke\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/51da9cb0-92fd-48d9-895f-e741876c2f9d.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Whatsapp Share\" border=\"0\" class=\"mcRssImage\"></a>\r\n                       <a class=\"whatsappshare\" style=\"vertical-align: bottom;margin: 0 3px 0;width: 0;height: 0;overflow: hidden;float: left;display: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" href=\"whatsapp://send?text=Today's Finimize quote: %E2%80%9CIt%E2%80%99s+easy+to+be+miserable.+Being+happy+is+tougher+%E2%80%94+and+cooler.%E2%80%9D\" data-action=\"share/whatsapp/share\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/8c77567e-6750-45ab-b0c4-fd24bdc10200.png\" class=\"whatsappshareimage mcRssImage\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" width=\"32\" alt=\"Whatsapp Share\" border=\"0\"></a>\r\n                                    </div>\r\n                        </div></div>\r\n                      </td>\r\n                    </tr>\r\n                <!--End Quote--><tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 25px 0;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-bottom: 3px solid #4EB9E7;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n\r\n                                    <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"100%\" style=\"max-width: 600px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                                    <tr>\r\n                                                        <td align=\"left\" bgcolor=\"#ffffff\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <h2 class=\"qatitle\" style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-size: 26px; color: #4EB9E7 !important; font-weight: 700; margin: 0; padding-bottom: 7px;\">Q&amp;A</h2>\r\n                                                        </td>\r\n                                                <td align=\"right\" bgcolor=\"#ffffff\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <a class=\"hoverlink\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=0b14f31246&e=e39741c606\" style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;font-size: 14px;color: #4EB9E7 !important;text-decoration: none;font-weight: 700;margin: 0;padding-bottom: 7px;text-align: right;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">RE: The ECB’s Big Surprise!</a>\r\n                                                        </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                </td>\r\n                            </tr>\r\n                            <!-- question row-->\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 0px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                        <tr>\r\n                                            <td align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                                <p style=\"font-size: 16px; color: #56585B; font-weight: 700; margin: 0; padding-bottom: 12px;\">Nepomuk asked:</p>\r\n                                                <p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">\r\n“Is it possible that the European Central Bank (ECB) will run out of bonds to buy?”<span></span></p>\r\n                                            </td>\r\n                                        </tr>\r\n\r\n                                    </table>\r\n                                </td>\r\n                            </tr>\r\n                            <!-- answer row-->\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 20px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                        <tr>\r\n                                            <td valign=\"top\" align=\"left\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 3px 0px;padding-right: 12px;vertical-align: top;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                                   <div class=\"share-links qa_avatar_holder\"><div style=\"box-sizing:border-box;position:relative;\">\r\n                                            <div class=\"\" style=\" padding:0;overflow:hidden;color:#56585B;font-family: Avenir Next,sans-serif;font-weight:600;font-size: 18px;text-transform: uppercase; line-height: 28px;text-align:left;\"> <a class=\"qa_avatar\" href=\"#\" style=\"width: 42px;height: 42px;vertical-align: bottom;margin: 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\"><img class=\"qa_avatar mcRssImage\" src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/536c42da-3878-456d-b028-499d70ce79e3.png\" alt=\"💬\" width=\"42\" border=\"0\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\"></a>\r\n                                                  </div>\r\n                                            </div></div>\r\n                                            </td>\r\n                                           <td align=\"left\" bgcolor=\"#F2F4F6\" style=\"border: 3px solid #F2F4F6;display: block;border-radius: 12px 12px 12px 12px;padding: 3px 3px 3px 12px;margin-right: 1px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                                        <tr>\r\n                                                            <td align=\"left\" style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n\r\n                                                                <p style=\"font-family: Open Sans, Helvetica, Arial, sans-serif; color: #56585B !important; font-size: 16px; line-height: 30px; margin: 5px 0;\">\r\n                                                              “In theory, yes the ECB could run out of bonds to buy – but governments could also issue more bonds, so the available supply could easily increase. Additionally, just last week <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=d364f9e86d&e=e39741c606\">the ECB removed a previous limit on the price it would pay for bonds</a> (based on what <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=a0a9dfbc4e&e=e39741c606\"><i>yield</i></a> those bonds are offering). This makes it even less likely that the ECB will run out of available bonds to buy.  A restriction the ECB could run up against is one limiting the amount of bonds it can buy from each country (which is based, loosely, on the size of the country). For example, it’s already close to that limit with Portuguese bonds. However, the ECB could conceivably change this rule as well. Overall, while it is possible for the ECB to run out of bonds to purchase, it seems unlikely right now.”</p>\r\n                                                            </td>\r\n                                                        </tr>\r\n                                                    </table>\r\n                                                </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                </td>\r\n                            </tr>\r\n                        </table>\r\n                    </td>\r\n                </tr>\r\n\r\n        <!--End Question Answer--> <tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0px;padding-bottom: 20px;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;border-top: 1px solid #D9DBDD;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                            <!-- content row-->\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 0px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                        <tr>\r\n                                            <td align=\"center\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;style=: ;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\" color:=\"\" #56585b;=\"\" font-size:=\"\" 16px;=\"\" text-align:center;font-size:=\"\" line-height:=\"\" 30px;=\"\" margin:=\"\" 0;\"=\"\">\r\n                                                     <div style=\"padding:20px 0px;overflow:hidden;color:#56585B;font-family: Avenir Next,sans-serif;font-weight:600;font-size: 16px;text-transform: uppercase; line-height: 30px;text-align:center;\">\r\n                                                <!--<a href=\"#\" target=\"_blank\" style=\"width:64px;height:28px;vertical-align:bottom;margin: 0px 0;display:inline-block;\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/f99971a1-64ca-4dcd-a855-788edd7818e6.png\" style=\"padding-top:10px; padding-bottom:20px;font-size:36px; display:block;vertical-align:middle;\" alt=\"👓\" width=\"64\" height=\"28\" border=\"0\"></a>-->\r\n                                                </div>\r\n                                            <h3 style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-size: 18px; color: #1e2225; font-weight: bold; margin: 0; padding-bottom: 12px;\">WHAT WE'RE READING</h3>\r\n<span style=\"font-family: Open Sans, Helvetica, Arial, sans-serif; text-align: center; color: #56585b; font-size: 16px; line-height: 30px; margin: 0;\">A look at how private equity amasses wealth by taking a deep dive into Hostess, the maker of Twinkies: <a style=\"color: #4EB9E7!important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=ed79269a81&e=e39741c606\">Read More</a></span></td>\r\n                                        </tr>\r\n                                    </table>\r\n                                </td>\r\n                            </tr>\r\n                        </table>\r\n                    </td>\r\n                </tr>\r\n\r\n        <!--End Reading / Promo --><tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0px;padding-bottom: 20px;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;border-top: 1px solid #D9DBDD;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                            <!-- content row-->\r\n                            <tr>\r\n                                <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 0px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                    <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                        <tr>\r\n                                            <td align=\"center\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 0px;text-align: center;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n     <div class=\"share-links\"><div style=\"box-sizing:border-box;position:relative;\">\r\n                                            <div style=\"padding:20px 0px;overflow:hidden;color:#56585B;font-family: Avenir Next,sans-serif;font-weight:600;font-size: 16px;text-transform: uppercase; line-height: 30px;text-align:center;\">\r\n                                                <a href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=65c3de1e36&e=e39741c606\" target=\"_blank\" style=\"width: 51px;height: 47px;vertical-align: bottom;margin: 0px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/1e62efc6-e92b-4c45-8e2c-df3b24036405.png\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" alt=\"💙\" width=\"51\" border=\"0\" class=\"mcRssImage\"></a>\r\n                                                </div>\r\n                                            </div></div>\r\n                                                <h3 style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-size: 18px; color: #1E2225; font-weight: 700; margin: 0; padding-bottom: 12px;\">SHARE FINIMIZE</h3>\r\n                                                <p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">\r\nGet credit for sharing Finimize with your friends and unlock <em>The Weekly Review</em>, Finimize Swag, and other perks as a Finimize Insider! </p>\r\n                                            </td>\r\n                                        </tr>\r\n                                        <tr>\r\n                                            <td align=\"center\" style=\"padding: 0px;padding-bottom: 10px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                                <table border=\"0\" cellspacing=\"0\" cellpadding=\"0\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                                        <tr>\r\n                                                          <td align=\"center\" style=\"font-family: Avenir Next,sans-serif;font-weight: 600;font-size: 16px;color: #ffffff !important;text-decoration: none;border-radius: 99px;background-color: #4eb9e7;padding: 0px;border: 1px solid #4eb9e7;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\" bgcolor=\"#4eb9e7\">\r\n                                                                <a class=\"bluebutton\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=4ea7ff5c9a&e=e39741c606\" target=\"_blank\" style=\"font-family: Avenir Next,sans-serif;font-weight: 600;font-size: 14px;color: #ffffff !important;text-decoration: none !important;border-radius: 99px;background-color: #4eb9e7;padding: 14px 24px;border: 1px solid #4eb9e7;display: block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;transition: 0.23s linear;\"><span style=\"color:#ffffff;\">INVITE FRIENDS, GET REWARDS</span></a>\r\n                                                                  </td>\r\n                                                              </tr>\r\n                                                              <tr>\r\n                                                              <td style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                                                <a class=\"nolink\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=db7cbc10ee&e=e39741c606\" style=\"text-decoration: none !important;outline: none !important;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\">\r\n                                                            <img border=\"0\" width=\"600\" class=\"img-max mcRssImage\" alt=\"Insider Progress Bar\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\" src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/8ce284cd-a5b9-4339-8b2d-de9e46c2526f.png\"></a>\r\n                                                              </td>\r\n                                                            </tr>\r\n                                                </table>\r\n                                            </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                </td>\r\n                            </tr>\r\n                        </table>\r\n                    </td>\r\n                </tr>\r\n\r\n        <!--End Share Box --><tr>\r\n            <td align=\"center\" valign=\"top\" style=\"padding-top: 20px;padding-bottom: 20px;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif;border-top: 1px solid #D9DBDD;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                    <!-- content row-->\r\n                    <tr>\r\n                        <td align=\"left\" bgcolor=\"#ffffff\" style=\"border-radius: 0 0 3px 3px;padding: 0px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                            <table cellspacing=\"0\" cellpadding=\"0\" border=\"0\" width=\"100%\" style=\"-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                                <tr>\r\n                                    <td align=\"center\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 15px 1px;text-align: center;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                        <h4 style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-size: 16px; color: #56585B; font-weight: 600; margin: 0; padding-bottom: 12px;\">You’re reading the: 🌎  American Edition</h4>\r\n                                        <p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">\r\n💭&nbsp;&nbsp;Have some feedback or ideas?&nbsp;&nbsp; <a href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=f22af1294c&e=e39741c606\" target=\"_blank\" style=\"color: #4EB9E7 !important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\">Tell Us!</a> </p>\r\n                                            <p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">\r\n🔧&nbsp;&nbsp;Want to change your settings?&nbsp;&nbsp; <a href=\"http://finimize.us10.list-manage.com/profile?u=fd92d4d6912bf051aceebbc27&id=abf5b8a24b&e=e39741c606\" target=\"_blank\" style=\"color: #4EB9E7 !important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\">Change Them</a> </p><p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">\r\n🤖&nbsp;&nbsp;Time travel to the last issue?&nbsp;&nbsp; <a href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=eaf7259d83&e=e39741c606\" style=\"color: #4EB9E7 !important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\">Look Back</a> </p><br>\r\n<p style=\"color: #56585B; font-size: 16px; line-height: 30px; margin: 0;\">Email doesn't look right?&nbsp;&nbsp; <a href=\"http://us10.campaign-archive2.com/?u=fd92d4d6912bf051aceebbc27&id=548d9d57f1&e=e39741c606\" style=\"color: #4EB9E7 !important;text-decoration: none;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" class=\"hoverlink\">View It Online</a></p>        <p style=\"color: #818488; font-size: 12px; line-height: 20px; margin: 0;\"><br>\r\n<a target=\"_blank\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=5dd1ae69df&e=e39741c606\" style=\"width: 180px;height: 55px;vertical-align: middle;margin: 0px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #4EB9E7;\" class=\"na\">\r\n<img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/4b2d6915-5e8b-4c10-8e4c-08b40bb88de1.png\" border=\"0\" width=\"180\" class=\"img-max mcRssImage\" alt=\"Get The App Beta\" style=\"max-width: 100%;width: 100%;padding-bottom: 0;display: inline;vertical-align: bottom;border: 0;height: auto;outline: none;text-decoration: none;-ms-interpolation-mode: bicubic;line-height: 100%;\">\r\n</a>\r\n<br><br><em>Image Credits: Giphy.com  Gabriele Maltinti / Shutterstock.com </em></p>\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n                        </td>\r\n                    </tr>\r\n<!--End Settings -->\r\n<!--\r\n            ></table></td></tr></table></td></tr></table>></table></td></tr></table></td></tr></table>\r\n\r\n\r\n\r\n<!--END RSS  -->\r\n            <!--[if (gte mso 9)|(IE)]>\r\n            </td>\r\n            </tr>\r\n            </table>\r\n            <![endif]-->\r\n        </table></td>\r\n    </tr>\r\n<!--Footer-->\r\n    <tr>\r\n        <td id=\"footer\" align=\"center\" height=\"100%\" valign=\"top\" width=\"100%\" bgcolor=\"#1E2225\" style=\"padding: 20px 15px 40px 15px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n            <!--[if (gte mso 9)|(IE)]>\r\n            <table align=\"center\" border=\"0\" cellspacing=\"0\" cellpadding=\"0\" width=\"600\">\r\n            <tr>\r\n            <td align=\"center\" valign=\"top\" width=\"600\">\r\n            <![endif]-->\r\n            <table align=\"center\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"max-width: 600px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;border-collapse: separate;\">\r\n                <tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0 0 5px 0;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                <!-- <img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/f8d67519-4124-409c-ad98-c9cc77b98ed3.png\" alt=\"finimize.\" width=\"200\" height=\"50\" border=\"0\" style=\"display: block;color:#ffffff;font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-size: 42px;\">    -->                </td>\r\n                </tr>\r\n                <tr>\r\n                    <td align=\"center\" valign=\"top\" style=\"padding: 0;font-family: Open Sans, Helvetica, Arial, sans-serif;color: #999999;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                        <p style=\"font-size: 14px; line-height: 20px; color: #999999;\">\r\n                           <span style=\"color: #ffffff;\"> Friend sent this to you?</span> <a href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=43167ae550&e=e39741c606\" target=\"_blank\" style=\"color: #ffffff !important;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">Sign up for Finimize here &rarr;</a>\r\n                            <br><br>\r\n                            Curious how we pick our stories? <a target=\"_blank\" href=\"http://finimize.us10.list-manage.com/track/click?u=fd92d4d6912bf051aceebbc27&id=3f27204a9c&e=e39741c606\" style=\"color: #999999;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">Read our curation policy</a>.\r\n                          <br><br>\r\n\r\nAll content provided by the Finimize Ltd. is for informational and educational purposes only and is not meant to represent trade or investment recommendations.\r\n                            You signed up to this mailing list at <a href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=8142b3a6e0&e=e39741c606\" style=\"color: #999999;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\">finimize.com</a> or through one of our partners.\r\n<br><br>\r\n                              Finimize Ltd. | 6th Floor, <br>2 Grand Canal Square, Dublin, Ireland.\r\n                          <br><br>\r\n                                                        If you want to unsubscribe from <em>all</em> daily emails, <a href=\"http://finimize.us10.list-manage.com/unsubscribe?u=fd92d4d6912bf051aceebbc27&id=abf5b8a24b&e=e39741c606&c=548d9d57f1\" style=\"color: #999999;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;\" target=\"_blank\">click here</a> :(\r\n\r\n                                              <br><br>  Copyright © 2016 Finimize, All rights reserved.\r\n\r\n\r\n\r\n                        </p>\r\n                    </td>\r\n                </tr>\r\n                <tr>\r\n                                     <td align=\"center\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 10px 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\">\r\n                                            <div class=\"footer-social-links\"><div style=\"box-sizing:border-box;position:relative;\">\r\n                                            <div style=\"padding:0;overflow:hidden;color:#ffffff;font-family: Avenir Next,sans-serif;font-weight:600;font-size: 18px;text-transform: uppercase; line-height: 28px;text-align:center;\">\r\n                                                <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 5px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #999999 !important;\" href=\"http://finimize.us10.list-manage2.com/track/click?u=fd92d4d6912bf051aceebbc27&id=bae5979799&e=e39741c606\" target=\"_blank\">\r\n                                            <img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/5ea3b68e-917d-439b-b1bd-ddb6fd37b6e6.png\" style=\"display: block;vertical-align: middle;padding-top: 0px;margin: 0 auto !important;-ms-interpolation-mode: bicubic;border: 0;height: auto;line-height: 100%;outline: none;text-decoration: none;\" width=\"32\" height=\"30\" alt=\"Twitter\"></a>\r\n                                                <a style=\"width: 32px;height: 30px;vertical-align: bottom;margin: 0 5px 0;display: inline-block;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;color: #999999 !important;\" href=\"http://finimize.us10.list-manage1.com/track/click?u=fd92d4d6912bf051aceebbc27&id=1cdbb3b4c8&e=e39741c606\" target=\"_blank\"><img src=\"https://gallery.mailchimp.com/fd92d4d6912bf051aceebbc27/images/2ed1e3ad-2682-4e4d-9c5b-18eaaba264db.png\" style=\"display: block;vertical-align: middle;padding-top: 0px;margin: 0px auto 0 !important;-ms-interpolation-mode: bicubic;border: 0;height: auto;line-height: 100%;outline: none;text-decoration: none;\" width=\"32\" height=\"30\" alt=\"Facebook\"></a>\r\n\r\n                                                        </div>\r\n                                            </div></div>\r\n\r\n                                          </td></tr>\r\n                <tr>\r\n                <td align=\"center\" style=\"font-family: Open Sans, Helvetica, Arial, sans-serif;padding: 0px;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;mso-table-lspace: 0pt;mso-table-rspace: 0pt;\"><br>\r\n                                       <p style=\"font-family: Avenir Next, Avenir, Open Sans, Helvetica, Arial, sans-serif; font-size: 14px; color: #999999; font-weight: 700; margin: 0; padding-bottom: 12px;\">You stay classy, Christine 😉</p>\r\n                    </td>\r\n                </tr>\r\n            </table>\r\n            <!--[if (gte mso 9)|(IE)]>\r\n            </td>\r\n            </tr>\r\n            </table>\r\n            <![endif]-->\r\n        </td>\r\n    </tr>\r\n</table>\r\n\r\n</td></tr></table></td></tr></table><img src=\"http://finimize.us10.list-manage.com/track/open.php?u=fd92d4d6912bf051aceebbc27&id=548d9d57f1&e=e39741c606\" height=\"1\" width=\"1\"></body>\r\n</html>\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/finimize.txt",
    "content": "Finance for our generation. Hi Christine, here's the news you need to know for December 13th. Reading\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/fittymi.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\" dir=\"ltr\">\r\n    <head>\r\n        <title>Fitty Mi Supper Club</title>\r\n        <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n        <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\r\n        <meta name=\"description\" content=\"Fitty Mi Supper Club Email Forms\">\r\n        <meta name=\"keywords\" content=\"Fitty Mi Supper Club\">\r\n        \r\n    <style type=\"text/css\">\r\n\t\tbody{\r\n\t\t\tfont:14px/20px 'Helvetica', Arial, sans-serif;\r\n\t\t\tmargin:0;\r\n\t\t\tpadding:75px 0 0 0;\r\n\t\t\ttext-align:center;\r\n\t\t\t-webkit-text-size-adjust:none;\r\n\t\t}\r\n\t\tp{\r\n\t\t\tpadding:0 0 10px 0;\r\n\t\t}\r\n\t\th1 img{\r\n\t\t\tmax-width:100%;\r\n\t\t\theight:auto !important;\r\n\t\t\tvertical-align:bottom;\r\n\t\t}\r\n\t\th2{\r\n\t\t\tfont-size:22px;\r\n\t\t\tline-height:28px;\r\n\t\t\tmargin:0 0 12px 0;\r\n\t\t}\r\n\t\th3{\r\n\t\t\tmargin:0 0 12px 0;\r\n\t\t}\r\n\t\t.headerBar{\r\n\t\t\tbackground:none;\r\n\t\t\tpadding:0;\r\n\t\t\tborder:none;\r\n\t\t}\r\n\t\t.wrapper{\r\n\t\t\twidth:600px;\r\n\t\t\tmargin:0 auto 10px auto;\r\n\t\t\ttext-align:left;\r\n\t\t}\r\n\t\tinput.button{\r\n\t\t\tborder:none !important;\r\n\t\t}\r\n\t\t.button{\r\n\t\t\tdisplay:inline-block;\r\n\t\t\tfont-weight:500;\r\n\t\t\tfont-size:16px;\r\n\t\t\tline-height:42px;\r\n\t\t\tfont-family:'Helvetica', Arial, sans-serif;\r\n\t\t\twidth:auto;\r\n\t\t\twhite-space:nowrap;\r\n\t\t\theight:42px;\r\n\t\t\tmargin:12px 5px 12px 0;\r\n\t\t\tpadding:0 22px;\r\n\t\t\ttext-decoration:none;\r\n\t\t\ttext-align:center;\r\n\t\t\tcursor:pointer;\r\n\t\t\tborder:0;\r\n\t\t\tborder-radius:3px;\r\n\t\t\tvertical-align:top;\r\n\t\t}\r\n\t\t.button span{\r\n\t\t\tdisplay:inline;\r\n\t\t\tfont-family:'Helvetica', Arial, sans-serif;\r\n\t\t\ttext-decoration:none;\r\n\t\t\tfont-weight:500;\r\n\t\t\tfont-style:normal;\r\n\t\t\tfont-size:16px;\r\n\t\t\tline-height:42px;\r\n\t\t\tcursor:pointer;\r\n\t\t\tborder:none;\r\n\t\t}\r\n\t\t.rounded6{\r\n\t\t\tborder-radius:6px;\r\n\t\t}\r\n\t\t.poweredWrapper{\r\n\t\t\tpadding:20px 0;\r\n\t\t\twidth:560px;\r\n\t\t\tmargin:0 auto;\r\n\t\t}\r\n\t\t.poweredBy{\r\n\t\t\tdisplay:block;\r\n\t\t}\r\n\t\tspan.or{\r\n\t\t\tdisplay:inline-block;\r\n\t\t\theight:32px;\r\n\t\t\tline-height:32px;\r\n\t\t\tpadding:0 5px;\r\n\t\t\tmargin:5px 5px 0 0;\r\n\t\t}\r\n\t\t.clear{\r\n\t\t\tclear:both;\r\n\t\t}\r\n\t\t.profile-list{\r\n\t\t\tdisplay:block;\r\n\t\t\tmargin:15px 20px;\r\n\t\t\tpadding:0;\r\n\t\t\tlist-style:none;\r\n\t\t\tborder-top:1px solid #eee;\r\n\t\t}\r\n\t\t.profile-list li{\r\n\t\t\tdisplay:block;\r\n\t\t\tmargin:0;\r\n\t\t\tpadding:5px 0;\r\n\t\t\tborder-bottom:1px solid #eee;\r\n\t\t}\r\n\t\thtml[dir=rtl] .wrapper,html[dir=rtl] .container,html[dir=rtl] label{\r\n\t\t\ttext-align:right !important;\r\n\t\t}\r\n\t\thtml[dir=rtl] ul.interestgroup_field label{\r\n\t\t\tpadding:0;\r\n\t\t}\r\n\t\thtml[dir=rtl] ul.interestgroup_field input{\r\n\t\t\tmargin-left:5px;\r\n\t\t}\r\n\t\thtml[dir=rtl] .hidden-from-view{\r\n\t\t\tright:-5000px;\r\n\t\t\tleft:auto;\r\n\t\t}\r\n\t\tbody,#bodyTable{\r\n\t\t\tbackground-color:#eeeeee;\r\n\t\t}\r\n\t\th1{\r\n\t\t\tfont-size:28px;\r\n\t\t\tline-height:110%;\r\n\t\t\tmargin-bottom:30px;\r\n\t\t\tmargin-top:0;\r\n\t\t\tpadding:0;\r\n\t\t}\r\n\t\t#templateContainer{\r\n\t\t\tbackground-color:none;\r\n\t\t}\r\n\t\t#templateBody{\r\n\t\t\tbackground-color:#ffffff;\r\n\t\t}\r\n\t\t.bodyContent{\r\n\t\t\tline-height:150%;\r\n\t\t\tfont-family:Helvetica;\r\n\t\t\tfont-size:14px;\r\n\t\t\tcolor:#333333;\r\n\t\t\tpadding:20px;\r\n\t\t}\r\n\t\ta:link,a:active,a:visited,a{\r\n\t\t\tcolor:#336699;\r\n\t\t}\r\n\t\t.button:link,.button:active,.button:visited,.button,.button span{\r\n\t\t\tbackground-color:#5d5d5d !important;\r\n\t\t\tcolor:#ffffff !important;\r\n\t\t}\r\n\t\t.button:hover{\r\n\t\t\tbackground-color:#444444 !important;\r\n\t\t\tcolor:#ffffff !important;\r\n\t\t}\r\n\t\tlabel{\r\n\t\t\tline-height:150%;\r\n\t\t\tfont-family:Helvetica;\r\n\t\t\tfont-size:16px;\r\n\t\t\tcolor:#5d5d5d;\r\n\t\t}\r\n\t\t.field-group input,select,textarea,.dijitInputField{\r\n\t\t\tfont-family:Helvetica;\r\n\t\t\tcolor:#5d5d5d !important;\r\n\t\t}\r\n\t\t.asterisk{\r\n\t\t\tcolor:#cc6600;\r\n\t\t\tfont-size:20px;\r\n\t\t}\r\n\t\tlabel .asterisk{\r\n\t\t\tvisibility:hidden;\r\n\t\t}\r\n\t\t.indicates-required{\r\n\t\t\tdisplay:none;\r\n\t\t}\r\n\t\t.field-help{\r\n\t\t\tcolor:#777;\r\n\t\t}\r\n\t\t.error,.errorText{\r\n\t\t\tcolor:#e85c41;\r\n\t\t\tfont-weight:bold;\r\n\t\t}\r\n\t@media (max-width: 620px){\r\n\t\tbody{\r\n\t\t\twidth:100%;\r\n\t\t\t-webkit-font-smoothing:antialiased;\r\n\t\t\tpadding:10px 0 0 0 !important;\r\n\t\t\tmin-width:300px !important;\r\n\t\t}\r\n\r\n}\t@media (max-width: 620px){\r\n\t\t.wrapper,.poweredWrapper{\r\n\t\t\twidth:auto !important;\r\n\t\t\tmax-width:600px !important;\r\n\t\t\tpadding:0 10px;\r\n\t\t}\r\n\r\n}\t@media (max-width: 620px){\r\n\t\t#templateContainer,#templateBody,#templateContainer table{\r\n\t\t\twidth:100% !important;\r\n\t\t\t-moz-box-sizing:border-box;\r\n\t\t\t-webkit-box-sizing:border-box;\r\n\t\t\tbox-sizing:border-box;\r\n\t\t}\r\n\r\n}\t@media (max-width: 620px){\r\n\t\t.addressfield span{\r\n\t\t\twidth:auto;\r\n\t\t\tfloat:none;\r\n\t\t\tpadding-right:0;\r\n\t\t}\r\n\r\n}\t@media (max-width: 620px){\r\n\t\t.captcha{\r\n\t\t\twidth:auto;\r\n\t\t\tfloat:none;\r\n\t\t}\r\n\r\n}</style></head>\r\n    <body leftmargin=\"0\" marginwidth=\"0\" topmargin=\"0\" marginheight=\"0\" offset=\"0\" style=\"font: 14px/20px 'Helvetica', Arial, sans-serif;margin: 0;padding: 75px 0 0 0;text-align: center;-webkit-text-size-adjust: none;background-color: #eeeeee;\">\r\n    \t<center>\r\n        \t<table border=\"0\" cellpadding=\"20\" cellspacing=\"0\" height=\"100%\" width=\"100%\" id=\"bodyTable\" style=\"background-color: #eeeeee;\">\r\n            \t<tr>\r\n                \t<td align=\"center\" valign=\"top\">\r\n                    \t<!-- // BEGIN CONTAINER -->\r\n                        <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\" id=\"templateContainer\" class=\"rounded6\" style=\"border-radius: 6px;background-color: none;\">\r\n                        \t<tr>\r\n                            \t<td align=\"center\" valign=\"top\">\r\n                                \t<!-- // BEGIN HEADER -->\r\n                                    <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\">\r\n                                    \t<tr>\r\n                                        \t<td>\r\n                                            \t<h1 style=\"font-size: 28px;line-height: 110%;margin-bottom: 30px;margin-top: 0;padding: 0;\">Fitty Mi Supper Club</h1>\r\n                                            </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                \t<!-- END HEADER \\\\ -->\r\n                                </td>\r\n                            </tr>\r\n                        \t<tr>\r\n                            \t<td align=\"center\" valign=\"top\">\r\n                                \t<!-- // BEGIN BODY -->\r\n                                \t<table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"600\" id=\"templateBody\" class=\"rounded6\" style=\"border-radius: 6px;background-color: #ffffff;\">\r\n                                    \t<tr>\r\n                                            \r\n                                            <td align=\"left\" valign=\"top\" class=\"bodyContent\" style=\"line-height: 150%;font-family: Helvetica;font-size: 14px;color: #333333;padding: 20px;\">\r\n                                                \r\n                                                <h2 style=\"font-size: 22px;line-height: 28px;margin: 0 0 12px 0;\">Please Confirm Subscription\r\n</h2>\r\n<a class=\"button\" href=\"https://fittymi.us14.list-manage.com/subscribe/confirm?u=883f8d639a9edbf20eb81e6f3&id=2de489bfe5&e=81ac72d108\" style=\"color: #ffffff !important;display: inline-block;font-weight: 500;font-size: 16px;line-height: 42px;font-family: 'Helvetica', Arial, sans-serif;width: auto;white-space: nowrap;height: 42px;margin: 12px 5px 12px 0;padding: 0 22px;text-decoration: none;text-align: center;cursor: pointer;border: 0;border-radius: 3px;vertical-align: top;background-color: #5d5d5d !important;\"><span style=\"display: inline;font-family: 'Helvetica', Arial, sans-serif;text-decoration: none;font-weight: 500;font-style: normal;font-size: 16px;line-height: 42px;cursor: pointer;border: none;background-color: #5d5d5d !important;color: #ffffff !important;\">Yes, subscribe me to this list.</span></a>\r\n<br>\r\n<div><p style=\"padding: 0 0 10px 0;\">If you received this email by mistake, simply delete it. You won't be subscribed if you don't click the confirmation link above.</p>\r\n<p style=\"padding: 0 0 10px 0;\">For questions about this list, please contact:\r\n<br><a href=\"mailto:mark@fittymi.com\" style=\"color: #336699;\">mark@fittymi.com</a></p>\r\n</div>\r\n\r\n\r\n<span itemscope itemtype=\"http://schema.org/EmailMessage\">\r\n  <span itemprop=\"description\" content=\"We need to confirm your email address.\"></span>\r\n  <span itemprop=\"action\" itemscope itemtype=\"http://schema.org/ConfirmAction\">\r\n    <meta itemprop=\"name\" content=\"Confirm Subscription\">\r\n    <span itemprop=\"handler\" itemscope itemtype=\"http://schema.org/HttpActionHandler\">\r\n      <meta itemprop=\"url\" content=\"https://fittymi.us14.list-manage.com/subscribe/smartmail-confirm?u=883f8d639a9edbf20eb81e6f3&id=2de489bfe5&e=81ac72d108&inline=true\">\r\n      <link itemprop=\"method\" href=\"http://schema.org/HttpRequestMethod/POST\">\r\n    </span>\r\n  </span>\r\n</span>\r\n\r\n\r\n                                            </td>\r\n                                            \r\n                                        </tr>\r\n                                    </table>\r\n                                    <!-- END BODY \\\\ -->\r\n                                </td>\r\n                            </tr>\r\n                        \t<tr>\r\n                            \t<td align=\"center\" valign=\"top\">\r\n                                \t<!-- // BEGIN FOOTER -->\r\n                                \t<table border=\"0\" cellpadding=\"20\" cellspacing=\"0\" width=\"600\">\r\n                                    \t<tr>\r\n                                        \t<td align=\"center\" valign=\"top\">\r\n                                                \r\n                                                <div>\r\n                                                    <span class=\"poweredBy\" style=\"display: block;\"><a href=\"http://www.mailchimp.com/monkey-rewards/?utm_source=freemium_newsletter&utm_medium=email&utm_campaign=monkey_rewards&aid=883f8d639a9edbf20eb81e6f3&afl=1\" style=\"color: #336699;\"><img src=\"https://cdn-images.mailchimp.com/monkey_rewards/MC_MonkeyReward_15.png\" border=\"0\" alt=\"Email Marketing Powered by MailChimp\" title=\"MailChimp Email Marketing\" width=\"139\" height=\"54\"></a></span>\r\n                                                </div>\r\n                                                \r\n                                            </td>\r\n                                        </tr>\r\n                                    </table>\r\n                                    <!-- END FOOTER \\\\ -->\r\n                                </td>\r\n                            </tr>\r\n                        </table>\r\n                        <!-- END CONTAINER \\\\ -->\r\n                    </td>\r\n                </tr>\r\n            </table>\r\n        </center>\r\n    </body>\r\n</html>\r\n\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/fittymi.txt",
    "content": "Fitty Mi Supper Club Please Confirm Subscription Yes, subscribe me to this list. If you received this\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/mit_events.html",
    "content": "<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<meta name=\"viewport\" content=\"width=device-width\">\r\n<link rel=\"stylesheet\" href=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/css/ink.css\">\r\n<link rel=\"stylesheet\" href=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/css/styles.css\">\r\n\r\n<style type=\"text/css\">\r\n\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] img{\r\n\t\t\twidth:auto !important;\r\n\t\t\theight:auto !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] center{\r\n\t\t\tmin-width:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .container{\r\n\t\t\twidth:95% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .row{\r\n\t\t\twidth:100% !important;\r\n\t\t\tdisplay:block !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .wrapper{\r\n\t\t\tdisplay:block !important;\r\n\t\t\tpadding-right:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns,table[class=body] .column{\r\n\t\t\ttable-layout:fixed !important;\r\n\t\t\tfloat:none !important;\r\n\t\t\twidth:100% !important;\r\n\t\t\tpadding-right:0px !important;\r\n\t\t\tpadding-left:0px !important;\r\n\t\t\tdisplay:block !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .wrapper.first .columns,table[class=body] .wrapper.first .column{\r\n\t\t\tdisplay:table !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] table.columns td,table[class=body] table.column td{\r\n\t\t\twidth:100% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.one,table[class=body] .column td.one{\r\n\t\t\twidth:8.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.two,table[class=body] .column td.two{\r\n\t\t\twidth:16.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.three,table[class=body] .column td.three{\r\n\t\t\twidth:25% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.four,table[class=body] .column td.four{\r\n\t\t\twidth:33.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.five,table[class=body] .column td.five{\r\n\t\t\twidth:41.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.six,table[class=body] .column td.six{\r\n\t\t\twidth:50% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.seven,table[class=body] .column td.seven{\r\n\t\t\twidth:58.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.eight,table[class=body] .column td.eight{\r\n\t\t\twidth:66.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.nine,table[class=body] .column td.nine{\r\n\t\t\twidth:75% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.ten,table[class=body] .column td.ten{\r\n\t\t\twidth:83.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.eleven,table[class=body] .column td.eleven{\r\n\t\t\twidth:91.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.twelve,table[class=body] .column td.twelve{\r\n\t\t\twidth:100% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] td.offset-by-one,table[class=body] td.offset-by-two,table[class=body] td.offset-by-three,table[class=body] td.offset-by-four,table[class=body] td.offset-by-five,table[class=body] td.offset-by-six,table[class=body] td.offset-by-seven,table[class=body] td.offset-by-eight,table[class=body] td.offset-by-nine,table[class=body] td.offset-by-ten,table[class=body] td.offset-by-eleven{\r\n\t\t\tpadding-left:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] table.columns td.expander{\r\n\t\t\twidth:1px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .right-text-pad,table[class=body] .text-pad-right{\r\n\t\t\tpadding-left:10px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .left-text-pad,table[class=body] .text-pad-left{\r\n\t\t\tpadding-right:10px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .hide-for-small,table[class=body] .show-for-desktop{\r\n\t\t\tdisplay:none !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .show-for-small,table[class=body] .hide-for-desktop{\r\n\t\t\tdisplay:inherit !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .row.footer .wrapper .seven.column,table[class=body] .row.footer .wrapper .five.column.last{\r\n\t\t\twidth:90% !important;\r\n\t\t\tpadding-left:10px !important;\r\n\t\t\tpadding-right:10px !important;\r\n\t\t}\r\n\r\n}</style></head>\r\n<body style=\"width: 100% !important;min-width: 100%;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;margin: 0;padding: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;text-align: left;line-height: 19px;font-size: 14px;\">\r\n  <table class=\"body\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;height: 100%;width: 100%;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"center\" align=\"center\" valign=\"top\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: center;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n        <center style=\"width: 100%; min-width: 580px;\">\r\n\r\n          <!-- Email Content START -->\r\n\r\n          <table class=\"row header\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"center\" align=\"center\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: center;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n                <center style=\"width: 100%; min-width: 580px;\">\r\n\r\n                  <table class=\"container\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: inherit;width: 580px;margin: 0 auto;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;padding-top: 0px;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;padding-bottom: 0px;\">\r\n<tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;padding-bottom: 0px;\">\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b9e275f098&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\">\r\n                                <img src=\"http://mta.mit.edu/sites/default/files/public/styles/600x210/public/mit_newsletter_header_image/collierheader_0.jpg\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                            </td>\r\n\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr>\r\n<tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns sub-header\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;padding-bottom: 0px;\">\r\n                              <div class=\"date\" style=\"background: #ed1c24; text-align: center; text-transform: uppercase; padding: 10px; color: white; font-weight: bold; font-size: 16px;\">Upcoming Events</div>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr>\r\n</table>\r\n</td>\r\n                    </tr></table>\r\n</center>\r\n              </td>\r\n            </tr></table>\r\n<table class=\"container\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: inherit;width: 580px;margin: 0 auto;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n\r\n                <table class=\"row\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;padding-right: 0px;\">\r\n\r\n                      <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"welcome-wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;padding-bottom: 0px;\">\r\n                            <div class=\"welcome-text\" style=\"margin: 5px 5px 0px 5px; padding: 5px 5px 14px 5px; border-bottom: 1px solid #cfd2d4;\">\r\n                                                          </div>\r\n                          </td>\r\n                          <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                        </tr></table>\r\n</td>\r\n                  </tr></table>\r\n<table class=\"row\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\">\r\n<tr class=\"item node-post post-nid-5434\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=8b1591aed3&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/jacobcollier09_web.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 07, 2016<span style=\"font-weight: normal;\"> | </span>5:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=7f32b83c84&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Jacob Collier</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">The Creative Process of Jacob Collier</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                This lecture demonstration by Jacob Collier will provide insight into his unique creative process.  Collier will join the MIT Festival Jazz...                                <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=203f900ada&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=54357f88c1&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">MIT Lecture Hall, 6-120</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\">Reservations not required</p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5292\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=d4bea63f2f&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/ams_0.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 07, 2016<span style=\"font-weight: normal;\"> | </span>5:15pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b0a150ad63&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">COSMOGONY AND MUSIC</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">The Banquet Song of Iopas in Virgil's Aeneid</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                MIT's Ancient &amp; Medieval Studies Colloquium Series presents:\r\nCosmogony and Music, The Banquet Song of Iopas in Virgil's Aeneid.\r\nEvan MacCarthy...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=12c6c02097&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"whereis.mit.edu\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">MIT Rm. 14E-304</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5438\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=71338cec11&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/mat_logo.png\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 10, 2016<span style=\"font-weight: normal;\"> | </span>6:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=d584ac0e9a&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">MAT Confessions: #Thisis2016</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Asian American Theater, 21M.846/847</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                A performance by 20 students in Claire Conceison's Asian American Theater class, composed from their own journal writings, inspired by their...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=fac3f9cd37&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\">Rehearsal Room A, Kresge Auditorium</span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5300\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=adf65a1196&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/jc.thumbnail_0.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 10, 2016<span style=\"font-weight: normal;\"> | </span>8:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=60cc84b632&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Jacob Collier: Imagination Off The Charts</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">a collaborative performance with FJE</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                A concert featuring musical phenom Jacob Collier, MIT Visiting Artist\r\n\r\n \r\n\r\nWho is Jacob Collier?\r\nHailed by The Guardian as “Jazz’s New...                                <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=41b616b433&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=fad94c9deb&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Kresge Auditorium</a></span> | <span style=\"font-weight: bold;\">$5</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b1dacd94fc&e=46b88902b8\" target=\"_blank\" style=\"color: rgb(237, 28, 36) !important; text-decoration: underline; font-weight: bold; font-size: 12px;\">Reserve a Seat</a></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5372\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=e0e7374068&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/mit-mta-deveau-10-18-16-3.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">From 12/11 to 12/14; times vary</p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b7a7a319f8&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">MIT Chamber Music Society</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Student Concert Series</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                DECEMBER 11, 3PM\r\n\r\nSMETANA String Quartet No. 1, \"From My Life\"\r\nLisa Kong '18, violin; Michelle Noh '18, violin; Aria Shi '18, viola; Courtney Guo...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=42d8050eed&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=fedc2465c3&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Killian Hall</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\">Reservations not required</p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5287\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=f22419ea7c&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/snowflakes.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 13, 2016<span style=\"font-weight: normal;\"> | </span>7:30pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=375d0806b6&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Holiday Pops Concert</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">MITSOLite, Adam Boyles, director</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                This concert will feature holiday season music including: Praetorius/arr. Treybig, In dulci jubilo, Morton Gould, It came upon a midnight clear;...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=4f88922d9c&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=02dd1ca873&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Kresge Auditorium</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"last node-post post-nid-5439\" style=\"border-bottom: none;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=94b8c3f626&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/beethoven.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 17, 2016<span style=\"font-weight: normal;\"> | </span>8:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=cef0f548c2&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Pathos Ensemble</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">MIT Student Chamber Orchestra</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                Beethoven's Symphony No. 3 will be performed by a student chamber orchestra conducted by MIT graduate student Dominique Hoskin.\r\n                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=16ee20a53e&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=a63cabae3c&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Kresge Auditorium</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n</table>\r\n<table class=\"row footer\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                      <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 30px 0px 10px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;background: #606a71;\">\r\n                            <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"seven column\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 58.333333%;padding-left: 10px;\">\r\n\r\n                                  <p class=\"footer-logo-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 12px;\">\r\n                                    <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/footer_logo.png\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: none; clear: both; display: block; margin-left: 5px;\"></p>\r\n\r\n                                  <p class=\"copyright-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 17px;\">Copyright © 2015 MIT Music and Theater Arts,<br> All rig hts reserved.</p>\r\n\r\n                                  <p class=\"address-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 18px;font-size: 13px;margin-bottom: 10px;\"><strong>MIT Music and Theater Arts</strong><br>\r\n                                  77 Massachusetts Avenue 4-243<br>\r\n                                  Cambridge, Massachusetts 02139<br><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=1f365a5a91&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">Campus map</a> | <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=68404bf123&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">Parking</a>\r\n                                </p>\r\n</td>\r\n                                <td class=\"five column last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;padding-right: 10px;width: 41.666666%;padding-left: 0px;\">\r\n\r\n                                  <p style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 10px;\">You are receiving this email because you have requested to receive the MIT Music and Theater Arts Calendar of Events.</p>\r\n                                  <p class=\"links-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 3px;margin-top: 15px;\">\r\n                                    <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=edf051bcb9&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">add us to your address book</a><br><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=65659ae644&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">unsubscribe from this list</a><br><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=2fbcd2f88d&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">forward to a friend</a>\r\n                                  </p>\r\n\r\n                                  <p class=\"follow-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 10px;margin-top: 13px;\">\r\n                                    <span style=\"display: inline-block; margin-top: 2px; vertical-align: middle;\">Follow MTA</span>\r\n                                    <a class=\"twitter-link\" href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=e3e5120754&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; display: inline-block; width: 26px; height: 26px; vertical-align: middle; margin-left: 20px; font-size: 13px;\">\r\n                                      <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/twitter_logo.png\" alt=\"twitter\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                                    <a class=\"facebook-link\" href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=6b7ae0bd6f&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; display: inline-block; width: 26px; height: 26px; vertical-align: middle; margin-left: 20px; font-size: 13px;\">\r\n                                      <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/facebook_logo.png\" alt=\"facebook\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                                  </p>\r\n                                </td>\r\n                                <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                              </tr></table>\r\n</td>\r\n\r\n                          <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                        </tr></table>\r\n</td>\r\n                  </tr></table>\r\n<!-- container end below -->\r\n</td>\r\n            </tr></table>\r\n<!-- Email Content END -->\r\n</center>\r\n      </td>\r\n    </tr></table>\r\n            <center>\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" id=\"canspamBarWrapper\" style=\"background-color:#FFFFFF; border-top:1px solid #E5E5E5;\">\r\n                    <tr>\r\n                        <td align=\"center\" valign=\"top\" style=\"padding-top:20px; padding-bottom:20px;\">\r\n                            <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" id=\"canspamBar\">\r\n                                <tr>\r\n                                    <td align=\"center\" valign=\"top\" style=\"color:#606060; font-family:Helvetica, Arial, sans-serif; font-size:11px; line-height:150%; padding-right:20px; padding-bottom:5px; padding-left:20px; text-align:center;\">\r\n                                        This email was sent to <a href=\"mailto:spang@mit.edu\" target=\"_blank\" style=\"color:#404040 !important;\">spang@mit.edu</a>\r\n                                        <br />\r\n                                        <a href=\"http://mit.us2.list-manage.com/about?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8&c=e75824be34\" target=\"_blank\" style=\"color:#404040 !important;\"><em>why did I get this?</em></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"http://mit.us2.list-manage1.com/unsubscribe?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8&c=e75824be34\" style=\"color:#404040 !important;\">unsubscribe from this list</a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"http://mit.us2.list-manage1.com/profile?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8\" style=\"color:#404040 !important;\">update subscription preferences</a>\r\n                                        <br />\r\n                                        MIT Music and Theater Arts &middot; 77 Massachusetts Avenue &middot; 4-243 &middot; Cambridge, Massachusetts 02139 &middot; USA\r\n                                        <br />\r\n                                        <br />\r\n\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n                        </td>\r\n                    </tr>\r\n                </table>\r\n                <style type=\"text/css\">\r\n                    @media only screen and (max-width: 480px){\r\n                        table#canspamBar td{font-size:14px !important;}\r\n                        table#canspamBar td a{display:block !important; margin-top:10px !important;}\r\n                    }\r\n                </style>\r\n            </center><img src=\"http://mit.us2.list-manage.com/track/open.php?u=9a8caadec06a0edf712df1a5e&id=e75824be34&e=46b88902b8\" height=\"1\" width=\"1\"></body>\r\n</html>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<meta name=\"viewport\" content=\"width=device-width\">\r\n<link rel=\"stylesheet\" href=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/css/ink.css\">\r\n<link rel=\"stylesheet\" href=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/css/styles.css\">\r\n\r\n<style type=\"text/css\">\r\n\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] img{\r\n\t\t\twidth:auto !important;\r\n\t\t\theight:auto !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] center{\r\n\t\t\tmin-width:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .container{\r\n\t\t\twidth:95% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .row{\r\n\t\t\twidth:100% !important;\r\n\t\t\tdisplay:block !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .wrapper{\r\n\t\t\tdisplay:block !important;\r\n\t\t\tpadding-right:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns,table[class=body] .column{\r\n\t\t\ttable-layout:fixed !important;\r\n\t\t\tfloat:none !important;\r\n\t\t\twidth:100% !important;\r\n\t\t\tpadding-right:0px !important;\r\n\t\t\tpadding-left:0px !important;\r\n\t\t\tdisplay:block !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .wrapper.first .columns,table[class=body] .wrapper.first .column{\r\n\t\t\tdisplay:table !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] table.columns td,table[class=body] table.column td{\r\n\t\t\twidth:100% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.one,table[class=body] .column td.one{\r\n\t\t\twidth:8.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.two,table[class=body] .column td.two{\r\n\t\t\twidth:16.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.three,table[class=body] .column td.three{\r\n\t\t\twidth:25% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.four,table[class=body] .column td.four{\r\n\t\t\twidth:33.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.five,table[class=body] .column td.five{\r\n\t\t\twidth:41.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.six,table[class=body] .column td.six{\r\n\t\t\twidth:50% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.seven,table[class=body] .column td.seven{\r\n\t\t\twidth:58.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.eight,table[class=body] .column td.eight{\r\n\t\t\twidth:66.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.nine,table[class=body] .column td.nine{\r\n\t\t\twidth:75% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.ten,table[class=body] .column td.ten{\r\n\t\t\twidth:83.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.eleven,table[class=body] .column td.eleven{\r\n\t\t\twidth:91.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.twelve,table[class=body] .column td.twelve{\r\n\t\t\twidth:100% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] td.offset-by-one,table[class=body] td.offset-by-two,table[class=body] td.offset-by-three,table[class=body] td.offset-by-four,table[class=body] td.offset-by-five,table[class=body] td.offset-by-six,table[class=body] td.offset-by-seven,table[class=body] td.offset-by-eight,table[class=body] td.offset-by-nine,table[class=body] td.offset-by-ten,table[class=body] td.offset-by-eleven{\r\n\t\t\tpadding-left:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] table.columns td.expander{\r\n\t\t\twidth:1px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .right-text-pad,table[class=body] .text-pad-right{\r\n\t\t\tpadding-left:10px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .left-text-pad,table[class=body] .text-pad-left{\r\n\t\t\tpadding-right:10px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .hide-for-small,table[class=body] .show-for-desktop{\r\n\t\t\tdisplay:none !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .show-for-small,table[class=body] .hide-for-desktop{\r\n\t\t\tdisplay:inherit !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .row.footer .wrapper .seven.column,table[class=body] .row.footer .wrapper .five.column.last{\r\n\t\t\twidth:90% !important;\r\n\t\t\tpadding-left:10px !important;\r\n\t\t\tpadding-right:10px !important;\r\n\t\t}\r\n\r\n}</style></head>\r\n<body style=\"width: 100% !important;min-width: 100%;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;margin: 0;padding: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;text-align: left;line-height: 19px;font-size: 14px;\">\r\n  <table class=\"body\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;height: 100%;width: 100%;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"center\" align=\"center\" valign=\"top\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: center;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n        <center style=\"width: 100%; min-width: 580px;\">\r\n\r\n          <!-- Email Content START -->\r\n\r\n          <table class=\"row header\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"center\" align=\"center\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: center;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n                <center style=\"width: 100%; min-width: 580px;\">\r\n\r\n                  <table class=\"container\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: inherit;width: 580px;margin: 0 auto;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;padding-top: 0px;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;padding-bottom: 0px;\">\r\n<tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;padding-bottom: 0px;\">\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=ce81b0e66e&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\">\r\n                                <img src=\"http://mta.mit.edu/sites/default/files/public/styles/600x210/public/mit_newsletter_header_image/sumieheader.jpg\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                            </td>\r\n\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr>\r\n<tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns sub-header\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;padding-bottom: 0px;\">\r\n                              <div class=\"date\" style=\"background: #ed1c24; text-align: center; text-transform: uppercase; padding: 10px; color: white; font-weight: bold; font-size: 16px;\">Upcoming Events</div>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr>\r\n</table>\r\n</td>\r\n                    </tr></table>\r\n</center>\r\n              </td>\r\n            </tr></table>\r\n<table class=\"container\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: inherit;width: 580px;margin: 0 auto;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n\r\n                <table class=\"row\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;padding-right: 0px;\">\r\n\r\n                      <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"welcome-wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;padding-bottom: 0px;\">\r\n                            <div class=\"welcome-text\" style=\"margin: 5px 5px 0px 5px; padding: 5px 5px 14px 5px; border-bottom: 1px solid #cfd2d4;\">\r\n                                                          </div>\r\n                          </td>\r\n                          <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                        </tr></table>\r\n</td>\r\n                  </tr></table>\r\n<table class=\"row\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\">\r\n<tr class=\"item node-post post-nid-5398\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=d37e232bd7&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/guitar.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">November 29, 2016<span style=\"font-weight: normal;\"> | </span>8:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=5a920b9f64&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">CMS Jazz Combos Concert</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Keala Kaumeheiwa, coach</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                The MIT Chamber Music Society Jazz Combos Concert Program will include:\r\n\r\nFascinating Rhythm - George &amp; Ira Gershwin\r\nDizzy Moods - Charles...                                <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=15e2e3b314&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=2675cbf116&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Killian Hall</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\">Reservations not required</p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5284\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=9cd866ac27&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/kaneko.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 01, 2016<span style=\"font-weight: normal;\"> | </span>5:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=1c6ced6686&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">MTA Composer Forum: Sumie Kaneko</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\"></p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                Japanese Koto and Shamisen player and jazz singer/songwriter Sumie (Sumi-é) Kaneko is known for her fusion of jazz with her Japanese roots. ...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=06c592400e&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=c817b6c2cb&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Lewis Music Library, 14E-109</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\">Reservations not required</p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5285\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=24e45e3a54&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/prism_0.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 02, 2016<span style=\"font-weight: normal;\"> | </span>8:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=f301bc8c61&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">MIT Wind Ensemble</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Prism II: Concert </p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                The Second Annual Prism Concert Spectacular, a concert by the MIT Wind Ensemble, Frederick Harris, Jr. Music Director; Kenneth Amis, Assistant...                                <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=8a7604d0ec&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=c0bfd71ea2&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Kresge Auditorium</a></span> | <span style=\"font-weight: bold;\">$5</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=f4b23ca5be&e=46b88902b8\" target=\"_blank\" style=\"color: rgb(237, 28, 36) !important; text-decoration: underline; font-weight: bold; font-size: 12px;\">Reserve a Seat</a></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5286\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=6c5ac1365d&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/gamelan_0.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 03, 2016<span style=\"font-weight: normal;\"> | </span>4:30pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b7d9eb62dd&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Gamelan Galak Tika</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">WORLD MUSIC DAY</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                A showcase of MIT's world music ensembles and their non-western cultures, World Music Day begins with a 4:30pm Concert by MIT Gamelan Galak Tika in...                                <a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b92cde5365&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=13f5533343&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Kresge Auditorium</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5288\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=ce83e477eb&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/lamine_2.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 03, 2016<span style=\"font-weight: normal;\"> | </span>7:30pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=a2ce625395&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Rambax, Senegalese Drumming Ensemble</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">WORLD MUSIC DAY</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                This Rambax performance under the direction of Lamine Touré is part of MIT World Music Day, a showcase of MIT's World Music ensembles and celebration...                                <a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=f7bce5003d&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b72741bc92&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Lobdell, MIT Student Center</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\">Reservations not required</p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5289\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=4f4e3d84dd&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/daviddeveau.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 04, 2016<span style=\"font-weight: normal;\"> | </span>4:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=33c8e61d2e&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Pianist David Deveau</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Schubert Sonatas</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                Deveau's all-Schubert program will include: Sonata in A major, D.959 and Sonata in B-flat major, D. 960.\r\n \r\n\r\nProgram Note\r\nFranz Schubert (...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=cb08810f98&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=43c8c3e8d7&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Kresge Auditorium</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\">Reservations not required</p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"last node-post post-nid-5292\" style=\"border-bottom: none;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=0ed0889c47&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/ams_0.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">December 07, 2016<span style=\"font-weight: normal;\"> | </span>5:15pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=ec1a412a2c&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">COSMOGONY AND MUSIC</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">The Banquet Song of Iopas in Virgil's Aeneid</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                MIT's Ancient &amp; Medieval Studies Colloquium Series presents:\r\nCosmogony and Music, The Banquet Song of Iopas in Virgil's Aeneid.\r\nEvan MacCarthy...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=a1aa4f71dc&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"whereis.mit.edu\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">MIT Rm. 14E-304</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n</table>\r\n<table class=\"row footer\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                      <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 30px 0px 10px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;background: #606a71;\">\r\n                            <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"seven column\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 58.333333%;padding-left: 10px;\">\r\n\r\n                                  <p class=\"footer-logo-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 12px;\">\r\n                                    <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/footer_logo.png\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: none; clear: both; display: block; margin-left: 5px;\"></p>\r\n\r\n                                  <p class=\"copyright-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 17px;\">Copyright © 2015 MIT Music and Theater Arts,<br> All rig hts reserved.</p>\r\n\r\n                                  <p class=\"address-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 18px;font-size: 13px;margin-bottom: 10px;\"><strong>MIT Music and Theater Arts</strong><br>\r\n                                  77 Massachusetts Avenue 4-243<br>\r\n                                  Cambridge, Massachusetts 02139<br><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=bcec305433&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">Campus map</a> | <a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=b42f1c8844&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">Parking</a>\r\n                                </p>\r\n</td>\r\n                                <td class=\"five column last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;padding-right: 10px;width: 41.666666%;padding-left: 0px;\">\r\n\r\n                                  <p style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 10px;\">You are receiving this email because you have requested to receive the MIT Music and Theater Arts Calendar of Events.</p>\r\n                                  <p class=\"links-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 3px;margin-top: 15px;\">\r\n                                    <a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=fd09fccee9&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">add us to your address book</a><br><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=617a026974&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">unsubscribe from this list</a><br><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=1400e5d550&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">forward to a friend</a>\r\n                                  </p>\r\n\r\n                                  <p class=\"follow-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 10px;margin-top: 13px;\">\r\n                                    <span style=\"display: inline-block; margin-top: 2px; vertical-align: middle;\">Follow MTA</span>\r\n                                    <a class=\"twitter-link\" href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=43c4454aaf&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; display: inline-block; width: 26px; height: 26px; vertical-align: middle; margin-left: 20px; font-size: 13px;\">\r\n                                      <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/twitter_logo.png\" alt=\"twitter\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                                    <a class=\"facebook-link\" href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=ef70b5b5bf&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; display: inline-block; width: 26px; height: 26px; vertical-align: middle; margin-left: 20px; font-size: 13px;\">\r\n                                      <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/facebook_logo.png\" alt=\"facebook\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                                  </p>\r\n                                </td>\r\n                                <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                              </tr></table>\r\n</td>\r\n\r\n                          <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                        </tr></table>\r\n</td>\r\n                  </tr></table>\r\n<!-- container end below -->\r\n</td>\r\n            </tr></table>\r\n<!-- Email Content END -->\r\n</center>\r\n      </td>\r\n    </tr></table>\r\n            <center>\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" id=\"canspamBarWrapper\" style=\"background-color:#FFFFFF; border-top:1px solid #E5E5E5;\">\r\n                    <tr>\r\n                        <td align=\"center\" valign=\"top\" style=\"padding-top:20px; padding-bottom:20px;\">\r\n                            <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" id=\"canspamBar\">\r\n                                <tr>\r\n                                    <td align=\"center\" valign=\"top\" style=\"color:#606060; font-family:Helvetica, Arial, sans-serif; font-size:11px; line-height:150%; padding-right:20px; padding-bottom:5px; padding-left:20px; text-align:center;\">\r\n                                        This email was sent to <a href=\"mailto:spang@mit.edu\" target=\"_blank\" style=\"color:#404040 !important;\">spang@mit.edu</a>\r\n                                        <br />\r\n                                        <a href=\"http://mit.us2.list-manage.com/about?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8&c=4567355d2a\" target=\"_blank\" style=\"color:#404040 !important;\"><em>why did I get this?</em></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"http://mit.us2.list-manage1.com/unsubscribe?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8&c=4567355d2a\" style=\"color:#404040 !important;\">unsubscribe from this list</a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"http://mit.us2.list-manage1.com/profile?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8\" style=\"color:#404040 !important;\">update subscription preferences</a>\r\n                                        <br />\r\n                                        MIT Music and Theater Arts &middot; 77 Massachusetts Avenue &middot; 4-243 &middot; Cambridge, Massachusetts 02139 &middot; USA\r\n                                        <br />\r\n                                        <br />\r\n\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n                        </td>\r\n                    </tr>\r\n                </table>\r\n                <style type=\"text/css\">\r\n                    @media only screen and (max-width: 480px){\r\n                        table#canspamBar td{font-size:14px !important;}\r\n                        table#canspamBar td a{display:block !important; margin-top:10px !important;}\r\n                    }\r\n                </style>\r\n            </center><img src=\"http://mit.us2.list-manage.com/track/open.php?u=9a8caadec06a0edf712df1a5e&id=4567355d2a&e=46b88902b8\" height=\"1\" width=\"1\"></body>\r\n</html>\n<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\">\r\n<html xmlns=\"http://www.w3.org/1999/xhtml\">\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<meta name=\"viewport\" content=\"width=device-width\">\r\n<link rel=\"stylesheet\" href=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/css/ink.css\">\r\n<link rel=\"stylesheet\" href=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/css/styles.css\">\r\n\r\n<style type=\"text/css\">\r\n\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] img{\r\n\t\t\twidth:auto !important;\r\n\t\t\theight:auto !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] center{\r\n\t\t\tmin-width:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .container{\r\n\t\t\twidth:95% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .row{\r\n\t\t\twidth:100% !important;\r\n\t\t\tdisplay:block !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .wrapper{\r\n\t\t\tdisplay:block !important;\r\n\t\t\tpadding-right:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns,table[class=body] .column{\r\n\t\t\ttable-layout:fixed !important;\r\n\t\t\tfloat:none !important;\r\n\t\t\twidth:100% !important;\r\n\t\t\tpadding-right:0px !important;\r\n\t\t\tpadding-left:0px !important;\r\n\t\t\tdisplay:block !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .wrapper.first .columns,table[class=body] .wrapper.first .column{\r\n\t\t\tdisplay:table !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] table.columns td,table[class=body] table.column td{\r\n\t\t\twidth:100% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.one,table[class=body] .column td.one{\r\n\t\t\twidth:8.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.two,table[class=body] .column td.two{\r\n\t\t\twidth:16.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.three,table[class=body] .column td.three{\r\n\t\t\twidth:25% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.four,table[class=body] .column td.four{\r\n\t\t\twidth:33.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.five,table[class=body] .column td.five{\r\n\t\t\twidth:41.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.six,table[class=body] .column td.six{\r\n\t\t\twidth:50% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.seven,table[class=body] .column td.seven{\r\n\t\t\twidth:58.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.eight,table[class=body] .column td.eight{\r\n\t\t\twidth:66.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.nine,table[class=body] .column td.nine{\r\n\t\t\twidth:75% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.ten,table[class=body] .column td.ten{\r\n\t\t\twidth:83.333333% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.eleven,table[class=body] .column td.eleven{\r\n\t\t\twidth:91.666666% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .columns td.twelve,table[class=body] .column td.twelve{\r\n\t\t\twidth:100% !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] td.offset-by-one,table[class=body] td.offset-by-two,table[class=body] td.offset-by-three,table[class=body] td.offset-by-four,table[class=body] td.offset-by-five,table[class=body] td.offset-by-six,table[class=body] td.offset-by-seven,table[class=body] td.offset-by-eight,table[class=body] td.offset-by-nine,table[class=body] td.offset-by-ten,table[class=body] td.offset-by-eleven{\r\n\t\t\tpadding-left:0 !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] table.columns td.expander{\r\n\t\t\twidth:1px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .right-text-pad,table[class=body] .text-pad-right{\r\n\t\t\tpadding-left:10px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .left-text-pad,table[class=body] .text-pad-left{\r\n\t\t\tpadding-right:10px !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .hide-for-small,table[class=body] .show-for-desktop{\r\n\t\t\tdisplay:none !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .show-for-small,table[class=body] .hide-for-desktop{\r\n\t\t\tdisplay:inherit !important;\r\n\t\t}\r\n\r\n}\t@media only screen and (max-width: 600px){\r\n\t\ttable[class=body] .row.footer .wrapper .seven.column,table[class=body] .row.footer .wrapper .five.column.last{\r\n\t\t\twidth:90% !important;\r\n\t\t\tpadding-left:10px !important;\r\n\t\t\tpadding-right:10px !important;\r\n\t\t}\r\n\r\n}</style></head>\r\n<body style=\"width: 100% !important;min-width: 100%;-webkit-text-size-adjust: 100%;-ms-text-size-adjust: 100%;margin: 0;padding: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;text-align: left;line-height: 19px;font-size: 14px;\">\r\n  <table class=\"body\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;height: 100%;width: 100%;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"center\" align=\"center\" valign=\"top\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: center;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n        <center style=\"width: 100%; min-width: 580px;\">\r\n\r\n          <!-- Email Content START -->\r\n\r\n          <table class=\"row header\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"center\" align=\"center\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: center;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n                <center style=\"width: 100%; min-width: 580px;\">\r\n\r\n                  <table class=\"container\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: inherit;width: 580px;margin: 0 auto;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;padding-top: 0px;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;padding-bottom: 0px;\">\r\n<tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;padding-bottom: 0px;\">\r\n                              <a href=\"http://mit.us2.list-manage2.com/track/click?u=9a8caadec06a0edf712df1a5e&id=17f378c63c&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\">\r\n                                <img src=\"http://mta.mit.edu/sites/default/files/public/styles/600x210/public/mit_newsletter_header_image/bassplayerheader_1.jpg\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                            </td>\r\n\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr>\r\n<tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns sub-header\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;padding-bottom: 0px;\">\r\n                              <div class=\"date\" style=\"background: #ed1c24; text-align: center; text-transform: uppercase; padding: 10px; color: white; font-weight: bold; font-size: 16px;\">Upcoming Events</div>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr>\r\n</table>\r\n</td>\r\n                    </tr></table>\r\n</center>\r\n              </td>\r\n            </tr></table>\r\n<table class=\"container\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: inherit;width: 580px;margin: 0 auto;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;\">\r\n\r\n                <table class=\"row\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;padding-right: 0px;\">\r\n\r\n                      <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"welcome-wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;padding-bottom: 0px;\">\r\n                            <div class=\"welcome-text\" style=\"margin: 5px 5px 0px 5px; padding: 5px 5px 14px 5px; border-bottom: 1px solid #cfd2d4;\">\r\n                                                          </div>\r\n                          </td>\r\n                          <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                        </tr></table>\r\n</td>\r\n                  </tr></table>\r\n<table class=\"row\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\">\r\n<tr class=\"item node-post post-nid-5264\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=89a5ed01de&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/tod.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">November 17, 2016<span style=\"font-weight: normal;\"> | </span>5:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=c3b788cb92&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Tod Machover</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">MTA Composer Forum</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                Recent Music for Pianos, Hyperensemble and Cities\r\n\r\nTod Machover, Muriel R. Cooper Professor of Music and Media, MIT Media Lab\r\n\r\n \r\n\r\nAbout...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=acea59efd1&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=09845a2796&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Lewis Music Library, 14E-109</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5293\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=e3ad77895b&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/icecream.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">November 17, 2016<span style=\"font-weight: normal;\"> | </span>7:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=0b537f6016&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">UNITING THROUGH VOICE AND MUSIC</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Celebrating MIT Values</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                In a time of change, we celebrate the enduring values that unite us at MIT. Join us for musical performances and reflections from MIT students and...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=6209dee8dc&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\">MIT Lobby 10</span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5260\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=033db5de66&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/mitso_thumb.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">November 18, 2016<span style=\"font-weight: normal;\"> | </span>8:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=53084078a2&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">MIT Symphony Orchestra</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Adam K. Boyles, music director</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                The program will feature:\r\n\r\nMozart, Overture to Così fan tutte; (Thus Do All Women, or The School for Lovers) an Italian opera buffa.\r\n\r\nPuccini,...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=c0840d0c78&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=9b647d6c0f&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Kresge Auditorium</a></span> | <span style=\"font-weight: bold;\">$5</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=32c3f52145&e=46b88902b8\" target=\"_blank\" style=\"color: rgb(237, 28, 36) !important; text-decoration: underline; font-weight: bold; font-size: 12px;\">Reserve a Seat</a></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"item node-post post-nid-5261\" style=\"border-bottom: 1px solid #cfd2d4;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=870de9d49e&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/cutter_copy.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">November 19, 2016<span style=\"font-weight: normal;\"> | </span>8:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=175677fae9&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">MIT Chamber Chorus</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">William Cutter, director</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                The program will feature Händel, The ways of Zion do mourn (HWV 264), a Funeral Anthem for Queen Caroline; Bach, Komm, du Süsse Todesstunde, BWV 161...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=1213378c68&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=0b38b784d1&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">MIT Chapel</a></span> | <span style=\"font-weight: bold;\">$5</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=fafc38164d&e=46b88902b8\" target=\"_blank\" style=\"color: rgb(237, 28, 36) !important; text-decoration: underline; font-weight: bold; font-size: 12px;\">Reserve a Seat</a></p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n<tr class=\"last node-post post-nid-5266\" style=\"border-bottom: none;padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                        <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0px 5px;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"four sub-columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 10px;width: 33.333333%;padding-left: 5px;padding-bottom: 13px;padding-top: 3px;\">\r\n\r\n                              <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=3536f5cca4&e=46b88902b8\" style=\"color: black; text-decoration: underline; font-weight: bold;\"><img src=\"http://mta.mit.edu/sites/default/files/public/styles/180x180/public/field_event_thumbnail/vje_0.jpg\" width=\"180\" height=\"180\" alt=\"\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none; height: auto !important;\"></a>\r\n                            </td>\r\n                            <td class=\"eight sub-columns last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;min-width: 0px;padding-right: 5px;width: 66.666666%;padding-left: 10px;padding-top: 3px;\">\r\n\r\n                              <p class=\"date\" style=\"margin: 0;color: rgb(96, 106, 113);font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 1em;font-size: 12px;margin-bottom: 6px;\">November 20, 2016<span style=\"font-weight: normal;\"> | </span>7:00pm </p>\r\n                              <p class=\"title\" style=\"margin: 0;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 14px;margin-bottom: 0;\"><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=4ca68144b2&e=46b88902b8\" style=\"color: rgb(237, 28, 36) !important; text-decoration: none; font-weight: bold; font-size: 16px;\">Vocal Jazz Ensemble</a></p>\r\n                              <p class=\"sub-title\" style=\"margin: 0;color: rgb(237, 28, 36) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 16px;margin-bottom: 0px;text-decoration: none;\">Standards with a Twist</p>\r\n\r\n                              <p class=\"teaser\" style=\"margin: 0;color: rgb(96, 106, 113) !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 21px;font-size: 15px;margin-bottom: 0px;margin-top: 5px;\">\r\n                                This program by the MIT Vocal Jazz Ensemble will feature Standards with a Twist, arrangements created by the members of the Vocal Jazz Ensemble and...                                <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=61b7ef1928&e=46b88902b8\" class=\"read-more\" style=\"color: rgb(96, 106, 113) !important; text-decoration: none; font-weight: normal; font-size: 15px; margin-top: 5px; line-height: 21px;\">Read More</a>                              </p>\r\n                              <p class=\"location-price\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;margin-top: 12px;text-decoration: none;\">\r\n                                <span style=\"font-weight: bold;\"><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=4c5f876205&e=46b88902b8\" target=\"_blank\" style=\"color: #606a71 !important; text-decoration: none; font-weight: bold; font-size: 12px;\">Killian Hall</a></span> | <span style=\"font-weight: bold;\">free</span>                              </p>\r\n                              <p class=\"reservation\" style=\"margin: 0;color: #606a71 !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: bold;padding: 0;text-align: left;line-height: 19px;font-size: 12px;margin-bottom: 0px;text-decoration: none;margin-top: 4px;text-transform: uppercase;\">Reservations not required</p>\r\n                            </td>\r\n                            <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                          </tr></table>\r\n</td>\r\n                    </tr>\r\n</table>\r\n<table class=\"row footer\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0px;vertical-align: top;text-align: left;width: 100%;position: relative;display: block;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"wrapper\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 10px 20px 0px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;position: relative;\">\r\n\r\n                      <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"twelve columns\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 30px 0px 10px 0px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 100%;background: #606a71;\">\r\n                            <table class=\"twelve columns\" style=\"border-spacing: 0;border-collapse: collapse;padding: 0;vertical-align: top;text-align: left;margin: 0 auto;width: 580px;\"><tr style=\"padding: 0;vertical-align: top;text-align: left;\">\r\n<td class=\"seven column\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;width: 58.333333%;padding-left: 10px;\">\r\n\r\n                                  <p class=\"footer-logo-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 12px;\">\r\n                                    <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/footer_logo.png\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: none; clear: both; display: block; margin-left: 5px;\"></p>\r\n\r\n                                  <p class=\"copyright-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 17px;\">Copyright © 2015 MIT Music and Theater Arts,<br> All rig hts reserved.</p>\r\n\r\n                                  <p class=\"address-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 18px;font-size: 13px;margin-bottom: 10px;\"><strong>MIT Music and Theater Arts</strong><br>\r\n                                  77 Massachusetts Avenue 4-243<br>\r\n                                  Cambridge, Massachusetts 02139<br><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=709e6cfca0&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">Campus map</a> | <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=f0e01c15c2&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">Parking</a>\r\n                                </p>\r\n</td>\r\n                                <td class=\"five column last\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;padding-right: 10px;width: 41.666666%;padding-left: 0px;\">\r\n\r\n                                  <p style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 10px;\">You are receiving this email because you have requested to receive the MIT Music and Theater Arts Calendar of Events.</p>\r\n                                  <p class=\"links-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 3px;margin-top: 15px;\">\r\n                                    <a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=ba3c76d3a3&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">add us to your address book</a><br><a href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=0347d2e871&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">unsubscribe from this list</a><br><a href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=8104a607a9&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; font-size: 13px;\">forward to a friend</a>\r\n                                  </p>\r\n\r\n                                  <p class=\"follow-wrapper\" style=\"margin: 0;color: white !important;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;padding: 0;text-align: left;line-height: 19px;font-size: 13px;margin-bottom: 10px;margin-top: 13px;\">\r\n                                    <span style=\"display: inline-block; margin-top: 2px; vertical-align: middle;\">Follow MTA</span>\r\n                                    <a class=\"twitter-link\" href=\"http://mit.us2.list-manage.com/track/click?u=9a8caadec06a0edf712df1a5e&id=ea1fb7c132&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; display: inline-block; width: 26px; height: 26px; vertical-align: middle; margin-left: 20px; font-size: 13px;\">\r\n                                      <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/twitter_logo.png\" alt=\"twitter\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                                    <a class=\"facebook-link\" href=\"http://mit.us2.list-manage1.com/track/click?u=9a8caadec06a0edf712df1a5e&id=d33d557aae&e=46b88902b8\" style=\"color: white !important; text-decoration: underline; font-weight: normal; display: inline-block; width: 26px; height: 26px; vertical-align: middle; margin-left: 20px; font-size: 13px;\">\r\n                                      <img src=\"http://mta.mit.edu/sites/all/modules/custom/mit_newsletter/templates/assets/images/facebook_logo.png\" alt=\"facebook\" style=\"outline: none; text-decoration: none; -ms-interpolation-mode: bicubic; width: auto; max-width: 100%; float: left; clear: both; display: block; border: none;\"></a>\r\n                                  </p>\r\n                                </td>\r\n                                <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                              </tr></table>\r\n</td>\r\n\r\n                          <td class=\"expander\" style=\"word-break: break-word;-webkit-hyphens: auto;-moz-hyphens: auto;hyphens: auto;border-collapse: collapse !important;padding: 0px 0px 10px;vertical-align: top;text-align: left;color: #222222;font-family: 'Helvetica', 'Arial', sans-serif;font-weight: normal;margin: 0;line-height: 19px;font-size: 14px;visibility: hidden;width: 0px;\"></td>\r\n                        </tr></table>\r\n</td>\r\n                  </tr></table>\r\n<!-- container end below -->\r\n</td>\r\n            </tr></table>\r\n<!-- Email Content END -->\r\n</center>\r\n      </td>\r\n    </tr></table>\r\n            <center>\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <br />\r\n                <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" id=\"canspamBarWrapper\" style=\"background-color:#FFFFFF; border-top:1px solid #E5E5E5;\">\r\n                    <tr>\r\n                        <td align=\"center\" valign=\"top\" style=\"padding-top:20px; padding-bottom:20px;\">\r\n                            <table border=\"0\" cellpadding=\"0\" cellspacing=\"0\" id=\"canspamBar\">\r\n                                <tr>\r\n                                    <td align=\"center\" valign=\"top\" style=\"color:#606060; font-family:Helvetica, Arial, sans-serif; font-size:11px; line-height:150%; padding-right:20px; padding-bottom:5px; padding-left:20px; text-align:center;\">\r\n                                        This email was sent to <a href=\"mailto:spang@mit.edu\" target=\"_blank\" style=\"color:#404040 !important;\">spang@mit.edu</a>\r\n                                        <br />\r\n                                        <a href=\"http://mit.us2.list-manage2.com/about?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8&c=c94d4a5dcf\" target=\"_blank\" style=\"color:#404040 !important;\"><em>why did I get this?</em></a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"http://mit.us2.list-manage.com/unsubscribe?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8&c=c94d4a5dcf\" style=\"color:#404040 !important;\">unsubscribe from this list</a>&nbsp;&nbsp;&nbsp;&nbsp;<a href=\"http://mit.us2.list-manage2.com/profile?u=9a8caadec06a0edf712df1a5e&id=cdbb4f57b1&e=46b88902b8\" style=\"color:#404040 !important;\">update subscription preferences</a>\r\n                                        <br />\r\n                                        MIT Music and Theater Arts &middot; 77 Massachusetts Avenue &middot; 4-243 &middot; Cambridge, Massachusetts 02139 &middot; USA\r\n                                        <br />\r\n                                        <br />\r\n\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n                        </td>\r\n                    </tr>\r\n                </table>\r\n                <style type=\"text/css\">\r\n                    @media only screen and (max-width: 480px){\r\n                        table#canspamBar td{font-size:14px !important;}\r\n                        table#canspamBar td a{display:block !important; margin-top:10px !important;}\r\n                    }\r\n                </style>\r\n            </center><img src=\"http://mit.us2.list-manage.com/track/open.php?u=9a8caadec06a0edf712df1a5e&id=c94d4a5dcf&e=46b88902b8\" height=\"1\" width=\"1\"></body>\r\n</html>\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/mit_events.txt",
    "content": "Upcoming Events December 07, 2016 | 5:00pm Jacob Collier The Creative Process of Jacob Collier This lecture\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/personal_capital.html",
    "content": "\r\n<!DOCTYPE html>\r\n<html lang=\"en\">\r\n\r\n<head>\r\n    <meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\" />\r\n    \r\n    <title>Personal Capital</title>\r\n    <style>\r\n        \r\n        body {margin:0; padding:0; -webkit-text-size-adjust:none; -ms-text-size-adjust:none;} img{line-height:100%; outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;} a img{border: none;} #backgroundTable {margin:0; padding:0; width:100% !important; } a, a:link{color:#2A5DB0; text-decoration: underline;} table td {border-collapse:collapse;} span {color: inherit; border-bottom: none;} span:hover { background-color: transparent; }\r\n\r\n        @media only screen{td[class=body] img{max-width:100% !important;height:auto !important}td[class=body] .blockquote .blockquote-inner{padding:0 0 15px 0 !important;margin-top:-40px;display:block}td[class=body] .blockquote.person{padding:30px 20px 20px !important}td[class=body] .blockquote div{width:58px;height:58px;overflow:hidden;border-radius:50%}td[class=body] .blockquote div img{min-width:92px !important;margin:-17px}td[class=body] .blockquote .title{padding:0 0 2px 10px !important}td[class=body] .blockquote .quote{padding:0 !important}}@media only screen and (max-width: 690px){td[class=body] .blockquote{padding:0 20px}td[class=body] .spacer{display:none}}@media only screen and (max-width: 500px){td[class=body]\r\n .col{display:block;width:100%;text-align:left;-moz-box-sizing:border-box;-webkit-box-sizing:border-box;box-sizing:border-box}td[class=body] .reverse-col-a{display:table-footer-group}td[class=body] .reverse-col-b{display:table-header-group}td[class=body] .footer .reverse-col-b img{margin:-27px auto 20px !important}td[class=body] .footer .col{text-align:center}td[class=body] .hero{background-size:100% auto}td[class=body] .hero-1,td[class=body] .hero-2,td[class=body] .hero-3{height:auto !important}td[class=body] .hero-1 .col:first-child,td[class=body] .hero-2 .col:first-child,td[class=body] .hero-3 .col:first-child{margin-bottom:25px}td[class=body] .hero-1 .col:first-child td,td[class=body] .hero-2 .col:first-child td,td[class=body] .hero-3 .col:first-child td{padding:0 25px\r\n !important}td[class=body] .hero-2 .content-wrapper{display:block;width:100%;height:auto !important;padding:30px 0}td[class=body] .hero-2 .content-wrapper td{padding-left:25px !important;padding-right:25px !important}td[class=body] .hero-3 .col:first-child td,td[class=body] .hero-3 .col td{padding:0 !important}td[class=body] .blockquote.person{display:block;padding-bottom:20px !important;margin:0 20px}td[class=body] .icon-copy{padding:26px 32px !important}td[class=body] .icon-copy img{margin:0 auto;width:auto !important}td[class=body] .icon-copy .col:first-child{margin-bottom:20px}td[class=body] .img-copy .col td,td[class=body] .img-copy .reverse-col-a td,td[class=body] .img-copy .reverse-col-b td{padding-right:0 !important}td[class=body] .img-copy .hero-img{margin:0 auto}td[class=body]\r\n .img-copy .hero-img td{padding-left:0 !important}td[class=body] .img-copy .reverse-col-b .hero-img{margin-bottom:20px}td[class=body] .article td{padding:30px 25px 0 !important}td[class=body] .callout{padding:35px 25px 40px !important}td[class=body] .callout .col:first-child{margin-bottom:25px}}@media only screen and (max-width: 500px) and (max-width: 350px){td[class=body] .hero{background-size:auto 100%}}\r\n    </style>\r\n</head><body style=\"background: #f1f1f1;font-family:Arial, Helvetica, sans-serif; font-size:1em;\">\r\n\r\n\r\n    <table id=\"backgroundTable\" width=\"100%\" cellspacing=\"0\" cellpadding=\"0\" border=\"0\" style=\"background:#f1f1f1;\">\r\n        <tr>\r\n            <td class=\"body\" align=\"center\" valign=\"top\" style=\"background:#3b3b3b; padding: 0 25px;\" width=\"100%\">\r\n                <table cellpadding=\"0\" cellspacing=\"0\">\r\n                    <tr>\r\n                        <td class=\"header\" width=\"650\" style=\"padding: 12px 0;\">\r\n                            <table cellpadding=\"0\" cellspacing=\"0\">\r\n                                <tr>\r\n                                    <td width=\"475\" align=\"left\">\r\n                                        <a href=\"http://click.email.personalcapital.com/?qs=5e2e9ec62f5a619453c81d9858ed0c4bc0f0c96a61d957d60db6891db41d501a6a9b9ca79c29c1df\"><img src=\"http://image.email.personalcapital.com/lib/fe92127274650d7c7c/m/1/pc_template_03.gif\" alt=\"Personal Capital\" style=\"display: block; border: 0;\" /></a>\r\n                                    </td>\r\n                                    <td width=\"175\" align=\"right\">\r\n                                        <table cellpadding=\"0\" cellspacing=\"0\">\r\n                                            <tr>\r\n                                                <td style=\"font-family: helvetica,arial,sans-serif; font-size: 14px; font-weight: bold; border: 1px solid #666666; border-radius: 6px;\">\r\n                                                    <a href=\"http://click.email.personalcapital.com/?qs=70fbcba67c989d8e817eb925144b7577d80a1abeceaf0e574ad2440f255b92a68778a149eacf072d\" style=\"color: #fff; text-decoration: none; display: inline-block; border-color: #3b3b3b; border-style: solid; border-top-width: 5px; border-bottom-width: 5px; border-left-width: 10px; border-right-width: 10px; border-radius: 10px;\">\r\n                                                        Sign In\r\n                                                    </a>\r\n                                                </td>\r\n                                            </tr>\r\n                                        </table>\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n                        </td>\r\n                    </tr>\r\n                </table>\r\n            </td>\r\n        </tr>\r\n        <tr>\r\n            <td class=\"body\" align=\"center\" valign=\"top\" style=\"background:#fafafa;\" width=\"100%\">\r\n                <table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" align=\"center\">\r\n    <tr>\r\n        <td align=\"center\">\r\n            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" bordercolor=\"\" width=\"100%\" bgcolor=\"\"><tr><td><table  width=\"100%\" bgcolor=\"\" border=\"0\" bordercolor=\"\" cellpadding=\"0\" cellspacing=\"0\"><tr><td style=\"font-family:Arial; font-size:13px\"><table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" align=\"center\">\r\n\t<tbody>\r\n\t\t<tr>\r\n\t\t\t<td align=\"center\" valign=\"top\" style=\"background-color: #000000;\" width=\"100%\">\r\n\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\r\n\t\t\t\t<tbody>\r\n\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t<td class=\"hero hero-3\" width=\"650\" align=\"center\" valign=\"bottom\">\r\n\t\t\t\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\r\n\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t<td align=\"center\" style=\"padding: 30px 35px; background-color: rgba(0, 0, 0, 0.74902);\">\r\n\t\t\t\t\t\t\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\r\n\t\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t<td class=\"col\" width=\"369\" align=\"left\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica, arial, sans-serif; font-size: 24px; color: #ffffff; padding: 0px 30px 0px 0px; line-height: 32px !important;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tYour November Statement is Ready to View</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t<td class=\"spacer\" width=\"1\" valign=\"middle\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t<img src=\"http://image.email.personalcapital.com/lib/fe92127274650d7c7c/m/1/pc_template_02.png\" alt=\"\" style=\"display: block; border: 0px;\" />\r\n\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t<td class=\"col\" width=\"210\" align=\"right\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"right\" style=\"font-family: helvetica, arial, sans-serif; font-size: 18px; color: #ffffff; padding: 0px 0px 0px 25px;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a href=\"http://click.email.personalcapital.com/?qs=5e2e9ec62f5a619408334c8b3ace99afbdd5ac855166e055844039fc27920a4386676a56a34c377f\" style=\"text-decoration: none; color: #ffffff; border-color: #3b8fe1; border-style: solid; border-width: 10px 28px; border-radius: 4px; display: inline-block; background-color: #3b8fe1;\">Get Report</a>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t</td>\r\n\t\t\t\t\t</tr>\r\n\t\t\t\t</tbody>\r\n\t\t\t</table>\r\n\t\t\t</td>\r\n\t\t</tr>\r\n\t</tbody>\r\n</table></td></tr></table></td></tr></table>\r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            <table cellpadding=\"0\" cellspacing=\"0\" border=\"0\" bordercolor=\"\" width=\"100%\" bgcolor=\"\"><tr><td><table  width=\"100%\" bgcolor=\"\" border=\"0\" bordercolor=\"\" cellpadding=\"0\" cellspacing=\"0\"><tr><td style=\"font-family:Arial; font-size:13px\"><table cellpadding=\"0\" cellspacing=\"0\" class=\"dashedBorder\" align=\"center\">\r\n\t<tbody>\r\n\t\t<tr>\r\n\t\t\t<td class=\"article\" width=\"650\" align=\"left\">\r\n\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\r\n\t\t\t\t<tbody>\r\n\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica, arial, sans-serif; font-size: 16px; color: #4c4c4c; padding: 30px 35px 0px; line-height: 24px !important;\"><i>\r\n\t\t\t\t\t\tWe are pleased to deliver your November month-end snapshot report. By clicking on the button above, you can view, download, or print your current November month-end report and previous month-end reports. If you'd like to discuss the report or any other financial planning topics, please schedule some time <a href=\"http://click.email.personalcapital.com/?qs=70fbcba67c989d8eec0451d6dbbc18d9c10f0b733f4221f26f13c251d4e5033c51c4fb9077c74de8\" target=\"_blank\" style=\"color: #3b8fe1; text-decoration: none;\">directly on my calendar</a>.\r\n\t\t\t\t\t\t</i>\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<strong>November Recap</strong>\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\tDonald Trump won the US presidential election, surprising most analysts. Stocks initially fell in November before quickly rebounding on hopes that Trump's policies will boost the economy. US stocks finished the month with solid gains, but all other major asset classes lost value. \r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\tWith one month to go in 2016, US stocks are now in prime position to extend their reign of asset class domination. US stocks have been either the best or second best performing asset class in six of the past seven years. Historically, streaks of this magnitude and duration by either US or International Stocks are not uncommon, but they never last forever. In our view, the longer this trend lasts, the more attractive a diversified approach with periodic rebalancing becomes on a forward-looking basis.  \r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\tSolid US economic growth data this month leaves the Fed squarely on track to raise interest rates in December. The short-term impact has been a boost to the dollar, which has been one factor in recent US stock leadership. This environment also puts pressure on bonds, with the Barclays Global Aggregate Bond Index suffering its worst monthly loss since its inception in 1990. Bonds in our managed portfolios held up better than the major aggregate indexes. TIPS, high yield corporate bonds and a modestly lower effective duration all helped. \r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\tBased on current economic data, we expect the Fed to raise rates this month, and more than likely rates will be raised at least twice in 2017. However, as long as the hikes remain spaced out and in small denominations, they shouldn't derail economic growth and will create only minor headwinds for stocks and bonds.  \r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\tDiversified Personal Capital managed accounts performed well compared to most globally diversified approaches in November. Within US stocks, small cap exposure helped our more evenly weighted US equity approach to modestly outperform major capitalization weighted indexes in most portfolios. \r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\tAs always, let me know if there is anything we can do or if you have any questions. \r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\tGreg\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t<p>\r\n\t\t\t\t\t\t</p>\r\n\t\t\t\t\t\t</td>\r\n\t\t\t\t\t</tr>\r\n\t\t\t\t</tbody>\r\n\t\t\t</table>\r\n\t\t\t</td>\r\n\t\t</tr>\r\n\t</tbody>\r\n</table></td></tr></table></td></tr></table>\r\n        </td>\r\n    </tr>   \r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            <table width=\"100%\" align=\"center\" cellspacing=\"0\" cellpadding=\"0\" class=\"dashedBorder\">\r\n\t<tbody>\r\n\t\t<tr>\r\n\t\t\t<td style=\"padding-top: 15px;\">&nbsp;</td>\r\n\t\t</tr>\r\n\t\t<tr>\r\n\t\t\t<td width=\"100%\" valign=\"top\" align=\"center\" style=\"background-color: #3b8fe1;\">\r\n\t\t\t<table cellspacing=\"0\" cellpadding=\"0\">\r\n\t\t\t\t<tbody>\r\n\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t<td width=\"650\">\r\n\t\t\t\t\t\t\t<table cellspacing=\"0\" cellpadding=\"0\">\r\n\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t<td class=\"callout\" style=\"padding: 60px 25px 60px;\">\r\n\t\t\t\t\t\t\t\t\t\t\t<!-- Service Advisor -->\r\n\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" cellpadding=\"0\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<!-- Advisor 2 Left Col -->\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td width=\"120\" align=\"left\" class=\"col\" style=\"padding: 30px;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img alt=\"Greg DePalma\" src=\"https://d1q4amq3lgzrzf.cloudfront.net/advisor/images/005F00000045DtMIAU.jpg\" />\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<!-- Advisor 2 Right Col -->\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td width=\"530\" align=\"left\" class=\"col\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" cellpadding=\"0\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica,arial,sans-serif; font-size: 16px; color: #ffffff; font-weight: 100;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tYour Advisor\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica,arial,sans-serif; font-size: 36px; color: #ffffff; padding-bottom: 10px; font-weight: 100;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tGreg DePalma\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica,arial,sans-serif; font-size: 16px; color: #ffffff; font-weight: 100; line-height: 20px;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tCall: 855-855-8143<br />\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tEmail: <a style=\"color: #ffffff; text-decoration: underline;\" href=\"mailto:gregory.depalma@personalcapital.com\">gregory.depalma@personalcapital.com</a><br />\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a style=\"color: #ffffff; text-decoration: underline;\" href=\"http://click.email.personalcapital.com/?qs=70fbcba67c989d8eec0451d6dbbc18d9c10f0b733f4221f26f13c251d4e5033c51c4fb9077c74de8\">Schedule a call with Greg</a>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\r\n\r\n\t\t\t\t\t\t\t<table cellspacing=\"0\" cellpadding=\"0\">\r\n\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t<td class=\"callout\" style=\"padding: 30px 25px 30px;\">\r\n\t\t\t\t\t\t\t\t\t\t<!-- Advisor -->\r\n\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" cellpadding=\"0\">\r\n\t\t\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<!-- Advisor 1 Left Col -->\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td width=\"120\" align=\"left\" class=\"col\" style=\"padding: 30px;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<img alt=\"Garrett Gunberg\" src=\"https://home.personalcapital.com/advisor/images/005F0000001EF0oIAG.jpg\" />\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t<td width=\"530\" align=\"left\" class=\"col\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<!-- Advisor 1 Right Col -->\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t<table cellspacing=\"0\" cellpadding=\"0\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica,arial,sans-serif; font-size: 16px; color: #ffffff; font-weight: 100;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tYour Advisor\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica,arial,sans-serif; font-size: 36px; color: #ffffff; padding-bottom: 10px; font-weight: 100;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tGarrett Gunberg\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<td align=\"left\" style=\"font-family: helvetica,arial,sans-serif; font-size: 16px; color: #ffffff; font-weight: 100; line-height: 20px;\">\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tCall: 855-855-7974<br />\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\tEmail: <a style=\"color: #ffffff; text-decoration: underline;\" href=\"mailto:garrett.gunberg@personalcapital.com\">garrett.gunberg@personalcapital.com</a><br />\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t<a style=\"color: #ffffff; text-decoration: underline;\" href=\"http://click.email.personalcapital.com/?qs=70fbcba67c989d8eafcc2999101e78ce966aabaf92646bffd802534a7027a7b54fb902f71d4109d1\">Schedule a call with Garrett</a>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\t\t\t</td>\r\n\t\t\t\t\t\t\t\t\t</tr>\r\n\t\t\t\t\t\t\t\t</tbody>\r\n\t\t\t\t\t\t\t</table>\r\n\t\t\t\t\t\t\t\r\n\r\n\t\t\t\t\t\t</td>\r\n\t\t\t\t\t</tr>\r\n\t\t\t\t</tbody>\r\n\t\t\t</table>\r\n\t\t\t</td>\r\n\t\t</tr>\r\n\t</tbody>\r\n</table>\r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n<table cellpadding=\"0\" cellspacing=\"0\" class=\"dashedBorder\" align=\"center\">\r\n\t<tbody>\r\n\t\t<tr>\r\n\t\t\t<td width=\"650\" align=\"center\">\r\n\t\t\t<table cellpadding=\"0\" cellspacing=\"0\">\r\n\t\t\t\t<tbody>\r\n\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t<td align=\"center\" style=\"font-family: helvetica, arial, sans-serif; font-size: 36px; color: #000001; padding: 30px 25px 0px;\">\r\n\t\t\t\t\t\t$3 Billion\r\n\t\t\t\t\t\t</td>\r\n\t\t\t\t\t</tr>\r\n\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t<td align=\"center\" style=\"font-family: helvetica, arial, sans-serif; font-size: 23px; color: #4c4c4c; padding: 8px 25px 0px;\">\r\n\t\t\t\t\t\tIn Assets Under Management\r\n\t\t\t\t\t\t</td>\r\n\t\t\t\t\t</tr>\r\n\t\t\t\t\t<tr>\r\n\t\t\t\t\t\t<td align=\"center\" style=\"font-family: helvetica, arial, sans-serif; font-size: 16px; color: #4c4c4c; padding: 12px 25px 0px; line-height: 24px !important;\">\r\n\t\t\t\t\t\tHundreds of thousands of families across the country are using Personal Capital to manage their money. We now manage over $3 billion in assets as an\r\n\t\t\t\t\t\t investment advisor, helping many of these families<br/>\r\n\t\t\t\t\t\t reach their long-term financial goals.   \t<br/>\r\n\t\t\t\t\t\t<br />\r\n\t\t\t\t\t\t\t\t\t\t\t\r\n\t\t\t\t\t\t</td>\r\n\t\t\t\t\t</tr>\r\n\t\t\t\t</tbody>\r\n\t\t\t</table>\r\n\t\t\t</td>\r\n\t\t</tr>\r\n\t</tbody>\r\n</table>\r\n        </td>\r\n    </tr>\r\n<tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n    <tr>\r\n        <td align=\"center\">\r\n            \r\n        </td>\r\n    </tr>\r\n</table>\r\n            </td>\r\n        </tr>\r\n        <tr>\r\n            <td class=\"body\" align=\"center\" valign=\"top\" style=\"background:#fafafa;\" width=\"100%\">\r\n                <table cellpadding=\"0\" cellspacing=\"0\">\r\n                    <tr>\r\n                        <td width=\"650\" align=\"center\" style=\"padding: 30px 0 40px;\">\r\n                            <img src=\"\" alt=\"\" style=\"display: block; border: 0;\" />\r\n                        </td>\r\n                    </tr>\r\n                </table>\r\n            </td>\r\n        </tr>\r\n        <tr>\r\n            <td class=\"body\" align=\"center\" valign=\"top\" style=\"background:#f1f1f1; padding: 0 20px; border-bottom: 10px solid #3b3b3b;\" width=\"100%\">\r\n                <table cellpadding=\"0\" cellspacing=\"0\">\r\n                    <tr>\r\n                        <td class=\"footer\" width=\"650\" align=\"center\" style=\"padding: 20px 0;\">\r\n                            <table cellpadding=\"0\" cellspacing=\"0\">\r\n                                <tr>\r\n                                    <td class=\"reverse-col-a\" width=\"300\" align=\"left\" valign=\"bottom\">\r\n                                        <table cellpadding=\"0\" cellspacing=\"0\">\r\n                                            <tr>\r\n                                                <td align=\"left\" style=\"padding: 0 0 15px;\">\r\n                                                    <a href=\"http://click.email.personalcapital.com/?qs=5e2e9ec62f5a619453c81d9858ed0c4bc0f0c96a61d957d60db6891db41d501a6a9b9ca79c29c1df\"><img src=\"http://image.email.personalcapital.com/lib/fe92127274650d7c7c/m/1/pc_template_09.gif\" alt=\"Personal Capital\" style=\"display: block; border: 0;\" /></a>\r\n                                                </td>\r\n                                            </tr>\r\n                                            <tr>\r\n                                                <td align=\"left\" style=\"font-family: helvetica,arial,sans-serif; font-size: 12px; line-height: 22px !important; color: #999999;\">\r\n                                                    Personal Capital is the smart way for you to understand, manage and grow your net worth.\r\n                                                </td>\r\n                                            </tr>\r\n                                        </table>\r\n                                    </td>\r\n                                    <td class=\"reverse-col-b\" width=\"350\" align=\"right\" valign=\"top\">\r\n                                        <img src=\"http://image.email.personalcapital.com/lib/fe92127274650d7c7c/m/1/MultiDeviceForTemplate1.png\" alt=\"\" style=\"display: block; border: 0; margin-top: -27px;\" />\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n                            <table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\r\n                                <tr>\r\n                                    <td height=\"1\" valign=\"middle\" style=\"padding: 20px 0;\">\r\n                                        <div style=\"width: 100%; height: 1px; background: #d5d5d5; font-size: 0;\">&nbsp;</div>\r\n                                    </td>\r\n                                    <td width=\"150\" align=\"center\" style=\"padding: 20px 0;\">\r\n                                        <table cellpadding=\"0\" cellspacing=\"0\">\r\n                                            <tr>\r\n                                                <td style=\"padding: 0 3px;\">\r\n                                                    <a href=\"http://click.email.personalcapital.com/?qs=5e2e9ec62f5a619467bdabdc8f81d85368f4d5b20b96ce558c7078bbf252a685afbb0b6db2c884f8\"><img src=\"http://image.email.personalcapital.com/lib/fe92127274650d7c7c/m/1/pc_template_11.gif\" alt=\"Facebook\" style=\"display: block; border: 0;\" /></a>\r\n                                                </td>\r\n                                                <td style=\"padding: 0 3px;\">\r\n                                                    <a href=\"http://click.email.personalcapital.com/?qs=5e2e9ec62f5a6194ec23b3d52c308fcf8efc8084d080381decbbf66166d32468b89241bbf93bc29a\"><img src=\"http://image.email.personalcapital.com/lib/fe92127274650d7c7c/m/1/pc_template_13.gif\" alt=\"Twitter\" style=\"display: block; border: 0;\" /></a>\r\n                                                </td>\r\n                                              \r\n                                                <td style=\"padding: 0 3px;\">\r\n                                                    <a href=\"http://click.email.personalcapital.com/?qs=5e2e9ec62f5a6194837f85f63b98f53d9c160ebb438009c1829a4f980db8ebc8f1974d2d5bc06ae0\"><img src=\"http://image.email.personalcapital.com/lib/fe92127274650d7c7c/m/1/pc_template_17.gif\" alt=\"LinkedIn\" style=\"display: block; border: 0;\" /></a>\r\n                                                </td>\r\n                                    \r\n                                            </tr>\r\n                                        </table>\r\n                                    </td>\r\n                                    <td height=\"1\" valign=\"middle\" style=\"padding: 20px 0;\">\r\n                                        <div style=\"width: 100%; height: 1px; background: #d5d5d5; font-size: 0;\">&nbsp;</div>\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n                            <table cellpadding=\"0\" cellspacing=\"0\" width=\"100%\" style=\"border-bottom:solid 1px #ccc;padding-bottom:20px;\">\r\n                                <tr>\r\n                                    <td class=\"col\" width=\"33.33%\" align=\"center\" valign=\"top\" style=\"padding: 15px 7px; font-family: helvetica,arial,sans-serif; font-size: 12px; line-height: 22px !important; color: #999999;\">\r\n                                        <strong>Silicon Valley</strong><br />\r\n                                        1 Circle Star Way, Suite 189<br />\r\n                                        San Carlos, California 94070\r\n                                    </td>\r\n                                    <td class=\"col\" width=\"33.33%\" align=\"center\" valign=\"top\" style=\"padding: 15px 7px; font-family: helvetica,arial,sans-serif; font-size: 12px; line-height: 22px !important; color: #999999;\">\r\n                                        <strong>San Francisco</strong><br />\r\n                                        500 Howard Street, Suite 400<br />\r\n                                        San Francisco, California 94105\r\n                                    </td>\r\n                                    <td class=\"col\" width=\"33.33%\" align=\"center\" valign=\"top\" style=\"padding: 15px 7px; font-family: helvetica,arial,sans-serif; font-size: 12px; line-height: 22px !important; color: #999999;\">\r\n                                        <strong>Denver</strong><br />\r\n                                        999 18th Street, Suite 800s<br />\r\n                                        Denver, Colorado 80202\r\n                                    </td>\r\n                                </tr>\r\n                            </table>\r\n<p style=\"color:#999;font-family:Helvetica,Arial,sans-serif;font-size:10px;margin-top:20px;\">\r\n\tToo much email? <a href=\"http://click.email.personalcapital.com/profile_center.aspx?qs=f5a10185ba21f8d4dc8e57a5cf3fe0d3ef10d88be8ccc5a37aa9b725547c494d6bf17b75e3bf3f9007562116d1fbb52fa922d53ae2cb4db3762c53825893aca7\" target=\"_blank\" style=\"color:#3b8fe1;\">Change your preferences</a> or <a href=\"http://click.email.personalcapital.com/unsub_center.aspx?qs=f5a10185ba21f8d489281ffd4bcc66c51cf05099b8669555bdb8b63ed4b9b28d30b21c8ff8adb25cac589d49d3a475aac0bef732923b4003cdc4940460c34ceb\" target=\"_blank\" style=\"color:#3b8fe1;\">unsubscribe</a>\r\n</p>\r\n                        </td>\r\n                    </tr>\r\n                </table>\r\n            </td>\r\n        </tr>\r\n    </table>\r\n<div style=\"display:none; white-space:nowrap; font:15px courier; line-height:0;\">\r\n  &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \r\n  &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; \r\n  &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;\r\n</div>\r\n    <!-- Exact Target tracking code -->\r\n    <img src=\"http://click.email.personalcapital.com/open.aspx?ffcb10-fe9d16797765047f77-fe3217707162047e711677-fe92127274650d7c7c-fec8167072610575-fe5b10747c6c077d7611-ff991072\" width=\"1\" height=\"1\">\r\n\r\n\r\n<!--\r\nX-Return-Path: bounce-207518_HTML-44983035-2921022-6211919-42@bounce.email.personalcapital.com\r\n-->\r\n</body></html>\r\n\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/MessageUtils/parseSnippet/personal_capital.txt",
    "content": "Sign In Your November Statement is Ready to View Get Report We are pleased to deliver your November month-end\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/Threading/remote-thread-id-no.js",
    "content": "module.exports = {\n  A: {\n    id: 1,\n    accountId: 'test-account-id',\n    subject: \"This is an email\",\n    body: \"<head></head><body>Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div><!-- <signature> -->Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div><!-- </signature> --></div></div><img class=\\\"n1-open\\\" width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></body>\",\n    headers: {\n      \"x-gm-thrid\": \"GMAILTHREAD1\",\n      \"Delivered-To\": \"jackiehluo@gmail.com\",\n      \"Date\": \"Fri, 17 Jun 2016 09:38:44 -0700 (PDT)\",\n      \"Message-Id\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"82y7eq1ipmadaxwcy6kr072bw-2147483647\",\n    },\n    from: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    to: [{\n      name: \"jackiehluo@gmail.com\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Hi Jackie, While browsing Nylas themes, I stumbled upon your website and looked at your work. Great \",\n  },\n  B: {\n    id: 2,\n    accountId: 'test-account-id',\n    subject: \"This is a reply with a different subject\",\n    body: \"<head></head><body>Sagar,<div><div><br></div><div>Aw, glad to hear it! Thanks for getting in touch!</div><br><!-- <signature> -->Jackie Luo<div>Software Engineer, Nylas</div><br><!-- </signature> --></div><div class=\\\"gmail_quote\\\">On Jun 17 2016, at 9:38 am, Sagar Sutar &lt;sagar_s@nid.edu&gt; wrote:<br><blockquote class=\\\"gmail_quote\\\" style=\\\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\\\">Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div>Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div></div></div><img width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></blockquote></div></body>\",\n    headers: {\n      \"x-gm-thrid\": \"GMAILTHREAD2\",\n      \"Date\": \"Fri, 17 Jun 2016 18:20:47 +0000\",\n      \"References\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"In-Reply-To\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"Message-Id\": \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"cq08iqwatp00kai4qnff7zbaj-2147483647\",\n    },\n    from: [{\n      name: \"Jackie Luo\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    to: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Sagar, Aw, glad to hear it! Thanks for getting in touch! Jackie Luo Software Engineer, Nylas\",\n  },\n};\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/Threading/remote-thread-id-yes.js",
    "content": "module.exports = {\n  A: {\n    id: 1,\n    accountId: 'test-account-id',\n    subject: \"This is an email\",\n    body: \"<head></head><body>Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div><!-- <signature> -->Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div><!-- </signature> --></div></div><img class=\\\"n1-open\\\" width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></body>\",\n    headers: {\n      \"x-gm-thrid\": \"GMAILTHREAD1\",\n      \"Delivered-To\": \"jackiehluo@gmail.com\",\n      \"Date\": \"Fri, 17 Jun 2016 09:38:44 -0700 (PDT)\",\n      \"Message-Id\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"82y7eq1ipmadaxwcy6kr072bw-2147483647\",\n    },\n    from: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    to: [{\n      name: \"jackiehluo@gmail.com\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Hi Jackie, While browsing Nylas themes, I stumbled upon your website and looked at your work. Great \",\n  },\n  B: {\n    id: 2,\n    accountId: 'test-account-id',\n    subject: \"This is a reply with a different subject\",\n    body: \"<head></head><body>Sagar,<div><div><br></div><div>Aw, glad to hear it! Thanks for getting in touch!</div><br><!-- <signature> -->Jackie Luo<div>Software Engineer, Nylas</div><br><!-- </signature> --></div><div class=\\\"gmail_quote\\\">On Jun 17 2016, at 9:38 am, Sagar Sutar &lt;sagar_s@nid.edu&gt; wrote:<br><blockquote class=\\\"gmail_quote\\\" style=\\\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\\\">Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div>Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div></div></div><img width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></blockquote></div></body>\",\n    headers: {\n      \"x-gm-thrid\": \"GMAILTHREAD1\",\n      \"Date\": \"Fri, 17 Jun 2016 18:20:47 +0000\",\n      \"References\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"In-Reply-To\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"Message-Id\": \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"cq08iqwatp00kai4qnff7zbaj-2147483647\",\n    },\n    from: [{\n      name: \"Jackie Luo\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    to: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Sagar, Aw, glad to hear it! Thanks for getting in touch! Jackie Luo Software Engineer, Nylas\",\n  },\n};\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/Threading/subject-matching-no.js",
    "content": "module.exports = {\n  A: {\n    id: 1,\n    accountId: 'test-account-id',\n    subject: \"Loved your work and interests\",\n    body: \"<head></head><body>Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div><!-- <signature> -->Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div><!-- </signature> --></div></div><img class=\\\"n1-open\\\" width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></body>\",\n    headers: {\n      \"Delivered-To\": \"jackiehluo@gmail.com\",\n      \"Received-SPF\": `pass (google.com: domain of sagy26.1991@gmail.com\n        designates 209.85.192.174 as permitted sender) client-ip=209.85.192.174;`,\n      \"Authentication-Results\": `mx.google.com;\n         spf=pass (google.com: domain of sagy26.1991@gmail.com designates\n         209.85.192.174 as permitted sender) smtp.mailfrom=sagy26.1991@gmail.com`,\n      \"X-Google-DKIM-Signature\": `v=1; a=rsa-sha256; c=relaxed/relaxed;\n          d=1e100.net; s=20130820;\n          h=x-gm-message-state:date:user-agent:message-id:to:from:subject\n           :mime-version;\n          bh=to3fCB9g4R6V18kpAAKSAlUeTC+N0rg4JckFbiaILA4=;\n          b=WfI5viTYPjviUur9Bd2rJQfpHxIm2xYRdxrN64bJGuX0TQlb7p8bDvCBNNhY3mTXJx\n           lsQzRX9RA4FMuDk0oz0mpviWtkpkZsDeyjpSmA+ONcPgdyPAezzPDvSWRzMZY21fiHxS\n           hr4I5AeFKesGcbvwtJu+S0fMGhdveC8E35oTA010Xfave6Xd55qGXy7hW+4xCfvIesy4\n           01oOaXWDmLHqixKO3SXwmGCcDzqn/IKXhB7UXkF0efSTwh8yid6v9iXdW+ovJ2qg9peI\n           HSnPIilYk8SaKoPdGDgYZykfUIgNrSugtK/vvGG2aN+9lhURxPfzhniWdNqdsgR7G4E7\n           7XqA==`,\n      \"X-Gm-Message-State\": \"ALyK8tIf7XyYaylyVf0qjzh8rhYz3rj/VQYaNLDjVq5ESH19ioJIgW7o9FbghP+wFYrBuw==\",\n      \"X-Received\": `by 10.98.111.138 with SMTP id k132mr3246291pfc.105.1466181525186;\n          Fri, 17 Jun 2016 09:38:45 -0700 (PDT)`,\n      \"Return-Path\": \"<sagy26.1991@gmail.com>\",\n      \"Received\": `from [127.0.0.1] (ec2-52-36-99-221.us-west-2.compute.amazonaws.com. [52.36.99.221])\n          by smtp.gmail.com with ESMTPSA id d69sm64179062pfj.31.2016.06.17.09.38.44\n          for <jackiehluo@gmail.com>\n          (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);\n          Fri, 17 Jun 2016 09:38:44 -0700 (PDT)`,\n      \"Date\": \"Fri, 17 Jun 2016 09:38:44 -0700 (PDT)\",\n      \"User-Agent\": \"NylasMailer/0.4\",\n      \"Message-Id\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"82y7eq1ipmadaxwcy6kr072bw-2147483647\",\n    },\n    from: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    to: [{\n      name: \"jackiehluo@gmail.com\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Hi Jackie, While browsing Nylas themes, I stumbled upon your website and looked at your work. Great \",\n  },\n  B: {\n    id: 2,\n    accountId: 'test-account-id',\n    subject: \"Re: Another unrelated email\",\n    body: \"<head></head><body>Sagar,<div><div><br></div><div>Aw, glad to hear it! Thanks for getting in touch!</div><br><!-- <signature> -->Jackie Luo<div>Software Engineer, Nylas</div><br><!-- </signature> --></div><div class=\\\"gmail_quote\\\">On Jun 17 2016, at 9:38 am, Sagar Sutar &lt;sagar_s@nid.edu&gt; wrote:<br><blockquote class=\\\"gmail_quote\\\" style=\\\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\\\">Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div>Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div></div></div><img width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></blockquote></div></body>\",\n    headers: {\n      \"Date\": \"Fri, 17 Jun 2016 18:20:47 +0000\",\n      \"References\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"In-Reply-To\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"User-Agent\": \"NylasMailer/0.4\",\n      \"Message-Id\": \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"cq08iqwatp00kai4qnff7zbaj-2147483647\",\n    },\n    from: [{\n      name: \"Jackie Luo\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    to: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Sagar, Aw, glad to hear it! Thanks for getting in touch! Jackie Luo Software Engineer, Nylas\",\n  },\n};\n"
  },
  {
    "path": "packages/client-sync/spec/fixtures/Threading/subject-matching-yes.js",
    "content": "module.exports = {\n  A: {\n    id: 1,\n    accountId: 'test-account-id',\n    subject: \"Loved your work and interests\",\n    body: \"<head></head><body>Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div><!-- <signature> -->Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div><!-- </signature> --></div></div><img class=\\\"n1-open\\\" width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></body>\",\n    headers: {\n      \"Delivered-To\": \"jackiehluo@gmail.com\",\n      \"Received-SPF\": `pass (google.com: domain of sagy26.1991@gmail.com\n        designates 209.85.192.174 as permitted sender) client-ip=209.85.192.174;`,\n      \"Authentication-Results\": `mx.google.com;\n         spf=pass (google.com: domain of sagy26.1991@gmail.com designates\n         209.85.192.174 as permitted sender) smtp.mailfrom=sagy26.1991@gmail.com`,\n      \"X-Google-DKIM-Signature\": `v=1; a=rsa-sha256; c=relaxed/relaxed;\n          d=1e100.net; s=20130820;\n          h=x-gm-message-state:date:user-agent:message-id:to:from:subject\n           :mime-version;\n          bh=to3fCB9g4R6V18kpAAKSAlUeTC+N0rg4JckFbiaILA4=;\n          b=WfI5viTYPjviUur9Bd2rJQfpHxIm2xYRdxrN64bJGuX0TQlb7p8bDvCBNNhY3mTXJx\n           lsQzRX9RA4FMuDk0oz0mpviWtkpkZsDeyjpSmA+ONcPgdyPAezzPDvSWRzMZY21fiHxS\n           hr4I5AeFKesGcbvwtJu+S0fMGhdveC8E35oTA010Xfave6Xd55qGXy7hW+4xCfvIesy4\n           01oOaXWDmLHqixKO3SXwmGCcDzqn/IKXhB7UXkF0efSTwh8yid6v9iXdW+ovJ2qg9peI\n           HSnPIilYk8SaKoPdGDgYZykfUIgNrSugtK/vvGG2aN+9lhURxPfzhniWdNqdsgR7G4E7\n           7XqA==`,\n      \"X-Gm-Message-State\": \"ALyK8tIf7XyYaylyVf0qjzh8rhYz3rj/VQYaNLDjVq5ESH19ioJIgW7o9FbghP+wFYrBuw==\",\n      \"X-Received\": `by 10.98.111.138 with SMTP id k132mr3246291pfc.105.1466181525186;\n          Fri, 17 Jun 2016 09:38:45 -0700 (PDT)`,\n      \"Return-Path\": \"<sagy26.1991@gmail.com>\",\n      \"Received\": `from [127.0.0.1] (ec2-52-36-99-221.us-west-2.compute.amazonaws.com. [52.36.99.221])\n          by smtp.gmail.com with ESMTPSA id d69sm64179062pfj.31.2016.06.17.09.38.44\n          for <jackiehluo@gmail.com>\n          (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);\n          Fri, 17 Jun 2016 09:38:44 -0700 (PDT)`,\n      \"Date\": \"Fri, 17 Jun 2016 09:38:44 -0700 (PDT)\",\n      \"User-Agent\": \"NylasMailer/0.4\",\n      \"Message-Id\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"82y7eq1ipmadaxwcy6kr072bw-2147483647\",\n    },\n    from: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    to: [{\n      name: \"jackiehluo@gmail.com\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Hi Jackie, While browsing Nylas themes, I stumbled upon your website and looked at your work. Great \",\n  },\n  B: {\n    id: 2,\n    accountId: 'test-account-id',\n    subject: \"Re: Loved your work and interests\",\n    body: \"<head></head><body>Sagar,<div><div><br></div><div>Aw, glad to hear it! Thanks for getting in touch!</div><br><!-- <signature> -->Jackie Luo<div>Software Engineer, Nylas</div><br><!-- </signature> --></div><div class=\\\"gmail_quote\\\">On Jun 17 2016, at 9:38 am, Sagar Sutar &lt;sagar_s@nid.edu&gt; wrote:<br><blockquote class=\\\"gmail_quote\\\" style=\\\"margin:0 0 0 .8ex;border-left:1px #ccc solid;padding-left:1ex;\\\">Hi Jackie,<div><div>While browsing Nylas&nbsp;themes, I stumbled upon your website and looked at your work.&nbsp;</div><div>Great work on projects, nice to see your multidisciplinary interests :)</div><div><div><br></div>Thanks,&nbsp;<div>Sagar Sutar</div><div>thesagarsutar.me</div></div></div><img width=\\\"0\\\" height=\\\"0\\\" style=\\\"border:0; width:0; height:0;\\\" src=\\\"https://link.nylas.com/open/8w734mdm7q9ivpc0cnq3ousy3/local-7b7d5479-575c?r=amFja2llaGx1b0BnbWFpbC5jb20=\\\"></blockquote></div></body>\",\n    headers: {\n      \"Date\": \"Fri, 17 Jun 2016 18:20:47 +0000\",\n      \"References\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"In-Reply-To\": \"<82y7eq1ipmadaxwcy6kr072bw-2147483647@nylas-mail.nylas.com>\",\n      \"User-Agent\": \"NylasMailer/0.4\",\n      \"Message-Id\": \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n      \"X-Inbox-Id\": \"cq08iqwatp00kai4qnff7zbaj-2147483647\",\n    },\n    from: [{\n      name: \"Jackie Luo\",\n      email: \"<jackiehluo@gmail.com>\",\n    }],\n    to: [{\n      name: \"Sagar Sutar\",\n      email: \"<sagar_s@nid.edu>\",\n    }],\n    cc: [],\n    bcc: [],\n    headerMessageId: \"<cq08iqwatp00kai4qnff7zbaj-2147483647@nylas-mail.nylas.com>\",\n    snippet: \"Sagar, Aw, glad to hear it! Thanks for getting in touch! Jackie Luo Software Engineer, Nylas\",\n  },\n};\n"
  },
  {
    "path": "packages/client-sync/spec/helpers.js",
    "content": "const fs = require('fs');\nconst path = require('path');\nconst LocalDatabaseConnector = require('../src/shared/local-database-connector')\n\nconst FIXTURES_PATH = path.join(__dirname, 'fixtures');\nconst ACCOUNT_ID = 'test-account-id';\n\nfunction forEachJSONFixture(relativePath, callback) {\n  const fixturesDir = path.join(FIXTURES_PATH, relativePath);\n  const filenames = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.json'));\n  filenames.forEach((filename) => {\n    const json = JSON.parse(fs.readFileSync(path.join(fixturesDir, filename)));\n    callback(filename, json);\n  });\n}\n\nfunction forEachHTMLAndTXTFixture(relativePath, callback) {\n  const fixturesDir = path.join(FIXTURES_PATH, relativePath);\n  const filenames = fs.readdirSync(fixturesDir).filter(f => f.endsWith('.html'));\n  filenames.forEach((filename) => {\n    const html = fs.readFileSync(path.join(fixturesDir, filename)).toString();\n    const basename = path.parse(filename).name;\n    const txt = fs.readFileSync(path.join(fixturesDir, `${basename}.txt`)).toString().replace(/\\n$/, '');\n    callback(filename, html, txt);\n  });\n}\n\nasync function getTestDatabase(accountId = ACCOUNT_ID) {\n  await LocalDatabaseConnector.ensureAccountDatabase(accountId)\n  return LocalDatabaseConnector.forAccount(accountId)\n}\n\nfunction destroyTestDatabase(accountId = ACCOUNT_ID) {\n  LocalDatabaseConnector.destroyAccountDatabase(accountId)\n}\n\nfunction mockImapBox() {\n  return {\n    setLabels: jasmine.createSpy('setLabels'),\n    removeLabels: jasmine.createSpy('removeLabels'),\n  }\n}\n\nconst silentLogger = {\n  info: () => {},\n  warn: () => {},\n  debug: () => {},\n  error: () => {},\n}\n\nmodule.exports = {\n  FIXTURES_PATH,\n  ACCOUNT_ID,\n  silentLogger,\n  forEachJSONFixture,\n  forEachHTMLAndTXTFixture,\n  mockImapBox,\n  getTestDatabase,\n  destroyTestDatabase,\n}\n"
  },
  {
    "path": "packages/client-sync/spec/local-sync-worker/imap-helpers-spec.es6",
    "content": "import {\n  ACCOUNT_ID,\n  mockImapBox,\n  getTestDatabase,\n} from '../helpers'\nimport IMAPHelpers from '../../src/local-sync-worker/imap-helpers'\n\nxdescribe('IMAPHelpers', function describeBlock() {\n  describe('setLabelsForMessages', () => {\n    beforeEach(async () => {\n      this.db = await getTestDatabase()\n      this.sentLabel = await this.db.Label.create({\n        id: 'sent',\n        accountId: ACCOUNT_ID,\n        name: '\\\\Sent',\n        role: 'sent',\n      })\n      this.l1 = await this.db.Label.create({\n        id: 'l1',\n        name: 'l1',\n        accountId: ACCOUNT_ID,\n      })\n      this.l2 = await this.db.Label.create({\n        id: 'l2',\n        name: 'l2',\n        accountId: ACCOUNT_ID,\n      })\n      this.l3 = await this.db.Label.create({\n        id: 'l3',\n        name: 'l3',\n        accountId: ACCOUNT_ID,\n      })\n\n      this.m1 = await this.db.Message.create({\n        id: 'm1',\n        folderImapUID: 1,\n        accountId: ACCOUNT_ID,\n      })\n      await this.m1.setLabels(['l1', 'l2'])\n      this.m1.labels = ['l1', 'l2']\n\n      this.m2 = await this.db.Message.create({\n        id: 'm2',\n        folderImapUID: 2,\n        accountId: ACCOUNT_ID,\n      })\n      await this.m2.setLabels(['l1', 'l2'])\n      this.m2.labels = ['l1', 'l2']\n\n      this.m3 = await this.db.Message.create({\n        id: 'm3',\n        folderImapUID: 3,\n        accountId: ACCOUNT_ID,\n      })\n      await this.m3.setLabels(['l1'])\n      this.m3.labels = ['l1']\n\n      this.messages = [this.m1, this.m2, this.m3]\n      const messagesByUID = {\n        1: this.m1,\n        2: this.m2,\n        3: this.m3,\n      }\n      this.box = mockImapBox()\n      this.box.removeLabels.andCallFake(async (uids, labelsToRemove) => {\n        if (!labelsToRemove || typeof labelsToRemove === 'string' || labelsToRemove.length === 0) {\n          throw new Error('labelsToRemove must be a non-empty array')\n        }\n        for (const uid of uids) {\n          const msg = messagesByUID[uid]\n          msg.labels = msg.labels.filter(l => !labelsToRemove.includes(l))\n        }\n      })\n      this.box.setLabels.andCallFake(async (uids, labelsToSet) => {\n        if (!labelsToSet || typeof labelsToSet === 'string' || labelsToSet.length === 0) {\n          throw new Error('labelsToSet must be a non-empty array')\n        }\n        for (const uid of uids) {\n          const msg = messagesByUID[uid]\n          msg.labels = labelsToSet\n        }\n      })\n    })\n\n    it('removes all labels for each message if labelIds is empty', async () => {\n      const labelIds = []\n      await IMAPHelpers.setLabelsForMessages({db: this.db, box: this.box, messages: this.messages, labelIds})\n      for (const msg of this.messages) {\n        expect(msg.labels.length).toBe(0)\n      }\n    });\n\n    it('does not remove the sent label from messages when removing all labels', async () => {\n      await this.m3.addLabel(this.sentLabel)\n      this.m3.labels.push(this.sentLabel.imapLabelIdentifier())\n\n      const labelIds = []\n      await IMAPHelpers.setLabelsForMessages({db: this.db, box: this.box, messages: this.messages, labelIds})\n\n      expect(this.m1.labels.length).toBe(0)\n      expect(this.m2.labels.length).toBe(0)\n      expect(this.m3.labels.length).toBe(1)\n      expect(this.m3.labels[0]).toEqual(this.sentLabel.imapLabelIdentifier())\n    });\n\n    it('does not try to remove labels if none present', async () => {\n      await this.m1.setLabels([])\n      this.m1.labels = []\n      await this.m3.setLabels([this.sentLabel])\n      this.m3.labels = [this.sentLabel.imapLabelIdentifier()]\n\n      const labelIds = []\n      await IMAPHelpers.setLabelsForMessages({\n        labelIds,\n        db: this.db,\n        box: this.box,\n        messages: [this.m1, this.m3],\n      })\n\n      expect(this.box.removeLabels).not.toHaveBeenCalled()\n      expect(this.m1.labels.length).toBe(0)\n      expect(this.m3.labels.length).toBe(1)\n      expect(this.m3.labels[0]).toEqual(this.sentLabel.imapLabelIdentifier())\n    });\n\n    it('sets the provided labels', async () => {\n      const labelIds = ['l1', 'l3']\n      await IMAPHelpers.setLabelsForMessages({db: this.db, box: this.box, messages: this.messages, labelIds})\n      for (const msg of this.messages) {\n        expect(msg.labels).toEqual = labelIds\n      }\n    });\n\n    it(`keeps the sent label on messages even if it wasn't provided in the labels to set`, async () => {\n      await this.m3.addLabel(this.sentLabel)\n      this.m3.labels.push(this.sentLabel.imapLabelIdentifier())\n\n      const labelIds = ['l1', 'l3']\n      await IMAPHelpers.setLabelsForMessages({db: this.db, box: this.box, messages: this.messages, labelIds})\n\n      expect(this.m1.labels).toEqual(labelIds)\n      expect(this.m2.labels).toEqual(labelIds)\n      expect(this.m3.labels).toEqual([...labelIds, this.sentLabel.imapLabelIdentifier()])\n    });\n\n    it(`does not attempt to add the sent label to messages even if the labels to set contain the sent label`, async () => {\n      const labelIds = ['l1', 'l3', this.sentLabel.imapLabelIdentifier()]\n      await IMAPHelpers.setLabelsForMessages({db: this.db, box: this.box, messages: this.messages, labelIds})\n      for (const msg of this.messages) {\n        expect(msg.labels).toEqual = labelIds.slice(0, 2)\n      }\n    })\n\n    it(`does not attempt to set labels if no labels to set`, async () => {\n      await this.m1.setLabels([])\n      this.m1.labels = []\n      const labelIds = [this.sentLabel.imapLabelIdentifier()]\n      await IMAPHelpers.setLabelsForMessages({db: this.db, box: this.box, messages: [this.m1], labelIds})\n      expect(this.box.removeLabels).not.toHaveBeenCalled()\n      expect(this.m1.labels.length).toBe(0)\n    })\n  });\n});\n"
  },
  {
    "path": "packages/client-sync/spec/local-sync-worker/sync-process-manager-spec.es6",
    "content": "import {Actions, IdentityStore} from 'nylas-exports'\nimport {createLogger} from '../../src/shared/logger'\nimport LocalDatabaseConnector from '../../src/shared/local-database-connector'\nimport SyncProcessManager from '../../src/local-sync-worker/sync-process-manager'\nimport SyncActivity from '../../src/shared/sync-activity'\n\ndescribe(\"SyncProcessManager\", () => {\n  beforeEach(async () => {\n    global.Logger = createLogger()\n    spyOn(IdentityStore, 'identity').andReturn(true)\n    const db = await LocalDatabaseConnector.forShared();\n    await db.Account.create({id: 'test-account'})\n  })\n  afterEach(async () => {\n    const db = await LocalDatabaseConnector.forShared();\n    const accounts = db.Account.findAll();\n    return Promise.all(accounts.map((account) => account.destroy()))\n  })\n  describe(\"when a sync worker is stuck\", () => {\n    beforeEach(() => {\n      spyOn(Actions, 'recordUserEvent')\n      spyOn(SyncProcessManager, 'removeWorkerForAccountId').andCallThrough()\n      spyOn(SyncProcessManager, 'addWorkerForAccount').andCallThrough()\n      spyOn(SyncActivity, 'getLastSyncActivityForAccount').andReturn({\n        time: Date.now() - 2 * SyncProcessManager.MAX_WORKER_SILENCE_MS,\n        activity: ['activity'],\n      })\n      // Make sure the health check interval isn't automatically started\n      SyncProcessManager._check_health_interval = 1\n    })\n    xit(\"detects it and recovers\", async () => {\n      await SyncProcessManager.start()\n      expect(SyncProcessManager.removeWorkerForAccountId.calls.length).toEqual(0)\n      expect(SyncProcessManager.addWorkerForAccount.calls.length).toEqual(1)\n\n      await SyncProcessManager._checkHealth()\n      expect(Actions.recordUserEvent.calls.length).toEqual(1)\n      expect(SyncProcessManager.removeWorkerForAccountId.calls.length).toEqual(1)\n      expect(SyncProcessManager.addWorkerForAccount.calls.length).toEqual(2)\n    })\n    xit(\"doesn't have zombie workers come back to life\", async () => {\n      await SyncProcessManager.start()\n\n      // Zombify a worker\n      const zombieSync = () => {\n        return new Promise(() => {}) // Never resolves\n      }\n      const zombieWorker = SyncProcessManager.workers()[0]\n      const origSync = zombieWorker.syncNow\n      zombieWorker.syncNow = zombieSync\n      zombieWorker.interrupt()\n      zombieWorker.syncNow()\n\n      // Make sure the worker is discarded by the manager\n      await SyncProcessManager._checkHealth()\n      expect(Actions.recordUserEvent.calls.length).toEqual(1)\n      expect(SyncProcessManager.removeWorkerForAccountId.calls.length).toEqual(1)\n      expect(SyncProcessManager.addWorkerForAccount.calls.length).toEqual(2)\n\n      // Try to get the zombie to sync again, check that it doesn't.\n      const lastStart = zombieWorker._syncStart;\n      zombieWorker.syncNow = origSync\n      zombieWorker.interrupt({reason: 'Playing Frankenstein'})\n      await zombieWorker.syncNow()\n      expect(zombieWorker._syncStart).toEqual(lastStart)\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-sync/spec/local-sync-worker/sync-tasks/fetch-folder-list-spec.js",
    "content": "\nconst FetchFolderList = require('../../../src/local-sync-worker/sync-tasks/fetch-folder-list.imap.es6');\nconst {forEachJSONFixture, silentLogger, getTestDatabase} = require('../../helpers');\n\nxdescribe(\"FetchFolderList\", function FetchFolderListSpecs() {\n  beforeEach(async () => {\n    this.db = await getTestDatabase()\n\n    this.stubImapBoxes = null;\n    this.imap = {\n      getBoxes: () => {\n        return Promise.resolve(this.stubImapBoxes);\n      },\n    };\n  });\n\n  describe(\"initial syncing\", () => {\n    forEachJSONFixture('FetchFolderList', (filename, json) => {\n      it(`should create folders and labels correctly for boxes (${filename})`, async () => {\n        const {boxes, expectedFolders, expectedLabels} = json;\n        const provider = filename.split('-')[0];\n        this.stubImapBoxes = boxes;\n\n        const task = new FetchFolderList(provider, silentLogger);\n        await task.run(this.db, this.imap);\n\n        const folders = await this.db.Folder.findAll();\n        expect(folders.map((f) => { return {name: f.name, role: f.role} })).toEqual(expectedFolders);\n\n        const labels = await this.db.Label.findAll();\n        expect(labels.map(f => { return {name: f.name, role: f.role} })).toEqual(expectedLabels);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-sync/spec/local-sync-worker/syncback-tasks/syncback-task-spec.es6",
    "content": "import {Errors} from 'isomorphic-core'\nimport {createLogger} from '../../../src/shared/logger'\nimport SyncbackTask from '../../../src/local-sync-worker/syncback-tasks/syncback-task'\n\nlet syncbackTask;\nconst TIMEOUT_DELAY = 10;\nlet fakeSetTimeout;\n\ndescribe(\"SyncbackTask\", () => {\n  beforeEach(() => {\n    global.Logger = createLogger()\n    const account = {id: 'account1'}\n    const syncbackRequest = {\n      status: 'NEW',\n    }\n    syncbackTask = new SyncbackTask(account, syncbackRequest)\n    fakeSetTimeout = window.setTimeout\n    window.setTimeout = window.originalSetTimeout\n  })\n  afterEach(() => {\n    window.setTimeout = fakeSetTimeout;\n  })\n  describe(\"when it takes too long\", () => {\n    beforeEach(() => {\n      syncbackTask._run = function* hello() {\n        yield new Promise((resolve) => {\n          setTimeout(resolve, TIMEOUT_DELAY + 5)\n        })\n      }\n    })\n\n    it(\"is stopped if retryable\", async () => {\n      syncbackTask._syncbackRequest.status = \"INPROGRESS-RETRYABLE\"\n      let error;\n      try {\n        await syncbackTask.run(null, null, {timeoutDelay: TIMEOUT_DELAY})\n      } catch (err) {\n        error = err\n      }\n      expect(error).toBeDefined()\n      expect(error instanceof Errors.RetryableError).toEqual(true)\n      expect(/interrupted/i.test(error.toString())).toEqual(true)\n    })\n\n    it(\"is not stopped if not retryable\", async () => {\n      syncbackTask._syncbackRequest.status = \"INPROGRESS-NOTRETRYABLE\"\n      // If this does end up being stopped, it'll throw an error.\n      await syncbackTask.run({timeoutDelay: TIMEOUT_DELAY})\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-sync/spec/message-processor/detect-thread-spec.js",
    "content": "/* eslint global-require: 0 */\n/* eslint import/no-dynamic-require: 0 */\nconst detectThread = require('../../src/message-processor/detect-thread');\nconst {FIXTURES_PATH, ACCOUNT_ID, getTestDatabase} = require('../helpers')\n\nfunction messagesFromFixture({Message}, folder, name) {\n  const {A, B} = require(`${FIXTURES_PATH}/Threading/${name}`)\n\n  const msgA = Message.build(A);\n  msgA.folder = folder;\n  msgA.labels = [];\n\n  const msgB = Message.build(B);\n  msgB.folder = folder;\n  msgB.labels = [];\n\n  return {msgA, msgB};\n}\n\nxdescribe('threading', function threadingSpecs() {\n  beforeEach(() => {\n    waitsForPromise({timeout: 1000}, async () => {\n      this.db = await getTestDatabase()\n      this.folder = await this.db.Folder.create({\n        id: 'test-folder-id',\n        accountId: ACCOUNT_ID,\n        version: 1,\n        name: 'Test Folder',\n        role: null,\n      });\n    });\n  });\n\n  describe(\"when remote thread ids are present\", () => {\n    it('threads emails with the same gthreadid', () => {\n      waitsForPromise(async () => {\n        const {msgA, msgB} = messagesFromFixture(this.db, this.folder, 'remote-thread-id-yes');\n        const threadA = await detectThread({db: this.db, message: msgA});\n        const threadB = await detectThread({db: this.db, message: msgB});\n        expect(threadB.id).toEqual(threadA.id);\n      });\n    });\n\n    it('does not thread other emails', () => {\n      waitsForPromise(async () => {\n        const {msgA, msgB} = messagesFromFixture(this.db, this.folder, 'remote-thread-id-no');\n        const threadA = await detectThread({db: this.db, message: msgA});\n        const threadB = await detectThread({db: this.db, message: msgB});\n        expect(threadB.id).not.toEqual(threadA.id);\n      });\n    });\n  });\n  describe(\"when subject matching\", () => {\n    it('threads emails with the same subject', () => {\n      waitsForPromise(async () => {\n        const {msgA, msgB} = messagesFromFixture(this.db, this.folder, 'subject-matching-yes');\n        const threadA = await detectThread({db: this.db, message: msgA});\n        const threadB = await detectThread({db: this.db, message: msgB});\n        expect(threadB.id).toEqual(threadA.id);\n      });\n    });\n\n    it('does not thread other emails', () => {\n      waitsForPromise(async () => {\n        const {msgA, msgB} = messagesFromFixture(this.db, this.folder, 'subject-matching-no');\n        const threadA = await detectThread({db: this.db, message: msgA});\n        const threadB = await detectThread({db: this.db, message: msgB});\n        expect(threadB.id).not.toEqual(threadA.id);\n      });\n    });\n  });\n});\n"
  },
  {
    "path": "packages/client-sync/spec/shared/interruptible-spec.es6",
    "content": "import Interruptible from '../../src/shared/interruptible'\n\ndescribe(\"Interruptible\", () => {\n  describe(\"when interrupted with forceReject\", () => {\n    it(\"the run method rejects immediately\", async () => {\n      function* neverResolves() {\n        yield new Promise(() => {})\n      }\n      const interruptible = new Interruptible()\n      const promise = interruptible.run(neverResolves)\n      interruptible.interrupt({forceReject: true})\n      try {\n        await promise;\n      } catch (err) {\n        expect(/interrupted/i.test(err.toString())).toEqual(true)\n      }\n      // The promse never resolves, so if it doesn't reject,\n      // this test will timeout.\n    })\n  })\n})\n"
  },
  {
    "path": "packages/client-sync/src/local-api/decorators/connections.js",
    "content": "/* eslint func-names:0 */\n\nconst LocalDatabaseConnector = require('../../shared/local-database-connector');\n\nmodule.exports = (server) => {\n  server.decorate('request', 'getAccountDatabase', function () {\n    const account = this.auth.credentials;\n    return LocalDatabaseConnector.forAccount(account.id);\n  });\n  server.decorate('request', 'logger', () => {\n    return global.Logger\n  }, {apply: true});\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/default-sync-policy.js",
    "content": "const DefaultSyncPolicy = {\n  intervals: {\n    active: 10 * 1000,\n    inactive: 5 * 60 * 1000,\n  },\n}\nmodule.exports = DefaultSyncPolicy;\n"
  },
  {
    "path": "packages/client-sync/src/local-api/index.js",
    "content": "/* eslint import/no-dynamic-require: 0 */\n/* eslint global-require: 0 */\nconst Hapi = require('hapi');\nconst HapiSwagger = require('hapi-swagger');\nconst HapiBoom = require('hapi-boom-decorators')\nconst HapiBasicAuth = require('hapi-auth-basic');\nconst Inert = require('inert');\nconst Vision = require('vision');\nconst Package = require('../../package');\nconst fs = require('fs');\nconst path = require('path');\nconst LocalDatabaseConnector = require('../shared/local-database-connector')\n\nconst server = new Hapi.Server({\n  connections: {\n    router: {\n      stripTrailingSlash: true,\n    },\n  },\n});\n\nlet port = 2578;\nif (NylasEnv.inDevMode()) port = 1337;\nserver.connection({port});\n\nconst plugins = [Inert, Vision, HapiBasicAuth, HapiBoom, {\n  register: HapiSwagger,\n  options: {\n    info: {\n      title: 'Nylas API Documentation',\n      version: Package.version,\n    },\n  },\n}];\n\nlet sharedDb = null;\n\nconst validate = (request, username, password, callback) => {\n  let getSharedDb = null;\n  if (sharedDb) {\n    getSharedDb = Promise.resolve(sharedDb)\n  } else {\n    getSharedDb = LocalDatabaseConnector.forShared()\n  }\n\n  getSharedDb.then((db) => {\n    sharedDb = db;\n    const {AccountToken} = db;\n\n    AccountToken.find({\n      where: {\n        value: username,\n      },\n    }).then((token) => {\n      if (!token) {\n        callback(null, false, {});\n        return\n      }\n      token.getAccount().then((account) => {\n        if (!account) {\n          callback(null, false, {});\n          return;\n        }\n        request.logger = request.logger.forAccount(account)\n        callback(null, true, account);\n      });\n    });\n  });\n};\n\nconst attach = (directory) => {\n  const routesDir = path.join(__dirname, directory)\n  fs.readdirSync(routesDir).forEach((filename) => {\n    if (filename.endsWith('.js') || filename.endsWith('.es6')) {\n      const routeFactory = require(path.join(routesDir, filename));\n      routeFactory(server);\n    }\n  });\n}\n\nserver.register(plugins, (err) => {\n  if (err) { throw err; }\n\n  attach('./routes')\n  attach('./decorators')\n\n  server.auth.strategy('api-consumer', 'basic', { validateFunc: validate });\n  server.auth.default('api-consumer');\n\n  server.start((startErr) => {\n    if (startErr) { throw startErr; }\n    global.Logger.log('API running', {url: server.info.uri});\n  });\n});\n"
  },
  {
    "path": "packages/client-sync/src/local-api/newrelic.js",
    "content": "const {NODE_ENV} = process.env\n/**\n * New Relic agent configuration.\n *\n * See lib/config.defaults.js in the agent distribution for a more complete\n * description of configuration variables and their potential values.\n */\nexports.config = {\n  /**\n   * Array of application names.\n   */\n  app_name: [`k2-api-${NODE_ENV}`],\n  logging: {\n    /**\n     * Level at which to log. 'trace' is most useful to New Relic when diagnosing\n     * issues with the agent, 'info' and higher will impose the least overhead on\n     * production applications.\n     */\n    level: 'info',\n  },\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/route-helpers.es6",
    "content": "import _ from 'underscore'\nimport Boom from 'boom';\nimport Serialization from './serialization';\nimport SyncProcessManager from '../local-sync-worker/sync-process-manager'\n\n\nconst wakeSyncWorker = _.debounce((accountId, reason) => {\n  SyncProcessManager.wakeWorkerForAccount(accountId, {interrupt: true, reason})\n}, 500, true) // `true` so that we debounce on the leading edge instead of the trailing edge\n\nexport async function createAndReplyWithSyncbackRequest(request, reply, syncRequestArgs = {}) {\n  try {\n    const account = request.auth.credentials\n    const {wakeSync = true} = syncRequestArgs\n    syncRequestArgs.accountId = account.id\n\n    const db = await request.getAccountDatabase()\n    const syncbackRequest = await db.SyncbackRequest.create(syncRequestArgs)\n\n    if (wakeSync) {\n      wakeSyncWorker(account.id, `Need to run task ${syncbackRequest.type}`)\n    }\n    reply(Serialization.jsonStringify(syncbackRequest))\n    return syncbackRequest\n  } catch (err) {\n    reply(Boom.wrap(err))\n    return null\n  }\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/auth.js",
    "content": "const { AuthHelpers } = require('isomorphic-core');\nconst DefaultSyncPolicy = require('../default-sync-policy')\nconst LocalDatabaseConnector = require('../../shared/local-database-connector')\nconst SyncProcessManager = require('../../local-sync-worker/sync-process-manager')\nconst {preventCreationOfDuplicateAccounts} = require('../../shared/dedupe-accounts')\n\nasync function upsertAccount(accountParams, credentials) {\n  accountParams.syncPolicy = DefaultSyncPolicy\n  accountParams.lastSyncCompletions = []\n  const db = await LocalDatabaseConnector.forShared()\n\n  // NOTE: See https://phab.nylas.com/D4425 for explanation of why this check\n  // is necessary\n  // TODO remove this check after it no longer affects users\n  await preventCreationOfDuplicateAccounts(db, accountParams)\n\n  const {account, token} = await db.Account.upsertWithCredentials(accountParams, credentials)\n  SyncProcessManager.addWorkerForAccount(account)\n  return {account, token}\n}\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'POST',\n    path: '/auth',\n    config: AuthHelpers.imapAuthRouteConfig(),\n    handler: AuthHelpers.imapAuthHandler(upsertAccount),\n  });\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/calendars.js",
    "content": "const Joi = require('joi');\n\n// TODO: This is a placeholder\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/calendars',\n    config: {\n      description: 'Returns calendars.',\n      notes: 'Notes go here',\n      tags: ['metadata'],\n      validate: {\n        query: {\n          limit: Joi.number().integer().min(1).max(2000).default(100),\n          offset: Joi.number().integer().min(0).default(0),\n          view: Joi.string().valid('count'),\n        },\n      },\n      response: {\n        schema: Joi.array(),\n      },\n    },\n    handler: (request, reply) => {\n      reply('[]');\n    },\n  });\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/categories.js",
    "content": "const Joi = require('joi');\nconst Serialization = require('../serialization');\nconst {createAndReplyWithSyncbackRequest} = require('../route-helpers');\n\nmodule.exports = (server) => {\n  ['Folder', 'Label'].forEach((klass) => {\n    const term = `${klass.toLowerCase()}s`;\n\n    server.route({\n      method: 'GET',\n      path: `/${term}`,\n      config: {\n        description: `${term}`,\n        notes: 'Notes go here',\n        tags: [term],\n        validate: {\n          query: {\n            limit: Joi.number().integer().min(1).max(2000).default(100),\n            offset: Joi.number().integer().min(0).default(0),\n          },\n        },\n        response: {\n          schema: Joi.array().items(\n            Serialization.jsonSchema(klass)\n          ),\n        },\n      },\n      async handler(request, reply) {\n        const db = await request.getAccountDatabase()\n        const Klass = db[klass];\n        const items = await Klass.findAll({\n          limit: request.query.limit,\n          offset: request.query.offset,\n        })\n        reply(Serialization.jsonStringify(items));\n      },\n    });\n\n    // TODO: consider adding some processing on routes that require display_name,\n    // so that we smartly add prefixes or proper separators and things like that.\n\n    server.route({\n      method: 'POST',\n      path: `/${term}`,\n      config: {\n        description: `Create ${term}`,\n        tags: [term],\n        validate: {\n          payload: {\n            display_name: Joi.string().required(),\n          },\n        },\n        response: {\n          schema: Serialization.jsonSchema('SyncbackRequest'),\n        },\n      },\n      async handler(request, reply) {\n        const {payload} = request\n        if (payload.display_name) {\n          createAndReplyWithSyncbackRequest(request, reply, {\n            type: \"CreateCategory\",\n            props: {\n              objectClass: klass,\n              displayName: payload.display_name,\n            },\n          })\n        }\n      },\n    })\n\n    server.route({\n      method: 'PUT',\n      path: `/${term}/{id}`,\n      config: {\n        description: `Update ${term}`,\n        tags: [term],\n        validate: {\n          params: {\n            id: Joi.string().required(),\n          },\n          payload: Joi.object().keys({\n            display_name: Joi.string(),\n            role: Joi.string(),\n          }).or('display_name', 'role'), // Require at least one of these fields\n        },\n        response: {\n          schema: Serialization.jsonSchema('SyncbackRequest'),\n        },\n      },\n      async handler(request, reply) {\n        const {display_name: displayName, role} = request.payload\n        if (role) {\n          const db = await request.getAccountDatabase()\n          const Klass = db[klass];\n\n          // Only one folder/label should have any given role\n          // Remove this role from any items that it currently belongs to\n          const labelsWithRole = await db.Label.findAll({\n            where: {role: role},\n          })\n          const foldersWithRole = await db.Folder.findAll({\n            where: {role: role},\n          })\n          const itemsWithRole = labelsWithRole.concat(foldersWithRole)\n          await Promise.all(itemsWithRole.map((item) => {\n            item.role = null;\n            return item.save();\n          }))\n\n          // Add the role to the target item\n          const targetItem = await Klass.findById(request.params.id)\n          targetItem.role = role;\n          await targetItem.save()\n\n          if (!displayName) {\n            reply(Serialization.jsonStringify(targetItem));\n          }\n        }\n        if (displayName) {\n          if (klass === 'Label') {\n            createAndReplyWithSyncbackRequest(request, reply, {\n              type: \"RenameLabel\",\n              props: {\n                labelId: request.params.id,\n                newLabelName: displayName,\n              },\n            })\n          } else {\n            createAndReplyWithSyncbackRequest(request, reply, {\n              type: \"RenameFolder\",\n              props: {\n                folderId: request.params.id,\n                newFolderName: displayName,\n              },\n            })\n          }\n        }\n      },\n    })\n\n    server.route({\n      method: 'DELETE',\n      path: `/${term}/{id}`,\n      config: {\n        description: `Delete ${term}`,\n        tags: [term],\n        validate: {\n          params: {\n            id: Joi.string().required(),\n          },\n        },\n        response: {\n          schema: Serialization.jsonSchema('SyncbackRequest'),\n        },\n      },\n      handler: (request, reply) => {\n        if (klass === 'Label') {\n          createAndReplyWithSyncbackRequest(request, reply, {\n            type: \"DeleteLabel\",\n            props: {\n              labelId: request.params.id,\n            },\n          })\n        } else {\n          createAndReplyWithSyncbackRequest(request, reply, {\n            type: \"DeleteFolder\",\n            props: {\n              folderId: request.params.id,\n            },\n          })\n        }\n      },\n    })\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/contacts.js",
    "content": "const Joi = require('joi');\nconst Serialization = require('../serialization');\nconst moment = require('moment-timezone');\n\nconst LOOKBACK_TIME = moment.duration(2, 'years').asMilliseconds();\nconst MIN_MESSAGE_WEIGHT = 0.01;\n\nconst getMessageWeight = (message, now) => {\n  const timeDiff = now - message.date.getTime();\n  const weight = 1.0 - (timeDiff / LOOKBACK_TIME);\n  return Math.max(weight, MIN_MESSAGE_WEIGHT);\n};\n\nconst calculateContactScores = (messages, result) => {\n  const now = Date.now();\n  for (const message of messages) {\n    const weight = getMessageWeight(message, now);\n    for (const recipient of message.getRecipients()) {\n      if (!recipient.email) {\n        continue;\n      }\n      const email = recipient.email.toLowerCase();\n      result[email] = result[email] ? (result[email] + weight) : weight;\n    }\n  }\n};\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/contacts',\n    config: {\n      description: 'Returns an array of contacts',\n      notes: 'Notes go here',\n      tags: ['contacts'],\n      validate: {\n        query: {\n          limit: Joi.number().integer().min(1).max(2000).default(100),\n          offset: Joi.number().integer().min(0).default(0),\n        },\n      },\n      response: {\n        schema: Joi.array().items(\n          Serialization.jsonSchema('Contact')\n        ),\n      },\n    },\n    handler: (request, reply) => {\n      request.getAccountDatabase().then((db) => {\n        const {Contact} = db;\n        Contact.findAll({\n          limit: request.query.limit,\n          offset: request.query.offset,\n        }).then((contacts) => {\n          reply(Serialization.jsonStringify(contacts))\n        })\n      })\n    },\n  })\n\n  server.route({\n    method: 'GET',\n    path: '/contacts/{id}',\n    config: {\n      description: 'Returns a contact with specified id.',\n      notes: 'Notes go here',\n      tags: ['contacts'],\n      validate: {\n        params: {\n          id: Joi.string(),\n        },\n      },\n      response: {\n        schema: Serialization.jsonSchema('Contact'),\n      },\n    },\n    handler: (request, reply) => {\n      request.getAccountDatabase().then(({Contact}) => {\n        const {params: {id}} = request\n\n        Contact.findOne({where: {id}}).then((contact) => {\n          if (!contact) {\n            return reply.notFound(`Contact ${id} not found`)\n          }\n          return reply(Serialization.jsonStringify(contact))\n        })\n        .catch((error) => {\n          request.info(error, 'Error fetching contacts')\n        })\n      })\n    },\n  })\n\n  // TODO: This is a placeholder\n  server.route({\n    method: 'GET',\n    path: '/contacts/rankings',\n    config: {\n      description: 'Returns contact rankings.',\n      notes: 'Notes go here',\n      tags: ['contacts'],\n    },\n    handler: async (request, reply) => {\n      const db = await request.getAccountDatabase()\n      const account = request.auth.credentials;\n      const {Message, Label, Folder} = db;\n\n      const result = {};\n      let lastID = 0;\n\n      const useLabels = account.provider === 'gmail';\n\n      const model = (useLabels ? Label : Folder);\n      const category = await model.findOne({\n        attributes: ['id', 'role'],\n        where: {role: 'sent'},\n      });\n\n      if (!category) {\n        reply('[]');\n        return;\n      }\n\n      while (true) {\n        const messages = await category.getMessages({\n          attributes: ['rowid', 'id', 'to', 'cc', 'bcc', 'date'],\n          where: {\n            'isDraft': false,                   // Don't include unsent things.\n            '$message.rowid$': {$gt: lastID},\n          },\n          order: [['rowid', 'ASC']],\n          limit: 100,\n        });\n\n        if (messages.length === 0) {\n          break;\n        }\n\n        calculateContactScores(messages, result);\n        lastID = Math.max(...messages.map(m => m.dataValues.rowid));\n      }\n      reply(JSON.stringify(Object.entries(result)));\n    },\n  })\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/drafts.js",
    "content": "const {MessageUtils, Errors: {APIError}} = require('isomorphic-core')\nconst Joi = require('joi');\n\n// TODO: This is a placeholder.\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/drafts',\n    config: {\n      description: 'Returns drafts.',\n      notes: 'Notes go here',\n      tags: ['drafts'],\n      validate: {\n        query: {\n          limit: Joi.number().integer().min(1).max(2000).default(100),\n          offset: Joi.number().integer().min(0).default(0),\n          view: Joi.string().valid('count'),\n        },\n      },\n      response: {\n        schema: Joi.array(),\n      },\n    },\n    handler: (request, reply) => {\n      reply('[]');\n    },\n  });\n\n  server.route({\n    method: ['PUT', 'POST'],\n    path: `/drafts/build`,\n    config: {\n      description: `Returns a ready-made draft message. Used by our send later plugin.`,\n      tags: ['drafts'],\n      payload: {\n        output: 'data',\n        parse: true,\n      },\n    },\n    handler: async (request, reply) => {\n      const db = await request.getAccountDatabase();\n      const account = request.auth.credentials;\n\n      let sentFolderName;\n      let sentFolder;\n      let trashFolderName;\n\n      if (account.provider === 'gmail') {\n        sentFolder = await db.Label.find({where: {role: 'sent'}});\n      } else {\n        sentFolder = await db.Folder.find({where: {role: 'sent'}});\n      }\n\n      if (sentFolder) {\n        sentFolderName = sentFolder.name;\n      } else {\n        throw new APIError(`Can't find sent folder name.`, 500);\n      }\n\n      const trashFolder = await db.Folder.find({where: {role: 'trash'}});\n\n      if (trashFolder) {\n        trashFolderName = trashFolder.name;\n      } else {\n        throw new APIError(`Can't find trash folder name.`, 500);\n      }\n\n      const message = await MessageUtils.buildForSend(db, request.payload);\n      const ret = Object.assign(message.toJSON(), { sentFolderName, trashFolderName });\n      reply(JSON.stringify(ret));\n    },\n  });\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/events.js",
    "content": "const Joi = require('joi');\n\n// TODO: This is a placeholder\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/events',\n    config: {\n      description: 'Returns events.',\n      notes: 'Notes go here',\n      tags: ['events'],\n      validate: {\n        query: {\n          limit: Joi.number().integer().min(1).max(2000).default(100),\n          offset: Joi.number().integer().min(0).default(0),\n          view: Joi.string().valid('count'),\n        },\n      },\n      response: {\n        schema: Joi.array(),\n      },\n    },\n    handler: (request, reply) => {\n      reply('[]');\n    },\n  });\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/files.js",
    "content": "const Joi = require('joi');\n\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/files/{id}/download',\n    config: {\n      description: 'Returns binary data for file with specified id.',\n      notes: 'Notes go here',\n      tags: ['files'],\n      validate: {\n        params: {\n          id: Joi.string(),\n        },\n      },\n    },\n    handler: (request, reply) => {\n      request.getAccountDatabase()\n      .then((db) => {\n        const {params: {id}} = request\n        const account = request.auth.credentials\n\n        db.File.findOne({where: {id}})\n        .then((file) => {\n          if (!file) {\n            return reply.notFound(`File ${id} not found`)\n          }\n          return file.fetch({account, db, logger: request.logger})\n          .then((stream) => reply(stream))\n        })\n        .catch((err) => {\n          request.logger.error(err, 'Error downloading file')\n          reply(err)\n        })\n      })\n    },\n  })\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/health.es6",
    "content": "const Joi = require('joi');\nconst SyncActivity = require('../../shared/sync-activity').default\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/health',\n    config: {\n      description: 'Returns information about the last recorded sync activity for all accounts',\n      tags: ['health'],\n    },\n    handler: (request, reply) => {\n      let response;\n      try {\n        response = SyncActivity.getLastSyncActivity()\n        response = JSON.stringify(response)\n        reply(response)\n      } catch (err) {\n        const context = response ? \"\" : \"Could not retrieve last sync activity\"\n        request.logger.error(err, context)\n        reply(err)\n      }\n    },\n  })\n\n  server.route({\n    method: 'GET',\n    path: '/health/{accountId}',\n    config: {\n      description: 'Returns information about the last recorded sync activity for the specified account',\n      tags: ['health'],\n      validate: {\n        params: {\n          accountId: Joi.string(),\n        },\n      },\n    },\n    handler: (request, reply) => {\n      let response;\n      try {\n        const {accountId} = request.params\n        response = SyncActivity.getLastSyncActivityForAccount(accountId)\n        response = JSON.stringify(response)\n        reply(response)\n      } catch (err) {\n        const context = response ? \"\" : \"Could not retrieve last sync activity\"\n        request.logger.error(err, context)\n        reply(err)\n      }\n    },\n  })\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/messages.js",
    "content": "const Joi = require('joi');\nconst {Provider} = require('isomorphic-core')\nconst Serialization = require('../serialization');\nconst {createAndReplyWithSyncbackRequest} = require('../route-helpers');\n\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/messages/{id}',\n    config: {\n      description: 'Returns message for specified id.',\n      notes: 'Notes go here',\n      tags: ['messages'],\n      validate: {\n        params: {\n          id: Joi.string(),\n        },\n      },\n      response: {\n        schema: Joi.alternatives().try(\n          Serialization.jsonSchema('Message'),\n          Joi.string()\n        ),\n      },\n    },\n    handler: (request, reply) => {\n      request.getAccountDatabase().then((db) => {\n        const {Message, Folder, Label, File} = db;\n        const {headers: {accept}} = request;\n        const {params: {id}} = request;\n        const account = request.auth.credentials;\n\n        Message.findOne({where: {id},\n          include: [{model: Folder}, {model: Label}, {model: File}]})\n        .then((message) => {\n          if (!message) {\n            return reply.notFound(`Message ${id} not found`)\n          }\n          if (accept === 'message/rfc822') {\n            return message.fetchRaw({account, logger: request.logger})\n            .then((rawMessage) =>\n              reply(rawMessage)\n            )\n          }\n          return reply(Serialization.jsonStringify(message));\n        })\n        .catch((err) => {\n          request.logger.error(err, 'Error fetching message')\n          reply(err)\n        })\n      })\n    },\n  })\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/ping.js",
    "content": "module.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/ping',\n    config: {\n      auth: false,\n    },\n    handler: (request, reply) => {\n      request.logger.info('----> Pong!')\n      reply(\"Pong\")\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/send.js",
    "content": "const Joi = require('joi');\nconst {ModelUtils} = require('isomorphic-core');\nconst {createAndReplyWithSyncbackRequest} = require('../route-helpers');\n\n\n// const recipient = Joi.object().keys({\n//   name: Joi.string().required(),\n//   email: Joi.string().email().required(),\n//   // Rest are optional\n//   account_id: Joi.string(),\n//   client_id: Joi.string(),\n//   id: Joi.string(),\n//   thirdPartyData: Joi.object(),\n//   server_id: Joi.string(),\n//   object: Joi.string(),\n// });\n\n// const recipientList = Joi.array().items(recipient);\n\nmodule.exports = (server) => {\n  // Closes out a multi-send session by marking the sending draft as sent\n  // and moving it to the user's Sent folder.\n  server.route({\n    method: 'POST',\n    path: '/ensure-message-in-sent-folder/{messageId}',\n    config: {\n      validate: {\n        payload: {\n          customSentMessage: Joi.boolean(),\n        },\n        params: {\n          messageId: Joi.string(),\n        },\n      },\n    },\n    async handler(request, reply) {\n      const {messageId} = request.params;\n      const {customSentMessage} = request.payload;\n\n      if (!ModelUtils.isValidId(messageId)) {\n        reply.badRequest(`messageId is not a base-36 integer`)\n        return\n      }\n      createAndReplyWithSyncbackRequest(request, reply, {\n        type: \"EnsureMessageInSentFolder\",\n        props: { messageId, customSentMessage },\n      })\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/routes/threads.js",
    "content": "const Joi = require('joi');\nconst stream = require('stream');\nconst _ = require('underscore');\nconst Serialization = require('../serialization');\nconst {createAndReplyWithSyncbackRequest} = require('../route-helpers')\nconst {searchClientForAccount} = require('../search');\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: '/threads',\n    config: {\n      description: 'Returns threads',\n      notes: 'Notes go here',\n      tags: ['threads'],\n      validate: {\n        query: {\n          limit: Joi.number().integer().min(1).max(2000).default(100),\n          offset: Joi.number().integer().min(0).default(0),\n        },\n      },\n      response: {\n        schema: Joi.alternatives().try([\n          Joi.array().items(\n            Serialization.jsonSchema('Thread')\n          ),\n          Joi.object().keys({\n            count: Joi.number().integer().min(0),\n          }),\n        ]),\n      },\n    },\n    handler: (request, reply) => {\n      request.getAccountDatabase().then((db) => {\n        const {Thread, Folder, Label, Message} = db;\n        Thread.findAll({\n          limit: request.query.limit,\n          offset: request.query.offset,\n          include: [\n            {model: Folder},\n            {model: Label},\n            {\n              model: Message,\n              as: 'messages',\n              attributes: _.without(Object.keys(Message.attributes), 'body'),\n            },\n          ],\n        }).then((threads) => {\n          reply(Serialization.jsonStringify(threads));\n        })\n      })\n    },\n  });\n\n  server.route({\n    method: 'GET',\n    path: '/threads/search/streaming',\n    config: {\n      description: 'Returns threads',\n      notes: 'Notes go here',\n      tags: ['threads'],\n      validate: {\n        query: {\n          limit: Joi.number().integer().min(1).max(2000).default(100),\n          q: Joi.string(),\n        },\n      },\n    },\n    handler: async (request, reply) => {\n      const account = request.auth.credentials;\n      const db = await request.getAccountDatabase();\n      const client = searchClientForAccount(account);\n      const source = await client.searchThreads(db, request.query.q, request.query.limit);\n\n      const outputStream = stream.Readable();\n      outputStream._read = () => { return };\n      const disposable = source.subscribe((str) => outputStream.push(str));\n      request.on(\"disconnect\", () => {\n        client.cancelSearchRequest();\n        disposable.dispose();\n      });\n      reply(outputStream);\n    },\n  });\n\n  server.route({\n    method: 'PUT',\n    path: '/threads/{id}',\n    config: {\n      description: 'Update a thread',\n      notes: 'Can move between folders',\n      tags: ['threads'],\n      validate: {\n        params: {\n          id: Joi.string(),\n          payload: {\n            unread: Joi.boolean(),\n            starred: Joi.boolean(),\n            labels: Joi.array(),\n            label_ids: Joi.array(),\n            folder: Joi.string(),\n            folder_id: Joi.string(),\n          },\n        },\n      },\n      response: {\n        schema: Serialization.jsonSchema('SyncbackRequest'),\n      },\n    },\n    handler: (request, reply) => {\n      const payload = request.payload\n      if ((payload.folder_id || payload.folder) && (payload.label_ids || payload.labels)) {\n        createAndReplyWithSyncbackRequest(request, reply, {\n          type: \"SetThreadFolderAndLabels\",\n          props: {\n            folderId: payload.folder_id || payload.folder,\n            labelIds: payload.label_ids || payload.labels,\n            threadId: request.params.id,\n          },\n        })\n      } else if (payload.folder_id || payload.folder) {\n        createAndReplyWithSyncbackRequest(request, reply, {\n          type: \"MoveThreadToFolder\",\n          props: {\n            folderId: payload.folder_id || payload.folder,\n            threadId: request.params.id,\n          },\n        })\n      } else if (payload.label_ids || payload.labels) {\n        createAndReplyWithSyncbackRequest(request, reply, {\n          type: \"SetThreadLabels\",\n          props: {\n            labelIds: payload.label_ids || payload.labels,\n            threadId: request.params.id,\n          },\n        })\n      } else if (payload.unread === false) {\n        createAndReplyWithSyncbackRequest(request, reply, {\n          type: \"MarkThreadAsRead\",\n          props: {\n            threadId: request.params.id,\n          },\n        })\n      } else if (payload.unread === true) {\n        createAndReplyWithSyncbackRequest(request, reply, {\n          type: \"MarkThreadAsUnread\",\n          props: {\n            threadId: request.params.id,\n          },\n        })\n      } else if (payload.starred === false) {\n        createAndReplyWithSyncbackRequest(request, reply, {\n          type: \"UnstarThread\",\n          props: {\n            threadId: request.params.id,\n          },\n        })\n      } else if (payload.starred === true) {\n        createAndReplyWithSyncbackRequest(request, reply, {\n          type: \"StarThread\",\n          props: {\n            threadId: request.params.id,\n          },\n        })\n      } else {\n        reply(\"Invalid thread update\").code(400)\n      }\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/search.js",
    "content": "const _ = require('underscore');\nconst Rx = require('rx-lite');\nconst {\n  ExponentialBackoffScheduler,\n  IMAPErrors,\n  IMAPConnectionPool,\n} = require('isomorphic-core')\nconst SyncProcessManager = require('../local-sync-worker/sync-process-manager')\nconst {\n  SearchQueryParser,\n  IMAPSearchQueryBackend,\n} = require('nylas-exports')\n\nconst MAX_IMAP_TIMEOUT_ERRORS = 5;\n\nconst getThreadsForMessages = (db, messages, limit) => {\n  if (messages.length === 0) {\n    return Promise.resolve([]);\n  }\n  const {Message, Folder, Label, Thread, File} = db;\n  const threadIds = _.uniq(messages.map((m) => m.threadId));\n  return Thread.findAll({\n    where: {id: threadIds},\n    include: [\n      {model: Folder},\n      {model: Label},\n      {\n        model: Message,\n        as: 'messages',\n        attributes: _.without(Object.keys(Message.attributes), 'body'),\n        include: [\n          {model: Folder},\n          {model: Label},\n          {model: File},\n        ],\n      },\n    ],\n    limit: limit,\n    order: [['lastMessageReceivedDate', 'DESC']],\n  });\n};\n\nclass SearchFolder {\n  constructor(folder, criteria) {\n    this.folder = folder;\n    this.criteria = criteria;\n  }\n\n  description() {\n    return 'IMAP folder search';\n  }\n\n  run(db, imap) {\n    return imap.openBox(this.folder.name).then((box) => {\n      return box.search(this.criteria);\n    });\n  }\n}\n\nclass ImapSearchClient {\n  constructor(account) {\n    this.account = account;\n    this._logger = global.Logger.forAccount(this.account);\n    this._cancelled = false;\n  }\n\n  async _getFoldersForSearch(db, query) {\n    const {Folder} = db;\n\n    const folderNames = IMAPSearchQueryBackend.folderNamesForQuery(query);\n    if (folderNames !== IMAPSearchQueryBackend.ALL_FOLDERS()) {\n      if (folderNames.length === 0) {\n        return [];\n      }\n\n      const result = await Folder.findAll({\n        where: {\n          accountId: this.account.id,\n          name: folderNames,\n        },\n      });\n      return result;\n    }\n\n    // We want to start the search with the 'inbox', 'sent' and 'archive'\n    // folders, if they exist.\n    const folders = await Folder.findAll({\n      where: {\n        accountId: this.account.id,\n        role: ['inbox', 'sent', 'archive'],\n      },\n    });\n\n    const accountFolders = await Folder.findAll({\n      where: {\n        accountId: this.account.id,\n        id: {$notIn: folders.map((f) => f.id)},\n      },\n    });\n\n    return folders.concat(accountFolders);\n  }\n\n  _getCriteriaForQuery(query, folder) {\n    return IMAPSearchQueryBackend.compile(query, folder);\n  }\n\n  async _search(db, query) {\n    const parsedQuery = SearchQueryParser.parse(query);\n    const folders = await this._getFoldersForSearch(db, parsedQuery);\n    let numTimeoutErrors = 0;\n    return Rx.Observable.create(async (observer) => {\n      const onConnected = async ([conn]) => {\n        // Remove folders as we process them so we don't re-search previously\n        // searched folders if there is an error later down the line.\n        while (folders.length > 0) {\n          const folder = folders[0];\n          const criteria = this._getCriteriaForQuery(parsedQuery, folder);\n          const uids = await this._searchFolder(conn, folder, criteria);\n          folders.shift();\n          if (uids.length > 0) {\n            observer.onNext({uids, folder});\n          }\n        }\n        observer.onCompleted();\n      };\n\n      const timeoutScheduler = new ExponentialBackoffScheduler({\n        baseDelay: 15 * 1000,\n        maxDelay: 5 * 60 * 1000,\n      });\n\n      const onTimeout = () => {\n        numTimeoutErrors += 1;\n        timeoutScheduler.nextDelay();\n      };\n\n      while (numTimeoutErrors < MAX_IMAP_TIMEOUT_ERRORS) {\n        try {\n          await IMAPConnectionPool.withConnectionsForAccount(this.account, {\n            desiredCount: 1,\n            logger: this._logger,\n            socketTimeout: timeoutScheduler.currentDelay(),\n            onConnected,\n          });\n          break;\n        } catch (err) {\n          if (err instanceof IMAPErrors.IMAPConnectionTimeoutError) {\n            onTimeout();\n            continue;\n          }\n          throw err;\n        }\n      }\n    });\n  }\n\n  _searchFolder(conn, folder, criteria) {\n    return conn.runOperation(new SearchFolder(folder, criteria))\n    .catch((error) => {\n      this._logger.error(`Search error: ${error}`);\n      return Promise.resolve([]);\n    });\n  }\n\n  cancelSearchRequest() {\n    this._cancelled = true;\n  }\n\n  async _cancelSyncbackTasks(db) {\n    await db.SyncbackRequest.update(\n      {status: 'CANCELLED'},\n      {\n        where: {\n          type: \"SyncUnknownUIDs\",\n          status: {$in: [\"NEW\", \"INPROGRESS-RETRYABLE\", \"INPROGRESS-NOTRETRYABLE\"]},\n          accountId: this.account.id,\n        },\n      });\n  }\n\n  async searchThreads(db, query, limit) {\n    const {Message} = db;\n    const uidFolderStream = await this._search(db, query);\n    // The first concatMap handles the fact that the async function returns promises\n    // of the new observable streams.\n    const messageListStreamStream = uidFolderStream.concatMap(async ({uids, folder}) => {\n      let messages = await Message.findAll({\n        attributes: ['id', 'threadId', 'folderImapUID'],\n        where: {folderImapUID: uids},\n      });\n\n      let knownUids = new Set(messages.map(m => parseInt(m.folderImapUID, 10)));\n      const unknownUids = uids.filter(uid => !knownUids.has(uid));\n\n      if (unknownUids.length === 0 || this._cancelled) {\n        return Rx.Observable.from([messages]);\n      }\n      // Sort into descending order so that we get the more recent messages sooner.\n      unknownUids.sort((a, b) => b - a);\n\n      await db.SyncbackRequest.create({\n        type: \"SyncUnknownUIDs\",\n        props: {folderId: folder.id, uids: unknownUids},\n        accountId: this.account.id,\n      })\n      SyncProcessManager.wakeWorkerForAccount(this.account.id, {interrupt: true, reason: 'Sync unknown UIDs'});\n\n      return Rx.Observable.create((observer) => {\n        observer.onNext(messages);\n        const findFn = async (remainingUids) => {\n          if (this._cancelled) {\n            await this._cancelSyncbackTasks(db);\n            observer.onCompleted();\n            return;\n          }\n\n          if (remainingUids.length === 0) {\n            observer.onCompleted();\n            return;\n          }\n\n          const newMessages = await Message.findAll({\n            attributes: ['id', 'threadId', 'folderImapUID'],\n            where: {folderImapUID: remainingUids},\n          });\n          messages = messages.concat(newMessages);\n          if (newMessages.length > 0) {\n            observer.onNext(newMessages);\n          }\n          knownUids = new Set(messages.map(m => parseInt(m.folderImapUID, 10)));\n          setTimeout(findFn, 1000, remainingUids.filter(uid => !knownUids.has(uid)));\n        };\n        findFn(unknownUids);\n      });\n    });\n    // Now that we've unwrapped the promises with the previous concatMap, we\n    // can flatten the observable into a stream of message lists.\n    return messageListStreamStream.concatMap(messageListStream => {\n      return messageListStream;\n    }).map(messages => {\n      return getThreadsForMessages(db, messages, limit);\n    }).concatMap((threads) => {\n      if (threads.length > 0) {\n        return `${JSON.stringify(threads)}\\n`;\n      }\n      return '\\n';\n    });\n  }\n}\n\nclass GmailSearchClient extends ImapSearchClient {\n  async _getFoldersForSearch(db/* , query*/) {\n    const allMail = await db.Folder.findOne({where: {role: 'all'}});\n    return [allMail];\n  }\n\n  _getCriteriaForQuery(query/* , folder*/) {\n    return [['X-GM-RAW', query]];\n  }\n}\n\nmodule.exports.searchClientForAccount = (account) => {\n  switch (account.provider) {\n    case 'gmail': {\n      return new GmailSearchClient(account);\n    }\n    case 'office365':\n    case 'imap': {\n      return new ImapSearchClient(account);\n    }\n    default: {\n      throw new Error(`Unsupported provider for search endpoint: ${account.provider}`);\n    }\n  }\n};\n"
  },
  {
    "path": "packages/client-sync/src/local-api/serialization.js",
    "content": "const Joi = require('joi');\n\nfunction replacer(key, value) {\n  // force remove any disallowed keys here\n  return value;\n}\n\nfunction jsonSchema(modelName) {\n  const models = ['Message', 'Thread', 'File', 'Error', 'SyncbackRequest', 'Account', 'Contact']\n\n  if (models.includes(modelName)) {\n    return Joi.object();\n  }\n  if (modelName === 'Error') {\n    return Joi.object().keys({\n      message: Joi.string(),\n      type: Joi.string(),\n    })\n  }\n  if (modelName === 'Account') {\n    // Ben: Disabled temporarily because folks keep changing the keys and it's hard\n    // to keep in sync. Might need to consider another approach to these.\n    // return Joi.object().keys({\n    //   id: Joi.number(),\n    //   object: Joi.string(),\n    //   email_address: Joi.string(),\n    //   provider: Joi.string(),\n    //   organization_unit: Joi.string(),\n    //   connection_settings: Joi.object(),\n    //   sync_policy: Joi.object(),\n    //   sync_error: Joi.object().allow(null),\n    //   first_sync_completion: Joi.number().allow(null),\n    //   last_sync_completions: Joi.array(),\n    // })\n  }\n  if (modelName === 'Folder') {\n    return Joi.object().keys({\n      id: Joi.string(),\n      object: Joi.string(),\n      account_id: Joi.string(),\n      name: Joi.string().allow(null),\n      display_name: Joi.string(),\n    })\n  }\n  if (modelName === 'Label') {\n    return Joi.object().keys({\n      id: Joi.string(),\n      object: Joi.string(),\n      account_id: Joi.string(),\n      name: Joi.string().allow(null),\n      display_name: Joi.string(),\n    })\n  }\n  return null;\n}\n\nfunction jsonStringify(models) {\n  return JSON.stringify(models, replacer, 2);\n}\n\nmodule.exports = {\n  jsonSchema,\n  jsonStringify,\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-dashboard/dropdown.jsx",
    "content": "import {React} from 'nylas-exports';\n\nexport default class Dropdown extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      closed: true,\n      selected: props.defaultOption,\n      onSelect: props.onSelect,\n    }\n  }\n\n  open() {\n    this.setState({closed: false});\n  }\n\n  selectAndClose(selection) {\n    this.setState({\n      closed: true,\n      selected: selection,\n    })\n    this.state.onSelect(selection);\n  }\n\n  close() {\n    this.setState({closed: true});\n  }\n\n  render() {\n    // Currently selected option (includes dropdown arrow)\n    const selectedOnClick = this.state.closed ? this.open : this.close;\n    const selected = (\n      <div className=\"dropdown-selected\" onClick={() => selectedOnClick.call(this)}>\n        {this.state.selected}\n        <img className=\"dropdown-arrow\" src=\"../images/dropdown.png\" alt=\"dropdown arrow\" />\n      </div>\n    );\n\n    // All options, not shown if dropdown is closed\n    const options = [];\n    let optionsWrapper = <span className=\"dropdown-options\" />;\n    if (!this.state.closed) {\n      for (const opt of this.props.options) {\n        options.push(\n          <div className=\"dropdown-option\" onMouseDown={() => this.selectAndClose.call(this, opt)}> {opt} </div>\n        );\n      }\n      optionsWrapper = (\n        <div className=\"dropdown-options\">\n          {options}\n        </div>\n      )\n    }\n\n    return (\n      <div className=\"dropdown-wrapper\" tabIndex=\"0\" onBlur={() => this.close.call(this)}>\n        {optionsWrapper}\n        {selected}\n      </div>\n    );\n  }\n\n}\n\nDropdown.propTypes = {\n  options: React.PropTypes.arrayOf(React.PropTypes.string),\n  defaultOption: React.PropTypes.string,\n  onSelect: React.PropTypes.func,\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-dashboard/elapsed-time.jsx",
    "content": "import {React, ReactDOM} from 'nylas-exports';\n\nsetInterval(() => {\n  const event = new Event('tick');\n  window.dispatchEvent(event);\n}, 1000);\n\nexport default class ElapsedTime extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      elapsed: 0,\n    }\n  }\n\n  componentDidMount() {\n    this.onTick = () => {\n      ReactDOM.findDOMNode(this.refs.timestamp).innerHTML = this.props.formatTime(\n        Date.now() - this.props.refTimestamp\n      );\n    };\n    window.addEventListener('tick', this.onTick);\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('tick', this.onTick);\n  }\n\n  render() {\n    return <span ref=\"timestamp\" />\n  }\n}\n\nElapsedTime.propTypes = {\n  refTimestamp: React.PropTypes.number, // milliseconds\n  formatTime: React.PropTypes.func,\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-dashboard/modal.jsx",
    "content": "import {React} from 'nylas-exports';\n\nexport default class Modal extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      open: false,\n      onOpen: props.onOpen || (() => {}),\n      onClose: props.onClose || (() => {}),\n    }\n  }\n\n  componentDidMount() {\n    this.keydownHandler = (e) => {\n      // Close modal on escape\n      if (e.keyCode === 27) {\n        this.close();\n      }\n    }\n    document.addEventListener('keydown', this.keydownHandler);\n  }\n\n  componentWillUnmount() {\n    document.removeEventListener('keydown', this.keydownHandler);\n  }\n\n  open() {\n    this.setState({open: true});\n    this.state.onOpen();\n  }\n\n  close() {\n    this.setState({open: false});\n    this.state.onClose();\n  }\n\n  // type can be 'button' or 'div'.\n  // Always closes modal after the callback\n  renderActionElem({title, type = 'button', action = () => {}, className = \"\"}) {\n    const callback = (e) => {\n      action(e);\n      this.close();\n    }\n    if (type === 'button') {\n      return (\n        <button className={className} onClick={callback}>\n          {title}\n        </button>\n      )\n    }\n    return (\n      <div className={className} onClick={callback}>\n        {title}\n      </div>\n    )\n  }\n\n  render() {\n    const activator = (\n      <div\n        className={this.props.openLink.className}\n        id={this.props.openLink.id}\n        onClick={() => this.open.call(this)}\n      >\n        {this.props.openLink.text}\n      </div>\n    )\n    if (!this.state.open) {\n      return activator;\n    }\n\n    const actionElems = [];\n    if (this.props.actionElems) {\n      for (const config of this.props.actionElems) {\n        actionElems.push(this.renderActionElem(config));\n      }\n    }\n\n    return (\n      <div>\n        {activator}\n        <div className=\"modal-bg\">\n          <div className={`${this.props.className || ''} modal`} id={this.props.id}>\n            <div className=\"modal-close-wrapper\">\n              <div className=\"modal-close\" onClick={() => this.close.call(this)} />\n            </div>\n            {this.props.children}\n            {actionElems}\n          </div>\n        </div>\n      </div>\n    )\n  }\n}\n\nModal.propTypes = {\n  openLink: React.PropTypes.object,\n  className: React.PropTypes.string,\n  id: React.PropTypes.string,\n  onOpen: React.PropTypes.func,\n  onClose: React.PropTypes.func,\n  actionElems: React.PropTypes.arrayOf(React.PropTypes.object),\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-dashboard/root.jsx",
    "content": "/* eslint react/react-in-jsx-scope: 0*/\n/* eslint no-console: 0*/\n/* eslint global-require: 0*/\n\nimport {React} from 'nylas-exports';\nimport SetAllSyncPolicies from './set-all-sync-policies';\nimport SyncGraph from './sync-graph';\nimport SyncbackRequestDetails from './syncback-request-details';\nimport ElapsedTime from './elapsed-time';\nimport Modal from './modal';\n\nimport LocalDatabaseConnector from '../shared/local-database-connector';\nimport SyncProcessManager from '../local-sync-worker/sync-process-manager';\n\nfunction calcAcctPosition(count) {\n  const width = 280;\n  const height = 490;\n  const marginTop = 0;\n  const marginSide = 0;\n\n  const acctsPerRow = Math.floor((window.innerWidth - 2 * marginSide) / width);\n  const row = Math.floor(count / acctsPerRow)\n  const col = count - (row * acctsPerRow);\n  const top = marginTop + (row * height);\n  const left = marginSide + (width * col);\n\n  return {left: left, top: top};\n}\n\nfunction formatSyncTimes(timestamp) {\n  return timestamp / 1000;\n}\n\nclass AccountCard extends React.Component {\n  static propTypes = {\n    account: React.PropTypes.object,\n    count: React.PropTypes.number,\n  };\n\n  onClearError = () => {\n    LocalDatabaseConnector.forShared().then(({Account}) => {\n      Account.find({where: {id: this.props.account.id}}).then((account) => {\n        account.syncError = null;\n        account.save().then(() => {\n          SyncProcessManager.wakeWorkerForAccount(account.id);\n        });\n      })\n    });\n  }\n\n  onResetSync = () => {\n    SyncProcessManager.removeWorkerForAccountId(this.props.account.id);\n    LocalDatabaseConnector.destroyAccountDatabase(this.props.account.id);\n    SyncProcessManager.addWorkerForAccount(this.props.account);\n  }\n\n  renderError() {\n    const account = this.props.account;\n    if (account.syncError === null) {\n      return false;\n    }\n    const {message, stack} = account.syncError\n    return (\n      <div>\n        <div className=\"section\">Error</div>\n        <Modal\n          openLink={{text: message, className: 'error-link'}}\n        >\n          <pre>{JSON.stringify(stack, null, 2)}</pre>\n        </Modal>\n        <div className=\"action-link\" onClick={this.onClearError}>Clear Error</div>\n      </div>\n    )\n  }\n\n  render() {\n    const {account} = this.props;\n    const errorClass = account.syncError ? ' errored' : ''\n\n    const numStoredSyncs = account.lastSyncCompletions.length;\n    const oldestSync = account.lastSyncCompletions[numStoredSyncs - 1];\n    const newestSync = account.lastSyncCompletions[0];\n    const avgBetweenSyncs = (newestSync - oldestSync) / (1000 * numStoredSyncs);\n\n    let firstSyncDuration = \"Incomplete\";\n    if (account.firstSyncCompletion) {\n      firstSyncDuration = (new Date(account.firstSyncCompletion / 1) - new Date(account.createdAt)) / 1000;\n    }\n\n    const position = calcAcctPosition(this.props.count);\n\n    return (\n      <div\n        className={`account${errorClass}`}\n        style={{top: `${position.top}px`, left: `${position.left}px`}}\n      >\n        <h3>{account.emailAddress} [{account.id}]</h3>\n        <button name=\"Reset sync\" onClick={this.onResetSync}>Reset sync</button>\n        <SyncbackRequestDetails accountId={account.id} />\n        <div className=\"stats\">\n          <b>First Sync Duration (sec)</b>:\n          <pre>{firstSyncDuration}</pre>\n          <b> Average Time Between Syncs (sec)</b>:\n          <pre>{avgBetweenSyncs}</pre>\n          <b>Time Since Last Sync (sec)</b>:\n          <pre>\n            <ElapsedTime refTimestamp={newestSync} formatTime={formatSyncTimes} />\n          </pre>\n          <b>Recent Syncs</b>:\n          <SyncGraph id={account.lastSyncCompletions.length} syncTimestamps={account.lastSyncCompletions} />\n        </div>\n        {this.renderError()}\n      </div>\n    );\n  }\n}\n\n\nexport default class Root extends React.Component {\n  static displayName = 'Root';\n\n  constructor() {\n    super();\n    this.state = {\n      accounts: {},\n      assignments: {},\n      activeAccountIds: [],\n    };\n  }\n\n  componentDidMount() {\n    // just periodically poll. This is crazy nasty and violates separation of\n    // concerns, but oh well. Replace it later.\n\n    this._timer = setInterval(() => {\n      LocalDatabaseConnector.forShared().then(({Account}) => {\n        Account.findAll().then((accounts) => {\n          this.setState({accounts});\n        });\n      });\n    }, 1500);\n  }\n\n  componentWillUnmount() {\n    clearTimeout(this._timer);\n  }\n\n  render() {\n    const ids = Object.keys(this.state.accounts);\n    let count = 0;\n    const content = (\n      <div id=\"accounts-wrapper\">\n        {\n          ids.sort((a, b) => a / 1 - b / 1).map((id) =>\n            <AccountCard\n              key={id}\n              active={this.state.activeAccountIds.includes(id)}\n              assignment={this.state.assignments[id]}\n              account={this.state.accounts[id]}\n              count={count++}\n            />\n          )\n        }\n      </div>\n    )\n\n    return (\n      <div>\n        <SetAllSyncPolicies accountIds={ids.map((id) => parseInt(id, 10))} />\n        {content}\n      </div>\n    )\n  }\n}\n\nRoot.propTypes = {\n  collapsed: React.PropTypes.bool,\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-dashboard/set-all-sync-policies.jsx",
    "content": "import {React} from 'nylas-exports';\nimport Modal from './modal';\n\nexport default class SetAllSyncPolicies extends React.Component {\n\n  applyToAllAccounts(accountIds) {\n    const req = new XMLHttpRequest();\n    const url = `${window.location.protocol}/sync-policy`;\n    req.open(\"POST\", url, true);\n    req.setRequestHeader(\"Content-type\", \"application/json\");\n    req.onreadystatechange = () => {\n      if (req.readyState === XMLHttpRequest.DONE) {\n        if (req.status === 200) {\n          this.setState({editMode: false});\n        }\n      }\n    }\n\n    const newPolicy = document.getElementById(`sync-policy-all`).value;\n    req.send(JSON.stringify({\n      sync_policy: newPolicy,\n      account_ids: accountIds,\n    }));\n  }\n\n  render() {\n    return (\n      <Modal\n        className=\"sync-policy\"\n        openLink={{\n          text: \"Set sync policies for currently displayed accounts\",\n          className: \"action-link\",\n          id: \"open-all-sync\",\n        }}\n        actionElems={[\n          {\n            title: \"Apply To All Displayed Accounts\",\n            action: () => this.applyToAllAccounts.call(this, this.props.accountIds),\n            type: 'button',\n            className: 'right-action',\n          }, {\n            title: \"Cancel\",\n            type: 'div',\n            className: 'action-link cancel',\n          },\n        ]}\n      >\n        <h3>Sync Policy</h3>\n        <textarea id=\"sync-policy-all\" />\n      </Modal>\n    )\n  }\n}\n\nSetAllSyncPolicies.propTypes = {\n  accountIds: React.PropTypes.arrayOf(React.PropTypes.number),\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-dashboard/sync-graph.jsx",
    "content": "import {React, ReactDOM} from 'nylas-exports';\n\nsetInterval(() => {\n  const event = new Event('graphtick')\n  window.dispatchEvent(event);\n}, 10000);\n\nexport default class SyncGraph extends React.Component {\n  componentDidMount() {\n    this.drawGraph(true);\n\n    this.onGraphTick = () => {\n      if (Date.now() - this.props.syncTimestamps[0] > 10000) {\n        this.drawGraph(false);\n      }\n    }\n    window.addEventListener('graphtick', this.onGraphTick);\n  }\n\n  componentDidUpdate() {\n    this.drawGraph(false);\n  }\n\n  componentWillUnmount() {\n    window.removeEventListener('graphtick', this.onGraphTick);\n  }\n\n  drawGraph(isInitial) {\n    const now = Date.now();\n    const config = SyncGraph.config;\n    const node = ReactDOM.findDOMNode(this);\n    const context = node.getContext('2d');\n\n    if (isInitial) {\n      const totalHeight = config.height + config.labelFontSize + config.labelTopMargin;\n      node.width = config.width * 2;\n      node.height = totalHeight * 2;\n      node.style.width = `${config.width}px`;\n      node.style.height = `${totalHeight}px`;\n      context.scale(2, 2);\n\n      // Axis labels\n      context.fillStyle = config.labelColor;\n      context.font = `${config.labelFontSize}px sans-serif`;\n      const fontY = config.height + config.labelFontSize + config.labelTopMargin;\n      const nowText = \"now\";\n      const nowWidth = context.measureText(nowText).width;\n      context.fillText(nowText, config.width - nowWidth - 1, fontY);\n      context.fillText(\"-30m\", 1, fontY);\n    }\n\n    // Background\n    // (This hides any previous data points, so we don't have to clear the canvas)\n    context.fillStyle = config.backgroundColor;\n    context.fillRect(0, 0, config.width, config.height);\n\n    // Data points\n    const pxPerSec = config.width / config.timeLength;\n    context.strokeStyle = config.dataColor;\n    context.beginPath();\n\n    for (const syncTimeMs of this.props.syncTimestamps) {\n      const secsAgo = (now - syncTimeMs) / 1000;\n      const pxFromRight = secsAgo * pxPerSec;\n      const pxFromLeft = config.width - pxFromRight;\n      context.moveTo(pxFromLeft, 0);\n      context.lineTo(pxFromLeft, config.height);\n    }\n    context.stroke();\n\n    // Tick marks\n    const interval = config.width / config.numTicks;\n    context.strokeStyle = config.tickColor;\n    context.beginPath();\n    for (let px = interval; px < config.width; px += interval) {\n      context.moveTo(px, config.height - config.tickHeight);\n      context.lineTo(px, config.height);\n    }\n    context.stroke();\n  }\n\n  render() {\n    return (\n      <canvas\n        width={SyncGraph.config.width}\n        height={SyncGraph.config.height + SyncGraph.config.labelFontSize + SyncGraph.config.labelTopMargin}\n        className=\"sync-graph\"\n      />\n    )\n  }\n\n}\n\nSyncGraph.config = {\n  height: 50, // Doesn't include labels\n  width: 240,\n  // timeLength is 30 minutes in seconds. If you change this, be sure to update\n  // syncGraphTimeLength in sync-worker.js and the axis labels in drawGraph()!\n  timeLength: 60 * 30,\n  numTicks: 10,\n  tickHeight: 10,\n  tickColor: 'white',\n  labelFontSize: 8,\n  labelTopMargin: 2,\n  labelColor: 'black',\n  backgroundColor: 'black',\n  dataColor: '#43a1ff',\n}\n\nSyncGraph.propTypes = {\n  syncTimestamps: React.PropTypes.arrayOf(React.PropTypes.number),\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-dashboard/syncback-request-details.jsx",
    "content": "import {React} from 'nylas-exports';\nimport Dropdown from './dropdown';\nimport Modal from './modal';\n\nexport default class SyncbackRequestDetails extends React.Component {\n  constructor(props) {\n    super(props);\n    this.state = {\n      accountId: props.accountId,\n      syncbackRequests: null,\n      counts: null,\n      statusFilter: 'all',\n      refreshInterval: null,\n    };\n  }\n\n  getDetails() {\n    const req = new XMLHttpRequest();\n    const url = `${window.location.protocol}/syncback-requests/${this.state.accountId}`;\n    req.open(\"GET\", url, true);\n    req.onreadystatechange = () => {\n      if (req.readyState === XMLHttpRequest.DONE) {\n        if (req.status === 200) {\n          this.setState({syncbackRequests: req.responseText});\n        } else {\n          console.error(req.responseText);\n        }\n      }\n    }\n    req.send();\n  }\n\n  getCounts() {\n    const since = Date.now() - 1000 * 60 * 60; // one hour ago\n    const req = new XMLHttpRequest();\n    const url = `${window.location.protocol}/syncback-requests/${this.state.accountId}/counts?since=${since}`;\n    req.open(\"GET\", url, true);\n    req.onreadystatechange = () => {\n      if (req.readyState === XMLHttpRequest.DONE) {\n        if (req.status === 200) {\n          this.setState({counts: JSON.parse(req.responseText)});\n        } else {\n          console.error(req.responseText);\n        }\n      }\n    }\n    req.send();\n  }\n\n  setStatusFilter(statusFilter) {\n    this.setState({statusFilter: statusFilter});\n  }\n\n  setAutoRefresh() {\n    if (document.getElementById('syncback-requests-auto')) {\n      const interval = setInterval(() => {\n        this.getCounts();\n        this.getDetails();\n      }, 3000);\n      this.setState({refreshInterval: interval});\n    } else {\n      clearInterval(this.state.refreshInterval);\n      this.setState({refreshInterval: null});\n    }\n  }\n\n  render() {\n    let counts = \"Loading...\";\n    if (this.state.counts) {\n      const total = this.state.counts.new + this.state.counts.failed\n        + this.state.counts.succeeded;\n      if (total === 0) {\n        counts = <div className=\"counts\"> No requests made in the last hour </div>\n      } else {\n        counts = (\n          <div className=\"counts\">\n            Of requests created in the last hour:\n            <span\n              style={{color: 'rgb(222, 68, 68)'}}\n              title={`${this.state.counts.failed} out of ${total}`}\n            >\n              {this.state.counts.failed / total * 100}&#37; failed\n            </span>\n            <span\n              style={{color: 'green'}}\n              title={`${this.state.counts.succeeded} out of ${total}`}\n            >\n              {this.state.counts.succeeded / total * 100}&#37; succeeded\n            </span>\n            <span\n              style={{color: 'rgb(98, 98, 179)'}}\n              title={`${this.state.counts.new} out of ${total}`}\n            >\n              {/* .new was throwing off my syntax higlighting, so ignoring linter*/}\n              {this.state.counts.new / total * 100}&#37; are still new\n            </span>\n          </div>\n        )\n      }\n    }\n\n    let details = \"Loading...\"\n    if (this.state.syncbackRequests) {\n      let reqs = JSON.parse(this.state.syncbackRequests);\n      if (this.state.statusFilter !== 'all') {\n        reqs = reqs.filter((req) => req.status === this.state.statusFilter);\n      }\n      const rows = [];\n      if (reqs.length === 0) {\n        rows.push(<tr><td>No results</td><td>-</td><td>-</td></tr>);\n      }\n      for (let i = reqs.length - 1; i >= 0; i--) {\n        const req = reqs[i];\n        const date = new Date(req.createdAt);\n        rows.push(<tr key={req.id} title={`id: ${req.id}`}>\n          <td> {req.status} </td>\n          <td> {req.type} </td>\n          <td> {date.toLocaleTimeString()}, {date.toLocaleDateString()} </td>\n        </tr>)\n      }\n      details = (\n        <table><tbody>\n          <tr>\n            <th>\n              Status:&nbsp;\n              <Dropdown\n                options={['all', 'FAILED', 'NEW', 'SUCCEEDED']}\n                defaultOption=\"all\"\n                onSelect={(status) => this.setStatusFilter.call(this, status)}\n              />\n            </th>\n            <th> Type </th>\n            <th> Created At </th>\n          </tr>\n          {rows}\n        </tbody></table>\n      );\n    }\n\n    return (\n      <Modal\n        id=\"syncback-request-details\"\n        openLink={{\n          text: \"Syncback Request Details\",\n          className: \"action-link\",\n        }}\n        onOpen={() => {\n          this.getDetails();\n          this.getCounts();\n        }}\n        onClose={() => {\n          clearInterval(this.state.refreshInterval);\n        }}\n      >\n        <input\n          id=\"syncback-requests-auto\"\n          type=\"checkbox\"\n          onChange={() => this.setAutoRefresh.call(this)}\n        />\n        Auto Refresh <br /><br />\n        <h3>Recent Stats</h3>\n        {counts}\n        <br />\n        <h3>Stored Syncback Requests</h3>\n        {details}\n      </Modal>\n    )\n  }\n}\n\nSyncbackRequestDetails.propTypes = {\n  accountId: React.PropTypes.number,\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/imap-helpers.js",
    "content": "const _ = require('underscore')\nconst {Errors: {APIError}} = require('isomorphic-core')\n\nconst IMAPHelpers = {\n  async messagesForThreadByFolder(db, threadId) {\n    const thread = await db.Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`IMAPHelpers.messagesForThreadByFolder - Can't find thread`, 404)\n    }\n    const messages = await thread.getMessages()\n    return _.groupBy(messages, 'folderId')\n  },\n\n  async forEachFolderOfThread({db, imap, threadMessages, callback}) {\n    if (!threadMessages.every(m => m.folderImapUID != null)) {\n      throw new APIError('All messages in a thread require an IMAP uid to perform an action')\n    }\n    const {Folder} = db\n    const msgsByFolder = _.groupBy(threadMessages, 'folderId')\n    const folderIds = Object.keys(msgsByFolder)\n    const folders = await Folder.findAll({where: {id: folderIds}})\n\n    for (const folder of folders) {\n      const msgsInFolder = msgsByFolder[folder.id]\n      if (msgsInFolder.length === 0) { continue }\n      const messageImapUIDs = msgsInFolder.map(m => m.folderImapUID)\n      const box = await imap.openBox(folder.name, {readOnly: false})\n      await callback({folder, messages: msgsInFolder, messageImapUIDs, box})\n    }\n  },\n\n  async forEachLabelSetOfMessages({messages, callback}) {\n    const messagesByLabelSet = new Map()\n    const labelIdentifiersByLabelSet = new Map()\n\n    await Promise.all(messages.map(async (message) => {\n      const labels = await message.getLabels()\n      if (!labels || labels.length === 0) {\n        return\n      }\n      const labelIdentifiers = labels.map(l => l.imapLabelIdentifier())\n      const labelSet = (\n        labelIdentifiers\n        .sort((l1, l2) => {\n          if (l1.toLowerCase() === l2.toLowerCase()) {\n            return 0\n          }\n          return l1.toLowerCase() < l2.toLowerCase() ? -1 : 1\n        })\n        .join('')\n      )\n      labelIdentifiersByLabelSet.set(labelSet, labelIdentifiers)\n      if (messagesByLabelSet.has(labelSet)) {\n        const currentMsgs = messagesByLabelSet.get(labelSet)\n        messagesByLabelSet.set(labelSet, [...currentMsgs, message])\n      } else {\n        messagesByLabelSet.set(labelSet, [message])\n      }\n    }))\n\n    for (const [labelSet, msgs] of messagesByLabelSet) {\n      const labelIdentifiers = labelIdentifiersByLabelSet.get(labelSet)\n      if (labelIdentifiers.length > 0) {\n        await callback({messages: msgs, labelIdentifiers})\n      }\n    }\n  },\n\n  async openMessageBox({messageId, db, imap}) {\n    const {Message} = db\n    const message = await Message.findById(messageId)\n    const folder = await message.getFolder()\n    if (!folder) {\n      throw new Error(`IMAPHelpers.openMessageBox - message does not have a folder`)\n    }\n    const box = await imap.openBox(folder.name)\n    return {box, message}\n  },\n\n  async setLabelsForMessages({db, box, messages, labelIds}) {\n    const sentLabel = await db.Label.find({where: {role: 'sent'}});\n    if (!sentLabel) {\n      throw new APIError('No Sent label present')\n    }\n    const sentLabelIdentifier = sentLabel.imapLabelIdentifier()\n    if (!labelIds || labelIds.length === 0) {\n      // If labelIds is empty, we need to get each message's labels and remove\n      // them, because an empty array is invalid input for `setLabels`\n      return IMAPHelpers.forEachLabelSetOfMessages({\n        messages,\n        async callback({messages: msgs, labelIdentifiers}) {\n          // We can't remove the Sent label in gmail, otherwise the operation will\n          // fail silently!\n          const labelsToRemove = labelIdentifiers.filter(l => l !== sentLabelIdentifier)\n          if (labelsToRemove.length > 0) {\n            await box.removeLabels(msgs.map(m => m.folderImapUID), labelsToRemove)\n          }\n        },\n      })\n    }\n\n    const labelsToSet = (\n      await db.Label.findAll({where: {id: labelIds}})\n      .map(label => label.imapLabelIdentifier())\n    )\n    return IMAPHelpers.forEachLabelSetOfMessages({\n      messages,\n      async callback({messages: msgs, labelIdentifiers}) {\n        const msgLabelsContainSent = labelIdentifiers.find(l => l === sentLabelIdentifier)\n        const labelsToSetContainSent = labelsToSet.find(l => l === sentLabelIdentifier)\n\n        let actualLabelsToSet = [...labelsToSet]\n        if (!msgLabelsContainSent && labelsToSetContainSent) {\n          // We can't try to add the Sent label in gmail, otherwise the operation will\n          // fail silently!\n          actualLabelsToSet = actualLabelsToSet.filter(l => l !== sentLabelIdentifier)\n        }\n        if (msgLabelsContainSent && !labelsToSetContainSent) {\n          // If we reach this condition, it means that we want to add\n          // labelsToSet, but we cant overwrite the Sent label, so we just add it to the\n          // labelsToSet\n          actualLabelsToSet.push(sentLabel.imapLabelIdentifier())\n        }\n\n        if (actualLabelsToSet.length > 0) {\n          await box.setLabels(msgs.map(m => m.folderImapUID), actualLabelsToSet)\n        }\n      },\n    })\n  },\n}\nmodule.exports = IMAPHelpers\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/index.js",
    "content": "const {AccountStore} = require('nylas-exports');\n\nconst LocalDatabaseConnector = require('../shared/local-database-connector')\nconst manager = require('./sync-process-manager')\n\n// Right now, it's a bit confusing because N1 has Account objects, and K2 has\n// Account objects. We want to sync all K2 Accounts, but when an N1 Account is\n// deleted, we want to delete the K2 account too.\n\nconst deletionsInProgress = new Set();\n\nasync function ensureK2Consistency() {\n  const {Account} = await LocalDatabaseConnector.forShared();\n  const k2Accounts = await Account.findAll();\n  const n1Accounts = AccountStore.accounts();\n  const n1Emails = n1Accounts.map(a => a.emailAddress);\n\n  const deletions = [];\n  for (const k2Account of k2Accounts) {\n    const deleted = !n1Emails.includes(k2Account.emailAddress);\n    if (deleted && !deletionsInProgress.has(k2Account.id)) {\n      const logger = global.Logger.forAccount(k2Account)\n      logger.warn(`Deleting K2 account ID ${k2Account.id} which could not be matched to an N1 account.`)\n      deletionsInProgress.add(k2Account.id)\n      await manager.removeWorkerForAccountId(k2Account.id);\n      LocalDatabaseConnector.destroyAccountDatabase(k2Account.id);\n      const deletion = k2Account.destroy().then(() => deletionsInProgress.delete(k2Account.id))\n      deletions.push(deletion)\n    }\n  }\n  return await Promise.all(deletions)\n}\n\nensureK2Consistency().then(() => {\n  // Step 1: Start all K2 Accounts\n  manager.start();\n});\n\n// Step 2: Watch N1 Accounts, ensure consistency when they change.\nAccountStore.listen(ensureK2Consistency);\n\nglobal.manager = manager;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/local-sync-delta-emitter.es6",
    "content": "import _ from 'underscore'\nimport {DeltaStreamBuilder} from 'isomorphic-core'\nimport {DatabaseStore, DeltaProcessor} from 'nylas-exports'\nimport TransactionConnector from '../shared/transaction-connector'\n\n\nexport default class LocalSyncDeltaEmitter {\n  constructor(account, db) {\n    this._db = db;\n    this._state = null\n    this._account = account;\n    this._disposable = {dispose: () => {}}\n    this._writeStateDebounced = _.debounce(this._writeState, 100)\n  }\n\n  async activate() {\n    if (this._disposable && this._disposable.dispose) {\n      this._disposable.dispose()\n    }\n    if (!this._state) {\n      this._state = await this._loadState()\n    }\n    const cursor = this._state.cursor || 0\n    this._disposable = DeltaStreamBuilder.buildDeltaObservable({\n      cursor,\n      db: this._db,\n      accountId: this._account.id,\n      deltasSource: TransactionConnector.getObservableForAccountId(this._account.id),\n    })\n    .subscribe((deltas) => {\n      this._onDeltasReceived(deltas)\n    })\n  }\n\n  deactivate() {\n    this._state = null\n    if (this._disposable && this._disposable.dispose) {\n      this._disposable.dispose()\n    }\n  }\n\n  _onDeltasReceived(deltas = []) {\n    const last = deltas[deltas.length - 1]\n    if (last) {\n      this._state.cursor = last.cursor;\n      this._writeStateDebounced();\n    }\n    DeltaProcessor.process(deltas, {source: \"localSync\"})\n  }\n\n  async _loadState() {\n    const json = await DatabaseStore.findJSONBlob(`LocalSyncStatus:${this._account.id}`)\n    if (json) {\n      return {\n        cursor: json.cursor || undefined,\n      }\n    }\n\n    // Migrate from old storage key\n    const oldState = await DatabaseStore.findJSONBlob(`NylasSyncWorker:${this._account.id}`)\n    if (!oldState) {\n      return {\n        cursor: undefined,\n      }\n    }\n\n    const {deltaCursors = {}} = oldState\n    return {\n      cursor: deltaCursors.localSync,\n    }\n  }\n\n  async _writeState() {\n    if (!this._state) { return }\n    await DatabaseStore.inTransaction(t =>\n      t.persistJSONBlob(`LocalSyncStatus:${this._account.id}`, this._state)\n    );\n  }\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/newrelic.js",
    "content": "const {NODE_ENV} = process.env\n/**\n * New Relic agent configuration.\n *\n * See lib/config.defaults.js in the agent distribution for a more complete\n * description of configuration variables and their potential values.\n */\nexports.config = {\n  /**\n   * Array of application names.\n   */\n  app_name: [`k2-sync-${NODE_ENV}`],\n  logging: {\n    /**\n     * Level at which to log. 'trace' is most useful to New Relic when diagnosing\n     * issues with the agent, 'info' and higher will impose the least overhead on\n     * production applications.\n     */\n    level: 'info',\n  },\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/send-task-manager.es6",
    "content": "/* eslint global-require: 0 */\nimport {SendmailClient} from 'isomorphic-core'\nimport {Actions} from 'nylas-exports'\nimport LocalDatabaseConnector from '../shared/local-database-connector'\nimport SendTaskRunner from './send-task-runner'\nimport {ensureGmailAccessToken} from './sync-utils'\n\nclass SendTaskManager {\n\n  async activate() {\n    this._unsubscribe = Actions.runSendRequest.listen(this._runSendRequest)\n    this._sendTaskRunnersByAccountId = {}\n  }\n\n  async deactivate() {\n    this._unsubscribe()\n  }\n\n  _runSendRequest = async (args) => {\n    const {onSuccess, onError, syncbackRequestJSON} = args\n    try {\n      const task = await this._createTask(syncbackRequestJSON)\n      const responseJSON = await this._runTask(task, onSuccess, onError)\n      onSuccess(responseJSON)\n    } catch (err) {\n      onError(err)\n    }\n  }\n\n  async _createTask(syncbackRequestJSON) {\n    const {type, accountId} = syncbackRequestJSON\n    const {Account} = await LocalDatabaseConnector.forShared()\n    const account = await Account.findById(accountId)\n\n    let Task;\n    if (type === 'SendMessage') {\n      Task = require('./syncback-tasks/send-message.smtp')\n    } else if (type === 'SendMessagePerRecipient') {\n      Task = require('./syncback-tasks/send-message-per-recipient.smtp')\n    }\n    // We don't pass in an actual instance of SyncbackRequest because we don't\n    // need any of its data to be persistent across app sessions. Status updates\n    // should work as expected within an app session due to object aliasing\n    return new Task(account, syncbackRequestJSON)\n  }\n\n  async _runTask(task) {\n    const {_account: account} = task\n    let newCredentials;\n    const logger = global.Logger.forAccount(account)\n    if (account.provider === 'gmail') {\n      newCredentials = await ensureGmailAccessToken({logger, account, expiryBufferInSecs: 2 * 60})\n    }\n    let sendTaskRunner = this._sendTaskRunnersByAccountId[account.id]\n    if (newCredentials || !sendTaskRunner || sendTaskRunner.expiresAtInMs < (Date.now() - 60 * 1000)) {\n      const db = await LocalDatabaseConnector.forAccount(account.id)\n      const smtp = new SendmailClient(account, logger)\n      sendTaskRunner = new SendTaskRunner({account, db, smtp, logger})\n      this._sendTaskRunnersByAccountId[account.id] = sendTaskRunner\n    }\n    return sendTaskRunner.runTask(task)\n  }\n\n}\n\nexport default new SendTaskManager()\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/send-task-runner.es6",
    "content": "import {ExponentialBackoffScheduler} from 'isomorphic-core'\nimport {Actions} from 'nylas-exports'\nimport {runWithRetryLogic} from './sync-utils'\n\nexport default class SendTaskRunner {\n  constructor({account, db, smtp, logger}) {\n    this._account = account\n    this._db = db\n    this._smtp = smtp\n    this._logger = logger\n    this.expiresAtInMs = account.decryptedCredentials().expiry_date * 1000\n  }\n\n  runTask = async (task) => {\n    const before = new Date();\n    if (before > this.expiresAtInMs) {\n      throw new Error('SendTaskRunner.runTask: stored credentials have expired')\n    }\n    // syncbackRequests for send tasks aren't persistent database instances\n    const syncbackRequestJSON = task.syncbackRequestObject();\n    this._logger.log(`🔃 📤 ${task.description()}`, syncbackRequestJSON.props)\n\n    const run = async () => {\n      syncbackRequestJSON.status = task.inProgressStatusType();\n      const responseJSON = await task.run(this._db, this._smtp)\n      syncbackRequestJSON.status = \"SUCCEEDED\";\n      const after = new Date();\n      this._logger.log(`🔃 📤 ${task.description()} Succeeded (${after.getTime() - before.getTime()}ms)`)\n      return responseJSON\n    }\n\n    const onRetryableError = (error, delay) => {\n      const after = new Date();\n      Actions.recordUserEvent('Retrying syncback task', {\n        accountId: this._account.id,\n        provider: this._account.provider,\n        errorMessage: error.message,\n      })\n      syncbackRequestJSON.status = \"NEW\";\n      this._logger.warn(`🔃 📤 ${task.description()} Failed with retryable error, retrying in ${delay}ms (This run took ${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequestJSON, error})\n    }\n\n    const retryScheduler = new ExponentialBackoffScheduler({\n      baseDelay: 1000,\n      maxDelay: 2 * 60 * 1000,\n    })\n\n    try {\n      const responseJSON = await runWithRetryLogic({run, onRetryableError, retryScheduler})\n      return responseJSON\n    } catch (error) {\n      const after = new Date();\n      const fingerprint = [\"{{ default }}\", \"syncback task\", error.message];\n      NylasEnv.reportError(error, {fingerprint: fingerprint});\n      syncbackRequestJSON.error = error;\n      syncbackRequestJSON.status = \"FAILED\";\n      this._logger.error(`🔃 📤 ${task.description()} Failed (${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequestJSON, error})\n      throw error\n    }\n  }\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-process-manager.es6",
    "content": "const _ = require('underscore')\nconst {ipcRenderer} = require('electron')\nconst {Actions, OnlineStatusStore, IdentityStore} = require('nylas-exports')\nconst SyncWorker = require('./sync-worker');\nconst LocalSyncDeltaEmitter = require('./local-sync-delta-emitter').default\nconst LocalDatabaseConnector = require('../shared/local-database-connector')\nconst SyncActivity = require('../shared/sync-activity').default\n\nconst MAX_WORKER_SILENCE_MS = Math.max(\n  SyncWorker.AC_SYNC_LOOP_INTERVAL_MS,\n  SyncWorker.BATTERY_SYNC_LOOP_INTERVAL_MS,\n  SyncWorker.MAX_SYNC_BACKOFF_MS,\n)\nconst CHECK_HEALTH_TIME_INTERVAL = 1 * 60 * 1000\n\nclass SyncProcessManager {\n  constructor() {\n    this._exiting = false;\n    this._resettingEmailCache = false\n    this._identityId = IdentityStore.identityId();\n    this._workersByAccountId = {};\n    this._localSyncDeltaEmittersByAccountId = new Map()\n\n    OnlineStatusStore.listen(this._onOnlineStatusChanged, this)\n    IdentityStore.listen(this._onIdentityChanged, this)\n    Actions.resetEmailCache.listen(this._resetEmailCache, this)\n    Actions.debugSync.listen(this._onDebugSync, this)\n    Actions.wakeLocalSyncWorkerForAccount.listen((accountId) =>\n      this.wakeWorkerForAccount(accountId, {interrupt: true})\n    );\n    ipcRenderer.on('app-resumed-from-sleep', () => {\n      this._wakeAllWorkers({reason: 'Computer resumed from sleep', interrupt: true})\n    })\n\n    this._checkHealthInterval = null;\n  }\n\n  _onOnlineStatusChanged() {\n    if (OnlineStatusStore.isOnline()) {\n      this._wakeAllWorkers({reason: 'Came back online', interrupt: true})\n    }\n  }\n\n  _onIdentityChanged() {\n    const newIdentityId = IdentityStore.identityId()\n    if (newIdentityId !== this._identityId) {\n      // The IdentityStore can trigger any number of times, but we only want to\n      // start sync if we previously didn't have an identity available\n      this._identityId = newIdentityId\n      this.start()\n    }\n  }\n\n  _onDebugSync() {\n    const win = NylasEnv.getCurrentWindow()\n    win.show()\n    win.maximize()\n    win.openDevTools()\n  }\n\n  _wakeAllWorkers({reason, interrupt} = {}) {\n    Object.keys(this._workersByAccountId).forEach((id) => {\n      this.wakeWorkerForAccount(id, {reason, interrupt})\n    })\n  }\n\n  async _resetEmailCache() {\n    if (this._resettingEmailCache) return;\n    this._resettingEmailCache = true\n    try {\n      try {\n        await Promise.all(\n          this.workers().map(w => w.destroy())\n        )\n        .timeout(500, 'Timed out while trying to stop sync')\n      } catch (err) {\n        global.Logger.warn('SyncProcessManager._resetEmailCache: Error while stopping sync', err)\n      }\n      const accountIds = Object.keys(this._workersByAccountId)\n      for (const accountId of accountIds) {\n        await LocalDatabaseConnector.destroyAccountDatabase(accountId)\n      }\n\n      ipcRenderer.send('command', 'application:relaunch-to-initial-windows', {\n        resetDatabase: true,\n      })\n    } catch (err) {\n      global.Logger.error('Error resetting email cache', err)\n    } finally {\n      this._resettingEmailCache = false\n    }\n  }\n\n  _checkHealthByAccountId = async (accountId) => {\n    const {time, activity} = SyncActivity.getLastSyncActivityForAccount(accountId);\n    if (time < Date.now() - this.MAX_WORKER_SILENCE_MS) {\n      const duration = Date.now() - time;\n      const {Account} = await LocalDatabaseConnector.forShared();\n      const account = await Account.findById(accountId)\n      const logger = global.Logger.forAccount(account)\n      logger.warn(`SyncProcessManager: Detected stuck worker. Last recorded activity: ${activity} at ${new Date(time)}`)\n\n      await this.removeWorkerForAccountId(accountId, {timeout: 5 * 1000})\n      await this.addWorkerForAccount(account)\n    }\n  }\n\n  _checkHealth = async () => {\n    return Promise.all(Object.keys(this._workersByAccountId).map(this._checkHealthByAccountId))\n  }\n\n  /**\n   * Useful for debugging.\n   */\n  async start() {\n    if (!IdentityStore.identity()) {\n      global.Logger.log(`SyncProcessManager: Can't start sync; no Nylas Identity present`)\n      return\n    }\n    global.Logger.log(`SyncProcessManager: Starting sync`)\n\n    const {Account} = await LocalDatabaseConnector.forShared();\n    const accounts = await Account.findAll();\n    await Promise.all(accounts.map(this.addWorkerForAccount));\n\n    if (!this._checkHealthInterval) {\n      this._checkHealthInterval = setInterval(this._checkHealth, this.CHECK_HEALTH_TIME_INTERVAL)\n    }\n  }\n\n  workers() {\n    return _.values(this._workersByAccountId)\n  }\n\n  dbs() {\n    return this.workers().map(w => w._db)\n  }\n\n  wakeWorkerForAccount(accountId, {reason = 'Waking sync', interrupt} = {}) {\n    const worker = this._workersByAccountId[accountId]\n    if (worker) {\n      worker.syncNow({reason, interrupt});\n    }\n  }\n\n  addWorkerForAccount = async (account) => {\n    await LocalDatabaseConnector.ensureAccountDatabase(account.id);\n    const logger = global.Logger.forAccount(account)\n\n    try {\n      if (this._workersByAccountId[account.id]) {\n        logger.warn(`SyncProcessManager.addWorkerForAccount: Worker for account already exists - skipping`)\n        return\n      }\n      const db = await LocalDatabaseConnector.forAccount(account.id);\n      this._workersByAccountId[account.id] = new SyncWorker(account, db, this);\n\n      const localSyncDeltaEmitter = new LocalSyncDeltaEmitter(account, db)\n      await localSyncDeltaEmitter.activate()\n      this._localSyncDeltaEmittersByAccountId.set(account.id, localSyncDeltaEmitter)\n      logger.log(`SyncProcessManager: Claiming Account Succeeded`)\n    } catch (err) {\n      logger.error(`SyncProcessManager: Claiming Account Failed`, err)\n    }\n  }\n\n  async removeWorkerForAccountId(accountId, {timeout} = {}) {\n    if (this._workersByAccountId[accountId]) {\n      await this._workersByAccountId[accountId].destroy({timeout})\n      delete this._workersByAccountId[accountId];\n    }\n\n    if (this._localSyncDeltaEmittersByAccountId.has(accountId)) {\n      this._localSyncDeltaEmittersByAccountId.get(accountId).deactivate();\n      this._localSyncDeltaEmittersByAccountId.delete(accountId)\n    }\n  }\n}\n\nwindow.$n.SyncProcessManager = new SyncProcessManager();\nwindow.$n.SyncProcessManager.MAX_WORKER_SILENCE_MS = MAX_WORKER_SILENCE_MS\nwindow.$n.SyncProcessManager.CHECK_HEALTH_TIME_INTERVAL = CHECK_HEALTH_TIME_INTERVAL\n\nmodule.exports = window.$n.SyncProcessManager\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-task-factory.js",
    "content": "/* eslint global-require: 0 */\nclass SyncTaskFactory {\n\n  static create(taskName, ...args) {\n    let Task = null;\n    switch (taskName) {\n      case \"FetchFolderList\":\n        Task = require('./sync-tasks/fetch-folder-list.imap'); break;\n      case \"FetchMessagesInFolder\":\n        Task = require('./sync-tasks/fetch-messages-in-folder.imap'); break;\n      case \"FetchNewMessagesInFolder\":\n        Task = require('./sync-tasks/fetch-new-messages-in-folder.imap'); break;\n      case \"FetchSpecificMessagesInFolder\":\n        Task = require('./sync-tasks/fetch-specific-messages-in-folder.imap'); break;\n      default:\n        throw new Error(`Task type not defined in sync task factory: ${taskName}`)\n    }\n    return new Task(...args)\n  }\n}\n\nmodule.exports = SyncTaskFactory\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-tasks/fetch-folder-list.imap.es6",
    "content": "const {Provider} = require('isomorphic-core');\nconst SyncTask = require('./sync-task')\nconst {localizedCategoryNames} = require('../sync-utils')\nconst SyncActivity = require('../../shared/sync-activity').default\n\nconst GMAIL_ROLES_WITH_FOLDERS = ['all', 'trash', 'spam'];\n\nconst assignedRolesCache = new Set();\n\nclass FetchFolderListIMAP extends SyncTask {\n  constructor(...args) {\n    super(...args)\n    this._provider = this._account.provider;\n  }\n\n  description() {\n    return `FetchFolderListIMAP`;\n  }\n\n  _classForMailboxWithRole(role, {Folder, Label}) {\n    if (this._provider === Provider.Gmail) {\n      return GMAIL_ROLES_WITH_FOLDERS.includes(role) ? Folder : Label;\n    }\n    return Folder;\n  }\n\n  async _roleAlreadyAssigned(role) {\n    if (assignedRolesCache.has(role)) {\n      return true\n    }\n    const Klass = this._classForMailboxWithRole(role, this._db);\n    const existing = await Klass.findAll({where: {role: role}})\n    if (existing.length > 0) {\n      assignedRolesCache.add(role)\n      return true\n    }\n    return false\n  }\n\n  _detectRole(boxName, box) {\n    return this._roleByAttr(box) || this._roleByName(boxName);\n  }\n\n  _roleByName(boxName) {\n    for (const role of Object.keys(localizedCategoryNames)) {\n      if (localizedCategoryNames[role].has(boxName.toLowerCase().trim())) {\n        return role;\n      }\n    }\n    return null;\n  }\n\n  _roleByAttr(box) {\n    for (const attrib of (box.attribs || [])) {\n      const role = {\n        '\\\\Sent': 'sent',\n        '\\\\Drafts': 'drafts',\n        '\\\\Junk': 'spam',\n        '\\\\Spam': 'spam',\n        '\\\\Trash': 'trash',\n        '\\\\All': 'all',\n        '\\\\Important': 'important',\n        '\\\\Flagged': 'starred',\n        '\\\\Inbox': 'inbox',\n      }[attrib];\n      if (role) {\n        return role;\n      }\n    }\n    return null;\n  }\n\n  async _updateCategoriesWithBoxes(categories, boxes) {\n    const stack = [];\n    const created = [];\n    const existing = new Set();\n\n    Object.keys(boxes).forEach((boxName) => {\n      stack.push([[boxName], boxes[boxName]]);\n    });\n\n    while (stack.length > 0) {\n      const [boxPath, box] = stack.pop();\n\n      if (!box.attribs) {\n        if (box.children) {\n          // In Fastmail, folders which are just containers for other folders\n          // have no attributes at all, just a children property. Add appropriate\n          // attribs so we can carry on.\n          box.attribs = ['\\\\HasChildren', '\\\\NoSelect'];\n        } else {\n          // Some boxes seem to come back as partial objects. Not sure why.\n          continue;\n        }\n      }\n\n      const boxName = boxPath.join(box.delimiter);\n\n      if (box.children && box.attribs.includes('\\\\HasChildren')) {\n        Object.keys(box.children).forEach((subname) => {\n          stack.push([[].concat(boxPath, [subname]), box.children[subname]]);\n        });\n      }\n\n      const lowerCaseAttrs = box.attribs.map(attr => attr.toLowerCase())\n      if (lowerCaseAttrs.includes('\\\\noselect') || lowerCaseAttrs.includes('\\\\nonexistent')) {\n        continue;\n      }\n\n      let category = categories.find((cat) => cat.name === boxName);\n      if (!category) {\n        let role = this._detectRole(boxName, box);\n        if (await this._roleAlreadyAssigned(role)) {\n          role = null;\n        }\n        const Klass = this._classForMailboxWithRole(role, this._db);\n        const {accountId} = this._db\n        category = Klass.build({\n          accountId,\n          id: Klass.hash({boxName, accountId}),\n          name: boxName,\n          role: role,\n        });\n        created.push(category);\n      } else if (!category.role) {\n        // if we update the category->role mapping to include more names, we\n        // need to be able to detect newly added roles on existing categories\n        const role = this._roleByName(boxName);\n        if (role && !(await this._roleAlreadyAssigned(role))) {\n          category.role = role;\n          await category.save();\n        }\n      }\n      existing.add(category);\n    }\n\n    // TODO: decide whether these are renames or deletes\n    const deleted = categories.filter(cat => !existing.has(cat));\n\n    for (const category of created) {\n      await category.save()\n    }\n\n    for (const category of deleted) {\n      await category.destroy()\n    }\n  }\n\n  // This operation is interruptible, see `SyncTask` for info on why we use\n  // `yield`\n  async * runTask(db, imap) {\n    const accountId = this._account.id\n    SyncActivity.reportSyncActivity(accountId, `Fetching folder list`)\n    this._logger.log(`🔜  Fetching folder list`)\n    this._db = db;\n\n    const boxes = yield imap.getBoxes();\n    const {Folder, Label} = this._db;\n\n    SyncActivity.reportSyncActivity(accountId, `Finding categories`)\n    const folders = yield Folder.findAll()\n    const labels = yield Label.findAll()\n    const all = [].concat(folders, labels);\n\n    SyncActivity.reportSyncActivity(accountId, `Updating categories`)\n    await this._updateCategoriesWithBoxes(all, boxes);\n\n    this._logger.log(`🔚  Fetching folder list done`)\n    SyncActivity.reportSyncActivity(accountId, `Done fetching folder list`)\n  }\n}\n\nmodule.exports = FetchFolderListIMAP;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-tasks/fetch-messages-in-folder.imap.es6",
    "content": "const _ = require('underscore');\nconst {IMAPConnection} = require('isomorphic-core');\nconst {Capabilities} = IMAPConnection;\nconst SyncTask = require('./sync-task')\nconst MessageProcessor = require('../../message-processor')\nconst SyncActivity = require('../../shared/sync-activity').default\n\nconst MessageFlagAttributes = ['id', 'threadId', 'folderImapUID', 'unread', 'starred', 'folderImapXGMLabels']\nconst FETCH_ATTRIBUTES_BATCH_SIZE = 1000;\nconst MIN_MESSAGE_BATCH_SIZE = 30;\nconst MAX_MESSAGE_BATCH_SIZE = 300;\nconst BATCH_SIZE_PER_SELECT_SEC = 60;\nconst GMAIL_INITIAL_PRIORITIZE_COUNT = 1000;\n\n\nclass FetchMessagesInFolderIMAP extends SyncTask {\n  constructor({account, folder, uids} = {}) {\n    super({account})\n    this._imap = null\n    this._box = null\n    this._db = null\n    this._folder = folder;\n    this._uids = uids;\n    if (!this._folder) {\n      throw new Error(\"FetchMessagesInFolderIMAP requires a category\")\n    }\n  }\n\n  description() {\n    return `FetchMessagesInFolderIMAP (${this._folder.name} - ${this._folder.id})`;\n  }\n\n  _isFirstSync() {\n    return this._folder.syncState.minUID == null;\n  }\n\n  async _recoverFromUIDInvalidity(boxUidvalidity) {\n    // UID invalidity means the server has asked us to delete all the UIDs for\n    // this folder and start from scratch. Instead of deleting all the messages,\n    // we just remove the folder ID and UID. We may re-assign the same message\n    // the same UID. Otherwise they're eventually garbage collected.\n    const {Message} = this._db;\n    await Message.update({\n      folderId: null,\n      folderImapUID: null,\n    }, {where: {folderId: this._folder.id}})\n\n    await this._folder.updateSyncState({\n      fetchedmax: null,\n      fetchedmin: null,\n      minUID: null,\n      uidvalidity: boxUidvalidity,\n    });\n  }\n\n  async _updateMessageAttributes(remoteUIDAttributes, localMessageAttributes) {\n    const {Label, Thread} = this._db;\n\n    const messageAttributesMap = {};\n    for (const msg of localMessageAttributes) {\n      messageAttributesMap[msg.folderImapUID] = msg;\n    }\n\n    const messagesWithChangedFlags = [];\n    const messagesWithChangedLabels = [];\n\n    // Step 1: Identify changed messages and update their attributes in place\n\n    const preloadedLabels = await Label.findAll();\n    for (const uid of Object.keys(remoteUIDAttributes)) {\n      const msg = messageAttributesMap[uid];\n      const attrs = remoteUIDAttributes[uid];\n\n      if (!msg) continue;\n\n      const unread = !attrs.flags.includes('\\\\Seen');\n      const starred = attrs.flags.includes('\\\\Flagged');\n      const xGmLabels = attrs['x-gm-labels'];\n      const xGmLabelsJSON = xGmLabels ? JSON.stringify(xGmLabels) : null;\n\n      if (msg.folderImapXGMLabels !== xGmLabelsJSON) {\n        await msg.setLabelsFromXGM(xGmLabels, {Label, preloadedLabels})\n        messagesWithChangedLabels.push(msg);\n      }\n\n      if (msg.unread !== unread || msg.starred !== starred) {\n        msg.unread = unread;\n        msg.starred = starred;\n        messagesWithChangedFlags.push(msg);\n      }\n    }\n\n    // Step 2: If flags were changed, apply the changes to the corresponding\n    // threads. We do this as a separate step so we can batch-load the threads.\n    if (messagesWithChangedFlags.length > 0) {\n      const threadIds = messagesWithChangedFlags.map(m => m.threadId);\n      const threads = await Thread.findAll({\n        attributes: ['id', 'unreadCount', 'starredCount'],\n        where: {id: threadIds},\n      });\n      const threadsById = {};\n      for (const thread of threads) {\n        threadsById[thread.id] = thread;\n      }\n      for (const msg of messagesWithChangedFlags) {\n        // unread = false, previous = true? Add -1 to unreadCount.\n        // IMPORTANT: Relies on messages changed above not having been saved yet!\n        threadsById[msg.threadId].unreadCount += msg.unread / 1 - msg.previous('unread') / 1;\n        threadsById[msg.threadId].starredCount += msg.starred / 1 - msg.previous('starred') / 1;\n      }\n      for (const thread of threads) {\n        await thread.save({fields: ['starredCount', 'unreadCount']})\n      }\n    }\n\n    // Step 3: Persist the messages we've updated\n    const messagesChanged = [].concat(messagesWithChangedFlags, messagesWithChangedLabels);\n    for (const messageChanged of messagesChanged) {\n      await messageChanged.save({fields: MessageFlagAttributes})\n    }\n\n    // Step 4: If message labels were changed, retrieve the impacted threads\n    // and re-compute their labels. This is fairly expensive at the moment.\n    if (messagesWithChangedLabels.length > 0) {\n      const threadIds = messagesWithChangedLabels.map(m => m.threadId);\n      const threads = await Thread.findAll({\n        attributes: ['id'],\n        where: {id: threadIds},\n      });\n      for (const thread of threads) {\n        await thread.updateLabelsAndFolders()\n      }\n    }\n    return {\n      numChangedLabels: messagesWithChangedLabels.length,\n      numChangedFlags: messagesWithChangedFlags.length,\n    }\n  }\n\n  async _removeDeletedMessages(remoteUIDAttributes, localMessageAttributes) {\n    const {Message} = this._db;\n\n    const removedUIDs = localMessageAttributes\n    .filter(msg => !remoteUIDAttributes[msg.folderImapUID])\n    .map(msg => msg.folderImapUID)\n\n    if (removedUIDs.length === 0) {\n      return;\n    }\n\n    await Message.update({\n      folderImapUID: null,\n      folderId: null,\n    }, {where: {folderId: this._folder.id, folderImapUID: removedUIDs}})\n  }\n\n  _getDesiredMIMEParts(struct) {\n    const desired = [];\n    const available = [];\n    const unseen = [struct];\n    const desiredTypes = new Set(['text/plain', 'text/html']);\n    // MIME structures can be REALLY FREAKING COMPLICATED. To simplify\n    // processing, we flatten the MIME structure by walking it depth-first,\n    // throwing away all multipart headers with the exception of\n    // multipart/alternative trees. We special case these, flattening via a\n    // recursive call and then extracting only HTML parts, since their\n    // equivalent nature allows us to pick our desired representation and throw\n    // away the rest.\n    while (unseen.length > 0) {\n      const part = unseen.shift();\n      if (part instanceof Array && (part[0].type !== 'alternative')) {\n        unseen.unshift(...part);\n      } else if (part instanceof Array && (part[0].type === 'alternative')) {\n        // Picking our desired alternative part(s) here vastly simplifies\n        // later parsing of the body, since we can then completely ignore\n        // mime structure without making any terrible mistakes. We assume\n        // here that all multipart/alternative MIME parts are arrays of\n        // text/plain vs text/html, which is ~always true (and if it isn't,\n        // the message is bound to be absurd in other ways and we can't\n        // guarantee sensible display).\n        part.shift();\n        const htmlParts = this._getDesiredMIMEParts(part).filter((p) => {\n          return p.mimeType === 'text/html';\n        });\n        if (htmlParts.length > 0) {\n          // Some bizarre emails contain multiple copies of the same MIME\n          // part with the same MIME type. Since multipart/alternative\n          // indicates that each MIME part is a representation of equivalent\n          // data, we can safely keep only one.\n          desired.push(htmlParts[0]);\n        }\n      } else {\n        if (part.size) { // will skip all multipart types\n          const mimeType = `${part.type}/${part.subtype}`;\n          available.push(mimeType);\n          const disposition = part.disposition ? part.disposition.type.toLowerCase() : null;\n          if (desiredTypes.has(mimeType) && (disposition !== 'attachment')) {\n            desired.push({\n              id: part.partID,\n              // encoding and charset may be null\n              transferEncoding: part.encoding,\n              charset: part.params ? part.params.charset : null,\n              mimeType,\n            });\n          }\n        }\n      }\n      // attachment metadata is extracted later---ignore for now\n    }\n\n    if (desired.length === 0 && available.length !== 0) {\n      this._logger.warn(`FetchMessagesInFolderIMAP: Could not find good part`, {\n        available_options: available.join(', '),\n      })\n    }\n\n    return desired;\n  }\n\n  /**\n   * Note: This function is an ES6 generator so we can `yield` at points\n   * we want to interrupt sync. This is enabled by `SyncOperation` and\n   * `Interruptible`\n   *\n   * This either fetches a range from `min` to `maxA`\n   * OR\n   * It can fetch a specific set of `uids`\n   */\n  async * _fetchAndProcessMessages({min, max, uids, throttle = true} = {}) {\n    let rangeQuery;\n    if (uids) {\n      if (min || max) {\n        throw new Error(`Cannot pass min/max AND uid set`);\n      }\n      rangeQuery = uids;\n    } else {\n      if (min < 0 || max < 0) {\n        throw new Error(`Min (${min}) and max (${max}) must be > 0`);\n      } // it's OK if max < min though, IMAP will just invert them\n      rangeQuery = `${min}:${max}`;\n    }\n\n    // this._logger.log(`FetchMessagesInFolderIMAP: Going to FETCH messages in range ${rangeQuery}`);\n    if (!this._syncWorker._batchProcessedUids.has(this._folder.name)) {\n      this._syncWorker._batchProcessedUids.set(this._folder.name, new Set())\n    }\n    const processedUids = this._syncWorker._batchProcessedUids.get(this._folder.name);\n\n    // We batch downloads by which MIME parts from the full message we want\n    // because we can fetch the same part on different UIDs with the same\n    // FETCH, thus minimizing round trips.\n    const uidsByPart = {};\n    const structsByUID = {};\n    const desiredPartsByUID = {};\n    yield this._box.fetchEach(rangeQuery, {struct: true}, ({attributes}) => {\n      if (!processedUids.has(attributes.uid)) {\n        const desiredParts = this._getDesiredMIMEParts(attributes.struct);\n        const key = JSON.stringify(desiredParts.map(p => p.id));\n        desiredPartsByUID[attributes.uid] = desiredParts;\n        structsByUID[attributes.uid] = attributes.struct;\n        uidsByPart[key] = uidsByPart[key] || [];\n        uidsByPart[key].push(attributes.uid);\n      }\n    })\n\n    // Prioritize the batches with the highest UIDs first, since these UIDs\n    // are usually the most recent messages\n    const maxUIDForBatch = {};\n    const partBatchesInOrder = Object.keys(uidsByPart)\n    for (const key of partBatchesInOrder) {\n      maxUIDForBatch[key] = Math.max(...uidsByPart[key]);\n    }\n    partBatchesInOrder.sort((a, b) => maxUIDForBatch[b] - maxUIDForBatch[a]);\n\n    let totalProcessedMessages = 0\n    for (const key of partBatchesInOrder) {\n      const desiredPartIDs = JSON.parse(key);\n      // headers are BIG (something like 30% of total storage for an average\n      // mailbox), so only download the ones we care about\n      const bodies = ['HEADER.FIELDS (FROM TO SUBJECT DATE CC BCC REPLY-TO IN-REPLY-TO REFERENCES MESSAGE-ID)'].concat(desiredPartIDs);\n\n      const messagesToProcess = []\n      yield this._box.fetchEach(\n        uidsByPart[key],\n        {bodies},\n        (imapMessage) => messagesToProcess.push(imapMessage)\n      );\n      // generally higher UIDs are newer, so process those first\n      messagesToProcess.sort((a, b) => b.attributes.uid - a.attributes.uid);\n\n      // Processing messages is not fire and forget.\n      // We need to wait for all of the messages in the range to be processed\n      // before actually updating the folder sync state. If we optimistically\n      // updated the fetched range, we would have to persist the processing\n      // queue to disk in case you quit the app and there are still messages\n      // left in the queue. Otherwise we would end up skipping messages.\n      for (const imapMessage of messagesToProcess) {\n        const uid = imapMessage.attributes.uid;\n        // This will resolve when the message is actually processed\n        await MessageProcessor.queueMessageForProcessing({\n          imapMessage,\n          struct: structsByUID[uid],\n          desiredParts: desiredPartsByUID[uid],\n          folderId: this._folder.id,\n          accountId: this._db.accountId,\n          throttle,\n        })\n        processedUids.add(uid);\n        this.emit('message-processed');\n\n        // If the user quits the app at this point, we will have to refetch\n        // these messages because the folder.syncState won't get updated, but\n        // that's ok.\n        yield // Yield to allow interruption\n      }\n      totalProcessedMessages += messagesToProcess.length;\n    }\n\n    // `uids` set is used for prioritizing specific uids. We can't update the\n    // range if this is passed because we still want to download the rest of\n    // the range later.\n    if (!uids) {\n      // Update our folder sync state to reflect the messages we've synced\n      const boxUidnext = this._box.uidnext;\n      const boxUidvalidity = this._box.uidvalidity;\n      const {fetchedmin, fetchedmax} = this._folder.syncState;\n      await this._folder.updateSyncState({\n        fetchedmin: fetchedmin ? Math.min(fetchedmin, min) : min,\n        fetchedmax: fetchedmax ? Math.max(fetchedmax, max) : max,\n        uidnext: boxUidnext,\n        uidvalidity: boxUidvalidity,\n      });\n      // to keep processedUids from growing without bound, expunge UIDs for\n      // ranges which have been recorded as fully downloaded\n      for (const uid of processedUids.values()) {\n        if (uid >= Math.min(min, max) && uid <= Math.max(min, max)) {\n          processedUids.delete(uid)\n        }\n      }\n    }\n\n    return totalProcessedMessages\n  }\n\n  _batchSizeForFolder() {\n    if (!this._syncWorker._latestOpenTimesByFolder.has(this._folder.name)) {\n      this._logger.log(`Unknown folder ${this._folder.name}, returning batch size of ${MIN_MESSAGE_BATCH_SIZE}`);\n      return MIN_MESSAGE_BATCH_SIZE;\n    }\n    const selectTimeSec = this._syncWorker._latestOpenTimesByFolder.get(this._folder.name) / 1000.0;\n    const batchSize = Math.floor(Math.min(Math.max(selectTimeSec * BATCH_SIZE_PER_SELECT_SEC, MIN_MESSAGE_BATCH_SIZE), MAX_MESSAGE_BATCH_SIZE));\n    this._logger.log(`Selecting folder ${this._folder.name} previously took ${selectTimeSec} seconds, returning batch size of ${batchSize}`);\n    return batchSize;\n  }\n\n  /**\n   * Note: This function is an ES6 generator so we can `yield` at points\n   * we want to interrupt sync. This is enabled by `SyncOperation` and\n   * `Interruptible`\n   */\n  async * _openMailboxAndEnsureValidity() {\n    const box = await this._imap.openBox(this._folder.name, {refetchBoxInfo: true});\n    this._syncWorker._latestOpenTimesByFolder.set(this._folder.name, this._imap.getLastOpenDuration());\n    yield\n\n    if (box.persistentUIDs === false) {\n      throw new Error(\"Mailbox does not support persistentUIDs.\");\n    }\n\n    const boxUidvalidity = box.uidvalidity;\n    const lastUIDValidity = this._folder.syncState.uidvalidity;\n\n    if (lastUIDValidity && (boxUidvalidity !== lastUIDValidity)) {\n      this._logger.log(`🔃  😵  📂 ${this._folder.name} - Recovering from UID invalidity`)\n      await this._recoverFromUIDInvalidity(boxUidvalidity)\n    }\n\n    return box;\n  }\n\n  async * _fetchFirstUnsyncedMessages(batchSize) {\n    const {provider} = this._account;\n    const folderRole = this._folder.role;\n    // TODO: In a few releases, simplify this code to remove the\n    // gmailInboxUIDsRemaining after most people have been migrated.\n    const gmailInitialUIDsRemaining = this._folder.syncState.gmailInitialUIDsRemaining || this._folder.syncState.gmailInboxUIDsRemaining;\n    const gmailInitialUIDsUnset = !gmailInitialUIDsRemaining;\n    const hasGmailInboxUIDsRemaining = gmailInitialUIDsRemaining && gmailInitialUIDsRemaining.length\n    let totalProcessedMessages = 0;\n    if (provider === \"gmail\" && folderRole === \"all\" && (gmailInitialUIDsUnset || hasGmailInboxUIDsRemaining)) {\n      // Track the first few UIDs in the inbox label & download these first.\n      // If the user restarts the app before all these UIDs are downloaded & we\n      // also pass the UID in the All Mail folder range downloads we will\n      // redownload them, but that's OK.\n      let initialUids;\n      if (!gmailInitialUIDsRemaining) {\n        // this._logger.log(`FetchMessagesInFolderIMAP: Fetching Gmail Inbox UIDs for prioritization`);\n        initialUids = await this._box.search([['X-GM-RAW', 'in:inbox OR in:sent']]);\n        // Gmail always returns UIDs in order from smallest to largest, so this\n        // gets us the most recent messages first.\n        initialUids = initialUids.slice(Math.max(initialUids.length - GMAIL_INITIAL_PRIORITIZE_COUNT, 0));\n        // Immediately persist to avoid issuing search again in case of interrupt\n        await this._folder.updateSyncState({\n          gmailInitialUIDsRemaining: initialUids,\n          fetchedmax: this._box.uidnext,\n        });\n      } else {\n        initialUids = gmailInitialUIDsRemaining\n      }\n      // continue fetching new mail first in the case that inbox uid download\n      // takes multiple batches\n      const fetchedmax = this._folder.syncState.fetchedmax || this._box.uidnext;\n      if (this._box.uidnext > fetchedmax) {\n        this._logger.log(`🔃 📂 ${this._folder.name} new messages present; fetching ${fetchedmax}:${this._box.uidnext}`);\n        totalProcessedMessages += yield this._fetchAndProcessMessages({min: fetchedmax, max: this._box.uidnext, throttle: false});\n      }\n      const batchSplitIndex = Math.max(initialUids.length - batchSize, 0);\n      const uidsFetchNow = initialUids.slice(batchSplitIndex);\n      const uidsFetchLater = initialUids.slice(0, batchSplitIndex);\n      // this._logger.log(`FetchMessagesInFolderIMAP: Remaining Gmail Inbox UIDs to download: ${uidsFetchLater.length}`);\n      totalProcessedMessages += yield this._fetchAndProcessMessages({uids: uidsFetchNow, throttle: false});\n      await this._folder.updateSyncState({ gmailInitialUIDsRemaining: uidsFetchLater });\n    } else {\n      const lowerbound = Math.max(1, this._box.uidnext - batchSize);\n      totalProcessedMessages += yield this._fetchAndProcessMessages({min: lowerbound, max: this._box.uidnext, throttle: false});\n      // We issue a UID FETCH ALL and record the correct minimum UID for the\n      // mailbox, which could be something much larger than 1 (especially for\n      // inbox because of archiving, which \"loses\" smaller UIDs over time). If\n      // we do not do this, and, say, the minimum UID in a mailbox is 100k\n      // (we've seen this!), the mailbox will not register as finished initial\n      // syncing for many many sync loop iterations beyond when it is actually\n      // complete, and we will issue many unnecessary FETCH commands.\n      //\n      // We do this _after_ fetching the first few messages in the mailbox in\n      // order to prioritize the time to first thread displayed on initial\n      // account connection.\n      const uids = await this._box.search([['UID', `1:${lowerbound}`]]);\n      let boxMinUid = uids[0] || 1;\n      // Using old-school min because uids may be an array of a million\n      // items. Math.min can't take that many arguments\n      for (const uid of uids) {\n        if (uid < boxMinUid) {\n          boxMinUid = uid;\n        }\n      }\n      await this._folder.updateSyncState({ minUID: boxMinUid });\n    }\n\n    return totalProcessedMessages\n  }\n\n  /**\n   * Note: This function is an ES6 generator so we can `yield` at points\n   * we want to interrupt sync. This is enabled by `SyncOperation` and\n   * `Interruptible`\n   */\n  async * _fetchUnsyncedMessages(batchSize) {\n    const savedSyncState = this._folder.syncState;\n    const boxUidnext = this._box.uidnext;\n\n    if (!savedSyncState.minUID) {\n      throw new Error(\"minUID is not set. You must restart the sync loop or check boxMinUid\")\n    }\n\n    let totalProcessedMessages = 0\n    if (savedSyncState.fetchedmax < boxUidnext) {\n      // this._logger.log(`FetchMessagesInFolderIMAP: fetching ${savedSyncState.fetchedmax}:${boxUidnext}`);\n      totalProcessedMessages += yield this._fetchAndProcessMessages({min: savedSyncState.fetchedmax, max: boxUidnext, throttle: false});\n    } else {\n      // this._logger.log('FetchMessagesInFolderIMAP: fetchedmax == uidnext, nothing more recent to fetch.')\n    }\n\n    if (savedSyncState.fetchedmin > savedSyncState.minUID) {\n      const lowerbound = Math.max(savedSyncState.minUID, savedSyncState.fetchedmin - batchSize);\n      // this._logger.log(`FetchMessagesInFolderIMAP: fetching ${lowerbound}:${savedSyncState.fetchedmin}`);\n      totalProcessedMessages += yield this._fetchAndProcessMessages({\n        min: lowerbound,\n        max: savedSyncState.fetchedmin,\n        throttle: this._syncWorker.throttlingEnabled(),\n      });\n    } else {\n      // this._logger.log(\"FetchMessagesInFolderIMAP: fetchedmin == minUID, nothing older to fetch.\")\n    }\n    return totalProcessedMessages\n  }\n\n  async * _fetchNextMessageBatch() {\n    // Since we expand the UID FETCH range without comparing to the UID list\n    // because UID SEARCH ALL can be slow (and big!), we may download fewer\n    // messages than the batch size (up to zero) --- keep going until full\n    // batch synced\n    let totalProcessedMessages = 0;\n    const moreToFetchAvailable = () => !this._folder.isSyncComplete() || this._box.uidnext > this._folder.syncState.fetchedmax\n    const batchSize = this._batchSizeForFolder(this._folder);\n    while (totalProcessedMessages < batchSize && moreToFetchAvailable()) {\n      if (this._isFirstSync()) {\n        const numProcessed = yield this._fetchFirstUnsyncedMessages(batchSize);\n        totalProcessedMessages += numProcessed;\n        continue;\n      }\n\n      const numProcessed = yield this._fetchUnsyncedMessages(batchSize);\n      totalProcessedMessages += numProcessed\n      if (numProcessed > 0) {\n        continue;\n      }\n\n      // Find where the gap in the UID space ends --- SEARCH can be slow on\n      // large mailboxes, but otherwise we could spin here arbitrarily long\n      // FETCHing empty space\n      let nextUid = 1;\n      // IMAP range searches include both ends of the range\n      const minSearchUid = this._folder.syncState.fetchedmin - 1;\n      if (minSearchUid) {\n        const uids = await this._box.search([['UID', `${this._folder.syncState.minUID}:${minSearchUid}`]]);\n        // Using old-school max because uids may be an array of a million\n        // items. Math.max can't take that many arguments\n        nextUid = uids[0] || 1;\n        for (const uid of uids) {\n          if (uid > nextUid) {\n            nextUid = uid;\n          }\n        }\n      }\n      this._logger.log(`🔃📂 ${this._folder.name} Found gap in UIDs; next fetchedmin is ${nextUid}`);\n      await this._folder.updateSyncState({ fetchedmin: nextUid });\n    }\n  }\n\n  /**\n   * We need to periodically check if any attributes have changed on\n   * messages. These are things like \"starred\" or \"unread\", etc. There are\n   * two types of IMAP boxes: one that supports \"highestmodseq\" via the\n   * \"CONDSTORE\" flag, and ones that do not. In the former case we can\n   * basically ask for the latest messages that have changes. In the\n   * latter case we have to slowly traverse through all messages in order\n   * to find updates.\n   *\n   * Note: This function is an ES6 generator so we can `yield` at points\n   * we want to interrupt sync. This is enabled by `SyncOperation` and\n   * `Interruptible`\n   */\n  * _fetchMessageAttributeChanges() {\n    const {fetchedmin, fetchedmax} = this._folder.syncState;\n    if ((fetchedmin === undefined) || (fetchedmax === undefined)) {\n      throw new Error(\"Unseen messages must be fetched at least once before the first update/delete scan.\")\n    }\n\n    if (this._supportsChangesSince()) {\n      yield this._fetchLatestAttributeChanges()\n    } else {\n      yield this._scanForAttributeChanges();\n    }\n  }\n\n  /**\n   * Some IMAP providers have \"CONDSTORE\" as a capibility. This allows us\n   * to ask for any messages that have had their attributes changed since\n   * a certain timestamp. This is a much nicer feature than slowly looking\n   * back through all messages for ones that have updated attributes.\n   */\n  _supportsChangesSince() {\n    return this._imap.serverSupports(Capabilities.Condstore)\n  }\n\n  /**\n   * For providers that have CONDSTORE enabled, we can use the\n   * `highestmodseq` and `changedsince` parameters to ask for messages\n   * that have had their attributes updated since a recent timestamp. We\n   * used to refer to this as a \"shallowScan\".\n   *\n   * Note: This function is an ES6 generator so we can `yield` at points\n   * we want to interrupt sync. This is enabled by `SyncOperation` and\n   * `Interruptible`\n   */\n  async * _fetchLatestAttributeChanges() {\n    const {highestmodseq} = this._folder.syncState;\n    const nextHighestmodseq = this._box.highestmodseq;\n    if (!highestmodseq || nextHighestmodseq === highestmodseq) {\n      await this._folder.updateSyncState({\n        highestmodseq: nextHighestmodseq,\n      });\n      return;\n    }\n\n    const start = Date.now()\n    this._logger.log(`🔃 🚩 ${this._folder.name} via highestmodseq of ${highestmodseq}`)\n\n    const remoteUIDAttributes = yield this._box.fetchUIDAttributes(`1:*`,\n      {modifiers: {changedsince: highestmodseq}});\n    const localMessageAttributes = yield this._db.Message.findAll({\n      where: {\n        folderId: this._folder.id,\n        folderImapUID: _.compact(Object.keys(remoteUIDAttributes)),\n      },\n      attributes: MessageFlagAttributes,\n    })\n\n    const {numChangedLabels, numChangedFlags} = await this._updateMessageAttributes(remoteUIDAttributes, localMessageAttributes)\n    await this._removeDeletedMessages(remoteUIDAttributes, localMessageAttributes)\n    await this._folder.updateSyncState({\n      highestmodseq: nextHighestmodseq,\n    });\n    this._logger.log(`🔃 🚩 ${this._folder.name} via highestmodseq of ${highestmodseq} - took ${Date.now() - start}ms to update ${numChangedLabels + numChangedFlags} messages & threads`)\n  }\n\n  /**\n   * For providers that do NOT have CONDSTORE enabled, we have to slowly\n   * go back through all messages to find if any attributes have changed.\n   * Since there may be millions of messages, we need to break this up\n   * into reasonable chunks to do it efficiently.\n   *\n   * We always scan the most recent FETCH_ATTRIBUTE_BATCH_SIZE messages\n   * since we assume those are the most likely to have their attributes\n   * updated.\n   *\n   * After we slowly scan backwards by the batch size over the rest of the\n   * mailbox over the next several syncs.\n   *\n   * Note: This function is an ES6 generator so we can `yield` at points\n   * we want to interrupt sync. This is enabled by `SyncOperation` and\n   * `Interruptible`\n   */\n  async * _scanForAttributeChanges() {\n    const {Message} = this._db;\n    const {fetchedmax, attributeFetchedMax} = this._folder.syncState;\n\n    const recentStart = Math.max(fetchedmax - FETCH_ATTRIBUTES_BATCH_SIZE, 1)\n    const recentRange = `${recentStart}:${fetchedmax}`;\n\n    const to = Math.min(attributeFetchedMax || recentStart, recentStart);\n    const from = Math.max(to - FETCH_ATTRIBUTES_BATCH_SIZE, 1)\n    const backScanRange = `${from}:${to}`;\n\n    const start = Date.now()\n    this._logger.log(`🔃 🚩 ${this._folder.name} via scan through ${recentRange} and ${backScanRange}`)\n\n    const recentAttrs = yield this._box.fetchUIDAttributes(recentRange)\n    const backScanAttrs = yield this._box.fetchUIDAttributes(backScanRange)\n    const remoteUIDAttributes = Object.assign({}, backScanAttrs, recentAttrs)\n    const localMessageAttributes = yield Message.findAll({\n      where: {\n        folderId: this._folder.id,\n        folderImapUID: _.compact(Object.keys(remoteUIDAttributes)),\n      },\n      attributes: MessageFlagAttributes,\n    })\n\n    const {numChangedLabels, numChangedFlags} = await this._updateMessageAttributes(remoteUIDAttributes, localMessageAttributes)\n    await this._removeDeletedMessages(remoteUIDAttributes, localMessageAttributes)\n\n    // this._logger.info(`FetchMessagesInFolder: Deep scan finished.`);\n    await this._folder.updateSyncState({\n      attributeFetchedMax: (from <= 1 ? recentStart : from),\n      lastAttributeScanTime: Date.now(),\n    });\n    this._logger.log(`🔃 🚩 ${this._folder.name} via scan through ${recentRange} and ${backScanRange} - took ${Date.now() - start}ms to update ${numChangedLabels + numChangedFlags} messages & threads`)\n  }\n\n  _shouldFetchMessages(boxStatus) {\n    if (boxStatus.name !== this._folder.name) {\n      throw new Error(`FetchMessagesInFolder::_shouldFetchMessages - boxStatus doesn't correspond to folder`)\n    }\n    if (!this._folder.isSyncComplete()) {\n      return true\n    }\n    const {syncState: {fetchedmax, uidvalidity}} = this._folder\n    return boxStatus.uidvalidity !== uidvalidity || boxStatus.uidnext > fetchedmax\n  }\n\n  _shouldFetchAttributes(boxStatus) {\n    if (boxStatus.name !== this._folder.name) {\n      throw new Error(`FetchMessagesInFolder::_shouldFetchAttributes - boxStatus doesn't correspond to folder`)\n    }\n    if (!this._folder.isSyncComplete()) {\n      return true\n    }\n    const {syncState} = this._folder\n    if (this._supportsChangesSince()) {\n      return syncState.highestmodseq !== boxStatus.highestmodseq\n    }\n    return true\n  }\n\n  _shouldSyncFolder(boxStatus) {\n    return this._shouldFetchMessages(boxStatus) || this._shouldFetchAttributes(boxStatus)\n  }\n\n  /**\n   * Note: This function is an ES6 generator so we can `yield` at points\n   * we want to interrupt sync. This is enabled by `SyncOperation` and\n   * `Interruptible`\n   */\n  async * runTask(db, imap, {syncWorker} = {}) {\n    this._logger.log(`🔜 📂 ${this._folder.name}`)\n    this._db = db;\n    this._imap = imap;\n    if (!syncWorker) {\n      throw new Error(`SyncWorker not passed to runTask`);\n    }\n    this._syncWorker = syncWorker;\n\n    const accountId = this._db.accountId\n    const folderName = this._folder.name\n    SyncActivity.reportSyncActivity(accountId, `Starting folder: ${folderName}`)\n\n    const latestBoxStatus = yield this._imap.getLatestBoxStatus(this._folder.name)\n\n    // If we haven't set any syncState at all, let's set it for the first time\n    // to generate a delta for N1\n    if (_.isEmpty(this._folder.syncState)) {\n      yield this._folder.updateSyncState({\n        uidnext: latestBoxStatus.uidnext,\n        uidvalidity: latestBoxStatus.uidvalidity,\n        fetchedmin: null,\n        fetchedmax: null,\n        minUID: null,\n        failedUIDs: [],\n      })\n    }\n\n    SyncActivity.reportSyncActivity(accountId, `Checking if folder needs sync: ${folderName}`)\n\n    if (!this._shouldSyncFolder(latestBoxStatus)) {\n      // Don't even attempt to issue an IMAP SELECT if there are absolutely no\n      // updates\n      this._logger.log(`🔚 📂 ${this._folder.name} has no updates at all - skipping sync`)\n      SyncActivity.reportSyncActivity(accountId, `Done with folder: ${folderName}`)\n      return;\n    }\n\n    SyncActivity.reportSyncActivity(accountId, `Checking what to fetch for folder: ${folderName}`)\n\n    this._box = yield this._openMailboxAndEnsureValidity();\n    const shouldFetchMessages = this._shouldFetchMessages(this._box)\n    const shouldFetchAttributes = this._shouldFetchAttributes(this._box)\n\n    // Do as little work as possible\n    if (shouldFetchMessages) {\n      SyncActivity.reportSyncActivity(accountId, `Fetching messages: ${folderName}`)\n      yield this._fetchNextMessageBatch()\n    } else {\n      this._logger.log(`🔚 📂 ${this._folder.name} has no new messages - skipping fetch messages`)\n    }\n    if (shouldFetchAttributes) {\n      SyncActivity.reportSyncActivity(accountId, `Fetching attributes: ${folderName}`)\n      yield this._fetchMessageAttributeChanges();\n    } else {\n      this._logger.log(`🔚 📂 ${this._folder.name} has no attribute changes - skipping fetch attributes`)\n    }\n    this._logger.log(`🔚 📂 ${this._folder.name} done`)\n    SyncActivity.reportSyncActivity(accountId, `Done with folder: ${folderName}`)\n  }\n}\n\nmodule.exports = FetchMessagesInFolderIMAP;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-tasks/fetch-new-messages-in-folder.imap.es6",
    "content": "const FetchMessagesInFolderIMAP = require('./fetch-messages-in-folder.imap')\n\n\n// TODO Eventually make FetchMessagesInFolderIMAP use this class and split it up\n// into smaller parts (not a fan of the multi level inheritance tree)\n\n/**\n * This sync task will only fetch /new/ messages in a folder\n */\nclass FetchNewMessagesInFolderIMAP extends FetchMessagesInFolderIMAP {\n\n  description() {\n    return `FetchNewMessagesInFolderIMAP (${this._folder.name} - ${this._folder.id})`;\n  }\n\n  async * runTask(db, imap, {syncWorker} = {}) {\n    this._logger.log(`🔜 📂 🆕  ${this._folder.name} - Looking for new messages`)\n    this._db = db;\n    this._imap = imap;\n    if (!syncWorker) {\n      throw new Error(`SyncWorker not passed to runTask`);\n    }\n    this._syncWorker = syncWorker;\n    const {syncState: {fetchedmax}} = this._folder\n\n    if (!fetchedmax) {\n      // Without a fetchedmax, can't tell what's new!\n      // If we haven't fetched anything on this folder, let's run a normal fetch\n      // operation\n      // Can't use `super` in this scenario because babel can't compile it under\n      // these conditions. User regular prototype instead\n      this._logger.log(`🔚 📂 🆕  ${this._folder.name} has no fetchedmax - running regular fetch operation`)\n      yield FetchMessagesInFolderIMAP.prototype.runTask.call(this, db, imap, {syncWorker})\n      return\n    }\n\n    const latestBoxStatus = yield this._imap.getLatestBoxStatus(this._folder.name)\n    if (latestBoxStatus.uidnext > fetchedmax) {\n      this._box = await this._imap.openBox(this._folder.name)\n      const boxUidnext = this._box.uidnext\n      yield this._fetchAndProcessMessages({min: fetchedmax, max: boxUidnext, throttle: false});\n    } else {\n      this._logger.log(`🔚 📂 🆕  ${this._folder.name} has no new messages - skipping fetch messages`)\n    }\n    this._logger.log(`🔚 📂 🆕  ${this._folder.name} done`)\n  }\n}\n\nmodule.exports = FetchNewMessagesInFolderIMAP;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-tasks/fetch-specific-messages-in-folder.imap.es6",
    "content": "const FetchMessagesInFolderIMAP = require('./fetch-messages-in-folder.imap')\n\n/*\n * This sync task will only fetch the specified set of UIDs\n */\nclass FetchSpecificMessagesInFolderIMAP extends FetchMessagesInFolderIMAP {\n  description() {\n    return `FetchSpecificMessagesInFolderIMAP (${this._folder.name} - ${this._folder.id})`;\n  }\n\n  async * runTask(db, imap, {syncWorker} = {}) {\n    this._logger.log(`🔜 📂 🆕  ${this._folder.name} - Looking for ${this._uids.length} specific UIDs`);\n    this._db = db;\n    this._imap = imap;\n    if (!syncWorker) {\n      throw new Error(`SyncWorker not passed to runTask`);\n    }\n    this._syncWorker = syncWorker;\n    const {syncState: {fetchedmin, fetchedmax}} = this._folder;\n\n    let uids = this._uids;\n    if (fetchedmin && fetchedmax) {\n      uids = uids.filter(uid => uid < fetchedmin || uid > fetchedmax);\n    }\n\n    if (uids.length === 0) {\n      this._logger.log(`🔜 📂 🆕  ${this._folder.name} - Already fetched all UIDs`);\n      return;\n    }\n\n    this._logger.log(`🔜 📂 🆕  ${this._folder.name} - Fetching ${uids.length} UIDs`);\n    this._box = await this._imap.openBox(this._folder.name)\n    yield this._fetchAndProcessMessages({uids, throttle: false});\n    this._logger.log(`🔚 📂 🆕  ${this._folder.name} done`);\n  }\n}\n\nmodule.exports = FetchSpecificMessagesInFolderIMAP;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-tasks/sync-task.js",
    "content": "const MessageProcessor = require('../../message-processor')\nconst Interruptible = require('../../shared/interruptible')\n\n/**\n * SyncTask represents an operation run by the SyncWorker.\n * Any IMAP sync operation that runs during sync should extend from this class\n * and implement `runTask`\n *\n * By default, this class ensures that we skip the operation if the message\n * processing queue is full, and ensures that the operation is interruptible by\n * extending from Interruptible.\n */\nclass SyncTask extends Interruptible {\n\n  constructor({account} = {}) {\n    super()\n    this._account = account\n    if (!this._account) {\n      throw new Error(\"SyncTask requires an account\")\n    }\n    this._logger = global.Logger.forAccount(this._account)\n  }\n\n  description() {\n    throw new Error(\"Must return a description\")\n  }\n\n  /**\n   * @returns a Promise that resolves when the operation has been executed to\n   * completion or interrupted. Rejects if an error is thrown.\n   */\n  async run(...args) {\n    if (MessageProcessor.queueIsFull()) {\n      this._logger.log(`🔃  Skipping sync operation - Message processing queue is full`)\n      return Promise.resolve()\n    }\n    return super.run(this.runTask, this, ...args)\n  }\n\n  /**\n   * Any class that extends from `SyncTask` must implement `runTask`\n   * as a generator function, meaning that it returns a\n   * generator object.\n   * (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*)\n   *\n   * This will allow it to be interrupted in between async operations. In order\n   * to indicate where the function can be interrupted, you must use the\n   * keyword `yield` instead of `await` to wait for the resolution of Promises.\n   *\n   * e.g.\n   * ```\n   * class MyTask extends SyncTask {\n   *\n   *   async * runTask(db, imap, syncWorker) {\n   *     // Use `yield` to indicate that we can interrupt the function after\n   *     // this async operation has resolved\n   *     const models = yield db.Messages.findAll()\n   *\n   *     // If the operation is interrupted, code execution will stop here!\n   *\n   *     // ...\n   *\n   *     await saveModels(models)\n   *     // `await` wont stop code execution even if operation is interrupted\n   *\n   *     // ...\n   *   }\n   * }\n   * ```\n   */\n  * runTask(db, imap, syncWorker) { // eslint-disable-line\n    throw new Error('Must implement `SyncTask::runTask`')\n  }\n}\n\nmodule.exports = SyncTask\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-utils.es6",
    "content": "import {Errors, IMAPErrors} from 'isomorphic-core'\nimport {APIError, NylasAPI, NylasAPIRequest, N1CloudAPI} from 'nylas-exports'\n\nasync function ensureGmailAccessToken({account, logger, forceRefresh = false, expiryBufferInSecs = 0} = {}) {\n  try {\n    const credentials = account.decryptedCredentials()\n    if (!credentials) {\n      throw new Error(\"ensureGmailAccessToken: There are no IMAP connection credentials for this account.\");\n    }\n\n    const currentUnixDate = Math.floor(Date.now() / 1000);\n    if (forceRefresh && (credentials.expiry_date > currentUnixDate)) {\n      console.warn(\"ensureGmailAccessToken: refreshing token, but token is not expired\");\n    }\n    const bufferedExpiryDate = credentials.expiry_date - expiryBufferInSecs;\n    if (forceRefresh || (currentUnixDate > bufferedExpiryDate)) {\n      const req = new NylasAPIRequest({\n        api: N1CloudAPI,\n        options: {\n          path: `/auth/gmail/refresh`,\n          method: 'POST',\n          accountId: account.emailAddress,\n        },\n      });\n\n      const newCredentials = await req.run()\n      account.setCredentials(newCredentials);\n      await account.save();\n      return newCredentials;\n    }\n    return null\n  } catch (err) {\n    logger.warn(`🔃  Unable to refresh access token.`, err);\n    if (err instanceof APIError) {\n      const {statusCode} = err\n      logger.error(`🔃  Unable to refresh access token. Got status code: ${statusCode}`, err);\n\n      const isNonPermanentError = (\n        // If we got a 5xx error from the server, that means that something is wrong\n        // on the Nylas API side. It could be a bad deploy, or a bug on Google's side.\n        // In both cases, we've probably been alerted and are working on the issue,\n        // so it makes sense to have the client retry.\n        statusCode >= 500 ||\n        !NylasAPI.PermanentErrorCodes.includes(statusCode)\n      )\n      if (isNonPermanentError) {\n        throw new IMAPErrors.IMAPTransientAuthenticationError(`Server error when trying to refresh token.`);\n      } else {\n        throw new IMAPErrors.IMAPAuthenticationError(`Unable to refresh access token`);\n      }\n    }\n    err.message = `Unknown error when refreshing access token: ${err.message}`\n    const fingerprint = [\"{{ default }}\", \"access token refresh\", err.message];\n    NylasEnv.reportError(err, {fingerprint,\n      rateLimit: {\n        ratePerHour: 30,\n        key: `SyncError:RefreshToken:${err.message}`,\n      },\n    })\n    throw err\n  }\n}\n\n\n// Awaiting this function will only wait for the first run. It returns after\n// retryable errors, but sets a timeout to run the task again later.\n// To await until the end result, use the public runWithRetryLogic() instead.\nasync function _runWithRetryLogic({run, retryScheduler, onSuccess, onError, onRetryableError}) {\n  try {\n    const result = await run()\n    onSuccess(result)\n  } catch (error) {\n    if (error instanceof Errors.RetryableError) {\n      let delay = 0\n      if (retryScheduler) {\n        retryScheduler.nextDelay()\n        delay = retryScheduler.currentDelay()\n      }\n      try {\n        await onRetryableError(error, delay) // this may re-throw the error to halt the retry process\n      } catch (err) {\n        onError(err)\n        return\n      }\n      setTimeout(_runWithRetryLogic, delay, {run, retryScheduler, onSuccess, onError, onRetryableError})\n    } else {\n      onError(error)\n    }\n  }\n}\n\nasync function runWithRetryLogic({run, retryScheduler, onRetryableError}) {\n  return new Promise((resolve, reject) => _runWithRetryLogic({\n    run,\n    retryScheduler,\n    onRetryableError,\n    onSuccess: resolve,\n    onError: reject,\n  }))\n}\n\n\nmodule.exports = {\n  ensureGmailAccessToken,\n  runWithRetryLogic,\n  // This folder list was generated by aggregating examples of user folders\n  // that were not properly labeled as trash, sent, or spam.\n  // This list was constructed semi automatically, and manuallly verified.\n  // Should we encounter problems with account folders in the future, add them\n  // below to test for them.\n  // Make sure these are lower case! (for comparison purposes)\n  localizedCategoryNames: {\n    trash: new Set([\n      'gel\\xc3\\xb6scht', 'papierkorb',\n      '\\xd0\\x9a\\xd0\\xbe\\xd1\\x80\\xd0\\xb7\\xd0\\xb8\\xd0\\xbd\\xd0\\xb0',\n      '[imap]/trash', 'papelera', 'borradores',\n      '[imap]/\\xd0\\x9a\\xd0\\xbe\\xd1\\x80',\n      '\\xd0\\xb7\\xd0\\xb8\\xd0\\xbd\\xd0\\xb0', 'deleted items',\n      '\\xd0\\xa1\\xd0\\xbc\\xd1\\x96\\xd1\\x82\\xd1\\x82\\xd1\\x8f',\n      'papierkorb/trash', 'gel\\xc3\\xb6schte elemente',\n      'deleted messages', '[gmail]/trash', 'inbox/trash', 'trash',\n      'mail/trash', 'inbox.trash']),\n    spam: new Set([\n      'roskaposti', 'inbox.spam', 'inbox.spam', 'skr\\xc3\\xa4ppost',\n      'spamverdacht', 'spam', 'spam', '[gmail]/spam', '[imap]/spam',\n      '\\xe5\\x9e\\x83\\xe5\\x9c\\xbe\\xe9\\x82\\xae\\xe4\\xbb\\xb6', 'junk',\n      'junk mail', 'junk e-mail']),\n    inbox: new Set([\n      'inbox',\n    ]),\n    sent: new Set([\n      'postausgang', 'inbox.gesendet', '[gmail]/sent mail',\n      '\\xeb\\xb3\\xb4\\xeb\\x82\\xbc\\xed\\x8e\\xb8\\xec\\xa7\\x80\\xed\\x95\\xa8',\n      'elementos enviados', 'sent', 'sent items', 'sent messages',\n      'inbox.papierkorb', 'odeslan\\xc3\\xa9', 'mail/sent-mail',\n      'ko\\xc5\\xa1', 'inbox.sentmail', 'gesendet',\n      'ko\\xc5\\xa1/sent items', 'gesendete elemente']),\n    archive: new Set([\n      'archive',\n    ]),\n    drafts: new Set([\n      'drafts', 'draft', 'brouillons',\n    ]),\n  },\n}\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/sync-worker.es6",
    "content": "import _ from 'underscore'\nimport {\n  Errors,\n  IMAPErrors,\n  SendmailClient,\n  MetricsReporter,\n  IMAPConnectionPool,\n  ExponentialBackoffScheduler,\n} from 'isomorphic-core';\nimport {\n  Actions,\n  Account,\n  IdentityStore,\n  BatteryStatusManager,\n} from 'nylas-exports'\nimport Interruptible from '../shared/interruptible'\nimport SyncTaskFactory from './sync-task-factory';\nimport SyncbackTaskRunner from './syncback-task-runner'\nimport SyncActivity from '../shared/sync-activity'\nimport {ensureGmailAccessToken} from './sync-utils'\n\n\nconst {SYNC_STATE_RUNNING, SYNC_STATE_AUTH_FAILED, SYNC_STATE_ERROR} = Account\nconst AC_SYNC_LOOP_INTERVAL_MS = 10 * 1000            // 10 sec\nconst BATTERY_SYNC_LOOP_INTERVAL_MS = 5 * 60 * 1000   //  5 min\nconst MAX_SYNC_BACKOFF_MS = 5 * 60 * 1000 // 5 min\n\nclass SyncWorker {\n  constructor(account, db, syncProcessManager) {\n    this._db = db;\n    this._manager = syncProcessManager;\n    this._smtp = null;\n    this._account = account;\n    this._currentTask = null\n    this._mainIMAPConn = null;\n    this._mailListenerIMAPConn = null\n    this._interruptible = new Interruptible()\n    this._logger = global.Logger.forAccount(account)\n\n    this._startTime = Date.now()\n    this._lastSyncTime = null\n    this._interrupted = false\n    this._syncInProgress = false\n    this._throttlingEnabled = false\n    this._destroyed = false\n    this._shouldIgnoreInboxFlagUpdates = false\n    this._numTimeoutErrors = 0;\n    this._requireTokenRefresh = false\n    this._batchProcessedUids = new Map();\n    this._latestOpenTimesByFolder = new Map();\n    // We use lists for the disposers as a failsafe in case there's some code\n    // path that could possibly end up with two or more simultaneous disposers.\n    // We used to have just a nullable field, but this led to leaking connections\n    // from the IMAP connection pool because we would overwrite the old disposer\n    // without calling it first.\n    this._mainIMAPConnDisposers = [];\n    this._mailListenerIMAPConnDisposers = [];\n\n    this._retryScheduler = new ExponentialBackoffScheduler({\n      baseDelay: 15 * 1000,\n      maxDelay: MAX_SYNC_BACKOFF_MS,\n    })\n\n    this._syncTimer = setTimeout(() => {\n      // TODO this is currently a hack to keep N1's account in sync and notify of\n      // sync errors. This should go away when we merge the databases\n      Actions.updateAccount(this._account.id, {syncState: SYNC_STATE_RUNNING})\n      this.syncNow({reason: 'Initial'});\n    }, 0);\n\n    // setup metrics collection. We do this in an isolated way by hooking onto\n    // the database, because otherwise things get /crazy/ messy and I don't like\n    // having counters and garbage everywhere.\n    if (!account.firstSyncCompletion) {\n      // TODO extract this into its own module, can use later on for exchange\n      let seen = 0;\n      db.Thread.addHook('afterCreate', 'metricsCollection', () => {\n        const identity = IdentityStore.identity()\n        const nylasId = identity ? identity.id : null;\n        if (seen === 0) {\n          MetricsReporter.reportEvent({\n            nylasId,\n            type: 'imap',\n            provider: account.provider,\n            accountId: account.id,\n            msecToFirstThread: (Date.now() - new Date(account.createdAt).getTime()),\n          })\n        }\n        if (seen === 500) {\n          this._throttlingEnabled = true\n          MetricsReporter.reportEvent({\n            nylasId,\n            type: 'imap',\n            provider: account.provider,\n            accountId: account.id,\n            msecToFirst500Threads: (Date.now() - new Date(account.createdAt).getTime()),\n          })\n        }\n\n        if (seen > 500) {\n          db.Thread.removeHook('afterCreate', 'metricsCollection')\n        }\n        seen += 1;\n      });\n    }\n  }\n\n  throttlingEnabled() {\n    return this._throttlingEnabled;\n  }\n\n  _getInboxFolder() {\n    return this._db.Folder.find({where: {role: ['all', 'inbox']}})\n  }\n\n  async _cleanupOrphanMessages() {\n    if (this._destroyed) { return null }\n    const {Message, Thread, Folder, Label} = this._db;\n\n    const messagesWithoutFolder = await Message.findAll({\n      attributes: ['id', 'threadId'],\n      limit: 1000,\n      where: {\n        folderId: null,\n        isSent: {$not: true},\n      },\n    })\n\n    const messageIdsWithSendInProgress = await this._db.SyncbackRequest.findAll({\n      limit: 100,\n      where: {\n        type: 'EnsureMessageInSentFolder',\n        status: {$notIn: ['SUCCEEDED', 'FAILED']},\n      },\n    })\n    .map(syncbackRequest => syncbackRequest.props.messageId)\n    const messagesWithoutImapUID = await Message.findAll({\n      attributes: ['id', 'threadId'],\n      limit: 1000,\n      where: {\n        folderImapUID: null,\n      },\n    })\n    // We don't want to remove messages that are currently being added to the\n    // sent folder, which we know wont have a folderImapUID while that is\n    // happening.\n    .filter(m => !messageIdsWithSendInProgress.includes(m.id))\n    .filter(m => Date.now() - m.date > 10 * 60 * 1000) // 10 min\n\n    const messagesToRemove = [...messagesWithoutFolder, ...messagesWithoutImapUID]\n    const affectedThreadIds = new Set();\n    await Promise.map(messagesToRemove, (msg) => {\n      affectedThreadIds.add(msg.threadId);\n      return msg.destroy();\n    });\n\n    const affectedThreads = await Thread.findAll({\n      where: {id: Array.from(affectedThreadIds)},\n      include: [{model: Folder}, {model: Label}],\n    });\n    return Promise.map(affectedThreads, (thread) => {\n      return thread.updateFromMessages({recompute: true, db: this._db})\n    })\n  }\n\n  async _ensureAccessToken() {\n    if (this._destroyed || this._account.provider !== 'gmail') { return null }\n\n    try {\n      const newCredentials = await ensureGmailAccessToken({\n        logger: this._logger,\n        account: this._account,\n        forceRefresh: this._requireTokenRefresh,\n        expiryBufferInSecs: 5 * 60, // try to avoid tokens expiring during the sync loop\n      })\n      this._requireTokenRefresh = false\n      return newCredentials\n    } catch (err) {\n      if (err instanceof IMAPErrors.IMAPAuthenticationError) {\n        // sync worker is persistent across reauths, so need to clear this flag\n        this._requireTokenRefresh = false\n      }\n      throw err;\n    }\n  }\n\n  async _ensureSMTPConnection(newCredentials) {\n    if (this._destroyed) { return }\n    if (!this._smtp || newCredentials) {\n      this._smtp = new SendmailClient(this._account, this._logger)\n    }\n  }\n\n  async _ensureMainIMAPConnection(conn) {\n    if (this._destroyed) { return }\n    if (this._mainIMAPConn === conn) {\n      return;\n    }\n\n    conn.on('queue-empty', () => {});\n\n    this._mainIMAPConn = conn;\n    this._mainIMAPConn._db = this._db;\n  }\n\n  async _ensureMailListenerIMAPConnection(newCredentials) {\n    if (this._destroyed) { return }\n    if (!newCredentials && this._mailListenerIMAPConn) {\n      await this._mailListenerIMAPConn.connect();\n      return\n    }\n\n    // We need to dispose of the old connection before trying to get a new one\n    // from the pool.\n    this._disposeMailListenerIMAPConnection();\n\n    await IMAPConnectionPool.withConnectionsForAccount(this._account, {\n      desiredCount: 1,\n      logger: this._logger,\n      socketTimeout: this._retryScheduler.currentDelay(),\n      onConnected: async ([listenerConn], onDone) => {\n        this._mailListenerIMAPConn = listenerConn;\n        this._mailListenerIMAPConn._db = this._db;\n        this._mailListenerIMAPConnDisposers.push(onDone);\n\n        // We don't want to listen for new mail while benchmarking initial\n        // sync b/c receiving new mail can significantly increase the variance\n        // in our measurements.\n        if (NylasEnv.inBenchmarkMode()) {\n          return true;\n        }\n\n        this._mailListenerIMAPConn.on('mail', () => {\n          this._onInboxUpdates(`You've got mail`);\n        })\n\n        // TODO Gmail does not emit `update` events\n        this._mailListenerIMAPConn.on('update', () => {\n          // `update` events happen when messages receive flag updates on the inbox\n          // (e.g. marking as unread or starred). We need to listen to that event for\n          // when those updates are performed from another mail client, but ignore\n          // them when they are caused from within N1.\n          if (this._shouldIgnoreInboxFlagUpdates) { return; }\n          this._onInboxUpdates(`There are flag updates on the inbox`);\n        })\n        return true\n      },\n    })\n  }\n\n  _onInboxUpdates = _.debounce((reason) => {\n    this.syncNow({reason, interrupt: true});\n  }, 100)\n\n  async _listenForNewMail() {\n    if (this._destroyed) { return }\n    this._logger.log('🔃  Listening for new mail...')\n    // Open the inbox folder on our dedicated mail listener connection to listen\n    // to new mail events\n    const inbox = await this._getInboxFolder();\n    if (inbox && this._mailListenerIMAPConn) {\n      await this._mailListenerIMAPConn.openBox(inbox.name)\n    }\n  }\n\n  _disposeConnections() {\n    this._disposeMainIMAPConnection()\n    this._disposeMailListenerIMAPConnection()\n  }\n\n  _disposeMainIMAPConnection() {\n    this._mainIMAPConn = null;\n    for (const disposer of this._mainIMAPConnDisposers) {\n      disposer();\n    }\n    this._mainIMAPConnDisposers = [];\n  }\n\n  _disposeMailListenerIMAPConnection() {\n    this._mailListenerIMAPConn = null;\n    for (const disposer of this._mailListenerIMAPConnDisposers) {\n      disposer();\n    }\n    this._mailListenerIMAPConnDisposers = [];\n  }\n\n  async _getFoldersToSync() {\n    const {Folder} = this._db;\n\n    // Don't sync spam until everything else has been synced\n    const allFolders = await Folder.findAll();\n    const foldersExceptSpam = allFolders.filter((f) => f.role !== 'spam')\n    const shouldIncludeSpam = foldersExceptSpam.every((f) => f.isSyncComplete())\n    const foldersToSync = shouldIncludeSpam ? allFolders : foldersExceptSpam;\n\n    // TODO make sure this order is correct/ unit tests!!\n    const priority = ['inbox', 'all', 'sent', 'archive', 'drafts', 'trash', 'spam'].reverse();\n    return foldersToSync.sort((a, b) =>\n      (priority.indexOf(a.role) - priority.indexOf(b.role)) * -1\n    )\n  }\n\n  async _onSyncError(error) {\n    if (this._destroyed) { return }\n    try {\n      this._disposeConnections();\n      this._logger.error(`🔃  SyncWorker: Errored while syncing account`, error)\n\n      // Check if we encountered an expired token error.\n      // We try to refresh Google OAuth2 access tokens in advance, but sometimes\n      // it doesn't work (e.g. the token expires during the sync loop). In this\n      // case, we need to immediately restart the sync loop & refresh the token.\n      // We don't want to save the error to the account in case refreshing the\n      // token fixes the issue.\n      //\n      // These error messages look like \"Error: Invalid credentials (Failure)\"\n      const isExpiredTokenError = (\n        this._account.provider === \"gmail\" &&\n        error instanceof IMAPErrors.IMAPAuthenticationError &&\n        /invalid credentials/i.test(error.message)\n      )\n      if (isExpiredTokenError) {\n        this._requireTokenRefresh = true\n        return\n      }\n\n      if (error instanceof IMAPErrors.IMAPConnectionTimeoutError) {\n        this._numTimeoutErrors += 1;\n        Actions.recordUserEvent('Timeout error in sync loop', {\n          accountId: this._account.id,\n          provider: this._account.provider,\n          socketTimeout: this._retryScheduler.currentDelay(),\n          numTimeoutErrors: this._numTimeoutErrors,\n        });\n      }\n\n      // Check if we've encountered a retryable/network error.\n      // If so, we don't want to save the error to the account, which will cause\n      // a red box to show up.\n      if (error instanceof Errors.RetryableError) {\n        this._retryScheduler.nextDelay()\n        return\n      }\n      // If we don't encounter consecutive RetryableErrors, reset the exponential\n      // backoff\n      this._retryScheduler.reset()\n\n      // Update account error state\n      const errorJSON = error.toJSON()\n      const fingerprint = [\"{{ default }}\", \"sync loop\", error.message];\n      NylasEnv.reportError(error, {fingerprint,\n        rateLimit: {\n          ratePerHour: 30,\n          key: `SyncError:SyncLoop:${error.message}`,\n        },\n      });\n\n      const isAuthError = error instanceof IMAPErrors.IMAPAuthenticationError\n      const accountSyncState = isAuthError ? SYNC_STATE_AUTH_FAILED : SYNC_STATE_ERROR;\n      // TODO this is currently a hack to keep N1's account in sync and notify of\n      // sync errors. This should go away when we merge the databases\n      Actions.updateAccount(this._account.id, {syncState: accountSyncState, syncError: errorJSON})\n\n      this._account.syncError = errorJSON\n      await this._account.save()\n    } catch (err) {\n      this._logger.error(`🔃  SyncWorker: Errored while handling error`, error)\n      err.message = `Error while handling sync loop error: ${err.message}`\n      NylasEnv.reportError(err)\n    }\n  }\n\n  async _onSyncDidComplete() {\n    if (this._destroyed) { return; }\n    const now = Date.now();\n\n    // Save metrics to the account object\n    if (!this._account.firstSyncCompletion) {\n      this._account.firstSyncCompletion = now;\n    }\n\n    const syncGraphTimeLength = 60 * 30; // 30 minutes, should be the same as SyncGraph.config.timeLength\n    let lastSyncCompletions = [].concat(this._account.lastSyncCompletions);\n    lastSyncCompletions = [now, ...lastSyncCompletions];\n    while (now - lastSyncCompletions[lastSyncCompletions.length - 1] > 1000 * syncGraphTimeLength) {\n      lastSyncCompletions.pop();\n    }\n\n    // TODO this is currently a hack to keep N1's account in sync and notify of\n    // sync errors. This should go away when we merge the databases\n    Actions.updateAccount(this._account.id, {syncState: SYNC_STATE_RUNNING})\n\n    this._account.lastSyncCompletions = lastSyncCompletions;\n    await this._account.save();\n\n    this._logger.log(`🔃 🔚 took ${now - this._syncStart}ms`)\n  }\n\n  async _scheduleNextSync(error) {\n    if (this._destroyed) { return; }\n    let reason;\n    let interval;\n    try {\n      const {Folder} = this._db;\n\n      const folders = await Folder.findAll();\n      const moreToSync = folders.some((f) => !f.isSyncComplete())\n\n      if (error != null) {\n        if (error instanceof Errors.RetryableError) {\n          interval = this._retryScheduler.currentDelay();\n        } else {\n          interval = AC_SYNC_LOOP_INTERVAL_MS;\n        }\n      } else {\n        const shouldSyncImmediately = (\n          moreToSync ||\n          this._interrupted ||\n          this._requireTokenRefresh\n        )\n        if (shouldSyncImmediately) {\n          interval = 1;\n        } else if (BatteryStatusManager.isBatteryCharging()) {\n          interval = AC_SYNC_LOOP_INTERVAL_MS;\n        } else {\n          interval = BATTERY_SYNC_LOOP_INTERVAL_MS;\n        }\n      }\n\n      reason = 'Normal schedule'\n      if (error != null) {\n        reason = `Sync errored: ${error.message}`\n      } else if (this._interrupted) {\n        reason = `Sync interrupted and restarted. Interrupt reason: ${reason}`\n      } else if (moreToSync) {\n        reason = `More to sync`\n      }\n    } catch (err) {\n      this._logger.error(`🔃  SyncWorker: Errored while scheduling next sync`, err)\n      err.message = `Error while scheduling next sync: ${err.message}`\n      NylasEnv.reportError(err)\n      interval = AC_SYNC_LOOP_INTERVAL_MS\n      reason = 'Errored while while scheduling next sync'\n    } finally {\n      const nextSyncIn = Math.max(1, this._lastSyncTime + interval - Date.now())\n      this._logger.log(`🔃 🔜 in ${nextSyncIn}ms - Reason: ${reason}`)\n\n      this._syncTimer = setTimeout(() => {\n        this.syncNow({reason});\n      }, nextSyncIn);\n    }\n  }\n\n  async _runTask(task) {\n    if (this._destroyed) { return }\n    this._currentTask = task\n    await this._mainIMAPConn.runOperation(this._currentTask, {syncWorker: this})\n    this._currentTask = null\n  }\n\n  // This function is interruptible. See Interruptible\n  async * _performSync() {\n    if (this._destroyed) { return }\n    const accountId = this._account.id\n    SyncActivity.reportSyncActivity(accountId, \"Starting worker sync\")\n    yield this._account.update({syncError: null});\n\n    const syncbackTaskRunner = new SyncbackTaskRunner({\n      db: this._db,\n      imap: this._mainIMAPConn,\n      smtp: this._smtp,\n      logger: this._logger,\n      account: this._account,\n      syncWorker: this,\n    })\n\n    SyncActivity.reportSyncActivity(accountId, \"Updating lingering tasks in progress\")\n    // Step 1: Mark all \"INPROGRESS-NOTRETRYABLE\" tasks as failed, and all\n    // \"INPROGRESS-RETRYABLE tasks as new\n    await syncbackTaskRunner.updateLingeringTasksInProgress()\n    yield // Yield to allow interruption\n\n    // Step 2: Run any available syncback tasks\n    // While running syncback tasks, we want to ignore `update` events on the\n    // inbox.\n    // `update` events happen when messages receive flag updates on the box,\n    // (e.g. marking as unread or starred). We need to listen to that event for\n    // when updates are performed from another mail client, but ignore\n    // them when they are caused from within N1 to prevent unecessary interrupts\n    SyncActivity.reportSyncActivity(accountId, \"Getting new syncback tasks\")\n    const tasks = yield syncbackTaskRunner.getNewSyncbackTasks()\n    this._shouldIgnoreInboxFlagUpdates = true\n    for (const task of tasks) {\n      SyncActivity.reportSyncActivity(accountId, `Running syncback task: ${task.description()}`)\n      await syncbackTaskRunner.runSyncbackTask(task)\n      yield  // Yield to allow interruption\n    }\n    this._shouldIgnoreInboxFlagUpdates = false\n\n    // Step 3: Fetch the folder list. We need to run this before syncing folders\n    // because we need folders to sync!\n    SyncActivity.reportSyncActivity(accountId, \"Running FetchFolderList task\")\n    await this._runTask(SyncTaskFactory.create('FetchFolderList', {account: this._account}))\n    yield  // Yield to allow interruption\n\n    // Step 4: Listen to new mail. We need to do this after we've fetched the\n    // folder list so we can correctly find the inbox folder on the very first\n    // sync loop\n    SyncActivity.reportSyncActivity(accountId, \"Listening for new mail\")\n    await this._listenForNewMail()\n    yield  // Yield to allow interruption\n\n    // Step 5: Sync each folder, sorted by inbox first\n    // TODO prioritize syncing all of inbox first if there's a ton of folders (e.g. imap\n    // accounts). If there are many folders, we would only sync the first n\n    // messages in the inbox and not go back to it until we've done the same for\n    // the rest of the folders, which would give the appearance of the inbox\n    // syncing slowly. This should only be done during initial sync.\n    // TODO Also consider using multiple imap connections, 1 for inbox, one for the\n    // rest\n    SyncActivity.reportSyncActivity(accountId, \"Getting folders to sync\")\n    const sortedFolders = yield this._getFoldersToSync()\n    for (const folder of sortedFolders) {\n      SyncActivity.reportSyncActivity(accountId, `Running FetchMessagesInFolder task: ${folder.name}`)\n      await this._runTask(SyncTaskFactory.create('FetchMessagesInFolder', {account: this._account, folder}))\n      yield  // Yield to allow interruption\n    }\n    await this._cleanupOrphanMessages();\n    SyncActivity.reportSyncActivity(accountId, \"Done with worker sync\")\n  }\n\n  // Public API:\n\n  async syncNow({reason, interrupt = false} = {}) {\n    if (this._destroyed) { return }\n    if (this._syncInProgress) {\n      if (interrupt) {\n        this.interrupt({reason})\n      }\n      return;\n    }\n\n    this._syncStart = Date.now()\n    clearTimeout(this._syncTimer);\n    this._syncTimer = null;\n    this._interrupted = false\n    this._syncInProgress = true\n\n    try {\n      await this._account.reload();\n    } catch (err) {\n      this._logger.error(`🔃  SyncWorker: Account could not be loaded. Sync worker will exit.`, err)\n      this._manager.removeWorkerForAccountId(this._account.id);\n      return;\n    }\n\n    this._logger.log(`🔃 🆕 Reason: ${reason}`)\n    let error;\n    try {\n      const newCredentials = await this._ensureAccessToken()\n      await this._ensureSMTPConnection(newCredentials);\n      await this._ensureMailListenerIMAPConnection(newCredentials);\n      await IMAPConnectionPool.withConnectionsForAccount(this._account, {\n        desiredCount: 1,\n        logger: this._logger,\n        socketTimeout: this._retryScheduler.currentDelay(),\n        onConnected: async ([mainConn], done) => {\n          this._mainIMAPConnDisposers.push(done);\n          await this._ensureMainIMAPConnection(mainConn);\n          await this._interruptible.run(this._performSync, this)\n          this._mainIMAPConnDisposers = this._mainIMAPConnDisposers.filter(d => d !== done);\n        },\n      });\n\n      await this._onSyncDidComplete();\n      this._numTimeoutErrors = 0;\n      this._retryScheduler.reset()\n    } catch (err) {\n      error = err\n      await this._onSyncError(error);\n    } finally {\n      this._lastSyncTime = Date.now()\n      this._syncInProgress = false\n      this._disposeMainIMAPConnection()\n      await this._scheduleNextSync(error)\n    }\n  }\n\n  interrupt({reason = 'No reason'} = {}) {\n    // We wrap this in a promise and don't use `async` keyword to make sure this\n    // returns a Bluebird promise that can be timed out\n    return new Promise(async (resolve) => {\n      try {\n        this._logger.log(`🔃 ✋ Interrupting sync! Reason: ${reason}`)\n        const interruptPromises = [this._interruptible.interrupt()]\n        if (this._currentTask) {\n          interruptPromises.push(this._currentTask.interrupt())\n        }\n        await Promise.all(interruptPromises)\n        resolve()\n      } finally {\n        this._interrupted = true\n      }\n    })\n  }\n\n  async destroy({timeout} = {}) {\n    this._destroyed = true;\n    clearTimeout(this._syncTimer);\n    this._syncTimer = null;\n    try {\n      if (this._syncInProgress) {\n        let promise = this.interrupt({reason: \"Sync worker destroyed\"})\n        if (timeout) {\n          promise = promise.timeout(timeout, 'Interrupt timed out while destroying worker')\n        }\n        await promise\n      }\n    } catch (err) {\n      err.message = `Error destroying sync worker: ${err.message}`\n      NylasEnv.reportError(err)\n    }\n    this._disposeConnections()\n  }\n}\n\nSyncWorker.AC_SYNC_LOOP_INTERVAL_MS = AC_SYNC_LOOP_INTERVAL_MS\nSyncWorker.BATTERY_SYNC_LOOP_INTERVAL_MS = BATTERY_SYNC_LOOP_INTERVAL_MS\nSyncWorker.MAX_SYNC_BACKOFF_MS = MAX_SYNC_BACKOFF_MS\n\nmodule.exports = SyncWorker;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-task-factory.js",
    "content": "/* eslint global-require: 0 */\n/**\n * Given a `SyncbackRequestObject` it creates the appropriate syncback task.\n *\n */\nclass SyncbackTaskFactory {\n\n  static create(account, syncbackRequest) {\n    let Task = null;\n    switch (syncbackRequest.type) {\n      case \"MoveThreadToFolder\":\n        Task = require('./syncback-tasks/move-thread-to-folder.imap'); break;\n      case \"SetThreadLabels\":\n        Task = require('./syncback-tasks/set-thread-labels.imap'); break;\n      case \"SetThreadFolderAndLabels\":\n        Task = require('./syncback-tasks/set-thread-folder-and-labels.imap'); break;\n      case \"MarkThreadAsRead\":\n        Task = require('./syncback-tasks/mark-thread-as-read.imap'); break;\n      case \"MarkThreadAsUnread\":\n        Task = require('./syncback-tasks/mark-thread-as-unread.imap'); break;\n      case \"StarThread\":\n        Task = require('./syncback-tasks/star-thread.imap'); break;\n      case \"UnstarThread\":\n        Task = require('./syncback-tasks/unstar-thread.imap'); break;\n      case \"CreateCategory\":\n        Task = require('./syncback-tasks/create-category.imap'); break;\n      case \"RenameFolder\":\n        Task = require('./syncback-tasks/rename-folder.imap'); break;\n      case \"RenameLabel\":\n        Task = require('./syncback-tasks/rename-label.imap'); break;\n      case \"DeleteFolder\":\n        Task = require('./syncback-tasks/delete-folder.imap'); break;\n      case \"DeleteLabel\":\n        Task = require('./syncback-tasks/delete-label.imap'); break;\n      case \"EnsureMessageInSentFolder\":\n        Task = require('./syncback-tasks/ensure-message-in-sent-folder.imap'); break;\n      case \"SyncUnknownUIDs\":\n        Task = require('./syncback-tasks/sync-unknown-uids.imap'); break;\n      default:\n        throw new Error(`Task type not defined in syncback-task-factory: ${syncbackRequest.type}`)\n    }\n    return new Task(account, syncbackRequest)\n  }\n}\n\nmodule.exports = SyncbackTaskFactory\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-task-runner.es6",
    "content": "import {Actions} from 'nylas-exports'\nimport SyncbackTask from './syncback-tasks/syncback-task'\nimport SyncbackTaskFactory from './syncback-task-factory';\nimport {runWithRetryLogic} from './sync-utils'\n\nconst PrioritizedTaskTypes = [\n  'EnsureMessageInSentFolder',\n]\n\n// These types of tasks are run elsewhere and should not be returned in\n// getNewSyncbackTasks(), or updated in updateLingeringTasksInProgress()\nconst IgnoredTaskTypes = [\n  'SendMessage',\n  'SendMessagePerRecipient',\n]\n\nclass SyncbackTaskRunner {\n\n  constructor({db, account, syncWorker, logger, imap, smtp} = {}) {\n    if (!db) {\n      throw new Error('SyncbackTaskRunner: need to pass db')\n    }\n    if (!account) {\n      throw new Error('SyncbackTaskRunner: need to pass account')\n    }\n    if (!logger) {\n      throw new Error('SyncbackTaskRunner: need to pass logger')\n    }\n    if (!imap) {\n      throw new Error('SyncbackTaskRunner: need to pass imap')\n    }\n    if (!smtp) {\n      throw new Error('SyncbackTaskRunner: need to pass smtp')\n    }\n    if (!syncWorker) {\n      throw new Error('SyncbackTaskRunner: need to pass syncWorker')\n    }\n    this._db = db\n    this._account = account\n    this._logger = logger\n    this._imap = imap\n    this._smtp = smtp\n    this._syncWorker = syncWorker\n  }\n\n  /**\n   * Returns a list of at most 100 Syncback requests, sorted by creation date\n   * (older first) and by how they affect message IMAP uids.\n   *\n   * We want to make sure that we run the tasks that affect IMAP uids last, and\n   * that we don't  run 2 tasks that will affect the same set of UIDS together,\n   * i.e. without running a sync loop in between them.\n   *\n   * For example, if there's a task to change the labels of a message, and also\n   * a task to move that message to another folder, we need to run the label\n   * change /first/, otherwise the message would be moved and it would receive a\n   * new IMAP uid, and then attempting to change labels with an old uid would\n   * fail.\n   */\n  async getNewSyncbackTasks() {\n    const {SyncbackRequest, Message} = this._db;\n\n    const prioritizedTasks = await SyncbackRequest.findAll({\n      limit: 100,\n      where: {type: PrioritizedTaskTypes, status: 'NEW'},\n      order: [['createdAt', 'ASC']],\n    })\n    .map((req) => SyncbackTaskFactory.create(this._account, req))\n    const otherTasks = await SyncbackRequest.findAll({\n      limit: 100,\n      where: {type: {$notIn: PrioritizedTaskTypes.concat(IgnoredTaskTypes)}, status: 'NEW'},\n      order: [['createdAt', 'ASC']],\n    })\n    .map((req) => SyncbackTaskFactory.create(this._account, req))\n\n    if (prioritizedTasks.length === 0 && otherTasks.length === 0) { return [] }\n\n    const tasksToProcess = [\n      ...prioritizedTasks,\n      ...otherTasks.filter(t => !t.affectsImapMessageUIDs()),\n    ]\n    const tasksAffectingUIDs = otherTasks.filter(t => t.affectsImapMessageUIDs())\n\n    const changeFolderTasks = tasksAffectingUIDs.filter(t =>\n      t.description() === 'RenameFolder' || t.description() === 'DeleteFolder'\n    )\n    if (changeFolderTasks.length > 0) {\n      // If we are renaming or deleting folders, those are the only tasks we\n      // want to process before executing any other tasks that may change uids.\n      // These operations may not change the uids of their messages, but we\n      // can't guarantee it, so to make sure, we will just run these.\n      const affectedFolderIds = new Set()\n      changeFolderTasks.forEach((task) => {\n        const {props: {folderId}} = task.syncbackRequestObject()\n        if (folderId && !affectedFolderIds.has(folderId)) {\n          tasksToProcess.push(task)\n          affectedFolderIds.add(folderId)\n        }\n      })\n      return tasksToProcess\n    }\n\n    // Otherwise, make sure that we don't process more than 1 task that will affect\n    // the UID of the same message\n    const affectedMessageIds = new Set()\n    for (const task of tasksAffectingUIDs) {\n      const {props: {messageId, threadId}} = task.syncbackRequestObject()\n      if (messageId) {\n        if (!affectedMessageIds.has(messageId)) {\n          tasksToProcess.push(task)\n          affectedMessageIds.add(messageId)\n        }\n      } else if (threadId) {\n        const messageIds = await Message.findAll({\n          attributes: ['id', 'threadId'],\n          where: {threadId}})\n        .map(m => m.id)\n        const shouldIncludeTask = messageIds.every(id => !affectedMessageIds.has(id))\n        if (shouldIncludeTask) {\n          tasksToProcess.push(task)\n          messageIds.forEach(id => affectedMessageIds.add(id))\n        }\n      }\n    }\n    return tasksToProcess\n  }\n\n  async updateLingeringTasksInProgress() {\n    // We use a very limited type of two-phase commit: before we start\n    // running a syncback task, we mark it as \"in progress\". If something\n    // happens during the syncback (the worker window crashes, or the power\n    // goes down), the task won't succeed or fail.\n    // By default, we will attempt to retry any INPROGRESS-RETRYABLE tasks\n    const {SyncbackRequest} = this._db;\n\n    const retryableRequests = await SyncbackRequest.findAll({\n      where: {status: 'INPROGRESS-RETRYABLE', type: {$notIn: IgnoredTaskTypes}},\n    });\n    const notRetryableRequests = await SyncbackRequest.findAll({\n      where: {status: 'INPROGRESS-NOTRETRYABLE', type: {$notIn: IgnoredTaskTypes}},\n    });\n\n    for (const retryableReq of retryableRequests) {\n      retryableReq.status = 'NEW';\n      await retryableReq.save();\n    }\n    for (const notRetryableReq of notRetryableRequests) {\n      notRetryableReq.status = 'FAILED';\n      const errorMessage = `App was closed while ${notRetryableReq.type} was in progress.`\n      notRetryableReq.error = new Error(errorMessage)\n      await notRetryableReq.save();\n    }\n  }\n\n  async runSyncbackTask(task) {\n    if (!task || !(task instanceof SyncbackTask)) {\n      throw new Error('runSyncbackTask: must pass a SyncbackTask')\n    }\n    const before = new Date();\n    const syncbackRequest = task.syncbackRequestObject();\n\n    this._logger.log(`🔃 📤 ${task.description()}`, syncbackRequest.props)\n\n    const run = async () => {\n      // Before anything, mark the task as in progress. This allows\n      // us to not run the same task twice.\n      syncbackRequest.status = task.inProgressStatusType();\n      await syncbackRequest.save();\n\n      const resource = task.resource()\n      let responseJSON;\n      switch (resource) {\n        case 'imap':\n          responseJSON = await this._imap.runOperation(task, {syncWorker: this._syncWorker})\n          break;\n        case 'smtp':\n          responseJSON = await task.run(this._db, this._smtp)\n          break;\n        default:\n          throw new Error(`runSyncbackTask: unknown resource. Must be one of ['imap', 'smtp']`)\n      }\n      syncbackRequest.status = \"SUCCEEDED\";\n      syncbackRequest.responseJSON = responseJSON || {};\n      await syncbackRequest.save();\n\n      const after = new Date();\n      this._logger.log(`🔃 📤 ${task.description()} Succeeded (${after.getTime() - before.getTime()}ms)`)\n    }\n\n    let retryableError;\n    const onRetryableError = async (error) => {\n      retryableError = error\n      const after = new Date();\n      syncbackRequest.status = \"NEW\";\n      await syncbackRequest.save();\n      this._logger.warn(`🔃 📤 ${task.description()} Failed with retryable error, retrying in next loop (${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequest.toJSON(), error})\n    }\n\n    try {\n      await runWithRetryLogic({run, onRetryableError})\n    } catch (error) {\n      const after = new Date();\n      const fingerprint = [\"{{ default }}\", \"syncback task\", error.message];\n      NylasEnv.reportError(error, {fingerprint: fingerprint});\n      syncbackRequest.error = error;\n      syncbackRequest.status = \"FAILED\";\n      this._logger.error(`🔃 📤 ${task.description()} Failed (${after.getTime() - before.getTime()}ms)`, {syncbackRequest: syncbackRequest.toJSON(), error})\n      await syncbackRequest.save();\n    }\n\n    if (retryableError) {\n      // Throw retryable error to interrupt and restart sync loop\n      // The sync loop will take care of backing off when handling retryable\n      // errors.\n      retryableError.message = `${task.description()} failed with retryable error: ${retryableError.message}`\n      throw retryableError\n    }\n  }\n}\n\nexport default SyncbackTaskRunner\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/create-category.imap.es6",
    "content": "const {SyncbackIMAPTask} = require('./syncback-task')\n\nclass CreateCategoryIMAP extends SyncbackIMAPTask {\n  description() {\n    return `CreateCategory`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {accountId} = db\n    const {objectClass, displayName} = this.syncbackRequestObject().props\n    yield imap.addBox(displayName)\n    const id = db[objectClass].hash({boxName: displayName, accountId})\n    const category = yield db[objectClass].create({\n      id,\n      accountId,\n      name: displayName,\n    })\n    return category.toJSON()\n  }\n}\nmodule.exports = CreateCategoryIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/delete-folder.imap.es6",
    "content": "const {SyncbackIMAPTask} = require('./syncback-task')\n\nclass DeleteFolderIMAP extends SyncbackIMAPTask {\n  description() {\n    return `DeleteFolder`;\n  }\n\n  affectsImapMessageUIDs() {\n    return true\n  }\n\n  async * _run(db, imap) {\n    const {folderId} = this.syncbackRequestObject().props\n    const folder = yield db.Folder.findById(folderId)\n    if (!folder) {\n      // Nothing to delete!\n      return\n    }\n    yield imap.delBox(folder.name);\n\n    // If IMAP succeeds, save updates to the db\n    yield folder.destroy()\n  }\n}\nmodule.exports = DeleteFolderIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/delete-label.imap.es6",
    "content": "const {SyncbackIMAPTask} = require('./syncback-task')\n\nclass DeleteLabelIMAP extends SyncbackIMAPTask {\n  description() {\n    return `DeleteLabel`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {labelId} = this.syncbackRequestObject().props.labelId\n    const label = yield db.Label.findById(labelId)\n    if (!label) {\n      // Nothing to delete!\n      return\n    }\n    yield imap.delBox(label.name);\n\n    // If IMAP succeeds, save updates to the db\n    yield label.destroy()\n  }\n}\nmodule.exports = DeleteLabelIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/ensure-message-in-sent-folder.imap.es6",
    "content": "const {\n  Provider,\n  Errors: {APIError},\n  MessageUtils: {getReplyHeaders, buildMime},\n} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst SyncTaskFactory = require('../sync-task-factory');\n\n\nasync function* deleteGmailSentMessages({db, imap, provider, headerMessageId}) {\n  if (provider !== 'gmail') { return }\n\n  const trash = yield db.Folder.find({where: {role: 'trash'}});\n  if (!trash) { throw new APIError(`Could not find folder with role 'trash'.`) }\n\n  const allMail = yield db.Folder.find({where: {role: 'all'}});\n  if (!allMail) { throw new APIError(`Could not find folder with role 'all'.`) }\n\n  // Move the message from all mail to trash and then delete it from there\n  const steps = [\n    {folder: allMail, deleteFn: (box, uid) => box.moveFromBox(uid, trash.name)},\n    {folder: trash, deleteFn: (box, uid) => box.addFlags(uid, 'DELETED')},\n  ]\n\n  for (const {folder, deleteFn} of steps) {\n    const box = yield imap.openBox(folder.name);\n    const uids = yield box.search([['HEADER', 'Message-ID', headerMessageId]])\n    for (const uid of uids) {\n      yield deleteFn(box, uid);\n    }\n    yield box.closeBox();\n  }\n}\n\nasync function* saveSentMessage({db, account, syncWorker, logger, imap, provider, customSentMessage, baseMessage}) {\n  const {Folder, Label} = db\n\n  // Case 1. If non gmail, save the message to the `sent` folder using IMAP\n  // Only gmail creates a sent message for us, so if we are using any other provider\n  // we need to save it manually ourselves.\n  if (provider !== 'gmail') {\n    const sentFolder = yield Folder.find({where: {role: 'sent'}});\n    if (!sentFolder) { throw new APIError(`Can't find sent folder - could not save message to sent folder.`) }\n\n    const rawMime = yield buildMime(baseMessage, {includeBcc: true});\n    const box = yield imap.openBox(sentFolder.name);\n    yield box.append(rawMime, {flags: 'SEEN'});\n\n    // If IMAP succeeds, fetch any new messages in the sent folder which\n    // should include the messages we just created there\n    // The sync operation will save the changes to the database.\n    // TODO add transaction\n    const syncOperation = SyncTaskFactory.create('FetchNewMessagesInFolder', {\n      account,\n      folder: sentFolder,\n    })\n    yield syncOperation.run(db, imap, {syncWorker})\n    return\n  }\n\n\n  // Showing as sent in gmail means adding the message to all mail and\n  // adding the sent label\n  const sentLabel = yield Label.find({where: {role: 'sent'}});\n  const allMailFolder = yield Folder.find({where: {role: 'all'}});\n  if (!sentLabel || !allMailFolder) {\n    throw new APIError('Could not save message to sent folder.')\n  }\n\n\n  // Case 2. If gmail, even though gmail saves sent messages automatically,\n  // if `customSentMessage` is true, it means we want to save the `baseMessage`\n  // as sent. This is because that means that we sent a message per recipient for\n  // tracking, but we actually /just/ want to show the baseMessage as sent\n  if (customSentMessage) {\n    const rawMime = yield buildMime(baseMessage, {includeBcc: true});\n    const box = yield imap.openBox(allMailFolder.name);\n\n    yield box.append(rawMime, {flags: 'SEEN'})\n\n    const {headerMessageId} = baseMessage\n    const uids = yield box.search([['HEADER', 'Message-ID', headerMessageId]])\n    // There should only be one uid in the array\n    yield box.setLabels(uids[0], sentLabel.imapLabelIdentifier());\n  }\n\n  // If IMAP succeeds, fetch any new messages in the sent folder which\n  // should include the messages we just created there\n  // The sync operation will save the changes to the database.\n  // TODO add transaction\n  const syncOperation = SyncTaskFactory.create('FetchNewMessagesInFolder', {\n    account,\n    folder: allMailFolder,\n  })\n  yield syncOperation.run(db, imap, {syncWorker})\n}\n\nasync function* setThreadingReferences(db, baseMessage) {\n  const {Message, Reference} = db\n  // TODO When the message was created for sending, we set the\n  // `inReplyToLocalMessageId` if it exists, and we set the temporary properties\n  // `inReplyTo` and `references` for sending.\n  // Since these properties aren't saved to the model, we need to recreate\n  // them again because they are necessary for building the correct raw mime\n  // message to add to the sent folder\n  // We should clean this up\n  const replyToMessage = yield Message.findById(\n    baseMessage.inReplyToLocalMessageId,\n    { include: [{model: Reference, as: 'references', attributes: ['id', 'rfc2822MessageId']}] }\n  )\n  if (replyToMessage) {\n    const {inReplyTo, references} = getReplyHeaders(replyToMessage);\n    baseMessage.inReplyTo = inReplyTo;\n    baseMessage.references = references;\n  }\n}\n\n/**\n * Ensures that sent messages show up in the sent folder.\n *\n * Gmail does this automatically. IMAP needs to do this manually.\n *\n * We sometimes request a  `customSentMessage` because we may have\n * individualized a bunch of messages via multi-send, or have link & open\n * tracking data that we don't want to see in our sent folder. Regardless\n * we need to make it look like you only sent 1 message. To do this we,\n * delete all of the messages Gmail automatically created (keyed by the\n * same Meassage-Id header we set), then stuff a copy of the original\n * message in the sent folder.\n */\nclass EnsureMessageInSentFolderIMAP extends SyncbackIMAPTask {\n  description() {\n    return `EnsureMessageInSentFolder`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap, {syncWorker} = {}) {\n    const {Message} = db\n    const {messageId, customSentMessage} = this.syncbackRequestObject().props\n\n    const baseMessage = yield Message.findById(messageId, {\n      include: [{model: db.Folder}, {model: db.Label}, {model: db.File}],\n    });\n\n    if (!baseMessage) {\n      throw new APIError(`Couldn't find message ${messageId} to stuff in sent folder`, 500)\n    }\n\n    yield setThreadingReferences(db, baseMessage)\n\n    const {provider} = this._account\n    const {headerMessageId} = baseMessage\n\n    // Gmail automatically creates sent messages when sending, so we\n    // delete each of the ones we sent to each recipient in the\n    // `SendMessagePerRecipient` task\n    //\n    // Each participant gets a message, but all of those messages have the\n    // same Message-ID header in them. This allows us to find all of the\n    // sent messages and clean them up\n    if (customSentMessage && provider === Provider.Gmail) {\n      try {\n        yield deleteGmailSentMessages({db, imap, provider, headerMessageId})\n      } catch (err) {\n        // Even if this fails, we need to finish attempting to save the\n        // baseMessage to the sent folder\n        this._logger.error(err, 'EnsureMessageInSentFolder: Failed to delete Gmail sent messages');\n      }\n    }\n\n    yield saveSentMessage({db, account: this._account, syncWorker, logger: this._logger, imap, provider, customSentMessage, baseMessage})\n    return baseMessage.toJSON()\n  }\n}\n\nmodule.exports = EnsureMessageInSentFolderIMAP;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/mark-thread-as-read.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst IMAPHelpers = require('../imap-helpers')\n\nclass MarkThreadAsRead extends SyncbackIMAPTask {\n  description() {\n    return `MarkThreadAsRead`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {sequelize, Thread} = db\n    const threadId = this.syncbackRequestObject().props.threadId\n    if (!threadId) {\n      throw new APIError('threadId is required')\n    }\n\n    const thread = yield Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`Can't find thread`, 404)\n    }\n    const threadMessages = yield thread.getMessages()\n    yield IMAPHelpers.forEachFolderOfThread({\n      db,\n      imap,\n      threadMessages,\n      callback({messageImapUIDs, box}) {\n        return box.addFlags(messageImapUIDs, 'SEEN')\n      },\n    })\n    // If IMAP succeeds, save the model updates\n    yield sequelize.transaction(async (transaction) => {\n      await Promise.all(threadMessages.map((m) => m.update({unread: false}, {transaction})))\n      await thread.update({unreadCount: 0}, {transaction})\n    })\n  }\n}\nmodule.exports = MarkThreadAsRead;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/mark-thread-as-unread.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst IMAPHelpers = require('../imap-helpers')\n\nclass MarkThreadAsUnread extends SyncbackIMAPTask {\n  description() {\n    return `MarkThreadAsUnread`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {sequelize, Thread} = db\n    const threadId = this.syncbackRequestObject().props.threadId\n    if (!threadId) {\n      throw new APIError('threadId is required')\n    }\n\n    const thread = yield Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`Can't find thread`, 404)\n    }\n    const threadMessages = yield thread.getMessages()\n    yield IMAPHelpers.forEachFolderOfThread({\n      db,\n      imap,\n      threadMessages,\n      callback({messageImapUIDs, box}) {\n        return box.delFlags(messageImapUIDs, 'SEEN')\n      },\n    })\n    // If IMAP succeeds, save the model updates\n    yield sequelize.transaction(async (transaction) => {\n      await Promise.all(threadMessages.map((m) => m.update({unread: true}, {transaction})))\n      await thread.update({unreadCount: threadMessages.length}, {transaction})\n    })\n  }\n}\nmodule.exports = MarkThreadAsUnread;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/move-thread-to-folder.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst IMAPHelpers = require('../imap-helpers')\nconst SyncTaskFactory = require('../sync-task-factory');\n\nclass MoveThreadToFolderIMAP extends SyncbackIMAPTask {\n  description() {\n    return `MoveThreadToFolder`;\n  }\n\n  affectsImapMessageUIDs() {\n    return true\n  }\n\n  async * _run(db, imap, {syncWorker} = {}) {\n    const {Thread, Folder} = db\n    const threadId = this.syncbackRequestObject().props.threadId\n    const targetFolderId = this.syncbackRequestObject().props.folderId\n    if (!threadId) {\n      throw new APIError('threadId is required')\n    }\n\n    if (!targetFolderId) {\n      throw new APIError('targetFolderId is required')\n    }\n\n    const targetFolder = yield Folder.findById(targetFolderId)\n    if (!targetFolder) {\n      throw new APIError('targetFolder not found', 404)\n    }\n\n    const thread = yield Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`Can't find thread`, 404)\n    }\n\n    const threadMessages = yield thread.getMessages()\n    yield IMAPHelpers.forEachFolderOfThread({\n      db,\n      imap,\n      threadMessages,\n      async callback({box, folder, messageImapUIDs}) {\n        if (folder.id === targetFolderId) {\n          return Promise.resolve()\n        }\n        return box.moveFromBox(messageImapUIDs, targetFolder.name)\n      },\n    })\n\n    // If IMAP succeeds, fetch any new messages in the target folder which\n    // should include the messages we just moved there\n    // The sync operation will save the changes to the database.\n    // TODO add transaction\n    const syncOperation = SyncTaskFactory.create('FetchNewMessagesInFolder', {\n      account: this._account,\n      folder: targetFolder,\n    })\n    yield syncOperation.run(db, imap, {syncWorker})\n  }\n}\nmodule.exports = MoveThreadToFolderIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/rename-folder.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\n\nclass RenameFolderIMAP extends SyncbackIMAPTask {\n  description() {\n    return `RenameFolder`;\n  }\n\n  affectsImapMessageUIDs() {\n    return true\n  }\n\n  async * _run(db, imap) {\n    const {sequelize, accountId, Folder} = db\n    const {folderId, newFolderName} = this.syncbackRequestObject().props.folderId\n    const oldFolder = yield Folder.findById(folderId)\n    yield imap.renameBox(oldFolder.name, newFolderName);\n\n    // After IMAP succeeds, update the db\n    const newId = Folder.hash({boxName: newFolderName, accountId})\n    let newFolder;\n    yield sequelize.transaction(async (transaction) => {\n      newFolder = await Folder.create({\n        id: newId,\n        accountId,\n        name: newFolderName,\n      }, {transaction})\n\n      // We can't do batch updates because we need to generate deltas for each\n      // message and thread\n      const messages = await oldFolder.getMessages({\n        transaction,\n        attributes: ['id'],\n        include: [{model: Folder, as: 'folders', attributes: ['id']}],\n      })\n      await Promise.all(messages.map(async (m) => {\n        await m.setFolder(newFolder, {transaction})\n        await m.save({transaction})\n      }))\n      const threads = await oldFolder.getThreads({\n        transaction,\n        attributes: ['id'],\n        include: [{model: Folder, as: 'folders', attributes: ['id']}],\n      })\n      await Promise.all(threads.map(async (t) => {\n        const nextFolders = [\n          newFolder,\n          ...t.folders.filter(f => f.id !== oldFolder.id),\n        ]\n        await t.setFolders(nextFolders, {transaction})\n        await t.save({transaction})\n      }))\n      await oldFolder.destroy({transaction})\n    })\n    if (!newFolder) {\n      throw new APIError(`Error renaming folder - can't save to database`)\n    }\n    return newFolder.toJSON()\n  }\n}\nmodule.exports = RenameFolderIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/rename-label.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\n\nclass RenameLabelIMAP extends SyncbackIMAPTask {\n  description() {\n    return `RenameLabel`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {sequelize, accountId, Label} = db\n    const {labelId, newLabelName} = this.syncbackRequestObject().props\n    const oldLabel = yield Label.findById(labelId)\n    yield imap.renameBox(oldLabel.name, newLabelName);\n\n    // After IMAP succeeds, update the db\n    const newId = Label.hash({boxName: newLabelName, accountId})\n    let newLabel;\n    yield sequelize.transaction(async (transaction) => {\n      newLabel = await Label.create({\n        id: newId,\n        accountId,\n        name: newLabelName,\n      }, {transaction})\n\n      // We can't do batch updates because we need to generate deltas for each\n      // message and thread\n      const messages = await oldLabel.getMessages({\n        transaction,\n        attributes: ['id'],\n        include: [{model: Label, as: 'labels', attributes: ['id']}],\n      })\n      await Promise.all(messages.map(async (m) => {\n        const nextLabels = [\n          newLabel,\n          ...m.labels.filter(l => l.id !== oldLabel.id),\n        ]\n        await m.setLabels(nextLabels, {transaction})\n        await m.save({transaction})\n      }))\n      const threads = await oldLabel.getThreads({\n        transaction,\n        attributes: ['id'],\n        include: [{model: Label, as: 'labels', attributes: ['id']}],\n      })\n      await Promise.all(threads.map(async (t) => {\n        const nextLabels = [\n          newLabel,\n          ...t.labels.filter(l => l.id !== oldLabel.id),\n        ]\n        await t.setLabels(nextLabels, {transaction})\n        await t.save({transaction})\n      }))\n      await oldLabel.destroy({transaction})\n    })\n    if (!newLabel) {\n      throw new APIError(`Error renaming label - can't save to database`)\n    }\n    return newLabel.toJSON()\n  }\n}\nmodule.exports = RenameLabelIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/send-message-per-recipient.smtp.es6",
    "content": "const {Errors: {APIError}, MessageUtils, TrackingUtils, ModelUtils} = require('isomorphic-core')\nconst {SyncbackSMTPTask} = require('./syncback-task')\n\n\n/**\n * This enables customized link and open tracking on a per-recipient basis\n * by delivering several messages to each recipient.\n *\n * Errors in this task always mean all message failed to send to all\n * receipients.\n *\n * If it failed to some recipients, we return a `failedRecipients` array\n * to notify the user.\n *\n * We later get EnsureMessageInSentFolder queued with the\n * `customSentMessage` flag set to ensure the newly delivered message shows\n * up in the sent folder and only a single message shows up in the sent\n * folder.\n */\nclass SendMessagePerRecipientSMTP extends SyncbackSMTPTask {\n  description() {\n    return `SendMessagePerRecipient`;\n  }\n\n  async * _run(db, smtp) {\n    const syncbackRequest = this.syncbackRequestObject()\n    const {\n      messagePayload,\n      usesOpenTracking,\n      usesLinkTracking,\n    } = syncbackRequest.props;\n    const baseMessage = yield MessageUtils.buildForSend(db, messagePayload)\n\n    let sendResult;\n    try {\n      sendResult = yield this._sendPerRecipient({\n        smtp, baseMessage, logger: this._logger, usesOpenTracking, usesLinkTracking,\n      })\n    } catch (err) {\n      throw new APIError('SendMessagePerRecipient: Sending failed for all recipients', 500);\n    }\n    /**\n     * Once messages have actually been delivered, we need to be very\n     * careful not to throw an error from this task. An Error in the send\n     * task implies failed delivery and the prompting of users to try\n     * again.\n     */\n    try {\n      /**\n       * When we send to multiple recipients, we only want 1 message in\n       * our sent folder that is void of tracking links.\n       *\n       * If the send is quick, we'll beat the sync loop and save a new\n       * message to the database. If the send is slow, on Gmail there may\n       * already be a message in our sent folder that was synced there.\n       */\n      let sentMessage = await db.Message.findById(baseMessage.id, {\n        include: [{model: db.Folder}, {model: db.Label}, {model: db.File}],\n      });\n      if (!sentMessage) {\n        sentMessage = baseMessage;\n      }\n\n      // We strip the tracking links because this is the message that we want to\n      // show the user as sent, so it shouldn't contain the tracking links\n      sentMessage.body = TrackingUtils.stripTrackingLinksFromBody(baseMessage.body)\n      sentMessage.setIsSent(true)\n\n      // We don't save the message until after successfully sending it.\n      // In the next sync loop, the message's labels and other data will\n      // be updated, and we can guarantee this because we control message\n      // id generation. The thread will be created or updated when we\n      // detect this message in the sync loop\n      await sentMessage.save()\n\n      return {\n        message: sentMessage.toJSON(),\n        failedRecipients: sendResult.failedRecipients,\n      }\n    } catch (err) {\n      this._logger.error('SendMessagePerRecipient: Failed to save the baseMessage to local sync database after it was successfully delivered', err);\n      return {message: {}, failedRecipients: []}\n    }\n  }\n\n  async _sendPerRecipient({smtp, baseMessage, usesOpenTracking, usesLinkTracking} = {}) {\n    const recipients = baseMessage.getRecipients()\n    const failedRecipients = []\n\n    await Promise.all(recipients.map(async recipient => {\n      const customBody = TrackingUtils.addRecipientToTrackingLinks({\n        recipient,\n        baseMessage,\n        usesOpenTracking,\n        usesLinkTracking,\n      })\n\n      const individualizedMessageValues = ModelUtils.copyModelValues(baseMessage, {\n        body: customBody,\n      })\n      // TODO we set these temporary properties which aren't stored in the\n      // database model because SendmailClient requires them to send the message\n      // with the correct headers.\n      // This should be cleaned up\n      individualizedMessageValues.references = baseMessage.references;\n      individualizedMessageValues.inReplyTo = baseMessage.inReplyTo;\n\n      try {\n        await smtp.sendCustom(individualizedMessageValues, {to: [recipient]})\n      } catch (error) {\n        this._logger.error(error, {recipient: recipient.email}, 'SendMessagePerRecipient: Failed to send to recipient');\n        failedRecipients.push(recipient.email)\n      }\n    }))\n    if (failedRecipients.length === recipients.length) {\n      throw new APIError('SendMessagePerRecipient: Sending failed for all recipients', 500);\n    }\n    return {failedRecipients}\n  }\n}\n\nmodule.exports = SendMessagePerRecipientSMTP;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/send-message.smtp.es6",
    "content": "const {MessageUtils, TrackingUtils} = require('isomorphic-core')\nconst {SyncbackSMTPTask} = require('../syncback-tasks/syncback-task')\n\n/**\n * This sets up the actual delivery of a message.\n *\n * Errors in this task always mean the message failed to deliver and it's\n * safe to retry\n *\n * We later get EnsureMessageInSentFolder queued to ensure the newly\n * delivered message shows up in the sent folder.\n */\nclass SendMessageSMTP extends SyncbackSMTPTask {\n  description() {\n    return `SendMessage`;\n  }\n\n  async * _run(db, smtp) {\n    const syncbackRequest = this.syncbackRequestObject()\n    const {messagePayload} = syncbackRequest.props\n    const message = yield MessageUtils.buildForSend(db, messagePayload);\n    await smtp.send(message);\n\n    try {\n      message.body = TrackingUtils.stripTrackingLinksFromBody(message.body)\n      message.setIsSent(true)\n      await message.save();\n      return {message: message.toJSON()}\n    } catch (err) {\n      this._logger.error(err, \"SendMessage: Failed to save the message to the local sync database after it was successfully delivered\")\n      return {message: {}}\n    }\n  }\n}\n\nmodule.exports = SendMessageSMTP;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/set-thread-folder-and-labels.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst IMAPHelpers = require('../imap-helpers')\nconst SyncTaskFactory = require('../sync-task-factory');\n\n\nclass SetThreadFolderAndLabelsIMAP extends SyncbackIMAPTask {\n  description() {\n    return `SetThreadFolderAndLabels`;\n  }\n\n  affectsImapMessageUIDs() {\n    return true\n  }\n\n\n  async * _run(db, imap, {syncWorker} = {}) {\n    const {Thread, Folder} = db\n    const threadId = this.syncbackRequestObject().props.threadId\n    const labelIds = this.syncbackRequestObject().props.labelIds\n    const targetFolderId = this.syncbackRequestObject().props.folderId\n    if (!threadId) {\n      throw new APIError('threadId is required')\n    }\n\n    if (!targetFolderId) {\n      throw new APIError('targetFolderId is required')\n    }\n\n    const targetFolder = yield Folder.findById(targetFolderId)\n    if (!targetFolder) {\n      throw new APIError('targetFolder not found', 404)\n    }\n\n    const thread = yield Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`Can't find thread`, 404)\n    }\n\n    const threadMessages = yield thread.getMessages()\n    yield IMAPHelpers.forEachFolderOfThread({\n      db,\n      imap,\n      threadMessages,\n      async callback({box, folder, messages, messageImapUIDs}) {\n        await IMAPHelpers.setLabelsForMessages({db, box, messages, labelIds})\n\n        if (folder.id === targetFolderId) {\n          return Promise.resolve()\n        }\n        return box.moveFromBox(messageImapUIDs, targetFolder.name)\n      },\n    })\n\n    // If IMAP succeeds, fetch any new messages in the target folder which\n    // should include the messages we just moved there\n    // The sync operation will save the changes to the database.\n    // TODO add transaction\n    const syncOperation = SyncTaskFactory.create('FetchNewMessagesInFolder', {\n      account: this._account,\n      folder: targetFolder,\n    })\n    yield syncOperation.run(db, imap, {syncWorker})\n  }\n}\nmodule.exports = SetThreadFolderAndLabelsIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/set-thread-labels.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst IMAPHelpers = require('../imap-helpers')\n\nclass SetThreadLabelsIMAP extends SyncbackIMAPTask {\n  description() {\n    return `SetThreadLabels`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {sequelize, Thread} = db\n    const threadId = this.syncbackRequestObject().props.threadId\n    const labelIds = this.syncbackRequestObject().props.labelIds\n    if (!threadId) {\n      throw new APIError('threadId is required')\n    }\n\n    const thread = yield Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`Can't find thread`, 404)\n    }\n\n    const threadMessages = yield thread.getMessages()\n    yield IMAPHelpers.forEachFolderOfThread({\n      db,\n      imap,\n      threadMessages,\n      async callback({box, messages}) {\n        return IMAPHelpers.setLabelsForMessages({db, box, messages, labelIds})\n      },\n    })\n\n    // If IMAP succeeds, save the model updates\n    yield sequelize.transaction(async (transaction) => {\n      await Promise.all(threadMessages.map(async (m) => m.setLabels(labelIds, {transaction})))\n      await thread.setLabels(labelIds, {transaction})\n    })\n  }\n}\nmodule.exports = SetThreadLabelsIMAP\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/star-thread.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst IMAPHelpers = require('../imap-helpers')\n\nclass StarThread extends SyncbackIMAPTask {\n  description() {\n    return `StarThread`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {sequelize, Thread} = db\n    const threadId = this.syncbackRequestObject().props.threadId\n    if (!threadId) {\n      throw new APIError('threadId is required')\n    }\n\n    const thread = yield Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`Can't find thread`, 404)\n    }\n    const threadMessages = yield thread.getMessages()\n    yield IMAPHelpers.forEachFolderOfThread({\n      db,\n      imap,\n      threadMessages,\n      callback({messageImapUIDs, box}) {\n        return box.addFlags(messageImapUIDs, 'FLAGGED')\n      },\n    })\n    // If IMAP succeeds, save the model updates\n    yield sequelize.transaction(async (transaction) => {\n      await Promise.all(threadMessages.map((m) => m.update({starred: true}, {transaction})))\n      await thread.update({starredCount: threadMessages.length}, {transaction})\n    })\n  }\n}\nmodule.exports = StarThread;\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/sync-unknown-uids.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst SyncTaskFactory = require('../sync-task-factory');\n\nconst UNKNOWN_UID_SYNC_BATCH_SIZE = 25;\nconst NUM_BATCHES_PER_TASK = 20; // 500 messages per task\n\nclass SyncUnknownUIDs extends SyncbackIMAPTask {\n  inProgressStatusType() {\n    return 'INPROGRESS-NOTRETRYABLE';\n  }\n\n  description() {\n    return `SyncUnknownUIDs`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false;\n  }\n\n  async * _run(db, imap, {syncWorker} = {}) {\n    this._db = db;\n    const {Folder} = db\n    const {uids, folderId} = this.syncbackRequestObject().props;\n    if (!uids || !uids.length) {\n      throw new APIError('uids are required');\n    }\n\n    if (!folderId) {\n      throw new APIError('folderId is required');\n    }\n\n    const folder = yield Folder.findById(folderId);\n    if (!folder) {\n      throw new APIError('folder not found', 404);\n    }\n\n    if (yield this._isCancelled()) {\n      return;\n    }\n\n    let remainingUids = uids;\n    // We work in smaller batches to reduce the result latency during search.\n    for (let i = 0; i < NUM_BATCHES_PER_TASK; ++i) {\n      const uidsToSync = remainingUids.slice(0, UNKNOWN_UID_SYNC_BATCH_SIZE);\n      this._syncOperation = SyncTaskFactory.create('FetchSpecificMessagesInFolder', {\n        account: this._account,\n        folder,\n        uids: uidsToSync,\n      });\n      this._syncOperation.on('message-processed', () => this.onMessageProcessed());\n      yield this._syncOperation.run(db, imap, {syncWorker})\n      this._syncOperation.removeAllListeners('message-processed');\n\n      if (yield this._isCancelled()) {\n        return;\n      }\n\n      remainingUids = remainingUids.slice(UNKNOWN_UID_SYNC_BATCH_SIZE);\n      if (remainingUids.length === 0) {\n        break;\n      }\n    }\n\n    // If there are still more UIDs to sync, queue another task to continue syncing.\n    // We do this style of chained syncback tasks so that we don't block the\n    // sync loop for too long.\n    if (remainingUids.length > 0) {\n      yield db.SyncbackRequest.create({\n        type: \"SyncUnknownUIDs\",\n        props: {folderId, uids: remainingUids},\n        accountId: this.syncbackRequestObject().accountId,\n      });\n    }\n  }\n\n  async _isCancelled() {\n    if (this._isCancelledCached) {\n      return this._isCancelledCached;\n    }\n\n    await this.syncbackRequestObject().reload();\n    if (this.syncbackRequestObject().status === 'CANCELLED') {\n      this._isCancelledCached = true;\n      return true;\n    }\n    return false;\n  }\n\n  async onMessageProcessed() {\n    if (await this._isCancelled()) {\n      await this._syncOperation.interrupt();\n    }\n  }\n}\nmodule.exports = SyncUnknownUIDs\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/syncback-task.es6",
    "content": "import {Actions} from 'nylas-exports'\nimport Interruptible from '../../shared/interruptible'\n\n// TODO: Choose a more appropriate timeout once we've gathered some metrics\nconst TIMEOUT_DELAY = 5 * 60 * 1000;\n\nclass SyncbackTask {\n  constructor(account, syncbackRequest) {\n    this._account = account;\n    this._syncbackRequest = syncbackRequest;\n    this._interruptible = new Interruptible()\n    if (!this._account) {\n      throw new Error(\"SyncbackTask requires an account\")\n    }\n    if (!this._syncbackRequest) {\n      throw new Error(\"SyncbackTask requires a syncbackRequest\")\n    }\n    this._logger = global.Logger.forAccount(this._account)\n  }\n\n  syncbackRequestObject() {\n    return this._syncbackRequest;\n  }\n\n  inProgressStatusType() {\n    return 'INPROGRESS-RETRYABLE'\n  }\n\n  description() {\n    throw new Error(\"Must return a description\")\n  }\n\n  resource() {\n    throw new Error(\"Must return a resource. Must be one of ['imap', 'smtp']\")\n  }\n\n  affectsImapMessageUIDs() {\n    throw new Error(\"Must implement `affectsImapMessageUIDs`\")\n  }\n\n  stop = () => {\n    // If we can't retry the task, we don't want to interrupt it.\n    if (this._syncbackRequest.status !== \"INPROGRESS-NOTRETRYABLE\") {\n      this._interruptible.interrupt({forceReject: true})\n      Actions.recordUserEvent(\"SyncbackTask Stopped\", {\n        accountId: this._account.id,\n        type: this._syncbackRequest.type,\n      })\n    }\n  }\n\n  async * _run() { // eslint-disable-line\n    throw new Error(\"Must implement a _run method\")\n  }\n\n  async run(db, imapOrSmtp, ctx = {}) {\n    const {timeoutDelay = TIMEOUT_DELAY} = ctx\n    const timeout = setTimeout(this.stop, timeoutDelay)\n    const startTime = Date.now()\n    const response = await this._interruptible.run(() => this._run(db, imapOrSmtp, ctx))\n\n    // Since we've already completed the task, we don't want to fail before\n    // we return the response. Wrap everything else in a try/catch and still\n    // return the response if an error is thrown.\n    try {\n      Actions.recordPerfMetric({\n        action: 'syncback-task-run',\n        accountId: this._account.id,\n        actionTimeMs: Date.now() - startTime,\n        maxValue: 10 * 60 * 1000,\n        type: this._syncbackRequest.type,\n        provider: this._account.provider,\n      })\n      clearTimeout(timeout)\n    } catch (err) {\n      // Don't throw\n    }\n    return response\n  }\n}\n\nexport class SyncbackIMAPTask extends SyncbackTask {\n  resource() {\n    return 'imap'\n  }\n}\n\nexport class SyncbackSMTPTask extends SyncbackTask {\n  resource() {\n    return 'smtp'\n  }\n\n  inProgressStatusType() {\n    return 'INPROGRESS-NOTRETRYABLE'\n  }\n}\n\nexport default SyncbackTask\n"
  },
  {
    "path": "packages/client-sync/src/local-sync-worker/syncback-tasks/unstar-thread.imap.es6",
    "content": "const {Errors: {APIError}} = require('isomorphic-core')\nconst {SyncbackIMAPTask} = require('./syncback-task')\nconst IMAPHelpers = require('../imap-helpers')\n\nclass UnstarThread extends SyncbackIMAPTask {\n  description() {\n    return `UnstarThread`;\n  }\n\n  affectsImapMessageUIDs() {\n    return false\n  }\n\n  async * _run(db, imap) {\n    const {sequelize, Thread} = db\n    const threadId = this.syncbackRequestObject().props.threadId\n    if (!threadId) {\n      throw new APIError('threadId is required')\n    }\n\n    const thread = yield Thread.findById(threadId)\n    if (!thread) {\n      throw new APIError(`Can't find thread`, 404)\n    }\n    const threadMessages = yield thread.getMessages()\n    yield IMAPHelpers.forEachFolderOfThread({\n      db,\n      imap,\n      threadMessages,\n      callback({messageImapUIDs, box}) {\n        return box.delFlags(messageImapUIDs, 'FLAGGED')\n      },\n    })\n    // If IMAP succeeds, save the model updates\n    yield sequelize.transaction(async (transaction) => {\n      await Promise.all(threadMessages.map((m) => m.update({starred: false}, {transaction})))\n      await thread.update({starredCount: 0}, {transaction})\n    })\n  }\n}\nmodule.exports = UnstarThread;\n"
  },
  {
    "path": "packages/client-sync/src/message-processor/detect-thread.js",
    "content": "// straight translated from the Nylas Sync Engine python codebase, see\n// https://github.com/nylas/cloud-core/blob/7db949fec9447b73e2ba9485d8903380414e8223/sync-engine/inbox/util/misc.py#L175-L183\n// for original!\nfunction cleanSubject(subject = \"\") {\n  const regex = new RegExp(/^((re|fw|fwd|aw|wg|undeliverable|undelivered):\\s*)+/ig);\n  return subject.replace(regex, () => \"\");\n}\n\nfunction emptyThread({Thread, accountId}, options = {}) {\n  const t = Thread.build(Object.assign({accountId}, options))\n  t.folders = [];\n  t.labels = [];\n  t.participants = [];\n  return t;\n}\n\nasync function findOrBuildByReferences(db, message) {\n  const {Thread, Reference, Label, Folder} = db;\n\n  let matchingRef = null;\n\n  // If we have a thread that matches the new message, at least one element\n  // of the new message's references will match an existing reference we've\n  // already synced and associated with the correct thread.\n  if (message.headerMessageId) {\n    matchingRef = await Reference.findOne({\n      where: {\n        rfc2822MessageId: message.references,\n      },\n      include: [\n        { model: Thread, include: [{model: Label}, {model: Folder}]},\n      ],\n    });\n  }\n\n  if (matchingRef && !matchingRef.thread) {\n    throw new Error(`Reference not properly cleaned up`)\n  }\n  return matchingRef ? matchingRef.thread : emptyThread(db, {});\n}\n\nasync function findOrBuildByRemoteThreadId(db, remoteThreadId) {\n  const {Thread, Label, Folder} = db;\n  const existing = await Thread.find({\n    where: {remoteThreadId},\n    include: [{model: Label}, {model: Folder}],\n  });\n  return existing || emptyThread(db, {remoteThreadId});\n}\n\nasync function detectThread({db, messageValues}) {\n  if (!(messageValues.labels instanceof Array)) {\n    throw new Error(\"detectThread expects labels to be an inflated array.\");\n  }\n  if (!messageValues.folder) {\n    throw new Error(\"detectThread expects folder value to be present.\");\n  }\n\n  let thread = null;\n  if (messageValues.gThrId) {\n    thread = await findOrBuildByRemoteThreadId(db, messageValues.gThrId)\n  } else {\n    thread = await findOrBuildByReferences(db, messageValues)\n  }\n\n  if (!(thread.labels instanceof Array)) {\n    throw new Error(\"detectThread expects thread.labels to be an inflated array.\");\n  }\n  if (!(thread.folders instanceof Array)) {\n    throw new Error(\"detectThread expects thread.folders to be an inflated array.\");\n  }\n\n  // update the basic properties of the thread\n  thread.accountId = messageValues.accountId;\n\n  // Threads may, locally, have the ID of any message within the thread.\n  // Message IDs are globally unique within an account---but not necessarily\n  // across accounts, due to hashing.\n  if (!thread.id) {\n    thread.id = `t:${messageValues.id}`\n  }\n\n  thread.subject = cleanSubject(messageValues.subject);\n  await thread.updateFromMessages({messages: [messageValues]});\n  return thread;\n}\n\nmodule.exports = detectThread\n"
  },
  {
    "path": "packages/client-sync/src/message-processor/extract-contacts.js",
    "content": "const Sequelize = require('sequelize');\n\nfunction isContactMeaningful(contact) {\n  // some suggestions: http://stackoverflow.com/questions/6317714/apache-camel-mail-to-identify-auto-generated-messages\n  const regex = new RegExp(/^(noreply|no-reply|donotreply|mailer|support|webmaster|news(letter)?@)/ig)\n\n  if (!contact.email) {\n    return false;\n  }\n  if (regex.test(contact.email) || contact.email.length > 60) {\n    return false\n  }\n  return true\n}\n\nasync function extractContacts({db, messageValues, logger = console} = {}) {\n  const {Contact} = db\n  let allContacts = [];\n  ['to', 'from', 'bcc', 'cc'].forEach((field) => {\n    allContacts = allContacts.concat(messageValues[field])\n  })\n\n  const meaningfulContacts = allContacts.filter(c => isContactMeaningful(c));\n  const contactsDataById = new Map()\n  meaningfulContacts.forEach(c => {\n    const id = Contact.hash(c)\n    const cdata = {\n      id,\n      name: c.name,\n      email: c.email,\n      accountId: messageValues.accountId,\n    }\n    contactsDataById.set(id, cdata)\n  })\n\n  const existingContacts = await Contact.findAll({\n    where: {\n      id: Array.from(contactsDataById.keys()),\n    },\n  })\n\n  for (const c of contactsDataById.values()) {\n    const existing = existingContacts.find(({id}) => id === c.id);\n\n    if (!existing) {\n      Contact.create(c).catch(Sequelize.ValidationError, (err) => {\n        if (err.name !== \"SequelizeUniqueConstraintError\") {\n          logger.warn('Unknown error inserting contact', err);\n          throw err;\n        } else {\n          // Another message with the same contact was processing concurrently,\n          // and beat us to inserting. Since contacts are never deleted within\n          // an account, we can safely assume that we can perform an update\n          // instead.\n          Contact.find({where: {id: c.id}}).then(\n            (row) => { row.update(c) });\n        }\n      });\n    } else {\n      existing.update(c);\n    }\n  }\n}\n\nmodule.exports = extractContacts\n"
  },
  {
    "path": "packages/client-sync/src/message-processor/extract-files.js",
    "content": "const mimelib = require('mimelib')\n\nfunction collectFilesFromStruct({db, messageValues, struct, fileIds = new Set()}) {\n  const {File} = db;\n  let collected = [];\n\n  for (const part of struct) {\n    if (part.constructor === Array) {\n      collected = collected.concat(collectFilesFromStruct({db, messageValues, struct: part, fileIds}));\n    } else {\n      const disposition = part.disposition || {}\n      const filename = mimelib.decodeMimeWord((disposition.params || {}).filename);\n\n      // Note that the contentId is stored in part.id, while the MIME part id\n      // is stored in part.partID\n      const match = /^<(.*)>$/.exec(part.id) // extract id from <id>\n      const contentId = match ? match[1] : part.id;\n\n      // Check if the part is an attachment. If it's inline, we also need\n      // to ensure that there is a filename and contentId because some clients\n      // use \"inline\" for text in the body.\n      const isAttachment = /(attachment)/gi.test(disposition.type) ||\n        (/(inline)/gi.test(disposition.type) && filename && contentId);\n\n      if (!isAttachment) continue\n\n      const partId = part.partID\n      const fileId = `${messageValues.id}-${partId}-${part.size}`\n      if (!fileIds.has(fileId)) {\n        collected.push(File.build({\n          id: fileId,\n          size: part.size,\n          partId: partId,\n          charset: part.params ? part.params.charset : null,\n          encoding: part.encoding,\n          filename: filename,\n          messageId: messageValues.id,\n          accountId: messageValues.accountId,\n          contentType: `${part.type}/${part.subtype}`,\n          contentId,\n        }));\n        fileIds.add(fileId)\n      }\n    }\n  }\n\n  return collected;\n}\n\nasync function extractFiles({db, messageValues, struct}) {\n  const files = collectFilesFromStruct({db, messageValues, struct});\n  if (files.length > 0) {\n    for (const file of files) {\n      await file.save()\n    }\n  }\n  return Promise.resolve(files)\n}\n\nmodule.exports = extractFiles\n"
  },
  {
    "path": "packages/client-sync/src/message-processor/index.js",
    "content": "const _ = require('underscore')\nconst os = require('os');\nconst fs = require('fs');\nconst path = require('path')\nconst mkdirp = require('mkdirp');\nconst detectThread = require('./detect-thread');\nconst extractFiles = require('./extract-files');\nconst extractContacts = require('./extract-contacts');\nconst {MessageUtils, TrackingUtils} = require('isomorphic-core');\nconst LocalDatabaseConnector = require('../shared/local-database-connector');\nconst {AccountStore, BatteryStatusManager} = require('nylas-exports');\nconst SyncActivity = require('../shared/sync-activity').default;\n\nconst MAX_QUEUE_LENGTH = 500\n// These CPU limits only apply when we're actually throttling. We don't\n// throttle for new mail, the first 500 threads, or for specific sets of\n// UIDs (e.g. during search for unsynced UIDs). Thus, we're essentially only\n// throttling when syncing the historical archive.\nconst MAX_CPU_USE_ON_AC = 0.5;\nconst MAX_CPU_USE_ON_BATTERY = 0.05;\nconst MAX_CHUNK_SIZE = 1;\n\nclass MessageProcessor {\n\n  constructor() {\n    // The queue is a chain of Promises\n    this._queue = Promise.resolve()\n    this._queueLength = 0\n    this._currentChunkSize = 0\n    this._currentChunkStart = Date.now();\n  }\n\n  queueLength() {\n    return this._queueLength\n  }\n\n  queueIsFull() {\n    return this._queueLength >= MAX_QUEUE_LENGTH\n  }\n\n  _maxCPUForProcessing() {\n    if (BatteryStatusManager.isBatteryCharging()) {\n      return MAX_CPU_USE_ON_AC;\n    }\n    return MAX_CPU_USE_ON_BATTERY;\n  }\n\n  _computeThrottlingTimeout() {\n    const timeSliceMs = Date.now() - this._currentChunkStart;\n    const maxCPU = this._maxCPUForProcessing();\n    return (timeSliceMs * (1.0 / maxCPU)) - timeSliceMs;\n  }\n\n  /**\n   * @returns Promise that resolves when message has been processed. This\n   * promise will never reject. If message processing fails, we will register\n   * the failure in the folder syncState.\n   */\n  queueMessageForProcessing({accountId, folderId, imapMessage, struct, desiredParts, throttle = true} = {}) {\n    return new Promise(async (resolve) => {\n      let logger;\n      let folder;\n      try {\n        const accountDb = await LocalDatabaseConnector.forShared()\n        const account = await accountDb.Account.findById(accountId)\n        const db = await LocalDatabaseConnector.forAccount(accountId);\n        const {Folder} = db\n        folder = await Folder.findById(folderId)\n        logger = global.Logger.forAccount(account)\n\n        this._queueLength++\n        this._queue = this._queue.then(async () => {\n          if (this._currentChunkSize === 0) {\n            this._currentChunkStart = Date.now();\n          }\n          this._currentChunkSize++;\n\n          await this._processMessage({db, accountId, folder, imapMessage, struct, desiredParts, logger})\n          this._queueLength--\n\n          // Throttle message processing to meter cpu usage\n          if (this._currentChunkSize === MAX_CHUNK_SIZE) {\n            if (throttle) {\n              await new Promise(r => setTimeout(r, this._computeThrottlingTimeout()));\n            }\n            this._currentChunkSize = 0;\n          }\n\n          // To save memory, we reset the Promise chain if the queue reaches a\n          // length of 0, otherwise we will continue referencing the entire chain\n          // of promises that came before\n          if (this._queueLength === 0) {\n            this._queue = Promise.resolve()\n          }\n          resolve();\n        });\n      } catch (err) {\n        if (logger && folder) {\n          await this._onError({imapMessage, desiredParts, folder, err, logger});\n        } else {\n          NylasEnv.reportError(err);\n        }\n        resolve();\n      }\n    })\n  }\n\n  async _processMessage({db, accountId, folder, imapMessage, struct, desiredParts, logger}) {\n    try {\n      const {Message, Folder, Label} = db;\n      const messageValues = await MessageUtils.parseFromImap(imapMessage, desiredParts, {\n        db,\n        folder,\n        accountId,\n      });\n\n      /**\n       * When we send messages, Gmail will automatically stuff messages in\n       * the sent folder that contain open & link tracking data. While we\n       * will eventually clean that up, if the send takes a while to\n       * multiple people (due to attachments) it's possible that we'll\n       * sync that recently sent message. If this happens, we want to\n       * ensure that no open and link tracking data is included.\n       */\n      if (AccountStore.isMyEmail(messageValues.from.map(f => f.email))) {\n        messageValues.body = TrackingUtils.stripTrackingLinksFromBody(messageValues.body)\n      }\n\n      const existingMessage = await Message.findById(messageValues.id, {\n        include: [{model: Folder, as: 'folder'}, {model: Label, as: 'labels'}],\n      });\n      let processedMessage;\n      if (existingMessage) {\n        // TODO: optimize to not do a full message parse for existing messages\n        processedMessage = await this._processExistingMessage({\n          logger,\n          struct,\n          messageValues,\n          existingMessage,\n        })\n      } else {\n        processedMessage = await this._processNewMessage({\n          logger,\n          struct,\n          messageValues,\n        })\n      }\n\n      // Inflate the serialized oldestProcessedDate value, if it exists\n      let oldestProcessedDate;\n      if (folder.syncState && folder.syncState.oldestProcessedDate) {\n        oldestProcessedDate = new Date(folder.syncState.oldestProcessedDate);\n      }\n      const justProcessedDate = messageValues.date ? new Date(messageValues.date) : new Date()\n\n      // Update the oldestProcessedDate if:\n      //   a) justProcessedDate is after the year 1980. We don't want to base this\n      //      off of messages with borked 1970 dates.\n      // AND\n      //   b) i) We haven't set oldestProcessedDate yet\n      //     OR\n      //      ii) justProcessedDate is before oldestProcessedDate and in a different\n      //          month. (We only use this to update the sync status in Nylas Mail,\n      //          which uses month precision. Updating a folder's syncState triggers\n      //          many re-renders in Nylas Mail, so we only do it as necessary.)\n      if (justProcessedDate > new Date(\"1980\") && (\n            !oldestProcessedDate || (\n              (justProcessedDate.getMonth() !== oldestProcessedDate.getMonth() ||\n                justProcessedDate.getFullYear() !== oldestProcessedDate.getFullYear()) &&\n              justProcessedDate < oldestProcessedDate))) {\n        await folder.updateSyncState({oldestProcessedDate: justProcessedDate})\n      }\n\n      const activity = `🔃 ✉️ (${folder.name}) \"${messageValues.subject}\" - ${messageValues.date}`\n      logger.log(activity)\n      SyncActivity.reportSyncActivity(accountId, activity)\n      return processedMessage\n    } catch (err) {\n      await this._onError({imapMessage, desiredParts, folder, err, logger});\n      return null\n    }\n  }\n\n  async _onError({imapMessage, desiredParts, folder, err, logger}) {\n    logger.error(`MessageProcessor: Could not build message`, {\n      err,\n      imapMessage,\n      desiredParts,\n    })\n    const fingerprint = [\"{{ default }}\", \"message processor\", err.message];\n    NylasEnv.reportError(err, {fingerprint,\n      rateLimit: {\n        ratePerHour: 30,\n        key: `MessageProcessorError:${err.message}`,\n      },\n    })\n\n    // Keep track of uids we failed to fetch\n    const {failedUIDs = []} = folder.syncState\n    const {uid} = imapMessage.attributes\n    if (uid) {\n      await folder.updateSyncState({failedUIDs: _.uniq(failedUIDs.concat([uid]))})\n    }\n\n    // Save parse errors for future debugging\n    if (process.env.NYLAS_DEBUG) {\n      const outJSON = JSON.stringify({imapMessage, desiredParts, result: {}});\n      const outDir = path.join(os.tmpdir(), \"k2-parse-errors\", folder.name)\n      const outFile = path.join(outDir, imapMessage.attributes.uid.toString());\n      mkdirp.sync(outDir);\n      fs.writeFileSync(outFile, outJSON);\n    }\n  }\n\n  // Replaces [\"<rfc2822messageid>\", ...] with [[object Reference], ...]\n  // Creates references that do not yet exist, and adds the correct\n  // associations as well\n  async _addReferences(db, message, thread, references) {\n    const {Reference} = db;\n\n    let existingReferences = [];\n    if (references.length > 0) {\n      existingReferences = await Reference.findAll({\n        where: {\n          rfc2822MessageId: references,\n        },\n      });\n    }\n\n    const refByMessageId = {};\n    for (const ref of existingReferences) {\n      refByMessageId[ref.rfc2822MessageId] = ref;\n    }\n    for (const mid of references) {\n      if (!refByMessageId[mid]) {\n        refByMessageId[mid] = await Reference.create({rfc2822MessageId: mid, threadId: thread.id});\n      }\n    }\n\n    const referencesInstances = references.map(mid => refByMessageId[mid]);\n    await message.addReferences(referencesInstances);\n    message.referencesOrder = referencesInstances.map(ref => ref.id);\n    await thread.addReferences(referencesInstances);\n  }\n\n  async _processNewMessage({messageValues, struct, logger = console} = {}) {\n    const {accountId} = messageValues;\n    const db = await LocalDatabaseConnector.forAccount(accountId);\n    const {Message} = db\n\n    const thread = await detectThread({db, messageValues});\n    messageValues.threadId = thread.id;\n    // The way that sequelize initializes objects doesn't guarantee that the\n    // object will have a value for `id` before initializing the `body` field\n    // (which we now depend on). By using `build` instead of `create`, we can\n    // initialize an object with just the `id` field and then use `update` to\n    // initialize the remaining fields and save the object to the database.\n    const createdMessage = Message.build({id: messageValues.id});\n    await createdMessage.update(messageValues);\n\n    if (messageValues.labels) {\n      await createdMessage.addLabels(messageValues.labels)\n      // Note that the labels aren't officially associated until save() is called later\n    }\n\n    await this._addReferences(db, createdMessage, thread, messageValues.references);\n\n    // TODO: need to delete dangling references somewhere (maybe at the\n    // end of the sync loop?)\n\n    const files = await extractFiles({db, messageValues, struct});\n    // Don't count inline images (files with contentIds) as attachments\n    if (files.some(f => !f.contentId) && !thread.hasAttachments) {\n      thread.hasAttachments = true;\n      await thread.save();\n    }\n    await extractContacts({db, messageValues, logger});\n\n    createdMessage.isProcessed = true;\n    await createdMessage.save()\n    return createdMessage\n  }\n\n  /**\n   * When we send a message we store an incomplete copy in the local\n   * database while we wait for the sync loop to discover the actually\n   * delivered one. We store this to keep track of our delivered state and\n   * to ensure it's in the sent folder.\n   *\n   * It'll have the basic ID, but no thread, labels, etc.\n   *\n   * We also get already processed messages because they may have had their\n   * folders or labels changed or had some other property updated with them,\n   * or because we interrupted the sync loop before the message was fully\n   * processed.\n   */\n  async _processExistingMessage({existingMessage, messageValues, struct} = {}) {\n    const {accountId} = messageValues;\n    const db = await LocalDatabaseConnector.forAccount(accountId);\n\n    /**\n     * There should never be a reason to update the body of a message\n     * already in the database.\n     *\n     * When we use link/open tracking on Gmail, we optimistically create a\n     * Message whose body is stripped of tracking pixels (so you don't\n     * self trigger). Since it takes time to delete the old draft on Gmail\n     * & restuff, it's possible to sync a message with a non-stripped body\n     * (which would cause you to self-trigger)). This prevents this from\n     * happening.\n     */\n    const newMessageWithoutBody = _.clone(messageValues)\n    delete newMessageWithoutBody.body;\n    await existingMessage.update(newMessageWithoutBody);\n    if (messageValues.labels && messageValues.labels.length > 0) {\n      await existingMessage.setLabels(messageValues.labels)\n    }\n\n    let thread = await existingMessage.getThread({\n      include: [{model: db.Folder, as: 'folders'}, {model: db.Label, as: 'labels'}],\n    });\n    if (!existingMessage.isProcessed) {\n      if (!thread) {\n        thread = await detectThread({db, messageValues});\n        existingMessage.threadId = thread.id;\n      } else {\n        await thread.updateFromMessages({db, messages: [existingMessage]})\n      }\n      await this._addReferences(db, existingMessage, thread, messageValues.references);\n      const files = await extractFiles({db, messageValues: existingMessage, struct});\n      // Don't count inline images (files with contentIds) as attachments\n      if (files.some(f => !f.contentId) && !thread.hasAttachments) {\n        thread.hasAttachments = true;\n        await thread.save();\n      }\n      await extractContacts({db, messageValues: existingMessage});\n      existingMessage.isProcessed = true;\n    } else {\n      if (!thread) {\n        throw new Error(`Existing processed message ${existingMessage.id} doesn't have thread`)\n      }\n    }\n\n    await existingMessage.save();\n    await thread.updateLabelsAndFolders();\n    return existingMessage\n  }\n}\n\nmodule.exports = new MessageProcessor()\n"
  },
  {
    "path": "packages/client-sync/src/models/contact.js",
    "content": "const crypto = require('crypto')\n\n/**\n * NOTE: SQLITE creates an index on the `primaryKey` (the ID) for you.\n * This \"Auto Index\" is called `sqlite_autoindex_contacts_1`.\n *\n * If you run `EXPLAIN QUERY PLAN SELECT * FROM contacts WHERE id=1` you\n * get:\n *   SEARCH TABLE contacts USING INDEX sqlite_autoindex_contacts_1\n * (id=?)\n */\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('contact', {\n    id: {type: Sequelize.STRING(65), primaryKey: true},\n    accountId: { type: Sequelize.STRING, allowNull: false },\n    version: Sequelize.INTEGER,\n    name: Sequelize.STRING,\n    email: Sequelize.STRING,\n  }, {\n    classMethods: {\n      hash({email}) {\n        return crypto.createHash('sha256').update(email, 'utf8').digest('hex');\n      },\n    },\n    instanceMethods: {\n      toJSON() {\n        return {\n          id: `${this.id}`,\n          account_id: this.accountId,\n          object: 'contact',\n          email: this.email,\n          name: this.name,\n        }\n      },\n    },\n  })\n}\n"
  },
  {
    "path": "packages/client-sync/src/models/file.js",
    "content": "const base64 = require('base64-stream');\nconst {\n  ExponentialBackoffScheduler,\n  IMAPErrors,\n  IMAPConnectionPool,\n} = require('isomorphic-core')\nconst {QuotedPrintableStreamDecoder} = require('../shared/stream-decoders')\nconst {Actions} = require('nylas-exports')\n\nconst MAX_IMAP_TIMEOUT_ERRORS = 5;\n\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('file', {\n    id: { type: Sequelize.STRING(500), primaryKey: true },\n    size: Sequelize.INTEGER,\n    partId: Sequelize.STRING,\n    version: Sequelize.INTEGER,\n    charset: Sequelize.STRING,\n    encoding: Sequelize.STRING,\n    filename: Sequelize.STRING(500),\n    messageId: { type: Sequelize.STRING, allowNull: false },\n    accountId: { type: Sequelize.STRING, allowNull: false },\n    contentType: Sequelize.STRING(500),\n    contentId: Sequelize.STRING(500),\n  }, {\n    indexes: [\n      {fields: ['messageId']},\n    ],\n    classMethods: {\n      associate: ({File, Message}) => {\n        File.belongsTo(Message)\n      },\n    },\n    instanceMethods: {\n      async fetch({account, db, logger}) {\n        const message = await this.getMessage()\n        const folder = await message.getFolder()\n\n        let numTimeoutErrors = 0;\n        let result = null;\n\n        const onConnected = async ([connection], done) => {\n          const imapBox = await connection.openBox(folder.name)\n          const stream = await imapBox.fetchMessageStream(message.folderImapUID, {\n            fetchOptions: {\n              bodies: this.partId ? [this.partId] : [],\n              struct: true,\n            },\n            onFetchComplete() {\n              done();\n            },\n          });\n\n          if (!stream) {\n            throw new Error(`Unable to fetch binary data for File ${this.id}`)\n          }\n\n          if (/quoted-printable/i.test(this.encoding)) {\n            result = stream.pipe(new QuotedPrintableStreamDecoder({charset: this.charset}));\n            return true;\n          } else if (/base64/i.test(this.encoding)) {\n            result = stream.pipe(base64.decode());\n            return true;\n          }\n\n          // If there is no encoding, or the encoding is something like\n          // '7bit', '8bit', or 'binary', just return the raw stream. This\n          // stream will be written directly to disk. It's then up to the\n          // user's computer to decide how to interpret the bytes we've\n          // dumped to disk.\n          result = stream;\n          return true;\n        };\n\n        const timeoutScheduler = new ExponentialBackoffScheduler({\n          baseDelay: 15 * 1000,\n          maxDelay: 5 * 60 * 1000,\n        });\n\n        const onTimeout = () => {\n          numTimeoutErrors += 1;\n          Actions.recordUserEvent('Timeout error downloading file', {\n            accountId: account.id,\n            provider: account.provider,\n            socketTimeout: timeoutScheduler.currentDelay(),\n            numTimeoutErrors,\n          });\n          timeoutScheduler.nextDelay();\n        };\n\n        while (numTimeoutErrors < MAX_IMAP_TIMEOUT_ERRORS) {\n          try {\n            await IMAPConnectionPool.withConnectionsForAccount(account, {\n              desiredCount: 1,\n              logger,\n              socketTimeout: timeoutScheduler.currentDelay(),\n              onConnected,\n            });\n            break;\n          } catch (err) {\n            if (err instanceof IMAPErrors.IMAPConnectionTimeoutError) {\n              onTimeout();\n              continue;\n            }\n            throw err;\n          }\n        }\n        return result;\n      },\n\n      toJSON() {\n        return {\n          id: this.id,\n          size: this.size,\n          object: 'file',\n          part_id: this.partId,\n          encoding: this.encoding,\n          filename: this.filename,\n          message_id: this.messageId,\n          account_id: this.accountId,\n          content_type: this.contentType,\n          content_id: this.contentId,\n        };\n      },\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/folder.es6",
    "content": "import _ from 'underscore'\nimport crypto from 'crypto'\nimport {DatabaseTypes} from 'isomorphic-core'\nimport {formatImapPath} from '../shared/imap-paths-utils'\n\nconst {JSONColumn} = DatabaseTypes\n\nexport default (sequelize, Sequelize) => {\n  return sequelize.define('folder', {\n    id: { type: Sequelize.STRING(65), primaryKey: true },\n    accountId: { type: Sequelize.STRING, allowNull: false },\n    version: Sequelize.INTEGER,\n    name: Sequelize.STRING,\n    role: Sequelize.STRING,\n    /**\n     * Sync state has the following shape, and it indicates how much of the\n     * folder we've synced and what's next for syncing:\n     *\n     * {\n     *   // Lowest (oldest) IMAP uid we've fetched in folder\n     *   fetchedmin,\n     *\n     *   // Highest (newest) IMAP uid we've fetched in folder\n     *   fetchedmax,\n     *\n     *   // Highest (most recent) uid in the folder. If this changes, it means\n     *   // there is new mail we haven't synced\n     *   uidnext,\n     *\n     *   // Flag provided by IMAP server to indicate if we need to indicate if\n     *   // we need resync whole folder\n     *   uidvalidity,\n     *\n     *   // Keeps track of the last uid we've scanned for attribtue changes when\n     *   // the server doesn't support CONDSTORE\n     *   attributeFetchedMax\n     *\n     *   // Timestamp when we last scanned attribute changes inside this folder\n     *   // This is only applicable when the server doesn't support CONDSTORE\n     *   lastAttributeScanTime,\n     *\n     *   // UIDs that failed to be fetched\n     *   failedUIDs,\n     * }\n    */\n    syncState: JSONColumn('syncState'),\n  }, {\n    classMethods: {\n      associate({Folder, Message, Thread, ThreadFolder}) {\n        Folder.hasMany(Message)\n        Folder.belongsToMany(Thread, {through: ThreadFolder})\n      },\n\n      hash({boxName, accountId}) {\n        return crypto.createHash('sha256').update(`${accountId}${boxName}`, 'utf8').digest('hex')\n      },\n    },\n    instanceMethods: {\n      isSyncComplete() {\n        if (!this.syncState) { return true }\n        return (\n          this.syncState.fetchedmin !== null &&\n          this.syncState.minUID !== null &&\n          this.syncState.fetchedmax !== null &&\n          (this.syncState.fetchedmin <= this.syncState.minUID) &&\n          (this.syncState.fetchedmax >= this.syncState.uidnext)\n        )\n      },\n\n      async updateSyncState(nextSyncState = {}, {transaction} = {}) {\n        if (_.isMatch(this.syncState, nextSyncState)) {\n          return Promise.resolve();\n        }\n        await this.reload({transaction}); // Fetch any recent syncState updates\n        this.syncState = Object.assign(this.syncState, nextSyncState);\n        return this.save({transaction});\n      },\n\n      syncProgress() {\n        if (!this.syncState) {\n          return {\n            approxPercentComplete: 0,\n            approxTotal: 0,\n            oldestProcessedDate: new Date(),\n          }\n        }\n        const {fetchedmax, fetchedmin, uidnext, minUID, oldestProcessedDate} = this.syncState;\n        return {\n          // based on % of uid space scanned, but space may be sparse\n          approxPercentComplete: (+fetchedmax - +fetchedmin + 1) /\n                                 (uidnext - Math.min(minUID, fetchedmin) + 1),\n          approxTotal: uidnext,\n          oldestProcessedDate: oldestProcessedDate,\n        }\n      },\n\n      toJSON() {\n        return {\n          id: `${this.id}`,\n          account_id: this.accountId,\n          object: 'folder',\n          name: this.role,\n          display_name: formatImapPath(this.name),\n          imap_name: this.name,\n          sync_progress: this.syncProgress(),\n          // intentionally overwrite any sync states stored in edgehill.db,\n          // since it may contain long arrays and cause perf degredation\n          // when serialized repeatedly\n          sync_state: null,\n        };\n      },\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/label.js",
    "content": "const crypto = require('crypto')\nconst {formatImapPath} = require('../shared/imap-paths-utils');\n\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('label', {\n    id: { type: Sequelize.STRING(65), primaryKey: true },\n    accountId: { type: Sequelize.STRING, allowNull: false },\n    version: Sequelize.INTEGER,\n    name: Sequelize.STRING,\n    role: Sequelize.STRING,\n  }, {\n    classMethods: {\n      associate({Label, Message, MessageLabel, Thread, ThreadLabel}) {\n        Label.belongsToMany(Message, {through: MessageLabel})\n        Label.belongsToMany(Thread, {through: ThreadLabel})\n      },\n\n      findXGMLabels(xGmLabels, {preloadedLabels} = {}) {\n        if (!xGmLabels) {\n          return Promise.resolve();\n        }\n        const labelNames = xGmLabels.filter(l => l[0] !== '\\\\')\n        const labelRoles = xGmLabels.filter(l => l[0] === '\\\\').map(l => l.substr(1).toLowerCase())\n\n        if (preloadedLabels) {\n          return Promise.resolve(\n            preloadedLabels.filter(l => labelNames.includes(l.name) || labelRoles.includes(l.role))\n          );\n        }\n        return this.findAll({\n          where: sequelize.or({name: labelNames}, {role: labelRoles}),\n        })\n      },\n\n      hash({boxName, accountId}) {\n        return crypto.createHash('sha256').update(`${accountId}${boxName}`, 'utf8').digest('hex')\n      },\n    },\n    instanceMethods: {\n      imapLabelIdentifier() {\n        if (this.role) {\n          return `\\\\${this.role[0].toUpperCase()}${this.role.slice(1)}`\n        }\n        return this.name;\n      },\n\n      toJSON() {\n        return {\n          id: `${this.id}`,\n          account_id: this.accountId,\n          object: 'label',\n          name: this.role,\n          display_name: formatImapPath(this.name),\n          imap_name: this.name,\n        };\n      },\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/message.js",
    "content": "const crypto = require('crypto')\nconst {\n  ExponentialBackoffScheduler,\n  IMAPErrors,\n  IMAPConnectionPool,\n  MessageBodyUtils,\n} = require('isomorphic-core')\nconst {DatabaseTypes: {JSONArrayColumn}} = require('isomorphic-core');\nconst {Errors: {APIError}} = require('isomorphic-core')\nconst {Actions} = require('nylas-exports')\n\nconst MAX_IMAP_TIMEOUT_ERRORS = 5;\n\nfunction validateRecipientsPresent(message) {\n  if (message.getRecipients().length === 0) {\n    throw new APIError(`No recipients specified`, 400);\n  }\n}\n\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('message', {\n    id: { type: Sequelize.STRING(65), primaryKey: true },\n    accountId: { type: Sequelize.STRING, allowNull: false },\n    version: Sequelize.INTEGER,\n    headerMessageId: { type: Sequelize.STRING, allowNull: true },\n    gMsgId: { type: Sequelize.STRING, allowNull: true },\n    gThrId: { type: Sequelize.STRING, allowNull: true },\n    body: {\n      type: Sequelize.TEXT,\n      get: function getBody() {\n        const val = this.getDataValue('body');\n        const result = MessageBodyUtils.tryReadBody(val);\n        if (result) {\n          return result;\n        }\n        return val;\n      },\n      set: function setBody(val) {\n        this.setDataValue('body', MessageBodyUtils.writeBody({\n          msgId: this.id,\n          body: val,\n        }));\n      },\n    },\n    subject: Sequelize.STRING(500),\n    snippet: Sequelize.STRING(255),\n    date: Sequelize.DATE,\n    // TODO: We do not currently sync drafts with the remote. When we add\n    // this feature, we need to be careful because this breaks the assumption\n    // that messages, modulo their flags and folders/labels, are immutable.\n    // Particularly, we will need to implement logic to make sure snippets\n    // stay in sync with the current message body.\n    isDraft: Sequelize.BOOLEAN,\n    isSent: Sequelize.BOOLEAN,\n    isSending: Sequelize.BOOLEAN, // Currently unused, left for potential future use\n    isProcessed: { type: Sequelize.BOOLEAN, defaultValue: false },\n    unread: Sequelize.BOOLEAN,\n    starred: Sequelize.BOOLEAN,\n    processed: Sequelize.INTEGER,\n    to: JSONArrayColumn('to'),\n    from: JSONArrayColumn('from', {\n      allowNull: true,\n    }),\n    cc: JSONArrayColumn('cc'),\n    bcc: JSONArrayColumn('bcc'),\n    replyTo: JSONArrayColumn('replyTo', {\n      allowNull: true,\n    }),\n    folderImapUID: { type: Sequelize.STRING, allowNull: true},\n    folderImapXGMLabels: { type: Sequelize.TEXT, allowNull: true},\n    // Only used for reconstructing In-Reply-To/References when\n    // placing newly sent messages in sent folder for generic IMAP/multi-send\n    inReplyToLocalMessageId: { type: Sequelize.STRING(65), allowNull: true },\n    // an array of IDs to Reference objects, specifying which order they\n    // appeared on the original message (so we don't muck up the order when\n    // sending replies, which could break other mail clients)\n    referencesOrder: JSONArrayColumn('referencesOrder', { allowNull: true }),\n    uploads: JSONArrayColumn('uploads', {\n      validate: {\n        uploadStructure(stringifiedArr) {\n          const arr = JSON.parse(stringifiedArr);\n          const requiredKeys = ['filename', 'targetPath', 'id']\n          arr.forEach((upload) => {\n            requiredKeys.forEach((key) => {\n              if (!Object.prototype.hasOwnProperty.call(upload, key)) {\n                throw new Error(`Upload must have '${key}' key.`)\n              }\n            })\n          })\n        },\n      },\n    }),\n  }, {\n    indexes: [\n      {fields: ['folderId']},\n      {fields: ['threadId']},\n      {fields: ['gMsgId']}, // Use in `searchThreads`\n      // TODO: when we add 2-way draft syncing, we're going to need this index\n      // {fields: ['isDraft']},\n      {fields: ['folderImapUID']}, // Use in `searchThreads`\n    ],\n    classMethods: {\n      associate({Message, Folder, Label, File, Thread, MessageLabel, Reference, MessageReference}) {\n        Message.belongsTo(Thread)\n        Message.belongsTo(Folder)\n        Message.belongsToMany(Label, {through: MessageLabel})\n        Message.hasMany(File, {onDelete: 'cascade', hooks: true})\n        Message.belongsToMany(Reference, {\n          through: MessageReference,\n          as: 'references',\n        })\n      },\n\n      hash({from = [], to = [], cc = [], bcc = [], date = '', subject = '', headerMessageId = ''} = {}) {\n        const emails = from.concat(to, cc, bcc)\n        .map(participant => participant.email)\n        .sort();\n        const participants = emails.join('')\n        const data = `${date}-${subject}-${participants}-${headerMessageId}`;\n        return crypto.createHash('sha256').update(data, 'utf8').digest('hex');\n      },\n\n      dateString(strOrDate) {\n        let date = strOrDate;\n        if (typeof strOrDate === 'string') {\n          date = new Date(Date.parse(strOrDate));\n        }\n        return date.toUTCString().replace(/GMT/, '+0000')\n      },\n\n      buildHeaderMessageId(id) {\n        return `<${id}@nylas-mail.nylas.com>`\n      },\n\n      requiredAssociationsForJSON({Folder, Label, File}) {\n        return [\n          {model: Folder},\n          {model: Label},\n          {model: File},\n        ]\n      },\n    },\n    instanceMethods: {\n      getRecipients() {\n        const {to, cc, bcc} = this;\n        return [].concat(to, cc, bcc);\n      },\n\n      async setLabelsFromXGM(xGmLabels, {Label, preloadedLabels} = {}) {\n        this.folderImapXGMLabels = JSON.stringify(xGmLabels);\n        const labels = await Label.findXGMLabels(xGmLabels, {preloadedLabels})\n        return this.setLabels(labels);\n      },\n\n      setIsSent(val) {\n        if (val) {\n          this.isDraft = false\n        }\n        this.isSent = val\n      },\n\n      async fetchRaw({account, logger}) {\n        const folder = await this.getFolder();\n        let numTimeoutErrors = 0;\n        let result = null;\n\n        const onConnected = async ([connection]) => {\n          const imapBox = await connection.openBox(folder.name);\n          const message = await imapBox.fetchMessage(this.folderImapUID);\n          if (!message) {\n            throw new Error(`Unable to fetch raw message for Message ${this.id}`);\n          }\n          // TODO: this can mangle the raw body of the email because it\n          // does not respect the charset specified in the headers, which\n          // MUST be decoded before you can figure out how to interpret the\n          // body MIME bytes\n          result = `${message.headers}${message.parts.TEXT}`;\n        };\n\n        const timeoutScheduler = new ExponentialBackoffScheduler({\n          baseDelay: 15 * 1000,\n          maxDelay: 5 * 60 * 1000,\n        });\n\n        const onTimeout = () => {\n          numTimeoutErrors += 1;\n          Actions.recordUserEvent('Timeout error downloading raw message', {\n            accountId: account.id,\n            provider: account.provider,\n            socketTimeout: timeoutScheduler.currentDelay(),\n            numTimeoutErrors,\n          });\n          timeoutScheduler.nextDelay();\n        };\n\n        while (numTimeoutErrors < MAX_IMAP_TIMEOUT_ERRORS) {\n          try {\n            await IMAPConnectionPool.withConnectionsForAccount(account, {\n              desiredCount: 1,\n              logger,\n              socketTimeout: timeoutScheduler.currentDelay(),\n              onConnected,\n            });\n            break;\n          } catch (err) {\n            if (err instanceof IMAPErrors.IMAPConnectionTimeoutError) {\n              onTimeout();\n              continue;\n            }\n            throw err;\n          }\n        }\n\n        return result;\n      },\n\n      toJSON() {\n        if (this.folderId && !this.folder) {\n          throw new Error(\"Message.toJSON called on a message where folder was not eagerly loaded.\")\n        }\n\n        // When we request messages as a sub-object of a thread, we only\n        // request the `id` field from the database. We still toJSON the\n        // Message though and need to protect `this.date` from null\n        // errors.\n        // Folders and labels can be null if the message is sending!\n        return {\n          id: this.id,\n          account_id: this.accountId,\n          object: this.isDraft ? 'draft' : 'message',\n          draft: this.isDraft,\n          body: this.body,\n          subject: this.subject,\n          snippet: this.snippet,\n          to: this.to,\n          from: this.from,\n          cc: this.cc,\n          bcc: this.bcc,\n          reply_to: this.replyTo,\n          date: this.date ? this.date.getTime() / 1000.0 : null,\n          unread: this.unread,\n          starred: this.starred,\n          files: this.files ? this.files.map(f => f.toJSON()) : [],\n          folder: this.folder ? this.folder.toJSON() : null,\n          labels: this.labels ? this.labels.map(l => l.toJSON()) : [],\n          imap_uid: this.folderImapUID,\n          thread_id: this.threadId,\n          message_id_header: this.headerMessageId,\n        };\n      },\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/messageLabel.js",
    "content": "/**\n * CREATE TABLE IF NOT EXISTS `messageLabels` (\n *   `createdAt` DATETIME NOT NULL,\n *   `updatedAt` DATETIME NOT NULL,\n *   `labelId` VARCHAR(65) NOT NULL REFERENCES `labels` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n *   `messageId` VARCHAR(65) NOT NULL REFERENCES `messages` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n *   PRIMARY KEY (`labelId`, `messageId`)\n * );\n *\n * sqlite_autoindex_messageLabels_1 (labelId, messageId)\n */\nmodule.exports = (sequelize) => {\n  return sequelize.define('messageLabel', {\n  }, {\n    indexes: [\n      // NOTE: When SQLite sets up this table, it creates an auto index in\n      // the order ['labelId', 'messageId']. This is the correct index we\n      // need for queries requesting Messages for a certain Label.\n      //\n      // We need to create one more index to allow queries from the\n      // reverse direction requesting Labels for a certain Message.\n      {fields: ['messageId', 'labelId']},\n    ],\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/messageReference.js",
    "content": "module.exports = (sequelize) => {\n  return sequelize.define('messageReference', {\n  }, {\n    indexes: [\n      // NOTE: When SQLite sets up this table, it creates an auto index in\n      // the order ['messageId', 'referenceId']. This is the correct index we\n      // need for queries requesting References for a certain Message.\n      //\n      // We need to create one more index to allow queries from the\n      // reverse direction requesting Messages for a certain Reference.\n      {fields: ['referenceId', 'messageId']},\n    ],\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/reference.js",
    "content": "// This model exists solely to store RFC2822 Message-IDs from the Message-Id,\n// In-Reply-To, and References message headers in an indexable fashion for\n// threading messages in providers that do not provide a thread ID from the\n// server.\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('reference', {\n    // We need to specify autoincrement: true here otherwise newly\n    // created models come back with the primary key set on an attribute\n    // named `null`, rather than `id`, and associations created with them\n    // will fail constraints because the referenceId will be NULL. WTF.\n    // See https://github.com/sequelize/sequelize/issues/1060\n    id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },\n    rfc2822MessageId: { type: Sequelize.STRING, unique: true },\n  }, {\n    indexes: [\n      { fields: ['rfc2822MessageId'] },\n      { fields: ['threadId'] },\n    ],\n    classMethods: {\n      associate: ({Thread, Reference, Message, MessageReference}) => {\n        Reference.belongsTo(Thread)\n        Reference.belongsToMany(Message, { through: MessageReference })\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "packages/client-sync/src/models/syncbackRequest.js",
    "content": "const {DatabaseTypes: {JSONColumn}} = require('isomorphic-core');\n\n/**\n * CREATE TABLE IF NOT EXISTS `syncbackRequests` (\n *   `id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` VARCHAR(255),\n *   `status` TEXT NOT NULL DEFAULT 'NEW',\n *   `error` TEXT,\n *   `props` TEXT,\n *   `responseJSON` TEXT,\n *   `accountId` VARCHAR(255) NOT NULL,\n *   `createdAt` DATETIME NOT NULL,\n *   `updatedAt` DATETIME NOT NULL\n * );\n */\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('syncbackRequest', {\n    type: Sequelize.STRING,\n    status: {\n      type: Sequelize.ENUM(\n        \"NEW\",\n        \"INPROGRESS-RETRYABLE\",\n        \"INPROGRESS-NOTRETRYABLE\",\n        \"SUCCEEDED\",\n        \"FAILED\",\n        \"CANCELLED\"\n      ),\n      defaultValue: \"NEW\",\n      allowNull: false,\n    },\n    error: JSONColumn('error'),\n    props: JSONColumn('props'),\n    responseJSON: JSONColumn('responseJSON'),\n    accountId: { type: Sequelize.STRING, allowNull: false },\n  }, {\n    indexes: [\n      {fields: ['status', 'type']},\n    ],\n    instanceMethods: {\n      toJSON() {\n        return {\n          id: `${this.id}`,\n          type: this.type,\n          error: this.error,\n          props: this.props,\n          response_json: this.responseJSON,\n          status: this.status,\n          object: 'syncbackRequest',\n          account_id: this.accountId,\n        }\n      },\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/thread.js",
    "content": "const {DatabaseTypes: {JSONArrayColumn}} = require('isomorphic-core');\n\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('thread', {\n    id: { type: Sequelize.STRING(65), primaryKey: true },\n    accountId: { type: Sequelize.STRING, allowNull: false },\n    version: Sequelize.INTEGER,\n    remoteThreadId: Sequelize.STRING,\n    subject: Sequelize.STRING(500),\n    snippet: Sequelize.STRING(255),\n    unreadCount: {\n      type: Sequelize.INTEGER,\n      get: function get() { return this.getDataValue('unreadCount') || 0 },\n    },\n    starredCount: {\n      type: Sequelize.INTEGER,\n      get: function get() { return this.getDataValue('starredCount') || 0 },\n    },\n    firstMessageDate: Sequelize.DATE,\n    lastMessageDate: Sequelize.DATE,\n    lastMessageReceivedDate: Sequelize.DATE,\n    lastMessageSentDate: Sequelize.DATE,\n    participants: JSONArrayColumn('participants'),\n    hasAttachments: {type: Sequelize.BOOLEAN, defaultValue: false},\n  }, {\n    indexes: [\n      { fields: ['subject'] },\n      { fields: ['remoteThreadId'] },\n    ],\n    classMethods: {\n      MAX_THREAD_LENGTH: 500,\n      requiredAssociationsForJSON: ({Folder, Label, Message}) => {\n        return [\n          {model: Folder},\n          {model: Label},\n          {\n            model: Message,\n            attributes: ['id'],\n          },\n        ]\n      },\n      associate: ({Thread, Folder, ThreadFolder, Label, ThreadLabel, Message, Reference}) => {\n        Thread.belongsToMany(Folder, {through: ThreadFolder})\n        Thread.belongsToMany(Label, {through: ThreadLabel})\n        Thread.hasMany(Message, {onDelete: 'cascade', hooks: true})\n        // TODO: what is the desired cascade behaviour for references?\n        Thread.hasMany(Reference)\n      },\n    },\n    instanceMethods: {\n      async updateLabelsAndFolders({transaction} = {}) {\n        const messages = await this.getMessages({attributes: ['id', 'folderId'], transaction});\n        const labelIds = new Set()\n        const folderIds = new Set()\n\n        await Promise.all(messages.map(async (msg) => {\n          const labels = await msg.getLabels({attributes: ['id'], transaction})\n          labels.forEach(({id}) => {\n            if (!id) return;\n            labelIds.add(id);\n          })\n          if (!msg.folderId) return;\n          folderIds.add(msg.folderId)\n        }));\n\n        await Promise.all([\n          this.setLabels(Array.from(labelIds), {transaction}),\n          this.setFolders(Array.from(folderIds), {transaction}),\n        ]);\n\n        return this.save({transaction});\n      },\n\n      // Updates the attributes that don't require an external set to prevent\n      // duplicates. Currently includes starred/unread counts, various date\n      // values, and snippet. Does not save the thread.\n      _updateSimpleMessageAttributes(message) {\n        // Update starred/unread counts\n        this.starredCount += message.starred ? 1 : 0;\n        this.unreadCount += message.unread ? 1 : 0;\n\n        // Update dates/snippet\n        if (!this.lastMessageDate || (message.date > this.lastMessageDate)) {\n          this.lastMessageDate = message.date;\n          this.snippet = message.snippet;\n        }\n        if (!this.firstMessageDate || (message.date < this.firstMessageDate)) {\n          this.firstMessageDate = message.date;\n        }\n\n        // Figure out if the message is sent and/or received and update more dates\n        // Note that `isReceived` is not mutually exclusive of `isSent` when\n        // labels are involved, because users can send emails to themselves.\n        const isSent = (\n          message.folder.role === 'sent' ||\n          !!message.labels.find(l => l.role === 'sent')\n        );\n        const isReceived = (\n          message.folder.role !== 'sent' ||\n          !!message.labels.find(l => l.role !== 'sent')\n        )\n\n        if (isSent && (!this.lastMessageSentDate || (message.date > this.lastMessageSentDate))) {\n          this.lastMessageSentDate = message.date;\n        }\n        if (isReceived && (!this.lastMessageReceivedDate || (message.date > this.lastMessageReceivedDate))) {\n          this.lastMessageReceivedDate = message.date;\n        }\n      },\n\n      async updateFromMessages({db, messages, recompute, transaction} = {}) {\n        if (!(this.folders instanceof Array) || !(this.labels instanceof Array)) {\n          throw new Error('Thread.updateFromMessages() expected .folders and .labels to be inflated arrays')\n        }\n\n        let _messages = messages;\n        if (recompute) {\n          if (!db) {\n            throw new Error('Cannot recompute thread attributes without a database reference.')\n          }\n          const {Label, Folder, File} = db;\n          _messages = await this.getMessages({\n            include: [{model: Label}, {model: Folder}, {model: File}],\n            attributes: {exclude: ['body']},\n          });\n          if (_messages.length === 0) {\n            return this.destroy();\n          }\n\n          this.folders = [];\n          this.labels = [];\n          this.participants = [];\n          this.unreadCount = 0;\n          this.starredCount = 0;\n          this.hasAttachments = false;\n          this.snippet = null;\n          this.lastMessageDate = null;\n          this.firstMessageDate = null;\n          this.lastMessageSentDate = null;\n          this.lastMessageReceivedDate = null;\n        } else {\n          // If we're not recomputing from all of the thread's messages, we need\n          // to know which messages to update from\n          if (!(_messages instanceof Array)) {\n            throw new Error('Thread.updateFromMessages() expected an array of messages')\n          }\n        }\n\n        const folderIds = new Set(this.folders.map(f => f.id));\n        const labelIds = new Set(this.labels.map(l => l.id));\n        const participantEmails = new Set(this.participants.map(p => p.email));\n\n        for (const message of _messages) {\n          if (!(message.labels instanceof Array)) {\n            throw new Error(\"Expected message.labels to be an inflated array.\");\n          }\n          if (!message.folder) {\n            throw new Error(\"Expected message.folder value to be present.\");\n          }\n\n          folderIds.add(message.folder.id)\n          message.labels.forEach(label => labelIds.add(label.id))\n\n          this._updateSimpleMessageAttributes(message);\n\n          const {to, cc, bcc, from} = message;\n          to.concat(cc, bcc, from).forEach(participant => {\n            if (participantEmails.has(participant.email)) {\n              return;\n            }\n            participantEmails.add(participant.email)\n            this.participants = [...this.participants, participant]\n          })\n\n          // message.files only needs to be inflated if we're recomputing\n          // the thread. Otherwise, .hasAttachments is set after we run\n          // extractFiles on each message.\n          if (!this.hasAttachments && message.files instanceof Array) {\n            this.hasAttachments = message.files.some(f => !f.contentId);\n          }\n        }\n\n        // Setting folders and labels cannot be done on a thread without an id\n        const savedThread = await this.save({transaction});\n\n        await Promise.all([\n          savedThread.setFolders(Array.from(folderIds), {transaction}),\n          savedThread.setLabels(Array.from(labelIds), {transaction}),\n        ])\n        return savedThread.save({transaction});\n      },\n\n      toJSON() {\n        if (!(this.labels instanceof Array)) {\n          throw new Error(\"Thread.toJSON called on a thread where labels were not eagerly loaded.\")\n        }\n        if (!(this.folders instanceof Array)) {\n          throw new Error(\"Thread.toJSON called on a thread where folders were not eagerly loaded.\")\n        }\n        if (!(this.messages instanceof Array)) {\n          throw new Error(\"Thread.toJSON called on a thread where messages were not eagerly loaded. (Only need the IDs!)\")\n        }\n\n        const response = {\n          id: `${this.id}`,\n          object: 'thread',\n          message_ids: this.messages.map(m => m.id),\n          folders: this.folders.map(f => f.toJSON()),\n          labels: this.labels.map(l => l.toJSON()),\n          account_id: this.accountId,\n          participants: this.participants,\n          subject: this.subject,\n          snippet: this.snippet,\n          unread: this.unreadCount > 0,\n          starred: this.starredCount > 0,\n          has_attachments: this.hasAttachments,\n          last_message_timestamp: this.lastMessageDate ? this.lastMessageDate.getTime() / 1000.0 : null,\n          last_message_sent_timestamp: this.lastMessageSentDate ? this.lastMessageSentDate.getTime() / 1000.0 : null,\n          last_message_received_timestamp: this.lastMessageReceivedDate ? this.lastMessageReceivedDate.getTime() / 1000.0 : null,\n        };\n\n        const expanded = this.messages[0] ? !!this.messages[0].accountId : false;\n        if (expanded) {\n          response.messages = this.messages;\n        } else {\n          response.message_ids = this.messages.map(m => m.id);\n        }\n\n        return response;\n      },\n    },\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/threadFolder.js",
    "content": "/**\n * CREATE TABLE IF NOT EXISTS `threadFolders` (\n *   `createdAt` DATETIME NOT NULL,\n *   `updatedAt` DATETIME NOT NULL,\n *   `threadId` VARCHAR(65) NOT NULL REFERENCES `threads` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n *   `folderId` VARCHAR(65) NOT NULL REFERENCES `folders` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n *   PRIMARY KEY (`threadId`, `folderId`)\n * );\n *\n * sqlite_autoindex_threadFolders_1 (threadId, folderId)\n */\nmodule.exports = (sequelize) => {\n  return sequelize.define('threadFolder', {\n  }, {\n    indexes: [\n      // NOTE: When SQLite sets up this table, it creates an auto index in\n      // the order ['threadId', 'folderId']. This is the correct index we\n      // need for queries requesting Folders for a certain Thread.\n      //\n      // We need to create one more index to allow queries from the\n      // reverse direction requesting Threads for a certain Folder.\n      {fields: ['folderId', 'threadId']},\n    ],\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/models/threadLabel.js",
    "content": "/**\n * CREATE TABLE IF NOT EXISTS `threadLabels` (\n *   `createdAt` DATETIME NOT NULL,\n *   `updatedAt` DATETIME NOT NULL,\n *   `labelId` VARCHAR(65) NOT NULL REFERENCES `labels` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n *   `threadId` VARCHAR(65) NOT NULL REFERENCES `threads` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,\n *   PRIMARY KEY (`labelId`, `threadId`)\n * );\n *\n * sqlite_autoindex_threadLabels_1 labelId, threadId\n */\nmodule.exports = (sequelize) => {\n  return sequelize.define('threadLabel', {\n  }, {\n    indexes: [\n      // NOTE: When SQLite sets up this table, it creates an auto index in\n      // the order ['labelId', 'threadId']. This is the correct index we\n      // need for queries requesting Threads for a certain Label.\n      //\n      // We need to create one more index to allow queries from the\n      // reverse direction requesting Labels for a certain Thread.\n      {fields: ['threadId', 'labelId']},\n    ],\n  });\n};\n"
  },
  {
    "path": "packages/client-sync/src/shared/database-extensions.js",
    "content": "const Rx = require('rx-lite');\nconst Sequelize = require('sequelize');\n\nSequelize.Model.prototype.streamAll = function streamAll(options = {}) {\n  return Rx.Observable.create((observer) => {\n    const chunkSize = options.chunkSize || 1000;\n    options.offset = 0;\n    options.limit = chunkSize;\n\n    const findFn = (opts) => {\n      this.findAll(opts).then((models = []) => {\n        observer.onNext(models)\n        if (models.length === chunkSize) {\n          opts.offset += chunkSize;\n          findFn(opts)\n        } else {\n          observer.onCompleted()\n        }\n      })\n    }\n\n    findFn(options)\n  })\n}\n\n"
  },
  {
    "path": "packages/client-sync/src/shared/dedupe-accounts.es6",
    "content": "import LocalDatabaseConnector from './local-database-connector'\nimport SyncProcessManager from '../local-sync-worker/sync-process-manager'\n\n// NOTE: See https://phab.nylas.com/D4425 for explanation of why these functions\n// are necessary\n// TODO remove these after they no longer affect users\n\nexport async function preventCreationOfDuplicateAccounts(db, accountParams) {\n  try {\n    const existing = await db.Account.findOne({where: {emailAddress: accountParams.emailAddress}})\n    const id = db.Account.hash(accountParams)\n    if (existing && existing.id !== id) {\n      console.warn('upsertAccount: Preventing creation of duplicate accounts with different settings')\n      await SyncProcessManager.removeWorkerForAccountId(existing.id);\n      await LocalDatabaseConnector.destroyAccountDatabase(existing.id);\n      await existing.destroy()\n      console.warn('upsertAccoun: Prevented creation of duplicate accounts with different settings')\n    }\n  } catch (err) {\n    err.message = `Error removing duplicate account with old settings: ${err.message}`\n    NylasEnv.reportError(err)\n  }\n}\n\nexport async function removeDuplicateAccountsWithOldSettings() {\n  try {\n    const db = await LocalDatabaseConnector.forShared()\n    const allAccounts = await db.Account.findAll()\n    const accountsByEmail = new Map()\n    const dupeAcctsWithOldSettings = []\n\n    for (const account of allAccounts) {\n      const {emailAddress: email} = account\n      if (accountsByEmail.has(email)) {\n        accountsByEmail.get(email).push(account)\n      } else {\n        accountsByEmail.set(email, [account])\n      }\n    }\n    for (const [email, accounts] of accountsByEmail) {  // eslint-disable-line\n      if (accounts.length <= 1) { continue }\n      for (const account of accounts) {\n        if (!account.connectionSettings.imap_security) {\n          dupeAcctsWithOldSettings.push(account)\n        }\n      }\n    }\n\n    if (dupeAcctsWithOldSettings.length === 0) { return }\n    console.warn('Sync: Found duplicate accounts with old settings')\n    for (const dupeAccount of dupeAcctsWithOldSettings) {\n      await LocalDatabaseConnector.destroyAccountDatabase(dupeAccount.id);\n      await dupeAccount.destroy()\n    }\n    console.warn('Sync: Removed duplicate accounts with old settings')\n  } catch (err) {\n    err.message = `Error removing duplicate account with old settings: ${err.message}`\n    NylasEnv.reportError(err)\n  }\n}\n"
  },
  {
    "path": "packages/client-sync/src/shared/imap-paths-utils.js",
    "content": "function formatImapPath(pathStr) {\n  if (!pathStr) {\n    throw new Error(\"Can not format an empty path!\");\n  }\n\n  // https://regex101.com/r/yeyZJh/1\n  const s = pathStr.replace(/^\\[Gmail]\\//, '');\n  return s;\n}\n\nmodule.exports = {formatImapPath}\n"
  },
  {
    "path": "packages/client-sync/src/shared/interruptible.js",
    "content": "const {EventEmitter} = require('events')\nconst {Errors} = require('isomorphic-core')\n\n/**\n * Interruptible objects allow you to run and interrupt functions by using\n * generator functions (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function*).\n *\n * In order to indicate where a generator function can be interrupted, you must use the\n * keyword `yield` instead of `await` to wait for the resolution of Promises.\n *\n * e.g.\n * ```\n * const interruptible = new Interruptible()\n *\n * async * interruptibleFunc(db) {\n *   // Use `yield` to indicate that we can interrupt the function after\n *   // this async operation has resolved\n *   const models = yield db.Messages.findAll()\n *\n *   // If the operation is interrupted, code execution will stop here!\n *\n *   // ...\n *\n *   await saveModels(models)\n *   // `await` wont stop code execution even if operation is interrupted\n *\n *   // ...\n * }\n *\n * await interruptible.run(interruptibleFunc, this, db)\n *\n * // Sometime later\n * interruptible.interrupt()\n * ```\n */\nclass Interruptible extends EventEmitter {\n  constructor() {\n    super()\n    this._interrupt = false\n    this._running = false\n    this._rejectWithinRun = null\n  }\n\n  interrupt({forceReject = false} = {}) {\n    if (!this._running) { return Promise.resolve() }\n\n    // Start listening before the interrupt, so we don't miss the 'interrupted' event\n    const promise = new Promise((resolve) => this.once('interrupted', resolve))\n    this._interrupt = true\n\n    if (forceReject && this._rejectWithinRun) {\n      // This will reject the `interruptible.run()` call and immediately return\n      // control to the code path that is awaiting it.\n      this._rejectWithinRun(new Errors.RetryableError('Forcefully interrupted'))\n    }\n\n    return promise\n  }\n\n  // This function executes the generator object through completion or until we\n  // are interrupted\n  _runGenerator(generatorObj) {\n    return new Promise(async (resolve, reject) => {\n      try {\n        if (!generatorObj || typeof generatorObj.next !== 'function') {\n          throw new Error('Interruptible: You must pass a generator function to run')\n        }\n        let step = {done: false};\n        let val;\n        const advance = async () => {\n          // Calling generator.next() will execute the generator function until\n          // it's next `yield` statement.\n          // The return value of next is an object with the following shape:\n          // {\n          //   value: 'some val', // `yield`ed value\n          //   done: false,       // is execution done?\n          // }\n          step = generatorObj.next(val)\n\n          if (typeof step.then === 'function') {\n            // Await it in case it is a promise.\n            step = await step\n          }\n\n          if (!step.value) {\n            // If no value, just continue advancing\n            val = step.value\n            return\n          }\n\n\n          if (typeof step.value.next === 'function') {\n            // step.value is a generator object, so let's run it recursively\n            val = await this._runGenerator(step.value)\n          } else {\n            // step.value could be a Promise or not, let's just `await` it\n            // anyway\n            val = await step.value\n          }\n          return\n        }\n\n        // Advance until done\n        while (!step.done) {\n          if (this._interrupt) {\n            this.emit('interrupted')\n            console.log('Operation Interrupted')\n            return resolve()\n          }\n          await advance()\n        }\n        return resolve(val)\n      } catch (err) {\n        return reject(err)\n      }\n    })\n  }\n\n  /**\n   * @returns a Promise that resolves when the generator function has been\n   * executed to completion, or when it has been interrupted. It will reject if\n   * the generator function throws an error at any point.\n   */\n  async run(generatorFunc, ctx, ...fnArgs) {\n    this._running = true\n    const generatorObj = generatorFunc.call(ctx, ...fnArgs)\n    const response = await new Promise(async (resolve, reject) => {\n      this._rejectWithinRun = (rejectValue) => {\n        reject(rejectValue)\n      }\n      let _response;\n      try {\n        _response = await this._runGenerator(generatorObj)\n      } catch (err) {\n        reject(err)\n        return;\n      }\n      resolve(_response)\n    })\n    this._interrupt = false\n    this._running = false\n    return response\n  }\n}\n\nmodule.exports = Interruptible\n"
  },
  {
    "path": "packages/client-sync/src/shared/local-database-connector.js",
    "content": "const createDebug = require('debug');\nconst Sequelize = require('sequelize');\nconst fs = require('fs');\nconst path = require('path');\nconst {StringUtils, loadModels, HookIncrementVersionOnSave, HookTransactionLog} = require('isomorphic-core');\nconst TransactionConnector = require('./transaction-connector')\n\nrequire('./database-extensions'); // Extends Sequelize on require\nconst debugVerbose = createDebug(\"sync:K2DB:all\");\n\nclass LocalDatabaseConnector {\n  constructor() {\n    this._cache = {};\n  }\n\n  _sequelizePoolForDatabase(dbname) {\n    const storage = NylasEnv.inSpecMode() ? ':memory:' : path.join(process.env.NYLAS_HOME, `${dbname}.sqlite`);\n    const dbLog = (q, time) => {\n      debugVerbose(StringUtils.trimTo(`🔷 (${time}ms) ${q}`))\n    }\n    const sequelize = new Sequelize(dbname, '', '', {\n      storage: storage,\n      dialect: \"sqlite\",\n      benchmark: debugVerbose.enabled,\n      logging: debugVerbose.enabled ? dbLog : false,\n    })\n    sequelize.query('PRAGMA page_size = 8192');\n    sequelize.query('PRAGMA cache_size = 4096');\n    return sequelize;\n  }\n\n  forAccount(accountId) {\n    if (!accountId) {\n      return Promise.reject(new Error(`You need to pass an accountId to init the database!`))\n    }\n\n    if (this._cache[accountId]) {\n      return this._cache[accountId];\n    }\n\n    const newSequelize = this._sequelizePoolForDatabase(`a-${accountId}`);\n    const db = loadModels(Sequelize, newSequelize, {\n      modelDirs: [path.resolve(__dirname, '..', 'models')],\n    })\n\n    HookTransactionLog(db, newSequelize, {\n      onCreatedTransaction: (transaction) => {\n        TransactionConnector.notifyDelta(db.accountId, transaction);\n      },\n    });\n\n    HookIncrementVersionOnSave(db, newSequelize);\n\n    db.sequelize = newSequelize;\n    db.Sequelize = Sequelize;\n    db.accountId = accountId;\n\n    this._cache[accountId] = newSequelize.authenticate().thenReturn(db);\n\n    return this._cache[accountId];\n  }\n\n  ensureAccountDatabase(accountId) {\n    return this.forAccount(accountId).then((db) => {\n      // this is a bit of a hack, because sequelize.sync() doesn't work with\n      // schemas. It's necessary to sync models individually and in the right order.\n      const models = ['Contact', 'Folder', 'Label', 'Transaction', 'Thread', 'ThreadLabel', 'ThreadFolder', 'Message', 'MessageLabel', 'Reference', 'MessageReference', 'File', 'SyncbackRequest'];\n      return Promise.each(models, (n) =>\n        db[n].sync()\n      )\n    });\n  }\n\n  async destroyAccountDatabase(accountId) {\n    if (NylasEnv.inSpecMode()) {\n      // The db is in memory, so we don't have to unlink it. Just drop the data.\n      return this.forAccount(accountId).then(db => {\n        delete this._cache[accountId];\n        return db.sequelize.drop()\n      });\n    }\n\n    const dbname = `a-${accountId}`;\n    const dbpath = path.join(process.env.NYLAS_HOME, `${dbname}.sqlite`);\n\n    try {\n      const err = fs.accessSync(dbpath, fs.F_OK);\n      if (!err) {\n        fs.unlinkSync(dbpath);\n      }\n    } catch (err) {\n      // Ignored\n    }\n\n    delete this._cache[accountId];\n    return Promise.resolve();\n  }\n\n  _sequelizeForShared() {\n    const sequelize = this._sequelizePoolForDatabase(`shared`);\n    const db = loadModels(Sequelize, sequelize)\n\n    db.sequelize = sequelize;\n    db.Sequelize = Sequelize;\n\n    return sequelize.authenticate().then(() =>\n      sequelize.sync()\n    ).thenReturn(db);\n  }\n\n  forShared() {\n    this._cache.shared = this._cache.shared || this._sequelizeForShared();\n    return this._cache.shared;\n  }\n}\n\nmodule.exports = new LocalDatabaseConnector()\n"
  },
  {
    "path": "packages/client-sync/src/shared/logger.es6",
    "content": "const _ = require('underscore')\n\nconst LOGGER_COLORS = [\n  '#E91E63',\n  '#9C27B0',\n  '#673AB7',\n  '#3F51B5',\n  '#2196F3',\n  '#009688',\n  '#4CAF50',\n  '#FF5722',\n  '#795548',\n  '#607D8B',\n]\nconst colorsByPrefix = {}\nlet curColor = 0\n\nfunction getColorForPrefix(prefix) {\n  if (colorsByPrefix[prefix]) {\n    return colorsByPrefix[prefix]\n  }\n  colorsByPrefix[prefix] = LOGGER_COLORS[curColor]\n  curColor = (curColor + 1) % LOGGER_COLORS.length\n  return colorsByPrefix[prefix]\n}\n\nfunction Logger(boundArgs = {}) {\n  if (!_.isObject(boundArgs)) {\n    throw new Error('Logger: Bound arguments must be an object')\n  }\n  const logger = {}\n  const loggerFns = ['log', 'debug', 'info', 'warn', 'error']\n  loggerFns.forEach((logFn) => {\n    logger[logFn] = (...args) => {\n      const {accountId, accountEmail, ...otherArgs} = boundArgs\n      const prefix = accountEmail || accountId\n      const suffix = !_.isEmpty(otherArgs) ? otherArgs : '';\n      let [first, ...extraArgs] = args\n      if (_.isObject(first)) {\n        [first, extraArgs] = [extraArgs, [first]]\n      }\n      if (prefix) {\n        const color = getColorForPrefix(prefix)\n        console[logFn](\n          `%c<${prefix}> %c${first}`,\n          `color: ${color}`,\n          `color: #333333`,\n          ...extraArgs,\n          suffix\n        )\n        return\n      }\n      console[logFn](`${first}`, ...extraArgs, suffix)\n    }\n  })\n  logger.boundArgs = boundArgs\n  logger.child = (extraBoundArgs) => Logger({...boundArgs, ...extraBoundArgs})\n  return logger\n}\n\nfunction createLogger() {\n  const childLogs = new Map()\n  const logger = Logger()\n\n  return Object.assign(logger, {\n    forAccount(account = {}) {\n      if (!childLogs.has(account.id)) {\n        const childLog = logger.child({\n          accountId: account.id,\n          accountEmail: account.emailAddress,\n        })\n        childLogs.set(account.id, childLog)\n      }\n      return childLogs.get(account.id)\n    },\n  })\n}\n\nmodule.exports = {createLogger}\n"
  },
  {
    "path": "packages/client-sync/src/shared/shim-sequelize.es6",
    "content": "const _ = require('underscore');\n\nconst DEFAULT_SEQUELIZE_SHIM_TIMEOUT = 60 * 1000; // 1 min\n\nconst shimObject = (obj) => {\n  Object.keys(obj).forEach(key => {\n    // Skip internal methods.\n    if (key.startsWith('_')) {\n      return;\n    }\n\n    const prop = obj[key];\n    // Only patch methods.\n    if (!_.isFunction(prop)) {\n      return;\n    }\n\n    obj[key] = function(...args) {  // eslint-disable-line\n      const result = prop.call(this, ...args);\n      if (result && _.isFunction(result.then)) {\n        return new Promise(async (resolve, reject) => {\n          try {\n            resolve(await result);\n          } catch (err) {\n            reject(err);\n          }\n        }).timeout(DEFAULT_SEQUELIZE_SHIM_TIMEOUT, `${key} timed out`);\n      }\n      return result;\n    };\n  });\n};\n\nexport default function shimSequelize(Sequelize) {\n  shimObject(Sequelize.Model);\n  shimObject(Sequelize.Instance.prototype);\n}\n"
  },
  {
    "path": "packages/client-sync/src/shared/stream-decoders.es6",
    "content": "import {Transform} from 'stream'\nconst mimelib = require('mimelib');\n\nexport class QuotedPrintableStreamDecoder extends Transform {\n  constructor(opts = {}) {\n    super(opts);\n    this.charset = opts.charset\n    this._text = \"\";\n  }\n\n  /**\n   * Overrides Transform::_transfor\n   *\n   * We can't decode quoted-printable in chunks, so we buffer it.\n   */\n  _transform = (chunk, encoding, cb) => {\n    this._text += chunk.toString();\n    cb();\n  }\n\n  /**\n   * Overrides Transform::_flush\n   *\n   * At the end of the stream, decode the whole buffer at once and flush\n   * it out the end.\n   */\n  _flush = (cb) => {\n    // If this.charset is null (a very common case for attachments),\n    // mimelib defaults to utf-8 as the charset.\n    this.push(mimelib.decodeQuotedPrintable(this._text, this.charset));\n    cb();\n  }\n}\n"
  },
  {
    "path": "packages/client-sync/src/shared/sync-activity.es6",
    "content": "class SyncActivity {\n  constructor() {\n    // Keyed by accountId, each value is structured like\n    // {\n    //   time: 1490293620249,\n    //   activity: (whatever was passed in, usually a string)\n    // }\n    this._lastActivityByAccountId = {}\n  }\n\n  reportSyncActivity = (accountId, activity) => {\n    if (!this._lastActivityByAccountId[accountId]) {\n      this._lastActivityByAccountId[accountId] = {};\n    }\n    const lastActivity = this._lastActivityByAccountId[accountId]\n    lastActivity.time = Date.now();\n    lastActivity.activity = activity;\n  }\n\n  getLastSyncActivityForAccount = (accountId) => {\n    return this._lastActivityByAccountId[accountId] || {}\n  }\n\n  getLastSyncActivity = () => {\n    return this._lastActivityByAccountId\n  }\n}\n\nexport default new SyncActivity()\n"
  },
  {
    "path": "packages/client-sync/src/shared/transaction-connector.js",
    "content": "const Rx = require('rx-lite')\nconst EventEmitter = require('events');\n\nclass TransactionConnector extends EventEmitter {\n  notifyDelta(accountId, transaction) {\n    this.emit(accountId, transaction);\n  }\n\n  getObservableForAccountId(accountId) {\n    return Rx.Observable.create((observer) => {\n      this.on(accountId, observer.onNext.bind(observer));\n    });\n  }\n}\n\nmodule.exports = new TransactionConnector();\n"
  },
  {
    "path": "packages/client-sync/stylesheets/index.less",
    "content": ".developer-bar .local-sync {\n  #accounts-wrapper {\n    position: relative;\n  }\n\n  .account {\n    position: absolute;\n    border-radius: 5px;\n    width: 270px;\n    height: 450px;\n    color: black;\n    background-color: rgb(255, 255, 255);\n    padding: 15px;\n    margin: 5px;\n    overflow: hidden;\n  }\n\n  .account h3 {\n    font-size: 13px;\n    margin: 0;\n    padding: 0;\n  }\n\n  .account .section {\n    font-size: 12px;\n    padding: 10px 0;\n    text-align: center;\n  }\n\n  .account.errored {\n    color: #a94442;\n    border-radius: 4px;\n    background-color: rgb(231, 195, 195);\n  }\n\n  .error-link {\n    font-weight: bold;\n  }\n\n  .error-link:hover {\n    cursor: pointer;\n    color: #702726;\n  }\n\n  #open-all-sync {\n    color: #ffffff;\n    padding-left: 5px;\n  }\n\n  .right-action {\n    float: right;\n    margin-top: 10px;\n  }\n\n  .action-link {\n    color: rgba(16, 83, 161, 0.88);\n    text-decoration: underline;\n    cursor: pointer;\n    margin: 5px 0;\n  }\n\n  .action-link.cancel {\n    margin-top: 10px;\n  }\n\n  .sync-policy textarea {\n    width: 100%;\n    height: 200px;\n    white-space: pre;\n  }\n\n  .modal {\n    background-color: white;\n    width: 50%;\n    margin: 10vh auto;\n    padding: 20px;\n    max-height: calc(80vh - 40px); /* minus padding */\n    overflow: auto;\n  }\n\n  .modal-bg {\n    position: fixed;\n    width: 100%;\n    height: 100%;\n    left: 0;\n    top: 0;\n    background-color: rgba(0, 0, 0, 0.3);\n    z-index: 10;\n  }\n\n  .modal-close-wrapper {\n    position: relative;\n    height: 0;\n    width: 0;\n    float: right;\n    top: -10px;\n  }\n\n  .modal-close {\n    position: absolute;\n    cursor: pointer;\n    font-size: 14px;\n    font-weight: bold;\n    background: url('../images/close.png') center center no-repeat;\n    background-size: 12px auto;\n    height: 12px;\n    width: 12px;\n    top: 12px;\n    right: 12px;\n  }\n\n  .sync-graph {\n    margin-top: 3px;\n  }\n\n  .stats b {\n    display: inline-block;\n    margin-top: 5px;\n    margin-bottom: 1px;\n  }\n\n  #syncback-request-details {\n    font-size: 15px;\n    color: black;\n  }\n\n  #syncback-request-details .counts {\n    margin: 10px;\n  }\n\n  #syncback-request-details span {\n    margin: 10px;\n  }\n\n  #syncback-request-details table {\n    width: 100%;\n    border: solid black 1px;\n    box-shadow: 1px 1px #333333;\n    margin: 10px 0;\n    border-collapse: collapse;\n  }\n\n  #syncback-request-details tr:nth-child(even) {\n    background-color: #F1F1F1;\n  }\n\n  #syncback-request-details tr:not(:first-child):hover {\n    background-color: #C9C9C9;\n  }\n\n  #syncback-request-details td, #syncback-request-details th {\n    text-align: center;\n    padding: 10px 5px;\n    border: solid black 1px;\n  }\n\n  .dropdown-arrow {\n    margin: 0 5px;\n    height: 7px;\n    vertical-align: middle;\n  }\n\n  .dropdown-options {\n    border: solid black 1px;\n    position: absolute;\n    background-color: white;\n    text-align: left;\n    display: inline;\n  }\n\n  .dropdown-option {\n    position: relative;\n    padding: 0 2px;\n  }\n\n  .dropdown-option:hover {\n    background-color: rgb(114, 163, 255);\n  }\n\n  .dropdown-selected {\n    display: inline;\n  }\n\n  .dropdown-wrapper {\n    display: inline;\n    cursor: pointer;\n    font-weight: normal;\n  }\n\n  .mini-account::after {\n    display: inline-block;\n    position: relative;\n    height: 100%;\n    width: 100%;\n    background-color: #666666;\n    content: \"\";\n    z-index: -1;\n  }\n\n  .mini-account {\n    background-color: rgb(0, 255, 157);\n    display: inline-block;\n    width: 10px;\n    height: 10px;\n  }\n\n  .mini-account.errored {\n    background-color: rgb(255, 38, 0);\n  }\n\n  .sum-accounts {\n    border-top: solid black 1px;\n    margin-top: 5px;\n    padding-top: 5px;\n  }\n}\n"
  },
  {
    "path": "packages/cloud-api/.gitignore",
    "content": "\n# Elastic Beanstalk Files\n.elasticbeanstalk/*\n!.elasticbeanstalk/*.cfg.yml\n!.elasticbeanstalk/*.global.yml\n"
  },
  {
    "path": "packages/cloud-api/README.md",
    "content": "# Cloud API\n\nThis is the Cloud API service for Nylas Mail. It exposes an API (powered by\nHapi) that provides endpoints for Auth, Metadata, Link & Open Tracking,\nMetrics, and Health monitoring\n\nFor details on how to run Cloud API, see the\n[cloud-core/README.md](https://github.com/nylas/nylas-mail-all/blob/master/packages/cloud-core/README.md)\nand run `npm run start-cloud` from the root of nylas-mail-all\n"
  },
  {
    "path": "packages/cloud-api/app.es6",
    "content": "// Super light-weight Flask-like server. Easier than express\n// https://github.com/hapijs/hapi\nimport Hapi from 'hapi';\n\n// Static file and directory handlers for hapi.js\n// https://github.com/hapijs/inert\nimport Inert from 'inert';\n\n// Templates rendering support for hapi.js\n// https://github.com/hapijs/vision\nimport Vision from 'vision';\n\n// HTTP-friendly error objects\n// https://github.com/hapijs/boom\nimport HapiBoom from 'hapi-boom-decorators'\n\n// Open API Swagger specs:\n// https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md\n// https://github.com/glennjones/hapi-swagger\nimport HapiSwagger from 'hapi-swagger';\n\n// Basic API user:pass Authentication\n// https://github.com/hapijs/hapi-auth-basic\nimport HapiBasicAuth from 'hapi-auth-basic';\n\n// Common Hapi utilities\nimport Hoek from 'hoek';\n\nimport Handlebars from 'handlebars'\n\nimport {Logger} from 'cloud-core';\n\nimport Package from './package.json';\nimport {apiAuthenticate} from './src/authentication'\nimport sentryPlugin from './src/sentry'\n\n/**\n * API Routes\n */\nimport registerAuthRoutes from './src/routes/auth'\nimport registerAdminRoutes from './src/routes/admin'\nimport registerPingRoutes from './src/routes/ping'\nimport registerDeltaRoutes from './src/routes/delta'\nimport registerMetadataRoutes from './src/routes/metadata'\nimport registerBlobsRoutes from './src/routes/blobs'\nimport registerHoneycombRoutes from './src/routes/honeycomb'\nimport registerLinkTrackingRoutes from './src/routes/link-tracking'\nimport registerOpenTrackingRoutes from './src/routes/open-tracking'\nimport registerStaticRoutes from './src/routes/static'\n\n/**\n * API Decorators\n */\nimport registerLoggerDecorator from './src/decorators/logger'\nimport registerErrorFormatDecorator from './src/decorators/error-format'\n\n// Swap out Node's native Promise for Bluebird, which allows us to\n// do fancy things like handle exceptions inside promise blocks\nglobal.Promise = require('bluebird');\n\nglobal.Logger = Logger.createLogger('n1cloud-api')\n\n// TODO: would be really nice if we could log some request context when\n// this happens, but not sure if there's a good way to do that...\n// (this can only happen in request paths during async handlers---otherwise\n// hapi catches the error and transforms it into a 5xx error)\nconst onUnhandledError = (err) => {\n  global.Logger.error(err)\n}\nprocess.on('uncaughtException', onUnhandledError)\nprocess.on('unhandledRejection', onUnhandledError)\n\nconst server = new Hapi.Server({\n  debug: { request: ['error'] },\n  connections: {\n    router: {\n      stripTrailingSlash: true,\n    },\n  },\n});\n\nserver.connection({ port: process.env.PORT });\n\n\n// Time all requests, based on\n// https://github.com/codewinds/hapi-elapsed/blob/master/lib/hapi-elapsed.js\nserver.on('request-internal', (request, event, tags) => {\n  if (tags.received) {\n    request.app.timing = { bench: new Hoek.Timer() };\n  }\n});\n\n// Log every request and status code for post analysis w/log aggregation tools\nserver.on('request-error', (request, err) => {\n  request.logger.info({\n    http_status: request.response.statusCode,\n    request_time_ms: request.app.timing.bench.elapsed(),\n    error: err.name,\n    error_message: err.message,\n    error_tb: err.stack,\n  }, 'request handled');\n});\nserver.on('response', (request) => {\n  if (request.response && request.response.statusCode < 500) {\n    request.logger.info({\n      http_status: request.response ? request.response.statusCode : null,\n      request_time_ms: request.app.timing.bench.elapsed(),\n    }, 'request handled');\n  }\n});\n\n\nconst plugins = [Inert, Vision, HapiBasicAuth, HapiBoom, {\n  register: HapiSwagger,\n  options: {\n    info: {\n      title: 'N1-Cloud API Documentation',\n      version: Package.version,\n    },\n  },\n}];\n\nserver.register(plugins, (err) => {\n  if (err) { throw err; }\n\n  server.auth.strategy('api-consumer', 'basic', {\n    validateFunc: apiAuthenticate,\n  });\n  server.auth.strategy('static-password', 'basic', {\n    validateFunc: (req, user, pass, cb) => {\n      if (user === \"nylas\" && pass === \"dressingontheside\") {\n        return cb(null, true, {})\n      }\n      return cb(null, false)\n    },\n  })\n  server.auth.default('api-consumer');\n\n  registerAuthRoutes(server)\n  registerAdminRoutes(server)\n  registerPingRoutes(server)\n  registerDeltaRoutes(server)\n  registerMetadataRoutes(server)\n  registerBlobsRoutes(server)\n  registerHoneycombRoutes(server)\n  registerLinkTrackingRoutes(server)\n  registerOpenTrackingRoutes(server)\n  registerStaticRoutes(server)\n\n  registerLoggerDecorator(server)\n  registerErrorFormatDecorator(server)\n\n  if (process.env.SENTRY_DSN) {\n    server.register({\n      register: sentryPlugin,\n      options: {\n        dsn: process.env.SENTRY_DSN,\n      }});\n  }\n\n  server.views({\n    engines: {\n      html: Handlebars,\n    },\n    relativeTo: __dirname,\n    path: 'src/views',\n    layoutPath: 'src/views/layout',\n    layout: 'default',\n  });\n\n  server.start((startErr) => {\n    if (startErr) { throw startErr; }\n    global.Logger.info({url: server.info.uri}, 'API running');\n  });\n});\n"
  },
  {
    "path": "packages/cloud-api/package.json",
    "content": "{\n  \"name\": \"cloud-api\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Nylas API\",\n  \"scripts\": {\n    \"start\": \"babel-node app.es6\",\n    \"test\": \"babel-node spec/run.es6\"\n  },\n  \"author\": \"Nylas\",\n  \"license\": \"ISC\",\n  \"devDependencies\": {\n    \"babel-cli\": \"6.x.x\",\n    \"jasmine\": \"2.x.x\"\n  },\n  \"dependencies\": {\n    \"atob\": \"2.0.3\",\n    \"babel-cli\": \"6.23.0\",\n    \"boom\": \"4.2.0\",\n    \"btoa\": \"1.1.2\",\n    \"cloud-core\": \"0.x.x\",\n    \"googleapis\": \"9.0.0\",\n    \"handlebars\": \"4.0.6\",\n    \"hapi\": \"16.1.0\",\n    \"hapi-auth-basic\": \"^4.2.0\",\n    \"hapi-boom-decorators\": \"2.2.2\",\n    \"hapi-swagger\": \"7.6.0\",\n    \"hoek\": \"4.1.0\",\n    \"inert\": \"4.0.0\",\n    \"isomorphic-core\": \"0.x.x\",\n    \"jasmine\": \"2.5.3\",\n    \"joi\": \"8.4.2\",\n    \"js-base64\": \"2.1.9\",\n    \"libhoney\": \"1.0.0-beta.2\",\n    \"moment\": \"2.x.x\",\n    \"moment-round\": \"1.x.x\",\n    \"moment-timezone\": \"0.5.x\",\n    \"raven\": \"1.1.4\",\n    \"request\": \"2.79.0\",\n    \"request-promise\": \"4.1.1\",\n    \"rx-lite\": \"4.0.8\",\n    \"vision\": \"4.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/cloud-api/spec/helpers.es6",
    "content": "import {DatabaseConnector} from 'cloud-core'\n\nlet testDB = null\n\nasync function getTestDatabase() {\n  testDB = testDB || await DatabaseConnector._sequelizeForShared({test: true})\n  return testDB\n}\n\nasync function destroyTestDatabase() {\n  if (testDB) {\n    await testDB.sequelize.drop()\n    testDB = null\n  }\n}\n\nfunction getMockServer() {\n  return {\n    routes: [],\n    route: function route(r) {\n      this.routes.push(r)\n    },\n  }\n}\n\nmodule.exports = {\n  getTestDatabase,\n  destroyTestDatabase,\n  getMockServer,\n}\n"
  },
  {
    "path": "packages/cloud-api/spec/jasmine/execute.es6",
    "content": "import Jasmine from 'jasmine'\nimport JasmineExtensions from './extensions'\n\nexport default function execute(extendOpts) {\n  const jasmine = new Jasmine()\n  jasmine.loadConfigFile('spec/jasmine/config.json')\n  const jasmineExtensions = new JasmineExtensions()\n  jasmineExtensions.extend(extendOpts)\n  jasmine.execute()\n}\n"
  },
  {
    "path": "packages/cloud-api/spec/jasmine/extensions.es6",
    "content": "import applyPolyfills from './polyfills'\n\nexport default class JasmineExtensions {\n  extend({beforeEach, afterEach} = {}) {\n    applyPolyfills()\n    global.it = this._makeItAsync(global.it)\n    global.fit = this._makeItAsync(global.fit)\n    global.beforeAll = this._makeEachOrAllFnAsync(global.beforeAll)\n    global.afterAll = this._makeEachOrAllFnAsync(global.afterAll)\n    global.beforeEach = this._makeEachOrAllFnAsync(global.beforeEach)\n    global.afterEach = this._makeEachOrAllFnAsync(global.afterEach)\n    if (beforeEach) {\n      global.beforeEach(beforeEach)\n    }\n    if (afterEach) {\n      global.afterEach(afterEach)\n    }\n  }\n\n  _runAsync(userFn, done) {\n    if (!userFn) {\n      done()\n      return true\n    }\n    const resp = userFn.apply(this);\n    if (resp && resp.then) {\n      return resp.then(done).catch((error) => {\n        // Throwing an error doesn't register as stopping the test. Instead, run an\n        // expect() that will fail and show us the error. We still need to call done()\n        // afterwards, or it will take the full timeout to fail.\n        expect(error).toBeUndefined()\n        done()\n      })\n    }\n    done()\n    return resp\n  }\n\n  _makeEachOrAllFnAsync(jasmineEachFn) {\n    const self = this;\n    return (userFn) => {\n      return jasmineEachFn(function asyncEachFn(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n\n  _makeItAsync(jasmineIt) {\n    const self = this;\n    return (desc, userFn) => {\n      return jasmineIt(desc, function asyncIt(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cloud-api/spec/jasmine/polyfills.es6",
    "content": "// We use Jasmine 1 in the client tests and Jasmine 2 in the cloud tests,\n// but isomorphic-core tests need to be run in both environments. Tests in\n// isomorphic-core should use Jasmine 1 syntax, and then we can add polyfills\n// here to make sure that they exist when we run in a Jasmine 2 environment.\n\nexport default function applyPolyfills() {\n  const origSpyOn = global.spyOn;\n  // There's no prototype to modify, so we have to modify the return\n  // values of spyOn as they're created.\n  global.spyOn = (object, methodName) => {\n    const originalValue = object[methodName]\n    const spy = origSpyOn(object, methodName)\n    object[methodName].originalValue = originalValue;\n    spy.andReturn = spy.and.returnValue;\n    spy.andCallFake = spy.and.callFake;\n    Object.defineProperty(spy.calls, 'length', {get: function getLength() { return this.count(); }})\n    return spy;\n  }\n}\n"
  },
  {
    "path": "packages/cloud-api/spec/metatdata-spec.es6",
    "content": "import {DatabaseConnector} from 'cloud-core'\nimport registerMetadataRoutes from '../src/routes/metadata'\nimport Sentry from '../src/sentry'\nimport {getMockServer} from './helpers'\n\ndescribe(\"Metadata route\", () => {\n  beforeEach(async function beforeEach() {\n    this.server = getMockServer()\n    registerMetadataRoutes(this.server)\n    const {Account} = await DatabaseConnector.forShared()\n    const account = await Account.create({id: 'test-account'})\n\n    const upsertPath = '/metadata/{objectId}/{pluginId}'\n    this.upsertRoute = this.server.routes.find(route => route.path === upsertPath)\n    this.baseRequest = {\n      auth: {\n        credentials: { account },\n      },\n      payload: {\n        version: 0,\n        objectType: 'message',\n        value: `{\"key\": \"value\"}`,\n      },\n      params: {\n        pluginId: 'test-plugin',\n        objectId: '129387',\n      },\n    }\n  })\n\n  it(\"creates new metadata\", async function it() {\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(this.baseRequest, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error).toBeUndefined()\n\n    const {Metadata} = await DatabaseConnector.forShared()\n    const metadata = await Metadata.findAll();\n    expect(metadata.length).toEqual(1)\n    expect(metadata[0].pluginId).toEqual('test-plugin')\n    expect(metadata[0].objectId).toEqual('129387')\n    expect(metadata[0].value).toEqual({key: 'value'})\n  })\n\n  it(\"updates existing metadata\", async function it() {\n    const {Metadata} = await DatabaseConnector.forShared()\n    Metadata.create({\n      accountId: this.baseRequest.auth.credentials.account.id,\n      pluginId: this.baseRequest.params.pluginId,\n      objectId: this.baseRequest.params.objectId,\n      objectType: this.baseRequest.payload.objectType,\n      value: {foo: \"bar\"},\n    })\n    const prevMetadata = await Metadata.findAll()\n    expect(prevMetadata.length).toEqual(1)\n\n    const request = Object.assign({}, this.baseRequest)\n    request.payload.version = 1\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(request, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error).toBeUndefined()\n\n    const afterMetadata = await Metadata.findAll();\n    expect(afterMetadata.length).toEqual(1)\n    expect(afterMetadata[0].pluginId).toEqual('test-plugin')\n    expect(afterMetadata[0].objectId).toEqual('129387')\n    expect(afterMetadata[0].value).toEqual({key: 'value'})\n  })\n\n  it(\"returns error for bad `value`\", async function it() {\n    const request = Object.assign({}, this.baseRequest)\n    request.payload.value = 'non-json string'\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(this.baseRequest, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error.includes(\"Invalid Request\")).toEqual(true)\n  })\n\n  it(\"returns error for bad `expiration`\", async function it() {\n    const request = Object.assign({}, this.baseRequest)\n    request.payload.value = '{\"expiration\": \"this is not a date\"}'\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(this.baseRequest, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error.includes(\"Invalid Request\")).toEqual(true)\n  })\n\n  it(\"updates equivalent thread metadata\", async function it() {\n    const {Metadata} = await DatabaseConnector.forShared()\n    Metadata.create({\n      accountId: this.baseRequest.auth.credentials.account.id,\n      pluginId: this.baseRequest.params.pluginId,\n      objectId: 't:1',\n      objectType: 'thread',\n      value: {foo: \"bar\"},\n    })\n    const prevMetadata = await Metadata.findAll()\n    expect(prevMetadata.length).toEqual(1)\n\n    const request = Object.assign({}, this.baseRequest)\n    request.params.objectId = 't:7'\n    request.payload.objectType = 'thread'\n    request.payload.messageIds = ['1', '7']\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(request, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error).toBeUndefined()\n\n    const afterMetadata = await Metadata.findAll();\n    expect(afterMetadata.length).toEqual(1)\n    expect(afterMetadata[0].pluginId).toEqual('test-plugin')\n    expect(afterMetadata[0].objectId).toEqual('t:1')\n    expect(afterMetadata[0].value).toEqual({key: 'value'})\n  })\n\n  it(\"doesn't update non-equivalent thread metadata\", async function it() {\n    const {Metadata} = await DatabaseConnector.forShared()\n    Metadata.create({\n      accountId: this.baseRequest.auth.credentials.account.id,\n      pluginId: this.baseRequest.params.pluginId,\n      objectId: 't:1',\n      objectType: 'thread',\n      value: {foo: \"bar\"},\n    })\n    const prevMetadata = await Metadata.findAll()\n    expect(prevMetadata.length).toEqual(1)\n\n    const request = Object.assign({}, this.baseRequest)\n    request.params.objectId = 't:7'\n    request.payload.objectType = 'thread'\n    request.payload.messageIds = ['5', '7']\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(request, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error).toBeUndefined()\n\n    const afterMetadata = await Metadata.findAll();\n    expect(afterMetadata.length).toEqual(2)\n    expect(afterMetadata[0].pluginId).toEqual('test-plugin')\n    expect(afterMetadata[0].objectId).toEqual('t:1')\n    expect(afterMetadata[0].value).toEqual({foo: 'bar'})\n  })\n\n  it(\"doesn't merge equivalent threads with different plugin ids\", async function it() {\n    const {Metadata} = await DatabaseConnector.forShared()\n    Metadata.create({\n      accountId: this.baseRequest.auth.credentials.account.id,\n      pluginId: 'other-plugin',\n      objectId: 't:1',\n      objectType: 'thread',\n      value: {foo: \"bar\"},\n    })\n    const prevMetadata = await Metadata.findAll()\n    expect(prevMetadata.length).toEqual(1)\n\n    const request = Object.assign({}, this.baseRequest)\n    request.params.objectId = 't:1'\n    request.payload.objectType = 'thread'\n    request.payload.messageIds = ['1']\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(request, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error).toBeUndefined()\n\n    const afterMetadata = await Metadata.findAll();\n    expect(afterMetadata.length).toEqual(2)\n    expect(afterMetadata[0].pluginId).toEqual('other-plugin')\n    expect(afterMetadata[0].objectId).toEqual('t:1')\n    expect(afterMetadata[0].value).toEqual({foo: 'bar'})\n  })\n\n  it(\"reconciles thread metadata when it receives a missing message link\",\n    async function it() {\n      const {Metadata} = await DatabaseConnector.forShared()\n      Metadata.create({\n        accountId: this.baseRequest.auth.credentials.account.id,\n        pluginId: this.baseRequest.params.pluginId,\n        objectId: 't:1',\n        objectType: 'thread',\n        value: {foo: \"bar\"},\n      })\n      Metadata.create({\n        accountId: this.baseRequest.auth.credentials.account.id,\n        pluginId: this.baseRequest.params.pluginId,\n        objectId: 't:4',\n        objectType: 'thread',\n        value: {hello: \"world\"},\n      })\n      const prevMetadata = await Metadata.findAll()\n      expect(prevMetadata.length).toEqual(2)\n\n      const request = Object.assign({}, this.baseRequest)\n      request.params.objectId = 't:7'\n      request.payload.objectType = 'thread'\n      request.payload.messageIds = ['1', '4', '7']\n      const reply = await new Promise((resolve, reject) => {\n        try {\n          this.upsertRoute.handler(request, resolve)\n        } catch (error) {\n          reject(error)\n        }\n      })\n      expect(reply.error).toBeUndefined()\n\n      const afterMetadata = await Metadata.findAll();\n      expect(afterMetadata.length).toEqual(1)\n      expect(afterMetadata[0].pluginId).toEqual('test-plugin')\n      expect(afterMetadata[0].objectId).toEqual('t:1')\n      expect(afterMetadata[0].value).toEqual({key: 'value', foo: 'bar', hello: 'world'})\n    }\n  )\n\n  // \"right\" means values from all equivalent metadata, and the latest value if\n  // there's a key conflict. Also tests that sentry is called if there is a key\n  // conflict\n  it(\"uses the right values when reconciling threads\", async function it() {\n    spyOn(Sentry, \"captureException\")\n\n    const {Metadata} = await DatabaseConnector.forShared()\n    await Metadata.create({\n      accountId: this.baseRequest.auth.credentials.account.id,\n      pluginId: this.baseRequest.params.pluginId,\n      objectId: 't:1',\n      objectType: 'thread',\n      value: {foo: \"bar\", some: 'thing'},\n    })\n    await Metadata.create({\n      accountId: this.baseRequest.auth.credentials.account.id,\n      pluginId: this.baseRequest.params.pluginId,\n      objectId: 't:4',\n      objectType: 'thread',\n      value: {foo: \"baz\", other: 'thing'},\n    })\n    await Metadata.create({\n      accountId: this.baseRequest.auth.credentials.account.id,\n      pluginId: this.baseRequest.params.pluginId,\n      objectId: 't:11',\n      objectType: 'thread',\n      value: {foo: \"boom\", hello: 'world'},\n    })\n    const prevMetadata = await Metadata.findAll()\n    expect(prevMetadata.length).toEqual(3)\n\n    const request = Object.assign({}, this.baseRequest)\n    request.params.objectId = 't:7'\n    request.payload.objectType = 'thread'\n    request.payload.messageIds = ['1', '4', '7', '11']\n    const reply = await new Promise((resolve, reject) => {\n      try {\n        this.upsertRoute.handler(request, resolve)\n      } catch (error) {\n        reject(error)\n      }\n    })\n    expect(reply.error).toBeUndefined()\n    expect(Sentry.captureException).toHaveBeenCalled()\n\n    const afterMetadata = await Metadata.findAll();\n    expect(afterMetadata.length).toEqual(1)\n    expect(afterMetadata[0].pluginId).toEqual('test-plugin')\n    expect(afterMetadata[0].objectId).toEqual('t:1')\n    expect(afterMetadata[0].value).toEqual({\n      key: 'value',\n      foo: 'boom',\n      some: 'thing',\n      other: 'thing',\n      hello: 'world',\n    })\n  })\n})\n"
  },
  {
    "path": "packages/cloud-api/spec/run.es6",
    "content": "import {DatabaseConnector} from 'cloud-core'\nimport {executeJasmine} from 'isomorphic-core'\nimport {getTestDatabase, destroyTestDatabase} from './helpers'\n\nexecuteJasmine({\n  beforeEach: () => {\n    global.Logger = console;\n    spyOn(DatabaseConnector, 'forShared').and.callFake(getTestDatabase)\n  },\n  afterEach: async () => {\n    await destroyTestDatabase();\n  },\n})\n"
  },
  {
    "path": "packages/cloud-api/src/authentication.es6",
    "content": "import request from 'request-promise';\nimport {DatabaseConnector} from 'cloud-core';\n\nlet db = null // cache this.\n\n/**\n * This is the validateFunc for https://github.com/hapijs/hapi-auth-basic\n *\n * API requests are of the form:\n * https://username:pass@n1.nylas.com/some/route\n *\n * The username field is the AccountToken of the N1 Cloud Account (aka\n * email account)\n *\n * The password field is the NylasID token. We no longer check the NylasID\n * token since this creates an unnecessary coupling between the Nylas Mail\n * Cloud APIs and the Redwood Billing database that stores the Nylas ID.\n * Given our usage, a valid account token is sufficient protection against\n * unfettered use of the cloud APIs.\n *\n * Then cb callback param must be called with the signature of:\n * function(err, isValid, credentials)\n */\nexport async function apiAuthenticate(req, username, password, cb) {\n  const accountToken = username\n  if (!db) { db = await DatabaseConnector.forShared() }\n  const token = await db.AccountToken.find({where: {value: accountToken}})\n  if (!token) return cb(null, false, {});\n  const account = await token.getAccount();\n  req.logger = req.logger.forAccount(account);\n  return cb(null, true, {account});\n}\n"
  },
  {
    "path": "packages/cloud-api/src/decorators/error-format.es6",
    "content": "export default function registerErrorFormatDecorator(server) {\n  server.ext('onPreResponse', (request, reply) => {\n    if (request.response && request.response.isBoom) {\n      request.response.reformat();\n      return reply.continue()\n    }\n    return reply.continue()\n  })\n}\n"
  },
  {
    "path": "packages/cloud-api/src/decorators/logger.es6",
    "content": "export default function registerLoggerDecorator(server) {\n  server.decorate('request', 'logger', (request) => {\n    const childLogger = global.Logger.child({\n      http_method: request.method.toUpperCase(),\n      remote_addr: request.info.remoteAddress,\n      remote_port: request.info.remotePort,\n      // path includes query params; pathname does not\n      endpoint: request.url.pathname,\n      http_request: request.url.path,\n      // http://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-request-tracing.html\n      request_uid: request.headers['X-Amzn-Trace-Id'] || request.id,\n    })\n    childLogger.forAccount = (account) => global.Logger.forAccount(account, childLogger);\n    return childLogger;\n  }, {apply: true});\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/admin.es6",
    "content": "import _ from 'underscore'\nimport moment from 'moment-timezone'\nimport {DatabaseConnector} from 'cloud-core';\n\nrequire('moment-round')\n\nexport default function registerAdminRoutes(server) {\n  server.route({\n    method: \"GET\",\n    path: \"/admin\",\n    config: {\n      auth: \"static-password\",\n    },\n    handler: async (request, reply) => {\n      const tz = request.query.tz || \"America/Los_Angeles\";\n      const now = moment().tz(tz);\n      let from = moment().tz(tz).subtract(5, 'days').floor(1, 'hours');\n      if (request.query.from) from = moment.tz(request.query.from, tz).floor(1, 'hours');\n      let to = moment().tz(tz).ceil(1, 'hours');\n      if (request.query.to) to = moment.tz(request.query.to, tz).ceil(1, 'hours');\n      let step = 1;\n      if (request.query.step) step = +(request.query.step);\n      const stepUnit = request.query.stepUnit || 'hour'\n\n      const db = await DatabaseConnector.forShared();\n      const TYPES = [\n        {typeId: \"thread-snooze\", typeName: \"Snooze\"},\n        {typeId: \"send-later\", typeName: \"Send Later\"},\n        {typeId: \"send-reminders\", typeName: \"Reminders\"},\n      ]\n      const jobData = []\n      for (const {typeId, typeName} of TYPES) {\n        const jobs = await db.CloudJob.findAll({\n          limit: 5000,\n          order: 'statusUpdatedAt DESC',\n          where: {\n            type: typeId,\n            statusUpdatedAt: {\n              $gt: from.toDate(),\n            },\n          },\n        })\n\n        const allHourBins = [];\n\n        const i = moment(from).tz(tz);\n        while (i.isSameOrBefore(to) && i.isSameOrBefore(now)) {\n          allHourBins.push({\n            start: i.valueOf(),\n            dayStr: i.format(\"ddd, MMM Do\"),\n            timeStr: i.format(\"HH\"),\n            jobs: [],\n          })\n          i.add(step, stepUnit);\n        }\n\n        for (const job of jobs) {\n          const jobDate = job.statusUpdatedAt.valueOf();\n          for (const bin of allHourBins) {\n            if (jobDate <= bin.start) {\n              bin.jobs.push(job);\n              break;\n            }\n          }\n        }\n\n        const grouped = _.groupBy(allHourBins, \"dayStr\");\n        const dayBins = [];\n        for (const dayStr of Object.keys(grouped)) {\n          dayBins.push({dayStr: dayStr, hourBins: grouped[dayStr]})\n        }\n        jobData.push({\n          typeId: typeId,\n          typeName: typeName,\n          dayBins: dayBins,\n        })\n      }\n      reply.view('admin', {jobData});\n    },\n  })\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/auth.es6",
    "content": "import Joi from 'joi';\nimport Boom from 'boom';\nimport google from 'googleapis';\nimport {GmailOAuthHelpers as GAuth, DatabaseConnector} from 'cloud-core';\nimport {\n  AuthHelpers,\n  IMAPErrors,\n} from 'isomorphic-core';\n\nconst {GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URL} = process.env;\nconst SCOPES = [\n  'https://www.googleapis.com/auth/userinfo.email',  // email address\n  'https://www.googleapis.com/auth/userinfo.profile',  // G+ profile\n  'https://mail.google.com/',  // email\n  'https://www.google.com/m8/feeds',  // contacts\n  'https://www.googleapis.com/auth/calendar',  // calendar\n];\nconst OAuth2 = google.auth.OAuth2;\n\nconst upsertAccount = (accountParams, credentials) => {\n  return DatabaseConnector.forShared().then(({Account}) =>\n    Account.upsertWithCredentials(accountParams, credentials)\n  );\n}\n\n/**\n * How Gmail Auth works:\n *\n * 0. The N1 AccountSettingsPageGmail, upon mounting, (via the\n * OAuthSignInPage) opens in the user's default browser a link to\n * GET /auth/gmail?state=SOME_KEY. We use the default browser because\n * people are likely already signed in there.\n *\n * 1. the /auth/gmail route uses our Nylas Gmail Client Secret (which is\n * why we have to do Gmail auth in N1 cloud) and redirects the user to the\n * Google account sign in page where they enter their credentials and 2FA\n * (if they have any).\n *\n * 2. Upon successful auth, Gmail redirects back to GMAIL_REDIRECT_URL,\n * which is set to /auth/gmail/oauthcallback. Here we exchange the code\n * we're given for a Google OAuth token. We also access the user's Google\n * profile to extract their name and email. We finally use those\n * connection settings to log into Google via IMAP. We then create an\n * Account object on N1 Cloud and create an N1 Cloud Account token that n1\n * can use to access the account object (and credentials) on N1 Cloud.\n * Once successful we create a `PendingAuthResponse` which will allow N1\n * to access the credentials via a one-use key that it set in the OAuth\n * State parameter in step 0\n *\n * 3. N1 now polls /auth/gmail/token with the key use used in step 0. Once\n * the corresponding `PendingAuthResponse` shows up in our database, we\n * give the xoauth2 token and an N1 Cloud Account token back to N1.\n *\n * 4. The xoauth2 token that we give back to N1 only works for an hour.\n * Every hour or so N1 must request a new one via the /auth/gmail/refresh\n * endpoint. Using the N1 Cloud Account token, we can lookup the account,\n * get the original Google refresh token, then use that refresh token to\n * get a new xoauth2 token and return it to N1.\n */\nexport default function registerAuthRoutes(server) {\n  server.route({\n    method: 'POST',\n    path: '/auth',\n    config: AuthHelpers.imapAuthRouteConfig(),\n    handler: AuthHelpers.imapAuthHandler(upsertAccount),\n  });\n\n  /**\n   * Gmail Auth Step 1\n   */\n  server.route({\n    method: 'GET',\n    path: '/auth/gmail',\n    config: {\n      description: 'Redirects to Gmail OAuth',\n      tags: ['accounts'],\n      auth: false,\n      validate: {\n        query: {\n          state: Joi.string().required(),\n        },\n      },\n    },\n    async handler(request, reply) {\n      request.logger.debug(\"Redirecting to Gmail OAuth\")\n      try {\n        const oauthClient = GAuth.newOAuthClient();\n        const authUrl = oauthClient.generateAuthUrl({\n          access_type: 'offline',\n          prompt: 'select_account consent',\n          scope: SCOPES,\n          state: request.query.state,\n        });\n        reply.redirect(authUrl)\n      } catch (err) {\n        reply(Boom.wrap(err))\n      }\n    },\n  });\n\n  /**\n   * Gmail Auth Step 2\n   */\n  server.route({\n    method: 'GET',\n    path: '/auth/gmail/oauthcallback',\n    config: {\n      description: 'Authenticates a new account.',\n      tags: ['accounts'],\n      auth: false,\n      validate: {\n        query: {\n          state: Joi.string().required(),\n          code: Joi.string(),\n          error: Joi.string(),\n        },\n      },\n    },\n    async handler(request, reply) {\n      request.logger.debug('Have Google OAuth Code. Exchanging for token')\n      const code = request.query.code\n      const n1Key = request.query.state\n      const error = request.query.error // Google sometimes passes the error back here\n      let profile = {};\n      let account = {};\n\n      // See onboarding-helpers:buildGmailSessionKey()\n      const nylasId = n1Key.split(\"-----\")[0]\n\n      try {\n        const client = GAuth.newOAuthClient()\n        const tok = await GAuth.exchangeCodeForGoogleToken(client, code);\n\n        profile = await GAuth.fetchGoogleProfile(client);\n        const settings = AuthHelpers.googleSettings(tok, profile.email)\n\n        request.logger.debug(\"Resolving IMAP connection\")\n\n        settings.resolved = await GAuth.resolveIMAPSettings(settings, request.logger)\n        account = await GAuth.createCloudAccount(settings, profile)\n\n        request.logger.debug(\"Creating PendingAuthResponse\")\n        await GAuth.createPendingAuthResponse(account, settings, n1Key)\n      } catch (err) {\n        const res = {\n          state_string: n1Key,\n          google_client_id: GMAIL_CLIENT_ID,\n          redirect_uri: GMAIL_REDIRECT_URL,\n          error: err.message,\n        }\n        const logger = request.logger.child({\n          account_provider: 'gmail',\n          account_email: account.emailAddress || profile.email,\n          error: err,\n          error_message: err.message,\n          error_source: err.source,\n        })\n\n        // TODO make sure we are considering all possible errors\n        if (error === 'access_denied') {\n          res.try_again = true\n          res.access_denied = true\n          logger.error('Encountered access denied error while exchanging gmail oauth code for token')\n        } else if (err instanceof IMAPErrors.IMAPAuthenticationError) {\n          res.try_again = true\n          res.imap_auth_error = true\n          logger.error('Encountered imap auth error while exchanging gmail oauth code for token')\n        } else if (err instanceof IMAPErrors.IMAPAuthenticationTimeoutError || err instanceof IMAPErrors.IMAPConnectionTimeoutError) {\n          res.try_again = true\n          res.auth_timeout = true\n          logger.error('Encountered imap timeout error while exchanging gmail oauth code for token')\n        } else if ((err.message || '').includes(\"invalid_grant\")) {\n          res.try_again = true\n          res.invalid_grant = true\n          logger.error('Encountered invalid grant error while exchanging gmail oauth code for token')\n        } else {\n          logger.error('Encountered unknown error while exchanging gmail oauth code for token')\n        }\n\n        reply.view('gmail-auth-failure', res)\n        return\n      }\n\n      reply.view('gmail-auth-success')\n    },\n  });\n\n  /**\n   * Gmail Auth Step 3\n   * N1 continues to poll this endpoint with the original key it set in\n   * the state parameter during Step 1\n   */\n  server.route({\n    method: \"GET\",\n    path: \"/auth/gmail/token\",\n    config: {\n      auth: false,\n      validate: {\n        query: {\n          key: Joi.string().required(),\n        },\n      },\n    },\n    async handler(request, reply) {\n      const {PendingAuthResponse} = await DatabaseConnector.forShared();\n      let tokenData = null;\n      try {\n        tokenData = await PendingAuthResponse.find({where:\n          {pendingAuthKey: request.query.key},\n        })\n        if (!tokenData) {\n          return reply(Boom.notFound())\n        }\n      } catch (err) {\n        return reply(Boom.notFound())\n      }\n      if (!tokenData.response) {\n        request.logger.error(\"Error getting access token, malformed PendingAuthResponse\")\n        return reply(Boom.badImplementation(\"Malformed PendingAuthResponse\", tokenData))\n      }\n      await tokenData.destroy()\n      return reply(tokenData.response)\n    },\n  });\n\n  server.route({\n    method: \"POST\",\n    path: \"/auth/gmail/refresh\",\n    handler(request, reply) {\n      const {account} = request.auth.credentials;\n      const oauthClient = new OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URL);\n      const credentials = account.decryptedCredentials();\n      oauthClient.setCredentials({ refresh_token: credentials.refresh_token });\n      oauthClient.refreshAccessToken((err, tokens) => {\n        if (err != null) {\n          request.logger.error(err, 'Error refreshing gmail access token.');\n          reply('Backend error: could not refresh Gmail access token. Please try again.').code(400);\n          return\n        }\n\n        const res = {}\n        res.access_token = tokens.access_token;\n        res.xoauth2 = AuthHelpers.generateXOAuth2Token(account.emailAddress, tokens.access_token);\n        res.expiry_date = Math.floor(tokens.expiry_date / 1000);\n        reply(res).code(200);\n      });\n    },\n  });\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/blobs.es6",
    "content": "const AWS = require('aws-sdk');\nconst fs = require('fs');\nconst path = require('path');\n\nconst NODE_ENV = process.env.NODE_ENV || 'production'\nconst BUCKET_NAME = process.env.BUCKET_NAME\nconst AWS_ACCESS_KEY_ID = process.env.BUCKET_AWS_ACCESS_KEY_ID\nconst AWS_SECRET_ACCESS_KEY = process.env.BUCKET_AWS_SECRET_ACCESS_KEY\n\nif (NODE_ENV !== 'development' &&\n  (!BUCKET_NAME || !AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY)) {\n  throw new Error(\"You need to define S3 access credentials.\")\n}\n\nAWS.config.update({\n  accessKeyId: AWS_ACCESS_KEY_ID,\n  secretAccessKey: AWS_SECRET_ACCESS_KEY })\n\nasync function localUpload(account, data, reply) {\n  const uploadId = `${account.id}-${data.id}`;\n  const filepath = path.join(\"/\", \"tmp\", \"uploads\", uploadId);\n  const file = fs.createWriteStream(filepath, { flags: 'w+' });\n\n  file.on('error', (err) => {\n    console.error(err)\n  });\n\n  data.file.pipe(file);\n\n  data.file.on('end', (err) => {\n    if (err) {\n      reply({error: err.toString()}).code(409);\n    }\n\n    const ret = {\n      filename: data.file.hapi.filename,\n    }\n    reply(JSON.stringify(ret));\n  })\n}\n\nasync function s3Upload(account, data, reply) {\n  const uploadId = `${account.id}-${data.id}`;\n  const s3 = new AWS.S3({apiVersion: '2006-03-01'});\n\n  // This is amazing. The AWS S3 SDK won't take a stream as an input. Hapi gives\n  // us a stream, but behind the scenes it's backed by a buffer holding the whole\n  // thing in memory. We just cut the middleman and give the S3 SDK what it\n  // wants.\n  const uploadedData = data.file._data;\n  s3.putObject({\n    Bucket: BUCKET_NAME,\n    Key: uploadId,\n    Body: uploadedData,\n  }, (err, response) => {\n    if (err) {\n      reply({type: \"error\", message: \"Couldn't upload data to S3\"}).code(500);\n    } else {\n      const ret = {\n        filename: data.file.hapi.filename,\n      }\n      reply(JSON.stringify(ret));\n    }\n  })\n\n  reply({filename: data.file.hapi.filename})\n}\n\nmodule.exports = (server) => {\n  const ONE_MEG = 1048576;\n  const MAX_ATTACHMENT_SIZE = 25 * ONE_MEG;\n  server.route({\n    method: ['PUT', 'POST'],\n    path: `/blobs`,\n    config: {\n      description: `Upload a draft attachment to S3.`,\n      tags: ['drafts'],\n      payload: {\n        output: 'stream',\n        parse: true,\n        maxBytes: MAX_ATTACHMENT_SIZE, // Limit upload size to 25 Megs.\n        allow: 'multipart/form-data',\n      },\n    },\n    handler: async (request, reply) => {\n      const data = request.payload;\n      const {account} = request.auth.credentials;\n\n      if (!data.id || !data.file) {\n        reply({error: `You need to supply a file and an id`}).code(400);\n      }\n\n      if (NODE_ENV === 'development') {\n        localUpload(account, data, reply);\n      } else {\n        s3Upload(account, data, reply);\n      }\n    },\n  });\n};\n"
  },
  {
    "path": "packages/cloud-api/src/routes/delta.es6",
    "content": "import Joi from 'joi';\nimport {DatabaseConnector, PubsubConnector} from 'cloud-core';\nimport {DeltaStreamBuilder} from 'isomorphic-core'\n\nexport default function registerDeltaRoutes(server) {\n  server.route({\n    method: 'GET',\n    path: '/delta/streaming',\n    config: {\n      validate: {\n        query: {\n          cursor: Joi.string().required(),\n        },\n      },\n    },\n    handler: (request, reply) => {\n      const {account} = request.auth.credentials;\n\n      request.logger.info(\"Starting /delta/streaming\")\n\n      DeltaStreamBuilder.buildAPIStream(request, {\n        accountId: account.id,\n        cursor: request.query.cursor,\n        databasePromise: DatabaseConnector.forShared(),\n        deltasSource: PubsubConnector.observeDeltas(account.id),\n      }).then((stream) => {\n        const streamTimeout = setTimeout(() => {\n          const response = request.raw.res; // request is the hapijs handler request object\n          request.logger.info('Delta stream connection timeout.')\n          response.end();\n        }, DeltaStreamBuilder.DELTA_CONNECTION_TIMEOUT_MS);\n\n        stream.once('end', () => {\n          clearTimeout(streamTimeout);\n          return stream.close();\n        });\n\n        reply(stream)\n      });\n    },\n  });\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/honeycomb.es6",
    "content": "import {MetricsReporter} from 'isomorphic-core'\n\nexport default function registerHoneycombRoutes(server) {\n  server.route({\n    method: 'POST',\n    path: '/ingest-metrics',\n    handler: (request, reply) => {\n      MetricsReporter.sendToHoneycomb(request.payload)\n      reply({success: true})\n    },\n  });\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/link-tracking.es6",
    "content": "import {decodeRecipient} from '../tracking-utils'\nconst Joi = require('joi')\nconst {DatabaseConnector} = require('cloud-core')\n\nconst PLUGIN_NAME = 'link-tracking'\n\nfunction updateMetadata({metadata, recipient, linkIdx}) {\n  if (!metadata) {\n    throw new Error(\"No metadata found, unable to update.\")\n  }\n\n  const FIVE_MINUTES = 60 * 5 // in seconds\n  const timestamp = Date.now() / 1000\n\n  if (!metadata.value || !metadata.value.links) {\n    throw new Error('Message metadata does not have links to track!')\n  }\n  const linkMetadata = metadata.value.links[linkIdx]\n\n  // Iterate backwards until you reach older timestamps or find the same\n  // recipient with a timestamp newer than five minutes\n  for (const click of linkMetadata.click_data.slice().reverse()) {\n    if (timestamp - click.timestamp > FIVE_MINUTES) {\n      break\n    }\n    if (click.recipient === recipient) {\n      return Promise.resolve()\n    }\n  }\n\n  const links = metadata.value.links\n  links[linkIdx] = {\n    url: linkMetadata.url,\n    click_count: linkMetadata.click_count + 1,\n    click_data: linkMetadata.click_data.concat({\n      timestamp: timestamp,\n      recipient: recipient,\n    }),\n    redirect_url: linkMetadata.url,\n  }\n  return metadata.updateValue({links})\n}\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: `/link/{messageId}/{linkIdx}`,\n    config: {\n      description: `link-tracking`,\n      notes: 'Notes go here',\n      tags: ['link-tracking'],\n      auth: false,\n      validate: {\n        params: {\n          messageId: Joi.string().required(),\n          linkIdx: Joi.number().integer().required(),\n        },\n        query: {\n          redirect: Joi.string().required(),\n          recipient: Joi.string(),\n          r: Joi.string(), // The deprecated recipient param\n        },\n      },\n    },\n    async handler(request, reply) {\n      const {messageId, linkIdx} = request.params\n      let {redirect} = request.query\n\n      if (!redirect) {\n        reply('').code(404)\n      } else if (!redirect.startsWith('http://') && !redirect.startsWith('https://')) {\n        redirect = `https://${redirect}`\n      }\n      const recipient = decodeRecipient(request.query)\n\n      const {Metadata} = await DatabaseConnector.forShared()\n      const metadata = await Metadata.find({\n        where: {\n          pluginId: PLUGIN_NAME,\n          objectId: messageId,\n          objectType: 'message',\n        },\n      })\n      try {\n        await updateMetadata({metadata, recipient, linkIdx})\n      } catch (err) {\n        request.logger.error(err, 'Error tracking link')\n      } finally {\n        reply.redirect(redirect)\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/metadata.es6",
    "content": "const Joi = require('joi');\nconst {DatabaseConnector} = require('cloud-core');\nconst Serialization = require('../serialization');\nconst Sentry = require('../sentry')\n\n\nfunction upsertMetadata({account, objectId, objectType, pluginId, version, value, expiration}) {\n  return DatabaseConnector.forShared().then(({Metadata}) => {\n    return Metadata.find({\n      where: {\n        accountId: account.id,\n        objectId: objectId,\n        objectType: objectType,\n        pluginId: pluginId,\n      },\n    }).then((existing) => {\n      if (existing) {\n        if (existing.version / 1 !== version / 1) {\n          return Promise.reject(new Error(\"Version Conflict\"));\n        }\n        existing.value = value;\n        existing.expiration = expiration;\n        return existing.save();\n      }\n      return Metadata.create({\n        accountId: account.id,\n        objectId: objectId,\n        objectType: objectType,\n        pluginId: pluginId,\n        version: 0,\n        value: value,\n        expiration: expiration,\n      })\n    })\n  })\n}\n\nasync function upsertThreadMetadata(args) {\n  const {messageIds, account, pluginId, expiration} = args\n  let {objectId, value, version} = args\n  // Thread ids can be any of their message ids prefixed with \"t:\", so we need\n  // to check all of them\n  const possibleIds = messageIds.map(msgId => `t:${msgId}`)\n  const {Metadata} = await DatabaseConnector.forShared()\n  const existing = await Metadata.findAll({\n    where: {\n      objectId: possibleIds,\n      accountId: account.id,\n      objectType: 'thread',\n      pluginId: pluginId,\n    },\n    order: [['updatedAt', 'ASC']], // important for merging in the right order\n  })\n\n  if (existing.length > 0) {\n    // There is metadata for an equivalent thread already. Update this metadata\n    // instead of creating a new one.\n    objectId = existing[0].objectId;\n    version = existing[0].version\n\n    if (existing.length > 1) {\n      // There's more that one metadata entry for equivalent threads. We need\n      // to merge these all back together.\n      const values = existing.map(metadata => metadata.value);\n      const keys = values.reduce((result, item) => result.concat(Object.keys(item)), [])\n      const keySet = new Set(keys)\n      if (keySet.size !== keys.length) {\n        // This should be very rare, but data may be incorrectly overwritten here.\n        Sentry.captureException(new Error(\"Key conflict while merging thread metadata\"), {\n          pluginId: pluginId,\n          accountId: account.id,\n        })\n      }\n      // Assign the metadata values such that the latest updates are applied last.\n      value = Object.assign({}, ...values, value)\n      // Delete these metadata, excpet for the one that we will update\n      await Promise.all(existing.filter(metadata => metadata.objectId !== objectId)\n        .map(metadata => metadata.destroy()))\n    }\n  }\n\n  return upsertMetadata({\n    account,\n    objectId,\n    pluginId,\n    version,\n    value,\n    expiration,\n    objectType: 'thread',\n  })\n}\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: `/metadata`,\n    config: {\n      description: `metadata`,\n      notes: 'Notes go here',\n      tags: ['metadata'],\n      validate: {\n        query: {\n          limit: Joi.number().integer().min(1).max(2000).default(100),\n          offset: Joi.number().integer().min(0).default(0),\n        },\n      },\n      response: {\n        schema: Joi.array().items(\n          Serialization.jsonSchema('Metadata')\n        ),\n      },\n    },\n    handler: (request, reply) => {\n      const {account} = request.auth.credentials;\n\n      DatabaseConnector.forShared().then(({Metadata}) => {\n        Metadata.findAll({\n          limit: request.query.limit,\n          offset: request.query.offset,\n          where: {\n            accountId: account.id,\n          },\n        }).then((items) => {\n          reply(Serialization.jsonStringify(items));\n        })\n      })\n    },\n  });\n\n  server.route({\n    method: ['PUT', 'POST'],\n    path: `/metadata/{objectId}/{pluginId}`,\n    config: {\n      description: `Update metadata`,\n      tags: ['metadata'],\n      validate: {\n        params: {\n          objectId: Joi.string(),\n          pluginId: Joi.string(),\n        },\n        payload: {\n          objectType: Joi.string().required(),\n          version: Joi.number().integer().required(),\n          value: Joi.string().required(),\n          messageIds: Joi.array().items(Joi.string()),\n        },\n      },\n    },\n    handler: async (request, reply) => {\n      try {\n        const {account} = request.auth.credentials;\n        const {objectType, messageIds, version} = request.payload;\n        let {value} = request.payload;\n        const {pluginId, objectId} = request.params;\n        try {\n          value = JSON.parse(value);\n        } catch (e) {\n          throw new Error(\"Invalid Request: `value` is not a parseable JSON string\")\n        }\n        let expiration = null;\n        if (value.expiration) {\n          expiration = new Date(value.expiration);\n          if (isNaN(expiration.valueOf())) {\n            throw new Error(\"Invalid Request: `expiration` is not a parseable date\")\n          }\n        }\n\n        let metadata;\n        if (objectType === \"thread\") {\n          metadata = await upsertThreadMetadata({\n            account,\n            objectId,\n            pluginId,\n            version,\n            value,\n            expiration,\n            messageIds,\n          })\n        } else {\n          metadata = await upsertMetadata({\n            account,\n            objectId,\n            objectType,\n            pluginId,\n            version,\n            value,\n            expiration,\n          })\n        }\n\n        reply(Serialization.jsonStringify(metadata));\n      } catch (error) {\n        if (error.message.includes('Invalid Request')) {\n          reply({error: error.toString()}).code(400);\n        } else if (error.message.includes('Version Conflict')) {\n          reply({error: error.toString()}).code(409);\n        } else {\n          reply({error: error.toString()}).code(500)\n        }\n      }\n    },\n  })\n\n  server.route({\n    method: 'DELETE',\n    path: `/metadata/{objectId}/{pluginId}`,\n    config: {\n      description: `Delete metadata`,\n      tags: ['metadata'],\n      validate: {\n        params: {\n          objectId: Joi.number().integer(),\n          pluginId: Joi.string(),\n        },\n        payload: {\n          objectType: Joi.string(),\n          version: Joi.number().integer(),\n        },\n      },\n    },\n    handler: (request, reply) => {\n      const {account} = request.auth.credentials;\n      const {version, objectType} = request.payload;\n      const {pluginId, objectId} = request.params;\n\n      upsertMetadata({account, objectId, objectType, pluginId, version, value: null})\n      .then((metadata) => {\n        reply(Serialization.jsonStringify(metadata));\n      })\n      .catch((err) => {\n        reply({error: err.toString()}).code(409);\n      })\n    },\n  })\n};\n"
  },
  {
    "path": "packages/cloud-api/src/routes/open-tracking.es6",
    "content": "import {decodeRecipient} from '../tracking-utils'\nconst Joi = require('joi')\nconst Path = require('path')\nconst {DatabaseConnector} = require('cloud-core')\n\nconst PLUGIN_NAME = 'open-tracking'\n\nfunction updateMetadata({metadata, recipient}) {\n  if (!metadata) {\n    throw new Error(\"No metadata found, unable to update.\")\n  }\n\n  const FIVE_MINUTES = 60 * 5 // in seconds\n  const timestamp = Date.now() / 1000\n\n  if (!metadata.value || !metadata.value.open_data) {\n    metadata.value = {\n      open_count: 0,\n      open_data: [],\n    }\n  }\n\n  // Iterate backwards until you reach older timestamps or find the same\n  // recipient with a timestamp newer than five minutes\n  for (const open of metadata.value.open_data.slice().reverse()) {\n    if (timestamp - open.timestamp > FIVE_MINUTES) {\n      break\n    }\n    if (open.recipient === recipient) {\n      return Promise.resolve()\n    }\n  }\n\n  return metadata.updateValue({\n    open_count: metadata.value.open_count + 1,\n    open_data: metadata.value.open_data.concat({\n      timestamp: timestamp,\n      recipient: recipient,\n    }),\n  })\n}\n\nmodule.exports = (server) => {\n  server.route({\n    method: 'GET',\n    path: `/open/{messageId}`,\n    config: {\n      description: `open-tracking`,\n      notes: 'Notes go here',\n      tags: ['open-tracking'],\n      auth: false,\n      validate: {\n        params: {\n          messageId: Joi.string().required(),\n        },\n        query: {\n          recipient: Joi.string(),\n          r: Joi.string(), // The deprecated recipient param\n        },\n      },\n    },\n    async handler(request, reply) {\n      const {messageId} = request.params\n      const recipient = decodeRecipient(request.query)\n\n      const {Metadata} = await DatabaseConnector.forShared()\n      const metadata = await Metadata.find({\n        where: {\n          pluginId: PLUGIN_NAME,\n          objectId: messageId,\n          objectType: 'message',\n        },\n      })\n      try {\n        await updateMetadata({metadata, recipient})\n      } catch (err) {\n        request.logger.error(err, 'Error tracking open')\n      } finally {\n        reply.file(Path.join(__dirname, '../../static/images/transparent.gif'), {\n          confine: false,\n        })\n        .header('Cache-Control', 'no-cache max-age=0')\n      }\n    },\n  })\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/ping.es6",
    "content": "import Boom from 'boom'\nimport {DatabaseConnector} from 'cloud-core';\n\nexport default function registerPingRoutes(server) {\n  server.route({\n    method: 'GET',\n    path: '/ping/basic',\n    config: { auth: false },\n    handler: (request, reply) => {\n      request.logger.info('---> Pong 200')\n      reply(\"Pong\")\n    },\n  });\n\n  server.route({\n    method: 'GET',\n    path: '/ping',\n    config: { auth: false },\n    handler: async (request, reply) => {\n      request.logger.debug('---> Ping DB');\n      try {\n        const db = await DatabaseConnector.forShared();\n        await db.sequelize.query('SELECT 1');\n        reply(\"DB Okay\")\n      } catch (err) {\n        reply(Boom.wrap(err, 500));\n      }\n    },\n  });\n\n  server.route({\n    method: 'GET',\n    path: '/ping/400',\n    config: { auth: false },\n    handler: (request, reply) => {\n      request.logger.debug('---> Pong 400');\n      reply(Boom.badRequest(\"Pong bad request\", {foo: 'bar'}))\n    },\n  });\n\n  server.route({\n    method: 'GET',\n    path: '/ping/401',\n    config: { auth: false },\n    handler: (request, reply) => {\n      request.logger.info('---> Pong 401');\n      reply(Boom.unauthorized('invalid password', 'sample', { ttl: 0, cache: null, foo: 'bar' }))\n    },\n  });\n\n  server.route({\n    method: 'GET',\n    path: '/ping/500',\n    config: { auth: false },\n    handler: (request, reply) => {\n      request.logger.info('---> Pong 500');\n      reply(Boom.badImplementation(\"Broken borked\", {bad: \"news\"}))\n    },\n  });\n\n  server.route({\n    method: 'GET',\n    path: '/ping/broken',\n    config: { auth: false },\n    handler: (request) => {\n      request.logger.info('---> Pong broken');\n      throw new Error(\"Broken Code\")\n    },\n  });\n\n  server.route({\n    method: 'GET',\n    path: '/ping/downstream_error',\n    config: { auth: false },\n    handler: (request, reply) => {\n      request.logger.debug('---> Pong downstream error');\n      const downstream = new Error(\"Downstream badness\");\n      reply(Boom.wrap(downstream, 400, \"Extra info here\"));\n    },\n  });\n}\n"
  },
  {
    "path": "packages/cloud-api/src/routes/static.es6",
    "content": "const Path = require('path')\n\nexport default function registerStaticRoutes(server) {\n  server.route({\n    method: 'GET',\n    path: '/static/{file*}',\n    config: {\n      auth: false,\n    },\n    handler: {\n      directory: {\n        path: Path.join(__dirname, '../../static/'),\n        redirectToSlash: true,\n        listing: true,\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "packages/cloud-api/src/sentry.es6",
    "content": "// Widely inspired by https://github.com/bendrucker/hapi-raven\n\nconst Raven = require('raven')\n\nexports.register = (server, options, next) => {\n  Raven.config(options.dsn, options.client).install();\n  server.expose('client', Raven);\n  server.on('request-error', (request, err) => {\n    const baseUrl = request.info.uri ||\n      request.info.host && `${server.info.protocol}://${request.info.host}` ||\n      server.info.uri\n\n    Raven.captureException(err, {\n      request: {\n        method: request.method,\n        query_string: request.query,\n        headers: request.headers,\n        cookies: request.state,\n        url: baseUrl + request.path,\n      },\n      extra: {\n        timestamp: request.info.received,\n        id: request.id,\n        remoteAddress: request.info.remoteAddress,\n      },\n      tags: options.tags,\n    })\n  })\n\n  next();\n}\n\nexports.register.attributes = {\n  name: 'sentry-plugin',\n  version: '1.0.0',\n}\n\nexports.captureException = Raven.captureException\n"
  },
  {
    "path": "packages/cloud-api/src/serialization.js",
    "content": "const Joi = require('joi');\n\nfunction replacer(key, value) {\n  // force remove any disallowed keys here\n  return value;\n}\n\nfunction jsonSchema(modelName) {\n  const models = ['Metadata']\n\n  if (models.includes(modelName)) {\n    return Joi.object();\n  }\n  if (modelName === 'Error') {\n    return Joi.object().keys({\n      message: Joi.string(),\n      type: Joi.string(),\n    })\n  }\n  return null;\n}\n\nfunction jsonStringify(models) {\n  return JSON.stringify(models, replacer, 2);\n}\n\nmodule.exports = {\n  jsonSchema,\n  jsonStringify,\n}\n"
  },
  {
    "path": "packages/cloud-api/src/tracking-utils.es6",
    "content": "import atob from 'atob'\n\n/**\n * As of April 26, 2017 we encode the recipient of an open/link track in\n * the `recipient` query field as a plain url-encoded email.\n *\n * Before we used to put an encoded email in the `r` query field and\n * encoded it with the following scheme:\n *\n *     btoa(recipient.email).replace(/\\+/g,'-').replace(/\\//g, '_')\n *\n * We revert to a much more human-transparent method of encoding the\n * recipient email to allow for easier understanding of the codebase and\n * open/link tracking performance.\n */\nexport function decodeRecipient(query = {}) {\n  if (query.recipient) {\n    return query.recipient\n  }\n\n  // Legacy encoding scheme\n  if (query.r) {\n    // reverse of btoa(recipient.email).replace(/\\+/g,'-').replace(/\\//g, '_')\n    return atob(query.r.replace(/_/g, \"/\").replace(/-/g, \"+\"))\n  }\n\n  return null\n}\n"
  },
  {
    "path": "packages/cloud-api/src/views/admin.html",
    "content": "<style>\n.NEW { background: #ddd; }\n.INPROGRESS-RETRYABLE { background: #419bf9; }\n.INPROGRESS-NOTRETRYABLE { background: #419bf9; }\n.SUCCEEDED { background: #5CB346; }\n.FAILED { background: #d9534f; }\n.WAITING-TO-RETRY { background: #f0ad4e; }\n.CANCELLED { background: #333; }\n\n.thread-snooze {}\n.n1-send-later {}\n.send-reminders {}\n\n#container {\n  width: 100%;\n  height: 100%;\n  padding: 0 20px 0 20px;\n}\n\nh1 {\n  font-size: 26px;\n  margin: 0;\n}\nh2 {\n  text-align: left;\n  font-size: 20px;\n  margin: 0 0 0 5px;\n}\n\n.container-wrap {\n  height: calc(100% - 39px);\n}\n.job-area.thread-snooze {\n  height: 60%;\n}\n.job-area.n1-send-later {\n  height: 20%;\n}\n.job-area.send-reminders {\n  height: 20%;\n}\n\n.day-wrap {\n  display: flex;\n  height: calc(100% - 30px);\n}\n.day {\n  margin: 0 5px;\n  height: calc(100% - 20px);\n}\n.day h3 {\n  margin: 0;\n  max-height: 14px;\n  overflow: hidden;\n  font-family: sans-serif;\n  font-size: 12px;\n  margin-bottom: 6px;\n}\n\n.hour-wrap {\n  height: 100%;\n  display: flex;\n}\n.hour {\n  height: 100%;\n  width: 12px;\n}\n.hour h4 {\n  margin-bottom: 5px;\n  font-family: sans-serif;\n  font-size: 8px;\n  padding-top: 5px;\n}\n.hour:nth-child(even) h4 {\n  visibility: hidden;\n}\n\n.job-wrap {\n  height: calc(100% - 19px);\n  display: flex;\n  flex-direction: column-reverse;\n}\n.job {\n  margin: 1px 1px 0 1px;\n  height: 10px;\n  width: 10px;\n}\n\n</style>\n<h1>Worker Jobs</h1>\n\n<div class=\"container-wrap\">\n{{#each jobData}}\n<div class=\"job-area {{this.typeId}}\" id={{this.typeId}}>\n  <h2>{{this.typeName}}</h2>\n  <div class=\"day-wrap\">\n    {{#each this.dayBins}}\n      <div class=\"day\">\n        <div class=\"hour-wrap\">\n          {{#each this.hourBins}}\n            <div class=\"hour {{this.timeStr}}\">\n              <div class=\"job-wrap\">\n                {{#each this.jobs}}\n                  <div class=\"job {{this.status}} {{this.type}}\"\n                       title=\"{{this.foremanId}} {{this.statusUpdatedAt}} {{this.error.message}}\"></div>\n                {{/each}}\n              </div>\n              <h4>{{this.timeStr}}</h4>\n            </div>\n          {{/each}}\n        </div>\n        <h3>{{this.dayStr}}</h3>\n      </div>\n    {{/each}}\n  </div>\n</div>\n{{/each}}\n</div>\n"
  },
  {
    "path": "packages/cloud-api/src/views/gmail-auth-failure.html",
    "content": "{{!< layout/default}}\n\n<div class='nylas-logo'></div>\n<form action=\"https://accounts.google.com/o/oauth2/auth?response_type=code&state={{ state_string }}&client_id={{ google_client_id }}&redirect_uri={{ redirect_uri }}&access_type=offline&scope=https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://mail.google.com/ https://www.google.com/m8/feeds https://www.googleapis.com/auth/calendar&prompt=select_account+consent\" method=\"post\">\n  <div id=\"white-box\">\n    <div id=\"user_action\" style=\"padding:30px;\">\n\n      <h2>Action Required</h2>\n        {{#if access_denied }}\n          <p class=\"alert\">\n            You'll need to approve access in order to start syncing this account!\n          </p>\n          <p>\n            Please try again and select \"Allow\".\n          </p>\n        {{else if auth_timeout }}\n          <p class=\"alert\">\n            It took too long to authenticate your account and the request timed out. Please try again.\n          </p>\n          <p>\n            If you're seeing this error multiple times, we unfortunately\n            may not support your email provider.\n          </p>\n        {{else if imap_auth_error }}\n          <p class=\"alert\">\n            Something went wrong when trying to authenticate your account!\n          </p>\n          <h4>Make sure Gmail is enabled</h4>\n          <p>\n            Make sure Gmail is enabled for your domain. Gmail can be turned on by a Google Apps administrator using\n            <a href=\"https://support.google.com/a/answer/57919?hl=en\" target=\"_blank\">this guide</a>.\n          </p>\n          <h4>Enable IMAP Access</h4>\n          <p>\n            Please make sure IMAP access is enabled for the account.  IMAP can be enabled by a Google Apps administrator using\n            <a href=\"https://support.google.com/a/answer/105694?hl=en\" target=\"_blank\">this guide</a>.\n          </p>\n          <h4>Enable IMAP for All Mail and Trash labels</h4>\n          <p>\n            Make sure \"Show in IMAP\" is checked for the \"All Mail\" and \"Trash\" labels.\n          </p>\n          <p><a href=\"https://mail.google.com/mail/#settings/labels\" target=\"_blank\"><img src=\"/static/images/all_trash_imap.png\"/></a></p>\n        {{else if invalid_grant }}\n          <p class=\"alert\">\n              Couldn't get an authorization token from Google! Please try authenticating again.\n          </p>\n        {{else}}\n          <p class=\"alert\">\n            An unknown error occured when authorizing your account.\n          </p>\n        {{/if}}\n\n        {{#if try_again}}\n          <input class=\"btn\" type=\"submit\" value=\"Try Again\" />\n        {{/if}}\n\n    </div>\n  </div>\n\n</form>\n"
  },
  {
    "path": "packages/cloud-api/src/views/gmail-auth-success.html",
    "content": "{{!< layout/default}}\n\n<div class='nylas-logo'></div>\n  <div id='clear-box'>\n    <h1> You're all set!</h1>\n    <p>Go back to Nylas Mail to finish linking your account and configuring the app.</p>\n  </div>\n\n  <div class='github-cta'>\n    <img class='gh-image' src=\"/static/images/gh-120px.png\">\n       <div class='gh-star'>\n       <a class=\"github-button\" href=\"https://github.com/nylas/nylas-mail\" data-icon=\"octicon-star\" data-style=\"mega\" data-count-href=\"/nylas/n1/stargazers\" data-count-api=\"/repos/nylas/n1#stargazers_count\" data-count-aria-label=\"# stargazers on GitHub\" aria-label=\"Star nylas/n1 on GitHub\">Star</a>\n       </div>\n       <p style='margin-top: 0px;'>\n       <strong>Did you know Nylas Mail is open source?</strong></br>\n         Show your support by starring us on GitHub!\n       </p>\n  </div>\n</div>\n\n<style>\n.github-cta {\n  margin: auto;\n  margin-top: 70px;\n  border-radius: 10px;\n  padding: 15px;\n  display: inline-block;\n  text-align: left;\n  width: 450px;\n  margin-left: -25px;\n}\n\n.gh-star {\n  padding-top: 10px;\n  margin-top: 12px;\n}\n\n.gh-image {\n  float: left;\n  margin-top: 3px;\n  padding-right: 20px;\n  height: 120px;\n  opacity: .85;\n}\n</style>\n\n<script async defer src=\"https://buttons.github.io/buttons.js\"></script>\n"
  },
  {
    "path": "packages/cloud-api/src/views/layout/default.html",
    "content": "<!DOCTYPE html>\n<html lang='en'>  \n<head>  \n  <meta charset=\"utf-8\" />\n  <meta name=\"viewport\" content=\"width=device-width, user-scalable=no\" />\n\n  <title>Nylas</title>\n\n  <link rel='stylesheet' href='/static/fonts/Avenir.css'>\n  <link rel='stylesheet' href='/static/css/index.css'>\n  <link rel='stylesheet' href='///fonts.googleapis.com/css?family=Open+Sans:300,400,600' type='text/css'>\n</head>  \n\n<body class='nylas-gradient-bg'>\n    <div id=\"container\">\n        {{{content}}}\n    </div>\n</body>  \n</html>  \n"
  },
  {
    "path": "packages/cloud-api/static/css/index.css",
    "content": "p {\n  color: #666;\n  font-family: sans-serif;\n  font-weight:300;\n  line-height: 1.5em;\n}\na {\n  color:#419bf9;\n}\nstrong {\n  font-weight:500;\n  font-family:sans-serif;\n}\n\n.alert {\n    text-align:center;\n    display:block;\n    text-shadow:0 1px rgba(255,255,255,0.7);\n    color: #BB4945;\n    background-color: #F2DEDE;\n    border:1px solid #EFD3D7;\n    padding: 16px;\n    font-size: 14px;\n    margin-bottom: 20px;\n}\n\n.alert p:first-child {\n    margin-top: 0px;\n    margin-bottom: 0px;\n}\n.btn {\n  outline:0;\n  padding: 0.44em 1em;\n  border-radius: 4px;\n  border: 0;\n  cursor: default;\n  display: inline-block;\n  color: #231f20;\n  background: linear-gradient(to top, rgba(241, 241, 241, 0.75) 0%, rgba(253, 253, 253, 0.75) 100%);\n  box-shadow: 0 0.5px 0 rgba(0, 0, 0, 0.15), 0 -0.5px 0 rgba(0, 0, 0, 0.15), 0.5px 0 0 rgba(0, 0, 0, 0.15), -0.5px 0 0 rgba(0, 0, 0, 0.15), 0 1px 1px rgba(0, 0, 0, 0.15);\n  line-height: 1.3em;\n  font-size: 15px;\n  text-decoration: none;\n}\n\n.btn:active {\n  cursor: default;\n  background-color: rgba(230, 230, 230, 0.75);\n  box-shadow: 0 0.5px 0 rgba(0,0,0,0.15), 0 -0.5px 0 rgba(0,0,0,0.15), 0.5px 0 0 rgba(0,0,0,0.15), -0.5px 0 0 rgba(0,0,0,0.15), 0 -1px 1px rgba(0, 0, 0, 0.21);\n}\n.btn:focus {\n  outline: none\n}\n\nhtml, body {\n    font-family:sans-serif;\n    background-color:#F7F7F7;\n    height: 100%;\n    border:0;\n    padding:0;\n    margin:0;\n}\n\nbody.nylas-gradient-bg {\n  background-color: #F6F6F6;\n  background-size: cover;\n  color: white;\n}\n\n.nylas-logo {\n  width: 90px;\n  height: 93px;\n  background: url(\"/static/images/logo@2x.png\") top left no-repeat;\n  background-size: cover;\n  margin: auto;\n  margin-bottom: 60px;\n}\n\nh1 {\n    padding:0; margin:0; margin-bottom:30px; font-family: 'FaktPro-Black', sans-serif; color:#393939; font-size: 40pt; font-weight: 300; text-align:center;\n}\nh2 {\n    padding:0; margin:0; margin-bottom:20px; font-family: 'FaktPro-Black', sans-serif; font-size:24pt; font-weight:300; color:#393939; text-align:center;\n}\nh3 {\n    padding:0; margin:0; margin-bottom:15px; font-family: 'FaktPro-Black', sans-serif; font-weight:300; color:#393939; text-align:left;\n}\nh4 {\n    padding:0; padding-top: 10px; margin:0; margin-bottom:10px; font-family: 'FaktPro-Black', sans-serif; font-size:16px; font-weight:300; color:#393939; text-align:center;\n}\ninput {\n    font-family: sans-serif;\n}\ninput.field {\n    width:100%; margin-bottom:20px; border:1px solid #D3D3D3; font-size:14px; padding:9px;\n}\ninput.host-field {\n    width:75%; margin-bottom:20px; margin-right:3%; border:1px solid #D3D3D3; font-size:14px; padding:9px;\n}\ninput.port-field {\n    width:22%; margin-bottom:20px; border:1px solid #D3D3D3; font-size:14px; padding:9px;\n}\n#footer {\n    border-top:1px solid #EBEBEB; background-color:#FBFBFB; padding:20px; padding-left:30px; padding-right:30px; text-align:center;\n}\n#provider-logo {\n    width:100px; height:80px; margin-bottom:30px; display:inline-block; text-align:center;\n}\n#container {\n    margin:auto; padding-top:10%; width:420px; text-align:center;\n}\n\n#white-box {\n    border:1px solid #D3D3D3;\n    background-color:white;\n    box-shadow:0 2px 3px rgba(0,0,0,0.05);\n}\n\n#clear-box {\n  margin:auto;\n  text-align:center;\n}\n\n#powered-by {\n    font-weight:400; color:#ccc; margin-top:40px;\n}\na.show_toggle {\n    font-size:13px;\n    color: #999;\n}\na.explain {\n    font-size:13px;\n    color: #999;\n}\na.error-help {\n    font-size: 13px;\n    color: #BB4945;\n}\n.help {\n    text-align:center; display:none; text-shadow:0 1px rgba(255,255,255,0.7); color: #333; background-color: #eee; border:1px solid #aaa; padding: 9px; font-size: 14px; margin-bottom: 20px; position: absolute; width: 240px;\n}\n\n#provider_choice input[type=radio]:not(old){\n    opacity : 0;\n}\n\n#user_action p {\n    font-size: 14px;\n}\n\n#loading {\n    display:none; text-shadow:0 1px rgba(255,255,255,0.7); color: #333; background-color: #eee; border:1px solid #aaa; padding: 9px; font-size: 14px; margin-bottom: 20px; position: absolute; width: 147px;\n    text-align: left;\n    padding-left: 100px;\n}\n#reset-container {\n    margin-top: 15px;\n}\n#reset-container a {\n    font-size:13px;\n    color: #999;\n}\n@media (max-width: 480px) {\n    body {\n        padding:0; margin:0;\n    }\n    #provider-logo {\n        margin-bottom: 0;\n    }\n    #container {\n        margin-top:0;\n        width:100%;\n    }\n    #white-box {\n        border-left:0;\n        border-right:0;\n    }\n    input.field {\n        font-size:18px;\n    }\n    input.btn {\n        width:100%;\n        font-size:18px;\n    }\n}\n"
  },
  {
    "path": "packages/cloud-api/static/fonts/Avenir.css",
    "content": "/**\n * @license\n * MyFonts Webfont Build ID 2958943, 2015-01-23T23:27:44-0500\n * \n * The fonts listed in this notice are subject to the End User License\n * Agreement(s) entered into by the website owner. All other parties are \n * explicitly restricted from using the Licensed Webfonts(s).\n * \n * You may obtain a valid license at the URLs below.\n * \n * Webfont: AvenirLTStd-MediumOblique by Linotype\n * URL: http://www.myfonts.com/fonts/linotype/avenir/65-mediumoblique/\n * Copyright: Copyright &#x00A9; 1981 - 2006 Linotype GmbH, www.linotype.com. All rights reserved. Copyright &#x00A9; 1989 - 2002 Adobe Systems Incorporated.  All Rights Reserved.\n * \n * Webfont: AvenirLTStd-Medium by Linotype\n * URL: http://www.myfonts.com/fonts/linotype/avenir/65-medium/\n * Copyright: Copyright &#x00A9; 1981 - 2006 Linotype GmbH, www.linotype.com. All rights reserved. Copyright &#x00A9; 1989 - 2002 Adobe Systems Incorporated.  All Rights Reserved.\n * \n * Webfont: AvenirLTStd-Light by Linotype\n * URL: http://www.myfonts.com/fonts/linotype/avenir/35-light/\n * Copyright: Copyright &#x00A9; 1989, 1995, 2002 Adobe Systems Incorporated.  All Rights Reserved. &#x00A9; 1981, 1995, 2002 Heidelberger Druckmaschinen AG. All rights reserved.\n * \n * Webfont: AvenirLTStd-Black by Linotype\n * URL: http://www.myfonts.com/fonts/linotype/avenir/95-black/\n * Copyright: Copyright &#x00A9; 1989, 1995, 2002 Adobe Systems Incorporated.  All Rights Reserved. &#x00A9; 1981, 1995, 2002 Heidelberger Druckmaschinen AG. All rights reserved.\n * \n * \n * License: http://www.myfonts.com/viewlicense?type=web&buildid=2958943\n * Licensed pageviews: 250,000\n * \n * © 2015 MyFonts Inc\n*/\n\n\n/* @import must be at top of file, otherwise CSS will not work */\n@import url(\"//hello.myfonts.net/count/2d265f\");\n\n  \n@font-face {font-family: 'AvenirLTStd-MediumOblique';src: url('webfonts/2D265F_0_0.eot');src: url('webfonts/2D265F_0_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2D265F_0_0.woff2') format('woff2'),url('webfonts/2D265F_0_0.woff') format('woff'),url('webfonts/2D265F_0_0.ttf') format('truetype');}\n \n  \n@font-face {font-family: 'AvenirLTStd-Medium';src: url('webfonts/2D265F_1_0.eot');src: url('webfonts/2D265F_1_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2D265F_1_0.woff2') format('woff2'),url('webfonts/2D265F_1_0.woff') format('woff'),url('webfonts/2D265F_1_0.ttf') format('truetype');}\n \n  \n@font-face {font-family: 'AvenirLTStd-Light';src: url('webfonts/2D265F_2_0.eot');src: url('webfonts/2D265F_2_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2D265F_2_0.woff2') format('woff2'),url('webfonts/2D265F_2_0.woff') format('woff'),url('webfonts/2D265F_2_0.ttf') format('truetype');}\n \n  \n@font-face {font-family: 'AvenirLTStd-Black';src: url('webfonts/2D265F_3_0.eot');src: url('webfonts/2D265F_3_0.eot?#iefix') format('embedded-opentype'),url('webfonts/2D265F_3_0.woff2') format('woff2'),url('webfonts/2D265F_3_0.woff') format('woff'),url('webfonts/2D265F_3_0.ttf') format('truetype');}\n\n\n/* Purchased from Linotype */\n@import url(\"http://fast.fonts.net/lt/1.css?apiType=css&c=a583385f-c01c-436f-a213-75a8dd3d9630&fontids=1437416,1437496,1437516\");\n\n@font-face{\nfont-family:\"Avenir Next LT W04 Ultra Light\";\nsrc:url(\"webfonts/1437416/1c2faa72-b1b1-421e-bc47-43ebd67cc8bd.eot?#iefix\");\nsrc:url(\"webfonts/1437416/1c2faa72-b1b1-421e-bc47-43ebd67cc8bd.eot?#iefix\") format(\"eot\"),url(\"webfonts/1437416/599179d8-2203-4a9f-b271-010a707271dc.woff2\") format(\"woff2\"),url(\"webfonts/1437416/3b0370cd-39d6-4a24-9c65-60787db0ebe0.woff\") format(\"woff\"),url(\"webfonts/1437416/55f63dc4-546d-4886-95bd-d2360a19f485.ttf\") format(\"truetype\"),url(\"webfonts/1437416/045bc73e-b69c-4a8e-86db-808e585eca39.svg#045bc73e-b69c-4a8e-86db-808e585eca39\") format(\"svg\");\n}\n\n@font-face{\nfont-family:\"Avenir Next LT W04 Regu1437496\";\nsrc:url(\"webfonts/1437496/483d8937-5e17-4378-9c51-aa91a3d9e1eb.eot?#iefix\");\nsrc:url(\"webfonts/1437496/483d8937-5e17-4378-9c51-aa91a3d9e1eb.eot?#iefix\") format(\"eot\"),url(\"webfonts/1437496/ed4b9060-b5ab-4379-8840-0b50a15258b7.woff2\") format(\"woff2\"),url(\"webfonts/1437496/9b47db0b-77fb-4bb0-b5c2-3c131a36fc4d.woff\") format(\"woff\"),url(\"webfonts/1437496/9c8b7e5f-b3ca-435d-a197-b3dfeae277a1.ttf\") format(\"truetype\"),url(\"webfonts/1437496/0f72ee75-31c8-42ba-b262-3e13b83a8fdf.svg#0f72ee75-31c8-42ba-b262-3e13b83a8fdf\") format(\"svg\");\n}\n\n@font-face{\nfont-family:\"Avenir Next LT W04 Demi1437516\";\nsrc:url(\"webfonts/1437516/47d79f32-82c5-4a74-9646-5150297aabc1.eot?#iefix\");\nsrc:url(\"webfonts/1437516/47d79f32-82c5-4a74-9646-5150297aabc1.eot?#iefix\") format(\"eot\"),url(\"webfonts/1437516/249228f0-61ac-40cc-a5a5-5609c9816e3f.woff2\") format(\"woff2\"),url(\"webfonts/1437516/efba18ed-80cc-49c4-997a-fbb140739d19.woff\") format(\"woff\"),url(\"webfonts/1437516/750a20ec-9242-42a8-b3bd-c4dcec552196.ttf\") format(\"truetype\"),url(\"webfonts/1437516/9505c912-495c-462c-899a-e61574ee9559.svg#9505c912-495c-462c-899a-e61574ee9559\") format(\"svg\");\n}\n"
  },
  {
    "path": "packages/cloud-core/README.md",
    "content": "# Cloud Core\n\n## Cloud Core the Library\nThis contains all shared resources for Nylas Mail Cloud services.\n\nYou may use Cloud Core through a regular import: `import cloud-core from\n'cloud-core'`\n\nIt is required as a dependency in the package.json of other modules.\n\nThis library isn't on the npm registry, but works as a dependency thanks to\n`lerna bootstrap`\n\nSee index.js for what gets explicitly exported by this library.\n\n# Cloud Infrastructure\n\nThis also contains scripts and config files used to deploy to production\ninfrastructure.\n\n# Getting Started\n\n## New to AWS:\n\n1. Create an AWS account and sign in\n\n1. Create your AWS IAM Security Credentials\n  1. Go to Console -> Home -> IAM -> Users -> {{Your Name}} ->\n     Security Credentials and click **Create access key**.\n\n     Note that your private key will only be shown upon creation! If\n     you've lost your private key you have to deactivate your old key and\n     create a new one.\n\n     You'll use your `AWS Access Key ID` and `AWS Secret Access Key` in\n     the next step to login to our AWS environment and make the\n     appropriate resources available.\n\n1. Install [AWS CLI](https://aws.amazon.com/cli/):\n  1. `brew install awscli` on Mac\n  1. `pip install --user awscli` on Linux.\n\n1. Run `aws configure` and add your AWS IAM Security Credentials (`AWS\n   Access Key ID` and `AWS Secret Access Key`)\n\n1. Install the [Elastic Benstalk CLI](https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/eb-cli3-install.html?icmpid=docs_elasticbeanstalk_console):\n  1. `brew install awsebcli` on Mac\n  1. `pip install --upgrade --user awsebcli` on Linux\n\n## New to Docker:\n\n1. Read [Understanding Docker](https://docs.docker.com/engine/understanding-docker/)\n\n1. Install [Docker](https://www.docker.com/products/overview) on your\n   machine.\n\n# Developing the Cloud Components Locally:\n\nOpen `cloud-core/pm2-dev.yml` and replace `XXXXXX` fields with values.\nYou need to generate a Google Client ID and Secret.\n\nFrom the root of the nylas-mail repository:\n\n```\nnpm install\nnpm run start-cloud\n```\n\nWe use [pm2](http://pm2.keymetrics.io/) to launch a variety of processes\n(sync, api, dashboard, processor, etc).\n\nThe `npm run start-cloud` command will run `pm2 start packages/cloud-core/pm2-dev.yml --no-daemon`\n\nYou can see the scripts that are running and their arguments in `pm2-dev.yml`\n\nThe `pm2-dev.yml` file sets up required environment variables for a dev\nenvironment. The prod environment variables are stored on the (Elastic\nBeanstalk AWS Console)[https://nylas.signin.aws.amazon.com/console].\n\nTo test to see if the basic API is up go to: `http://lvh.me:5100/ping`.\nYou should see `pong`. (`lvh.me` is a DNS hack that redirects back to 127.0.0.1.)\n\n## Debugging\n\nFrom the root of the nylas-mail repository:\n\n```\nnpm run start-cloud-debug\n```\n\nwill run `pm2 start packages/cloud-core/pm2-debug-cloud-api.yml --no-daemon`,\nwhich passes in an `--inspect` flag to the cloud-api interpreter. This will\nallow you to live debug using chrome web tools.\n\nA useful tool to automatically connect to the chrome dev tools without knowing\nthe url is\n[NIM](https://chrome.google.com/webstore/detail/nim-node-inspector-manage/gnhhdgbaldcilmgcpfddgdbkhjohddkj)\n\nYou can either set breakpoints through the inspector, or by putting `debugger;`\nstatements in your code.\n"
  },
  {
    "path": "packages/cloud-core/_n1cloud_docker_launcher.sh",
    "content": "#!/bin/bash\n# This is run from the DOCKERFILE\n# The cwd context is where the DOCKERFILE is at the root of /nylas-mail-all\n\n[ -z \"$1\" ] && echo '{\"docker_startup\": \"FAILED\", \"error\": \"must include an AWS_SERVICE_NAME as arg1\"}' && exit 1\nAWS_SERVICE_NAME=\"$1\"\n\ncase $AWS_SERVICE_NAME in \n    api|n1cloud-api)\n        APP=\"n1cloud-api\"\n        echo '{\"docker_startup\": \"'$APP'\"}'\n        ./node_modules/pm2/bin/pm2 start packages/cloud-core/pm2-prod-n1cloud-api.yml\n        ./node_modules/pm2/bin/pm2 logs --raw\n        ;;\n    workers|worker|n1cloud-worker|n1cloud-workers)\n        APP=\"n1cloud-workers\"\n        echo '{\"docker_startup\": \"'$APP'\"}'\n        ./node_modules/pm2/bin/pm2 start packages/cloud-core/pm2-prod-n1cloud-workers.yml\n        ./node_modules/pm2/bin/pm2 logs --raw\n        ;;\n    ei)\n        APP=\"executiveintro\"\n        echo '{\"docker_startup\": \"'$APP'\"}'\n        ## Uncomment these lines and update as necessary.\n        #./node_modules/pm3/bin/pm2 start packages/cloud-core/pm2-prod-ei-frontend.yml\n        #./node_modules/pm3/bin/pm2 start packages/cloud-core/pm2-prod-ei-backend.yml\n        ./node_modules/pm2/bin/pm2 logs --raw\n        ;;\n     *)\n        echo '{\"docker_startup\": \"FAILED\", \"error\": \"unknown AWS_SERVICE_NAME name '$AWS_SERVICE_NAME'\"}'\n        exit 2\n        ;;\nesac\n"
  },
  {
    "path": "packages/cloud-core/build/build-n1-cloud.js",
    "content": "const fs = require('fs-extra');\nconst glob = require('glob');\nconst path = require('path');\nconst babel = require('babel-core');\n\n// This moves us out of /packages/cloud-core/build to the nylas-mail-all root\nprocess.chdir(path.join(__dirname, \"..\", \"..\", \"..\"))\n\nfs.removeSync(\"n1_cloud_dist\")\nfs.copySync(\"packages/cloud-api\", \"n1_cloud_dist/cloud-api\")\nfs.copySync(\"packages/cloud-workers\", \"n1_cloud_dist/cloud-workers\")\n\nfs.copySync(\"packages/cloud-core\", \"n1_cloud_dist/cloud-core\")\nfs.copySync(\"packages/isomorphic-core\", \"n1_cloud_dist/isomorphic-core\")\n\nglob.sync(\"n1_cloud_dist/**/*+(.es6|.js)\", {absolute: true}).forEach((es6Path) => {\n  if (/node_modules/.test(es6Path)) return\n  const outPath = es6Path.replace(path.extname(es6Path), '.js');\n  console.log(`---> Compiling ${es6Path.slice(es6Path.indexOf(\"/n1_cloud_dist\") + 15)}`);\n\n  const babelConfig = JSON.parse(fs.readFileSync(\".babelrc\"))\n  const res = babel.transformFileSync(es6Path, {\n    presets: babelConfig.presets || [],\n    plugins: babelConfig.plugins || [],\n    sourceMaps: true,\n    sourceRoot: '/',\n    sourceMapTarget: path.relative(\"n1_cloud_dist/\", outPath),\n    sourceFileName: path.relative(\"n1_cloud_dist/\", es6Path),\n  });\n\n  fs.writeFileSync(outPath, `${res.code}\\n//# sourceMappingURL=${path.basename(outPath)}.map\\n`);\n  fs.writeFileSync(`${outPath}.map`, JSON.stringify(res.map));\n  if (/.es6$/.test(es6Path)) {\n    fs.unlinkSync(es6Path);\n  }\n});\n\n// Lerna bootstrap creates symlinks. Unfortunately it creates absolute\n// path symlinks that reference the pre-copied, uncompiled files. This\n// does a direct copy for each of the leran bootstrap links to ensure we\n// don't encounter symlink path problems on prod\n//\n// Fix cloud-core symlinks\nfs.removeSync(\"n1_cloud_dist/cloud-core/node_modules/isomorphic-core\")\nfs.copySync(\"n1_cloud_dist/isomorphic-core\", \"n1_cloud_dist/cloud-core/node_modules/isomorphic-core\")\n\n// Fix cloud-api symlinks\nfs.removeSync(\"n1_cloud_dist/cloud-api/node_modules/isomorphic-core\")\nfs.removeSync(\"n1_cloud_dist/cloud-api/node_modules/cloud-core\")\nfs.copySync(\"n1_cloud_dist/isomorphic-core\", \"n1_cloud_dist/cloud-api/node_modules/isomorphic-core\")\nfs.copySync(\"n1_cloud_dist/cloud-core\", \"n1_cloud_dist/cloud-api/node_modules/cloud-core\")\n\n// Fix cloud-workers symlinks\nfs.removeSync(\"n1_cloud_dist/cloud-workers/node_modules/isomorphic-core\")\nfs.removeSync(\"n1_cloud_dist/cloud-workers/node_modules/cloud-core\")\nfs.copySync(\"n1_cloud_dist/isomorphic-core\", \"n1_cloud_dist/cloud-workers/node_modules/isomorphic-core\")\nfs.copySync(\"n1_cloud_dist/cloud-core\", \"n1_cloud_dist/cloud-workers/node_modules/cloud-core\")\n\n"
  },
  {
    "path": "packages/cloud-core/database-connector.es6",
    "content": "const Sequelize = require('sequelize');\nconst fs = require('fs');\nconst path = require('path');\nconst {loadModels, HookIncrementVersionOnSave, HookTransactionLog} = require('isomorphic-core')\nconst PubsubConnector = require('./pubsub-connector');\n\nrequire('./database-extensions'); // Extends Sequelize on require\n\n// If we're running locally, create the sqlite directory if\n// it's not present.\nlet STORAGE_DIR;\nif (!process.env.DB_HOSTNAME) {\n  const os = require('os')\n  STORAGE_DIR = path.join(os.homedir(), '.nylas-cloud-storage');\n  try {\n    if (!fs.existsSync(STORAGE_DIR)) {\n      fs.mkdirSync(STORAGE_DIR);\n    }\n  } catch (err) {\n    global.Logger.error(err, 'Error creating storage directory')\n  }\n}\n\nclass DatabaseConnector {\n  constructor() {\n    this._cache = {};\n  }\n\n  _sequelizePoolForDatabase(dbname, {test} = {}) {\n    if (!test && process.env.DB_HOSTNAME) {\n      return new Sequelize(dbname, process.env.DB_USERNAME, process.env.DB_PASSWORD, {\n        host: process.env.DB_HOSTNAME,\n        dialect: \"mysql\",\n        dialectOptions: {\n          charset: 'utf8mb4',\n        },\n        logging: false,\n        pool: {\n          min: 1,\n          max: 15,\n          idle: 5000,\n        },\n      });\n    }\n\n    const storage = test ? ':memory:' : path.join(STORAGE_DIR, `${dbname}.sqlite`)\n    return new Sequelize(dbname, '', '', {\n      storage: storage,\n      dialect: \"sqlite\",\n      logging: false,\n    })\n  }\n\n  _sequelizeForShared(options) {\n    const sequelize = this._sequelizePoolForDatabase(process.env.DB_NAME, options);\n    const db = loadModels(Sequelize, sequelize, {\n      modelDirs: [path.join(__dirname, 'models')],\n    })\n\n    HookTransactionLog(db, sequelize, {\n      only: ['metadata'],\n      onCreatedTransaction: (transaction) => {\n        PubsubConnector.notifyDelta(transaction.accountId, transaction.toJSON());\n      },\n    });\n    HookIncrementVersionOnSave(db, sequelize);\n\n    db.sequelize = sequelize;\n    db.Sequelize = Sequelize;\n\n    return sequelize.authenticate().then(() =>\n      sequelize.sync()\n    ).thenReturn(db);\n  }\n\n  async forShared() {\n    this._cache.shared = this._cache.shared || this._sequelizeForShared();\n    return this._cache.shared;\n  }\n}\n\nmodule.exports = new DatabaseConnector()\n"
  },
  {
    "path": "packages/cloud-core/database-extensions.js",
    "content": "const Rx = require('rx-lite');\nconst Sequelize = require('sequelize');\n\nSequelize.Model.prototype.streamAll = function streamAll(options = {}) {\n  return Rx.Observable.create((observer) => {\n    const chunkSize = options.chunkSize || 1000;\n    options.offset = 0;\n    options.limit = chunkSize;\n\n    const findFn = (opts) => {\n      this.findAll(opts).then((models = []) => {\n        observer.onNext(models)\n        if (models.length === chunkSize) {\n          opts.offset = chunkSize;\n          findFn(opts)\n        } else {\n          observer.onCompleted()\n        }\n      })\n    }\n\n    findFn(options)\n  })\n}\n\n"
  },
  {
    "path": "packages/cloud-core/gmail-oauth-helpers.es6",
    "content": "import google from 'googleapis';\nimport {Provider, IMAPConnection, AuthHelpers} from 'isomorphic-core'\nimport DatabaseConnector from './database-connector'\n\nconst OAuth2 = google.auth.OAuth2;\nconst {GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URL} = process.env;\n\nclass GmailOAuthHelpers {\n  newOAuthClient() {\n    return new OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URL);\n  }\n\n  async exchangeCodeForGoogleToken(client, oAuthCode) {\n    return new Promise((resolve, reject) => {\n      client.getToken(oAuthCode, (err, googleToken) => {\n        if (err) {\n          return reject(err)\n        }\n        client.setCredentials(googleToken);\n        return resolve(googleToken)\n      })\n    })\n  }\n\n  async fetchGoogleProfile(client) {\n    return new Promise((resolve, reject) => {\n      google.oauth2({version: 'v2', auth: client})\n      .userinfo.get((err, googleProfile) => {\n        if (err) {\n          return reject(err)\n        }\n        return resolve(googleProfile)\n      })\n    })\n  }\n\n  async resolveIMAPSettings(imapSettings, logger) {\n    const imap = await IMAPConnection.connect({\n      logger: logger,\n      settings: Object.assign({},\n        imapSettings.connectionSettings,\n        imapSettings.connectionCredentials,\n      ),\n      db: {},\n    })\n    imap.end();\n    return imap.getResolvedSettings()\n  }\n\n  async createCloudAccount(imapSettings, googleProfile) {\n    const db = await DatabaseConnector.forShared()\n    return db.Account.upsertWithCredentials({\n      name: googleProfile.name,\n      provider: Provider.Gmail,\n      emailAddress: googleProfile.email,\n      connectionSettings: imapSettings.connectionSettings,\n    }, imapSettings.connectionCredentials)\n  }\n\n  async refreshAccessToken(account) {\n    const oauthClient = new OAuth2(GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET, GMAIL_REDIRECT_URL);\n    const credentials = account.decryptedCredentials()\n    const refreshToken = credentials.refresh_token;\n    oauthClient.setCredentials({ refresh_token: refreshToken });\n\n    return new Promise((resolve, reject) => {\n      oauthClient.refreshAccessToken(async (err, tokens) => {\n        if (err) {\n          reject(err);\n        }\n        const res = {}\n        res.access_token = tokens.access_token;\n        res.xoauth2 = AuthHelpers.generateXOAuth2Token(account.emailAddress,\n                                                tokens.access_token);\n        res.expiry_date = Math.floor(tokens.expiry_date / 1000);\n        const newCredentials = Object.assign(credentials, res);\n        account.setCredentials(newCredentials);\n        await account.save();\n        resolve(newCredentials);\n      });\n    });\n  }\n\n  async createPendingAuthResponse({account, token}, imapSettings, n1Key) {\n    const response = account.toJSON();\n    response.account_token = token.value;\n    response.resolved_settings = imapSettings.resolved;\n\n    const db = await DatabaseConnector.forShared()\n    return db.PendingAuthResponse.create({\n      response: JSON.stringify(response),\n      pendingAuthKey: n1Key,\n    })\n  }\n}\nexport default new GmailOAuthHelpers()\n"
  },
  {
    "path": "packages/cloud-core/index.js",
    "content": "/* eslint global-require: 0 */\nmodule.exports = {\n  DatabaseConnector: require('./database-connector'),\n  PubsubConnector: require('./pubsub-connector'),\n  Logger: require('./logger'),\n  GmailOAuthHelpers: require('./gmail-oauth-helpers').default,\n}\n"
  },
  {
    "path": "packages/cloud-core/log-streams.js",
    "content": "const stream = require('stream');\nconst PrettyStream = require('bunyan-prettystream')\n\n\nclass StringStream extends stream.Writable {\n  constructor() {\n    super();\n    this.chunks = [];\n  }\n\n  _write(chunk, enc, next) {\n    this.chunks.push(chunk);\n    next();\n  }\n\n  toString() {\n    return Buffer.concat(this.chunks).toString();\n  }\n\n  reset() {\n    this.chunks = [];\n  }\n}\n\nconst testStream = {\n  level: 'info',\n  stream: new StringStream(),\n}\n\nconst stdoutStream = {\n  level: 'info',\n  stream: process.stdout,\n}\n\nconst getLogStreams = (name, env) => {\n  switch (env) {\n    case 'development': {\n      const prettyStdOut = new PrettyStream({\n        mode: 'pm2',\n        lessThan: 'error',\n      });\n      const prettyStdErr = new PrettyStream({\n        mode: 'pm2',\n      });\n      prettyStdOut.pipe(process.stdout);\n      prettyStdErr.pipe(process.stderr);\n      return [\n        {\n          type: 'raw',\n          level: 'error',\n          stream: prettyStdErr,\n          reemitErrorEvents: true,\n        },\n        {\n          type: 'raw',\n          level: 'debug',\n          stream: prettyStdOut,\n          reemitErrorEvents: true,\n        },\n      ]\n    }\n    case 'test': {\n      return [\n        testStream,\n      ]\n    }\n    default: {\n      return [\n        stdoutStream,\n      ]\n    }\n  }\n}\n\nmodule.exports = {getLogStreams, testStream}\n"
  },
  {
    "path": "packages/cloud-core/logger.js",
    "content": "const bunyan = require('bunyan')\nconst {getLogStreams} = require('./log-streams')\nconst NODE_ENV = process.env.NODE_ENV || 'unknown'\n\n/**\n * We format our logs as JSON via Bunyan.\n * Read https://github.com/trentm/node-bunyan for more about how Bunyan\n * configures the log output.\n *\n * On production cloud infrastructure, we output logs to a flat file on the EC2\n * machine. That flat file is then uploaded to 2 cloud services:\n *\n * 1. Elasticsearch / Kibana - for raw log viewing / filtering. We have\n * filebeat installed on each AWS machine to do this automatically. SSH into a\n * cloud machine and see /etc/filebeat/filebeat.yml for details.\n *\n * 2. Honeycomb - for aggregate log statistics. We have honeytail installed on\n * each AWS machine to upload logs automatically. SSH into a cloud box and see\n * /etc/sv/honeytail/run\n *\n * From the Bunyan Docs:\n * log.info();     // Returns a boolean: is the \"info\" level enabled?\n *                 // This is equivalent to `log.isInfoEnabled()` or\n *                 // `log.isEnabledFor(INFO)` in log4j.\n *\n * log.info('hi');                     // Log a simple string message (or number).\n * log.info('hi %s', bob, anotherVar); // Uses `util.format` for msg formatting.\n *\n * log.info({foo: 'bar'}, 'hi');\n *                 // The first field can optionally be a \"fields\" object, which\n *                 // is merged into the log record.\n *\n * log.info(err);  // Special case to log an `Error` instance to the record.\n *                 // This adds an \"err\" field with exception details\n *                 // (including the stack) and sets \"msg\" to the exception\n *                 // message.\n * log.info(err, 'more on this: %s', more);\n *                 // ... or you can specify the \"msg\".\n *\n * log.info({foo: 'bar', err: err}, 'some msg about this error');\n *                 // To pass in an Error *and* other fields, use the `err`\n *                 // field name for the Error instance.\n *\n * You may use:\n * log.trace()\n * log.debug()\n * log.info()\n * log.warn()\n * log.error()\n * log.fatal()\n */\nfunction createLogger(name, env = NODE_ENV) {\n  const logger = bunyan.createLogger({\n    name,\n    env,\n    serializers: bunyan.stdSerializers,\n    streams: getLogStreams(name, env),\n  })\n\n  return Object.assign(logger, {\n    forAccount(account = {}, parentLogger = logger) {\n      return parentLogger.child({\n        account_id: account.id,\n        account_email: account.emailAddress,\n        account_provider: account.provider,\n        n1_id: account.n1IdentityToken || 'Not available',\n      });\n    },\n  });\n}\n\nmodule.exports = {\n  createLogger,\n}\n"
  },
  {
    "path": "packages/cloud-core/migrations/01-expirationDate-metadata.es6",
    "content": "module.exports = {\n  up: async (queryInterface, Sequelize) => {\n    const {sequelize} = queryInterface;\n    console.log(\"querying db\");\n    await sequelize.query(\"ALTER TABLE metadata ADD COLUMN `expiration` DATETIME\");\n  },\n  down: async (queryInterface, Sequelize) => {\n    const {sequelize} = queryInterface;\n    await sequelize.query(\"ALTER TABLE metadata DROP COLUMN `expiration`\");\n  },\n}\n"
  },
  {
    "path": "packages/cloud-core/models/cloud-job.es6",
    "content": "const {DatabaseTypes: {JSONColumn},\n       DBUtils: {MAX_INDEXABLE_LENGTH}} = require('isomorphic-core');\n\nmodule.exports = (sequelize, Sequelize) => {\n  const CloudJob = sequelize.define('CloudJob', {\n    id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },\n    accountId: {type: Sequelize.STRING(MAX_INDEXABLE_LENGTH), allowNull: false},\n    metadataId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    workerId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    foremanId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    type: {type: Sequelize.STRING(MAX_INDEXABLE_LENGTH), allowNull: false},\n    claimedAt: Sequelize.DATE,\n    statusUpdatedAt: Sequelize.DATE,\n    attemptNumber: {type: Sequelize.INTEGER, defaultValue: 0, allowNull: false},\n    retryAt: Sequelize.DATE,\n    status: {\n      type: Sequelize.ENUM(\n        \"NEW\",\n        \"INPROGRESS-RETRYABLE\",\n        \"INPROGRESS-NOTRETRYABLE\",\n        \"SUCCEEDED\",\n        \"FAILED\",\n        \"WAITING-TO-RETRY\",\n        \"CANCELLED\"\n      ),\n      defaultValue: \"NEW\",\n      allowNull: false,\n    },\n    error: JSONColumn('error'),\n  }, {\n    indexes: [\n      { fields: ['type'] },\n      { fields: ['foremanId'] },\n      { fields: ['status'] },\n      { fields: ['statusUpdatedAt'] },\n      { fields: ['attemptNumber'] },\n    ],\n  });\n\n  return CloudJob;\n};\n"
  },
  {
    "path": "packages/cloud-core/models/metadata.js",
    "content": "const _ = require('underscore')\nconst {DatabaseTypes: {JSONColumn},\n       DBUtils: {MAX_INDEXABLE_LENGTH}} = require('isomorphic-core');\n\nmodule.exports = (sequelize, Sequelize) => {\n  const Metadata = sequelize.define('metadata', {\n    id: { type: Sequelize.INTEGER, primaryKey: true, autoIncrement: true },\n    accountId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    value: JSONColumn('value', { columnType: Sequelize.LONGTEXT }),\n    version: Sequelize.INTEGER,\n    pluginId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    objectId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    objectType: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    expiration: Sequelize.DATE,\n  }, {\n    indexes: [\n      { fields: ['objectId', 'objectType'] },\n      { fields: ['expiration'] },\n      { fields: ['pluginId'] },\n    ],\n    instanceMethods: {\n      toJSON() {\n        return {\n          id: `${this.id}`,\n          value: this.value,\n          object: \"metadata\",\n          version: this.version,\n          plugin_id: this.pluginId,\n          object_id: this.objectId,\n          account_id: this.accountId,\n          object_type: this.objectType,\n        };\n      },\n      updateValue(value, {transaction} = {}) {\n        if (!_.isObject(this.value)) {\n          throw new Error(`Metadata.updateValue: \\`value\\` must be defined`)\n        }\n        this.value = Object.assign({}, this.value, value)\n        if (transaction) {\n          return this.save({transaction})\n        }\n        return sequelize.transaction((t) => {\n          return this.save({transaction: t})\n        })\n      },\n      async clearExpiration({transaction} = {}) {\n        if (!_.isObject(this.value)) {\n          throw new Error(`Metadata.clearExpiration: Can't clear expiration without a \\`value\\``)\n        }\n        // We need to update the `expiration` column, but also the `expiration`\n        // field inside our json `value` so that we generate the correct deltas\n        // for Nylas Mail\n        this.value = Object.assign({}, this.value, {expiration: null})\n        this.expiration = null\n        await this.save({transaction})\n      },\n    },\n  });\n\n  return Metadata;\n};\n"
  },
  {
    "path": "packages/cloud-core/models/pending-auth-response.js",
    "content": "module.exports = (sequelize, Sequelize) => {\n  const PendingAuthResponse = sequelize.define('pendingAuthResponse', {\n    response: Sequelize.TEXT('long'),\n    pendingAuthKey: Sequelize.STRING,\n  }, {\n    classMethods: {\n      associate: () => {\n      },\n    },\n  });\n\n  return PendingAuthResponse;\n};\n"
  },
  {
    "path": "packages/cloud-core/package.json",
    "content": "{\n  \"name\": \"cloud-core\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Core shared packages\",\n  \"main\": \"index.js\",\n  \"dependencies\": {\n    \"aws-sdk\": \"2.25.0\",\n    \"bunyan\": \"1.8.0\",\n    \"bunyan-prettystream\": \"github:emorikawa/node-bunyan-prettystream\",\n    \"googleapis\": \"9.0.0\",\n    \"isomorphic-core\": \"0.x.x\",\n    \"mysql\": \"2.12.0\",\n    \"promise-props\": \"1.0.0\",\n    \"promise.prototype.finally\": \"2.0.1\",\n    \"redis\": \"2.6.3\",\n    \"request\": \"2.79.0\",\n    \"rx-lite\": \"4.0.8\",\n    \"sequelize\": \"3.28.0\",\n    \"signalfx\": \"3.0.1\",\n    \"sqlite3\": \"3.1.8\",\n    \"underscore\": \"1.8.3\",\n    \"xoauth2\": \"1.2.0\"\n  },\n  \"devDependencies\": {\n    \"babel-cli\": \"6.x.x\",\n    \"jasmine\": \"2.x.x\"\n  },\n  \"author\": \"Nylas\",\n  \"license\": \"ISC\",\n  \"scripts\": {\n    \"test\": \"NODE_ENV=test babel-node spec/run.es6\"\n  }\n}\n"
  },
  {
    "path": "packages/cloud-core/pm2-debug-cloud-api.yml",
    "content": "apps:\n  - script   : packages/cloud-api/app.es6\n    interpreter : node_modules/.bin/babel-node\n    interpreter_args: \"--inspect\"\n    watch    : [\"packages\"]\n    name     : api\n    env      :\n      PORT: 5100\n      DB_ENCRYPTION_ALGORITHM : \"aes-256-ctr\"\n      DB_ENCRYPTION_PASSWORD : \"XXXXXXXX\"\n      GMAIL_CLIENT_ID : \"XXXXXXXXXXXXX\"\n      GMAIL_CLIENT_SECRET : \"XXXXXXXXXXXXX\"\n      GMAIL_REDIRECT_URL : \"http://localhost:5100/auth/gmail/oauthcallback\"\n      NODE_ENV: 'development'\n      HONEY_DATASET: 'n1-cloud-staging'\n      HONEY_WRITE_KEY: 'XXXXXXXXXXXXX'\n  - script   : packages/cloud-workers/app.es6\n    interpreter : node_modules/.bin/babel-node\n    interpreter_args: \"--inspect\"\n    watch    : [\"packages\"]\n    name     : workers\n    env      :\n      DB_ENCRYPTION_ALGORITHM : \"aes-256-ctr\"\n      DB_ENCRYPTION_PASSWORD : \"XXXXXXXX\"\n      NODE_ENV: 'development'\n  - script   : redis-server\n    name     : redis\n"
  },
  {
    "path": "packages/cloud-core/pm2-dev.yml",
    "content": "apps:\n  - script   : packages/cloud-api/app.es6\n    interpreter : node_modules/.bin/babel-node\n    watch    : [\"packages\"]\n    name     : api\n    env      :\n      PORT: 5100\n      DB_NAME : \"nylasmailclouddb\"\n      DB_ENCRYPTION_ALGORITHM : \"aes-256-ctr\"\n      DB_ENCRYPTION_PASSWORD : \"XXXXXX\"\n      GMAIL_CLIENT_ID : \"XXXXXXXXXXXXX\"\n      GMAIL_CLIENT_SECRET : \"XXXXXXXXXXXXX\"\n      GMAIL_REDIRECT_URL : \"http://localhost:5100/auth/gmail/oauthcallback\"\n      NODE_ENV: 'development'\n      HONEY_DATASET: 'n1-cloud-staging'\n      HONEY_WRITE_KEY: 'XXXXXXXXXXXXX'\n      BILLING_URL: 'https://billing-staging.nylas.com'\n    # billing.nylas.com is backed by a Python app from cloud-core.git; for ease\n    # of development we default to using the staging version. use this env if\n    # you're also making changes to the billing service & are running it locally.\n    env_billing_local:\n      BILLING_URL: 'http://billing.lvh.me:5555'\n  - script   : packages/cloud-workers/app.es6\n    interpreter : node_modules/.bin/babel-node\n    watch    : [\"packages\"]\n    name     : workers\n    env      :\n      DB_NAME : \"nylasmailclouddb\"\n      DB_ENCRYPTION_ALGORITHM : \"aes-256-ctr\"\n      DB_ENCRYPTION_PASSWORD : \"XXXXXXX\"\n      GMAIL_CLIENT_ID : \"XXXXXXXXXXXXX\"\n      GMAIL_CLIENT_SECRET : \"XXXXXXXXXXXXX\"\n      GMAIL_REDIRECT_URL : \"http://localhost:5100/auth/gmail/oauthcallback\"\n      NODE_ENV: 'development'\n      BILLING_URL: 'https://billing-staging.nylas.com'\n  - script   : packages/cloud-core/scripts/run-redis.sh\n    name     : redis\n"
  },
  {
    "path": "packages/cloud-core/pm2-prod-api.yml",
    "content": "apps:\n  - script   : n1_cloud_dist/cloud-api/app.js\n    name     : api\n    instances: 0\n    exec_mode: cluster\n    env      :\n      PORT: 5100\n"
  },
  {
    "path": "packages/cloud-core/pm2-prod-workers.yml",
    "content": "apps:\n  - script   : n1_cloud_dist/cloud-workers/app.js\n    name     : workers\n    instances: 1\n    exec_mode: fork\n"
  },
  {
    "path": "packages/cloud-core/pubsub-connector.js",
    "content": "const Rx = require('rx-lite')\nconst redis = require(\"redis\");\nconst {PromiseUtils} = require('isomorphic-core');\n\nPromiseUtils.promisifyAll(redis.RedisClient.prototype);\nPromiseUtils.promisifyAll(redis.Multi.prototype);\n\n\nclass PubsubConnector {\n  constructor() {\n    this._broadcastClient = null;\n    this._listenClient = null;\n    this._listenClientSubs = {};\n  }\n\n  buildClient(accountId, {onClose} = {}) {\n    const client = redis.createClient(process.env.REDIS_URL || null);\n    global.Logger.info({account_id: accountId}, \"Connecting to Redis\")\n    client.on(\"error\", (...args) => {\n      global.Logger.error(...args);\n      if (onClose) onClose();\n    });\n    client.on(\"end\", () => {\n      global.Logger.info({account_id: accountId}, \"Redis disconnected\");\n      if (onClose) onClose();\n    })\n    return client;\n  }\n\n  broadcastClient() {\n    if (!this._broadcastClient) {\n      this._broadcastClient = this.buildClient(\"broadcast\", {onClose: () => {\n        // We null out the memoized broadcast client. In case it closes\n        // for any reason, we want to make sure the next time it's\n        // requested, we'll create a new one.\n        this._broadcastClient = null\n      }});\n    }\n    return this._broadcastClient;\n  }\n\n  notifyDelta(accountId, transactionJSON) {\n    this.broadcastClient().publish(`deltas-${accountId}`, JSON.stringify(transactionJSON))\n  }\n\n  observeDeltas(accountId) {\n    return Rx.Observable.create((observer) => {\n      const sub = this.buildClient(accountId);\n      sub.on(\"message\", (channel, transactionJSONString) => {\n        observer.onNext(JSON.parse(transactionJSONString))\n      })\n      sub.subscribe(`deltas-${accountId}`);\n      return () => {\n        global.Logger.info({account_id: accountId}, \"Closing Redis\")\n        sub.unsubscribe();\n        sub.quit();\n      }\n    })\n  }\n}\n\nmodule.exports = new PubsubConnector()\n"
  },
  {
    "path": "packages/cloud-core/scripts/migrate-db.es6",
    "content": "import Umzug from 'umzug'\nimport {DatabaseConnector} from '../packages/cloud-core'\n\nasync function activate() {\n  // Perform migrations before starting sync\n  const db = await DatabaseConnector.forShared();\n\n  const umzug = new Umzug({\n    storage: 'sequelize',\n    storageOptions: {\n      sequelize: db.sequelize,\n      modelName: 'migration',\n      tableName: 'migrations',\n    },\n    migrations: {\n      path: `../migrations`,\n      params: [db.sequelize.getQueryInterface(), db.sequelize],\n      pattern: /^\\d+[\\w-]+\\.es6$/,\n    },\n    logging: console.log,\n  });\n\n  return umzug;\n}\n\nasync function upgrade() {\n  const umzug = await activate();\n  const pending = await umzug.pending();\n  if (pending.length > 0) {\n    console.log(`Running ${pending.length} migration(s).`)\n  } else {\n    console.log(`No new migrations to run.`)\n  }\n\n  return umzug.up() // run all pending migrations\n}\n\nasync function downgrade() {\n  const umzug = await activate();\n  console.log(`Running 1 down migration.`)\n\n  return umzug.down()\n}\n\nasync function main() {\n  if (process.argv.length !== 3) {\n    console.log(\"usage: migrate-db up|down\")\n  } else if (process.argv[2] === 'up') {\n    await upgrade();\n  } else if (process.argv[2] === 'down') {\n    await downgrade();\n  }\n}\n\nmain();\n"
  },
  {
    "path": "packages/cloud-core/scripts/run-redis.sh",
    "content": "#!/bin/bash\n\n# Redis is one of those servers which by default accept connections from\n# everywhere. Luckily, homebrew and presumably debian come with sane defaults.\n# However, they're located in different directories.\nif [[ $(uname) = 'Darwin' ]]; then\n    echo \"Running redis from Homebrew...\"\n    redis-server /usr/local/etc/redis.conf\nfi\n\nif [[ $(uname) = 'Linux' ]]; then\n    # redis-server package may have redis running by default; don't crash if so\n    pgrep -lf redis-server\n    if [ $? -ne 0 ]; then\n        echo \"Running redis\"\n        redis-server /etc/redis/redis.conf\n    else\n        echo \"Redis already running\"\n        sleep infinity\n    fi\nfi\n"
  },
  {
    "path": "packages/cloud-core/spec/jasmine/execute.es6",
    "content": "import Jasmine from 'jasmine'\nimport JasmineExtensions from './extensions'\n\nexport default function execute(extendOpts) {\n  const jasmine = new Jasmine()\n  jasmine.loadConfigFile('spec/jasmine/config.json')\n  const jasmineExtensions = new JasmineExtensions()\n  jasmineExtensions.extend(extendOpts)\n  jasmine.execute()\n}\n"
  },
  {
    "path": "packages/cloud-core/spec/jasmine/extensions.es6",
    "content": "import applyPolyfills from './polyfills'\n\nexport default class JasmineExtensions {\n  extend({beforeEach, afterEach} = {}) {\n    applyPolyfills()\n    global.it = this._makeItAsync(global.it)\n    global.fit = this._makeItAsync(global.fit)\n    global.beforeAll = this._makeEachOrAllFnAsync(global.beforeAll)\n    global.afterAll = this._makeEachOrAllFnAsync(global.afterAll)\n    global.beforeEach = this._makeEachOrAllFnAsync(global.beforeEach)\n    global.afterEach = this._makeEachOrAllFnAsync(global.afterEach)\n    if (beforeEach) {\n      global.beforeEach(beforeEach)\n    }\n    if (afterEach) {\n      global.afterEach(afterEach)\n    }\n  }\n\n  _runAsync(userFn, done) {\n    if (!userFn) {\n      done()\n      return true\n    }\n    const resp = userFn.apply(this);\n    if (resp && resp.then) {\n      return resp.then(done).catch((error) => {\n        // Throwing an error doesn't register as stopping the test. Instead, run an\n        // expect() that will fail and show us the error. We still need to call done()\n        // afterwards, or it will take the full timeout to fail.\n        expect(error).toBeUndefined()\n        done()\n      })\n    }\n    done()\n    return resp\n  }\n\n  _makeEachOrAllFnAsync(jasmineEachFn) {\n    const self = this;\n    return (userFn) => {\n      return jasmineEachFn(function asyncEachFn(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n\n  _makeItAsync(jasmineIt) {\n    const self = this;\n    return (desc, userFn) => {\n      return jasmineIt(desc, function asyncIt(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cloud-core/spec/jasmine/polyfills.es6",
    "content": "// We use Jasmine 1 in the client tests and Jasmine 2 in the cloud tests,\n// but isomorphic-core tests need to be run in both environments. Tests in\n// isomorphic-core should use Jasmine 1 syntax, and then we can add polyfills\n// here to make sure that they exist when we run in a Jasmine 2 environment.\n\nexport default function applyPolyfills() {\n  const origSpyOn = global.spyOn;\n  // There's no prototype to modify, so we have to modify the return\n  // values of spyOn as they're created.\n  global.spyOn = (object, methodName) => {\n    const originalValue = object[methodName]\n    const spy = origSpyOn(object, methodName)\n    object[methodName].originalValue = originalValue;\n    spy.andReturn = spy.and.returnValue;\n    spy.andCallFake = spy.and.callFake;\n    Object.defineProperty(spy.calls, 'length', {get: function getLength() { return this.count(); }})\n    return spy;\n  }\n}\n"
  },
  {
    "path": "packages/cloud-core/spec/logger-spec.js",
    "content": "const {createLogger} = require('../logger');\nconst {testStream} = require('../log-streams');\n\ndescribe(\"createLogger\", () => {\n  it(\"should log msgs correctly\", () => {\n    const globalLogger = createLogger(\"specs\");\n\n    globalLogger.info('default log');\n    let logOutput = testStream.stream.toString();\n    let parsedLog = JSON.parse(logOutput);\n    expect(parsedLog.msg).toEqual('default log');\n    testStream.stream.reset();\n\n    const childLogger = globalLogger.child({child: true});\n    childLogger.info('child logger');\n    logOutput = testStream.stream.toString();\n    parsedLog = JSON.parse(logOutput);\n    expect(parsedLog.child).toEqual(true);\n    testStream.stream.reset();\n\n    const fakeAccount = {\n      id: 'abcde',\n      emailAddress: 'ben.bitdiddle@mit.edu',\n      provider: 'imap',\n    };\n    const loggerForAccount = globalLogger.forAccount(fakeAccount, childLogger);\n    loggerForAccount.info('log for account');\n    logOutput = testStream.stream.toString();\n    parsedLog = JSON.parse(logOutput);\n    expect(parsedLog.account_id).toEqual(fakeAccount.id);\n    expect(parsedLog.account_email).toEqual(fakeAccount.emailAddress);\n    expect(parsedLog.account_provider).toEqual(fakeAccount.provider);\n    expect(parsedLog.n1_id).toEqual('Not available');\n    expect(parsedLog.child).toEqual(true);\n    testStream.stream.reset();\n  });\n});\n"
  },
  {
    "path": "packages/cloud-core/spec/run.es6",
    "content": "import {executeJasmine} from 'isomorphic-core';\nexecuteJasmine();\n"
  },
  {
    "path": "packages/cloud-workers/README.md",
    "content": "# Cloud Workers\n\nThis is a Cloud worker service for Nylas Mail. It provides background workers\nto process features such as Reminders, Snooze, and Send Later. It is heavily\nreliant on the Metadata services exposed by Cloud API.\n\nFor details on how to run Cloud Workers, see the\n[cloud-core/README.md](https://github.com/nylas/nylas-mail-all/blob/master/packages/cloud-core/README.md)\nand run `npm run start-cloud` from the root of the repository."
  },
  {
    "path": "packages/cloud-workers/app.es6",
    "content": "/* eslint object-property-newline:0 */\nimport Foreman from './src/foreman'\nimport SnoozeWorker from './src/workers/snooze'\nimport SendLaterWorker from './src/workers/send-later'\nimport SendRemindersWorker from './src/workers/send-reminders'\nimport {setupMonitoring} from './src/monitoring'\nconst {DatabaseConnector, Logger} = require('cloud-core')\n\nglobal.Promise = require('bluebird');\nglobal.Logger = Logger.createLogger('cloud-workers')\n\nlet foremans = []\nasync function start() {\n  const db = await DatabaseConnector.forShared();\n  const logger = global.Logger;\n\n  logger.info(\"Starting Cloud Workers\")\n\n  foremans = [\n    new Foreman({db, logger,\n      pluginId: \"thread-snooze\",\n      WorkerClass: SnoozeWorker,\n    }),\n    new Foreman({db, logger,\n      pluginId: \"send-later\",\n      WorkerClass: SendLaterWorker,\n    }),\n    new Foreman({db, logger,\n      pluginId: \"send-reminders\",\n      WorkerClass: SendRemindersWorker,\n    }),\n  ]\n  foremans.forEach(f => f.run()) // Don't await\n}\n\nlet restartTimeout = null;\nasync function restart() {\n  global.Logger.warn(\"Restarting app due to unhandled error\")\n  clearTimeout(restartTimeout);\n  foremans.forEach(f => f.stop());\n  restartTimeout = setTimeout(() => {\n    start();\n  }, 30 * 1000);\n}\n\nconst onUnhandledError = (err) => {\n  restart();\n  global.Logger.error(err)\n}\nprocess.on('uncaughtException', onUnhandledError)\nprocess.on('unhandledRejection', onUnhandledError)\n\nsetupMonitoring(global.Logger);\nstart();\n"
  },
  {
    "path": "packages/cloud-workers/package.json",
    "content": "{\n  \"name\": \"cloud-workers\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Message processing pipeline\",\n  \"main\": \"index.js\",\n  \"author\": \"Nylas\",\n  \"license\": \"ISC\",\n  \"scripts\": {\n    \"test\": \"babel-node spec/run.es6\"\n  },\n  \"devDependencies\": {\n    \"babel-cli\": \"6.x.x\",\n    \"jasmine\": \"2.x.x\"\n  },\n  \"dependencies\": {\n    \"bluebird\": \"3.4.x\",\n    \"boom\": \"4.2.0\",\n    \"cloud-core\": \"0.x.x\",\n    \"googleapis\": \"9.0.0\",\n    \"handlebars\": \"4.0.6\",\n    \"hapi\": \"16.1.0\",\n    \"hapi-auth-basic\": \"4.2.0\",\n    \"hapi-boom-decorators\": \"2.2.2\",\n    \"hapi-swagger\": \"7.6.0\",\n    \"isomorphic-core\": \"x.x.x\",\n    \"joi\": \"8.4.2\",\n    \"js-base64\": \"2.1.9\",\n    \"libhoney\": \"1.0.0-beta.2\",\n    \"mimelib\": \"0.2.19\",\n    \"node-dogstatsd\": \"0.0.6\",\n    \"raven\": \"1.1.4\",\n    \"request\": \"2.79.0\",\n    \"request-promise\": \"4.1.1\",\n    \"rx-lite\": \"4.0.8\",\n    \"sequelize\": \"github:nylas/sequelize#nylas-3.30.1\",\n    \"tmp\": \"0.0.31\",\n    \"umzug\": \"1.11.0\",\n    \"underscore\": \"1.8.3\",\n    \"vision\": \"4.1.0\"\n  }\n}\n"
  },
  {
    "path": "packages/cloud-workers/spec/jasmine/execute.es6",
    "content": "import Jasmine from 'jasmine'\nimport JasmineExtensions from './extensions'\n\nexport default function execute(extendOpts) {\n  const jasmine = new Jasmine()\n  jasmine.loadConfigFile('spec/jasmine/config.json')\n  const jasmineExtensions = new JasmineExtensions()\n  jasmineExtensions.extend(extendOpts)\n  jasmine.execute()\n}\n"
  },
  {
    "path": "packages/cloud-workers/spec/jasmine/extensions.es6",
    "content": "import applyPolyfills from './polyfills'\n\nexport default class JasmineExtensions {\n  extend({beforeEach, afterEach} = {}) {\n    applyPolyfills()\n    global.it = this._makeItAsync(global.it)\n    global.fit = this._makeItAsync(global.fit)\n    global.beforeAll = this._makeEachOrAllFnAsync(global.beforeAll)\n    global.afterAll = this._makeEachOrAllFnAsync(global.afterAll)\n    global.beforeEach = this._makeEachOrAllFnAsync(global.beforeEach)\n    global.afterEach = this._makeEachOrAllFnAsync(global.afterEach)\n    if (beforeEach) {\n      global.beforeEach(beforeEach)\n    }\n    if (afterEach) {\n      global.afterEach(afterEach)\n    }\n  }\n\n  _runAsync(userFn, done) {\n    if (!userFn) {\n      done()\n      return true\n    }\n    const resp = userFn.apply(this);\n    if (resp && resp.then) {\n      return resp.then(done).catch((error) => {\n        // Throwing an error doesn't register as stopping the test. Instead, run an\n        // expect() that will fail and show us the error. We still need to call done()\n        // afterwards, or it will take the full timeout to fail.\n        expect(error).toBeUndefined()\n        done()\n      })\n    }\n    done()\n    return resp\n  }\n\n  _makeEachOrAllFnAsync(jasmineEachFn) {\n    const self = this;\n    return (userFn) => {\n      return jasmineEachFn(function asyncEachFn(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n\n  _makeItAsync(jasmineIt) {\n    const self = this;\n    return (desc, userFn) => {\n      return jasmineIt(desc, function asyncIt(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cloud-workers/spec/jasmine/polyfills.es6",
    "content": "// We use Jasmine 1 in the client tests and Jasmine 2 in the cloud tests,\n// but isomorphic-core tests need to be run in both environments. Tests in\n// isomorphic-core should use Jasmine 1 syntax, and then we can add polyfills\n// here to make sure that they exist when we run in a Jasmine 2 environment.\n\nexport default function applyPolyfills() {\n  const origSpyOn = global.spyOn;\n  // There's no prototype to modify, so we have to modify the return\n  // values of spyOn as they're created.\n  global.spyOn = (object, methodName) => {\n    const originalValue = object[methodName]\n    const spy = origSpyOn(object, methodName)\n    object[methodName].originalValue = originalValue;\n    spy.andReturn = spy.and.returnValue;\n    spy.andCallFake = spy.and.callFake;\n    Object.defineProperty(spy.calls, 'length', {get: function getLength() { return this.count(); }})\n    return spy;\n  }\n}\n"
  },
  {
    "path": "packages/cloud-workers/spec/run.es6",
    "content": "import {executeJasmine} from 'isomorphic-core'\nexecuteJasmine()\n"
  },
  {
    "path": "packages/cloud-workers/src/cloud-worker.es6",
    "content": "import moment from 'moment'\nimport {Errors, IMAPConnectionPool} from 'isomorphic-core'\nimport {GmailOAuthHelpers} from 'cloud-core'\n\nconst DEFAULT_SOCKET_TIMEOUT = +(process.env.DEFAULT_SOCKET_TIMEOUT || 5 * 60 * 1000);\nconst RETRY_BASE_SEC = +(process.env.RETRY_BASE_SEC || 5);\n\nexport default class CloudWorker {\n  constructor(cloudJob, {db, logger}) {\n    this.job = cloudJob\n    this.db = db\n    this.initLogger = logger.child({\n      jobId: cloudJob.id,\n      accountId: cloudJob.accountId,\n      contextClass: this.constructor.name,\n      contextType: \"Worker\",\n    }); // Should be a child of Foreman's logger\n  }\n\n  async run() {\n    try {\n      const account = await this.db.Account.findById(this.job.accountId);\n      if (!this.logger) {\n        this.logger = this.initLogger.child({\n          email: account.emailAddress,\n          provider: account.provider,\n        })\n      }\n\n      this.logger.info(`Running ${this.constructor.name}. Initial status: ${this.job.status}. Attempt number: ${this.job.attemptNumber}`);\n\n      await this._markInProgress();\n      await this._ensureAccessToken(account)\n      await IMAPConnectionPool.withConnectionsForAccount(account, {\n        logger: this.logger,\n        desiredCount: 1,\n        socketTimeout: DEFAULT_SOCKET_TIMEOUT,\n        onConnected: async ([connection]) => {\n          await this._runWithConnection({connection, account})\n        },\n      })\n    } catch (err) {\n      await this._onError(err)\n    }\n  }\n\n  async _runWithConnection({connection, account}) {\n    const metadatum = await this.db.Metadata.findById(this.job.metadataId);\n    if (!metadatum) {\n      this.logger.error(`Can't find metadata ${this.job.metadataId} for job: ${this.job.id}`)\n      throw new Error(\"Can't find metadata\")\n    }\n    await this.performAction({metadatum, account, connection});\n    await this.db.CloudJob.update(\n      {status: \"SUCCEEDED\", statusUpdatedAt: new Date()},\n      {where: {id: this.job.id}}\n    );\n    this.logger.info(`${this.constructor.name} Succeeded`)\n  }\n\n  async _onError(err) {\n    return this.db.sequelize.transaction(async (t) => {\n      const job = await this.db.CloudJob.findById(this.job.id, {transaction: t});\n      job.error = {\n        message: err.message,\n        name: err.constructor.name,\n        stack: err.stack,\n      }\n      if (err instanceof Errors.RetryableError) {\n        job.status = \"WAITING-TO-RETRY\";\n        const sec = RETRY_BASE_SEC * (2 ** job.attemptNumber);\n        job.retryAt = moment().add(sec, 'seconds').toDate();\n        this.logger.info(`Failed with a retryable error. Will retry in ${sec} seconds around ${job.retryAt}`)\n      } else {\n        job.status = \"FAILED\"\n        this.logger.info(`Failed with a permanent error`)\n      }\n      job.statusUpdatedAt = new Date()\n      await job.save({transaction: t});\n      this.logger.error(err)\n    })\n  }\n\n  async _ensureAccessToken(account) {\n    const currentUnixDate = Math.floor(Date.now() / 1000);\n    const credentials = account.decryptedCredentials()\n    if (account.provider === 'gmail') {\n      if (!credentials.xoauth2 || !credentials.expiry_date ||\n          currentUnixDate > credentials.expiry_date) {\n        this.logger.info(`Refreshing access token for account id: ${account.id}`);\n        await GmailOAuthHelpers.refreshAccessToken(account);\n      }\n    }\n  }\n\n  async _markInProgress() {\n    return this.db.sequelize.transaction(async (t) => {\n      const job = await this.db.CloudJob.findById(this.job.id, {transaction: t});\n      job.status = \"INPROGRESS-RETRYABLE\";\n      job.statusUpdatedAt = new Date();\n      const attemptNum = job.attemptNumber;\n      job.attemptNumber = attemptNum + 1; // beware magic setter method\n      await job.save({transaction: t})\n    })\n  }\n\n  pluginId() {\n    throw new Error(\"You should override this!\");\n  }\n\n  async performAction() {\n    throw new Error(\"You should override this!\");\n  }\n}\n"
  },
  {
    "path": "packages/cloud-workers/src/foreman.es6",
    "content": "import moment from 'moment'\nimport {StatsD} from 'node-dogstatsd'\nconst STATSD_HOST = process.env.STATSD_HOST || \"172.17.0.1\"\nconst stats = new StatsD(STATSD_HOST, 8125)\n\nexport default class Foreman {\n  constructor({db, logger, pluginId, WorkerClass}) {\n    this.db = db;\n    this.pluginId = pluginId\n    this.WorkerClass = WorkerClass\n    this.foremanId = `${this.constructor.name}-${pluginId}-${process.pid}`\n    this.logger = logger.child({\n      foremanId: this.foremanId,\n      contextClass: this.constructor.name,\n      contextType: \"Foreman\",\n      pluginId: pluginId,\n    });\n    this.runTimeout = null;\n\n    this.MAX_RETRIES = +(process.env.MAX_RETRIES || 10);\n    this.MAX_METADATA_GRAB = +(process.env.MAX_METADATA_GRAB || 100);\n    this.DEAD_THRESHOLD_MIN = +(process.env.DEAD_THRESHOLD_MIN || 10);\n    this.MAX_JOBS_PER_FOREMAN = +(process.env.MAX_JOBS_PER_FOREMAN || 20);\n    this.FOREMAN_CHECK_INTERVAL = +(process.env.FOREMAN_CHECK_INTERVAL || 10 * 1000);\n  }\n\n  async run() {\n    clearTimeout(this.runTimeout);\n    try {\n      this.logger.info(`❤️`);\n      await this.createJobsFromMetadata();\n      await this.cleanupDeadJobs();\n      const newJobs = await this.claimJobs();\n      this.runWorkers(newJobs); // Do NOT await. Does nothing if no new jobs\n      stats.gauge(`cloud-workers.heartbeat.${this.pluginId}`, 1)\n    } finally {\n      clearTimeout(this.runTimeout);\n      this.runTimeout = setTimeout(this.run.bind(this), this.FOREMAN_CHECK_INTERVAL);\n    }\n  }\n\n  stop() {\n    this.logger.info(`Stopping`);\n    clearTimeout(this.runTimeout);\n  }\n\n  /**\n   * This kicks off the workers and we do NOT await for them. As soon as they\n   * run they'll update their status in the database.\n   */\n  async runWorkers(newJobs = []) {\n    if (newJobs.length === 0) return\n    const workers = newJobs.map((job) =>\n      new this.WorkerClass(job, {db: this.db, logger: this.logger})\n    );\n    for (const worker of workers) {\n      // DO NOT await\n      worker.run(); // Logging in worker\n    }\n  }\n\n  /**\n   * Our plugins use Metadata to record if they want some work done by the\n   * `expiration` field.\n   *\n   * We want to as quickly as possible convert the Metadata into first class\n   * `CloudJob` objects.\n   */\n  async createJobsFromMetadata() {\n    await this.db.sequelize.transaction(async (t) => {\n      const expiredMetadata = await this.db.Metadata.findAll({\n        attributes: [\"id\", \"value\", \"pluginId\", \"accountId\", \"expiration\"],\n        transaction: t,\n        limit: this.MAX_METADATA_GRAB,\n        where: {pluginId: this.pluginId, expiration: {$lte: new Date()}}, // Indexed\n      });\n\n      if (expiredMetadata.length === 0) {\n        this.logger.debug(`No newly expired metadata`);\n        return\n      }\n\n      const newJobData = expiredMetadata.map((metadatum) => {\n        return {\n          type: metadatum.pluginId,\n          metadataId: metadatum.id,\n          accountId: metadatum.accountId,\n          foremanId: this.foremanId,\n        }\n      })\n\n      this.logger.info(`Creating ${newJobData.length} new CloudJobs for newly expired metadata`);\n      await this.db.CloudJob.bulkCreate(newJobData, {transaction: t})\n      // Immediately mark Metadata as no longer expired now that we've created\n      // jobs for them\n      for (const expiredMetadatum of expiredMetadata) {\n        try {\n          await expiredMetadatum.clearExpiration({transaction: t})\n        } catch (err) {\n          this.logger.error(err);\n          this.logger.info(\"Deleting corrupted metadata\");\n          await expiredMetadatum.destroy();\n        }\n      }\n    })\n  }\n\n  /**\n   * In a single transaction this will grab available jobs for the plugin\n   * across the whole DB, and mark them as started.\n   *\n   * We claim both new jobs for the plugin\n   * AND\n   * We claim jobs that are in progress but claimed a long time ago, likely\n   * indicating that they died and need to be retried.\n   */\n  async claimJobs() {\n    let claimableJobs = []\n    await this.db.sequelize.transaction(async (t) => {\n      claimableJobs = await this.db.CloudJob.findAll({\n        transaction: t,\n        limit: await this.currentJobLimit(t),\n        where: {\n          $or: [\n            // New jobs.\n            {type: this.pluginId, status: \"NEW\"}, // Indexed!\n            // Failed, but retryable jobs.\n            {\n              type: this.pluginId, // indexed\n              status: \"WAITING-TO-RETRY\", // indexed\n              attemptNumber: {$lt: this.MAX_RETRIES}, // indexed\n              retryAt: {$lte: new Date()}, // NOT indexed\n            }, // Indexed!\n            // Likely dead jobs\n            {\n              type: this.pluginId, // indexed\n              status: \"INPROGRESS-RETRYABLE\", // indexed\n              attemptNumber: {$lt: this.MAX_RETRIES}, // indexed\n              statusUpdatedAt: {$lte: moment().subtract(this.DEAD_THRESHOLD_MIN, 'minutes').toDate()}, // Indexed!\n            },\n          ],\n        },\n      });\n      if (claimableJobs.length === 0) {\n        this.logger.debug(`No CloudJobs to claim`)\n        return\n      }\n      this.logger.info(`Claiming ${claimableJobs.length} CloudJobs`);\n      await this.db.CloudJob.update( // Bulk update\n        {\n          foremanId: this.foremanId,\n          status: \"INPROGRESS-RETRYABLE\",\n          claimedAt: new Date(),\n          statusUpdatedAt: new Date(),\n        },\n        {\n          transaction: t,\n          where: {id: {$in: claimableJobs.map(j => j.id)}}, // Indexed!\n        }\n      );\n    })\n    return claimableJobs\n  }\n\n  async cleanupDeadJobs() {\n    await this.db.sequelize.transaction(async (t) => {\n      const deadJobs = await this.db.CloudJob.findAll({\n        transaction: t,\n        where: {\n          type: this.pluginId,\n          status: {$ne: \"FAILED\"},\n          attemptNumber: {$gte: this.MAX_RETRIES}, // Indexed\n        }},\n      );\n\n      if (deadJobs.length === 0) {\n        this.logger.debug(`No dead CloudJobs to cleanup`)\n        return\n      }\n      const e = new Error(\"Job failed too many times\")\n      await this.db.CloudJob.update( // Bulk update!\n        {\n          status: \"FAILED\",\n          error: {message: e.message, stack: e.stack, name: e.constructor.name},\n          statusUpdatedAt: new Date(),\n        },\n        {\n          transaction: t,\n          where: {id: {$in: deadJobs.map(j => j.id)}}, // Indexed!\n        }\n      );\n      this.logger.info(`Cleaned up ${deadJobs.length} dead jobs`)\n    })\n  }\n\n  async currentJobLimit(transaction) {\n    const inProgress = await this.asyncNumInProgress(transaction);\n    this.logger.debug(`${inProgress} CloudJobs in progress`);\n    return Math.max((this.MAX_JOBS_PER_FOREMAN - inProgress), 0);\n  }\n\n  async asyncNumInProgress(transaction) {\n    return this.db.CloudJob.count({\n      transaction: transaction,\n      where: {\n        foremanId: this.foremanId, // Indexed!\n        $or: [\n          {status: \"INPROGRESS-RETRYABLE\"}, // Indexed!\n          {status: \"INPROGRESS-NOTRETRYABLE\"},\n        ],\n      },\n    })\n  }\n}\n"
  },
  {
    "path": "packages/cloud-workers/src/monitoring.es6",
    "content": "import Hapi from 'hapi';\nimport Boom from 'boom'\nimport {DatabaseConnector} from 'cloud-core';\n\nconst MAX_TIME_BETWEEN_ITERATIONS = 5 * 60 * 1000;\n\nexport function setupMonitoring(logger) {\n  const server = new Hapi.Server({\n    debug: false,\n    connections: {\n      router: {\n        stripTrailingSlash: true,\n      },\n    },\n  });\n\n  server.connection({ port: process.env.PORT || 8080 });\n\n  server.route({\n    method: 'GET',\n    path: '/ping',\n    config: { auth: false },\n    handler: async (request, reply) => {\n      logger.info('---> Ping DB');\n      try {\n        const db = await DatabaseConnector.forShared();\n        await db.sequelize.query('SELECT 1');\n\n        if (new Date() - global.lastRun >= MAX_TIME_BETWEEN_ITERATIONS) {\n          reply(Boom.wrap(\"Main loop seems to be stuck.\", 500));\n        } else {\n          reply(\"DB Okay\")\n        }\n      } catch (err) {\n        reply(Boom.wrap(err, 500));\n      }\n    },\n  });\n\n  server.start((startErr) => {\n    if (startErr) { throw startErr; }\n    console.info('Watchdog server running.');\n  });\n}\n"
  },
  {
    "path": "packages/cloud-workers/src/sentry.es6",
    "content": "const Raven = require('raven');\n\nexport default Raven.config(process.env.SENTRY_DSN || \"https://c88e3b7525e04b4b88d925d948686ec3:8db413d88d1a407bb8cf2f62489a4ec1@sentry.nylas.com/28\").install();\n"
  },
  {
    "path": "packages/cloud-workers/src/workers/send-later.es6",
    "content": "import fs from 'fs';\nimport AWS from 'aws-sdk';\nimport path from 'path';\nimport tmp from 'tmp';\nimport {Promise} from 'bluebird';\nimport {DatabaseConnector} from 'cloud-core'\nimport {SendmailClient, MessageUtils, TrackingUtils, ModelUtils} from 'isomorphic-core'\nimport CloudWorker from '../cloud-worker'\n\nPromise.promisifyAll(fs);\n\nconst NODE_ENV = process.env.NODE_ENV || 'production'\nconst BUCKET_NAME = process.env.BUCKET_NAME\nconst AWS_ACCESS_KEY_ID = process.env.BUCKET_AWS_ACCESS_KEY_ID\nconst AWS_SECRET_ACCESS_KEY = process.env.BUCKET_AWS_SECRET_ACCESS_KEY\n\nif (NODE_ENV !== 'development' &&\n  (!BUCKET_NAME || !AWS_ACCESS_KEY_ID || !AWS_SECRET_ACCESS_KEY)) {\n  throw new Error(\"You need to define S3 access credentials.\")\n}\n\nAWS.config.update({\n  accessKeyId: AWS_ACCESS_KEY_ID,\n  secretAccessKey: AWS_SECRET_ACCESS_KEY })\n\nconst s3 = new AWS.S3({apiVersion: '2006-03-01'});\n\n\nexport default class SendLaterWorker extends CloudWorker {\n  pluginId() {\n    return 'send-later';\n  }\n\n  _identifyFolderRole(folderTable, role, prefix) {\n    // identify the sent folder in the list of IMAP boxes node-imap returns.\n    // this is a little complex because node-imap tries to\n    // abstract IMAP folders – regular IMAP doesn't support recursive folders,\n    // so we need to rebuild the actual name of the folder to be able to save\n    // files in it. Hence, the recursive function passing a prefix around.\n    for (const folderName of Object.keys(folderTable)) {\n      const folder = folderTable[folderName];\n      if (folder.attribs.indexOf(role) !== -1) {\n        return prefix + folderName;\n      }\n\n      let recursiveResult = null;\n      if (folder.children) {\n        if (prefix !== '') {\n          recursiveResult = this._identifyFolderRole(folder.children, role,\n                                                    prefix + folderName + folder.delimiter);\n        } else {\n          recursiveResult = this._identifyFolderRole(folder.children, role,\n                                                    folderName + folder.delimiter);\n        }\n\n        if (recursiveResult) {\n          return recursiveResult;\n        }\n      }\n    }\n\n    return null;\n  }\n\n  identifySentFolder(folderTable) {\n    return this._identifyFolderRole(folderTable, '\\\\Sent', '');\n  }\n\n  identifyTrashFolder(folderTable) {\n    return this._identifyFolderRole(folderTable, '\\\\Trash', '');\n  }\n\n  async fetchLocalAttachment(accountId, objectId) {\n    const uploadId = `${accountId}-${objectId}`;\n    const filepath = path.join(\"/tmp\", \"uploads\", uploadId);\n    return fs.readFileAsync(filepath)\n  }\n\n  async deleteLocalAttachment(accountId, objectId) {\n    const uploadId = `${accountId}-${objectId}`;\n    const filepath = path.join(\"/tmp\", \"uploads\", uploadId);\n    await fs.unlinkAsync(filepath);\n  }\n\n  async fetchS3Attachment(accountId, objectId) {\n    const uploadId = `${accountId}-${objectId}`;\n\n    return new Promise((resolve, reject) => {\n      s3.getObject({\n        Bucket: BUCKET_NAME,\n        Key: uploadId,\n      }, (err, data) => {\n        if (err) {\n          reject(err);\n        }\n\n        const body = data.Body;\n        resolve(body);\n      })\n    });\n  }\n\n  async deleteS3Attachment(accountId, objectId) {\n    const uploadId = `${accountId}-${objectId}`;\n\n    return new Promise((resolve, reject) => {\n      s3.deleteObject({\n        Bucket: BUCKET_NAME,\n        Key: uploadId,\n      }, (err, data) => {\n        if (err) {\n          reject(err);\n        }\n\n        resolve(data);\n      })\n    });\n  }\n\n  async sendPerRecipient({account, baseMessage, usesOpenTracking, usesLinkTracking, logger = console} = {}) {\n    const recipients = [].concat(baseMessage.to, baseMessage.cc, baseMessage.bcc);\n    const failedRecipients = []\n\n    for (const recipient of recipients) {\n      const customBody = TrackingUtils.addRecipientToTrackingLinks({\n        recipient,\n        baseMessage,\n        usesOpenTracking,\n        usesLinkTracking,\n      })\n\n      const individualizedMessage = ModelUtils.deepClone(baseMessage);\n      individualizedMessage.body = customBody;\n      // TODO we set these temporary properties which aren't stored in the\n      // database model because SendmailClient requires them to send the message\n      // with the correct headers.\n      // This should be cleaned up\n      individualizedMessage.references = baseMessage.references;\n      individualizedMessage.inReplyTo = baseMessage.inReplyTo;\n\n      try {\n        const sender = new SendmailClient(account, logger);\n        await sender.sendCustom(individualizedMessage, {to: [recipient]})\n      } catch (error) {\n        logger.error({err: error, recipient: recipient.email}, 'SendMessagePerRecipient: Failed to send to recipient');\n        failedRecipients.push(recipient.email)\n      }\n    }\n    if (failedRecipients.length === recipients.length) {\n      throw new Error('SendMessagePerRecipient: Sending failed for all recipients', 500);\n    }\n    return {failedRecipients}\n  }\n\n  async cleanupSentMessages(account, conn, sender, logger, message) {\n    await conn.connect();\n\n    const boxes = await conn.getBoxes();\n\n    const sentName = message.sentFolderName || this.identifySentFolder(boxes);\n    const trashName = message.trashFolderName || this.identifyTrashFolder(boxes);\n\n    const sentBox = await conn.openBox(sentName);\n    logger.debug(\"Opened sent box\", sentName);\n    // Remove all existing messages.\n    const uids = await sentBox.search([['HEADER', 'Message-ID', message.message_id_header]]) || []\n    logger.debug(\"Found Gmail's optimistically placed message UIDs in sent folder\", uids);\n    for (const uid of uids) {\n      logger.debug(\"Moving from sent to trash\", uid);\n      await sentBox.moveFromBox(uid, trashName);\n    }\n    await sentBox.closeBox();\n\n    // Now, go the trash folder and remove all messages marked as deleted.\n    const trashBox = await conn.openBox(trashName);\n    logger.debug(\"Opened trash box\", trashName);\n    const trashUids = await trashBox.search([['HEADER', 'Message-ID', message.message_id_header]])\n    logger.debug(\"Found message UIDs in trash\", uids);\n    for (const uid of trashUids) {\n      logger.debug(\"Fully removing from trash\", uid);\n      await trashBox.addFlags(uid, 'DELETED')\n    }\n    await trashBox.closeBox({expunge: true});\n\n    const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms))\n\n    /**\n     * When you send a message through Gmail, it automatically puts a\n     * message in your sent mail folder. But for a while, the message and\n     * the draft have the same ID.\n     */\n    if (account.provider === 'gmail') {\n      logger.debug(\"Waiting to add sent email to sent folder\");\n      await sleep(5000);\n    }\n\n    // Add a single message without tracking information.\n    const box = await conn.openBox(sentName);\n    const rawMime = await MessageUtils.buildMime(message, {includeBcc: true});\n    await box.append(rawMime, {flags: 'SEEN'});\n    await box.closeBox();\n  }\n\n  async hydrateAttachments(baseMessage, accountId) {\n    if (!baseMessage.uploads) {\n      baseMessage.uploads = []\n      return baseMessage\n    }\n    // We get a basic JSON message from the metadata database. We need to set\n    // some fields (e.g: the `attachments` field) for it to be ready to send.\n    // We call this \"hydrating\" it.\n    const attachments = [];\n    for (const upload of baseMessage.uploads) {\n      const attach = {};\n      attach.filename = upload.filename;\n\n      let attachmentContents;\n      if (NODE_ENV === 'development') {\n        attachmentContents = await this.fetchLocalAttachment(accountId, upload.id);\n      } else {\n        attachmentContents = await this.fetchS3Attachment(accountId, upload.id);\n      }\n\n      // This is very cumbersome. There is a bug in the npm module we use to\n      // generate MIME messages – we can't pass it the buffer we get form S3\n      // because it will fail in mysterious ways 5 functions down the stack.\n      // To make things more complicated, the original author of the module\n      // took it offline. After wrestling with this for a couple day, I decided\n      // to simply write the file to a temporary directory before attaching it.\n      // It's not pretty but it does the job.\n      const tmpFile = Promise.promisify(tmp.file, {multiArgs: true});\n      const writeFile = Promise.promisify(fs.writeFile);\n\n      const [filePath, , cleanupCallback] = await tmpFile();\n      await writeFile(filePath, attachmentContents);\n      attach.targetPath = filePath;\n      attach.cleanupCallback = cleanupCallback;\n\n      if (upload.inline) {\n        attach.inline = upload.inline;\n      }\n\n      attachments.push(attach);\n    }\n\n    baseMessage.uploads = attachments;\n    return baseMessage;\n  }\n\n  async cleanupAttachments(logger, baseMessage, accountId) {\n    if (!baseMessage.uploads) { return }\n    // Remove all attachments after sending a message.\n    for (const upload of baseMessage.uploads) {\n      if (NODE_ENV === 'development') {\n        await this.deleteLocalAttachment(accountId, upload.id);\n      } else {\n        await this.deleteS3Attachment(accountId, upload.id);\n      }\n\n      if (upload.cleanupCallback) {\n        await upload.cleanupCallback();\n      }\n    }\n  }\n\n  _buildMessageFromJSON(json = {}) {\n    const baseMessage = json;\n    baseMessage.date = new Date(+json.date * 1000);\n    return baseMessage\n  }\n\n  async performAction({metadatum, account, connection}) {\n    const db = await DatabaseConnector.forShared();\n\n    if (Object.keys(metadatum.value || {}).length === 0) {\n      throw new Error(\"Can't send later, no metadata value\")\n    }\n    const logger = global.Logger.forAccount(account);\n    const sender = new SendmailClient(account, logger);\n    const usesOpenTracking = metadatum.value.usesOpenTracking || false;\n    const usesLinkTracking = metadatum.value.usesLinkTracking || false;\n\n    let baseMessage = this._buildMessageFromJSON(metadatum.value)\n    baseMessage = await this.hydrateAttachments(baseMessage, account.id);\n\n    await this.sendPerRecipient({\n      db,\n      account,\n      baseMessage,\n      usesOpenTracking,\n      usesLinkTracking,\n      logger,\n    });\n\n    await this.db.sequelize.transaction(async (t) => {\n      const job = await this.db.CloudJob.findById(this.job.id, {transaction: t});\n      job.status = \"INPROGRESS-NOTRETRYABLE\";\n      await job.save({transaction: t})\n    })\n    logger.info(\"Successfully sent message\");\n\n    // Now, remove all multisend messages from the user's mailbox. We wrap this\n    // block in a pokemon exception handler because we don't want to send messages\n    // again if it fails.\n    try {\n      await this.cleanupSentMessages(account, connection, sender, logger, baseMessage);\n      await this.cleanupAttachments(logger, baseMessage, account.id);\n      logger.info(\"Successfully put delayed message in sent folder\");\n    } catch (err) {\n      this.logger.error(`Error while trying to process metadatum ${metadatum.id}`, err);\n    }\n  }\n}\n"
  },
  {
    "path": "packages/cloud-workers/src/workers/send-reminders.es6",
    "content": "import moment from 'moment'\nimport {GmailOAuthHelpers} from 'cloud-core'\nimport {SendmailClient} from 'isomorphic-core'\nimport CloudWorker from '../cloud-worker'\n// This assumes there can't be any whitespace in the Message-Id value.\n// (Couldn't just use .+ because that doesn't match < or >)\n// https://regex101.com/r/1C1WCl/1\nconst messageIdRegexp = /^\\s*message-id: ([^\\s]+)\\s*$/im\n\n// Search the imapBox for messages that are in reply to origMessageId. Messages\n// are not considered new replies if their id is present in seenMessageIdSet.\nconst asyncHasNewReply = ({imapBox, origMessageId, seenMessageIdSet, origDate}) => {\n  return new Promise(async (resolve, reject) => {\n    try {\n      let count = 0;\n\n      // We subtract one day to make sure there aren't any timezone issues.\n      const sinceDateString = moment(origDate).subtract(1, 'day').format('MMMM D, YYYY')\n\n      const replyUIDS = await imapBox.search([\n        ['HEADER', 'IN-REPLY-TO', origMessageId],\n        ['SINCE', sinceDateString],\n      ])\n      if (replyUIDS.length === 0) {\n        resolve(false)\n        return;\n      }\n      let resolved = false;\n      // We have to fetch the messages to check their message-id headers. Boo.\n      imapBox.fetchEach(replyUIDS, {bodies: 'HEADER'},\n        // This callback will get called asynchronously for each message,\n        // but we only want to call resolve once.\n        ({headers}) => {\n          if (resolved) { return; } // We already found a new reply\n          const match = headers.toString().match(messageIdRegexp)\n          if (match) {\n            const replyMessageId = match[1];\n            if (!seenMessageIdSet.has(replyMessageId)) {\n              if (!resolved) { // Don't call resolve more than once\n                resolved = true;\n                resolve(true)\n              }\n              return;\n            }\n          } else {\n            this.logger.error({\n              err: new Error(\"Couldn't find a Message-Id header\"),\n              inReplyTo: origMessageId,\n            })\n          }\n          count++;\n          if (count === replyUIDS.length) {\n            // We've gone through every single message, there were no new replies\n            resolve(false)\n          }\n        }\n      )\n    } catch (error) {\n      reject(error)\n    }\n  })\n}\n\nexport default class SendRemindersWorker extends CloudWorker {\n  pluginId() {\n    return 'send-reminders';\n  }\n\n  async performAction({metadatum, account, connection}) {\n    const {messageIdHeaders, folderImapNames, replyTo, subject} = metadatum.value\n    if (!messageIdHeaders || !folderImapNames || !replyTo) {\n      throw new Error(\"Can't send reminder, no metadata value\")\n    }\n    const messageIdSet = new Set(messageIdHeaders)\n    for (const folderImapName of folderImapNames) {\n      const box = await connection.openBox(folderImapName)\n      for (const messageId of messageIdHeaders) {\n        // TODO: Eventually, we should make the app send the message date with\n        // the metadata. At this time we're trying to get a release out and\n        // don't want to wait for yet another build, so for now the createdAt\n        // date of the metadatum should be close enough, especially given the 1\n        // day buffer within asyncHasNewReply().\n        const hasNewReply = await asyncHasNewReply({\n          imapBox: box,\n          origmessageId: messageId,\n          seenMessageIdSet: messageIdSet,\n          origDate: metadatum.createdAt,\n        })\n        if (hasNewReply) {\n          this.logger.info(\"Skipping reminder, thread has already been replied to\")\n          return Promise.resolve();\n        }\n      }\n    }\n    const {accountId, objectId, pluginId} = metadatum\n    let sender;\n    try {\n      sender = new SendmailClient(account, this.logger)\n    } catch (error) {\n      // The cloud version of the account might not have the xoauth2 token yet\n      if (/missing xoauth2 token/i.test(error.message)) {\n        await GmailOAuthHelpers.refreshAccessToken(account)\n        sender = new SendmailClient(account, this.logger)\n      } else {\n        throw error\n      }\n    }\n    const message = {\n      to: [{name: account.name, email: account.emailAddress}],\n      from: [{name: `${account.name} via Nylas Mail`, email: account.emailAddress}],\n      subject: subject,\n      body: \"Nylas Mail Reminder:<br/><br/>This thread has been moved to \" +\n        \"the top of your inbox by Nylas because no one has replied to your \" +\n        \"message<br/><br/>--The Nylas Team\",\n      inReplyTo: replyTo,\n    }\n    await sender.send(message)\n    const threadMetadata = await this.db.Metadata.find({\n      where: {\n        accountId: accountId,\n        objectId: `t:${objectId}`,\n        objectType: 'thread',\n        pluginId: pluginId,\n      },\n    })\n    if (threadMetadata) {\n      // `threadMetadata.value.shouldNotify = true` doesn't work, so use Object.assign\n      threadMetadata.value = Object.assign(threadMetadata.value, {shouldNotify: true})\n      return threadMetadata.save()\n    }\n    // If there are equivalent thread metadata with different ids,\n    // the mail client should catch this and syncback the metadata\n    // with all the message ids so the cloud can reconcile them.\n    return this.db.Metadata.create({\n      accountId: accountId,\n      objectId: `t:${objectId}`,\n      objectType: 'thread',\n      pluginId: pluginId,\n      version: 0,\n      value: {shouldNotify: true},\n      expiration: null,\n    })\n  }\n}\n"
  },
  {
    "path": "packages/cloud-workers/src/workers/snooze.es6",
    "content": "import CloudWorker from '../cloud-worker'\n\n// FIXME: change this to something like \"Nylas Snoozed\".\nconst SNOOZE_FOLDER_NAME = \"N1-Snoozed\";\n\nexport default class SnoozeWorker extends CloudWorker {\n  pluginId() {\n    return 'thread-snooze'\n  }\n\n  async performAction({metadatum, connection}) {\n    if (!metadatum.value.header) {\n      throw new Error(\"Can't unsnooze, no message-id-header\")\n    }\n\n    const box = await connection.openBox(SNOOZE_FOLDER_NAME)\n    const results = await box.search([['HEADER', 'MESSAGE-ID', metadatum.value.header]])\n\n    this.logger.debug(`Found ${results.length} message with HEADER MESSAGE-ID: ${metadatum.value.header}. Moving back to Inbox.`);\n\n    for (const result of results) {\n      box.moveFromBox(result, \"INBOX\")\n    }\n  }\n}\n"
  },
  {
    "path": "packages/isomorphic-core/README.md",
    "content": "# Isomorphic Core\n\nIsomorphic refers to javascript that can be run on both the client and the\nserver.\n\nThis is shared code for mail and utilities that is designed to run both on\ndeployed cloud servers and from within the Nylas Mail client.\n\nUse through a regular import: `import iso-core from 'isomorphic-core'`\n\nIt is required as a dependency in the package.json of other modules.\n\nThis library isn't on the npm registry, but works as a dependency thanks to\n`lerna bootstrap`\n\nSee index.js for what gets explicitly exported by this library.\n\n## Important Usage Notes:\n\nSince this code runs in both the client and the server, you must be careful\nwith what libraries you use. Some common gotchas:\n\n- You can't use `NylasEnv` or `NylasExports`. These are injected only in the\n  client.\n- If you require a 3rd party library, it must be added to the \"dependencies\" of\n  isomorphic-core's `package.json`\n- You may use modern javascript syntax. Both the client and server get compiled\n  with the same .babelrc setting\n"
  },
  {
    "path": "packages/isomorphic-core/index.js",
    "content": "/* eslint global-require: 0 */\nmodule.exports = {\n  Provider: {\n    Gmail: 'gmail',\n    IMAP: 'imap',\n  },\n  Imap: require('imap'),\n  Errors: require('./src/errors'),\n  IMAPErrors: require('./src/imap-errors'),\n  SMTPErrors: require('./src/smtp-errors'),\n  loadModels: require('./src/load-models'),\n  AuthHelpers: require('./src/auth-helpers'),\n  PromiseUtils: require('./src/promise-utils'),\n  DatabaseTypes: require('./src/database-types'),\n  IMAPConnection: require('./src/imap-connection').default,\n  IMAPConnectionPool: require('./src/imap-connection-pool'),\n  MessageBodyUtils: require('./src/message-body-utils'),\n  SendmailClient: require('./src/sendmail-client'),\n  DeltaStreamBuilder: require('./src/delta-stream-builder'),\n  HookTransactionLog: require('./src/hook-transaction-log'),\n  HookIncrementVersionOnSave: require('./src/hook-increment-version-on-save'),\n  BackoffScheduler: require('./src/backoff-schedulers').BackoffScheduler,\n  ExponentialBackoffScheduler: require('./src/backoff-schedulers').ExponentialBackoffScheduler,\n  CommonProviderSettings: require('imap-provider-settings').CommonProviderSettings,\n  MetricsReporter: require('./src/metrics-reporter').default,\n  MessageUtils: require('./src/message-utils'),\n  TrackingUtils: require('./src/tracking-utils').default,\n  ModelUtils: require('./src/model-utils').default,\n  executeJasmine: require('./spec/jasmine/execute').default,\n  StringUtils: require('./src/string-utils'),\n  TLSUtils: require('./src/tls-utils'),\n  DBUtils: require('./src/db-utils'),\n  ShellUtils: require('./src/shell-utils'),\n}\n"
  },
  {
    "path": "packages/isomorphic-core/package.json",
    "content": "{\n  \"name\": \"isomorphic-core\",\n  \"version\": \"0.0.1\",\n  \"description\": \"Packages use isomorphically on n1-cloud and client-sync\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"test\": \"babel-node spec/run.es6\"\n  },\n  \"dependencies\": {\n    \"atob\": \"2.0.3\",\n    \"btoa\": \"1.1.2\",\n    \"imap\": \"github:jstejada/node-imap#fix-parse-body-list\",\n    \"imap-provider-settings\": \"github:nylas/imap-provider-settings#e9913d1\",\n    \"jasmine\": \"2.x.x\",\n    \"joi\": \"8.4.2\",\n    \"libhoney\": \"1.0.0-beta.2\",\n    \"nodemailer\": \"2.5.0\",\n    \"promise-props\": \"1.0.0\",\n    \"promise.prototype.finally\": \"1.0.1\",\n    \"rx-lite\": \"4.0.8\",\n    \"sequelize\": \"3.28.0\",\n    \"underscore\": \"1.8.3\",\n    \"xoauth2\": \"1.2.0\",\n    \"he\": \"1.1.0\",\n    \"iconv\": \"2.2.1\",\n    \"mimelib\": \"0.2.19\"\n  },\n  \"author\": \"Nylas\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "packages/isomorphic-core/spec/backoff-scheduler-spec.es6",
    "content": "import {BackoffScheduler, ExponentialBackoffScheduler} from '../src/backoff-schedulers'\n\n\ndescribe('BackoffSchedulers', function describeBlock() {\n  describe('BackoffScheduler', () => {\n    function linearBackoff(base, numTries) {\n      return base * numTries\n    }\n\n    it('calculates the next delay correctly with no jitter', () => {\n      const scheduler = new BackoffScheduler({\n        jitter: false,\n        baseDelay: 2,\n        maxDelay: 5,\n        getNextBackoffDelay: linearBackoff,\n      })\n      expect(scheduler.nextDelay()).toEqual(0)\n      expect(scheduler.nextDelay()).toEqual(2)\n      expect(scheduler.nextDelay()).toEqual(4)\n      expect(scheduler.nextDelay()).toEqual(5)\n      expect(scheduler.nextDelay()).toEqual(5)\n    })\n\n    it('calculates the next delay correctly with jitter', () => {\n      spyOn(Math, 'random').andReturn(0.5)\n      const scheduler = new BackoffScheduler({\n        jitter: true,\n        baseDelay: 2,\n        maxDelay: 5,\n        getNextBackoffDelay: linearBackoff,\n      })\n      expect(scheduler.nextDelay()).toEqual(0)\n      expect(scheduler.nextDelay()).toEqual(1)\n      expect(scheduler.nextDelay()).toEqual(2)\n      expect(scheduler.nextDelay()).toEqual(3)\n      expect(scheduler.nextDelay()).toEqual(4)\n      expect(scheduler.nextDelay()).toEqual(5)\n      expect(scheduler.nextDelay()).toEqual(5)\n    })\n  });\n\n  describe('ExponentialBackoffScheduler', () => {\n    it('calculates the next delay correctly with no jitter', () => {\n      const scheduler = new ExponentialBackoffScheduler({\n        jitter: false,\n        baseDelay: 2,\n        maxDelay: 10,\n      })\n      expect(scheduler.nextDelay()).toEqual(2)\n      expect(scheduler.nextDelay()).toEqual(4)\n      expect(scheduler.nextDelay()).toEqual(8)\n      expect(scheduler.nextDelay()).toEqual(10)\n      expect(scheduler.nextDelay()).toEqual(10)\n    })\n\n    it('calculates the next delay correctly with no jitter', () => {\n      spyOn(Math, 'random').andReturn(0.5)\n      const scheduler = new ExponentialBackoffScheduler({\n        jitter: true,\n        baseDelay: 2,\n        maxDelay: 10,\n      })\n      expect(scheduler.nextDelay()).toEqual(1)\n      expect(scheduler.nextDelay()).toEqual(2)\n      expect(scheduler.nextDelay()).toEqual(4)\n      expect(scheduler.nextDelay()).toEqual(8)\n      expect(scheduler.nextDelay()).toEqual(10)\n      expect(scheduler.nextDelay()).toEqual(10)\n    })\n  });\n});\n"
  },
  {
    "path": "packages/isomorphic-core/spec/imap-connection-pool-spec.es6",
    "content": "import IMAPConnectionPool from '../src/imap-connection-pool';\nimport IMAPConnection from '../src/imap-connection';\nimport {IMAPConnectionTimeoutError, IMAPSocketError} from '../src/imap-errors';\n\ndescribe('IMAPConnectionPool', function describeBlock() {\n  beforeEach(() => {\n    this.account = {\n      id: 'test-account',\n      decryptedCredentials: () => { return {}; },\n      connectionSettings: {\n        imap_host: 'imap.foobar.com',\n      },\n    };\n    IMAPConnectionPool._poolMap = {};\n    this.logger = {};\n    spyOn(IMAPConnection.prototype, 'connect').andCallFake(function connectFake() {\n      return this;\n    });\n    spyOn(IMAPConnection.prototype, 'end').andCallFake(() => {});\n  });\n\n  it('opens IMAP connection and properly returns to pool at end of scope', async () => {\n    let invokedCallback = false;\n    await IMAPConnectionPool.withConnectionsForAccount(this.account, {\n      desiredCount: 1,\n      logger: this.logger,\n      socketTimeout: 5 * 1000,\n      onConnected: ([conn]) => {\n        expect(conn instanceof IMAPConnection).toBe(true);\n        invokedCallback = true;\n        return false;\n      },\n    });\n    expect(invokedCallback).toBe(true);\n    expect(IMAPConnection.prototype.connect.calls.length).toBe(1);\n    expect(IMAPConnection.prototype.end.calls.length).toBe(1);\n  });\n\n  it('opens multiple IMAP connections and properly returns to pool at end of scope', async () => {\n    let invokedCallback = false;\n    await IMAPConnectionPool.withConnectionsForAccount(this.account, {\n      desiredCount: 2,\n      logger: this.logger,\n      socketTimeout: 5 * 1000,\n      onConnected: ([conn, otherConn]) => {\n        expect(conn instanceof IMAPConnection).toBe(true);\n        expect(otherConn instanceof IMAPConnection).toBe(true);\n        invokedCallback = true;\n        return false;\n      },\n    });\n    expect(invokedCallback).toBe(true);\n    expect(IMAPConnection.prototype.connect.calls.length).toBe(2);\n    expect(IMAPConnection.prototype.end.calls.length).toBe(2);\n  });\n\n  it('opens an IMAP connection properly and only returns to pool on done', async () => {\n    let invokedCallback = false;\n    let doneCallback = null;\n    await IMAPConnectionPool.withConnectionsForAccount(this.account, {\n      desiredCount: 1,\n      logger: this.logger,\n      socketTimeout: 5 * 1000,\n      onConnected: ([conn], done) => {\n        expect(conn instanceof IMAPConnection).toBe(true);\n        invokedCallback = true;\n        doneCallback = done;\n        return true;\n      },\n    });\n    expect(invokedCallback).toBe(true);\n    expect(IMAPConnection.prototype.connect.calls.length).toBe(1);\n    expect(IMAPConnection.prototype.end.calls.length).toBe(0);\n    expect(IMAPConnectionPool._poolMap[this.account.id]._availableConns.length === 2);\n    doneCallback();\n    expect(IMAPConnectionPool._poolMap[this.account.id]._availableConns.length === 3);\n  });\n\n  it('waits for an available IMAP connection', async () => {\n    let invokedCallback = false;\n    let doneCallback = null;\n    await IMAPConnectionPool.withConnectionsForAccount(this.account, {\n      desiredCount: 3,\n      logger: this.logger,\n      socketTimeout: 5 * 1000,\n      onConnected: ([conn], done) => {\n        expect(conn instanceof IMAPConnection).toBe(true);\n        invokedCallback = true;\n        doneCallback = done;\n        return true;\n      },\n    });\n    expect(invokedCallback).toBe(true);\n    expect(IMAPConnection.prototype.connect.calls.length).toBe(3);\n    expect(IMAPConnection.prototype.end.calls.length).toBe(0);\n\n    invokedCallback = false;\n    const promise = IMAPConnectionPool.withConnectionsForAccount(this.account, {\n      desiredCount: 1,\n      logger: this.logger,\n      socketTimeout: 5 * 1000,\n      onConnected: ([conn]) => {\n        expect(conn instanceof IMAPConnection).toBe(true);\n        invokedCallback = true;\n        return false;\n      },\n    });\n\n    expect(IMAPConnectionPool._poolMap[this.account.id]._queue.length).toBe(1)\n    doneCallback();\n    await promise;\n\n    expect(invokedCallback).toBe(true);\n    expect(IMAPConnection.prototype.connect.calls.length).toBe(4);\n    expect(IMAPConnection.prototype.end.calls.length).toBe(4);\n  });\n\n  it('does not retry on IMAP connection timeout', async () => {\n    let invokeCount = 0;\n    try {\n      await IMAPConnectionPool.withConnectionsForAccount(this.account, {\n        desiredCount: 1,\n        logger: this.logger,\n        socketTimeout: 5 * 1000,\n        onConnected: ([conn]) => {\n          expect(conn instanceof IMAPConnection).toBe(true);\n          if (invokeCount === 0) {\n            invokeCount += 1;\n            throw new IMAPConnectionTimeoutError();\n          }\n          invokeCount += 1;\n          return false;\n        },\n      });\n    } catch (err) {\n      expect(err instanceof IMAPConnectionTimeoutError).toBe(true);\n    }\n\n    expect(invokeCount).toBe(1);\n    expect(IMAPConnection.prototype.connect.calls.length).toBe(1);\n    expect(IMAPConnection.prototype.end.calls.length).toBe(1);\n  });\n\n  it('does not retry on other IMAP error', async () => {\n    let invokeCount = 0;\n    let errorCount = 0;\n    try {\n      await IMAPConnectionPool.withConnectionsForAccount(this.account, {\n        desiredCount: 1,\n        logger: this.logger,\n        socketTimeout: 5 * 1000,\n        onConnected: ([conn]) => {\n          expect(conn instanceof IMAPConnection).toBe(true);\n          if (invokeCount === 0) {\n            invokeCount += 1;\n            throw new IMAPSocketError();\n          }\n          invokeCount += 1;\n          return false;\n        },\n      });\n    } catch (err) {\n      errorCount += 1;\n    }\n\n    expect(invokeCount).toBe(1);\n    expect(errorCount).toBe(1);\n    expect(IMAPConnection.prototype.connect.calls.length).toBe(1);\n    expect(IMAPConnection.prototype.end.calls.length).toBe(1);\n  });\n});\n"
  },
  {
    "path": "packages/isomorphic-core/spec/jasmine/config.json",
    "content": "{\n  \"spec_dir\": \"spec\",\n  \"spec_files\": [\n    \"**/*[sS]pec.{js,es6}\"\n  ],\n  \"helpers\": [\n    \"helpers/**/*.{js,es6}\"\n  ],\n  \"stopSpecOnExpectationFailure\": false,\n  \"random\": false\n}\n"
  },
  {
    "path": "packages/isomorphic-core/spec/jasmine/execute.es6",
    "content": "import Jasmine from 'jasmine'\nimport JasmineExtensions from './extensions'\n\nexport default function execute(extendOpts) {\n  const jasmine = new Jasmine()\n  jasmine.loadConfigFile('spec/jasmine/config.json')\n  const jasmineExtensions = new JasmineExtensions()\n  jasmineExtensions.extend(extendOpts)\n  jasmine.execute()\n}\n"
  },
  {
    "path": "packages/isomorphic-core/spec/jasmine/extensions.es6",
    "content": "import applyPolyfills from './polyfills'\n\nexport default class JasmineExtensions {\n  extend({beforeEach, afterEach} = {}) {\n    applyPolyfills()\n    global.it = this._makeItAsync(global.it)\n    global.fit = this._makeItAsync(global.fit)\n    global.beforeAll = this._makeEachOrAllFnAsync(global.beforeAll)\n    global.afterAll = this._makeEachOrAllFnAsync(global.afterAll)\n    global.beforeEach = this._makeEachOrAllFnAsync(global.beforeEach)\n    global.afterEach = this._makeEachOrAllFnAsync(global.afterEach)\n    if (beforeEach) {\n      global.beforeEach(beforeEach)\n    }\n    if (afterEach) {\n      global.afterEach(afterEach)\n    }\n  }\n\n  _runAsync(userFn, done) {\n    if (!userFn) {\n      done()\n      return true\n    }\n    const resp = userFn.apply(this);\n    if (resp && resp.then) {\n      return resp.then(done).catch((error) => {\n        // Throwing an error doesn't register as stopping the test. Instead, run an\n        // expect() that will fail and show us the error. We still need to call done()\n        // afterwards, or it will take the full timeout to fail.\n        expect(error).toBeUndefined()\n        done()\n      })\n    }\n    done()\n    return resp\n  }\n\n  _makeEachOrAllFnAsync(jasmineEachFn) {\n    const self = this;\n    return (userFn) => {\n      return jasmineEachFn(function asyncEachFn(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n\n  _makeItAsync(jasmineIt) {\n    const self = this;\n    return (desc, userFn) => {\n      return jasmineIt(desc, function asyncIt(done) {\n        self._runAsync.call(this, userFn, done)\n      })\n    }\n  }\n}\n"
  },
  {
    "path": "packages/isomorphic-core/spec/jasmine/polyfills.es6",
    "content": "// We use Jasmine 1 in the client tests and Jasmine 2 in the cloud tests,\n// but isomorphic-core tests need to be run in both environments. Tests in\n// isomorphic-core should use Jasmine 1 syntax, and then we can add polyfills\n// here to make sure that they exist when we run in a Jasmine 2 environment.\n\nexport default function applyPolyfills() {\n  const origSpyOn = global.spyOn;\n  // There's no prototype to modify, so we have to modify the return\n  // values of spyOn as they're created.\n  global.spyOn = (object, methodName) => {\n    const originalValue = object[methodName]\n    const spy = origSpyOn(object, methodName)\n    object[methodName].originalValue = originalValue;\n    spy.andReturn = spy.and.returnValue;\n    spy.andCallFake = spy.and.callFake;\n    Object.defineProperty(spy.calls, 'length', {get: function getLength() { return this.count(); }})\n    return spy;\n  }\n}\n"
  },
  {
    "path": "packages/isomorphic-core/spec/message-utils-spec.js",
    "content": "/*\nconst {parseFromImap, parseSnippet, parseContacts} = require('../src/message-utils');\nconst {forEachJSONFixture, forEachHTMLAndTXTFixture, ACCOUNT_ID, getTestDatabase} = require('./helpers');\n\nxdescribe('MessageUtils', function MessageUtilsSpecs() {\n  beforeEach(() => {\n    waitsForPromise(async () => {\n      const db = await getTestDatabase()\n      const folder = await db.Folder.create({\n        id: 'test-folder-id',\n        accountId: ACCOUNT_ID,\n        version: 1,\n        name: 'Test Folder',\n        role: null,\n      });\n      this.options = { accountId: ACCOUNT_ID, db, folder };\n    })\n  })\n\n  describe(\"parseFromImap\", () => {\n    forEachJSONFixture('MessageUtils/parseFromImap', (filename, json) => {\n      it(`should correctly build message properties for ${filename}`, () => {\n        const {imapMessage, desiredParts, result} = json;\n        // requiring these to match makes it overly arduous to generate test\n        // cases from real accounts\n        const excludeKeys = new Set(['id', 'accountId', 'folderId', 'folder', 'labels']);\n\n        waitsForPromise(async () => {\n          const actual = await parseFromImap(imapMessage, desiredParts, this.options);\n          for (const key of Object.keys(result)) {\n            if (!excludeKeys.has(key)) {\n              expect(actual[key]).toEqual(result[key]);\n            }\n          }\n        });\n      });\n    })\n  });\n});\n\nconst snippetTestCases = [{\n  purpose: 'trim whitespace in basic plaintext',\n  body: '<pre>The quick brown fox\\n\\n\\tjumps over the lazy</pre>',\n  snippet: 'The quick brown fox jumps over the lazy',\n}, {\n  purpose: 'truncate long plaintext without breaking words',\n  body: '<pre>The quick brown fox jumps over the lazy dog and then the lazy dog rolls over and sighs. The fox turns around in a circle and then jumps onto a bush! It grins wickedly and wags its fat tail. As the lazy dog puts its head on its paws and cracks a sleepy eye open, a slow grin forms on its face. The fox has fallen into the bush and is yelping and squeaking.</pre>',\n  snippet: 'The quick brown fox jumps over the lazy dog and then the lazy dog rolls over and sighs. The fox turns',\n}, {\n  purpose: 'process basic HTML correctly',\n  body: '<html><title>All About Ponies</title><h1>PONIES AND RAINBOWS AND UNICORNS</h1><p>Unicorns are native to the hillsides of Flatagonia.</p></html>',\n  snippet: 'PONIES AND RAINBOWS AND UNICORNS Unicorns are native to the hillsides of Flatagonia.',\n}, {\n  purpose: 'properly strip rogue styling inside of <body> and trim whitespace in HTML',\n  body: '<html>\\n  <head></head>\\n  <body>\\n    <style>\\n    body { width: 100% !important; min-width: 100%; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: 100%; -ms-text-size-adjust: 100%; margin: 0; padding: 0; background: #fafafa;\\n    </style>\\n  <p>Look ma, no            CSS!</p></body></html>',\n  snippet: 'Look ma, no CSS!',\n}, {\n  purpose: 'properly process <br/> and <div/>',\n  body: '<p>Unicorns are <div>native</div>to the<br/>hillsides of<br/>Flatagonia.</p>',\n  snippet: 'Unicorns are native to the hillsides of Flatagonia.',\n}, {\n  purpose: 'properly strip out HTML comments',\n  body: '<p>Unicorns are<!-- an HTML comment! -->native to the</p>',\n  snippet: 'Unicorns are native to the',\n}, {\n  purpose: \"don't add extraneous spaces after text format markup\",\n  body: `\n  <td style=\"padding: 0px 10px\">\n            Hey there, <b>Nylas</b>!<br>\n            You have a new follower on Product Hunt.\n          </td>`,\n  snippet: 'Hey there, Nylas! You have a new follower on Product Hunt.',\n},\n]\n\nconst contactsTestCases = [{\n  purpose: \"not erroneously split contact names on commas\",\n  // NOTE: inputs must be in same format as output by mimelib.parseHeader\n  input: ['\"Little Bo Peep, The Hill\" <bopeep@example.com>'],\n  output: [{name: \"Little Bo Peep, The Hill\", email: \"bopeep@example.com\"}],\n}, {\n  purpose: \"extract two separate contacts, removing quotes properly & respecing unicode\",\n  input: ['AppleBees Zé <a@example.com>, \"Tiger Zen\" b@example.com'],\n  output: [\n    {name: 'AppleBees Zé', email: 'a@example.com'},\n    {name: 'Tiger Zen', email: 'b@example.com'},\n  ],\n}, {\n  purpose: \"correctly concatenate multiple array elements (from multiple header lines)\",\n  input: ['Yubi Key <yubi@example.com>', 'Smokey the Bear <smokey@example.com>'],\n  output: [\n    {name: 'Yubi Key', email: 'yubi@example.com'},\n    {name: 'Smokey the Bear', email: 'smokey@example.com'},\n  ],\n},\n]\n\ndescribe('MessageUtilsHelpers', function MessageUtilsHelperSpecs() {\n  describe('parseSnippet (basic)', () => {\n    snippetTestCases.forEach(({purpose, body, snippet}) => {\n      it(`should ${purpose}`, () => {\n        const parsedSnippet = parseSnippet(body);\n        expect(parsedSnippet).toEqual(snippet);\n      });\n    });\n  });\n  describe('parseSnippet (real world)', () => {\n    forEachHTMLAndTXTFixture('MessageUtils/parseSnippet', (filename, html, txt) => {\n      it(`should correctly extract the snippet from the html`, () => {\n        const parsedSnippet = parseSnippet(html);\n        expect(parsedSnippet).toEqual(txt);\n      });\n    });\n  });\n  describe('parseContacts (basic)', () => {\n    contactsTestCases.forEach(({purpose, input, output}) => {\n      it(`should ${purpose}`, () => {\n        const parsedContacts = parseContacts(input);\n        expect(parsedContacts).toEqual(output);\n      });\n    });\n  });\n});\n*/\n"
  },
  {
    "path": "packages/isomorphic-core/spec/run.es6",
    "content": "import executeJasmine from './jasmine/execute'\nexecuteJasmine()\n"
  },
  {
    "path": "packages/isomorphic-core/src/auth-helpers.es6",
    "content": "/* eslint camelcase: 0 */\nimport _ from 'underscore'\nimport Joi from 'joi'\nimport atob from 'atob';\nimport nodemailer from 'nodemailer';\nimport {CommonProviderSettings} from 'imap-provider-settings';\nimport {INSECURE_TLS_OPTIONS, SECURE_TLS_OPTIONS} from './tls-utils';\nimport IMAPConnection from './imap-connection'\nimport {RetryableError} from './errors'\nimport {convertSmtpError} from './smtp-errors'\n\nconst {GMAIL_CLIENT_ID, GMAIL_CLIENT_SECRET} = process.env;\n\nconst imapSmtpSettings = Joi.object().keys({\n  imap_host: [Joi.string().ip().required(), Joi.string().hostname().required()],\n  imap_port: Joi.number().integer().required(),\n  imap_username: Joi.string().required(),\n  imap_password: Joi.string().required(),\n  smtp_host: [Joi.string().ip().required(), Joi.string().hostname().required()],\n  smtp_port: Joi.number().integer().required(),\n  smtp_username: Joi.string().required(),\n  smtp_password: Joi.string().required(),\n  // new options - not required() for backcompat\n  smtp_security: Joi.string(),\n  imap_security: Joi.string(),\n  imap_allow_insecure_ssl: Joi.boolean(),\n  smtp_allow_insecure_ssl: Joi.boolean(),\n  // TODO: deprecated options - eventually remove!\n  smtp_custom_config: Joi.object(),\n  ssl_required: Joi.boolean(),\n}).required();\n\nconst resolvedGmailSettings = Joi.object().keys({\n  xoauth2: Joi.string().required(),\n  expiry_date: Joi.number().integer().required(),\n  imap_allow_insecure_ssl: Joi.boolean(),\n  smtp_allow_insecure_ssl: Joi.boolean(),\n}).required();\n\nconst office365Settings = Joi.object().keys({\n  name: Joi.string().required(),\n  type: Joi.string().valid('office365').required(),\n  email: Joi.string().required(),\n  password: Joi.string().required(),\n  username: Joi.string().required(),\n  imap_allow_insecure_ssl: Joi.boolean(),\n  smtp_allow_insecure_ssl: Joi.boolean(),\n}).required();\n\nexport const SUPPORTED_PROVIDERS = new Set(\n  ['gmail', 'office365', 'imap', 'icloud', 'yahoo', 'fastmail']\n);\n\nexport function generateXOAuth2Token(username, accessToken) {\n  // See https://developers.google.com/gmail/xoauth2_protocol\n  // for more details.\n  const s = `user=${username}\\x01auth=Bearer ${accessToken}\\x01\\x01`\n  return new Buffer(s).toString('base64');\n}\n\nexport function googleSettings(googleToken, email) {\n  const connectionSettings = Object.assign({\n    imap_username: email,\n    smtp_username: email,\n  }, CommonProviderSettings.gmail);\n\n  if (googleToken.imap_allow_insecure_ssl === true) {\n    connectionSettings.imap_allow_insecure_ssl = true\n  }\n\n  if (googleToken.smtp_allow_insecure_ssl === true) {\n    connectionSettings.smtp_allow_insecure_ssl = true\n  }\n\n  const connectionCredentials = {\n    expiry_date: Math.floor(googleToken.expiry_date / 1000),\n  };\n  if (GMAIL_CLIENT_ID && GMAIL_CLIENT_SECRET) {\n    // cloud-only credentials\n    connectionCredentials.client_id = GMAIL_CLIENT_ID;\n    connectionCredentials.client_secret = GMAIL_CLIENT_SECRET;\n    connectionCredentials.access_token = googleToken.access_token;\n    connectionCredentials.refresh_token = googleToken.refresh_token;\n  }\n  if (googleToken.xoauth2) {\n    connectionCredentials.xoauth2 = googleToken.xoauth2;\n  } else {\n    connectionCredentials.xoauth2 = generateXOAuth2Token(email, googleToken.access_token)\n  }\n  return {connectionSettings, connectionCredentials}\n}\n\nexport function credentialsForProvider({provider, settings, email} = {}) {\n  if (provider === \"gmail\") {\n    const {connectionSettings, connectionCredentials} = googleSettings(settings, email)\n    return {connectionSettings, connectionCredentials}\n  } else if (provider === \"office365\") {\n    const connectionSettings = CommonProviderSettings[provider];\n\n    if (settings.imap_allow_insecure_ssl === true) {\n      connectionSettings.imap_allow_insecure_ssl = true\n    }\n\n    if (settings.smtp_allow_insecure_ssl === true) {\n      connectionSettings.smtp_allow_insecure_ssl = true\n    }\n\n    const connectionCredentials = {\n      imap_username: email,\n      imap_password: settings.password || settings.imap_password,\n      smtp_username: email,\n      smtp_password: settings.password || settings.smtp_password,\n    }\n    return {connectionSettings, connectionCredentials}\n  } else if (SUPPORTED_PROVIDERS.has(provider)) {\n    const connectionSettings = _.pick(settings, [\n      'imap_host', 'imap_port', 'imap_security',\n      'smtp_host', 'smtp_port', 'smtp_security',\n      'smtp_allow_insecure_ssl',\n      'imap_allow_insecure_ssl',\n    ]);\n    // BACKCOMPAT ONLY - remove eventually & make _security params required!\n    if (!connectionSettings.imap_security) {\n      switch (connectionSettings.imap_port) {\n        case 993:\n          connectionSettings.imap_security = \"SSL / TLS\";\n          break;\n        default:\n          connectionSettings.imap_security = \"none\";\n          break;\n      }\n    }\n    if (!connectionSettings.smtp_security) {\n      switch (connectionSettings.smtp_security) {\n        case 465:\n          connectionSettings.smtp_security = \"SSL / TLS\";\n          break;\n        default:\n          connectionSettings.smtp_security = 'STARTTLS';\n          break;\n      }\n    }\n    // END BACKCOMPAT\n    const connectionCredentials = _.pick(settings, [\n      'imap_username', 'imap_password',\n      'smtp_username', 'smtp_password',\n    ]);\n    return {connectionSettings, connectionCredentials}\n  }\n  throw new Error(`Invalid provider: ${provider}`)\n}\n\nfunction bearerToken(xoauth2) {\n  // We have to unpack the access token from the entire XOAuth2\n  // token because it is re-packed during the SMTP connection login.\n  // https://github.com/nodemailer/smtp-connection/blob/master/lib/smtp-connection.js#L1418\n  const bearer = \"Bearer \";\n  const decoded = atob(xoauth2);\n  const tokenIndex = decoded.indexOf(bearer) + bearer.length;\n  return decoded.substring(tokenIndex, decoded.length - 2);\n}\n\nexport function smtpConfigFromSettings(provider, connectionSettings, connectionCredentials) {\n  const {smtp_host, smtp_port, smtp_security, smtp_allow_insecure_ssl} = connectionSettings;\n  const config = {\n    host: smtp_host,\n    port: smtp_port,\n    secure: smtp_security === 'SSL / TLS',\n  };\n  if (smtp_security === 'STARTTLS') {\n    config.requireTLS = true;\n  }\n  if (smtp_allow_insecure_ssl) {\n    config.tls = INSECURE_TLS_OPTIONS;\n  } else {\n    config.tls = SECURE_TLS_OPTIONS;\n  }\n\n  if (provider === 'gmail') {\n    const {xoauth2} = connectionCredentials;\n    if (!xoauth2) {\n      throw new Error(\"Missing XOAuth2 Token\")\n    }\n\n    const token = bearerToken(xoauth2);\n\n    config.auth = { user: connectionSettings.smtp_username, xoauth2: token }\n  } else if (SUPPORTED_PROVIDERS.has(provider)) {\n    const {smtp_username, smtp_password} = connectionCredentials\n    config.auth = { user: smtp_username, pass: smtp_password}\n  } else {\n    throw new Error(`${provider} not yet supported`)\n  }\n\n  return config;\n}\n\nexport function imapAuthRouteConfig() {\n  return {\n    description: 'Authenticates a new account.',\n    tags: ['accounts'],\n    auth: false,\n    validate: {\n      payload: {\n        email: Joi.string().email().required(),\n        name: Joi.string().required(),\n        provider: Joi.string().valid(...SUPPORTED_PROVIDERS).required(),\n        settings: Joi.alternatives().try(imapSmtpSettings, office365Settings, resolvedGmailSettings),\n      },\n    },\n  }\n}\n\n/**\n * NOTE: This gets run both on the cloud and on the client. On the cloud,\n * logger goes to our ELK stack and can be viewed via Kibana.\n *\n * The client, in onboarding-helpers.es6 will first run this code on the\n * cloud, and then run this code locally in the client-sync\n *\n * This is because both the cloud and the client must have valid auth\n * tokens.\n *\n * Since the client does not have reporting access to our ELK stack, it\n * sends the auth errors to Mixpanel\n */\nexport function imapAuthHandler(upsertAccount) {\n  const MAX_RETRIES = 2\n  const authHandler = async (request, reply, retryNum = 0) => {\n    const {email, provider, name} = request.payload;\n\n    const {connectionSettings, connectionCredentials} = credentialsForProvider(request.payload)\n\n    const smtpConfig = smtpConfigFromSettings(provider, connectionSettings, connectionCredentials);\n    const smtpTransport = nodemailer.createTransport(Object.assign({\n      connectionTimeout: 30000,\n    }, smtpConfig));\n\n    // All IMAP accounts require a valid SMTP server for sending, and we\n    // never want to allow folks to connect accounts and find out later\n    // that they entered the wrong SMTP credentials. So verify here also!\n    const testSMTP = () => {\n      return smtpTransport.verify()\n      .then(c => { if (c && c.end) c.end() })\n      .catch((error) => {\n        throw convertSmtpError(error, {connectionSettings: smtpConfig});\n      });\n    }\n\n    const testIMAP = () => {\n      return IMAPConnection.connect({\n        settings: Object.assign({}, connectionSettings, connectionCredentials),\n        logger: request.logger,\n        db: {}, // stub DB\n      }).then(c => { if (c && c.end) c.end() })\n    }\n\n    const upsertAccountWithParams = () => {\n      const accountParams = {\n        name: name,\n        provider: provider,\n        emailAddress: email,\n        connectionSettings: connectionSettings,\n      }\n      return upsertAccount(accountParams, connectionCredentials)\n    }\n\n    const buildReply = ({account, token}) => {\n      const response = account.toJSON();\n      response.account_token = token.value;\n      return JSON.stringify(response);\n    }\n\n    const retryRequest = (err, logger, message, statusCode) => {\n      if (retryNum < MAX_RETRIES) {\n        setTimeout(() => {\n          request.logger.info(`${err.constructor.name}. Retry #${retryNum + 1}`)\n          authHandler(request, reply, retryNum + 1)\n        }, 100)\n        return\n      }\n      logger.error(`AUTH ERROR: Failed after ${retryNum} retries`)\n      reply({message, type: \"api_error\"}).code(statusCode);\n      return\n    }\n\n    try {\n      await testSMTP();\n      await testIMAP();\n      const account = await upsertAccountWithParams();\n      return reply(buildReply(account))\n    } catch (err) {\n      const logger = request.logger.child({\n        account_name: name,\n        account_email: email,\n        account_provider: provider,\n        connection_settings: connectionSettings,\n        error_tb: err.stack,\n        error_name: err.constructor.name,\n        error_message: err.message,\n        error_status_code: err.statusCode,\n        error_user_message: err.userMessage,\n      })\n\n      const message = err.userMessage || err.message;\n      const statusCode = err.statusCode || 500;\n\n      if (err instanceof RetryableError) {\n        return retryRequest(err, logger, message, statusCode)\n      }\n\n      logger.error(`AUTH ERROR`)\n      return reply({message, type: \"api_error\"}).code(statusCode);\n    }\n  }\n  return authHandler\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/backoff-schedulers.es6",
    "content": "const BASE_TIMEOUT = 2 * 1000\nconst MAX_TIMEOUT = 5 * 60 * 1000\n\nfunction exponentialBackoff(base, numTries) {\n  return base * (2 ** numTries)\n}\n\nexport class BackoffScheduler {\n\n  constructor({baseDelay, maxDelay, getNextBackoffDelay, jitter = true} = {}) {\n    this._numTries = 0\n    this._currentDelay = 0\n    this._jitter = jitter\n    this._maxDelay = maxDelay || MAX_TIMEOUT\n    this._baseDelay = baseDelay || BASE_TIMEOUT\n    if (!getNextBackoffDelay) {\n      throw new Error('BackoffScheduler: Must pass `getNextBackoffDelay` function')\n    }\n    this._getNextBackoffDelay = getNextBackoffDelay\n  }\n\n  numTries() {\n    return this._numTries;\n  }\n\n  currentDelay() {\n    return this._currentDelay;\n  }\n\n  reset() {\n    this._numTries = 0\n    this._currentDelay = 0\n  }\n\n  nextDelay() {\n    const nextDelay = this._calcNextDelay()\n    this._numTries++\n    this._currentDelay = nextDelay\n    return nextDelay\n  }\n\n  _calcNextDelay() {\n    let nextDelay = this._getNextBackoffDelay(this._baseDelay, this._numTries)\n    if (this._jitter) {\n      // Why jitter? See:\n      // https://www.awsarchitectureblog.com/2015/03/backoff.html\n      nextDelay *= Math.random()\n    }\n    return Math.min(nextDelay, this._maxDelay)\n  }\n}\n\nexport class ExponentialBackoffScheduler extends BackoffScheduler {\n  constructor(opts = {}) {\n    super({...opts, getNextBackoffDelay: exponentialBackoff})\n  }\n}\n\n"
  },
  {
    "path": "packages/isomorphic-core/src/database-types.js",
    "content": "const Sequelize = require('sequelize');\n\nmodule.exports = {\n  JSONColumn(fieldName, options = {}) {\n    return Object.assign(options, {\n      type: options.columnType || Sequelize.TEXT,\n      get() {\n        try {\n          const val = this.getDataValue(fieldName);\n          if (!val) {\n            const {defaultValue} = options\n            return defaultValue ? Object.assign({}, defaultValue) : {};\n          }\n          return JSON.parse(val);\n        } catch (err) {\n          console.error(err);\n          return null\n        }\n      },\n      set(val) {\n        this.setDataValue(fieldName, JSON.stringify(val));\n      },\n      defaultValue: undefined,\n    })\n  },\n  JSONArrayColumn(fieldName, options = {}) {\n    return Object.assign(options, {\n      type: Sequelize.TEXT,\n      get() {\n        try {\n          const val = this.getDataValue(fieldName);\n          if (!val) {\n            const {defaultValue} = options\n            return defaultValue || [];\n          }\n          const arr = JSON.parse(val)\n          if (!Array.isArray(arr)) {\n            throw new Error('JSONArrayType should be an array')\n          }\n          return JSON.parse(val);\n        } catch (err) {\n          console.error(err);\n          return null\n        }\n      },\n      set(val) {\n        if (!Array.isArray(val)) {\n          throw new Error('JSONArrayType should be an array')\n        }\n        this.setDataValue(fieldName, JSON.stringify(val));\n      },\n      defaultValue: undefined,\n    })\n  },\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/db-utils.es6",
    "content": "// In order to be able to represent 4-byte characters such as some emoji, we\n// must use the 'utf8mb4' character set on MySQL. Any table using this\n// character set can't have indexes on fields longer than this length without\n// triggering the error\n//\n// ERROR 1071 (42000): Specified key was too long; max key length is 767 bytes\n//\n// (or, without sql_mode = TRADITIONAL - getting silently truncated!)\nconst MAX_INDEXABLE_LENGTH = 191;\n\nexport {MAX_INDEXABLE_LENGTH};\n"
  },
  {
    "path": "packages/isomorphic-core/src/delta-stream-builder.js",
    "content": "const _ = require('underscore');\nconst Rx = require('rx-lite')\nconst stream = require('stream');\nconst DELTA_CONNECTION_TIMEOUT_MS = 15 * 60000;\nconst OBSERVABLE_TIMEOUT_MS = DELTA_CONNECTION_TIMEOUT_MS - (1 * 60000);\n\n/**\n * A Transaction references objects that changed. This finds and inflates\n * those objects.\n *\n * Resolves to an array of transactions with their `attributes` set to be\n * the inflated model they reference.\n */\nfunction inflateTransactions(db, accountId, transactions = [], sourceName) {\n  const transactionJSONs = transactions.map((t) => (t.toJSON ? t.toJSON() : t))\n\n  transactionJSONs.forEach((t) => {\n    t.cursor = t.id;\n    t.accountId = accountId;\n  });\n\n  const byModel = _.groupBy(transactionJSONs, \"object\");\n  const byObjectIds = _.groupBy(transactionJSONs, \"objectId\");\n\n  return Promise.all(Object.keys(byModel).map((modelName) => {\n    const modelIds = byModel[modelName].filter(t => t.event !== 'delete').map(t => t.objectId);\n    const modelConstructorName = modelName.charAt(0).toUpperCase() + modelName.slice(1);\n    const ModelKlass = db[modelConstructorName]\n\n    let includes = [];\n    if (ModelKlass.requiredAssociationsForJSON) {\n      includes = ModelKlass.requiredAssociationsForJSON(db)\n    }\n    return ModelKlass.findAll({\n      where: {id: modelIds},\n      include: includes,\n    }).then((models) => {\n      const remaining = _.difference(modelIds, models.map(m => `${m.id}`))\n      if (remaining.length !== 0) {\n        const badTrans = byModel[modelName].filter(t =>\n          remaining.includes(t.objectId))\n        console.error(`While inflating ${sourceName} transactions, we couldn't find models for some ${modelName} IDs`, remaining, badTrans)\n      }\n      for (const model of models) {\n        const transactionsForModel = byObjectIds[model.id];\n        for (const t of transactionsForModel) {\n          t.attributes = model.toJSON();\n        }\n      }\n    });\n  })).then(() => transactionJSONs)\n}\n\nfunction stringifyTransactions(db, accountId, transactions = [], sourceName) {\n  return inflateTransactions(db, accountId, transactions, sourceName)\n  .then((transactionJSONs) => {\n    return `${transactionJSONs.map(JSON.stringify).join(\"\\n\")}\\n`;\n  });\n}\n\nfunction transactionsSinceCursor(db, cursor, accountId) {\n  return db.Transaction.streamAll({where: { id: {$gt: cursor || 0}, accountId }});\n}\n\nmodule.exports = {\n  DELTA_CONNECTION_TIMEOUT_MS: DELTA_CONNECTION_TIMEOUT_MS,\n  buildAPIStream(request, {databasePromise, cursor, accountId, deltasSource}) {\n    return databasePromise.then((db) => {\n      const source = Rx.Observable.merge(\n        transactionsSinceCursor(db, cursor, accountId).flatMap((ts) =>\n          stringifyTransactions(db, accountId, ts, \"initial\")),\n        deltasSource.flatMap((t) =>\n          stringifyTransactions(db, accountId, [t], \"new\")),\n        Rx.Observable.interval(1000).map(() => \"\\n\")\n      ).timeout(OBSERVABLE_TIMEOUT_MS);\n\n      const outputStream = stream.Readable();\n      outputStream._read = () => { return };\n      const disposable = source.subscribe((str) => outputStream.push(str))\n      // See the following for why we need to set up the listeners on the raw\n      // stream.\n      // http://stackoverflow.com/questions/26221000/detecting-when-a-long-request-has-ended-in-nodejs-express\n      // https://github.com/hapijs/discuss/issues/322#issuecomment-235999544\n      //\n      // Hapi's disconnect event only fires on error or unexpected aborts: https://hapijs.com/api#response-events\n      request.raw.req.on('error', (error) => {\n        request.logger.error({err: error}, 'Delta connection stream errored')\n        disposable.dispose()\n      })\n      request.raw.req.on('close', () => {\n        request.logger.info('Delta connection stream was closed')\n        disposable.dispose()\n      })\n      request.raw.req.on('end', () => {\n        request.logger.info('Delta connection stream ended')\n        disposable.dispose()\n      })\n      request.on(\"disconnect\", () => {\n        request.logger.info('Delta connection request was disconnected')\n        disposable.dispose()\n      });\n\n      return outputStream;\n    });\n  },\n\n  buildDeltaObservable({db, cursor, accountId, deltasSource}) {\n    return Rx.Observable.merge(\n      transactionsSinceCursor(db, cursor, accountId).flatMap((ts) =>\n        inflateTransactions(db, accountId, ts, \"initial\")),\n      deltasSource.flatMap((t) =>\n        inflateTransactions(db, accountId, [t], \"new\"))\n    )\n  },\n\n  buildCursor({databasePromise}) {\n    return databasePromise.then(({Transaction}) => {\n      return Transaction.findOne({order: [['id', 'DESC']]}).then((t) => {\n        return t ? t.id : 0;\n      });\n    });\n  },\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/env-helpers.es6",
    "content": "\nexport function isClientEnv() {\n  return typeof window !== 'undefined' && typeof window.NylasEnv !== 'undefined'\n}\n\nexport function isCloudEnv() {\n  return !isClientEnv()\n}\n\nexport function inDevMode() {\n  if (isClientEnv()) {\n    return window.NylasEnv.inDevMode();\n  }\n  return process.env.NODE_ENV !== 'production';\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/errors.es6",
    "content": "export class NylasError extends Error {\n  toJSON() {\n    let json = {}\n    if (super.toJSON) {\n      // Chromium `Error`s have a `toJSON`, but Node `Error`s do NOT!\n      json = super.toJSON()\n    }\n    Object.getOwnPropertyNames(this).forEach((key) => {\n      json[key] = this[key];\n    });\n    return json\n  }\n}\n\nexport class APIError extends NylasError {\n  constructor(message, statusCode, data) {\n    super(message);\n    this.statusCode = statusCode;\n    this.data = data;\n  }\n}\n\n/**\n * An abstract base class that can be used to indicate Errors that may fix\n * themselves when retried\n */\nexport class RetryableError extends NylasError { }\n"
  },
  {
    "path": "packages/isomorphic-core/src/hook-increment-version-on-save.js",
    "content": "const _ = require('underscore');\n\nmodule.exports = (db) => {\n  for (const modelName of Object.keys(db)) {\n    const model = db[modelName];\n\n    const allIgnoredFields = (changedFields) => {\n      return _.isEqual(changedFields, ['syncState']);\n    }\n\n    model.beforeCreate('increment-version-c', (instance) => {\n      instance.version = 1;\n    });\n    model.beforeUpdate('increment-version-u', (instance) => {\n      if (!allIgnoredFields(Object.keys(instance._changed))) {\n        instance.version = instance.version ? instance.version + 1 : 1;\n      }\n    });\n  }\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/hook-transaction-log.js",
    "content": "const _ = require('underscore')\n\nmodule.exports = (db, sequelize, {only, onCreatedTransaction} = {}) => {\n  if (!db.Transaction) {\n    throw new Error(\"Cannot enable transaction logging, there is no Transaction model class in this database.\")\n  }\n  const isTransaction = ($modelOptions) => {\n    return $modelOptions.name.singular === \"transaction\"\n  }\n\n  const allIgnoredFields = (changedFields) => {\n    return _.isEqual(changedFields, ['updatedAt', 'version'])\n  }\n\n  const transactionLogger = (event) => {\n    return ({dataValues, _changed, $modelOptions}) => {\n      let name = $modelOptions.name.singular;\n      if (name === 'metadatum') {\n        name = 'metadata';\n      }\n\n      if (name === 'reference') {\n        return;\n      }\n\n      if (name === 'message' && dataValues.isDraft) {\n        // TODO: when draft syncing support added, remove this and force\n        // transactions for all drafts in db to sync to app\n        return;\n      }\n\n      if ((only && !only.includes(name)) || isTransaction($modelOptions)) {\n        return;\n      }\n\n      const changedFields = Object.keys(_changed)\n      if (event !== 'delete' && (changedFields.length === 0 || allIgnoredFields(changedFields))) {\n        return;\n      }\n\n      const accountId = db.accountId ? db.accountId : dataValues.accountId;\n      if (!accountId) {\n        throw new Error(\"Assertion failure: Cannot create a transaction - could not resolve accountId.\")\n      }\n\n      const transactionData = Object.assign({event}, {\n        object: name,\n        objectId: dataValues.id,\n        accountId: accountId,\n        changedFields: changedFields,\n      });\n\n      db.Transaction.create(transactionData).then(onCreatedTransaction)\n    }\n  }\n\n  sequelize.addHook(\"afterCreate\", transactionLogger(\"create\"))\n  sequelize.addHook(\"afterUpdate\", transactionLogger(\"modify\"))\n\n  // NOTE: Hooking UPSERT requires Sequelize 4.x. We're\n  // on version 3 right now, but leaving this here for when we upgrade.\n  sequelize.addHook(\"afterUpsert\", transactionLogger(\"modify\"))\n  sequelize.addHook(\"afterDestroy\", transactionLogger(\"delete\"))\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/imap-box.es6",
    "content": "const _ = require('underscore');\nconst {RetryableError} = require('./errors')\nconst {IMAPConnectionNotReadyError} = require('./imap-errors');\n\n/*\nIMAPBox uses Proxy to wrap the \"box\" exposed by node-imap. It provides higher-level\nprimitives, but you can still call through to properties / methods of the node-imap\nbox, ala `imapbox.uidvalidity`\n*/\nclass IMAPBox {\n  constructor(imapConn, box) {\n    this._conn = imapConn\n    this._box = box\n\n    return new Proxy(this, {\n      get(obj, prop) {\n        const val = (prop in obj) ? obj[prop] : obj._box[prop];\n\n        if (_.isFunction(val)) {\n          const myBox = obj._box.name;\n          const openBox = obj._conn.getOpenBoxName()\n          if (myBox !== openBox) {\n            return () => {\n              throw new RetryableError(`IMAPBox::${prop} - Mailbox is no longer selected on the IMAPConnection (${myBox} != ${openBox}).`);\n            }\n          }\n        }\n\n        return val;\n      },\n    })\n  }\n\n  _withPreparedConnection(cb) {\n    return this._conn._withPreparedConnection(cb)\n  }\n\n  /**\n   * @param {array|string} range - can be a single message identifier,\n   * a message identifier range (e.g. '2504:2507' or '*' or '2504:*'),\n   * an array of message identifiers, or an array of message identifier ranges.\n   * @param {Object} options\n   * @param {function} forEachMessageCallback - function to be called with each\n   * message as it comes in\n   * @return {Promise} that will feed each message as it becomes ready\n   */\n  async fetchEach(range, options, forEachMessageCallback) {\n    if (!options) {\n      throw new Error(\"IMAPBox.fetch now requires an options object.\")\n    }\n    if (range.length === 0) {\n      return Promise.resolve()\n    }\n\n    return this._withPreparedConnection((imap) => {\n      return new Promise((resolve, reject) => {\n        const f = imap.fetch(range, options);\n        f.on('message', (imapMessage) => {\n          const parts = {};\n          let headers = null;\n          let attributes = null;\n          imapMessage.on('attributes', (attrs) => {\n            attributes = attrs;\n          });\n          imapMessage.on('body', (stream, info) => {\n            const chunks = [];\n\n            stream.on('data', (chunk) => {\n              chunks.push(chunk);\n            });\n\n            stream.once('end', () => {\n              const full = Buffer.concat(chunks);\n              if (info.which === 'HEADER') {\n                headers = full;\n              } else {\n                parts[info.which] = full;\n              }\n            });\n          });\n          imapMessage.once('end', () => {\n            // attributes is an object containing ascii strings, but parts and\n            // headers are undecoded binary Buffers (since the data for mime\n            // parts cannot be decoded to strings without looking up charset data\n            // in metadata, and this function's job is only to fetch the raw data)\n            forEachMessageCallback({attributes, headers, parts});\n          });\n        })\n        f.once('error', reject);\n        f.once('end', resolve);\n      })\n    });\n  }\n\n  /**\n   * @return {Promise} that resolves to requested message\n   */\n  async fetchMessage(uid) {\n    if (!uid) {\n      throw new Error(\"IMAPConnection.fetchMessage requires a message uid.\")\n    }\n    let message;\n    await this.fetchEach([uid], {bodies: ['HEADER', 'TEXT']}, (msg) => { message = msg; })\n    return message\n  }\n\n  async fetchMessageStream(uid, {fetchOptions, onFetchComplete} = {}) {\n    if (!uid) {\n      throw new Error(\"IMAPConnection.fetchStream requires a message uid.\")\n    }\n    if (!fetchOptions) {\n      throw new Error(\"IMAPConnection.fetchStream requires an options object.\")\n    }\n    return this._withPreparedConnection((imap) => {\n      return new Promise((resolve, reject) => {\n        const f = imap.fetch(uid, fetchOptions);\n        f.on('message', (imapMessage) => {\n          imapMessage.on('body', (stream) => {\n            resolve(stream)\n          })\n        })\n        f.once('error', reject)\n        f.once('end', onFetchComplete || (() => {}));\n      })\n    })\n  }\n\n  /**\n   * @param {array|string} range - can be a single message identifier,\n   * a message identifier range (e.g. '2504:2507' or '*' or '2504:*'),\n   * an array of message identifiers, or an array of message identifier ranges.\n   * @return {Promise} that resolves to a map of uid -> attributes for every\n   * message in the range\n   */\n  async fetchUIDAttributes(range, fetchOptions = {}) {\n    return this._withPreparedConnection((imap) => {\n      return new Promise((resolve, reject) => {\n        const attributesByUID = {};\n        const f = imap.fetch(range, fetchOptions);\n        f.on('message', (msg) => {\n          msg.on('attributes', (attrs) => {\n            attributesByUID[attrs.uid] = attrs;\n          })\n        });\n        f.once('error', reject);\n        f.once('end', () => resolve(attributesByUID));\n      })\n    });\n  }\n\n  addFlags(range, flags) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::addFlags`)\n    }\n\n    return this._withPreparedConnection((imap) => imap.addFlagsAsync(range, flags))\n  }\n\n  delFlags(range, flags) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::delFlags`)\n    }\n    return this._withPreparedConnection((imap) => imap.delFlagsAsync(range, flags))\n  }\n\n  moveFromBox(range, folderName) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::moveFromBox`)\n    }\n    return this._withPreparedConnection((imap) => imap.moveAsync(range, folderName))\n  }\n\n  setLabels(range, labels) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::moveFromBox`)\n    }\n    return this._withPreparedConnection((imap) => imap.setLabelsAsync(range, labels))\n  }\n\n  removeLabels(range, labels) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::moveFromBox`)\n    }\n    return this._withPreparedConnection((imap) => imap.delLabelsAsync(range, labels))\n  }\n\n  append(rawMime, options) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::append`)\n    }\n    return this._withPreparedConnection((imap) => imap.appendAsync(rawMime, options))\n  }\n\n  search(criteria) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::search`)\n    }\n    return this._withPreparedConnection((imap) => imap.searchAsync(criteria))\n  }\n\n  closeBox({expunge = true} = {}) {\n    if (!this._conn._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPBox::closeBox`)\n    }\n    return this._withPreparedConnection((imap) => imap.closeBoxAsync(expunge))\n  }\n}\n\nmodule.exports = IMAPBox;\n"
  },
  {
    "path": "packages/isomorphic-core/src/imap-connection-pool.es6",
    "content": "const IMAPConnection = require('./imap-connection').default;\nconst {inDevMode} = require('./env-helpers')\n\nconst MAX_DEV_MODE_CONNECTIONS = 3\nconst MAX_GMAIL_CONNECTIONS = 7;\nconst MAX_O365_CONNECTIONS = 5;\nconst MAX_ICLOUD_CONNECTIONS = 5;\nconst MAX_IMAP_CONNECTIONS = 5;\n\nclass AccountConnectionPool {\n  constructor(maxConnections) {\n    this._availableConns = new Array(maxConnections).fill(null);\n    this._queue = [];\n  }\n\n  async _genConnection(account, socketTimeout, logger) {\n    const settings = account.connectionSettings;\n    const credentials = account.decryptedCredentials();\n\n    if (!settings || !settings.imap_host) {\n      throw new Error(\"_genConnection: There are no IMAP connection settings for this account.\");\n    }\n    if (!credentials) {\n      throw new Error(\"_genConnection: There are no IMAP connection credentials for this account.\");\n    }\n\n    const conn = new IMAPConnection({\n      db: null,\n      settings: Object.assign({}, settings, credentials, {socketTimeout}),\n      logger,\n      account,\n    });\n\n    return conn.connect();\n  }\n\n  async withConnections({account, desiredCount, logger, socketTimeout, onConnected}) {\n    // If we wake up from the first await but don't have enough connections in\n    // the pool then we need to prepend ourselves to the queue until there are\n    // enough. This guarantees that the queue is fair.\n    let prependToQueue = false;\n    while (this._availableConns.length < desiredCount) {\n      await new Promise((resolve) => {\n        if (prependToQueue) {\n          this._queue.unshift(resolve);\n        } else {\n          this._queue.push(resolve);\n        }\n      });\n      prependToQueue = true;\n    }\n\n    let conns = [];\n    let keepOpen = false;\n    let calledOnDone = false;\n\n    const onDone = () => {\n      if (calledOnDone) { return }\n      calledOnDone = true\n      keepOpen = false;\n\n      conns.filter(Boolean).forEach(conn => conn.end());\n      conns.filter(Boolean).forEach(conn => conn.removeAllListeners());\n      conns.fill(null);\n      this._availableConns = conns.concat(this._availableConns);\n      if (this._queue.length > 0) {\n        const resolveWaitForConnection = this._queue.shift();\n        resolveWaitForConnection();\n      }\n    };\n\n    try {\n      for (let i = 0; i < desiredCount; ++i) {\n        conns.push(this._availableConns.shift());\n      }\n      conns = await Promise.all(\n        conns.map(() => this._genConnection(account, socketTimeout, logger))\n      );\n\n      keepOpen = await onConnected(conns, onDone);\n      if (!keepOpen) {\n        onDone();\n      }\n    } catch (err) {\n      onDone()\n      throw err;\n    }\n  }\n}\n\nclass IMAPConnectionPool {\n  constructor() {\n    this._poolMap = {};\n  }\n\n  _maxConnectionsForAccount(account) {\n    if (inDevMode()) {\n      return MAX_DEV_MODE_CONNECTIONS;\n    }\n\n    switch (account.provider) {\n      case 'gmail': return MAX_GMAIL_CONNECTIONS;\n      case 'office365': return MAX_O365_CONNECTIONS;\n      case 'icloud': return MAX_ICLOUD_CONNECTIONS;\n      case 'imap': return MAX_IMAP_CONNECTIONS;\n      default: return MAX_DEV_MODE_CONNECTIONS;\n    }\n  }\n\n  async withConnectionsForAccount(account, {desiredCount, logger, socketTimeout, onConnected}) {\n    if (!this._poolMap[account.id]) {\n      this._poolMap[account.id] = new AccountConnectionPool(this._maxConnectionsForAccount(account));\n    }\n\n    const pool = this._poolMap[account.id];\n    await pool.withConnections({account, desiredCount, logger, socketTimeout, onConnected});\n  }\n}\n\nmodule.exports = new IMAPConnectionPool();\n"
  },
  {
    "path": "packages/isomorphic-core/src/imap-connection.es6",
    "content": "import Imap from 'imap';\nimport _ from 'underscore';\nimport xoauth2 from 'xoauth2';\nimport EventEmitter from 'events';\nimport {INSECURE_TLS_OPTIONS, SECURE_TLS_OPTIONS} from './tls-utils';\nimport PromiseUtils from './promise-utils';\nimport IMAPBox from './imap-box';\nimport {RetryableError} from './errors'\nimport {\n  convertImapError,\n  IMAPConnectionTimeoutError,\n  IMAPConnectionNotReadyError,\n  IMAPConnectionEndedError,\n} from './imap-errors';\n\n\nconst Capabilities = {\n  Gmail: 'X-GM-EXT-1',\n  Quota: 'QUOTA',\n  UIDPlus: 'UIDPLUS',\n  Condstore: 'CONDSTORE',\n  Search: 'ESEARCH',\n  Sort: 'SORT',\n}\nconst ONE_HOUR_SECS = 60 * 60;\nconst AUTH_TIMEOUT_MS = 30 * 1000;\nconst DEFAULT_SOCKET_TIMEOUT_MS = 30 * 1000;\n\nexport default class IMAPConnection extends EventEmitter {\n\n  static DefaultSocketTimeout = DEFAULT_SOCKET_TIMEOUT_MS;\n\n  static connect(...args) {\n    return new IMAPConnection(...args).connect()\n  }\n\n  static asyncResolveIMAPSettings(baseSettings) {\n    let autotls;\n    // BACKCOMPAT ONLY - remove the if conditional on this eventually\n    if (baseSettings.imap_security) {\n      switch (baseSettings.imap_security) {\n        case 'STARTTLS':\n          autotls = 'required';\n          break;\n        case 'SSL / TLS':\n          autotls = 'never';\n          break;\n        default:\n          autotls = 'always';\n          break;\n      }\n    } else {\n      // old code used the default value\n      autotls = 'never';\n    }\n    const settings = {\n      host: baseSettings.imap_host,\n      port: baseSettings.imap_port,\n      user: baseSettings.imap_username,\n      password: baseSettings.imap_password,\n      // TODO: ssl_required is a deprecated setting, remove eventually\n      tls: baseSettings.imap_security === 'SSL / TLS' || baseSettings.ssl_required,\n      autotls: autotls,\n      socketTimeout: baseSettings.socketTimeout || DEFAULT_SOCKET_TIMEOUT_MS,\n      authTimeout: baseSettings.authTimeout || AUTH_TIMEOUT_MS,\n    }\n    // TODO: second part of || is for backcompat only, remove eventually (old\n    // settings were insecure by default)\n    if (baseSettings.imap_allow_insecure_ssl || baseSettings.imap_allow_insecure_ssl === undefined) {\n      settings.tlsOptions = INSECURE_TLS_OPTIONS;\n    } else {\n      settings.tlsOptions = SECURE_TLS_OPTIONS;\n    }\n\n    if (process.env.NYLAS_DEBUG) {\n      settings.debug = console.log;\n    }\n\n    // This account uses XOAuth2, and we have the client_id + refresh token\n    if (baseSettings.refresh_token) {\n      const xoauthFields = ['client_id', 'client_secret', 'imap_username', 'refresh_token'];\n      if (Object.keys(_.pick(baseSettings, xoauthFields)).length !== 4) {\n        return Promise.reject(new Error(`IMAPConnection: Expected ${xoauthFields.join(',')} when given refresh_token`))\n      }\n      return new Promise((resolve, reject) => {\n        xoauth2.createXOAuth2Generator({\n          clientId: baseSettings.client_id,\n          clientSecret: baseSettings.client_secret,\n          user: baseSettings.imap_username,\n          refreshToken: baseSettings.refresh_token,\n        })\n        .getToken((err, token) => {\n          if (err) { return reject(err) }\n          delete settings.password;\n          settings.xoauth2 = token;\n          settings.expiry_date = Math.floor(Date.now() / 1000) + ONE_HOUR_SECS;\n          return resolve(settings);\n        });\n      });\n    }\n\n    // This account uses XOAuth2, and we have a token given to us by the\n    // backend, which has the client secret.\n    if (baseSettings.xoauth2) {\n      delete settings.password;\n      settings.xoauth2 = baseSettings.xoauth2;\n      settings.expiry_date = baseSettings.expiry_date;\n    }\n\n    return Promise.resolve(settings);\n  }\n\n  constructor({db, account, settings, logger} = {}) {\n    super();\n\n    if (!(settings instanceof Object)) {\n      throw new Error(\"IMAPConnection: Must be instantiated with `settings`\")\n    }\n    if (!logger) {\n      throw new Error(\"IMAPConnection: Must be instantiated with `logger`\")\n    }\n\n    this._logger = logger;\n    this._db = db;\n    this._account = account;\n    this._queue = [];\n    this._currentOperation = null;\n    this._baseSettings = settings;\n    this._resolvedSettings = null;\n    this._imap = null;\n    this._connectPromise = null;\n    this._isOpeningBox = false;\n    this._lastOpenDuration = null;\n  }\n\n  async connect() {\n    if (!this._connectPromise) {\n      this._connectPromise = new Promise(async (resolve, reject) => {\n        try {\n          this._resolvedSettings = await IMAPConnection.asyncResolveIMAPSettings(this._baseSettings)\n          await this._buildUnderlyingConnection()\n          resolve(this)\n        } catch (err) {\n          reject(err)\n        }\n      })\n    }\n    return this._connectPromise;\n  }\n\n  end() {\n    if (this._imap) {\n      this._imap.end();\n      this._imap = null;\n    }\n    this._queue = [];\n    this._connectPromise = null;\n  }\n\n  async _buildUnderlyingConnection() {\n    this._imap = PromiseUtils.promisifyAll(new Imap(this._resolvedSettings));\n    return this._withPreparedConnection(() => {\n      return new Promise((resolve) => {\n        // `mail` event is emitted when new mail arrives in the currently open mailbox.\n        let lastMailEventBox = null;\n        this._imap.on('mail', () => {\n          // Fix https://github.com/mscdex/node-imap/issues/585\n          if (this._isOpeningBox) { return }\n          if (!this._imap) { return }\n          if (lastMailEventBox === null || lastMailEventBox === this._imap._box.name) {\n            // Fix https://github.com/mscdex/node-imap/issues/445\n            this.emit('mail');\n          }\n          lastMailEventBox = this._imap._box.name\n        });\n\n        // Emitted if the UID validity value for the currently open mailbox\n        // changes during the current session.\n        this._imap.on('uidvalidity', () => this.emit('uidvalidity'))\n\n        // Emitted when message metadata (e.g. flags) changes externally.\n        this._imap.on('update', () => this.emit('update'))\n\n        this._imap.on('alert', (msg) => {\n          this._logger.info({imap_server_msg: msg}, `IMAP server message`)\n        });\n\n        this._imap.once('ready', () => {\n          resolve()\n        });\n\n        this._imap.once('error', () => {\n          this.end();\n        });\n\n        this._imap.once('end', () => {\n          this._logger.warn('Underlying IMAP connection has ended')\n          this.end();\n        });\n\n        this._imap.connect();\n      });\n    })\n  }\n\n  /**\n   * @return {Promise} that resolves/rejects when the Promise returned by the\n   * passed-in callback resolves or rejects, and additionally will reject when\n   * the IMAP connection closes, ends or times out.\n   * This is important for 2 main reasons:\n   * - node-imap can sometimes hang the current operation after the connection\n   *   has emmitted an `end` event. For this reason, we need to manually reject\n   *   and end() on `end` event.\n   * - node-imap does not seem to respect the socketTimeout setting, so it won't\n   *   actually time out an operation after the specified timeout has passed.\n   *   For this reason, we have to manually reject when the timeout period has\n   *   passed.\n   * @param {function} callback - This callback will receive as a single arg\n   * a node-imap connection instance, and should return a Promise.\n   */\n  async _withPreparedConnection(callback) {\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::_withPreparedConnection`)\n    }\n\n    if (this._isOpeningBox) {\n      throw new RetryableError('IMAPConnection: Cannot operate on connection while opening a box.')\n    }\n\n    let onEnded = null;\n    let onErrored = null;\n\n    try {\n      return await new Promise(async (resolve, reject) => {\n        const socketTimeout = setTimeout(() => {\n          reject(new IMAPConnectionTimeoutError('Socket timed out'))\n        }, this._resolvedSettings.socketTimeout)\n\n        const wrappedResolve = (result) => {\n          clearTimeout(socketTimeout)\n          resolve(result)\n        }\n        const wrappedReject = (error) => {\n          clearTimeout(socketTimeout)\n          const convertedError = convertImapError(error, {connectionSettings: this._resolvedSettings})\n          reject(convertedError)\n          this.end()\n        }\n\n        onEnded = () => {\n          wrappedReject(new IMAPConnectionEndedError())\n        };\n        onErrored = (error) => {\n          wrappedReject(error);\n        };\n\n        this._imap.on('error', onErrored);\n        this._imap.on('end', onEnded);\n\n        try {\n          const result = await callback(this._imap)\n          wrappedResolve(result)\n        } catch (error) {\n          wrappedReject(error)\n        }\n      })\n    } finally {\n      if (this._imap) {\n        this._imap.removeListener('error', onErrored);\n        this._imap.removeListener('end', onEnded);\n      }\n    }\n  }\n\n  getResolvedSettings() {\n    return this._resolvedSettings\n  }\n\n  getOpenBoxName() {\n    return (this._imap && this._imap._box) ? this._imap._box.name : null;\n  }\n\n  serverSupports(capability) {\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::serverSupports`)\n    }\n    return this._imap.serverSupports(capability);\n  }\n\n  getLastOpenDuration() {\n    if (this._isOpeningBox) {\n      throw new RetryableError('IMAPConnection: Cannot operate on connection while opening a box.')\n    }\n    return this._lastOpenDuration;\n  }\n\n  /**\n   * @return {Promise} that resolves to instance of IMAPBox\n   */\n  async openBox(folderName, {readOnly = false, refetchBoxInfo = false} = {}) {\n    if (!folderName) {\n      throw new Error('IMAPConnection::openBox - You must provide a folder name')\n    }\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::openBox`)\n    }\n    if (!refetchBoxInfo && folderName === this.getOpenBoxName()) {\n      return Promise.resolve(new IMAPBox(this, this._imap._box));\n    }\n    return this._withPreparedConnection(async (imap) => {\n      try {\n        this._isOpeningBox = true\n        this._lastOpenDuration = null;\n        const before = Date.now();\n        const box = await imap.openBoxAsync(folderName, readOnly)\n        this._lastOpenDuration = Date.now() - before;\n        this._isOpeningBox = false\n        return new IMAPBox(this, box)\n      } finally {\n        this._isOpeningBox = false\n      }\n    })\n  }\n\n  async getLatestBoxStatus(folderName) {\n    if (!folderName) {\n      throw new Error('IMAPConnection::getLatestBoxStatus - You must provide a folder name')\n    }\n    if (folderName === this.getOpenBoxName()) {\n      // If the box is already open, we need to re-issue a SELECT in order to\n      // get the latest stats from the box (e.g. latest uidnext, etc)\n      return this.openBox(folderName, {refetchBoxInfo: true})\n    }\n    return this._withPreparedConnection((imap) => imap.statusAsync(folderName))\n  }\n\n  async getBoxes() {\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::getBoxes`)\n    }\n    return this._withPreparedConnection((imap) => imap.getBoxesAsync())\n  }\n\n  async addBox(folderName) {\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::addBox`)\n    }\n    return this._withPreparedConnection((imap) => imap.addBoxAsync(folderName))\n  }\n\n  async renameBox(oldFolderName, newFolderName) {\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::renameBox`)\n    }\n    return this._withPreparedConnection((imap) => imap.renameBoxAsync(oldFolderName, newFolderName))\n  }\n\n  async delBox(folderName) {\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::delBox`)\n    }\n    return this._withPreparedConnection((imap) => imap.delBoxAsync(folderName))\n  }\n\n  async runOperation(operation, ctx) {\n    if (!this._imap) {\n      throw new IMAPConnectionNotReadyError(`IMAPConnection::runOperation`)\n    }\n    return new Promise((resolve, reject) => {\n      this._queue.push({operation, ctx, resolve, reject});\n      if (this._imap.state === 'authenticated' && !this._currentOperation) {\n        this._processNextOperation();\n      }\n    });\n  }\n\n  _processNextOperation() {\n    if (this._currentOperation) {\n      return;\n    }\n    this._currentOperation = this._queue.shift();\n    if (!this._currentOperation) {\n      this.emit('queue-empty');\n      return;\n    }\n\n    const {operation, ctx, resolve, reject} = this._currentOperation;\n    const resultPromise = operation.run(this._db, this, ctx);\n    if (resultPromise.constructor.name !== \"Promise\") {\n      reject(new Error(`Expected ${operation.constructor.name} to return promise.`))\n    }\n\n    resultPromise.then((maybeResult) => {\n      this._currentOperation = null;\n      // this._logger.info({\n      //   operation_type: operation.constructor.name,\n      //   operation_description: operation.description(),\n      // }, `Finished sync operation`)\n      resolve(maybeResult);\n      this._processNextOperation();\n    })\n    .catch((err) => {\n      this._currentOperation = null;\n      this._logger.error({\n        err: err,\n        operation_type: operation.constructor.name,\n        operation_description: operation.description(),\n      }, `IMAPConnection - operation errored`)\n      reject(err);\n    })\n  }\n}\n\nIMAPConnection.Capabilities = Capabilities;\n"
  },
  {
    "path": "packages/isomorphic-core/src/imap-errors.es6",
    "content": "import {NylasError, RetryableError} from './errors'\n\nexport class IMAPRetryableError extends RetryableError {\n  constructor(msg) {\n    super(msg)\n    this.userMessage = \"We were unable to reach your IMAP provider. Please try again.\";\n    this.statusCode = 408;\n  }\n}\n\n/**\n * IMAPErrors that originate from NodeIMAP. See `convertImapError` for\n * documentation on underlying causes\n */\nexport class IMAPSocketError extends IMAPRetryableError { }\nexport class IMAPConnectionTimeoutError extends IMAPRetryableError { }\nexport class IMAPAuthenticationTimeoutError extends IMAPRetryableError { }\nexport class IMAPTransientAuthenticationError extends IMAPRetryableError { }\n\nexport class IMAPProtocolError extends NylasError {\n  constructor(msg) {\n    super(msg)\n    this.userMessage = \"IMAP protocol error. Try to first remove your account from Nylas Mail, then re-add it and try again.\"\n    this.statusCode = 401\n  }\n}\nexport class IMAPAuthenticationError extends NylasError {\n  constructor(msg) {\n    super(msg)\n    this.userMessage = \"Incorrect IMAP username or password.\";\n    this.statusCode = 401;\n  }\n}\n\nexport class IMAPConnectionNotReadyError extends IMAPRetryableError {\n  constructor(funcName) {\n    super(`${funcName} - You must call connect() first.`);\n  }\n}\n\nexport class IMAPConnectionEndedError extends IMAPRetryableError {\n  constructor(msg = \"The IMAP Connection was ended.\") {\n    super(msg);\n  }\n}\n\n/**\n * Certificate validation failures may correct themselves over long spans\n * of time, but not over the short spans of time in which it'd make sense\n * for us to retry.\n */\nexport class IMAPCertificateError extends NylasError {\n  constructor(msg, host) {\n    super(msg)\n    const hostStr = host ? ` \"${host}\"` : ''\n    this.userMessage = `Certificate Error: We couldn't verify the identity of the IMAP server${hostStr}.`\n    this.statusCode = 495\n  }\n}\n\n/**\n * IMAPErrors may come from:\n *\n * 1. Underlying IMAP provider (Fastmail, Yahoo, etc)\n * 2. Node IMAP\n * 3. K2 code\n *\n * NodeIMAP puts a `source` attribute on `Error` objects to indicate where\n * a particular error came from. See https://github.com/mscdex/node-imap/blob/master/lib/Connection.js\n *\n * These may have the following values:\n *\n *   - \"socket-timeout\": Created by NodeIMAP when `config.socketTimeout`\n *     expires on the base Node `net.Socket` and socket.on('timeout') fires\n *     Message: 'Socket timed out while talking to server'\n *\n *   - \"timeout\": Created by NodeIMAP when `config.connTimeout` has been\n *     reached when trying to connect the socket.\n *     Message: 'Timed out while connecting to server'\n *\n *   - \"socket\": Created by Node's `net.Socket` on error. See:\n *     https://nodejs.org/api/net.html#net_event_error_1\n *     Message: Various from `net.Socket`\n *\n *   - \"protocol\": Created by NodeIMAP when `bad` or `no` types come back\n *     from the IMAP protocol.\n *     Message: Various from underlying IMAP protocol\n *\n *   - \"authentication\": Created by underlying IMAP connection or NodeIMAP\n *     in a few scenarios.\n *     Message: Various from underlying IMAP connection\n *              OR: No supported authentication method(s) available. Unable to login.\n *              OR: Logging in is disabled on this server\n *\n *   - \"timeout-auth\": Created by NodeIMAP when `config.authTimeout` has\n *     been reached when trying to authenticate\n *     Message: 'Timed out while authenticating with server'\n *\n */\nexport function convertImapError(imapError, {connectionSettings = {}} = {}) {\n  let error = imapError;\n\n  if (/try again/i.test(imapError.message)) {\n    error = new RetryableError(imapError)\n    error.source = imapError.source\n    return error\n  }\n  if (/system error/i.test(imapError.message)) {\n    // System Errors encountered in the wild so far have been retryable.\n    error = new RetryableError(imapError)\n    error.source = imapError.source\n    return error\n  }\n  if (/user is authenticated but not connected/i.test(imapError.message)) {\n    // We need to treat this type of error as retryable\n    // See https://github.com/mscdex/node-imap/issues/523 for more details\n    error = new RetryableError(imapError)\n    error.source = imapError.source\n    return error\n  }\n  if (/server unavailable/i.test(imapError.message)) {\n    // Server Unavailable encountered in the wild so far have been retryable.\n    error = new RetryableError(imapError)\n    error.source = imapError.source\n    return error\n  }\n\n  const isCertificateError = (\n    imapError.code === \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\" ||\n    imapError.code === \"SELF_SIGNED_CERT_IN_CHAIN\" ||\n    /certificate/i.test(imapError.message)\n  )\n  if (isCertificateError) {\n    error = new IMAPCertificateError(imapError, connectionSettings.host);\n    error.source = imapError.source\n    return error\n  }\n\n  switch (imapError.source) {\n    case \"socket-timeout\":\n      error = new IMAPConnectionTimeoutError(imapError); break;\n    case \"timeout\":\n      error = new IMAPConnectionTimeoutError(imapError); break;\n    case \"socket\":\n      error = new IMAPSocketError(imapError); break;\n    case \"protocol\":\n      error = new IMAPProtocolError(imapError); break;\n    case \"authentication\":\n      error = new IMAPAuthenticationError(imapError); break;\n    case \"timeout-auth\":\n      error = new IMAPAuthenticationTimeoutError(imapError); break;\n    default:\n      break;\n  }\n  error.source = imapError.source\n  return error\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/load-models.js",
    "content": "const fs = require('fs');\nconst path = require('path');\n\n\nfunction loadModels(Sequelize, sequelize, {loadShared = true, modelDirs = [], schema} = {}) {\n  if (loadShared) {\n    modelDirs.unshift(path.join(__dirname, 'models'))\n  }\n\n  const db = {};\n\n  for (const modelsDir of modelDirs) {\n    for (const filename of fs.readdirSync(modelsDir)) {\n      if (filename.endsWith('.js') || filename.endsWith('.es6')) {\n        let model = sequelize.import(path.join(modelsDir, filename));\n        if (schema) {\n          model = model.schema(schema);\n        }\n        db[model.name[0].toUpperCase() + model.name.substr(1)] = model;\n      }\n    }\n  }\n\n  Object.keys(db).forEach((modelName) => {\n    if (\"associate\" in db[modelName]) {\n      db[modelName].associate(db);\n    }\n  });\n\n  return db;\n}\n\nmodule.exports = loadModels\n"
  },
  {
    "path": "packages/isomorphic-core/src/message-body-utils.es6",
    "content": "const fs = require('fs')\nconst mkdirp = require('mkdirp')\nconst path = require('path')\nconst zlib = require('zlib')\n\nconst MAX_PATH_DIRS = 5;\nconst FILE_EXTENSION = 'nylasmail'\n\nfunction baseMessagePath() {\n  return path.join(process.env.NYLAS_HOME, 'messages');\n}\n\nexport function tryReadBody(val) {\n  try {\n    const parsed = JSON.parse(val);\n    if (parsed && parsed.path && parsed.path.startsWith(baseMessagePath())) {\n      if (parsed.compressed) {\n        return zlib.gunzipSync(fs.readFileSync(parsed.path)).toString();\n      }\n      return fs.readFileSync(parsed.path, {encoding: 'utf8'});\n    }\n  } catch (err) {\n    // ignore. this is valid. JSON parse should fail if the body isn't\n    // metadata about the flat zipped file, but rather a plain text\n  }\n  return null;\n}\n\nexport function pathForBodyFile(msgId) {\n  const pathGroups = [];\n  let remainingId = msgId;\n  while (pathGroups.length < MAX_PATH_DIRS) {\n    pathGroups.push(remainingId.substring(0, 2));\n    remainingId = remainingId.substring(2);\n  }\n  const bodyPath = path.join(...pathGroups);\n  return path.join(baseMessagePath(), bodyPath, `${remainingId}.${FILE_EXTENSION}`);\n}\n\n// NB: The return value of this function is what gets written into the database.\nexport function writeBody({msgId, body} = {}) {\n  const bodyPath = pathForBodyFile(msgId);\n  const bodyDir = path.dirname(bodyPath);\n\n  const compressedBody = zlib.gzipSync(body);\n  const dbEntry = {\n    path: bodyPath,\n    compressed: true,\n  };\n\n  // It's possible that gzipping actually makes the body larger. If that's the\n  // case then just write the uncompressed body instead.\n  let bodyToWrite = compressedBody;\n  if (compressedBody.length >= body.length) {\n    dbEntry.compressed = false;\n    bodyToWrite = body;\n  }\n\n  const result = JSON.stringify(dbEntry);\n  // If the JSON db entry would be longer than the body itself then just write\n  // the body directly into the database.\n  if (result.length > body.length) {\n    return body;\n  }\n\n  try {\n    if (!fs.existsSync(bodyPath)) {\n      mkdirp.sync(bodyDir);\n    }\n\n    fs.writeFileSync(bodyPath, bodyToWrite);\n    return result;\n  } catch (err) {\n    // If anything bad happens while trying to write to disk just store the\n    // body in the database.\n    return body;\n  }\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/message-utils.es6",
    "content": "/* eslint no-useless-escape: 0 */\nimport mailcomposer from 'mailcomposer'\nconst mimelib = require('mimelib');\nconst encoding = require('encoding');\nconst he = require('he');\nconst os = require('os');\nconst fs = require('fs');\nconst path = require('path');\nconst mkdirp = require('mkdirp');\nconst {APIError} = require('./errors');\nconst {deepClone} = require('./model-utils').default;\nconst TrackingUtils = require('./tracking-utils').default\n\n// Aiming for the former in length, but the latter is the hard db cutoff\nconst SNIPPET_SIZE = 100;\nconst SNIPPET_MAX_SIZE = 255;\n\n// Format of input: ['a@example.com, B <b@example.com>', 'c@example.com'],\n// where each element of the array is the unparsed contents of a single\n// element of the same header field. (It's totally valid to have multiple\n// From/To/etc. headers on the same email.)\nfunction parseContacts(input) {\n  if (!input || input.length === 0 || !input[0]) {\n    return [];\n  }\n  let contacts = [];\n  for (const headerLine of input) {\n    const values = mimelib.parseAddresses(headerLine);\n    if (!values || values.length === 0) {\n      continue;\n    }\n    contacts = contacts.concat(values.map(v => {\n      if (!v || v.length === 0) {\n        return null\n      }\n      const {name, address: email} = v;\n      return {name, email};\n    })\n    .filter(c => c != null))\n  }\n  return contacts;\n}\n\nfunction parseSnippet(body) {\n  const doc = new DOMParser().parseFromString(body, 'text/html')\n  const skipTags = new Set(['TITLE', 'SCRIPT', 'STYLE', 'IMG']);\n  const noSpaceTags = new Set(['B', 'I', 'STRONG', 'EM', 'SPAN']);\n\n  const treeWalker = document.createTreeWalker(doc, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT, (node) => {\n    if (skipTags.has(node.tagName)) {\n      // skip this node and all its children\n      return NodeFilter.FILTER_REJECT;\n    }\n    if (node.nodeType === Node.TEXT_NODE) {\n      const nodeValue = node.nodeValue ? node.nodeValue.trim() : null;\n      if (nodeValue) {\n        return NodeFilter.FILTER_ACCEPT;\n      }\n      return NodeFilter.FILTER_SKIP;\n    }\n    return NodeFilter.FILTER_ACCEPT;\n  });\n\n  let extractedText = \"\";\n  let lastNodeTag = \"\";\n  while (treeWalker.nextNode()) {\n    if (treeWalker.currentNode.nodeType === Node.ELEMENT_NODE) {\n      lastNodeTag = treeWalker.currentNode.nodeName;\n    } else {\n      if (extractedText && !noSpaceTags.has(lastNodeTag)) {\n        extractedText += \" \";\n      }\n      extractedText += treeWalker.currentNode.nodeValue;\n      if (extractedText.length > SNIPPET_MAX_SIZE) {\n        break;\n      }\n    }\n  }\n  const snippetText = extractedText.trim();\n\n  // clean up and trim snippet\n  let trimmed = snippetText.replace(/[\\n\\r]/g, ' ').replace(/\\s\\s+/g, ' ').substr(0, SNIPPET_MAX_SIZE);\n  if (trimmed) {\n    // TODO: strip quoted text from snippets also\n    // trim down to approx. SNIPPET_SIZE w/out cutting off words right in the\n    // middle (if possible)\n    const wordBreak = trimmed.indexOf(' ', SNIPPET_SIZE);\n    if (wordBreak !== -1) {\n      trimmed = trimmed.substr(0, wordBreak);\n    }\n  }\n  return trimmed;\n}\n\n// In goes arrays of text, out comes arrays of RFC2822 Message-Ids. Luckily,\n// these days most all text in In-Reply-To, Message-Id, and References headers\n// actually conforms to the spec.\nfunction parseReferences(input) {\n  if (!input || !input.length || !input[0]) {\n    return [];\n  }\n  const references = new Set();\n  for (const headerLine of input) {\n    for (const ref of headerLine.split(/[\\s,]+/)) {\n      if (/^<.*>$/.test(ref)) {\n        references.add(ref);\n      }\n    }\n  }\n  return Array.from(references);\n}\n\nfunction htmlifyPlaintext(text) {\n  const escapedText = he.escape(text);\n  return `<pre class=\"nylas-plaintext\">${escapedText}</pre>`;\n}\n\nfunction getReplyHeaders(messageReplyingTo) {\n  let inReplyTo;\n  let references;\n  if (messageReplyingTo.headerMessageId) {\n    inReplyTo = messageReplyingTo.headerMessageId;\n    if (messageReplyingTo.references) {\n      const refById = {};\n      for (const ref of messageReplyingTo.references) {\n        refById[ref.id] = ref;\n      }\n      references = [];\n      for (const referenceId of messageReplyingTo.referencesOrder) {\n        references.push(refById[referenceId].rfc2822MessageId);\n      }\n      if (!references.includes(messageReplyingTo.headerMessageId)) {\n        references.push(messageReplyingTo.headerMessageId);\n      }\n    } else {\n      references = [messageReplyingTo.headerMessageId];\n    }\n  }\n  return {inReplyTo, references}\n}\n\nfunction bodyFromParts(imapMessage, desiredParts) {\n  let body = '';\n  for (const {id, mimeType, transferEncoding, charset} of desiredParts) {\n    let decoded = '';\n    // see https://www.w3.org/Protocols/rfc1341/5_Content-Transfer-Encoding.html\n    if ((/quot(ed)?[-/]print(ed|able)?/gi).test(transferEncoding)) {\n      decoded = mimelib.decodeQuotedPrintable(imapMessage.parts[id], charset);\n    } else if ((/base64/gi).test(transferEncoding)) {\n      decoded = mimelib.decodeBase64(imapMessage.parts[id], charset);\n    } else {\n      // Treat this as having no encoding and decode based only on the charset\n      //\n      // According to https://tools.ietf.org/html/rfc2045#section-5.2,\n      // this should default to ascii; however, if we don't get a charset,\n      // it's possible clients (like nodemailer) encoded the data as utf-8\n      // anyway. Since ascii is a strict subset of utf-8, it's safer to\n      // try and decode as utf-8 if we don't have the charset.\n      //\n      // (This applies to decoding quoted-printable and base64 as well. The\n      // mimelib library, if charset is null, will default to utf-8)\n      //\n      decoded = encoding.convert(imapMessage.parts[id], 'utf-8', charset).toString('utf-8');\n    }\n    // desiredParts are in order of the MIME tree walk, e.g. 1.1, 1.2, 2...,\n    // and for multipart/alternative arrays, we have already pulled out the\n    // highest fidelity part (generally HTML).\n    //\n    // Therefore, the correct way to display multiple parts is to simply\n    // concatenate later ones with the body of the previous MIME parts.\n    //\n    // This may seem kind of weird, but some MUAs _do_ send out whack stuff\n    // like an HTML body followed by a plaintext footer.\n    if (mimeType === 'text/plain') {\n      body += htmlifyPlaintext(decoded);\n    } else {\n      body += decoded;\n    }\n  }\n  // sometimes decoding results in a NUL-terminated body string, which makes\n  // SQLite blow up with an 'unrecognized token' error\n  body = body.replace(/\\0/g, '');\n\n  return body;\n}\n\n// Since we only fetch the MIME structure and specific desired MIME parts from\n// IMAP, we unfortunately can't use an existing library like mailparser to parse\n// the message, and have to do fun stuff like deal with character sets and\n// content-transfer-encodings ourselves.\nasync function parseFromImap(imapMessage, desiredParts, {db, accountId, folder}) {\n  const {Message, Label} = db;\n  const {attributes} = imapMessage;\n\n  // this key name can change depending on which subset of headers we're downloading,\n  // so to prevent having to update this code every time we change the set,\n  // dynamically look up the key instead\n  const headerKey = Object.keys(imapMessage.parts).filter(k => k.startsWith('HEADER'))[0]\n  const headers = imapMessage.parts[headerKey].toString('ascii')\n  const parsedHeaders = mimelib.parseHeaders(headers);\n  for (const key of ['x-gm-thrid', 'x-gm-msgid', 'x-gm-labels']) {\n    parsedHeaders[key] = attributes[key];\n  }\n\n  const parsedMessage = {\n    to: parseContacts(parsedHeaders.to),\n    cc: parseContacts(parsedHeaders.cc),\n    bcc: parseContacts(parsedHeaders.bcc),\n    from: parseContacts(parsedHeaders.from),\n    replyTo: parseContacts(parsedHeaders['reply-to']),\n    accountId: accountId,\n    body: bodyFromParts(imapMessage, desiredParts),\n    snippet: null,\n    unread: !attributes.flags.includes('\\\\Seen'),\n    starred: attributes.flags.includes('\\\\Flagged'),\n    // We limit drafts to the drafts and all mail folders because some clients\n    // may send messages and improperly leave the draft flag set, and also\n    // because we want to exclude drafts moved to the trash from the drafts view\n    // see https://github.com/nylas/cloud-core/commit/1433921a166ddcba7c269158d65febb7928767d8\n    // & associated phabricator bug https://phab.nylas.com/T5696\n    isDraft: (\n      ['drafts', 'all'].includes(folder.role) &&\n      (\n        attributes.flags.includes('\\\\Draft') ||\n        (parsedHeaders['x-gm-labels'] || []).includes('\\\\Draft')\n      )\n    ),\n    // We prefer the date from the message headers because the date is one of\n    // the fields we use for generating unique message IDs, and the server\n    // INTERNALDATE, `attributes.date`, may differ across accounts for the same\n    // message. If the Date header is not included in the message, we fall\n    // back to the INTERNALDATE and it's possible we'll generate different IDs\n    // for the same message delivered to different accounts (which is better\n    // than having message ID collisions for different messages, which could\n    // happen if we did not include the date).\n    date: parsedHeaders.date ? parsedHeaders.date[0] : imapMessage.attributes.date,\n    folderImapUID: attributes.uid,\n    folderId: folder.id,\n    folder: null,\n    labels: [],\n    headerMessageId: parseReferences(parsedHeaders['message-id'])[0],\n    // References are not saved on the message model itself, but are later\n    // converted to associated Reference objects so we can index them. Since we\n    // don't do tree threading, we don't need to care about In-Reply-To\n    // separately, and can simply associate them all in the same way.\n    // Generally, References already contains the Message-IDs in In-Reply-To,\n    // but we concat and dedupe just in case.\n    references: parseReferences(\n      (parsedHeaders.references || []).concat(\n        (parsedHeaders['in-reply-to'] || []), (parsedHeaders['message-id'] || [])\n      )\n    ),\n    gMsgId: parsedHeaders['x-gm-msgid'],\n    gThrId: parsedHeaders['x-gm-thrid'],\n    subject: parsedHeaders.subject ? parsedHeaders.subject[0] : '(no subject)',\n  }\n\n\n  /**\n   * mimelib will return a string date with the leading zero of single\n   * digit dates truncated. e.g. February 1 instead of February 01. When\n   * we set Message Date headers, we use javascript's `toUTCString` which\n   * zero pads digit dates. To make the hashes line up, we need to ensure\n   * that the date string used in the ID generation is also zero-padded.\n   */\n  const messageForHashing = deepClone(parsedMessage)\n  messageForHashing.date = Message.dateString(parsedMessage.date);\n  // Inversely to `buildForSend`, we leave the date header as it is so that the\n  // format is consistent for the generative IDs, then convert it to a Date object\n  parsedMessage.id = Message.hash(messageForHashing)\n  parsedMessage.date = new Date(Date.parse(parsedMessage.date));\n\n  parsedMessage.snippet = parseSnippet(parsedMessage.body);\n\n  parsedMessage.folder = folder;\n\n  const xGmLabels = parsedHeaders['x-gm-labels']\n  if (xGmLabels) {\n    parsedMessage.folderImapXGMLabels = JSON.stringify(xGmLabels)\n    parsedMessage.labels = await Label.findXGMLabels(xGmLabels)\n  }\n\n  if (process.env.NYLAS_DEBUG) {\n    const outJSON = JSON.stringify({imapMessage, desiredParts, result: parsedMessage});\n    const outDir = path.join(os.tmpdir(), \"k2-parse-output\", folder.name)\n    const outFile = path.join(outDir, imapMessage.attributes.uid.toString());\n    mkdirp.sync(outDir);\n    fs.writeFileSync(outFile, outJSON);\n  }\n\n  return parsedMessage;\n}\n\n\nasync function buildForSend(db, json) {\n  const {Thread, Message, Reference} = db\n  let replyToThread;\n  let replyToMessage;\n\n  if (json.thread_id != null) {\n    replyToThread = await Thread.find({\n      where: {id: json.thread_id},\n      include: [{\n        model: Message,\n        as: 'messages',\n        attributes: ['id'],\n      }],\n    });\n  }\n\n  if (json.reply_to_message_id != null) {\n    replyToMessage = await Message.findById(\n      json.reply_to_message_id,\n      { include: [{model: Reference, as: 'references', attributes: ['id', 'rfc2822MessageId']}] }\n    )\n  }\n\n  if (replyToThread && replyToMessage) {\n    if (!replyToThread.messages.find((msg) => msg.id === replyToMessage.id)) {\n      throw new APIError(`Message ${replyToMessage.id} is not in thread ${replyToThread.id}`, 400)\n    }\n  }\n\n  let thread;\n  let replyHeaders = {};\n  let inReplyToLocalMessageId;\n  if (replyToMessage) {\n    inReplyToLocalMessageId = replyToMessage.id;\n    replyHeaders = getReplyHeaders(replyToMessage);\n    thread = await replyToMessage.getThread();\n  } else if (replyToThread) {\n    thread = replyToThread;\n    const previousMessages = thread.messages.filter(msg => !msg.isDraft);\n    if (previousMessages.length > 0) {\n      const lastMessage = previousMessages[previousMessages.length - 1]\n      inReplyToLocalMessageId = lastMessage.id;\n      replyHeaders = getReplyHeaders(lastMessage);\n    }\n  }\n\n  const {inReplyTo, references} = replyHeaders\n  const date = new Date()\n  const message = {\n    accountId: json.account_id,\n    threadId: thread ? thread.id : null,\n    headerMessageId: Message.buildHeaderMessageId(json.client_id),\n    from: json.from,\n    to: json.to,\n    cc: json.cc,\n    bcc: json.bcc,\n    replyTo: json.reply_to,\n    subject: json.subject,\n    body: json.body,\n    unread: false,\n    isDraft: json.draft,\n    isSent: false,\n    version: 0,\n    date: date,\n    inReplyToLocalMessageId: inReplyToLocalMessageId,\n    uploads: json.uploads,\n  }\n  // We have to clone the message and change the date for hashing because the\n  // date we get later when we parse from IMAP is a different format, per the\n  // nodemailer buildmail function that gives us the raw message and replaces\n  // the date header with this modified UTC string\n  // https://github.com/nodemailer/buildmail/blob/master/lib/buildmail.js#L470\n  const messageForHashing = deepClone(message)\n  messageForHashing.date = Message.dateString(date)\n  message.id = Message.hash(messageForHashing);\n  message.body = TrackingUtils.prepareTrackingLinks(message.id, message.body)\n  const instance = Object.assign(Message.build({id: message.id}), message);\n\n  // TODO we set these temporary properties which aren't stored in the database\n  // model because SendmailClient requires them to send the message with the\n  // correct headers.\n  // This should be cleaned up\n  instance.inReplyTo = inReplyTo;\n  instance.references = references;\n  return instance;\n}\n\nconst formatParticipants = (participants) => {\n  // Something weird happens with the mime building when the participant name\n  // has an @ symbol in it (e.g. a name and email of hello@gmail.com turns into\n  // 'hello@ <gmail.com hello@gmail.com>'), so replace it with whitespace.\n  return participants.map(p => `${p.name.replace('@', ' ')} <${p.email}>`).join(',');\n}\n\n// Transforms the message into a json object with the properties formatted in\n// the way mailer libraries (e.g. nodemailer, mailcomposer) expect.\nfunction getMailerPayload(message) {\n  const msgData = {};\n  for (const field of ['from', 'to', 'cc', 'bcc']) {\n    if (message[field]) {\n      msgData[field] = formatParticipants(message[field])\n    }\n  }\n  msgData.date = message.date;\n  msgData.subject = message.subject;\n  msgData.html = message.body;\n  msgData.messageId = message.headerMessageId || message.message_id_header;\n\n  msgData.attachments = []\n  const uploads = message.uploads || []\n  for (const upload of uploads) {\n    msgData.attachments.push({\n      filename: mimelib.encodeMimeWord(upload.filename),\n      content: fs.createReadStream(upload.targetPath),\n      cid: upload.inline ? upload.id : null,\n    })\n  }\n\n  if (message.replyTo) {\n    msgData.replyTo = formatParticipants(message.replyTo);\n  }\n\n  msgData.inReplyTo = message.inReplyTo;\n  msgData.references = message.references;\n  // message.headers is usually unset, but in the case that we do add\n  // headers elsewhere, we don't want to override them here\n  msgData.headers = message.headers || {};\n  msgData.headers['User-Agent'] = `NylasMailer-K2`\n\n  return msgData;\n}\n\nasync function buildMime(message, {includeBcc = false} = {}) {\n  const payload = getMailerPayload(message)\n  const builder = mailcomposer(payload)\n  const mimeNode = await (new Promise((resolve, reject) => {\n    builder.build((error, result) => (\n      error ? reject(error) : resolve(result)\n    ))\n  }));\n  if (!includeBcc || !message.bcc || message.bcc.length === 0) {\n    return mimeNode.toString('ascii')\n  }\n  return `Bcc: ${formatParticipants(message.bcc)}\\n${mimeNode.toString('ascii')}`\n}\n\nmodule.exports = {\n  buildForSend,\n  getReplyHeaders,\n  parseFromImap,\n  parseSnippet,\n  parseContacts,\n  getMailerPayload,\n  buildMime,\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/metrics-reporter.es6",
    "content": "/**\n * NOTE: Metrics collection is disabled. Implement these methods to send\n * sync performance data to a service like Honeycomb.\n */\nclass MetricsReporter {\n\n  async collectCPUUsage() {\n    return Promise.resolve();\n  }\n\n  async reportEvent(data) { //eslint-disable-line\n    // noop\n  }\n}\n\nexport default new MetricsReporter();\n"
  },
  {
    "path": "packages/isomorphic-core/src/migrations/20160617002207-create-user.js",
    "content": "/* eslint no-unused-vars: 0 */\n\nmodule.exports = {\n  up: function up(queryInterface, Sequelize) {\n    return queryInterface.createTable('Users', {\n      id: {\n        allowNull: false,\n        autoIncrement: true,\n        primaryKey: true,\n        type: Sequelize.INTEGER,\n      },\n      first_name: {\n        type: Sequelize.STRING,\n      },\n      last_name: {\n        type: Sequelize.STRING,\n      },\n      bio: {\n        type: Sequelize.TEXT,\n      },\n      createdAt: {\n        allowNull: false,\n        type: Sequelize.DATE,\n      },\n      updatedAt: {\n        allowNull: false,\n        type: Sequelize.DATE,\n      },\n    });\n  },\n  down: function down(queryInterface, Sequelize) {\n    return queryInterface.dropTable('Users');\n  },\n};\n"
  },
  {
    "path": "packages/isomorphic-core/src/model-utils.es6",
    "content": "import _ from 'underscore'\n\nfunction deepClone(object, customizer, stackSeen = [], stackRefs = []) {\n  let newObject;\n  if (!_.isObject(object)) { return object; }\n  if (_.isFunction(object)) { return object; }\n\n  if (_.isArray(object)) {\n    // http://perfectionkills.com/how-ecmascript-5-still-does-not-allow-to-subclass-an-array/\n    newObject = [];\n  } else if (object instanceof Date) {\n    // You can't clone dates by iterating through `getOwnPropertyNames`\n    // of the Date object. We need to special-case Dates.\n    newObject = new Date(object);\n  } else {\n    newObject = Object.create(Object.getPrototypeOf(object));\n  }\n\n  // Circular reference check\n  const seenIndex = stackSeen.indexOf(object);\n  if (seenIndex >= 0) { return stackRefs[seenIndex]; }\n  stackSeen.push(object); stackRefs.push(newObject);\n\n  // It's important to use getOwnPropertyNames instead of Object.keys to\n  // get the non-enumerable items as well.\n  for (const key of Array.from(Object.getOwnPropertyNames(object))) {\n    const newVal = deepClone(object[key], customizer, stackSeen, stackRefs);\n    if (_.isFunction(customizer)) {\n      newObject[key] = customizer(key, newVal);\n    } else {\n      newObject[key] = newVal;\n    }\n  }\n  return newObject;\n}\n\nfunction copyModelValues(model, updates = {}) {\n  const fields = Object.keys(model.dataValues)\n  const data = {}\n  for (const field of fields) {\n  // We can't just copy over the values directly from `dataValues` because\n  // they are the raw values, and we would ignore custom getters.\n  // Rather, we access them from the model instance.\n  // For example our JSON database type, is simply a string and the custom\n  // getter parses it into json. We want to get the parsed json, not the\n  // string\n    data[field] = model[field]\n  }\n  return Object.assign({}, data, updates)\n}\n\nfunction isValidId(value) {\n  if (value == null) { return false; }\n  if (isNaN(parseInt(value, 36))) {\n    return false\n  }\n  return true\n}\n\nexport default {\n  deepClone,\n  isValidId,\n  copyModelValues,\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/models/account-token.js",
    "content": "module.exports = (sequelize, Sequelize) => {\n  const AccountToken = sequelize.define('accountToken', {\n    value: {\n      type: Sequelize.UUID,\n      defaultValue: Sequelize.UUIDV4,\n    },\n  }, {\n    indexes: [\n      { fields: ['value'] },\n    ],\n    classMethods: {\n      associate: ({Account}) => {\n        AccountToken.belongsTo(Account, {\n          onDelete: \"CASCADE\",\n          foreignKey: {\n            allowNull: false,\n          },\n        });\n      },\n    },\n  });\n\n  return AccountToken;\n};\n"
  },
  {
    "path": "packages/isomorphic-core/src/models/account.js",
    "content": "const crypto = require('crypto');\n\nconst {JSONColumn, JSONArrayColumn} = require('../database-types');\nconst {credentialsForProvider, smtpConfigFromSettings} = require('../auth-helpers');\nconst {MAX_INDEXABLE_LENGTH} = require('../db-utils');\n\n\nconst {DB_ENCRYPTION_ALGORITHM, DB_ENCRYPTION_PASSWORD} = process.env;\n\nmodule.exports = (sequelize, Sequelize) => {\n  const Account = sequelize.define('account', {\n    id: { type: Sequelize.STRING(65), primaryKey: true },\n    name: Sequelize.STRING,\n    provider: Sequelize.STRING,\n    emailAddress: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    connectionSettings: JSONColumn('connectionSettings'),\n    connectionCredentials: Sequelize.TEXT,\n    syncPolicy: JSONColumn('syncPolicy'),\n    syncError: JSONColumn('syncError'),\n    firstSyncCompletion: {\n      type: Sequelize.STRING(14),\n      allowNull: true,\n      defaultValue: null,\n    },\n    lastSyncCompletions: JSONArrayColumn('lastSyncCompletions'),\n  }, {\n    indexes: [\n      {\n        unique: true,\n        fields: ['id'],\n      },\n    ],\n    classMethods: {\n      associate(data = {}) {\n        Account.hasMany(data.AccountToken, {as: 'tokens', onDelete: 'cascade', hooks: true})\n      },\n      hash({emailAddress, connectionSettings} = {}) {\n        const idString = `${emailAddress}${JSON.stringify(connectionSettings)}`;\n        return crypto.createHash('sha256').update(idString, 'utf8').digest('hex')\n      },\n      upsertWithCredentials(accountParams, credentials) {\n        if (!accountParams || !credentials || !accountParams.emailAddress || !accountParams.connectionSettings) {\n          throw new Error(\"Need to pass accountParams and credentials to upsertWithCredentials\")\n        }\n        const id = Account.hash(accountParams)\n        return Account.findById(id).then((existing) => {\n          const account = existing || Account.build(Object.assign({id}, accountParams))\n\n          // always update with the latest credentials\n          account.setCredentials(credentials);\n\n          return account.save().then((saved) => {\n            return sequelize.models.accountToken.create({accountId: saved.id}).then((token) => {\n              return Promise.resolve({account: saved, token: token})\n            })\n          });\n        });\n      },\n    },\n    instanceMethods: {\n      toJSON() {\n        return {\n          id: this.id,\n          name: this.name,\n          object: 'account',\n          organization_unit: (this.provider === 'gmail') ? 'label' : 'folder',\n          provider: this.provider,\n          email_address: this.emailAddress,\n          connection_settings: this.connectionSettings,\n          sync_policy: this.syncPolicy,\n          sync_error: this.syncError,\n          first_sync_completion: this.firstSyncCompletion / 1,\n          last_sync_completions: this.lastSyncCompletions,\n          created_at: this.createdAt,\n        }\n      },\n\n      errored() {\n        return this.syncError != null;\n      },\n\n      setCredentials(json) {\n        if (!(json instanceof Object)) {\n          throw new Error(\"Call setCredentials with JSON!\")\n        }\n\n        if (DB_ENCRYPTION_ALGORITHM && DB_ENCRYPTION_PASSWORD) {\n          const cipher = crypto.createCipher(DB_ENCRYPTION_ALGORITHM, DB_ENCRYPTION_PASSWORD)\n          let crypted = cipher.update(JSON.stringify(json), 'utf8', 'hex')\n          crypted += cipher.final('hex');\n          this.connectionCredentials = crypted;\n        } else {\n          this.connectionCredentials = JSON.stringify(json);\n        }\n      },\n\n      decryptedCredentials() {\n        let dec = null;\n        if (DB_ENCRYPTION_ALGORITHM && DB_ENCRYPTION_PASSWORD) {\n          const decipher = crypto.createDecipher(DB_ENCRYPTION_ALGORITHM, DB_ENCRYPTION_PASSWORD)\n          dec = decipher.update(this.connectionCredentials, 'hex', 'utf8')\n          dec += decipher.final('utf8');\n        } else {\n          dec = this.connectionCredentials;\n        }\n\n        try {\n          return JSON.parse(dec);\n        } catch (err) {\n          return null;\n        }\n      },\n\n      smtpConfig() {\n        // We always call credentialsForProvider() here because n1Cloud\n        // sometimes needs to send emails for accounts which did not have their\n        // full SMTP settings saved to the database.\n        const {connectionSettings, connectionCredentials} = credentialsForProvider({\n          provider: this.provider,\n          settings: Object.assign({}, this.decryptedCredentials(), this.connectionSettings),\n          email: this.emailAddress,\n        });\n        return smtpConfigFromSettings(this.provider, connectionSettings, connectionCredentials);\n      },\n    },\n  });\n\n  return Account;\n};\n"
  },
  {
    "path": "packages/isomorphic-core/src/models/transaction.js",
    "content": "const {JSONArrayColumn} = require('../database-types');\nconst {MAX_INDEXABLE_LENGTH} = require('../db-utils');\n\nmodule.exports = (sequelize, Sequelize) => {\n  return sequelize.define('transaction', {\n    event: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    object: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    objectId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    accountId: Sequelize.STRING(MAX_INDEXABLE_LENGTH),\n    changedFields: JSONArrayColumn('changedFields'),\n  }, {\n    indexes: [\n      { fields: ['accountId'] },\n    ],\n    instanceMethods: {\n      toJSON: function toJSON() {\n        return {\n          id: `${this.id}`,\n          event: this.event,\n          object: this.object,\n          objectId: `${this.objectId}`,\n        }\n      },\n    },\n  });\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/promise-utils.js",
    "content": "/* eslint no-restricted-syntax: 0 */\n\nrequire('promise.prototype.finally')\nconst props = require('promise-props');\nconst _ = require('underscore')\n\nglobal.Promise.prototype.thenReturn = function thenReturn(value) {\n  return this.then(function then() { return Promise.resolve(value); })\n}\n\nfunction sleep(ms) {\n  return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\nfunction each(iterable, iterator) {\n  return Promise.resolve(iterable).then((array) => {\n    return new Promise((resolve, reject) => {\n      Array.from(array).reduce((prevPromise, item, idx, len) => (\n        prevPromise.then(() => Promise.resolve(iterator(item, idx, len)))\n      ), Promise.resolve())\n      .then(() => resolve(iterable))\n      .catch((err) => reject(err))\n    })\n  })\n}\n\nfunction promisify(nodeFn) {\n  return function wrapper(...fnArgs) {\n    return new Promise((resolve, reject) => {\n      nodeFn.call(this, ...fnArgs, (err, ...results) => {\n        if (err) {\n          reject(err)\n          return\n        }\n        resolve(...results)\n      });\n    })\n  }\n}\n\nfunction promisifyAll(obj) {\n  for (const key in obj) {\n    if (!key.endsWith('Async') && _.isFunction(obj[key])) {\n      obj[`${key}Async`] = promisify(obj[key])\n    }\n  }\n  return obj\n}\n\nmodule.exports = {\n  each,\n  sleep,\n  promisify,\n  promisifyAll,\n  props: props,\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/sendmail-client.es6",
    "content": "/* eslint no-useless-escape: 0 */\nimport nodemailer from 'nodemailer'\nimport {APIError} from './errors'\nimport {convertSmtpError} from './smtp-errors'\nimport {getMailerPayload, buildMime} from './message-utils'\n\nconst MAX_RETRIES = 1;\n\nclass SendmailClient {\n\n  constructor(account, logger) {\n    this._smtpConfig = account.smtpConfig()\n    this._transporter = nodemailer.createTransport(Object.assign(this._smtpConfig, {pool: true}));\n    this._logger = logger;\n  }\n\n  async _send(msgData) {\n    let error;\n    let results;\n\n    // disable nodemailer's automatic X-Mailer header\n    msgData.xMailer = false;\n    for (let i = 0; i <= MAX_RETRIES; i++) {\n      try {\n        results = await this._transporter.sendMail(msgData);\n      } catch (err) {\n        // Keep retrying for MAX_RETRIES\n        error = convertSmtpError(err, {connectionSettings: this._smtpConfig});\n        this._logger.error(err);\n      }\n      if (!results) {\n        continue;\n      }\n      const {rejected, pending} = results;\n      if ((rejected && rejected.length > 0) || (pending && pending.length > 0)) {\n        // At least one recipient was rejected by the server,\n        // but at least one recipient got it. Don't retry; throw an\n        // error so that we fail to client.\n        throw new APIError('Sending to at least one recipient failed', 402, {results});\n      }\n      return\n    }\n    this._logger.error('Max sending retries reached');\n\n    let userMessage = 'Sending failed';\n    let statusCode = 500;\n    if (error && error.userMessage && error.statusCode) {\n      userMessage = `Sending failed - ${error.userMessage}`;\n      statusCode = error.statusCode;\n    }\n\n    const {host, port, secure} = this._transporter.transporter.options;\n    throw new APIError(userMessage, statusCode, {\n      originalError: error,\n      smtp_host: host,\n      smtp_port: port,\n      smtp_use_ssl: secure,\n    });\n  }\n\n  async send(message) {\n    if (message.isSent) {\n      throw new Error(`Cannot send message ${message.id}, it has already been sent`);\n    }\n    const payload = getMailerPayload(message)\n    await this._send(payload);\n  }\n\n  async sendCustom(customMessage, recipients) {\n    const envelope = {};\n    for (const field of Object.keys(recipients)) {\n      envelope[field] = recipients[field].map(r => r.email);\n    }\n    envelope.from = customMessage.from.map(c => c.email)\n    const raw = await buildMime(customMessage);\n    await this._send({raw, envelope});\n  }\n}\n\nmodule.exports = SendmailClient;\n"
  },
  {
    "path": "packages/isomorphic-core/src/shell-utils.es6",
    "content": "const childProcess = require('child_process')\n\nexport async function spawn(cmd, args, opts = {}) {\n  return new Promise((resolve, reject) => {\n    const env = Object.assign({}, process.env, opts.env || {})\n    delete opts.env\n    const options = Object.assign({env}, opts);\n    const proc = childProcess.spawn(cmd, args, options)\n    let stdout = ''\n    let stderr = ''\n    proc.stdout.on(\"data\", (data) => {\n      stdout += data\n    })\n    proc.stderr.on(\"data\", (data) => {\n      stderr += data\n    })\n    proc.on(\"error\", reject)\n    proc.on(\"exit\", () => resolve({stdout, stderr}))\n  })\n}\n\nexport function exec(cmd, opts = {}) {\n  return new Promise((resolve, reject) => {\n    childProcess.exec(cmd, opts, (err, stdout) => {\n      if (err) {\n        return reject(err)\n      }\n      return resolve(stdout)\n    })\n  })\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/smtp-errors.es6",
    "content": "import {NylasError, RetryableError} from './errors'\n\nexport class SMTPRetryableError extends RetryableError {\n  constructor(msg) {\n    super(msg)\n    this.userMessage = \"We were unable to reach your SMTP server. Please try again.\"\n    this.statusCode = 408\n  }\n}\n\nexport class SMTPConnectionTimeoutError extends SMTPRetryableError { }\nexport class SMTPConnectionEndedError extends SMTPRetryableError { }\nexport class SMTPConnectionTLSError extends SMTPRetryableError { }\n\nexport class SMTPProtocolError extends NylasError {\n  constructor(msg) {\n    super(msg)\n    this.userMessage = \"SMTP protocol error. Please check your SMTP settings.\"\n    this.statusCode = 401\n  }\n}\n\nexport class SMTPConnectionDNSError extends NylasError {\n  constructor(msg) {\n    super(msg)\n    this.userMessage = \"We were unable to look up your SMTP host. Please check the SMTP server name.\"\n    this.statusCode = 401\n  }\n}\n\nexport class SMTPAuthenticationError extends NylasError {\n  constructor(msg) {\n    super(msg)\n    this.userMessage = \"Incorrect SMTP username or password.\"\n    this.statusCode = 401\n  }\n}\n\nexport class SMTPCertificateError extends NylasError {\n  constructor(msg, host) {\n    super(msg)\n    const hostStr = host ? ` \"${host}\"` : ''\n    this.userMessage = `Certificate Error: We couldn't verify the identity of the SMTP server${hostStr}.`\n    this.statusCode = 495\n  }\n}\n\n/* Nodemailer's errors are just regular old Error objects, so we have to\n * test the error message to determine more about what they mean\n */\nexport function convertSmtpError(err, {connectionSettings = {}} = {}) {\n  // TODO: what error is thrown if you're offline?\n  // TODO: what error is thrown if the message you're sending is too large?\n  if (/(?:connection timeout)|(?:connect etimedout)/i.test(err.message)) {\n    return new SMTPConnectionTimeoutError(err)\n  }\n  if (/(?:connection|socket) closed?/i.test(err.message)) {\n    const smtpErr = SMTPConnectionEndedError(err)\n    if (err.code) {\n      // e.g. https://github.com/nodemailer/nodemailer/blob/master/lib/smtp-transport/index.js#L184-L185\n      smtpErr.code = err.code;\n    }\n  }\n\n  const isCertificateError = (\n    err.code === \"UNABLE_TO_VERIFY_LEAF_SIGNATURE\" ||\n    err.code === \"SELF_SIGNED_CERT_IN_CHAIN\" ||\n    /certificate/i.test(err.message)\n  )\n  if (isCertificateError) {\n    return new SMTPCertificateError(err, connectionSettings.host);\n  }\n\n  if (/error initiating tls/i.test(err.message)) {\n    return new SMTPConnectionTLSError(err);\n  }\n  if (/getaddrinfo enotfound/i.test(err.message)) {\n    return new SMTPConnectionDNSError(err);\n  }\n  if (/unknown protocol/i.test(err.message)) {\n    return new SMTPProtocolError(err);\n  }\n  if (/(?:invalid login)|(?:username and password not accepted)|(?:incorrect username or password)|(?:authentication failed)/i.test(err.message)) {\n    return new SMTPAuthenticationError(err);\n  }\n\n  return err;\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/string-utils.es6",
    "content": "export function trimTo(str, size) {\n  const g = window || global || {}\n  const TRIM_SIZE = size || process.env.TRIM_SIZE || g.TRIM_SIZE || 256;\n  let trimed = str;\n  if (str.length >= TRIM_SIZE) {\n    trimed = `${str.slice(0, TRIM_SIZE / 2)}…${str.slice(str.length - TRIM_SIZE / 2, str.length)}`\n  }\n  return trimed\n}\n"
  },
  {
    "path": "packages/isomorphic-core/src/tls-utils.es6",
    "content": "import constants from 'constants';\n\nconst INSECURE_TLS_OPTIONS = {\n  secureProtocol: 'SSLv23_method',\n  rejectUnauthorized: false,\n}\n\nconst SECURE_TLS_OPTIONS = {\n  secureProtocol: 'SSLv23_method',\n  // See similar code in cloud-core for explanation of each flag:\n  // https://github.com/nylas/cloud-core/blob/e70f9e023b880090564b62fca8532f56ec77bfc3/sync-engine/inbox/auth/generic.py#L397-L435\n  secureOptions: constants.SSL_OP_NO_SSLv3 | constants.SSL_OP_NO_SSLv2 | constants.SSL_OP_NO_COMPRESSION | constants.SSL_OP_CIPHER_SERVER_PREFERENCE | constants.SSL_OP_SINGLE_DH_USE | constants.SSL_OP_SINGLE_ECDH_USE,\n}\n\nexport {SECURE_TLS_OPTIONS, INSECURE_TLS_OPTIONS};\n"
  },
  {
    "path": "packages/isomorphic-core/src/tracking-utils.es6",
    "content": "/**\n * These contain utility methods for link and open tracking.\n *\n * We need to perform some final transforms on the tracking links just\n * before send. Since we send from:\n *\n * 1) client-sync: SendMessageSMTP\n * 2) client-sync: SendMessagePerRecipientSMTP\n * 3) cloud-workers: sendPerRecipient\n *\n * we need to store these functions in isomorphic-core\n *\n * Open/Link tracking is a multi-step process.\n */\nimport url from 'url'\n\nclass TrackingUtils {\n  /**\n   * STEP 1: Put Message ID in tracking links\n   *\n   * When OpenTrackingComposerExtension or LinkTrackingComposerExtension\n   * insert tracking links into a draft body, these composer plugins don't\n   * yet know what the messageId will be. Once we generate a messageId in\n   * build for send, we replace the hardcoded `MESSAGE_ID` placeholder with\n   * the actual messageId. Have the messageId in the link is necessary for\n   * the cloud-api to figure out what link or open tracking pixel someone\n   * accessed.\n   */\n  prepareTrackingLinks(messageId, originalBody) {\n    const regex = new RegExp(`(https?://.+?)MESSAGE_ID`, 'g')\n    const body = originalBody.replace(regex, `$1${messageId}`);\n    return this.addSrcToOpenTrackingPixel(body);\n  }\n\n  /**\n   * STEP 2: Update open tracking src parameter\n   *\n   * Open tracking uses an image who's src points to our cloud-api. We\n   * don't actually want to give the img a `src` until this step since the\n   * link doesn't actually resolve to anything until now. Since we\n   * immediately render all changes to draft bodies in the client-app, if\n   * we don't do this the cloud-api servers will get a whole bunch of `GET\n   * /open/MESSAGE_ID` stub requests from the incomplete open pixel\n   */\n  addSrcToOpenTrackingPixel(originalBody) {\n    return originalBody.replace(\"data-open-tracking-src\", \"src\")\n  }\n\n  /**\n   * STEP 3: Add individualized recipient data to tracking links\n   *\n   * By default the open and link tracking urls don't indicate who that link\n   * is tailored to. When we send to individual people, we need to add an\n   * extra parameter to the end of the tracking url with the email of who\n   * it's being sent to.\n   *\n   * We use the `recipient` query parameter to indicate who the Recipient is.\n   *\n   * The cloud-api routes/link-tracking and routes/open-tracking know to\n   * look for the `recipient` query param when determining who clicked the\n   * link.\n   */\n  addRecipientToTrackingLinks({baseMessage, recipient, usesOpenTracking, usesLinkTracking} = {}) {\n    let body = baseMessage.body\n\n    if (usesOpenTracking) {\n      // This adds a `recipient` param to the open tracking src url.\n      body = body.replace(/<img class=\"n1-open\".*?src=\"(.*?)\">/g, (match, src) => {\n        const newSrc = this._addRecipientToUrl(src, recipient.email)\n        return `<img class=\"n1-open\" width=\"0\" height=\"0\" style=\"border:0; width:0; height:0;\" src=\"${newSrc}\">`;\n      });\n    }\n\n    if (usesLinkTracking) {\n      // This adds a `recipient` param to the link tracking tracking href url.\n      body = body.replace(this._urlLinkTagRegex(), (match, prefix, href, suffix, content, closingTag) => {\n        const newHref = this._addRecipientToUrl(href, recipient.email)\n        return `${prefix}${newHref}${suffix}${content}${closingTag}`;\n      });\n    }\n\n    return body;\n  }\n\n  /**\n   * STEP 4: Remove all link data from your own emails to prevent\n   * self-triggering\n   *\n   * When we save a message to a user's sent folder, we don't want that\n   * message to have link tracking data in it. Immediately after the message\n   * is sent, we save a stripped-version of the message to the database.\n   */\n  stripTrackingLinksFromBody(originalBody) {\n    // Removes open tracking images.\n    let body = originalBody.replace(/<img class=\"n1-open\".*?>/g, \"\");\n\n    // Replaces link tracking links with the original link.\n    // Link tracking looks like:\n    // <a href=\"https://n1.nylas.com/link/81ae3fe62cb9e5d674d94ea5d7c3f0e65fb2a93fe357f2db5452575a7c5d0165/0?redirect=https%3A%2F%2Fnylas.com%3Fref%3Dn1&r=ZXZhbkBldmFubW9yaWthd2EuY29t\">Nylas Mail</a>\n    //\n    // See https://regex101.com/r/Tr0LLT/1 for this._urlLinkTagRegex on example\n    // link.\n    // The link-tracking/lib/link-tracking-composer-extension.es6\n    // will add a `redirect` query param that has the original url.\n    body = body.replace(this._urlLinkTagRegex(), (match, prefix, href, suffix, content, closingTag) => {\n      if (!/nylas\\.com/.test(href)) return match\n      const originalUrl = (url.parse(href, true).query || {}).redirect\n      if (!originalUrl) return match\n      return `${prefix}${originalUrl}${suffix}${content}${closingTag}`;\n    });\n    return body;\n  }\n\n  _addRecipientToUrl(originalUrl, email) {\n    const parsed = url.parse(originalUrl, true);\n    const query = parsed.query || {}\n    query.recipient = email;\n    parsed.query = query;\n    parsed.search = null // so the format will use the query. See url docs.\n    return parsed.format()\n  }\n\n  // Copied from regexp-utils.coffee.\n  // Test cases: https://regex101.com/r/cK0zD8/4\n  // Catches link tags containing which are:\n  // - Non empty\n  // - Not a mailto: link\n  // Returns the following capturing groups:\n  // 1. start of the opening a tag to href=\"\n  // 2. The contents of the href without quotes\n  // 3. the rest of the opening a tag\n  // 4. the contents of the a tag\n  // 5. the closing tag\n  _urlLinkTagRegex() {\n    return new RegExp(/(<a.*?href\\s*?=\\s*?['\"])((?!mailto).+?)(['\"].*?>)([\\s\\S]*?)(<\\/a>)/gim);\n  }\n\n}\n\nexport default new TrackingUtils();\n"
  },
  {
    "path": "scripts/benchmark-initial-sync.sh",
    "content": "#!/bin/bash\n\nset -e\n\nfunction print_help {\n  OUTPUT=$(cat <<- EOM\n\n    A script to benchmark the initial sync performance of Nylas Mail. To use,\n    simply run the script after authing whatever accounts you wish to measure\n    in your development version of Nylas Mail. The benchmarking script will\n    clear all of the data except for your accounts and open and close Nylas\n    Mail several times, printing out the number of messages synced after each\n    iteration.\n  )\n  echo \"$OUTPUT\"\n}\n\nif [[ $1 == '-h' || $1 == '--help' ]]\nthen\n  print_help\n  exit 0\nfi\n\nCWD=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nNYLAS_DIR=\"$HOME/.nylas-bench\"\nEDGEHILL_DB=\"$NYLAS_DIR/edgehill.db\"\nTIME_LIMIT=120\nITERS=5\n\nfor i in `seq 1 $ITERS`\ndo\n  bash $CWD/drop-data-except-accounts.sh > /dev/null\n\n  (npm run benchmark &> /dev/null &)\n\n  sleep $TIME_LIMIT\n\n  ELECTRON_PID=`ps aux | grep \"Electron packages/client-app\" | grep -v grep | awk '{print $2}'`\n  kill -9 $ELECTRON_PID\n\n  MESSAGE_COUNT=`sqlite3 $EDGEHILL_DB 'SELECT COUNT(*) FROM Message'`\n  echo \"Synced Messages: $MESSAGE_COUNT\"\n\n  # Sometimes it takes a while to shutdown\n  while [[ $ELECTRON_PID != '' ]]\n  do\n    sleep 1\n    ELECTRON_PID=`ps aux | grep \"Electron packages/client-app\" | grep -v grep | awk '{print $2}'`\n  done\ndone\n"
  },
  {
    "path": "scripts/benchmark-new-commits.sh",
    "content": "#!/bin/bash\n\nset -e\n\nfunction get_next_commit() {\n  local LAST_COMMIT=$1\n  local NEXT_COMMIT=$(git log master ^$LAST_COMMIT --ancestry-path --pretty=oneline | cut -d\" \" -f1 | tail -n 1)\n  echo \"$NEXT_COMMIT\"\n}\n\nBENCHMARK_RESULTS_DIR=\"$HOME/.benchmark_results\"\nmkdir -p $BENCHMARK_RESULTS_DIR\n\nCWD=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\ncd $CWD/..\n\ngit checkout -q master\ngit pull -q --rebase\n\nLAST_COMMIT=$(cat $BENCHMARK_RESULTS_DIR/last_commit)\nNEXT_COMMIT=$(get_next_commit $LAST_COMMIT)\n\nwhile [[ $NEXT_COMMIT != '' ]]\ndo\n  echo $NEXT_COMMIT\n  git checkout -q $NEXT_COMMIT\n  bash $CWD/benchmark-initial-sync.sh > \"$BENCHMARK_RESULTS_DIR/$NEXT_COMMIT-results.txt\"\n  echo \"$NEXT_COMMIT\" > \"$BENCHMARK_RESULTS_DIR/last_commit\"\n  NEXT_COMMIT=$(get_next_commit $NEXT_COMMIT)\ndone\n"
  },
  {
    "path": "scripts/daily.js",
    "content": "#!/usr/bin/env babel-node\nconst childProcess = require('child_process')\nconst path = require('path')\nconst mkdirp = require('mkdirp')\nconst semver = require('semver')\nconst program = require('commander')\n\nconst TMP_DIR = path.join(__dirname, '..', 'tmp')\n\nasync function spawn(cmd, args, opts = {}) {\n  return new Promise((resolve, reject) => {\n    const env = Object.assign({}, process.env, opts.env || {})\n    delete opts.env\n    const options = Object.assign({stdio: 'inherit', env}, opts);\n    const proc = childProcess.spawn(cmd, args, options)\n    proc.on(\"error\", reject)\n    proc.on(\"exit\", resolve)\n  })\n}\n\nfunction exec(cmd, opts = {}) {\n  return new Promise((resolve, reject) => {\n    childProcess.exec(cmd, opts, (err, stdout) => {\n      if (err) {\n        return reject(err)\n      }\n      return resolve(stdout)\n    })\n  })\n}\n\nfunction git(subCmd, opts = {}) {\n  const optsString = Object.keys(opts).reduce((prev, key) => {\n    const optVal = opts[key]\n    if (optVal == null) {\n      return key.length > 1 ? `${prev} --${key}` : `${prev} -${key}`\n    }\n    return key.length > 1 ? `${prev} --${key}=${optVal}` : `${prev} -${key} ${optVal}`\n  }, '')\n  return exec(`git ${subCmd} ${optsString}`, {cwd: './'})\n}\n\nasync function prependToFile(filepath, string) {\n  await exec(`echo \"${string}\" > ${TMP_DIR}/tmpfile`)\n  await exec(`cat ${filepath} >> ${TMP_DIR}/tmpfile`)\n  await exec(`mv ${TMP_DIR}/tmpfile ${filepath}`)\n}\n\nasync function sliceFileLines(filepath, idx) {\n  await exec(`tail -n +${1 + idx} ${filepath} > ${TMP_DIR}/tmpfile`)\n  await exec(`mv ${TMP_DIR}/tmpfile ${filepath}`)\n}\n\nasync function updateChangelogFile(changelogString) {\n  mkdirp.sync(TMP_DIR)\n  await sliceFileLines('./packages/client-app/CHANGELOG.md', 2)\n  await prependToFile('./packages/client-app/CHANGELOG.md', changelogString)\n}\n\nfunction getFormattedLogs(mainLog) {\n  const formattedMainLog = (\n    mainLog\n    .filter(line => line.length > 0)\n    .filter(line => !/^bump/i.test(line) && !/changelog/i.test(line))\n    .map(line => `  + ${line.replace('*', '\\\\*')}`)\n    .join('\\n')\n  )\n  return `${formattedMainLog}\\n`\n}\n\nfunction getChangelogHeader(nextVersion) {\n  const date = new Date().toLocaleDateString()\n  return (\n    `# Nylas Mail Changelog\n\n### ${nextVersion} (${date})\n\n`\n  )\n}\n\nfunction validateArgs(args) {\n  if (args.editChangelog && !process.env.EDITOR) {\n    throw new Error(`You can't edit the changelog without a default EDITOR in your env`)\n  }\n}\n\n// TODO add progress indicators with ora\n// TODO add options\n// --update-daily-channel\n// --notify\n// --quiet\nasync function main(args) {\n  validateArgs(args)\n\n  // Pull latest changes\n  try {\n    await git(`checkout master`)\n    await git(`pull --rebase`)\n  } catch (err) {\n    console.error(err)\n    process.exit(1)\n  }\n\n  const pkg = require('../packages/client-app/package.json')  //eslint-disable-line\n  const currentVersion = pkg.version\n  const nextVersion = semver.inc(currentVersion, 'patch')\n\n  // Make sure working directory is clean\n  try {\n    await exec('git diff --exit-code && git diff --cached --exit-code')\n    const untrackedFiles = await exec('git ls-files --others --exclude-standard') || ''\n    if (untrackedFiles.length > 0) {\n      throw new Error('Working directory has untracked files')\n    }\n  } catch (err) {\n    console.error('Git working directory is not clean!')\n    process.exit(1)\n  }\n\n  // Make sure there is a diff to build\n  let mainLog = '';\n  try {\n    mainLog = (await git(`log ${currentVersion}..master --format='%s'`)).split('\\n')\n    if (mainLog.length <= 1) {\n      console.error(`There are no changes to build since ${currentVersion}`)\n      process.exit(1)\n    }\n  } catch (err) {\n    console.error(err)\n    process.exit(1)\n  }\n\n  // Update CHANGELOG\n  try {\n    const commitLogSinceLatestVersion = await getFormattedLogs(mainLog)\n    const changelogHeader = getChangelogHeader(nextVersion)\n    const changelogString = `${changelogHeader}${commitLogSinceLatestVersion}`\n    await updateChangelogFile(changelogString)\n    console.log(changelogString)\n  } catch (err) {\n    console.error('Could not update changelog file')\n    console.error(err)\n    process.exit(1)\n  }\n\n  // Allow editing\n  if (args.editChangelog) {\n    try {\n      await spawn(process.env.EDITOR, ['./packages/client-app/CHANGELOG.md'], {stdio: 'inherit'})\n    } catch (err) {\n      console.error('Error editing CHANGELOG.md')\n      console.error(err)\n      process.exit(1)\n    }\n  }\n\n  // Bump patch version in package.json\n  try {\n    await exec('npm --no-git-tag-version version patch', {cwd: 'packages/client-app'})\n  } catch (err) {\n    console.error('Could not bump version in package.json')\n    console.error(err)\n    process.exit(1)\n  }\n\n  if (args.noCommit) {\n    return\n  }\n\n  // Commit changes\n  try {\n    await git('add .')\n    await git(`commit -m 'bump(version): ${nextVersion}'`)\n  } catch (err) {\n    console.error('Could not commit changes')\n    console.error(err)\n    process.exit(1)\n  }\n\n  if (args.noTag) {\n    return\n  }\n\n  // Tag commit\n  try {\n    await git(`tag ${nextVersion}`)\n  } catch (err) {\n    console.error('Could not tag commit')\n    console.error(err)\n    process.exit(1)\n  }\n\n  if (args.noPush) {\n    return\n  }\n\n  // Push changes\n  try {\n    await git(`push origin master --tags`)\n  } catch (err) {\n    console.error('Could not tag commit')\n    console.error(err)\n    process.exit(1)\n  }\n\n  // Build locally. This should only be used when building from our in-office\n  // coffee machine mac mini\n  if (args.build) {\n    try {\n      await spawn('git', ['clean', '-xdf'])\n      await spawn('cp', ['-r', '../n1-keys-and-certificates', 'packages/client-app/build/resources/certs'])\n      await spawn('npm', ['install'], {env: {INSTALL_TARGET: 'client'}})\n      await spawn('npm', ['run', 'build-client'], {env: {SIGN_BUILD: true}})\n      await spawn('codesign', ['--verify', '--deep', '--verbose=2', '\"packages/client-app/dist/Nylas Mail-darwin-x64/Nylas Mail.app\"'])\n      // await spawn('npm', ['run', 'upload-client'])\n    } catch (err) {\n      console.error('Errored while running build')\n      console.error(err)\n      process.exit(1)\n    }\n\n    // TODO Update `daily` channel\n\n    // TODO send out notification email\n  }\n\n  console.log('Done!')\n}\n\nprogram\n.version('0.0.1')\n.usage('[options]')\n.description('This script will bump the version in package.json, edit the changelog with the latest\\n  git log (for easier editing), commit and tag the changes, and push to Github to trigger\\n  a build')\n.option('--edit-changelog', 'Open your $EDITOR to edit CHANGELOG before commiting version bump.')\n.option('--no-commit', 'Wether to commit changes to CHANGELOG.md and package.json')\n.option('--no-tag', 'Wether to tag the version bump commit (no-op if --no-commit is used)')\n.option('--no-push', 'Wether to push changes to the Github remote')\n.option('--build', 'Wether to build the app locally. This should only be used when building from our in-office Mac Mini by the coffee machine')\n.parse(process.argv)\n\nmain(program)\n"
  },
  {
    "path": "scripts/drop-data-except-accounts.sh",
    "content": "#!/bin/bash\n\nset -e\n\nCWD=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nNYLAS_DIR=\"$HOME/.nylas-bench\"\nTRUNCATE_TABLES=\"\nAccount \nAccountPluginMetadata \nCalendar \nCategory \nContact \nContactSearch \nEvent \nEventSearch \nFile \nMessage \nMessageBody \nMessagePluginMetadata \nProviderSyncbackRequest\nThread \nThreadCategory \nThreadContact \nThreadCounts \nThreadPluginMetadata \nThreadSearch\"\nSQLITE_DIR=$CWD/sqlite\nSQLITE_BIN=$SQLITE_DIR/sqlite3\nSQLITE_SRC_DIR=\"$SQLITE_DIR/sqlite-amalgamation-3170000\"\n\n# Build the sqlite3 amalgamation if necessary because we need FTS5 support.\nif [[ ! -e \"$SQLITE_BIN\" ]]\nthen\n  mkdir -p $SQLITE_DIR\n  curl -s \"https://www.sqlite.org/2017/sqlite-amalgamation-3170000.zip\" > \"$SQLITE_DIR/sqlite-amalgamation.zip\"\n  unzip -o -d $SQLITE_DIR \"$SQLITE_DIR/sqlite-amalgamation.zip\"\n  clang -DSQLITE_DISABLE_INTRINSIC -DSQLITE_ENABLE_FTS5 \"$SQLITE_SRC_DIR/sqlite3.c\" \"$SQLITE_SRC_DIR/shell.c\" -o $SQLITE_BIN\nfi\n\nrm -f $NYLAS_DIR/a-*.sqlite\n\nEDGEHILL_DB=$NYLAS_DIR/edgehill.db\n\nfor TABLE in $TRUNCATE_TABLES\ndo\n  COMMAND=\"DELETE FROM $TABLE\"\n  $SQLITE_BIN $EDGEHILL_DB \"DELETE FROM $TABLE\"\ndone\n\n$SQLITE_BIN $EDGEHILL_DB 'DELETE FROM JSONBlob WHERE client_id != \"NylasID\"'\n"
  },
  {
    "path": "scripts/postinstall.es6",
    "content": "import fs from 'fs-plus'\nimport path from 'path'\nimport childProcess from 'child_process'\n\nconst TARGET_ALL = 'all'\nconst TARGET_CLOUD = 'cloud'\nconst TARGET_CLIENT = 'client'\n\nasync function spawn(cmd, args, opts = {}) {\n  return new Promise((resolve, reject) => {\n    const options = Object.assign({stdio: 'inherit'}, opts);\n    const proc = childProcess.spawn(cmd, args, options)\n    proc.on(\"error\", reject)\n    proc.on(\"exit\", resolve)\n  })\n}\n\nfunction unlinkIfExistsSync(p) {\n  try {\n    if (fs.lstatSync(p)) {\n      fs.removeSync(p);\n    }\n  } catch (err) {\n    return\n  }\n}\n\nfunction installClientSyncPackage() {\n  console.log(\"\\n---> Linking client-sync\")\n  // link client-sync\n  const clientSyncDir = path.resolve(path.join('packages', 'client-sync'));\n  const destination = path.resolve(path.join('packages', 'client-app', 'internal_packages', 'client-sync'));\n  unlinkIfExistsSync(destination);\n  fs.symlinkSync(clientSyncDir, destination, 'dir');\n}\n\nasync function lernaBootstrap(installTarget) {\n  console.log(\"\\n---> Installing packages\");\n  const lernaCmd = process.platform === 'win32' ? 'lerna.cmd' : 'lerna';\n  const args = [\"bootstrap\"]\n  switch (installTarget) {\n    case TARGET_CLIENT:\n      args.push(`--ignore='cloud-*'`)\n      break\n    case TARGET_CLOUD:\n      args.push(`--ignore='client-*'`)\n      break\n    default:\n      break\n  }\n  await spawn(path.join('node_modules', '.bin', lernaCmd), args)\n}\n\nconst npmEnvs = {\n  system: process.env,\n  apm: Object.assign({}, process.env, {\n    NPM_CONFIG_TARGET: '0.10.40',\n  }),\n  electron: Object.assign({}, process.env, {\n    NPM_CONFIG_TARGET: '1.4.15',\n    NPM_CONFIG_ARCH: process.arch,\n    NPM_CONFIG_TARGET_ARCH: process.arch,\n    NPM_CONFIG_DISTURL: 'https://atom.io/download/electron',\n    NPM_CONFIG_RUNTIME: 'electron',\n    NPM_CONFIG_BUILD_FROM_SOURCE: true,\n  }),\n};\n\nasync function npm(cmd, options) {\n  const {cwd, env} = Object.assign({cwd: '.', env: 'system'}, options);\n  const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm'\n  await spawn(npmCmd, [cmd], {\n    cwd: path.resolve(__dirname, '..', cwd),\n    env: npmEnvs[env],\n  })\n}\n\nasync function electronRebuild() {\n  if (!fs.existsSync(path.join(\"packages\", \"client-app\", \"apm\"))) {\n    console.log(\"\\n---> No client app to rebuild. Moving on\")\n    return;\n  }\n  await npm('install', {\n    cwd: path.join('packages', 'client-app', 'apm'),\n    env: 'apm',\n  })\n  await npm('rebuild', {\n    cwd: path.join('packages', 'client-app'),\n    env: 'electron',\n  })\n}\n\nconst getJasmineDir = (packageName) => path.resolve(\n  path.join('packages', packageName, 'spec', 'jasmine')\n)\nconst getJasmineConfigPath = (packageName) => path.resolve(\n  path.join(getJasmineDir(packageName), 'config.json')\n)\n\nfunction linkJasmineConfigs() {\n  console.log(\"\\n---> Linking Jasmine configs\");\n  const linkToPackages = ['cloud-api', 'cloud-core', 'cloud-workers']\n  const from = getJasmineConfigPath('isomorphic-core')\n  for (const packageName of linkToPackages) {\n    const packageDir = path.join('packages', packageName)\n    if (!fs.existsSync(packageDir)) {\n      console.log(\"\\n---> No cloud packages to link. Moving on\")\n      return\n    }\n\n    const jasmineDir = getJasmineDir(packageName)\n    if (!fs.existsSync(jasmineDir)) {\n      fs.mkdirSync(jasmineDir)\n    }\n    const to = getJasmineConfigPath(packageName)\n    unlinkIfExistsSync(to)\n    fs.symlinkSync(from, to, 'file')\n  }\n}\n\nfunction linkIsomorphicCoreSpecs() {\n  console.log(\"\\n---> Linking isomorphic-core specs to client-app specs\")\n  const from = path.resolve(path.join('packages', 'isomorphic-core', 'spec'))\n  const to = path.resolve(path.join('packages', 'client-app', 'spec', 'isomorphic-core'))\n  unlinkIfExistsSync(to)\n  fs.symlinkSync(from, to, 'dir')\n}\n\nfunction getInstallTarget() {\n  const {INSTALL_TARGET} = process.env\n  if (!INSTALL_TARGET) {\n    return TARGET_ALL\n  }\n  if (![TARGET_ALL, TARGET_CLIENT, TARGET_CLOUD].includes(INSTALL_TARGET)) {\n    throw new Error(`postinstall: INSTALL_TARGET must be one of client, cloud, or all. It was set to ${INSTALL_TARGET}`)\n  }\n  return INSTALL_TARGET\n}\n\nasync function main() {\n  try {\n    const installTarget = getInstallTarget()\n    console.log(`\\n---> Installing for target ${installTarget}`);\n\n    if ([TARGET_ALL, TARGET_CLIENT].includes(installTarget)) {\n      installClientSyncPackage()\n    }\n\n    await lernaBootstrap(installTarget);\n\n    if ([TARGET_ALL, TARGET_CLIENT].includes(installTarget)) {\n      if (process.platform === \"darwin\") {\n        // Given that `lerna bootstrap` does not install optional dependencies, we\n        // need to manually run `npm install` inside `client-app` so\n        // `node-mac-notifier` get's correctly installed and included in the build\n        // See https://github.com/lerna/lerna/issues/121\n        console.log(\"\\n---> Reinstalling client-app dependencies to include optional dependencies\");\n        await npm('install', {cwd: 'packages/client-app'})\n      }\n      await electronRebuild();\n      linkJasmineConfigs();\n      linkIsomorphicCoreSpecs();\n    }\n  } catch (err) {\n    console.error(err);\n    process.exit(1);\n  }\n}\nmain()\n"
  },
  {
    "path": "scripts/requirements.txt",
    "content": "appdirs==1.4.3\ngspread==0.6.2\nhttplib2==0.10.3\noauth2client==4.0.0\npackaging==16.8\npyasn1==0.2.3\npyasn1-modules==0.0.8\npyparsing==2.2.0\npython-dateutil==2.6.0\nrequests==2.13.0\nrsa==3.4.2\nsix==1.10.0\n"
  },
  {
    "path": "scripts/run-once-per-day.sh",
    "content": "#!/bin/bash\n\n# This just checks the time every 60 seconds to see if it should run the benchmarks.\n# This is a hack to work around cron's/automator's inability to start GUI apps\n# using `npm start`.\n\nCWD=\"$( cd \"$( dirname \"${BASH_SOURCE[0]}\" )\" && pwd )\"\nBENCHMARK_SCRIPT=$CWD/benchmark-new-commits.sh\nUPLOAD_SCRIPT=$CWD/upload-benchmark-data.py\nBENCHMARK_RESULTS_DIR=$HOME/.benchmark_results\nTARGET_TIME=\"$1\"\n\nif [[ $# != 1 ]]\nthen\n  echo \"Usage: run-once-per-day.sh '23:00'\"\n  exit 1\nfi\n\nwhile [[ true ]]\ndo\n    CURRENT_TIME=$(date +\"%H:%M\")\n\n    if [[ $CURRENT_TIME = $TARGET_TIME ]]\n    then\n        /bin/bash $BENCHMARK_SCRIPT\n        python $UPLOAD_SCRIPT $BENCHMARK_RESULTS_DIR\n    fi\n    sleep 60\ndone\n\n"
  },
  {
    "path": "scripts/upload-benchmark-data.py",
    "content": "import os\nimport re\nimport sys\nimport subprocess\n\nfrom dateutil.parser import parse as parse_datestr\nfrom glob import glob\n\nimport gspread\n\nfrom oauth2client.service_account import ServiceAccountCredentials\n\nscope = ['https://spreadsheets.google.com/feeds']\n\n\ndef usage():\n    print \"./scripts/upload-benchmark-data.py <datadir>\"\n\n\ndef anymean(filename):\n    output = subprocess.check_output(['./scripts/toolbox/any_mean.py', filename])\n    if output == '':\n        return 0.0, 0.0\n\n    # e.g. 'Synced Messages: 77.00 +-0.00'\n    synced_messages, confidence_interval = re.match('^Synced Messages: ([0-9.]+) (\\+-[0-9.]+)$', output).groups()\n    return synced_messages, confidence_interval\n\n\ndef update_spreadsheet(datadir):\n    credentials = ServiceAccountCredentials.from_json_keyfile_name('client_secret.json', scope)\n    gc = gspread.authorize(credentials)\n    worksheet = gc.open(\"Nylas Mail Benchmarks\").sheet1\n\n    filenames = []\n    for filename in glob('{datadir}/*-results.txt'.format(datadir=datadir)):\n        gitsha = re.match('^(.*)-results.txt$', os.path.basename(filename)).groups(0)[0]\n        formatted_datetime = subprocess.check_output(['git', 'show', '-s', '--format=%ci', gitsha])\n        parsed_datetime = parse_datestr(formatted_datetime)\n        filenames.append((filename, gitsha, parsed_datetime))\n\n    new_data = []\n    for filename, gitsha, parsed_datetime in sorted(filenames, key=lambda t: t[2]):\n        synced_messages, confidence_interval = anymean(filename)\n        row = (parsed_datetime.strftime(\"%Y-%m-%d %H:%M:%S\"), gitsha, synced_messages, confidence_interval)\n        new_data.append(row)\n        print row\n\n    # TODO: might want to use the batch upload api in order to not run into rate-limits\n    for i, new_row in enumerate(new_data):\n        row_num = i+2\n        existing_row = worksheet.range('A{row_num}:D{row_num}'.format(row_num=row_num))\n        for j, cell in enumerate(existing_row):\n            col_num = j+1\n            cell.value = new_row[j]\n            print \"updating cell {row_num}:{col_num} with {val}\".format(row_num=row_num, col_num=col_num, val=cell.value)\n        worksheet.update_cells(existing_row)\n\n\ndef main():\n    if len(sys.argv) != 2:\n        usage()\n        return 1\n\n    datadir = sys.argv[1]\n    update_spreadsheet(datadir)\n    return 0\n\nif __name__ == '__main__':\n    sys.exit(main())\n"
  }
]