<?php

/**
 * Postmark PHP class
 * 
 * Copyright 2011, Markus Hedlund, Mimmin AB, www.mimmin.com
 * Licensed under the MIT License.
 * Redistributions of files must retain the above copyright notice.
 *
 * @author Markus Hedlund (markus@mimmin.com) at mimmin (www.mimmin.com)
 * @copyright Copyright 2009 - 2011, Markus Hedlund, Mimmin AB, www.mimmin.com
 * @version 0.4.5
 * @license http://www.opensource.org/licenses/mit-license.php The MIT License
 * 
 * Usage:
 * Mail_Postmark::compose()
 *      ->addTo('address@example.com', 'Name')
 *      ->subject('Subject')
 *      ->messagePlain('Plaintext message')
 *	    ->tag('Test tag')
 *      ->send();
 * 
 * or:
 * 
 * $email = new Mail_Postmark();
 * $email->addTo('address@example.com', 'Name')
 *      ->subject('Subject')
 *      ->messagePlain('Plaintext message')
 *	    ->tag('Test tag')
 *      ->send();
 */
interface Mail_Postmark_Adapter_Interface
{
	public static function getApiKey();
	public static function setupDefaults(Mail_Postmark &$mail);
	public static function log($logData);
}
class Mail_Postmark
{
	const DEBUG_OFF = 0;
	const DEBUG_VERBOSE = 1;
	const DEBUG_RETURN = 2;
	const TESTING_API_KEY = 'POSTMARK_API_TEST';
	const MAX_ATTACHMENT_SIZE = 10485760;	// 10 MB
	const RECIPIENT_TYPE_TO = 'to';
	const RECIPIENT_TYPE_CC = 'cc';
	const RECIPIENT_TYPE_BCC = 'bcc';
	
	static $_mimeTypes = array('ai' => 'application/postscript', 'avi' => 'video/x-msvideo', 'doc' => 'application/msword', 'eps' => 'application/postscript', 'gif' => 'image/gif', 'htm' => 'text/html', 'html' => 'text/html', 'jpeg' => 'image/jpeg', 'jpg' => 'image/jpeg', 'mov' => 'video/quicktime', 'mp3' => 'audio/mpeg', 'mpg' => 'video/mpeg', 'pdf' => 'application/pdf', 'ppt' => 'application/vnd.ms-powerpoint', 'ps' => 'application/postscript', 'rtf' => 'application/rtf', 'tif' => 'image/tiff', 'tiff' => 'image/tiff', 'txt' => 'text/plain', 'xls' => 'application/vnd.ms-excel', 'csv' => 'text/comma-separated-values', 'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', 'flv' => 'video/x-flv', 'ics' => 'text/calendar', 'log' => 'text/plain', 'png' => 'image/png', 'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation', 'psd' => 'image/photoshop', 'rm' => 'application/vnd.rn-realmedia', 'swf' => 'application/x-shockwave-flash', 'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', 'xml' => 'text/xml');
	
	private $_apiKey;
	private $_from;
	private $_to = array();
	private $_cc = array();
	private $_bcc = array();
	private $_replyTo;
	private $_subject;
	private $_tag;
	private $_messagePlain;
	private $_messageHtml;
	private $_headers = array();
	private $_attachments = array();
	private $_debugMode = self::DEBUG_OFF;
	
	/**
	* Initialize
	*/
	public function __construct()
	{
		if (class_exists('Mail_Postmark_Adapter')) {
			require_once('Adapter_Interface.php');
			
			$reflection = new ReflectionClass('Mail_Postmark_Adapter');
			
			if (!$reflection->implementsInterface('Mail_Postmark_Adapter_Interface')) {
				trigger_error('Mail_Postmark_Adapter must implement interface Mail_Postmark_Adapter_Interface', E_USER_ERROR);
			}
			
			$this->_apiKey = Mail_Postmark_Adapter::getApiKey();
			
			Mail_Postmark_Adapter::setupDefaults($this);
			
		} else {
			$this->_apiKey = '82dad99b-d87d-48e0-9490-2367f1bb6416';
			
			if (defined('POSTMARKAPP_MAIL_FROM_ADDRESS')) {
				$this->from(
					POSTMARKAPP_MAIL_FROM_ADDRESS,
					defined('POSTMARKAPP_MAIL_FROM_NAME') ? POSTMARKAPP_MAIL_FROM_NAME : null
				);
			}
		}
		
		$this->messageHtml(null)->messagePlain(null);
	}
	
