diff --git a/src/applications/differential/editor/comment/DifferentialCommentEditor.php b/src/applications/differential/editor/comment/DifferentialCommentEditor.php
index ab84984024..a12adcd878 100644
--- a/src/applications/differential/editor/comment/DifferentialCommentEditor.php
+++ b/src/applications/differential/editor/comment/DifferentialCommentEditor.php
@@ -1,341 +1,349 @@
 <?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 DifferentialCommentEditor {
 
   protected $revision;
   protected $actorPHID;
   protected $action;
 
   protected $attachInlineComments;
   protected $message;
   protected $addCC;
   protected $changedByCommit;
   protected $addedReviewers = array();
 
+  private $parentMessageID;
+
   public function __construct(
     DifferentialRevision $revision,
     $actor_phid,
     $action) {
 
     $this->revision = $revision;
     $this->actorPHID  = $actor_phid;
     $this->action   = $action;
   }
 
+  public function setParentMessageID($parent_message_id) {
+    $this->parentMessageID = $parent_message_id;
+    return $this;
+  }
+
   public function setMessage($message) {
     $this->message = $message;
     return $this;
   }
 
   public function setAttachInlineComments($attach) {
     $this->attachInlineComments = $attach;
     return $this;
   }
 
   public function setAddCC($add) {
     $this->addCC = $add;
     return $this;
   }
 
   public function setChangedByCommit($changed_by_commit) {
     $this->changedByCommit = $changed_by_commit;
     return $this;
   }
 
   public function getChangedByCommit() {
     return $this->changedByCommit;
   }
 
   public function setAddedReviewers($added_reviewers) {
     $this->addedReviewers = $added_reviewers;
     return $this;
   }
 
   public function getAddedReviewers() {
     return $this->addedReviewers;
   }
 
   public function save() {
     $revision = $this->revision;
     $action = $this->action;
     $actor_phid = $this->actorPHID;
     $actor_is_author = ($actor_phid == $revision->getAuthorPHID());
     $revision_status = $revision->getStatus();
 
     $revision->loadRelationships();
     $reviewer_phids = $revision->getReviewers();
     if ($reviewer_phids) {
       $reviewer_phids = array_combine($reviewer_phids, $reviewer_phids);
     }
 
     $metadata = array();
 
     switch ($action) {
       case DifferentialAction::ACTION_COMMENT:
         break;
 
       case DifferentialAction::ACTION_RESIGN:
         if ($actor_is_author) {
           throw new Exception('You can not resign from your own revision!');
         }
         if (isset($reviewer_phids[$actor_phid])) {
           DifferentialRevisionEditor::alterReviewers(
             $revision,
             $reviewer_phids,
             $rem = array($actor_phid),
             $add = array(),
             $actor_phid);
         }
         break;
 
       case DifferentialAction::ACTION_ABANDON:
         if (!$actor_is_author) {
           throw new Exception('You can only abandon your revisions.');
         }
         if ($revision_status == DifferentialRevisionStatus::COMMITTED) {
           throw new Exception('You can not abandon a committed revision.');
         }
         if ($revision_status == DifferentialRevisionStatus::ABANDONED) {
           $action = DifferentialAction::ACTION_COMMENT;
           break;
         }
 
         $revision
           ->setStatus(DifferentialRevisionStatus::ABANDONED)
           ->save();
         break;
 
       case DifferentialAction::ACTION_ACCEPT:
         if ($actor_is_author) {
           throw new Exception('You can not accept your own revision.');
         }
         if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
             ($revision_status != DifferentialRevisionStatus::NEEDS_REVISION)) {
           $action = DifferentialAction::ACTION_COMMENT;
           break;
         }
 
         $revision
           ->setStatus(DifferentialRevisionStatus::ACCEPTED)
           ->save();
 
         if (!isset($reviewer_phids[$actor_phid])) {
           DifferentialRevisionEditor::alterReviewers(
             $revision,
             $reviewer_phids,
             $rem = array(),
             $add = array($actor_phid),
             $actor_phid);
         }
         break;
 
       case DifferentialAction::ACTION_REQUEST:
         if (!$actor_is_author) {
           throw new Exception('You must own a revision to request review.');
         }
         if (($revision_status != DifferentialRevisionStatus::NEEDS_REVISION) &&
             ($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
           $action = DifferentialAction::ACTION_COMMENT;
           break;
         }
 
         $revision
           ->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)
           ->save();
         break;
 
       case DifferentialAction::ACTION_REJECT:
         if ($actor_is_author) {
           throw new Exception(
             'You can not request changes to your own revision.');
         }
         if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
             ($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
           $action = DifferentialAction::ACTION_COMMENT;
           break;
         }
 
         if (!isset($reviewer_phids[$actor_phid])) {
           DifferentialRevisionEditor::alterReviewers(
             $revision,
             $reviewer_phids,
             $rem = array(),
             $add = array($actor_phid),
             $actor_phid);
         }
 
         $revision
           ->setStatus(DifferentialRevisionStatus::NEEDS_REVISION)
           ->save();
         break;
 
       case DifferentialAction::ACTION_RETHINK:
         if (!$actor_is_author) {
           throw new Exception(
             "You can not plan changes to somebody else's revision");
         }
         if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
             ($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
           $action = DifferentialAction::ACTION_COMMENT;
           break;
         }
 
         $revision
           ->setStatus(DifferentialRevisionStatus::NEEDS_REVISION)
           ->save();
         break;
 
       case DifferentialAction::ACTION_RECLAIM:
         if (!$actor_is_author) {
           throw new Exception('You can not reclaim a revision you do not own.');
         }
         if ($revision_status != DifferentialRevisionStatus::ABANDONED) {
           $action = DifferentialAction::ACTION_COMMENT;
           break;
         }
         $revision
           ->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)
           ->save();
         break;
 
       case DifferentialAction::ACTION_COMMIT:
         if (!$actor_is_author) {
           throw new Exception('You can not commit a revision you do not own.');
         }
         $revision
           ->setStatus(DifferentialRevisionStatus::COMMITTED)
           ->save();
         break;
 
       case DifferentialAction::ACTION_ADDREVIEWERS:
         $added_reviewers = $this->getAddedReviewers();
         foreach ($added_reviewers as $k => $user_phid) {
           if ($user_phid == $revision->getAuthorPHID()) {
             unset($added_reviewers[$k]);
           }
           if (!empty($reviewer_phids[$user_phid])) {
             unset($added_reviewers[$k]);
           }
         }
 
         $added_reviewers = array_unique($added_reviewers);
 
         if ($added_reviewers) {
           DifferentialRevisionEditor::alterReviewers(
             $revision,
             $reviewer_phids,
             $rem = array(),
             $add = $added_reviewers,
             $actor_phid);
 
           $key = DifferentialComment::METADATA_ADDED_REVIEWERS;
           $metadata[$key] = $added_reviewers;
 
         } else {
           $action = DifferentialAction::ACTION_COMMENT;
         }
         break;
 
       default:
         throw new Exception('Unsupported action.');
     }
 
     if ($this->addCC) {
       DifferentialRevisionEditor::addCC(
         $revision,
         $this->actorPHID,
         $this->actorPHID);
     }
 
     // Reload relationships to pick up any reviewer/CC changes.
     $revision->loadRelationships();
 
     $inline_comments = array();
     if ($this->attachInlineComments) {
       $inline_comments = id(new DifferentialInlineComment())->loadAllWhere(
         'authorPHID = %s AND revisionID = %d AND commentID IS NULL',
         $this->actorPHID,
         $revision->getID());
     }
 
     $comment = id(new DifferentialComment())
       ->setAuthorPHID($this->actorPHID)
       ->setRevisionID($revision->getID())
       ->setAction($action)
       ->setContent((string)$this->message)
       ->setMetadata($metadata)
       ->save();
 
     $changesets = array();
     if ($inline_comments) {
       $load_ids = mpull($inline_comments, 'getChangesetID');
       if ($load_ids) {
         $load_ids = array_unique($load_ids);
         $changesets = id(new DifferentialChangeset())->loadAllWhere(
           'id in (%Ld)',
           $load_ids);
       }
       foreach ($inline_comments as $inline) {
         $inline->setCommentID($comment->getID());
         $inline->save();
       }
     }
 
     $phids = array($this->actorPHID);
     $handles = id(new PhabricatorObjectHandleData($phids))
       ->loadHandles();
     $actor_handle = $handles[$this->actorPHID];
 
     $xherald_header = HeraldTranscript::loadXHeraldRulesHeader(
       $revision->getPHID());
 
     id(new DifferentialCommentMail(
       $revision,
       $actor_handle,
       $comment,
       $changesets,
       $inline_comments))
       ->setToPHIDs(
         array_merge(
           $revision->getReviewers(),
           array($revision->getAuthorPHID())))
       ->setCCPHIDs($revision->getCCPHIDs())
       ->setChangedByCommit($this->getChangedByCommit())
       ->setXHeraldRulesHeader($xherald_header)
