diff --git a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php
index b5ec522fe7..98bb0f630b 100644
--- a/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php
+++ b/src/applications/differential/field/selector/DifferentialDefaultFieldSelector.php
@@ -1,92 +1,113 @@
 <?php
 
 /*
  * Copyright 2012 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.
  */
 
 final class DifferentialDefaultFieldSelector
   extends DifferentialFieldSelector {
 
   public function getFieldSpecifications() {
     $fields = array(
       new DifferentialTitleFieldSpecification(),
       new DifferentialSummaryFieldSpecification(),
     );
 
     if (PhabricatorEnv::getEnvConfig('differential.show-test-plan-field')) {
       $fields[] = new DifferentialTestPlanFieldSpecification();
     }
 
     $fields = array_merge(
       $fields,
       array(
         new DifferentialRevisionStatusFieldSpecification(),
         new DifferentialAuthorFieldSpecification(),
         new DifferentialReviewersFieldSpecification(),
         new DifferentialReviewedByFieldSpecification(),
         new DifferentialCCsFieldSpecification(),
         new DifferentialUnitFieldSpecification(),
         new DifferentialLintFieldSpecification(),
         new DifferentialCommitsFieldSpecification(),
         new DifferentialDependenciesFieldSpecification(),
         new DifferentialManiphestTasksFieldSpecification(),
       ));
 
     if (PhabricatorEnv::getEnvConfig('differential.show-host-field')) {
       $fields[] = new DifferentialHostFieldSpecification();
       $fields[] = new DifferentialPathFieldSpecification();
     }
 
     $fields = array_merge(
       $fields,
       array(
         new DifferentialBranchFieldSpecification(),
         new DifferentialArcanistProjectFieldSpecification(),
         new DifferentialApplyPatchFieldSpecification(),
         new DifferentialRevisionIDFieldSpecification(),
         new DifferentialGitSVNIDFieldSpecification(),
         new DifferentialDateModifiedFieldSpecification(),
         new DifferentialDateCreatedFieldSpecification(),
         new DifferentialAuditorsFieldSpecification(),
       ));
 
     return $fields;
   }
 
   public function sortFieldsForRevisionList(array $fields) {
     assert_instances_of($fields, 'DifferentialFieldSpecification');
 
     $map = array();
     foreach ($fields as $field) {
       $map[get_class($field)] = $field;
     }
 
     $map = array_select_keys(
       $map,
       array(
         'DifferentialRevisionIDFieldSpecification',
         'DifferentialTitleFieldSpecification',
         'DifferentialRevisionStatusFieldSpecification',
         'DifferentialAuthorFieldSpecification',
         'DifferentialReviewersFieldSpecification',
         'DifferentialDateModifiedFieldSpecification',
         'DifferentialDateCreatedFieldSpecification',
       )) + $map;
 
     return array_values($map);
   }
 
+  public function sortFieldsForMail(array $fields) {
+    assert_instances_of($fields, 'DifferentialFieldSpecification');
+
+    $map = array();
+    foreach ($fields as $field) {
+      $map[get_class($field)] = $field;
+    }
+
+    $map = array_select_keys(
+      $map,
+      array(
+        'DifferentialSummaryFieldSpecification',
+        'DifferentialTestPlanFieldSpecification',
+        'DifferentialRevisionIDFieldSpecification',
+        'DifferentialBranchFieldSpecification',
+        'DifferentialCommitsFieldSpecification',
+      )) + $map;
+
+    return array_values($map);
+  }
+
 }
 
diff --git a/src/applications/differential/field/selector/DifferentialFieldSelector.php b/src/applications/differential/field/selector/DifferentialFieldSelector.php
index d4070e5564..523b5e829e 100644
--- a/src/applications/differential/field/selector/DifferentialFieldSelector.php
+++ b/src/applications/differential/field/selector/DifferentialFieldSelector.php
@@ -1,36 +1,41 @@
 <?php
 
 /*
  * Copyright 2012 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 DifferentialFieldSelector {
 
   final public function __construct() {
     // <empty>
   }
 
   final public static function newSelector() {
     return PhabricatorEnv::newObjectFromConfig('differential.field-selector');
   }
 
   abstract public function getFieldSpecifications();
 
   public function sortFieldsForRevisionList(array $fields) {
     assert_instances_of($fields, 'DifferentialFieldSpecification');
     return $fields;
   }
 
+  public function sortFieldsForMail(array $fields) {
+    assert_instances_of($fields, 'DifferentialFieldSpecification');
+    return $fields;
+  }
+
 }
diff --git a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
index ec8ceabc93..af1cbe89ca 100644
--- a/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialBranchFieldSpecification.php
@@ -1,41 +1,58 @@
 <?php
 
 /*
  * Copyright 2012 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.
  */
 
 final class DifferentialBranchFieldSpecification
   extends DifferentialFieldSpecification {
 
   public function shouldAppearOnRevisionView() {
     return true;
   }
 
   public function renderLabelForRevisionView() {
     return 'Branch:';
   }
 
   public function renderValueForRevisionView() {
     $diff = $this->getDiff();
 
     $branch = $diff->getBranch();
     if ($branch == '') {
       return null;
     }
 
     return phutil_escape_html($branch);
   }
 
+  public function renderValueForMail() {
+    $status = $this->getRevision()->getStatus();
+
+    if ($status != ArcanistDifferentialRevisionStatus::NEEDS_REVISION &&
+        $status != ArcanistDifferentialRevisionStatus::ACCEPTED) {
+      return null;
+    }
+
+    $diff = $this->getRevision()->loadActiveDiff();
+    if ($diff) {
+      $branch = $diff->getBranch();
+      if ($branch) {
+        return "BRANCH\n  $branch";
+      }
+    }
+  }
+
 }
