diff --git a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
index 386ac84d7..3f51168f8 100644
--- a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
@@ -1,447 +1,470 @@
 <?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) {
     $phids = array_unique($phids);
     $this->setParam('to', $phids);
     return $this;
   }
 
   public function addCCs(array $phids) {
     $phids = array_unique($phids);
     $this->setParam('cc', $phids);
     return $this;
   }
 
   public function addHeader($name, $value) {
     $this->parameters['headers'][$name] = $value;
     return $this;
   }
 
   public function addAttachment(PhabricatorMetaMTAAttachment $attachment) {
     $this->parameters['attachments'][] = $attachment;
     return $this;
   }
 
   public function getAttachments() {
     return $this->getParam('attachments');
   }
 
   public function setAttachments(array $attachments) {
     $this->setParam('attachments', $attachments);
     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;
   }
 
   /**
    * 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 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 = null;
 
     if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) {
       $ret = $this->sendNow();
     } else {
       $ret = $this->save();
     }
 
     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();
+            $emails = $this->getDeliverableEmailsFromHandles($value, $handles);
+            if ($emails) {
+              $mailer->addTos($emails);
+            } else {
+              if ($value) {
+                throw new Exception(
+                  "All 'To' objects are undeliverable (e.g., disabled users).");
+              } else {
+                throw new Exception("No 'To' specified!");
+              }
             }
-            $mailer->addTos($emails);
             break;
           case 'cc':
-            $emails = array();
-            foreach ($value as $phid) {
-              $emails[] = $handles[$phid]->getEmail();
+            $emails = $this->getDeliverableEmailsFromHandles($value, $handles);
+            if ($emails) {
+              $mailer->addCCs($emails);
             }
-            $mailer->addCCs($emails);
             break;
           case 'headers':
             foreach ($value as $header_key => $header_value) {
               $mailer->addHeader($header_key, $header_value);
             }
             break;
           case 'attachments':
             foreach ($value as $attachment) {
               $mailer->addAttachment(
                 $attachment->getData(),
                 $attachment->getFileName(),
                 $attachment->getMimeType()
               );
             }
             break;
           case 'body':
             $mailer->setBody($value);
             break;
           case 'subject':
             $mailer->setSubject($value);
             break;
           case 'is-html':
             if ($value) {
               $mailer->setIsHTML(true);
             }
             break;
           case 'is-bulk':
             if ($value) {
               if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) {
                 $mailer->addHeader('Precedence', 'bulk');
               }
             }
             break;
           case 'thread-id':
             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;
           default:
             // Just discard.
         }
       }
 
       $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
 
     } catch (Exception $ex) {
       $this->setStatus(self::STATUS_FAIL);
       $this->setMessage($ex->getMessage());
       return $this->save();
     }
 
     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);
     }
 
     return $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);
   }
 
+  private function getDeliverableEmailsFromHandles(
+    array $phids,
+    array $handles) {
+
+    $emails = array();
+    foreach ($phids as $phid) {
+      if ($handles[$phid]->isDisabled()) {
+        continue;
+      }
+      if (!$handles[$phid]->isComplete()) {
+        continue;
+      }
+      $emails[] = $handles[$phid]->getEmail();
+    }
+
+    return $emails;
+  }
+
 }
diff --git a/src/applications/people/storage/user/PhabricatorUser.php b/src/applications/people/storage/user/PhabricatorUser.php
index 433cfd2c1..ca5eb1b5c 100644
--- a/src/applications/people/storage/user/PhabricatorUser.php
+++ b/src/applications/people/storage/user/PhabricatorUser.php
@@ -1,407 +1,415 @@
 <?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 PhabricatorUser extends PhabricatorUserDAO {
 
   const SESSION_TABLE = 'phabricator_session';
   const NAMETOKEN_TABLE = 'user_nametoken';
 
   protected $phid;
   protected $userName;
   protected $realName;
   protected $email;
   protected $passwordSalt;
   protected $passwordHash;
   protected $profileImagePHID;
   protected $timezoneIdentifier = '';
 
   protected $consoleEnabled = 0;
   protected $consoleVisible = 0;
   protected $consoleTab = '';
 
   protected $conduitCertificate;
 
   protected $isSystemAgent = 0;
   protected $isAdmin = 0;
   protected $isDisabled = 0;
 
   private $preferences = null;
 
   protected function readField($field) {
-    if ($field === 'profileImagePHID') {
-      return nonempty(
-        $this->profileImagePHID,
-        PhabricatorEnv::getEnvConfig('user.default-profile-image-phid'));
-    }
-    if ($field === 'timezoneIdentifier') {
-      // If the user hasn't set one, guess the server's time.
-      return nonempty(
-        $this->timezoneIdentifier,
-        date_default_timezone_get());
+    switch ($field) {
+      case 'profileImagePHID':
+        return nonempty(
+          $this->profileImagePHID,
+          PhabricatorEnv::getEnvConfig('user.default-profile-image-phid'));
+      case 'timezoneIdentifier':
+        // If the user hasn't set one, guess the server's time.
+        return nonempty(
+          $this->timezoneIdentifier,
+          date_default_timezone_get());
+      // Make sure these return booleans.
+      case 'isAdmin':
+        return (bool)$this->isAdmin;
+      case 'isDisabled':
+        return (bool)$this->isDisabled;
+      case 'isSystemAgent':
+        return (bool)$this->isSystemAgent;
+      default:
+        return parent::readField($field);
     }
-    return parent::readField($field);
   }
 
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_PARTIAL_OBJECTS => true,
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPHIDConstants::PHID_TYPE_USER);
   }
 
   public function setPassword($password) {
     if (!$this->getPHID()) {
       throw new Exception(
         "You can not set a password for an unsaved user because their PHID ".
         "is a salt component in the password hash.");
     }
 
     if (!strlen($password)) {
       $this->setPasswordHash('');
     } else {
       $this->setPasswordSalt(md5(mt_rand()));
       $hash = $this->hashPassword($password);
       $this->setPasswordHash($hash);
     }
     return $this;
   }
 
   public function isLoggedIn() {
     return !($this->getPHID() === null);
   }
 
   public function save() {
     if (!$this->getConduitCertificate()) {
       $this->setConduitCertificate($this->generateConduitCertificate());
     }
     $result = parent::save();
 
     $this->updateNameTokens();
     PhabricatorSearchUserIndexer::indexUser($this);
 
     return $result;
   }
 
   private function generateConduitCertificate() {
     return Filesystem::readRandomCharacters(255);
   }
 
   public function comparePassword($password) {
     if (!strlen($password)) {
       return false;
     }
     if (!strlen($this->getPasswordHash())) {
       return false;
     }
     $password = $this->hashPassword($password);
     return ($password === $this->getPasswordHash());
   }
 
   private function hashPassword($password) {
     $password = $this->getUsername().
                 $password.
                 $this->getPHID().
                 $this->getPasswordSalt();
     for ($ii = 0; $ii < 1000; $ii++) {
       $password = md5($password);
     }
     return $password;
   }
 
   const CSRF_CYCLE_FREQUENCY  = 3600;
   const CSRF_TOKEN_LENGTH     = 16;
 
   const EMAIL_CYCLE_FREQUENCY = 86400;
   const EMAIL_TOKEN_LENGTH    = 24;
 
   public function getCSRFToken($offset = 0) {
     return $this->generateToken(
       time() + (self::CSRF_CYCLE_FREQUENCY * $offset),
       self::CSRF_CYCLE_FREQUENCY,
       PhabricatorEnv::getEnvConfig('phabricator.csrf-key'),
       self::CSRF_TOKEN_LENGTH);
   }
 
   public function validateCSRFToken($token) {
 
     // When the user posts a form, we check that it contains a valid CSRF token.
     // Tokens cycle each hour (every CSRF_CYLCE_FREQUENCY seconds) and we accept
     // either the current token, the next token (users can submit a "future"
     // token if you have two web frontends that have some clock skew) or any of
     // the last 6 tokens. This means that pages are valid for up to 7 hours.
     // There is also some Javascript which periodically refreshes the CSRF
     // tokens on each page, so theoretically pages should be valid indefinitely.
     // However, this code may fail to run (if the user loses their internet
     // connection, or there's a JS problem, or they don't have JS enabled).
     // Choosing the size of the window in which we accept old CSRF tokens is
     // an issue of balancing concerns between security and usability. We could
     // choose a very narrow (e.g., 1-hour) window to reduce vulnerability to
     // attacks using captured CSRF tokens, but it's also more likely that real
     // users will be affected by this, e.g. if they close their laptop for an
     // hour, open it back up, and try to submit a form before the CSRF refresh
     // can kick in. Since the user experience of submitting a form with expired
     // CSRF is often quite bad (you basically lose data, or it's a big pain to
     // recover at least) and I believe we gain little additional protection
     // by keeping the window very short (the overwhelming value here is in
     // preventing blind attacks, and most attacks which can capture CSRF tokens
     // can also just capture authentication information [sniffing networks]
     // or act as the user [xss]) the 7 hour default seems like a reasonable
     // balance. Other major platforms have much longer CSRF token lifetimes,
     // like Rails (session duration) and Django (forever), which suggests this
     // is a reasonable analysis.
     $csrf_window = 6;
 
     for ($ii = -$csrf_window; $ii <= 1; $ii++) {
       $valid = $this->getCSRFToken($ii);
       if ($token == $valid) {
         return true;
       }
     }
     return false;
   }
 
   private function generateToken($epoch, $frequency, $key, $len) {
     $time_block = floor($epoch / $frequency);
     $vec = $this->getPHID().$this->getPasswordHash().$key.$time_block;
     return substr(sha1($vec), 0, $len);
   }
 
   /**
    * Issue a new session key to this user. Phabricator supports different
    * types of sessions (like "web" and "conduit") and each session type may
    * have multiple concurrent sessions (this allows a user to be logged in on
    * multiple browsers at the same time, for instance).
    *
    * Note that this method is transport-agnostic and does not set cookies or
    * issue other types of tokens, it ONLY generates a new session key.
    *
    * You can configure the maximum number of concurrent sessions for various
    * session types in the Phabricator configuration.
    *
    * @param   string  Session type, like "web".
    * @return  string  Newly generated session key.
    */
   public function establishSession($session_type) {
     $conn_w = $this->establishConnection('w');
 
     if (strpos($session_type, '-') !== false) {
       throw new Exception("Session type must not contain hyphen ('-')!");
     }
 
     // We allow multiple sessions of the same type, so when a caller requests
     // a new session of type "web", we give them the first available session in
     // "web-1", "web-2", ..., "web-N", up to some configurable limit. If none
     // of these sessions is available, we overwrite the oldest session and
     // reissue a new one in its place.
 
     $session_limit = 1;
     switch ($session_type) {
       case 'web':
         $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.web');
         break;
       case 'conduit':
         $session_limit = PhabricatorEnv::getEnvConfig('auth.sessions.conduit');
         break;
       default:
         throw new Exception("Unknown session type '{$session_type}'!");
     }
 
     $session_limit = (int)$session_limit;
     if ($session_limit <= 0) {
       throw new Exception(
         "Session limit for '{$session_type}' must be at least 1!");
     }
 
     // Load all the currently active sessions.
     $sessions = queryfx_all(
       $conn_w,
       'SELECT type, sessionStart FROM %T WHERE userPHID = %s AND type LIKE %>',
       PhabricatorUser::SESSION_TABLE,
       $this->getPHID(),
       $session_type.'-');
 
     // Choose which 'type' we'll actually establish, i.e. what number we're
     // going to append to the basic session type. To do this, just check all
     // the numbers sequentially until we find an available session.
     $establish_type = null;
     $sessions = ipull($sessions, null, 'type');
     for ($ii = 1; $ii <= $session_limit; $ii++) {
       if (empty($sessions[$session_type.'-'.$ii])) {
         $establish_type = $session_type.'-'.$ii;
         break;
       }
     }
 
     // If we didn't find an available session, choose the oldest session and
     // overwrite it.
     if (!$establish_type) {
       $sessions = isort($sessions, 'sessionStart');
       $oldest = reset($sessions);
       $establish_type = $oldest['type'];
     }
 
     // Consume entropy to generate a new session key, forestalling the eventual
     // heat death of the universe.
     $session_key = Filesystem::readRandomCharacters(40);
 
     // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     queryfx(
       $conn_w,
       'INSERT INTO %T '.
         '(userPHID, type, sessionKey, sessionStart)'.
       ' VALUES '.
         '(%s, %s, %s, UNIX_TIMESTAMP()) '.
       'ON DUPLICATE KEY UPDATE '.
         'sessionKey = VALUES(sessionKey), '.
         'sessionStart = VALUES(sessionStart)',
       self::SESSION_TABLE,
       $this->getPHID(),
       $establish_type,
       $session_key);
 
     $log = PhabricatorUserLog::newLog(
       $this,
       $this,
       PhabricatorUserLog::ACTION_LOGIN);
     $log->setDetails(
       array(
         'session_type' => $session_type,
         'session_issued' => $establish_type,
       ));
     $log->setSession($session_key);
     $log->save();
 
     return $session_key;
   }
 
   public function destroySession($session_key) {
     $conn_w = $this->establishConnection('w');
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE userPHID = %s AND sessionKey = %s',
       self::SESSION_TABLE,
       $this->getPHID(),
       $session_key);
   }
 
   private function generateEmailToken($offset = 0) {
     return $this->generateToken(
       time() + ($offset * self::EMAIL_CYCLE_FREQUENCY),
       self::EMAIL_CYCLE_FREQUENCY,
       PhabricatorEnv::getEnvConfig('phabricator.csrf-key').$this->getEmail(),
       self::EMAIL_TOKEN_LENGTH);
   }
 
   public function validateEmailToken($token) {
     for ($ii = -1; $ii <= 1; $ii++) {
       $valid = $this->generateEmailToken($ii);
       if ($token == $valid) {
         return true;
       }
     }
     return false;
   }
 
   public function getEmailLoginURI() {
     $token = $this->generateEmailToken();
     $uri = PhabricatorEnv::getProductionURI('/login/etoken/'.$token.'/');
     $uri = new PhutilURI($uri);
     return $uri->alter('email', $this->getEmail());
   }
 
   public function loadPreferences() {
     if ($this->preferences) {
       return $this->preferences;
     }
 
     $preferences = id(new PhabricatorUserPreferences())->loadOneWhere(
       'userPHID = %s',
       $this->getPHID());
 
     if (!$preferences) {
       $preferences = new PhabricatorUserPreferences();
       $preferences->setUserPHID($this->getPHID());
 
       $default_dict = array(
         PhabricatorUserPreferences::PREFERENCE_TITLES => 'glyph',
         PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '');
 
       $preferences->setPreferences($default_dict);
     }
 
     $this->preferences = $preferences;
     return $preferences;
   }
 
   private static function tokenizeName($name) {
     if (function_exists('mb_strtolower')) {
       $name = mb_strtolower($name, 'UTF-8');
     } else {
       $name = strtolower($name);
     }
     $name = trim($name);
     if (!strlen($name)) {
       return array();
     }
     return preg_split('/\s+/', $name);
   }
 
   /**
    * Populate the nametoken table, which used to fetch typeahead results. When
    * a user types "linc", we want to match "Abraham Lincoln" from on-demand
    * typeahead sources. To do this, we need a separate table of name fragments.
    */
   public function updateNameTokens() {
     $tokens = array_merge(
       self::tokenizeName($this->getRealName()),
       self::tokenizeName($this->getUserName()));
     $tokens = array_unique($tokens);
     $table  = self::NAMETOKEN_TABLE;
     $conn_w = $this->establishConnection('w');
 
     $sql = array();
     foreach ($tokens as $token) {
       $sql[] = qsprintf(
         $conn_w,
         '(%d, %s)',
         $this->getID(),
         $token);
     }
 
     queryfx(
       $conn_w,
       'DELETE FROM %T WHERE userID = %d',
       $table,
       $this->getID());
     if ($sql) {
       queryfx(
         $conn_w,
         'INSERT INTO %T (userID, token) VALUES %Q',
         $table,
         implode(', ', $sql));
     }
   }
 
 }
