diff --git a/conf/default.conf.php b/conf/default.conf.php
index 2b9eaf0fd..4c44ce01c 100644
--- a/conf/default.conf.php
+++ b/conf/default.conf.php
@@ -1,709 +1,714 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 return array(
 
   // The root URI which Phabricator is installed on.
   // Example: "http://phabricator.example.com/"
   'phabricator.base-uri'        => null,
 
   // If you have multiple environments, provide the production environment URI
   // here so that emails, etc., generated in development/sandbox environments
   // contain the right links.
   'phabricator.production-uri'  => null,
 
   // Setting this to 'true' will invoke a special setup mode which helps guide
   // you through setting up Phabricator.
   'phabricator.setup'           => false,
 
   // The default PHID for users who haven't uploaded a profile image. It should
   // be 50x50px.
   'user.default-profile-image-phid' => 'PHID-FILE-4d61229816cfe6f2b2a3',
 
 // -- IMPORTANT! Security! -------------------------------------------------- //
 
   // IMPORTANT: By default, Phabricator serves files from the same domain the
   // application lives on. This is convenient but not secure: it creates
   // a vulnerability where an external attacker can:
   //
   //  - Convince a privileged user to upload a file which appears to be an
   //    image or some other inoccuous type of file (the file is actually both
   //    a JAR and an image); and
   //  - convince the user to give them the URI for the image; and
   //  - convince the user to click a link to a site which embeds the "image"
   //    using an <applet /> tag. This steals the user's credentials.
   //
   // If the attacker is internal, they can execute the first two steps
   // themselves and need only convince another user to click a link in order to
   // steal their credentials.
   //
   // To avoid this, you should configure a second domain in the same way you
   // have the primary domain configured (e.g., point it at the same machine and
   // set up the same vhost rules) and provide it here. For instance, if your
   // primary install is on "http://www.phabricator-example.com/", you could
   // configure "http://www.phabricator-files.com/" and specify the entire
   // domain (with protocol) here. This will enforce that viewable files are
   // served only from the alternate domain. Ideally, you should use a completely
   // separate domain name rather than just a different subdomain.
   //
   // It is STRONGLY RECOMMENDED that you configure this. Phabricator makes this
   // attack difficult, but it is viable unless you isolate the file domain.
   'security.alternate-file-domain'  => null,
 
 // -- DarkConsole ----------------------------------------------------------- //
 
   // DarkConsole is a administrative debugging/profiling tool built into
   // Phabricator. You can leave it disabled unless you're developing against
   // Phabricator.
 
   // Determines whether or not DarkConsole is available. DarkConsole exposes
   // some data like queries and stack traces, so you should be careful about
   // turning it on in production (although users can not normally see it, even
   // if the deployment configuration enables it).
   'darkconsole.enabled'         => false,
 
   // Always enable DarkConsole, even for logged out users. This potentially
   // exposes sensitive information to users, so make sure untrusted users can
   // not access an install running in this mode. You should definitely leave
   // this off in production. It is only really useful for using DarkConsole
   // utilties to debug or profile logged-out pages. You must set
   // 'darkconsole.enabled' to use this option.
   'darkconsole.always-on'       => false,
 
 
   // Allows you to mask certain configuration values from appearing in the
   // "Config" tab of DarkConsole.
   'darkconsole.config-mask'     => array(
     'mysql.pass',
     'amazon-ses.secret-key',
     'recaptcha.private-key',
     'phabricator.csrf-key',
     'facebook.application-secret',
     'github.application-secret',
   ),
 
 
 // --  MySQL  --------------------------------------------------------------- //
 
   // The username to use when connecting to MySQL.
   'mysql.user' => 'root',
 
   // The password to use when connecting to MySQL.
   'mysql.pass' => '',
 
   // The MySQL server to connect to. If you want to connect to a different
   // port than the default (which is 3306), specify it in the hostname
   // (e.g., db.example.com:1234).
   'mysql.host' => 'localhost',
 
 
 // -- Email ----------------------------------------------------------------- //
 
   // Some Phabricator tools send email notifications, e.g. when Differential
   // revisions are updated or Maniphest tasks are changed. These options allow
   // you to configure how email is delivered.
 
   // You can test your mail setup by going to "MetaMTA" in the web interface,
   // clicking "Send New Message", and then composing a message.
 
   // Default address to send mail "From".
   'metamta.default-address'     => 'noreply@example.com',
 
   // Domain used to generate Message-IDs.
   'metamta.domain'              => 'example.com',
 
   // When a user takes an action which generates an email notification (like
   // commenting on a Differential revision), Phabricator can either send that
   // mail "From" the user's email address (like "alincoln@logcabin.com") or
   // "From" the 'metamta.default-address' address. The user experience is
   // generally better if Phabricator uses the user's real address as the "From"
   // since the messages are easier to organize when they appear in mail clients,
   // but this will only work if the server is authorized to send email on behalf
   // of the "From" domain. Practically, this means:
   //    - If you are doing an install for Example Corp and all the users will
   //      have corporate @corp.example.com addresses and any hosts Phabricator
   //      is running on are authorized to send email from corp.example.com,
   //      you can enable this to make the user experience a little better.
   //    - If you are doing an install for an open source project and your
   //      users will be registering via Facebook and using personal email
   //      addresses, you MUST NOT enable this or virtually all of your outgoing
   //      email will vanish into SFP blackholes.
   //    - If your install is anything else, you're much safer leaving this
   //      off since the risk in turning it on is that your outgoing mail will
   //      mostly never arrive.
   'metamta.can-send-as-user'    => false,
 
   // Adapter class to use to transmit mail to the MTA. The default uses
   // PHPMailerLite, which will invoke "sendmail". This is appropriate
   // if sendmail actually works on your host, but if you haven't configured mail
   // it may not be so great. You can also use Amazon SES, by changing this to
   // 'PhabricatorMailImplementationAmazonSESAdapter', signing up for SES, and
   // filling in your 'amazon-ses.access-key' and 'amazon-ses.secret-key' below.
   'metamta.mail-adapter'        =>
     'PhabricatorMailImplementationPHPMailerLiteAdapter',
 
   // When email is sent, try to hand it off to the MTA immediately. This may
   // be worth disabling if your MTA infrastructure is slow or unreliable. If you
   // disable this option, you must run the 'metamta_mta.php' daemon or mail
   // won't be handed off to the MTA. If you're using Amazon SES it can be a
   // little slugish sometimes so it may be worth disabling this and moving to
   // the daemon after you've got your install up and running. If you have a
   // properly configured local MTA it should not be necessary to disable this.
   'metamta.send-immediately'    => true,
 
   // If you're using Amazon SES to send email, provide your AWS access key
   // and AWS secret key here. To set up Amazon SES with Phabricator, you need
   // to:
   //  - Make sure 'metamta.mail-adapter' is set to:
   //    "PhabricatorMailImplementationAmazonSESAdapter"
   //  - Make sure 'metamta.can-send-as-user' is false.
   //  - Make sure 'metamta.default-address' is configured to something sensible.
   //  - Make sure 'metamta.default-address' is a validated SES "From" address.
   'amazon-ses.access-key'       =>  null,
   'amazon-ses.secret-key'       =>  null,
 
   // If you're using Sendgrid to send email, provide your access credentials
   // here. This will use the REST API. You can also use Sendgrid as a normal
   // SMTP service.
   'sendgrid.api-user'           => null,
   'sendgrid.api-key'            => null,
 
   // You can configure a reply handler domain so that email sent from Maniphest
   // will have a special "Reply To" address like "T123+82+af19f@example.com"
   // that allows recipients to reply by email and interact with tasks. For
   // instructions on configurating reply handlers, see the article
   // "Configuring Inbound Email" in the Phabricator documentation. By default,
   // this is set to 'null' and Phabricator will use a generic 'noreply@' address
   // or the address of the acting user instead of a special reply handler
   // address (see 'metamta.default-address'). If you set a domain here,
   // Phabricator will begin generating private reply handler addresses. See
   // also 'metamta.maniphest.reply-handler' to further configure behavior.
   // This key should be set to the domain part after the @, like "example.com".
   'metamta.maniphest.reply-handler-domain' => null,
 
   // You can follow the instructions in "Configuring Inbound Email" in the
   // Phabricator documentation and set 'metamta.maniphest.reply-handler-domain'
   // to support updating Maniphest tasks by email. If you want more advanced
   // customization than this provides, you can override the reply handler
   // class with an implementation of your own. This will allow you to do things
   // like have a single public reply handler or change how private reply
   // handlers are generated and validated.
   // This key should be set to a loadable subclass of
   // PhabricatorMailReplyHandler (and possibly of ManiphestReplyHandler).
   'metamta.maniphest.reply-handler' => 'ManiphestReplyHandler',
 
   // If you don't want phabricator to take up an entire domain
   // (or subdomain for that matter), you can use this and set a common
   // prefix for mail sent by phabricator. It will make use of the fact that
   // a mail-address such as phabricator+D123+1hjk213h@example.com will be
   // delivered to the phabricator users mailbox.
   // Set this to the left part of the email address and it well get
   // prepended to all outgoing mail. If you want to use e.g.
   // 'phabricator@example.com' this should be set to 'phabricator'.
   'metamta.single-reply-handler-prefix' => null,
 
   // Prefix prepended to mail sent by Maniphest. You can change this to
   // distinguish between testing and development installs, for example.
   'metamta.maniphest.subject-prefix' => '[Maniphest]',
 
   // See 'metamta.maniphest.reply-handler-domain'. This does the same thing,
   // but allows email replies via Differential.
   'metamta.differential.reply-handler-domain' => null,
 
   // See 'metamta.maniphest.reply-handler'. This does the same thing, but
   // affects Differential.
   'metamta.differential.reply-handler' => 'DifferentialReplyHandler',
 
   // Prefix prepended to mail sent by Differential.
   'metamta.differential.subject-prefix' => '[Differential]',
 
+  // Set this to true if you want patches to be attached to mail from
+  // Differential.  This won't work if you are using SendGrid as your mail
+  // adapter.
+  'metamta.differential.attach-patches' => false,
+
   // By default, Phabricator generates unique reply-to addresses and sends a
   // separate email to each recipient when you enable reply handling. This is
   // more secure than using "From" to establish user identity, but can mean
   // users may receive multiple emails when they are on mailing lists. Instead,
   // you can use a single, non-unique reply to address and authenticate users
   // based on the "From" address by setting this to 'true'. This trades away
   // a little bit of security for convenience, but it's reasonable in many
   // installs. Object interactions are still protected using hashes in the
   // single public email address, so objects can not be replied to blindly.
   'metamta.public-replies' => false,
 
   // You can configure an email address like "bugs@phabricator.example.com"
   // which will automatically create Maniphest tasks when users send email
   // to it. This relies on the "From" address to authenticate users, so it is
   // is not completely secure. To set this up, enter a complete email
   // address like "bugs@phabricator.example.com" and then configure mail to
   // that address so it routed to Phabricator (if you've already configured
   // reply handlers, you're probably already done). See "Configuring Inbound
   // Email" in the documentation for more information.
   'metamta.maniphest.public-create-email' => null,
 
   // If you enable 'metamta.public-replies', Phabricator uses "From" to
   // authenticate users. You can additionally enable this setting to try to
   // authenticate with 'Reply-To'. Note that this is completely spoofable and
   // insecure (any user can set any 'Reply-To' address) but depending on the
   // nature of your install or other deliverability conditions this might be
   // okay. Generally, you can't do much more by spoofing Reply-To than be
   // annoying (you can write but not read content). But, you know, this is
   // still **COMPLETELY INSECURE**.
   'metamta.insecure-auth-with-reply-to' => false,
 
 
 // -- Auth ------------------------------------------------------------------ //
 
   // Can users login with a username/password, or by following the link from
   // a password reset email? You can disable this and configure one or more
   // OAuth providers instead.
   'auth.password-auth-enabled'  => true,
 
   // Maximum number of simultaneous web sessions each user is permitted to have.
   // Setting this to "1" will prevent a user from logging in on more than one
   // browser at the same time.
   'auth.sessions.web'           => 5,
 
   // Maximum number of simultaneous Conduit sessions each user is permitted
   // to have.
   'auth.sessions.conduit'       => 3,
 
   // Set this true to enable the Settings -> SSH Public Keys panel, which will
   // allow users to associated SSH public keys with their accounts. This is only
   // really useful if you're setting up services over SSH and want to use
   // Phabricator for authentication; in most situations you can leave this
   // disabled.
   'auth.sshkeys.enabled'        => false,
 
 
 // -- Accounts -------------------------------------------------------------- //
 
   // Is basic account information (email, real name, profile picture) editable?
   // If you set up Phabricator to automatically synchronize account information
   // from some other authoritative system, you can disable this to ensure
   // information remains consistent across both systems.
   'account.editable'            => true,
 
 
 // --  Facebook  ------------------------------------------------------------ //
 
   // Can users use Facebook credentials to login to Phabricator?
   'facebook.auth-enabled'       => false,
 
   // Can users use Facebook credentials to create new Phabricator accounts?
   'facebook.registration-enabled' => true,
 
   // Are Facebook accounts permanently linked to Phabricator accounts, or can
   // the user unlink them?
   'facebook.auth-permanent'     => false,
 
   // The Facebook "Application ID" to use for Facebook API access.
   'facebook.application-id'     => null,
 
   // The Facebook "Application Secret" to use for Facebook API access.
   'facebook.application-secret' => null,
 
 
 // -- Github ---------------------------------------------------------------- //
 
   // Can users use Github credentials to login to Phabricator?
   'github.auth-enabled'         => false,
 
   // Can users use Github credentials to create new Phabricator accounts?
   'github.registration-enabled' => true,
 
   // Are Github accounts permanently linked to Phabricator accounts, or can
   // the user unlink them?
   'github.auth-permanent'       => false,
 
   // The Github "Client ID" to use for Github API access.
   'github.application-id'       => null,
 
   // The Github "Secret" to use for Github API access.
   'github.application-secret'   => null,
 
 
 // -- Google ---------------------------------------------------------------- //
 
   // Can users use Google credentials to login to Phabricator?
   'google.auth-enabled'         => false,
 
   // Can users use Google credentials to create new Phabricator accounts?
   'google.registration-enabled' => true,
 
   // Are Google accounts permanently linked to Phabricator accounts, or can
   // the user unlink them?
   'google.auth-permanent'       => false,
 
   // The Google "Client ID" to use for Google API access.
   'google.application-id'       => null,
 
   // The Google "Client Secret" to use for Google API access.
   'google.application-secret'   => null,
 
 // -- Recaptcha ------------------------------------------------------------- //
 
   // Is Recaptcha enabled? If disabled, captchas will not appear.
   'recaptcha.enabled'           => false,
 
   // Your Recaptcha public key, obtained from Recaptcha.
   'recaptcha.public-key'        => null,
 
   // Your Recaptcha private key, obtained from Recaptcha.
   'recaptcha.private-key'       => null,
 
 
 // -- Misc ------------------------------------------------------------------ //
 
   // This is hashed with other inputs to generate CSRF tokens. If you want, you
   // can change it to some other string which is unique to your install. This
   // will make your install more secure in a vague, mostly theoretical way. But
   // it will take you like 3 seconds of mashing on your keyboard to set it up so
   // you might as well.
   'phabricator.csrf-key'        => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3',
 
   // This is hashed with other inputs to generate mail tokens. If you want, you
   // can change it to some other string which is unique to your install. In
   // particular, you will want to do this if you accidentally send a bunch of
   // mail somewhere you shouldn't have, to invalidate all old reply-to
   // addresses.
   'phabricator.mail-key'        => '5ce3e7e8787f6e40dfae861da315a5cdf1018f12',
 
 
   // This is hashed with other inputs to generate file secret keys. Changing
   // it will invalidate all file URIs if you have an alternate file domain
   // configured (see 'security.alternate-file-domain').
   'phabricator.file-key'        => 'ade8dadc8b4382067069a4d4798112191af8a190',
 
   // Version string displayed in the footer. You probably should leave this
   // alone.
   'phabricator.version'         => 'UNSTABLE',
 
   // PHP requires that you set a timezone in your php.ini before using date
   // functions, or it will emit a warning. If this isn't possible (for instance,
   // because you are using HPHP) you can set some valid constant for
   // date_default_timezone_set() here and Phabricator will set it on your
   // behalf, silencing the warning.
   'phabricator.timezone'        => null,
 
   // When unhandled exceptions occur, stack traces are hidden by default.
   // You can enable traces for development to make it easier to debug problems.
   'phabricator.show-stack-traces' => false,
 
   // When users write comments which have URIs, they'll be automaticaly linked
   // if the protocol appears in this set. This whitelist is primarily to prevent
   // security issues like javascript:// URIs.
   'uri.allowed-protocols' => array(
     'http'  => true,
     'https' => true,
   ),
 
   // Tokenizers are UI controls which let the user select other users, email
   // addresses, project names, etc., by typing the first few letters and having
   // the control autocomplete from a list. They can load their data in two ways:
   // either in a big chunk up front, or as the user types. By default, the data
   // is loaded in a big chunk. This is simpler and performs better for small
   // datasets. However, if you have a very large number of users or projects,
   // (in the ballpark of more than a thousand), loading all that data may become
   // slow enough that it's worthwhile to query on demand instead. This makes
   // the typeahead slightly less responsive but overall performance will be much
   // better if you have a ton of stuff. You can figure out which setting is
   // best for your install by changing this setting and then playing with a
   // user tokenizer (like the user selectors in Maniphest or Differential) and
   // seeing which setting loads faster and feels better.
   'tokenizer.ondemand'          => false,
 
 // -- Files ----------------------------------------------------------------- //
 
   // Lists which uploaded file types may be viewed in the browser. If a file
   // has a mime type which does not appear in this list, it will always be
   // downloaded instead of displayed. This is a security consideration: if a
   // user uploads a file of type "text/html" and it is displayed as
   // "text/html", they can easily execute XSS attacks. This is also a usability
   // consideration, since browsers tend to freak out when viewing enormous
   // binary files.
   //
   // The keys in this array are viewable mime types; the values are the mime
   // types they will be delivered as when they are viewed in the browser.
   //
   // IMPORTANT: Making any file types viewable is a security vulnerability if
   // you do not configure 'security.alternate-file-domain' above.
   'files.viewable-mime-types' => array(
     'image/jpeg'  => 'image/jpeg',
     'image/jpg'   => 'image/jpg',
     'image/png'   => 'image/png',
     'image/gif'   => 'image/gif',
     'text/plain'  => 'text/plain; charset=utf-8',
   ),
 
   // Phabricator can proxy images from other servers so you can paste the URI
   // to a funny picture of a cat into the comment box and have it show up as an
   // image. However, this means the webserver Phabricator is running on will
   // make HTTP requests to arbitrary URIs. If the server has access to internal
   // resources, this could be a security risk. You should only enable it if you
   // are installed entirely a VPN and VPN access is required to access
   // Phabricator, or if the webserver has no special access to anything. If
   // unsure, it is safer to leave this disabled.
   'files.enable-proxy' => false,
 
 
 // -- Storage --------------------------------------------------------------- //
 
   // Phabricator allows users to upload files, and can keep them in various
   // storage engines. This section allows you to configure which engines
   // Phabricator will use, and how it will use them.
 
   // The largest filesize Phabricator will store in the MySQL BLOB storage
   // engine, which just uses a database table to store files. While this isn't a
   // best practice, it's really easy to set up. This is hard-limited by the
   // value of 'max_allowed_packet' in MySQL (since this often defaults to 1MB,
   // the default here is slightly smaller than 1MB). Set this to 0 to disable
   // use of the MySQL blob engine.
   'storage.mysql-engine.max-size' => 1000000,
 
   // Phabricator provides a local disk storage engine, which just writes files
   // to some directory on local disk. The webserver must have read/write
   // permissions on this directory. This is straightforward and suitable for
   // most installs, but will not scale past one web frontend unless the path
   // is actually an NFS mount, since you'll end up with some of the files
   // written to each web frontend and no way for them to share. To use the
   // local disk storage engine, specify the path to a directory here. To
   // disable it, specify null.
   'storage.local-disk.path'       => null,
 
   // If you want to store files in Amazon S3, specify an AWS access and secret
   // key here and a bucket name below.
   'amazon-s3.access-key'          =>  null,
   'amazon-s3.secret-key'          =>  null,
 
   // Set this to a valid Amazon S3 bucket to store files there. You must also
   // configure S3 access keys above.
   'storage.s3.bucket'             => null,
 
   // Phabricator uses a storage engine selector to choose which storage engine
   // to use when writing file data. If you add new storage engines or want to
   // provide very custom rules (e.g., write images to one storage engine and
   // other files to a different one), you can provide an alternate
   // implementation here. The default engine will use choose MySQL, Local Disk,
   // and S3, in that order, if they have valid configurations above and a file
   // fits within configured limits.
   'storage.engine-selector' => 'PhabricatorDefaultFileStorageEngineSelector',
 
 
 // -- Search ---------------------------------------------------------------- //
 
   // Phabricator uses a search engine selector to choose which search engine
   // to use when indexing and reconstructing documents, and when executing
   // queries. You can override the engine selector to provide a new selector
   // class which can select some custom engine you implement, if you want to
   // store your documents in some search engine which does not have default
   // support.
   'search.engine-selector'  => 'PhabricatorDefaultSearchEngineSelector',
 
 
 // -- Differential ---------------------------------------------------------- //
 
   'differential.revision-custom-detail-renderer'  => null,
 
   // Array for custom remarkup rules. The array should have a list of
   // class names of classes that extend PhutilRemarkupRule
   'differential.custom-remarkup-rules' => null,
 
   // Array for custom remarkup block rules. The array should have a list of
   // class names of classes that extend PhutilRemarkupEngineBlockRule
   'differential.custom-remarkup-block-rules' => null,
 
   // Set display word-wrap widths for Differential. Specify a dictionary of
   // regular expressions mapping to column widths. The filename will be matched
   // against each regexp in order until one matches. The default configuration
   // uses a width of 100 for Java and 80 for other languages. Note that 80 is
   // the greatest column width of all time. Changes here will not be immediately
   // reflected in old revisions unless you purge the render cache.
   'differential.wordwrap' => array(
     '/\.java$/' => 100,
     '/.*/'      => 80,
   ),
 
   // List of file regexps were whitespace is meaningful and should not
   // use 'ignore-all' by default
   'differential.whitespace-matters' => array(
     '/\.py$/',
   ),
 
   'differential.field-selector' => 'DifferentialDefaultFieldSelector',
 
   // If you set this to true, users can "!accept" revisions via email (normally,
   // they can take other actions but can not "!accept"). This action is disabled
   // by default because email authentication can be configured to be very weak,
   // and, socially, email "!accept" is kind of sketchy and implies revisions may
   // not actually be receiving thorough review.
   'differential.enable-email-accept' => false,
 
 
 // -- Maniphest ------------------------------------------------------------- //
 
   'maniphest.enabled' => true,
 
   // Array of custom fields for Maniphest tasks. For details on adding custom
   // fields to Maniphest, see "Maniphest User Guide: Adding Custom Fields".
   'maniphest.custom-fields' => array(),
 
   // Class which drives custom field construction. See "Maniphest User Guide:
   // Adding Custom Fields" in the documentation for more information.
   'maniphest.custom-task-extensions-class' => 'ManiphestDefaultTaskExtensions',
 
 // -- Remarkup -------------------------------------------------------------- //
 
   // If you enable this, linked YouTube videos will be embeded inline. This has
   // mild security implications (you'll leak referrers to YouTube) and is pretty
   // silly (but sort of awesome).
   'remarkup.enable-embedded-youtube' => false,
 
 
 // -- Garbage Collection ---------------------------------------------------- //
 
   // Phabricator generates various logs and caches in the database which can
   // be garbage collected after a while to make the total data size more
   // manageable. To run garbage collection, launch a
   // PhabricatorGarbageCollector daemon.
 
   // Since the GC daemon can issue large writes and table scans, you may want to
   // run it only during off hours or make sure it is scheduled so it doesn't
   // overlap with backups. This determines when the daemon can start running
   // each day.
   'gcdaemon.run-at'    => '12 AM',
 
   // How many seconds after 'gcdaemon.run-at' the daemon may collect garbage
   // for. By default it runs continuously, but you can set it to run for a
   // limited period of time. For instance, if you do backups at 3 AM, you might
   // run garbage collection for an hour beforehand. This is not a high-precision
   // limit so you may want to leave some room for the GC to actually stop, and
   // if you set it to something like 3 seconds you're on your own.
   'gcdaemon.run-for'   => 24 * 60 * 60,
 
   // These 'ttl' keys configure how much old data the GC daemon keeps around.
   // Objects older than the ttl will be collected. Set any value to 0 to store
   // data indefinitely.
 
   'gcdaemon.ttl.herald-transcripts'         => 30 * (24 * 60 * 60),
   'gcdaemon.ttl.daemon-logs'                =>  7 * (24 * 60 * 60),
   'gcdaemon.ttl.differential-parse-cache'   => 14 * (24 * 60 * 60),
 
 
 // -- Feed ------------------------------------------------------------------ //
 
   // If you set this to true, you can embed Phabricator activity feeds in other
   // pages using iframes. These feeds are completely public, and a login is not
   // required to view them! This is intended for things like open source
   // projects that want to expose an activity feed on the project homepage.
   'feed.public' => false,
 
 // -- Customization --------------------------------------------------------- //
 
   // Paths to additional phutil libraries to load.
   'load-libraries' => array(),
 
   'aphront.default-application-configuration-class' =>
     'AphrontDefaultApplicationConfiguration',
 
   'controller.oauth-registration' =>
     'PhabricatorOAuthDefaultRegistrationController',
 
 
   // Directory that phd (the Phabricator daemon control script) should use to
   // track running daemons.
   'phd.pid-directory' => '/var/tmp/phd',
 
   // This value is an input to the hash function when building resource hashes.
   // It has no security value, but if you accidentally poison user caches (by
   // pushing a bad patch or having something go wrong with a CDN, e.g.) you can
   // change this to something else and rebuild the Celerity map to break user
   // caches. Unless you are doing Celerity development, it is exceptionally
   // unlikely that you need to modify this.
   'celerity.resource-hash' => 'd9455ea150622ee044f7931dabfa52aa',
 
   // In a development environment, it is desirable to force static resources
   // (CSS and JS) to be read from disk on every request, so that edits to them
   // appear when you reload the page even if you haven't updated the resource
   // maps. This setting ensures requests will be verified against the state on
   // disk. Generally, you should leave this off in production (caching behavior
   // and performance improve with it off) but turn it on in development. (These
   // settings are the defaults.)
   'celerity.force-disk-reads' => false,
 
   // You can respond to various application events by installing listeners,
   // which will receive callbacks when interesting things occur. Specify a list
   // of classes which extend PhabricatorEventListener here.
   'events.listeners'  => array(),
 
 // -- Pygments -------------------------------------------------------------- //
 
   // Phabricator can highlight PHP by default, but if you want syntax
   // highlighting for other languages you should install the python package
   // 'Pygments', make sure the 'pygmentize' script is available in the
   // $PATH of the webserver, and then enable this.
   'pygments.enabled'            => false,
 
   // In places that we display a dropdown to syntax-highlight code,
   // this is where that list is defined.
   // Syntax is 'lexer-name' => 'Display Name',
   'pygments.dropdown-choices' => array(
     'apacheconf' => 'Apache Configuration',
     'bash' => 'Bash Scripting',
     'brainfuck' => 'Brainf*ck',
     'c' => 'C',
     'cpp' => 'C++',
     'css' => 'CSS',
     'diff' => 'Diff',
     'django' => 'Django Templating',
     'erb' => 'Embedded Ruby/ERB',
     'erlang' => 'Erlang',
     'html' => 'HTML',
     'infer' => 'Infer from title (extension)',
     'java' => 'Java',
     'js' => 'Javascript',
     'mysql' => 'MySQL',
     'perl' => 'Perl',
     'php' => 'PHP',
     'text' => 'Plain Text',
     'python' => 'Python',
     'rainbow' => 'Rainbow',
     'remarkup' => 'Remarkup',
     'ruby' => 'Ruby',
     'xml' => 'XML',
   ),
 
   'pygments.dropdown-default' => 'infer',
 
   // This is an override list of regular expressions which allows you to choose
   // what language files are highlighted as. If your projects have certain rules
   // about filenames or use unusual or ambiguous language extensions, you can
   // create a mapping here. This is an ordered dictionary of regular expressions
   // which will be tested against the filename. They should map to either an
   // explicit language as a string value, or a numeric index into the captured
   // groups as an integer.
   'syntax.filemap' => array(
     // Example: Treat all '*.xyz' files as PHP.
     // '@\\.xyz$@' => 'php',
 
     // Example: Treat 'httpd.conf' as 'apacheconf'.
     // '@/httpd\\.conf$@' => 'apacheconf',
 
     // Example: Treat all '*.x.bak' file as '.x'. NOTE: we map to capturing
     // group 1 by specifying the mapping as "1".
     // '@\\.([^.]+)\\.bak$@' => 1,
   ),
 
 );
