[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |