<?php

/**
 * Email
 *
 * A simple email handling class which supports
 * multiple email services. Check out the email subfolder
 * for all available services
 *
 * @package   Kirby Toolkit
 * @author    Bastian Allgeier <bastian@getkirby.com>
 * @link      http://getkirby.com
 * @copyright Bastian Allgeier
 * @license   http://www.opensource.org/licenses/mit-license.php MIT License
 */
class Email extends Obj {

  const ERROR_INVALID_RECIPIENT = 0;
  const ERROR_INVALID_SENDER = 1;
  const ERROR_INVALID_REPLY_TO = 2;
  const ERROR_INVALID_SUBJECT = 3;
  const ERROR_INVALID_BODY = 4;
  const ERROR_INVALID_SERVICE = 5;
  const ERROR_DISABLED = 6;

  static public $services = array();
  static public $disabled = false;

  public $error = null;

  public function __set($key, $value) {
    $this->$key = $value;
  }

  /**
   * Validates the constructed email
   * to make sure it can be sent at all
   */
  public function validate() {
    if(!v::email($this->to))      throw new Error('Invalid recipient', static::ERROR_INVALID_RECIPIENT);
    if(!v::email($this->from))    throw new Error('Invalid sender', static::ERROR_INVALID_SENDER);
    if(!v::email($this->replyTo)) throw new Error('Invalid reply address', static::ERROR_INVALID_REPLY_TO);
    if(!isset($this->subject))    throw new Error('Missing subject', static::ERROR_INVALID_SUBJECT);
    if(!isset($this->body))       throw new Error('Missing body', static::ERROR_INVALID_BODY);
  }

  /**
   * Public getter for the error exception
   *
   * @return Exception
   */
  public function error() {
    return $this->error;
  }

  /**
   * Extracts the email address from an address string
   *
   * @return string
   */
  protected function extractAddress($string) {
    if(v::email($string)) return $string;
    preg_match('/<(.*?)>/i', $string, $array);
    return (empty($array[1])) ? $string : $array[1];
  }

  /**
   * Sends the constructed email
   *
   * @param array $params Optional way to set values for the email
   * @return boolean
   */
  public function send($params = null) {

    try {

      // fail silently if sending emails is disabled
      if(static::$disabled) throw new Error('Sending emails is disabled', static::ERROR_DISABLED);

      // overwrite already set values
      if(is_array($params) and !empty($params)) {
        if(isset($params['service'])) $this->service = $params['service'];
        if(isset($params['options'])) $this->options = $params['options'];
        if(isset($params['to']))      $this->to      = $params['to'];
        if(isset($params['from']))    $this->from    = $params['from'];
        if(isset($params['replyTo'])) $this->replyTo = $params['replyTo'];
        if(isset($params['subject'])) $this->subject = $params['subject'];
        if(isset($params['body']))    $this->body    = $params['body'];
      }

      // default service
      if(empty($this->service)) $this->service = 'mail';

      // if there's no dedicated reply to address, use the from address
      if(empty($this->replyTo)) $this->replyTo = $this->from;

      // validate the email
      $this->validate();

      // check if the email service is available
      if(!isset(static::$services[$this->service])) {
        throw new Error('The email service is not available: ' . $this->service, static::ERROR_INVALID_SERVICE);
      }

      // run the service
      call(static::$services[$this->service], $this);

      // reset the error
      $this->error = null;
      return true;

    } catch(Exception $e) {
      $this->error = $e;
      return false;
    }

  }

}


/**
 * Default mail driver
 */
email::$services['mail'] = function($email) {

  $headers = array(
    'From: ' . $email->from,
    'Reply-To: ' . $email->replyTo,
    'Return-Path: ' . $email->replyTo,
    'Message-ID: <' . time() . '-' . $email->from . '>',
    'X-Mailer: PHP v' . phpversion(),
    'Content-Type: text/plain; charset=utf-8',
    'Content-Transfer-Encoding: 8bit',
  );

  ini_set('sendmail_from', $email->from);
  $send = mail($email->to, str::utf8($email->subject), str::utf8($email->body), implode(PHP_EOL, $headers));
  ini_restore('sendmail_from');

  if(!$send) {
    throw new Error('The email could not be sent');
  }

};