diff --git a/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php b/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php
index 1d1c24c7b..296807901 100644
--- a/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php
+++ b/src/applications/conduit/method/differential/getdiff/ConduitAPI_differential_getdiff_Method.php
@@ -1,131 +1,78 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * @group conduit
  */
 class ConduitAPI_differential_getdiff_Method extends ConduitAPIMethod {
 
   public function getMethodDescription() {
     return "Load the content of a diff from Differential.";
   }
 
   public function defineParamTypes() {
     return array(
       'revision_id' => 'optional id',
       'diff_id'     => 'optional id',
     );
   }
 
   public function defineReturnType() {
     return 'nonempty dict';
   }
 
   public function defineErrorTypes() {
     return array(
       'ERR_BAD_REVISION'    => 'No such revision exists.',
       'ERR_BAD_DIFF'        => 'No such diff exists.',
     );
   }
 
   protected function execute(ConduitAPIRequest $request) {
     $diff = null;
 
     $revision_id = $request->getValue('revision_id');
     if ($revision_id) {
       $revision = id(new DifferentialRevision())->load($revision_id);
       if (!$revision) {
         throw new ConduitException('ERR_BAD_REVISION');
       }
       $diff = id(new DifferentialDiff())->loadOneWhere(
         'revisionID = %d ORDER BY id DESC LIMIT 1',
         $revision->getID());
     } else {
       $diff_id = $request->getValue('diff_id');
       if ($diff_id) {
         $diff = id(new DifferentialDiff())->load($diff_id);
       }
     }
 
     if (!$diff) {
       throw new ConduitException('ERR_BAD_DIFF');
     }
 
     $diff->attachChangesets($diff->loadChangesets());
     // TODO: We could batch this to improve performance.
     foreach ($diff->getChangesets() as $changeset) {
       $changeset->attachHunks($changeset->loadHunks());
     }
 
-    return $this->createDiffDict($diff);
-  }
-
-  public static function createDiffDict(DifferentialDiff $diff) {
-    $dict = array(
-      'id' => $diff->getID(),
-      'parent' => $diff->getParentRevisionID(),
-      'revisionID' => $diff->getRevisionID(),
-      'sourceControlBaseRevision' => $diff->getSourceControlBaseRevision(),
-      'sourceControlPath' => $diff->getSourceControlPath(),
-      'unitStatus' => $diff->getUnitStatus(),
-      'lintStatus' => $diff->getLintStatus(),
-      'changes' => array(),
-      'properties' => array(),
-    );
-
-    foreach ($diff->getChangesets() as $changeset) {
-      $hunks = array();
-      foreach ($changeset->getHunks() as $hunk) {
-        $hunks[] = array(
-          'oldOffset' => $hunk->getOldOffset(),
-          'newOffset' => $hunk->getNewOffset(),
-          'oldLength' => $hunk->getOldLen(),
-          'newLength' => $hunk->getNewLen(),
-          'addLines'  => null,
-          'delLines'  => null,
-          'isMissingOldNewline' => null,
-          'isMissingNewNewline' => null,
-          'corpus'    => $hunk->getChanges(),
-        );
-      }
-      $change = array(
-        'metadata'      => $changeset->getMetadata(),
-        'oldPath'       => $changeset->getOldFile(),
-        'currentPath'   => $changeset->getFileName(),
-        'awayPaths'     => $changeset->getAwayPaths(),
-        'oldProperties' => $changeset->getOldProperties(),
-        'newProperties' => $changeset->getNewProperties(),
-        'type'          => $changeset->getChangeType(),
-        'fileType'      => $changeset->getFileType(),
-        'commitHash'    => null,
-        'hunks'         => $hunks,
-      );
-      $dict['changes'][] = $change;
-    }
-
-    $properties = id(new DifferentialDiffProperty())->loadAllWhere(
-      'diffID = %d',
-      $diff->getID());
-    foreach ($properties as $property) {
-      $dict['properties'][$property->getName()] = $property->getData();
-    }
-
-    return $dict;
+    return $diff->getDiffDict();
   }
 
 }
