<?php
/** $Id: Form.php 51 2009-07-30 19:06:06Z lundav0629 $ */
// {{{ license

// +----------------------------------------------------------------------+
// | Copyright (c) 2011 Brooks Institute                                  |
// +----------------------------------------------------------------------+
// | This source file is subject to the GNU Lesser Public License (LGPL), |
// | that is bundled with this package in the file LICENSE, and is        |
// | available at through the world-wide-web at                           |
// | http://www.fsf.org/copyleft/lesser.html                              |
// | If you did not receive a copy of the LGPL and are unable to          |
// | obtain it through the world-wide-web, you can get it by writing the  |
// | Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, |
// | MA 02111-1307, USA.                                                  |
// +----------------------------------------------------------------------+
// | Authors: David Lundgren <dlundgren@syberisle.net>                    |
// +----------------------------------------------------------------------+

// {{{ Requires
/**
 * Required PEAR classes
 */
require_once('Mail.php');
require_once(
'Mail/mime.php');

// }}}
// {{{ defines

define('ASTERISK_FAX_RX_FAIL',    0);
define('ASTERISK_FAX_RX_SUCCESS'1);
define('ASTERISK_FAX_TX_FAIL',    2);  
define('ASTERISK_FAX_TX_SUCCESS'3);
define('ASTERISK_FAX_PENDING',    5);
define('ASTERISK_FAX_ERROR',      6);

// }}}
// {{{ Asterisk
/**
 * This class is designed to be used from the Asterisk server. It currently
 * only works against a realtime database that is designed for Brooks Institute.
 *
 * @class  Asterisk
 * @author David Lundgren
 */
class Asterisk
{
    
// {{{ properties

    /**
     * Error message
     */
    
public $error;

    
/**
     * The data from the database query
     */
    
protected $_data;

    
/**
     * The MySQL database connection
     */
    
protected $_db;

    
/**
     * The Asterisk MySQL configuration.
     * @var array
     */
    
protected $_config;


    
/**
     * Whether or not we are in debug mode.
     */
    
public $debug false;

    
// }}}
    // {{{ functions

    /**
     * Loads the database connection from either the configuration or the globally
     * set s_mysql* variables
     *
     * @param   string  $in_config The namespace of the configuration to load into
     * @param   string  $in_file   The file to load into the namespace.
     * @returns boolean True on success, False on failure.
     */
    
protected function _loadConfigFile($in_config$in_file)
    {
        if ( ! 
file_exists($in_file)) {
            return 
false;
        }

        
$this->_config[$in_config] = parse_ini_file($in_filetrue);
        return 
true;
    }

    
/**
     * Class constructor. Responsible for loading the configuration file and
     * connecting to the mysql database.
     *
     * @returns void
     */
    
function __construct()
    {
        if (empty(
$GLOBALS['a_config'])) {
            die(
'Missing configuration information');
        }
        
$this->_config $GLOBALS['a_config'];

        if ( ! empty(
$this->_config['files']['mysql_conf']) && $this->_loadConfigFile('database'$this->_config['files']['mysql_conf'])) {
            
$s_section 'asterisk';
            if ( ! empty(
$this->_config['database']['section'])) {
                
$s_section $this->_config['database']['section'];
            }
            
$this->_config['database'] = $this->_config['database'][$s_section];
        }

        
$this->_db = new mysqli($this->_config['database']['dbhost'],
                                
$this->_config['database']['dbuser'],
                                
$this->_config['database']['dbpass'],
                                
$this->_config['database']['dbname'],
                                (int)
$this->_config['database']['dbport']);            

        if (empty(
$this->_config['database']['max_packet'])) {
            
// Assume an 8K packet is small enough
            
$this->_config['database']['max_packet'] = 8192;
        }

        if ( ! 
$this->_db) {
            
$this->setError('Database Error: no connection to server.');
        }

    }

    
/**
     * Function for debugging
     *
     * @param  string  $in_msg The message to print.
     * @param  boolean $in_die Whether or not to terminate the script.
     * @return void
     */
    
function debug($in_msg$in_die false)
    {
        if ( ! 
$this->debug) {
            return;
        }

        echo 
"$in_msg\n";
        if (
$in_die) {
            die(
"DEBUG terminated\n");
        }
    }

    
/**
     * Generic function to get the data from _data to the user or set it.
     *
     * @param   string $in_method The method that is being called.
     * @param   array  $in_params The parameters passed to $in_method.
     * @returns mixed  Null if non-existent, void, or the data.
     */
    
function __call($in_method$in_params)
    {
        
/* translate CamelCase into underscore */
        
$s_name strtolower(preg_replace('#([A-Z])#''_\1'$in_method));
        if (
'get_' == substr($s_name04)) {
            
$s_name str_replace('get_'''$s_name);
            if ( ! empty(
$this->_data[$s_name])) {
                return 
$this->_data[$s_name];
            }
        }
        elseif (
'set_' == substr($s_name04)) {
            
$s_name str_replace('set_'''$s_name);
            
$this->_data[$s_name] = $in_params[0];
        }

        return 
null;
    }

    
/**
     * Sends a blob to the database from the given file.
     *
     * @param   object  $in_stmt  The MySQL Statement object.
     * @param   integer $in_field The index of the field in the query.
     * @param   string  $in_file  The file to load into the BLOB field.
     * @returns void
     */
    
function sendBlob($in_stmt$in_field$in_file)
    {
        
$o_file fopen($in_file'r');
        while ( ! 
feof($o_file)) {
            
$in_stmt->send_long_data($in_fieldfread($o_file$this->_config['database']['max_packet'] - 1024));
        }
        
fclose($o_file);
    }

    
/**
     * Sets the error to true, and sets the error message. Meant to be used
     * as a return value on error.
     *
     * @param   string $in_msg The error message.
     * @returns false always.
     */
    
function setError($in_msg)
    {
        
$this->error true;
        
$this->error_message $in_msg;
        return 
false;
    }
}

