<?php

class AmazonSES
{
	public $Multithread = false;
	public $CampaignID	= 0;
	public $SubscriberID = 0;
	public $Timezone = '';
	public $SendRate = 0;
	public $IsAutoresponder = false;

    public $amazonSES_base_url = '';
    public $debug = FALSE;

    public $aws_access_key_id = "";
    public $aws_secret_key = "";

    private $_region;
    private $_host;
    private $_endpoint;
    private $_amz_date;
    private $_date;
    private $_method;

	    protected function
	    make_required_http_headers ($query = array()) {
	        if($this->Timezone=='' || $this->Timezone==0) $this->Timezone = date_default_timezone_get();
			date_default_timezone_set($this->Timezone);

	        $this->_generateSignature($query);

		$headers = array();
		$headers[] = 'Authorization: '.$this->_headers['Authorization'];
		$headers[] = 'content-type: application/x-www-form-urlencoded';
		$headers[] = 'host: '.$this->_host;
		$headers[] = 'x-amz-date: '.$this->_headers['x-amz-date'];

	        return $headers;
	    }

	    public function
	    make_required_http_headers_for_query_string ($query_str) {
	        if($this->Timezone=='' || $this->Timezone==0) $this->Timezone = date_default_timezone_get();
			date_default_timezone_set($this->Timezone);

			$this->_refreshDate();

			//Get region from host, eg. 'us-east-1'
			$region_explode = explode('.', $this->amazonSES_base_url);
			$region = $region_explode[1];
			$this->_region = $region;
			$content_type = 'content-type:application/x-www-form-urlencoded';
			$this->_method = 'POST';
			$this->_headers = array();

	        $canonical_uri = '/';
			$this->_host = 'email' . '.' . $this->_region . '.' . 'amazonaws.com';
			$this->amazonSES_base_url = 'https://' . 'email' . '.' . $this->_region . '.' . 'amazonaws.com';

	        $canonical_headers = $content_type . "\n" . 'host:' . $this->_host . "\n" . 'x-amz-date:' . $this->_amz_date . "\n";
	        $signed_headers = 'content-type;host;x-amz-date';
	        $payload_hash = hash('sha256', $query_str);
	        $canonical_request = $this->_method . "\n" . $canonical_uri . "\n" . '' . "\n" . $canonical_headers . "\n" . $signed_headers . "\n" . $payload_hash;
	        $credential_scope = $this->_date . '/' . $this->_region . '/' . 'email' . '/aws4_request';
	        $string_to_sign =  'AWS4-HMAC-SHA256' . "\n" . $this->_amz_date . "\n" . $credential_scope . "\n" . hash('sha256', $canonical_request);
	        $signing_key = $this->_generateSignatureKey();
	        $signature = hash_hmac('sha256', $string_to_sign, $signing_key);

	        $headers = array();
	        $headers[] = 'Authorization: AWS4-HMAC-SHA256 Credential=' . $this->aws_access_key_id . '/' . $credential_scope . ', SignedHeaders=' . $signed_headers . ', Signature=' . $signature;
			$headers[] = 'content-type: application/x-www-form-urlencoded';
			$headers[] = 'host: '.$this->_host;
			$headers[] = 'x-amz-date: '.$this->_amz_date;

	        return $headers;
	    }