diff --git a/src/applications/conduit/method/differential/getdiff/__init__.php b/src/applications/conduit/method/differential/getdiff/__init__.php
index 5ad4547ee..15399190d 100644
--- a/src/applications/conduit/method/differential/getdiff/__init__.php
+++ b/src/applications/conduit/method/differential/getdiff/__init__.php
@@ -1,18 +1,17 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'applications/conduit/method/base');
 phutil_require_module('phabricator', 'applications/conduit/protocol/exception');
 phutil_require_module('phabricator', 'applications/differential/storage/diff');
-phutil_require_module('phabricator', 'applications/differential/storage/diffproperty');
 phutil_require_module('phabricator', 'applications/differential/storage/revision');
 
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('ConduitAPI_differential_getdiff_Method.php');
diff --git a/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php b/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php
index 30ccd480a..04ef4014f 100644
--- a/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php
+++ b/src/applications/conduit/method/differential/getrevision/ConduitAPI_differential_getrevision_Method.php
@@ -1,120 +1,119 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * @group conduit
  */
 class ConduitAPI_differential_getrevision_Method extends ConduitAPIMethod {
 
   public function getMethodDescription() {
     return "Load the content of a revision from Differential.";
   }
 
   public function defineParamTypes() {
     return array(
       'revision_id' => 'required id',
     );
   }
 
   public function defineReturnType() {
     return 'nonempty dict';
   }
 
   public function defineErrorTypes() {
     return array(
       'ERR_BAD_REVISION'    => 'No such revision exists.',
     );
   }
 
   protected function execute(ConduitAPIRequest $request) {
     $diff = null;
 
     $revision_id = $request->getValue('revision_id');
     $revision = id(new DifferentialRevision())->load($revision_id);
     if (!$revision) {
       throw new ConduitException('ERR_BAD_REVISION');
     }
 
     $revision->loadRelationships();
     $reviewer_phids = array_values($revision->getReviewers());
 
     $diffs = $revision->loadDiffs();
 
     $diff_dicts = array();
     foreach ($diffs as $diff) {
       $diff->attachChangesets($diff->loadChangesets());
       // TODO: We could batch this to improve performance.
       foreach ($diff->getChangesets() as $changeset) {
         $changeset->attachHunks($changeset->loadHunks());
       }
-      $diff_dicts[] =
-        ConduitAPI_differential_getdiff_Method::createDiffDict($diff);
+      $diff_dicts[] = $diff->getDiffDict();
     }
 
     $commit_dicts = array();
     $commit_phids = $revision->loadCommitPHIDs();
     $handles = id(new PhabricatorObjectHandleData($commit_phids))
       ->loadHandles();
 
     foreach ($commit_phids as $commit_phid) {
       $commit_dicts[] = array(
         'fullname'      => $handles[$commit_phid]->getFullName(),
         'dateCommitted' => $handles[$commit_phid]->getTimestamp(),
       );
     }
 
     $auxiliary_fields = $this->loadAuxiliaryFields($revision);
 
     $dict = array(
       'id' => $revision->getID(),
       'phid' => $revision->getPHID(),
       'authorPHID' => $revision->getAuthorPHID(),
       'uri' => PhabricatorEnv::getURI('/D'.$revision->getID()),
       'title' => $revision->getTitle(),
       'status' => $revision->getStatus(),
       'statusName'  => DifferentialRevisionStatus::getNameForRevisionStatus(
         $revision->getStatus()),
       'summary' => $revision->getSummary(),
       'testPlan' => $revision->getTestPlan(),
       'lineCount' => $revision->getLineCount(),
       'reviewerPHIDs' => $reviewer_phids,
       'diffs' => $diff_dicts,
       'commits' => $commit_dicts,
       'auxiliary' => $auxiliary_fields,
     );
 
     return $dict;
   }
 
   private function loadAuxiliaryFields(DifferentialRevision $revision) {
     $aux_fields = DifferentialFieldSelector::newSelector()
       ->getFieldSpecifications();
     foreach ($aux_fields as $key => $aux_field) {
       if (!$aux_field->shouldAppearOnConduitView()) {
         unset($aux_fields[$key]);
       }
     }
 
     $aux_fields = DifferentialAuxiliaryField::loadFromStorage(
       $revision,
       $aux_fields);
 
     return mpull($aux_fields, 'getValueForConduit', 'getKeyForConduit');
   }
 
 }
