Path: blob/master/src/applications/metamta/replyhandler/PhabricatorMailReplyHandler.php
12256 views
<?php12abstract class PhabricatorMailReplyHandler extends Phobject {34private $mailReceiver;5private $applicationEmail;6private $actor;7private $excludePHIDs = array();8private $unexpandablePHIDs = array();910final public function setMailReceiver($mail_receiver) {11$this->validateMailReceiver($mail_receiver);12$this->mailReceiver = $mail_receiver;13return $this;14}1516final public function getMailReceiver() {17return $this->mailReceiver;18}1920public function setApplicationEmail(21PhabricatorMetaMTAApplicationEmail $email) {22$this->applicationEmail = $email;23return $this;24}2526public function getApplicationEmail() {27return $this->applicationEmail;28}2930final public function setActor(PhabricatorUser $actor) {31$this->actor = $actor;32return $this;33}3435final public function getActor() {36return $this->actor;37}3839final public function setExcludeMailRecipientPHIDs(array $exclude) {40$this->excludePHIDs = $exclude;41return $this;42}4344final public function getExcludeMailRecipientPHIDs() {45return $this->excludePHIDs;46}4748public function setUnexpandablePHIDs(array $phids) {49$this->unexpandablePHIDs = $phids;50return $this;51}5253public function getUnexpandablePHIDs() {54return $this->unexpandablePHIDs;55}5657abstract public function validateMailReceiver($mail_receiver);58abstract public function getPrivateReplyHandlerEmailAddress(59PhabricatorUser $user);6061public function getReplyHandlerDomain() {62return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');63}6465abstract protected function receiveEmail(66PhabricatorMetaMTAReceivedMail $mail);6768public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {69return $this->receiveEmail($mail);70}7172public function supportsPrivateReplies() {73return (bool)$this->getReplyHandlerDomain() &&74!$this->supportsPublicReplies();75}7677public function supportsPublicReplies() {78if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {79return false;80}8182if (!$this->getReplyHandlerDomain()) {83return false;84}8586return (bool)$this->getPublicReplyHandlerEmailAddress();87}8889final public function supportsReplies() {90return $this->supportsPrivateReplies() ||91$this->supportsPublicReplies();92}9394public function getPublicReplyHandlerEmailAddress() {95return null;96}9798protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {99100$receiver = $this->getMailReceiver();101$receiver_id = $receiver->getID();102$domain = $this->getReplyHandlerDomain();103104// We compute a hash using the object's own PHID to prevent an attacker105// from blindly interacting with objects that they haven't ever received106// mail about by just sending to D1@, D2@, etc...107108$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);109110$hash = PhabricatorObjectMailReceiver::computeMailHash(111$mail_key,112$receiver->getPHID());113114$address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";115return $this->getSingleReplyHandlerPrefix($address);116}117118protected function getSingleReplyHandlerPrefix($address) {119$single_handle_prefix = PhabricatorEnv::getEnvConfig(120'metamta.single-reply-handler-prefix');121return ($single_handle_prefix)122? $single_handle_prefix.'+'.$address123: $address;124}125126protected function getDefaultPrivateReplyHandlerEmailAddress(127PhabricatorUser $user,128$prefix) {129130$receiver = $this->getMailReceiver();131$receiver_id = $receiver->getID();132$user_id = $user->getID();133134$mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);135136$hash = PhabricatorObjectMailReceiver::computeMailHash(137$mail_key,138$user->getPHID());139$domain = $this->getReplyHandlerDomain();140141$address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";142return $this->getSingleReplyHandlerPrefix($address);143}144145final protected function enhanceBodyWithAttachments(146$body,147array $attachments) {148149if (!$attachments) {150return $body;151}152153$files = id(new PhabricatorFileQuery())154->setViewer($this->getActor())155->withPHIDs($attachments)156->execute();157158$output = array();159$output[] = $body;160161// We're going to put all the non-images first in a list, then embed162// the images.163$head = array();164$tail = array();165foreach ($files as $file) {166if ($file->isViewableImage()) {167$tail[] = $file;168} else {169$head[] = $file;170}171}172173if ($head) {174$list = array();175foreach ($head as $file) {176$list[] = ' - {'.$file->getMonogram().', layout=link}';177}178$output[] = implode("\n", $list);179}180181if ($tail) {182$list = array();183foreach ($tail as $file) {184$list[] = '{'.$file->getMonogram().'}';185}186$output[] = implode("\n\n", $list);187}188189$output = implode("\n\n", $output);190191return rtrim($output);192}193194195/**196* Produce a list of mail targets for a given to/cc list.197*198* Each target should be sent a separate email, and contains the information199* required to generate it with appropriate permissions and configuration.200*201* @param list<phid> List of "To" PHIDs.202* @param list<phid> List of "CC" PHIDs.203* @return list<PhabricatorMailTarget> List of targets.204*/205final public function getMailTargets(array $raw_to, array $raw_cc) {206list($to, $cc) = $this->expandRecipientPHIDs($raw_to, $raw_cc);207list($to, $cc) = $this->loadRecipientUsers($to, $cc);208list($to, $cc) = $this->filterRecipientUsers($to, $cc);209210if (!$to && !$cc) {211return array();212}213214$template = id(new PhabricatorMailTarget())215->setRawToPHIDs($raw_to)216->setRawCCPHIDs($raw_cc);217218// Set the public reply address as the default, if one exists. We219// might replace this with a private address later.220if ($this->supportsPublicReplies()) {221$reply_to = $this->getPublicReplyHandlerEmailAddress();222if ($reply_to) {223$template->setReplyTo($reply_to);224}225}226227$supports_private_replies = $this->supportsPrivateReplies();228$mail_all = !PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');229$targets = array();230if ($mail_all) {231$target = id(clone $template)232->setViewer(PhabricatorUser::getOmnipotentUser())233->setToMap($to)234->setCCMap($cc);235236$targets[] = $target;237} else {238$map = $to + $cc;239240foreach ($map as $phid => $user) {241// Preserve the original To/Cc information on the target.242if (isset($to[$phid])) {243$target_to = array($phid => $user);244$target_cc = array();245} else {246$target_to = array();247$target_cc = array($phid => $user);248}249250$target = id(clone $template)251->setViewer($user)252->setToMap($target_to)253->setCCMap($target_cc);254255if ($supports_private_replies) {256$reply_to = $this->getPrivateReplyHandlerEmailAddress($user);257if ($reply_to) {258$target->setReplyTo($reply_to);259}260}261262$targets[] = $target;263}264}265266return $targets;267}268269270/**271* Expand lists of recipient PHIDs.272*273* This takes any compound recipients (like projects) and looks up all their274* members.275*276* @param list<phid> List of To PHIDs.277* @param list<phid> List of CC PHIDs.278* @return pair<list<phid>, list<phid>> Expanded PHID lists.279*/280private function expandRecipientPHIDs(array $to, array $cc) {281$to_result = array();282$cc_result = array();283284// "Unexpandable" users have disengaged from an object (for example,285// by resigning from a revision).286287// If such a user is still a direct recipient (for example, they're still288// on the Subscribers list) they're fair game, but group targets (like289// projects) will no longer include them when expanded.290291$unexpandable = $this->getUnexpandablePHIDs();292$unexpandable = array_fuse($unexpandable);293294$all_phids = array_merge($to, $cc);295if ($all_phids) {296$map = id(new PhabricatorMetaMTAMemberQuery())297->setViewer(PhabricatorUser::getOmnipotentUser())298->withPHIDs($all_phids)299->execute();300foreach ($to as $phid) {301foreach ($map[$phid] as $expanded) {302if ($expanded !== $phid) {303if (isset($unexpandable[$expanded])) {304continue;305}306}307$to_result[$expanded] = $expanded;308}309}310foreach ($cc as $phid) {311foreach ($map[$phid] as $expanded) {312if ($expanded !== $phid) {313if (isset($unexpandable[$expanded])) {314continue;315}316}317$cc_result[$expanded] = $expanded;318}319}320}321322// Remove recipients from "CC" if they're also present in "To".323$cc_result = array_diff_key($cc_result, $to_result);324325return array(array_values($to_result), array_values($cc_result));326}327328329/**330* Load @{class:PhabricatorUser} objects for each recipient.331*332* Invalid recipients are dropped from the results.333*334* @param list<phid> List of To PHIDs.335* @param list<phid> List of CC PHIDs.336* @return pair<wild, wild> Maps from PHIDs to users.337*/338private function loadRecipientUsers(array $to, array $cc) {339$to_result = array();340$cc_result = array();341342$all_phids = array_merge($to, $cc);343if ($all_phids) {344// We need user settings here because we'll check translations later345// when generating mail.346$users = id(new PhabricatorPeopleQuery())347->setViewer(PhabricatorUser::getOmnipotentUser())348->withPHIDs($all_phids)349->needUserSettings(true)350->execute();351$users = mpull($users, null, 'getPHID');352353foreach ($to as $phid) {354if (isset($users[$phid])) {355$to_result[$phid] = $users[$phid];356}357}358foreach ($cc as $phid) {359if (isset($users[$phid])) {360$cc_result[$phid] = $users[$phid];361}362}363}364365return array($to_result, $cc_result);366}367368369/**370* Remove recipients who do not have permission to view the mail receiver.371*372* @param map<string, PhabricatorUser> Map of "To" users.373* @param map<string, PhabricatorUser> Map of "CC" users.374* @return pair<wild, wild> Filtered user maps.375*/376private function filterRecipientUsers(array $to, array $cc) {377$to_result = array();378$cc_result = array();379380$all_users = $to + $cc;381if ($all_users) {382$can_see = array();383$object = $this->getMailReceiver();384foreach ($all_users as $phid => $user) {385$visible = PhabricatorPolicyFilter::hasCapability(386$user,387$object,388PhabricatorPolicyCapability::CAN_VIEW);389if ($visible) {390$can_see[$phid] = true;391}392}393394foreach ($to as $phid => $user) {395if (!empty($can_see[$phid])) {396$to_result[$phid] = $all_users[$phid];397}398}399400foreach ($cc as $phid => $user) {401if (!empty($can_see[$phid])) {402$cc_result[$phid] = $all_users[$phid];403}404}405}406407return array($to_result, $cc_result);408}409410}411412413