diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1282,6 +1282,7 @@ 'PhabricatorApplicationTransactionInterface' => 'applications/transactions/interface/PhabricatorApplicationTransactionInterface.php', 'PhabricatorApplicationTransactionNoEffectException' => 'applications/transactions/exception/PhabricatorApplicationTransactionNoEffectException.php', 'PhabricatorApplicationTransactionNoEffectResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionNoEffectResponse.php', + 'PhabricatorApplicationTransactionNotificationWorker' => 'applications/transactions/worker/PhabricatorApplicationTransactionNotificationWorker.php', 'PhabricatorApplicationTransactionQuery' => 'applications/transactions/query/PhabricatorApplicationTransactionQuery.php', 'PhabricatorApplicationTransactionResponse' => 'applications/transactions/response/PhabricatorApplicationTransactionResponse.php', 'PhabricatorApplicationTransactionShowOlderController' => 'applications/transactions/controller/PhabricatorApplicationTransactionShowOlderController.php', @@ -1937,6 +1938,7 @@ 'PhabricatorManiphestApplication' => 'applications/maniphest/application/PhabricatorManiphestApplication.php', 'PhabricatorManiphestConfigOptions' => 'applications/maniphest/config/PhabricatorManiphestConfigOptions.php', 'PhabricatorManiphestTaskTestDataGenerator' => 'applications/maniphest/lipsum/PhabricatorManiphestTaskTestDataGenerator.php', + 'PhabricatorManiphestTransactionNotificationWorker' => 'applications/maniphest/worker/PhabricatorManiphestTransactionNotificationWorker.php', 'PhabricatorMarkupCache' => 'applications/cache/storage/PhabricatorMarkupCache.php', 'PhabricatorMarkupEngine' => 'infrastructure/markup/PhabricatorMarkupEngine.php', 'PhabricatorMarkupInterface' => 'infrastructure/markup/PhabricatorMarkupInterface.php', @@ -4469,6 +4471,7 @@ 'PhabricatorApplicationTransactionFeedStory' => 'PhabricatorFeedStory', 'PhabricatorApplicationTransactionNoEffectException' => 'Exception', 'PhabricatorApplicationTransactionNoEffectResponse' => 'AphrontProxyResponse', + 'PhabricatorApplicationTransactionNotificationWorker' => 'PhabricatorWorker', 'PhabricatorApplicationTransactionQuery' => 'PhabricatorCursorPagedPolicyAwareQuery', 'PhabricatorApplicationTransactionResponse' => 'AphrontProxyResponse', 'PhabricatorApplicationTransactionShowOlderController' => 'PhabricatorApplicationTransactionController', @@ -5159,6 +5162,7 @@ 'PhabricatorManiphestApplication' => 'PhabricatorApplication', 'PhabricatorManiphestConfigOptions' => 'PhabricatorApplicationConfigOptions', 'PhabricatorManiphestTaskTestDataGenerator' => 'PhabricatorTestDataGenerator', + 'PhabricatorManiphestTransactionNotificationWorker' => 'PhabricatorApplicationTransactionNotificationWorker', 'PhabricatorMarkupCache' => 'PhabricatorCacheDAO', 'PhabricatorMarkupOneOff' => 'PhabricatorMarkupInterface', 'PhabricatorMarkupPreviewController' => 'PhabricatorController', diff --git a/src/applications/maniphest/editor/ManiphestTransactionEditor.php b/src/applications/maniphest/editor/ManiphestTransactionEditor.php --- a/src/applications/maniphest/editor/ManiphestTransactionEditor.php +++ b/src/applications/maniphest/editor/ManiphestTransactionEditor.php @@ -13,6 +13,10 @@ return pht('Maniphest Tasks'); } + protected function getTransactionNotificationWorkerClass() { + return 'PhabricatorManiphestTransactionNotificationWorker'; + } + public function getTransactionTypes() { $types = parent::getTransactionTypes(); @@ -406,14 +410,6 @@ return $xactions; } - protected function getMailSubjectPrefix() { - return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); - } - - protected function getMailThreadID(PhabricatorLiskDAO $object) { - return 'maniphest-task-'.$object->getPHID(); - } - protected function getMailTo(PhabricatorLiskDAO $object) { return array( $object->getOwnerPHID(), @@ -435,6 +431,9 @@ return $phids; } + // FIXME: Move it to a ManiphestTransaction? + // This is currently duplicated in ManiphestTransactionNotificationWorker! + // And also needed in email preferences. TODO: Find a place to put this stuff public function getMailTagsMap() { return array( ManiphestTransaction::MAILTAG_STATUS => @@ -458,67 +457,6 @@ ); } - protected function buildReplyHandler(PhabricatorLiskDAO $object) { - return id(new ManiphestReplyHandler()) - ->setMailReceiver($object); - } - - protected function buildMailTemplate(PhabricatorLiskDAO $object) { - $id = $object->getID(); - $title = $object->getTitle(); - - return id(new PhabricatorMetaMTAMail()) - ->setSubject("T{$id}: {$title}") - ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle()); - } - - protected function buildMailBody( - PhabricatorLiskDAO $object, - array $xactions) { - - $body = parent::buildMailBody($object, $xactions); - - if ($this->getIsNewObject()) { - $body->addTextSection( - pht('TASK DESCRIPTION'), - $object->getDescription()); - } - - $body->addLinkSection( - pht('TASK DETAIL'), - PhabricatorEnv::getProductionURI('/T'.$object->getID())); - - - $board_phids = array(); - $type_column = ManiphestTransaction::TYPE_PROJECT_COLUMN; - foreach ($xactions as $xaction) { - if ($xaction->getTransactionType() == $type_column) { - $new = $xaction->getNewValue(); - $project_phid = idx($new, 'projectPHID'); - if ($project_phid) { - $board_phids[] = $project_phid; - } - } - } - - if ($board_phids) { - $projects = id(new PhabricatorProjectQuery()) - ->setViewer($this->requireActor()) - ->withPHIDs($board_phids) - ->execute(); - - foreach ($projects as $project) { - $body->addLinkSection( - pht('WORKBOARD'), - PhabricatorEnv::getProductionURI( - '/project/board/'.$project->getID().'/')); - } - } - - - return $body; - } - protected function shouldPublishFeedStory( PhabricatorLiskDAO $object, array $xactions) { diff --git a/src/applications/maniphest/worker/PhabricatorManiphestTransactionNotificationWorker.php b/src/applications/maniphest/worker/PhabricatorManiphestTransactionNotificationWorker.php new file mode 100644 --- /dev/null +++ b/src/applications/maniphest/worker/PhabricatorManiphestTransactionNotificationWorker.php @@ -0,0 +1,117 @@ +withPHIDs(array($phid)) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->executeOne(); + return $task; + } + + protected function loadXActions($xactions) { + $xactions = id(new ManiphestTransactionQuery()) + ->withPHIDs($xactions) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->execute(); + return $xactions; + } + + + /* -( Sending Mail )---------------------------------------------------- */ + protected function buildReplyHandler(PhabricatorLiskDAO $object) { + return id(new ManiphestReplyHandler()) + ->setMailReceiver($object); + } + + protected function getMailSubjectPrefix() { + return PhabricatorEnv::getEnvConfig('metamta.maniphest.subject-prefix'); + } + + protected function getMailThreadID(PhabricatorLiskDAO $object) { + return 'maniphest-task-'.$object->getPHID(); + } + + // FIXME: This is duplicated from the TransactionEditor. + // Move it somewhere else + public function getMailTagsMap() { + return array( + ManiphestTransaction::MAILTAG_STATUS => + pht("A task's status changes."), + ManiphestTransaction::MAILTAG_OWNER => + pht("A task's owner changes."), + ManiphestTransaction::MAILTAG_PRIORITY => + pht("A task's priority changes."), + ManiphestTransaction::MAILTAG_CC => + pht("A task's subscribers change."), + ManiphestTransaction::MAILTAG_PROJECTS => + pht("A task's associated projects change."), + ManiphestTransaction::MAILTAG_UNBLOCK => + pht('One of the tasks a task is blocked by changes status.'), + ManiphestTransaction::MAILTAG_COLUMN => + pht('A task is moved between columns on a workboard.'), + ManiphestTransaction::MAILTAG_COMMENT => + pht('Someone comments on a task.'), + ManiphestTransaction::MAILTAG_OTHER => + pht('Other task activity not listed above occurs.'), + ); + } + + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + $id = $object->getID(); + $title = $object->getTitle(); + + return id(new PhabricatorMetaMTAMail()) + ->setSubject("T{$id}: {$title}") + ->addHeader('Thread-Topic', "T{$id}: ".$object->getOriginalTitle()); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions, + PhabricatorUser $viewer) { + + $body = parent::buildMailBody($object, $xactions, $viewer); + + if ($this->getIsNewObject()) { + $body->addTextSection( + pht('TASK DESCRIPTION'), + $object->getDescription()); + } + + $body->addLinkSection( + pht('TASK DETAIL'), + PhabricatorEnv::getProductionURI('/T'.$object->getID())); + + + $board_phids = array(); + $type_column = ManiphestTransaction::TYPE_PROJECT_COLUMN; + foreach ($xactions as $xaction) { + if ($xaction->getTransactionType() == $type_column) { + $new = $xaction->getNewValue(); + $project_phid = idx($new, 'projectPHID'); + if ($project_phid) { + $board_phids[] = $project_phid; + } + } + } + + if ($board_phids) { + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->withPHIDs($board_phids) + ->execute(); + + foreach ($projects as $project) { + $body->addLinkSection( + pht('WORKBOARD'), + PhabricatorEnv::getProductionURI( + '/project/board/'.$project->getID().'/')); + } + } + + return $body; + } +} diff --git a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php --- a/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ b/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -197,6 +197,35 @@ // for now. $recipients = $tos + $ccs; + // Check if all recipients have proper permissions to the object + // Remove them from the list otherwise + $recipient_users = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs(array_keys($recipients)) + ->execute(); + $recipient_users = mpull($recipient_users, null, 'getPHID'); + + // Check if user has permissions to view this object + foreach ($recipients as $phid => $recipient) { + if ($this->mailReceiver + && $this->mailReceiver instanceof PhabricatorPolicyInterface + && idx($recipient_users, $phid) + && $recipient_users[$phid] instanceof PhabricatorUser) { + if (!PhabricatorPolicyFilter::hasCapability( + $recipient_users[$phid], + $this->mailReceiver, + PhabricatorPolicyCapability::CAN_VIEW)) { + // User has no permission to this object + // so remove them from the recipient list + unset($recipients[$phid]); + } + } + } + + if (!$recipients) { + return $result; + } + // When multiplexing mail, explicitly include To/Cc information in the // message body and headers. diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -29,7 +29,6 @@ private $actingAsPHID; private $disableEmail; - /** * Get the class name for the application this editor is a part of. * @@ -791,29 +790,36 @@ $this->loadHandles($xactions); - $mail = null; - if (!$this->getDisableEmail()) { - if ($this->shouldSendMail($object, $xactions)) { - $mail = $this->sendMail($object, $xactions); + $notifications_sent = $this->sendNotifications($object, $xactions); + + // Fall back to the old code if no notification worker is in use + if (!$notifications_sent) { + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned + $mail = null; + if (!$this->getDisableEmail()) { + if ($this->shouldSendMail($object, $xactions)) { + $mail = $this->sendMail($object, $xactions); + } } - } - if ($this->supportsSearch()) { - id(new PhabricatorSearchIndexer()) - ->queueDocumentForIndexing( - $object->getPHID(), - $this->getSearchContextParameter($object, $xactions)); - } + if ($this->supportsSearch()) { + id(new PhabricatorSearchIndexer()) + ->queueDocumentForIndexing( + $object->getPHID(), + $this->getSearchContextParameter($object, $xactions)); + } - if ($this->shouldPublishFeedStory($object, $xactions)) { - $mailed = array(); - if ($mail) { - $mailed = $mail->buildRecipientList(); + if ($this->shouldPublishFeedStory($object, $xactions)) { + $mailed = array(); + if ($mail) { + $mailed = $mail->buildRecipientList(); + } + $this->publishFeedStory( + $object, + $xactions, + $mailed); } - $this->publishFeedStory( - $object, - $xactions, - $mailed); } $this->didApplyTransactions($xactions); @@ -1893,6 +1899,57 @@ return $xaction->isCommentTransaction(); } +/* -( Notification Worker (Mail & Feeds) )----------------------------------- */ + + protected function getTransactionNotificationWorkerClass() { + throw new Exception( + 'This function needs to be implemented to be able to render Email'); + } + + protected function sendNotifications( + PhabricatorLiskDAO $object, + array $xactions) { + + try { + $worker_class = $this->getTransactionNotificationWorkerClass(); + } catch (Exception $e) { + return false; + } + + $herald_xscript = $this->getHeraldTranscript(); + if ($herald_xscript) { + $herald_header = $herald_xscript->getXHeraldRulesHeader(); + $herald_header = HeraldTranscript::saveXHeraldRulesHeader( + $object->getPHID(), + $herald_header); + } else { + $herald_header = HeraldTranscript::loadXHeraldRulesHeader( + $object->getPHID()); + } + + PhabricatorWorker::scheduleTask( + $worker_class, + array( + 'object' => $object->getPHID(), + 'xactions' => mpull($xactions, 'getPHID'), + 'shouldSendMail' => $this->shouldSendMail($object, $xactions), + 'shouldPublishFeedStory' => + $this->shouldPublishFeedStory($object, $xactions), + 'mailTo' => $this->getMailTo($object), + 'mailCC' => $this->getMailCC($object), + 'actingAsPHID' => $this->getActingAsPHID(), + 'isNewObject' => $this->getIsNewObject(), + 'heraldHeader' => $herald_header, + 'excludeMailRecipientPHIDs' => $this->getExcludeMailRecipientPHIDs(), + 'parentMessageID' => $this->getParentMessageID(), + ), + array( + 'priority' => PhabricatorWorker::PRIORITY_ALERTS, // FIXME: right prio? + )); + + return true; + } + /* -( Sending Mail )------------------------------------------------------- */ @@ -1910,13 +1967,14 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function sendMail( PhabricatorLiskDAO $object, array $xactions) { // Check if any of the transactions are visible. If we don't have any // visible transactions, don't send the mail. - $any_visible = false; foreach ($xactions as $xaction) { if (!$xaction->shouldHideForMail($xactions)) { @@ -1931,8 +1989,8 @@ $email_to = array_filter(array_unique($this->getMailTo($object))); $email_cc = array_filter(array_unique($this->getMailCC($object))); - $phids = array_merge($email_to, $email_cc); + $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireActor()) ->withPHIDs($phids) @@ -2006,6 +2064,8 @@ return $template; } + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned private function addMailProjectMetadata( PhabricatorLiskDAO $object, PhabricatorMetaMTAMail $template) { @@ -2044,6 +2104,8 @@ } + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getMailThreadID(PhabricatorLiskDAO $object) { return $object->getPHID(); } @@ -2052,6 +2114,8 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getStrongestAction( PhabricatorLiskDAO $object, array $xactions) { @@ -2062,6 +2126,8 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function buildReplyHandler(PhabricatorLiskDAO $object) { throw new Exception('Capability not supported.'); } @@ -2069,6 +2135,8 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getMailSubjectPrefix() { throw new Exception('Capability not supported.'); } @@ -2077,6 +2145,9 @@ /** * @task mail */ + // FIXME: Move it to a ManiphestTransaction? + // This is currently duplicated in ManiphestTransactionNotificationWorker! + // And also needed in email preferences. TODO: Find a place to put this stuff protected function getMailTags( PhabricatorLiskDAO $object, array $xactions) { @@ -2092,6 +2163,8 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned public function getMailTagsMap() { // TODO: We should move shared mail tags, like "comment", here. return array(); @@ -2101,6 +2174,8 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getMailAction( PhabricatorLiskDAO $object, array $xactions) { @@ -2111,6 +2186,8 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function buildMailTemplate(PhabricatorLiskDAO $object) { throw new Exception('Capability not supported.'); } @@ -2187,6 +2264,8 @@ /** * @task mail */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function buildMailBody( PhabricatorLiskDAO $object, array $xactions) { @@ -2273,6 +2352,8 @@ /** * @task feed */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getFeedStoryType() { return 'PhabricatorApplicationTransactionFeedStory'; } @@ -2281,6 +2362,8 @@ /** * @task feed */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getFeedRelatedPHIDs( PhabricatorLiskDAO $object, array $xactions) { @@ -2306,6 +2389,8 @@ /** * @task feed */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getFeedNotifyPHIDs( PhabricatorLiskDAO $object, array $xactions) { @@ -2319,6 +2404,8 @@ /** * @task feed */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function getFeedStoryData( PhabricatorLiskDAO $object, array $xactions) { @@ -2336,6 +2423,8 @@ /** * @task feed */ + // NotificationWorkerGrepToken + // Remove this code when all apps have transitioned protected function publishFeedStory( PhabricatorLiskDAO $object, array $xactions, diff --git a/src/applications/transactions/worker/PhabricatorApplicationTransactionNotificationWorker.php b/src/applications/transactions/worker/PhabricatorApplicationTransactionNotificationWorker.php new file mode 100644 --- /dev/null +++ b/src/applications/transactions/worker/PhabricatorApplicationTransactionNotificationWorker.php @@ -0,0 +1,511 @@ +getFailureCount() * 15); + } + + + protected function getActingAsPHID() { + return $this->actingAsPHID; + } + + protected function getIsNewObject() { + return $this->isNewObject; + } + + protected function expandPHIDs($phids) { + // Expand recipients (get projects members) + $map = id(new PhabricatorMetaMTAMemberQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($phids) + ->execute(); + + $results = array(); + foreach ($phids as $phid) { + if (isset($map[$phid])) { + foreach ($map[$phid] as $expanded_phid) { + $results[$expanded_phid] = $expanded_phid; + } + } else { + $results[$phid] = $phid; + } + } + return $results; + } + + protected function loadUsers(array $phids) { + $users = id(new PhabricatorPeopleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($phids) + ->execute(); + $this->users = mpull($users, null, 'getPHID'); + } + + protected function filterUsersWithoutPermission($object) { + foreach ($this->users as $key => $user) { + // Check if the receiving user has permissions to view this object + if ($object + && $object instanceof PhabricatorPolicyInterface + && $user + && $user instanceof PhabricatorUser) { + if (!PhabricatorPolicyFilter::hasCapability( + $user, + $object, + PhabricatorPolicyCapability::CAN_VIEW)) { + // User has no permission to this object + // so remove them from all lists + unset($this->users[$key]); + $phid = array($user->getPHID()); + $this->mailTo = array_diff($this->mailTo, $phid); + $this->mailTo = array_diff($this->mailTo, $phid); + } + } + } + } + + protected function getUser($phid) { + return idx($this->users, $phid, null); + } + + public function doWork() { + $task_data = $this->getTaskData(); + + $this->shouldSendMail = + idx($task_data, 'shouldSendMail', false); + + $this->shouldPublishFeedStory = + idx($task_data, 'shouldPublishFeedStory', false); + + if (!idx($task_data, 'actingAsPHID')) { + throw new PhabricatorWorkerPermanentFailureException( + pht('Missing actingAsPHID in Tasks Data')); + } + $this->actingAsPHID = $task_data['actingAsPHID']; + + if (!idx($task_data, 'mailTo')) { + throw new PhabricatorWorkerPermanentFailureException( + pht('Missing mailTo in Tasks Data')); + } + $this->mailTo = $task_data['mailTo']; + + if (!idx($task_data, 'mailCC')) { + throw new PhabricatorWorkerPermanentFailureException( + pht('Missing mailCC in Tasks Data')); + } + $this->mailCC = $task_data['mailCC']; + + $this->heraldHeader = + idx($task_data, 'heraldHeader', null); + + $this->isNewObject = + idx($task_data, 'isNewObject', false); + + $this->excludeMailRecipientPHIDs = + idx($task_data, 'excludeMailRecipientPHIDs'); + + $this->parentMessageID = + idx($task_data, 'parentMessageID'); + + // Load object and xactions + $object = $this->loadObject($task_data['object']); + if (!$object) { + throw new PhabricatorWorkerPermanentFailureException( + pht('Unable to load object!')); + } + + $xactions = $this->loadXActions($task_data['xactions']); + assert_instances_of($xactions, 'PhabricatorApplicationTransaction'); + + // Filter non-visible xactions + $feed_xactions = mfilter($xactions, 'shouldHideForFeed', true); + $mail_xactions = array(); + foreach ($xactions as $xaction) { + if (!$xaction->shouldHideForMail($xactions)) { + $mail_xactions[] = $xaction; + } + } + + // Expand TO & CC (Turn Project PHIDs into its Member PHIDs) + $this->mailTo = $this->expandPHIDs( + array_filter(array_unique($this->mailTo))); + $this->mailCC = $this->expandPHIDs( + array_filter(array_unique($this->mailCC))); + + // Load corresponding User Objects + $this->loadUsers(array_merge($this->mailTo, $this->mailCC)); + + // Filter Users without permission to the object notified about + $this->filterUsersWithoutPermission($object); + + // Send Mail + $mailed_phids = array(); + if ($mail_xactions) { + $mailed_phids = $this->sendMail($object, $mail_xactions); + } + + // Publish Feed Stories + if ($feed_xactions) { + $this->publishFeedStory($object, $feed_xactions, $mailed_phids); + } + + return true; + } + + protected function sendMail($object, $xactions) { + $email_to = $this->getMailTo($object); + $email_cc = $this->getMailCC($object); + $recipients = array_merge($email_to, $email_cc); + + $handles = id(new PhabricatorHandleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($recipients) + ->execute(); + + $to_handles = array_select_keys($handles, $email_to); + $cc_handles = array_select_keys($handles, $email_cc); + + $emails = array(); + $send_as_one_email = false; + // Render the email for each user individually + // so correct permissions are preserved + foreach ($recipients as $recipient) { + $viewer = $this->getUser($recipient); + + $template = $this->buildMailTemplate($object); + $template->addPHIDHeaders('X-Phabricator-To', array_keys($email_to)); + $template->addPHIDHeaders('X-Phabricator-Cc', array_keys($email_cc)); + + if (!PhabricatorMetaMTAMail::shouldMultiplexAllMail()) { + // If config is set to not multiplex we render as the triggering user + $viewer = $this->getUser($this->getActingAsPHID()); + $template->addTos($email_to); + $template->addCCs($email_cc); + $send_as_one_email = true; + } else { + $template->addTos(array($recipient)); + } + + // This stuff doesen't change per user. We still handle + // it here to support sending in the users locale in the future. + $mail_tags = $this->getMailTags($object, $xactions); + $action = $this->getMailAction($object, $xactions); + $reply_handler = $this->buildReplyHandler($object); + $reply_section = $reply_handler->getReplyHandlerInstructions(); + + // Render the actual mail body + $body = $this->buildMailBody($object, $xactions, $viewer); + + if ($reply_section !== null) { + $body->addReplySection($reply_section); + } + + $body->addEmailPreferenceSection(); + + // FIXME?: This changes the emails a bit + // (adds a RECIPIENTS header at the bottom) + // Before this those were added by raw concatenation to the body + $body->addPlainTextSection('RECIPIENTS', + $reply_handler->getRecipientsSummary($to_handles, $cc_handles)); + $body->addHTMLSection('RECIPIENTS', + $reply_handler->getRecipientsSummaryHTML($to_handles, $cc_handles)); + + // TODO: Reply-To handling is still missing + + $template + ->setFrom($this->getActingAsPHID()) + ->setSubjectPrefix($this->getMailSubjectPrefix()) + ->setVarySubjectPrefix('['.$action.']') + ->setThreadID($this->getMailThreadID($object), $this->getIsNewObject()) + ->setRelatedPHID($object->getPHID()) + ->setExcludeMailRecipientPHIDs($this->excludeMailRecipientPHIDs) + ->setMailTags($mail_tags) + ->setIsBulk(true) + ->setBody($body->render()) + ->setHTMLBody($body->renderHTML()); + + foreach ($body->getAttachments() as $attachment) { + $template->addAttachment($attachment); + } + + if ($this->heraldHeader) { + $template->addHeader('X-Herald-Rules', $this->heraldHeader); + } + + if ($object instanceof PhabricatorProjectInterface) { + $this->addMailProjectMetadata($object, $template, $viewer); + } + + if ($this->parentMessageID) { + $template->setParentMessageID($this->parentMessageID); + } + + $emails[] = $template; + + if ($send_as_one_email) { + // We send all just a single email for all users + break; + } + } + + $mailed_phids = array(); + foreach ($emails as $mail) { + $mail->saveAndSend(); + // The mailed_phids are collected here so we know we've + // sent them email and don't have to notify them in the Web UI + $mailed_phids += $mail->buildRecipientList(); + } + + return $mailed_phids; + } + + + /* -( Sending Mail )---------------------------------------------------- */ + protected function buildMailTemplate(PhabricatorLiskDAO $object) { + throw new Exception('Capability not supported.'); + } + + protected function getMailSubjectPrefix() { + throw new Exception('Capability not supported.'); + } + + private function getMailTo(PhabricatorLiskDAO $object) { + return $this->mailTo; + } + + private function getMailCC(PhabricatorLiskDAO $object) { + return $this->mailCC; + } + + private function addMailProjectMetadata( + PhabricatorLiskDAO $object, + PhabricatorMetaMTAMail $template, + PhabricatorUser $viewer) { + + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + + if (!$project_phids) { + return; + } + + $handles = id(new PhabricatorHandleQuery()) + ->setViewer($viewer) + ->withPHIDs($project_phids) + ->execute(); + + $project_tags = array(); + foreach ($handles as $handle) { + if (!$handle->isComplete()) { + continue; + } + $project_tags[] = '<'.$handle->getObjectName().'>'; + } + + if (!$project_tags) { + return; + } + + $project_tags = implode(', ', $project_tags); + $template->addHeader('X-Phabricator-Projects', $project_tags); + } + + + protected function getMailThreadID(PhabricatorLiskDAO $object) { + return $object->getPHID(); + } + + protected function getMailTags( + PhabricatorLiskDAO $object, + array $xactions) { + $tags = array(); + + foreach ($xactions as $xaction) { + $tags[] = $xaction->getMailTags(); + } + + return array_mergev($tags); + } + + public function getMailTagsMap() { + // TODO: We should move shared mail tags, like "comment", here. + return array(); + } + + protected function getStrongestAction( + PhabricatorLiskDAO $object, + array $xactions) { + return last(msort($xactions, 'getActionStrength')); + } + + protected function getMailAction( + PhabricatorLiskDAO $object, + array $xactions) { + return $this->getStrongestAction($object, $xactions)->getActionName(); + } + + protected function buildMailBody( + PhabricatorLiskDAO $object, + array $xactions, + PhabricatorUser $viewer) { + + $headers = array(); + $comments = array(); + + foreach ($xactions as $xaction) { + if ($xaction->shouldHideForMail($xactions)) { + continue; + } + + $header = $xaction->getTitleForMail(); + if ($header !== null) { + $headers[] = $header; + } + + $comment = $xaction->getBodyForMail(); + if ($comment !== null) { + $comments[] = $comment; + } + } + + $body = new PhabricatorMetaMTAMailBody(); + $body->setViewer($viewer); + $body->addRawSection(implode("\n", $headers)); + + foreach ($comments as $comment) { + $body->addRemarkupSection($comment); + } + + if ($object instanceof PhabricatorCustomFieldInterface) { + $field_list = PhabricatorCustomField::getObjectFields( + $object, + PhabricatorCustomField::ROLE_TRANSACTIONMAIL); + $field_list->setViewer($viewer); + $field_list->readFieldsFromStorage($object); + + foreach ($field_list->getFields() as $field) { + $field->updateTransactionMailBody( + $body, + $this, + $xactions); + } + } + + return $body; + } + + /* -( Publishing Feed Stories )------------------------------------------ */ + + /** + * @task feed + */ + protected function getFeedStoryType() { + return 'PhabricatorApplicationTransactionFeedStory'; + } + + + /** + * @task feed + */ + protected function getFeedRelatedPHIDs( + PhabricatorLiskDAO $object, + array $xactions) { + + $phids = array( + $object->getPHID(), + $this->getActingAsPHID(), + ); + + if ($object instanceof PhabricatorProjectInterface) { + $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( + $object->getPHID(), + PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); + foreach ($project_phids as $project_phid) { + $phids[] = $project_phid; + } + } + + return $phids; + } + + + /** + * @task feed + */ + protected function getFeedNotifyPHIDs( + PhabricatorLiskDAO $object, + array $xactions) { + + return array_unique(array_merge( + $this->getMailTo($object), + $this->getMailCC($object))); + } + + + /** + * @task feed + */ + protected function getFeedStoryData( + PhabricatorLiskDAO $object, + array $xactions) { + + $xactions = msort($xactions, 'getActionStrength'); + $xactions = array_reverse($xactions); + + return array( + 'objectPHID' => $object->getPHID(), + 'transactionPHIDs' => mpull($xactions, 'getPHID'), + ); + } + + + /** + * @task feed + */ + protected function publishFeedStory( + PhabricatorLiskDAO $object, + array $xactions, + array $mailed_phids) { + + $related_phids = $this->getFeedRelatedPHIDs($object, $xactions); + $subscribed_phids = $this->getFeedNotifyPHIDs($object, $xactions); + + $story_type = $this->getFeedStoryType(); + $story_data = $this->getFeedStoryData($object, $xactions); + + id(new PhabricatorFeedStoryPublisher()) + ->setStoryType($story_type) + ->setStoryData($story_data) + ->setStoryTime(time()) + ->setStoryAuthorPHID($this->getActingAsPHID()) + ->setRelatedPHIDs($related_phids) + ->setPrimaryObjectPHID($object->getPHID()) + ->setSubscribedPHIDs($subscribed_phids) + ->setMailRecipientPHIDs($mailed_phids) + ->setMailTags($this->getMailTags($object, $xactions)) + ->publish(); + } + +}