diff --git a/src/applications/conpherence/controller/ConpherenceUpdateController.php b/src/applications/conpherence/controller/ConpherenceUpdateController.php
index 4fe4ddcb1e..cf193f4419 100644
--- a/src/applications/conpherence/controller/ConpherenceUpdateController.php
+++ b/src/applications/conpherence/controller/ConpherenceUpdateController.php
@@ -1,209 +1,187 @@
 <?php
 
 /**
  * @group conpherence
  */
 final class ConpherenceUpdateController extends
   ConpherenceController {
 
   private $conpherenceID;
 
   public function setConpherenceID($conpherence_id) {
     $this->conpherenceID = $conpherence_id;
     return $this;
   }
   public function getConpherenceID() {
     return $this->conpherenceID;
   }
   public function willProcessRequest(array $data) {
     $this->setConpherenceID(idx($data, 'id'));
   }
 
   public function processRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
     $conpherence_id = $this->getConpherenceID();
     if (!$conpherence_id) {
       return new Aphront404Response();
     }
 
     $conpherence = id(new ConpherenceThreadQuery())
       ->setViewer($user)
       ->withIDs(array($conpherence_id))
       ->executeOne();
     $supported_formats = PhabricatorFile::getTransformableImageFormats();
 
     $updated = false;
     $error_view = null;
     $e_file = array();
     $errors = array();
     if ($request->isFormPost()) {
       $content_source = PhabricatorContentSource::newForSource(
         PhabricatorContentSource::SOURCE_WEB,
         array(
           'ip' => $request->getRemoteAddr()
         ));
+      $editor = id(new ConpherenceEditor())
+        ->setContentSource($content_source)
+        ->setActor($user);
 
       $action = $request->getStr('action');
       switch ($action) {
         case 'message':
           $message = $request->getStr('text');
-          $files = array();
-          $file_phids =
-            PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
-              array($message)
-            );
-          if ($file_phids) {
-            $files = id(new PhabricatorFileQuery())
-              ->setViewer($user)
-              ->withPHIDs($file_phids)
-              ->execute();
-          }
-          $xactions = array();
-          if ($files) {
-            $xactions[] = id(new ConpherenceTransaction())
-              ->setTransactionType(ConpherenceTransactionType::TYPE_FILES)
-              ->setNewValue(array('+' => mpull($files, 'getPHID')));
-          }
-          $xactions[] = id(new ConpherenceTransaction())
-            ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
-            ->attachComment(
-              id(new ConpherenceTransactionComment())
-              ->setContent($message)
-              ->setConpherencePHID($conpherence->getPHID())
-            );
+          $xactions = $editor->generateTransactionsFromText(
+            $conpherence,
+            $message
+          );
           $time = time();
           $conpherence->openTransaction();
-          $xactions = id(new ConpherenceEditor())
-            ->setContentSource($content_source)
-            ->setActor($user)
-            ->applyTransactions($conpherence, $xactions);
+          $xactions = $editor->applyTransactions($conpherence, $xactions);
           $last_xaction = end($xactions);
           $xaction_phid = $last_xaction->getPHID();
           $behind = ConpherenceParticipationStatus::BEHIND;
           $up_to_date = ConpherenceParticipationStatus::UP_TO_DATE;
           $participants = $conpherence->getParticipants();
           foreach ($participants as $phid => $participant) {
             if ($phid != $user->getPHID()) {
               if ($participant->getParticipationStatus() != $behind) {
                 $participant->setBehindTransactionPHID($xaction_phid);
               }
               $participant->setParticipationStatus($behind);
               $participant->setDateTouched($time);
             } else {
               $participant->setParticipationStatus($up_to_date);
               $participant->setDateTouched($time);
             }
             $participant->save();
           }
           $updated = $conpherence->saveTransaction();
           break;
         case 'metadata':
           $xactions = array();
           $images = $request->getArr('image');
           if ($images) {
             // just take the first one
             $file_phid = reset($images);
             $file = id(new PhabricatorFileQuery())
               ->setViewer($user)
               ->withPHIDs(array($file_phid))
               ->executeOne();
             $okay = $file->isTransformableImage();
             if ($okay) {
               $xformer = new PhabricatorImageTransformer();
               $xformed = $xformer->executeThumbTransform(
                 $file,
                 $x = 50,
                 $y = 50);
               $image_phid = $xformed->getPHID();
               $xactions[] = id(new ConpherenceTransaction())
                 ->setTransactionType(ConpherenceTransactionType::TYPE_PICTURE)
                 ->setNewValue($image_phid);
             } else {
               $e_file[] = $file;
               $errors[] =
                 pht('This server only supports these image formats: %s.',
                   implode(', ', $supported_formats));
             }
           }
           $title = $request->getStr('title');
           if ($title != $conpherence->getTitle()) {
             $xactions[] = id(new ConpherenceTransaction())
               ->setTransactionType(ConpherenceTransactionType::TYPE_TITLE)
               ->setNewValue($title);
           }
 
           if ($xactions) {
             $conpherence->openTransaction();
-            $xactions = id(new ConpherenceEditor())
-              ->setContentSource($content_source)
-              ->setActor($user)
+            $xactions = $editor
               ->setContinueOnNoEffect(true)
               ->applyTransactions($conpherence, $xactions);
             $updated = $conpherence->saveTransaction();
           } else if (empty($errors)) {
             $errors[] = pht(
               'That was a non-update. Try cancel.'
             );
           }
           break;
         default:
           throw new Exception('Unknown action: '.$action);
           break;
       }
     }
 
     if ($updated) {
       return id(new AphrontRedirectResponse())->setURI(
         $this->getApplicationURI($conpherence_id.'/')
       );
     }
 
     if ($errors) {
       $error_view = id(new AphrontErrorView())
         ->setTitle(pht('Errors editing conpherence.'))
         ->setInsideDialogue(true)
         ->setErrors($errors);
     }
 
     $form = id(new AphrontFormLayoutView())
       ->appendChild(
         id(new AphrontFormTextControl())
         ->setLabel(pht('Title'))
         ->setName('title')
         ->setValue($conpherence->getTitle())
       )
       ->appendChild(
         id(new AphrontFormMarkupControl())
         ->setLabel(pht('Image'))
         ->setValue(phutil_render_tag(
           'img',
           array(
             'src' => $conpherence->loadImageURI(),
           ))
         )
       )
       ->appendChild(
         id(new AphrontFormDragAndDropUploadControl())
         ->setLabel(pht('Change Image'))
         ->setName('image')
         ->setValue($e_file)
         ->setCaption('Supported formats: '.implode(', ', $supported_formats))
         );
 
     require_celerity_resource('conpherence-update-css');
     return id(new AphrontDialogResponse())
       ->setDialog(
         id(new AphrontDialogView())
         ->setUser($user)
         ->setTitle(pht('Update Conpherence'))
         ->setWidth(AphrontDialogView::WIDTH_FORM)
         ->setSubmitURI($this->getApplicationURI('update/'.$conpherence_id.'/'))
         ->addHiddenInput('action', 'metadata')
         ->appendChild($error_view)
         ->appendChild($form)
         ->addSubmitButton()
         ->addCancelButton($this->getApplicationURI($conpherence->getID().'/'))
       );
   }
 }