+      ->setParentMessageID($this->parentMessageID)
       ->send();
 
     $event_data = array(
       'revision_id'          => $revision->getID(),
       'revision_phid'        => $revision->getPHID(),
       'revision_name'        => $revision->getTitle(),
       'revision_author_phid' => $revision->getAuthorPHID(),
       'action'               => $comment->getAction(),
       'feedback_content'     => $comment->getContent(),
       'actor_phid'           => $this->actorPHID,
     );
     id(new PhabricatorTimelineEvent('difx', $event_data))
       ->recordEvent();
 
     // TODO: Move to a daemon?
     PhabricatorSearchDifferentialIndexer::indexRevision($revision);
 
     return $comment;
   }
 
 }
diff --git a/src/applications/differential/mail/base/DifferentialMail.php b/src/applications/differential/mail/base/DifferentialMail.php
index b497266f42..8ea77d456e 100644
--- a/src/applications/differential/mail/base/DifferentialMail.php
+++ b/src/applications/differential/mail/base/DifferentialMail.php
@@ -1,315 +1,322 @@
 <?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();
 
     $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());
 
     $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;
   }
 
   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;
   }
 
 }
diff --git a/src/applications/differential/replyhandler/DifferentialReplyHandler.php b/src/applications/differential/replyhandler/DifferentialReplyHandler.php
index df0e11077a..ae7be54e1b 100644
--- a/src/applications/differential/replyhandler/DifferentialReplyHandler.php
+++ b/src/applications/differential/replyhandler/DifferentialReplyHandler.php
@@ -1,162 +1,171 @@
 <?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 DifferentialReplyHandler extends PhabricatorMailReplyHandler {
 
+  private $receivedMail;
+
   public function validateMailReceiver($mail_receiver) {
     if (!($mail_receiver instanceof DifferentialRevision)) {
       throw new Exception("Receiver is not a DifferentialRevision!");
     }
   }
 
   public function getPrivateReplyHandlerEmailAddress(
     PhabricatorObjectHandle $handle) {
     return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'D');
   }
 
   public function getReplyHandlerDomain() {
     return PhabricatorEnv::getEnvConfig(
       'metamta.differential.reply-handler-domain');
   }
 
   /*
    * Generate text like the following from the supported commands.
    * "
    *
    * ACTIONS
    * Reply to comment, or !accept, !reject, !abandon, !resign, !reclaim.
    *
    * "
    */
   public function getReplyHandlerInstructions() {
     if (!$this->supportsReplies()) {
       return null;
     }
 
     $supported_commands = $this->getSupportedCommands();
     $text = '';
     if (empty($supported_commands)) {
       return $text;
     }
 
     $comment_command_printed = false;
     if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) {
       $text .= 'Reply to comment';
       $comment_command_printed = true;
 
       $supported_commands = array_diff(
         $supported_commands, array(DifferentialAction::ACTION_COMMENT));
     }
 
     if (!empty($supported_commands)) {
       if ($comment_command_printed) {
         $text .= ', or ';
       }
 
       $modified_commands = array();
       foreach ($supported_commands as $command) {
         $modified_commands[] = '!'.$command;
       }
 
       $text .= implode(', ', $modified_commands);
     }
 
     $text .= ".";
 
     return $text;
   }
 
   public function getSupportedCommands() {
     return array(
       DifferentialAction::ACTION_COMMENT,
       DifferentialAction::ACTION_REJECT,
       DifferentialAction::ACTION_ABANDON,
       DifferentialAction::ACTION_RECLAIM,
       DifferentialAction::ACTION_RESIGN,
       DifferentialAction::ACTION_RETHINK,
       'unsubscribe',
     );
   }
 
   public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
