<?php

/** Additional storage action since maildrop does not support the "c-flag" as
 * in procmail. */
define('MAILDROP_STORAGE_ACTION_STOREANDFORWARD', 100);

/**
 * The Ingo_Script_maildrop:: class represents a maildrop script generator.
 *
 * $Horde: ingo/lib/Script/maildrop.php,v 1.7.2.6 2006/04/24 12:57:04 jan Exp $
 *
 * Copyright 2005-2006 Matt Weyland <mathias@weyland.ch>
 *
 * See the enclosed file LICENSE for license information (ASL).  If you
 * did not receive this file, see http://www.horde.org/licenses/asl.php.
 *
 * @author  Matt Weyland <mathias@weyland.ch>
 * @since   Ingo 1.1
 * @package Ingo
 */
class Ingo_Script_maildrop extends Ingo_Script {

    /**
     * The list of actions allowed (implemented) for this driver.
     *
     * @var array
     */
    var $_actions = array(
        INGO_STORAGE_ACTION_KEEP,
        INGO_STORAGE_ACTION_MOVE,
        INGO_STORAGE_ACTION_DISCARD,
        INGO_STORAGE_ACTION_REDIRECT,
        INGO_STORAGE_ACTION_REDIRECTKEEP,
        INGO_STORAGE_ACTION_REJECT
    );

    /**
     * The categories of filtering allowed.
     *
     * @var array
     */
    var $_categories = array(
        INGO_STORAGE_ACTION_BLACKLIST,
        INGO_STORAGE_ACTION_WHITELIST,
        INGO_STORAGE_ACTION_FORWARD
    );

    /**
     * The types of tests allowed (implemented) for this driver.
     *
     * @var array
     */
    var $_types = array(
        INGO_STORAGE_TYPE_HEADER
    );

    /**
     * The list of tests allowed (implemented) for this driver.
     *
     * @var array
     */
    var $_tests = array(
        'contains',
        'begins with',
        'ends with',
        'regex'
    );

    /**
     * Can tests be case sensitive?
     *
     * @var boolean
     */
    var $_casesensitive = true;

    /**
     * Does the driver support the stop-script option?
     *
     * @var boolean
     */
    var $_supportStopScript = false;

    /**
     * Does the driver require a script file to be generated?
     *
     * @var boolean
     */
    var $_scriptfile = true;

    /**
     * The recipes that make up the code.
     *
     * @var array
     */
    var $_recipes = array();

    /**
     * Returns a script previously generated with generate().
     *
     * @return string  The maildrop script.
     */
    function toCode()
    {
        $code = '';
        foreach ($this->_recipes as $item) {
            $code .= $item->generate() . "\n";
        }
        return rtrim($code);
    }

    /**
     * Generates the maildrop script to do the filtering specified in
     * the rules.
     *
     * @return string  The maildrop script.
     */
    function generate()
    {
        $filters = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_FILTERS);

        $this->addItem(new Maildrop_Comment(_("maildrop script generated by Ingo") . ' (' . date('F j, Y, g:i a') . ')'));

        /* Add variable information, if present. */
        if (!empty($this->_params['variables']) &&
            is_array($this->_params['variables'])) {
            foreach ($this->_params['variables'] as $key => $val) {
                $this->addItem(new Maildrop_Variable(array('name' => $key, 'value' => $val)));
            }
        }

        foreach ($filters->getFilterlist() as $filter) {
            switch ($filter['action']) {
            case INGO_STORAGE_ACTION_BLACKLIST:
                $this->generateBlacklist(!empty($filter['disable']));
                break;

            case INGO_STORAGE_ACTION_WHITELIST:
                $this->generateWhitelist(!empty($filter['disable']));
                break;

            case INGO_STORAGE_ACTION_FORWARD:
                $this->generateForward(!empty($filter['disable']));
                break;

            default:
                if (in_array($filter['action'], $this->_actions)) {
                    /* Create filter if using AND. */
                    $recipe = &new Maildrop_Recipe($filter, $this->_params);
                    foreach ($filter['conditions'] as $condition) {
                        $recipe->addCondition($condition);
                    }
                    $this->addItem(new Maildrop_Comment($filter['name'], !empty($filter['disable']), true));
                    $this->addItem($recipe);
                }
            }
        }