/**
 * Amazon mail driver
 */
email::$services['amazon'] = function($email) {

  if(empty($email->options['key']))    throw new Error('Missing Amazon API key');
  if(empty($email->options['secret'])) throw new Error('Missing Amazon API secret');

  $setup = array(
    'Action'                           => 'SendEmail',
    'Destination.ToAddresses.member.1' => $email->to,
    'ReplyToAddresses.member.1'        => $email->replyTo,
    'ReturnPath'                       => $email->replyTo,
    'Source'                           => $email->from,
    'Message.Subject.Data'             => $email->subject,
    'Message.Body.Text.Data'           => $email->body
  );

  $params = array();

  foreach($setup as $key => $value) {
    $params[] = $key . '=' . str_replace('%7E', '~', rawurlencode($value));
  }

  sort($params, SORT_STRING);

  $host      = a::get($email->options, 'host', 'email.us-east-1.amazonaws.com');
  $url       = 'https://' . $host . '/';
  $date      = gmdate('D, d M Y H:i:s e');
  $signature = base64_encode(hash_hmac('sha256', $date, $email->options['secret'], true));
  $query     = implode('&', $params);
  $headers   = array();
  $auth      = 'AWS3-HTTPS AWSAccessKeyId=' . $email->options['key'];
  $auth     .= ',Algorithm=HmacSHA256,Signature=' . $signature;

  $headers[] = 'Date: ' . $date;
  $headers[] = 'Host: ' . $host;
  $headers[] = 'X-Amzn-Authorization: '. $auth;
  $headers[] = 'Content-Type: application/x-www-form-urlencoded';

  $email->response = remote::post($url, array(
    'data'    => $query,
    'headers' => $headers
  ));

  if(!in_array($email->response->code(), array(200, 201, 202, 204))) {
    throw new Error('The mail could not be sent!', $email->response->code());
  }

};

/**
 * Mailgun mail driver
 */
email::$services['mailgun'] = function($email) {

  if(empty($email->options['key']))    throw new Error('Missing Mailgun API key');
  if(empty($email->options['domain'])) throw new Error('Missing Mailgun API domain');

  $url  = 'https://api.mailgun.net/v2/' . $email->options['domain'] . '/messages';
  $auth = base64_encode('api:' . $email->options['key']);

  $headers = array(
    'Accept: application/json',
    'Authorization: Basic ' . $auth
  );

  $data = array(
    'from'       => $email->from,
    'to'         => $email->to,
    'subject'    => $email->subject,
    'text'       => $email->body,
    'h:Reply-To' => $email->replyTo,
  );

  $email->response = remote::post($url, array(
    'data'    => $data,
    'headers' => $headers
  ));

  if($email->response->code() != 200) {
    throw new Error('The mail could not be sent!');
  }

};

/**
 * Postmark mail driver
 */
email::$services['postmark'] = function($email) {

  if(empty($email->options['key'])) throw new Error('Invalid Postmark API Key');

  // reset the api key if we are in test mode
  if(a::get($email->options, 'test')) $email->options['key'] = 'POSTMARK_API_TEST';

  // the url for postmarks api
  $url = 'https://api.postmarkapp.com/email';

  $headers = array(
    'Accept: application/json',
    'Content-Type: application/json',
    'X-Postmark-Server-Token: ' . $email->options['key']
  );

  $data = array(
    'From'     => $email->from,
    'To'       => $email->to,
    'ReplyTo'  => $email->replyTo,
    'Subject'  => $email->subject,
    'TextBody' => $email->body
  );

  // fetch the response
  $email->response = remote::post($url, array(
    'data'    => json_encode($data),
    'headers' => $headers
  ));

  if($email->response->code() != 200) {
    throw new Error('The mail could not be sent');
  }

};