diff --git a/src/applications/differential/mail/DifferentialMail.php b/src/applications/differential/mail/DifferentialMail.php index 2f539caf50..4b607ae22f 100644 --- a/src/applications/differential/mail/DifferentialMail.php +++ b/src/applications/differential/mail/DifferentialMail.php @@ -1,421 +1,426 @@ getRevision(); $title = $revision->getTitle(); $id = $revision->getID(); return "D{$id}: {$title}"; } abstract protected function renderVaryPrefix(); 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(); $body = $this->buildBody(); $attachments = $this->buildAttachments(); $template = new PhabricatorMetaMTAMail(); $actor_handle = $this->getActorHandle(); $reply_handler = $this->getReplyHandler(); if ($actor_handle) { $template->setFrom($actor_handle->getPHID()); } $template ->setSubject($this->renderSubject()) ->setSubjectPrefix($this->getSubjectPrefix()) ->setVarySubjectPrefix($this->renderVaryPrefix()) ->setBody($body) ->setIsHTML($this->shouldMarkMailAsHTML()) ->setParentMessageID($this->parentMessageID) - ->addHeader('Thread-Topic', $this->getRevision()->getTitle()); + ->addHeader('Thread-Topic', $this->getThreadTopic()); $template->setAttachments($attachments); $template->setThreadID( $this->getThreadID(), $this->isFirstMailAboutRevision()); if ($this->heraldRulesHeader) { $template->addHeader('X-Herald-Rules', $this->heraldRulesHeader); } $revision = $this->revision; if ($revision) { if ($revision->getAuthorPHID()) { $template->addHeader( 'X-Differential-Author', '<'.$revision->getAuthorPHID().'>'); } if ($revision->getReviewers()) { $template->addHeader( 'X-Differential-Reviewers', '<'.implode('>, <', $revision->getReviewers()).'>'); } if ($revision->getCCPHIDs()) { $template->addHeader( 'X-Differential-CCs', '<'.implode('>, <', $revision->getCCPHIDs()).'>'); // Determine explicit CCs (those added by humans) and put them in a // header so users can differentiate between Herald CCs and human CCs. $relation_subscribed = DifferentialRevision::RELATION_SUBSCRIBED; $raw = $revision->getRawRelations($relation_subscribed); $reason_phids = ipull($raw, 'reasonPHID'); $reason_handles = id(new PhabricatorObjectHandleData($reason_phids)) ->loadHandles(); $explicit_cc = array(); foreach ($raw as $relation) { if (!$relation['reasonPHID']) { continue; } $type = $reason_handles[$relation['reasonPHID']]->getType(); if ($type == PhabricatorPHIDConstants::PHID_TYPE_USER) { $explicit_cc[] = $relation['objectPHID']; } } if ($explicit_cc) { $template->addHeader( 'X-Differential-Explicit-CCs', '<'.implode('>, <', $explicit_cc).'>'); } } } $template->setIsBulk(true); $template->setRelatedPHID($this->getRevision()->getPHID()); $mailtags = $this->getMailTags(); if ($mailtags) { $template->setMailTags($mailtags); } $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(); $event = new PhabricatorEvent( PhabricatorEventType::TYPE_DIFFERENTIAL_WILLSENDMAIL, array( 'mail' => $template, ) ); PhutilEventEngine::dispatchEvent($event); $template = $event->getValue('mail'); $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 getMailTags() { return array(); } protected function getSubjectPrefix() { return PhabricatorEnv::getEnvConfig('metamta.differential.subject-prefix'); } 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 .= <<replyHandler) { $this->replyHandler = self::newReplyHandlerForRevision($this->getRevision()); } return $this->replyHandler; } public static function newReplyHandlerForRevision( DifferentialRevision $revision) { $reply_handler = PhabricatorEnv::newObjectFromConfig( 'metamta.differential.reply-handler'); $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(); return "differential-rev-{$phid}-req"; } + protected function getThreadTopic() { + $phid = $this->getRevision()->getPHID(); + return "Differential Revision {$phid}"; + } + 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) { assert_instances_of($inline_comments, 'PhabricatorInlineCommentInterface'); $this->inlineComments = $inline_comments; return $this; } public function getInlineComments() { return $this->inlineComments; } protected function renderAuxFields($phase) { $selector = DifferentialFieldSelector::newSelector(); $aux_fields = $selector->sortFieldsForMail( $selector->getFieldSpecifications()); $body = array(); foreach ($aux_fields as $field) { $field->setRevision($this->getRevision()); $text = $field->renderValueForMail($phase); if ($text !== null) { $body[] = $text; $body[] = null; } } return implode("\n", $body); } 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; } protected function renderHandleList(array $handles, array $phids) { assert_instances_of($handles, 'PhabricatorObjectHandle'); $names = array(); foreach ($phids as $phid) { $names[] = $handles[$phid]->getName(); } return implode(', ', $names); } } diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php index 4876fb5849..d9307b3aa2 100644 --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -1,440 +1,440 @@ auxiliaryFields = $fields; return $this; } public function setParentMessageID($parent_message_id) { $this->parentMessageID = $parent_message_id; return $this; } public function applyTransactions(ManiphestTask $task, array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $email_cc = $task->getCCPHIDs(); $email_to = array(); $email_to[] = $task->getOwnerPHID(); $pri_changed = $this->isCreate($transactions); foreach ($transactions as $key => $transaction) { $type = $transaction->getTransactionType(); $new = $transaction->getNewValue(); $email_to[] = $transaction->getAuthorPHID(); $value_is_phid_set = false; 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(); $value_is_phid_set = true; 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(); $value_is_phid_set = true; break; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); if (!$aux_key) { throw new Exception( "Expected 'aux:key' metadata on TYPE_AUXILIARY transaction."); } $old = $task->getAuxiliaryAttribute($aux_key); break; default: throw new Exception('Unknown action type.'); } $old_cmp = $old; $new_cmp = $new; if ($value_is_phid_set) { // Normalize the old and new values if they are PHID sets so we don't // get any no-op transactions where the values differ only by keys, // order, duplicates, etc. if (is_array($old)) { $old = array_filter($old); $old = array_unique($old); sort($old); $old = array_values($old); $old_cmp = $old; } if (is_array($new)) { $new = array_filter($new); $new = array_unique($new); $transaction->setNewValue($new); $new_cmp = $new; sort($new_cmp); $new_cmp = array_values($new_cmp); } } if (($old !== null) && ($old_cmp == $new_cmp)) { 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: if ($new) { $handles = id(new PhabricatorObjectHandleData(array($new))) ->loadHandles(); $task->setOwnerOrdering($handles[$new]->getName()); } else { $task->setOwnerOrdering(null); } $task->setOwnerPHID($new); break; case ManiphestTransactionType::TYPE_CCS: $task->setCCPHIDs($new); break; case ManiphestTransactionType::TYPE_PRIORITY: $task->setPriority($new); $pri_changed = true; 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; case ManiphestTransactionType::TYPE_AUXILIARY: $aux_key = $transaction->getMetadataValue('aux:key'); $task->setAuxiliaryAttribute($aux_key, $new); break; default: throw new Exception('Unknown action type.'); } $transaction->setOldValue($old); $transaction->setNewValue($new); } } if ($pri_changed) { $subpriority = ManiphestTransactionEditor::getNextSubpriority( $task->getPriority(), null); $task->setSubpriority($subpriority); } $task->save(); foreach ($transactions as $transaction) { $transaction->setTaskID($task->getID()); $transaction->save(); } $email_to[] = $task->getOwnerPHID(); $email_cc = array_merge( $email_cc, $task->getCCPHIDs()); $this->publishFeedStory($task, $transactions); // 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); $view->setAuxiliaryFields($this->auxiliaryFields); list($action, $body) = $view->renderForEmail($with_date = false); $is_create = $this->isCreate($transactions); $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(); $mailtags = $this->getMailTags($transactions); $template = id(new PhabricatorMetaMTAMail()) ->setSubject("T{$task_id}: {$title}") ->setSubjectPrefix($this->getSubjectPrefix()) ->setVarySubjectPrefix("[{$action}]") ->setFrom($transaction->getAuthorPHID()) ->setParentMessageID($this->parentMessageID) - ->addHeader('Thread-Topic', 'Maniphest Task '.$task->getID()) + ->addHeader('Thread-Topic', 'Maniphest Task '.$task->getPHID()) ->setThreadID($thread_id, $is_create) ->setRelatedPHID($task->getPHID()) ->setIsBulk(true) ->setMailTags($mailtags) ->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_object = PhabricatorEnv::newObjectFromConfig( 'metamta.maniphest.reply-handler'); $handler_object->setMailReceiver($task); return $handler_object; } private function publishFeedStory(ManiphestTask $task, array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $actions = array(ManiphestAction::ACTION_UPDATE); $comments = null; foreach ($transactions as $transaction) { if ($transaction->hasComments()) { $comments = $transaction->getComments(); } $type = $transaction->getTransactionType(); switch ($type) { case ManiphestTransactionType::TYPE_OWNER: $actions[] = ManiphestAction::ACTION_ASSIGN; break; case ManiphestTransactionType::TYPE_STATUS: if ($task->getStatus() != ManiphestTaskStatus::STATUS_OPEN) { $actions[] = ManiphestAction::ACTION_CLOSE; } else if ($this->isCreate($transactions)) { $actions[] = ManiphestAction::ACTION_CREATE; } else { $actions[] = ManiphestAction::ACTION_REOPEN; } break; default: $actions[] = $type; break; } } $action_type = ManiphestAction::selectStrongestAction($actions); $owner_phid = $task->getOwnerPHID(); $actor_phid = head($transactions)->getAuthorPHID(); $author_phid = $task->getAuthorPHID(); id(new PhabricatorFeedStoryPublisher()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_MANIPHEST) ->setStoryData(array( 'taskPHID' => $task->getPHID(), 'transactionIDs' => mpull($transactions, 'getID'), 'ownerPHID' => $owner_phid, 'action' => $action_type, 'comments' => $comments, 'description' => $task->getDescription(), )) ->setStoryTime(time()) ->setStoryAuthorPHID($actor_phid) ->setRelatedPHIDs( array_merge( array_filter( array( $task->getPHID(), $author_phid, $actor_phid, $owner_phid, )), $task->getProjectPHIDs())) ->setPrimaryObjectPHID($task->getPHID()) ->setSubscribedPHIDs( array_merge( array_filter( array( $author_phid, $owner_phid, $actor_phid)), $task->getCCPHIDs())) ->publish(); } private function isCreate(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $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; } } return $is_create; } private function getMailTags(array $transactions) { assert_instances_of($transactions, 'ManiphestTransaction'); $tags = array(); foreach ($transactions as $xaction) { switch ($xaction->getTransactionType()) { case ManiphestTransactionType::TYPE_CCS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_CC; break; case ManiphestTransactionType::TYPE_PROJECTS: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PROJECTS; break; case ManiphestTransactionType::TYPE_PRIORITY: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_PRIORITY; break; default: $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_OTHER; break; } if ($xaction->hasComments()) { $tags[] = MetaMTANotificationType::TYPE_MANIPHEST_COMMENT; } } return array_unique($tags); } public static function getNextSubpriority($pri, $sub) { if ($sub === null) { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d ORDER BY subpriority ASC LIMIT 1', $pri); if ($next) { return $next->getSubpriority() - ((double)(2 << 16)); } } else { $next = id(new ManiphestTask())->loadOneWhere( 'priority = %d AND subpriority > %s ORDER BY subpriority ASC LIMIT 1', $pri, $sub); if ($next) { return ($sub + $next->getSubpriority()) / 2; } } return (double)(2 << 32); } }