        return $this->toCode();
    }

    /**
     * Generates the maildrop script to handle the blacklist specified in
     * the rules.
     *
     * @param boolean $disable  Disable the blacklist?
     */
    function generateBlacklist($disable = false)
    {
        $blacklist = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_BLACKLIST);
        $bl_addr = $blacklist->getBlacklist();
        $bl_folder = $blacklist->getBlacklistFolder();

        $bl_type = (empty($bl_folder)) ? INGO_STORAGE_ACTION_DISCARD : INGO_STORAGE_ACTION_MOVE;

        if (!empty($bl_addr)) {
            $this->addItem(new Maildrop_Comment(_("Blacklisted Addresses"), $disable, true));
            $params = array('action-value' => $bl_folder,
                            'action' => $bl_type,
                            'disable' => $disable);

            foreach ($bl_addr as $address) {
                if (!empty($address)) {
                    $recipe = &new Maildrop_Recipe($params, $this->_params);
                    $recipe->addCondition(array('field' => 'From', 'value' => $address));
                    $this->addItem($recipe);
                }
            }
        }
    }

    /**
     * Generates the maildrop script to handle the whitelist specified in
     * the rules.
     *
     * @param boolean $disable  Disable the whitelist?
     */
    function generateWhitelist($disable = false)
    {
        $whitelist = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_WHITELIST);
        $wl_addr = $whitelist->getWhitelist();

        if (!empty($wl_addr)) {
            $this->addItem(new Maildrop_Comment(_("Whitelisted Addresses"), $disable, true));
            foreach ($wl_addr as $address) {
                if (!empty($address)) {
                    $recipe = &new Maildrop_Recipe(array('action' => INGO_STORAGE_ACTION_KEEP, 'disable' => $disable), $this->_params);
                    $recipe->addCondition(array('field' => 'From', 'value' => $address));
                    $this->addItem($recipe);
                }
            }
        }
    }

    /**
     * Generates the maildrop script to handle mail forwards.
     *
     * @param boolean $disable  Disable forwarding?
     */
    function generateForward($disable = false)
    {
        $forward = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_FORWARD);
        $addresses = $forward->getForwardAddresses();

        if (!empty($addresses)) {
            $this->addItem(new Maildrop_Comment(_("Forwards"), $disable, true));
            $params = array('action' => INGO_STORAGE_ACTION_FORWARD,
                            'action-value' => $addresses,
                            'disable' => $disable);
            if ($forward->getForwardKeep()) {
                $params['action'] = MAILDROP_STORAGE_ACTION_STOREANDFORWARD;
            }
            $recipe = &new Maildrop_Recipe($params, $this->_params);
            $recipe->addCondition(array('field' => 'From', 'value' => ''));
            $this->addItem($recipe);
        }
    }

    /**
     * Adds an item to the recipe list.
     *
     * @param object $item  The item to add to the recipe list.
     *                      The object should have a generate() function.
     */
    function addItem($item)
    {
        $this->_recipes[] = $item;
    }

}

/**
 * The Maildrop_Comment:: class represents a maildrop comment.  This is
 * a pretty simple class, but it makes the code in Ingo_Script_maildrop::
 * cleaner as it provides a generate() function and can be added to the
 * recipe list the same way as a recipe can be.
 *
 * @author  Matt Weyland <mathias@weyland.ch>
 * @since   Ingo 1.1
 * @package Ingo
 */
class Maildrop_Comment {

    /**
     * The comment text.
     *
     * @var string
     */
    var $_comment = '';

    /**
     * Constructs a new maildrop comment.
     *
     * @param string $comment   Comment to be generated.
     * @param boolean $disable  Output 'DISABLED' comment?
     * @param boolean $header   Output a 'header' comment?
     */
    function Maildrop_Comment($comment, $disable = false, $header = false)
    {
        if ($disable) {
            $comment = _("DISABLED: ") . $comment;
        }

        if ($header) {
            $this->_comment .= "##### $comment #####";
        } else {
            $this->_comment .= "# $comment";
        }
    }

    /**
     * Returns the comment stored by this object.
     *
     * @return string  The comment stored by this object.
     */
    function generate()
    {
        return $this->_comment;
    }

}

/**
 * The Maildrop_Recipe:: class represents a maildrop recipe.
 *
 * @author  Matt Weyland <mathias@weyland.ch>
 * @since   Ingo 1.1
 * @package Ingo
 */
class Maildrop_Recipe {

    var $_action = array();
    var $_conditions = array();
    var $_disable = '';
    var $_flags = '';
    var $_params = array();
    var $_combine = '';
    var $_valid = true;

    /**
     * Constructs a new maildrop recipe.
     *
     * @param array $params        Array of parameters.
     *                             REQUIRED FIELDS:
     *                             'action'
     *                             OPTIONAL FIELDS:
     *                             'action-value' (only used if the
     *                             'action' requires it)
     * @param array $scriptparams  Array of parameters passed to
     *                             Ingo_Script_maildrop::.
     */
    function Maildrop_Recipe($params = array(), $scriptparams = array())
    {
        $this->_disable = !empty($params['disable']);
        $this->_params = $scriptparams;
        $this->_action[] = 'exception {';

        switch ($params['action']) {
        case INGO_STORAGE_ACTION_KEEP:
            $this->_action[] = '   to "${DEFAULT}"';
            break;

        case INGO_STORAGE_ACTION_MOVE:
            $this->_action[] = '   to ' . $this->maildropPath($params['action-value']);
            break;

        case INGO_STORAGE_ACTION_DISCARD:
            $this->_action[] = '   to "/dev/null"';
            break;

        case INGO_STORAGE_ACTION_REDIRECT:
            $this->_action[] = '   to "! ' . $params['action-value'] . '"';
            break;

        case INGO_STORAGE_ACTION_REDIRECTKEEP:
            $this->_action[] = '   cc "! ' . $params['action-value'] . '"';
            $this->_action[] = '   to "${DEFAULT}"';
            break;

        case INGO_STORAGE_ACTION_REJECT:
            $this->_action[] = '   EXITCODE=' . $params['action-value'];
            $this->_action[] = '   exit';
            break;

        case INGO_STORAGE_ACTION_FORWARD:
        case MAILDROP_STORAGE_ACTION_STOREANDFORWARD:
            foreach ($params['action-value'] as $address) {
                if (!empty($address)) {
                    $this->_action[] = '  cc "! ' . $address . '"';
                }
            }

            /* The 'to' must be the last action, because maildrop stops
               processing after it. */
            if ($params['action'] == MAILDROP_STORAGE_ACTION_STOREANDFORWARD) {
                $this->_action[] = ' to "${DEFAULT}"';
            } else {
                $this->_action[] = ' exit';
            }
            break;

        default:
            $this->_valid = false;
            break;
        }

        $this->_action[] = '}';

        if (isset($params['combine']) &&
            ($params['combine'] == INGO_STORAGE_COMBINE_ALL)) {
            $this->_combine = '&& ';
        } else {
            $this->_combine = '|| ';
        }
    }

