3 abstract class PhabricatorMailReplyHandler
extends Phobject
{
6 private $applicationEmail;
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;
17 final public function getMailReceiver() {
18 return $this->mailReceiver
;
21 public function setApplicationEmail(
22 PhabricatorMetaMTAApplicationEmail
$email) {
23 $this->applicationEmail
= $email;
27 public function getApplicationEmail() {
28 return $this->applicationEmail
;
31 final public function setActor(PhabricatorUser
$actor) {
32 $this->actor
= $actor;
36 final public function getActor() {
40 final public function setExcludeMailRecipientPHIDs(array $exclude) {
41 $this->excludePHIDs
= $exclude;
45 final public function getExcludeMailRecipientPHIDs() {
46 return $this->excludePHIDs
;
49 public function setUnexpandablePHIDs(array $phids) {
50 $this->unexpandablePHIDs
= $phids;
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')) {
83 if (!$this->getReplyHandlerDomain()) {
87 return (bool)$this->getPublicReplyHandlerEmailAddress();
90 final public function supportsReplies() {
91 return $this->supportsPrivateReplies() ||
92 $this->supportsPublicReplies();
95 public function getPublicReplyHandlerEmailAddress() {
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(
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
127 protected function getDefaultPrivateReplyHandlerEmailAddress(
128 PhabricatorUser
$user,
131 $receiver = $this->getMailReceiver();
132 $receiver_id = $receiver->getID();
133 $user_id = $user->getID();
135 $mail_key = PhabricatorMetaMTAMailProperties
::loadMailKey($receiver);
137 $hash = PhabricatorObjectMailReceiver
::computeMailHash(
140 $domain = $this->getReplyHandlerDomain();
142 $address = "{$prefix}{$receiver_id}+{$user_id}+{$hash}@{$domain}";
143 return $this->getSingleReplyHandlerPrefix($address);
146 final protected function enhanceBodyWithAttachments(
148 array $attachments) {
154 $files = id(new PhabricatorFileQuery())
155 ->setViewer($this->getActor())
156 ->withPHIDs($attachments)
162 // We're going to put all the non-images first in a list, then embed
166 foreach ($files as $file) {
167 if ($file->isViewableImage()) {
176 foreach ($head as $file) {
177 $list[] = ' - {'.$file->getMonogram().', layout=link}';
179 $output[] = implode("\n", $list);
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);
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();
224 $template->setReplyTo($reply_to);
228 $supports_private_replies = $this->supportsPrivateReplies();
229 $mail_all = !PhabricatorEnv
::getEnvConfig('metamta.one-mail-per-recipient');
232 $target = id(clone $template)
233 ->setViewer(PhabricatorUser
::getOmnipotentUser())
237 $targets[] = $target;
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();
247 $target_to = array();
248 $target_cc = array($phid => $user);
251 $target = id(clone $template)
253 ->setToMap($target_to)
254 ->setCCMap($target_cc);
256 if ($supports_private_replies) {
257 $reply_to = $this->getPrivateReplyHandlerEmailAddress($user);
259 $target->setReplyTo($reply_to);
263 $targets[] = $target;
272 * Expand lists of recipient PHIDs.
274 * This takes any compound recipients (like projects) and looks up all their
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);
297 $map = id(new PhabricatorMetaMTAMemberQuery())
298 ->setViewer(PhabricatorUser
::getOmnipotentUser())
299 ->withPHIDs($all_phids)
301 foreach ($to as $phid) {
302 foreach ($map[$phid] as $expanded) {
303 if ($expanded !== $phid) {
304 if (isset($unexpandable[$expanded])) {
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])) {
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);
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)
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;
384 $object = $this->getMailReceiver();
385 foreach ($all_users as $phid => $user) {
386 $visible = PhabricatorPolicyFilter
::hasCapability(
389 PhabricatorPolicyCapability
::CAN_VIEW
);
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);