#!/usr/bin/php
<?php
/*
 vim: set expandtab tabstop=4 softtabstop=4 shiftwidth=4:
  Codificación: UTF-8
  +----------------------------------------------------------------------+
  | Elastix version 2.0                                                  |
  | http://www.elastix.org                                               |
  +----------------------------------------------------------------------+
  | Copyright (c) 2006 Palosanto Solutions S. A.                         |
  +----------------------------------------------------------------------+
  | Cdla. Nueva Kennedy Calle E 222 y 9na. Este                          |
  | Telfs. 2283-268, 2294-440, 2284-356                                  |
  | Guayaquil - Ecuador                                                  |
  | http://www.palosanto.com                                             |
  +----------------------------------------------------------------------+
  | The contents of this file are subject to the General Public License  |
  | (GPL) Version 2 (the "License"); you may not use this file except in |
  | compliance with the License. You may obtain a copy of the License at |
  | http://www.opensource.org/licenses/gpl-license.php                   |
  |                                                                      |
  | Software distributed under the License is distributed on an "AS IS"  |
  | basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See  |
  | the License for the specific language governing rights and           |
  | limitations under the License.                                       |
  +----------------------------------------------------------------------+
  | The Original Code is: Elastix Open Source.                           |
  | The Initial Developer of the Original Code is PaloSanto Solutions    |
  +----------------------------------------------------------------------+
  $Id: elastix-faxevent.php,v 1.1 2007/01/09 23:49:36 alex Exp $
*/
// Ugly - should not depend on anything under /var/www/html
ini_set('include_path', '/var/www/html:'.ini_get('include_path'));

require_once 'Console/Getopt.php';

define('HYLAFAX_SPOOLDIR', '/var/spool/hylafax');
define('ELASTIX_FAXDIR', '/var/www/faxes');
define('DSN_FAX', 'sqlite:/var/www/db/fax.db');

load_default_timezone();

// Parse command-line options
$opt = Console_Getopt::getopt($argv, '', array(
    'send',     // If specified, run sent fax behavior
    'recv',     // If specified, run received fax behavior
    //'debug',    // Enable debug messages on fax log database
));
if (PEAR::isError($opt)) error_exit($opt->getMessage()."\n");
try {
    foreach ($opt[0] as $option) switch ($option[0]) {
    case '--send':
        exit(action_faxSentEvent($opt) ? 0 : 1);
    case '--recv':
        exit(action_faxRecvEvent($opt) ? 0 : 1);
    }
} catch (PDOException $e) {
	error_exit('FATAL: unable to read or update fax database - '.$e->getMessage()."\n");
}
error_exit("No action specified (--send,--recv)\n");

function error_exit($sMsg, $errorcode = 1)
{
    fwrite(STDERR, $sMsg);
    exit($errorcode);
}

function load_default_timezone()
{
    $sDefaultTimezone = @date_default_timezone_get();
    if ($sDefaultTimezone == 'UTC') {
        $sDefaultTimezone = 'America/New_York';
        if (file_exists('/etc/sysconfig/clock')) {
            foreach (file('/etc/sysconfig/clock') as $s) {
                $regs = NULL;
                if (preg_match('/^ZONE\s*=\s*"(.+)"/', $s, $regs)) {
                    $sDefaultTimezone = $regs[1];
                }
            }
        }
    }
    date_default_timezone_set($sDefaultTimezone);
}

/**
 * Log a message to the internal fax log table
 * 
 * @param   resource    $conn       Database connection
 * @param   string      $severity   One of DEBUG, WARN, ERR
 * @param   string      $step       Name of component where message is emitted
 * @param   string      $message    Text to insert into log
 * 
 * @return  void
 */
function dblog($conn, $severity, $message)
{
    // TODO: expand log table definition so it will separate severity/step/PID
    $fullmessage = sprintf('(PID=%d) %s : %s',
        posix_getpid(), $severity, $message);
    $sth = $conn->prepare('INSERT INTO SysLog (logdate, logtext) VALUES (?, ?)');
    $sth->execute(array(date('Y-m-d H:i:s'), $fullmessage));
}

