<?php
// $Id: messaging_method.class.inc,v 1.1.2.1 2010/01/25 14:07:56 jareyero Exp $
/**
 * @file
 * Drupal Messaging Framework - Send_Method class file
 */

/**
 * Sending method, implements all specific method functionality
 * 
 * Old callback functions are
 * - send
 * - destination
 */
class Messaging_Send_Method {
  // Method key
  public $method;
  // Full info array
  public $info;

  function __construct($info, $method) {
    $this->method = $method;
    $this->info = $info;
  }
  
  function get_info($property = NULL) {
    if ($property) {
      return isset($this->info[$property]) ? $this->info[$property] : NULL;
    }
    else {
      return $this->info;
    }
  }
  
  /**
   * Send message to destination by calling the method's send callback
   */
  function send($destination, $message) {
    // Translate destination object to old fashion parameters
    $callback = $this->get_callback('send');
    $params = $message->get_params($this->method);
    return $this->callback_invoke($callback, $destination, $message, $params);
  }
  /**
   * Send a message to multiple destinations
   * 
   * @param $source
   *   Message or message template
   * @param $destinations
   *   Array of destinations for this method
   */
  function send_multiple($source, $destinations) {
    $result = array();
    $template = $source->build($this->method, NULL);
    $template->destinations = $destinations;
    $template->prepare();
    $template->render();
    foreach ($destinations as $index => $destination) {
      $message = clone $template;
      $message->destination = $destination;
      $result[$index] = $message->send();
    }
    return $result;
  }  
  /**
   * Queue multiple copies of the same message
   * 
   * @param $source
   *   Message or message template
   * @param $destinations
   *   Array of destinations for this method
   */
  function queue_multiple($source, $destinations) {
    $result = array();
    $template = $source->build($this->method, NULL);
    $template->destinations = $destinations;
    $template->prepare();
    $template->queue = 1;
    $template->render();
    foreach ($destinations as $index => $destination) {
      $message = clone $template;
      $message->destination = $destination;
      $result[$index] = $message->queue();
    }
    return $result;
  }
  /**
   * Get destination for user
   */
  function user_destination($account) {
    if (($property = $this->destination) && !empty($account->$property)) {
      // Get destination property from user account
      return $account->$property;
    }
    elseif ($callback = $this->get_callback('destination')) {
      // Backwards compatibility, call destination callback
      return $this->callback_invoke($callback, $account);
    }
  }
  /**
   * Check user availability
   */
  /**
   * Get default sender
   */
  function get_default_sender() {
    return array(
      'name' => variable_get('site_name', 'Drupal'),
      'from' => '',
    );
  }
  /**
   * Prepare message for processing with this method
   */
  function message_prepare($message) {
    // If the messaging method is of type push, cron processing will be enabled
    if ($message->queue && ($this->type & MESSAGING_TYPE_PUSH)) {
      $this->cron = 1;
    }
    // It will be queued always for pull methods, cron disabled though, so it will wait till it's pulled
    if (!$message->queue && ($this->type & MESSAGING_TYPE_PULL)) {
      $message->queue = 1;
      $message->cron = 0;
    }
  }
  /**
   * Check destination, user availability, etc...
   * 
   * At this stage it is still possible to redirect to other sending method with $message->redirect = TRUE
   */
  function message_check($message) {
    
  }
  
  /**
   * Run before sending prepared message
   */
  function message_presend($message) {
  }
  /**
   * Send a message to a single destination
   */
  function message_send($message) {
    $message->prepare();
    $message->render();
    return $this->send($message->destination, $message);
  }
  /**
   * Queue the message
   */
  function message_queue($message) {
    $message->result = TRUE;
    $message->queue = 1;
    $message->save();
    return $message->result;
  }

  /**
   * Test message sending
   */
  function message_test($message) {
    $message->log('Emulating message sending (test run)', array('message' => (string)$message));
  }
  /**
   * Run after sending message
   */
  function message_aftersend($message) {
    // Depending on parameters and what's happened so far we make the final queue/log decision
    if ($message->queue || $message->log) {
      $message->store();
    }
  }
  /**
   * Run after message has been queued
   */
  function message_afterqueue($message) {
  }

  /**
   * Render a template object
   * 
   * It builds subject and body properties
   */
  function message_render($template) {
    $info = $this->info + array('glue' => ' ', 'filter' => NULL);
    // Render body if anything to render
    if ($body = $template->body) {
      // Apply footer prefix if provided and the message has a footer element. 
      // Note: If $body is a string the final isset($body['footer']) will be true
      if (is_array($body) && !empty($info['footer']) && isset($body['footer'])) {
        $body['footer_prefix'] = $info['footer'];
        $body['footer_text'] = $body['footer'];
        unset($body['footer']);
      }
      $template->body = $this->render_text($body, $info['glue'], $info['filter']);
    }
    // Render subject if anything to render
    if ($subject = $template->subject) {
      // Render separately subject and body info, adding default parameters
      $info += array('subject_glue' => ' ');
      $template->subject = $this->check_subject($this->render_text($subject, $info['subject_glue']));
    }
    return $template; 
  }
  