diff --git a/src/applications/phid/handle/PhabricatorObjectHandle.php b/src/applications/phid/handle/PhabricatorObjectHandle.php
index 050c5a284..cb1ae91f9 100644
--- a/src/applications/phid/handle/PhabricatorObjectHandle.php
+++ b/src/applications/phid/handle/PhabricatorObjectHandle.php
@@ -1,183 +1,221 @@
 <?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 PhabricatorObjectHandle {
 
   private $uri;
   private $phid;
   private $type;
   private $name;
   private $email;
   private $fullName;
   private $imageURI;
   private $timestamp;
   private $alternateID;
   private $status = 'open';
   private $complete;
+  private $disabled;
 
   public function setURI($uri) {
     $this->uri = $uri;
     return $this;
   }
 
   public function getURI() {
     return $this->uri;
   }
 
   public function setPHID($phid) {
     $this->phid = $phid;
     return $this;
   }
 
   public function getPHID() {
     return $this->phid;
   }
 
   public function setName($name) {
     $this->name = $name;
     return $this;
   }
 
   public function getName() {
     return $this->name;
   }
 
   public function setStatus($status) {
     $this->status = $status;
     return $this;
   }
 
   public function getStatus() {
     return $this->status;
   }
 
   public function setFullName($full_name) {
     $this->fullName = $full_name;
     return $this;
   }
 
   public function getFullName() {
     if ($this->fullName !== null) {
       return $this->fullName;
     }
     return $this->getName();
   }
 
   public function setType($type) {
     $this->type = $type;
     return $this;
   }
 
   public function getType() {
     return $this->type;
   }
 
   public function setEmail($email) {
     $this->email = $email;
     return $this;
   }
 
   public function getEmail() {
     return $this->email;
   }
 
   public function setImageURI($uri) {
     $this->imageURI = $uri;
     return $this;
   }
 
   public function getImageURI() {
     return $this->imageURI;
   }
 
   public function setTimestamp($timestamp) {
     $this->timestamp = $timestamp;
     return $this;
   }
 
   public function getTimestamp() {
     return $this->timestamp;
   }
 
   public function setAlternateID($alternate_id) {
     $this->alternateID = $alternate_id;
     return $this;
   }
 
   public function getAlternateID() {
     return $this->alternateID;
   }
 
   public function getTypeName() {
     static $map = array(
       PhabricatorPHIDConstants::PHID_TYPE_USER => 'User',
       PhabricatorPHIDConstants::PHID_TYPE_TASK => 'Task',
       PhabricatorPHIDConstants::PHID_TYPE_DREV => 'Revision',
       PhabricatorPHIDConstants::PHID_TYPE_CMIT => 'Commit',
       PhabricatorPHIDConstants::PHID_TYPE_WIKI => 'Phriction',
     );
 
     return idx($map, $this->getType());
   }
 
+
+  /**
+   * Set whether or not the underlying object is complete. See
+   * @{method:getComplete} for an explanation of what it means to be complete.
+   *
+   * @param bool True if the handle represents a complete object.
+   * @return this
+   */
   public function setComplete($complete) {
     $this->complete = $complete;
     return $this;
   }
 
+
   /**
    * Determine if the handle represents an object which was completely loaded
    * (i.e., the underlying object exists) vs an object which could not be
    * completely loaded (e.g., the type or data for the PHID could not be
    * identified or located).
    *
    * Basically, @{class:PhabricatorObjectHandleData} gives you back a handle for
    * any PHID you give it, but it gives you a complete handle only for valid
    * PHIDs.
    *
    * @return bool True if the handle represents a complete object.
    */
   public function isComplete() {
     return $this->complete;
   }
 
+
+  /**
+   * Set whether or not the underlying object is disabled. See
+   * @{method:getDisabled} for an explanation of what it means to be disabled.
+   *
+   * @param bool True if the handle represents a disabled object.
+   * @return this
+   */
+  public function setDisabled($disabled) {
+    $this->disabled = $disabled;
+    return $this;
+  }
+
+
+  /**
+   * Determine if the handle represents an object which has been disabled --
+   * for example, disabled users, archived projects, etc. These objects are
+   * complete and exist, but should be excluded from some system interactions
+   * (for instance, they usually should not appear in typeaheads, and should
+   * not have mail/notifications delivered to or about them).
+   *
+   * @return bool True if the handle represents a disabled object.
+   */
+  public function isDisabled() {
+    return $this->disabled;
+  }
+
+
   public function renderLink() {
 
     switch ($this->getType()) {
       case PhabricatorPHIDConstants::PHID_TYPE_USER:
         $name = $this->getName();
         break;
       default:
         $name = $this->getFullName();
     }
 
     $class = null;
     if ($this->status != PhabricatorObjectHandleStatus::STATUS_OPEN) {
       $class = 'handle-status-'.phutil_escape_html($this->status);
     }
 
     return phutil_render_tag(
       'a',
       array(
         'href'  => $this->getURI(),
         'class' => $class,
       ),
       phutil_escape_html($name));
   }
 
 }