function dblog_exit($conn, $message)
{
	dblog($conn, 'ERR', $message);
    exit(1);
}

/* These are the assumptions on execution of the notification for a sent fax:
 * - The entire program runs as user uucp of group uucp.
 * - Initial working directory is HYLAFAX_SPOOLDIR
 * - In addition to the --send parameter, there are three additional parameters.
 *   These parameters are: the fax completion file (doneq/qNNN), the fax status
 *   ('done', 'failed'), and a timestamp (currently unused). The fax completion
 *   file is a path relative to HYLAFAX_SPOOLDIR.   
 */
function action_faxSentEvent($opt)
{
	if (count($opt[1]) < 2)
        error_exit("ERR: invoke with --send doneq/qNN [done|failed]\n");

    // Open database for log messages and configuration
    $conn = new PDO(DSN_FAX);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Load and parse the fax properties file
    $sFaxPropertiesPath = HYLAFAX_SPOOLDIR.'/'.$opt[1][0];
    if (!file_exists($sFaxPropertiesPath))
        dblog_exit($conn, "Fax properties file not found: $sFaxPropertiesPath");
    $faxinfo = array('properties' => array(), 'files' => array());
    foreach (file($sFaxPropertiesPath) as $s) {
    	$regs = NULL;
        $s = trim($s);
        if ($s[0] == '!') {
        	// File component for fax - format in index 0, relative path in index 3
            // !postscript:0::docq/doc17.ps
            $faxinfo['files'][] = explode(':', substr($s, 1));
        } elseif (preg_match('/^(\w+):(.*)$/', $s, $regs)) {
        	// Property of fax job
            $faxinfo['properties'][$regs[1]] = $regs[2];
        }
    }
    
    commonFaxProcessing($conn, $faxinfo, 'out', $opt[1][1]);
    dblog($conn, 'INFO', sprintf('Logged SENT fax through %s with commid %s number %s sender %s status %s',
        $faxinfo['properties']['modem'], $faxinfo['properties']['commid'],
        $faxinfo['properties']['number'], $faxinfo['properties']['sender'],
        $opt[1][1]));

    $conn = NULL;
    return TRUE;
}

/* These are the assumptions on execution of the notification for a received fax:
 * - The entire program runs as user uucp of group uucp.
 * - In addition to the --recv parameter, there are six additional parameters.
 *   - TIFF file for the received fax
 *   - Modem device for received fax
 *   - Comm ID for the received fax
 *   - Error message, if any, for the received job
 *   - Number for Caller ID for the incoming call --> company number
 *   - Name of the Caller ID for the incoming call --> company name
 */
