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);
|
|
|
|
}
|
|
|
|
|
2012-08-28 23:09:37 +02:00
|
|
|
/**
|
|
|
|
* Parses "to" addresses, looking for a public create email address
|
|
|
|
* first and if not found parsing the "to" address for reply handler
|
2013-02-08 00:17:11 +01:00
|
|
|
* information: receiver name, user id, and hash. If nothing can be
|
|
|
|
* found, it then loads user phids for as many to: email addresses as
|
|
|
|
* it can, theoretically falling back to create a conpherence amongst
|
|
|
|
* those users.
|
2012-08-28 23:09:37 +02:00
|
|
|
*/
|
|
|
|
private function getPhabricatorToInformation() {
|
|
|
|
// Only one "public" create address so far
|
|
|
|
$create_task = PhabricatorEnv::getEnvConfig(
|
|
|
|
'metamta.maniphest.public-create-email');
|
|
|
|
|
|
|
|
// For replies, look for an object address with a format like:
|
|
|
|
// D291+291+b0a41ca848d66dcc@example.com
|
|
|
|
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
|
|
|
|
'metamta.single-reply-handler-prefix');
|
|
|
|
|
|
|
|
$prefixPattern = ($single_handle_prefix)
|
|
|
|
? preg_quote($single_handle_prefix, '/') . '\+'
|
|
|
|
: '';
|
2013-01-26 18:14:19 +01:00
|
|
|
$pattern = "/^{$prefixPattern}((?:D|T|C|E)\d+)\+([\w]+)\+([a-f0-9]{16})@/U";
|
2012-08-28 23:09:37 +02:00
|
|
|
|
|
|
|
$phabricator_address = null;
|
|
|
|
$receiver_name = null;
|
|
|
|
$user_id = null;
|
|
|
|
$hash = null;
|
2013-02-08 00:17:11 +01:00
|
|
|
$user_names = array();
|
2012-08-28 23:09:37 +02:00
|
|
|
foreach ($this->getToAddresses() as $address) {
|
|
|
|
if ($address == $create_task) {
|
|
|
|
$phabricator_address = $address;
|
|
|
|
// it's okay to stop here because we just need to map a create
|
|
|
|
// address to an application and don't need / won't have more
|
|
|
|
// information in these cases.
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
$matches = null;
|
|
|
|
$ok = preg_match(
|
|
|
|
$pattern,
|
|
|
|
$address,
|
|
|
|
$matches);
|
|
|
|
|
|
|
|
if ($ok) {
|
|
|
|
$phabricator_address = $address;
|
|
|
|
$receiver_name = $matches[1];
|
|
|
|
$user_id = $matches[2];
|
|
|
|
$hash = $matches[3];
|
|
|
|
break;
|
|
|
|
}
|
2013-02-08 00:17:11 +01:00
|
|
|
|
|
|
|
$parts = explode('@', $address);
|
|
|
|
$maybe_name = trim($parts[0]);
|
|
|
|
$maybe_domain = trim($parts[1]);
|
|
|
|
$mail_domain = PhabricatorEnv::getEnvConfig('metamta.domain');
|
|
|
|
if ($mail_domain == $maybe_domain &&
|
|
|
|
PhabricatorUser::validateUsername($maybe_name)) {
|
|
|
|
$user_names[] = $maybe_name;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2012-08-28 23:09:37 +02:00
|
|
|
return array(
|
|
|
|
$phabricator_address,
|
|
|
|
$receiver_name,
|
|
|
|
$user_id,
|
2013-02-08 00:17:11 +01:00
|
|
|
$hash,
|
2012-08-28 23:09:37 +02:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
// TODO: Once everything can receive mail, nuke this.
|
|
|
|
$can_receive = false;
|
|
|
|
if ($receiver instanceof ManiphestCreateMailReceiver) {
|
|
|
|
$can_receive = true;
|
|
|
|
}
|
|
|
|
if ($receiver instanceof ConpherenceCreateThreadMailReceiver) {
|
|
|
|
$can_receive = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($can_receive) {
|
|
|
|
$receiver->receiveMail($this, $sender);
|
|
|
|
return $this->setMessage('OK')->save();
|
|
|
|
}
|
|
|
|
|
2013-05-14 01:32:19 +02:00
|
|
|
} catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
|
|
|
|
$this
|
|
|
|
->setStatus($ex->getStatusCode())
|
|
|
|
->setMessage($ex->getMessage())
|
|
|
|
->save();
|
|
|
|
return $this;
|
2013-01-04 02:04:30 +01:00
|
|
|
}
|
|
|
|
|
2012-08-28 23:09:37 +02:00
|
|
|
list($to,
|
|
|
|
$receiver_name,
|
|
|
|
$user_id,
|
2013-05-17 12:51:57 +02:00
|
|
|
$hash) = $this->getPhabricatorToInformation();
|
|
|
|
if (!$to) {
|
2012-08-28 23:09:37 +02:00
|
|
|
$raw_to = idx($this->headers, 'to');
|
|
|
|
return $this->setMessage("Unrecognized 'to' format: {$raw_to}")->save();
|
|
|
|
}
|
2011-07-04 18:45:42 +02:00
|
|
|
|
|
|
|
$from = idx($this->headers, 'from');
|
|
|
|
|
2013-05-17 12:49:29 +02:00
|
|
|
$user = $sender;
|
2011-05-05 08:09:42 +02:00
|
|
|
|
|
|
|
$receiver = self::loadReceiverObject($receiver_name);
|
|
|
|
if (!$receiver) {
|
|
|
|
return $this->setMessage("Invalid object '{$receiver_name}'")->save();
|
|
|
|
}
|
|
|
|
|
|
|
|
$this->setRelatedPHID($receiver->getPHID());
|
|
|
|
|
|
|
|
if ($receiver instanceof ManiphestTask) {
|
2011-05-16 21:31:18 +02:00
|
|
|
$editor = new ManiphestTransactionEditor();
|
2012-10-10 19:18:23 +02:00
|
|
|
$editor->setActor($user);
|
2011-05-16 21:31:18 +02:00
|
|
|
$handler = $editor->buildReplyHandler($receiver);
|
2011-05-05 08:09:42 +02:00
|
|
|
} else if ($receiver instanceof DifferentialRevision) {
|
2011-05-16 21:31:18 +02:00
|
|
|
$handler = DifferentialMail::newReplyHandlerForRevision($receiver);
|
2012-02-27 21:57:57 +01:00
|
|
|
} else if ($receiver instanceof PhabricatorRepositoryCommit) {
|
|
|
|
$handler = PhabricatorAuditCommentEditor::newReplyHandlerForCommit(
|
|
|
|
$receiver);
|
2013-01-26 01:03:54 +01:00
|
|
|
} else if ($receiver instanceof ConpherenceThread) {
|
2013-01-26 19:49:20 +01:00
|
|
|
$handler = id(new ConpherenceReplyHandler())
|
|
|
|
->setMailReceiver($receiver);
|
2011-05-05 08:09:42 +02:00
|
|
|
}
|
|
|
|
|
2011-05-16 21:31:18 +02:00
|
|
|
$handler->setActor($user);
|
2012-10-10 19:18:23 +02:00
|
|
|
$handler->setExcludeMailRecipientPHIDs(
|
|
|
|
$this->loadExcludeMailRecipientPHIDs());
|
2012-08-28 23:09:37 +02:00
|
|
|
$handler->processEmail($this);
|
2011-05-16 21:31:18 +02:00
|
|
|
|
2011-05-05 08:09:42 +02:00
|
|
|
$this->setMessage('OK');
|
|
|
|
|
|
|
|
return $this->save();
|
|
|
|
}
|
|
|
|
|
2011-05-16 21:31:18 +02:00
|
|
|
public function getCleanTextBody() {
|
2011-05-05 08:09:42 +02:00
|
|
|
$body = idx($this->bodies, 'text');
|
|
|
|
|
2012-04-09 00:04:12 +02:00
|
|
|
$parser = new PhabricatorMetaMTAEmailBodyParser();
|
|
|
|
return $parser->stripTextBody($body);
|
2011-05-05 08:09:42 +02:00
|
|
|
}
|
|
|
|
|
2012-07-12 22:33:26 +02:00
|
|
|
public function getRawTextBody() {
|
|
|
|
return idx($this->bodies, 'text');
|
|
|
|
}
|
|
|
|
|
2011-05-05 08:09:42 +02:00
|
|
|
public static function loadReceiverObject($receiver_name) {
|
|
|
|
if (!$receiver_name) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$receiver_type = $receiver_name[0];
|
|
|
|
$receiver_id = substr($receiver_name, 1);
|
|
|
|
|
|
|
|
$class_obj = null;
|
|
|
|
switch ($receiver_type) {
|
|
|
|
case 'T':
|
2012-05-31 01:38:53 +02:00
|
|
|
$class_obj = new ManiphestTask();
|
2011-05-05 08:09:42 +02:00
|
|
|
break;
|
|
|
|
case 'D':
|
2012-05-31 01:38:53 +02:00
|
|
|
$class_obj = new DifferentialRevision();
|
2011-05-05 08:09:42 +02:00
|
|
|
break;
|
2012-02-27 21:57:57 +01:00
|
|
|
case 'C':
|
2012-05-31 01:38:53 +02:00
|
|
|
$class_obj = new PhabricatorRepositoryCommit();
|
2012-02-27 21:57:57 +01:00
|
|
|
break;
|
2013-01-26 01:03:54 +01:00
|
|
|
case 'E':
|
|
|
|
$class_obj = new ConpherenceThread();
|
|
|
|
break;
|
2011-05-05 08:09:42 +02:00
|
|
|
default:
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $class_obj->load($receiver_id);
|
|
|
|
}
|
|
|
|
|
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
|
|
|
}
|