diff --git a/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php b/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php
index 245bf6049c..41958397cf 100644
--- a/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialCommitsFieldSpecification.php
@@ -1,53 +1,83 @@
 <?php
 
 /*
- * Copyright 2011 Facebook, Inc.
+ * Copyright 2012 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.
  */
 
 final class DifferentialCommitsFieldSpecification
   extends DifferentialFieldSpecification {
 
   public function shouldAppearOnRevisionView() {
     return true;
   }
 
   public function getRequiredHandlePHIDsForRevisionView() {
     return $this->getCommitPHIDs();
   }
 
   public function renderLabelForRevisionView() {
     return 'Commits:';
   }
 
   public function renderValueForRevisionView() {
     $commit_phids = $this->getCommitPHIDs();
     if (!$commit_phids) {
       return null;
     }
 
     $links = array();
     foreach ($commit_phids as $commit_phid) {
       $links[] = $this->getHandle($commit_phid)->renderLink();
     }
 
     return implode('<br />', $links);
   }
 
   private function getCommitPHIDs() {
     $revision = $this->getRevision();
     return $revision->getCommitPHIDs();
   }
 
+  public function renderValueForMail() {
+    $revision = $this->getRevision();
+
+    if ($revision->getStatus() != ArcanistDifferentialRevisionStatus::CLOSED) {
+      return null;
+    }
+
+    $phids = $revision->loadCommitPHIDs();
+    if (!$phids) {
+      return null;
+    }
+
+    $body = array();
+    $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
+    if (count($handles) == 1) {
+      $body[] = "COMMIT";
+    } else {
+      // This is unlikely to ever happen since we'll send this mail the
+      // first time we discover a commit, but it's not impossible if data
+      // was migrated, etc.
+      $body[] = "COMMITS";
+    }
+
+    foreach ($handles as $handle) {
+      $body[] = '  '.PhabricatorEnv::getProductionURI($handle->getURI());
+    }
+
+    return implode("\n", $body);
+  }
+
 }