diff --git a/src/applications/conpherence/editor/ConpherenceEditor.php b/src/applications/conpherence/editor/ConpherenceEditor.php
index f993696ed0..9304e0f14e 100644
--- a/src/applications/conpherence/editor/ConpherenceEditor.php
+++ b/src/applications/conpherence/editor/ConpherenceEditor.php
@@ -1,181 +1,217 @@
 <?php
 
 /**
  * @group conpherence
  */
 final class ConpherenceEditor extends PhabricatorApplicationTransactionEditor {
 
+  public function generateTransactionsFromText(
+    ConpherenceThread $conpherence,
+    $text) {
+
+    $files = array();
+    $file_phids =
+      PhabricatorMarkupEngine::extractFilePHIDsFromEmbeddedFiles(
+        array($text)
+      );
+    // Since these are extracted from text, we might be re-including the
+    // same file -- e.g. a mock under discussion. Filter files we
+    // already have.
+    $existing_file_phids = $conpherence->getFilePHIDs();
+    $file_phids = array_diff($file_phids, $existing_file_phids);
+    if ($file_phids) {
+      $files = id(new PhabricatorFileQuery())
+        ->setViewer($this->getActor())
+        ->withPHIDs($file_phids)
+        ->execute();
+    }
+    $xactions = array();
+    if ($files) {
+      $xactions[] = id(new ConpherenceTransaction())
+        ->setTransactionType(ConpherenceTransactionType::TYPE_FILES)
+        ->setNewValue(array('+' => mpull($files, 'getPHID')));
+    }
+    $xactions[] = id(new ConpherenceTransaction())
+      ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
+      ->attachComment(
+        id(new ConpherenceTransactionComment())
+        ->setContent($text)
+        ->setConpherencePHID($conpherence->getPHID())
+      );
+    return $xactions;
+  }
+
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
     $types[] = PhabricatorTransactions::TYPE_COMMENT;
 
     $types[] = ConpherenceTransactionType::TYPE_TITLE;
     $types[] = ConpherenceTransactionType::TYPE_PICTURE;
     $types[] = ConpherenceTransactionType::TYPE_PARTICIPANTS;
     $types[] = ConpherenceTransactionType::TYPE_FILES;
 
     return $types;
   }
 
   protected function getCustomTransactionOldValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ConpherenceTransactionType::TYPE_TITLE:
         return $object->getTitle();
       case ConpherenceTransactionType::TYPE_PICTURE:
         return $object->getImagePHID();
       case ConpherenceTransactionType::TYPE_PARTICIPANTS:
         return $object->getParticipantPHIDs();
       case ConpherenceTransactionType::TYPE_FILES:
         return $object->getFilePHIDs();
     }
   }
 
   protected function getCustomTransactionNewValue(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ConpherenceTransactionType::TYPE_TITLE:
       case ConpherenceTransactionType::TYPE_PICTURE:
         return $xaction->getNewValue();
       case ConpherenceTransactionType::TYPE_PARTICIPANTS:
       case ConpherenceTransactionType::TYPE_FILES:
         return $this->getPHIDTransactionNewValue($xaction);
     }
   }
 
   protected function applyCustomInternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ConpherenceTransactionType::TYPE_TITLE:
         $object->setTitle($xaction->getNewValue());
         break;
       case ConpherenceTransactionType::TYPE_PICTURE:
         $object->setImagePHID($xaction->getNewValue());
         break;
     }
   }
 
   /**
    * For now this only supports adding more files and participants.
    */
   protected function applyCustomExternalTransaction(
     PhabricatorLiskDAO $object,
     PhabricatorApplicationTransaction $xaction) {
 
     switch ($xaction->getTransactionType()) {
       case ConpherenceTransactionType::TYPE_FILES:
         $editor = id(new PhabricatorEdgeEditor())
           ->setActor($this->getActor());
         $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
         foreach ($xaction->getNewValue() as $file_phid) {
           $editor->addEdge(
             $object->getPHID(),
             $edge_type,
             $file_phid
           );
         }
         $editor->save();
         break;
       case ConpherenceTransactionType::TYPE_PARTICIPANTS:
         foreach ($xaction->getNewValue() as $participant) {
           if ($participant == $this->getActor()->getPHID()) {
             $status = ConpherenceParticipationStatus::UP_TO_DATE;
           } else {
             $status = ConpherenceParticipationStatus::BEHIND;
           }
           id(new ConpherenceParticipant())
             ->setConpherencePHID($object->getPHID())
             ->setParticipantPHID($participant)
             ->setParticipationStatus($status)
             ->setDateTouched(time())
             ->setBehindTransactionPHID($xaction->getPHID())
             ->save();
         }
         break;
      }
   }
 
   protected function mergeTransactions(
     PhabricatorApplicationTransaction $u,
     PhabricatorApplicationTransaction $v) {
 
     $type = $u->getTransactionType();
     switch ($type) {
       case ConpherenceTransactionType::TYPE_TITLE:
       case ConpherenceTransactionType::TYPE_PICTURE:
         return $v;
       case ConpherenceTransactionType::TYPE_FILES:
       case ConpherenceTransactionType::TYPE_PARTICIPANTS:
         return $this->mergePHIDTransactions($u, $v);
     }
 
     return parent::mergeTransactions($u, $v);
   }
 
   protected function supportsMail() {
     return true;
   }
 
   protected function buildReplyHandler(PhabricatorLiskDAO $object) {
     return id(new ConpherenceReplyHandler())
       ->setActor($this->getActor())
       ->setMailReceiver($object);
   }
 
   protected function buildMailTemplate(PhabricatorLiskDAO $object) {
     $id = $object->getID();
     $title = $object->getTitle();
     if (!$title) {
       $title = pht(
         '%s sent you a message.',
         $this->getActor()->getUserName()
       );
     }
     $phid = $object->getPHID();
 
     return id(new PhabricatorMetaMTAMail())
       ->setSubject("E{$id}: {$title}")
       ->addHeader('Thread-Topic', "E{$id}: {$phid}");
   }
 
   protected function getMailTo(PhabricatorLiskDAO $object) {
     $participants = $object->getParticipants();
     return array_keys($participants);
   }
 
   protected function getMailCC(PhabricatorLiskDAO $object) {
     return array();
   }
 
   protected function buildMailBody(
     PhabricatorLiskDAO $object,
     array $xactions) {
 
     $body = parent::buildMailBody($object, $xactions);
     $body->addTextSection(
       pht('CONPHERENCE DETAIL'),
       PhabricatorEnv::getProductionURI('/conpherence/'.$object->getID().'/'));
 
     return $body;
   }
 
   protected function getMailSubjectPrefix() {
     return PhabricatorEnv::getEnvConfig('metamta.conpherence.subject-prefix');
   }
 
   protected function supportsFeed() {
     return false;
   }
 
   protected function supportsSearch() {
     return false;
   }
 
 }