+    $this->receivedMail = $mail;
     $this->handleAction($mail->getCleanTextBody());
   }
 
   public function handleAction($body) {
     // all commands start with a bang and separated from the body by a newline
     // to make sure that actual feedback text couldn't trigger an action.
     // unrecognized commands will be parsed as part of the comment.
     $command = DifferentialAction::ACTION_COMMENT;
     $supported_commands = $this->getSupportedCommands();
     $regex = "/\A\n*!(" . implode('|', $supported_commands) . ")\n*/";
     $matches = array();
     if (preg_match($regex, $body, $matches)) {
       $command = $matches[1];
       $body = trim(str_replace('!' . $command, '', $body));
     }
 
     $actor = $this->getActor();
     if (!$actor) {
       throw new Exception('No actor is set for the reply action.');
     }
 
     switch ($command) {
       case 'unsubscribe':
         $this->unsubscribeUser($this->getMailReceiver(), $actor);
         // TODO: Send the user a confirmation email?
         return null;
     }
 
     try {
       $editor = new DifferentialCommentEditor(
         $this->getMailReceiver(),
         $actor->getPHID(),
         $command);
 
+      // NOTE: We have to be careful about this because Facebook's
+      // implementation jumps straight into handleAction() and will not have
+      // a PhabricatorMetaMTAReceivedMail object.
+      if ($this->receivedMail) {
+        $editor->setParentMessageID($this->receivedMail->getMessageID());
+      }
       $editor->setMessage($body);
       $editor->setAddCC(($command != DifferentialAction::ACTION_RESIGN));
       $comment = $editor->save();
 
       return $comment->getID();
 
     } catch (Exception $ex) {
       $exception_mail = new DifferentialExceptionMail(
         $this->getMailReceiver(),
         $ex,
         $body);
 
       $exception_mail->setToPHIDs(array($this->getActor()->getPHID()));
       $exception_mail->send();
 
       throw $ex;
     }
   }
 
   private function unsubscribeUser(
     DifferentialRevision $revision,
     PhabricatorUser $user) {
 
     $revision->loadRelationships();
     DifferentialRevisionEditor::removeCCAndUpdateRevision(
       $revision,
       $user->getPHID(),
       $user->getPHID());
   }
 
 
 }
