diff --git a/conf/default.conf.php b/conf/default.conf.php index be32c7b456..302d3ea45d 100644 --- a/conf/default.conf.php +++ b/conf/default.conf.php @@ -1,143 +1,149 @@ null, // The Conduit URI for API access to this install. Normally this is just // the 'base-uri' plus "/api/" (e.g. "http://phabricator.example.com/api/"), // but make sure you specify 'https' if you have HTTPS configured. 'phabricator.conduit-uri' => null, 'phabricator.csrf-key' => '0b7ec0592e0a2829d8b71df2fa269b2c6172eca3', 'phabricator.version' => 'UNSTABLE', // The default PHID for users who haven't uploaded a profile image. It should // be 50x50px. 'user.default-profile-image-phid' => 'PHID-FILE-f57aaefce707fc4060ef', // When email is sent, try to hand it off to the MTA immediately. The only // reason to disable this is if your MTA infrastructure is completely // terrible. If you disable this option, you must run the 'metamta_mta.php' // daemon or mail won't be handed off to the MTA. 'metamta.send-immediately' => true, - + // "Reply-To" email address to use for no-reply emails. 'metamta.noreply' => 'noreply@example.com', + 'metamta.mail-adapter' => + 'PhabricatorMailImplementationPHPMailerLiteAdapter', + + 'amazon-ses.access-key' => null, + 'amazon-ses.secret-key' => null, + // -- Access Control -------------------------------------------------------- // // Phabricator users have one of three access levels: "anyone", "verified", // or "admin". "anyone" means every user, including users who do not have // accounts or are not logged into the system. "verified" is users who have // accounts, are logged in, and have satisfied whatever verification steps // the configuration requires (e.g., email verification and/or manual // approval). "admin" is verified users with the "administrator" flag set. // These configuration options control which access level is required to read // data from Phabricator (e.g., view revisions and comments in Differential) // and write data to Phabricator (e.g., upload files and create diffs). By // default they are both set to "verified", meaning only verified user // accounts can interact with the system in any meaningful way. // If you are configuring an install for an open source project, you may // want to reduce the "phabricator.read-access" requirement to "anyone". This // will allow anyone to browse Phabricator content, even without logging in. // Alternatively, you could raise the "phabricator.write-access" requirement // to "admin", effectively creating a read-only install. // Controls the minimum access level required to read data from Phabricator // (e.g., view revisions in Differential). Allowed values are "anyone", // "verified", or "admin". Note that "anyone" includes users who are not // logged in! You should leave this at 'verified' unless you want your data // to be publicly readable (e.g., you are developing open source software). 'phabricator.read-access' => 'verified', // Controls the minimum access level required to write data to Phabricator // (e.g., create new revisions in Differential). Allowed values are // "verified" or "admin". Setting this to "admin" will effectively create a // read-only install. 'phabricator.write-access' => 'verified', // -- DarkConsole ----------------------------------------------------------- // // DarkConsole is a administrative debugging/profiling tool built into // Phabricator. You can leave it disabled unless you're developing against // Phabricator. // Determines whether or not DarkConsole is available. DarkConsole exposes // some data like queries and stack traces, so you should be careful about // turning it on in production (although users can not normally see it, even // if the deployment configuration enables it). 'darkconsole.enabled' => true, // Always enable DarkConsole, even for logged out users. This potentially // exposes sensitive information to users, so make sure untrusted users can // not access an install running in this mode. You should definitely leave // this off in production. It is only really useful for using DarkConsole // utilties to debug or profile logged-out pages. You must set // 'darkconsole.enabled' to use this option. 'darkconsole.always-on' => false, // -- MySQL --------------------------------------------------------------- // // The username to use when connecting to MySQL. 'mysql.user' => 'root', // The password to use when connecting to MySQL. 'mysql.pass' => '', // The MySQL server to connect to. 'mysql.host' => 'localhost', // -- Facebook ------------------------------------------------------------ // // Can users use Facebook credentials to login to Phabricator? 'facebook.auth-enabled' => false, // The Facebook "Application ID" to use for Facebook API access. 'facebook.application-id' => null, // The Facebook "Application Secret" to use for Facebook API access. 'facebook.application-secret' => null, // -- Recaptcha ------------------------------------------------------------- // // Is Recaptcha enabled? If disabled, captchas will not appear. 'recaptcha.enabled' => false, // Your Recaptcha public key, obtained from Recaptcha. 'recaptcha.public-key' => null, // Your Recaptcha private key, obtained from Recaptcha. 'recaptcha.private-key' => null, ); diff --git a/externals/amazon-ses/ses.php b/externals/amazon-ses/ses.php new file mode 100644 index 0000000000..f6ccfa6014 --- /dev/null +++ b/externals/amazon-ses/ses.php @@ -0,0 +1,703 @@ +__accessKey; } + public function getSecretKey() { return $this->__secretKey; } + public function getHost() { return $this->__host; } + + protected $__verifyHost = 1; + protected $__verifyPeer = 1; + + // verifyHost and verifyPeer determine whether curl verifies ssl certificates. + // It may be necessary to disable these checks on certain systems. + // These only have an effect if SSL is enabled. + public function verifyHost() { return $this->__verifyHost; } + public function enableVerifyHost($enable = true) { $this->__verifyHost = $enable; } + + public function verifyPeer() { return $this->__verifyPeer; } + public function enableVerifyPeer($enable = true) { $this->__verifyPeer = $enable; } + + /** + * Constructor + * + * @param string $accessKey Access key + * @param string $secretKey Secret key + * @return void + */ + public function __construct($accessKey = null, $secretKey = null, $host = 'email.us-east-1.amazonaws.com') { + if ($accessKey !== null && $secretKey !== null) { + $this->setAuth($accessKey, $secretKey); + } + $this->__host = $host; + } + + /** + * Set AWS access key and secret key + * + * @param string $accessKey Access key + * @param string $secretKey Secret key + * @return void + */ + public function setAuth($accessKey, $secretKey) { + $this->__accessKey = $accessKey; + $this->__secretKey = $secretKey; + } + + /** + * Lists the email addresses that have been verified and can be used as the 'From' address + * + * @return An array containing two items: a list of verified email addresses, and the request id. + */ + public function listVerifiedEmailAddresses() { + $rest = new SimpleEmailServiceRequest($this, 'GET'); + $rest->setParameter('Action', 'ListVerifiedEmailAddresses'); + + $rest = $rest->getResponse(); + if($rest->error === false && $rest->code !== 200) { + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + } + if($rest->error !== false) { + $this->__triggerError('listVerifiedEmailAddresses', $rest->error); + return false; + } + + $response = array(); + if(!isset($rest->body)) { + return $response; + } + + $addresses = array(); + foreach($rest->body->ListVerifiedEmailAddressesResult->VerifiedEmailAddresses->member as $address) { + $addresses[] = (string)$address; + } + + $response['Addresses'] = $addresses; + $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; + + return $response; + } + + /** + * Requests verification of the provided email address, so it can be used + * as the 'From' address when sending emails through SimpleEmailService. + * + * After submitting this request, you should receive a verification email + * from Amazon at the specified address containing instructions to follow. + * + * @param string email The email address to get verified + * @return The request id for this request. + */ + public function verifyEmailAddress($email) { + $rest = new SimpleEmailServiceRequest($this, 'POST'); + $rest->setParameter('Action', 'VerifyEmailAddress'); + $rest->setParameter('EmailAddress', $email); + + $rest = $rest->getResponse(); + if($rest->error === false && $rest->code !== 200) { + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + } + if($rest->error !== false) { + $this->__triggerError('verifyEmailAddress', $rest->error); + return false; + } + + $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; + return $response; + } + + /** + * Removes the specified email address from the list of verified addresses. + * + * @param string email The email address to remove + * @return The request id for this request. + */ + public function deleteVerifiedEmailAddress($email) { + $rest = new SimpleEmailServiceRequest($this, 'DELETE'); + $rest->setParameter('Action', 'DeleteVerifiedEmailAddress'); + $rest->setParameter('EmailAddress', $email); + + $rest = $rest->getResponse(); + if($rest->error === false && $rest->code !== 200) { + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + } + if($rest->error !== false) { + $this->__triggerError('deleteVerifiedEmailAddress', $rest->error); + return false; + } + + $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; + return $response; + } + + /** + * Retrieves information on the current activity limits for this account. + * See http://docs.amazonwebservices.com/ses/latest/APIReference/API_GetSendQuota.html + * + * @return An array containing information on this account's activity limits. + */ + public function getSendQuota() { + $rest = new SimpleEmailServiceRequest($this, 'GET'); + $rest->setParameter('Action', 'GetSendQuota'); + + $rest = $rest->getResponse(); + if($rest->error === false && $rest->code !== 200) { + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + } + if($rest->error !== false) { + $this->__triggerError('getSendQuota', $rest->error); + return false; + } + + $response = array(); + if(!isset($rest->body)) { + return $response; + } + + $response['Max24HourSend'] = (string)$rest->body->GetSendQuotaResult->Max24HourSend; + $response['MaxSendRate'] = (string)$rest->body->GetSendQuotaResult->MaxSendRate; + $response['SentLast24Hours'] = (string)$rest->body->GetSendQuotaResult->SentLast24Hours; + $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; + + return $response; + } + + /** + * Retrieves statistics for the last two weeks of activity on this account. + * See http://docs.amazonwebservices.com/ses/latest/APIReference/API_GetSendStatistics.html + * + * @return An array of activity statistics. Each array item covers a 15-minute period. + */ + public function getSendStatistics() { + $rest = new SimpleEmailServiceRequest($this, 'GET'); + $rest->setParameter('Action', 'GetSendStatistics'); + + $rest = $rest->getResponse(); + if($rest->error === false && $rest->code !== 200) { + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + } + if($rest->error !== false) { + $this->__triggerError('getSendStatistics', $rest->error); + return false; + } + + $response = array(); + if(!isset($rest->body)) { + return $response; + } + + $datapoints = array(); + foreach($rest->body->GetSendStatisticsResult->SendDataPoints->member as $datapoint) { + $p = array(); + $p['Bounces'] = (string)$datapoint->Bounces; + $p['Complaints'] = (string)$datapoint->Complaints; + $p['DeliveryAttempts'] = (string)$datapoint->DeliveryAttempts; + $p['Rejects'] = (string)$datapoint->Rejects; + $p['Timestamp'] = (string)$datapoint->Timestamp; + + $datapoints[] = $p; + } + + $response['SendDataPoints'] = $datapoints; + $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; + + return $response; + } + + + /** + * Given a SimpleEmailServiceMessage object, submits the message to the service for sending. + * + * @return An array containing the unique identifier for this message and a separate request id. + * Returns false if the provided message is missing any required fields. + */ + public function sendEmail($sesMessage) { + if(!$sesMessage->validate()) { + return false; + } + + $rest = new SimpleEmailServiceRequest($this, 'POST'); + $rest->setParameter('Action', 'SendEmail'); + + $i = 1; + foreach($sesMessage->to as $to) { + $rest->setParameter('Destination.ToAddresses.member.'.$i, $to); + $i++; + } + + if(is_array($sesMessage->cc)) { + $i = 1; + foreach($sesMessage->cc as $cc) { + $rest->setParameter('Destination.CcAddresses.member.'.$i, $cc); + $i++; + } + } + + if(is_array($sesMessage->bcc)) { + $i = 1; + foreach($sesMessage->bcc as $bcc) { + $rest->setParameter('Destination.BccAddresses.member.'.$i, $bcc); + $i++; + } + } + + if(is_array($sesMessage->replyto)) { + $i = 1; + foreach($sesMessage->replyto as $replyto) { + $rest->setParameter('ReplyToAddresses.member.'.$i, $replyto); + $i++; + } + } + + $rest->setParameter('Source', $sesMessage->from); + + if($sesMessage->returnpath != null) { + $rest->setParameter('ReturnPath', $sesMessage->returnpath); + } + + if($sesMessage->subject != null && strlen($sesMessage->subject) > 0) { + $rest->setParameter('Message.Subject.Data', $sesMessage->subject); + if($sesMessage->subjectCharset != null && strlen($sesMessage->subjectCharset) > 0) { + $rest->setParameter('Message.Subject.Charset', $sesMessage->subjectCharset); + } + } + + + if($sesMessage->messagetext != null && strlen($sesMessage->messagetext) > 0) { + $rest->setParameter('Message.Body.Text.Data', $sesMessage->messagetext); + if($sesMessage->messageTextCharset != null && strlen($sesMessage->messageTextCharset) > 0) { + $rest->setParameter('Message.Body.Text.Charset', $sesMessage->messageTextCharset); + } + } + + if($sesMessage->messagehtml != null && strlen($sesMessage->messagehtml) > 0) { + $rest->setParameter('Message.Body.Html.Data', $sesMessage->messagehtml); + if($sesMessage->messageHtmlCharset != null && strlen($sesMessage->messageHtmlCharset) > 0) { + $rest->setParameter('Message.Body.Html.Charset', $sesMessage->messageHtmlCharset); + } + } + + $rest = $rest->getResponse(); + if($rest->error === false && $rest->code !== 200) { + $rest->error = array('code' => $rest->code, 'message' => 'Unexpected HTTP status'); + } + if($rest->error !== false) { + $this->__triggerError('sendEmail', $rest->error); + return false; + } + + $response['MessageId'] = (string)$rest->body->SendEmailResult->MessageId; + $response['RequestId'] = (string)$rest->body->ResponseMetadata->RequestId; + return $response; + } + + /** + * Trigger an error message + * + * @internal Used by member functions to output errors + * @param array $error Array containing error information + * @return string + */ + public function __triggerError($functionname, $error) + { + if($error == false) { + trigger_error(sprintf("SimpleEmailService::%s(): Encountered an error, but no description given", $functionname), E_USER_WARNING); + } + else if(isset($error['curl']) && $error['curl']) + { + trigger_error(sprintf("SimpleEmailService::%s(): %s %s", $functionname, $error['code'], $error['message']), E_USER_WARNING); + } + else if(isset($error['Error'])) + { + $e = $error['Error']; + $message = sprintf("SimpleEmailService::%s(): %s - %s: %s\nRequest Id: %s\n", $functionname, $e['Type'], $e['Code'], $e['Message'], $error['RequestId']); + trigger_error($message, E_USER_WARNING); + } + } + + /** + * Callback handler for 503 retries. + * + * @internal Used by SimpleDBRequest to call the user-specified callback, if set + * @param $attempt The number of failed attempts so far + * @return The retry delay in microseconds, or 0 to stop retrying. + */ + public function __executeServiceTemporarilyUnavailableRetryDelay($attempt) + { + if(is_callable($this->__serviceUnavailableRetryDelayCallback)) { + $callback = $this->__serviceUnavailableRetryDelayCallback; + return $callback($attempt); + } + return 0; + } +} + +final class SimpleEmailServiceRequest +{ + private $ses, $verb, $parameters = array(); + public $response; + + /** + * Constructor + * + * @param string $ses The SimpleEmailService object making this request + * @param string $action action + * @param string $verb HTTP verb + * @return mixed + */ + function __construct($ses, $verb) { + $this->ses = $ses; + $this->verb = $verb; + $this->response = new STDClass; + $this->response->error = false; + } + + /** + * Set request parameter + * + * @param string $key Key + * @param string $value Value + * @param boolean $replace Whether to replace the key if it already exists (default true) + * @return void + */ + public function setParameter($key, $value, $replace = true) { + if(!$replace && isset($this->parameters[$key])) + { + $temp = (array)($this->parameters[$key]); + $temp[] = $value; + $this->parameters[$key] = $temp; + } + else + { + $this->parameters[$key] = $value; + } + } + + /** + * Get the response + * + * @return object | false + */ + public function getResponse() { + + $params = array(); + foreach ($this->parameters as $var => $value) + { + if(is_array($value)) + { + foreach($value as $v) + { + $params[] = $var.'='.$this->__customUrlEncode($v); + } + } + else + { + $params[] = $var.'='.$this->__customUrlEncode($value); + } + } + + sort($params, SORT_STRING); + + // must be in format 'Sun, 06 Nov 1994 08:49:37 GMT' + $date = gmdate('D, d M Y H:i:s e'); + + $query = implode('&', $params); + + $headers = array(); + $headers[] = 'Date: '.$date; + $headers[] = 'Host: '.$this->ses->getHost(); + + $auth = 'AWS3-HTTPS AWSAccessKeyId='.$this->ses->getAccessKey(); + $auth .= ',Algorithm=HmacSHA256,Signature='.$this->__getSignature($date); + $headers[] = 'X-Amzn-Authorization: '.$auth; + + $url = 'https://'.$this->ses->getHost().'/'; + + // Basic setup + $curl = curl_init(); + curl_setopt($curl, CURLOPT_USERAGENT, 'SimpleEmailService/php'); + + curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, ($this->ses->verifyHost() ? 1 : 0)); + curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, ($this->ses->verifyPeer() ? 1 : 0)); + + // Request types + switch ($this->verb) { + case 'GET': + $url .= '?'.$query; + break; + case 'POST': + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb); + curl_setopt($curl, CURLOPT_POSTFIELDS, $query); + $headers[] = 'Content-Type: application/x-www-form-urlencoded'; + break; + case 'DELETE': + $url .= '?'.$query; + curl_setopt($curl, CURLOPT_CUSTOMREQUEST, 'DELETE'); + break; + default: break; + } + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_HEADER, false); + + curl_setopt($curl, CURLOPT_URL, $url); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, false); + curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback')); + curl_setopt($curl, CURLOPT_FOLLOWLOCATION, true); + + // Execute, grab errors + if (curl_exec($curl)) { + $this->response->code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + } else { + $this->response->error = array( + 'curl' => true, + 'code' => curl_errno($curl), + 'message' => curl_error($curl), + 'resource' => $this->resource + ); + } + + @curl_close($curl); + + // Parse body into XML + if ($this->response->error === false && isset($this->response->body)) { + $this->response->body = simplexml_load_string($this->response->body); + + // Grab SES errors + if (!in_array($this->response->code, array(200, 201, 202, 204)) + && isset($this->response->body->Error)) { + $error = $this->response->body->Error; + $output = array(); + $output['curl'] = false; + $output['Error'] = array(); + $output['Error']['Type'] = (string)$error->Type; + $output['Error']['Code'] = (string)$error->Code; + $output['Error']['Message'] = (string)$error->Message; + $output['RequestId'] = (string)$this->response->body->RequestId; + + $this->response->error = $output; + unset($this->response->body); + } + } + + return $this->response; + } + + /** + * CURL write callback + * + * @param resource &$curl CURL resource + * @param string &$data Data + * @return integer + */ + private function __responseWriteCallback(&$curl, &$data) { + $this->response->body .= $data; + return strlen($data); + } + + /** + * Contributed by afx114 + * URL encode the parameters as per http://docs.amazonwebservices.com/AWSECommerceService/latest/DG/index.html?Query_QueryAuth.html + * 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 + * See: http://www.morganney.com/blog/API/AWS-Product-Advertising-API-Requires-a-Signed-Request.php + * + * @param string $var String to encode + * @return string + */ + private function __customUrlEncode($var) { + return str_replace('%7E', '~', rawurlencode($var)); + } + + /** + * Generate the auth string using Hmac-SHA256 + * + * @internal Used by SimpleDBRequest::getResponse() + * @param string $string String to sign + * @return string + */ + private function __getSignature($string) { + return base64_encode(hash_hmac('sha256', $string, $this->ses->getSecretKey(), true)); + } +} + + +final class SimpleEmailServiceMessage { + + // these are public for convenience only + // these are not to be used outside of the SimpleEmailService class! + public $to, $cc, $bcc, $replyto; + public $from, $returnpath; + public $subject, $messagetext, $messagehtml; + public $subjectCharset, $messageTextCharset, $messageHtmlCharset; + + function __construct() { + $to = array(); + $cc = array(); + $bcc = array(); + $replyto = array(); + + $from = null; + $returnpath = null; + + $subject = null; + $messagetext = null; + $messagehtml = null; + + $subjectCharset = null; + $messageTextCharset = null; + $messageHtmlCharset = null; + } + + + /** + * addTo, addCC, addBCC, and addReplyTo have the following behavior: + * If a single address is passed, it is appended to the current list of addresses. + * If an array of addresses is passed, that array is merged into the current list. + */ + function addTo($to) { + if(!is_array($to)) { + $this->to[] = $to; + } + else { + $this->to = array_merge($this->to, $to); + } + } + + function addCC($cc) { + if(!is_array($cc)) { + $this->cc[] = $cc; + } + else { + $this->cc = array_merge($this->cc, $cc); + } + } + + function addBCC($bcc) { + if(!is_array($bcc)) { + $this->bcc[] = $bcc; + } + else { + $this->bcc = array_merge($this->bcc, $bcc); + } + } + + function addReplyTo($replyto) { + if(!is_array($replyto)) { + $this->replyto[] = $replyto; + } + else { + $this->replyto = array_merge($this->replyto, $replyto); + } + } + + function setFrom($from) { + $this->from = $from; + } + + function setReturnPath($returnpath) { + $this->returnpath = $returnpath; + } + + function setSubject($subject) { + $this->subject = $subject; + } + + function setSubjectCharset($charset) { + $this->subjectCharset = $charset; + } + + function setMessageFromString($text, $html = null) { + $this->messagetext = $text; + $this->messagehtml = $html; + } + + function setMessageFromFile($textfile, $htmlfile = null) { + if(file_exists($textfile) && is_file($textfile) && is_readable($textfile)) { + $this->messagetext = file_get_contents($textfile); + } + if(file_exists($htmlfile) && is_file($htmlfile) && is_readable($htmlfile)) { + $this->messagehtml = file_get_contents($htmlfile); + } + } + + function setMessageFromURL($texturl, $htmlurl = null) { + $this->messagetext = file_get_contents($texturl); + if($htmlurl !== null) { + $this->messagehtml = file_get_contents($htmlurl); + } + } + + function setMessageCharset($textCharset, $htmlCharset = null) { + $this->messageTextCharset = $textCharset; + $this->messageHtmlCharset = $htmlCharset; + } + + /** + * Validates whether the message object has sufficient information to submit a request to SES. + * This does not guarantee the message will arrive, nor that the request will succeed; + * instead, it makes sure that no required fields are missing. + * + * This is used internally before attempting a SendEmail or SendRawEmail request, + * but it can be used outside of this file if verification is desired. + * May be useful if e.g. the data is being populated from a form; developers can generally + * use this function to verify completeness instead of writing custom logic. + * + * @return boolean + */ + public function validate() { + if(count($this->to) == 0) + return false; + if($this->from == null || strlen($this->from) == 0) + return false; + if($this->messagetext == null) + return false; + return true; + } +} diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index 8dd50530f0..5f80b68b39 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1,396 +1,398 @@ array( 'Aphront400Response' => 'aphront/response/400', 'Aphront404Response' => 'aphront/response/404', 'AphrontAjaxResponse' => 'aphront/response/ajax', 'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration', 'AphrontController' => 'aphront/controller', 'AphrontDatabaseConnection' => 'storage/connection/base', 'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration', 'AphrontDefaultApplicationController' => 'aphront/default/controller', 'AphrontDialogResponse' => 'aphront/response/dialog', 'AphrontDialogView' => 'view/dialog', 'AphrontErrorView' => 'view/form/error', 'AphrontException' => 'aphront/exception/base', 'AphrontFileResponse' => 'aphront/response/file', 'AphrontFormCheckboxControl' => 'view/form/control/checkbox', 'AphrontFormControl' => 'view/form/control/base', 'AphrontFormDividerControl' => 'view/form/control/divider', 'AphrontFormFileControl' => 'view/form/control/file', 'AphrontFormMarkupControl' => 'view/form/control/markup', 'AphrontFormPasswordControl' => 'view/form/control/password', 'AphrontFormRecaptchaControl' => 'view/form/control/recaptcha', 'AphrontFormSelectControl' => 'view/form/control/select', 'AphrontFormStaticControl' => 'view/form/control/static', 'AphrontFormSubmitControl' => 'view/form/control/submit', 'AphrontFormTextAreaControl' => 'view/form/control/textarea', 'AphrontFormTextControl' => 'view/form/control/text', 'AphrontFormTokenizerControl' => 'view/form/control/tokenizer', 'AphrontFormView' => 'view/form/base', 'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql', 'AphrontNullView' => 'view/null', 'AphrontPageView' => 'view/page/base', 'AphrontPanelView' => 'view/layout/panel', 'AphrontQueryConnectionException' => 'storage/exception/connection', 'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost', 'AphrontQueryCountException' => 'storage/exception/count', 'AphrontQueryDuplicateKeyException' => 'storage/exception/duplicatekey', 'AphrontQueryException' => 'storage/exception/base', 'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing', 'AphrontQueryParameterException' => 'storage/exception/parameter', 'AphrontQueryRecoverableException' => 'storage/exception/recoverable', 'AphrontRedirectException' => 'aphront/exception/redirect', 'AphrontRedirectResponse' => 'aphront/response/redirect', 'AphrontRequest' => 'aphront/request', 'AphrontRequestFailureView' => 'view/page/failure', 'AphrontResponse' => 'aphront/response/base', 'AphrontSideNavView' => 'view/layout/sidenav', 'AphrontTableView' => 'view/control/table', 'AphrontURIMapper' => 'aphront/mapper', 'AphrontView' => 'view/base', 'AphrontWebpageResponse' => 'aphront/response/webpage', 'CelerityAPI' => 'infrastructure/celerity/api', 'CelerityResourceController' => 'infrastructure/celerity/controller', 'CelerityResourceMap' => 'infrastructure/celerity/map', 'CelerityStaticResourceResponse' => 'infrastructure/celerity/response', 'ConduitAPIMethod' => 'applications/conduit/method/base', 'ConduitAPIRequest' => 'applications/conduit/protocol/request', 'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect', 'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/creatediff', 'ConduitAPI_differential_createrevision_Method' => 'applications/conduit/method/differential/createrevision', 'ConduitAPI_differential_find_Method' => 'applications/conduit/method/differential/find', 'ConduitAPI_differential_parsecommitmessage_Method' => 'applications/conduit/method/differential/parsecommitmessage', 'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/setdiffproperty', 'ConduitAPI_differential_updaterevision_Method' => 'applications/conduit/method/differential/updaterevision', 'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload', 'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find', 'ConduitException' => 'applications/conduit/protocol/exception', 'DarkConsole' => 'aphront/console/api', 'DarkConsoleController' => 'aphront/console/controller', 'DarkConsoleCore' => 'aphront/console/core', 'DarkConsoleErrorLogPlugin' => 'aphront/console/plugin/errorlog', 'DarkConsoleErrorLogPluginAPI' => 'aphront/console/plugin/errorlog/api', 'DarkConsolePlugin' => 'aphront/console/plugin/base', 'DarkConsoleRequestPlugin' => 'aphront/console/plugin/request', 'DarkConsoleServicesPlugin' => 'aphront/console/plugin/services', 'DarkConsoleServicesPluginAPI' => 'aphront/console/plugin/services/api', 'DarkConsoleXHProfPlugin' => 'aphront/console/plugin/xhprof', 'DarkConsoleXHProfPluginAPI' => 'aphront/console/plugin/xhprof/api', 'DifferentialAction' => 'applications/differential/constants/action', 'DifferentialAddCommentView' => 'applications/differential/view/addcomment', 'DifferentialCCWelcomeMail' => 'applications/differential/mail/ccwelcome', 'DifferentialChangeType' => 'applications/differential/constants/changetype', 'DifferentialChangeset' => 'applications/differential/storage/changeset', 'DifferentialChangesetDetailView' => 'applications/differential/view/changesetdetailview', 'DifferentialChangesetListView' => 'applications/differential/view/changesetlistview', 'DifferentialChangesetParser' => 'applications/differential/parser/changeset', 'DifferentialChangesetViewController' => 'applications/differential/controller/changesetview', 'DifferentialComment' => 'applications/differential/storage/comment', 'DifferentialCommentEditor' => 'applications/differential/editor/comment', 'DifferentialCommentMail' => 'applications/differential/mail/comment', 'DifferentialCommentPreviewController' => 'applications/differential/controller/commentpreview', 'DifferentialCommentSaveController' => 'applications/differential/controller/commentsave', 'DifferentialCommitMessage' => 'applications/differential/parser/commitmessage', 'DifferentialCommitMessageParserException' => 'applications/differential/parser/commitmessage/exception', 'DifferentialController' => 'applications/differential/controller/base', 'DifferentialDAO' => 'applications/differential/storage/base', 'DifferentialDiff' => 'applications/differential/storage/diff', 'DifferentialDiffContentMail' => 'applications/differential/mail/diffcontent', 'DifferentialDiffCreateController' => 'applications/differential/controller/diffcreate', 'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty', 'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents', 'DifferentialDiffViewController' => 'applications/differential/controller/diffview', 'DifferentialHunk' => 'applications/differential/storage/hunk', 'DifferentialInlineComment' => 'applications/differential/storage/inlinecomment', 'DifferentialInlineCommentEditController' => 'applications/differential/controller/inlinecommentedit', 'DifferentialInlineCommentPreviewController' => 'applications/differential/controller/inlinecommentpreview', 'DifferentialInlineCommentView' => 'applications/differential/view/inlinecomment', 'DifferentialLintStatus' => 'applications/differential/constants/lintstatus', 'DifferentialMail' => 'applications/differential/mail/base', 'DifferentialMarkupEngineFactory' => 'applications/differential/parser/markup', 'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff', 'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest', 'DifferentialRevision' => 'applications/differential/storage/revision', 'DifferentialRevisionCommentListView' => 'applications/differential/view/revisioncommentlist', 'DifferentialRevisionCommentView' => 'applications/differential/view/revisioncomment', 'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem', 'DifferentialRevisionDetailView' => 'applications/differential/view/revisiondetail', 'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit', 'DifferentialRevisionEditor' => 'applications/differential/editor/revision', 'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist', 'DifferentialRevisionListData' => 'applications/differential/data/revisionlist', 'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus', 'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/revisionupdatehistory', 'DifferentialRevisionViewController' => 'applications/differential/controller/revisionview', 'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus', 'Javelin' => 'infrastructure/javelin/api', 'LiskDAO' => 'storage/lisk/dao', 'Phabricator404Controller' => 'applications/base/controller/404', 'PhabricatorAuthController' => 'applications/auth/controller/base', 'PhabricatorConduitAPIController' => 'applications/conduit/controller/api', 'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog', 'PhabricatorConduitConsoleController' => 'applications/conduit/controller/console', 'PhabricatorConduitController' => 'applications/conduit/controller/base', 'PhabricatorConduitDAO' => 'applications/conduit/storage/base', 'PhabricatorConduitLogController' => 'applications/conduit/controller/log', 'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog', 'PhabricatorController' => 'applications/base/controller/base', 'PhabricatorDirectoryCategory' => 'applications/directory/storage/category', 'PhabricatorDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete', 'PhabricatorDirectoryCategoryEditController' => 'applications/directory/controller/categoryedit', 'PhabricatorDirectoryCategoryListController' => 'applications/directory/controller/categorylist', 'PhabricatorDirectoryController' => 'applications/directory/controller/base', 'PhabricatorDirectoryDAO' => 'applications/directory/storage/base', 'PhabricatorDirectoryItem' => 'applications/directory/storage/item', 'PhabricatorDirectoryItemDeleteController' => 'applications/directory/controller/itemdelete', 'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit', 'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist', 'PhabricatorDirectoryMainController' => 'applications/directory/controller/main', 'PhabricatorDraft' => 'applications/draft/storage/draft', 'PhabricatorDraftDAO' => 'applications/draft/storage/base', 'PhabricatorEmailLoginController' => 'applications/auth/controller/email', 'PhabricatorEmailTokenController' => 'applications/auth/controller/emailtoken', 'PhabricatorEnv' => 'infrastructure/env', 'PhabricatorFacebookAuthController' => 'applications/auth/controller/facebookauth', 'PhabricatorFacebookAuthDiagnosticsController' => 'applications/auth/controller/facebookauth/diagnostics', 'PhabricatorFile' => 'applications/files/storage/file', 'PhabricatorFileController' => 'applications/files/controller/base', 'PhabricatorFileDAO' => 'applications/files/storage/base', 'PhabricatorFileListController' => 'applications/files/controller/list', 'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob', 'PhabricatorFileURI' => 'applications/files/uri', 'PhabricatorFileUploadController' => 'applications/files/controller/upload', 'PhabricatorFileViewController' => 'applications/files/controller/view', 'PhabricatorLiskDAO' => 'applications/base/storage/lisk', 'PhabricatorLoginController' => 'applications/auth/controller/login', 'PhabricatorLogoutController' => 'applications/auth/controller/logout', 'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/base', + 'PhabricatorMailImplementationAmazonSESAdapter' => 'applications/metamta/adapter/amazonses', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite', 'PhabricatorMetaMTAController' => 'applications/metamta/controller/base', 'PhabricatorMetaMTADAO' => 'applications/metamta/storage/base', 'PhabricatorMetaMTAListController' => 'applications/metamta/controller/list', 'PhabricatorMetaMTAMail' => 'applications/metamta/storage/mail', 'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist', 'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit', 'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists', 'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send', 'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view', 'PhabricatorObjectHandle' => 'applications/phid/handle', 'PhabricatorObjectHandleData' => 'applications/phid/handle/data', 'PhabricatorPHID' => 'applications/phid/storage/phid', 'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate', 'PhabricatorPHIDController' => 'applications/phid/controller/base', 'PhabricatorPHIDDAO' => 'applications/phid/storage/base', 'PhabricatorPHIDListController' => 'applications/phid/controller/list', 'PhabricatorPHIDLookupController' => 'applications/phid/controller/lookup', 'PhabricatorPHIDType' => 'applications/phid/storage/type', 'PhabricatorPHIDTypeEditController' => 'applications/phid/controller/typeedit', 'PhabricatorPHIDTypeListController' => 'applications/phid/controller/typelist', 'PhabricatorPeopleController' => 'applications/people/controller/base', 'PhabricatorPeopleEditController' => 'applications/people/controller/edit', 'PhabricatorPeopleListController' => 'applications/people/controller/list', 'PhabricatorPeopleProfileController' => 'applications/people/controller/profile', 'PhabricatorStandardPageView' => 'view/page/standard', 'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common', 'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base', 'PhabricatorUser' => 'applications/people/storage/user', 'PhabricatorUserDAO' => 'applications/people/storage/base', 'PhabricatorUserSettingsController' => 'applications/people/controller/settings', 'PhabricatorXHProfController' => 'applications/xhprof/controller/base', 'PhabricatorXHProfProfileController' => 'applications/xhprof/controller/profile', 'PhabricatorXHProfProfileSymbolView' => 'applications/xhprof/view/symbol', 'PhabricatorXHProfProfileTopLevelView' => 'applications/xhprof/view/toplevel', ), 'function' => array( '_qsprintf_check_scalar_type' => 'storage/qsprintf', '_qsprintf_check_type' => 'storage/qsprintf', 'celerity_generate_unique_node_id' => 'infrastructure/celerity/api', 'celerity_register_resource_map' => 'infrastructure/celerity/map', 'javelin_render_tag' => 'infrastructure/javelin/markup', 'phabricator_format_relative_time' => 'view/utils', 'phabricator_format_timestamp' => 'view/utils', 'phabricator_format_units_generic' => 'view/utils', 'qsprintf' => 'storage/qsprintf', 'queryfx' => 'storage/queryfx', 'queryfx_all' => 'storage/queryfx', 'queryfx_one' => 'storage/queryfx', 'require_celerity_resource' => 'infrastructure/celerity/api', 'vqsprintf' => 'storage/qsprintf', 'vqueryfx' => 'storage/queryfx', 'vqueryfx_all' => 'storage/queryfx', 'xsprintf_query' => 'storage/qsprintf', ), 'requires_class' => array( 'Aphront400Response' => 'AphrontResponse', 'Aphront404Response' => 'AphrontResponse', 'AphrontAjaxResponse' => 'AphrontResponse', 'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration', 'AphrontDefaultApplicationController' => 'AphrontController', 'AphrontDialogResponse' => 'AphrontResponse', 'AphrontDialogView' => 'AphrontView', 'AphrontErrorView' => 'AphrontView', 'AphrontFileResponse' => 'AphrontResponse', 'AphrontFormCheckboxControl' => 'AphrontFormControl', 'AphrontFormControl' => 'AphrontView', 'AphrontFormDividerControl' => 'AphrontFormControl', 'AphrontFormFileControl' => 'AphrontFormControl', 'AphrontFormMarkupControl' => 'AphrontFormControl', 'AphrontFormPasswordControl' => 'AphrontFormControl', 'AphrontFormRecaptchaControl' => 'AphrontFormControl', 'AphrontFormSelectControl' => 'AphrontFormControl', 'AphrontFormStaticControl' => 'AphrontFormControl', 'AphrontFormSubmitControl' => 'AphrontFormControl', 'AphrontFormTextAreaControl' => 'AphrontFormControl', 'AphrontFormTextControl' => 'AphrontFormControl', 'AphrontFormTokenizerControl' => 'AphrontFormControl', 'AphrontFormView' => 'AphrontView', 'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection', 'AphrontNullView' => 'AphrontView', 'AphrontPageView' => 'AphrontView', 'AphrontPanelView' => 'AphrontView', 'AphrontQueryConnectionException' => 'AphrontQueryException', 'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException', 'AphrontQueryCountException' => 'AphrontQueryException', 'AphrontQueryDuplicateKeyException' => 'AphrontQueryException', 'AphrontQueryObjectMissingException' => 'AphrontQueryException', 'AphrontQueryParameterException' => 'AphrontQueryException', 'AphrontQueryRecoverableException' => 'AphrontQueryException', 'AphrontRedirectException' => 'AphrontException', 'AphrontRedirectResponse' => 'AphrontResponse', 'AphrontRequestFailureView' => 'AphrontView', 'AphrontSideNavView' => 'AphrontView', 'AphrontTableView' => 'AphrontView', 'AphrontWebpageResponse' => 'AphrontResponse', 'CelerityResourceController' => 'AphrontController', 'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_createrevision_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_find_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_parsecommitmessage_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod', 'ConduitAPI_differential_updaterevision_Method' => 'ConduitAPIMethod', 'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod', 'ConduitAPI_user_find_Method' => 'ConduitAPIMethod', 'DarkConsoleController' => 'PhabricatorController', 'DarkConsoleErrorLogPlugin' => 'DarkConsolePlugin', 'DarkConsoleRequestPlugin' => 'DarkConsolePlugin', 'DarkConsoleServicesPlugin' => 'DarkConsolePlugin', 'DarkConsoleXHProfPlugin' => 'DarkConsolePlugin', 'DifferentialAddCommentView' => 'AphrontView', 'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail', 'DifferentialChangeset' => 'DifferentialDAO', 'DifferentialChangesetDetailView' => 'AphrontView', 'DifferentialChangesetListView' => 'AphrontView', 'DifferentialChangesetViewController' => 'DifferentialController', 'DifferentialComment' => 'DifferentialDAO', 'DifferentialCommentMail' => 'DifferentialMail', 'DifferentialCommentPreviewController' => 'DifferentialController', 'DifferentialCommentSaveController' => 'DifferentialController', 'DifferentialController' => 'PhabricatorController', 'DifferentialDAO' => 'PhabricatorLiskDAO', 'DifferentialDiff' => 'DifferentialDAO', 'DifferentialDiffContentMail' => 'DifferentialMail', 'DifferentialDiffCreateController' => 'DifferentialController', 'DifferentialDiffProperty' => 'DifferentialDAO', 'DifferentialDiffTableOfContentsView' => 'AphrontView', 'DifferentialDiffViewController' => 'DifferentialController', 'DifferentialHunk' => 'DifferentialDAO', 'DifferentialInlineComment' => 'DifferentialDAO', 'DifferentialInlineCommentEditController' => 'DifferentialController', 'DifferentialInlineCommentPreviewController' => 'DifferentialController', 'DifferentialInlineCommentView' => 'AphrontView', 'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail', 'DifferentialReviewRequestMail' => 'DifferentialMail', 'DifferentialRevision' => 'DifferentialDAO', 'DifferentialRevisionCommentListView' => 'AphrontView', 'DifferentialRevisionCommentView' => 'AphrontView', 'DifferentialRevisionDetailView' => 'AphrontView', 'DifferentialRevisionEditController' => 'DifferentialController', 'DifferentialRevisionListController' => 'DifferentialController', 'DifferentialRevisionUpdateHistoryView' => 'AphrontView', 'DifferentialRevisionViewController' => 'DifferentialController', 'Phabricator404Controller' => 'PhabricatorController', 'PhabricatorAuthController' => 'PhabricatorController', 'PhabricatorConduitAPIController' => 'PhabricatorConduitController', 'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO', 'PhabricatorConduitConsoleController' => 'PhabricatorConduitController', 'PhabricatorConduitController' => 'PhabricatorController', 'PhabricatorConduitDAO' => 'PhabricatorLiskDAO', 'PhabricatorConduitLogController' => 'PhabricatorConduitController', 'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO', 'PhabricatorController' => 'AphrontController', 'PhabricatorDirectoryCategory' => 'PhabricatorDirectoryDAO', 'PhabricatorDirectoryCategoryDeleteController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryCategoryEditController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryCategoryListController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryController' => 'PhabricatorController', 'PhabricatorDirectoryDAO' => 'PhabricatorLiskDAO', 'PhabricatorDirectoryItem' => 'PhabricatorDirectoryDAO', 'PhabricatorDirectoryItemDeleteController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController', 'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController', 'PhabricatorDraft' => 'PhabricatorDraftDAO', 'PhabricatorDraftDAO' => 'PhabricatorLiskDAO', 'PhabricatorEmailLoginController' => 'PhabricatorAuthController', 'PhabricatorEmailTokenController' => 'PhabricatorAuthController', 'PhabricatorFacebookAuthController' => 'PhabricatorAuthController', 'PhabricatorFacebookAuthDiagnosticsController' => 'PhabricatorAuthController', 'PhabricatorFile' => 'PhabricatorFileDAO', 'PhabricatorFileController' => 'PhabricatorController', 'PhabricatorFileDAO' => 'PhabricatorLiskDAO', 'PhabricatorFileListController' => 'PhabricatorFileController', 'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO', 'PhabricatorFileUploadController' => 'PhabricatorFileController', 'PhabricatorFileViewController' => 'PhabricatorFileController', 'PhabricatorLiskDAO' => 'LiskDAO', 'PhabricatorLoginController' => 'PhabricatorAuthController', 'PhabricatorLogoutController' => 'PhabricatorAuthController', + 'PhabricatorMailImplementationAmazonSESAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter', 'PhabricatorMetaMTAController' => 'PhabricatorController', 'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO', 'PhabricatorMetaMTAListController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO', 'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController', 'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController', 'PhabricatorPHID' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController', 'PhabricatorPHIDController' => 'PhabricatorController', 'PhabricatorPHIDDAO' => 'PhabricatorLiskDAO', 'PhabricatorPHIDListController' => 'PhabricatorPHIDController', 'PhabricatorPHIDLookupController' => 'PhabricatorPHIDController', 'PhabricatorPHIDType' => 'PhabricatorPHIDDAO', 'PhabricatorPHIDTypeEditController' => 'PhabricatorPHIDController', 'PhabricatorPHIDTypeListController' => 'PhabricatorPHIDController', 'PhabricatorPeopleController' => 'PhabricatorController', 'PhabricatorPeopleEditController' => 'PhabricatorPeopleController', 'PhabricatorPeopleListController' => 'PhabricatorPeopleController', 'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController', 'PhabricatorStandardPageView' => 'AphrontPageView', 'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController', 'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController', 'PhabricatorUser' => 'PhabricatorUserDAO', 'PhabricatorUserDAO' => 'PhabricatorLiskDAO', 'PhabricatorUserSettingsController' => 'PhabricatorPeopleController', 'PhabricatorXHProfController' => 'PhabricatorController', 'PhabricatorXHProfProfileController' => 'PhabricatorXHProfController', 'PhabricatorXHProfProfileSymbolView' => 'AphrontView', 'PhabricatorXHProfProfileTopLevelView' => 'AphrontView', ), 'requires_interface' => array( ), )); diff --git a/src/applications/metamta/adapter/amazonses/PhabricatorMailImplementationAmazonSESAdapter.php b/src/applications/metamta/adapter/amazonses/PhabricatorMailImplementationAmazonSESAdapter.php new file mode 100644 index 0000000000..aa73909f11 --- /dev/null +++ b/src/applications/metamta/adapter/amazonses/PhabricatorMailImplementationAmazonSESAdapter.php @@ -0,0 +1,94 @@ +message = newv('SimpleEmailServiceMessage', array()); + } + + public function setFrom($email) { + $this->message->setFrom($email); + return $this; + } + + public function addReplyTo($email) { + $this->message->addReplyTo($email); + return $this; + } + + public function addTos(array $emails) { + foreach ($emails as $email) { + $this->message->addTo($email); + } + return $this; + } + + public function addCCs(array $emails) { + foreach ($emails as $email) { + $this->message->addCC($email); + } + return $this; + } + + public function addHeader($header_name, $header_value) { + // SES does not currently support custom headers. + return $this; + } + + public function setBody($body) { + $this->body = $body; + return $this; + } + + public function setSubject($subject) { + $this->message->setSubject($subject); + return $this; + } + + public function setIsHTML($is_html) { + $this->isHTML = true; + return $this; + } + + public function hasValidRecipients() { + return true; + } + + public function send() { + if ($this->isHTML) { + $this->message->setMessageFromString($this->body, $this->body); + } else { + $this->message->setMessageFromString($this->body); + } + + $key = PhabricatorEnv::getEnvConfig('amazon-ses.access-key'); + $secret = PhabricatorEnv::getEnvConfig('amazon-ses.secret-key'); + + $service = new SimpleEmailService($key, $secret); + return $service->sendEmail($this->message); + } + +} diff --git a/src/applications/metamta/storage/mail/__init__.php b/src/applications/metamta/adapter/amazonses/__init__.php similarity index 55% copy from src/applications/metamta/storage/mail/__init__.php copy to src/applications/metamta/adapter/amazonses/__init__.php index 715dad61dd..5fb8d3710c 100644 --- a/src/applications/metamta/storage/mail/__init__.php +++ b/src/applications/metamta/adapter/amazonses/__init__.php @@ -1,17 +1,16 @@ status = self::STATUS_QUEUE; $this->retryCount = 0; $this->nextRetry = time(); $this->parameters = array(); parent::__construct(); } public function getConfiguration() { return array( self::CONFIG_SERIALIZATION => array( 'parameters' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } protected function setParam($param, $value) { $this->parameters[$param] = $value; return $this; } protected function getParam($param) { return idx($this->parameters, $param); } public function getSubject() { return $this->getParam('subject'); } public function addTos(array $phids) { $this->setParam('to', $phids); return $this; } public function addCCs(array $phids) { $this->setParam('cc', $phids); return $this; } public function addHeader($name, $value) { $this->parameters['headers'][$name] = $value; return $this; } public function setFrom($from) { $this->setParam('from', $from); return $this; } public function setReplyTo($reply_to) { $this->setParam('reply-to', $reply_to); return $this; } public function setSubject($subject) { $this->setParam('subject', $subject); return $this; } public function setBody($body) { $this->setParam('body', $body); return $this; } public function setIsHTML($html) { $this->setParam('is-html', $html); return $this; } public function getSimulatedFailureCount() { return nonempty($this->getParam('simulated-failures'), 0); } public function setSimulatedFailureCount($count) { $this->setParam('simulated-failures', $count); return $this; } public function save() { $try_send = (PhabricatorEnv::getEnvConfig('metamta.send-immediately')) && (!$this->getID()); $ret = parent::save(); if ($try_send) { - $mailer = new PhabricatorMailImplementationPHPMailerLiteAdapter(); + $class_name = PhabricatorEnv::getEnvConfig('metamta.mail-adapter'); + PhutilSymbolLoader::loadClass($class_name); + $mailer = newv($class_name, array()); $this->sendNow($force_send = false, $mailer); } return $ret; } public function sendNow( $force_send = false, PhabricatorMailImplementationAdapter $mailer) { if (!$force_send) { if ($this->getStatus() != self::STATUS_QUEUE) { throw new Exception("Trying to send an already-sent mail!"); } if (time() < $this->getNextRetry()) { throw new Exception("Trying to send an email before next retry!"); } } try { $parameters = $this->parameters; $phids = array(); foreach ($parameters as $key => $value) { switch ($key) { case 'from': case 'to': case 'cc': if (!is_array($value)) { $value = array($value); } foreach (array_filter($value) as $phid) { $phids[] = $phid; } break; } } $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); foreach ($this->parameters as $key => $value) { switch ($key) { case 'from': $mailer->setFrom($handles[$value]->getEmail()); break; case 'reply-to': $mailer->addReplyTo($value); break; case 'to': $emails = array(); foreach ($value as $phid) { $emails[] = $handles[$phid]->getEmail(); } $mailer->addTos($emails); break; case 'cc': $emails = array(); foreach ($value as $phid) { $emails[] = $handles[$phid]->getEmail(); } $mailer->addCCs($emails); break; case 'headers': foreach ($value as $header_key => $header_value) { $mailer->addHeader($header_key, $header_value); } break; case 'body': $mailer->setBody($value); break; case 'subject': $mailer->setSubject($value); break; case 'is-html': if ($value) { $mailer->setIsHTML(true); } break; default: // Just discard. } } $mailer->addHeader('X-Mail-Transport-Agent', 'MetaMTA'); } catch (Exception $ex) { $this->setStatus(self::STATUS_FAIL); $this->setMessage($ex->getMessage()); $this->save(); return; } if ($this->getRetryCount() < $this->getSimulatedFailureCount()) { $ok = false; $error = 'Simulated failure.'; } else { try { $ok = $mailer->send(); + $error = null; } catch (Exception $ex) { $ok = false; $error = $ex->getMessage(); } } if (!$ok) { $this->setMessage($error); if ($this->getRetryCount() > self::MAX_RETRIES) { $this->setStatus(self::STATUS_FAIL); } else { $this->setRetryCount($this->getRetryCount() + 1); $next_retry = time() + ($this->getRetryCount() * self::RETRY_DELAY); $this->setNextRetry($next_retry); } } else { $this->setStatus(self::STATUS_SENT); } $this->save(); } public static function getReadableStatus($status_code) { static $readable = array( self::STATUS_QUEUE => "Queued for Delivery", self::STATUS_FAIL => "Delivery Failed", self::STATUS_SENT => "Sent", ); $status_code = coalesce($status_code, '?'); return idx($readable, $status_code, $status_code); } } diff --git a/src/applications/metamta/storage/mail/__init__.php b/src/applications/metamta/storage/mail/__init__.php index 715dad61dd..8e7330cba4 100644 --- a/src/applications/metamta/storage/mail/__init__.php +++ b/src/applications/metamta/storage/mail/__init__.php @@ -1,17 +1,17 @@