diff --git a/resources/sql/patches/136.sex.sql b/resources/sql/patches/136.sex.sql
new file mode 100644
index 000000000..be7d4e210
--- /dev/null
+++ b/resources/sql/patches/136.sex.sql
@@ -0,0 +1,2 @@
+ALTER TABLE `phabricator_user`.`user`
+  ADD `sex` char(1) COLLATE utf8_bin AFTER `email`;
diff --git a/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php b/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php
index 41ef344a9..8c8e75c85 100644
--- a/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php
+++ b/src/applications/people/controller/settings/panels/profile/PhabricatorUserProfileSettingsPanelController.php
@@ -1,176 +1,195 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 final class PhabricatorUserProfileSettingsPanelController
   extends PhabricatorUserSettingsPanelController {
 
   public function processRequest() {
 
     $request = $this->getRequest();
     $user = $request->getUser();
 
     $profile = id(new PhabricatorUserProfile())->loadOneWhere(
       'userPHID = %s',
       $user->getPHID());
     if (!$profile) {
       $profile = new PhabricatorUserProfile();
       $profile->setUserPHID($user->getPHID());
     }
 
     $supported_formats = PhabricatorFile::getTransformableImageFormats();
 
     $e_image = null;
     $errors = array();
     if ($request->isFormPost()) {
       $profile->setTitle($request->getStr('title'));
       $profile->setBlurb($request->getStr('blurb'));
 
+      $sex = $request->getStr('sex');
+      if (in_array($sex, array('m', 'f'))) {
+        $user->setSex($sex);
+      } else {
+        $user->setSex(null);
+      }
+
       if (!empty($_FILES['image'])) {
         $err = idx($_FILES['image'], 'error');
         if ($err != UPLOAD_ERR_NO_FILE) {
           $file = PhabricatorFile::newFromPHPUpload(
             $_FILES['image'],
             array(
               'authorPHID' => $user->getPHID(),
             ));
           $okay = $file->isTransformableImage();
           if ($okay) {
             $xformer = new PhabricatorImageTransformer();
 
             // Generate the large picture for the profile page.
             $large_xformed = $xformer->executeProfileTransform(
               $file,
               $width = 280,
               $min_height = 140,
               $max_height = 420);
             $profile->setProfileImagePHID($large_xformed->getPHID());
 
             // Generate the small picture for comments, etc.
             $small_xformed = $xformer->executeProfileTransform(
               $file,
               $width = 50,
               $min_height = 50,
               $max_height = 50);
             $user->setProfileImagePHID($small_xformed->getPHID());
           } else {
             $e_image = 'Not Supported';
             $errors[] =
               'This server only supports these image formats: '.
               implode(', ', $supported_formats).'.';
           }
         }
       }
 
       if (!$errors) {
         $user->save();
         $profile->save();
         $response = id(new AphrontRedirectResponse())
           ->setURI('/settings/page/profile/?saved=true');
         return $response;
       }
     }
 
     $error_view = null;
     if ($errors) {
       $error_view = new AphrontErrorView();
       $error_view->setTitle('Form Errors');
       $error_view->setErrors($errors);
     } else {
       if ($request->getStr('saved')) {
         $error_view = new AphrontErrorView();
         $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
         $error_view->setTitle('Changes Saved');
         $error_view->appendChild('<p>Your changes have been saved.</p>');
         $error_view = $error_view->render();
       }
     }
 
     $file = id(new PhabricatorFile())->loadOneWhere(
       'phid = %s',
       $user->getProfileImagePHID());
     if ($file) {
       $img_src = $file->getBestURI();
     } else {
       $img_src = null;
     }
     $profile_uri = PhabricatorEnv::getURI('/p/'.$user->getUsername().'/');
 
+    $sexes = array(
+      '' => 'Unknown',
+      'm' => 'Male',
+      'f' => 'Female',
+    );
+
     $form = new AphrontFormView();
     $form
       ->setUser($request->getUser())
       ->setAction('/settings/page/profile/')
       ->setEncType('multipart/form-data')
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel('Title')
           ->setName('title')
           ->setValue($profile->getTitle())
           ->setCaption('Serious business title.'))
+      ->appendChild(
+        id(new AphrontFormSelectControl())
+          ->setOptions($sexes)
+          ->setLabel('Sex')
+          ->setName('sex')
+          ->setValue($user->getSex()))
       ->appendChild(
         id(new AphrontFormMarkupControl())
           ->setLabel('Profile URI')
           ->setValue(
             phutil_render_tag(
               'a',
               array(
                 'href' => $profile_uri,
               ),
               phutil_escape_html($profile_uri))))
       ->appendChild(
         '<p class="aphront-form-instructions">Write something about yourself! '.
         'Make sure to include <strong>important information</strong> like '.
         'your favorite pokemon and which Starcraft race you play.</p>')
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setLabel('Blurb')
           ->setName('blurb')
           ->setValue($profile->getBlurb()))
       ->appendChild(
         id(new AphrontFormMarkupControl())
           ->setLabel('Profile Image')
           ->setValue(
             phutil_render_tag(
               'img',
               array(
                 'src' => $img_src,
               ))))
       ->appendChild(
         id(new AphrontFormFileControl())
           ->setLabel('Change Image')
           ->setName('image')
           ->setError($e_image)
           ->setCaption('Supported formats: '.implode(', ', $supported_formats)))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue('Save')
           ->addCancelButton('/p/'.$user->getUsername().'/'));
 
     $panel = new AphrontPanelView();
     $panel->setHeader('Edit Profile Details');
     $panel->appendChild($form);
     $panel->setWidth(AphrontPanelView::WIDTH_FORM);
 
     return id(new AphrontNullView())
       ->appendChild(
         array(
           $error_view,
           $panel,
         ));
   }
 
 }