diff --git a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
index 75067cd628..a2828a8b98 100644
--- a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
@@ -1,229 +1,237 @@
 <?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 ManiphestTransactionEditor {
 
+  private $parentMessageID;
+
+  public function setParentMessageID($parent_message_id) {
+    $this->parentMessageID = $parent_message_id;
+    return $this;
+  }
+
   public function applyTransactions($task, array $transactions) {
 
     $email_cc = $task->getCCPHIDs();
 
     $email_to = array();
     $email_to[] = $task->getOwnerPHID();
 
     foreach ($transactions as $key => $transaction) {
       $type = $transaction->getTransactionType();
       $new = $transaction->getNewValue();
       $email_to[] = $transaction->getAuthorPHID();
 
       switch ($type) {
         case ManiphestTransactionType::TYPE_NONE:
           $old = null;
           break;
         case ManiphestTransactionType::TYPE_STATUS:
           $old = $task->getStatus();
           break;
         case ManiphestTransactionType::TYPE_OWNER:
           $old = $task->getOwnerPHID();
           break;
         case ManiphestTransactionType::TYPE_CCS:
           $old = $task->getCCPHIDs();
           break;
         case ManiphestTransactionType::TYPE_PRIORITY:
           $old = $task->getPriority();
           break;
         case ManiphestTransactionType::TYPE_ATTACH:
           $old = $task->getAttached();
           break;
         case ManiphestTransactionType::TYPE_TITLE:
           $old = $task->getTitle();
           break;
         case ManiphestTransactionType::TYPE_DESCRIPTION:
           $old = $task->getDescription();
           break;
         case ManiphestTransactionType::TYPE_PROJECTS:
           $old = $task->getProjectPHIDs();
           break;
         default:
           throw new Exception('Unknown action type.');
       }
 
       if (($old !== null) && ($old == $new)) {
         if (count($transactions) > 1 && !$transaction->hasComments()) {
           // If we have at least one other transaction and this one isn't
           // doing anything and doesn't have any comments, just throw it
           // away.
           unset($transactions[$key]);
           continue;
         } else {
           $transaction->setOldValue(null);
           $transaction->setNewValue(null);
           $transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE);
         }
       } else {
         switch ($type) {
           case ManiphestTransactionType::TYPE_NONE:
             break;
           case ManiphestTransactionType::TYPE_STATUS:
             $task->setStatus($new);
             break;
           case ManiphestTransactionType::TYPE_OWNER:
             $task->setOwnerPHID($new);
             break;
           case ManiphestTransactionType::TYPE_CCS:
             $task->setCCPHIDs($new);
             break;
           case ManiphestTransactionType::TYPE_PRIORITY:
             $task->setPriority($new);
             break;
           case ManiphestTransactionType::TYPE_ATTACH:
             $task->setAttached($new);
             break;
           case ManiphestTransactionType::TYPE_TITLE:
             $task->setTitle($new);
             break;
           case ManiphestTransactionType::TYPE_DESCRIPTION:
             $task->setDescription($new);
             break;
           case ManiphestTransactionType::TYPE_PROJECTS:
             $task->setProjectPHIDs($new);
             break;
           default:
             throw new Exception('Unknown action type.');
         }
 
         $transaction->setOldValue($old);
         $transaction->setNewValue($new);
       }
 
     }
 
     $task->save();
     foreach ($transactions as $transaction) {
       $transaction->setTaskID($task->getID());
       $transaction->save();
     }
 
     $email_to[] = $task->getOwnerPHID();
     $email_cc = array_merge(
       $email_cc,
       $task->getCCPHIDs());
 
     // TODO: Do this offline via timeline
     PhabricatorSearchManiphestIndexer::indexTask($task);
 
     $this->sendEmail($task, $transactions, $email_to, $email_cc);
   }
 
   protected function getSubjectPrefix() {
     return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
   }
 
   private function sendEmail($task, $transactions, $email_to, $email_cc) {
     $email_to = array_filter(array_unique($email_to));
     $email_cc = array_filter(array_unique($email_cc));
 
     $phids = array();
     foreach ($transactions as $transaction) {
       foreach ($transaction->extractPHIDs() as $phid) {
         $phids[$phid] = true;
       }
     }
     foreach ($email_to as $phid) {
       $phids[$phid] = true;
     }
     foreach ($email_cc as $phid) {
       $phids[$phid] = true;
     }
     $phids = array_keys($phids);
 
     $handles = id(new PhabricatorObjectHandleData($phids))
       ->loadHandles();
 
     $view = new ManiphestTransactionDetailView();
     $view->setTransactionGroup($transactions);
     $view->setHandles($handles);
     list($action, $body) = $view->renderForEmail($with_date = false);
 
     $is_create = false;
     foreach ($transactions as $transaction) {
       $type = $transaction->getTransactionType();
       if (($type == ManiphestTransactionType::TYPE_STATUS) &&
           ($transaction->getOldValue() === null) &&
           ($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) {
         $is_create = true;
       }
     }
 
     $task_uri = PhabricatorEnv::getURI('/T'.$task->getID());
 
     $reply_handler = $this->buildReplyHandler($task);
 
     if ($is_create) {
       $body .=
         "\n\n".
         "TASK DESCRIPTION\n".
         "  ".$task->getDescription();
     }
 
     $body .=
       "\n\n".
       "TASK DETAIL\n".
       "  ".$task_uri."\n";
 
     $reply_instructions = $reply_handler->getReplyHandlerInstructions();
     if ($reply_instructions) {
       $body .=
         "\n".
         "REPLY HANDLER ACTIONS\n".
         "  ".$reply_instructions."\n";
     }
 
     $thread_id = '<maniphest-task-'.$task->getPHID().'>';
     $task_id = $task->getID();
     $title = $task->getTitle();
     $prefix = $this->getSubjectPrefix();
     $subject = trim("{$prefix} [{$action}] T{$task_id}: {$title}");
 
     $template = id(new PhabricatorMetaMTAMail())
       ->setSubject($subject)
       ->setFrom($transaction->getAuthorPHID())
+      ->setParentMessageID($this->parentMessageID)
       ->addHeader('Thread-Topic', 'Maniphest Task '.$task->getID())
       ->setThreadID($thread_id, $is_create)
       ->setRelatedPHID($task->getPHID())
       ->setBody($body);
 
     $mails = $reply_handler->multiplexMail(
       $template,
       array_select_keys($handles, $email_to),
       array_select_keys($handles, $email_cc));
 
     foreach ($mails as $mail) {
       $mail->saveAndSend();
     }
   }
 
   public function buildReplyHandler(ManiphestTask $task) {
     $handler_class = PhabricatorEnv::getEnvConfig(
       'metamta.maniphest.reply-handler');
 
     $handler_object = newv($handler_class, array());
     $handler_object->setMailReceiver($task);
 
     return $handler_object;
   }
 }