function action_faxRecvEvent($opt)
{
    if (count($opt[1]) < 6)
        error_exit("ERR: invoke with --recv tifffile modemdevice commid errormessage CIDNumber CIDName\n");

    // Open database for log messages and configuration
    $conn = new PDO(DSN_FAX);
    $conn->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

    // Run faxinfo on the tifffile to fetch basic information on the fax
    // Synthetize a faxinfo structure for use with commonFaxProcessing()
    /*
    recvq/fax000000003.tif (ftp://elastix.palosanto.com:4559/recvq/fax000000003.tif):
              Sender: 
               Pages: 1
             Quality: Normal
                Size: ISO A4
            Received: 2007:06:11 16:30:20
     Time To Receive: 0:38
         Signal Rate: 9600 bit/s
         Data Format: 2-D MR
       Error Correct: No
             CallID1: 
             CallID2: 
         Received On: ttyIAX1
              CommID: 000000003 (ftp://elastix.palosanto.com:4559/log/c000000003)
    */
    $faxinfo = array(
        'properties'    =>  array(
            'modem'     =>  $opt[1][1],
            'commid'    =>  $opt[1][2],
            'status'    =>  $opt[1][3],
            'number'    =>  $opt[1][4],
            'sender'    =>  $opt[1][5],
            'mailaddr'  =>  NULL,
        ),
        'files'         =>  array(
            array('tiff', 0, NULL, $opt[1][0]),
        ),
    );
    $output = $retval = NULL;
    exec('/usr/sbin/faxinfo -n '.escapeshellarg($opt[1][0]), $output, $retval);
    if ($retval != 0) dblog_exit($conn,
        "Failed to run: $command\n".
        "Return value: $retval\n".
        "Command output:\n".
        implode("\n", $output));
    foreach ($output as $s) {
    	$regs = NULL;
        if (preg_match('/^(\w+):\s*(.*)$/', trim($s), $regs)) {
        	switch ($regs[1]) {
        	case 'Sender':
                $sender = trim($regs[2]);
                if ($sender == '' || 
                    stripos($sender, 'unknown') !== FALSE || 
                    stripos($sender, 'unspecified') !== FALSE)
                    $sender = 'XXXXXXX';
                if (empty($faxinfo['properties']['sender']))
                    $faxinfo['properties']['sender'] = $sender;
                if (empty($faxinfo['properties']['number']))
                    $faxinfo['properties']['number'] = preg_replace ('/\W/', '', $sender);
                break;
            // The following properties are available but unused (for now)
            case 'Pages':
            case 'Received':
            case 'Page':
            default:
                break;
        	}
        }
    }
    
    commonFaxProcessing($conn, $faxinfo, 'in', $opt[1][3] != '' ? 'failed' : 'done');
    dblog($conn, 'INFO', sprintf('Logged RECEIVED fax through %s with commid %s number %s sender %s status %s',
        $faxinfo['properties']['modem'], $faxinfo['properties']['commid'],
        $faxinfo['properties']['number'], $faxinfo['properties']['sender'],
        $opt[1][3] != '' ? 'failed' : 'done'));

    $conn = NULL;
    return TRUE;
}

