diff --git a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
index bf4b962e2..3b798259b 100644
--- a/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
+++ b/src/applications/metamta/management/PhabricatorMailManagementShowOutboundWorkflow.php
@@ -1,161 +1,162 @@
 <?php
 
 final class PhabricatorMailManagementShowOutboundWorkflow
   extends PhabricatorMailManagementWorkflow {
 
   protected function didConstruct() {
     $this
       ->setName('show-outbound')
       ->setSynopsis('Show diagnostic details about outbound mail.')
       ->setExamples(
         '**show-outbound** --id 1 --id 2')
       ->setArguments(
         array(
           array(
             'name'    => 'id',
             'param'   => 'id',
             'help'    => 'Show details about outbound mail with given ID.',
             'repeat'  => true,
           ),
           array(
             'name' => 'dump-html',
             'help' => pht(
               'Dump the HTML body of the mail. You can redirect it to a '.
               'file and then open it in a browser.'),
           ),
         ));
   }
 
   public function execute(PhutilArgumentParser $args) {
     $console = PhutilConsole::getConsole();
 
     $ids = $args->getArg('id');
     if (!$ids) {
       throw new PhutilArgumentUsageException(
         "Use the '--id' flag to specify one or more messages to show.");
     }
 
     $messages = id(new PhabricatorMetaMTAMail())->loadAllWhere(
       'id IN (%Ld)',
       $ids);
 
     if ($ids) {
       $ids = array_fuse($ids);
       $missing = array_diff_key($ids, $messages);
       if ($missing) {
         throw new PhutilArgumentUsageException(
           'Some specified messages do not exist: '.
           implode(', ', array_keys($missing)));
       }
     }
 
     $last_key = last_key($messages);
     foreach ($messages as $message_key => $message) {
       if ($args->getArg('dump-html')) {
         $html_body = $message->getHTMLBody();
         if (strlen($html_body)) {
           $template =
             "<!doctype html><html><body>{$html_body}</body></html>";
           $console->writeOut("%s\n", $html_body);
         } else {
           $console->writeErr(
             "%s\n",
             pht('(This message has no HTML body.)'));
         }
         continue;
       }
 
       $info = array();
 
       $info[] = pht('PROPERTIES');
       $info[] = pht('ID: %d', $message->getID());
       $info[] = pht('Status: %s', $message->getStatus());
       $info[] = pht('Related PHID: %s', $message->getRelatedPHID());
       $info[] = pht('Message: %s', $message->getMessage());
 
       $info[] = null;
       $info[] = pht('PARAMETERS');
       $parameters = $message->getParameters();
       foreach ($parameters as $key => $value) {
         if ($key == 'body') {
           continue;
         }
 
         if ($key == 'html-body') {
           continue;
         }
 
         if ($key == 'headers') {
           continue;
         }
 
         if ($key == 'attachments') {
           continue;
         }
 
         if (!is_scalar($value)) {
           $value = json_encode($value);
         }
 
         $info[] = pht('%s: %s', $key, $value);
       }
 
       $info[] = null;
       $info[] = pht('HEADERS');
       foreach (idx($parameters, 'headers', array()) as $header) {
         list($name, $value) = $header;
         $info[] = "{$name}: {$value}";
       }
 
       $attachments = idx($parameters, 'attachments');
       if ($attachments) {
         $info[] = null;
         $info[] = pht('ATTACHMENTS');
         foreach ($attachments as $attachment) {
           $info[] = idx($attachment, 'filename', pht('Unnamed File'));
         }
       }
 
       $actors = $message->loadAllActors();
       $actors = array_select_keys(
         $actors,
         array_merge($message->getToPHIDs(), $message->getCcPHIDs()));
       $info[] = null;
       $info[] = pht('RECIPIENTS');
       foreach ($actors as $actor) {
         if ($actor->isDeliverable()) {
           $info[] = '  '.coalesce($actor->getName(), $actor->getPHID());
         } else {
           $info[] = '! '.coalesce($actor->getName(), $actor->getPHID());
-          foreach ($actor->getUndeliverableReasons() as $reason) {
-            $info[] = '    - '.$reason;
-          }
+        }
+        foreach ($actor->getDeliverabilityReasons() as $reason) {
+          $desc = PhabricatorMetaMTAActor::getReasonDescription($reason);
+          $info[] = '    - '.$desc;
         }
       }
 
       $info[] = null;
       $info[] = pht('TEXT BODY');
       if (strlen($message->getBody())) {
         $info[] = $message->getBody();
       } else {
         $info[] = pht('(This message has no text body.)');
       }
 
       $info[] = null;
       $info[] = pht('HTML BODY');
       if (strlen($message->getHTMLBody())) {
         $info[] = $message->getHTMLBody();
         $info[] = null;
       } else {
         $info[] = pht('(This message has no HTML body.)');
       }
 
       $console->writeOut('%s', implode("\n", $info));
 
       if ($message_key != $last_key) {
         $console->writeOut("\n%s\n\n", str_repeat('-', 80));
       }
     }
   }
 
 }
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActor.php b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
index 8580a7bc3..e314570a5 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAActor.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAActor.php
@@ -1,50 +1,117 @@
 <?php
 
 final class PhabricatorMetaMTAActor {
 
+  const STATUS_DELIVERABLE = 'deliverable';
+  const STATUS_UNDELIVERABLE = 'undeliverable';
+
+  const REASON_UNLOADABLE = 'unloadable';
+  const REASON_UNMAILABLE = 'unmailable';
+  const REASON_NO_ADDRESS = 'noaddress';
+  const REASON_DISABLED = 'disabled';
+  const REASON_MAIL_DISABLED = 'maildisabled';
+  const REASON_EXTERNAL_TYPE = 'exernaltype';
+  const REASON_RESPONSE = 'response';
+  const REASON_SELF = 'self';
+  const REASON_MAILTAGS = 'mailtags';
+  const REASON_BOT = 'bot';
+  const REASON_FORCE = 'force';
+
   private $phid;
   private $emailAddress;
   private $name;
+  private $status = self::STATUS_DELIVERABLE;
   private $reasons = array();
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setEmailAddress($email_address) {
     $this->emailAddress = $email_address;
     return $this;
   }
 
   public function getEmailAddress() {
     return $this->emailAddress;
   }
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function setUndeliverable($reason) {
     $this->reasons[] = $reason;
+    $this->status = self::STATUS_UNDELIVERABLE;
+    return $this;
+  }
+
+  public function setDeliverable($reason) {
+    $this->reasons[] = $reason;
+    $this->status = self::STATUS_DELIVERABLE;
     return $this;
   }
 
   public function isDeliverable() {
-    return empty($this->reasons);
+    return ($this->status === self::STATUS_DELIVERABLE);
   }
 
-  public function getUndeliverableReasons() {
+  public function getDeliverabilityReasons() {
     return $this->reasons;
   }
 
+  public static function getReasonDescription($reason) {
+    $descriptions = array(
+      self::REASON_DISABLED => pht(
+        'This user is disabled; disabled users do not receive mail.'),
+      self::REASON_BOT => pht(
+        'This user is a bot; bot accounts do not receive mail.'),
+      self::REASON_NO_ADDRESS => pht(
+        'Unable to load an email address for this PHID.'),
+      self::REASON_EXTERNAL_TYPE => pht(
+        'Only external accounts of type "email" are deliverable; this '.
+        'account has a different type.'),
+      self::REASON_UNMAILABLE => pht(
+        'This PHID type does not correspond to a mailable object.'),
+      self::REASON_RESPONSE => pht(
+        'This message is a response to another email message, and this '.
+        'recipient received the original email message, so we are not '.
+        'sending them this substantially similar message (for example, '.
+        'the sender used "Reply All" instead of "Reply" in response to '.
+        'mail from Phabricator).'),
+      self::REASON_SELF => pht(
+        'This recipient is the user whose actions caused delivery of '.
+        'this message, but they have set preferences so they do not '.
+        'receive mail about their own actions (Settings > Email '.
+        'Preferences > Self Actions).'),
+      self::REASON_MAIL_DISABLED => pht(
+        'This recipient has disabled all email notifications '.
+        '(Settings > Email Preferences > Email Notifications).'),
+      self::REASON_MAILTAGS => pht(
+        'This mail has tags which control which users receive it, and '.
+        'this recipient has not elected to receive mail with any of '.
+        'the tags on this message (Settings > Email Preferences).'),
+      self::REASON_UNLOADABLE => pht(
+        'Unable to load user record for this PHID.'),
+      self::REASON_FORCE => pht(
+        'Delivery of this mail is forced and ignores deliver preferences. '.
+        'Mail which uses forced delivery is usually related to account '.
+        'management or authentication. For example, password reset email '.
+        'ignores mail preferences.'),
+    );
+
+    return idx($descriptions, $reason, pht('Unknown Reason ("%s")', $reason));
+  }
+
+
 }
diff --git a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
index 58178edb1..1f593472a 100644
--- a/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
+++ b/src/applications/metamta/query/PhabricatorMetaMTAActorQuery.php
@@ -1,192 +1,182 @@
 <?php
 
 final class PhabricatorMetaMTAActorQuery extends PhabricatorQuery {
 
   private $phids = array();
   private $viewer;
 
   public function setViewer(PhabricatorUser $viewer) {
     $this->viewer = $viewer;
     return $this;
   }
 
   public function getViewer() {
     return $this->viewer;
   }
 
   public function withPHIDs(array $phids) {
     $this->phids = $phids;
     return $this;
   }
 
   public function execute() {
     $phids = array_fuse($this->phids);
     $actors = array();
     $type_map = array();
     foreach ($phids as $phid) {
       $type_map[phid_get_type($phid)][] = $phid;
       $actors[$phid] = id(new PhabricatorMetaMTAActor())->setPHID($phid);
     }
 
     // TODO: Move this to PhabricatorPHIDType, or the objects, or some
     // interface.
 
     foreach ($type_map as $type => $phids) {
       switch ($type) {
         case PhabricatorPeopleUserPHIDType::TYPECONST:
           $this->loadUserActors($actors, $phids);
           break;
         case PhabricatorPeopleExternalPHIDType::TYPECONST:
           $this->loadExternalUserActors($actors, $phids);
           break;
         case PhabricatorMailingListListPHIDType::TYPECONST:
           $this->loadMailingListActors($actors, $phids);
           break;
         default:
           $this->loadUnknownActors($actors, $phids);
           break;
       }
     }
 
     return $actors;
   }
 
   private function loadUserActors(array $actors, array $phids) {
     assert_instances_of($actors, 'PhabricatorMetaMTAActor');
 
     $emails = id(new PhabricatorUserEmail())->loadAllWhere(
       'userPHID IN (%Ls) AND isPrimary = 1',
       $phids);
     $emails = mpull($emails, null, 'getUserPHID');
 
     $users = id(new PhabricatorPeopleQuery())
       ->setViewer($this->getViewer())
       ->withPHIDs($phids)
       ->execute();
     $users = mpull($users, null, 'getPHID');
 
     foreach ($phids as $phid) {
       $actor = $actors[$phid];
 
       $user = idx($users, $phid);
       if (!$user) {
-        $actor->setUndeliverable(
-          pht('Unable to load user record for this PHID.'));
+        $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNLOADABLE);
       } else {
         $actor->setName($this->getUserName($user));
         if ($user->getIsDisabled()) {
-          $actor->setUndeliverable(
-            pht('This user is disabled; disabled users do not receive mail.'));
+          $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_DISABLED);
         }
         if ($user->getIsSystemAgent()) {
-          $actor->setUndeliverable(
-            pht('This user is a bot; bot accounts do not receive mail.'));
+          $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_BOT);
         }
 
         // NOTE: We do send email to unapproved users, and to unverified users,
         // because it would otherwise be impossible to get them to verify their
         // email addresses. Possibly we should white-list this kind of mail and
         // deny all other types of mail.
       }
 
       $email = idx($emails, $phid);
       if (!$email) {
-        $actor->setUndeliverable(
-          pht('Unable to load email record for this PHID.'));
+        $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_NO_ADDRESS);
       } else {
         $actor->setEmailAddress($email->getAddress());
       }
     }
   }
 
   private function loadExternalUserActors(array $actors, array $phids) {
     assert_instances_of($actors, 'PhabricatorMetaMTAActor');
 
     $xusers = id(new PhabricatorExternalAccountQuery())
       ->setViewer($this->getViewer())
       ->withPHIDs($phids)
       ->execute();
     $xusers = mpull($xusers, null, 'getPHID');
 
     foreach ($phids as $phid) {
       $actor = $actors[$phid];
 
       $xuser = idx($xusers, $phid);
       if (!$xuser) {
-        $actor->setUndeliverable(
-          pht('Unable to load external user record for this PHID.'));
+        $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNLOADABLE);
         continue;
       }
 
       $actor->setName($xuser->getDisplayName());
 
       if ($xuser->getAccountType() != 'email') {
-        $actor->setUndeliverable(
-          pht(
-            'Only external accounts of type "email" are deliverable; this '.
-            'account has a different type.'));
+        $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_EXTERNAL_TYPE);
         continue;
       }
 
       $actor->setEmailAddress($xuser->getAccountID());
     }
   }
 
   private function loadMailingListActors(array $actors, array $phids) {
     assert_instances_of($actors, 'PhabricatorMetaMTAActor');
 
     $lists = id(new PhabricatorMailingListQuery())
       ->setViewer($this->getViewer())
       ->withPHIDs($phids)
       ->execute();
     $lists = mpull($lists, null, 'getPHID');
 
     foreach ($phids as $phid) {
       $actor = $actors[$phid];
 
       $list = idx($lists, $phid);
       if (!$list) {
-        $actor->setUndeliverable(
-          pht(
-            'Unable to load mailing list record for this PHID.'));
+        $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNLOADABLE);
         continue;
       }
 
       $actor->setName($list->getName());
       $actor->setEmailAddress($list->getEmail());
     }
   }
 
   private function loadUnknownActors(array $actors, array $phids) {
     foreach ($phids as $phid) {
       $actor = $actors[$phid];
-      $actor->setUndeliverable(pht('This PHID type is not mailable.'));
+      $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNMAILABLE);
     }
   }
 
 
   /**
    * Small helper function to make sure we format the username properly as
    * specified by the `metamta.user-address-format` configuration value.
    */
   private function getUserName(PhabricatorUser $user) {
     $format = PhabricatorEnv::getEnvConfig('metamta.user-address-format');
 
     switch ($format) {
       case 'short':
         $name = $user->getUserName();
         break;
       case 'real':
         $name = strlen($user->getRealName()) ?
           $user->getRealName() : $user->getUserName();
         break;
       case 'full':
       default:
         $name = $user->getFullName();
         break;
     }
 
     return $name;
   }
 
 }
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
index 7bf19a935..5d5cccf37 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php
@@ -1,964 +1,951 @@
 <?php
 
 /**
  * @task recipients   Managing Recipients
  */
 final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
 
   const STATUS_QUEUE = 'queued';
   const STATUS_SENT  = 'sent';
   const STATUS_FAIL  = 'fail';
   const STATUS_VOID  = 'void';
 
   const RETRY_DELAY   = 5;
 
   protected $parameters;
   protected $status;
   protected $message;
   protected $relatedPHID;
 
   private $recipientExpansionMap;
 
   public function __construct() {
 
     $this->status     = self::STATUS_QUEUE;
     $this->parameters = array();
 
     parent::__construct();
   }
 
   protected function getConfiguration() {
     return array(
       self::CONFIG_SERIALIZATION => array(
         'parameters'  => self::SERIALIZATION_JSON,
       ),
       self::CONFIG_COLUMN_SCHEMA => array(
         'status' => 'text32',
         'relatedPHID' => 'phid?',
 
         // T6203/NULLABILITY
         // This should just be empty if there's no body.
         'message' => 'text?',
       ),
       self::CONFIG_KEY_SCHEMA => array(
         'status' => array(
           'columns' => array('status'),
         ),
         'relatedPHID' => array(
           'columns' => array('relatedPHID'),
         ),
         'key_created' => array(
           'columns' => array('dateCreated'),
         ),
       ),
     ) + parent::getConfiguration();
   }
 
   protected function setParam($param, $value) {
     $this->parameters[$param] = $value;
     return $this;
   }
 
   protected function getParam($param, $default = null) {
     return idx($this->parameters, $param, $default);
   }
 
   /**
    * Set tags (@{class:MetaMTANotificationType} constants) which identify the
    * content of this mail in a general way. These tags are used to allow users
    * to opt out of receiving certain types of mail, like updates when a task's
    * projects change.
    *
    * @param list<const> List of @{class:MetaMTANotificationType} constants.
    * @return this
    */
   public function setMailTags(array $tags) {
     $this->setParam('mailtags', array_unique($tags));
     return $this;
   }
 
   public function getMailTags() {
     return $this->getParam('mailtags', array());
   }
 
   /**
    * 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) {
     $phids = array_unique($phids);
     $this->setParam('to', $phids);
     return $this;
   }
 
   public function addRawTos(array $raw_email) {
 
     // Strip addresses down to bare emails, since the MailAdapter API currently
     // requires we pass it just the address (like `alincoln@logcabin.org`), not
     // a full string like `"Abraham Lincoln" <alincoln@logcabin.org>`.
     foreach ($raw_email as $key => $email) {
       $object = new PhutilEmailAddress($email);
       $raw_email[$key] = $object->getAddress();
     }
 
     $this->setParam('raw-to', $raw_email);
     return $this;
   }
 
   public function addCCs(array $phids) {
     $phids = array_unique($phids);
     $this->setParam('cc', $phids);
     return $this;
   }
 
   public function setExcludeMailRecipientPHIDs(array $exclude) {
     $this->setParam('exclude', $exclude);
     return $this;
   }
 
   private function getExcludeMailRecipientPHIDs() {
     return $this->getParam('exclude', array());
   }
 
   public function getTranslation(array $objects) {
     $default_translation = PhabricatorEnv::getEnvConfig('translation.provider');
     $return = null;
     $recipients = array_merge(
       idx($this->parameters, 'to', array()),
       idx($this->parameters, 'cc', array()));
     foreach (array_select_keys($objects, $recipients) as $object) {
       $translation = null;
       if ($object instanceof PhabricatorUser) {
         $translation = $object->getTranslation();
       }
       if (!$translation) {
         $translation = $default_translation;
       }
       if ($return && $translation != $return) {
         return $default_translation;
       }
       $return = $translation;
     }
 
     if (!$return) {
       $return = $default_translation;
     }
 
     return $return;
   }
 
   public function addPHIDHeaders($name, array $phids) {
     foreach ($phids as $phid) {
       $this->addHeader($name, '<'.$phid.'>');
     }
     return $this;
   }
 
   public function addHeader($name, $value) {
     $this->parameters['headers'][] = array($name, $value);
     return $this;
   }
 
   public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
     $this->parameters['attachments'][] = $attachment->toDictionary();
     return $this;
   }
 
   public function getAttachments() {
     $dicts = $this->getParam('attachments');
 
     $result = array();
     foreach ($dicts as $dict) {
       $result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict);
     }
     return $result;
   }
 
   public function setAttachments(array $attachments) {
     assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment');
     $this->setParam('attachments', mpull($attachments, 'toDictionary'));
     return $this;
   }
 
   public function setFrom($from) {
     $this->setParam('from', $from);
     return $this;
   }
 
   public function setRawFrom($raw_email, $raw_name) {
     $this->setParam('raw-from', array($raw_email, $raw_name));
     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 setSubjectPrefix($prefix) {
     $this->setParam('subject-prefix', $prefix);
     return $this;
   }
 
   public function setVarySubjectPrefix($prefix) {
     $this->setParam('vary-subject-prefix', $prefix);
     return $this;
   }
 
   public function setBody($body) {
     $this->setParam('body', $body);
     return $this;
   }
 
   public function setHTMLBody($html) {
     $this->setParam('html-body', $html);
     return $this;
   }
 
   public function getBody() {
     return $this->getParam('body');
   }
 
   public function getHTMLBody() {
     return $this->getParam('html-body');
   }
 
   public function setIsErrorEmail($is_error) {
     $this->setParam('is-error', $is_error);
     return $this;
   }
 
   public function getIsErrorEmail() {
     return $this->getParam('is-error', false);
   }
 
   public function getToPHIDs() {
     return $this->getParam('to', array());
   }
 
   public function getRawToAddresses() {
     return $this->getParam('raw-to', array());
   }
 
   public function getCcPHIDs() {
     return $this->getParam('cc', array());
   }
 
   /**
    * Force delivery of a message, even if recipients have preferences which
    * would otherwise drop the message.
    *
    * This is primarily intended to let users who don't want any email still
    * receive things like password resets.
    *
    * @param bool  True to force delivery despite user preferences.
    * @return this
    */
   public function setForceDelivery($force) {
     $this->setParam('force', $force);
     return $this;
   }
 
   public function getForceDelivery() {
     return $this->getParam('force', false);
   }
 
   /**
    * Flag that this is an auto-generated bulk message and should have bulk
    * headers added to it if appropriate. Broadly, this means some flavor of
    * "Precedence: bulk" or similar, but is implementation and configuration
    * dependent.
    *
    * @param bool  True if the mail is automated bulk mail.
    * @return this
    */
   public function setIsBulk($is_bulk) {
     $this->setParam('is-bulk', $is_bulk);
     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. The mail will eventually be
    * delivered by the MetaMTA daemon.
    *
    * @return this
    */
   public function saveAndSend() {
     return $this->save();
   }
 
   public function save() {
     if ($this->getID()) {
       return parent::save();
     }
 
     // NOTE: When mail is sent from CLI scripts that run tasks in-process, we
     // may re-enter this method from within scheduleTask(). The implementation
     // is intended to avoid anything awkward if we end up reentering this
     // method.
 
     $this->openTransaction();
       // Save to generate a task ID.
       $result = parent::save();
 
       // Queue a task to send this mail.
       $mailer_task = PhabricatorWorker::scheduleTask(
         'PhabricatorMetaMTAWorker',
         $this->getID(),
         array(
           'priority' => PhabricatorWorker::PRIORITY_ALERTS,
         ));
 
     $this->saveTransaction();
 
     return $result;
   }
 
   public function buildDefaultMailer() {
     return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
   }
 
 
   /**
    * 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!');
       }
     }
 
     try {
       $params = $this->parameters;
 
       $actors = $this->loadAllActors();
       $deliverable_actors = $this->filterDeliverableActors($actors);
 
       $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address');
       if (empty($params['from'])) {
         $mailer->setFrom($default_from);
       }
 
       $is_first = idx($params, 'is-first-message');
       unset($params['is-first-message']);
 
       $is_threaded = (bool)idx($params, 'thread-id');
 
       $reply_to_name = idx($params, 'reply-to-name', '');
       unset($params['reply-to-name']);
 
       $add_cc = array();
       $add_to = array();
 
       // Only try to use preferences if everything is multiplexed, so we
       // get consistent behavior.
       $use_prefs = self::shouldMultiplexAllMail();
 
       $prefs = null;
       if ($use_prefs) {
 
         // If multiplexing is enabled, some recipients will be in "Cc"
         // rather than "To". We'll move them to "To" later (or supply a
         // dummy "To") but need to look for the recipient in either the
         // "To" or "Cc" fields here.
         $target_phid = head(idx($params, 'to', array()));
         if (!$target_phid) {
           $target_phid = head(idx($params, 'cc', array()));
         }
 
         if ($target_phid) {
           $user = id(new PhabricatorUser())->loadOneWhere(
             'phid = %s',
             $target_phid);
           if ($user) {
             $prefs = $user->loadPreferences();
           }
         }
       }
 
       foreach ($params as $key => $value) {
         switch ($key) {
           case 'raw-from':
             list($from_email, $from_name) = $value;
             $mailer->setFrom($from_email, $from_name);
             break;
           case 'from':
             $from = $value;
             $actor_email = null;
             $actor_name = null;
             $actor = idx($actors, $from);
             if ($actor) {
               $actor_email = $actor->getEmailAddress();
               $actor_name = $actor->getName();
             }
             $can_send_as_user = $actor_email &&
               PhabricatorEnv::getEnvConfig('metamta.can-send-as-user');
 
             if ($can_send_as_user) {
               $mailer->setFrom($actor_email, $actor_name);
             } else {
               $from_email = coalesce($actor_email, $default_from);
               $from_name = coalesce($actor_name, pht('Phabricator'));
 
               if (empty($params['reply-to'])) {
                 $params['reply-to'] = $from_email;
                 $params['reply-to-name'] = $from_name;
               }
 
               $mailer->setFrom($default_from, $from_name);
             }
             break;
           case 'reply-to':
             $mailer->addReplyTo($value, $reply_to_name);
             break;
           case 'to':
             $to_phids = $this->expandRecipients($value);
             $to_actors = array_select_keys($deliverable_actors, $to_phids);
             $add_to = array_merge(
               $add_to,
               mpull($to_actors, 'getEmailAddress'));
             break;
           case 'raw-to':
             $add_to = array_merge($add_to, $value);
             break;
           case 'cc':
             $cc_phids = $this->expandRecipients($value);
             $cc_actors = array_select_keys($deliverable_actors, $cc_phids);
             $add_cc = array_merge(
               $add_cc,
               mpull($cc_actors, 'getEmailAddress'));
             break;
           case 'headers':
             foreach ($value as $pair) {
               list($header_key, $header_value) = $pair;
 
               // NOTE: If we have \n in a header, SES rejects the email.
               $header_value = str_replace("\n", ' ', $header_value);
 
               $mailer->addHeader($header_key, $header_value);
             }
             break;
           case 'attachments':
             $value = $this->getAttachments();
             foreach ($value as $attachment) {
               $mailer->addAttachment(
                 $attachment->getData(),
                 $attachment->getFilename(),
                 $attachment->getMimeType());
             }
             break;
           case 'subject':
             $subject = array();
 
             if ($is_threaded) {
               $add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix');
 
               if ($prefs) {
                 $add_re = $prefs->getPreference(
                   PhabricatorUserPreferences::PREFERENCE_RE_PREFIX,
                   $add_re);
               }
 
               if ($add_re) {
                 $subject[] = 'Re:';
               }
             }
 
             $subject[] = trim(idx($params, 'subject-prefix'));
 
             $vary_prefix = idx($params, 'vary-subject-prefix');
             if ($vary_prefix != '') {
               $use_subject = PhabricatorEnv::getEnvConfig(
                 'metamta.vary-subjects');
 
               if ($prefs) {
                 $use_subject = $prefs->getPreference(
                   PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT,
                   $use_subject);
               }
 
               if ($use_subject) {
                 $subject[] = $vary_prefix;
               }
             }
 
             $subject[] = $value;
 
             $mailer->setSubject(implode(' ', array_filter($subject)));
             break;
           case 'is-bulk':
             if ($value) {
               $mailer->addHeader('Precedence', 'bulk');
             }
             break;
           case 'thread-id':
 
             // NOTE: Gmail freaks out about In-Reply-To and References which
             // aren't in the form "<string@domain.tld>"; this is also required
             // by RFC 2822, although some clients are more liberal in what they
             // accept.
             $domain = PhabricatorEnv::getEnvConfig('metamta.domain');
             $value = '<'.$value.'@'.$domain.'>';
 
             if ($is_first && $mailer->supportsMessageIDHeader()) {
               $mailer->addHeader('Message-ID',  $value);
             } else {
               $in_reply_to = $value;
               $references = array($value);
               $parent_id = $this->getParentMessageID();
               if ($parent_id) {
                 $in_reply_to = $parent_id;
                 // By RFC 2822, the most immediate parent should appear last
                 // in the "References" header, so this order is intentional.
                 $references[] = $parent_id;
               }
               $references = implode(' ', $references);
               $mailer->addHeader('In-Reply-To', $in_reply_to);
               $mailer->addHeader('References',  $references);
             }
             $thread_index = $this->generateThreadIndex($value, $is_first);
             $mailer->addHeader('Thread-Index', $thread_index);
             break;
           case 'mailtags':
             // Handled below.
             break;
           case 'subject-prefix':
           case 'vary-subject-prefix':
             // Handled above.
             break;
           default:
             // Just discard.
         }
       }
 
       $body = idx($params, 'body', '');
       $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit');
       if (strlen($body) > $max) {
         $body = id(new PhutilUTF8StringTruncator())
           ->setMaximumBytes($max)
           ->truncateString($body);
         $body .= "\n";
         $body .= pht('(This email was truncated at %d bytes.)', $max);
       }
       $mailer->setBody($body);
 
       $html_emails = false;
       if ($use_prefs && $prefs) {
         $html_emails = $prefs->getPreference(
           PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS,
           $html_emails);
       }
 
       if ($html_emails && isset($params['html-body'])) {
         $mailer->setHTMLBody($params['html-body']);
       }
 
       if (!$add_to && !$add_cc) {
         $this->setStatus(self::STATUS_VOID);
         $this->setMessage(
           'Message has no valid recipients: all To/Cc are disabled, invalid, '.
           'or configured not to receive this mail.');
         return $this->save();
       }
 
       if ($this->getIsErrorEmail()) {
         $all_recipients = array_merge($add_to, $add_cc);
         if ($this->shouldRateLimitMail($all_recipients)) {
           $this->setStatus(self::STATUS_VOID);
           $this->setMessage(
             pht(
               'This is an error email, but one or more recipients have '.
               'exceeded the error email rate limit. Declining to deliver '.
               'message.'));
           return $this->save();
         }
       }
 
       if (PhabricatorEnv::getEnvConfig('phabricator.silent')) {
         $this->setStatus(self::STATUS_VOID);
         $this->setMessage(
           pht(
             'Phabricator is running in silent mode. See `phabricator.silent` '.
             'in the configuration to change this setting.'));
         return $this->save();
       }
 
       $mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes');
       $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
 
       // Some clients respect this to suppress OOF and other auto-responses.
       $mailer->addHeader('X-Auto-Response-Suppress', 'All');
 
       // If the message has mailtags, filter out any recipients who don't want
       // to receive this type of mail.
       $mailtags = $this->getParam('mailtags');
       if ($mailtags) {
         $tag_header = array();
         foreach ($mailtags as $mailtag) {
           $tag_header[] = '<'.$mailtag.'>';
         }
         $tag_header = implode(', ', $tag_header);
         $mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header);
       }
 
       // Some mailers require a valid "To:" in order to deliver mail. If we
       // don't have any "To:", try to fill it in with a placeholder "To:".
       // If that also fails, move the "Cc:" line to "To:".
       if (!$add_to) {
         $placeholder_key = 'metamta.placeholder-to-recipient';
         $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key);
         if ($placeholder !== null) {
           $add_to = array($placeholder);
         } else {
           $add_to = $add_cc;
           $add_cc = array();
         }
       }
 
       $add_to = array_unique($add_to);
       $add_cc = array_diff(array_unique($add_cc), $add_to);
 
       $mailer->addTos($add_to);
       if ($add_cc) {
         $mailer->addCCs($add_cc);
       }
     } catch (Exception $ex) {
       $this
         ->setStatus(self::STATUS_FAIL)
         ->setMessage($ex->getMessage())
         ->save();
 
       throw $ex;
     }
 
     try {
       $ok = $mailer->send();
       if (!$ok) {
         // TODO: At some point, we should clean this up and make all mailers
         // throw.
         throw new Exception(
           pht('Mail adapter encountered an unexpected, unspecified failure.'));
       }
 
       $this->setStatus(self::STATUS_SENT);
       $this->save();
 
       return $this;
     } catch (PhabricatorMetaMTAPermanentFailureException $ex) {
       $this
         ->setStatus(self::STATUS_FAIL)
         ->setMessage($ex->getMessage())
         ->save();
 
       throw $ex;
     } catch (Exception $ex) {
       $this
         ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString())
         ->save();
 
       throw $ex;
     }
   }
 
   public static function getReadableStatus($status_code) {
     static $readable = array(
       self::STATUS_QUEUE => 'Queued for Delivery',
       self::STATUS_FAIL  => 'Delivery Failed',
       self::STATUS_SENT  => 'Sent',
       self::STATUS_VOID  => 'Void',
     );
     $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);
   }
 
   public static function shouldMultiplexAllMail() {
     return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
   }
 
 
 /* -(  Managing Recipients  )------------------------------------------------ */
 
 
   /**
    * Get all of the recipients for this mail, after preference filters are
    * applied. This list has all objects to whom delivery will be attempted.
    *
    * Note that this expands recipients into their members, because delivery
    * is never directly attempted to aggregate actors like projects.
    *
    * @return  list<phid>  A list of all recipients to whom delivery will be
    *                      attempted.
    * @task recipients
    */
   public function buildRecipientList() {
     $actors = $this->loadAllActors();
     $actors = $this->filterDeliverableActors($actors);
     return mpull($actors, 'getPHID');
   }
 
   public function loadAllActors() {
     $actor_phids = $this->getAllActorPHIDs();
     $actor_phids = $this->expandRecipients($actor_phids);
     return $this->loadActors($actor_phids);
   }
 
   private function getAllActorPHIDs() {
     return array_merge(
       array($this->getParam('from')),
       $this->getToPHIDs(),
       $this->getCcPHIDs());
   }
 
   /**
    * Expand a list of recipient PHIDs (possibly including aggregate recipients
    * like projects) into a deaggregated list of individual recipient PHIDs.
    * For example, this will expand project PHIDs into a list of the project's
    * members.
    *
    * @param list<phid>  List of recipient PHIDs, possibly including aggregate
    *                    recipients.
    * @return list<phid> Deaggregated list of mailable recipients.
    */
   private function expandRecipients(array $phids) {
     if ($this->recipientExpansionMap === null) {
       $all_phids = $this->getAllActorPHIDs();
       $this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())
         ->setViewer(PhabricatorUser::getOmnipotentUser())
         ->withPHIDs($all_phids)
         ->execute();
     }
 
     $results = array();
     foreach ($phids as $phid) {
       foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
         $results[$recipient_phid] = $recipient_phid;
       }
     }
 
     return array_keys($results);
   }
 
   private function filterDeliverableActors(array $actors) {
     assert_instances_of($actors, 'PhabricatorMetaMTAActor');
     $deliverable_actors = array();
     foreach ($actors as $phid => $actor) {
       if ($actor->isDeliverable()) {
         $deliverable_actors[$phid] = $actor;
       }
     }
     return $deliverable_actors;
   }
 
   private function loadActors(array $actor_phids) {
     $actor_phids = array_filter($actor_phids);
     $viewer = PhabricatorUser::getOmnipotentUser();
 
     $actors = id(new PhabricatorMetaMTAActorQuery())
       ->setViewer($viewer)
       ->withPHIDs($actor_phids)
       ->execute();
 
     if (!$actors) {
       return array();
     }
 
     if ($this->getForceDelivery()) {
       // If we're forcing delivery, skip all the opt-out checks.
+      foreach ($actors as $actor) {
+        $actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);
+      }
       return $actors;
     }
 
     // Exclude explicit recipients.
     foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
       $actor = idx($actors, $phid);
       if (!$actor) {
         continue;
       }
