Index: src/__phutil_library_map__.php =================================================================== --- src/__phutil_library_map__.php +++ src/__phutil_library_map__.php @@ -1648,6 +1648,7 @@ 'PhabricatorMetaMTAMailTestCase' => 'applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php', 'PhabricatorMetaMTAMailgunReceiveController' => 'applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php', 'PhabricatorMetaMTAMailingList' => 'applications/mailinglists/storage/PhabricatorMetaMTAMailingList.php', + 'PhabricatorMetaMTAMemberQuery' => 'applications/metamta/query/PhabricatorMetaMTAMemberQuery.php', 'PhabricatorMetaMTAPermanentFailureException' => 'applications/metamta/exception/PhabricatorMetaMTAPermanentFailureException.php', 'PhabricatorMetaMTAReceivedMail' => 'applications/metamta/storage/PhabricatorMetaMTAReceivedMail.php', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'applications/metamta/exception/PhabricatorMetaMTAReceivedMailProcessingException.php', @@ -4325,6 +4326,7 @@ 0 => 'PhabricatorMetaMTADAO', 1 => 'PhabricatorPolicyInterface', ), + 'PhabricatorMetaMTAMemberQuery' => 'PhabricatorQuery', 'PhabricatorMetaMTAPermanentFailureException' => 'Exception', 'PhabricatorMetaMTAReceivedMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAReceivedMailProcessingException' => 'Exception', Index: src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php =================================================================== --- /dev/null +++ src/applications/metamta/query/PhabricatorMetaMTAMemberQuery.php @@ -0,0 +1,69 @@ +viewer = $viewer; + return $this; + } + + public function getViewer() { + return $this->viewer; + } + + public function withPHIDs(array $phids) { + $this->phids = $phids; + return $this; + } + + public function execute() { + $phids = array_fuse($this->phids); + $actors = array(); + $type_map = array(); + foreach ($phids as $phid) { + $type_map[phid_get_type($phid)][] = $phid; + } + + // TODO: Generalize this somewhere else. + + $results = array(); + foreach ($type_map as $type => $phids) { + switch ($type) { + case PhabricatorProjectPHIDTypeProject::TYPECONST: + // TODO: For now, project members are always on the "mailing list" + // implied by the project, but we should differentiate members and + // subscribers (i.e., allow you to unsubscribe from mail about + // a project). + + $projects = id(new PhabricatorProjectQuery()) + ->setViewer($this->getViewer()) + ->needMembers(true) + ->withPHIDs($phids) + ->execute(); + + $projects = mpull($projects, null, 'getPHID'); + foreach ($phids as $phid) { + $project = idx($projects, $phid); + if (!$project) { + $results[$phid] = array(); + } else { + $results[$phid] = $project->getMemberPHIDs(); + } + } + break; + default: + break; + } + } + + return $results; + } + +} Index: src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php =================================================================== --- src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php +++ src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php @@ -198,6 +198,15 @@ } } + // TODO: This is pretty messy. We should really be doing all of this + // multiplexing in the task queue, but that requires significant rewriting + // in the general case. ApplicationTransactions can do it fairly easily, + // but other mail sites currently can not, so we need to support this + // junky version until they catch up and we can swap things over. + + $to_handles = $this->expandRecipientHandles($to_handles); + $cc_handles = $this->expandRecipientHandles($cc_handles); + $tos = mpull($to_handles, null, 'getPHID'); $ccs = mpull($cc_handles, null, 'getPHID'); @@ -219,6 +228,8 @@ $body .= $this->getRecipientsSummary($to_handles, $cc_handles); foreach ($recipients as $phid => $recipient) { + + $mail = clone $mail_template; if (isset($to_handles[$phid])) { $mail->addTos(array($phid)); @@ -332,4 +343,32 @@ return rtrim($body); } + private function expandRecipientHandles(array $handles) { + if (!$handles) { + return array(); + } + + $phids = mpull($handles, 'getPHID'); + $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 id(new PhabricatorHandleQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($results) + ->execute(); + } + } Index: src/applications/metamta/storage/PhabricatorMetaMTAMail.php =================================================================== --- src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -19,6 +19,7 @@ private $excludePHIDs = array(); private $overrideNoSelfMail = false; + private $recipientExpansionMap; public function __construct() { @@ -379,7 +380,8 @@ $mailer->addReplyTo($value, $reply_to_name); break; case 'to': - $to_actors = array_select_keys($deliverable_actors, $value); + $to_phids = $this->expandRecipients($value); + $to_actors = array_select_keys($deliverable_actors, $to_phids); $add_to = array_merge( $add_to, mpull($to_actors, 'getEmailAddress')); @@ -388,7 +390,8 @@ $add_to = array_merge($add_to, $value); break; case 'cc': - $cc_actors = array_select_keys($deliverable_actors, $value); + $cc_phids = $this->expandRecipients($value); + $cc_actors = array_select_keys($deliverable_actors, $cc_phids); $add_cc = array_merge( $add_cc, mpull($cc_actors, 'getEmailAddress')); @@ -685,9 +688,54 @@ $this->getToPHIDs(), $this->getCcPHIDs()); + $this->loadRecipientExpansions($actor_phids); + $actor_phids = $this->expandRecipients($actor_phids); + return $this->loadActors($actor_phids); } + private function loadRecipientExpansions(array $phids) { + $expansions = id(new PhabricatorMetaMTAMemberQuery()) + ->setViewer(PhabricatorUser::getOmnipotentUser()) + ->withPHIDs($phids) + ->execute(); + + $this->recipientExpansionMap = $expansions; + + return $this; + } + + /** + * Expand a list of recipient PHIDs (possibly including aggregate recipients + * like projects) into a deaggregated list of individual recipient PHIDs. + * For example, this will expand project PHIDs into a list of the project's + * members. + * + * @param list List of recipient PHIDs, possibly including aggregate + * recipients. + * @return list Deaggregated list of mailable recipients. + */ + private function expandRecipients(array $phids) { + if ($this->recipientExpansionMap === null) { + throw new Exception( + pht( + 'Call loadRecipientExpansions() before expandRecipients()!')); + } + + $results = array(); + foreach ($phids as $phid) { + if (!isset($this->recipientExpansionMap[$phid])) { + $results[$phid] = $phid; + } else { + foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) { + $results[$recipient_phid] = $recipient_phid; + } + } + } + + return array_keys($results); + } + private function filterDeliverableActors(array $actors) { assert_instances_of($actors, 'PhabricatorMetaMTAActor'); $deliverable_actors = array();