diff --git a/src/applications/conpherence/mail/ConpherenceReplyHandler.php b/src/applications/conpherence/mail/ConpherenceReplyHandler.php
index 60aa0e152e..f7de36cb25 100644
--- a/src/applications/conpherence/mail/ConpherenceReplyHandler.php
+++ b/src/applications/conpherence/mail/ConpherenceReplyHandler.php
@@ -1,85 +1,80 @@
 <?php
 
 /**
  * @group conpherence
  */
 final class ConpherenceReplyHandler extends PhabricatorMailReplyHandler {
 
   public function validateMailReceiver($mail_receiver) {
     if (!($mail_receiver instanceof ConpherenceThread)) {
       throw new Exception("Mail receiver is not a ConpherenceThread!");
     }
   }
 
   public function getPrivateReplyHandlerEmailAddress(
     PhabricatorObjectHandle $handle) {
     return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'E');
   }
 
   public function getPublicReplyHandlerEmailAddress() {
     return $this->getDefaultPublicReplyHandlerEmailAddress('E');
   }
 
   public function getReplyHandlerInstructions() {
     if ($this->supportsReplies()) {
       return pht('Reply to comment and attach files.');
     } else {
       return null;
     }
   }
 
   protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
     $conpherence = $this->getMailReceiver();
     $user = $this->getActor();
     if (!$conpherence->getPHID()) {
       $conpherence
         ->attachParticipants(array())
         ->attachFilePHIDs(array());
     } else {
       $edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
       $file_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
         $conpherence->getPHID(),
         $edge_type
       );
       $conpherence->attachFilePHIDs($file_phids);
       $participants = id(new ConpherenceParticipant())
         ->loadAllWhere('conpherencePHID = %s', $conpherence->getPHID());
       $participants = mpull($participants, null, 'getParticipantPHID');
       $conpherence->attachParticipants($participants);
     }
 
