Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / metamta / replyhandler / PhabricatorMailReplyHandler.php
blob72e23ec2c18c6873c775820574ed6c531b047cca
1 <?php
3 abstract class PhabricatorMailReplyHandler extends Phobject {
5 private $mailReceiver;
6 private $applicationEmail;
7 private $actor;
8 private $excludePHIDs = array();
9 private $unexpandablePHIDs = array();
11 final public function setMailReceiver($mail_receiver) {
12 $this->validateMailReceiver($mail_receiver);
13 $this->mailReceiver = $mail_receiver;
14 return $this;
17 final public function getMailReceiver() {
18 return $this->mailReceiver;
21 public function setApplicationEmail(
22 PhabricatorMetaMTAApplicationEmail $email) {
23 $this->applicationEmail = $email;
24 return $this;
27 public function getApplicationEmail() {
28 return $this->applicationEmail;
31 final public function setActor(PhabricatorUser $actor) {
32 $this->actor = $actor;
33 return $this;
36 final public function getActor() {
37 return $this->actor;
40 final public function setExcludeMailRecipientPHIDs(array $exclude) {
41 $this->excludePHIDs = $exclude;
42 return $this;
45 final public function getExcludeMailRecipientPHIDs() {
46 return $this->excludePHIDs;
49 public function setUnexpandablePHIDs(array $phids) {
50 $this->unexpandablePHIDs = $phids;
51 return $this;
54 public function getUnexpandablePHIDs() {
55 return $this->unexpandablePHIDs;
58 abstract public function validateMailReceiver($mail_receiver);
59 abstract public function getPrivateReplyHandlerEmailAddress(
60 PhabricatorUser $user);
62 public function getReplyHandlerDomain() {
63 return PhabricatorEnv::getEnvConfig('metamta.reply-handler-domain');
66 abstract protected function receiveEmail(
67 PhabricatorMetaMTAReceivedMail $mail);
69 public function processEmail(PhabricatorMetaMTAReceivedMail $mail) {
70 return $this->receiveEmail($mail);
73 public function supportsPrivateReplies() {
74 return (bool)$this->getReplyHandlerDomain() &&
75 !$this->supportsPublicReplies();
78 public function supportsPublicReplies() {
79 if (!PhabricatorEnv::getEnvConfig('metamta.public-replies')) {
80 return false;
83 if (!$this->getReplyHandlerDomain()) {
84 return false;
87 return (bool)$this->getPublicReplyHandlerEmailAddress();
90 final public function supportsReplies() {
91 return $this->supportsPrivateReplies() ||
92 $this->supportsPublicReplies();
95 public function getPublicReplyHandlerEmailAddress() {
96 return null;
99 protected function getDefaultPublicReplyHandlerEmailAddress($prefix) {
101 $receiver = $this->getMailReceiver();
102 $receiver_id = $receiver->getID();
103 $domain = $this->getReplyHandlerDomain();
105 // We compute a hash using the object's own PHID to prevent an attacker
106 // from blindly interacting with objects that they haven't ever received
107 // mail about by just sending to D1@, D2@, etc...
109 $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);
111 $hash = PhabricatorObjectMailReceiver::computeMailHash(
112 $mail_key,
113 $receiver->getPHID());
115 $address = "{$prefix}{$receiver_id}+public+{$hash}@{$domain}";
116 return $this->getSingleReplyHandlerPrefix($address);
119 protected function getSingleReplyHandlerPrefix($address) {
120 $single_handle_prefix = PhabricatorEnv::getEnvConfig(
121 'metamta.single-reply-handler-prefix');
122 return ($single_handle_prefix)
123 ? $single_handle_prefix.'+'.$address
124 : $address;
127 protected function getDefaultPrivateReplyHandlerEmailAddress(
128 PhabricatorUser $user,
129 $prefix) {
131 $receiver = $this->getMailReceiver();
132 $receiver_id = $receiver->getID();
133 $user_id = $user->getID();
135 $mail_key = PhabricatorMetaMTAMailProperties::loadMailKey($receiver);
137 $hash = PhabricatorObjectMailReceiver::computeMailHash(
138 $mail_key,
139 $user->getPHID());
140 $domain = $this->getReplyHandlerDomain();
142 $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
143 return $this->getSingleReplyHandlerPrefix($address);
146 final protected function enhanceBodyWithAttachments(
147 $body,
148 array $attachments) {
150 if (!$attachments) {
151 return $body;
154 $files = id(new PhabricatorFileQuery())
155 ->setViewer($this->getActor())
156 ->withPHIDs($attachments)
157 ->execute();
159 $output = array();
160 $output[] = $body;
162 // We're going to put all the non-images first in a list, then embed
163 // the images.
164 $head = array();
165 $tail = array();
166 foreach ($files as $file) {
167 if ($file->isViewableImage()) {
168 $tail[] = $file;
169 } else {
170 $head[] = $file;
174 if ($head) {
175 $list = array();
176 foreach ($head as $file) {
177 $list[] = ' - {'.$file->getMonogram().', layout=link}';
179 $output[] = implode("\n", $list);
182 if ($tail) {
183 $list = array();
184 foreach ($tail as $file) {
185 $list[] = '{'.$file->getMonogram().'}';
187 $output[] = implode("\n\n", $list);
190 $output = implode("\n\n", $output);
192 return rtrim($output);
197 * Produce a list of mail targets for a given to/cc list.
199 * Each target should be sent a separate email, and contains the information
200 * required to generate it with appropriate permissions and configuration.
202 * @param list<phid> List of "To" PHIDs.
203 * @param list<phid> List of "CC" PHIDs.
204 * @return list<PhabricatorMailTarget> List of targets.
206 final public function getMailTargets(array $raw_to, array $raw_cc) {
207 list($to, $cc) = $this->expandRecipientPHIDs($raw_to, $raw_cc);
208 list($to, $cc) = $this->loadRecipientUsers($to, $cc);
209 list($to, $cc) = $this->filterRecipientUsers($to, $cc);
211 if (!$to && !$cc) {
212 return array();
215 $template = id(new PhabricatorMailTarget())
216 ->setRawToPHIDs($raw_to)
217 ->setRawCCPHIDs($raw_cc);
219 // Set the public reply address as the default, if one exists. We
220 // might replace this with a private address later.
221 if ($this->supportsPublicReplies()) {
222 $reply_to = $this->getPublicReplyHandlerEmailAddress();
223 if ($reply_to) {
224 $template->setReplyTo($reply_to);
228 $supports_private_replies = $this->supportsPrivateReplies();
229 $mail_all = !PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
230 $targets = array();
231 if ($mail_all) {
232 $target = id(clone $template)
233 ->setViewer(PhabricatorUser::getOmnipotentUser())
234 ->setToMap($to)
235 ->setCCMap($cc);
237 $targets[] = $target;
238 } else {
239 $map = $to + $cc;
241 foreach ($map as $phid => $user) {
242 // Preserve the original To/Cc information on the target.
243 if (isset($to[$phid])) {
244 $target_to = array($phid => $user);
245 $target_cc = array();
246 } else {
247 $target_to = array();
248 $target_cc = array($phid => $user);
251 $target = id(clone $template)
252 ->setViewer($user)
253 ->setToMap($target_to)
254 ->setCCMap($target_cc);
256 if ($supports_private_replies) {
257 $reply_to = $this->getPrivateReplyHandlerEmailAddress($user);
258 if ($reply_to) {
259 $target->setReplyTo($reply_to);
263 $targets[] = $target;
267 return $targets;
272 * Expand lists of recipient PHIDs.
274 * This takes any compound recipients (like projects) and looks up all their
275 * members.
277 * @param list<phid> List of To PHIDs.
278 * @param list<phid> List of CC PHIDs.
279 * @return pair<list<phid>, list<phid>> Expanded PHID lists.
281 private function expandRecipientPHIDs(array $to, array $cc) {
282 $to_result = array();
283 $cc_result = array();
285 // "Unexpandable" users have disengaged from an object (for example,
286 // by resigning from a revision).
288 // If such a user is still a direct recipient (for example, they're still
289 // on the Subscribers list) they're fair game, but group targets (like
290 // projects) will no longer include them when expanded.
292 $unexpandable = $this->getUnexpandablePHIDs();
293 $unexpandable = array_fuse($unexpandable);
295 $all_phids = array_merge($to, $cc);
296 if ($all_phids) {
297 $map = id(new PhabricatorMetaMTAMemberQuery())
298 ->setViewer(PhabricatorUser::getOmnipotentUser())
299 ->withPHIDs($all_phids)
300 ->execute();
301 foreach ($to as $phid) {
302 foreach ($map[$phid] as $expanded) {
303 if ($expanded !== $phid) {
304 if (isset($unexpandable[$expanded])) {
305 continue;
308 $to_result[$expanded] = $expanded;
311 foreach ($cc as $phid) {
312 foreach ($map[$phid] as $expanded) {
313 if ($expanded !== $phid) {
314 if (isset($unexpandable[$expanded])) {
315 continue;
318 $cc_result[$expanded] = $expanded;
323 // Remove recipients from "CC" if they're also present in "To".
324 $cc_result = array_diff_key($cc_result, $to_result);
326 return array(array_values($to_result), array_values($cc_result));
331 * Load @{class:PhabricatorUser} objects for each recipient.
333 * Invalid recipients are dropped from the results.
335 * @param list<phid> List of To PHIDs.
336 * @param list<phid> List of CC PHIDs.
337 * @return pair<wild, wild> Maps from PHIDs to users.
339 private function loadRecipientUsers(array $to, array $cc) {
340 $to_result = array();
341 $cc_result = array();
343 $all_phids = array_merge($to, $cc);
344 if ($all_phids) {
345 // We need user settings here because we'll check translations later
346 // when generating mail.
347 $users = id(new PhabricatorPeopleQuery())
348 ->setViewer(PhabricatorUser::getOmnipotentUser())
349 ->withPHIDs($all_phids)
350 ->needUserSettings(true)
351 ->execute();
352 $users = mpull($users, null, 'getPHID');
354 foreach ($to as $phid) {
355 if (isset($users[$phid])) {
356 $to_result[$phid] = $users[$phid];
359 foreach ($cc as $phid) {
360 if (isset($users[$phid])) {
361 $cc_result[$phid] = $users[$phid];
366 return array($to_result, $cc_result);
371 * Remove recipients who do not have permission to view the mail receiver.
373 * @param map<string, PhabricatorUser> Map of "To" users.
374 * @param map<string, PhabricatorUser> Map of "CC" users.
375 * @return pair<wild, wild> Filtered user maps.
377 private function filterRecipientUsers(array $to, array $cc) {
378 $to_result = array();
379 $cc_result = array();
381 $all_users = $to + $cc;
382 if ($all_users) {
383 $can_see = array();
384 $object = $this->getMailReceiver();
385 foreach ($all_users as $phid => $user) {
386 $visible = PhabricatorPolicyFilter::hasCapability(
387 $user,
388 $object,
389 PhabricatorPolicyCapability::CAN_VIEW);
390 if ($visible) {
391 $can_see[$phid] = true;
395 foreach ($to as $phid => $user) {
396 if (!empty($can_see[$phid])) {
397 $to_result[$phid] = $all_users[$phid];
401 foreach ($cc as $phid => $user) {
402 if (!empty($can_see[$phid])) {
403 $cc_result[$phid] = $all_users[$phid];
408 return array($to_result, $cc_result);