Correct Aphlict websocket URI construction after PHP8 compatibility changes
[phabricator.git] / src / applications / metamta / storage / PhabricatorMetaMTAMail.php
blobcc3ae82bef2512b294256c97f7413dbe98855ea9
1 <?php
3 /**
4 * @task recipients Managing Recipients
5 */
6 final class PhabricatorMetaMTAMail
7 extends PhabricatorMetaMTADAO
8 implements
9 PhabricatorPolicyInterface,
10 PhabricatorDestructibleInterface {
12 const RETRY_DELAY = 5;
14 protected $actorPHID;
15 protected $parameters = array();
16 protected $status;
17 protected $message;
18 protected $relatedPHID;
20 private $recipientExpansionMap;
21 private $routingMap;
23 public function __construct() {
25 $this->status = PhabricatorMailOutboundStatus::STATUS_QUEUE;
26 $this->parameters = array(
27 'sensitive' => true,
28 'mustEncrypt' => false,
31 parent::__construct();
34 protected function getConfiguration() {
35 return array(
36 self::CONFIG_AUX_PHID => true,
37 self::CONFIG_SERIALIZATION => array(
38 'parameters' => self::SERIALIZATION_JSON,
40 self::CONFIG_COLUMN_SCHEMA => array(
41 'actorPHID' => 'phid?',
42 'status' => 'text32',
43 'relatedPHID' => 'phid?',
45 // T6203/NULLABILITY
46 // This should just be empty if there's no body.
47 'message' => 'text?',
49 self::CONFIG_KEY_SCHEMA => array(
50 'status' => array(
51 'columns' => array('status'),
53 'key_actorPHID' => array(
54 'columns' => array('actorPHID'),
56 'relatedPHID' => array(
57 'columns' => array('relatedPHID'),
59 'key_created' => array(
60 'columns' => array('dateCreated'),
63 ) + parent::getConfiguration();
66 public function generatePHID() {
67 return PhabricatorPHID::generateNewPHID(
68 PhabricatorMetaMTAMailPHIDType::TYPECONST);
71 protected function setParam($param, $value) {
72 $this->parameters[$param] = $value;
73 return $this;
76 protected function getParam($param, $default = null) {
77 // Some old mail was saved without parameters because no parameters were
78 // set or encoding failed. Recover in these cases so we can perform
79 // mail migrations, see T9251.
80 if (!is_array($this->parameters)) {
81 $this->parameters = array();
84 return idx($this->parameters, $param, $default);
87 /**
88 * These tags are used to allow users to opt out of receiving certain types
89 * of mail, like updates when a task's projects change.
91 * @param list<const>
92 * @return this
94 public function setMailTags(array $tags) {
95 $this->setParam('mailtags', array_unique($tags));
96 return $this;
99 public function getMailTags() {
100 return $this->getParam('mailtags', array());
104 * In Gmail, conversations will be broken if you reply to a thread and the
105 * server sends back a response without referencing your Message-ID, even if
106 * it references a Message-ID earlier in the thread. To avoid this, use the
107 * parent email's message ID explicitly if it's available. This overwrites the
108 * "In-Reply-To" and "References" headers we would otherwise generate. This
109 * needs to be set whenever an action is triggered by an email message. See
110 * T251 for more details.
112 * @param string The "Message-ID" of the email which precedes this one.
113 * @return this
115 public function setParentMessageID($id) {
116 $this->setParam('parent-message-id', $id);
117 return $this;
120 public function getParentMessageID() {
121 return $this->getParam('parent-message-id');
124 public function getSubject() {
125 return $this->getParam('subject');
128 public function addTos(array $phids) {
129 $phids = array_unique($phids);
130 $this->setParam('to', $phids);
131 return $this;
134 public function addRawTos(array $raw_email) {
136 // Strip addresses down to bare emails, since the MailAdapter API currently
137 // requires we pass it just the address (like `alincoln@logcabin.org`), not
138 // a full string like `"Abraham Lincoln" <alincoln@logcabin.org>`.
139 foreach ($raw_email as $key => $email) {
140 $object = new PhutilEmailAddress($email);
141 $raw_email[$key] = $object->getAddress();
144 $this->setParam('raw-to', $raw_email);
145 return $this;
148 public function addCCs(array $phids) {
149 $phids = array_unique($phids);
150 $this->setParam('cc', $phids);
151 return $this;
154 public function setExcludeMailRecipientPHIDs(array $exclude) {
155 $this->setParam('exclude', $exclude);
156 return $this;
159 private function getExcludeMailRecipientPHIDs() {
160 return $this->getParam('exclude', array());
163 public function setMutedPHIDs(array $muted) {
164 $this->setParam('muted', $muted);
165 return $this;
168 private function getMutedPHIDs() {
169 return $this->getParam('muted', array());
172 public function setForceHeraldMailRecipientPHIDs(array $force) {
173 $this->setParam('herald-force-recipients', $force);
174 return $this;
177 private function getForceHeraldMailRecipientPHIDs() {
178 return $this->getParam('herald-force-recipients', array());
181 public function addPHIDHeaders($name, array $phids) {
182 $phids = array_unique($phids);
183 foreach ($phids as $phid) {
184 $this->addHeader($name, '<'.$phid.'>');
186 return $this;
189 public function addHeader($name, $value) {
190 $this->parameters['headers'][] = array($name, $value);
191 return $this;
194 public function getHeaders() {
195 return $this->getParam('headers', array());
198 public function addAttachment(PhabricatorMailAttachment $attachment) {
199 $this->parameters['attachments'][] = $attachment->toDictionary();
200 return $this;
203 public function getAttachments() {
204 $dicts = $this->getParam('attachments', array());
206 $result = array();
207 foreach ($dicts as $dict) {
208 $result[] = PhabricatorMailAttachment::newFromDictionary($dict);
210 return $result;
213 public function getAttachmentFilePHIDs() {
214 $file_phids = array();
216 $dictionaries = $this->getParam('attachments');
217 if ($dictionaries) {
218 foreach ($dictionaries as $dictionary) {
219 $file_phid = idx($dictionary, 'filePHID');
220 if ($file_phid) {
221 $file_phids[] = $file_phid;
226 return $file_phids;
229 public function loadAttachedFiles(PhabricatorUser $viewer) {
230 $file_phids = $this->getAttachmentFilePHIDs();
232 if (!$file_phids) {
233 return array();
236 return id(new PhabricatorFileQuery())
237 ->setViewer($viewer)
238 ->withPHIDs($file_phids)
239 ->execute();
242 public function setAttachments(array $attachments) {
243 assert_instances_of($attachments, 'PhabricatorMailAttachment');
244 $this->setParam('attachments', mpull($attachments, 'toDictionary'));
245 return $this;
248 public function setFrom($from) {
249 $this->setParam('from', $from);
250 $this->setActorPHID($from);
251 return $this;
254 public function getFrom() {
255 return $this->getParam('from');
258 public function setRawFrom($raw_email, $raw_name) {
259 $this->setParam('raw-from', array($raw_email, $raw_name));
260 return $this;
263 public function getRawFrom() {
264 return $this->getParam('raw-from');
267 public function setReplyTo($reply_to) {
268 $this->setParam('reply-to', $reply_to);
269 return $this;
272 public function getReplyTo() {
273 return $this->getParam('reply-to');
276 public function setSubject($subject) {
277 $this->setParam('subject', $subject);
278 return $this;
281 public function setSubjectPrefix($prefix) {
282 $this->setParam('subject-prefix', $prefix);
283 return $this;
286 public function getSubjectPrefix() {
287 return $this->getParam('subject-prefix');
290 public function setVarySubjectPrefix($prefix) {
291 $this->setParam('vary-subject-prefix', $prefix);
292 return $this;
295 public function getVarySubjectPrefix() {
296 return $this->getParam('vary-subject-prefix');
299 public function setBody($body) {
300 $this->setParam('body', $body);
301 return $this;
304 public function setSensitiveContent($bool) {
305 $this->setParam('sensitive', $bool);
306 return $this;
309 public function hasSensitiveContent() {
310 return $this->getParam('sensitive', true);
313 public function setMustEncrypt($bool) {
314 return $this->setParam('mustEncrypt', $bool);
317 public function getMustEncrypt() {
318 return $this->getParam('mustEncrypt', false);
321 public function setMustEncryptURI($uri) {
322 return $this->setParam('mustEncrypt.uri', $uri);
325 public function getMustEncryptURI() {
326 return $this->getParam('mustEncrypt.uri');
329 public function setMustEncryptSubject($subject) {
330 return $this->setParam('mustEncrypt.subject', $subject);
333 public function getMustEncryptSubject() {
334 return $this->getParam('mustEncrypt.subject');
337 public function setMustEncryptReasons(array $reasons) {
338 return $this->setParam('mustEncryptReasons', $reasons);
341 public function getMustEncryptReasons() {
342 return $this->getParam('mustEncryptReasons', array());
345 public function setMailStamps(array $stamps) {
346 return $this->setParam('stamps', $stamps);
349 public function getMailStamps() {
350 return $this->getParam('stamps', array());
353 public function setMailStampMetadata($metadata) {
354 return $this->setParam('stampMetadata', $metadata);
357 public function getMailStampMetadata() {
358 return $this->getParam('stampMetadata', array());
361 public function getMailerKey() {
362 return $this->getParam('mailer.key');
365 public function setTryMailers(array $mailers) {
366 return $this->setParam('mailers.try', $mailers);
369 public function setHTMLBody($html) {
370 $this->setParam('html-body', $html);
371 return $this;
374 public function getBody() {
375 return $this->getParam('body');
378 public function getHTMLBody() {
379 return $this->getParam('html-body');
382 public function setIsErrorEmail($is_error) {
383 $this->setParam('is-error', $is_error);
384 return $this;
387 public function getIsErrorEmail() {
388 return $this->getParam('is-error', false);
391 public function getToPHIDs() {
392 return $this->getParam('to', array());
395 public function getRawToAddresses() {
396 return $this->getParam('raw-to', array());
399 public function getCcPHIDs() {
400 return $this->getParam('cc', array());
403 public function setMessageType($message_type) {
404 return $this->setParam('message.type', $message_type);
407 public function getMessageType() {
408 return $this->getParam(
409 'message.type',
410 PhabricatorMailEmailMessage::MESSAGETYPE);
416 * Force delivery of a message, even if recipients have preferences which
417 * would otherwise drop the message.
419 * This is primarily intended to let users who don't want any email still
420 * receive things like password resets.
422 * @param bool True to force delivery despite user preferences.
423 * @return this
425 public function setForceDelivery($force) {
426 $this->setParam('force', $force);
427 return $this;
430 public function getForceDelivery() {
431 return $this->getParam('force', false);
435 * Flag that this is an auto-generated bulk message and should have bulk
436 * headers added to it if appropriate. Broadly, this means some flavor of
437 * "Precedence: bulk" or similar, but is implementation and configuration
438 * dependent.
440 * @param bool True if the mail is automated bulk mail.
441 * @return this
443 public function setIsBulk($is_bulk) {
444 $this->setParam('is-bulk', $is_bulk);
445 return $this;
448 public function getIsBulk() {
449 return $this->getParam('is-bulk');
453 * Use this method to set an ID used for message threading. MetaMTA will
454 * set appropriate headers (Message-ID, In-Reply-To, References and
455 * Thread-Index) based on the capabilities of the underlying mailer.
457 * @param string Unique identifier, appropriate for use in a Message-ID,
458 * In-Reply-To or References headers.
459 * @param bool If true, indicates this is the first message in the thread.
460 * @return this
462 public function setThreadID($thread_id, $is_first_message = false) {
463 $this->setParam('thread-id', $thread_id);
464 $this->setParam('is-first-message', $is_first_message);
465 return $this;
468 public function getThreadID() {
469 return $this->getParam('thread-id');
472 public function getIsFirstMessage() {
473 return (bool)$this->getParam('is-first-message');
477 * Save a newly created mail to the database. The mail will eventually be
478 * delivered by the MetaMTA daemon.
480 * @return this
482 public function saveAndSend() {
483 return $this->save();
487 * @return this
489 public function save() {
490 if ($this->getID()) {
491 return parent::save();
494 // NOTE: When mail is sent from CLI scripts that run tasks in-process, we
495 // may re-enter this method from within scheduleTask(). The implementation
496 // is intended to avoid anything awkward if we end up reentering this
497 // method.
499 $this->openTransaction();
500 // Save to generate a mail ID and PHID.
501 $result = parent::save();
503 // Write the recipient edges.
504 $editor = new PhabricatorEdgeEditor();
505 $edge_type = PhabricatorMetaMTAMailHasRecipientEdgeType::EDGECONST;
506 $recipient_phids = array_merge(
507 $this->getToPHIDs(),
508 $this->getCcPHIDs());
509 $expanded_phids = $this->expandRecipients($recipient_phids);
510 $all_phids = array_unique(array_merge(
511 $recipient_phids,
512 $expanded_phids));
513 foreach ($all_phids as $curr_phid) {
514 $editor->addEdge($this->getPHID(), $edge_type, $curr_phid);
516 $editor->save();
518 $this->saveTransaction();
520 // Queue a task to send this mail.
521 $mailer_task = PhabricatorWorker::scheduleTask(
522 'PhabricatorMetaMTAWorker',
523 $this->getID(),
524 array(
525 'priority' => PhabricatorWorker::PRIORITY_ALERTS,
528 return $result;
532 * Attempt to deliver an email immediately, in this process.
534 * @return void
536 public function sendNow() {
537 if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) {
538 throw new Exception(pht('Trying to send an already-sent mail!'));
541 $mailers = self::newMailers(
542 array(
543 'outbound' => true,
544 'media' => array(
545 $this->getMessageType(),
549 $try_mailers = $this->getParam('mailers.try');
550 if ($try_mailers) {
551 $mailers = mpull($mailers, null, 'getKey');
552 $mailers = array_select_keys($mailers, $try_mailers);
555 return $this->sendWithMailers($mailers);
558 public static function newMailers(array $constraints) {
559 PhutilTypeSpec::checkMap(
560 $constraints,
561 array(
562 'types' => 'optional list<string>',
563 'inbound' => 'optional bool',
564 'outbound' => 'optional bool',
565 'media' => 'optional list<string>',
568 $mailers = array();
570 $config = PhabricatorEnv::getEnvConfig('cluster.mailers');
572 $adapters = PhabricatorMailAdapter::getAllAdapters();
573 $next_priority = -1;
575 foreach ($config as $spec) {
576 $type = $spec['type'];
577 if (!isset($adapters[$type])) {
578 throw new Exception(
579 pht(
580 'Unknown mailer ("%s")!',
581 $type));
584 $key = $spec['key'];
585 $mailer = id(clone $adapters[$type])
586 ->setKey($key);
588 $priority = idx($spec, 'priority');
589 if (!$priority) {
590 $priority = $next_priority;
591 $next_priority--;
593 $mailer->setPriority($priority);
595 $defaults = $mailer->newDefaultOptions();
596 $options = idx($spec, 'options', array()) + $defaults;
597 $mailer->setOptions($options);
599 $mailer->setSupportsInbound(idx($spec, 'inbound', true));
600 $mailer->setSupportsOutbound(idx($spec, 'outbound', true));
602 $media = idx($spec, 'media');
603 if ($media !== null) {
604 $mailer->setMedia($media);
607 $mailers[] = $mailer;
610 // Remove mailers with the wrong types.
611 if (isset($constraints['types'])) {
612 $types = $constraints['types'];
613 $types = array_fuse($types);
614 foreach ($mailers as $key => $mailer) {
615 $mailer_type = $mailer->getAdapterType();
616 if (!isset($types[$mailer_type])) {
617 unset($mailers[$key]);
622 // If we're only looking for inbound mailers, remove mailers with inbound
623 // support disabled.
624 if (!empty($constraints['inbound'])) {
625 foreach ($mailers as $key => $mailer) {
626 if (!$mailer->getSupportsInbound()) {
627 unset($mailers[$key]);
632 // If we're only looking for outbound mailers, remove mailers with outbound
633 // support disabled.
634 if (!empty($constraints['outbound'])) {
635 foreach ($mailers as $key => $mailer) {
636 if (!$mailer->getSupportsOutbound()) {
637 unset($mailers[$key]);
642 // Select only the mailers which can transmit messages with requested media
643 // types.
644 if (!empty($constraints['media'])) {
645 foreach ($mailers as $key => $mailer) {
646 $supports_any = false;
647 foreach ($constraints['media'] as $medium) {
648 if ($mailer->supportsMessageType($medium)) {
649 $supports_any = true;
650 break;
654 if (!$supports_any) {
655 unset($mailers[$key]);
660 $sorted = array();
661 $groups = mgroup($mailers, 'getPriority');
662 krsort($groups);
663 foreach ($groups as $group) {
664 // Reorder services within the same priority group randomly.
665 shuffle($group);
666 foreach ($group as $mailer) {
667 $sorted[] = $mailer;
671 return $sorted;
674 public function sendWithMailers(array $mailers) {
675 if (!$mailers) {
676 $any_mailers = self::newMailers(array());
678 // NOTE: We can end up here with some custom list of "$mailers", like
679 // from a unit test. In that case, this message could be misleading. We
680 // can't really tell if the caller made up the list, so just assume they
681 // aren't tricking us.
683 if ($any_mailers) {
684 $void_message = pht(
685 'No configured mailers support sending outbound mail.');
686 } else {
687 $void_message = pht(
688 'No mailers are configured.');
691 return $this
692 ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
693 ->setMessage($void_message)
694 ->save();
697 $actors = $this->loadAllActors();
699 // If we're sending one mail to everyone, some recipients will be in
700 // "Cc" rather than "To". We'll move them to "To" later (or supply a
701 // dummy "To") but need to look for the recipient in either the
702 // "To" or "Cc" fields here.
703 $target_phid = head($this->getToPHIDs());
704 if (!$target_phid) {
705 $target_phid = head($this->getCcPHIDs());
707 $preferences = $this->loadPreferences($target_phid);
709 // Attach any files we're about to send to this message, so the recipients
710 // can view them.
711 $viewer = PhabricatorUser::getOmnipotentUser();
712 $files = $this->loadAttachedFiles($viewer);
713 foreach ($files as $file) {
714 $file->attachToObject($this->getPHID());
717 $type_map = PhabricatorMailExternalMessage::getAllMessageTypes();
718 $type = idx($type_map, $this->getMessageType());
719 if (!$type) {
720 throw new Exception(
721 pht(
722 'Unable to send message with unknown message type "%s".',
723 $type));
726 $exceptions = array();
727 foreach ($mailers as $mailer) {
728 try {
729 $message = $type->newMailMessageEngine()
730 ->setMailer($mailer)
731 ->setMail($this)
732 ->setActors($actors)
733 ->setPreferences($preferences)
734 ->newMessage($mailer);
735 } catch (Exception $ex) {
736 $exceptions[] = $ex;
737 continue;
740 if (!$message) {
741 // If we don't get a message back, that means the mail doesn't actually
742 // need to be sent (for example, because recipients have declined to
743 // receive the mail). Void it and return.
744 return $this
745 ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID)
746 ->save();
749 try {
750 $mailer->sendMessage($message);
751 } catch (PhabricatorMetaMTAPermanentFailureException $ex) {
752 // If any mailer raises a permanent failure, stop trying to send the
753 // mail with other mailers.
754 $this
755 ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL)
756 ->setMessage($ex->getMessage())
757 ->save();
759 throw $ex;
760 } catch (Exception $ex) {
761 $exceptions[] = $ex;
762 continue;
765 // Keep track of which mailer actually ended up accepting the message.
766 $mailer_key = $mailer->getKey();
767 if ($mailer_key !== null) {
768 $this->setParam('mailer.key', $mailer_key);
771 // Now that we sent the message, store the final deliverability outcomes
772 // and reasoning so we can explain why things happened the way they did.
773 $actor_list = array();
774 foreach ($actors as $actor) {
775 $actor_list[$actor->getPHID()] = array(
776 'deliverable' => $actor->isDeliverable(),
777 'reasons' => $actor->getDeliverabilityReasons(),
780 $this->setParam('actors.sent', $actor_list);
781 $this->setParam('routing.sent', $this->getParam('routing'));
782 $this->setParam('routingmap.sent', $this->getRoutingRuleMap());
784 return $this
785 ->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT)
786 ->save();
789 // If we make it here, no mailer could send the mail but no mailer failed
790 // permanently either. We update the error message for the mail, but leave
791 // it in the current status (usually, STATUS_QUEUE) and try again later.
793 $messages = array();
794 foreach ($exceptions as $ex) {
795 $messages[] = $ex->getMessage();
797 $messages = implode("\n\n", $messages);
799 $this
800 ->setMessage($messages)
801 ->save();
803 if (count($exceptions) === 1) {
804 throw head($exceptions);
807 throw new PhutilAggregateException(
808 pht('Encountered multiple exceptions while transmitting mail.'),
809 $exceptions);
813 public static function shouldMailEachRecipient() {
814 return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient');
818 /* -( Managing Recipients )------------------------------------------------ */
822 * Get all of the recipients for this mail, after preference filters are
823 * applied. This list has all objects to whom delivery will be attempted.
825 * Note that this expands recipients into their members, because delivery
826 * is never directly attempted to aggregate actors like projects.
828 * @return list<phid> A list of all recipients to whom delivery will be
829 * attempted.
830 * @task recipients
832 public function buildRecipientList() {
833 $actors = $this->loadAllActors();
834 $actors = $this->filterDeliverableActors($actors);
835 return mpull($actors, 'getPHID');
838 public function loadAllActors() {
839 $actor_phids = $this->getExpandedRecipientPHIDs();
840 return $this->loadActors($actor_phids);
843 public function getExpandedRecipientPHIDs() {
844 $actor_phids = $this->getAllActorPHIDs();
845 return $this->expandRecipients($actor_phids);
848 private function getAllActorPHIDs() {
849 return array_merge(
850 array($this->getParam('from')),
851 $this->getToPHIDs(),
852 $this->getCcPHIDs());
856 * Expand a list of recipient PHIDs (possibly including aggregate recipients
857 * like projects) into a deaggregated list of individual recipient PHIDs.
858 * For example, this will expand project PHIDs into a list of the project's
859 * members.
861 * @param list<phid> List of recipient PHIDs, possibly including aggregate
862 * recipients.
863 * @return list<phid> Deaggregated list of mailable recipients.
865 public function expandRecipients(array $phids) {
866 if ($this->recipientExpansionMap === null) {
867 $all_phids = $this->getAllActorPHIDs();
868 $this->recipientExpansionMap = id(new PhabricatorMetaMTAMemberQuery())
869 ->setViewer(PhabricatorUser::getOmnipotentUser())
870 ->withPHIDs($all_phids)
871 ->execute();
874 $results = array();
875 foreach ($phids as $phid) {
876 foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) {
877 $results[$recipient_phid] = $recipient_phid;
881 return array_keys($results);
884 private function filterDeliverableActors(array $actors) {
885 assert_instances_of($actors, 'PhabricatorMetaMTAActor');
886 $deliverable_actors = array();
887 foreach ($actors as $phid => $actor) {
888 if ($actor->isDeliverable()) {
889 $deliverable_actors[$phid] = $actor;
892 return $deliverable_actors;
895 private function loadActors(array $actor_phids) {
896 $actor_phids = array_filter($actor_phids);
897 $viewer = PhabricatorUser::getOmnipotentUser();
899 $actors = id(new PhabricatorMetaMTAActorQuery())
900 ->setViewer($viewer)
901 ->withPHIDs($actor_phids)
902 ->execute();
904 if (!$actors) {
905 return array();
908 if ($this->getForceDelivery()) {
909 // If we're forcing delivery, skip all the opt-out checks. We don't
910 // bother annotating reasoning on the mail in this case because it should
911 // always be obvious why the mail hit this rule (e.g., it is a password
912 // reset mail).
913 foreach ($actors as $actor) {
914 $actor->setDeliverable(PhabricatorMetaMTAActor::REASON_FORCE);
916 return $actors;
919 // Exclude explicit recipients.
920 foreach ($this->getExcludeMailRecipientPHIDs() as $phid) {
921 $actor = idx($actors, $phid);
922 if (!$actor) {
923 continue;
925 $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_RESPONSE);
928 // Before running more rules, save a list of the actors who were
929 // deliverable before we started running preference-based rules. This stops
930 // us from trying to send mail to disabled users just because a Herald rule
931 // added them, for example.
932 $deliverable = array();
933 foreach ($actors as $phid => $actor) {
934 if ($actor->isDeliverable()) {
935 $deliverable[] = $phid;
939 // Exclude muted recipients. We're doing this after saving deliverability
940 // so that Herald "Send me an email" actions can still punch through a
941 // mute.
943 foreach ($this->getMutedPHIDs() as $muted_phid) {
944 $muted_actor = idx($actors, $muted_phid);
945 if (!$muted_actor) {
946 continue;
948 $muted_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_MUTED);
951 // For the rest of the rules, order matters. We're going to run all the
952 // possible rules in order from weakest to strongest, and let the strongest
953 // matching rule win. The weaker rules leave annotations behind which help
954 // users understand why the mail was routed the way it was.
956 // Exclude the actor if their preferences are set.
957 $from_phid = $this->getParam('from');
958 $from_actor = idx($actors, $from_phid);
959 if ($from_actor) {
960 $from_user = id(new PhabricatorPeopleQuery())
961 ->setViewer($viewer)
962 ->withPHIDs(array($from_phid))
963 ->needUserSettings(true)
964 ->execute();
965 $from_user = head($from_user);
966 if ($from_user) {
967 $pref_key = PhabricatorEmailSelfActionsSetting::SETTINGKEY;
968 $exclude_self = $from_user->getUserSetting($pref_key);
969 if ($exclude_self) {
970 $from_actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_SELF);
975 $all_prefs = id(new PhabricatorUserPreferencesQuery())
976 ->setViewer(PhabricatorUser::getOmnipotentUser())
977 ->withUserPHIDs($actor_phids)
978 ->needSyntheticPreferences(true)
979 ->execute();
980 $all_prefs = mpull($all_prefs, null, 'getUserPHID');
982 $value_email = PhabricatorEmailTagsSetting::VALUE_EMAIL;
984 // Exclude all recipients who have set preferences to not receive this type
985 // of email (for example, a user who says they don't want emails about task
986 // CC changes).
987 $tags = $this->getParam('mailtags');
988 if ($tags) {
989 foreach ($all_prefs as $phid => $prefs) {
990 $user_mailtags = $prefs->getSettingValue(
991 PhabricatorEmailTagsSetting::SETTINGKEY);
993 // The user must have elected to receive mail for at least one
994 // of the mailtags.
995 $send = false;
996 foreach ($tags as $tag) {
997 if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) {
998 $send = true;
999 break;
1003 if (!$send) {
1004 $actors[$phid]->setUndeliverable(
1005 PhabricatorMetaMTAActor::REASON_MAILTAGS);
1010 foreach ($deliverable as $phid) {
1011 switch ($this->getRoutingRule($phid)) {
1012 case PhabricatorMailRoutingRule::ROUTE_AS_NOTIFICATION:
1013 $actors[$phid]->setUndeliverable(
1014 PhabricatorMetaMTAActor::REASON_ROUTE_AS_NOTIFICATION);
1015 break;
1016 case PhabricatorMailRoutingRule::ROUTE_AS_MAIL:
1017 $actors[$phid]->setDeliverable(
1018 PhabricatorMetaMTAActor::REASON_ROUTE_AS_MAIL);
1019 break;
1020 default:
1021 // No change.
1022 break;
1026 // If recipients were initially deliverable and were added by "Send me an
1027 // email" Herald rules, annotate them as such and make them deliverable
1028 // again, overriding any changes made by the "self mail" and "mail tags"
1029 // settings.
1030 $force_recipients = $this->getForceHeraldMailRecipientPHIDs();
1031 $force_recipients = array_fuse($force_recipients);
1032 if ($force_recipients) {
1033 foreach ($deliverable as $phid) {
1034 if (isset($force_recipients[$phid])) {
1035 $actors[$phid]->setDeliverable(
1036 PhabricatorMetaMTAActor::REASON_FORCE_HERALD);
1041 // Exclude recipients who don't want any mail. This rule is very strong
1042 // and runs last.
1043 foreach ($all_prefs as $phid => $prefs) {
1044 $exclude = $prefs->getSettingValue(
1045 PhabricatorEmailNotificationsSetting::SETTINGKEY);
1046 if ($exclude) {
1047 $actors[$phid]->setUndeliverable(
1048 PhabricatorMetaMTAActor::REASON_MAIL_DISABLED);
1052 // Unless delivery was forced earlier (password resets, confirmation mail),
1053 // never send mail to unverified addresses.
1054 foreach ($actors as $phid => $actor) {
1055 if ($actor->getIsVerified()) {
1056 continue;
1059 $actor->setUndeliverable(PhabricatorMetaMTAActor::REASON_UNVERIFIED);
1062 return $actors;
1065 public function getDeliveredHeaders() {
1066 return $this->getParam('headers.sent');
1069 public function setDeliveredHeaders(array $headers) {
1070 $headers = $this->flattenHeaders($headers);
1071 return $this->setParam('headers.sent', $headers);
1074 public function getUnfilteredHeaders() {
1075 $unfiltered = $this->getParam('headers.unfiltered');
1077 if ($unfiltered === null) {
1078 // Older versions of Phabricator did not filter headers, and thus did
1079 // not record unfiltered headers. If we don't have unfiltered header
1080 // data just return the delivered headers for compatibility.
1081 return $this->getDeliveredHeaders();
1084 return $unfiltered;
1087 public function setUnfilteredHeaders(array $headers) {
1088 $headers = $this->flattenHeaders($headers);
1089 return $this->setParam('headers.unfiltered', $headers);
1092 private function flattenHeaders(array $headers) {
1093 assert_instances_of($headers, 'PhabricatorMailHeader');
1095 $list = array();
1096 foreach ($list as $header) {
1097 $list[] = array(
1098 $header->getName(),
1099 $header->getValue(),
1103 return $list;
1106 public function getDeliveredActors() {
1107 return $this->getParam('actors.sent');
1110 public function getDeliveredRoutingRules() {
1111 return $this->getParam('routing.sent');
1114 public function getDeliveredRoutingMap() {
1115 return $this->getParam('routingmap.sent');
1118 public function getDeliveredBody() {
1119 return $this->getParam('body.sent');
1122 public function setDeliveredBody($body) {
1123 return $this->setParam('body.sent', $body);
1126 public function getURI() {
1127 return '/mail/detail/'.$this->getID().'/';
1131 /* -( Routing )------------------------------------------------------------ */
1134 public function addRoutingRule($routing_rule, $phids, $reason_phid) {
1135 $routing = $this->getParam('routing', array());
1136 $routing[] = array(
1137 'routingRule' => $routing_rule,
1138 'phids' => $phids,
1139 'reasonPHID' => $reason_phid,
1141 $this->setParam('routing', $routing);
1143 // Throw the routing map away so we rebuild it.
1144 $this->routingMap = null;
1146 return $this;
1149 private function getRoutingRule($phid) {
1150 $map = $this->getRoutingRuleMap();
1152 $info = idx($map, $phid, idx($map, 'default'));
1153 if ($info) {
1154 return idx($info, 'rule');
1157 return null;
1160 private function getRoutingRuleMap() {
1161 if ($this->routingMap === null) {
1162 $map = array();
1164 $routing = $this->getParam('routing', array());
1165 foreach ($routing as $route) {
1166 $phids = $route['phids'];
1167 if ($phids === null) {
1168 $phids = array('default');
1171 foreach ($phids as $phid) {
1172 $new_rule = $route['routingRule'];
1174 $current_rule = idx($map, $phid);
1175 if ($current_rule === null) {
1176 $is_stronger = true;
1177 } else {
1178 $is_stronger = PhabricatorMailRoutingRule::isStrongerThan(
1179 $new_rule,
1180 $current_rule);
1183 if ($is_stronger) {
1184 $map[$phid] = array(
1185 'rule' => $new_rule,
1186 'reason' => $route['reasonPHID'],
1192 $this->routingMap = $map;
1195 return $this->routingMap;
1198 /* -( Preferences )-------------------------------------------------------- */
1201 private function loadPreferences($target_phid) {
1202 $viewer = PhabricatorUser::getOmnipotentUser();
1204 if (self::shouldMailEachRecipient()) {
1205 $preferences = id(new PhabricatorUserPreferencesQuery())
1206 ->setViewer($viewer)
1207 ->withUserPHIDs(array($target_phid))
1208 ->needSyntheticPreferences(true)
1209 ->executeOne();
1210 if ($preferences) {
1211 return $preferences;
1215 return PhabricatorUserPreferences::loadGlobalPreferences($viewer);
1218 public function shouldRenderMailStampsInBody($viewer) {
1219 $preferences = $this->loadPreferences($viewer->getPHID());
1220 $value = $preferences->getSettingValue(
1221 PhabricatorEmailStampsSetting::SETTINGKEY);
1223 return ($value == PhabricatorEmailStampsSetting::VALUE_BODY_STAMPS);
1227 /* -( PhabricatorPolicyInterface )----------------------------------------- */
1230 public function getCapabilities() {
1231 return array(
1232 PhabricatorPolicyCapability::CAN_VIEW,
1236 public function getPolicy($capability) {
1237 return PhabricatorPolicies::POLICY_NOONE;
1240 public function hasAutomaticCapability($capability, PhabricatorUser $viewer) {
1241 $actor_phids = $this->getExpandedRecipientPHIDs();
1242 return in_array($viewer->getPHID(), $actor_phids);
1245 public function describeAutomaticCapability($capability) {
1246 return pht(
1247 'The mail sender and message recipients can always see the mail.');
1251 /* -( PhabricatorDestructibleInterface )----------------------------------- */
1254 public function destroyObjectPermanently(
1255 PhabricatorDestructionEngine $engine) {
1257 $files = $this->loadAttachedFiles($engine->getViewer());
1258 foreach ($files as $file) {
1259 $engine->destroyObject($file);
1262 $this->delete();