-    $body = $mail->getCleanTextBody();
-    $body = trim($body);
-    $file_phids = $mail->getAttachments();
-    $body = $this->enhanceBodyWithAttachments($body, $file_phids);
-
-    $xactions = array();
-    if ($file_phids) {
-      $xactions[] = id(new ConpherenceTransaction())
-        ->setTransactionType(ConpherenceTransactionType::TYPE_FILES)
-        ->setNewValue(array('+' => $file_phids));
-    }
-    $xactions[] = id(new ConpherenceTransaction())
-      ->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
-      ->attachComment(
-        id(new ConpherenceTransactionComment())
-        ->setContent($body)
-        ->setConpherencePHID($conpherence->getPHID())
-      );
-
     $content_source = PhabricatorContentSource::newForSource(
       PhabricatorContentSource::SOURCE_EMAIL,
       array(
         'id' => $mail->getID(),
       ));
 
     $editor = id(new ConpherenceEditor())
       ->setActor($user)
       ->setContentSource($content_source)
-      ->setParentMessageID($mail->getMessageID())
-      ->applyTransactions($conpherence, $xactions);
+      ->setParentMessageID($mail->getMessageID());
+
+    $body = $mail->getCleanTextBody();
+    $body = trim($body);
+    $file_phids = $mail->getAttachments();
+    $body = $this->enhanceBodyWithAttachments(
+      $body,
+      $file_phids,
+      '{F%d}'
+    );
+    $xactions = $editor->generateTransactionsFromText(
+      $conpherence,
+      $body
+    );
+
+    $editor->applyTransactions($conpherence, $xactions);
 
     return null;
   }
 
 }
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index d6b862e23d..51784d7b96 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,320 +1,322 @@
 <?php
 
 abstract class PhabricatorMailReplyHandler {
 
   private $mailReceiver;
   private $actor;
   private $excludePHIDs = array();
 
   final public function setMailReceiver($mail_receiver) {
     $this->validateMailReceiver($mail_receiver);
     $this->mailReceiver = $mail_receiver;
     return $this;
   }
 
   final public function getMailReceiver() {
     return $this->mailReceiver;
   }
 
   final public function setActor(PhabricatorUser $actor) {
     $this->actor = $actor;
     return $this;
   }
 
   final public function getActor() {
     return $this->actor;
   }
 
   final public function setExcludeMailRecipientPHIDs(array $exclude) {
     $this->excludePHIDs = $exclude;
     return $this;
   }
 
   final public function getExcludeMailRecipientPHIDs() {
     return $this->excludePHIDs;
   }
 
   abstract public function validateMailReceiver($mail_receiver);
   abstract public function getPrivateReplyHandlerEmailAddress(
     PhabricatorObjectHandle $handle);
   public function getReplyHandlerDomain() {
     return PhabricatorEnv::getEnvConfig(
       'metamta.reply-handler-domain'
     );
   }
   abstract public function getReplyHandlerInstructions();
   abstract protected function receiveEmail(
     PhabricatorMetaMTAReceivedMail $mail);
 
   public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
     $error = $this->sanityCheckEmail($mail);
 
     if ($error) {
       if ($this->shouldSendErrorEmail($mail)) {
         $this->sendErrorEmail($error, $mail);
       }
       return null;
     }
 
     return $this->receiveEmail($mail);
   }
 
   private function sanityCheckEmail(PhabricatorMetaMTAReceivedMail $mail) {
     $body        = $mail->getCleanTextBody();
     $attachments = $mail->getAttachments();
 
     if (empty($body) && empty($attachments)) {
       return 'Empty email body. Email should begin with an !action and / or '.
              'text to comment. Inline replies and signatures are ignored.';
     }
 
     return null;
   }
 
   /**
    * Only send an error email if the user is talking to just Phabricator. We
    * can assume if there is only one To address it is a Phabricator address
    * since this code is running and everything.
    */
   private function shouldSendErrorEmail(PhabricatorMetaMTAReceivedMail $mail) {
     return (count($mail->getToAddresses()) == 1) &&
            (count($mail->getCCAddresses()) == 0);
   }
 
   private function sendErrorEmail($error,
                                   PhabricatorMetaMTAReceivedMail $mail) {
     $template = new PhabricatorMetaMTAMail();
     $template->setSubject('Exception: unable to process your mail request');
     $template->setBody($this->buildErrorMailBody($error, $mail));
     $template->setRelatedPHID($mail->getRelatedPHID());
     $phid = $this->getActor()->getPHID();
     $tos = array(
       $phid => PhabricatorObjectHandleData::loadOneHandle($phid)
     );
     $mails = $this->multiplexMail($template, $tos, array());
 
     foreach ($mails as $email) {
       $email->saveAndSend();
     }
 
     return true;
   }
 
   private function buildErrorMailBody($error,
                                       PhabricatorMetaMTAReceivedMail $mail) {
     $original_body = $mail->getRawTextBody();
 
     $main_body = <<<EOBODY
 Your request failed because an error was encoutered while processing it:
 
   ERROR: {$error}
 
   -- Original Body -------------------------------------------------------------
 
   {$original_body}
 
 EOBODY;
 
     $body = new PhabricatorMetaMTAMailBody();
     $body->addRawSection($main_body);
     $body->addReplySection($this->getReplyHandlerInstructions());
 
     return $body->render();
   }
 
   public function supportsPrivateReplies() {
     return (bool)$this->getReplyHandlerDomain() &&
            !$this->supportsPublicReplies();
   }
 
   public function supportsPublicReplies() {
     if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
       return false;
     }
     if (!$this->getReplyHandlerDomain()) {
       return false;
     }
     return (bool)$this->getPublicReplyHandlerEmailAddress();
   }
 
   final public function supportsReplies() {
     return $this->supportsPrivateReplies() ||
            $this->supportsPublicReplies();
   }
 
   public function getPublicReplyHandlerEmailAddress() {
     return null;
   }
 
   final public function getRecipientsSummary(
     array $to_handles,
     array $cc_handles) {
     assert_instances_of($to_handles, 'PhabricatorObjectHandle');
     assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
 
     $body = '';
 
     if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
       if ($to_handles) {
         $body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
       }
       if ($cc_handles) {
         $body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
       }
     }
 
     return $body;
   }
 
   final public function multiplexMail(
     PhabricatorMetaMTAMail $mail_template,
     array $to_handles,
     array $cc_handles) {
     assert_instances_of($to_handles, 'PhabricatorObjectHandle');
     assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
 
     $result = array();
 
     // If MetaMTA is configured to always multiplex, skip the single-email
     // case.
     if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
       // If private replies are not supported, simply send one email to all
       // recipients and CCs. This covers cases where we have no reply handler,
       // or we have a public reply handler.
       if (!$this->supportsPrivateReplies()) {
         $mail = clone $mail_template;
         $mail->addTos(mpull($to_handles, 'getPHID'));
         $mail->addCCs(mpull($cc_handles, 'getPHID'));
 
         if ($this->supportsPublicReplies()) {
           $reply_to = $this->getPublicReplyHandlerEmailAddress();
           $mail->setReplyTo($reply_to);
         }
 
         $result[] = $mail;
 
         return $result;
       }
     }
 
     $tos = mpull($to_handles, null, 'getPHID');
     $ccs = mpull($cc_handles, null, 'getPHID');
 
     // Merge all the recipients together. TODO: We could keep the CCs as real
     // CCs and send to a "noreply@domain.com" type address, but keep it simple
     // for now.
     $recipients = $tos + $ccs;
 
     // When multiplexing mail, explicitly include To/Cc information in the
     // message body and headers.
 
     $mail_template = clone $mail_template;
 
     $mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos));
     $mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs));
 
     $body = $mail_template->getBody();
     $body .= "\n";
     $body .= $this->getRecipientsSummary($to_handles, $cc_handles);
 
     foreach ($recipients as $phid => $recipient) {
       $mail = clone $mail_template;
       if (isset($to_handles[$phid])) {
         $mail->addTos(array($phid));
       } else if (isset($cc_handles[$phid])) {
         $mail->addCCs(array($phid));
       } else {
         // not good - they should be a to or a cc
         continue;
       }
 
       $mail->setBody($body);
 
       $reply_to = null;
       if (!$reply_to && $this->supportsPrivateReplies()) {
         $reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient);
       }
 
       if (!$reply_to && $this->supportsPublicReplies()) {
         $reply_to = $this->getPublicReplyHandlerEmailAddress();
       }
 
       if ($reply_to) {
         $mail->setReplyTo($reply_to);
       }
 
       $result[] = $mail;
     }
 
     return $result;
   }
 
   protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
 
     $receiver = $this->getMailReceiver();
     $receiver_id = $receiver->getID();
     $domain = $this->getReplyHandlerDomain();
 
     // We compute a hash using the object's own PHID to prevent an attacker
     // from blindly interacting with objects that they haven't ever received
     // mail about by just sending to D1@, D2@, etc...
     $hash = PhabricatorMetaMTAReceivedMail::computeMailHash(
       $receiver->getMailKey(),
       $receiver->getPHID());
 
     $address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
     return $this->getSingleReplyHandlerPrefix($address);
   }
 
   protected function getSingleReplyHandlerPrefix($address) {
     $single_handle_prefix = PhabricatorEnv::getEnvConfig(
       'metamta.single-reply-handler-prefix');
     return ($single_handle_prefix)
       ? $single_handle_prefix . '+' . $address
       : $address;
   }
 
   protected function getDefaultPrivateReplyHandlerEmailAddress(
     PhabricatorObjectHandle $handle,
     $prefix) {
 
     if ($handle->getType() != PhabricatorPHIDConstants::PHID_TYPE_USER) {
       // You must be a real user to get a private reply handler address.
       return null;
     }
 
     $receiver = $this->getMailReceiver();
     $receiver_id = $receiver->getID();
     $user_id = $handle->getAlternateID();
     $hash = PhabricatorMetaMTAReceivedMail::computeMailHash(
       $receiver->getMailKey(),
       $handle->getPHID());
     $domain = $this->getReplyHandlerDomain();
 
     $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
     return $this->getSingleReplyHandlerPrefix($address);
   }
 
-  final protected function enhanceBodyWithAttachments($body,
-                                                      array $attachments) {
+  final protected function enhanceBodyWithAttachments(
+    $body,
+    array $attachments,
+    $format = '- {F%d, layout=link}') {
     if (!$attachments) {
       return $body;
     }
 
     $files = id(new PhabricatorFile())
       ->loadAllWhere('phid in (%Ls)', $attachments);
 
     // if we have some text then double return before adding our file list
     if ($body) {
       $body .= "\n\n";
     }
 
     foreach ($files as $file) {
-      $file_str = sprintf('- {F%d, layout=link}', $file->getID());
+      $file_str = sprintf($format, $file->getID());
       $body .= $file_str."\n";
     }
 
     return rtrim($body);
   }
 
 }