diff --git a/src/applications/people/controller/settings/panels/profile/__init__.php b/src/applications/people/controller/settings/panels/profile/__init__.php
index 545d4a981..f4fbe293c 100644
--- a/src/applications/people/controller/settings/panels/profile/__init__.php
+++ b/src/applications/people/controller/settings/panels/profile/__init__.php
@@ -1,29 +1,30 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'aphront/response/redirect');
 phutil_require_module('phabricator', 'applications/files/storage/file');
 phutil_require_module('phabricator', 'applications/files/transform');
 phutil_require_module('phabricator', 'applications/people/controller/settings/panels/base');
 phutil_require_module('phabricator', 'applications/people/storage/profile');
 phutil_require_module('phabricator', 'infrastructure/env');
 phutil_require_module('phabricator', 'view/form/base');
 phutil_require_module('phabricator', 'view/form/control/file');
 phutil_require_module('phabricator', 'view/form/control/markup');
+phutil_require_module('phabricator', 'view/form/control/select');
 phutil_require_module('phabricator', 'view/form/control/submit');
 phutil_require_module('phabricator', 'view/form/control/text');
 phutil_require_module('phabricator', 'view/form/control/textarea');
 phutil_require_module('phabricator', 'view/form/error');
 phutil_require_module('phabricator', 'view/layout/panel');
 phutil_require_module('phabricator', 'view/null');
 
 phutil_require_module('phutil', 'markup');
 phutil_require_module('phutil', 'utils');
 
 
 phutil_require_source('PhabricatorUserProfileSettingsPanelController.php');