// Common functionality shared by sent/received fax processing
function commonFaxProcessing($conn, $faxinfo, $sDirection, $status)
{
    $sRepresentativeName = basename($faxinfo['files'][0][3]);
    $regs = NULL;
    if (preg_match('/^(.*)\.(\w+)$/', $sRepresentativeName, $regs))
        $sRepresentativeName = $regs[1].'.pdf';

    // Translate modem device name to database ID
    $idModemDevice = getIdModemDevice($conn, $faxinfo['properties']['modem']); 
    
    // Create required folder for PDF of processed fax
    $sDestDir = createFaxDirectory($conn, $sDirection,
        $faxinfo['properties']['number'], $faxinfo['properties']['commid']);
    $sFaxesDir = ELASTIX_FAXDIR.'/'.$sDestDir;

    // Convert all of the component files into PDF
    $pdflist = array();
    foreach ($faxinfo['files'] as $k => $fileinfo) {
        $sFileFormat = $fileinfo[0];
        $sSourcePath = HYLAFAX_SPOOLDIR.'/'.$fileinfo[3];
        $sDestPath = $sFaxesDir.'/'."fax$k.pdf";
        if (!file_exists($sSourcePath))
            dblog_exit($conn, 'Component file not found in spool dir: '.$fileinfo[3]);
        
        switch ($sFileFormat) {
        case 'post':
        case 'postscript':
            // Normalize PostScript, then convert to PDF
            $sTempPath = dirname($sSourcePath).'/'.basename($sSourcePath).'.ps2';
            invokeCommand($conn,
                '/usr/bin/eps2eps '.escapeshellarg($sSourcePath).' '.escapeshellarg($sTempPath));
            invokeCommand($conn,
                '/usr/bin/ps2pdfwr '.escapeshellarg($sTempPath).' '.escapeshellarg($sDestPath));
            if (!unlink($sTempPath))
                dblog($conn, 'WARN', 'Failed to remove temporary file: '.$sTempPath);
            $pdflist[] = $sDestPath;
            break;
        case 'tiff':
            // Convert from TIFF to PDF
            invokeCommand($conn,
                '/usr/bin/tiff2pdf -o '.escapeshellarg($sDestPath).' '.escapeshellarg($sSourcePath));
            $pdflist[] = $sDestPath;
            break;
        case 'pdf':
            // Simple file copy
            if (!copy($sSourcePath, $sDestPath))
                dblog_exit($conn, "Failed to copy PDF: ");
            $pdflist[] = $sDestPath;
            break;
        default:
            // Unimplemented source component type
            dblog($conn, 'WARN', "Unimplemented component type $sFileFormat for {$fileinfo[3]}");
            break;
        }
        
        /* Do not unlink tiff on incoming fax - avantfax may be chained to this
         * script, and it needs to look at the script too. */
        if ($sDirection == 'out' && !unlink($sSourcePath)) 
            dblog($conn, 'WARN', 'Failed to remove fax component: '.$fileinfo[3]);
    }
    
    $sDestPath = $sFaxesDir.'/fax.pdf';
    if (count($pdflist) > 1) {
        // Combine all of the PDF files in one big PDF for output
        invokeCommand($conn,
            '/usr/bin/gs '.implode(' ', array_map(
                'escapeshellarg', 
                array_merge(
                    array('-q', '-dNOPAUSE', '-dBATCH', '-sDEVICE=pdfwrite',
                        '-dCompatibilityLevel=1.4', '-sPAPERSIZE=a4', '-dSAFER',
                        '-sOutputFile='.$sDestPath, '-f'),
                    $pdflist))));
        if (count($pdflist) != count(array_filter(array_map('unlink', $pdflist))))
            dblog($conn, 'WARN', 'Failed to remove all temporar PDF files');
    } elseif (count($pdflist) == 1) {
        // Rename cannot fail due to filesystem boundaries - same directory
        if (!rename($pdflist[0], $sDestPath))
            dblog_exit($conn, 'Failed to rename file: '.$pdflist[0]);
    } else {
    	// This should not happen
        dblog_exit($conn, 'No files found in supported formats');
    }
    
    // Required since reception might leave file as 0600
    chmod($sDestPath, 0644);
    
    insertFaxRecord($conn,
        $sRepresentativeName,
        $faxinfo['properties']['modem'],
        $idModemDevice,
        $faxinfo['properties']['commid'],
        $status, // done|failed
        $faxinfo['properties']['status'],
        $faxinfo['properties']['sender'],
        $faxinfo['properties']['number'],
        $sDirection,
        $sDestDir);
    
    // Send email with attached fax
    $emailList = array();
    if ($idModemDevice != 0) {
        $sDestinationEmail = readNotificationEmail($conn, $idModemDevice);
        if (!is_null($sDestinationEmail)) $emailList[] = $sDestinationEmail;
    }
    if (isset($faxinfo['properties']['mailaddr']) && !empty($faxinfo['properties']['mailaddr'])) {
        $sDestinationEmail = $faxinfo['properties']['mailaddr'];
        foreach (array('/@localhost$/', '/@localhost.localdomain/', '/@127.0.0.1/') as $regexp) {
            if (!is_null($sDestinationEmail) && preg_match($regexp, $sDestinationEmail))
                $sDestinationEmail = NULL;
        }
        if (!is_null($sDestinationEmail)) $emailList[] = $sDestinationEmail;
    }    
    foreach ($emailList as $sDestinationEmail) {
        sendEmailFax($conn, $sRepresentativeName, $sDestinationEmail,
            $faxinfo['properties']['sender'], $faxinfo['properties']['number'],
            "$sDestDir/fax.pdf", $status, $faxinfo['properties']['status']);
    }
    if (count($emailList) <= 0) {
        dblog($conn, 'WARN', 'No notification emails were available for this job');
    }
}

/**
 * Create the destination directory for a fax document based on a number of
 * fax parameters.
 * 
 * @param   resource    $conn   Database connection for logging
 * @param   string      $type   One of 'in' or 'out'
 * @param   string      $number Number that sent or received the fax
 * @param   string      $commID Identification number of the fax job
 * 
 * @return  string      Newly created directory relative to ELASTIX_FAXDIR
 */
