Page MenuHomestyx hydra

No OneTemporary

diff --git a/src/applications/differential/editor/comment/DifferentialCommentEditor.php b/src/applications/differential/editor/comment/DifferentialCommentEditor.php
index ab84984024..a12adcd878 100644
--- a/src/applications/differential/editor/comment/DifferentialCommentEditor.php
+++ b/src/applications/differential/editor/comment/DifferentialCommentEditor.php
@@ -1,341 +1,349 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialCommentEditor {
protected $revision;
protected $actorPHID;
protected $action;
protected $attachInlineComments;
protected $message;
protected $addCC;
protected $changedByCommit;
protected $addedReviewers = array();
+ private $parentMessageID;
+
public function __construct(
DifferentialRevision $revision,
$actor_phid,
$action) {
$this->revision = $revision;
$this->actorPHID = $actor_phid;
$this->action = $action;
}
+ public function setParentMessageID($parent_message_id) {
+ $this->parentMessageID = $parent_message_id;
+ return $this;
+ }
+
public function setMessage($message) {
$this->message = $message;
return $this;
}
public function setAttachInlineComments($attach) {
$this->attachInlineComments = $attach;
return $this;
}
public function setAddCC($add) {
$this->addCC = $add;
return $this;
}
public function setChangedByCommit($changed_by_commit) {
$this->changedByCommit = $changed_by_commit;
return $this;
}
public function getChangedByCommit() {
return $this->changedByCommit;
}
public function setAddedReviewers($added_reviewers) {
$this->addedReviewers = $added_reviewers;
return $this;
}
public function getAddedReviewers() {
return $this->addedReviewers;
}
public function save() {
$revision = $this->revision;
$action = $this->action;
$actor_phid = $this->actorPHID;
$actor_is_author = ($actor_phid == $revision->getAuthorPHID());
$revision_status = $revision->getStatus();
$revision->loadRelationships();
$reviewer_phids = $revision->getReviewers();
if ($reviewer_phids) {
$reviewer_phids = array_combine($reviewer_phids, $reviewer_phids);
}
$metadata = array();
switch ($action) {
case DifferentialAction::ACTION_COMMENT:
break;
case DifferentialAction::ACTION_RESIGN:
if ($actor_is_author) {
throw new Exception('You can not resign from your own revision!');
}
if (isset($reviewer_phids[$actor_phid])) {
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$rem = array($actor_phid),
$add = array(),
$actor_phid);
}
break;
case DifferentialAction::ACTION_ABANDON:
if (!$actor_is_author) {
throw new Exception('You can only abandon your revisions.');
}
if ($revision_status == DifferentialRevisionStatus::COMMITTED) {
throw new Exception('You can not abandon a committed revision.');
}
if ($revision_status == DifferentialRevisionStatus::ABANDONED) {
$action = DifferentialAction::ACTION_COMMENT;
break;
}
$revision
->setStatus(DifferentialRevisionStatus::ABANDONED)
->save();
break;
case DifferentialAction::ACTION_ACCEPT:
if ($actor_is_author) {
throw new Exception('You can not accept your own revision.');
}
if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
($revision_status != DifferentialRevisionStatus::NEEDS_REVISION)) {
$action = DifferentialAction::ACTION_COMMENT;
break;
}
$revision
->setStatus(DifferentialRevisionStatus::ACCEPTED)
->save();
if (!isset($reviewer_phids[$actor_phid])) {
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$rem = array(),
$add = array($actor_phid),
$actor_phid);
}
break;
case DifferentialAction::ACTION_REQUEST:
if (!$actor_is_author) {
throw new Exception('You must own a revision to request review.');
}
if (($revision_status != DifferentialRevisionStatus::NEEDS_REVISION) &&
($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
$action = DifferentialAction::ACTION_COMMENT;
break;
}
$revision
->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)
->save();
break;
case DifferentialAction::ACTION_REJECT:
if ($actor_is_author) {
throw new Exception(
'You can not request changes to your own revision.');
}
if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
$action = DifferentialAction::ACTION_COMMENT;
break;
}
if (!isset($reviewer_phids[$actor_phid])) {
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$rem = array(),
$add = array($actor_phid),
$actor_phid);
}
$revision
->setStatus(DifferentialRevisionStatus::NEEDS_REVISION)
->save();
break;
case DifferentialAction::ACTION_RETHINK:
if (!$actor_is_author) {
throw new Exception(
"You can not plan changes to somebody else's revision");
}
if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
$action = DifferentialAction::ACTION_COMMENT;
break;
}
$revision
->setStatus(DifferentialRevisionStatus::NEEDS_REVISION)
->save();
break;
case DifferentialAction::ACTION_RECLAIM:
if (!$actor_is_author) {
throw new Exception('You can not reclaim a revision you do not own.');
}
if ($revision_status != DifferentialRevisionStatus::ABANDONED) {
$action = DifferentialAction::ACTION_COMMENT;
break;
}
$revision
->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)
->save();
break;
case DifferentialAction::ACTION_COMMIT:
if (!$actor_is_author) {
throw new Exception('You can not commit a revision you do not own.');
}
$revision
->setStatus(DifferentialRevisionStatus::COMMITTED)
->save();
break;
case DifferentialAction::ACTION_ADDREVIEWERS:
$added_reviewers = $this->getAddedReviewers();
foreach ($added_reviewers as $k => $user_phid) {
if ($user_phid == $revision->getAuthorPHID()) {
unset($added_reviewers[$k]);
}
if (!empty($reviewer_phids[$user_phid])) {
unset($added_reviewers[$k]);
}
}
$added_reviewers = array_unique($added_reviewers);
if ($added_reviewers) {
DifferentialRevisionEditor::alterReviewers(
$revision,
$reviewer_phids,
$rem = array(),
$add = $added_reviewers,
$actor_phid);
$key = DifferentialComment::METADATA_ADDED_REVIEWERS;
$metadata[$key] = $added_reviewers;
} else {
$action = DifferentialAction::ACTION_COMMENT;
}
break;
default:
throw new Exception('Unsupported action.');
}
if ($this->addCC) {
DifferentialRevisionEditor::addCC(
$revision,
$this->actorPHID,
$this->actorPHID);
}
// Reload relationships to pick up any reviewer/CC changes.
$revision->loadRelationships();
$inline_comments = array();
if ($this->attachInlineComments) {
$inline_comments = id(new DifferentialInlineComment())->loadAllWhere(
'authorPHID = %s AND revisionID = %d AND commentID IS NULL',
$this->actorPHID,
$revision->getID());
}
$comment = id(new DifferentialComment())
->setAuthorPHID($this->actorPHID)
->setRevisionID($revision->getID())
->setAction($action)
->setContent((string)$this->message)
->setMetadata($metadata)
->save();
$changesets = array();
if ($inline_comments) {
$load_ids = mpull($inline_comments, 'getChangesetID');
if ($load_ids) {
$load_ids = array_unique($load_ids);
$changesets = id(new DifferentialChangeset())->loadAllWhere(
'id in (%Ld)',
$load_ids);
}
foreach ($inline_comments as $inline) {
$inline->setCommentID($comment->getID());
$inline->save();
}
}
$phids = array($this->actorPHID);
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$actor_handle = $handles[$this->actorPHID];
$xherald_header = HeraldTranscript::loadXHeraldRulesHeader(
$revision->getPHID());
id(new DifferentialCommentMail(
$revision,
$actor_handle,
$comment,
$changesets,
$inline_comments))
->setToPHIDs(
array_merge(
$revision->getReviewers(),
array($revision->getAuthorPHID())))
->setCCPHIDs($revision->getCCPHIDs())
->setChangedByCommit($this->getChangedByCommit())
->setXHeraldRulesHeader($xherald_header)
+ ->setParentMessageID($this->parentMessageID)
->send();
$event_data = array(
'revision_id' => $revision->getID(),
'revision_phid' => $revision->getPHID(),
'revision_name' => $revision->getTitle(),
'revision_author_phid' => $revision->getAuthorPHID(),
'action' => $comment->getAction(),
'feedback_content' => $comment->getContent(),
'actor_phid' => $this->actorPHID,
);
id(new PhabricatorTimelineEvent('difx', $event_data))
->recordEvent();
// TODO: Move to a daemon?
PhabricatorSearchDifferentialIndexer::indexRevision($revision);
return $comment;
}
}
diff --git a/src/applications/differential/mail/base/DifferentialMail.php b/src/applications/differential/mail/base/DifferentialMail.php
index b497266f42..8ea77d456e 100644
--- a/src/applications/differential/mail/base/DifferentialMail.php
+++ b/src/applications/differential/mail/base/DifferentialMail.php
@@ -1,315 +1,322 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
abstract class DifferentialMail {
protected $to = array();
protected $cc = array();
protected $actorHandle;
protected $revision;
protected $comment;
protected $changesets;
protected $inlineComments;
protected $isFirstMailAboutRevision;
protected $isFirstMailToRecipients;
protected $heraldTranscriptURI;
protected $heraldRulesHeader;
protected $replyHandler;
+ protected $parentMessageID;
abstract protected function renderSubject();
abstract protected function renderBody();
public function setActorHandle($actor_handle) {
$this->actorHandle = $actor_handle;
return $this;
}
public function getActorHandle() {
return $this->actorHandle;
}
protected function getActorName() {
$handle = $this->getActorHandle();
if ($handle) {
return $handle->getName();
}
return '???';
}
+ public function setParentMessageID($parent_message_id) {
+ $this->parentMessageID = $parent_message_id;
+ return $this;
+ }
+
public function setXHeraldRulesHeader($header) {
$this->heraldRulesHeader = $header;
return $this;
}
public function send() {
$to_phids = $this->getToPHIDs();
if (!$to_phids) {
throw new Exception('No "To:" users provided!');
}
$cc_phids = $this->getCCPHIDs();
$subject = $this->buildSubject();
$body = $this->buildBody();
$template = new PhabricatorMetaMTAMail();
$actor_handle = $this->getActorHandle();
$reply_handler = $this->getReplyHandler();
if ($actor_handle) {
$template->setFrom($actor_handle->getPHID());
}
$template
->setSubject($subject)
->setBody($body)
->setIsHTML($this->shouldMarkMailAsHTML())
+ ->setParentMessageID($this->parentMessageID)
->addHeader('Thread-Topic', $this->getRevision()->getTitle());
$template->setThreadID(
$this->getThreadID(),
$this->isFirstMailAboutRevision());
if ($this->heraldRulesHeader) {
$template->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
}
$template->setRelatedPHID($this->getRevision()->getPHID());
$phids = array();
foreach ($to_phids as $phid) {
$phids[$phid] = true;
}
foreach ($cc_phids as $phid) {
$phids[$phid] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))->loadHandles();
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $to_phids),
array_select_keys($handles, $cc_phids));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
protected function getSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix');
}
protected function buildSubject() {
return trim($this->getSubjectPrefix().' '.$this->renderSubject());
}
protected function shouldMarkMailAsHTML() {
return false;
}
protected function buildBody() {
$body = $this->renderBody();
$reply_handler = $this->getReplyHandler();
$reply_instructions = $reply_handler->getReplyHandlerInstructions();
if ($reply_instructions) {
$body .=
"\nREPLY HANDLER ACTIONS\n".
" {$reply_instructions}\n";
}
if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) {
$manage_uri = PhabricatorEnv::getProductionURI(
'/herald/view/differential/');
$xscript_uri = $this->getHeraldTranscriptURI();
$body .= <<<EOTEXT
MANAGE HERALD DIFFERENTIAL RULES
{$manage_uri}
WHY DID I GET THIS EMAIL?
{$xscript_uri}
Tip: use the X-Herald-Rules header to filter Herald messages in your client.
EOTEXT;
}
return $body;
}
public function getReplyHandler() {
if ($this->replyHandler) {
return $this->replyHandler;
}
$handler_class = PhabricatorEnv::getEnvConfig(
'metamta.differential.reply-handler');
$reply_handler = self::newReplyHandlerForRevision($this->getRevision());
$this->replyHandler = $reply_handler;
return $this->replyHandler;
}
public static function newReplyHandlerForRevision(
DifferentialRevision $revision) {
$handler_class = PhabricatorEnv::getEnvConfig(
'metamta.differential.reply-handler');
$reply_handler = newv($handler_class, array());
$reply_handler->setMailReceiver($revision);
return $reply_handler;
}
protected function formatText($text) {
$text = explode("\n", $text);
foreach ($text as &$line) {
$line = rtrim(' '.$line);
}
unset($line);
return implode("\n", $text);
}
public function setToPHIDs(array $to) {
$this->to = $this->filterContactPHIDs($to);
return $this;
}
public function setCCPHIDs(array $cc) {
$this->cc = $this->filterContactPHIDs($cc);
return $this;
}
protected function filterContactPHIDs(array $phids) {
return $phids;
// TODO: actually do this?
// Differential revisions use Subscriptions for CCs, so any arbitrary
// PHID can end up CC'd to them. Only try to actually send email PHIDs
// which have ToolsHandle types that are marked emailable. If we don't
// filter here, sending the email will fail.
/*
$handles = array();
prep(new ToolsHandleData($phids, $handles));
foreach ($handles as $phid => $handle) {
if (!$handle->isEmailable()) {
unset($handles[$phid]);
}
}
return array_keys($handles);
*/
}
protected function getToPHIDs() {
return $this->to;
}
protected function getCCPHIDs() {
return $this->cc;
}
public function setRevision($revision) {
$this->revision = $revision;
return $this;
}
public function getRevision() {
return $this->revision;
}
protected function getThreadID() {
$phid = $this->getRevision()->getPHID();
$domain = PhabricatorEnv::getEnvConfig('metamta.domain');
return "<differential-rev-{$phid}-req@{$domain}>";
}
public function setComment($comment) {
$this->comment = $comment;
return $this;
}
public function getComment() {
return $this->comment;
}
public function setChangesets($changesets) {
$this->changesets = $changesets;
return $this;
}
public function getChangesets() {
return $this->changesets;
}
public function setInlineComments(array $inline_comments) {
$this->inlineComments = $inline_comments;
return $this;
}
public function getInlineComments() {
return $this->inlineComments;
}
public function renderRevisionDetailLink() {
$uri = $this->getRevisionURI();
return "REVISION DETAIL\n {$uri}";
}
public function getRevisionURI() {
return PhabricatorEnv::getProductionURI('/D'.$this->getRevision()->getID());
}
public function setIsFirstMailToRecipients($first) {
$this->isFirstMailToRecipients = $first;
return $this;
}
public function isFirstMailToRecipients() {
return $this->isFirstMailToRecipients;
}
public function setIsFirstMailAboutRevision($first) {
$this->isFirstMailAboutRevision = $first;
return $this;
}
public function isFirstMailAboutRevision() {
return $this->isFirstMailAboutRevision;
}
public function setHeraldTranscriptURI($herald_transcript_uri) {
$this->heraldTranscriptURI = $herald_transcript_uri;
return $this;
}
public function getHeraldTranscriptURI() {
return $this->heraldTranscriptURI;
}
}
diff --git a/src/applications/differential/replyhandler/DifferentialReplyHandler.php b/src/applications/differential/replyhandler/DifferentialReplyHandler.php
index df0e11077a..ae7be54e1b 100644
--- a/src/applications/differential/replyhandler/DifferentialReplyHandler.php
+++ b/src/applications/differential/replyhandler/DifferentialReplyHandler.php
@@ -1,162 +1,171 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class DifferentialReplyHandler extends PhabricatorMailReplyHandler {
+ private $receivedMail;
+
public function validateMailReceiver($mail_receiver) {
if (!($mail_receiver instanceof DifferentialRevision)) {
throw new Exception("Receiver is not a DifferentialRevision!");
}
}
public function getPrivateReplyHandlerEmailAddress(
PhabricatorObjectHandle $handle) {
return $this->getDefaultPrivateReplyHandlerEmailAddress($handle, 'D');
}
public function getReplyHandlerDomain() {
return PhabricatorEnv::getEnvConfig(
'metamta.differential.reply-handler-domain');
}
/*
* Generate text like the following from the supported commands.
* "
*
* ACTIONS
* Reply to comment, or !accept, !reject, !abandon, !resign, !reclaim.
*
* "
*/
public function getReplyHandlerInstructions() {
if (!$this->supportsReplies()) {
return null;
}
$supported_commands = $this->getSupportedCommands();
$text = '';
if (empty($supported_commands)) {
return $text;
}
$comment_command_printed = false;
if (in_array(DifferentialAction::ACTION_COMMENT, $supported_commands)) {
$text .= 'Reply to comment';
$comment_command_printed = true;
$supported_commands = array_diff(
$supported_commands, array(DifferentialAction::ACTION_COMMENT));
}
if (!empty($supported_commands)) {
if ($comment_command_printed) {
$text .= ', or ';
}
$modified_commands = array();
foreach ($supported_commands as $command) {
$modified_commands[] = '!'.$command;
}
$text .= implode(', ', $modified_commands);
}
$text .= ".";
return $text;
}
public function getSupportedCommands() {
return array(
DifferentialAction::ACTION_COMMENT,
DifferentialAction::ACTION_REJECT,
DifferentialAction::ACTION_ABANDON,
DifferentialAction::ACTION_RECLAIM,
DifferentialAction::ACTION_RESIGN,
DifferentialAction::ACTION_RETHINK,
'unsubscribe',
);
}
public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
+ $this->receivedMail = $mail;
$this->handleAction($mail->getCleanTextBody());
}
public function handleAction($body) {
// all commands start with a bang and separated from the body by a newline
// to make sure that actual feedback text couldn't trigger an action.
// unrecognized commands will be parsed as part of the comment.
$command = DifferentialAction::ACTION_COMMENT;
$supported_commands = $this->getSupportedCommands();
$regex = "/\A\n*!(" . implode('|', $supported_commands) . ")\n*/";
$matches = array();
if (preg_match($regex, $body, $matches)) {
$command = $matches[1];
$body = trim(str_replace('!' . $command, '', $body));
}
$actor = $this->getActor();
if (!$actor) {
throw new Exception('No actor is set for the reply action.');
}
switch ($command) {
case 'unsubscribe':
$this->unsubscribeUser($this->getMailReceiver(), $actor);
// TODO: Send the user a confirmation email?
return null;
}
try {
$editor = new DifferentialCommentEditor(
$this->getMailReceiver(),
$actor->getPHID(),
$command);
+ // NOTE: We have to be careful about this because Facebook's
+ // implementation jumps straight into handleAction() and will not have
+ // a PhabricatorMetaMTAReceivedMail object.
+ if ($this->receivedMail) {
+ $editor->setParentMessageID($this->receivedMail->getMessageID());
+ }
$editor->setMessage($body);
$editor->setAddCC(($command != DifferentialAction::ACTION_RESIGN));
$comment = $editor->save();
return $comment->getID();
} catch (Exception $ex) {
$exception_mail = new DifferentialExceptionMail(
$this->getMailReceiver(),
$ex,
$body);
$exception_mail->setToPHIDs(array($this->getActor()->getPHID()));
$exception_mail->send();
throw $ex;
}
}
private function unsubscribeUser(
DifferentialRevision $revision,
PhabricatorUser $user) {
$revision->loadRelationships();
DifferentialRevisionEditor::removeCCAndUpdateRevision(
$revision,
$user->getPHID(),
$user->getPHID());
}
}
diff --git a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
index 75067cd628..a2828a8b98 100644
--- a/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
+++ b/src/applications/maniphest/editor/transaction/ManiphestTransactionEditor.php
@@ -1,229 +1,237 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class ManiphestTransactionEditor {
+ private $parentMessageID;
+
+ public function setParentMessageID($parent_message_id) {
+ $this->parentMessageID = $parent_message_id;
+ return $this;
+ }
+
public function applyTransactions($task, array $transactions) {
$email_cc = $task->getCCPHIDs();
$email_to = array();
$email_to[] = $task->getOwnerPHID();
foreach ($transactions as $key => $transaction) {
$type = $transaction->getTransactionType();
$new = $transaction->getNewValue();
$email_to[] = $transaction->getAuthorPHID();
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
$old = null;
break;
case ManiphestTransactionType::TYPE_STATUS:
$old = $task->getStatus();
break;
case ManiphestTransactionType::TYPE_OWNER:
$old = $task->getOwnerPHID();
break;
case ManiphestTransactionType::TYPE_CCS:
$old = $task->getCCPHIDs();
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$old = $task->getPriority();
break;
case ManiphestTransactionType::TYPE_ATTACH:
$old = $task->getAttached();
break;
case ManiphestTransactionType::TYPE_TITLE:
$old = $task->getTitle();
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$old = $task->getDescription();
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$old = $task->getProjectPHIDs();
break;
default:
throw new Exception('Unknown action type.');
}
if (($old !== null) && ($old == $new)) {
if (count($transactions) > 1 && !$transaction->hasComments()) {
// If we have at least one other transaction and this one isn't
// doing anything and doesn't have any comments, just throw it
// away.
unset($transactions[$key]);
continue;
} else {
$transaction->setOldValue(null);
$transaction->setNewValue(null);
$transaction->setTransactionType(ManiphestTransactionType::TYPE_NONE);
}
} else {
switch ($type) {
case ManiphestTransactionType::TYPE_NONE:
break;
case ManiphestTransactionType::TYPE_STATUS:
$task->setStatus($new);
break;
case ManiphestTransactionType::TYPE_OWNER:
$task->setOwnerPHID($new);
break;
case ManiphestTransactionType::TYPE_CCS:
$task->setCCPHIDs($new);
break;
case ManiphestTransactionType::TYPE_PRIORITY:
$task->setPriority($new);
break;
case ManiphestTransactionType::TYPE_ATTACH:
$task->setAttached($new);
break;
case ManiphestTransactionType::TYPE_TITLE:
$task->setTitle($new);
break;
case ManiphestTransactionType::TYPE_DESCRIPTION:
$task->setDescription($new);
break;
case ManiphestTransactionType::TYPE_PROJECTS:
$task->setProjectPHIDs($new);
break;
default:
throw new Exception('Unknown action type.');
}
$transaction->setOldValue($old);
$transaction->setNewValue($new);
}
}
$task->save();
foreach ($transactions as $transaction) {
$transaction->setTaskID($task->getID());
$transaction->save();
}
$email_to[] = $task->getOwnerPHID();
$email_cc = array_merge(
$email_cc,
$task->getCCPHIDs());
// TODO: Do this offline via timeline
PhabricatorSearchManiphestIndexer::indexTask($task);
$this->sendEmail($task, $transactions, $email_to, $email_cc);
}
protected function getSubjectPrefix() {
return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix');
}
private function sendEmail($task, $transactions, $email_to, $email_cc) {
$email_to = array_filter(array_unique($email_to));
$email_cc = array_filter(array_unique($email_cc));
$phids = array();
foreach ($transactions as $transaction) {
foreach ($transaction->extractPHIDs() as $phid) {
$phids[$phid] = true;
}
}
foreach ($email_to as $phid) {
$phids[$phid] = true;
}
foreach ($email_cc as $phid) {
$phids[$phid] = true;
}
$phids = array_keys($phids);
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$view = new ManiphestTransactionDetailView();
$view->setTransactionGroup($transactions);
$view->setHandles($handles);
list($action, $body) = $view->renderForEmail($with_date = false);
$is_create = false;
foreach ($transactions as $transaction) {
$type = $transaction->getTransactionType();
if (($type == ManiphestTransactionType::TYPE_STATUS) &&
($transaction->getOldValue() === null) &&
($transaction->getNewValue() == ManiphestTaskStatus::STATUS_OPEN)) {
$is_create = true;
}
}
$task_uri = PhabricatorEnv::getURI('/T'.$task->getID());
$reply_handler = $this->buildReplyHandler($task);
if ($is_create) {
$body .=
"\n\n".
"TASK DESCRIPTION\n".
" ".$task->getDescription();
}
$body .=
"\n\n".
"TASK DETAIL\n".
" ".$task_uri."\n";
$reply_instructions = $reply_handler->getReplyHandlerInstructions();
if ($reply_instructions) {
$body .=
"\n".
"REPLY HANDLER ACTIONS\n".
" ".$reply_instructions."\n";
}
$thread_id = '<maniphest-task-'.$task->getPHID().'>';
$task_id = $task->getID();
$title = $task->getTitle();
$prefix = $this->getSubjectPrefix();
$subject = trim("{$prefix} [{$action}] T{$task_id}: {$title}");
$template = id(new PhabricatorMetaMTAMail())
->setSubject($subject)
->setFrom($transaction->getAuthorPHID())
+ ->setParentMessageID($this->parentMessageID)
->addHeader('Thread-Topic', 'Maniphest Task '.$task->getID())
->setThreadID($thread_id, $is_create)
->setRelatedPHID($task->getPHID())
->setBody($body);
$mails = $reply_handler->multiplexMail(
$template,
array_select_keys($handles, $email_to),
array_select_keys($handles, $email_cc));
foreach ($mails as $mail) {
$mail->saveAndSend();
}
}
public function buildReplyHandler(ManiphestTask $task) {
$handler_class = PhabricatorEnv::getEnvConfig(
'metamta.maniphest.reply-handler');
$handler_object = newv($handler_class, array());
$handler_object->setMailReceiver($task);
return $handler_object;
}
}
diff --git a/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php b/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
index 004b971c5a..b86f5685f7 100644
--- a/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
+++ b/src/applications/maniphest/replyhandler/ManiphestReplyHandler.php
@@ -1,121 +1,121 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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 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;
}
}
public function receiveEmail(PhabricatorMetaMTAReceivedMail $mail) {
$task = $this->getMailReceiver();
$user = $this->getActor();
$body = $mail->getCleanTextBody();
$body = trim($body);
$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];
}
$xactions = array();
$files = $mail->getAttachments();
if ($files) {
$file_xaction = new ManiphestTransaction();
$file_xaction->setAuthorPHID($user->getPHID());
$file_xaction->setTransactionType(ManiphestTransactionType::TYPE_ATTACH);
$phid_type = PhabricatorPHIDConstants::PHID_TYPE_FILE;
$new = $task->getAttached();
foreach ($files as $file_phid) {
$new[$phid_type][$file_phid] = array();
}
$file_xaction->setNewValue($new);
$xactions[] = $file_xaction;
}
$ttype = ManiphestTransactionType::TYPE_NONE;
$new_value = null;
switch ($command) {
case 'close':
$ttype = ManiphestTransactionType::TYPE_STATUS;
$new_value = ManiphestTaskStatus::STATUS_CLOSED_RESOLVED;
break;
case 'claim':
$ttype = ManiphestTransactionType::TYPE_OWNER;
$new_value = $user->getPHID();
break;
case 'unsubscribe':
$ttype = ManiphestTransactionType::TYPE_CCS;
$ccs = $task->getCCPHIDs();
foreach ($ccs as $k => $phid) {
if ($phid == $user->getPHID()) {
unset($ccs[$k]);
}
}
$new_value = array_values($ccs);
break;
}
$xaction = new ManiphestTransaction();
$xaction->setAuthorPHID($user->getPHID());
$xaction->setTransactionType($ttype);
$xaction->setNewValue($new_value);
$xaction->setComments($body);
$xactions[] = $xaction;
$editor = new ManiphestTransactionEditor();
+ $editor->setParentMessageID($mail->getMessageID());
$editor->applyTransactions($task, $xactions);
-
}
}
diff --git a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
index eaca3beb30..7ea4ecec31 100644
--- a/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
+++ b/src/applications/metamta/storage/mail/PhabricatorMetaMTAMail.php
@@ -1,369 +1,395 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* See #394445 for an explanation of why this thing even exists.
*/
class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO {
const STATUS_QUEUE = 'queued';
const STATUS_SENT = 'sent';
const STATUS_FAIL = 'fail';
const MAX_RETRIES = 250;
const RETRY_DELAY = 5;
protected $parameters;
protected $status;
protected $message;
protected $retryCount;
protected $nextRetry;
protected $relatedPHID;
public function __construct() {
$this->status = self::STATUS_QUEUE;
$this->retryCount = 0;
$this->nextRetry = time();
$this->parameters = array();
parent::__construct();
}
public function getConfiguration() {
return array(
self::CONFIG_SERIALIZATION => array(
'parameters' => self::SERIALIZATION_JSON,
),
) + parent::getConfiguration();
}
protected function setParam($param, $value) {
$this->parameters[$param] = $value;
return $this;
}
protected function getParam($param) {
return idx($this->parameters, $param);
}
+ /**
+ * In Gmail, conversations will be broken if you reply to a thread and the
+ * server sends back a response without referencing your Message-ID, even if
+ * it references a Message-ID earlier in the thread. To avoid this, use the
+ * parent email's message ID explicitly if it's available. This overwrites the
+ * "In-Reply-To" and "References" headers we would otherwise generate. This
+ * needs to be set whenever an action is triggered by an email message. See
+ * T251 for more details.
+ *
+ * @param string The "Message-ID" of the email which precedes this one.
+ * @return this
+ */
+ public function setParentMessageID($id) {
+ $this->setParam('parent-message-id', $id);
+ return $this;
+ }
+
+ public function getParentMessageID() {
+ return $this->getParam('parent-message-id');
+ }
+
public function getSubject() {
return $this->getParam('subject');
}
public function addTos(array $phids) {
$this->setParam('to', $phids);
return $this;
}
public function addCCs(array $phids) {
$this->setParam('cc', $phids);
return $this;
}
public function addHeader($name, $value) {
$this->parameters['headers'][$name] = $value;
return $this;
}
public function setFrom($from) {
$this->setParam('from', $from);
return $this;
}
public function setReplyTo($reply_to) {
$this->setParam('reply-to', $reply_to);
return $this;
}
public function setSubject($subject) {
$this->setParam('subject', $subject);
return $this;
}
public function setBody($body) {
$this->setParam('body', $body);
return $this;
}
public function getBody() {
return $this->getParam('body');
}
public function setIsHTML($html) {
$this->setParam('is-html', $html);
return $this;
}
public function getSimulatedFailureCount() {
return nonempty($this->getParam('simulated-failures'), 0);
}
public function setSimulatedFailureCount($count) {
$this->setParam('simulated-failures', $count);
return $this;
}
/**
* Use this method to set an ID used for message threading. MetaMTA will
* set appropriate headers (Message-ID, In-Reply-To, References and
* Thread-Index) based on the capabilities of the underlying mailer.
*
* @param string Unique identifier, appropriate for use in a Message-ID,
* In-Reply-To or References headers.
* @param bool If true, indicates this is the first message in the thread.
* @return this
*/
public function setThreadID($thread_id, $is_first_message = false) {
$this->setParam('thread-id', $thread_id);
$this->setParam('is-first-message', $is_first_message);
return $this;
}
/**
* Save a newly created mail to the database and attempt to send it
* immediately if the server is configured for immediate sends. When
* applications generate new mail they should generally use this method to
* deliver it. If the server doesn't use immediate sends, this has the same
* effect as calling save(): the mail will eventually be delivered by the
* MetaMTA daemon.
*
* @return this
*/
public function saveAndSend() {
$ret = $this->save();
if (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) {
$this->sendNow();
}
return $ret;
}
public function buildDefaultMailer() {
$class_name = PhabricatorEnv::getEnvConfig('metamta.mail-adapter');
PhutilSymbolLoader::loadClass($class_name);
return newv($class_name, array());
}
/**
* Attempt to deliver an email immediately, in this process.
*
* @param bool Try to deliver this email even if it has already been
* delivered or is in backoff after a failed delivery attempt.
* @param PhabricatorMailImplementationAdapter Use a specific mail adapter,
* instead of the default.
*
* @return void
*/
public function sendNow(
$force_send = false,
PhabricatorMailImplementationAdapter $mailer = null) {
if ($mailer === null) {
$mailer = $this->buildDefaultMailer();
}
if (!$force_send) {
if ($this->getStatus() != self::STATUS_QUEUE) {
throw new Exception("Trying to send an already-sent mail!");
}
if (time() < $this->getNextRetry()) {
throw new Exception("Trying to send an email before next retry!");
}
}
try {
$parameters = $this->parameters;
$phids = array();
foreach ($parameters as $key => $value) {
switch ($key) {
case 'from':
case 'to':
case 'cc':
if (!is_array($value)) {
$value = array($value);
}
foreach (array_filter($value) as $phid) {
$phids[] = $phid;
}
break;
}
}
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$params = $this->parameters;
$default = PhabricatorEnv::getEnvConfig('metamta.default-address');
if (empty($params['from'])) {
$mailer->setFrom($default);
} else if (!PhabricatorEnv::getEnvConfig('metamta.can-send-as-user')) {
$from = $params['from'];
$handle = $handles[$from];
if (empty($params['reply-to'])) {
$params['reply-to'] = $handle->getEmail();
$params['reply-to-name'] = $handle->getFullName();
}
$mailer->setFrom(
$default,
$handle->getFullName());
unset($params['from']);
}
$is_first = !empty($params['is-first-message']);
unset($params['is-first-message']);
$reply_to_name = idx($params, 'reply-to-name', '');
unset($params['reply-to-name']);
foreach ($params as $key => $value) {
switch ($key) {
case 'from':
$mailer->setFrom($handles[$value]->getEmail());
break;
case 'reply-to':
$mailer->addReplyTo($value, $reply_to_name);
break;
case 'to':
$emails = array();
foreach ($value as $phid) {
$emails[] = $handles[$phid]->getEmail();
}
$mailer->addTos($emails);
break;
case 'cc':
$emails = array();
foreach ($value as $phid) {
$emails[] = $handles[$phid]->getEmail();
}
$mailer->addCCs($emails);
break;
case 'headers':
foreach ($value as $header_key => $header_value) {
$mailer->addHeader($header_key, $header_value);
}
break;
case 'body':
$mailer->setBody($value);
break;
case 'subject':
$mailer->setSubject($value);
break;
case 'is-html':
if ($value) {
$mailer->setIsHTML(true);
}
break;
case 'thread-id':
if ($is_first && $mailer->supportsMessageIDHeader()) {
$mailer->addHeader('Message-ID', $value);
} else {
- $mailer->addHeader('In-Reply-To', $value);
- $mailer->addHeader('References', $value);
+ $in_reply_to = $value;
+ $parent_id = $this->getParentMessageID();
+ if ($parent_id) {
+ $in_reply_to = $parent_id;
+ }
+ $mailer->addHeader('In-Reply-To', $in_reply_to);
+ $mailer->addHeader('References', $in_reply_to);
}
$thread_index = $this->generateThreadIndex($value, $is_first);
$mailer->addHeader('Thread-Index', $thread_index);
break;
default:
// Just discard.
}
}
$mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA');
} catch (Exception $ex) {
$this->setStatus(self::STATUS_FAIL);
$this->setMessage($ex->getMessage());
$this->save();
return;
}
if ($this->getRetryCount() < $this->getSimulatedFailureCount()) {
$ok = false;
$error = 'Simulated failure.';
} else {
try {
$ok = $mailer->send();
$error = null;
} catch (Exception $ex) {
$ok = false;
$error = $ex->getMessage()."\n".$ex->getTraceAsString();
}
}
if (!$ok) {
$this->setMessage($error);
if ($this->getRetryCount() > self::MAX_RETRIES) {
$this->setStatus(self::STATUS_FAIL);
} else {
$this->setRetryCount($this->getRetryCount() + 1);
$next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY);
$this->setNextRetry($next_retry);
}
} else {
$this->setStatus(self::STATUS_SENT);
}
$this->save();
}
public static function getReadableStatus($status_code) {
static $readable = array(
self::STATUS_QUEUE => "Queued for Delivery",
self::STATUS_FAIL => "Delivery Failed",
self::STATUS_SENT => "Sent",
);
$status_code = coalesce($status_code, '?');
return idx($readable, $status_code, $status_code);
}
private function generateThreadIndex($seed, $is_first_mail) {
// When threading, Outlook ignores the 'References' and 'In-Reply-To'
// headers that most clients use. Instead, it uses a custom 'Thread-Index'
// header. The format of this header is something like this (from
// camel-exchange-folder.c in Evolution Exchange):
/* A new post to a folder gets a 27-byte-long thread index. (The value
* is apparently unique but meaningless.) Each reply to a post gets a
* 32-byte-long thread index whose first 27 bytes are the same as the
* parent's thread index. Each reply to any of those gets a
* 37-byte-long thread index, etc. The Thread-Index header contains a
* base64 representation of this value.
*/
// The specific implementation uses a 27-byte header for the first email
// a recipient receives, and a random 5-byte suffix (32 bytes total)
// thereafter. This means that all the replies are (incorrectly) siblings,
// but it would be very difficult to keep track of the entire tree and this
// gets us reasonable client behavior.
$base = substr(md5($seed), 0, 27);
if (!$is_first_mail) {
// Not totally sure, but it seems like outlook orders replies by
// thread-index rather than timestamp, so to get these to show up in the
// right order we use the time as the last 4 bytes.
$base .= ' '.pack('N', time());
}
return base64_encode($base);
}
}
diff --git a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
index cafaf174a6..cdd978feef 100644
--- a/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
+++ b/src/applications/metamta/storage/receivedmail/PhabricatorMetaMTAReceivedMail.php
@@ -1,147 +1,151 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
class PhabricatorMetaMTAReceivedMail extends PhabricatorMetaMTADAO {
protected $headers = array();
protected $bodies = array();
protected $attachments = array();
protected $relatedPHID;
protected $authorPHID;
protected $message;
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) {
$normalized[strtolower($name)] = $value;
}
$this->headers = $normalized;
return $this;
}
+ public function getMessageID() {
+ return idx($this->headers, 'message-id');
+ }
+
public function processReceivedMail() {
$to = idx($this->headers, 'to');
// Accept a match either at the beginning of the address or after an open
// angle bracket, as in:
// "some display name" <D1+xyz+asdf@example.com>
$matches = null;
$ok = preg_match(
'/(?:^|<)((?:D|T)\d+)\+(\d+)\+([a-f0-9]{16})@/U',
$to,
$matches);
if (!$ok) {
return $this->setMessage("Unrecognized 'to' format: {$to}")->save();
}
$receiver_name = $matches[1];
$user_id = $matches[2];
$hash = $matches[3];
$user = id(new PhabricatorUser())->load($user_id);
if (!$user) {
return $this->setMessage("Invalid user '{$user_id}'")->save();
}
if ($user->getIsDisabled()) {
return $this->setMessage("User '{$user_id}' is disabled")->save();
}
$this->setAuthorPHID($user->getPHID());
$receiver = self::loadReceiverObject($receiver_name);
if (!$receiver) {
return $this->setMessage("Invalid object '{$receiver_name}'")->save();
}
$this->setRelatedPHID($receiver->getPHID());
$expect_hash = self::computeMailHash(
$receiver->getMailKey(),
$user->getPHID());
if ($expect_hash != $hash) {
return $this->setMessage("Invalid mail hash!")->save();
}
if ($receiver instanceof ManiphestTask) {
$editor = new ManiphestTransactionEditor();
$handler = $editor->buildReplyHandler($receiver);
} else if ($receiver instanceof DifferentialRevision) {
$handler = DifferentialMail::newReplyHandlerForRevision($receiver);
}
$handler->setActor($user);
$handler->receiveEmail($this);
$this->setMessage('OK');
return $this->save();
}
public function getCleanTextBody() {
$body = idx($this->bodies, 'text');
$parser = new PhabricatorMetaMTAEmailBodyParser($body);
return $parser->stripQuotedText();
}
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':
$class_obj = newv('ManiphestTask', array());
break;
case 'D':
$class_obj = newv('DifferentialRevision', array());
break;
default:
return null;
}
return $class_obj->load($receiver_id);
}
public static function computeMailHash($mail_key, $phid) {
$global_mail_key = PhabricatorEnv::getEnvConfig('phabricator.mail-key');
$hash = sha1($mail_key.$global_mail_key.$phid);
return substr($hash, 0, 16);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sun, Nov 16, 2:41 AM (2 h, 26 m)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
338682
Default Alt Text
(53 KB)

Event Timeline