diff --git a/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php b/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
index 004b971c5a..b86f5685f7 100644
--- a/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
@@ -1,121 +1,121 @@
 <?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 ManiphestReplyHandler extends PhabricatorMailReplyHandler {
 
   public function validateMailReceiver($mail_receiver) {
     if (!($mail_receiver instanceof ManiphestTask)) {
       throw new Exception("Mail receiver is not a ManiphestTask!");
     }
   }
 
   public function getPrivateReplyHandlerEmailAddress(
     PhabricatorObjectHandle $handle) {
     return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
   }
 
   public function getReplyHandlerDomain() {
     return PhabricatorEnv::getEnvConfig(
       'metamta.maniphest.reply-handler-domain');
   }
 
   public function getReplyHandlerInstructions() {
     if ($this->supportsReplies()) {
       return "Reply to comment or attach files, or !close, !claim, or ".
              "!unsubscribe.";
     } else {
       return null;
     }
   }
 
   public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
 
     $task = $this->getMailReceiver();
     $user = $this->getActor();
 
     $body = $mail->getCleanTextBody();
     $body = trim($body);
 
     $lines = explode("\n", trim($body));
     $first_line = head($lines);
 
     $command = null;
     $matches = null;
     if (preg_match('/^!(\w+)/', $first_line, $matches)) {
       $lines = array_slice($lines, 1);
       $body = implode("\n", $lines);
       $body = trim($body);
 
       $command = $matches[1];
     }
 
     $xactions = array();
 
     $files = $mail->getAttachments();
     if ($files) {
       $file_xaction = new ManiphestTransaction();
       $file_xaction->setAuthorPHID($user->getPHID());
       $file_xaction->setTransactionType(ManiphestTransactionType::TYPE_ATTACH);
 
       $phid_type = PhabricatorPHIDConstants::PHID_TYPE_FILE;
       $new = $task->getAttached();
       foreach ($files as $file_phid) {
         $new[$phid_type][$file_phid] = array();
       }
 
       $file_xaction->setNewValue($new);
       $xactions[] = $file_xaction;
     }
 
     $ttype = ManiphestTransactionType::TYPE_NONE;
     $new_value = null;
     switch ($command) {
       case 'close':
         $ttype = ManiphestTransactionType::TYPE_STATUS;
         $new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
         break;
       case 'claim':
         $ttype = ManiphestTransactionType::TYPE_OWNER;
         $new_value = $user->getPHID();
         break;
       case 'unsubscribe':
         $ttype = ManiphestTransactionType::TYPE_CCS;
         $ccs = $task->getCCPHIDs();
         foreach ($ccs as $k => $phid) {
           if ($phid == $user->getPHID()) {
             unset($ccs[$k]);
           }
         }
         $new_value = array_values($ccs);
         break;
     }
 
     $xaction = new ManiphestTransaction();
     $xaction->setAuthorPHID($user->getPHID());
     $xaction->setTransactionType($ttype);
     $xaction->setNewValue($new_value);
     $xaction->setComments($body);
 
     $xactions[] = $xaction;
 
     $editor = new ManiphestTransactionEditor();
+    $editor->setParentMessageID($mail->getMessageID());
     $editor->applyTransactions($task, $xactions);
-
   }
 
 }