	/**
	* Add a physical file as an attachment
	* Options:
	* - filenameAlias, use a different filename for the attachment
	* 
	* @param string $filename Location of the file
	* @param array $options An optional array with options
	* @throws InvalidArgumentException If file doesn't exist
	* @throws OverflowException If maximum attachment size has been reached
	* @return Mail_Postmark
	*/
	public function &addAttachment($filename, $options = array())
	{
		if (!is_file($filename)) {
			throw new InvalidArgumentException("File \"{$filename}\" does not exist");
		}
		
		$this->addCustomAttachment(
			isset($options['filenameAlias']) ? $options['filenameAlias'] : basename($filename),
			file_get_contents($filename),
			$this->_getMimeType($filename)
		);
		
		return $this;
	}
	
	/**
	* Add a BCC address
	* 
	* @param string $address E-mail address used in BCC
	* @param string $name Optional. Name used in BCC
	* @throws InvalidArgumentException On invalid address
	* @throws OverflowException If there are too many email recipients
	* @return Mail_Postmark
	*/
	public function &addBcc($address, $name = null)
	{
		$this->_addRecipient(self::RECIPIENT_TYPE_BCC, $address, $name);
		return $this;
	}
	
	/**
	* Add a CC address
	* 
	* @param string $address E-mail address used in CC
	* @param string $name Optional. Name used in CC
	* @throws InvalidArgumentException On invalid address
	* @throws OverflowException If there are too many email recipients
	* @return Mail_Postmark
	*/
	public function &addCc($address, $name = null)
	{
		$this->_addRecipient(self::RECIPIENT_TYPE_CC, $address, $name);
		return $this;
	}
	
	/**
	* Add an attachment.
	* 
	* @param string $filename What to call the file
	* @param string $content Raw file data
	* @param string $mimeType The mime type of the file
	* @throws OverflowException If maximum attachment size has been reached
	* @return Mail_Postmark
	*/
	public function &addCustomAttachment($filename, $content, $mimeType)
	{
		$length = strlen($content);
		$lengthSum = 0;
		
		foreach ($this->_attachments as $file) {
			$lengthSum += $file['length'];
		}
		
		if ($lengthSum + $length > self::MAX_ATTACHMENT_SIZE) {
			throw new OverflowException("Maximum attachment size reached");
		}
		
		$this->_attachments[$filename] = array(
			'content' => base64_encode($content),
			'mimeType' => $mimeType,
			'length' => $length
		);
		
		return $this;
	}
	
	/**
	* Add a custom header
	* 
	* @param string $name Custom header name
	* @param string $value Custom header value
	* @return Mail_Postmark
	*/
	public function &addHeader($name, $value)
	{
		$this->_headers[$name] = $value;
		return $this;
	}
	
	/**
	* Add a receiver
	* 
	* @param string $address E-mail address used in To
	* @param string $name Optional. Name used in To
	* @throws InvalidArgumentException On invalid address
	* @throws OverflowException If there are too many email recipients
	* @return Mail_Postmark
	*/
	public function &addTo($address, $name = null)
	{
		$this->_addRecipient(self::RECIPIENT_TYPE_TO, $address, $name);
		return $this;
	}
	
	/**
	* New e-mail
	* 
	* @return Mail_Postmark
	*/
	public static function compose()
	{
		return new self();
	}
	
	/**
	* Turns debug output on
	* 
	* @param int $mode One of the debug constants
	* @return Mail_Postmark
	*/
	public function &debug($mode = self::DEBUG_VERBOSE)
	{
		$this->_debugMode = $mode;
		return $this;
	}
	
	/**
	* Specify sender. Overwrites default From. Note that the address
	* must first be added in the Postmarkapp admin interface
	* 
	* @param string $address E-mail address used in From
	* @param string $name Optional. Name used in From
	* @throws InvalidArgumentException On invalid address
	* @return Mail_Postmark
	*/
	public function &from($address, $name = null)
	{
		if (!$this->_validateAddress($address)) {
			throw new InvalidArgumentException("From address \"{$address}\" is invalid");
		}
		
		$this->_from = array('address' => $address, 'name' => $name);
		return $this;
	}
	
	/**
	* Specify sender name. Overwrites default From name, but doesn't change address.
	* 
	* @param string $name Name used in From
	* @return Mail_Postmark
	*/
	public function &fromName($name)
	{
		$this->_from['name'] = $name;
		return $this;
	}
	
	/**
	* Add HTML message. Can be used in conjunction with messagePlain()
	* 
	* @param string $message E-mail message
	* @return Mail_Postmark
	*/
	public function &messageHtml($message)
	{
		$this->_messageHtml = $message;
		return $this;
	}
	
	/**
	* Add plaintext message. Can be used in conjunction with messageHtml()
	* @param string $message E-mail message
	* @return Mail_Postmark
	*/
	public function &messagePlain($message)
	{
		$this->_messagePlain = $message;
		return $this;
	}
	