function createFaxDirectory($conn, $type, $number, $commID)
{
    // Check whether base directory is writable. Expected 0775 asterisk.uucp
    $sDestDir = (($type == 'in') ? 'recvd' : 'sent');
    if (!is_writable(ELASTIX_FAXDIR.'/'.$sDestDir)) dblog_exit($conn, "Directory not writable: $sDestDir");

    // Build path with system date
    $curdate = getdate();
    $sDestDir .= '/'.implode('/', array($curdate['year'], $curdate['mon'], $curdate['mday'], $number, $commID));
    if (!is_dir(ELASTIX_FAXDIR.'/'.$sDestDir)) {
        $oldmask = umask(0);
        $r = mkdir(ELASTIX_FAXDIR.'/'.$sDestDir, 0777, TRUE);
        umask($oldmask);
        if (!$r) dblog_exit($conn, "Failed to create document path: $sDestDir");
    }
    return $sDestDir;
}

// Helper function to run a command and exit with logging if unsuccessful.
function invokeCommand($conn, $command)
{
    $output = $retval = NULL;
    exec("$command 2>&1", $output, $retval);
    if ($retval != 0) dblog_exit($conn,
        "Failed to run: $command\n".
        "Return value: $retval\n".
        "Command output:\n".
        implode("\n", $output));
}

/**
 * Procedure to fetch database ID of fax definition from modem device name
 * 
 * @param   resource    $conn                   Database connection
 * @param   string      $sModemDevice           Device name (e.g. ttyIAX1)
 * 
 * @return  mixed       Database ID of device
 */
function getIdModemDevice($conn, $sModemDevice)
{
    // This should return NULL but 'info_fax_recvq.fax_destiny_id' column is defined as NOT NULL.
    if ($sModemDevice == 'any') return 0;

    // Locate ID of fax device record given the modem device
    $regs = NULL;
    if (!preg_match('/^ttyIAX(\d+)$/', $sModemDevice, $regs))
        dblog_exit($conn, "(internal) Failed to extract dev_id from $sModemDevice");
    $sth = $conn->prepare('SELECT id FROM fax WHERE dev_id = ?');
    $sth->execute(array($regs[1]));
    $tuple = $sth->fetch(PDO::FETCH_ASSOC);
    $sth->closeCursor();
    if (!$tuple) {
        dblog($conn, "(internal) Failed to find existing dev_id for $sModemDevice, replacing with first device...");
        $sth = $conn->query('SELECT id FROM fax ORDER BY dev_id LIMIT 1');
        $tuple = $sth->fetch(PDO::FETCH_ASSOC);
        $sth->closeCursor();
        if (!$tuple) dblog_exit('(internal) No fax devices found - this should not happen!');
    }
    return (int)$tuple['id'];
}

/**
 * Procedure to insert a fax record into the database.
 * 
 * @param   resource    $conn                   Database connection
 * @param   string      $sRepresentativeName    Document name to show on interface
 * @param   string      $sModemDevice           Device name (e.g. ttyIAX1)
 * @param   integer     $idModemDevice          Database ID for fax TTY.
 * @param   string      $idComm                 Communication ID
 * @param   string      $sStatus                Status as reported by Hylafax
 * @param   string      $sErrorMsg              Error message from operation
 * @param   string      $sCompanyName           Company name for fax job
 * @param   string      $sCompanyNumber         Company number for fax job
 * @param   string      $sDirection             One of 'in' or 'out'
 * @param   string      $sFaxDir                Location of 'fax.pdf' relative to ELASTIX_FAXDIR
 * 
 * @return  void
 */