diff --git a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php
index 3322af127..91bf8096e 100644
--- a/src/applications/phid/handle/data/PhabricatorObjectHandleData.php
+++ b/src/applications/phid/handle/data/PhabricatorObjectHandleData.php
@@ -1,487 +1,488 @@
 <?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 PhabricatorObjectHandleData {
 
   private $phids;
 
   public function __construct(array $phids) {
     $this->phids = array_unique($phids);
   }
 
   public function loadObjects() {
     $types = array();
     foreach ($this->phids as $phid) {
       $type = $this->lookupType($phid);
       $types[$type][] = $phid;
     }
 
     $objects = array_fill_keys($this->phids, null);
     foreach ($types as $type => $phids) {
       switch ($type) {
         case PhabricatorPHIDConstants::PHID_TYPE_USER:
           $user_dao = newv('PhabricatorUser', array());
           $users = $user_dao->loadAllWhere(
             'phid in (%Ls)',
             $phids);
           foreach ($users as $user) {
             $objects[$user->getPHID()] = $user;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
           $commit_dao = newv('PhabricatorRepositoryCommit', array());
           $commits = $commit_dao->loadAllWhere(
             'phid IN (%Ls)',
             $phids);
           $commit_data = array();
           if ($commits) {
             $data_dao = newv('PhabricatorRepositoryCommitData', array());
             $commit_data = $data_dao->loadAllWhere(
               'commitID IN (%Ld)',
               mpull($commits, 'getID'));
             $commit_data = mpull($commit_data, null, 'getCommitID');
           }
           foreach ($commits as $commit) {
             $data = idx($commit_data, $commit->getID());
             if ($data) {
               $commit->attachCommitData($data);
               $objects[$commit->getPHID()] = $commit;
             } else {
              // If we couldn't load the commit data, just act as though we
              // couldn't load the object at all so we don't load half an object.
             }
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_TASK:
           $task_dao = newv('ManiphestTask', array());
           $tasks = $task_dao->loadAllWhere(
             'phid IN (%Ls)',
             $phids);
           foreach ($tasks as $task) {
             $objects[$task->getPHID()] = $task;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_DREV:
           $revision_dao = newv('DifferentialRevision', array());
           $revisions = $revision_dao->loadAllWhere(
             'phid IN (%Ls)',
             $phids);
           foreach ($revisions as $revision) {
             $objects[$revision->getPHID()] = $revision;
           }
           break;
       }
     }
 
     return $objects;
   }
 
   public function loadHandles() {
 
     $types = array();
     foreach ($this->phids as $phid) {
       $type = $this->lookupType($phid);
       $types[$type][] = $phid;
     }
 
     $handles = array();
 
     $external_loaders = PhabricatorEnv::getEnvConfig('phid.external-loaders');
 
     foreach ($types as $type => $phids) {
       switch ($type) {
         case PhabricatorPHIDConstants::PHID_TYPE_MAGIC:
           // Black magic!
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             switch ($phid) {
               case ManiphestTaskOwner::OWNER_UP_FOR_GRABS:
                 $handle->setName('Up For Grabs');
                 $handle->setFullName('upforgrabs (Up For Grabs)');
                 $handle->setComplete(true);
                 break;
               default:
                 $handle->setName('Foul Magicks');
                 break;
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_USER:
           $class = 'PhabricatorUser';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $users = $object->loadAllWhere('phid IN (%Ls)', $phids);
           $users = mpull($users, null, 'getPHID');
 
           $image_phids = mpull($users, 'getProfileImagePHID');
           $image_phids = array_unique(array_filter($image_phids));
 
           $images = array();
           if ($image_phids) {
             $images = id(new PhabricatorFile())->loadAllWhere(
               'phid IN (%Ls)',
               $image_phids);
             $images = mpull($images, 'getViewURI', 'getPHID');
           }
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($users[$phid])) {
               $handle->setName('Unknown User');
             } else {
               $user = $users[$phid];
               $handle->setName($user->getUsername());
               $handle->setURI('/p/'.$user->getUsername().'/');
               $handle->setEmail($user->getEmail());
               $handle->setFullName(
                 $user->getUsername().' ('.$user->getRealName().')');
               $handle->setAlternateID($user->getID());
               $handle->setComplete(true);
+              $handle->setDisabled($user->getIsDisabled());
 
               $img_uri = idx($images, $user->getProfileImagePHID());
               if ($img_uri) {
                 $handle->setImageURI($img_uri);
               }
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_MLST:
           $class = 'PhabricatorMetaMTAMailingList';
 
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $lists = $object->loadAllWhere('phid IN (%Ls)', $phids);
           $lists = mpull($lists, null, 'getPHID');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($lists[$phid])) {
               $handle->setName('Unknown Mailing List');
             } else {
               $list = $lists[$phid];
               $handle->setEmail($list->getEmail());
               $handle->setName($list->getName());
               $handle->setURI($list->getURI());
               $handle->setFullName($list->getName());
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_DREV:
           $class = 'DifferentialRevision';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $revs = $object->loadAllWhere('phid in (%Ls)', $phids);
           $revs = mpull($revs, null, 'getPHID');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($revs[$phid])) {
               $handle->setName('Unknown Revision');
             } else {
               $rev = $revs[$phid];
               $handle->setName($rev->getTitle());
               $handle->setURI('/D'.$rev->getID());
               $handle->setFullName('D'.$rev->getID().': '.$rev->getTitle());
               $handle->setComplete(true);
 
               $status = $rev->getStatus();
               if (($status == DifferentialRevisionStatus::COMMITTED) ||
                   ($status == DifferentialRevisionStatus::ABANDONED)) {
                 $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED;
                 $handle->setStatus($closed);
               }
 
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_CMIT:
           $class = 'PhabricatorRepositoryCommit';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $commits = $object->loadAllWhere('phid in (%Ls)', $phids);
           $commits = mpull($commits, null, 'getPHID');
 
           $repository_ids = mpull($commits, 'getRepositoryID');
           $repositories = id(new PhabricatorRepository())->loadAllWhere(
             'id in (%Ld)', array_unique($repository_ids));
           $callsigns = mpull($repositories, 'getCallsign');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($commits[$phid]) ||
                 !isset($callsigns[$repository_ids[$phid]])) {
               $handle->setName('Unknown Commit');
             } else {
               $commit = $commits[$phid];
               $callsign = $callsigns[$repository_ids[$phid]];
               $repository = $repositories[$repository_ids[$phid]];
               $commit_identifier = $commit->getCommitIdentifier();
 
               // In case where the repository for the commit was deleted,
               // we don't have have info about the repository anymore.
               if ($repository) {
                 $vcs = $repository->getVersionControlSystem();
                 if ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
                   $short_identifier = substr($commit_identifier, 0, 16);
                 } else {
                   $short_identifier = $commit_identifier;
                 }
 
                 $handle->setName('r'.$callsign.$short_identifier);
               } else {
 
                 $handle->setName('Commit '.'r'.$callsign.$commit_identifier);
               }
 
               $handle->setURI('/r'.$callsign.$commit_identifier);
               $handle->setFullName('r'.$callsign.$commit_identifier);
               $handle->setTimestamp($commit->getEpoch());
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_TASK:
           $class = 'ManiphestTask';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $tasks = $object->loadAllWhere('phid in (%Ls)', $phids);
           $tasks = mpull($tasks, null, 'getPHID');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($tasks[$phid])) {
               $handle->setName('Unknown Revision');
             } else {
               $task = $tasks[$phid];
               $handle->setName($task->getTitle());
               $handle->setURI('/T'.$task->getID());
               $handle->setFullName('T'.$task->getID().': '.$task->getTitle());
               $handle->setComplete(true);
               if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) {
                 $closed = PhabricatorObjectHandleStatus::STATUS_CLOSED;
                 $handle->setStatus($closed);
               }
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_FILE:
           $class = 'PhabricatorFile';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $files = $object->loadAllWhere('phid IN (%Ls)', $phids);
           $files = mpull($files, null, 'getPHID');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($files[$phid])) {
               $handle->setName('Unknown File');
             } else {
               $file = $files[$phid];
               $handle->setName($file->getName());
               $handle->setURI($file->getViewURI());
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_PROJ:
           $class = 'PhabricatorProject';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $projects = $object->loadAllWhere('phid IN (%Ls)', $phids);
           $projects = mpull($projects, null, 'getPHID');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($projects[$phid])) {
               $handle->setName('Unknown Project');
             } else {
               $project = $projects[$phid];
               $handle->setName($project->getName());
               $handle->setURI('/project/view/'.$project->getID().'/');
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_REPO:
           $class = 'PhabricatorRepository';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $repositories = $object->loadAllWhere('phid in (%Ls)', $phids);
           $repositories = mpull($repositories, null, 'getPHID');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($repositories[$phid])) {
               $handle->setName('Unknown Repository');
             } else {
               $repository = $repositories[$phid];
               $handle->setName($repository->getCallsign());
               $handle->setURI('/diffusion/'.$repository->getCallsign().'/');
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_OPKG:
           $class = 'PhabricatorOwnersPackage';
           PhutilSymbolLoader::loadClass($class);
           $object = newv($class, array());
 
           $packages = $object->loadAllWhere('phid in (%Ls)', $phids);
           $packages = mpull($packages, null, 'getPHID');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($packages[$phid])) {
               $handle->setName('Unknown Package');
             } else {
               $package = $packages[$phid];
               $handle->setName($package->getName());
               $handle->setURI('/owners/package/'.$package->getID().'/');
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_APRJ:
           $project_dao = newv('PhabricatorRepositoryArcanistProject', array());
 
           $projects = $project_dao->loadAllWhere(
             'phid IN (%Ls)',
             $phids);
           $projects = mpull($projects, null, 'getPHID');
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($projects[$phid])) {
               $handle->setName('Unknown Arcanist Project');
             } else {
               $project = $projects[$phid];
               $handle->setName($project->getName());
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         case PhabricatorPHIDConstants::PHID_TYPE_WIKI:
           $document_dao = newv('PhrictionDocument', array());
           $content_dao  = newv('PhrictionContent', array());
 
           $conn = $document_dao->establishConnection('r');
           $documents = queryfx_all(
             $conn,
             'SELECT * FROM %T document JOIN %T content
               ON document.contentID = content.id
               WHERE document.phid IN (%Ls)',
               $document_dao->getTableName(),
               $content_dao->getTableName(),
               $phids);
           $documents = ipull($documents, null, 'phid');
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setPHID($phid);
             $handle->setType($type);
             if (empty($documents[$phid])) {
               $handle->setName('Unknown Document');
             } else {
               $info = $documents[$phid];
               $handle->setName($info['title']);
               $handle->setURI(PhrictionDocument::getSlugURI($info['slug']));
               $handle->setComplete(true);
             }
             $handles[$phid] = $handle;
           }
           break;
         default:
           $loader = null;
           if (isset($external_loaders[$type])) {
             $loader = $external_loaders[$type];
           } else if (isset($external_loaders['*'])) {
             $loader = $external_loaders['*'];
           }
 
           if ($loader) {
             PhutilSymbolLoader::loadClass($loader);
             $object = newv($loader, array());
             $handles += $object->loadHandles($phids);
             break;
           }
 
           foreach ($phids as $phid) {
             $handle = new PhabricatorObjectHandle();
             $handle->setType($type);
             $handle->setPHID($phid);
             $handle->setName('Unknown Object');
             $handle->setFullName('An Unknown Object');
             $handles[$phid] = $handle;
           }
           break;
       }
     }
 
     return $handles;
   }
 
   private function lookupType($phid) {
     $matches = null;
     if (preg_match('/^PHID-([^-]{4})-/', $phid, $matches)) {
       return $matches[1];
     }
     return PhabricatorPHIDConstants::PHID_TYPE_UNKNOWN;
   }
 
 }