diff --git a/src/applications/conduit/method/differential/getrevision/__init__.php b/src/applications/conduit/method/differential/getrevision/__init__.php
index 66949aaa7..7865804c4 100644
--- a/src/applications/conduit/method/differential/getrevision/__init__.php
+++ b/src/applications/conduit/method/differential/getrevision/__init__.php
@@ -1,22 +1,21 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'applications/conduit/method/base');
-phutil_require_module('phabricator', 'applications/conduit/method/differential/getdiff');
 phutil_require_module('phabricator', 'applications/conduit/protocol/exception');
 phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
 phutil_require_module('phabricator', 'applications/differential/field/selector/base');
 phutil_require_module('phabricator', 'applications/differential/storage/auxiliaryfield');
 phutil_require_module('phabricator', 'applications/differential/storage/revision');
 phutil_require_module('phabricator', 'applications/phid/handle/data');
 phutil_require_module('phabricator', 'infrastructure/env');
 
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('ConduitAPI_differential_getrevision_Method.php');
diff --git a/src/applications/differential/mail/base/DifferentialMail.php b/src/applications/differential/mail/base/DifferentialMail.php
index 3b0b67c91..ea898bed4 100644
--- a/src/applications/differential/mail/base/DifferentialMail.php
+++ b/src/applications/differential/mail/base/DifferentialMail.php
@@ -1,330 +1,354 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 abstract class DifferentialMail {
 
   protected $to = array();
   protected $cc = array();
 
   protected $actorHandle;
 
   protected $revision;
   protected $comment;
   protected $changesets;
   protected $inlineComments;
   protected $isFirstMailAboutRevision;
   protected $isFirstMailToRecipients;
   protected $heraldTranscriptURI;
   protected $heraldRulesHeader;
   protected $replyHandler;
   protected $parentMessageID;
 
   abstract protected function renderSubject();
   abstract protected function renderBody();
 
   public function setActorHandle($actor_handle) {
     $this->actorHandle = $actor_handle;
     return $this;
   }
 
   public function getActorHandle() {
     return $this->actorHandle;
   }
 
   protected function getActorName() {
     $handle = $this->getActorHandle();
     if ($handle) {
       return $handle->getName();
     }
     return '???';
   }
 
   public function setParentMessageID($parent_message_id) {
     $this->parentMessageID = $parent_message_id;
     return $this;
   }
 
   public function setXHeraldRulesHeader($header) {
     $this->heraldRulesHeader = $header;
     return $this;
   }
 
   public function send() {
     $to_phids = $this->getToPHIDs();
     if (!$to_phids) {
       throw new Exception('No "To:" users provided!');
     }
 
-    $cc_phids = $this->getCCPHIDs();
-    $subject  = $this->buildSubject();
-    $body     = $this->buildBody();
+    $cc_phids    = $this->getCCPHIDs();
+    $subject     = $this->buildSubject();
+    $body        = $this->buildBody();
+    $attachments = $this->buildAttachments();
 
     $template = new PhabricatorMetaMTAMail();
     $actor_handle = $this->getActorHandle();
     $reply_handler = $this->getReplyHandler();
 
     if ($actor_handle) {
       $template->setFrom($actor_handle->getPHID());
     }
 
     $template
       ->setSubject($subject)
       ->setBody($body)
       ->setIsHTML($this->shouldMarkMailAsHTML())
       ->setParentMessageID($this->parentMessageID)
       ->addHeader('Thread-Topic', $this->getRevision()->getTitle());
 
+    foreach ($attachments as $attachment) {
+      $template->addAttachment(
+        $attachment['data'],
+        $attachment['filename'],
+        $attachment['mimetype']
+      );
+    }
+
     $template->setThreadID(
       $this->getThreadID(),
       $this->isFirstMailAboutRevision());
 
     if ($this->heraldRulesHeader) {
       $template->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
     }
 
     $template->setRelatedPHID($this->getRevision()->getPHID());
 
     $phids = array();
     foreach ($to_phids as $phid) {
       $phids[$phid] = true;
     }
     foreach ($cc_phids as $phid) {
       $phids[$phid] = true;
     }
     $phids = array_keys($phids);
 
     $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
 
     $mails = $reply_handler->multiplexMail(
       $template,
       array_select_keys($handles, $to_phids),
       array_select_keys($handles, $cc_phids));
 
     foreach ($mails as $mail) {
       $mail->saveAndSend();
     }
   }
 
   protected function getSubjectPrefix() {
     return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
   }
 
   protected function buildSubject() {
     return trim($this->getSubjectPrefix().' '.$this->renderSubject());
   }
 
   protected function shouldMarkMailAsHTML() {
     return false;
   }
 
   protected function buildBody() {
 
     $body = $this->renderBody();
 
     $reply_handler = $this->getReplyHandler();
     $reply_instructions = $reply_handler->getReplyHandlerInstructions();
     if ($reply_instructions) {
       $body .=
         "\nREPLY HANDLER ACTIONS\n".
         "  {$reply_instructions}\n";
     }
 
     if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) {
       $manage_uri = PhabricatorEnv::getProductionURI(
         '/herald/view/differential/');
 
       $xscript_uri = $this->getHeraldTranscriptURI();
       $body .= <<<EOTEXT
 
 MANAGE HERALD DIFFERENTIAL RULES
   {$manage_uri}
 
 WHY DID I GET THIS EMAIL?
   {$xscript_uri}
 
 Tip: use the X-Herald-Rules header to filter Herald messages in your client.
 
 EOTEXT;
     }
 
     return $body;
   }
 
