From dbedb012eb7ecf46354dff3e8a460337ca4fcd0f Mon Sep 17 00:00:00 2001 From: epriestley Date: Thu, 26 May 2011 10:00:26 -0700 Subject: [PATCH] Add support for SendGrid as an outbound mail adapter Summary: SendGrid is a popular mail delivery platform, similar to Amazon SES. Provide support for delivering email via their REST API. Test Plan: Created a SendGrid account, configured my local install to use it, sent some mail, received mail. Reviewers: tuomaspelkonen, jungejason, aran CC: ccheever Differential Revision: 347 --- conf/default.conf.php | 6 + src/__phutil_library_map__.php | 2 + ...catorMailImplementationSendGridAdapter.php | 166 ++++++++++++++++++ .../metamta/adapter/sendgrid/__init__.php | 16 ++ src/docs/configuring_outbound_email.diviner | 34 +++- 5 files changed, 218 insertions(+), 6 deletions(-) create mode 100644 src/applications/metamta/adapter/sendgrid/PhabricatorMailImplementationSendGridAdapter.php create mode 100644 src/applications/metamta/adapter/sendgrid/__init__.php diff --git a/conf/default.conf.php b/conf/default.conf.php index bcbd48230c..831059cb81 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -143,6 +143,12 @@ return array( 'amazon-ses.access-key' => null, 'amazon-ses.secret-key' => null, + // If you're using Sendgrid to send email, provide your access credentials + // here. This will use the REST API. You can also use Sendgrid as a normal + // SMTP service. + 'sendgrid.api-user' => null, + 'sendgrid.api-key' => null, + // You can configure a reply handler domain so that email sent from Maniphest // will have a special "Reply To" address like "T123+82+af19f@example.com" // that allows recipients to reply by email and interact with tasks. For diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 89fbb6740e..3cecc446b4 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -339,6 +339,7 @@ phutil_register_library_map(array( 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/base', 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/amazonses', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite', + 'PhabricatorMailImplementationSendGridAdapter' => 'applications/metamta/adapter/sendgrid', 'PhabricatorMailImplementationTestAdapter' => 'applications/metamta/adapter/test', 'PhabricatorMailReplyHandler' => 'applications/metamta/replyhandler/base', 'PhabricatorMetaMTAController' => 'applications/metamta/controller/base', @@ -772,6 +773,7 @@ phutil_register_library_map(array( 'PhabricatorLogoutController' => 'PhabricatorAuthController', 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationPHPMailerLiteAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', + 'PhabricatorMailImplementationSendGridAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationTestAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', diff --git a/src/applications/metamta/adapter/sendgrid/PhabricatorMailImplementationSendGridAdapter.php b/src/applications/metamta/adapter/sendgrid/PhabricatorMailImplementationSendGridAdapter.php new file mode 100644 index 0000000000..d19d4927b0 --- /dev/null +++ b/src/applications/metamta/adapter/sendgrid/PhabricatorMailImplementationSendGridAdapter.php @@ -0,0 +1,166 @@ +params['from'] = $email; + $this->params['from-name'] = $name; + return $this; + } + + public function addReplyTo($email, $name = '') { + if (empty($this->params['reply-to'])) { + $this->params['reply-to'] = array(); + } + $this->params['reply-to'][] = array( + 'email' => $email, + 'name' => $name, + ); + return $this; + } + + public function addTos(array $emails) { + foreach ($emails as $email) { + $this->params['tos'][] = $email; + } + return $this; + } + + public function addCCs(array $emails) { + foreach ($emails as $email) { + $this->params['ccs'][] = $email; + } + return $this; + } + + public function addHeader($header_name, $header_value) { + $this->params['headers'][] = array($header_name, $header_value); + return $this; + } + + public function setBody($body) { + $this->params['body'] = $body; + return $this; + } + + public function setSubject($subject) { + $this->params['subject'] = $subject; + return $this; + } + + public function setIsHTML($is_html) { + $this->params['is-html'] = $is_html; + return $this; + } + + public function supportsMessageIDHeader() { + return false; + } + + public function send() { + + $user = PhabricatorEnv::getEnvConfig('sendgrid.api-user'); + $key = PhabricatorEnv::getEnvConfig('sendgrid.api-key'); + + if (!$user || !$key) { + throw new Exception( + "Configure 'sendgrid.api-user' and 'sendgrid.api-key' to use ". + "SendGrid for mail delivery."); + } + + $params = array(); + + $ii = 0; + foreach (idx($this->params, 'tos', array()) as $to) { + $params['to['.($ii++).']'] = $to; + } + + $params['subject'] = idx($this->params, 'subject'); + if (idx($this->params, 'is-html')) { + $params['html'] = idx($this->params, 'body'); + } else { + $params['text'] = idx($this->params, 'body'); + } + + $params['from'] = idx($this->params, 'from'); + if (idx($this->params['from-name'])) { + $params['fromname'] = idx($this->params, 'fromname'); + } + + if (idx($this->params, 'replyto')) { + $replyto = $this->params['replyto']; + + // Pick off the email part, no support for the name part in this API. + $params['replyto'] = $replyto[0]; + } + + $headers = idx($this->params, 'headers', array()); + + // See SendGrid Support Ticket #29390; there's no explicit REST API support + // for CC right now but it works if you add a generic "Cc" header. + // + // SendGrid said this is supported: + // "You can use CC as you are trying to do there [by adding a generic + // header]. It is supported despite our limited documentation to this + // effect, I am glad you were able to figure it out regardless. ..." + if (idx($this->params, 'ccs')) { + $headers[] = array('Cc', implode(', ', $this->params['ccs'])); + } + + if ($headers) { + // Convert to dictionary. + $headers = ipull($headers, 1, 0); + $headers = json_encode($headers); + $params['headers'] = $headers; + } + + $params['api_user'] = $user; + $params['api_key'] = $key; + + $future = new HTTPSFuture( + 'https://sendgrid.com/api/mail.send.json', + $params); + + list($code, $body) = $future->resolve(); + + if ($code !== 200) { + throw new Exception("REST API call failed with HTTP code {$code}."); + } + + $response = json_decode($body, true); + if (!is_array($response)) { + throw new Exception("Failed to JSON decode response: {$body}"); + } + + if ($response['message'] !== 'success') { + $errors = implode(";", $response['errors']); + throw new Exception("Request failed with errors: {$errors}."); + } + + return true; + } + +} + diff --git a/src/applications/metamta/adapter/sendgrid/__init__.php b/src/applications/metamta/adapter/sendgrid/__init__.php new file mode 100644 index 0000000000..8a58bcf65a --- /dev/null +++ b/src/applications/metamta/adapter/sendgrid/__init__.php @@ -0,0 +1,16 @@ +. It is easy to configure, but not free. + +You can configure SendGrid in two ways: you can send via SMTP or via the REST +API. To use SMTP, just configure ##sendmail## and leave Phabricator's setup +with defaults. To use the REST API, follow the instructions in this section. + +To configure Phabricator to use SendGrid, set these configuration keys: + + - **metamta.mail-adapter**: set to + "PhabricatorMailImplementationSendGridAdapter". + - **sendgrid.api-user**: set to your SendGrid login name. + - **sendgrid.api-key**: set to your SendGrid password. + +If you're logged into your SendGrid account, you may be able to find this +information easily by visiting . + = Adapter: Custom = You can provide a custom adapter by writing a concrete subclass of