// }}}
// {{{ Asterisk_Phone

/**
 * Handles SIP Phones within the context of the Asterisk realtime Fax solution.
 */
class Asterisk_Phone extends Asterisk
{
    
// {{{ functions
    /**
     * Fill the data by looking up the extension
     *
     * @param   string $in_ext The extension to lookup.
     * @returns integer The count of the _data array that was filled in.
     */
    
public function fillByExtension($in_ext)
    {
        
$in_ext = (int)$in_ext// sanitize the input
        
try {
            
$this->_data $this->_db->query("SELECT e.email, e.extension, e.mailbox, e.type, sd.secret
                                              FROM extensions AS e
                                                LEFT JOIN sip_devices AS sd ON (sd.id = e.id)
                                              WHERE e.extension=
$in_ext")->fetch_assoc();
        }
        catch (
Exception $e) {
            
$this->_data = array();
        }
        return 
count($this->_data);
    }

    
/**
     * Fill the data by looking up the MAC address
     *
     * @param   string $in_ext The MAC Address to lookup.
     * @returns integer The count of the _data array that was filled in.
     */
    
public function fillByMacAddress($in_mac)
    {
        
$in_mac $this->_db->real_escape_string($in_mac); // sanitize the input
        
try {
            
$this->_data $this->_db->query("SELECT extension FROM extensions WHERE mac_address='$in_mac'")->fetch_assoc();
        }
        catch (
Exception $e) {
            
$this->_data = array();
        }
        return 
count($this->_data);
    }
}

// }}}
// {{{ Asterisk_Fax
/**
 * Handles Faxes within the context of the Asterisk realtime Fax solution.
 */
class Asterisk_Fax extends Asterisk
{
    
// {{{ properties

    /**
     * Errors are set to ASTERSK_TX_FAIL unless otherwise specified below
     */
    
protected $errors = array(
        
// Link Problems
        
'Far end cannot receive at the resolution of the image',
        
'Far end cannot receive at the size of image',

        
// TIFF file problems
        
'TIFF/F file cannot be opened',
        
'TIFF/F page not found',
        
'TIFF/F format is not compatible',
        
'TIFF/F page number tag missing',
        
'Incorrect values for TIFF/F tags',
        
'Bad TIFF/F header - incorrect values in fields',
        
'Cannot allocate memory for more pages',

        
// General problems
        
'Disconnected after permitted retries',

    );

    protected 
$replacementKeys;
    protected 
$replacementValues;


    protected 
$log;
    
    
// }}}
    // {{{ functions

    /**
     * Class constructor that sets up the replacementKeys as well as the various
     * tables that this class uses.
     */
    
function __construct()
    {
        
$this->replacementKeys = array('{FAX_TO_NUMBER}',
                                       
'{FAX_FROM_NUMBER}',
                                       
'{FAX_DATE}',
                                       
'{FAX_PAGES}',
                                       
'{FAX_CALLERID}',
                                       
'{FAX_CALLERID_NAME}',
                                       
'{FAX_CALLERID_NUMBER}',
                                       
'{FAX_ID}');
        
parent::__construct();
        
$this->table            $this->_config['data']['asterisk_faxes_table'];
        
$this->dataTable        $this->_config['data']['asterisk_fax_data_table'];
        
$this->extensionsTable  $this->_config['data']['asterisk_extensions_table'];

        if (empty(
$this->extensionsTable)) {
            
$this->extensionsTable 'extensions';
        }

        if (empty(
$this->table)) {
            
$this->table 'faxes';
        }

        if (empty(
$this->dataTable)) {
            
$this->dataTable 'fax_data';
        }

        if (!empty(
$this->_config['fax_log'])) {
            
$this->log fopen($this->_config['fax_log'], 'a+');
        }
    }

    
/**
     * Class destructor. Automatically deletes the mp3 and original message
     * file if they have been used.
     *
     * @returns void
     */
    
function __destruct()
    {
        if ( ! empty(
$this->pdf_file)) {
            
unlink($this->pdf_file);
        }

        if (
$this->log) {
            
fclose($this->log);
        }
        
parent::__destruct();
    }