+  /**
+   * You can override this method in a subclass and return array of attachments
+   * to be sent with the email.  Each attachment is a dictionary with 'data',
+   * 'filename' and 'mimetype' keys.  For example:
+   *
+   *   array(
+   *     'data' => 'some text',
+   *     'filename' => 'example.txt',
+   *     'mimetype' => 'text/plain'
+   *   );
+   */
+  protected function buildAttachments() {
+    return array();
+  }
+
   public function getReplyHandler() {
     if ($this->replyHandler) {
       return $this->replyHandler;
     }
 
     $handler_class = PhabricatorEnv::getEnvConfig(
       'metamta.differential.reply-handler');
 
     $reply_handler = self::newReplyHandlerForRevision($this->getRevision());
 
     $this->replyHandler = $reply_handler;
 
     return $this->replyHandler;
   }
 
   public static function newReplyHandlerForRevision(
     DifferentialRevision $revision) {
 
     $handler_class = PhabricatorEnv::getEnvConfig(
       'metamta.differential.reply-handler');
 
     $reply_handler = newv($handler_class, array());
     $reply_handler->setMailReceiver($revision);
 
     return $reply_handler;
   }
 
 
   protected function formatText($text) {
     $text = explode("\n", $text);
     foreach ($text as &$line) {
       $line = rtrim('  '.$line);
     }
     unset($line);
     return implode("\n", $text);
   }
 
   public function setToPHIDs(array $to) {
     $this->to = $this->filterContactPHIDs($to);
     return $this;
   }
 
   public function setCCPHIDs(array $cc) {
     $this->cc = $this->filterContactPHIDs($cc);
     return $this;
   }
 
   protected function filterContactPHIDs(array $phids) {
     return $phids;
 
     // TODO: actually do this?
 
     // Differential revisions use Subscriptions for CCs, so any arbitrary
     // PHID can end up CC'd to them. Only try to actually send email PHIDs
     // which have ToolsHandle types that are marked emailable. If we don't
     // filter here, sending the email will fail.
 /*
     $handles = array();
     prep(new ToolsHandleData($phids, $handles));
     foreach ($handles as $phid => $handle) {
       if (!$handle->isEmailable()) {
         unset($handles[$phid]);
       }
     }
     return array_keys($handles);
 */
   }
 
   protected function getToPHIDs() {
     return $this->to;
   }
 
   protected function getCCPHIDs() {
     return $this->cc;
   }
 
   public function setRevision($revision) {
     $this->revision = $revision;
     return $this;
   }
 
   public function getRevision() {
     return $this->revision;
   }
 
   protected function getThreadID() {
     $phid = $this->getRevision()->getPHID();
     $domain = PhabricatorEnv::getEnvConfig('metamta.domain');
     return "<differential-rev-{$phid}-req@{$domain}>";
   }
 
   public function setComment($comment) {
     $this->comment = $comment;
     return $this;
   }
 
   public function getComment() {
     return $this->comment;
   }
 
   public function setChangesets($changesets) {
     $this->changesets = $changesets;
     return $this;
   }
 
   public function getChangesets() {
     return $this->changesets;
   }
 
   public function setInlineComments(array $inline_comments) {
     $this->inlineComments = $inline_comments;
     return $this;
   }
 
   public function getInlineComments() {
     return $this->inlineComments;
   }
 
   public function renderRevisionDetailLink() {
     $uri = $this->getRevisionURI();
     return "REVISION DETAIL\n  {$uri}";
   }
 
   public function getRevisionURI() {
     return PhabricatorEnv::getProductionURI('/D'.$this->getRevision()->getID());
   }
 
   public function setIsFirstMailToRecipients($first) {
     $this->isFirstMailToRecipients = $first;
     return $this;
   }
 
   public function isFirstMailToRecipients() {
     return $this->isFirstMailToRecipients;
   }
 
   public function setIsFirstMailAboutRevision($first) {
     $this->isFirstMailAboutRevision = $first;
     return $this;
   }
 
   public function isFirstMailAboutRevision() {
     return $this->isFirstMailAboutRevision;
   }
 
   public function setHeraldTranscriptURI($herald_transcript_uri) {
     $this->heraldTranscriptURI = $herald_transcript_uri;
     return $this;
   }
 
   public function getHeraldTranscriptURI() {
     return $this->heraldTranscriptURI;
   }
 
   protected function renderHandleList(array $handles, array $phids) {
     $names = array();
     foreach ($phids as $phid) {
       $names[] = $handles[$phid]->getName();
     }
     return implode(', ', $names);
   }
 
 }