diff --git a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
index eaca3beb30..7ea4ecec31 100644
--- a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
@@ -1,369 +1,395 @@
 <?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.
  */
 
 /**
  * See #394445 for an explanation of why this thing even exists.
  */
 class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
 
   const STATUS_QUEUE = 'queued';
   const STATUS_SENT  = 'sent';
   const STATUS_FAIL  = 'fail';
 
   const MAX_RETRIES   = 250;
   const RETRY_DELAY   = 5;
 
   protected $parameters;
   protected $status;
   protected $message;
   protected $retryCount;
   protected $nextRetry;
   protected $relatedPHID;
 
   public function __construct() {
 
     $this->status     = self::STATUS_QUEUE;
     $this->retryCount = 0;
     $this->nextRetry  = time();
     $this->parameters = array();
 
     parent::__construct();
   }
 
   public function getConfiguration() {
     return array(
       self::CONFIG_SERIALIZATION => array(
         'parameters'  => self::SERIALIZATION_JSON,
       ),
     ) + parent::getConfiguration();
   }
 
   protected function setParam($param, $value) {
     $this->parameters[$param] = $value;
     return $this;
   }
 
   protected function getParam($param) {
     return idx($this->parameters, $param);
   }
 
+  /**
+   * In Gmail, conversations will be broken if you reply to a thread and the
+   * server sends back a response without referencing your Message-ID, even if
+   * it references a Message-ID earlier in the thread. To avoid this, use the
+   * parent email's message ID explicitly if it's available. This overwrites the
+   * "In-Reply-To" and "References" headers we would otherwise generate. This
+   * needs to be set whenever an action is triggered by an email message. See
+   * T251 for more details.
+   *
+   * @param   string The "Message-ID" of the email which precedes this one.
+   * @return  this
+   */
+  public function setParentMessageID($id) {
+    $this->setParam('parent-message-id', $id);
+    return $this;
+  }
+
+  public function getParentMessageID() {
+    return $this->getParam('parent-message-id');
+  }
+
   public function getSubject() {
     return $this->getParam('subject');
   }
 
   public function addTos(array $phids) {
     $this->setParam('to', $phids);
     return $this;
   }
 
   public function addCCs(array $phids) {
     $this->setParam('cc', $phids);
     return $this;
   }
 
   public function addHeader($name, $value) {
     $this->parameters['headers'][$name] = $value;
     return $this;
   }
 
   public function setFrom($from) {
     $this->setParam('from', $from);
     return $this;
   }
 
   public function setReplyTo($reply_to) {
     $this->setParam('reply-to', $reply_to);
     return $this;
   }
 
   public function setSubject($subject) {
     $this->setParam('subject', $subject);
     return $this;
   }
 
   public function setBody($body) {
     $this->setParam('body', $body);
     return $this;
   }
 
   public function getBody() {
     return $this->getParam('body');
   }
 
   public function setIsHTML($html) {
     $this->setParam('is-html', $html);
     return $this;
   }
 
   public function getSimulatedFailureCount() {
     return nonempty($this->getParam('simulated-failures'), 0);
   }
 
   public function setSimulatedFailureCount($count) {
     $this->setParam('simulated-failures', $count);
     return $this;
   }
 
   /**
    * Use this method to set an ID used for message threading. MetaMTA will
    * set appropriate headers (Message-ID, In-Reply-To, References and
    * Thread-Index) based on the capabilities of the underlying mailer.
    *
    * @param string  Unique identifier, appropriate for use in a Message-ID,
    *                In-Reply-To or References headers.
    * @param bool    If true, indicates this is the first message in the thread.
    * @return this
    */
   public function setThreadID($thread_id, $is_first_message = false) {
     $this->setParam('thread-id', $thread_id);
     $this->setParam('is-first-message', $is_first_message);
     return $this;
   }
 
   /**
    * Save a newly created mail to the database and attempt to send it
    * immediately if the server is configured for immediate sends. When
    * applications generate new mail they should generally use this method to
    * deliver it. If the server doesn't use immediate sends, this has the same
    * effect as calling save(): the mail will eventually be delivered by the
    * MetaMTA daemon.
    *
    * @return this
    */
   public function saveAndSend() {
     $ret = $this->save();
 
     if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) {
       $this->sendNow();
     }
 
     return $ret;
   }
 
 
   public function buildDefaultMailer() {
     $class_name = PhabricatorEnv::getEnvConfig('metamta.mail-adapter');
     PhutilSymbolLoader::loadClass($class_name);
     return newv($class_name, array());
   }
 
   /**
    * Attempt to deliver an email immediately, in this process.
    *
    * @param bool  Try to deliver this email even if it has already been
    *              delivered or is in backoff after a failed delivery attempt.
    * @param PhabricatorMailImplementationAdapter Use a specific mail adapter,
    *              instead of the default.
    *
    * @return void
    */
   public function sendNow(
     $force_send = false,
     PhabricatorMailImplementationAdapter $mailer = null) {
 
     if ($mailer === null) {
       $mailer = $this->buildDefaultMailer();
     }
 
     if (!$force_send) {
       if ($this->getStatus() != self::STATUS_QUEUE) {
         throw new Exception("Trying to send an already-sent mail!");
       }
 
       if (time() < $this->getNextRetry()) {
         throw new Exception("Trying to send an email before next retry!");
       }
     }
 
     try {
       $parameters = $this->parameters;
       $phids = array();
       foreach ($parameters as $key => $value) {
         switch ($key) {
           case 'from':
           case 'to':
           case 'cc':
             if (!is_array($value)) {
               $value = array($value);
             }
             foreach (array_filter($value) as $phid) {
               $phids[] = $phid;
             }
             break;
         }
       }
 
       $handles = id(new PhabricatorObjectHandleData($phids))
         ->loadHandles();
 
       $params = $this->parameters;
       $default = PhabricatorEnv::getEnvConfig('metamta.default-address');
       if (empty($params['from'])) {
         $mailer->setFrom($default);
       } else if (!PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) {
         $from = $params['from'];
         $handle = $handles[$from];
         if (empty($params['reply-to'])) {
           $params['reply-to'] = $handle->getEmail();
           $params['reply-to-name'] = $handle->getFullName();
         }
         $mailer->setFrom(
           $default,
           $handle->getFullName());
         unset($params['from']);
       }
 
       $is_first = !empty($params['is-first-message']);
       unset($params['is-first-message']);
 
       $reply_to_name = idx($params, 'reply-to-name', '');
       unset($params['reply-to-name']);
 
       foreach ($params as $key => $value) {
         switch ($key) {
           case 'from':
             $mailer->setFrom($handles[$value]->getEmail());
             break;
           case 'reply-to':
             $mailer->addReplyTo($value, $reply_to_name);
             break;
           case 'to':
             $emails = array();
             foreach ($value as $phid) {
               $emails[] = $handles[$phid]->getEmail();
             }
             $mailer->addTos($emails);
             break;
           case 'cc':
             $emails = array();
             foreach ($value as $phid) {
               $emails[] = $handles[$phid]->getEmail();
             }
             $mailer->addCCs($emails);
             break;
           case 'headers':
             foreach ($value as $header_key => $header_value) {
               $mailer->addHeader($header_key, $header_value);
             }
             break;
           case 'body':
             $mailer->setBody($value);
             break;
           case 'subject':
             $mailer->setSubject($value);
             break;
           case 'is-html':
             if ($value) {
               $mailer->setIsHTML(true);
             }
             break;
           case 'thread-id':
             if ($is_first && $mailer->supportsMessageIDHeader()) {
               $mailer->addHeader('Message-ID',  $value);
             } else {
-              $mailer->addHeader('In-Reply-To', $value);
-              $mailer->addHeader('References',  $value);
+              $in_reply_to = $value;
+              $parent_id = $this->getParentMessageID();
+              if ($parent_id) {
+                $in_reply_to = $parent_id;
+              }
+              $mailer->addHeader('In-Reply-To', $in_reply_to);
+              $mailer->addHeader('References',  $in_reply_to);
             }
             $thread_index = $this->generateThreadIndex($value, $is_first);
             $mailer->addHeader('Thread-Index', $thread_index);
             break;
           default:
             // Just discard.
         }
       }
 
       $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
 
     } catch (Exception $ex) {
       $this->setStatus(self::STATUS_FAIL);
       $this->setMessage($ex->getMessage());
       $this->save();
       return;
     }
 
     if ($this->getRetryCount() < $this->getSimulatedFailureCount()) {
       $ok = false;
       $error = 'Simulated failure.';
     } else {
       try {
         $ok = $mailer->send();
         $error = null;
       } catch (Exception $ex) {
         $ok = false;
         $error = $ex->getMessage()."\n".$ex->getTraceAsString();
       }
     }
 
     if (!$ok) {
       $this->setMessage($error);
       if ($this->getRetryCount() > self::MAX_RETRIES) {
         $this->setStatus(self::STATUS_FAIL);
       } else {
         $this->setRetryCount($this->getRetryCount() + 1);
         $next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY);
         $this->setNextRetry($next_retry);
       }
     } else {
       $this->setStatus(self::STATUS_SENT);
     }
 
     $this->save();
   }
 
   public static function getReadableStatus($status_code) {
     static $readable = array(
       self::STATUS_QUEUE => "Queued for Delivery",
       self::STATUS_FAIL  => "Delivery Failed",
       self::STATUS_SENT  => "Sent",
     );
     $status_code = coalesce($status_code, '?');
     return idx($readable, $status_code, $status_code);
   }
 
   private function generateThreadIndex($seed, $is_first_mail) {
     // When threading, Outlook ignores the 'References' and 'In-Reply-To'
     // headers that most clients use. Instead, it uses a custom 'Thread-Index'
     // header. The format of this header is something like this (from
     // camel-exchange-folder.c in Evolution Exchange):
 
     /* A new post to a folder gets a 27-byte-long thread index. (The value
      * is apparently unique but meaningless.) Each reply to a post gets a
      * 32-byte-long thread index whose first 27 bytes are the same as the
      * parent's thread index. Each reply to any of those gets a
      * 37-byte-long thread index, etc. The Thread-Index header contains a
      * base64 representation of this value.
      */
 
     // The specific implementation uses a 27-byte header for the first email
     // a recipient receives, and a random 5-byte suffix (32 bytes total)
     // thereafter. This means that all the replies are (incorrectly) siblings,
     // but it would be very difficult to keep track of the entire tree and this
     // gets us reasonable client behavior.
 
     $base = substr(md5($seed), 0, 27);
     if (!$is_first_mail) {
       // Not totally sure, but it seems like outlook orders replies by
       // thread-index rather than timestamp, so to get these to show up in the
       // right order we use the time as the last 4 bytes.
       $base .= ' '.pack('N', time());
     }
 
     return base64_encode($base);
   }
 
 }