    function 
getCallerIdName()
    {
        
$s_callerId $this->getCallerId();
        if (
preg_match('/^"([^\"]*)"/i'$s_callerId$a_matches)) {
            return 
$a_matches[1];
        }
        return 
$this->getNotifyName();
    }

    function 
getCallerIdNumber()
    {
        
$s_callerId $this->getCallerId();
        if (
preg_match('/\<([^\>]*)\>$/i'$s_callerId$a_matches)) {
            return 
$a_matches[1];
        }
        return 
$this->getFromNumber();
    }
    function 
log($in_msg)
    {
            
$this->debug($in_msg);
        if (!
is_object($this->log)) {
            return;
        }
        
$s_pid getmypid();
        
fwrite($this->log"[$s_pid$in_msg\n");
    }

    
/**
     * Fills in the replacement keys with the replacement values
     *
     * @returns void
     */
    
protected function _fillInReplacementValues()
    {
        
$this->replacementValues = array($this->getToNumber(),
                                         
$this->getFromNumber(),
                                         
$this->getFaxDate(),
                                         
$this->getPages(),
                                         
$this->getCallerId(),
                                         
$this->getCallerIdName(),
                                         
$this->getCallerIdNumber(),
                                         
$this->getId());
    }


    
/**
     * Fill the data by looking up the ID
     *
     * @param   string $in_id The ID to lookup.
     * @returns integer The count of the _data array that was filled in.
     */
    
function fillById($in_id)
    {
        
$in_id = (int)$in_id;
        try {
            
$this->_data $this->_db->query("SELECT f.*
                                              FROM 
$this->table AS f
                                              WHERE f.id='
$in_id'")->fetch_assoc();
        }
        catch (
Exception $e) {
            
$this->_data = array();
        }
        return 
count($this->_data);        
    }

    
/**
     * Fill the data by looking up the fax file name
     *
     * @param   string  $in_file The file to lookup.
     * @returns integer The count of the _data array that was filled in.
     */
    
function fillByFaxFile($in_file)
    {
        
$in_file $this->_db->real_escape_string($in_file);
        try {
            
$this->_data $this->_db->query("SELECT f.id, f.file_name, f.date_created, f.from_number, f.to_number, f.caller_id, f.pages, e.email
                                              FROM 
$this->table AS f
                                                LEFT JOIN 
$this->extensionsTable AS e ON (e.extension = f.to_number)
                                              WHERE file_name='
$in_file'")->fetch_assoc();
        }
        catch (
Exception $e) {
            
$this->_data = array();
        }
        return 
count($this->_data);
    }

    
/**
     * Determines the caller id, caller id number, and to number from the local
     * station information.
     *
     * @param   string $in_value The LocalStation information.
     * @returns void
     */
    
function setLocalStation($in_value)
    {
        
$this->_data['local_station'] = $in_value;
        if (
preg_match('/^"([^"]*)" <([^>]*)>$/i'$in_value$a_match)) {
            
$this->setCallerId($a_match[1]);
            
$this->setCallerIdNumber($a_match[2]);
            
$this->setToNumber(substr($a_match[2], -4));
        }
    }

    
/**
     * Returns the Fax date. Formatted per the configuration if set.
     *
     * @returns string The date of the fax.
     */
    
function getFaxDate()
    {
        if ( ! empty(
$this->_config['fax']['in']['emaildateformat'])) {
            return 
date($this->_config['fax']['in']['emaildateformat'], strtotime($this->getDateCreated()));
        }

        return 
$this->getDateCreated();
    }

    
/**
     * Converts the file into PDF using ImageMagick's convert utility.
     *
     * @param   string $in_file The file to convert to PDF.
     * @returns string The PDF file name.
     */
    
function convertToPdf($in_file)
    {
        if ( ! empty(
$this->pdf_file)) {
            return 
$this->pdf_file;
        }

        
$tmp_file str_replace('.tiff''.pdf'$in_file);
        
exec("/usr/bin/convert {$in_file} {$tmp_file} 2>&1"$a_output$s_result);
        if (
$s_result) {
            return 
false;
        }

        
$this->pdf_file $tmp_file;
        return 
$this->pdf_file;
    }

    
/**
     * Sets the error as well as the Fax error and updates the database with the
     * error.
     *
     * @param   string  $in_msg The error message.
     * @returns boolean False always.
     */
    
function setError($in_msg)
    {
        
$s_faxError $this->_db->real_escape_string($in_msg);
        
$s_id       $this->getId();
        
$this->_db->query("UPDATE $this->table SET error='$s_faxError' WHERE id='$s_id'");
        return 
false;
    }

    
// }}}
    // {{{ FAX: Transmit
    /**
     * Generates the Call file that Asterisk uses to actually send the fax.
     *
     * @param   string  $in_name   The name of this fax.
     * @param   array   $in_params The parameters of the call file.
     * @returns boolean True on creating, false otherwise.
     */
    
function generateCallFile($in_name$in_params)
    {   
        foreach(
$in_params as $s_name => $s_value) {
            if (
'SetVar' == $s_name) {
                foreach(
$s_value as $s_key => $s_val) {
                    
$a_output[] = "SetVar: $s_key=$s_val";
                }
            }
            else {
                
$a_output[] = "$s_name$s_value";
            }
        }

        
$s_file $this->_config['fax']['out']['call_path'] . "/$in_name";

        
$s_output implode("\n"$a_output);
        
$s_output str_replace($this->replacementKeys,
                                
$this->replacementValues,
                                
$s_output);
        if (
file_put_contents($s_file$s_output)) {
            return 
true;
        }

        return 
false;
    }

    
/**
     * Generates the Fax data and call file for Asterisk.
     *
     * @param   boolean $in_isRetry Whether or not this is a retry attempt.
     * @returns boolean True on success, False on failure.
     */
    
function transmit($in_isRetry false)
    {
        
$s_toNumber $this->getToNumber();
        if (empty(
$s_toNumber)) {
            return 
$this->setError('No fax number specified.');
        }

        
// we need to save the data out from the database
        
$s_id = (int)$this->getId();
        try {
            
$o_faxData $this->_db->query("SELECT data FROM $this->dataTable WHERE fax_id='$s_id'")->fetch_object();
        }
        catch ( 
Exception $e) {
            return 
$this->setError('Fax data is unavailable from database.');
        }

        
$s_attempts $this->getAttempts();
        
$pth_spoolFile $this->_config['fax']['out']['spool_path'] . "/{$s_id}.fax";
        if ( ! 
file_put_contents($pth_spoolFile$o_faxData->data)) {
            return 
$this->setError('Fax data could not be saved.');
        }

        
// the data is saved out to the spool file, now generate the call file
        
$this->_fillInReplacementValues();
        
        
array_push($this->replacementKeys,   '{FAX_TIFF}');
        
array_push($this->replacementValues$pth_spoolFile);

        
// generate the Asterisk Callfile
        
$a_callParams $this->_config['fax']['out']['call_params'];
        if ( ! 
$this->generateCallFile("{$s_id}-{$s_attempts}"$a_callParams)) {
            return 
$this->setError('Failed to generate the fax call.');
        }

        
$this->debug('Transmital'true);

        
// update the retry attempt
        
if ($in_isRetry) {
            
$s_query "UPDATE $this->table SET attempts=attempts+1 WHERE id='$s_id'";
            
$this->_db->query($s_query);
        }

        return 
true;
    }

    
/**
     * Determines if this fax has exceeded the maximum retries
     *
     * @return boolean True if exceeeded, false otherwise.
     */
    
function hasExceededRetries()
    {
        
$s_status $this->getStatus();
        if (
$s_status == ASTERISK_FAX_TX_FAIL ||
            
$s_status == ASTERISK_FAX_TX_SUCCESS) {
            return 
true;
        }
        
        
// NOTE: the system starts off at 1 retry (SQL default)
        
$s_maxRetries = (int)$this->_config['fax']['out']['max_retries'];
        if (!
$s_maxRetries) {
            
$s_maxRetries 3;
        }
        
$s_attempts   $this->getAttempts();
        return (
$s_attempts $s_maxRetries) ? true false;
    }

    
/**
     * Checks if the error is fatal or not.
     *
     * @return boolean True if exceeded, false otherwise.
     */
    
function isFatalError()
    {
        if (
false === array_search($s_error$this->errors)) {
            return 
false;
        }
        
        return 
true;
    }

    
/**
     * Parses the error and changes the systemto handle it appropriately.
     *
     * @return void
     */
    
function parseError()
    {
        
$s_error $this->getError();
        if (empty(
$s_error)) {
                
$this->debug('no error detected');
            return;
        }
        
$this->debug($s_error);

        
$s_id $this->getId();

        if (
false === array_search($s_error$this->errors) && ! $this->hasExceededRetries()) {
            if (
$this->debug) {
                
$this->debug("resending fax: $s_id");
                return;
            }

            
$this->setStatus(ASTERISK_FAX_PENDING);
            
$this->log("resending fax: $s_id");
            
$this->_db->query("UPDATE $this->table SET error='', fax_status='" ASTERISK_FAX_PENDING "' WHERE id='$s_id'");
            return;
        }

        
// set it as failed so the user knows
        
if ($this->debug) {
            
$this->debug("setting fax as failed: $s_id");
            return;
        }
        
$this->setStatus(ASTERISK_FAX_TX_FAIL);
        
$this->_db->query("UPDATE $this->table SET fax_status='" ASTERISK_FAX_TX_FAIL "' WHERE id='$s_id'");            
    }

    
/**
     * Sends a notification about the Fax transmittal.
     *
     * @returns boolean True on success, False on failure.
     */
    
function sendTransmitNotification()
    {
        
// Asterisk sendFax does not fail in the case when a call is place but
        // the fax is ignored by the other side. We need to check and verify the
        // errors in these cases.
        
$this->checkTransmitForError();

        
$s_email $this->getNotifyEmail();
        if (empty(
$s_email)) {
            
// do not attempt to send a report to nobody
            
return false;
        }

        
$o_mail =& Mail::factory($this->_config['mailer']['type'], $this->_config['mailer']['params']);
        if (
PEAR::isError($o_mail)) {
            
$this->error $o_mail->getMessage();
            return 
false;
        }

        
$s_reportType = ($this->getFaxStatus() == ASTERISK_FAX_TX_FAIL) ? 'report_failure' 'report_success';

        
// replacements
        
$this->_fillInReplacementValues();
        
array_push($this->replacementKeys,   '{FAX_NOTIFY_NAME}');
        
array_push($this->replacementValues$this->getNotifyName());

        
// Generate the email headers 
        
$s_subject str_replace($this->replacementKeys,
                                 
$this->replacementValues,
                                 
$this->_config['fax']['out'][$s_reportType]['subject']);
        
$a_headers = array(
            
'From'    => '"'.$this->_config['mailer']['from']['name'].'" <'.$this->_config['mailer']['from']['email'].'>',
            
'To'      => $s_email,
            
'Subject' => $s_subject,
        );

        
// Generate the body
        
$s_body str_replace($this->replacementKeys,
                              
$this->replacementValues,
                              
$this->_config['fax']['out'][$s_reportType]['message']);


        
$this->debug("E-mail sent to $s_email"true);

        
// send the message
        
$o_results $o_mail->send($s_email$a_headers$s_body);

        if (
PEAR::isError($o_results)) {
            
$this->error $o_results->getMessage();
            return 
false;
        }

        return 
true;
    }

    
// }}}
    // {{{ FAX: Receive
    /**
     * Receives a fax.
     *
     * @returns True on success, False otherwise.
     */
    
function receive()
    {
        
$s_file $this->getFileName();
        
$s_faxFormat 'TIFF';
        if ( ! empty(
$this->_config['fax']['in']['savepdf']) && $this->_config['fax']['in']['savepdf'] === true) {
            
$s_file $this->convertToPdf($s_file);
            
$s_faxFormat 'PDF';
        }
    
        
$s_fax file_get_contents($s_file);
        if ( ! 
$s_fax) {
            
$this->error 'Unable to load the fax file.';
            return 
false;
        }

        
$s_id $this->getId();

        
// Update the fax format
        
$s_query "UPDATE $this->table SET format=? WHERE id=?";
        
$o_sql $this->_db->prepare($s_query);
        
$o_sql->bind_param('si'$s_faxFormat$s_id);
        
$o_sql->execute();

        
// Insert the fax
        
$s_query "INSERT INTO $this->dataTable (fax_id, data) VALUES(?,?)";
        
$o_sql $this->_db->prepare($s_query);
        
$s_tmpNull null;
        
$o_sql->bind_param('ib'$s_id$s_tmpNull);

        
$this->sendBlob(&$o_sql1$s_file);
        
$o_sql->execute();


        return 
true;
    }

    
/**
     * Sends a notification that a Fax was received.
     *
     * @returns boolean True on success, False on failure.
     */
    
function sendReceiveNotification()
    {
        
$s_type $this->getEmail();
        if (
$s_type == 'notify-url') {
            return 
$this->receiveReportToUrl();
        }
        else {
            return 
$this->receiveReportToEmail();
        }
    }

    
/**
     * Sends a notification to a URL that a Fax was received.
     *
     * @returns boolean True on success, False on failure.
     */
    
function receiveReportToUrl()
    {
        
$s_id $this->getId();
        
$a_json = array(
            
'api_key' => $this->_config['fax']['in']['notify_api_key'],
            
'action'  => 'asterisk-rxfax',
            
'fax_id'  => $this->getId()
        );

        
$a_params = array('http' => array('method'  => 'POST',
                                          
'content' => json_encode($a_json)));
        
$o_ctx stream_context_create($a_params);

        
$s_url $this->_config['fax']['in']['notify_url'];
        
$o_url fopen($s_url'rb'false$o_ctx);
        if ( ! 
$o_url) {
            return 
false;
        }

        
// no need for a response as this is just a notification.
        
fclose($o_url);
        return 
true;
    }

    
/**
     * Updates the From number so that it is a number.
     * 
     * @returns void
     */
    
function fixFromNumber()
    {
        if (
'unknown' !== strtolower($this->getFromNumber())) {
            return;
        }
        
        
$s_callerId $this->getCallerId();
        if (
'' == trim($s_callerId)) {
            
// There is no use setting to anything other than unknown
            
return;
        }

        
$b_update false;
        if (
preg_match('/^"[^"]" <([^\>]+)>/i'$s_callerId$a_matches)) {
            
$this->setFromNumber($a_matches[1]);
            
$b_update true;
        }
        elseif (
preg_match('/^[0-9]+$/i'$s_callerId)) {
            
$this->setFromNumber($s_callerId);
            
$b_update true;
        }

        if (
$b_update) {
            
$s_id $this->getId();
            
$s_query "UPDATE $this->table SET from_number=? WHERE id=?";
            
$o_sql $this->_db->prepare($s_query);
            
$s_number $this->getFromNumber();
            
$o_sql->bind_param('si'$s_number$s_id);
            
$o_sql->execute();
        }
    }

    
/**
     * Sends a notification to an email that a Fax was received.
     *
     * @returns boolean True on success, False on failure.
     */
    
function receiveReportToEmail()
    {
        
$s_email $this->getEmail();
        if (empty(
$s_email)) {
            return 
false;
        }

        
$o_mail =& Mail::factory($this->_config['mailer']['type'], $this->_config['mailer']['params']);
        if (
PEAR::isError($o_mail)) {
            
$this->error $o_mail->getMessage();
            return 
false;
        }

        
// Try to determine the callerid if from_number is unknown... and update it
        
$this->fixFromNumber();

        
// subject/message body replacements
        
$this->_fillInReplacementValues();

        
$s_emailAddress $this->getEmail();
        
// Generate the email headers 
        
$s_subject str_replace($this->replacementKeys,
                                 
$this->replacementValues,
                                 
$this->_config['fax']['in']['emailsubject']);
        
$a_headers = array(
            
'From'    => '"'.$this->_config['fax']['in']['fromstring'].'" <'.$this->_config['fax']['in']['serveremail'].'>',
            
'To'      => $s_emailAddress,
            
'Subject' => $s_subject,
        );

        
// Generate the message body
        
$s_body str_replace($this->replacementKeys,
                              
$this->replacementValues,
                              
$this->_config['fax']['in']['emailbody']);
        
$o_mime = new Mail_mime("\n");
        
$o_mime->setTXTBody($s_body);

        
// Generate the fax attachment
        
$s_fileName 'fax-{FAX_FROM_NUMBER}-{FAX_DATE}';
        if ( ! empty(
$this->_config['fax']['in']['emailfile'])) {
            
$s_fileName $this->_config['fax']['in']['emailfile'];
        }
        
$s_fileName str_replace($this->replacementKeys,
                                  
$this->replacementValues,
                                  
$s_fileName);

        
$s_file $this->getFileName();
        if ( ! empty(
$this->_config['fax']['in']['sendpdf']) && $this->_config['fax']['in']['sendpdf'] === true) {
            
// we need to convert the TIFF to PDF
            
$s_origFile $s_file;
            
$s_file $this->convertToPdf($s_file);
            
$s_fileName .= '.pdf';
            
$o_mime->addAttachment($s_file'application/pdf'$s_fileNametrue);
        }
        else {
            
$s_fileName .= '.tiff';
            
$o_mime->addAttachment($s_file'image/tiff'$s_fileNametrue);
        }

        
$s_body $o_mime->get();
        
$a_headers $o_mime->headers($a_headers);

        
// send the message
        
$o_results $o_mail->send($s_emailAddress$a_headers$s_body);

        if (
PEAR::isError($o_results)) {
            
$this->error $o_results->getMessage();
            return 
false;
        }

        return 
true;
    }
    
    
// }}} 
}
// }}}
// {{{ Asterisk_VoiceMessage
class Asterisk_VoiceMessage extends Asterisk
{
    
// {{{ properties

    
protected $replacementKeys;
    protected 
$replacementValues;

    
// }}}
    // {{{ functions

    /**
     * Class constructor that sets up the replacementKeys as well as the various
     * tables that this class uses.
     */
    
function __construct()
    {
        
$this->replacementKeys = array('{VM_NAME}',
                                       
'{VM_DUR}',
                                       
'{VM_MSGNUM}',
                                       
'{VM_MAILBOX}',
                                       
'{VM_CALLERID}',
                                       
'{VM_CIDNUM}',
                                       
'{VM_CIDNAME}',
                                       
'{VM_DATE}');
        
parent::__construct();
        
$this->table               $this->_config['data']['asterisk_vm_archive_table'];
        
$this->voicemessagesTable  $this->_config['data']['asterisk_voicemessages_table'];
        
$this->extensionsTable     $this->_config['data']['asterisk_extensions_table'];

        if (empty(
$this->extensionsTable)) {
            
$this->extensionsTable 'extensions';
        }

        if (empty(
$this->table)) {
            
$this->table 'vm_archive';
        }

        if (empty(
$this->dataTable)) {
            
$this->dataTable 'voicemessages';
        }
    }

    
/**
     * Class destructor. Automatically deletes the mp3 and original message
     * file if they have been used.
     *
     * @return void
     */
    
function __destruct()
    {
        if ( ! empty(
$this->mp3_file)) {
            
unlink($this->mp3_file);
        }
        if ( ! empty(
$this->message_file)) {
            
unlink($this->message_file);
        }
        
parent::__destruct();
    }

    
/**
     * Fills in the replacement keys with the replacement values
     *
     * @return void
     */
    
protected function _fillInReplacementValues()
    {
        
$this->replacementValues = array($this->getVmName(),
                                         
$this->getVmDur(true),
                                         
$this->getVmMsgnum(),
                                         
$this->getVmMailbox(),
                                         
$this->getVmCallerid(),
                                         
$this->getVmCidnum(),
                                         
$this->getVmCidName(),
                                         
$this->getVmDate());
    }

    
/**
     * Converts the given time to seconds. Input is hours:minutes:seconds or
     * minutes:seconds.
     *
     * @param   string  $in_time The time to convert to seconds.
     * @returns integer The seconds rendition of the time.
     */
    
protected function _timeToSeconds($in_time)
    {
        
$s_seconds 0;
        
$a_time explode(':',$in_time);
        if (
count($a_time) == 3) {
            
// hours:minutes:seconds
            
$s_seconds += $a_time[0] * 3600;
            
$s_seconds += $a_time[1] * 60;
            
$s_seconds += $a_time[2];
        }
        elseif (
count($a_time) == 2) {
            
// minutes:seconds
            
$s_seconds += $a_time[0] * 60;
            
$s_seconds += $a_time[1];
        }
        else {
            
// seconds
            
$s_seconds $in_time;
        }
        return 
$s_seconds;
    }

    
/**
     * Converts seconds into a duration format
     *
     * @author christian
     * @url    http://snippets.aktagon.com/snippets/122-How-to-format-number-of-seconds-as-duration-with-PHP
     */
    
protected function _secondsToTime($seconds_count)
     {
        
$delimiter  ':';
        
$seconds $seconds_count 60;
        
$minutes floor($seconds_count/60);
        
$hours   floor($seconds_count/3600);
 
        
$seconds str_pad($seconds2"0"STR_PAD_LEFT);
        
$minutes str_pad($minutes2"0"STR_PAD_LEFT).$delimiter;
 
        if(
$hours 0) {
            
$hours str_pad($hours2"0"STR_PAD_LEFT).$delimiter;
        }
        else {
            
$hours '';
        }

        return 
"$hours$minutes$seconds";
     }

    
/**
     * Parses the email into it's header, body and attachment parts.
     *
     * NOTE: Asterisk currently only sends one voicemail per email.
     *
     * @param  string  $in_msg The message to parse.
     * @return boolean True on success, False on failure.
     */
    
function parseEmail($in_msg)
    {
        
// Initialize the decoder 
        
$a_params = array(
            
'include_bodies' => true,
            
'decode_bodies'  => false,
            
'decode_headers' => true,
            
'crlf'          => "\n",
        );

        
$pth_tmpFolder $this->_config['voicemessage']['tmp_path'];

        
// cycle through the attachments and find the attachments
        
$o_email = new FF_Model_Email($in_msg);
        
$a_attachments $o_email->getAttachments();
        
        
$this->headers $o_email->getHeaders();
        if (empty(
$this->headers['from'])) {
            return 
$this->setError('Missing header field: from');
        }

        
// parse the body
        
$a_body explode("\n"$o_email->getBody());
        foreach(
$a_body as $s_line) {
            @list(
$k$v) = explode(':'trim($s_line), 2);
            
$k 'setVm' ucfirst(trim($k));
            
$this->$k(trim($v));
        }

        
/* Asterisk uses DAYNAME, MONTHNAME, DAY, YEAR at HH:MM:SS AP which PHP
         * cannot parse properly.
         */
        
$this->setVmDate(str_replace(' at '' '$this->getVmDate()));

        
// swap the duration to seconds
        
$this->setVmDur($this->_timeToSeconds($this->getVmDur()));

        
// Save the attachments
        
$a_validAttachments = array();
        foreach(
$a_attachments as $a_attachment) {
            if ( ! 
preg_match('/\.wav/i'$a_attachment['name'])) {
                continue;
            }

            
$s_name $a_attachment['name'];

            
/* we can't use the name of the attachment because asterisk
             * uses msg####.wav which may overwrite if we have multiple vm's
             * coming in at the same time. Instead we use a tmp name.
             */

            
$a_validAttachments[] = $s_file tempnam($pth_tmpFolder'vm-');
            if ( ! 
file_put_contents($s_file$a_attachment['data'])) {
                return 
$this->setError("Unable to save the message to: $s_file.");
                return 
false;
            }
            else {
                
$this->message_file $s_file;
                return 
true;
            }
        }

        return 
$this->setError('Unable to parse the email.');
    }

    
/**
     * Converts the given file into an MP3 file.
     *
     * @param  string $in_file The name of the file to convert to MP3 format.
     * @return string The filename with .mp3.
     */
    
function convertToMp3($in_file)
    {
        if ( ! empty(
$this->mp3_file)) {
            return 
$this->mp3_file;
        }
        
$tmp_file str_replace('.wav''.mp3'$in_file);
        if (
false === stristr('.mp3'$tmp_file)) {
            
$tmp_file "$in_file.mp3";
        }
        
exec("/usr/bin/lame --quiet --preset voice {$in_file} {$tmp_file} 2>&1"$a_output$s_result);
        if (
$s_result) {
            return 
false;
        }

        
$this->mp3_file $tmp_file;
        return 
$this->mp3_file;
    }

    
/**
     * Archives the voicemessage into the archive table.
     *
     * @return True on success, False on failure.
     */
    
function archive()
    {
        
$s_file $this->message_file;
        
$s_format 'WAV';
        if ( ! empty(
$this->_config['voicemessage']['savemp3']) && $this->_config['voicemessage']['savemp3'] == true) {
            
$s_file $this->convertToMp3($s_file);
            
$s_format 'MP3';
        }

        
$s_query "INSERT INTO $this->table (data, date_created, caller_id, duration, mailbox, format) VALUES (?,?,?,?,?,?)";
        
$o_sql $this->_db->prepare($s_query);
        if ( ! 
$o_sql) {
            return 
$this->setError("Database Error: $this->_db->error");
        }
        
$s_tmpNull null;

        
$o_sql->bind_param('bssiis',
            
$s_tmpNull,
            
date('Y-m-d H:i:s'strtotime($this->getVmDate())),
            
$this->getVmCallerid(),
            
$this->getVmDur(),
            
$this->getVmMailbox(),
            
$s_format);

        
$this->sendBlob(&$o_sql0$s_file);
        if ( ! 
$o_sql->execute()) {
            return 
$this->setError("Database Error: $this->_db->error");
        }

        return 
true;
    }



    
/**
     * Gets the VM Duration field
     */
    
function getVmDur($in_formatted false)
    {
        if (
$in_formatted) {
            return 
$this->_secondsToTime($this->_data['vm_dur']);
        }
        return 
$this->_data['vm_dur'];
    }

    
/**
     * Send the email to the user it was originally intended for.
     */
    
function sendEmail()
    {
        
$o_mail =& Mail::factory($this->_config['mailer']['type'], $this->_config['mailer']['params']);
        if (
PEAR::isError($o_mail)) {
            return 
$this->setError('Mail Error: ' $o_mail->getMessage());
        }

        
// subject/message body replacements
        
$this->_fillInReplacementValues();

        
$s_emailAddress $this->headers['to'];

        
// Generate the email headers 
        
$s_subject str_replace($this->replacementKeys,
                                 
$this->replacementValues,
                                 
$this->_config['voicemessage']['subject']);
        
$a_headers = array(
            
'From'    => '"'.$this->_config['voicemessage']['from']['name'].'" <'.$this->_config['voicemessage']['from']['email'].'>',
            
'To'      => $s_emailAddress,
            
'Subject' => $s_subject,
        );

        
// Generate the message body
        
$s_body str_replace($this->replacementKeys,
                              
$this->replacementValues,
                              
$this->_config['voicemessage']['body']);
        
$o_mime = new Mail_mime("\n");
        
$o_mime->setTXTBody($s_body);

        
// Generate the fax attachment

        
$s_fileName 'vm-{VM_CIDNUM}';
        if ( ! empty(
$this->_config['voicemessage']['filename'])) {
            
$s_fileName $this->_config['voicemessage']['filename'];
        }
        
$s_fileName str_replace($this->replacementKeys,
                                  
$this->replacementValues,
                                  
$s_fileName);

        
$s_file $this->message_file;
        if ( ! empty(
$this->_config['voicemessage']['sendmp3']) && $this->_config['voicemessage']['sendmp3'] == true) {
            
$s_file $this->convertToMp3($s_file);
            
$s_fileName .= '.mp3';
            
$o_mime->addAttachment($s_file'audio/mp3'$s_fileNametrue);
        }
        else {
            
$s_fileName .= '.wav';
            
$o_mime->addAttachment($s_file'audio/wav'$s_fileNametrue);
        }

        
$s_body $o_mime->get();
        
$a_headers $o_mime->headers($a_headers);

        
// send the message
        
$o_results $o_mail->send($s_emailAddress$a_headers$s_body);

        if (
PEAR::isError($o_results)) {
            return 
$this->setError('Mail Error: ' $o_mail->getMessage());
        }

        return 
true;
    }
}

// }}}