	/**
	* Specify reply-to
	* 
	* @param string $address E-mail address used in To
	* @param string $name Optional. Name used in To
	* @throws InvalidArgumentException On invalid address
	* @return Mail_Postmark
	*/
	public function &replyTo($address, $name = null)
	{
		if (!$this->_validateAddress($address)) {
			throw new InvalidArgumentException("Reply To address \"{$address}\" is invalid");
		}
		
		$this->_replyTo = array('address' => $address, 'name' => $name);
		return $this;
	}
	
	/**
	* Sends the e-mail. Prints debug output if debug mode is turned on
	* 
	* @throws Exception If HTTP code 422, Exception with API error code and Postmark message, otherwise HTTP code.
	* @throws BadMethodCallException If From address, To address or Subject is missing
	* @return boolean|array True if success, array if DEBUG_RETURN is enabled
	*/
	public function send()
	{
		$this->_validateData();
		$data = $this->_prepareData();
		$headers = array(
			'Accept: application/json',
			'Content-Type: application/json',
			'X-Postmark-Server-Token: ' . $this->_apiKey
		);
		
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, 'https://api.postmarkapp.com/email');
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
		curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
		curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
		curl_setopt($ch, CURLOPT_CAINFO, dirname(__FILE__) . '/Certificate/cacert.pem');
		
		$return = curl_exec($ch);
		$curlError = curl_error($ch);
		$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
		
		$this->_log(array(
			'messageData' => $data,
			'return' => $return,
			'curlError' => $curlError,
			'httpCode' => $httpCode
		));
		
		if (($this->_debugMode & self::DEBUG_VERBOSE) === self::DEBUG_VERBOSE) {
			echo "JSON: " . json_encode($data)
				. "\nHeaders: \n\t" . implode("\n\t", $headers) 
				. "\nReturn:\n{$return}"
				. "\nCurl error: {$curlError}"
				. "\nHTTP code: {$httpCode}";
		}
		
		if ($curlError !== '') {
			throw new Exception($curlError);
		}
		
		if (!$this->_isTwoHundred($httpCode)) {
			if ($httpCode == 422) {
				$return = json_decode($return);
				throw new Exception($return->Message, $return->ErrorCode);
			} else {
				throw new Exception("Error while mailing. Postmark returned HTTP code {$httpCode} with message \"{$return}\"", $httpCode);
			}
		}
		
		if (($this->_debugMode & self::DEBUG_RETURN) === self::DEBUG_RETURN) {
			return array(
				'json' => json_encode($data),
				'headers' => $headers,
				'return' => $return,
				'curlError' => $curlError,
				'httpCode' => $httpCode
			);
		}
		
