Differential D11329 Diff 27202 src/applications/transactions/worker/PhabricatorApplicationTransactionEmailWorker.php
Changeset View
Changeset View
Standalone View
Standalone View
src/applications/transactions/worker/PhabricatorApplicationTransactionEmailWorker.php
- This file was added.
| <?php | |||||
| abstract class PhabricatorApplicationTransactionEmailWorker | |||||
| extends PhabricatorWorker { | |||||
| private $actingAsPHID; | |||||
| private $isNewObject; | |||||
| private $heraldHeader; | |||||
| private $excludeMailRecipientPHIDs; | |||||
| private $parentMessageID; | |||||
| abstract protected function loadObject($phid); | |||||
| abstract protected function loadXActions($xactions); | |||||
| abstract protected function buildReplyHandler(PhabricatorLiskDAO $object); | |||||
| public function getMaximumRetryCount() { | |||||
| return 250; | |||||
| } | |||||
| public function getWaitBeforeRetry(PhabricatorWorkerTask $task) { | |||||
| return ($task->getFailureCount() * 15); | |||||
| } | |||||
| public function getActingAsPHID() { | |||||
| return $this->actingAsPHID; | |||||
| } | |||||
| public function getIsNewObject() { | |||||
| return $this->isNewObject; | |||||
| } | |||||
| public function doWork() { | |||||
| $task_data = $this->getTaskData(); | |||||
| 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')); | |||||
| } | |||||
| $mailTo = $task_data['mailTo']; | |||||
| if (!idx($task_data, 'mailCC')) { | |||||
| throw new PhabricatorWorkerPermanentFailureException( | |||||
| pht('Missing mailCC in Tasks Data')); | |||||
| } | |||||
| $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'); | |||||
| $this->aux_data = idx($task_data, 'aux_data', null); | |||||
| $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'); | |||||
| $email_to = $this->expandPHIDs(array_filter(array_unique($mailTo))); | |||||
| $email_cc = $this->expandPHIDs(array_filter(array_unique($mailCC))); | |||||
| $recipients = array_merge($email_to, $email_cc); | |||||
| $users = id(new PhabricatorPeopleQuery()) | |||||
| ->setViewer(PhabricatorUser::getOmnipotentUser()) | |||||
| ->withPHIDs($recipients) | |||||
| ->execute(); | |||||
| $users = mpull($users, null, 'getPHID'); | |||||
| foreach ($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($users[$key]); | |||||
| $phid = array($user->getPHID()); | |||||
| $recipients = array_diff($recipients, $phid); | |||||
| $email_to = array_diff($email_to, $phid); | |||||
| $email_cc = array_diff($email_cc, $phid); | |||||
| } | |||||
| } | |||||
| } | |||||
| $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 = $users[$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 = $users[$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) | |||||
| $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 users just a single email | |||||
| break; | |||||
| } | |||||
| } | |||||
| foreach ($emails as $mail) { | |||||
| $mail->saveAndSend(); | |||||
| } | |||||
| return true; | |||||
| } | |||||
| 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; | |||||
| } | |||||
| // FIXME: Make abstract? | |||||
| protected function buildMailTemplate(PhabricatorLiskDAO $object) { | |||||
| throw new Exception('Capability not supported.'); | |||||
| } | |||||
| // FIXME: Make abstract? | |||||
| protected function getMailSubjectPrefix() { | |||||
| throw new Exception('Capability not supported.'); | |||||
| } | |||||
| private function addMailProjectMetadata( | |||||
| PhabricatorLiskDAO $object, | |||||
| PhabricatorMetaMTAMail $template, | |||||
| PhabricatorUser $viewer) { | |||||
| $project_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( | |||||
| $object->getPHID(), | |||||
| PhabricatorProjectObjectHasProjectEdgeType::EDGECONST); | |||||
| if (!$project_phids) { | |||||
| return; | |||||
| } | |||||
| // TODO: This viewer isn't quite right. It would be slightly better to use | |||||
| // the mail recipient, but that's not very easy given the way rendering | |||||
| // works today. | |||||
| $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; | |||||
| } | |||||
| } | |||||