diff --git a/src/applications/differential/field/specification/DifferentialFieldSpecification.php b/src/applications/differential/field/specification/DifferentialFieldSpecification.php
index 334eab01e4..1edd7fc27f 100644
--- a/src/applications/differential/field/specification/DifferentialFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialFieldSpecification.php
@@ -1,892 +1,909 @@
 <?php
 
 /*
  * Copyright 2012 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.
  */
 
 /**
  * Describes and implements the behavior for a custom field on Differential
  * revisions. Along with other configuration, you can extend this class to add
  * custom fields to Differential revisions and commit messages.
  *
  * Generally, you should implement all methods from the storage task and then
  * the methods from one or more interface tasks.
  *
  * @task storage Field Storage
  * @task edit Extending the Revision Edit Interface
  * @task view Extending the Revision View Interface
  * @task list Extending the Revision List Interface
+ * @task mail Extending the E-mail Interface
  * @task conduit Extending the Conduit View Interface
  * @task commit Extending Commit Messages
  * @task load Loading Additional Data
  * @task context Contextual Data
  */
 abstract class DifferentialFieldSpecification {
 
   private $revision;
   private $diff;
   private $handles;
   private $diffProperties;
   private $user;
 
 
 /* -(  Storage  )------------------------------------------------------------ */
 
 
   /**
    * Return a unique string used to key storage of this field's value, like
    * "mycompany.fieldname" or similar. You can return null (the default) to
    * indicate that this field does not use any storage. This is appropriate for
    * display fields, like @{class:DifferentialLinesFieldSpecification}. If you
    * implement this, you must also implement @{method:getValueForStorage} and
    * @{method:setValueFromStorage}.
    *
    * @return string|null  Unique key which identifies this field in auxiliary
    *                      field storage. Maximum length is 32. Alternatively,
    *                      null (default) to indicate that this field does not
    *                      use auxiliary field storage.
    * @task storage
    */
   public function getStorageKey() {
     return null;
   }
 
 
   /**
    * Return a serialized representation of the field value, appropriate for
    * storing in auxiliary field storage. You must implement this method if
    * you implement @{method:getStorageKey}.
    *
    * @return string Serialized field value.
    * @task storage
    */
   public function getValueForStorage() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Set the field's value given a serialized storage value. This is called
    * when the field is loaded; if no data is available, the value will be
    * null. You must implement this method if you implement
    * @{method:getStorageKey}.
    *
    * @param string|null Serialized field representation (from
    *                    @{method:getValueForStorage}) or null if no value has
    *                    ever been stored.
    * @return this
    * @task storage
    */
   public function setValueFromStorage($value) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
 /* -(  Extending the Revision Edit Interface  )------------------------------ */
 
 
   /**
    * Determine if this field should appear on the "Edit Revision" interface. If
    * you return true from this method, you must implement
    * @{method:setValueFromRequest}, @{method:renderEditControl} and
    * @{method:validateField}.
    *
    * For a concrete example of a field which implements an edit interface, see
    * @{class:DifferentialRevertPlanFieldSpecification}.
    *
    * @return bool True to indicate that this field implements an edit interface.
    * @task edit
    */
   public function shouldAppearOnEdit() {
     return false;
   }
 
 
   /**
    * Set the field's value from an HTTP request. Generally, you should read
    * the value of some field name you emitted in @{method:renderEditControl}
    * and save it into the object, e.g.:
    *
    *   $this->value = $request->getStr('my-custom-field');
    *
    * If you have some particularly complicated field, you may need to read
    * more data; this is why you have access to the entire request.
    *
    * You must implement this if you implement @{method:shouldAppearOnEdit}.
    *
    * You should not perform field validation here; instead, you should implement
    * @{method:validateField}.
    *
    * @param AphrontRequest HTTP request representing a user submitting a form
    *                       with this field in it.
    * @return this
    * @task edit
    */
   public function setValueFromRequest(AphrontRequest $request) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Build a renderable object (generally, some @{class:AphrontFormControl})
    * which can be appended to a @{class:AphrontFormView} and represents the
    * interface the user sees on the "Edit Revision" screen when interacting
    * with this field.
    *
    * For example:
    *
    *   return id(new AphrontFormTextControl())
    *     ->setLabel('Custom Field')
    *     ->setName('my-custom-key')
    *     ->setValue($this->value);
    *
    * You must implement this if you implement @{method:shouldAppearOnEdit}.
    *
    * @return AphrontView|string Something renderable.
    * @task edit
    */
   public function renderEditControl() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * This method will be called after @{method:setValueFromRequest} but before
    * the field is saved. It gives you an opportunity to inspect the field value
    * and throw a @{class:DifferentialFieldValidationException} if there is a
    * problem with the value the user has provided (for example, the value the
    * user entered is not correctly formatted). This method is also called after
    * @{method:setValueFromParsedCommitMessage} before the revision is saved.
    *
    * By default, fields are not validated.
    *
    * @return void
    * @task edit
    */
   public function validateField() {
     return;
   }
 
   /**
    * Hook for applying revision changes via the editor. Normally, you should
    * not implement this, but a number of builtin fields use the revision object
    * itself as storage. If you need to do something similar for whatever reason,
    * this method gives you an opportunity to interact with the editor or
    * revision before changes are saved (for example, you can write the field's
    * value into some property of the revision).
    *
    * @param DifferentialRevisionEditor  Active editor which is applying changes
    *                                    to the revision.
    * @return void
    * @task edit
    */
   public function willWriteRevision(DifferentialRevisionEditor $editor) {
     return;
   }
 
   /**
    * Hook after an edit operation has completed. This allows you to update
    * link tables or do other write operations which should happen after the
    * revision is saved. Normally you don't need to implement this.
    *
    *
    * @param DifferentialRevisionEditor  Active editor which has just applied
    *                                    changes to the revision.
    * @return void
    * @task edit
    */
   public function didWriteRevision(DifferentialRevisionEditor $editor) {
     return;
   }
 
 
 /* -(  Extending the Revision View Interface  )------------------------------ */
 
 
   /**
    * Determine if this field should appear on the revision detail view
    * interface. One use of this interface is to add purely informational
    * fields to the revision view, without any sort of backing storage.
    *
    * If you return true from this method, you must implement the methods
    * @{method:renderLabelForRevisionView} and
    * @{method:renderValueForRevisionView}.
    *
    * @return bool True if this field should appear when viewing a revision.
    * @task view
    */
   public function shouldAppearOnRevisionView() {
     return false;
   }
 
 
   /**
    * Return a string field label which will appear in the revision detail
    * table.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnRevisionView}.
    *
    * @return string Label for field in revision detail view.
    * @task view
    */
   public function renderLabelForRevisionView() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Return a markup block representing the field for the revision detail
    * view. Note that you can return null to suppress display (for instance,
    * if the field shows related objects of some type and the revision doesn't
    * have any related objects).
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnRevisionView}.
    *
    * @return string|null Display markup for field value, or null to suppress
    *                     field rendering.
    * @task view
    */
   public function renderValueForRevisionView() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Load users, their current statuses and return a markup with links to the
    * user profiles and information about their current status.
    *
    * @return string Display markup.
    * @task view
    */
   public function renderUserList(array $user_phids) {
     if (!$user_phids) {
       return '<em>None</em>';
     }
 
     $statuses = id(new PhabricatorUserStatus())->loadCurrentStatuses(
       $user_phids);
 
     $links = array();
     foreach ($user_phids as $user_phid) {
       $handle = $this->getHandle($user_phid);
       $extra = null;
       $status = idx($statuses, $handle->getPHID());
       if ($handle->isDisabled()) {
         $extra = ' <strong>(disabled)</strong>';
       } else if ($status) {
         $until = phabricator_date($status->getDateTo(), $this->getUser());
         if ($status->getStatus() == PhabricatorUserStatus::STATUS_SPORADIC) {
           $extra = ' <strong title="until '.$until.'">(sporadic)</strong>';
         } else {
           $extra = ' <strong title="until '.$until.'">(away)</strong>';
         }
       }
       $links[] = $handle->renderLink().$extra;
     }
 
     return implode(', ', $links);
   }
 
 
   /**
    * Return a markup block representing a warning to display with the comment
    * box when preparing to accept a diff. A return value of null indicates no
    * warning box should be displayed for this field.
    *
    * @return string|null Display markup for warning box, or null for no warning
    */
   public function renderWarningBoxForRevisionAccept() {
     return null;
   }
 
 
 /* -(  Extending the Revision List Interface  )------------------------------ */
 
 
   /**
    * Determine if this field should appear in the table on the revision list
    * interface.
    *
    * @return bool True if this field should appear in the table.
    *
    * @task list
    */
   public function shouldAppearOnRevisionList() {
     return false;
   }
 
 
   /**
    * Return a column header for revision list tables.
    *
    * @return string Column header.
    *
    * @task list
    */
   public function renderHeaderForRevisionList() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
   /**
    * Optionally, return a column class for revision list tables.
    *
    * @return string CSS class for table cells.
    *
    * @task list
    */
   public function getColumnClassForRevisionList() {
     return null;
   }
 
 
   /**
    * Return a table cell value for revision list tables.
    *
    * @param DifferentialRevision The revision to render a value for.
    * @return string Table cell value.
    *
    * @task list
    */
   public function renderValueForRevisionList(DifferentialRevision $revision) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
