@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 for multiple mailers of the same type

Summary:
Depends on D19000. Ref T13053. Ref T12677. Currently, most mailers are configured with a bunch of `<mailer>.setting-name` global config options.

This means that you can't configure two different SMTP servers, which is a reasonable thing to want to do in the brave new world of mail failover.

It also means you can't configure two Mailgun accounts or two SES accounts. Although this might seem a little silly, we've had more service disruptions because of policy issues / administrative error (where a particular account was disabled) than actual downtime, so maybe it's not completely ridiculous.

Realign mailers so they can take configuration directly in an explicit way. A later change will add new configuration to take advantage of this and let us move away from having ~10 global options for this stuff eventually.

(This also makes writing third-party mailers easier.)

Test Plan:
Processed some mail, ran existing unit tests. But I wasn't especially thorough.

I expect later changes to provide some tools to make this more testable, so I'll vet each provider more thoroughly and add coverage for multiple mailers after that stuff is ready.

Reviewers: amckinley

Reviewed By: amckinley

Maniphest Tasks: T13053, T12677

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

+229 -31
+40
src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php
··· 2 2 3 3 abstract class PhabricatorMailImplementationAdapter extends Phobject { 4 4 5 + private $key; 6 + private $options = array(); 7 + 5 8 abstract public function setFrom($email, $name = ''); 6 9 abstract public function addReplyTo($email, $name = ''); 7 10 abstract public function addTos(array $emails); ··· 11 14 abstract public function setBody($plaintext_body); 12 15 abstract public function setHTMLBody($html_body); 13 16 abstract public function setSubject($subject); 17 + 14 18 15 19 /** 16 20 * Some mailers, notably Amazon SES, do not support us setting a specific ··· 31 35 * @return bool True on success. 32 36 */ 33 37 abstract public function send(); 38 + 39 + final public function setKey($key) { 40 + $this->key = $key; 41 + return $this; 42 + } 43 + 44 + final public function getKey() { 45 + return $this->key; 46 + } 47 + 48 + final public function getOption($key) { 49 + if (!array_key_exists($key, $this->options)) { 50 + throw new Exception( 51 + pht( 52 + 'Mailer ("%s") is attempting to access unknown option ("%s").', 53 + get_class($this), 54 + $key)); 55 + } 56 + 57 + return $this->options[$key]; 58 + } 59 + 60 + final public function setOptions(array $options) { 61 + $this->validateOptions($options); 62 + $this->options = $options; 63 + return $this; 64 + } 65 + 66 + abstract protected function validateOptions(array $options); 67 + 68 + abstract public function newDefaultOptions(); 69 + abstract public function newLegacyOptions(); 70 + 71 + public function prepareForSend() { 72 + return; 73 + } 34 74 35 75 }
+31 -5
src/applications/metamta/adapter/PhabricatorMailImplementationAmazonSESAdapter.php
··· 6 6 private $message; 7 7 private $isHTML; 8 8 9 - public function __construct() { 10 - parent::__construct(); 9 + public function prepareForSend() { 10 + parent::prepareForSend(); 11 11 $this->mailer->Mailer = 'amazon-ses'; 12 12 $this->mailer->customMailer = $this; 13 13 } ··· 17 17 return false; 18 18 } 19 19 20 + protected function validateOptions(array $options) { 21 + PhutilTypeSpec::checkMap( 22 + $options, 23 + array( 24 + 'access-key' => 'string', 25 + 'secret-key' => 'string', 26 + 'endpoint' => 'string', 27 + )); 28 + } 29 + 30 + public function newDefaultOptions() { 31 + return array( 32 + 'access-key' => null, 33 + 'secret-key' => null, 34 + 'endpoint' => null, 35 + ); 36 + } 37 + 38 + public function newLegacyOptions() { 39 + return array( 40 + 'access-key' => PhabricatorEnv::getEnvConfig('amazon-ses.access-key'), 41 + 'secret-key' => PhabricatorEnv::getEnvConfig('amazon-ses.secret-key'), 42 + 'endpoint' => PhabricatorEnv::getEnvConfig('amazon-ses.endpoint'), 43 + ); 44 + } 45 + 20 46 /** 21 47 * @phutil-external-symbol class SimpleEmailService 22 48 */ 23 49 public function executeSend($body) { 24 - $key = PhabricatorEnv::getEnvConfig('amazon-ses.access-key'); 25 - $secret = PhabricatorEnv::getEnvConfig('amazon-ses.secret-key'); 26 - $endpoint = PhabricatorEnv::getEnvConfig('amazon-ses.endpoint'); 50 + $key = $this->getOption('access-key'); 51 + $secret = $this->getOption('secret-key'); 52 + $endpoint = $this->getOption('endpoint'); 27 53 28 54 $root = phutil_get_library_root('phabricator'); 29 55 $root = dirname($root);
+25 -2
src/applications/metamta/adapter/PhabricatorMailImplementationMailgunAdapter.php
··· 71 71 return true; 72 72 } 73 73 74 + protected function validateOptions(array $options) { 75 + PhutilTypeSpec::checkMap( 76 + $options, 77 + array( 78 + 'api-key' => 'string', 79 + 'domain' => 'string', 80 + )); 81 + } 82 + 83 + public function newDefaultOptions() { 84 + return array( 85 + 'api-key' => null, 86 + 'domain' => null, 87 + ); 88 + } 89 + 90 + public function newLegacyOptions() { 91 + return array( 92 + 'api-key' => PhabricatorEnv::getEnvConfig('mailgun.api-key'), 93 + 'domain' => PhabricatorEnv::getEnvConfig('mailgun.domain'), 94 + ); 95 + } 96 + 74 97 public function send() { 75 - $key = PhabricatorEnv::getEnvConfig('mailgun.api-key'); 76 - $domain = PhabricatorEnv::getEnvConfig('mailgun.domain'); 98 + $key = $this->getOption('api-key'); 99 + $domain = $this->getOption('domain'); 77 100 $params = array(); 78 101 79 102 $params['to'] = implode(', ', idx($this->params, 'tos', array()));
+46 -9
src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerAdapter.php
··· 5 5 6 6 private $mailer; 7 7 8 + protected function validateOptions(array $options) { 9 + PhutilTypeSpec::checkMap( 10 + $options, 11 + array( 12 + 'host' => 'string|null', 13 + 'port' => 'int', 14 + 'user' => 'string|null', 15 + 'password' => 'string|null', 16 + 'protocol' => 'string|null', 17 + 'encoding' => 'string', 18 + 'mailer' => 'string', 19 + )); 20 + } 21 + 22 + public function newDefaultOptions() { 23 + return array( 24 + 'host' => null, 25 + 'port' => 25, 26 + 'user' => null, 27 + 'password' => null, 28 + 'protocol' => null, 29 + 'encoding' => 'base64', 30 + 'mailer' => 'smtp', 31 + ); 32 + } 33 + 34 + public function newLegacyOptions() { 35 + return array( 36 + 'host' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-host'), 37 + 'port' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-port'), 38 + 'user' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-user'), 39 + 'password' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-passsword'), 40 + 'protocol' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'), 41 + 'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'), 42 + 'mailer' => PhabricatorEnv::getEnvConfig('phpmailer.mailer'), 43 + ); 44 + } 45 + 8 46 /** 9 47 * @phutil-external-symbol class PHPMailer 10 48 */ 11 - public function __construct() { 49 + public function prepareForSend() { 12 50 $root = phutil_get_library_root('phabricator'); 13 51 $root = dirname($root); 14 52 require_once $root.'/externals/phpmailer/class.phpmailer.php'; 15 53 $this->mailer = new PHPMailer($use_exceptions = true); 16 54 $this->mailer->CharSet = 'utf-8'; 17 55 18 - $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'); 56 + $encoding = $this->getOption('encoding'); 19 57 $this->mailer->Encoding = $encoding; 20 58 21 59 // By default, PHPMailer sends one mail per recipient. We handle ··· 23 61 // send mail exactly like we ask. 24 62 $this->mailer->SingleTo = false; 25 63 26 - $mailer = PhabricatorEnv::getEnvConfig('phpmailer.mailer'); 64 + $mailer = $this->getOption('mailer'); 27 65 if ($mailer == 'smtp') { 28 66 $this->mailer->IsSMTP(); 29 - $this->mailer->Host = PhabricatorEnv::getEnvConfig('phpmailer.smtp-host'); 30 - $this->mailer->Port = PhabricatorEnv::getEnvConfig('phpmailer.smtp-port'); 31 - $user = PhabricatorEnv::getEnvConfig('phpmailer.smtp-user'); 67 + $this->mailer->Host = $this->getOption('host'); 68 + $this->mailer->Port = $this->getOption('port'); 69 + $user = $this->getOption('user'); 32 70 if ($user) { 33 71 $this->mailer->SMTPAuth = true; 34 72 $this->mailer->Username = $user; 35 - $this->mailer->Password = 36 - PhabricatorEnv::getEnvConfig('phpmailer.smtp-password'); 73 + $this->mailer->Password = $this->getOption('password'); 37 74 } 38 75 39 - $protocol = PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'); 76 + $protocol = $this->getOption('protocol'); 40 77 if ($protocol) { 41 78 $protocol = phutil_utf8_strtolower($protocol); 42 79 $this->mailer->SMTPSecure = $protocol;
+22 -2
src/applications/metamta/adapter/PhabricatorMailImplementationPHPMailerLiteAdapter.php
··· 8 8 9 9 protected $mailer; 10 10 11 + protected function validateOptions(array $options) { 12 + PhutilTypeSpec::checkMap( 13 + $options, 14 + array( 15 + 'encoding' => 'string', 16 + )); 17 + } 18 + 19 + public function newDefaultOptions() { 20 + return array( 21 + 'encoding' => 'base64', 22 + ); 23 + } 24 + 25 + public function newLegacyOptions() { 26 + return array( 27 + 'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'), 28 + ); 29 + } 30 + 11 31 /** 12 32 * @phutil-external-symbol class PHPMailerLite 13 33 */ 14 - public function __construct() { 34 + public function prepareForSend() { 15 35 $root = phutil_get_library_root('phabricator'); 16 36 $root = dirname($root); 17 37 require_once $root.'/externals/phpmailer/class.phpmailer-lite.php'; 18 38 $this->mailer = new PHPMailerLite($use_exceptions = true); 19 39 $this->mailer->CharSet = 'utf-8'; 20 40 21 - $encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'); 41 + $encoding = $this->getOption('encoding'); 22 42 $this->mailer->Encoding = $encoding; 23 43 24 44 // By default, PHPMailerLite sends one mail per recipient. We handle
+25 -2
src/applications/metamta/adapter/PhabricatorMailImplementationSendGridAdapter.php
··· 8 8 9 9 private $params = array(); 10 10 11 + protected function validateOptions(array $options) { 12 + PhutilTypeSpec::checkMap( 13 + $options, 14 + array( 15 + 'api-user' => 'string', 16 + 'api-key' => 'string', 17 + )); 18 + } 19 + 20 + public function newDefaultOptions() { 21 + return array( 22 + 'api-user' => null, 23 + 'api-key' => null, 24 + ); 25 + } 26 + 27 + public function newLegacyOptions() { 28 + return array( 29 + 'api-user' => PhabricatorEnv::getEnvConfig('sendgrid.api-user'), 30 + 'api-key' => PhabricatorEnv::getEnvConfig('sendgrid.api-key'), 31 + ); 32 + } 33 + 11 34 public function setFrom($email, $name = '') { 12 35 $this->params['from'] = $email; 13 36 $this->params['from-name'] = $name; ··· 73 96 74 97 public function send() { 75 98 76 - $user = PhabricatorEnv::getEnvConfig('sendgrid.api-user'); 77 - $key = PhabricatorEnv::getEnvConfig('sendgrid.api-key'); 99 + $user = $this->getOption('api-user'); 100 + $key = $this->getOption('api-key'); 78 101 79 102 if (!$user || !$key) { 80 103 throw new Exception(
+16 -2
src/applications/metamta/adapter/PhabricatorMailImplementationTestAdapter.php
··· 8 8 extends PhabricatorMailImplementationAdapter { 9 9 10 10 private $guts = array(); 11 - private $config; 11 + private $config = array(); 12 + 13 + protected function validateOptions(array $options) { 14 + PhutilTypeSpec::checkMap( 15 + $options, 16 + array()); 17 + } 12 18 13 - public function __construct(array $config = array()) { 19 + public function newDefaultOptions() { 20 + return array(); 21 + } 22 + 23 + public function newLegacyOptions() { 24 + return array(); 25 + } 26 + 27 + public function prepareForSend(array $config = array()) { 14 28 $this->config = $config; 15 29 } 16 30
+21 -8
src/applications/metamta/storage/PhabricatorMetaMTAMail.php
··· 453 453 return $result; 454 454 } 455 455 456 - public function buildDefaultMailer() { 457 - return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); 458 - } 459 - 460 - 461 456 /** 462 457 * Attempt to deliver an email immediately, in this process. 463 458 * ··· 468 463 throw new Exception(pht('Trying to send an already-sent mail!')); 469 464 } 470 465 471 - $mailers = array( 472 - $this->buildDefaultMailer(), 473 - ); 466 + $mailers = $this->newMailers(); 474 467 475 468 return $this->sendWithMailers($mailers); 476 469 } 477 470 471 + private function newMailers() { 472 + $mailers = array(); 473 + 474 + $mailer = PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); 475 + 476 + $defaults = $mailer->newDefaultOptions(); 477 + $options = $mailer->newLegacyOptions(); 478 + 479 + $options = $options + $defaults; 480 + 481 + $mailer 482 + ->setKey('default') 483 + ->setOptions($options); 484 + 485 + $mailer->prepareForSend(); 486 + 487 + $mailers[] = $mailer; 488 + 489 + return $mailers; 490 + } 478 491 479 492 public function sendWithMailers(array $mailers) { 480 493 $exceptions = array();
+3 -1
src/applications/metamta/storage/__tests__/PhabricatorMetaMTAMailTestCase.php
··· 182 182 $supports_message_id, 183 183 $is_first_mail) { 184 184 185 - $mailer = new PhabricatorMailImplementationTestAdapter( 185 + $mailer = new PhabricatorMailImplementationTestAdapter(); 186 + 187 + $mailer->prepareForSend( 186 188 array( 187 189 'supportsMessageIDHeader' => $supports_message_id, 188 190 ));