[ Index ] |
PHP Cross Reference of Phabricator |
[Summary view] [Print] [Text view]
1 <?php 2 3 /** 4 * @task recipients Managing Recipients 5 */ 6 final class PhabricatorMetaMTAMail extends PhabricatorMetaMTADAO { 7 8 const STATUS_QUEUE = 'queued'; 9 const STATUS_SENT = 'sent'; 10 const STATUS_FAIL = 'fail'; 11 const STATUS_VOID = 'void'; 12 13 const RETRY_DELAY = 5; 14 15 protected $parameters; 16 protected $status; 17 protected $message; 18 protected $relatedPHID; 19 20 private $recipientExpansionMap; 21 22 public function __construct() { 23 24 $this->status = self::STATUS_QUEUE; 25 $this->parameters = array(); 26 27 parent::__construct(); 28 } 29 30 public function getConfiguration() { 31 return array( 32 self::CONFIG_SERIALIZATION => array( 33 'parameters' => self::SERIALIZATION_JSON, 34 ), 35 self::CONFIG_COLUMN_SCHEMA => array( 36 'status' => 'text32', 37 'relatedPHID' => 'phid?', 38 39 // T6203/NULLABILITY 40 // This should just be empty if there's no body. 41 'message' => 'text?', 42 ), 43 self::CONFIG_KEY_SCHEMA => array( 44 'status' => array( 45 'columns' => array('status'), 46 ), 47 'relatedPHID' => array( 48 'columns' => array('relatedPHID'), 49 ), 50 'key_created' => array( 51 'columns' => array('dateCreated'), 52 ), 53 ), 54 ) + parent::getConfiguration(); 55 } 56 57 protected function setParam($param, $value) { 58 $this->parameters[$param] = $value; 59 return $this; 60 } 61 62 protected function getParam($param, $default = null) { 63 return idx($this->parameters, $param, $default); 64 } 65 66 /** 67 * Set tags (@{class:MetaMTANotificationType} constants) which identify the 68 * content of this mail in a general way. These tags are used to allow users 69 * to opt out of receiving certain types of mail, like updates when a task's 70 * projects change. 71 * 72 * @param list<const> List of @{class:MetaMTANotificationType} constants. 73 * @return this 74 */ 75 public function setMailTags(array $tags) { 76 $this->setParam('mailtags', array_unique($tags)); 77 return $this; 78 } 79 80 public function getMailTags() { 81 return $this->getParam('mailtags', array()); 82 } 83 84 /** 85 * In Gmail, conversations will be broken if you reply to a thread and the 86 * server sends back a response without referencing your Message-ID, even if 87 * it references a Message-ID earlier in the thread. To avoid this, use the 88 * parent email's message ID explicitly if it's available. This overwrites the 89 * "In-Reply-To" and "References" headers we would otherwise generate. This 90 * needs to be set whenever an action is triggered by an email message. See 91 * T251 for more details. 92 * 93 * @param string The "Message-ID" of the email which precedes this one. 94 * @return this 95 */ 96 public function setParentMessageID($id) { 97 $this->setParam('parent-message-id', $id); 98 return $this; 99 } 100 101 public function getParentMessageID() { 102 return $this->getParam('parent-message-id'); 103 } 104 105 public function getSubject() { 106 return $this->getParam('subject'); 107 } 108 109 public function addTos(array $phids) { 110 $phids = array_unique($phids); 111 $this->setParam('to', $phids); 112 return $this; 113 } 114 115 public function addRawTos(array $raw_email) { 116 117 // Strip addresses down to bare emails, since the MailAdapter API currently 118 // requires we pass it just the address (like `[email protected]`), not 119 // a full string like `"Abraham Lincoln" <[email protected]>`. 120 foreach ($raw_email as $key => $email) { 121 $object = new PhutilEmailAddress($email); 122 $raw_email[$key] = $object->getAddress(); 123 } 124 125 $this->setParam('raw-to', $raw_email); 126 return $this; 127 } 128 129 public function addCCs(array $phids) { 130 $phids = array_unique($phids); 131 $this->setParam('cc', $phids); 132 return $this; 133 } 134 135 public function setExcludeMailRecipientPHIDs(array $exclude) { 136 $this->setParam('exclude', $exclude); 137 return $this; 138 } 139 140 private function getExcludeMailRecipientPHIDs() { 141 return $this->getParam('exclude', array()); 142 } 143 144 public function getTranslation(array $objects) { 145 $default_translation = PhabricatorEnv::getEnvConfig('translation.provider'); 146 $return = null; 147 $recipients = array_merge( 148 idx($this->parameters, 'to', array()), 149 idx($this->parameters, 'cc', array())); 150 foreach (array_select_keys($objects, $recipients) as $object) { 151 $translation = null; 152 if ($object instanceof PhabricatorUser) { 153 $translation = $object->getTranslation(); 154 } 155 if (!$translation) { 156 $translation = $default_translation; 157 } 158 if ($return && $translation != $return) { 159 return $default_translation; 160 } 161 $return = $translation; 162 } 163 164 if (!$return) { 165 $return = $default_translation; 166 } 167 168 return $return; 169 } 170 171 public function addPHIDHeaders($name, array $phids) { 172 foreach ($phids as $phid) { 173 $this->addHeader($name, '<'.$phid.'>'); 174 } 175 return $this; 176 } 177 178 public function addHeader($name, $value) { 179 $this->parameters['headers'][] = array($name, $value); 180 return $this; 181 } 182 183 public function addAttachment(PhabricatorMetaMTAAttachment $attachment) { 184 $this->parameters['attachments'][] = $attachment->toDictionary(); 185 return $this; 186 } 187 188 public function getAttachments() { 189 $dicts = $this->getParam('attachments'); 190 191 $result = array(); 192 foreach ($dicts as $dict) { 193 $result[] = PhabricatorMetaMTAAttachment::newFromDictionary($dict); 194 } 195 return $result; 196 } 197 198 public function setAttachments(array $attachments) { 199 assert_instances_of($attachments, 'PhabricatorMetaMTAAttachment'); 200 $this->setParam('attachments', mpull($attachments, 'toDictionary')); 201 return $this; 202 } 203 204 public function setFrom($from) { 205 $this->setParam('from', $from); 206 return $this; 207 } 208 209 public function setReplyTo($reply_to) { 210 $this->setParam('reply-to', $reply_to); 211 return $this; 212 } 213 214 public function setSubject($subject) { 215 $this->setParam('subject', $subject); 216 return $this; 217 } 218 219 public function setSubjectPrefix($prefix) { 220 $this->setParam('subject-prefix', $prefix); 221 return $this; 222 } 223 224 public function setVarySubjectPrefix($prefix) { 225 $this->setParam('vary-subject-prefix', $prefix); 226 return $this; 227 } 228 229 public function setBody($body) { 230 $this->setParam('body', $body); 231 return $this; 232 } 233 234 public function setHTMLBody($html) { 235 $this->setParam('html-body', $html); 236 return $this; 237 } 238 239 public function getBody() { 240 return $this->getParam('body'); 241 } 242 243 public function getHTMLBody() { 244 return $this->getParam('html-body'); 245 } 246 247 public function setIsErrorEmail($is_error) { 248 $this->setParam('is-error', $is_error); 249 return $this; 250 } 251 252 public function getIsErrorEmail() { 253 return $this->getParam('is-error', false); 254 } 255 256 public function getToPHIDs() { 257 return $this->getParam('to', array()); 258 } 259 260 public function getRawToAddresses() { 261 return $this->getParam('raw-to', array()); 262 } 263 264 public function getCcPHIDs() { 265 return $this->getParam('cc', array()); 266 } 267 268 /** 269 * Force delivery of a message, even if recipients have preferences which 270 * would otherwise drop the message. 271 * 272 * This is primarily intended to let users who don't want any email still 273 * receive things like password resets. 274 * 275 * @param bool True to force delivery despite user preferences. 276 * @return this 277 */ 278 public function setForceDelivery($force) { 279 $this->setParam('force', $force); 280 return $this; 281 } 282 283 public function getForceDelivery() { 284 return $this->getParam('force', false); 285 } 286 287 /** 288 * Flag that this is an auto-generated bulk message and should have bulk 289 * headers added to it if appropriate. Broadly, this means some flavor of 290 * "Precedence: bulk" or similar, but is implementation and configuration 291 * dependent. 292 * 293 * @param bool True if the mail is automated bulk mail. 294 * @return this 295 */ 296 public function setIsBulk($is_bulk) { 297 $this->setParam('is-bulk', $is_bulk); 298 return $this; 299 } 300 301 /** 302 * Use this method to set an ID used for message threading. MetaMTA will 303 * set appropriate headers (Message-ID, In-Reply-To, References and 304 * Thread-Index) based on the capabilities of the underlying mailer. 305 * 306 * @param string Unique identifier, appropriate for use in a Message-ID, 307 * In-Reply-To or References headers. 308 * @param bool If true, indicates this is the first message in the thread. 309 * @return this 310 */ 311 public function setThreadID($thread_id, $is_first_message = false) { 312 $this->setParam('thread-id', $thread_id); 313 $this->setParam('is-first-message', $is_first_message); 314 return $this; 315 } 316 317 /** 318 * Save a newly created mail to the database. The mail will eventually be 319 * delivered by the MetaMTA daemon. 320 * 321 * @return this 322 */ 323 public function saveAndSend() { 324 return $this->save(); 325 } 326 327 public function save() { 328 if ($this->getID()) { 329 return parent::save(); 330 } 331 332 // NOTE: When mail is sent from CLI scripts that run tasks in-process, we 333 // may re-enter this method from within scheduleTask(). The implementation 334 // is intended to avoid anything awkward if we end up reentering this 335 // method. 336 337 $this->openTransaction(); 338 // Save to generate a task ID. 339 $result = parent::save(); 340 341 // Queue a task to send this mail. 342 $mailer_task = PhabricatorWorker::scheduleTask( 343 'PhabricatorMetaMTAWorker', 344 $this->getID(), 345 PhabricatorWorker::PRIORITY_ALERTS); 346 347 $this->saveTransaction(); 348 349 return $result; 350 } 351 352 public function buildDefaultMailer() { 353 return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); 354 } 355 356 357 /** 358 * Attempt to deliver an email immediately, in this process. 359 * 360 * @param bool Try to deliver this email even if it has already been 361 * delivered or is in backoff after a failed delivery attempt. 362 * @param PhabricatorMailImplementationAdapter Use a specific mail adapter, 363 * instead of the default. 364 * 365 * @return void 366 */ 367 public function sendNow( 368 $force_send = false, 369 PhabricatorMailImplementationAdapter $mailer = null) { 370 371 if ($mailer === null) { 372 $mailer = $this->buildDefaultMailer(); 373 } 374 375 if (!$force_send) { 376 if ($this->getStatus() != self::STATUS_QUEUE) { 377 throw new Exception('Trying to send an already-sent mail!'); 378 } 379 } 380 381 try { 382 $params = $this->parameters; 383 384 $actors = $this->loadAllActors(); 385 $deliverable_actors = $this->filterDeliverableActors($actors); 386 387 $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address'); 388 if (empty($params['from'])) { 389 $mailer->setFrom($default_from); 390 } 391 392 $is_first = idx($params, 'is-first-message'); 393 unset($params['is-first-message']); 394 395 $is_threaded = (bool)idx($params, 'thread-id'); 396 397 $reply_to_name = idx($params, 'reply-to-name', ''); 398 unset($params['reply-to-name']); 399 400 $add_cc = array(); 401 $add_to = array(); 402 403 // Only try to use preferences if everything is multiplexed, so we 404 // get consistent behavior. 405 $use_prefs = self::shouldMultiplexAllMail(); 406 407 $prefs = null; 408 if ($use_prefs) { 409 410 // If multiplexing is enabled, some recipients will be in "Cc" 411 // rather than "To". We'll move them to "To" later (or supply a 412 // dummy "To") but need to look for the recipient in either the 413 // "To" or "Cc" fields here. 414 $target_phid = head(idx($params, 'to', array())); 415 if (!$target_phid) { 416 $target_phid = head(idx($params, 'cc', array())); 417 } 418 419 if ($target_phid) { 420 $user = id(new PhabricatorUser())->loadOneWhere( 421 'phid = %s', 422 $target_phid); 423 if ($user) { 424 $prefs = $user->loadPreferences(); 425 } 426 } 427 } 428 429 foreach ($params as $key => $value) { 430 switch ($key) { 431 case 'from': 432 $from = $value; 433 $actor_email = null; 434 $actor_name = null; 435 $actor = idx($actors, $from); 436 if ($actor) { 437 $actor_email = $actor->getEmailAddress(); 438 $actor_name = $actor->getName(); 439 } 440 $can_send_as_user = $actor_email && 441 PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); 442 443 if ($can_send_as_user) { 444 $mailer->setFrom($actor_email, $actor_name); 445 } else { 446 $from_email = coalesce($actor_email, $default_from); 447 $from_name = coalesce($actor_name, pht('Phabricator')); 448 449 if (empty($params['reply-to'])) { 450 $params['reply-to'] = $from_email; 451 $params['reply-to-name'] = $from_name; 452 } 453 454 $mailer->setFrom($default_from, $from_name); 455 } 456 break; 457 case 'reply-to': 458 $mailer->addReplyTo($value, $reply_to_name); 459 break; 460 case 'to': 461 $to_phids = $this->expandRecipients($value); 462 $to_actors = array_select_keys($deliverable_actors, $to_phids); 463 $add_to = array_merge( 464 $add_to, 465 mpull($to_actors, 'getEmailAddress')); 466 break; 467 case 'raw-to': 468 $add_to = array_merge($add_to, $value); 469 break; 470 case 'cc': 471 $cc_phids = $this->expandRecipients($value); 472 $cc_actors = array_select_keys($deliverable_actors, $cc_phids); 473 $add_cc = array_merge( 474 $add_cc, 475 mpull($cc_actors, 'getEmailAddress')); 476 break; 477 case 'headers': 478 foreach ($value as $pair) { 479 list($header_key, $header_value) = $pair; 480 481 // NOTE: If we have \n in a header, SES rejects the email. 482 $header_value = str_replace("\n", ' ', $header_value); 483 484 $mailer->addHeader($header_key, $header_value); 485 } 486 break; 487 case 'attachments': 488 $value = $this->getAttachments(); 489 foreach ($value as $attachment) { 490 $mailer->addAttachment( 491 $attachment->getData(), 492 $attachment->getFilename(), 493 $attachment->getMimeType()); 494 } 495 break; 496 case 'subject': 497 $subject = array(); 498 499 if ($is_threaded) { 500 $add_re = PhabricatorEnv::getEnvConfig('metamta.re-prefix'); 501 502 if ($prefs) { 503 $add_re = $prefs->getPreference( 504 PhabricatorUserPreferences::PREFERENCE_RE_PREFIX, 505 $add_re); 506 } 507 508 if ($add_re) { 509 $subject[] = 'Re:'; 510 } 511 } 512 513 $subject[] = trim(idx($params, 'subject-prefix')); 514 515 $vary_prefix = idx($params, 'vary-subject-prefix'); 516 if ($vary_prefix != '') { 517 $use_subject = PhabricatorEnv::getEnvConfig( 518 'metamta.vary-subjects'); 519 520 if ($prefs) { 521 $use_subject = $prefs->getPreference( 522 PhabricatorUserPreferences::PREFERENCE_VARY_SUBJECT, 523 $use_subject); 524 } 525 526 if ($use_subject) { 527 $subject[] = $vary_prefix; 528 } 529 } 530 531 $subject[] = $value; 532 533 $mailer->setSubject(implode(' ', array_filter($subject))); 534 break; 535 case 'is-bulk': 536 if ($value) { 537 if (PhabricatorEnv::getEnvConfig('metamta.precedence-bulk')) { 538 $mailer->addHeader('Precedence', 'bulk'); 539 } 540 } 541 break; 542 case 'thread-id': 543 544 // NOTE: Gmail freaks out about In-Reply-To and References which 545 // aren't in the form "<[email protected]>"; this is also required 546 // by RFC 2822, although some clients are more liberal in what they 547 // accept. 548 $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); 549 $value = '<'.$value.'@'.$domain.'>'; 550 551 if ($is_first && $mailer->supportsMessageIDHeader()) { 552 $mailer->addHeader('Message-ID', $value); 553 } else { 554 $in_reply_to = $value; 555 $references = array($value); 556 $parent_id = $this->getParentMessageID(); 557 if ($parent_id) { 558 $in_reply_to = $parent_id; 559 // By RFC 2822, the most immediate parent should appear last 560 // in the "References" header, so this order is intentional. 561 $references[] = $parent_id; 562 } 563 $references = implode(' ', $references); 564 $mailer->addHeader('In-Reply-To', $in_reply_to); 565 $mailer->addHeader('References', $references); 566 } 567 $thread_index = $this->generateThreadIndex($value, $is_first); 568 $mailer->addHeader('Thread-Index', $thread_index); 569 break; 570 case 'mailtags': 571 // Handled below. 572 break; 573 case 'subject-prefix': 574 case 'vary-subject-prefix': 575 // Handled above. 576 break; 577 default: 578 // Just discard. 579 } 580 } 581 582 $body = idx($params, 'body', ''); 583 $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); 584 if (strlen($body) > $max) { 585 $body = id(new PhutilUTF8StringTruncator()) 586 ->setMaximumBytes($max) 587 ->truncateString($body); 588 $body .= "\n"; 589 $body .= pht('(This email was truncated at %d bytes.)', $max); 590 } 591 $mailer->setBody($body); 592 593 $html_emails = false; 594 if ($use_prefs && $prefs) { 595 $html_emails = $prefs->getPreference( 596 PhabricatorUserPreferences::PREFERENCE_HTML_EMAILS, 597 $html_emails); 598 } 599 600 if ($html_emails && isset($params['html-body'])) { 601 $mailer->setHTMLBody($params['html-body']); 602 } 603 604 if (!$add_to && !$add_cc) { 605 $this->setStatus(self::STATUS_VOID); 606 $this->setMessage( 607 'Message has no valid recipients: all To/Cc are disabled, invalid, '. 608 'or configured not to receive this mail.'); 609 return $this->save(); 610 } 611 612 if ($this->getIsErrorEmail()) { 613 $all_recipients = array_merge($add_to, $add_cc); 614 if ($this->shouldRateLimitMail($all_recipients)) { 615 $this->setStatus(self::STATUS_VOID); 616 $this->setMessage( 617 pht( 618 'This is an error email, but one or more recipients have '. 619 'exceeded the error email rate limit. Declining to deliver '. 620 'message.')); 621 return $this->save(); 622 } 623 } 624 625 $mailer->addHeader('X-Phabricator-Sent-This-Message', 'Yes'); 626 $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA'); 627 628 // Some clients respect this to suppress OOF and other auto-responses. 629 $mailer->addHeader('X-Auto-Response-Suppress', 'All'); 630 631 // If the message has mailtags, filter out any recipients who don't want 632 // to receive this type of mail. 633 $mailtags = $this->getParam('mailtags'); 634 if ($mailtags) { 635 $tag_header = array(); 636 foreach ($mailtags as $mailtag) { 637 $tag_header[] = '<'.$mailtag.'>'; 638 } 639 $tag_header = implode(', ', $tag_header); 640 $mailer->addHeader('X-Phabricator-Mail-Tags', $tag_header); 641 } 642 643 // Some mailers require a valid "To:" in order to deliver mail. If we 644 // don't have any "To:", try to fill it in with a placeholder "To:". 645 // If that also fails, move the "Cc:" line to "To:". 646 if (!$add_to) { 647 $placeholder_key = 'metamta.placeholder-to-recipient'; 648 $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key); 649 if ($placeholder !== null) { 650 $add_to = array($placeholder); 651 } else { 652 $add_to = $add_cc; 653 $add_cc = array(); 654 } 655 } 656 657 $add_to = array_unique($add_to); 658 $add_cc = array_diff(array_unique($add_cc), $add_to); 659 660 $mailer->addTos($add_to); 661 if ($add_cc) { 662 $mailer->addCCs($add_cc); 663 } 664 } catch (Exception $ex) { 665 $this 666 ->setStatus(self::STATUS_FAIL) 667 ->setMessage($ex->getMessage()) 668 ->save(); 669 670 throw $ex; 671 } 672 673 try { 674 $ok = $mailer->send(); 675 if (!$ok) { 676 // TODO: At some point, we should clean this up and make all mailers 677 // throw. 678 throw new Exception( 679 pht('Mail adapter encountered an unexpected, unspecified failure.')); 680 } 681 682 $this->setStatus(self::STATUS_SENT); 683 $this->save(); 684 685 return $this; 686 } catch (PhabricatorMetaMTAPermanentFailureException $ex) { 687 $this 688 ->setStatus(self::STATUS_FAIL) 689 ->setMessage($ex->getMessage()) 690 ->save(); 691 692 throw $ex; 693 } catch (Exception $ex) { 694 $this 695 ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString()) 696 ->save(); 697 698 throw $ex; 699 } 700 } 701 702 public static function getReadableStatus($status_code) { 703 static $readable = array( 704 self::STATUS_QUEUE => 'Queued for Delivery', 705 self::STATUS_FAIL => 'Delivery Failed', 706 self::STATUS_SENT => 'Sent', 707 self::STATUS_VOID => 'Void', 708 ); 709 $status_code = coalesce($status_code, '?'); 710 return idx($readable, $status_code, $status_code); 711 } 712 713 private function generateThreadIndex($seed, $is_first_mail) { 714 // When threading, Outlook ignores the 'References' and 'In-Reply-To' 715 // headers that most clients use. Instead, it uses a custom 'Thread-Index' 716 // header. The format of this header is something like this (from 717 // camel-exchange-folder.c in Evolution Exchange): 718 719 /* A new post to a folder gets a 27-byte-long thread index. (The value 720 * is apparently unique but meaningless.) Each reply to a post gets a 721 * 32-byte-long thread index whose first 27 bytes are the same as the 722 * parent's thread index. Each reply to any of those gets a 723 * 37-byte-long thread index, etc. The Thread-Index header contains a 724 * base64 representation of this value. 725 */ 726 727 // The specific implementation uses a 27-byte header for the first email 728 // a recipient receives, and a random 5-byte suffix (32 bytes total) 729 // thereafter. This means that all the replies are (incorrectly) siblings, 730 // but it would be very difficult to keep track of the entire tree and this 731 // gets us reasonable client behavior. 732 733 $base = substr(md5($seed), 0, 27); 734 if (!$is_first_mail) { 735 // Not totally sure, but it seems like outlook orders replies by 736 // thread-index rather than timestamp, so to get these to show up in the 737 // right order we use the time as the last 4 bytes. 738 $base .= ' '.pack('N', time()); 739 } 740 741 return base64_encode($base); 742 } 743 744 public static function shouldMultiplexAllMail() { 745 return PhabricatorEnv::getEnvConfig('metamta.one-mail-per-recipient'); 746 } 747 748 749 /* -( Managing Recipients )------------------------------------------------ */ 750 751 752 /** 753 * Get all of the recipients for this mail, after preference filters are 754 * applied. This list has all objects to whom delivery will be attempted. 755 * 756 * @return list<phid> A list of all recipients to whom delivery will be 757 * attempted. 758 * @task recipients 759 */ 760 public function buildRecipientList() { 761 $actors = $this->loadActors( 762 array_merge( 763 $this->getToPHIDs(), 764 $this->getCcPHIDs())); 765 $actors = $this->filterDeliverableActors($actors); 766 return mpull($actors, 'getPHID'); 767 } 768 769 public function loadAllActors() { 770 $actor_phids = array_merge( 771 array($this->getParam('from')), 772 $this->getToPHIDs(), 773 $this->getCcPHIDs()); 774 775 $this->loadRecipientExpansions($actor_phids); 776 $actor_phids = $this->expandRecipients($actor_phids); 777 778 return $this->loadActors($actor_phids); 779 } 780 781 private function loadRecipientExpansions(array $phids) { 782 $expansions = id(new PhabricatorMetaMTAMemberQuery()) 783 ->setViewer(PhabricatorUser::getOmnipotentUser()) 784 ->withPHIDs($phids) 785 ->execute(); 786 787 $this->recipientExpansionMap = $expansions; 788 789 return $this; 790 } 791 792 /** 793 * Expand a list of recipient PHIDs (possibly including aggregate recipients 794 * like projects) into a deaggregated list of individual recipient PHIDs. 795 * For example, this will expand project PHIDs into a list of the project's 796 * members. 797 * 798 * @param list<phid> List of recipient PHIDs, possibly including aggregate 799 * recipients. 800 * @return list<phid> Deaggregated list of mailable recipients. 801 */ 802 private function expandRecipients(array $phids) { 803 if ($this->recipientExpansionMap === null) { 804 throw new Exception( 805 pht( 806 'Call loadRecipientExpansions() before expandRecipients()!')); 807 } 808 809 $results = array(); 810 foreach ($phids as $phid) { 811 if (!isset($this->recipientExpansionMap[$phid])) { 812 $results[$phid] = $phid; 813 } else { 814 foreach ($this->recipientExpansionMap[$phid] as $recipient_phid) { 815 $results[$recipient_phid] = $recipient_phid; 816 } 817 } 818 } 819 820 return array_keys($results); 821 } 822 823 private function filterDeliverableActors(array $actors) { 824 assert_instances_of($actors, 'PhabricatorMetaMTAActor'); 825 $deliverable_actors = array(); 826 foreach ($actors as $phid => $actor) { 827 if ($actor->isDeliverable()) { 828 $deliverable_actors[$phid] = $actor; 829 } 830 } 831 return $deliverable_actors; 832 } 833 834 private function loadActors(array $actor_phids) { 835 $actor_phids = array_filter($actor_phids); 836 $viewer = PhabricatorUser::getOmnipotentUser(); 837 838 $actors = id(new PhabricatorMetaMTAActorQuery()) 839 ->setViewer($viewer) 840 ->withPHIDs($actor_phids) 841 ->execute(); 842 843 if (!$actors) { 844 return array(); 845 } 846 847 if ($this->getForceDelivery()) { 848 // If we're forcing delivery, skip all the opt-out checks. 849 return $actors; 850 } 851 852 // Exclude explicit recipients. 853 foreach ($this->getExcludeMailRecipientPHIDs() as $phid) { 854 $actor = idx($actors, $phid); 855 if (!$actor) { 856 continue; 857 } 858 $actor->setUndeliverable( 859 pht( 860 'This message is a response to another email message, and this '. 861 'recipient received the original email message, so we are not '. 862 'sending them this substantially similar message (for example, '. 863 'the sender used "Reply All" instead of "Reply" in response to '. 864 'mail from Phabricator).')); 865 } 866 867 // Exclude the actor if their preferences are set. 868 $from_phid = $this->getParam('from'); 869 $from_actor = idx($actors, $from_phid); 870 if ($from_actor) { 871 $from_user = id(new PhabricatorPeopleQuery()) 872 ->setViewer($viewer) 873 ->withPHIDs(array($from_phid)) 874 ->execute(); 875 $from_user = head($from_user); 876 if ($from_user) { 877 $pref_key = PhabricatorUserPreferences::PREFERENCE_NO_SELF_MAIL; 878 $exclude_self = $from_user 879 ->loadPreferences() 880 ->getPreference($pref_key); 881 if ($exclude_self) { 882 $from_actor->setUndeliverable( 883 pht( 884 'This recipient is the user whose actions caused delivery of '. 885 'this message, but they have set preferences so they do not '. 886 'receive mail about their own actions (Settings > Email '. 887 'Preferences > Self Actions).')); 888 } 889 } 890 } 891 892 $all_prefs = id(new PhabricatorUserPreferences())->loadAllWhere( 893 'userPHID in (%Ls)', 894 $actor_phids); 895 $all_prefs = mpull($all_prefs, null, 'getUserPHID'); 896 897 // Exclude recipients who don't want any mail. 898 foreach ($all_prefs as $phid => $prefs) { 899 $exclude = $prefs->getPreference( 900 PhabricatorUserPreferences::PREFERENCE_NO_MAIL, 901 false); 902 if ($exclude) { 903 $actors[$phid]->setUndeliverable( 904 pht( 905 'This recipient has disabled all email notifications '. 906 '(Settings > Email Preferences > Email Notifications).')); 907 } 908 } 909 910 $value_email = PhabricatorUserPreferences::MAILTAG_PREFERENCE_EMAIL; 911 912 // Exclude all recipients who have set preferences to not receive this type 913 // of email (for example, a user who says they don't want emails about task 914 // CC changes). 915 $tags = $this->getParam('mailtags'); 916 if ($tags) { 917 foreach ($all_prefs as $phid => $prefs) { 918 $user_mailtags = $prefs->getPreference( 919 PhabricatorUserPreferences::PREFERENCE_MAILTAGS, 920 array()); 921 922 // The user must have elected to receive mail for at least one 923 // of the mailtags. 924 $send = false; 925 foreach ($tags as $tag) { 926 if (((int)idx($user_mailtags, $tag, $value_email)) == $value_email) { 927 $send = true; 928 break; 929 } 930 } 931 932 if (!$send) { 933 $actors[$phid]->setUndeliverable( 934 pht( 935 'This mail has tags which control which users receive it, and '. 936 'this recipient has not elected to receive mail with any of '. 937 'the tags on this message (Settings > Email Preferences).')); 938 } 939 } 940 } 941 942 return $actors; 943 } 944 945 private function shouldRateLimitMail(array $all_recipients) { 946 try { 947 PhabricatorSystemActionEngine::willTakeAction( 948 $all_recipients, 949 new PhabricatorMetaMTAErrorMailAction(), 950 1); 951 return false; 952 } catch (PhabricatorSystemActionRateLimitException $ex) { 953 return true; 954 } 955 } 956 957 958 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Sun Nov 30 09:20:46 2014 | Cross-referenced by PHPXref 0.7.1 |