  /**
   * Composes message from different parts, recursively and applies filter
   * 
   * Filter is applied now only once
   * 
   * @param $text
   *   Simple string or array of message parts
   *   It may have named elements like #prefix and #text
   *   or it may be single strings to render straight forward
   * @param $glue
   *   Text to glue all lines together
   * @param $filter
   *   Input format to apply to the results
   */
  function render_text($text, $glue, $filter = NULL) {
    if (!$text) {
      return '';
    }
    elseif (is_array($text)) {
      $elements = array();
      // First render children recursively, without filtering
      foreach (element_children($text) as $key) {
        $elements[$key] = $this->render_text($text[$key], $glue);
      }
      // Apply theme if set, just implode if not
      if (!empty($text['#theme'])) {
        $output = theme($text['#theme'], $text, $glue);
      }
      else {
        $output = implode($glue, $elements);
      }
    } else {
      $output = $text;
    }
    // The filter is applied now only once
    if ($filter) {
      $output = check_markup($output, $filter, FALSE);
    }
    return $output;   
  }
  
  /**
   * Converts strings to plain utf-8 single line
   */
  static function check_subject($text) {
    $text = messaging_check_plain($text);
    // taken from _sanitizeHeaders() in PEAR mail() : http://pear.php.net/package/Mail
    $text = preg_replace('=((0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', NULL, $text);
    return $text;
  }
  
  /**
   * Implement for testing
   */
  function test($destination, $message) {
    return TRUE;
  }
  /**
   * Magic function, getting non existent properties
   */
  public function __get($name) {
    if (isset($this->info[$name])) {
      return $this->info[$name];
    }
  }

  /**
   * Magic function, invoking non existent methods
   * 
   * For backwards compatibility with old array callbacks
   */
  public function __call($name, $arguments) {
    // If we have an old style callback like this, go for it
    if ($callback = $this->get_callback($name)) {
      if (is_array($callback)) { // It is an array: function, arg1, arg2...
        $function = array_shift($callback);
        $arguments = array_unshift($callback, $arguments);
      } else { // It is just a function name
        $function = $callback;
      }
      return call_user_func_array($function, $params); 
    }
  }
  /**
   * Get callback from method info
   */
  public function get_callback($type) {
    return $this->get_info($type . ' callback');
  }
  /**
   * Invoke callback with a variable number of arguments
   */
  public function callback_invoke() {
    $args = func_get_args();
    $callback = array_shift($args);
    if (is_array($callback)) { // It is an array: function, arg1, arg2...
      $function = array_shift($callback);
      $params = $callback;
    } else { // It is just a function name
      $function = $callback;
      $params = array();
    }
    // Merge parameters and go for it
    $params = array_merge($params, $args);
    return call_user_func_array($function, $params);
  }
}

/**
 * Base class for mail sending methods
 */
class Messaging_Mail_Method extends Messaging_Send_Method {
  public $default_from;

  function __construct($info, $method) {
    parent::__construct($info, $method);
    $this->default_from = variable_get('site_mail', ini_get('sendmail_from'));
  }
  function get_default_sender() {
    return array(
      'name' => variable_get('site_name', 'Drupal'),
      'from' => $this->default_from,
    );    
  }
  
  /**
   * Rebuild message in Drupal mail format

   * @param $message
   *   Message object
   */
  function message_prepare($message) {
    if (empty($message->params['from'])) {
      if (!empty($message->sender_account) && !empty($message->sender_account->mail)) {
        $from = check_plain($message->sender_account->name) . ' <'.$message->sender_account->mail.'>';
      } elseif(!empty($message->sender_name) && $default_from) {
        $from = check_plain($message->sender_name) . ' <'.$this->default_from.'>';
      } else {
        $from = $this->default_from;
      }
      $message->params['from'] = $from;
    }
    $message->params['mail_headers'] = $this->mail_headers($message);
  }
  /**
   * Get mail headers. Helper function for mail methods
   */
  function mail_headers($message) {
    
    $headers = !empty($message->params['headers']) ? $message->params['headers'] : array();
    
    // Add some default headers
    $headers += array(
      'MIME-Version'              => '1.0',
      'Content-Type'              => 'text/plain; charset=UTF-8; format=flowed; delsp=yes',
      'Content-Transfer-Encoding' => '8Bit',
      'X-Mailer'                  => 'Drupal'
    );

    $from = !empty($message->params['from']) ? $message->params['from'] : $this->default_from;
  
    // Set default headers depending on data
    $headers += array(
      'From' => $from,
      'Reply-To' => $from,
    );
    
    if ($this->default_from) {
      // To prevent e-mail from looking like spam, the addresses in the Sender and
      // Return-Path headers should have a domain authorized to use the originating
      // SMTP server. Errors-To is redundant, but shouldn't hurt.
      $more_headers['Sender'] = $more_headers['Return-Path'] = $more_headers['Errors-To'] = $this->default_from;
      $headers += $more_headers;
    }  
  
    return $headers;
  }
  /**
   * Message ready for sending, invoke mail alter
   */
  function message_presend($message) {
    $mail = array(
      'id'       => 'messaging_'. (!empty($message->type) ? 'message-'.$message->type : 'message'),
      'to'       => $message->destination,
      'from'     => $message->params['from'],
      'language' => $message->get_language(),
      'params'   => $message->params,
      'subject'  => $message->subject,
      'body'     => $message->body,
      'headers'  => $message->params['mail_headers'],
      'attachments' => !empty($message->files) ? $message->files : array(),
    );
    // Invoke hook_mail_alter()
    drupal_alter('mail', $mail);
    // Rebuild message with results
    $message->destination = $mail['to'];
    $message->params = $mail['params']; 
    $message->params['from'] = $mail['from'];
    $message->params['mail_headers'] = $mail['headers'];
    $message->subject = $mail['subject'];
    $message->body = $mail['body'];
    // Other parts are not suppossed to change, like language, files
  }
}