function insertFaxRecord($conn, $sRepresentativeName, $sModemDevice, $idModemDevice,
    $idComm, $sStatus, $sErrorMsg, $sCompanyName, $sCompanyNumber, $sDirection,
    $sFaxDir)
{
    // Actually insert the fax record
	$sqlInsertFax = <<<INSERT_FAX
INSERT INTO info_fax_recvq (pdf_file, modemdev, commID, status, errormsg, 
    company_name, company_fax, fax_destiny_id, date, type, faxpath)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
INSERT_FAX;
    $sth = $conn->prepare($sqlInsertFax);
    $sth->execute(array($sRepresentativeName, $sModemDevice, $idComm, $sStatus,
        $sErrorMsg, $sCompanyName, $sCompanyNumber, $idModemDevice,
        date('Y-m-d H:i:s'), $sDirection, $sFaxDir));
}

/** 
 * Fetch a notification e-mail from the modem device ID
 *
 * @param   string      $idModemDevice          Database ID for fax TTY.
 * 
 * @return  string  E-Mail address for notification, or NULL 
 */
function readNotificationEmail($conn, $idModemDevice)
{
    // Read destination email for notification
    $sth = $conn->prepare('SELECT email FROM fax WHERE id = ?');
    $sth->execute(array($idModemDevice));
    $tuple = $sth->fetch(PDO::FETCH_ASSOC);
    $sth->closeCursor();
    return isset($tuple['email']) ? $tuple['email'] : NULL;
}

/**
 * Procedure to send an email with a sent/received fax.
 * 
 * @param   resource    $conn                   Database connection
 * @param   string      $sRepresentativeName    Document name to show on interface
 * @param   string      $sDestinationEmail      Destination email
 * @param   string      $sCompanyName           Company name for fax job
 * @param   string      $sCompanyNumber         Company number for fax job
 * @param   string      $sPdfPath               Path to fax PDF relative to ELASTIX_FAXDIR
 * 
 * @return void
 */
function sendEmailFax($conn, $sRepresentativeName, $sDestinationEmail, $sCompanyName,
    $sCompanyNumber, $sPdfPath, $status, $errmsg)
{
	// Read configured values for fax email template
    $emailInfo = array(
        'remite'    =>  'elastix@example.com',
        'remitente' =>  'Fax Elastix',
        'subject'   =>  'Fax '.$sRepresentativeName,
        'content'   =>  "Fax $sRepresentativeName of $sCompanyName - $sCompanyNumber",
    );
    $replaceStrings = array(
        array('{NAME_PDF}',             basename($sRepresentativeName, '.pdf')),
        array('{COMPANY_NAME_FROM}',    $sCompanyName),
        array('{COMPANY_NUMBER_FROM}',  $sCompanyNumber),
        array('{JOB_STATUS}',           ($status == 'done') ? $status : "$status: $errmsg"),
    );
    $sth = $conn->query(
        'SELECT remite, remitente, subject, content FROM configuration_fax_mail WHERE id = 1');
    while ($tuple = $sth->fetch(PDO::FETCH_ASSOC)) {
        foreach ($replaceStrings as $replace) {
            $tuple['subject'] = str_replace($replace[0], $replace[1], $tuple['subject']);
            $tuple['content'] = str_replace($replace[0], $replace[1], $tuple['content']);
        }
        foreach ($tuple as $k => $v) $tuple[$k] = utf8_decode($v);
        $emailInfo = $tuple;
    }
    
    // Send mail...
    require_once('libs/phpmailer/class.phpmailer.php');
    $mail = new PHPMailer();
    $mail->From = $emailInfo['remite'];
    $mail->FromName = $emailInfo['remitente'];
    $mail->AddAddress($sDestinationEmail);
    $mail->WordWrap = 50;                                 // set word wrap to 50 characters
    $mail->AddAttachment(ELASTIX_FAXDIR.'/'.$sPdfPath);
    $mail->IsHTML(false);                                  // set email format to TEXT
        
    $mail->Subject = $emailInfo['subject'];
    $mail->Body    = $emailInfo['content'];
    $mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
    
    if (!$mail->Send())
        dblog_exit('Failed to send email: '.$emailInfo['subject']);
}
?>
