Page Menu
Home
GnuPG
Search
Configure Global Search
Log In
Files
F26446506
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Size
43 KB
Subscribers
None
View Options
diff --git a/src/applications/conpherence/mail/ConpherenceReplyHandler.php b/src/applications/conpherence/mail/ConpherenceReplyHandler.php
index d3bf51162..02019d53e 100644
--- a/src/applications/conpherence/mail/ConpherenceReplyHandler.php
+++ b/src/applications/conpherence/mail/ConpherenceReplyHandler.php
@@ -1,97 +1,96 @@
<?php
/**
* @group conpherence
*/
final class ConpherenceReplyHandler extends PhabricatorMailReplyHandler {
private $mailAddedParticipantPHIDs;
public function setMailAddedParticipantPHIDs(array $phids) {
$this->mailAddedParticipantPHIDs = $phids;
return $this;
}
public function getMailAddedParticipantPHIDs() {
return $this->mailAddedParticipantPHIDs;
}
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ConpherenceThread)) {
throw new Exception("Mail receiver is not a ConpherenceThread!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'E');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('E');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return pht('Reply to comment and attach files.');
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$conpherence = $this->getMailReceiver();
$user = $this->getActor();
if (!$conpherence->getPHID()) {
$conpherence
->attachParticipants(array())
->attachFilePHIDs(array());
} else {
$edge_type = PhabricatorEdgeConfig::TYPE_OBJECT_HAS_FILE;
$file_phids = PhabricatorEdgeQuery::loadDestinationPHIDs(
$conpherence->getPHID(),
$edge_type);
$conpherence->attachFilePHIDs($file_phids);
$participants = id(new ConpherenceParticipant())
->loadAllWhere('conpherencePHID = %s', $conpherence->getPHID());
$participants = mpull($participants, null, 'getParticipantPHID');
$conpherence->attachParticipants($participants);
}
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$editor = id(new ConpherenceEditor())
->setActor($user)
->setContentSource($content_source)
->setParentMessageID($mail->getMessageID());
$body = $mail->getCleanTextBody();
- $body = trim($body);
$file_phids = $mail->getAttachments();
$body = $this->enhanceBodyWithAttachments(
$body,
$file_phids,
'{F%d}');
$xactions = array();
if ($this->getMailAddedParticipantPHIDs()) {
$xactions[] = id(new ConpherenceTransaction())
->setTransactionType(ConpherenceTransactionType::TYPE_PARTICIPANTS)
->setNewValue(array('+' => $this->getMailAddedParticipantPHIDs()));
}
$xactions = array_merge(
$xactions,
$editor->generateTransactionsFromText(
$conpherence,
$body));
$editor->applyTransactions($conpherence, $xactions);
return $conpherence;
}
}
diff --git a/src/applications/files/mail/FileReplyHandler.php b/src/applications/files/mail/FileReplyHandler.php
index 2f77c815a..077b9d121 100644
--- a/src/applications/files/mail/FileReplyHandler.php
+++ b/src/applications/files/mail/FileReplyHandler.php
@@ -1,92 +1,81 @@
<?php
/**
* @group file
*/
final class FileReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorFile)) {
throw new Exception('Mail receiver is not a PhabricatorFile.');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'F');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('F');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return pht('Reply to comment or !unsubscribe.');
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$actor = $this->getActor();
$file = $this->getMailReceiver();
- $body = $mail->getCleanTextBody();
- $body = trim($body);
+ $body_data = $mail->parseBody();
+ $body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
- $lines = explode("\n", trim($body));
- $first_line = head($lines);
-
$xactions = array();
- $command = null;
- $matches = null;
- if (preg_match('/^!(\w+)/', $first_line, $matches)) {
- $lines = array_slice($lines, 1);
- $body = implode("\n", $lines);
- $body = trim($body);
-
- $command = $matches[1];
- }
+ $command = $body_data['body'];
switch ($command) {
case 'unsubscribe':
$xaction = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(array('-' => array($actor->getPHID())));
$xactions[] = $xaction;
break;
}
$xactions[] = id(new PhabricatorFileTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorFileTransactionComment())
->setContent($body));
$editor = id(new PhabricatorFileEditor())
->setActor($actor)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setIsPreview(false);
try {
$xactions = $editor->applyTransactions($file, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
// just do nothing, though unclear why you're sending a blank email
return true;
}
$head_xaction = head($xactions);
return $head_xaction->getID();
}
}
diff --git a/src/applications/legalpad/mail/LegalpadReplyHandler.php b/src/applications/legalpad/mail/LegalpadReplyHandler.php
index 30befc331..7ae85873a 100644
--- a/src/applications/legalpad/mail/LegalpadReplyHandler.php
+++ b/src/applications/legalpad/mail/LegalpadReplyHandler.php
@@ -1,99 +1,89 @@
<?php
/**
* @group legalpad
*/
final class LegalpadReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof LegalpadDocument)) {
throw new Exception("Mail receiver is not a LegalpadDocument!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'L');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('L');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return 'Reply to comment or !unsubscribe.';
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$actor = $this->getActor();
$document = $this->getMailReceiver();
- $body = $mail->getCleanTextBody();
- $body = trim($body);
+ $body_data = $mail->parseBody();
+ $body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
- $lines = explode("\n", trim($body));
- $first_line = head($lines);
$xactions = array();
- $command = null;
- $matches = null;
- if (preg_match('/^!(\w+)/', $first_line, $matches)) {
- $lines = array_slice($lines, 1);
- $body = implode("\n", $lines);
- $body = trim($body);
-
- $command = $matches[1];
- }
+ $command = $body_data['command'];
switch ($command) {
case 'unsubscribe':
$xaction = id(new LegalpadTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(array('-' => array($actor->getPHID())));
$xactions[] = $xaction;
break;
}
$xactions[] = id(new LegalpadTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new LegalpadTransactionComment())
->setDocumentID($document->getID())
->setLineNumber(0)
->setLineLength(0)
->setContent($body));
$editor = id(new LegalpadDocumentEditor())
->setActor($actor)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setIsPreview(false);
try {
$xactions = $editor->applyTransactions($document, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
// just do nothing, though unclear why you're sending a blank email
return true;
}
$head_xaction = head($xactions);
return $head_xaction->getID();
}
}
diff --git a/src/applications/maniphest/mail/ManiphestReplyHandler.php b/src/applications/maniphest/mail/ManiphestReplyHandler.php
index 5f9b90d36..084d55d38 100644
--- a/src/applications/maniphest/mail/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/mail/ManiphestReplyHandler.php
@@ -1,181 +1,189 @@
<?php
/**
* @group maniphest
*/
final class ManiphestReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof ManiphestTask)) {
throw new Exception("Mail receiver is not a ManiphestTask!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'T');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('T');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler-domain');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return "Reply to comment or attach files, or !close, !claim, or ".
"!unsubscribe.";
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
// NOTE: We'll drop in here on both the "reply to a task" and "create a
// new task" workflows! Make sure you test both if you make changes!
$task = $this->getMailReceiver();
$is_new_task = !$task->getID();
$user = $this->getActor();
- $body = $mail->getCleanTextBody();
- $body = trim($body);
+ $body_data = $mail->parseBody();
+ $body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$xactions = array();
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$template = new ManiphestTransaction();
$is_unsub = false;
if ($is_new_task) {
// If this is a new task, create a "User created this task." transaction
// and then set the title and description.
$xaction = clone $template;
$xaction->setTransactionType(ManiphestTransaction::TYPE_STATUS);
$xaction->setNewValue(ManiphestTaskStatus::STATUS_OPEN);
$xactions[] = $xaction;
$task->setAuthorPHID($user->getPHID());
$task->setTitle(nonempty($mail->getSubject(), 'Untitled Task'));
$task->setDescription($body);
$task->setPriority(ManiphestTaskPriority::getDefaultPriority());
} else {
- $lines = explode("\n", trim($body));
- $first_line = head($lines);
- $command = null;
- $matches = null;
- if (preg_match('/^!(\w+)/', $first_line, $matches)) {
- $lines = array_slice($lines, 1);
- $body = implode("\n", $lines);
- $body = trim($body);
-
- $command = $matches[1];
- }
+ $command = $body_data['command'];
+ $command_value = $body_data['command_value'];
$ttype = PhabricatorTransactions::TYPE_COMMENT;
$new_value = null;
switch ($command) {
case 'close':
$ttype = ManiphestTransaction::TYPE_STATUS;
$new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
break;
case 'claim':
$ttype = ManiphestTransaction::TYPE_OWNER;
$new_value = $user->getPHID();
break;
+ case 'assign':
+ $ttype = ManiphestTransaction::TYPE_OWNER;
+ if ($command_value) {
+ $assign_users = id(new PhabricatorPeopleQuery())
+ ->setViewer($user)
+ ->withUsernames(array($command_value))
+ ->execute();
+ if ($assign_users) {
+ $assign_user = head($assign_users);
+ $new_value = $assign_user->getPHID();
+ }
+ }
+ // assign to the user by default
+ if (!$new_value) {
+ $new_value = $user->getPHID();
+ }
+ break;
case 'unsubscribe':
$is_unsub = true;
$ttype = ManiphestTransaction::TYPE_CCS;
$ccs = $task->getCCPHIDs();
foreach ($ccs as $k => $phid) {
if ($phid == $user->getPHID()) {
unset($ccs[$k]);
}
}
$new_value = array_values($ccs);
break;
}
if ($ttype != PhabricatorTransactions::TYPE_COMMENT) {
$xaction = clone $template;
$xaction->setTransactionType($ttype);
$xaction->setNewValue($new_value);
$xactions[] = $xaction;
}
if (strlen($body)) {
$xaction = clone $template;
$xaction->setTransactionType(PhabricatorTransactions::TYPE_COMMENT);
$xaction->attachComment(
id(new ManiphestTransactionComment())
->setContent($body));
$xactions[] = $xaction;
}
}
$ccs = $mail->loadCCPHIDs();
$old_ccs = $task->getCCPHIDs();
$new_ccs = array_merge($old_ccs, $ccs);
if (!$is_unsub) {
$new_ccs[] = $user->getPHID();
}
$new_ccs = array_unique($new_ccs);
if (array_diff($new_ccs, $old_ccs)) {
$cc_xaction = clone $template;
$cc_xaction->setTransactionType(ManiphestTransaction::TYPE_CCS);
$cc_xaction->setNewValue($new_ccs);
$xactions[] = $cc_xaction;
}
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_WILLEDITTASK,
array(
'task' => $task,
'mail' => $mail,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
$task = $event->getValue('task');
$xactions = $event->getValue('transactions');
$editor = id(new ManiphestTransactionEditorPro())
->setActor($user)
->setParentMessageID($mail->getMessageID())
->setExcludeMailRecipientPHIDs($this->getExcludeMailRecipientPHIDs())
->setContinueOnNoEffect(true)
->setContinueOnMissingFields(true)
->setContentSource($content_source)
->applyTransactions($task, $xactions);
$event = new PhabricatorEvent(
PhabricatorEventType::TYPE_MANIPHEST_DIDEDITTASK,
array(
'task' => $task,
'new' => $is_new_task,
'transactions' => $xactions,
));
$event->setUser($user);
PhutilEventEngine::dispatchEvent($event);
}
}
diff --git a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
index 0e35fa938..1ea3a1bab 100644
--- a/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
+++ b/src/applications/metamta/parser/PhabricatorMetaMTAEmailBodyParser.php
@@ -1,59 +1,102 @@
<?php
final class PhabricatorMetaMTAEmailBodyParser {
+ /**
+ * Mails can have bodies such as
+ *
+ * !claim
+ *
+ * taking this task
+ *
+ * Or
+ *
+ * !assign epriestley
+ *
+ * please, take this task I took; its hard
+ *
+ * This function parses such an email body and returns a dictionary
+ * containing a clean body text (e.g. "taking this task"), a $command
+ * (e.g. !claim, !assign) and a $command_value (e.g. "epriestley" in the
+ * !assign example.)
+ *
+ * @return dict
+ */
+ public function parseBody($body) {
+ $body = $this->stripTextBody($body);
+ $lines = explode("\n", trim($body));
+ $first_line = head($lines);
+
+ $command = null;
+ $command_value = null;
+ $matches = null;
+ if (preg_match('/^!(\w+)\s*(\S+)?/', $first_line, $matches)) {
+ $lines = array_slice($lines, 1);
+ $body = implode("\n", $lines);
+ $body = trim($body);
+
+ $command = $matches[1];
+ $command_value = idx($matches, 2);
+ }
+
+ return array(
+ 'body' => $body,
+ 'command' => $command,
+ 'command_value' => $command_value);
+ }
+
public function stripTextBody($body) {
- return $this->stripSignature($this->stripQuotedText($body));
+ return trim($this->stripSignature($this->stripQuotedText($body)));
}
private function stripQuotedText($body) {
$body = preg_replace(
'/^\s*On\b.*\bwrote:.*?/msU',
'',
$body);
// Outlook english
$body = preg_replace(
'/^\s*-----Original Message-----.*?/imsU',
'',
$body);
// Outlook danish
$body = preg_replace(
'/^\s*-----Oprindelig Meddelelse-----.*?/imsU',
'',
$body);
// See example in T3217.
$body = preg_replace(
'/^________________________________________\s+From:.*?/imsU',
'',
$body);
return rtrim($body);
}
private function stripSignature($body) {
// Quasi-"standard" delimiter, for lols see:
// https://bugzilla.mozilla.org/show_bug.cgi?id=58406
$body = preg_replace(
'/^-- +$.*/sm',
'',
$body);
// HTC Mail application (mobile)
$body = preg_replace(
'/^\s*^Sent from my HTC smartphone.*/sm',
'',
$body);
// Apple iPhone
$body = preg_replace(
'/^\s*^Sent from my iPhone\s*$.*/sm',
'',
$body);
return rtrim($body);
}
}
diff --git a/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php b/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php
index ccdebf9d7..1f62d6029 100644
--- a/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php
+++ b/src/applications/metamta/parser/__tests__/PhabricatorMetaMTAEmailBodyParserTestCase.php
@@ -1,117 +1,155 @@
<?php
final class PhabricatorMetaMTAEmailBodyParserTestCase
extends PhabricatorTestCase {
public function testQuotedTextStripping() {
$bodies = $this->getEmailBodies();
foreach ($bodies as $body) {
$parser = new PhabricatorMetaMTAEmailBodyParser();
$stripped = $parser->stripTextBody($body);
$this->assertEqual("OKAY", $stripped);
}
}
+ public function testEmailBodyCommandParsing() {
+ $bodies = $this->getEmailBodiesWithFullCommands();
+ foreach ($bodies as $body) {
+ $parser = new PhabricatorMetaMTAEmailBodyParser();
+ $body_data = $parser->parseBody($body);
+ $this->assertEqual('OKAY', $body_data['body']);
+ $this->assertEqual('whatevs', $body_data['command']);
+ $this->assertEqual('dude', $body_data['command_value']);
+ }
+ $bodies = $this->getEmailBodiesWithPartialCommands();
+ foreach ($bodies as $body) {
+ $parser = new PhabricatorMetaMTAEmailBodyParser();
+ $body_data = $parser->parseBody($body);
+ $this->assertEqual('OKAY', $body_data['body']);
+ $this->assertEqual('whatevs', $body_data['command']);
+ $this->assertEqual(null, $body_data['command_value']);
+ }
+ }
+
+ private function getEmailBodiesWithFullCommands() {
+ $bodies = $this->getEmailBodies();
+ $with_commands = array();
+ foreach ($bodies as $body) {
+ $with_commands[] = "!whatevs dude\n" . $body;
+ }
+ return $with_commands;
+ }
+
+ private function getEmailBodiesWithPartialCommands() {
+ $bodies = $this->getEmailBodies();
+ $with_commands = array();
+ foreach ($bodies as $body) {
+ $with_commands[] = "!whatevs\n" . $body;
+ }
+ return $with_commands;
+ }
+
+
private function getEmailBodies() {
$trailing_space = ' ';
return array(
<<<EOEMAIL
OKAY
On May 30, 2011, at 8:36 PM, Someone wrote:
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
On Fri, May 27, 2011 at 9:39 AM, Someone <
somebody@somewhere.com> wrote:
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
On Fri, May 27, 2011 at 9:39 AM, Someone
<somebody@somewhere.com> wrote:
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----Oprindelig Meddelelse-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----Original Message-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----oprindelig meddelelse-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
-----original message-----
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
Sent from my HTC smartphone on the Now Network from Sprint!
-Reply message ----- From: "somebody (someone)" <
somebody@somewhere.com>
To: <somebody@somewhere.com>
Subject: Some Text Date: Mon, Apr 2, 2012 1:42 pm
> ...
EOEMAIL
,
<<<EOEMAIL
OKAY
--{$trailing_space}
Abraham Lincoln
Supreme Galactic Emperor
EOEMAIL
,
<<<EOEMAIL
OKAY
Sent from my iPhone
EOEMAIL
,
<<<EOMAIL
OKAY
________________________________________
From: Abraham Lincoln <alincoln@logcab.in>
Subject: Core World Tariffs
EOMAIL
,
);
}
}
diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
index 07245a50a..4278ce2bc 100644
--- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
+++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
@@ -1,335 +1,335 @@
<?php
abstract class PhabricatorMailReplyHandler {
private $mailReceiver;
private $actor;
private $excludePHIDs = array();
final public function setMailReceiver($mail_receiver) {
$this->validateMailReceiver($mail_receiver);
$this->mailReceiver = $mail_receiver;
return $this;
}
final public function getMailReceiver() {
return $this->mailReceiver;
}
final public function setActor(PhabricatorUser $actor) {
$this->actor = $actor;
return $this;
}
final public function getActor() {
return $this->actor;
}
final public function setExcludeMailRecipientPHIDs(array $exclude) {
$this->excludePHIDs = $exclude;
return $this;
}
final public function getExcludeMailRecipientPHIDs() {
return $this->excludePHIDs;
}
abstract public function validateMailReceiver($mail_receiver);
abstract public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle);
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.reply-handler-domain');
}
abstract public function getReplyHandlerInstructions();
abstract protected function receiveEmail(
PhabricatorMetaMTAReceivedMail $mail);
public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
$error = $this->sanityCheckEmail($mail);
if ($error) {
if ($this->shouldSendErrorEmail($mail)) {
$this->sendErrorEmail($error, $mail);
}
return null;
}
return $this->receiveEmail($mail);
}
private function sanityCheckEmail(PhabricatorMetaMTAReceivedMail $mail) {
$body = $mail->getCleanTextBody();
$attachments = $mail->getAttachments();
if (empty($body) && empty($attachments)) {
return 'Empty email body. Email should begin with an !action and / or '.
'text to comment. Inline replies and signatures are ignored.';
}
return null;
}
/**
* Only send an error email if the user is talking to just Phabricator. We
* can assume if there is only one To address it is a Phabricator address
* since this code is running and everything.
*/
private function shouldSendErrorEmail(PhabricatorMetaMTAReceivedMail $mail) {
return (count($mail->getToAddresses()) == 1) &&
(count($mail->getCCAddresses()) == 0);
}
private function sendErrorEmail($error,
PhabricatorMetaMTAReceivedMail $mail) {
$template = new PhabricatorMetaMTAMail();
$template->setSubject('Exception: unable to process your mail request');
$template->setBody($this->buildErrorMailBody($error, $mail));
$template->setRelatedPHID($mail->getRelatedPHID());
$phid = $this->getActor()->getPHID();
$handle = id(new PhabricatorHandleQuery())
->setViewer($this->getActor())
->withPHIDs(array($phid))
->executeOne();
$tos = array($phid => $handle);
$mails = $this->multiplexMail($template, $tos, array());
foreach ($mails as $email) {
$email->saveAndSend();
}
return true;
}
private function buildErrorMailBody($error,
PhabricatorMetaMTAReceivedMail $mail) {
$original_body = $mail->getRawTextBody();
$main_body = <<<EOBODY
Your request failed because an error was encoutered while processing it:
ERROR: {$error}
-- Original Body -------------------------------------------------------------
{$original_body}
EOBODY;
$body = new PhabricatorMetaMTAMailBody();
$body->addRawSection($main_body);
$body->addReplySection($this->getReplyHandlerInstructions());
return $body->render();
}
public function supportsPrivateReplies() {
return (bool)$this->getReplyHandlerDomain() &&
!$this->supportsPublicReplies();
}
public function supportsPublicReplies() {
if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
return false;
}
if (!$this->getReplyHandlerDomain()) {
return false;
}
return (bool)$this->getPublicReplyHandlerEmailAddress();
}
final public function supportsReplies() {
return $this->supportsPrivateReplies() ||
$this->supportsPublicReplies();
}
public function getPublicReplyHandlerEmailAddress() {
return null;
}
final public function getRecipientsSummary(
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$body = '';
if (PhabricatorEnv::getEnvConfig('metamta.recipients.show-hints')) {
if ($to_handles) {
$body .= "To: ".implode(', ', mpull($to_handles, 'getName'))."\n";
}
if ($cc_handles) {
$body .= "Cc: ".implode(', ', mpull($cc_handles, 'getName'))."\n";
}
}
return $body;
}
final public function multiplexMail(
PhabricatorMetaMTAMail $mail_template,
array $to_handles,
array $cc_handles) {
assert_instances_of($to_handles, 'PhabricatorObjectHandle');
assert_instances_of($cc_handles, 'PhabricatorObjectHandle');
$result = array();
// If MetaMTA is configured to always multiplex, skip the single-email
// case.
if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) {
// If private replies are not supported, simply send one email to all
// recipients and CCs. This covers cases where we have no reply handler,
// or we have a public reply handler.
if (!$this->supportsPrivateReplies()) {
$mail = clone $mail_template;
$mail->addTos(mpull($to_handles, 'getPHID'));
$mail->addCCs(mpull($cc_handles, 'getPHID'));
if ($this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
return $result;
}
}
$tos = mpull($to_handles, null, 'getPHID');
$ccs = mpull($cc_handles, null, 'getPHID');
// Merge all the recipients together. TODO: We could keep the CCs as real
// CCs and send to a "noreply@domain.com" type address, but keep it simple
// for now.
$recipients = $tos + $ccs;
// When multiplexing mail, explicitly include To/Cc information in the
// message body and headers.
$mail_template = clone $mail_template;
$mail_template->addPHIDHeaders('X-Phabricator-To', array_keys($tos));
$mail_template->addPHIDHeaders('X-Phabricator-Cc', array_keys($ccs));
$body = $mail_template->getBody();
$body .= "\n";
$body .= $this->getRecipientsSummary($to_handles, $cc_handles);
foreach ($recipients as $phid => $recipient) {
$mail = clone $mail_template;
if (isset($to_handles[$phid])) {
$mail->addTos(array($phid));
} else if (isset($cc_handles[$phid])) {
$mail->addCCs(array($phid));
} else {
// not good - they should be a to or a cc
continue;
}
$mail->setBody($body);
$reply_to = null;
if (!$reply_to && $this->supportsPrivateReplies()) {
$reply_to = $this->getPrivateReplyHandlerEmailAddress($recipient);
}
if (!$reply_to && $this->supportsPublicReplies()) {
$reply_to = $this->getPublicReplyHandlerEmailAddress();
}
if ($reply_to) {
$mail->setReplyTo($reply_to);
}
$result[] = $mail;
}
return $result;
}
protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$domain = $this->getReplyHandlerDomain();
// We compute a hash using the object's own PHID to prevent an attacker
// from blindly interacting with objects that they haven't ever received
// mail about by just sending to D1@, D2@, etc...
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$receiver->getPHID());
$address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
protected function getSingleReplyHandlerPrefix($address) {
$single_handle_prefix = PhabricatorEnv::getEnvConfig(
'metamta.single-reply-handler-prefix');
return ($single_handle_prefix)
? $single_handle_prefix . '+' . $address
: $address;
}
protected function getDefaultPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle,
$prefix) {
if ($handle->getType() != PhabricatorPeoplePHIDTypeUser::TYPECONST) {
// You must be a real user to get a private reply handler address.
return null;
}
$user = id(new PhabricatorPeopleQuery())
->setViewer(PhabricatorUser::getOmnipotentUser())
->withPHIDs(array($handle->getPHID()))
->executeOne();
if (!$user) {
// This may happen if a user was subscribed to something, and was then
// deleted.
return null;
}
$receiver = $this->getMailReceiver();
$receiver_id = $receiver->getID();
$user_id = $user->getID();
$hash = PhabricatorObjectMailReceiver::computeMailHash(
$receiver->getMailKey(),
$handle->getPHID());
$domain = $this->getReplyHandlerDomain();
$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
return $this->getSingleReplyHandlerPrefix($address);
}
- final protected function enhanceBodyWithAttachments(
+ final protected function enhanceBodyWithAttachments(
$body,
array $attachments,
$format = '- {F%d, layout=link}') {
if (!$attachments) {
return $body;
}
// TODO: (T603) What's the policy here?
$files = id(new PhabricatorFile())
->loadAllWhere('phid in (%Ls)', $attachments);
// if we have some text then double return before adding our file list
if ($body) {
$body .= "\n\n";
}
foreach ($files as $file) {
$file_str = sprintf($format, $file->getID());
$body .= $file_str."\n";
}
return rtrim($body);
}
}
diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
index 0b7b7c904..1648a5e14 100644
--- a/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php
@@ -1,249 +1,254 @@
<?php
final class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
protected $headers = array();
protected $bodies = array();
protected $attachments = array();
protected $status = '';
protected $relatedPHID;
protected $authorPHID;
protected $message;
protected $messageIDHash = '';
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'headers' => self::SERIALIZATION_JSON,
'bodies' => self::SERIALIZATION_JSON,
'attachments' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
public function setHeaders(array $headers) {
// Normalize headers to lowercase.
$normalized = array();
foreach ($headers as $name => $value) {
$name = $this->normalizeMailHeaderName($name);
if ($name == 'message-id') {
$this->setMessageIDHash(PhabricatorHash::digestForIndex($value));
}
$normalized[$name] = $value;
}
$this->headers = $normalized;
return $this;
}
public function getHeader($key, $default = null) {
$key = $this->normalizeMailHeaderName($key);
return idx($this->headers, $key, $default);
}
private function normalizeMailHeaderName($name) {
return strtolower($name);
}
public function getMessageID() {
return $this->getHeader('Message-ID');
}
public function getSubject() {
return $this->getHeader('Subject');
}
public function getCCAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'cc'));
}
public function getToAddresses() {
return $this->getRawEmailAddresses(idx($this->headers, 'to'));
}
public function loadExcludeMailRecipientPHIDs() {
$addresses = array_merge(
$this->getToAddresses(),
$this->getCCAddresses());
return $this->loadPHIDsFromAddresses($addresses);
}
final public function loadCCPHIDs() {
return $this->loadPHIDsFromAddresses($this->getCCAddresses());
}
private function loadPHIDsFromAddresses(array $addresses) {
if (empty($addresses)) {
return array();
}
$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);
}
public function processReceivedMail() {
try {
$this->dropMailFromPhabricator();
$this->dropMailAlreadyReceived();
$receiver = $this->loadReceiver();
$sender = $receiver->loadSender($this);
$receiver->validateSender($this, $sender);
$this->setAuthorPHID($sender->getPHID());
$receiver->receiveMail($this, $sender);
} catch (PhabricatorMetaMTAReceivedMailProcessingException $ex) {
$this
->setStatus($ex->getStatusCode())
->setMessage($ex->getMessage())
->save();
return $this;
} catch (Exception $ex) {
$this
->setStatus(MetaMTAReceivedMailStatus::STATUS_UNHANDLED_EXCEPTION)
->setMessage(pht('Unhandled Exception: %s', $ex->getMessage()))
->save();
throw $ex;
}
return $this->setMessage('OK')->save();
}
public function getCleanTextBody() {
- $body = idx($this->bodies, 'text');
-
+ $body = $this->getRawTextBody();
$parser = new PhabricatorMetaMTAEmailBodyParser();
return $parser->stripTextBody($body);
}
+ public function parseBody() {
+ $body = $this->getRawTextBody();
+ $parser = new PhabricatorMetaMTAEmailBodyParser();
+ return $parser->parseBody($body);
+ }
+
public function getRawTextBody() {
return idx($this->bodies, 'text');
}
/**
* 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;
}
private function getRawEmailAddresses($addresses) {
$raw_addresses = array();
foreach (explode(',', $addresses) as $address) {
$raw_addresses[] = $this->getRawEmailAddress($address);
}
return array_filter($raw_addresses);
}
/**
* 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);
}
/**
* 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);
}
}
diff --git a/src/applications/paste/mail/PasteReplyHandler.php b/src/applications/paste/mail/PasteReplyHandler.php
index 93f3f7952..4c585b1e5 100644
--- a/src/applications/paste/mail/PasteReplyHandler.php
+++ b/src/applications/paste/mail/PasteReplyHandler.php
@@ -1,92 +1,84 @@
<?php
/**
* @group paste
*/
final class PasteReplyHandler extends PhabricatorMailReplyHandler {
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof PhabricatorPaste)) {
throw new Exception('Mail receiver is not a PhabricatorPaste.');
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'P');
}
public function getPublicReplyHandlerEmailAddress() {
return $this->getDefaultPublicReplyHandlerEmailAddress('P');
}
public function getReplyHandlerInstructions() {
if ($this->supportsReplies()) {
return pht('Reply to comment or !unsubscribe.');
} else {
return null;
}
}
protected function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$actor = $this->getActor();
$paste = $this->getMailReceiver();
- $body = $mail->getCleanTextBody();
- $body = trim($body);
+ $body_data = $mail->parseBody();
+ $body = $body_data['body'];
$body = $this->enhanceBodyWithAttachments($body, $mail->getAttachments());
$content_source = PhabricatorContentSource::newForSource(
PhabricatorContentSource::SOURCE_EMAIL,
array(
'id' => $mail->getID(),
));
$lines = explode("\n", trim($body));
$first_line = head($lines);
$xactions = array();
- $command = null;
- $matches = null;
- if (preg_match('/^!(\w+)/', $first_line, $matches)) {
- $lines = array_slice($lines, 1);
- $body = implode("\n", $lines);
- $body = trim($body);
-
- $command = $matches[1];
- }
+ $command = $body_data['command'];
switch ($command) {
case 'unsubscribe':
$xaction = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS)
->setNewValue(array('-' => array($actor->getPHID())));
$xactions[] = $xaction;
break;
}
$xactions[] = id(new PhabricatorPasteTransaction())
->setTransactionType(PhabricatorTransactions::TYPE_COMMENT)
->attachComment(
id(new PhabricatorPasteTransactionComment())
->setContent($body));
$editor = id(new PhabricatorPasteEditor())
->setActor($actor)
->setContentSource($content_source)
->setContinueOnNoEffect(true)
->setIsPreview(false);
try {
$xactions = $editor->applyTransactions($paste, $xactions);
} catch (PhabricatorApplicationTransactionNoEffectException $ex) {
// just do nothing, though unclear why you're sending a blank email
return true;
}
$head_xaction = head($xactions);
return $head_xaction->getID();
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Thu, Jul 17, 1:16 AM (1 d, 47 m)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
42/1e/a67d3404226247e7a8cfe279fa31
Attached To
rPHAB Phabricator
Event Timeline
Log In to Comment