@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Prepare mail transmission to support failover across multiple mailers

Summary:
Ref T13053. Ref T12677. This restructures the calls and error handling logic so that we can pass in a list of multiple mailers and get retry logic.

This doesn't actually ever use multiple mailers yet, and shouldn't change any behavior. I'll add multiple-mailer coverage a little further in, since there's currently no way to effectively test which of several mailers ended up transmitting a message.

Test Plan:
- This has test coverage; tests still pass.
- Poked around locally doing things that send mail, saw mail appear to send. I'm not attached to a real mailer though so my confidence in local testing is only so-so.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13053, T12677

Differential Revision: https://secure.phabricator.com/D18998

+329 -296
+325 -292
src/applications/metamta/storage/PhabricatorMetaMTAMail.php
··· 461 461 /** 462 462 * Attempt to deliver an email immediately, in this process. 463 463 * 464 - * @param bool Try to deliver this email even if it has already been 465 - * delivered or is in backoff after a failed delivery attempt. 466 - * @param PhabricatorMailImplementationAdapter Use a specific mail adapter, 467 - * instead of the default. 468 - * 469 464 * @return void 470 465 */ 471 - public function sendNow( 472 - $force_send = false, 473 - PhabricatorMailImplementationAdapter $mailer = null) { 466 + public function sendNow() { 467 + if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) { 468 + throw new Exception(pht('Trying to send an already-sent mail!')); 469 + } 470 + 471 + $mailers = array( 472 + $this->buildDefaultMailer(), 473 + ); 474 + 475 + return $this->sendWithMailers($mailers); 476 + } 477 + 478 + 479 + public function sendWithMailers(array $mailers) { 480 + $exceptions = array(); 481 + foreach ($mailers as $template_mailer) { 482 + $mailer = null; 483 + 484 + try { 485 + $mailer = $this->buildMailer($template_mailer); 486 + } catch (Exception $ex) { 487 + $exceptions[] = $ex; 488 + continue; 489 + } 474 490 475 - if ($mailer === null) { 476 - $mailer = $this->buildDefaultMailer(); 491 + if (!$mailer) { 492 + // If we don't get a mailer back, that means the mail doesn't 493 + // actually need to be sent (for example, because recipients have 494 + // declined to receive the mail). Void it and return. 495 + return $this 496 + ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID) 497 + ->save(); 498 + } 499 + 500 + try { 501 + $ok = $mailer->send(); 502 + if (!$ok) { 503 + // TODO: At some point, we should clean this up and make all mailers 504 + // throw. 505 + throw new Exception( 506 + pht( 507 + 'Mail adapter encountered an unexpected, unspecified '. 508 + 'failure.')); 509 + } 510 + } catch (PhabricatorMetaMTAPermanentFailureException $ex) { 511 + // If any mailer raises a permanent failure, stop trying to send the 512 + // mail with other mailers. 513 + $this 514 + ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL) 515 + ->setMessage($ex->getMessage()) 516 + ->save(); 517 + 518 + throw $ex; 519 + } catch (Exception $ex) { 520 + $exceptions[] = $ex; 521 + continue; 522 + } 523 + 524 + return $this 525 + ->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT) 526 + ->save(); 527 + } 528 + 529 + // If we make it here, no mailer could send the mail but no mailer failed 530 + // permanently either. We update the error message for the mail, but leave 531 + // it in the current status (usually, STATUS_QUEUE) and try again later. 532 + 533 + $messages = array(); 534 + foreach ($exceptions as $ex) { 535 + $messages[] = $ex->getMessage(); 477 536 } 537 + $messages = implode("\n\n", $messages); 478 538 479 - if (!$force_send) { 480 - if ($this->getStatus() != PhabricatorMailOutboundStatus::STATUS_QUEUE) { 481 - throw new Exception(pht('Trying to send an already-sent mail!')); 482 - } 539 + $this 540 + ->setMessage($messages) 541 + ->save(); 542 + 543 + if (count($exceptions) === 1) { 544 + throw head($exceptions); 483 545 } 484 546 485 - try { 486 - $headers = $this->generateHeaders(); 547 + throw new PhutilAggregateException( 548 + pht('Encountered multiple exceptions while transmitting mail.'), 549 + $exceptions); 550 + } 487 551 488 - $params = $this->parameters; 552 + private function buildMailer(PhabricatorMailImplementationAdapter $mailer) { 553 + $headers = $this->generateHeaders(); 489 554 490 - $actors = $this->loadAllActors(); 491 - $deliverable_actors = $this->filterDeliverableActors($actors); 555 + $params = $this->parameters; 492 556 493 - $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address'); 494 - if (empty($params['from'])) { 495 - $mailer->setFrom($default_from); 496 - } 557 + $actors = $this->loadAllActors(); 558 + $deliverable_actors = $this->filterDeliverableActors($actors); 497 559 498 - $is_first = idx($params, 'is-first-message'); 499 - unset($params['is-first-message']); 560 + $default_from = PhabricatorEnv::getEnvConfig('metamta.default-address'); 561 + if (empty($params['from'])) { 562 + $mailer->setFrom($default_from); 563 + } 500 564 501 - $is_threaded = (bool)idx($params, 'thread-id'); 502 - $must_encrypt = $this->getMustEncrypt(); 565 + $is_first = idx($params, 'is-first-message'); 566 + unset($params['is-first-message']); 503 567 504 - $reply_to_name = idx($params, 'reply-to-name', ''); 505 - unset($params['reply-to-name']); 568 + $is_threaded = (bool)idx($params, 'thread-id'); 569 + $must_encrypt = $this->getMustEncrypt(); 506 570 507 - $add_cc = array(); 508 - $add_to = array(); 571 + $reply_to_name = idx($params, 'reply-to-name', ''); 572 + unset($params['reply-to-name']); 509 573 510 - // If we're sending one mail to everyone, some recipients will be in 511 - // "Cc" rather than "To". We'll move them to "To" later (or supply a 512 - // dummy "To") but need to look for the recipient in either the 513 - // "To" or "Cc" fields here. 514 - $target_phid = head(idx($params, 'to', array())); 515 - if (!$target_phid) { 516 - $target_phid = head(idx($params, 'cc', array())); 517 - } 574 + $add_cc = array(); 575 + $add_to = array(); 518 576 519 - $preferences = $this->loadPreferences($target_phid); 577 + // If we're sending one mail to everyone, some recipients will be in 578 + // "Cc" rather than "To". We'll move them to "To" later (or supply a 579 + // dummy "To") but need to look for the recipient in either the 580 + // "To" or "Cc" fields here. 581 + $target_phid = head(idx($params, 'to', array())); 582 + if (!$target_phid) { 583 + $target_phid = head(idx($params, 'cc', array())); 584 + } 520 585 521 - foreach ($params as $key => $value) { 522 - switch ($key) { 523 - case 'raw-from': 524 - list($from_email, $from_name) = $value; 525 - $mailer->setFrom($from_email, $from_name); 526 - break; 527 - case 'from': 528 - $from = $value; 529 - $actor_email = null; 530 - $actor_name = null; 531 - $actor = idx($actors, $from); 532 - if ($actor) { 533 - $actor_email = $actor->getEmailAddress(); 534 - $actor_name = $actor->getName(); 535 - } 536 - $can_send_as_user = $actor_email && 537 - PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); 586 + $preferences = $this->loadPreferences($target_phid); 538 587 539 - if ($can_send_as_user) { 540 - $mailer->setFrom($actor_email, $actor_name); 541 - } else { 542 - $from_email = coalesce($actor_email, $default_from); 543 - $from_name = coalesce($actor_name, pht('Phabricator')); 588 + foreach ($params as $key => $value) { 589 + switch ($key) { 590 + case 'raw-from': 591 + list($from_email, $from_name) = $value; 592 + $mailer->setFrom($from_email, $from_name); 593 + break; 594 + case 'from': 595 + $from = $value; 596 + $actor_email = null; 597 + $actor_name = null; 598 + $actor = idx($actors, $from); 599 + if ($actor) { 600 + $actor_email = $actor->getEmailAddress(); 601 + $actor_name = $actor->getName(); 602 + } 603 + $can_send_as_user = $actor_email && 604 + PhabricatorEnv::getEnvConfig('metamta.can-send-as-user'); 544 605 545 - if (empty($params['reply-to'])) { 546 - $params['reply-to'] = $from_email; 547 - $params['reply-to-name'] = $from_name; 548 - } 606 + if ($can_send_as_user) { 607 + $mailer->setFrom($actor_email, $actor_name); 608 + } else { 609 + $from_email = coalesce($actor_email, $default_from); 610 + $from_name = coalesce($actor_name, pht('Phabricator')); 549 611 550 - $mailer->setFrom($default_from, $from_name); 551 - } 552 - break; 553 - case 'reply-to': 554 - $mailer->addReplyTo($value, $reply_to_name); 555 - break; 556 - case 'to': 557 - $to_phids = $this->expandRecipients($value); 558 - $to_actors = array_select_keys($deliverable_actors, $to_phids); 559 - $add_to = array_merge( 560 - $add_to, 561 - mpull($to_actors, 'getEmailAddress')); 562 - break; 563 - case 'raw-to': 564 - $add_to = array_merge($add_to, $value); 565 - break; 566 - case 'cc': 567 - $cc_phids = $this->expandRecipients($value); 568 - $cc_actors = array_select_keys($deliverable_actors, $cc_phids); 569 - $add_cc = array_merge( 570 - $add_cc, 571 - mpull($cc_actors, 'getEmailAddress')); 572 - break; 573 - case 'attachments': 574 - $attached_viewer = PhabricatorUser::getOmnipotentUser(); 575 - $files = $this->loadAttachedFiles($attached_viewer); 576 - foreach ($files as $file) { 577 - $file->attachToObject($this->getPHID()); 612 + if (empty($params['reply-to'])) { 613 + $params['reply-to'] = $from_email; 614 + $params['reply-to-name'] = $from_name; 578 615 } 579 616 580 - // If the mail content must be encrypted, don't add attachments. 581 - if ($must_encrypt) { 582 - break; 583 - } 617 + $mailer->setFrom($default_from, $from_name); 618 + } 619 + break; 620 + case 'reply-to': 621 + $mailer->addReplyTo($value, $reply_to_name); 622 + break; 623 + case 'to': 624 + $to_phids = $this->expandRecipients($value); 625 + $to_actors = array_select_keys($deliverable_actors, $to_phids); 626 + $add_to = array_merge( 627 + $add_to, 628 + mpull($to_actors, 'getEmailAddress')); 629 + break; 630 + case 'raw-to': 631 + $add_to = array_merge($add_to, $value); 632 + break; 633 + case 'cc': 634 + $cc_phids = $this->expandRecipients($value); 635 + $cc_actors = array_select_keys($deliverable_actors, $cc_phids); 636 + $add_cc = array_merge( 637 + $add_cc, 638 + mpull($cc_actors, 'getEmailAddress')); 639 + break; 640 + case 'attachments': 641 + $attached_viewer = PhabricatorUser::getOmnipotentUser(); 642 + $files = $this->loadAttachedFiles($attached_viewer); 643 + foreach ($files as $file) { 644 + $file->attachToObject($this->getPHID()); 645 + } 584 646 585 - $value = $this->getAttachments(); 586 - foreach ($value as $attachment) { 587 - $mailer->addAttachment( 588 - $attachment->getData(), 589 - $attachment->getFilename(), 590 - $attachment->getMimeType()); 591 - } 647 + // If the mail content must be encrypted, don't add attachments. 648 + if ($must_encrypt) { 592 649 break; 593 - case 'subject': 594 - $subject = array(); 650 + } 651 + 652 + $value = $this->getAttachments(); 653 + foreach ($value as $attachment) { 654 + $mailer->addAttachment( 655 + $attachment->getData(), 656 + $attachment->getFilename(), 657 + $attachment->getMimeType()); 658 + } 659 + break; 660 + case 'subject': 661 + $subject = array(); 595 662 596 - if ($is_threaded) { 597 - if ($this->shouldAddRePrefix($preferences)) { 598 - $subject[] = 'Re:'; 599 - } 663 + if ($is_threaded) { 664 + if ($this->shouldAddRePrefix($preferences)) { 665 + $subject[] = 'Re:'; 600 666 } 667 + } 601 668 602 - $subject[] = trim(idx($params, 'subject-prefix')); 669 + $subject[] = trim(idx($params, 'subject-prefix')); 603 670 604 - // If mail content must be encrypted, we replace the subject with 605 - // a generic one. 606 - if ($must_encrypt) { 607 - $subject[] = pht('Object Updated'); 608 - } else { 609 - $vary_prefix = idx($params, 'vary-subject-prefix'); 610 - if ($vary_prefix != '') { 611 - if ($this->shouldVarySubject($preferences)) { 612 - $subject[] = $vary_prefix; 613 - } 671 + // If mail content must be encrypted, we replace the subject with 672 + // a generic one. 673 + if ($must_encrypt) { 674 + $subject[] = pht('Object Updated'); 675 + } else { 676 + $vary_prefix = idx($params, 'vary-subject-prefix'); 677 + if ($vary_prefix != '') { 678 + if ($this->shouldVarySubject($preferences)) { 679 + $subject[] = $vary_prefix; 614 680 } 615 - 616 - $subject[] = $value; 617 681 } 618 682 619 - $mailer->setSubject(implode(' ', array_filter($subject))); 620 - break; 621 - case 'thread-id': 683 + $subject[] = $value; 684 + } 622 685 623 - // NOTE: Gmail freaks out about In-Reply-To and References which 624 - // aren't in the form "<string@domain.tld>"; this is also required 625 - // by RFC 2822, although some clients are more liberal in what they 626 - // accept. 627 - $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); 628 - $value = '<'.$value.'@'.$domain.'>'; 686 + $mailer->setSubject(implode(' ', array_filter($subject))); 687 + break; 688 + case 'thread-id': 689 + 690 + // NOTE: Gmail freaks out about In-Reply-To and References which 691 + // aren't in the form "<string@domain.tld>"; this is also required 692 + // by RFC 2822, although some clients are more liberal in what they 693 + // accept. 694 + $domain = PhabricatorEnv::getEnvConfig('metamta.domain'); 695 + $value = '<'.$value.'@'.$domain.'>'; 629 696 630 - if ($is_first && $mailer->supportsMessageIDHeader()) { 631 - $headers[] = array('Message-ID', $value); 632 - } else { 633 - $in_reply_to = $value; 634 - $references = array($value); 635 - $parent_id = $this->getParentMessageID(); 636 - if ($parent_id) { 637 - $in_reply_to = $parent_id; 638 - // By RFC 2822, the most immediate parent should appear last 639 - // in the "References" header, so this order is intentional. 640 - $references[] = $parent_id; 641 - } 642 - $references = implode(' ', $references); 643 - $headers[] = array('In-Reply-To', $in_reply_to); 644 - $headers[] = array('References', $references); 697 + if ($is_first && $mailer->supportsMessageIDHeader()) { 698 + $headers[] = array('Message-ID', $value); 699 + } else { 700 + $in_reply_to = $value; 701 + $references = array($value); 702 + $parent_id = $this->getParentMessageID(); 703 + if ($parent_id) { 704 + $in_reply_to = $parent_id; 705 + // By RFC 2822, the most immediate parent should appear last 706 + // in the "References" header, so this order is intentional. 707 + $references[] = $parent_id; 645 708 } 646 - $thread_index = $this->generateThreadIndex($value, $is_first); 647 - $headers[] = array('Thread-Index', $thread_index); 648 - break; 649 - default: 650 - // Other parameters are handled elsewhere or are not relevant to 651 - // constructing the message. 652 - break; 653 - } 709 + $references = implode(' ', $references); 710 + $headers[] = array('In-Reply-To', $in_reply_to); 711 + $headers[] = array('References', $references); 712 + } 713 + $thread_index = $this->generateThreadIndex($value, $is_first); 714 + $headers[] = array('Thread-Index', $thread_index); 715 + break; 716 + default: 717 + // Other parameters are handled elsewhere or are not relevant to 718 + // constructing the message. 719 + break; 654 720 } 721 + } 655 722 656 - $stamps = $this->getMailStamps(); 657 - if ($stamps) { 658 - $headers[] = array('X-Phabricator-Stamps', implode(', ', $stamps)); 659 - } 723 + $stamps = $this->getMailStamps(); 724 + if ($stamps) { 725 + $headers[] = array('X-Phabricator-Stamps', implode(', ', $stamps)); 726 + } 660 727 661 - $raw_body = idx($params, 'body', ''); 662 - $body = $raw_body; 663 - if ($must_encrypt) { 664 - $parts = array(); 665 - $parts[] = pht( 666 - 'The content for this message can only be transmitted over a '. 667 - 'secure channel. To view the message content, follow this '. 668 - 'link:'); 728 + $raw_body = idx($params, 'body', ''); 729 + $body = $raw_body; 730 + if ($must_encrypt) { 731 + $parts = array(); 732 + $parts[] = pht( 733 + 'The content for this message can only be transmitted over a '. 734 + 'secure channel. To view the message content, follow this '. 735 + 'link:'); 669 736 670 - $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); 737 + $parts[] = PhabricatorEnv::getProductionURI($this->getURI()); 671 738 672 - $body = implode("\n\n", $parts); 673 - } else { 674 - $body = $raw_body; 675 - } 739 + $body = implode("\n\n", $parts); 740 + } else { 741 + $body = $raw_body; 742 + } 676 743 677 - $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); 678 - if (strlen($body) > $max) { 679 - $body = id(new PhutilUTF8StringTruncator()) 680 - ->setMaximumBytes($max) 681 - ->truncateString($body); 682 - $body .= "\n"; 683 - $body .= pht('(This email was truncated at %d bytes.)', $max); 684 - } 685 - $mailer->setBody($body); 744 + $max = PhabricatorEnv::getEnvConfig('metamta.email-body-limit'); 745 + if (strlen($body) > $max) { 746 + $body = id(new PhutilUTF8StringTruncator()) 747 + ->setMaximumBytes($max) 748 + ->truncateString($body); 749 + $body .= "\n"; 750 + $body .= pht('(This email was truncated at %d bytes.)', $max); 751 + } 752 + $mailer->setBody($body); 686 753 687 - // If we sent a different message body than we were asked to, record 688 - // what we actually sent to make debugging and diagnostics easier. 689 - if ($body !== $raw_body) { 690 - $this->setParam('body.sent', $body); 691 - } 754 + // If we sent a different message body than we were asked to, record 755 + // what we actually sent to make debugging and diagnostics easier. 756 + if ($body !== $raw_body) { 757 + $this->setParam('body.sent', $body); 758 + } 692 759 693 - if ($must_encrypt) { 694 - $send_html = false; 695 - } else { 696 - $send_html = $this->shouldSendHTML($preferences); 697 - } 760 + if ($must_encrypt) { 761 + $send_html = false; 762 + } else { 763 + $send_html = $this->shouldSendHTML($preferences); 764 + } 698 765 699 - if ($send_html && isset($params['html-body'])) { 700 - $mailer->setHTMLBody($params['html-body']); 701 - } 766 + if ($send_html && isset($params['html-body'])) { 767 + $mailer->setHTMLBody($params['html-body']); 768 + } 702 769 703 - // Pass the headers to the mailer, then save the state so we can show 704 - // them in the web UI. If the mail must be encrypted, we remove headers 705 - // which are not on a strict whitelist to avoid disclosing information. 706 - $filtered_headers = $this->filterHeaders($headers, $must_encrypt); 707 - foreach ($filtered_headers as $header) { 708 - list($header_key, $header_value) = $header; 709 - $mailer->addHeader($header_key, $header_value); 710 - } 711 - $this->setParam('headers.unfiltered', $headers); 712 - $this->setParam('headers.sent', $filtered_headers); 770 + // Pass the headers to the mailer, then save the state so we can show 771 + // them in the web UI. If the mail must be encrypted, we remove headers 772 + // which are not on a strict whitelist to avoid disclosing information. 773 + $filtered_headers = $this->filterHeaders($headers, $must_encrypt); 774 + foreach ($filtered_headers as $header) { 775 + list($header_key, $header_value) = $header; 776 + $mailer->addHeader($header_key, $header_value); 777 + } 778 + $this->setParam('headers.unfiltered', $headers); 779 + $this->setParam('headers.sent', $filtered_headers); 713 780 714 - // Save the final deliverability outcomes and reasoning so we can 715 - // explain why things happened the way they did. 716 - $actor_list = array(); 717 - foreach ($actors as $actor) { 718 - $actor_list[$actor->getPHID()] = array( 719 - 'deliverable' => $actor->isDeliverable(), 720 - 'reasons' => $actor->getDeliverabilityReasons(), 721 - ); 722 - } 723 - $this->setParam('actors.sent', $actor_list); 781 + // Save the final deliverability outcomes and reasoning so we can 782 + // explain why things happened the way they did. 783 + $actor_list = array(); 784 + foreach ($actors as $actor) { 785 + $actor_list[$actor->getPHID()] = array( 786 + 'deliverable' => $actor->isDeliverable(), 787 + 'reasons' => $actor->getDeliverabilityReasons(), 788 + ); 789 + } 790 + $this->setParam('actors.sent', $actor_list); 724 791 725 - $this->setParam('routing.sent', $this->getParam('routing')); 726 - $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); 792 + $this->setParam('routing.sent', $this->getParam('routing')); 793 + $this->setParam('routingmap.sent', $this->getRoutingRuleMap()); 727 794 728 - if (!$add_to && !$add_cc) { 729 - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID); 730 - $this->setMessage( 731 - pht( 732 - 'Message has no valid recipients: all To/Cc are disabled, '. 733 - 'invalid, or configured not to receive this mail.')); 734 - return $this->save(); 735 - } 795 + if (!$add_to && !$add_cc) { 796 + $this->setMessage( 797 + pht( 798 + 'Message has no valid recipients: all To/Cc are disabled, '. 799 + 'invalid, or configured not to receive this mail.')); 736 800 737 - if ($this->getIsErrorEmail()) { 738 - $all_recipients = array_merge($add_to, $add_cc); 739 - if ($this->shouldRateLimitMail($all_recipients)) { 740 - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID); 741 - $this->setMessage( 742 - pht( 743 - 'This is an error email, but one or more recipients have '. 744 - 'exceeded the error email rate limit. Declining to deliver '. 745 - 'message.')); 746 - return $this->save(); 747 - } 748 - } 801 + return null; 802 + } 749 803 750 - if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { 751 - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID); 804 + if ($this->getIsErrorEmail()) { 805 + $all_recipients = array_merge($add_to, $add_cc); 806 + if ($this->shouldRateLimitMail($all_recipients)) { 752 807 $this->setMessage( 753 808 pht( 754 - 'Phabricator is running in silent mode. See `%s` '. 755 - 'in the configuration to change this setting.', 756 - 'phabricator.silent')); 757 - return $this->save(); 758 - } 809 + 'This is an error email, but one or more recipients have '. 810 + 'exceeded the error email rate limit. Declining to deliver '. 811 + 'message.')); 759 812 760 - // Some mailers require a valid "To:" in order to deliver mail. If we 761 - // don't have any "To:", try to fill it in with a placeholder "To:". 762 - // If that also fails, move the "Cc:" line to "To:". 763 - if (!$add_to) { 764 - $placeholder_key = 'metamta.placeholder-to-recipient'; 765 - $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key); 766 - if ($placeholder !== null) { 767 - $add_to = array($placeholder); 768 - } else { 769 - $add_to = $add_cc; 770 - $add_cc = array(); 771 - } 813 + return null; 772 814 } 815 + } 773 816 774 - $add_to = array_unique($add_to); 775 - $add_cc = array_diff(array_unique($add_cc), $add_to); 817 + if (PhabricatorEnv::getEnvConfig('phabricator.silent')) { 818 + $this->setMessage( 819 + pht( 820 + 'Phabricator is running in silent mode. See `%s` '. 821 + 'in the configuration to change this setting.', 822 + 'phabricator.silent')); 776 823 777 - $mailer->addTos($add_to); 778 - if ($add_cc) { 779 - $mailer->addCCs($add_cc); 780 - } 781 - } catch (Exception $ex) { 782 - $this 783 - ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL) 784 - ->setMessage($ex->getMessage()) 785 - ->save(); 786 - 787 - throw $ex; 824 + return null; 788 825 } 789 826 790 - try { 791 - $ok = $mailer->send(); 792 - if (!$ok) { 793 - // TODO: At some point, we should clean this up and make all mailers 794 - // throw. 795 - throw new Exception( 796 - pht('Mail adapter encountered an unexpected, unspecified failure.')); 827 + // Some mailers require a valid "To:" in order to deliver mail. If we 828 + // don't have any "To:", try to fill it in with a placeholder "To:". 829 + // If that also fails, move the "Cc:" line to "To:". 830 + if (!$add_to) { 831 + $placeholder_key = 'metamta.placeholder-to-recipient'; 832 + $placeholder = PhabricatorEnv::getEnvConfig($placeholder_key); 833 + if ($placeholder !== null) { 834 + $add_to = array($placeholder); 835 + } else { 836 + $add_to = $add_cc; 837 + $add_cc = array(); 797 838 } 798 - 799 - $this->setStatus(PhabricatorMailOutboundStatus::STATUS_SENT); 800 - $this->save(); 801 - 802 - return $this; 803 - } catch (PhabricatorMetaMTAPermanentFailureException $ex) { 804 - $this 805 - ->setStatus(PhabricatorMailOutboundStatus::STATUS_FAIL) 806 - ->setMessage($ex->getMessage()) 807 - ->save(); 839 + } 808 840 809 - throw $ex; 810 - } catch (Exception $ex) { 811 - $this 812 - ->setMessage($ex->getMessage()."\n".$ex->getTraceAsString()) 813 - ->save(); 841 + $add_to = array_unique($add_to); 842 + $add_cc = array_diff(array_unique($add_cc), $add_to); 814 843 815 - throw $ex; 844 + $mailer->addTos($add_to); 845 + if ($add_cc) { 846 + $mailer->addCCs($add_cc); 816 847 } 848 + 849 + return $mailer; 817 850 } 818 851 819 852 private function generateThreadIndex($seed, $is_first_mail) {
+4 -4
src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
··· 18 18 $mail->addTos(array($phid)); 19 19 20 20 $mailer = new PhabricatorMailImplementationTestAdapter(); 21 - $mail->sendNow($force = true, $mailer); 21 + $mail->sendWithMailers(array($mailer)); 22 22 $this->assertEqual( 23 23 PhabricatorMailOutboundStatus::STATUS_SENT, 24 24 $mail->getStatus()); ··· 31 31 $mailer = new PhabricatorMailImplementationTestAdapter(); 32 32 $mailer->setFailTemporarily(true); 33 33 try { 34 - $mail->sendNow($force = true, $mailer); 34 + $mail->sendWithMailers(array($mailer)); 35 35 } catch (Exception $ex) { 36 36 // Ignore. 37 37 } ··· 47 47 $mailer = new PhabricatorMailImplementationTestAdapter(); 48 48 $mailer->setFailPermanently(true); 49 49 try { 50 - $mail->sendNow($force = true, $mailer); 50 + $mail->sendWithMailers(array($mailer)); 51 51 } catch (Exception $ex) { 52 52 // Ignore. 53 53 } ··· 191 191 192 192 $mail = new PhabricatorMetaMTAMail(); 193 193 $mail->setThreadID($thread_id, $is_first_mail); 194 - $mail->sendNow($force = true, $mailer); 194 + $mail->sendWithMailers(array($mailer)); 195 195 196 196 $guts = $mailer->getGuts(); 197 197 $dict = ipull($guts['headers'], 1, 0);