@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.

Update SES API to use AWSv4 signatures

Summary:
Ref T13570. Fixes T13235. In most cases, we use modern (v4) signatures for almost all AWS API calls, and have for several years.

However, sending email via SES currently uses an older piece of external code which uses the older (v3) signature method.

AWS is retiring v3 signatures on October 1 2020, so this pathway will stop working.

Update the pathway to use `PhutilAWSFuture`, which provides v4 signatures.

T13235 discusses poor error messages from SES. Switching to Futures fixes this for free, as they have more useful error handling.

Test Plan:
- Configured an SES mailer, including the new `region` parameter.
- Used `bin/mail send-test` to send mail via SES.
- Sent invalid mail (from an unverified address); got a more useful error message.
- Grepped for removed external, no hits.

Maniphest Tasks: T13570, T13235

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

+50 -734
-722
externals/amazon-ses/ses.php
··· 1 - <?php 2 - /** 3 - * 4 - * Copyright (c) 2011, Dan Myers. 5 - * Parts copyright (c) 2008, Donovan Schonknecht. 6 - * All rights reserved. 7 - * 8 - * Redistribution and use in source and binary forms, with or without 9 - * modification, are permitted provided that the following conditions are met: 10 - * 11 - * - Redistributions of source code must retain the above copyright notice, 12 - * this list of conditions and the following disclaimer. 13 - * - Redistributions in binary form must reproduce the above copyright 14 - * notice, this list of conditions and the following disclaimer in the 15 - * documentation and/or other materials provided with the distribution. 16 - * 17 - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18 - * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19 - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20 - * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21 - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22 - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23 - * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24 - * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25 - * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26 - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27 - * POSSIBILITY OF SUCH DAMAGE. 28 - * 29 - * This is a modified BSD license (the third clause has been removed). 30 - * The BSD license may be found here: 31 - * http://www.opensource.org/licenses/bsd-license.php 32 - * 33 - * Amazon Simple Email Service is a trademark of Amazon.com, Inc. or its affiliates. 34 - * 35 - * SimpleEmailService is based on Donovan Schonknecht's Amazon S3 PHP class, found here: 36 - * http://undesigned.org.za/2007/10/22/amazon-s3-php-class 37 - * 38 - */ 39 - 40 - /** 41 - * Amazon SimpleEmailService PHP class 42 - * 43 - * @link http://sourceforge.net/projects/php-aws-ses/ 44 - * version 0.8.1 45 - * 46 - */ 47 - class SimpleEmailService 48 - { 49 - protected $__accessKey; // AWS Access key 50 - protected $__secretKey; // AWS Secret key 51 - protected $__host; 52 - 53 - public function getAccessKey() { return $this->__accessKey; } 54 - public function getSecretKey() { return $this->__secretKey; } 55 - public function getHost() { return $this->__host; } 56 - 57 - protected $__verifyHost = 1; 58 - protected $__verifyPeer = 1; 59 - 60 - // verifyHost and verifyPeer determine whether curl verifies ssl certificates. 61 - // It may be necessary to disable these checks on certain systems. 62 - // These only have an effect if SSL is enabled. 63 - public function verifyHost() { return $this->__verifyHost; } 64 - public function enableVerifyHost($enable = true) { $this->__verifyHost = $enable; } 65 - 66 - public function verifyPeer() { return $this->__verifyPeer; } 67 - public function enableVerifyPeer($enable = true) { $this->__verifyPeer = $enable; } 68 - 69 - // If you use exceptions, errors will be communicated by throwing a 70 - // SimpleEmailServiceException. By default, they will be trigger_error()'d. 71 - protected $__useExceptions = 0; 72 - public function useExceptions() { return $this->__useExceptions; } 73 - public function enableUseExceptions($enable = true) { $this->__useExceptions = $enable; } 74 - 75 - /** 76 - * Constructor 77 - * 78 - * @param string $accessKey Access key 79 - * @param string $secretKey Secret key 80 - * @return void 81 - */ 82 - public function __construct($accessKey = null, $secretKey = null, $host = 'email.us-east-1.amazonaws.com') { 83 - if (!function_exists('simplexml_load_string')) { 84 - throw new Exception( 85 - pht( 86 - 'The PHP SimpleXML extension is not available, but this '. 87 - 'extension is required to send mail via Amazon SES, because '. 88 - 'Amazon SES returns API responses in XML format. Install or '. 89 - 'enable the SimpleXML extension.')); 90 - } 91 - 92 - // Catch mistakes with reading the wrong column out of the SES 93 - // documentation. See T10728. 94 - if (preg_match('(-smtp)', $host)) { 95 - throw new Exception( 96 - pht( 97 - 'Amazon SES is not configured correctly: the configured SES '. 98 - 'endpoint ("%s") is an SMTP endpoint. Instead, use an API (HTTPS) '. 99 - 'endpoint.', 100 - $host)); 101 - } 102 - 103 - if ($accessKey !== null && $secretKey !== null) { 104 - $this->setAuth($accessKey, $secretKey); 105 - } 106 - 107 - $this->__host = $host; 108 - } 109 - 110 - /** 111 - * Set AWS access key and secret key 112 - * 113 - * @param string $accessKey Access key 114 - * @param string $secretKey Secret key 115 - * @return void 116 - */ 117 - public function setAuth($accessKey, $secretKey) { 118 - $this->__accessKey = $accessKey; 119 - $this->__secretKey = $secretKey; 120 - } 121 - 122 - /** 123 - * Lists the email addresses that have been verified and can be used as the 'From' address 124 - * 125 - * @return An array containing two items: a list of verified email addresses, and the request id. 126 - */ 127 - public function listVerifiedEmailAddresses() { 128 - $rest = new SimpleEmailServiceRequest($this, 'GET'); 129 - $rest->setParameter('Action', 'ListVerifiedEmailAddresses'); 130 - 131 - $rest = $rest->getResponse(); 132 - 133 - $response = array(); 134 - if(!isset($rest->body)) { 135 - return $response; 136 - } 137 - 138 - $addresses = array(); 139 - foreach($rest->body->ListVerifiedEmailAddressesResult->VerifiedEmailAddresses->member as $address) { 140 - $addresses[] = (string)$address; 141 - } 142 - 143 - $response['Addresses'] = $addresses; 144 - $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; 145 - 146 - return $response; 147 - } 148 - 149 - /** 150 - * Requests verification of the provided email address, so it can be used 151 - * as the 'From' address when sending emails through SimpleEmailService. 152 - * 153 - * After submitting this request, you should receive a verification email 154 - * from Amazon at the specified address containing instructions to follow. 155 - * 156 - * @param string email The email address to get verified 157 - * @return The request id for this request. 158 - */ 159 - public function verifyEmailAddress($email) { 160 - $rest = new SimpleEmailServiceRequest($this, 'POST'); 161 - $rest->setParameter('Action', 'VerifyEmailAddress'); 162 - $rest->setParameter('EmailAddress', $email); 163 - 164 - $rest = $rest->getResponse(); 165 - 166 - $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; 167 - return $response; 168 - } 169 - 170 - /** 171 - * Removes the specified email address from the list of verified addresses. 172 - * 173 - * @param string email The email address to remove 174 - * @return The request id for this request. 175 - */ 176 - public function deleteVerifiedEmailAddress($email) { 177 - $rest = new SimpleEmailServiceRequest($this, 'DELETE'); 178 - $rest->setParameter('Action', 'DeleteVerifiedEmailAddress'); 179 - $rest->setParameter('EmailAddress', $email); 180 - 181 - $rest = $rest->getResponse(); 182 - 183 - $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; 184 - return $response; 185 - } 186 - 187 - /** 188 - * Retrieves information on the current activity limits for this account. 189 - * See http://docs.amazonwebservices.com/ses/latest/APIReference/API_GetSendQuota.html 190 - * 191 - * @return An array containing information on this account's activity limits. 192 - */ 193 - public function getSendQuota() { 194 - $rest = new SimpleEmailServiceRequest($this, 'GET'); 195 - $rest->setParameter('Action', 'GetSendQuota'); 196 - 197 - $rest = $rest->getResponse(); 198 - 199 - $response = array(); 200 - if(!isset($rest->body)) { 201 - return $response; 202 - } 203 - 204 - $response['Max24HourSend'] = (string)$rest->body->GetSendQuotaResult->Max24HourSend; 205 - $response['MaxSendRate'] = (string)$rest->body->GetSendQuotaResult->MaxSendRate; 206 - $response['SentLast24Hours'] = (string)$rest->body->GetSendQuotaResult->SentLast24Hours; 207 - $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; 208 - 209 - return $response; 210 - } 211 - 212 - /** 213 - * Retrieves statistics for the last two weeks of activity on this account. 214 - * See http://docs.amazonwebservices.com/ses/latest/APIReference/API_GetSendStatistics.html 215 - * 216 - * @return An array of activity statistics. Each array item covers a 15-minute period. 217 - */ 218 - public function getSendStatistics() { 219 - $rest = new SimpleEmailServiceRequest($this, 'GET'); 220 - $rest->setParameter('Action', 'GetSendStatistics'); 221 - 222 - $rest = $rest->getResponse(); 223 - 224 - $response = array(); 225 - if(!isset($rest->body)) { 226 - return $response; 227 - } 228 - 229 - $datapoints = array(); 230 - foreach($rest->body->GetSendStatisticsResult->SendDataPoints->member as $datapoint) { 231 - $p = array(); 232 - $p['Bounces'] = (string)$datapoint->Bounces; 233 - $p['Complaints'] = (string)$datapoint->Complaints; 234 - $p['DeliveryAttempts'] = (string)$datapoint->DeliveryAttempts; 235 - $p['Rejects'] = (string)$datapoint->Rejects; 236 - $p['Timestamp'] = (string)$datapoint->Timestamp; 237 - 238 - $datapoints[] = $p; 239 - } 240 - 241 - $response['SendDataPoints'] = $datapoints; 242 - $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; 243 - 244 - return $response; 245 - } 246 - 247 - 248 - public function sendRawEmail($raw) { 249 - $rest = new SimpleEmailServiceRequest($this, 'POST'); 250 - $rest->setParameter('Action', 'SendRawEmail'); 251 - $rest->setParameter('RawMessage.Data', base64_encode($raw)); 252 - 253 - $rest = $rest->getResponse(); 254 - 255 - $response['MessageId'] = (string)$rest->body->SendEmailResult->MessageId; 256 - $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; 257 - return $response; 258 - } 259 - 260 - /** 261 - * Given a SimpleEmailServiceMessage object, submits the message to the service for sending. 262 - * 263 - * @return An array containing the unique identifier for this message and a separate request id. 264 - * Returns false if the provided message is missing any required fields. 265 - */ 266 - public function sendEmail($sesMessage) { 267 - if(!$sesMessage->validate()) { 268 - return false; 269 - } 270 - 271 - $rest = new SimpleEmailServiceRequest($this, 'POST'); 272 - $rest->setParameter('Action', 'SendEmail'); 273 - 274 - $i = 1; 275 - foreach($sesMessage->to as $to) { 276 - $rest->setParameter('Destination.ToAddresses.member.'.$i, $to); 277 - $i++; 278 - } 279 - 280 - if(is_array($sesMessage->cc)) { 281 - $i = 1; 282 - foreach($sesMessage->cc as $cc) { 283 - $rest->setParameter('Destination.CcAddresses.member.'.$i, $cc); 284 - $i++; 285 - } 286 - } 287 - 288 - if(is_array($sesMessage->bcc)) { 289 - $i = 1; 290 - foreach($sesMessage->bcc as $bcc) { 291 - $rest->setParameter('Destination.BccAddresses.member.'.$i, $bcc); 292 - $i++; 293 - } 294 - } 295 - 296 - if(is_array($sesMessage->replyto)) { 297 - $i = 1; 298 - foreach($sesMessage->replyto as $replyto) { 299 - $rest->setParameter('ReplyToAddresses.member.'.$i, $replyto); 300 - $i++; 301 - } 302 - } 303 - 304 - $rest->setParameter('Source', $sesMessage->from); 305 - 306 - if($sesMessage->returnpath != null) { 307 - $rest->setParameter('ReturnPath', $sesMessage->returnpath); 308 - } 309 - 310 - if($sesMessage->subject != null && strlen($sesMessage->subject) > 0) { 311 - $rest->setParameter('Message.Subject.Data', $sesMessage->subject); 312 - if($sesMessage->subjectCharset != null && strlen($sesMessage->subjectCharset) > 0) { 313 - $rest->setParameter('Message.Subject.Charset', $sesMessage->subjectCharset); 314 - } 315 - } 316 - 317 - 318 - if($sesMessage->messagetext != null && strlen($sesMessage->messagetext) > 0) { 319 - $rest->setParameter('Message.Body.Text.Data', $sesMessage->messagetext); 320 - if($sesMessage->messageTextCharset != null && strlen($sesMessage->messageTextCharset) > 0) { 321 - $rest->setParameter('Message.Body.Text.Charset', $sesMessage->messageTextCharset); 322 - } 323 - } 324 - 325 - if($sesMessage->messagehtml != null && strlen($sesMessage->messagehtml) > 0) { 326 - $rest->setParameter('Message.Body.Html.Data', $sesMessage->messagehtml); 327 - if($sesMessage->messageHtmlCharset != null && strlen($sesMessage->messageHtmlCharset) > 0) { 328 - $rest->setParameter('Message.Body.Html.Charset', $sesMessage->messageHtmlCharset); 329 - } 330 - } 331 - 332 - $rest = $rest->getResponse(); 333 - 334 - $response['MessageId'] = (string)$rest->body->SendEmailResult->MessageId; 335 - $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; 336 - return $response; 337 - } 338 - 339 - /** 340 - * Trigger an error message 341 - * 342 - * @internal Used by member functions to output errors 343 - * @param array $error Array containing error information 344 - * @return string 345 - */ 346 - public function __triggerError($functionname, $error) 347 - { 348 - if($error == false) { 349 - $message = sprintf("SimpleEmailService::%s(): Encountered an error, but no description given", $functionname); 350 - } 351 - else if(isset($error['curl']) && $error['curl']) 352 - { 353 - $message = sprintf("SimpleEmailService::%s(): %s %s", $functionname, $error['code'], $error['message']); 354 - } 355 - else if(isset($error['Error'])) 356 - { 357 - $e = $error['Error']; 358 - $message = sprintf("SimpleEmailService::%s(): %s - %s: %s\nRequest Id: %s\n", $functionname, $e['Type'], $e['Code'], $e['Message'], $error['RequestId']); 359 - } 360 - 361 - if ($this->useExceptions()) { 362 - throw new SimpleEmailServiceException($message); 363 - } else { 364 - trigger_error($message, E_USER_WARNING); 365 - } 366 - } 367 - 368 - /** 369 - * Callback handler for 503 retries. 370 - * 371 - * @internal Used by SimpleDBRequest to call the user-specified callback, if set 372 - * @param $attempt The number of failed attempts so far 373 - * @return The retry delay in microseconds, or 0 to stop retrying. 374 - */ 375 - public function __executeServiceTemporarilyUnavailableRetryDelay($attempt) 376 - { 377 - if(is_callable($this->__serviceUnavailableRetryDelayCallback)) { 378 - $callback = $this->__serviceUnavailableRetryDelayCallback; 379 - return $callback($attempt); 380 - } 381 - return 0; 382 - } 383 - } 384 - 385 - final class SimpleEmailServiceRequest 386 - { 387 - private $ses, $verb, $parameters = array(); 388 - public $response; 389 - 390 - /** 391 - * Constructor 392 - * 393 - * @param string $ses The SimpleEmailService object making this request 394 - * @param string $action action 395 - * @param string $verb HTTP verb 396 - * @return mixed 397 - */ 398 - function __construct($ses, $verb) { 399 - $this->ses = $ses; 400 - $this->verb = $verb; 401 - $this->response = new STDClass; 402 - $this->response->error = false; 403 - } 404 - 405 - /** 406 - * Set request parameter 407 - * 408 - * @param string $key Key 409 - * @param string $value Value 410 - * @param boolean $replace Whether to replace the key if it already exists (default true) 411 - * @return void 412 - */ 413 - public function setParameter($key, $value, $replace = true) { 414 - if(!$replace && isset($this->parameters[$key])) 415 - { 416 - $temp = (array)($this->parameters[$key]); 417 - $temp[] = $value; 418 - $this->parameters[$key] = $temp; 419 - } 420 - else 421 - { 422 - $this->parameters[$key] = $value; 423 - } 424 - } 425 - 426 - /** 427 - * Get the response 428 - * 429 - * @return object | false 430 - */ 431 - public function getResponse() { 432 - 433 - $params = array(); 434 - foreach ($this->parameters as $var => $value) 435 - { 436 - if(is_array($value)) 437 - { 438 - foreach($value as $v) 439 - { 440 - $params[] = $var.'='.$this->__customUrlEncode($v); 441 - } 442 - } 443 - else 444 - { 445 - $params[] = $var.'='.$this->__customUrlEncode($value); 446 - } 447 - } 448 - 449 - sort($params, SORT_STRING); 450 - 451 - // must be in format 'Sun, 06 Nov 1994 08:49:37 GMT' 452 - $date = gmdate('D, d M Y H:i:s e'); 453 - 454 - $query = implode('&', $params); 455 - 456 - $headers = array(); 457 - $headers[] = 'Date: '.$date; 458 - $headers[] = 'Host: '.$this->ses->getHost(); 459 - 460 - $auth = 'AWS3-HTTPS AWSAccessKeyId='.$this->ses->getAccessKey(); 461 - $auth .= ',Algorithm=HmacSHA256,Signature='.$this->__getSignature($date); 462 - $headers[] = 'X-Amzn-Authorization: '.$auth; 463 - 464 - $url = 'https://'.$this->ses->getHost().'/'; 465 - 466 - // Basic setup 467 - $curl = curl_init(); 468 - curl_setopt($curl, CURLOPT_USERAGENT, 'SimpleEmailService/php'); 469 - 470 - curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, ($this->ses->verifyHost() ? 2 : 0)); 471 - curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, ($this->ses->verifyPeer() ? 1 : 0)); 472 - 473 - // Request types 474 - switch ($this->verb) { 475 - case 'GET': 476 - $url .= '?'.$query; 477 - break; 478 - case 'POST': 479 - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); 480 - curl_setopt($curl, CURLOPT_POSTFIELDS, $query); 481 - $headers[] = 'Content-Type: application/x-www-form-urlencoded'; 482 - break; 483 - case 'DELETE': 484 - $url .= '?'.$query; 485 - curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); 486 - break; 487 - default: break; 488 - } 489 - curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); 490 - curl_setopt($curl, CURLOPT_HEADER, false); 491 - 492 - curl_setopt($curl, CURLOPT_URL, $url); 493 - curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); 494 - curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); 495 - curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); 496 - 497 - // Execute, grab errors 498 - if (!curl_exec($curl)) { 499 - throw new SimpleEmailServiceException( 500 - pht( 501 - 'Encountered an error while making an HTTP request to Amazon SES '. 502 - '(cURL Error #%d): %s', 503 - curl_errno($curl), 504 - curl_error($curl))); 505 - } 506 - 507 - $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); 508 - if ($this->response->code != 200) { 509 - throw new SimpleEmailServiceException( 510 - pht( 511 - 'Unexpected HTTP status while making request to Amazon SES: '. 512 - 'expected 200, got %s.', 513 - $this->response->code)); 514 - } 515 - 516 - @curl_close($curl); 517 - 518 - // Parse body into XML 519 - if ($this->response->error === false && isset($this->response->body)) { 520 - $this->response->body = simplexml_load_string($this->response->body); 521 - 522 - // Grab SES errors 523 - if (!in_array($this->response->code, array(200, 201, 202, 204)) 524 - && isset($this->response->body->Error)) { 525 - $error = $this->response->body->Error; 526 - $output = array(); 527 - $output['curl'] = false; 528 - $output['Error'] = array(); 529 - $output['Error']['Type'] = (string)$error->Type; 530 - $output['Error']['Code'] = (string)$error->Code; 531 - $output['Error']['Message'] = (string)$error->Message; 532 - $output['RequestId'] = (string)$this->response->body->RequestId; 533 - 534 - $this->response->error = $output; 535 - unset($this->response->body); 536 - } 537 - } 538 - 539 - return $this->response; 540 - } 541 - 542 - /** 543 - * CURL write callback 544 - * 545 - * @param resource &$curl CURL resource 546 - * @param string &$data Data 547 - * @return integer 548 - */ 549 - private function __responseWriteCallback(&$curl, &$data) { 550 - if(!isset($this->response->body)) $this->response->body = ''; 551 - $this->response->body .= $data; 552 - return strlen($data); 553 - } 554 - 555 - /** 556 - * Contributed by afx114 557 - * URL encode the parameters as per http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?Query_QueryAuth.html 558 - * PHP's rawurlencode() follows RFC 1738, not RFC 3986 as required by Amazon. The only difference is the tilde (~), so convert it back after rawurlencode 559 - * See: http://www.morganney.com/blog/API/AWS-Product-Advertising-API-Requires-a-Signed-Request.php 560 - * 561 - * @param string $var String to encode 562 - * @return string 563 - */ 564 - private function __customUrlEncode($var) { 565 - return str_replace('%7E', '~', rawurlencode($var)); 566 - } 567 - 568 - /** 569 - * Generate the auth string using Hmac-SHA256 570 - * 571 - * @internal Used by SimpleDBRequest::getResponse() 572 - * @param string $string String to sign 573 - * @return string 574 - */ 575 - private function __getSignature($string) { 576 - return base64_encode(hash_hmac('sha256', $string, $this->ses->getSecretKey(), true)); 577 - } 578 - } 579 - 580 - 581 - final class SimpleEmailServiceMessage { 582 - 583 - // these are public for convenience only 584 - // these are not to be used outside of the SimpleEmailService class! 585 - public $to, $cc, $bcc, $replyto; 586 - public $from, $returnpath; 587 - public $subject, $messagetext, $messagehtml; 588 - public $subjectCharset, $messageTextCharset, $messageHtmlCharset; 589 - 590 - function __construct() { 591 - $to = array(); 592 - $cc = array(); 593 - $bcc = array(); 594 - $replyto = array(); 595 - 596 - $from = null; 597 - $returnpath = null; 598 - 599 - $subject = null; 600 - $messagetext = null; 601 - $messagehtml = null; 602 - 603 - $subjectCharset = null; 604 - $messageTextCharset = null; 605 - $messageHtmlCharset = null; 606 - } 607 - 608 - 609 - /** 610 - * addTo, addCC, addBCC, and addReplyTo have the following behavior: 611 - * If a single address is passed, it is appended to the current list of addresses. 612 - * If an array of addresses is passed, that array is merged into the current list. 613 - */ 614 - function addTo($to) { 615 - if(!is_array($to)) { 616 - $this->to[] = $to; 617 - } 618 - else { 619 - $this->to = array_merge($this->to, $to); 620 - } 621 - } 622 - 623 - function addCC($cc) { 624 - if(!is_array($cc)) { 625 - $this->cc[] = $cc; 626 - } 627 - else { 628 - $this->cc = array_merge($this->cc, $cc); 629 - } 630 - } 631 - 632 - function addBCC($bcc) { 633 - if(!is_array($bcc)) { 634 - $this->bcc[] = $bcc; 635 - } 636 - else { 637 - $this->bcc = array_merge($this->bcc, $bcc); 638 - } 639 - } 640 - 641 - function addReplyTo($replyto) { 642 - if(!is_array($replyto)) { 643 - $this->replyto[] = $replyto; 644 - } 645 - else { 646 - $this->replyto = array_merge($this->replyto, $replyto); 647 - } 648 - } 649 - 650 - function setFrom($from) { 651 - $this->from = $from; 652 - } 653 - 654 - function setReturnPath($returnpath) { 655 - $this->returnpath = $returnpath; 656 - } 657 - 658 - function setSubject($subject) { 659 - $this->subject = $subject; 660 - } 661 - 662 - function setSubjectCharset($charset) { 663 - $this->subjectCharset = $charset; 664 - } 665 - 666 - function setMessageFromString($text, $html = null) { 667 - $this->messagetext = $text; 668 - $this->messagehtml = $html; 669 - } 670 - 671 - function setMessageFromFile($textfile, $htmlfile = null) { 672 - if(file_exists($textfile) && is_file($textfile) && is_readable($textfile)) { 673 - $this->messagetext = file_get_contents($textfile); 674 - } 675 - if(file_exists($htmlfile) && is_file($htmlfile) && is_readable($htmlfile)) { 676 - $this->messagehtml = file_get_contents($htmlfile); 677 - } 678 - } 679 - 680 - function setMessageFromURL($texturl, $htmlurl = null) { 681 - $this->messagetext = file_get_contents($texturl); 682 - if($htmlurl !== null) { 683 - $this->messagehtml = file_get_contents($htmlurl); 684 - } 685 - } 686 - 687 - function setMessageCharset($textCharset, $htmlCharset = null) { 688 - $this->messageTextCharset = $textCharset; 689 - $this->messageHtmlCharset = $htmlCharset; 690 - } 691 - 692 - /** 693 - * Validates whether the message object has sufficient information to submit a request to SES. 694 - * This does not guarantee the message will arrive, nor that the request will succeed; 695 - * instead, it makes sure that no required fields are missing. 696 - * 697 - * This is used internally before attempting a SendEmail or SendRawEmail request, 698 - * but it can be used outside of this file if verification is desired. 699 - * May be useful if e.g. the data is being populated from a form; developers can generally 700 - * use this function to verify completeness instead of writing custom logic. 701 - * 702 - * @return boolean 703 - */ 704 - public function validate() { 705 - if(count($this->to) == 0) 706 - return false; 707 - if($this->from == null || strlen($this->from) == 0) 708 - return false; 709 - if($this->messagetext == null) 710 - return false; 711 - return true; 712 - } 713 - } 714 - 715 - 716 - /** 717 - * Thrown by SimpleEmailService when errors occur if you call 718 - * enableUseExceptions(true). 719 - */ 720 - final class SimpleEmailServiceException extends Exception { 721 - 722 - }
+2
src/__phutil_library_map__.php
··· 2177 2177 'PeopleUserLogGarbageCollector' => 'applications/people/garbagecollector/PeopleUserLogGarbageCollector.php', 2178 2178 'Phabricator404Controller' => 'applications/base/controller/Phabricator404Controller.php', 2179 2179 'PhabricatorAWSConfigOptions' => 'applications/config/option/PhabricatorAWSConfigOptions.php', 2180 + 'PhabricatorAWSSESFuture' => 'applications/metamta/future/PhabricatorAWSSESFuture.php', 2180 2181 'PhabricatorAccessControlTestCase' => 'applications/base/controller/__tests__/PhabricatorAccessControlTestCase.php', 2181 2182 'PhabricatorAccessLog' => 'infrastructure/log/PhabricatorAccessLog.php', 2182 2183 'PhabricatorAccessLogConfigOptions' => 'applications/config/option/PhabricatorAccessLogConfigOptions.php', ··· 8486 8487 'PeopleUserLogGarbageCollector' => 'PhabricatorGarbageCollector', 8487 8488 'Phabricator404Controller' => 'PhabricatorController', 8488 8489 'PhabricatorAWSConfigOptions' => 'PhabricatorApplicationConfigOptions', 8490 + 'PhabricatorAWSSESFuture' => 'PhutilAWSFuture', 8489 8491 'PhabricatorAccessControlTestCase' => 'PhabricatorTestCase', 8490 8492 'PhabricatorAccessLog' => 'Phobject', 8491 8493 'PhabricatorAccessLogConfigOptions' => 'PhabricatorApplicationConfigOptions',
+23 -11
src/applications/metamta/adapter/PhabricatorMailAmazonSESAdapter.php
··· 17 17 array( 18 18 'access-key' => 'string', 19 19 'secret-key' => 'string', 20 + 'region' => 'string', 20 21 'endpoint' => 'string', 21 22 )); 22 23 } ··· 25 26 return array( 26 27 'access-key' => null, 27 28 'secret-key' => null, 29 + 'region' => null, 28 30 'endpoint' => null, 29 31 ); 30 32 } ··· 45 47 $mailer->Send(); 46 48 } 47 49 48 - 49 - 50 - /** 51 - * @phutil-external-symbol class SimpleEmailService 52 - */ 53 50 public function executeSend($body) { 54 51 $key = $this->getOption('access-key'); 52 + 55 53 $secret = $this->getOption('secret-key'); 54 + $secret = new PhutilOpaqueEnvelope($secret); 55 + 56 + $region = $this->getOption('region'); 56 57 $endpoint = $this->getOption('endpoint'); 57 58 58 - $root = phutil_get_library_root('phabricator'); 59 - $root = dirname($root); 60 - require_once $root.'/externals/amazon-ses/ses.php'; 59 + $data = array( 60 + 'Action' => 'SendRawEmail', 61 + 'RawMessage.Data' => base64_encode($body), 62 + ); 61 63 62 - $service = new SimpleEmailService($key, $secret, $endpoint); 63 - $service->enableUseExceptions(true); 64 - return $service->sendRawEmail($body); 64 + $data = phutil_build_http_querystring($data); 65 + 66 + $future = id(new PhabricatorAWSSESFuture()) 67 + ->setAccessKey($key) 68 + ->setSecretKey($secret) 69 + ->setRegion($region) 70 + ->setEndpoint($endpoint) 71 + ->setHTTPMethod('POST') 72 + ->setData($data); 73 + 74 + $future->resolve(); 75 + 76 + return true; 65 77 } 66 78 67 79 }
+1
src/applications/metamta/adapter/__tests__/PhabricatorMailAdapterTestCase.php
··· 12 12 array( 13 13 'access-key' => 'test', 14 14 'secret-key' => 'test', 15 + 'region' => 'test', 15 16 'endpoint' => 'test', 16 17 ), 17 18 ),
+21
src/applications/metamta/future/PhabricatorAWSSESFuture.php
··· 1 + <?php 2 + 3 + final class PhabricatorAWSSESFuture extends PhutilAWSFuture { 4 + 5 + private $parameters; 6 + 7 + public function getServiceName() { 8 + return 'ses'; 9 + } 10 + 11 + protected function didReceiveResult($result) { 12 + list($status, $body, $headers) = $result; 13 + 14 + if (!$status->isError()) { 15 + return $body; 16 + } 17 + 18 + return parent::didReceiveResult($result); 19 + } 20 + 21 + }
+3 -1
src/docs/user/configuration/configuring_outbound_email.diviner
··· 247 247 248 248 - `access-key`: Required string. Your Amazon SES access key. 249 249 - `secret-key`: Required string. Your Amazon SES secret key. 250 - - `endpoint`: Required string. Your Amazon SES endpoint. 250 + - `region`: Required string. Your Amazon SES region, like `us-west-2`. 251 + - `endpoint`: Required string. Your Amazon SES endpoint, like 252 + `email.us-west-2.amazonaws.com`. 251 253 252 254 NOTE: Amazon SES **requires you to verify your "From" address**. Configure 253 255 which "From" address to use by setting `metamta.default-address` in your