    /**
     * Adds a flag to the recipe.
     *
     * @param string $flag  String of flags to append to the current flags.
     */
    function addFlag($flag)
    {
        $this->_flags .= $flag;
    }

    /**
     * Adds a condition to the recipe.
     *
     * @param optonal array $condition  Array of parameters. Required keys
     *                                  are 'field' and 'value'. 'case' is
     *                                  an optional keys.
     */
    function addCondition($condition = array())
    {
        $flag = (!empty($condition['case'])) ? 'D' : '';
        if (empty($this->_conditions)) {
            $this->addFlag($flag);
        }

        $match = (isset($condition['match'])) ? $condition['match'] : null;
        $string = '/^' . $condition['field'] . ': ';

        switch ($match) {
        case 'regex':
            $string .= $condition['value'];
            break;

        case 'begins with':
            $string .= preg_quote($condition['value'], '/');
            break;

        case 'ends with':
            $string .= '.*' . preg_quote($condition['value'], '/') . '$';
            break;

        case 'contains':
        default:
            $string .= '.*' . preg_quote($condition['value'], '/');
            break;
        }

        $string .= '/';

        $this->_conditions[] = array('condition' => $string, 'flags' => $flag);
    }

    /**
     * Generates maildrop code to represent the recipe.
     *
     * @return string  maildrop code to represent the recipe.
     */
    function generate()
    {
        $text = array();

        if (!$this->_valid) {
            return '';
        }

        $text[] = "if( \\";

        if (count($this->_conditions > 1)) {
            $nest = false;
            foreach ($this->_conditions as $condition) {
                $cond = ($nest) ? $this->_combine : '   ';
                $text[] = $cond . $condition['condition'] . ':h' . $condition['flags'] . " \\";
                $nest = true;
            }
        }

        $text[] = ')';

        foreach ($this->_action as $val) {
            $text[] = $val;
        }

        if ($this->_disable) {
            $code = '';
            foreach ($text as $val) {
                $comment = new Maildrop_Comment($val);
                $code .= $comment->generate() . "\n";
            }
            return $code . "\n";
        } else {
            return implode("\n", $text) . "\n";
        }
    }

    /**
     * Returns a maildrop-ready mailbox path, converting IMAP folder
     * pathname conventions as necessary.
     *
     * @param string $folder  The IMAP folder name.
     *
     * @return string  The maildrop mailbox path.
     */
    function maildropPath($folder)
    {
        /* NOTE: '$DEFAULT' here is a literal, not a PHP variable. */
        if (isset($this->_params) &&
            ($this->_params['path_style'] == 'maildir')) {
            if (empty($folder) || ($folder == 'INBOX')) {
                return 'to "${DEFAULT}';
            }
            if (substr($folder, 0, 6) == 'INBOX.') {
                $folder = substr($folder, 6);
            }
            return '"${DEFAULT}/.' . $folder . '/"';
        } else {
            if (empty($folder) || ($folder == 'INBOX')) {
                return 'to ${DEFAULT}';
            }
            return str_replace(' ', '\ ', $folder);
        }
    }

}

/**
 * The Maildrop_Variable:: class represents a Maildrop variable.
 *
 * @author  Matt Weyland <mathias@weyland.ch>
 * @since   Ingo 1.1
 * @package Ingo
 */
class Maildrop_Variable {

    var $_name;
    var $_value;

    /**
     * Constructs a new maildrop variable.
     *
     * @param array $params  Array of parameters. Expected fields are 'name'
     *                       and 'value'.
     */
    function Maildrop_Variable($params = array())
    {
        $this->_name = $params['name'];
        $this->_value = $params['value'];
    }

    /**
     * Generates maildrop code to represent the variable.
     *
     * @return string  maildrop code to represent the variable.
     */
    function generate()
    {
        return $this->_name . '=' . $this->_value . "\n";
    }

}
