mirror of
https://we.phorge.it/source/phorge.git
synced 2024-11-11 17:32:41 +01:00
2793828795
Summary: It seems that Outlook and Mail.app mostly ignores the threading headers and thread primarily by subject. They are also very picky about the Re: part in the header. I guess that's because users of these clients often hit Reply when they want to create a new message to the sender of an e-mail. We need both of these applications to work with the same setting because we don't use multiplexing to prevent sending multiple e-mails to people in lists. I also believe that the default behavior should just work in most setups. I've tried several different combinations of putting "Re:" and none of them seems to always work in both clients. This diff at least adds more abstraction to the code which should prevent copy/paste errors (two fixed by this diff!). Test Plan: Sent several e-mails with varying subject, verified that they look as before in Outlook and Mail.app. Reviewers: epriestley, nh Reviewed By: epriestley CC: aran, Korvin Differential Revision: https://secure.phabricator.com/D2709
440 lines
13 KiB
PHP
440 lines
13 KiB
PHP
<?php
|
|
|
|
/*
|
|
* Copyright 2012 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.
|
|
*/
|
|
|
|
/**
|
|
* @group maniphest
|
|
*/
|
|
final class ManiphestTransactionEditor {
|
|
|
|
private $parentMessageID;
|
|
private $auxiliaryFields = array();
|
|
|
|
public function setAuxiliaryFields(array $fields) {
|
|
assert_instances_of($fields, 'ManiphestAuxiliaryFieldSpecification');
|
|
$this->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())
|
|
->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);
|
|
}
|
|
|
|
|
|
}
|