Skip to content

Instantly share code, notes, and snippets.

@mote0230
Forked from rantastic/mail.php
Last active April 27, 2024 08:23
Show Gist options
  • Select an option

  • Save mote0230/624229e33e253ebaf34a40d9fca8ea25 to your computer and use it in GitHub Desktop.

Select an option

Save mote0230/624229e33e253ebaf34a40d9fca8ea25 to your computer and use it in GitHub Desktop.

Revisions

  1. mote0230 revised this gist Oct 12, 2020. 2 changed files with 92 additions and 40 deletions.
    4 changes: 3 additions & 1 deletion mail.php
    Original file line number Diff line number Diff line change
    @@ -5,7 +5,9 @@
    require_once('ses.php');

    //get credentials at http://aws.amazon.com My Account / Console > Security Credentials
    $ses = new SimpleEmailService('ACCESSkeyID', 'SECRETaccessKEY');
    $ses = new SimpleEmailService('ACCESSkeyID', 'SECRETaccessKEY', 'eu-west-1', 'email.eu-west-1.amazonaws.com');

    //print_r($ses->listVerifiedEmailAddresses());

    $m = new SimpleEmailServiceMessage();
    //note that from and to emails must be verified using AWS SES dashboard. Can remove limitations here https://aws-portal.amazon.com/gp/aws/html-forms-controller/contactus/SESProductionAccess2011Q3.
    128 changes: 89 additions & 39 deletions ses.php
    Original file line number Diff line number Diff line change
    @@ -48,11 +48,13 @@ class SimpleEmailService
    {
    protected $__accessKey; // AWS Access key
    protected $__secretKey; // AWS Secret key
    protected $__region; //AWS Region
    protected $__host;

    public function getAccessKey() { return $this->__accessKey; }
    public function getSecretKey() { return $this->__secretKey; }
    public function getHost() { return $this->__host; }
    public function getRegion() { return $this->__region; }

    protected $__verifyHost = 1;
    protected $__verifyPeer = 1;
    @@ -73,10 +75,11 @@ public function enableVerifyPeer($enable = true) { $this->__verifyPeer = $enable
    * @param string $secretKey Secret key
    * @return void
    */
    public function __construct($accessKey = null, $secretKey = null, $host = 'email.us-east-1.amazonaws.com') {
    public function __construct($accessKey = null, $secretKey = null, $region = 'us-east-1', $host = 'email.us-east-1.amazonaws.com') {
    if ($accessKey !== null && $secretKey !== null) {
    $this->setAuth($accessKey, $secretKey);
    }
    $this->__region = $region;
    $this->__host = $host;
    }

    @@ -363,14 +366,14 @@ public function __triggerError($functionname, $error)
    trigger_error($message, E_USER_WARNING);
    }
    else {
    trigger_error(sprintf("SimpleEmailService::%s(): Encountered an error: %s", $functionname, $error), E_USER_WARNING);
    trigger_error(sprintf("SimpleEmailService::%s(): Encountered an error: %s", $functionname, implode($error)), E_USER_WARNING);
    }
    }
    }

    final class SimpleEmailServiceRequest
    {
    private $ses, $verb, $parameters = array();
    private $ses, $verb, $parameters = array(), $headers, $date, $amz_date;
    public $response;

    /**
    @@ -384,8 +387,11 @@ final class SimpleEmailServiceRequest
    function __construct($ses, $verb) {
    $this->ses = $ses;
    $this->verb = $verb;
    $this->headers = [];
    $this->response = new STDClass;
    $this->response->error = false;
    $this->date = gmdate('Ymd');
    $this->amz_date = gmdate('Ymd\THis\Z');
    }

    /**
    @@ -409,51 +415,91 @@ public function setParameter($key, $value, $replace = true) {
    }
    }

    /**
    * Create and returns binary hmac sha256
    *
    * @return hmac sha256.
    */
    private function _generateSignatureKey()
    {
    $date_h = hash_hmac('sha256', $this->date, 'AWS4' . $this->ses->getSecretkey(), true);
    $region_h = hash_hmac('sha256', $this->ses->getRegion(), $date_h, true);
    $service_h = hash_hmac('sha256', 'email', $region_h, true);
    $signing_h = hash_hmac('sha256', 'aws4_request', $service_h, true);

    return $signing_h;
    }


    /**
    * Signing AWS Requests with Signature Version 4
    *
    * @param array $parameters It contains request parameters.
    *
    * @ref http://docs.aws.amazon.com/general/latest/gr/sigv4_signing.html
    *
    * @return void
    */
    private function _generateSignature($parameters = array())
    {
    $this->headers = [];
    $canonical_uri = '/';

    ksort($parameters);

    $query_parameters = '';
    $canonical_headers = '';
    $signed_headers = '';
    $body = '';
    $qp = http_build_query($parameters, '', '&', PHP_QUERY_RFC3986);
    if ($parameters['Action'] == "SendRawEmail" || $parameters['Action'] == "SendEmail") {
    // $query_parameters = $qp;
    $body = $qp;
    $canonical_headers .= 'content-type:' . 'application/x-www-form-urlencoded' . "\n";
    $signed_headers .= 'content-type;';
    // $this->headers['Content-Type'] = 'application/x-www-form-urlencoded';
    } else {
    $query_parameters = $qp;
    }


    $canonical_headers .= 'host:email.' . $this->ses->getRegion() . '.amazonaws.com' . "\n" . 'x-amz-date:' . $this->amz_date . "\n";
    $signed_headers .= 'host;x-amz-date';
    $payload_hash = hash('sha256', $body);

    // task1
    $canonical_request = $this->verb . "\n" . $canonical_uri . "\n" . $query_parameters . "\n" . $canonical_headers . "\n" . $signed_headers . "\n" . $payload_hash;

    // task2
    $credential_scope = $this->date . '/' . $this->ses->getRegion() . '/email/aws4_request';
    $string_to_sign = 'AWS4-HMAC-SHA256' . "\n" . $this->amz_date . "\n" . $credential_scope . "\n" . hash('sha256', $canonical_request);

    // task3
    $signing_key = $this->_generateSignatureKey();
    $signature = hash_hmac('sha256', $string_to_sign, $signing_key);
    $this->headers[] = 'Authorization: ' . 'AWS4-HMAC-SHA256' . ' Credential=' . $this->ses->getAccessKey() . '/' . $credential_scope . ', SignedHeaders=' . $signed_headers . ', Signature=' . $signature;
    $this->headers[] = 'x-amz-date: ' . $this->amz_date;
    $this->headers[] = 'Date: ' . $this->date;
    $this->headers[] = 'Host: ' . $this->ses->getHost();

    return $qp;
    }

    /**
    * 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;
    $query = $this->_generateSignature($this->parameters);

    $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_VERIFYHOST, ($this->ses->verifyHost() ? 2 : 0));
    curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, ($this->ses->verifyPeer() ? 1 : 0));

    // Request types
    @@ -464,17 +510,17 @@ public function getResponse() {
    case 'POST':
    curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $this->verb);
    curl_setopt($curl, CURLOPT_POSTFIELDS, $query);
    $headers[] = 'Content-Type: application/x-www-form-urlencoded';
    $this->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_HTTPHEADER, $this->headers);
    curl_setopt($curl, CURLOPT_HEADER, false);

    //curl_setopt($curl, CURLOPT_VERBOSE, true);
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, false);
    curl_setopt($curl, CURLOPT_WRITEFUNCTION, array(&$this, '__responseWriteCallback'));
    @@ -526,7 +572,11 @@ public function getResponse() {
    * @return integer
    */
    private function __responseWriteCallback(&$curl, &$data) {
    $this->response->body .= $data;
    if (!isset($this->response->body)) {
    $this->response->body = $data;
    } else {
    $this->response->body .= $data;
    }
    return strlen($data);
    }

  2. @rantastic rantastic revised this gist Apr 16, 2012. 1 changed file with 2 additions and 1 deletion.
    3 changes: 2 additions & 1 deletion mail.php
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,6 @@
    <?php
    //http://www.orderingdisorder.com/aws/ses/

    //class docs: http://www.orderingdisorder.com/aws/ses/

    require_once('ses.php');

  3. @rantastic rantastic created this gist Apr 16, 2012.
    17 changes: 17 additions & 0 deletions mail.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,17 @@
    <?php
    //http://www.orderingdisorder.com/aws/ses/

    require_once('ses.php');

    //get credentials at http://aws.amazon.com My Account / Console > Security Credentials
    $ses = new SimpleEmailService('ACCESSkeyID', 'SECRETaccessKEY');

    $m = new SimpleEmailServiceMessage();
    //note that from and to emails must be verified using AWS SES dashboard. Can remove limitations here https://aws-portal.amazon.com/gp/aws/html-forms-controller/contactus/SESProductionAccess2011Q3.
    $m->addTo('[email protected]');
    $m->setFrom('John Doe <[email protected]>');
    $m->setSubject('Amazon php SES test');
    $m->setMessageFromString('Message Body');

    print_r($ses->sendEmail($m));
    ?>
    707 changes: 707 additions & 0 deletions ses.php
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,707 @@
    <?php
    /**
    *
    * Copyright (c) 2011, Dan Myers.
    * Parts copyright (c) 2008, Donovan Schonknecht.
    * All rights reserved.
    *
    * Redistribution and use in source and binary forms, with or without
    * modification, are permitted provided that the following conditions are met:
    *
    * - Redistributions of source code must retain the above copyright notice,
    * this list of conditions and the following disclaimer.
    * - Redistributions in binary form must reproduce the above copyright
    * notice, this list of conditions and the following disclaimer in the
    * documentation and/or other materials provided with the distribution.
    *
    * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
    * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
    * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
    * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
    * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
    * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
    * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
    * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
    * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    * POSSIBILITY OF SUCH DAMAGE.
    *
    * This is a modified BSD license (the third clause has been removed).
    * The BSD license may be found here:
    * http://www.opensource.org/licenses/bsd-license.php
    *
    * Amazon Simple Email Service is a trademark of Amazon.com, Inc. or its affiliates.
    *
    * SimpleEmailService is based on Donovan Schonknecht's Amazon S3 PHP class, found here:
    * http://undesigned.org.za/2007/10/22/amazon-s3-php-class
    *
    */

    /**
    * Amazon SimpleEmailService PHP class
    *
    * @link http://sourceforge.net/projects/php-aws-ses/
    * version 0.8.2
    *
    */
    class SimpleEmailService
    {
    protected $__accessKey; // AWS Access key
    protected $__secretKey; // AWS Secret key
    protected $__host;

    public function getAccessKey() { return $this->__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()) {
    $this->__triggerError('sendEmail', 'Message failed validation.');
    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);
    }
    else {
    trigger_error(sprintf("SimpleEmailService::%s(): Encountered an error: %s", $functionname, $error), E_USER_WARNING);
    }
    }
    }

    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() {
    $this->to = array();
    $this->cc = array();
    $this->bcc = array();
    $this->replyto = array();

    $this->from = null;
    $this->returnpath = null;

    $this->subject = null;
    $this->messagetext = null;
    $this->messagehtml = null;

    $this->subjectCharset = null;
    $this->messageTextCharset = null;
    $this->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);
    } else {
    $this->messagetext = null;
    }
    if(file_exists($htmlfile) && is_file($htmlfile) && is_readable($htmlfile)) {
    $this->messagehtml = file_get_contents($htmlfile);
    } else {
    $this->messagehtml = null;
    }
    }

    function setMessageFromURL($texturl, $htmlurl = null) {
    if($texturl !== null) {
    $this->messagetext = file_get_contents($texturl);
    } else {
    $this->messagetext = null;
    }
    if($htmlurl !== null) {
    $this->messagehtml = file_get_contents($htmlurl);
    } else {
    $this->messagehtml = null;
    }
    }

    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;
    // messages require at least one of: subject, messagetext, messagehtml.
    if(($this->subject == null || strlen($this->subject) == 0)
    && ($this->messagetext == null || strlen($this->messagetext) == 0)
    && ($this->messagehtml == null || strlen($this->messagehtml) == 0))
    {
    return false;
    }

    return true;
    }
    }