diff --git a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php --- a/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php +++ b/src/applications/metamta/adapter/PhabricatorMailImplementationAdapter.php @@ -3,6 +3,7 @@ abstract class PhabricatorMailImplementationAdapter extends Phobject { private $key; + private $priority; private $options = array(); final public function getAdapterType() { @@ -57,6 +58,15 @@ return $this->key; } + final public function setPriority($priority) { + $this->priority = $priority; + return $this; + } + + final public function getPriority() { + return $this->priority; + } + final public function getOption($key) { if (!array_key_exists($key, $this->options)) { throw new Exception( diff --git a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php --- a/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTAMailgunReceiveController.php @@ -8,14 +8,31 @@ } private function verifyMessage() { - $api_key = PhabricatorEnv::getEnvConfig('mailgun.api-key'); $request = $this->getRequest(); $timestamp = $request->getStr('timestamp'); $token = $request->getStr('token'); $sig = $request->getStr('signature'); - $hash = hash_hmac('sha256', $timestamp.$token, $api_key); - return phutil_hashes_are_identical($sig, $hash); + // An install may configure multiple Mailgun mailers, and we might receive + // inbound mail from any of them. Test the signature to see if it matches + // any configured Mailgun mailer. + + $mailers = PhabricatorMetaMTAMail::newMailers(); + $mailgun_type = PhabricatorMailImplementationMailgunAdapter::ADAPTERTYPE; + foreach ($mailers as $mailer) { + if ($mailer->getAdapterType() != $mailgun_type) { + continue; + } + + $api_key = $mailer->getOption('api-key'); + + $hash = hash_hmac('sha256', $timestamp.$token, $api_key); + if (phutil_hashes_are_identical($sig, $hash)) { + return true; + } + } + + return false; } public function handleRequest(AphrontRequest $request) { diff --git a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php --- a/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php +++ b/src/applications/metamta/controller/PhabricatorMetaMTASendGridReceiveController.php @@ -8,6 +8,26 @@ } public function handleRequest(AphrontRequest $request) { + $mailers = PhabricatorMetaMTAMail::newMailers(); + $sendgrid_type = PhabricatorMailImplementationSendGridAdapter::ADAPTERTYPE; + + // SendGrid doesn't sign payloads so we can't be sure that SendGrid + // actually sent this request, but require a configured SendGrid mailer + // before we activate this endpoint. + + $has_sendgrid = false; + foreach ($mailers as $mailer) { + if ($mailer->getAdapterType() != $sendgrid_type) { + continue; + } + + $has_sendgrid = true; + break; + } + + if (!$has_sendgrid) { + return new Aphront404Response(); + } // No CSRF for SendGrid. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); diff --git a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php --- a/src/applications/metamta/storage/PhabricatorMetaMTAMail.php +++ b/src/applications/metamta/storage/PhabricatorMetaMTAMail.php @@ -463,33 +463,85 @@ throw new Exception(pht('Trying to send an already-sent mail!')); } - $mailers = $this->newMailers(); + $mailers = self::newMailers(); return $this->sendWithMailers($mailers); } - private function newMailers() { + public static function newMailers() { $mailers = array(); - $mailer = PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); + $config = PhabricatorEnv::getEnvConfig('cluster.mailers'); + if ($config === null) { + $mailer = PhabricatorEnv::newObjectFromConfig('metamta.mail-adapter'); - $defaults = $mailer->newDefaultOptions(); - $options = $mailer->newLegacyOptions(); + $defaults = $mailer->newDefaultOptions(); + $options = $mailer->newLegacyOptions(); - $options = $options + $defaults; + $options = $options + $defaults; - $mailer - ->setKey('default') - ->setOptions($options); + $mailer + ->setKey('default') + ->setPriority(-1) + ->setOptions($options); - $mailer->prepareForSend(); + $mailers[] = $mailer; + } else { + $adapters = PhabricatorMailImplementationAdapter::getAllAdapters(); + $next_priority = -1; + + foreach ($config as $spec) { + $type = $spec['type']; + if (!isset($adapters[$type])) { + throw new Exception( + pht( + 'Unknown mailer ("%s")!', + $type)); + } - $mailers[] = $mailer; + $key = $spec['key']; + $mailer = id(clone $adapters[$type]) + ->setKey($key); - return $mailers; + $priority = idx($spec, 'priority'); + if (!$priority) { + $priority = $next_priority; + $next_priority--; + } + $mailer->setPriority($priority); + + $defaults = $mailer->newDefaultOptions(); + $options = idx($spec, 'options', array()) + $defaults; + $mailer->setOptions($options); + } + } + + $sorted = array(); + $groups = mgroup($mailers, 'getPriority'); + ksort($groups); + foreach ($groups as $group) { + // Reorder services within the same priority group randomly. + shuffle($group); + foreach ($group as $mailer) { + $sorted[] = $mailer; + } + } + + foreach ($sorted as $mailer) { + $mailer->prepareForSend(); + } + + return $sorted; } public function sendWithMailers(array $mailers) { + if (!$mailers) { + return $this + ->setStatus(PhabricatorMailOutboundStatus::STATUS_VOID) + ->setMessage(pht('No mailers are configured.')) + ->save(); + } + $exceptions = array(); foreach ($mailers as $template_mailer) { $mailer = null; @@ -865,6 +917,12 @@ $mailer->addCCs($add_cc); } + // Keep track of which mailer actually ended up accepting the message. + $mailer_key = $mailer->getKey(); + if ($mailer_key !== null) { + $this->setParam('mailer.key', $mailer_key); + } + return $mailer; } diff --git a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php --- a/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php +++ b/src/applications/transactions/editor/PhabricatorApplicationTransactionEditor.php @@ -2575,12 +2575,13 @@ $mail = $this->buildMailForTarget($object, $xactions, $target); - if ($this->mustEncrypt) { - $mail - ->setMustEncrypt(true) - ->setMustEncryptReasons($this->mustEncrypt); + if ($mail) { + if ($this->mustEncrypt) { + $mail + ->setMustEncrypt(true) + ->setMustEncryptReasons($this->mustEncrypt); + } } - } catch (Exception $ex) { $caught = $ex; }