		return true;
	}
	
	/**
	* Specify subject
	* 
	* @param string $subject E-mail subject
	* @return Mail_Postmark
	*/
	public function &subject($subject)
	{
		$this->_subject = $subject;
		return $this;
	}

	/**
	* You can categorize outgoing email using the optional Tag  property.
	* If you use different tags for the different types of emails your 
	* application generates, you will be able to get detailed statistics
	* for them through the Postmark user interface.
	* Only 1 tag per email is supported.
	* 
	* @param string $tag One tag
	* @return Mail_Postmark
	*/
	public function &tag($tag)
	{
		$this->_tag = $tag;
		return $this;
	}
	
	/**
	* Specify receiver. Use addTo to add more.
	* 
	* @deprecated Use addTo.
	* @param string $address E-mail address used in To
	* @param string $name Optional. Name used in To
	* @return Mail_Postmark
	*/
	public function &to($address, $name = null)
	{
		$this->_to = array();
		$this->addTo($address, $name);
		return $this;
	}
	
	/**
	* @param string $type Either 'to', 'cc' or 'bcc'
	* @param string $address
	* @param string|null $name
	* @throws InvalidArgumentException On invalid address
	* @throws OverflowException If there are too many email recipients
	*/
	public function _addRecipient($type, $address, $name = null)
	{
		$address = trim($address);
		
		if (!$this->_validateAddress($address)) {
			throw new InvalidArgumentException("Address \"{$address}\" is invalid");
		}
		
		if (count($this->_to) + count($this->_cc) + count($this->_bcc) === 20) {
			throw new OverflowException('Too many email recipients');
		}
		
		$data = array('address' => $address, 'name' => $name);
		
		switch ($type) {
			case self::RECIPIENT_TYPE_TO:
				$this->_to[] = $data;
			break;
			
			case self::RECIPIENT_TYPE_CC:
				$this->_cc[] = $data;
			break;
			
			case self::RECIPIENT_TYPE_BCC:
				$this->_bcc[] = $data;
			break;
		}
	}
	
	private function _createAddress($address, $name = null)
	{
		if (isset($name)) {
			return '"' . str_replace('"', '', $name) . '" <' . $address . '>';
		} else {
			return $address;
		}
	}
	
	/**
	* Try to detect the mime type
	*/
	private function _getMimeType($filename)
	{
		$extension = pathinfo($filename, PATHINFO_EXTENSION);
		
		if (isset(self::$_mimeTypes[$extension])) {
			return self::$_mimeTypes[$extension];
			
		} else if (function_exists('mime_content_type')) {
			 return mime_content_type($filename);
		
		} else if (function_exists('finfo_file')) {
			 $fh = finfo_open(FILEINFO_MIME);
			 $mime = finfo_file($fh, $filename);
			 finfo_close($fh);
			 return $mime;
		
		} else if ($image = getimagesize($filename)) {
			return $image[2];
			
		} else {
			return 'application/octet-stream';
		}
	}
	
	/**
	* If a number is 200-299
	*/
	private function _isTwoHundred($value)
	{
		return intval($value / 100) == 2;
	}
	
	/**
	* Call the logger method, if one exists
	* 
	* @param array $logData
	*/
	private function _log($logData)
	{
		if (class_exists('Mail_Postmark_Adapter')) {
			Mail_Postmark_Adapter::log($logData);
		}
	}
	
	/**
	* Prepares the data array
	*/
	private function _prepareData()
	{
		$data = array(
			'Subject' => $this->_subject
		);
		
		$data['From'] = $this->_createAddress($this->_from['address'], $this->_from['name']);
		$data['To'] = array();
		$data['Cc'] = array();
		$data['Bcc'] = array();
		
		foreach ($this->_to as $to) {
			$data['To'][] = $this->_createAddress($to['address'], $to['name']);
		}
		
		foreach ($this->_cc as $cc) {
			$data['Cc'][] = $this->_createAddress($cc['address'], $cc['name']);
		}
		
		foreach ($this->_bcc as $bcc) {
			$data['Bcc'][] = $this->_createAddress($bcc['address'], $bcc['name']);
		}
		
		$data['To'] = implode(', ', $data['To']);
		
		if (empty($data['Cc'])) {
			unset($data['Cc']);
		} else {
			$data['Cc'] = implode(', ', $data['Cc']);
		}
		
		if (empty($data['Bcc'])) {
			unset($data['Bcc']);
		} else {
			$data['Bcc'] = implode(', ', $data['Bcc']);
		}
		
		if ($this->_replyTo !== null) {
			$data['ReplyTo'] = $this->_createAddress($this->_replyTo['address'], $this->_replyTo['name']);
		}
		
		if ($this->_messageHtml !== null) {
			$data['HtmlBody'] = $this->_messageHtml;
		}
		
		if ($this->_messagePlain !== null) {
			$data['TextBody'] = $this->_messagePlain;
		}

		if ($this->_tag !== null) {
			$data['Tag'] = $this->_tag;
		}
		
		if (!empty($this->_headers)) {
			$data['Headers'] = array();
			
			foreach ($this->_headers as $name => $value) {
				$data['Headers'][] = array('Name' => $name, 'Value' => $value);
			}
		}
		
		if (!empty($this->_attachments)) {
			$data['Attachments'] = array();
			
			foreach ($this->_attachments as $filename => $file) {
				$data['Attachments'][] = array(
					'Name' => $filename,
					'Content' => $file['content'],
					'ContentType' => $file['mimeType']
				);
			}
		}
		
		return $data;
	}
	
	/**
	* Validates an e-mailadress
	*/
	private function _validateAddress($email)
	{
		// http://php.net/manual/en/function.filter-var.php
		// return filter_var($email, FILTER_VALIDATE_EMAIL) !== false;
		// filter_var proved to be unworthy (passed foo..bar@domain.com as valid),
		// and was therefore replace with
		$regex = "/^([\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+\.)*[\w\!\#$\%\&\'\*\+\-\/\=\?\^\`{\|\}\~]+@((((([a-z0-9]{1}[a-z0-9\-]{0,62}[a-z0-9]{1})|[a-z])\.)+[a-z]{2,6})|(\d{1,3}\.){3}\d{1,3}(\:\d{1,5})?)$/i";
		// from http://fightingforalostcause.net/misc/2006/compare-email-regex.php
		return preg_match($regex, $email) === 1;
	}
	
	/**
	* Validate that the email can be sent
	* 
	* @throws BadMethodCallException If From address, To address or Subject is missing
	*/
	private function _validateData()
	{
		if ($this->_from['address'] === null) {
			throw new BadMethodCallException('From address is not set');
		}
		
		if (empty($this->_to)) {
			throw new BadMethodCallException('No To address is set');
		}
		
		if (!isset($this->_subject)) {
			throw new BadMethodCallException('Subject is not set');
		}
	}
}