diff --git a/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
index 95ad604a5..f08d6f3a8 100644
--- a/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
+++ b/src/applications/differential/mail/reviewrequest/DifferentialReviewRequestMail.php
@@ -1,80 +1,115 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 abstract class DifferentialReviewRequestMail extends DifferentialMail {
 
   protected $comments;
 
   public function setComments($comments) {
     $this->comments = $comments;
     return $this;
   }
 
   public function getComments() {
     return $this->comments;
   }
 
   public function __construct(
     DifferentialRevision $revision,
     PhabricatorObjectHandle $actor,
     array $changesets) {
 
     $this->setRevision($revision);
     $this->setActorHandle($actor);
     $this->setChangesets($changesets);
   }
 
   protected function renderReviewersLine() {
     $reviewers = $this->getRevision()->getReviewers();
     $handles = id(new PhabricatorObjectHandleData($reviewers))->loadHandles();
     return 'Reviewers: '.$this->renderHandleList($handles, $reviewers);
   }
 
   protected function renderReviewRequestBody() {
     $revision = $this->getRevision();
 
     $body = array();
     if ($this->isFirstMailToRecipients()) {
       $body[] = $this->formatText($revision->getSummary());
       $body[] = null;
 
       $body[] = 'TEST PLAN';
       $body[] = $this->formatText($revision->getTestPlan());
       $body[] = null;
     } else {
       if (strlen($this->getComments())) {
         $body[] = $this->formatText($this->getComments());
         $body[] = null;
       }
     }
 
     $body[] = $this->renderRevisionDetailLink();
     $body[] = null;
 
     $changesets = $this->getChangesets();
     if ($changesets) {
       $body[] = 'AFFECTED FILES';
       foreach ($changesets as $changeset) {
         $body[] = '  '.$changeset->getFilename();
       }
       $body[] = null;
     }
 
     return implode("\n", $body);
   }
+
+  protected function buildAttachments() {
+    $attachments = array();
+    if (PhabricatorEnv::getEnvConfig('metamta.differential.attach-patches')) {
+      $revision = $this->getRevision();
+      $revision_id = $revision->getID();
+
+      $diffs = $revision->loadDiffs();
+      $diff_number = count($diffs);
+      $diff = array_pop($diffs);
+
+      $filename = "D{$revision_id}.{$diff_number}.diff";
+
+      $diff->attachChangesets($diff->loadChangesets());
+      // TODO: We could batch this to improve performance.
+      foreach ($diff->getChangesets() as $changeset) {
+        $changeset->attachHunks($changeset->loadHunks());
+      }
+      $diff_dict = $diff->getDiffDict();
+
+      $changes = array();
+      foreach ($diff_dict['changes'] as $changedict) {
+        $changes[] = ArcanistDiffChange::newFromDictionary($changedict);
+      }
+      $bundle = ArcanistBundle::newFromChanges($changes);
+      $unified_diff = $bundle->toUnifiedDiff();
+
+      $attachments[] = array(
+        'data' => $unified_diff,
+        'filename' => $filename,
+        'mimetype' => 'text/x-diff; charset=utf-8'
+      );
+    }
+    return $attachments;
+  }
 }
