1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-26 16:52:41 +01:00

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
This commit is contained in:
epriestley 2018-02-05 15:58:07 -08:00
parent 7765299f83
commit 7f2c90fbd1
9 changed files with 229 additions and 31 deletions

View file

@ -2,6 +2,9 @@
abstract class PhabricatorMailImplementationAdapter extends Phobject {
private $key;
private $options = array();
abstract public function setFrom($email, $name = '');
abstract public function addReplyTo($email, $name = '');
abstract public function addTos(array $emails);
@ -12,6 +15,7 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject {
abstract public function setHTMLBody($html_body);
abstract public function setSubject($subject);
/**
* Some mailers, notably Amazon SES, do not support us setting a specific
* Message-ID header.
@ -32,4 +36,40 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject {
*/
abstract public function send();
final public function setKey($key) {
$this->key = $key;
return $this;
}
final public function getKey() {
return $this->key;
}
final public function getOption($key) {
if (!array_key_exists($key, $this->options)) {
throw new Exception(
pht(
'Mailer ("%s") is attempting to access unknown option ("%s").',
get_class($this),
$key));
}
return $this->options[$key];
}
final public function setOptions(array $options) {
$this->validateOptions($options);
$this->options = $options;
return $this;
}
abstract protected function validateOptions(array $options);
abstract public function newDefaultOptions();
abstract public function newLegacyOptions();
public function prepareForSend() {
return;
}
}

View file

@ -6,8 +6,8 @@ final class PhabricatorMailImplementationAmazonSESAdapter
private $message;
private $isHTML;
public function __construct() {
parent::__construct();
public function prepareForSend() {
parent::prepareForSend();
$this->mailer->Mailer = 'amazon-ses';
$this->mailer->customMailer = $this;
}
@ -17,13 +17,39 @@ final class PhabricatorMailImplementationAmazonSESAdapter
return false;
}
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'access-key' => 'string',
'secret-key' => 'string',
'endpoint' => 'string',
));
}
public function newDefaultOptions() {
return array(
'access-key' => null,
'secret-key' => null,
'endpoint' => null,
);
}
public function newLegacyOptions() {
return array(
'access-key' => PhabricatorEnv::getEnvConfig('amazon-ses.access-key'),
'secret-key' => PhabricatorEnv::getEnvConfig('amazon-ses.secret-key'),
'endpoint' => PhabricatorEnv::getEnvConfig('amazon-ses.endpoint'),
);
}
/**
* @phutil-external-symbol class SimpleEmailService
*/
public function executeSend($body) {
$key = PhabricatorEnv::getEnvConfig('amazon-ses.access-key');
$secret = PhabricatorEnv::getEnvConfig('amazon-ses.secret-key');
$endpoint = PhabricatorEnv::getEnvConfig('amazon-ses.endpoint');
$key = $this->getOption('access-key');
$secret = $this->getOption('secret-key');
$endpoint = $this->getOption('endpoint');
$root = phutil_get_library_root('phabricator');
$root = dirname($root);

View file

@ -71,9 +71,32 @@ final class PhabricatorMailImplementationMailgunAdapter
return true;
}
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'api-key' => 'string',
'domain' => 'string',
));
}
public function newDefaultOptions() {
return array(
'api-key' => null,
'domain' => null,
);
}
public function newLegacyOptions() {
return array(
'api-key' => PhabricatorEnv::getEnvConfig('mailgun.api-key'),
'domain' => PhabricatorEnv::getEnvConfig('mailgun.domain'),
);
}
public function send() {
$key = PhabricatorEnv::getEnvConfig('mailgun.api-key');
$domain = PhabricatorEnv::getEnvConfig('mailgun.domain');
$key = $this->getOption('api-key');
$domain = $this->getOption('domain');
$params = array();
$params['to'] = implode(', ', idx($this->params, 'tos', array()));

View file

@ -5,17 +5,55 @@ final class PhabricatorMailImplementationPHPMailerAdapter
private $mailer;
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'host' => 'string|null',
'port' => 'int',
'user' => 'string|null',
'password' => 'string|null',
'protocol' => 'string|null',
'encoding' => 'string',
'mailer' => 'string',
));
}
public function newDefaultOptions() {
return array(
'host' => null,
'port' => 25,
'user' => null,
'password' => null,
'protocol' => null,
'encoding' => 'base64',
'mailer' => 'smtp',
);
}
public function newLegacyOptions() {
return array(
'host' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-host'),
'port' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-port'),
'user' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-user'),
'password' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-passsword'),
'protocol' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol'),
'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'),
'mailer' => PhabricatorEnv::getEnvConfig('phpmailer.mailer'),
);
}
/**
* @phutil-external-symbol class PHPMailer
*/
public function __construct() {
public function prepareForSend() {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer.php';
$this->mailer = new PHPMailer($use_exceptions = true);
$this->mailer->CharSet = 'utf-8';
$encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding');
$encoding = $this->getOption('encoding');
$this->mailer->Encoding = $encoding;
// By default, PHPMailer sends one mail per recipient. We handle
@ -23,20 +61,19 @@ final class PhabricatorMailImplementationPHPMailerAdapter
// send mail exactly like we ask.
$this->mailer->SingleTo = false;
$mailer = PhabricatorEnv::getEnvConfig('phpmailer.mailer');
$mailer = $this->getOption('mailer');
if ($mailer == 'smtp') {
$this->mailer->IsSMTP();
$this->mailer->Host = PhabricatorEnv::getEnvConfig('phpmailer.smtp-host');
$this->mailer->Port = PhabricatorEnv::getEnvConfig('phpmailer.smtp-port');
$user = PhabricatorEnv::getEnvConfig('phpmailer.smtp-user');
$this->mailer->Host = $this->getOption('host');
$this->mailer->Port = $this->getOption('port');
$user = $this->getOption('user');
if ($user) {
$this->mailer->SMTPAuth = true;
$this->mailer->Username = $user;
$this->mailer->Password =
PhabricatorEnv::getEnvConfig('phpmailer.smtp-password');
$this->mailer->Password = $this->getOption('password');
}
$protocol = PhabricatorEnv::getEnvConfig('phpmailer.smtp-protocol');
$protocol = $this->getOption('protocol');
if ($protocol) {
$protocol = phutil_utf8_strtolower($protocol);
$this->mailer->SMTPSecure = $protocol;

View file

@ -8,17 +8,37 @@ class PhabricatorMailImplementationPHPMailerLiteAdapter
protected $mailer;
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'encoding' => 'string',
));
}
public function newDefaultOptions() {
return array(
'encoding' => 'base64',
);
}
public function newLegacyOptions() {
return array(
'encoding' => PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding'),
);
}
/**
* @phutil-external-symbol class PHPMailerLite
*/
public function __construct() {
public function prepareForSend() {
$root = phutil_get_library_root('phabricator');
$root = dirname($root);
require_once $root.'/externals/phpmailer/class.phpmailer-lite.php';
$this->mailer = new PHPMailerLite($use_exceptions = true);
$this->mailer->CharSet = 'utf-8';
$encoding = PhabricatorEnv::getEnvConfig('phpmailer.smtp-encoding');
$encoding = $this->getOption('encoding');
$this->mailer->Encoding = $encoding;
// By default, PHPMailerLite sends one mail per recipient. We handle

View file

@ -8,6 +8,29 @@ final class PhabricatorMailImplementationSendGridAdapter
private $params = array();
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array(
'api-user' => 'string',
'api-key' => 'string',
));
}
public function newDefaultOptions() {
return array(
'api-user' => null,
'api-key' => null,
);
}
public function newLegacyOptions() {
return array(
'api-user' => PhabricatorEnv::getEnvConfig('sendgrid.api-user'),
'api-key' => PhabricatorEnv::getEnvConfig('sendgrid.api-key'),
);
}
public function setFrom($email, $name = '') {
$this->params['from'] = $email;
$this->params['from-name'] = $name;
@ -73,8 +96,8 @@ final class PhabricatorMailImplementationSendGridAdapter
public function send() {
$user = PhabricatorEnv::getEnvConfig('sendgrid.api-user');
$key = PhabricatorEnv::getEnvConfig('sendgrid.api-key');
$user = $this->getOption('api-user');
$key = $this->getOption('api-key');
if (!$user || !$key) {
throw new Exception(

View file

@ -8,9 +8,23 @@ final class PhabricatorMailImplementationTestAdapter
extends PhabricatorMailImplementationAdapter {
private $guts = array();
private $config;
private $config = array();
public function __construct(array $config = array()) {
protected function validateOptions(array $options) {
PhutilTypeSpec::checkMap(
$options,
array());
}
public function newDefaultOptions() {
return array();
}
public function newLegacyOptions() {
return array();
}
public function prepareForSend(array $config = array()) {
$this->config = $config;
}

View file

@ -453,11 +453,6 @@ final class PhabricatorMetaMTAMail
return $result;
}
public function buildDefaultMailer() {
return PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
}
/**
* Attempt to deliver an email immediately, in this process.
*
@ -468,13 +463,31 @@ final class PhabricatorMetaMTAMail
throw new Exception(pht('Trying to send an already-sent mail!'));
}
$mailers = array(
$this->buildDefaultMailer(),
);
$mailers = $this->newMailers();
return $this->sendWithMailers($mailers);
}
private function newMailers() {
$mailers = array();
$mailer = PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter');
$defaults = $mailer->newDefaultOptions();
$options = $mailer->newLegacyOptions();
$options = $options + $defaults;
$mailer
->setKey('default')
->setOptions($options);
$mailer->prepareForSend();
$mailers[] = $mailer;
return $mailers;
}
public function sendWithMailers(array $mailers) {
$exceptions = array();

View file

@ -182,7 +182,9 @@ final class PhabricatorMetaMTAMailTestCase extends PhabricatorTestCase {
$supports_message_id,
$is_first_mail) {
$mailer = new PhabricatorMailImplementationTestAdapter(
$mailer = new PhabricatorMailImplementationTestAdapter();
$mailer->prepareForSend(
array(
'supportsMessageIDHeader' => $supports_message_id,
));