diff --git a/src/applications/people/storage/user/PhabricatorUser.php b/src/applications/people/storage/user/PhabricatorUser.php
index ae28af560..b5d495ac5 100644
--- a/src/applications/people/storage/user/PhabricatorUser.php
+++ b/src/applications/people/storage/user/PhabricatorUser.php
@@ -1,525 +1,526 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 final class PhabricatorUser extends PhabricatorUserDAO {
 
   const SESSION_TABLE = 'phabricator_session';
   const NAMETOKEN_TABLE = 'user_nametoken';
 
   protected $phid;
   protected $userName;
   protected $realName;
   protected $email;
+  protected $sex;
   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) {
     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);
     }
   }
 
   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) {
 
     if (!$this->getPHID()) {
       return true;
     }
 
     // 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(PhabricatorHash::digest($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!");
     }
 
     // NOTE: Session establishment is sensitive to race conditions, as when
     // piping `arc` to `arc`:
     //
     //   arc export ... | arc paste ...
     //
     // To avoid this, we overwrite an old session only if it hasn't been
     // re-established since we read it.
 
     // Consume entropy to generate a new session key, forestalling the eventual
     // heat death of the universe.
     $session_key = Filesystem::readRandomCharacters(40);
 
     // Load all the currently active sessions.
     $sessions = queryfx_all(
       $conn_w,
       'SELECT type, sessionKey, sessionStart FROM %T
         WHERE userPHID = %s AND type LIKE %>',
       PhabricatorUser::SESSION_TABLE,
       $this->getPHID(),
       $session_type.'-');
     $sessions = ipull($sessions, null, 'type');
     $sessions = isort($sessions, 'sessionStart');
 
     $existing_sessions = array_keys($sessions);
 
     // UNGUARDED WRITES: Logging-in users don't have CSRF stuff yet.
     $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
 
     $retries = 0;
     while (true) {
 
 
       // 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;
       for ($ii = 1; $ii <= $session_limit; $ii++) {
         $try_type = $session_type.'-'.$ii;
         if (!in_array($try_type, $existing_sessions)) {
           $establish_type = $try_type;
           $expect_key = $session_key;
           $existing_sessions[] = $try_type;
 
           // Ensure the row exists so we can issue an update below. We don't
           // care if we race here or not.
           queryfx(
             $conn_w,
             'INSERT IGNORE INTO %T (userPHID, type, sessionKey, sessionStart)
               VALUES (%s, %s, %s, 0)',
             self::SESSION_TABLE,
             $this->getPHID(),
             $establish_type,
             $session_key);
           break;
         }
       }
 
       // If we didn't find an available session, choose the oldest session and
       // overwrite it.
       if (!$establish_type) {
         $oldest = reset($sessions);
         $establish_type = $oldest['type'];
         $expect_key = $oldest['sessionKey'];
       }
 
       // This is so that we'll only overwrite the session if it hasn't been
       // refreshed since we read it. If it has, the session key will be
       // different and we know we're racing other processes. Whichever one
       // won gets the session, we go back and try again.
 
       queryfx(
         $conn_w,
         'UPDATE %T SET sessionKey = %s, sessionStart = UNIX_TIMESTAMP()
           WHERE userPHID = %s AND type = %s AND sessionKey = %s',
         self::SESSION_TABLE,
         $session_key,
         $this->getPHID(),
         $establish_type,
         $expect_key);
 
       if ($conn_w->getAffectedRows()) {
         // The update worked, so the session is valid.
         break;
       } else {
         // We know this just got grabbed, so don't try it again.
         unset($sessions[$establish_type]);
       }
 
       if (++$retries > $session_limit) {
         throw new Exception("Failed to establish a session!");
       }
     }
 
     $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_EDITOR => '',
         PhabricatorUserPreferences::PREFERENCE_MONOSPACED => '');
 
       $preferences->setPreferences($default_dict);
     }
 
     $this->preferences = $preferences;
     return $preferences;
   }
 
   public function loadEditorLink($path, $line, $callsign) {
     $editor = $this->loadPreferences()->getPreference(
       PhabricatorUserPreferences::PREFERENCE_EDITOR);
     if ($editor) {
       return strtr($editor, array(
         '%%' => '%',
         '%f' => phutil_escape_uri($path),
         '%l' => phutil_escape_uri($line),
         '%r' => phutil_escape_uri($callsign),
       ));
     }
   }
 
   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));
     }
   }
 
   public function sendWelcomeEmail(PhabricatorUser $admin) {
     $admin_username = $admin->getUserName();
     $admin_realname = $admin->getRealName();
     $user_username = $this->getUserName();
     $is_serious = PhabricatorEnv::getEnvConfig('phabricator.serious-business');
 
     $base_uri = PhabricatorEnv::getProductionURI('/');
 
     $uri = $this->getEmailLoginURI();
     $body = <<<EOBODY
 Welcome to Phabricator!
 
 {$admin_username} ({$admin_realname}) has created an account for you.
 
   Username: {$user_username}
 
 To login to Phabricator, follow this link and set a password:
 
   {$uri}
 
 After you have set a password, you can login in the future by going here:
 
   {$base_uri}
 
 EOBODY;
 
     if (!$is_serious) {
       $body .= <<<EOBODY
 
 Love,
 Phabricator
 
 EOBODY;
     }
 
     $mail = id(new PhabricatorMetaMTAMail())
       ->addTos(array($this->getPHID()))
       ->setSubject('[Phabricator] Welcome to Phabricator')
       ->setBody($body)
       ->setFrom($admin->getPHID())
       ->saveAndSend();
   }
 
   public static function validateUsername($username) {
     return (bool)preg_match('/^[a-zA-Z0-9]+$/', $username);
   }
 
 }