diff --git a/src/applications/differential/mail/reviewrequest/__init__.php b/src/applications/differential/mail/reviewrequest/__init__.php
index 2189b1648..e47543ab0 100644
--- a/src/applications/differential/mail/reviewrequest/__init__.php
+++ b/src/applications/differential/mail/reviewrequest/__init__.php
@@ -1,15 +1,19 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
+phutil_require_module('arcanist', 'parser/bundle');
+phutil_require_module('arcanist', 'parser/diff/change');
+
 phutil_require_module('phabricator', 'applications/differential/mail/base');
 phutil_require_module('phabricator', 'applications/phid/handle/data');
+phutil_require_module('phabricator', 'infrastructure/env');
 
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('DifferentialReviewRequestMail.php');
diff --git a/src/applications/differential/storage/diff/DifferentialDiff.php b/src/applications/differential/storage/diff/DifferentialDiff.php
index 18c0e2e43..310ca810e 100644
--- a/src/applications/differential/storage/diff/DifferentialDiff.php
+++ b/src/applications/differential/storage/diff/DifferentialDiff.php
@@ -1,149 +1,201 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DifferentialDiff extends DifferentialDAO {
 
   protected $revisionID;
   protected $authorPHID;
 
   protected $sourceMachine;
   protected $sourcePath;
 
   protected $sourceControlSystem;
   protected $sourceControlBaseRevision;
   protected $sourceControlPath;
 
   protected $lintStatus;
   protected $unitStatus;
 
   protected $lineCount;
 
   protected $branch;
 
   protected $parentRevisionID;
   protected $arcanistProjectPHID;
   protected $creationMethod;
   protected $repositoryUUID;
 
   protected $description;
 
   private $unsavedChangesets = array();
   private $changesets;
 
   public function addUnsavedChangeset(DifferentialChangeset $changeset) {
     if ($this->changesets === null) {
       $this->changesets = array();
     }
     $this->unsavedChangesets[] = $changeset;
     $this->changesets[] = $changeset;
     return $this;
   }
 
   public function attachChangesets(array $changesets) {
     $this->changesets = $changesets;
     return $this;
   }
 
   public function getChangesets() {
     if ($this->changesets === null) {
       throw new Exception("Must load and attach changesets first!");
     }
     return $this->changesets;
   }
 
   public function loadChangesets() {
     if (!$this->getID()) {
       return array();
     }
     return id(new DifferentialChangeset())->loadAllWhere(
       'diffID = %d',
       $this->getID());
   }
 
   public function loadArcanistProject() {
     if (!$this->getArcanistProjectPHID()) {
       return null;
     }
     return id(new PhabricatorRepositoryArcanistProject())->loadOneWhere(
       'phid = %s',
       $this->getArcanistProjectPHID());
   }
 
   public function save() {
 // TODO: sort out transactions
 //    $this->openTransaction();
       $ret = parent::save();
       foreach ($this->unsavedChangesets as $changeset) {
         $changeset->setDiffID($this->getID());
         $changeset->save();
       }
 //    $this->saveTransaction();
     return $ret;
   }
 
   public function delete() {
 //    $this->openTransaction();
       foreach ($this->loadChangesets() as $changeset) {
         $changeset->delete();
       }
       $ret = parent::delete();
 //    $this->saveTransaction();
     return $ret;
   }
 
   public static function newFromRawChanges(array $changes) {
     $diff = new DifferentialDiff();
 
     $lines = 0;
     foreach ($changes as $change) {
       $changeset = new DifferentialChangeset();
       $add_lines = 0;
       $del_lines = 0;
       foreach ($change->getHunks() as $hunk) {
         $dhunk = new DifferentialHunk();
         $dhunk->setOldOffset($hunk->getOldOffset());
         $dhunk->setOldLen($hunk->getOldLength());
         $dhunk->setNewOffset($hunk->getNewOffset());
         $dhunk->setNewLen($hunk->getNewLength());
         $dhunk->setChanges($hunk->getCorpus());
         $changeset->addUnsavedHunk($dhunk);
         $add_lines += $hunk->getAddLines();
         $del_lines += $hunk->getDelLines();
         $lines += $add_lines + $del_lines;
       }
 
       $changeset->setOldFile($change->getOldPath());
       $changeset->setFilename($change->getCurrentPath());
       $changeset->setChangeType($change->getType());
 
       $changeset->setFileType($change->getFileType());
       $changeset->setMetadata($change->getAllMetadata());
       $changeset->setOldProperties($change->getOldProperties());
       $changeset->setNewProperties($change->getNewProperties());
       $changeset->setAwayPaths($change->getAwayPaths());
       $changeset->setAddLines($add_lines);
       $changeset->setDelLines($del_lines);
 
       $diff->addUnsavedChangeset($changeset);
     }
     $diff->setLineCount($lines);
 
     return $diff;
   }
 
+  public function getDiffDict() {
+    $dict = array(
+      'id' => $this->getID(),
+      'parent' => $this->getParentRevisionID(),
+      'revisionID' => $this->getRevisionID(),
+      'sourceControlBaseRevision' => $this->getSourceControlBaseRevision(),
+      'sourceControlPath' => $this->getSourceControlPath(),
+      'unitStatus' => $this->getUnitStatus(),
+      'lintStatus' => $this->getLintStatus(),
+      'changes' => array(),
+      'properties' => array(),
+    );
+
+    foreach ($this->getChangesets() as $changeset) {
+      $hunks = array();
+      foreach ($changeset->getHunks() as $hunk) {
+        $hunks[] = array(
+          'oldOffset' => $hunk->getOldOffset(),
+          'newOffset' => $hunk->getNewOffset(),
+          'oldLength' => $hunk->getOldLen(),
+          'newLength' => $hunk->getNewLen(),
+          'addLines'  => null,
+          'delLines'  => null,
+          'isMissingOldNewline' => null,
+          'isMissingNewNewline' => null,
+          'corpus'    => $hunk->getChanges(),
+        );
+      }
+      $change = array(
+        'metadata'      => $changeset->getMetadata(),
+        'oldPath'       => $changeset->getOldFile(),
+        'currentPath'   => $changeset->getFileName(),
+        'awayPaths'     => $changeset->getAwayPaths(),
+        'oldProperties' => $changeset->getOldProperties(),
+        'newProperties' => $changeset->getNewProperties(),
+        'type'          => $changeset->getChangeType(),
+        'fileType'      => $changeset->getFileType(),
+        'commitHash'    => null,
+        'hunks'         => $hunks,
+      );
+      $dict['changes'][] = $change;
+    }
+
+    $properties = id(new DifferentialDiffProperty())->loadAllWhere(
+      'diffID = %d',
+      $this->getID());
+    foreach ($properties as $property) {
+      $dict['properties'][$property->getName()] = $property->getData();
+    }
+
+    return $dict;
+  }
 }
diff --git a/src/applications/differential/storage/diff/__init__.php b/src/applications/differential/storage/diff/__init__.php
index 719264253..cdd9d21fe 100644
--- a/src/applications/differential/storage/diff/__init__.php
+++ b/src/applications/differential/storage/diff/__init__.php
@@ -1,17 +1,18 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'applications/differential/storage/base');
 phutil_require_module('phabricator', 'applications/differential/storage/changeset');
+phutil_require_module('phabricator', 'applications/differential/storage/diffproperty');
 phutil_require_module('phabricator', 'applications/differential/storage/hunk');
 phutil_require_module('phabricator', 'applications/repository/storage/arcanistproject');
 
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('DifferentialDiff.php');