diff --git a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
index cafaf174a6..cdd978feef 100644
--- a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
@@ -1,147 +1,151 @@
 <?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 PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
 
   protected $headers = array();
   protected $bodies = array();
   protected $attachments = array();
 
   protected $relatedPHID;
   protected $authorPHID;
   protected $message;
 
   public function getConfiguration() {
     return array(
       self::CONFIG_SERIALIZATION => array(
         'headers'     => self::SERIALIZATION_JSON,
         'bodies'      => self::SERIALIZATION_JSON,
         'attachments' => self::SERIALIZATION_JSON,
       ),
     ) + parent::getConfiguration();
   }
 
   public function setHeaders(array $headers) {
     // Normalize headers to lowercase.
     $normalized = array();
     foreach ($headers as $name => $value) {
       $normalized[strtolower($name)] = $value;
     }
     $this->headers = $normalized;
     return $this;
   }
 
+  public function getMessageID() {
+    return idx($this->headers, 'message-id');
+  }
+
   public function processReceivedMail() {
     $to = idx($this->headers, 'to');
 
     // Accept a match either at the beginning of the address or after an open
     // angle bracket, as in:
     //    "some display name" <D1+xyz+asdf@example.com>
     $matches = null;
     $ok = preg_match(
       '/(?:^|<)((?:D|T)\d+)\+(\d+)\+([a-f0-9]{16})@/U',
       $to,
       $matches);
 
     if (!$ok) {
       return $this->setMessage("Unrecognized 'to' format: {$to}")->save();
     }
 
     $receiver_name = $matches[1];
     $user_id = $matches[2];
     $hash = $matches[3];
 
     $user = id(new PhabricatorUser())->load($user_id);
     if (!$user) {
       return $this->setMessage("Invalid user '{$user_id}'")->save();
     }
 
     if ($user->getIsDisabled()) {
       return $this->setMessage("User '{$user_id}' is disabled")->save();
     }
 
     $this->setAuthorPHID($user->getPHID());
 
     $receiver = self::loadReceiverObject($receiver_name);
     if (!$receiver) {
       return $this->setMessage("Invalid object '{$receiver_name}'")->save();
     }
 
     $this->setRelatedPHID($receiver->getPHID());
 
     $expect_hash = self::computeMailHash(
       $receiver->getMailKey(),
       $user->getPHID());
     if ($expect_hash != $hash) {
       return $this->setMessage("Invalid mail hash!")->save();
     }
 
     if ($receiver instanceof ManiphestTask) {
       $editor = new ManiphestTransactionEditor();
       $handler = $editor->buildReplyHandler($receiver);
     } else if ($receiver instanceof DifferentialRevision) {
       $handler = DifferentialMail::newReplyHandlerForRevision($receiver);
     }
 
     $handler->setActor($user);
     $handler->receiveEmail($this);
 
     $this->setMessage('OK');
 
     return $this->save();
   }
 
   public function getCleanTextBody() {
     $body = idx($this->bodies, 'text');
 
     $parser = new PhabricatorMetaMTAEmailBodyParser($body);
     return $parser->stripQuotedText();
   }
 
   public static function loadReceiverObject($receiver_name) {
     if (!$receiver_name) {
       return null;
     }
 
     $receiver_type = $receiver_name[0];
     $receiver_id   = substr($receiver_name, 1);
 
     $class_obj = null;
     switch ($receiver_type) {
       case 'T':
         $class_obj = newv('ManiphestTask', array());
         break;
       case 'D':
         $class_obj = newv('DifferentialRevision', array());
         break;
       default:
         return null;
     }
 
     return $class_obj->load($receiver_id);
   }
 
   public static function computeMailHash($mail_key, $phid) {
     $global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
 
     $hash = sha1($mail_key.$global_mail_key.$phid);
     return substr($hash, 0, 16);
   }
 
 
 }