-      $actor->setUndeliverable(
-        pht(
-          'This message is a response to another email message, and this '.
-          'recipient received the original email message, so we are not '.
-          'sending them this substantially similar message (for example, '.
-          'the sender used "Reply All" instead of "Reply" in response to '.
-          'mail from Phabricator).'));
+      $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);
     }
 
     // Exclude the actor if their preferences are set.
     $from_phid = $this->getParam('from');
     $from_actor = idx($actors, $from_phid);
     if ($from_actor) {
       $from_user = id(new PhabricatorPeopleQuery())
         ->setViewer($viewer)
         ->withPHIDs(array($from_phid))
         ->execute();
       $from_user = head($from_user);
       if ($from_user) {
         $pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL;
         $exclude_self = $from_user
           ->loadPreferences()
           ->getPreference($pref_key);
         if ($exclude_self) {
-          $from_actor->setUndeliverable(
-            pht(
-              'This recipient is the user whose actions caused delivery of '.
-              'this message, but they have set preferences so they do not '.
-              'receive mail about their own actions (Settings > Email '.
-              'Preferences > Self Actions).'));
+          $from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);
         }
       }
     }
 
     $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere(
       'userPHID in (%Ls)',
       $actor_phids);
     $all_prefs = mpull($all_prefs, null, 'getUserPHID');
 
     // Exclude recipients who don't want any mail.
     foreach ($all_prefs as $phid => $prefs) {
       $exclude = $prefs->getPreference(
         PhabricatorUserPreferences::PREFERENCE_NO_MAIL,
         false);
       if ($exclude) {
         $actors[$phid]->setUndeliverable(
-          pht(
-            'This recipient has disabled all email notifications '.
-            '(Settings > Email Preferences > Email Notifications).'));
+          PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);
       }
     }
 
     $value_email = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL;
 
     // Exclude all recipients who have set preferences to not receive this type
     // of email (for example, a user who says they don't want emails about task
     // CC changes).
     $tags = $this->getParam('mailtags');
     if ($tags) {
       foreach ($all_prefs as $phid => $prefs) {
         $user_mailtags = $prefs->getPreference(
           PhabricatorUserPreferences::PREFERENCE_MAILTAGS,
           array());
 
         // The user must have elected to receive mail for at least one
         // of the mailtags.
         $send = false;
         foreach ($tags as $tag) {
           if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
             $send = true;
             break;
           }
         }
 
         if (!$send) {
           $actors[$phid]->setUndeliverable(
-            pht(
-              'This mail has tags which control which users receive it, and '.
-              'this recipient has not elected to receive mail with any of '.
-              'the tags on this message (Settings > Email Preferences).'));
+            PhabricatorMetaMTAActor::REASON_MAILTAGS);
         }
       }
     }
 
     return $actors;
   }
 
   private function shouldRateLimitMail(array $all_recipients) {
     try {
       PhabricatorSystemActionEngine::willTakeAction(
         $all_recipients,
         new PhabricatorMetaMTAErrorMailAction(),
         1);
       return false;
     } catch (PhabricatorSystemActionRateLimitException $ex) {
       return true;
     }
   }
 
 
 }