2011-05-05 08:09:42 +02:00
|
|
|
<?php
|
|
|
|
|
2012-03-14 00:21:04 +01:00
|
|
|
final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
|
2011-05-05 08:09:42 +02:00
|
|
|
|
|
|
|
protected $headers = array();
|
|
|
|
protected $bodies = array();
|
|
|
|
protected $attachments = array();
|
2013-05-14 01:32:19 +02:00
|
|
|
protected $status = '';
|
2011-05-05 08:09:42 +02:00
|
|
|
|
|
|
|
protected $relatedPHID;
|
|
|
|
protected $authorPHID;
|
|
|
|
protected $message;
|
2013-05-14 01:32:19 +02:00
|
|
|
protected $messageIDHash = '';
|
2011-05-05 08:09:42 +02:00
|
|
|
|
|
|
|
public function getConfiguration() {
|
|
|
|
return array(
|
|
|
|
self::CONFIG_SERIALIZATION => array(
|
|
|
|
'headers' => self::SERIALIZATION_JSON,
|
|
|
|
'bodies' => self::SERIALIZATION_JSON,
|
|
|
|
'attachments' => self::SERIALIZATION_JSON,
|
|
|
|
),
|
|
|
|
) + parent::getConfiguration();
|
|
|
|
}
|
|
|
|
|
2011-05-30 20:07:05 +02:00
|
|
|
public function setHeaders(array $headers) {
|
|
|
|
// Normalize headers to lowercase.
|
|
|
|
$normalized = array();
|
|
|
|
foreach ($headers as $name => $value) {
|
2013-05-14 01:32:19 +02:00
|
|
|
$name = $this->normalizeMailHeaderName($name);
|
|
|
|
if ($name == 'message-id') {
|
|
|
|
$this->setMessageIDHash(PhabricatorHash::digestForIndex($value));
|
|
|
|
}
|
|
|
|
$normalized[$name] = $value;
|
2011-05-30 20:07:05 +02:00
|
|
|
}
|
|
|
|
$this->headers = $normalized;
|
|
|
|
return $this;
|
|
|
|
}
|
|
|
|
|
2013-05-14 01:32:19 +02:00
|
|
|
public function getHeader($key, $default = null) {
|
|
|
|
$key = $this->normalizeMailHeaderName($key);
|
|
|
|
return idx($this->headers, $key, $default);
|
|
|
|
}
|
|
|
|
|
|
|
|
private function normalizeMailHeaderName($name) {
|
|
|
|
return strtolower($name);
|
|
|
|
}
|
|
|
|
|
2011-06-22 21:41:19 +02:00
|
|
|
public function getMessageID() {
|
2013-05-14 01:32:19 +02:00
|
|
|
return $this->getHeader('Message-ID');
|
2011-06-22 21:41:19 +02:00
|
|
|
}
|
|
|
|
|
2011-07-04 18:45:42 +02:00
|
|
|
public function getSubject() {
|
2013-05-14 01:32:19 +02:00
|
|
|
return $this->getHeader('Subject');
|
2011-07-04 18:45:42 +02:00
|
|
|
}
|
|
|
|
|
2012-08-28 23:09:37 +02:00
|
|
|
public function getCCAddresses() {
|
|
|
|
return $this->getRawEmailAddresses(idx($this->headers, 'cc'));
|
|
|
|
}
|
|
|
|
|
|
|
|
public function getToAddresses() {
|
|
|
|
return $this->getRawEmailAddresses(idx($this->headers, 'to'));
|
|
|
|
}
|
|
|
|
|
2013-05-17 12:51:57 +02:00
|
|
|
public function loadExcludeMailRecipientPHIDs() {
|
2012-10-10 19:18:23 +02:00
|
|
|
$addresses = array_merge(
|
|
|
|
$this->getToAddresses(),
|
2013-02-19 22:33:10 +01:00
|
|
|
$this->getCCAddresses());
|
2012-10-10 19:18:23 +02:00
|
|
|
|
2012-11-01 23:18:06 +01:00
|
|
|
return $this->loadPHIDsFromAddresses($addresses);
|
|
|
|
}
|
|
|
|
|
|
|
|
final public function loadCCPHIDs() {
|
|
|
|
return $this->loadPHIDsFromAddresses($this->getCCAddresses());
|
|
|
|
}
|
|
|
|
|
|
|
|
private function loadPHIDsFromAddresses(array $addresses) {
|
2012-11-16 15:56:44 +01:00
|
|
|
if (empty($addresses)) {
|
|
|
|
return array();
|
|
|
|
}
|
2012-10-10 19:18:23 +02:00
|
|
|
$users = id(new PhabricatorUserEmail())
|
|
|
|
->loadAllWhere('address IN (%Ls)', $addresses);
|
|
|
|
$user_phids = mpull($users, 'getUserPHID');
|
|
|
|
|
|
|
|
$mailing_lists = id(new PhabricatorMetaMTAMailingList())
|
|
|
|
->loadAllWhere('email in (%Ls)', $addresses);
|
|
|
|
$mailing_list_phids = mpull($mailing_lists, 'getPHID');
|
|
|
|
|
|
|
|
return array_merge($user_phids, $mailing_list_phids);
|
|
|
|
}
|
|
|
|
|
2011-05-05 08:09:42 +02:00
|
|
|
public function processReceivedMail() {
|
2012-05-22 15:02:05 +02:00
|
|
|
|
2013-05-14 01:32:19 +02:00
|
|
|
try {
|
|
|
|
$this->dropMailFromPhabricator();
|
|
|
|
$this->dropMailAlreadyReceived();
|
2013-05-14 19:57:41 +02:00
|
|
|
|
|
|
|
$receiver = $this->loadReceiver();
|
2013-05-15 17:44:54 +02:00
|
|
|
$sender = $receiver->loadSender($this);
|
2013-05-17 12:49:29 +02:00
|
|
|
$receiver->validateSender($this, $sender);
|
2013-05-14 19:57:41 +02:00
|
|
|
|
2013-05-17 12:51:57 +02:00
|
|
|
$this->setAuthorPHID($sender->getPHID());
|
|
|
|
|
2013-05-17 19:00:49 +02:00
|
|
|
$receiver->receiveMail($this, $sender);
|
2013-05-14 01:32:19 +02:00
|
|
|
} catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
|
|
|
|
$this
|
|
|
|
->setStatus($ex->getStatusCode())
|
|
|
|
->setMessage($ex->getMessage())
|
|
|
|
->save();
|
|
|
|
return $this;
|
2013-05-17 19:00:49 +02:00
|
|
|
} catch (Exception $ex) {
|
|
|
|
$this
|
|
|
|
->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION)
|
|
|
|
->setMessage(pht('Unhandled Exception: %s', $ex->getMessage()))
|
|
|
|
->save();
|
2011-05-05 08:09:42 +02:00
|
|
|
|
2013-05-17 19:00:49 +02:00
|
|
|
throw $ex;
|
2011-05-05 08:09:42 +02:00
|
|
|
}
|
|
|
|
|
2013-05-17 19:00:49 +02:00
|
|
|
return $this->setMessage('OK')->save();
|
2011-05-05 08:09:42 +02:00
|
|
|
}
|
|
|
|
|
2011-05-16 21:31:18 +02:00
|
|
|
public function getCleanTextBody() {
|
2013-10-14 21:29:41 +02:00
|
|
|
$body = $this->getRawTextBody();
|
2012-04-09 00:04:12 +02:00
|
|
|
$parser = new PhabricatorMetaMTAEmailBodyParser();
|
|
|
|
return $parser->stripTextBody($body);
|
2011-05-05 08:09:42 +02:00
|
|
|
}
|
|
|
|
|
2013-10-14 21:29:41 +02:00
|
|
|
public function parseBody() {
|
|
|
|
$body = $this->getRawTextBody();
|
|
|
|
$parser = new PhabricatorMetaMTAEmailBodyParser();
|
|
|
|
return $parser->parseBody($body);
|
|
|
|
}
|
|
|
|
|
2012-07-12 22:33:26 +02:00
|
|
|
public function getRawTextBody() {
|
|
|
|
return idx($this->bodies, 'text');
|
|
|
|
}
|
|
|
|
|
2011-07-04 18:45:42 +02:00
|
|
|
/**
|
|
|
|
* Strip an email address down to the actual user@domain.tld part if
|
|
|
|
* necessary, since sometimes it will have formatting like
|
|
|
|
* '"Abraham Lincoln" <alincoln@logcab.in>'.
|
|
|
|
*/
|
|
|
|
private function getRawEmailAddress($address) {
|
|
|
|
$matches = null;
|
|
|
|
$ok = preg_match('/<(.*)>/', $address, $matches);
|
|
|
|
if ($ok) {
|
|
|
|
$address = $matches[1];
|
|
|
|
}
|
|
|
|
return $address;
|
|
|
|
}
|
|
|
|
|
2012-08-28 23:09:37 +02:00
|
|
|
private function getRawEmailAddresses($addresses) {
|
|
|
|
$raw_addresses = array();
|
|
|
|
foreach (explode(',', $addresses) as $address) {
|
|
|
|
$raw_addresses[] = $this->getRawEmailAddress($address);
|
|
|
|
}
|
2012-11-08 21:21:50 +01:00
|
|
|
return array_filter($raw_addresses);
|
2012-08-28 23:09:37 +02:00
|
|
|
}
|
|
|
|
|
2013-05-14 01:32:19 +02:00
|
|
|
/**
|
|
|
|
* If Phabricator sent the mail, always drop it immediately. This prevents
|
|
|
|
* loops where, e.g., the public bug address is also a user email address
|
|
|
|
* and creating a bug sends them an email, which loops.
|
|
|
|
*/
|
|
|
|
private function dropMailFromPhabricator() {
|
|
|
|
if (!$this->getHeader('x-phabricator-sent-this-message')) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
throw new PhabricatorMetaMTAReceivedMailProcessingException(
|
|
|
|
MetaMTAReceivedMailStatus::STATUS_FROM_PHABRICATOR,
|
|
|
|
"Ignoring email with 'X-Phabricator-Sent-This-Message' header to avoid ".
|
|
|
|
"loops.");
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If this mail has the same message ID as some other mail, and isn't the
|
|
|
|
* first mail we we received with that message ID, we drop it as a duplicate.
|
|
|
|
*/
|
|
|
|
private function dropMailAlreadyReceived() {
|
|
|
|
$message_id_hash = $this->getMessageIDHash();
|
|
|
|
if (!$message_id_hash) {
|
|
|
|
// No message ID hash, so we can't detect duplicates. This should only
|
|
|
|
// happen with very old messages.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$messages = $this->loadAllWhere(
|
|
|
|
'messageIDHash = %s ORDER BY id ASC LIMIT 2',
|
|
|
|
$message_id_hash);
|
|
|
|
$messages_count = count($messages);
|
|
|
|
if ($messages_count <= 1) {
|
|
|
|
// If we only have one copy of this message, we're good to process it.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$first_message = reset($messages);
|
|
|
|
if ($first_message->getID() == $this->getID()) {
|
|
|
|
// If this is the first copy of the message, it is okay to process it.
|
|
|
|
// We may not have been able to to process it immediately when we received
|
|
|
|
// it, and could may have received several copies without processing any
|
|
|
|
// yet.
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
$message = sprintf(
|
|
|
|
'Ignoring email with message id hash "%s" that has been seen %d '.
|
|
|
|
'times, including this message.',
|
|
|
|
$message_id_hash,
|
|
|
|
$messages_count);
|
|
|
|
|
|
|
|
throw new PhabricatorMetaMTAReceivedMailProcessingException(
|
|
|
|
MetaMTAReceivedMailStatus::STATUS_DUPLICATE,
|
|
|
|
$message);
|
|
|
|
}
|
|
|
|
|
2013-05-14 19:57:41 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Load a concrete instance of the @{class:PhabricatorMailReceiver} which
|
|
|
|
* accepts this mail, if one exists.
|
|
|
|
*/
|
|
|
|
private function loadReceiver() {
|
|
|
|
$receivers = id(new PhutilSymbolLoader())
|
|
|
|
->setAncestorClass('PhabricatorMailReceiver')
|
|
|
|
->loadObjects();
|
|
|
|
|
|
|
|
$accept = array();
|
|
|
|
foreach ($receivers as $key => $receiver) {
|
|
|
|
if (!$receiver->isEnabled()) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if ($receiver->canAcceptMail($this)) {
|
|
|
|
$accept[$key] = $receiver;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$accept) {
|
|
|
|
throw new PhabricatorMetaMTAReceivedMailProcessingException(
|
|
|
|
MetaMTAReceivedMailStatus::STATUS_NO_RECEIVERS,
|
|
|
|
"No concrete, enabled subclasses of `PhabricatorMailReceiver` can ".
|
|
|
|
"accept this mail.");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (count($accept) > 1) {
|
|
|
|
$names = implode(', ', array_keys($accept));
|
|
|
|
throw new PhabricatorMetaMTAReceivedMailProcessingException(
|
|
|
|
MetaMTAReceivedMailStatus::STATUS_ABUNDANT_RECEIVERS,
|
|
|
|
"More than one `PhabricatorMailReceiver` claims to accept this mail.");
|
|
|
|
}
|
|
|
|
|
|
|
|
return head($accept);
|
|
|
|
}
|
|
|
|
|
2011-05-05 08:09:42 +02:00
|
|
|
}
|