    /**
     * Create and returns binary hmac sha256
     *
     * @return hmac sha256.
     */
    private function _generateSignatureKey()
    {
        $date_h = hash_hmac('sha256', $this->_date, 'AWS4' . $this->aws_secret_key, true);
        $region_h = hash_hmac('sha256', $this->_region, $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;
    }

	/**
     *  Refresh amzdate and date
     *
     *  @return void
     */
    private function _refreshDate()
    {
        $this->_amz_date = gmdate('Ymd\THis\Z');
        $this->_date = gmdate('Ymd');
    }

	private function _generateSignature($parameters = array()) {

		$this->_refreshDate();

		//Get region from host, eg. 'us-east-1'
		$region_explode = explode('.', $this->amazonSES_base_url);
		$region = $region_explode[1];
		$this->_region = $region;
		$content_type = 'content-type:application/x-www-form-urlencoded';

		//Request method
		$this->_method = 'POST';

		//Headers to pass to API
		$this->_headers = array();

        $canonical_uri = '/';

		//eg 'Action' => 'GetSendQuota';
        ksort($parameters);

		//Build parameters to send to API
        $request_parameters = http_build_query($parameters, '', '&');

		$this->_host = 'email' . '.' . $this->_region . '.' . 'amazonaws.com';
		$this->amazonSES_base_url = 'https://' . 'email' . '.' . $this->_region . '.' . 'amazonaws.com';

        $canonical_headers = $content_type . "\n" . 'host:' . $this->_host . "\n" . 'x-amz-date:' . $this->_amz_date . "\n";
        $signed_headers = 'content-type;host;x-amz-date';
        $payload_hash = hash('sha256', $request_parameters);

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

        // task2
        $credential_scope = $this->_date . '/' . $this->_region . '/' . '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->aws_access_key_id . '/' . $credential_scope . ', SignedHeaders=' . $signed_headers . ', Signature=' . $signature;
        $this->_headers['x-amz-date'] = $this->_amz_date;
	}

    protected function
    make_query_string
    ($query) {
        $query_str = "";
        foreach ($query as $k => $v)
            { $query_str .= urlencode($k)."=".urlencode($v).'&'; }

        return rtrim($query_str, '&');
    }

    protected function
    parse_amazonSES_error
    ($response) {
        $sxe = simplexml_load_string($response);

        // If the error response can not be parsed properly,
        // then just return the original response content.
        if (($sxe === FALSE) or ($sxe->getName() !== "ErrorResponse"))
            { return $response; }

        return "{$sxe->Error->Code}"
               .(($sxe->Error->Message)?" - {$sxe->Error->Message}":"");
    }

    protected function
    make_request
    ($query) {
	    error_reporting(0);
	$dbHost=$dbUser=$dbPass=$dbName='';
		if(file_exists('../config.php')) require '../config.php';
		$server_path_array1 = explode('/', $_SERVER['SCRIPT_FILENAME']);
		$delimiter = $server_path_array1[count($server_path_array1)-1];
		if($delimiter=='send-now.php') $delimiter = 'includes';
		$server_path_array = explode($delimiter, $_SERVER['SCRIPT_FILENAME']);
		$server_path = $server_path_array[0];
		if(file_exists($server_path.'includes/config.php')) require $server_path.'includes/config.php';
		if(isset($dbPort)) $mysqli = new mysqli($dbHost, $dbUser, $dbPass, $dbName, $dbPort);
		else $mysqli = new mysqli($dbHost, $dbUser, $dbPass, $dbName);

		//Get SES endpoint
		$q = 'SELECT ses_endpoint FROM login LIMIT 1';
		$r = mysqli_query($mysqli, $q);
		if ($r) while($row = mysqli_fetch_array($r)) $this->amazonSES_base_url = 'https://'.$row['ses_endpoint'];

	        // Prepare headers and query string.
	        $query_str = $this->make_query_string($query);
	        $request_url = $this->amazonSES_base_url;
	        $http_headers = $this->Multithread ? array() : $this->make_required_http_headers($query);

        if ($this->debug) {
            echo "<pre>[AmazonSESDebug] Query Parameters:\n\"";
            print_r($query);
            echo "\"\n";

            printf("[AmazonSES Debug] Http Headers:\n\"%s\"\n",
                                      implode("\n", $http_headers));
            printf("[AmazonSES Debug] Query String:\n\"%s\"\n", $query_str);
            echo "</pre>";
        }

        //if multithreading is needed,
        if($this->Multithread)
        {
	        //Insert SES query into queue
	        $query_str_escaped = mysqli_real_escape_string($mysqli, $query_str);
		        $http_headers_escaped = '';
	        $q = 'INSERT INTO queue (query_str, http_headers, campaign_id, subscriber_id)
				  SELECT "'.$query_str_escaped.'", "'.$http_headers_escaped.'", '.$this->CampaignID.', '.$this->SubscriberID.'
				  FROM DUAL
				  WHERE NOT EXISTS (
					SELECT 1 FROM queue FORCE INDEX (q_campaign_subscriber) WHERE campaign_id = '.$this->CampaignID.' AND subscriber_id = '.$this->SubscriberID.' LIMIT 1
				  )';
	        mysqli_query($mysqli, $q);
	        if(mysqli_affected_rows($mysqli) > 0)
	        {
		        $q4 = 'UPDATE subscribers SET last_campaign = '.$this->CampaignID.' WHERE id = '.$this->SubscriberID; //Update last_campaign in subscribers table
				mysqli_query($mysqli, $q4);
	        }

	        //Check if there are more than X (where X is the send rate) emails in queue, send them in parallel to SES
	        $q2 = 'SELECT id, query_str, http_headers, subscriber_id FROM queue FORCE INDEX (q_campaign_sent_id) WHERE campaign_id = '.$this->CampaignID.' AND sent = 0 ORDER BY id ASC LIMIT '.$this->SendRate;
	        $r2 = mysqli_query($mysqli, $q2);
	        if ($r2 && mysqli_num_rows($r2) >= $this->SendRate)
	        {
		        $id_array = array();
		        $time_started = microtime(true);

		        if (!function_exists('on_request_done'))
				{
						function on_request_done($content, $url, $ch, $callback_data)
						{
							global $mysqli;
							$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
						    if ($httpcode === 200) //if success
						    {
							$messageIDArray = explode('<MessageId>', $content);
						        $messageIDArray2 = isset($messageIDArray[1]) ? explode('</MessageId>', $messageIDArray[1]) : array('');
						        $messageID = mysqli_real_escape_string($mysqli, $messageIDArray2[0]);
						        $subscriber_id = (int)$callback_data[0];
						        $campaign_id = (int)$callback_data[4];
						        $queue_id = (int)$callback_data[5];

						        $q4 = 'UPDATE queue SET sent = 1, query_str = NULL, http_headers = NULL WHERE id = '.$queue_id;
						        $q5 = 'UPDATE campaigns SET recipients = recipients+1, timeout_check = NULL WHERE recipients < to_send AND id = '.$campaign_id;
						        $q9 = 'UPDATE subscribers SET messageID = "'.$messageID.'" WHERE id = '.$subscriber_id;
								mysqli_query($mysqli, $q4);
								mysqli_query($mysqli, $q5);
								mysqli_query($mysqli, $q9);
						    }
						    else //if fail
						    {
						        $response_http_status_code = (int)$httpcode;
						        $response_http_status_code = $response_http_status_code === 0 ? curl_errno($ch) : $response_http_status_code;
						        $campaign_id = (int)$callback_data[4];
						        $val = $callback_data[0].':'.$response_http_status_code;

						        $q7 = 'SELECT errors FROM campaigns WHERE id = '.$campaign_id;
							$r7 = mysqli_query($mysqli, $q7);
							if ($r7)
							{
							    while($row = mysqli_fetch_array($r7))
							    {
									$errors = $row['errors'];

									if($errors=='')
												$val = $callback_data[0].':'.$response_http_status_code;
											else
											{
												$errors .= ','.$callback_data[0].':'.$response_http_status_code;
												$val = $errors;
											}
							    }
							}

							        //update campaigns' errors column
							        $val = mysqli_real_escape_string($mysqli, $val);
							        $q6 = 'UPDATE campaigns SET errors = "'.$val.'" WHERE id = '.$campaign_id;
									mysqli_query($mysqli, $q6);
						    }
						}
					}

		        $mh = curl_multi_init();
		        $outstanding_requests = array();

	            while($row = mysqli_fetch_array($r2))
	            {
				$queue_id = $row['id'];
				$queue = stripslashes($row['query_str']);
				$http_headers = $this->make_required_http_headers_for_query_string($queue);
				$subscriber_id = $row['subscriber_id'];
						array_push($id_array, $queue_id);

				// Prepare curl.
			        $ch = curl_init();
			        curl_setopt($ch, CURLOPT_URL, $request_url);
			        curl_setopt($ch, CURLOPT_POST, $queue);
			        curl_setopt($ch, CURLOPT_POSTFIELDS, $queue);
			        curl_setopt($ch, CURLOPT_HTTPHEADER, $http_headers);
			        curl_setopt($ch, CURLOPT_HEADER, true);
			        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
			        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
					curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
					curl_setopt($ch, CURLOPT_CAINFO, $server_path.'certs/cacert.pem');


			        //add handle
					curl_multi_add_handle($mh, $ch);

					$ch_array_key = (int)$ch;

				        $outstanding_requests[$ch_array_key] = array(
				            'url' => $request_url,
				            'callback' => 'on_request_done',
				            'user_data' => array($subscriber_id, $queue, $request_url, $http_headers, $this->CampaignID, $queue_id)
				        );
		            }
					$id_array = array();

					//execute the handles
					$active = null;
				do
				{
				    $mrc = curl_multi_exec($mh, $active);
				    usleep(1);

				    while ($info=curl_multi_info_read($mh))
		            {
			$ch = $info['handle'];
			            $ch_array_key = (int)$ch;

			            $request = $outstanding_requests[$ch_array_key];

			            $url = $request['url'];
			            $content = curl_multi_getcontent($ch);
			            $callback = $request['callback'];
			            $user_data = $request['user_data'];

				            call_user_func($callback, $content, $url, $ch, $user_data);

			            unset($outstanding_requests[$ch_array_key]);

			            curl_multi_remove_handle($mh, $ch);
				    }
				} while ($mrc == CURLM_CALL_MULTI_PERFORM || ($active && $mrc == CURLM_OK));

				curl_multi_close($mh);

					//throttling
				$time_taken = microtime(true) - $time_started;
				$usleep = ceil((1 - $time_taken) * 1000000);
				if($time_taken < 1) usleep($usleep);
	        }
        }

        else
        {
	//Get server path
	$server_path_array2 = explode('includes/', $_SERVER['SCRIPT_FILENAME']);
		    $server_path2 = $server_path_array2[0];
		    if(count($server_path_array2)==1) $server_path2 = $server_path;

	        // Prepare curl.
	        $cr = curl_init();
	        curl_setopt($cr, CURLOPT_URL, $request_url);
	        curl_setopt($cr, CURLOPT_POST, $query_str);
	        curl_setopt($cr, CURLOPT_POSTFIELDS, $query_str);
	        curl_setopt($cr, CURLOPT_HTTPHEADER, $http_headers);
	        curl_setopt($cr, CURLOPT_HEADER, true);
	        curl_setopt($cr, CURLOPT_RETURNTRANSFER, true);
	        curl_setopt($cr, CURLOPT_SSL_VERIFYHOST, 2);
			curl_setopt($cr, CURLOPT_SSL_VERIFYPEER, 1);
			curl_setopt($cr, CURLOPT_CAINFO, $server_path2.'certs/cacert.pem');

		        // Make the request and fetch response.
		        $response = curl_exec($cr);
		        $response_http_status_code = curl_getinfo($cr, CURLINFO_HTTP_CODE);

		        // Separate header and content.
		        $tmpar = explode("\r\n\r\n", $response, 2);
		        $response_http_headers = $tmpar[0];
		        $response_content = isset($tmpar[1]) ? $tmpar[1] : '';

		        if($response_http_status_code != 200)
		        {
			        //Response title
			        $response_title_array1 = explode('<Code>', $response_content);
			        $response_title_array2 = isset($response_title_array1[1]) ? explode('</Code>', $response_title_array1[1]) : array('');
			        $response_title = $response_title_array2[0];

			        //Response message
			        $response_msg_array1 = explode('<Message>', $response_content);
			        $response_msg_array2 = isset($response_msg_array1[1]) ? explode('</Message>', $response_msg_array1[1]) : array(curl_error($cr));
			        $response_msg = $response_msg_array2[0];

			        //Response requestID
			        $response_rqid_array1 = explode('<RequestId>', $response_content);
			        $response_rqid_array2 = isset($response_rqid_array1[1]) ? explode('</RequestId>', $response_rqid_array1[1]) : array('');
			        $response_rqid = $response_rqid_array2[0];

			        $the_error = array();
			        $the_error[0] = (string)$response_http_status_code;
			        $the_error['code'] = $response_http_status_code;
			        $the_error['full_error'] = $response_title.' (Request ID: '.$response_rqid.'): '.$response_msg;

			        //Write to error_log
			        error_log($the_error['full_error']);

			        if($this->CampaignID != '' && $this->SubscriberID != '')
			        {
				$q7 = 'SELECT errors FROM campaigns WHERE id = '.$this->CampaignID;
				$r7 = mysqli_query($mysqli, $q7);
				$val = $this->SubscriberID.':'.$response_http_status_code;
				if ($r7)
				{
				    while($row = mysqli_fetch_array($r7))
				    {
						$errors = $row['errors'];
						if($errors!='') $val = $errors.','.$val;
				    }
				}
				$val = mysqli_real_escape_string($mysqli, $val);
				        $q6 = 'UPDATE campaigns SET errors = "'.$val.'" WHERE id = '.$this->CampaignID;
						mysqli_query($mysqli, $q6);
			        }

			        return $the_error;
				}

	        //Check if there's a campaign/autoresponder ID
	        if($this->CampaignID != '')
	        {
		//Get message ID from response
		        $messageIDArray = explode('<MessageId>', $response);
		        $messageIDArray2 = explode('</MessageId>', $messageIDArray[1]);
		        $messageID = $messageIDArray2[0];

		        //Update subscriber's messageID if it's an Autoresponder
		        if($this->IsAutoresponder)
		        {
			        $q9 = 'UPDATE subscribers SET messageID = "'.$messageID.'" WHERE id = '.$this->SubscriberID;
			mysqli_query($mysqli, $q9);
		        }
		        //Increment recipients number in campaigns table if it's a campaign
		else
		{
					$q5 = 'UPDATE campaigns SET recipients = recipients+1 WHERE id = '.$this->CampaignID;
					$q9 = 'UPDATE subscribers SET last_campaign = '.$this->CampaignID.', messageID = "'.$messageID.'" WHERE id = '.$this->SubscriberID;
					mysqli_query($mysqli, $q5);
					mysqli_query($mysqli, $q9);
			        }
				}

			//Return the successful HTTP result to PHPMailer. AmazonSESSend()
			//expects the documented array(status code, response content), but
			//this synchronous path previously fell through and returned NULL.
			return array((string)$response_http_status_code, $response_content);
		}
	    }

    //***********************************************************************
    // Name: send_mail
    // Description:
    //    Send mail using amazonSES. Provide $header, $subject,
    //    $body appropriately. The $recipients and $from are mostly expe-
    //    rimental and unneccessary as documented in the SES api.
    //
    //    Return an array in the form -
    //        array(http_status_code, response_content)
    //    if the http_status_code is something other than "200", then
    //    the response_content is an error message parsed from the response.
    //***********************************************************************

    public function
    send_mail
    ($header, $subject, $body,
     $recipients=FALSE, $from=FALSE) {
        // Make sure that there is a blank line between header and body.
        $raw_mail = rtrim($header, "\r\n")."\n\n".$body;

        // Prepare query.
        //*********************************************************//
        $query = array();
        $query["Action"] = "SendRawEmail";

        // Add optional Destination.member.N request parameter.
        if ($recipients) {
            $mcnt = 1;
            foreach ($recipients as $recipient) {
                $query["Destinations.member.{$mcnt}"] = $recipient;
                $mcnt += 1;
            }
        }

        // Add mail data.
        $query["RawMessage.Data"] = base64_encode($raw_mail);
        //*********************************************************//

        // Send the mail and forward the result array to the caller.
        return $this->make_request($query);
    }

    public function
    request_verification
    ($email_address) {
        $query = array();

        $query["Action"] = "VerifyEmailAddress";
        $query["EmailAddress"] = $email_address;
        $query["X-Amz-Expires"] = 604800;

        return $this->make_request($query);
    }
}

/* End of file */
