[ Index ]

PHP Cross Reference of Phabricator

title

Body

[close]

/src/applications/metamta/storage/ -> PhabricatorMetaMTAReceivedMail.php (source)

   1  <?php
   2  
   3  final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
   4  
   5    protected $headers = array();
   6    protected $bodies = array();
   7    protected $attachments = array();
   8    protected $status = '';
   9  
  10    protected $relatedPHID;
  11    protected $authorPHID;
  12    protected $message;
  13    protected $messageIDHash = '';
  14  
  15    public function getConfiguration() {
  16      return array(
  17        self::CONFIG_SERIALIZATION => array(
  18          'headers'     => self::SERIALIZATION_JSON,
  19          'bodies'      => self::SERIALIZATION_JSON,
  20          'attachments' => self::SERIALIZATION_JSON,
  21        ),
  22        self::CONFIG_COLUMN_SCHEMA => array(
  23          'relatedPHID' => 'phid?',
  24          'authorPHID' => 'phid?',
  25          'message' => 'text?',
  26          'messageIDHash' => 'bytes12',
  27          'status' => 'text32',
  28        ),
  29        self::CONFIG_KEY_SCHEMA => array(
  30          'relatedPHID' => array(
  31            'columns' => array('relatedPHID'),
  32          ),
  33          'authorPHID' => array(
  34            'columns' => array('authorPHID'),
  35          ),
  36          'key_messageIDHash' => array(
  37            'columns' => array('messageIDHash'),
  38          ),
  39          'key_created' => array(
  40            'columns' => array('dateCreated'),
  41          ),
  42        ),
  43      ) + parent::getConfiguration();
  44    }
  45  
  46    public function setHeaders(array $headers) {
  47      // Normalize headers to lowercase.
  48      $normalized = array();
  49      foreach ($headers as $name => $value) {
  50        $name = $this->normalizeMailHeaderName($name);
  51        if ($name == 'message-id') {
  52          $this->setMessageIDHash(PhabricatorHash::digestForIndex($value));
  53        }
  54        $normalized[$name] = $value;
  55      }
  56      $this->headers = $normalized;
  57      return $this;
  58    }
  59  
  60    public function getHeader($key, $default = null) {
  61      $key = $this->normalizeMailHeaderName($key);
  62      return idx($this->headers, $key, $default);
  63    }
  64  
  65    private function normalizeMailHeaderName($name) {
  66      return strtolower($name);
  67    }
  68  
  69    public function getMessageID() {
  70      return $this->getHeader('Message-ID');
  71    }
  72  
  73    public function getSubject() {
  74      return $this->getHeader('Subject');
  75    }
  76  
  77    public function getCCAddresses() {
  78      return $this->getRawEmailAddresses(idx($this->headers, 'cc'));
  79    }
  80  
  81    public function getToAddresses() {
  82      return $this->getRawEmailAddresses(idx($this->headers, 'to'));
  83    }
  84  
  85    public function loadExcludeMailRecipientPHIDs() {
  86      $addresses = array_merge(
  87        $this->getToAddresses(),
  88        $this->getCCAddresses());
  89  
  90      return $this->loadPHIDsFromAddresses($addresses);
  91    }
  92  
  93    final public function loadCCPHIDs() {
  94      return $this->loadPHIDsFromAddresses($this->getCCAddresses());
  95    }
  96  
  97    private function loadPHIDsFromAddresses(array $addresses) {
  98      if (empty($addresses)) {
  99        return array();
 100      }
 101      $users = id(new PhabricatorUserEmail())
 102        ->loadAllWhere('address IN (%Ls)', $addresses);
 103      $user_phids = mpull($users, 'getUserPHID');
 104  
 105      $mailing_lists = id(new PhabricatorMetaMTAMailingList())
 106        ->loadAllWhere('email in (%Ls)', $addresses);
 107      $mailing_list_phids = mpull($mailing_lists, 'getPHID');
 108  
 109      return array_merge($user_phids,  $mailing_list_phids);
 110    }
 111  
 112    public function processReceivedMail() {
 113  
 114      try {
 115        $this->dropMailFromPhabricator();
 116        $this->dropMailAlreadyReceived();
 117  
 118        $receiver = $this->loadReceiver();
 119        $sender = $receiver->loadSender($this);
 120        $receiver->validateSender($this, $sender);
 121  
 122        $this->setAuthorPHID($sender->getPHID());
 123  
 124        $receiver->receiveMail($this, $sender);
 125      } catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
 126        switch ($ex->getStatusCode()) {
 127          case MetaMTAReceivedMailStatus::STATUS_DUPLICATE:
 128          case MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR:
 129            // Don't send an error email back in these cases, since they're
 130            // very unlikely to be the sender's fault.
 131            break;
 132          case MetaMTAReceivedMailStatus::STATUS_EMPTY_IGNORED:
 133            // This error is explicitly ignored.
 134            break;
 135          default:
 136            $this->sendExceptionMail($ex);
 137            break;
 138        }
 139  
 140        $this
 141          ->setStatus($ex->getStatusCode())
 142          ->setMessage($ex->getMessage())
 143          ->save();
 144        return $this;
 145      } catch (Exception $ex) {
 146        $this->sendExceptionMail($ex);
 147  
 148        $this
 149          ->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION)
 150          ->setMessage(pht('Unhandled Exception: %s', $ex->getMessage()))
 151          ->save();
 152  
 153        throw $ex;
 154      }
 155  
 156      return $this->setMessage('OK')->save();
 157    }
 158  
 159    public function getCleanTextBody() {
 160      $body = $this->getRawTextBody();
 161      $parser = new PhabricatorMetaMTAEmailBodyParser();
 162      return $parser->stripTextBody($body);
 163    }
 164  
 165    public function parseBody() {
 166      $body = $this->getRawTextBody();
 167      $parser = new PhabricatorMetaMTAEmailBodyParser();
 168      return $parser->parseBody($body);
 169    }
 170  
 171    public function getRawTextBody() {
 172      return idx($this->bodies, 'text');
 173    }
 174  
 175    /**
 176     * Strip an email address down to the actual [email protected] part if
 177     * necessary, since sometimes it will have formatting like
 178     * '"Abraham Lincoln" <[email protected]>'.
 179     */
 180    private function getRawEmailAddress($address) {
 181      $matches = null;
 182      $ok = preg_match('/<(.*)>/', $address, $matches);
 183      if ($ok) {
 184        $address = $matches[1];
 185      }
 186      return $address;
 187    }
 188  
 189    private function getRawEmailAddresses($addresses) {
 190      $raw_addresses = array();
 191      foreach (explode(',', $addresses) as $address) {
 192        $raw_addresses[] = $this->getRawEmailAddress($address);
 193      }
 194      return array_filter($raw_addresses);
 195    }
 196  
 197    /**
 198     * If Phabricator sent the mail, always drop it immediately. This prevents
 199     * loops where, e.g., the public bug address is also a user email address
 200     * and creating a bug sends them an email, which loops.
 201     */
 202    private function dropMailFromPhabricator() {
 203      if (!$this->getHeader('x-phabricator-sent-this-message')) {
 204        return;
 205      }
 206  
 207      throw new PhabricatorMetaMTAReceivedMailProcessingException(
 208        MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR,
 209        pht(
 210          "Ignoring email with 'X-Phabricator-Sent-This-Message' header to ".
 211          "avoid loops."));
 212    }
 213  
 214    /**
 215     * If this mail has the same message ID as some other mail, and isn't the
 216     * first mail we we received with that message ID, we drop it as a duplicate.
 217     */
 218    private function dropMailAlreadyReceived() {
 219      $message_id_hash = $this->getMessageIDHash();
 220      if (!$message_id_hash) {
 221        // No message ID hash, so we can't detect duplicates. This should only
 222        // happen with very old messages.
 223        return;
 224      }
 225  
 226      $messages = $this->loadAllWhere(
 227        'messageIDHash = %s ORDER BY id ASC LIMIT 2',
 228        $message_id_hash);
 229      $messages_count = count($messages);
 230      if ($messages_count <= 1) {
 231        // If we only have one copy of this message, we're good to process it.
 232        return;
 233      }
 234  
 235      $first_message = reset($messages);
 236      if ($first_message->getID() == $this->getID()) {
 237        // If this is the first copy of the message, it is okay to process it.
 238        // We may not have been able to to process it immediately when we received
 239        // it, and could may have received several copies without processing any
 240        // yet.
 241        return;
 242      }
 243  
 244      $message = pht(
 245        'Ignoring email with "Message-ID" hash "%s" that has been seen %d '.
 246        'times, including this message.',
 247        $message_id_hash,
 248        $messages_count);
 249  
 250      throw new PhabricatorMetaMTAReceivedMailProcessingException(
 251        MetaMTAReceivedMailStatus::STATUS_DUPLICATE,
 252        $message);
 253    }
 254  
 255  
 256    /**
 257     * Load a concrete instance of the @{class:PhabricatorMailReceiver} which
 258     * accepts this mail, if one exists.
 259     */
 260    private function loadReceiver() {
 261      $receivers = id(new PhutilSymbolLoader())
 262        ->setAncestorClass('PhabricatorMailReceiver')
 263        ->loadObjects();
 264  
 265      $accept = array();
 266      foreach ($receivers as $key => $receiver) {
 267        if (!$receiver->isEnabled()) {
 268          continue;
 269        }
 270        if ($receiver->canAcceptMail($this)) {
 271          $accept[$key] = $receiver;
 272        }
 273      }
 274  
 275      if (!$accept) {
 276        throw new PhabricatorMetaMTAReceivedMailProcessingException(
 277          MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
 278          pht(
 279            'Phabricator can not process this mail because no application '.
 280            'knows how to handle it. Check that the address you sent it to is '.
 281            'correct.'.
 282            "\n\n".
 283            '(No concrete, enabled subclass of PhabricatorMailReceiver can '.
 284            'accept this mail.)'));
 285      }
 286  
 287      if (count($accept) > 1) {
 288        $names = implode(', ', array_keys($accept));
 289        throw new PhabricatorMetaMTAReceivedMailProcessingException(
 290          MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS,
 291          pht(
 292            'Phabricator is not able to process this mail because more than '.
 293            'one application is willing to accept it, creating ambiguity. '.
 294            'Mail needs to be accepted by exactly one receiving application.'.
 295            "\n\n".
 296            'Accepting receivers: %s.',
 297            $names));
 298      }
 299  
 300      return head($accept);
 301    }
 302  
 303    private function sendExceptionMail(Exception $ex) {
 304      $from = $this->getHeader('from');
 305      if (!strlen($from)) {
 306        return;
 307      }
 308  
 309      if ($ex instanceof PhabricatorMetaMTAReceivedMailProcessingException) {
 310        $status_code = $ex->getStatusCode();
 311        $status_name = MetaMTAReceivedMailStatus::getHumanReadableName(
 312          $status_code);
 313  
 314        $title = pht('Error Processing Mail (%s)', $status_name);
 315        $description = $ex->getMessage();
 316      } else {
 317        $title = pht('Error Processing Mail (%s)', get_class($ex));
 318        $description = pht('%s: %s', get_class($ex), $ex->getMessage());
 319      }
 320  
 321      // TODO: Since headers don't necessarily have unique names, this may not
 322      // really be all the headers. It would be nice to pass the raw headers
 323      // through from the upper layers where possible.
 324  
 325      $headers = array();
 326      foreach ($this->headers as $key => $value) {
 327        $headers[] = pht('%s: %s', $key, $value);
 328      }
 329      $headers = implode("\n", $headers);
 330  
 331      $body = pht(<<<EOBODY
 332  Your email to Phabricator was not processed, because an error occurred while
 333  trying to handle it:
 334  
 335  %s
 336  
 337  -- Original Message Body -----------------------------------------------------
 338  
 339  %s
 340  
 341  -- Original Message Headers --------------------------------------------------
 342  
 343  %s
 344  
 345  EOBODY
 346  ,
 347        wordwrap($description, 78),
 348        $this->getRawTextBody(),
 349        $headers);
 350  
 351      $mail = id(new PhabricatorMetaMTAMail())
 352        ->setIsErrorEmail(true)
 353        ->setForceDelivery(true)
 354        ->setSubject($title)
 355        ->addRawTos(array($from))
 356        ->setBody($body)
 357        ->saveAndSend();
 358    }
 359  
 360  }


Generated: Sun Nov 30 09:20:46 2014 Cross-referenced by PHPXref 0.7.1