+/* -(  Extending the E-mail Interface  )------------------------------------- */
+
+
+  /**
+   * Return plain text to render in e-mail messages. The text may span
+   * multiple lines.
+   *
+   * @return string|null Plain text, or null for no message.
+   *
+   * @task mail
+   */
+  public function renderValueForMail() {
+    return null;
+  }
+
+
 /* -(  Extending the Conduit Interface  )------------------------------------ */
 
 
   /**
    * @task conduit
    */
   public function shouldAppearOnConduitView() {
     return false;
   }
 
   /**
    * @task conduit
    */
   public function getValueForConduit() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * @task conduit
    */
   public function getKeyForConduit() {
     $key = $this->getStorageKey();
     if ($key === null) {
       throw new DifferentialFieldSpecificationIncompleteException($this);
     }
     return $key;
   }
 
 
 /* -(  Extending Commit Messages  )------------------------------------------ */
 
 
   /**
    * Determine if this field should appear in commit messages. You should return
    * true if this field participates in any part of the commit message workflow,
    * even if it is not rendered by default.
    *
    * If you implement this method, you must implement
    * @{method:getCommitMessageKey} and
    * @{method:setValueFromParsedCommitMessage}.
    *
    * @return bool True if this field appears in commit messages in any capacity.
    * @task commit
    */
   public function shouldAppearOnCommitMessage() {
     return false;
   }
 
   /**
    * Key which identifies this field in parsed commit messages. Commit messages
    * exist in two forms: raw textual commit messages and parsed dictionaries of
    * fields. This method must return a unique string which identifies this field
    * in dictionaries. Principally, this dictionary is shipped to and from arc
    * over Conduit. Keys should be appropriate property names, like "testPlan"
    * (not "Test Plan") and must be globally unique.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnCommitMessage}.
    *
    * @return string Key which identifies the field in dictionaries.
    * @task commit
    */
   public function getCommitMessageKey() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * Set this field's value from a value in a parsed commit message dictionary.
    * Afterward, this field will go through the normal write workflows and the
    * change will be permanently stored via either the storage mechanisms (if
    * your field implements them), revision write hooks (if your field implements
    * them) or discarded (if your field implements neither, e.g. is just a
    * display field).
    *
    * The value you receive will either be null or something you originally
    * returned from @{method:parseValueFromCommitMessage}.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnCommitMessage}.
    *
    * @param mixed Field value from a parsed commit message dictionary.
    * @return this
    * @task commit
    */
   public function setValueFromParsedCommitMessage($value) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * In revision control systems which read revision information from the
    * working copy, the user may edit the commit message outside of invoking
    * "arc diff --edit". When they do this, only some fields (those fields which
    * can not be edited by other users) are safe to overwrite. For instance, it
    * is fine to overwrite "Summary" because no one else can edit it, but not
    * to overwrite "Reviewers" because reviewers may have been added or removed
    * via the web interface.
    *
    * If a field is safe to overwrite when edited in a working copy commit
    * message, return true. If the authoritative value should always be used,
    * return false. By default, fields can not be overwritten.
    *
    * arc will only attempt to overwrite field values if run with "--verbatim".
    *
    * @return bool True to indicate the field is save to overwrite.
    * @task commit
    */
   public function shouldOverwriteWhenCommitMessageIsEdited() {
     return false;
   }
 
   /**
    * Return true if this field should be suggested to the user during
    * "arc diff --edit". Basicially, return true if the field is something the
    * user might want to fill out (like "Summary"), and false if it's a
    * system/display/readonly field (like "Differential Revision"). If this
    * method returns true, the field will be rendered even if it has no value
    * during edit and update operations.
    *
    * @return bool True to indicate the field should appear in the edit template.
    * @task commit
    */
   public function shouldAppearOnCommitMessageTemplate() {
     return true;
   }
 
   /**
    * Render a human-readable label for this field, like "Summary" or
    * "Test Plan". This is distinct from the commit message key, but generally
    * they should be similar.
    *
    * @return string Human-readable field label for commit messages.
    * @task commit
    */
   public function renderLabelForCommitMessage() {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * Render a human-readable value for this field when it appears in commit
    * messages (for instance, lists of users should be rendered as user names).
    *
    * The ##$is_edit## parameter allows you to distinguish between commit
    * messages being rendered for editing and those being rendered for amending
    * or commit. Some fields may decline to render a value in one mode (for
    * example, "Reviewed By" appears only when doing commit/amend, not while
    * editing).
    *
    * @param bool True if the message is being edited.
    * @return string Human-readable field value.
    * @task commit
    */
   public function renderValueForCommitMessage($is_edit) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
   /**
    * Return one or more labels which this field parses in commit messages. For
    * example, you might parse all of "Task", "Tasks" and "Task Numbers" or
    * similar. This is just to make it easier to get commit messages to parse
    * when users are typing in the fields manually as opposed to using a
    * template, by accepting alternate spellings / pluralizations / etc. By
    * default, only the label returned from @{method:renderLabelForCommitMessage}
    * is parsed.
    *
    * @return list List of supported labels that this field can parse from commit
    *              messages.
    * @task commit
    */
   public function getSupportedCommitMessageLabels() {
     return array($this->renderLabelForCommitMessage());
   }
 
   /**
    * Parse a raw text block from a commit message into a canonical
    * representation of the field value. For example, the "CC" field accepts a
    * comma-delimited list of usernames and emails and parses them into valid
    * PHIDs, emitting a PHID list.
    *
    * If you encounter errors (like a nonexistent username) while parsing,
    * you should throw a @{class:DifferentialFieldParseException}.
    *
    * Generally, this method should accept whatever you return from
    * @{method:renderValueForCommitMessage} and parse it back into a sensible
    * representation.
    *
    * You must implement this method if you return true from
    * @{method:shouldAppearOnCommitMessage}.
    *
    * @param string
    * @return mixed The canonical representation of the field value. For example,
    *               you should lookup usernames and object references.
    * @task commit
    */
   public function parseValueFromCommitMessage($value) {
     throw new DifferentialFieldSpecificationIncompleteException($this);
   }
 
 
 /* -(  Loading Additional Data  )-------------------------------------------- */
 
 
   /**
    * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
    * field to render correctly.
    *
    * This is a convenience method which makes the handles available on all
    * interfaces where the field appears. If your field needs handles on only
    * some interfaces (or needs different handles on different interfaces) you
    * can overload the more specific methods to customize which interfaces you
    * retrieve handles for. Requesting only the handles you need will improve
    * the performance of your field.
    *
    * You can later retrieve these handles by calling @{method:getHandle}.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   protected function getRequiredHandlePHIDs() {
     return array();
   }
 
 
   /**
    * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
    * field to render correctly on the view interface.
    *
    * This is a more specific version of @{method:getRequiredHandlePHIDs} which
    * can be overridden to improve field performance by loading only data you
    * need.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   public function getRequiredHandlePHIDsForRevisionView() {
     return $this->getRequiredHandlePHIDs();
   }
 
 
   /**
    * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
    * field to render correctly on the list interface.
    *
    * This is a more specific version of @{method:getRequiredHandlePHIDs} which
    * can be overridden to improve field performance by loading only data you
    * need.
    *
    * @param DifferentialRevision The revision to pull PHIDs for.
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   public function getRequiredHandlePHIDsForRevisionList(
     DifferentialRevision $revision) {
     return array();
   }
 
 
   /**
    * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
    * field to render correctly on the edit interface.
    *
    * This is a more specific version of @{method:getRequiredHandlePHIDs} which
    * can be overridden to improve field performance by loading only data you
    * need.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   public function getRequiredHandlePHIDsForRevisionEdit() {
     return $this->getRequiredHandlePHIDs();
   }
 
   /**
    * Specify which @{class:PhabricatorObjectHandle}s need to be loaded for your
    * field to render correctly on the commit message interface.
    *
    * This is a more specific version of @{method:getRequiredHandlePHIDs} which
    * can be overridden to improve field performance by loading only data you
    * need.
    *
    * @return list List of PHIDs to load handles for.
    * @task load
    */
   public function getRequiredHandlePHIDsForCommitMessage() {
     return $this->getRequiredHandlePHIDs();
   }
 
   /**
    * Specify which diff properties this field needs to load.
    *
    * @return list List of diff property keys this field requires.
    * @task load
    */
   public function getRequiredDiffProperties() {
     return array();
   }
 
   /**
    * Parse a list of users into a canonical PHID list.
    *
    * @param string Raw list of comma-separated user names.
    * @return list List of corresponding PHIDs.
    * @task load
    */
   protected function parseCommitMessageUserList($value) {
     return $this->parseCommitMessageObjectList($value, $mailables = false);
   }
 
   /**
    * Parse a list of mailable objects into a canonical PHID list.
    *
    * @param string Raw list of comma-separated mailable names.
    * @return list List of corresponding PHIDs.
    * @task load
    */
   protected function parseCommitMessageMailableList($value) {
     return $this->parseCommitMessageObjectList($value, $mailables = true);
   }
 
 
   /**
    * Parse and lookup a list of object names, converting them to PHIDs.
    *
    * @param string Raw list of comma-separated object names.
    * @param bool   True to include mailing lists.
    * @param bool   True to make a best effort. By default, an exception is
    *               thrown if any item is invalid.
    * @return list List of corresponding PHIDs.
    * @task load
    */
   public static function parseCommitMessageObjectList(
     $value,
     $include_mailables,
     $allow_partial = false) {
 
     $value = array_unique(array_filter(preg_split('/[\s,]+/', $value)));
     if (!$value) {
       return array();
     }
 
     $object_map = array();
 
     $users = id(new PhabricatorUser())->loadAllWhere(
       '(username IN (%Ls))',
       $value);
 
     $user_map = mpull($users, 'getPHID', 'getUsername');
     foreach ($user_map as $username => $phid) {
       // Usernames may have uppercase letters in them. Put both names in the
       // map so we can try the original case first, so that username *always*
       // works in weird edge cases where some other mailable object collides.
       $object_map[$username] = $phid;
       $object_map[strtolower($username)] = $phid;
     }
 
     if ($include_mailables) {
       $mailables = id(new PhabricatorMetaMTAMailingList())->loadAllWhere(
         '(email IN (%Ls)) OR (name IN (%Ls))',
         $value,
         $value);
       $object_map += mpull($mailables, 'getPHID', 'getName');
       $object_map += mpull($mailables, 'getPHID', 'getEmail');
     }
 
     $invalid = array();
     $results = array();
     foreach ($value as $name) {
       if (empty($object_map[$name])) {
         if (empty($object_map[strtolower($name)])) {
           $invalid[] = $name;
         } else {
           $results[] = $object_map[strtolower($name)];
         }
       } else {
         $results[] = $object_map[$name];
       }
     }
 
     if ($invalid && !$allow_partial) {
       $invalid = implode(', ', $invalid);
       $what = $include_mailables
         ? "users and mailing lists"
         : "users";
       throw new DifferentialFieldParseException(
         "Commit message references nonexistent {$what}: {$invalid}.",
         array_unique($results));
     }
 
     return array_unique($results);
   }
 
 
 /* -(  Contextual Data  )---------------------------------------------------- */
 
 
   /**
    * @task context
    */
   final public function setRevision(DifferentialRevision $revision) {
     $this->revision = $revision;
     $this->didSetRevision();
     return $this;
   }
 
   /**
    * @task context
    */
   protected function didSetRevision() {
     return;
   }
 
 
   /**
    * @task context
    */
   final public function setDiff(DifferentialDiff $diff) {
     $this->diff = $diff;
     return $this;
   }
 
   /**
    * @task context
    */
   final public function setHandles(array $handles) {
     assert_instances_of($handles, 'PhabricatorObjectHandle');
     $this->handles = $handles;
     return $this;
   }
 
   /**
    * @task context
    */
   final public function setDiffProperties(array $diff_properties) {
     $this->diffProperties = $diff_properties;
     return $this;
   }
 
   /**
    * @task context
    */
   final public function setUser(PhabricatorUser $user) {
     $this->user = $user;
     return $this;
   }
 
   /**
    * @task context
    */
   final protected function getRevision() {
     if (empty($this->revision)) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     return $this->revision;
   }
 
   /**
    * @task context
    */
   final protected function getDiff() {
     if (empty($this->diff)) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     return $this->diff;
   }
 
   /**
    * @task context
    */
   final protected function getUser() {
     if (empty($this->user)) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     return $this->user;
   }
 
   /**
    * Get the handle for an object PHID. You must overload
    * @{method:getRequiredHandlePHIDs} (or a more specific version thereof)
    * and include the PHID you want in the list for it to be available here.
    *
    * @return PhabricatorObjectHandle Handle to the object.
    * @task context
    */
   final protected function getHandle($phid) {
     if ($this->handles === null) {
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     if (empty($this->handles[$phid])) {
       $class = get_class($this);
       throw new Exception(
         "A differential field (of class '{$class}') is attempting to retrieve ".
         "a handle ('{$phid}') which it did not request. Return all handle ".
         "PHIDs you need from getRequiredHandlePHIDs().");
     }
     return $this->handles[$phid];
   }
 
   /**
    * Get a diff property which this field previously requested by returning
    * the key from @{method:getRequiredDiffProperties}.
    *
    * @param  string      Diff property key.
    * @return string|null Diff property, or null if the property does not have
    *                     a value.
    * @task context
    */
   final public function getDiffProperty($key) {
     if ($this->diffProperties === null) {
       // This will be set to some (possibly empty) array if we've loaded
       // properties, so null means diff properties aren't available in this
       // context.
       throw new DifferentialFieldDataNotAvailableException($this);
     }
     if (!array_key_exists($key, $this->diffProperties)) {
       $class = get_class($this);
       throw new Exception(
         "A differential field (of class '{$class}') is attempting to retrieve ".
         "a diff property ('{$key}') which it did not request. Return all ".
         "diff property keys you need from getRequiredDiffProperties().");
     }
     return $this->diffProperties[$key];
   }
 
 }
diff --git a/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php
index 96e8c956ce..0e2c23e046 100644
--- a/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php
+++ b/src/applications/differential/field/specification/DifferentialRevisionIDFieldSpecification.php
@@ -1,107 +1,112 @@
 <?php
 
 /*
  * Copyright 2012 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.
  */
 
 final class DifferentialRevisionIDFieldSpecification
   extends DifferentialFieldSpecification {
 
   private $id;
 
   protected function didSetRevision() {
     $this->id = $this->getRevision()->getID();
   }
 
   public function shouldAppearOnCommitMessage() {
     return true;
   }
 
   public function shouldAppearOnCommitMessageTemplate() {
     return false;
   }
 
   public function getCommitMessageKey() {
     return 'revisionID';
   }
 
   public function setValueFromParsedCommitMessage($value) {
     $this->id = $value;
     return $this;
   }
 
   public function renderLabelForCommitMessage() {
     return 'Differential Revision';
   }
 
   public function renderValueForCommitMessage($is_edit) {
     if (!$this->id) {
       return null;
     }
     return PhabricatorEnv::getProductionURI('/D'.$this->id);
   }
 
   public function parseValueFromCommitMessage($value) {
     $rev = trim($value);
 
     if (!strlen($rev)) {
       return null;
     }
 
     if (is_numeric($rev)) {
       // TODO: Eventually, remove support for bare revision numbers.
       return (int)$rev;
     }
 
     $rev = self::parseRevisionIDFromURI($rev);
     if ($rev !== null) {
       return $rev;
     }
 
     $example_uri = PhabricatorEnv::getProductionURI('/D123');
     throw new DifferentialFieldParseException(
       "Commit references invalid 'Differential Revision'. Expected a ".
       "Phabricator URI like '{$example_uri}', got '{$value}'.");
   }
 
   public static function parseRevisionIDFromURI($uri) {
     $path = id(new PhutilURI($uri))->getPath();
 
     $matches = null;
     if (preg_match('#^/D(\d+)$#', $path, $matches)) {
       $id = (int)$matches[1];
       // Make sure the URI is the same as our URI. Basically, we want to ignore
       // commits from other Phabricator installs.
       if ($uri == PhabricatorEnv::getProductionURI('/D'.$id)) {
         return $id;
       }
     }
 
     return null;
   }
 
   public function shouldAppearOnRevisionList() {
     return true;
   }
 
   public function renderHeaderForRevisionList() {
     return 'ID';
   }
 
   public function renderValueForRevisionList(DifferentialRevision $revision) {
     return 'D'.$revision->getID();
   }
 
+  public function renderValueForMail() {
+    $uri = PhabricatorEnv::getProductionURI('/D'.$this->id);
+    return "REVISION DETAIL\n  {$uri}";
+  }
+
 }
diff --git a/src/applications/differential/mail/DifferentialCommentMail.php b/src/applications/differential/mail/DifferentialCommentMail.php
index 478e7b635c..7fec3f4973 100644
--- a/src/applications/differential/mail/DifferentialCommentMail.php
+++ b/src/applications/differential/mail/DifferentialCommentMail.php
@@ -1,204 +1,179 @@
 <?php
 
 /*
  * Copyright 2012 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.
  */
 
 final class DifferentialCommentMail extends DifferentialMail {
 
   protected $changedByCommit;
 
   public function setChangedByCommit($changed_by_commit) {
     $this->changedByCommit = $changed_by_commit;
     return $this;
   }
 
   public function getChangedByCommit() {
     return $this->changedByCommit;
   }
 
   public function __construct(
     DifferentialRevision $revision,
     PhabricatorObjectHandle $actor,
     DifferentialComment $comment,
     array $changesets,
     array $inline_comments) {
     assert_instances_of($changesets, 'DifferentialChangeset');
     assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface');
 
     $this->setRevision($revision);
     $this->setActorHandle($actor);
     $this->setComment($comment);
     $this->setChangesets($changesets);
     $this->setInlineComments($inline_comments);
 
   }
 
   protected function getMailTags() {
     $comment = $this->getComment();
     $action = $comment->getAction();
 
     $tags = array();
     switch ($action) {
       case DifferentialAction::ACTION_ADDCCS:
         $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CC;
         break;
       case DifferentialAction::ACTION_CLOSE:
         $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_CLOSED;
         break;
     }
 
     if (strlen(trim($comment->getContent()))) {
       switch ($action) {
         case DifferentialAction::ACTION_CLOSE:
           // Commit comments are auto-generated and not especially interesting,
           // so don't tag them as having a comment.
           break;
         default:
           $tags[] = MetaMTANotificationType::TYPE_DIFFERENTIAL_COMMENT;
           break;
       }
     }
 
     return $tags;
   }
 
   protected function renderVarySubject() {
     $verb = ucwords($this->getVerb());
     return "[{$verb}] ".$this->renderSubject();
   }
 
   protected function getVerb() {
     $comment = $this->getComment();
     $action = $comment->getAction();
     $verb = DifferentialAction::getActionPastTenseVerb($action);
     return $verb;
   }
 
   protected function renderBody() {
 
     $comment = $this->getComment();
 
     $actor = $this->getActorName();
     $name  = $this->getRevision()->getTitle();
     $verb  = $this->getVerb();
 
     $body  = array();
 
     $body[] = "{$actor} has {$verb} the revision \"{$name}\".";
 
     // If the commented added reviewers or CCs, list them explicitly.
     $meta = $comment->getMetadata();
     $m_reviewers = idx(
       $meta,
       DifferentialComment::METADATA_ADDED_REVIEWERS,
       array());
     $m_cc = idx(
       $meta,
       DifferentialComment::METADATA_ADDED_CCS,
       array());
     $load = array_merge($m_reviewers, $m_cc);
     if ($load) {
       $handles = id(new PhabricatorObjectHandleData($load))->loadHandles();
       if ($m_reviewers) {
         $body[] = 'Added Reviewers: '.$this->renderHandleList(
           $handles,
           $m_reviewers);
       }
       if ($m_cc) {
         $body[] = 'Added CCs: '.$this->renderHandleList(
           $handles,
           $m_cc);
       }
     }
 
     $body[] = null;
 
     $content = $comment->getContent();
     if (strlen($content)) {
       $body[] = $this->formatText($content);
       $body[] = null;
     }
 
     if ($this->getChangedByCommit()) {
       $body[] = 'CHANGED PRIOR TO COMMIT';
       $body[] = '  '.$this->getChangedByCommit();
       $body[] = null;
     }
 
     $inlines = $this->getInlineComments();
     if ($inlines) {
       $body[] = 'INLINE COMMENTS';
       $changesets = $this->getChangesets();
       foreach ($inlines as $inline) {
         $changeset = $changesets[$inline->getChangesetID()];
         if (!$changeset) {
           throw new Exception('Changeset missing!');
         }
         $file = $changeset->getFilename();
         $start = $inline->getLineNumber();
         $len = $inline->getLineLength();
         if ($len) {
           $range = $start.'-'.($start + $len);
         } else {
           $range = $start;
         }
         $content = $inline->getContent();
         $body[] = $this->formatText("{$file}:{$range} {$content}");
       }
       $body[] = null;
     }
 
-    $body[] = $this->renderRevisionDetailLink();
-    $body[] = null;
-
-    $revision = $this->getRevision();
-    $status = $revision->getStatus();
-
-    if ($status == ArcanistDifferentialRevisionStatus::NEEDS_REVISION ||
-        $status == ArcanistDifferentialRevisionStatus::ACCEPTED) {
-      $diff = $revision->loadActiveDiff();
-      if ($diff) {
-        $branch = $diff->getBranch();
-        if ($branch) {
-          $body[] = "BRANCH\n  $branch";
-          $body[] = null;
-        }
-      }
-    }
-
-    if ($status == ArcanistDifferentialRevisionStatus::CLOSED) {
-      $phids = $revision->loadCommitPHIDs();
-      if ($phids) {
-        $handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
-        if (count($handles) == 1) {
-          $body[] = "COMMIT";
-        } else {
-          // This is unlikely to ever happen since we'll send this mail the
-          // first time we discover a commit, but it's not impossible if data
-          // was migrated, etc.
-          $body[] = "COMMITS";
-        }
-
-        foreach ($handles as $handle) {
-          $body[] = '  '.PhabricatorEnv::getProductionURI($handle->getURI());
-        }
+    $selector = DifferentialFieldSelector::newSelector();
+    $aux_fields = $selector->sortFieldsForMail(
+      $selector->getFieldSpecifications());
+
+    foreach ($aux_fields as $field) {
+      $field->setRevision($this->getRevision());
+      $text = $field->renderValueForMail();
+      if ($text !== null) {
+        $body[] = $text;
         $body[] = null;
       }
     }
 
     return implode("\n", $body);
   }
 }