<?php
/**
 * The Ingo_Script_sieve class represents a Sieve Script.
 *
 * $Horde: ingo/lib/Script/sieve.php,v 1.63.10.38 2009/08/18 22:35:03 jan Exp $
 *
 * 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  Mike Cochrane <mike@graftonhall.co.nz>
 * @package Ingo
 */
class Ingo_Script_sieve 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_MOVEKEEP,
        INGO_STORAGE_ACTION_REJECT,
        INGO_STORAGE_ACTION_FLAGONLY,
        INGO_STORAGE_ACTION_NOTIFY
    );

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

    /**
     * The list of tests allowed (implemented) for this driver.
     *
     * @var array
     */
    var $_tests = array(
        'contains', 'not contain', 'is', 'not is', 'begins with',
        'not begins with', 'ends with', 'not ends with', 'exists', 'not exist',
        'less than', 'less than or equal to', 'equal', 'not equal',
        'greater than', 'greater than or equal to', 'regex', 'matches',
        'not matches'
    );

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

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

    /**
     * Does the driver support setting IMAP flags?
     *
     * @var boolean
     */
    var $_supportIMAPFlags = true;

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

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

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

    /**
     * The blocks that have to appear at the end of the code.
     *
     * @var array
     */
    var $_endBlocks = array();

    /**
     * Returns a script previously generated with generate().
     *
     * @return string  The Sieve script.
     */
    function toCode()
    {
        $date_format = $GLOBALS['prefs']->getValue('date_format');
        // %R and %r don't work on Windows, but who runs a Sieve backend on a
        // Windows server?
        $time_format = $GLOBALS['prefs']->getValue('twentyFour') ? '%R' : '%r';
        $code = "# Sieve Filter\n# "
            . _("Generated by Ingo (http://www.horde.org/ingo/)") . ' ('
            . trim(strftime($date_format . ', ' . $time_format))
            . ")\n\n";
        $code = String::convertCharset($code, NLS::getCharset(), 'UTF-8');
        $requires = $this->requires();

        if (count($requires) > 1) {
            $stringlist = '';
            foreach ($this->requires() as $require) {
                $stringlist .= (empty($stringlist)) ? '"' : ', "';
                $stringlist .= $require . '"';
            }
            $code .= 'require [' . $stringlist . '];' . "\n\n";
        } elseif (count($requires) == 1) {
            foreach ($this->requires() as $require) {
                $code .= 'require "' . $require . '";' . "\n\n";
            }
        }

        foreach ($this->_blocks as $block) {
            $code .= $block->toCode() . "\n";
        }

        return rtrim($code) . "\n";
    }

    /**
     * Escape a string according to Sieve RFC 3028 [2.4.2].
     *
     * @param string $string      The string to escape.
     * @param boolean $regexmode  Is the escaped string a regex value?
     *                            Defaults to no.
     *
     * @return string  The escaped string.
     */
    function escapeString($string, $regexmode = false)
    {
        /* Remove any backslashes in front of commas. */
        $string = str_replace('\,', ',', $string);

        if ($regexmode) {
            return str_replace('"', addslashes('"'), $string);
        } else {
            return str_replace(array('\\', '"'), array(addslashes('\\'), addslashes('"')), $string);
        }
    }

    /**
     * Checks if all rules are valid.
     *
     * @return boolean|string  True if all rules are valid, an error message
     *                         otherwise.
     */
    function check()
    {
        foreach ($this->_blocks as $block) {
            $res = $block->check();
            if ($res !== true) {
                return $res;
            }
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        $requires = array();
        foreach ($this->_blocks as $block) {
            $requires = array_merge($requires, $block->requires());
        }

        return array_unique($requires);
    }

    /**
     * Adds all blocks necessary for the forward rule.
     */
    function _addForwardBlocks()
    {
        if (!$this->_validRule(INGO_STORAGE_ACTION_FORWARD)) {
            return;
        }

        $forward = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_FORWARD);
        $fwd_addr = $forward->getForwardAddresses();
        if (empty($fwd_addr)) {
            return;
        }

        $action = array();
        foreach ($fwd_addr as $addr) {
            $addr = trim($addr);
            if (!empty($addr)) {
                $action[] = new Sieve_Action_Redirect(array('address' => $addr));
            }
        }

        if (count($action)) {
            if($forward->getForwardKeep()) {
                $this->_endBlocks[] = new Sieve_Comment(_("Forward Keep Action"));
                $if = new Sieve_If(new Sieve_Test_True());
                $if->setActions(array(new Sieve_Action_Keep(),
                                      new Sieve_Action_Stop()));
                $this->_endBlocks[] = $if;
            } else {
                $action[] = new Sieve_Action_Stop();
            }
        }

        $this->_blocks[] = new Sieve_Comment(_("Forwards"));

        $test = new Sieve_Test_True();
        $if = new Sieve_If($test);
        $if->setActions($action);
        $this->_blocks[] = $if;
    }

    /**
     * Adds all blocks necessary for the blacklist rule.
     */
    function _addBlacklistBlocks()
    {
        if (!$this->_validRule(INGO_STORAGE_ACTION_BLACKLIST)) {
            return;
        }

        $blacklist = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_BLACKLIST);
        $bl_addr = $blacklist->getBlacklist();
        $folder = $blacklist->getBlacklistFolder();
        if (empty($bl_addr)) {
            return;
        }

        $action = array();
        if (empty($folder)) {
            $action[] = new Sieve_Action_Discard();
        } elseif ($folder == INGO_BLACKLIST_MARKER) {
            $action[] = new Sieve_Action_Addflag(array('flags' => INGO_STORAGE_FLAG_DELETED));
            $action[] = new Sieve_Action_Keep();
            $action[] = new Sieve_Action_Removeflag(array('flags' => INGO_STORAGE_FLAG_DELETED));
        } else {
            $action[] = new Sieve_Action_Fileinto(array('folder' => $folder));
        }

        $action[] = new Sieve_Action_Stop();

        $this->_blocks[] = new Sieve_Comment(_("Blacklisted Addresses"));

        /* Split the test up to only do 5 addresses at a time. */
        $temp = array();
        $wildcards = array();
        foreach ($bl_addr as $address) {
            if (!empty($address)) {
                if ((strstr($address, '*') !== false) ||
                    (strstr($address, '?') !== false)) {
                    $wildcards[] = $address;
                } else {
                    $temp[] = $address;
                }
            }
            if (count($temp) == 5) {
                $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $temp)));
                $if = new Sieve_If($test);
                $if->setActions($action);
                $this->_blocks[] = $if;
                $temp = array();
            }
            if (count($wildcards) == 5) {
                $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'match-type' => ':matches', 'addresses' => implode("\n", $wildcards)));
                $if = new Sieve_If($test);
                $if->setActions($action);
                $this->_blocks[] = $if;
                $wildcards = array();
            }
        }

        if ($temp) {
            $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $temp)));
            $if = new Sieve_If($test);
            $if->setActions($action);
            $this->_blocks[] = $if;
        }

        if ($wildcards) {
            $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'match-type' => ':matches', 'addresses' => implode("\n", $wildcards)));
            $if = new Sieve_If($test);
            $if->setActions($action);
            $this->_blocks[] = $if;
        }
    }

    /**
     * Adds all blocks necessary for the whitelist rule.
     */
    function _addWhitelistBlocks()
    {
        if (!$this->_validRule(INGO_STORAGE_ACTION_WHITELIST)) {
            return;
        }

        $whitelist = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_WHITELIST);
        $wl_addr = $whitelist->getWhitelist();
        if (empty($wl_addr)) {
            return;
        }

        $this->_blocks[] = new Sieve_Comment(_("Whitelisted Addresses"));

        $action = array(new Sieve_Action_Keep(), new Sieve_Action_Stop());
        $test = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $wl_addr)));
        $if = new Sieve_If($test);
        $if->setActions($action);
        $this->_blocks[] = $if;
    }

    /**
     * Adds all blocks necessary for the vacation rule.
     */
    function _addVacationBlocks()
    {
        if (!$this->_validRule(INGO_STORAGE_ACTION_VACATION)) {
            return;
        }

        $vacation = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_VACATION);
        $vacation_addr = $vacation->getVacationAddresses();
        if (!count($vacation_addr)) {
            return;
        }

        $vals = array(
            'subject' => String::convertCharset($vacation->getVacationSubject(), NLS::getCharset(), 'UTF-8'),
            'days' => $vacation->getVacationDays(),
            'addresses' => $vacation_addr,
            'start' => $vacation->getVacationStart(),
            'start_year' => $vacation->getVacationStartYear(),
            'start_month' => $vacation->getVacationStartMonth(),
            'start_day' => $vacation->getVacationStartDay(),
            'end' => $vacation->getVacationEnd(),
            'end_year' => $vacation->getVacationEndYear(),
            'end_month' => $vacation->getVacationEndMonth(),
            'end_day' => $vacation->getVacationEndDay(),
            'reason' => String::convertCharset($vacation->getVacationReason(), NLS::getCharset(), 'UTF-8'),
        );

        $action = $tests = array();
        $action[] = new Sieve_Action_Vacation($vals);

        if ($vacation->getVacationIgnorelist()) {
            require_once 'Horde/MIME/Headers.php';
            $mime_headers = new MIME_Headers();
            $headers = $mime_headers->listHeaders();
            $headers['Mailing-List'] = null;
            $tmp = new Sieve_Test_Exists(array('headers' => implode("\n", array_keys($headers))));
            $tests[] = new Sieve_Test_Not($tmp);
            $vals = array('headers' => 'Precedence',
                          'match-type' => ':is',
                          'strings' => "list\nbulk\njunk",
                          'comparator' => 'i;ascii-casemap');
            $tmp = new Sieve_Test_Header($vals);
            $tests[] = new Sieve_Test_Not($tmp);
            $vals = array('headers' => 'To',
                          'match-type' => ':matches',
                          'strings' => 'Multiple recipients of*',
                          'comparator' => 'i;ascii-casemap');
            $tmp = new Sieve_Test_Header($vals);
            $tests[] = new Sieve_Test_Not($tmp);
        }

        $addrs = array();
        foreach ($vacation->getVacationExcludes() as $addr) {
            $addr = trim($addr);
            if (!empty($addr)) {
                $addrs[] = $addr;
            }
        }

        if ($addrs) {
            $tmp = new Sieve_Test_Address(array('headers' => "From\nSender\nResent-From", 'addresses' => implode("\n", $addrs)));
            $tests[] = new Sieve_Test_Not($tmp);
        }

        $this->_blocks[] = new Sieve_Comment(_("Vacation"));

        if ($tests) {
            $test = new Sieve_Test_Allof($tests);
            $if = new Sieve_If($test);
            $if->setActions($action);
            $this->_blocks[] = $if;
        } else {
            $this->_blocks[] = $action[0];
        }
    }

    /**
     * Adds all blocks necessary for the spam rule.
     */
    function _addSpamBlocks()
    {
        if (!$this->_validRule(INGO_STORAGE_ACTION_SPAM)) {
            return;
        }

        $spam = &$GLOBALS['ingo_storage']->retrieve(INGO_STORAGE_ACTION_SPAM);
        if ($spam === false) {
            return;
        }

        $this->_blocks[] = new Sieve_Comment(_("Spam Filter"));

        $actions = array();
        $actions[] = new Sieve_Action_Fileinto(array(
            'folder' => $spam->getSpamFolder()
        ));

        if ($GLOBALS['conf']['spam']['compare'] == 'numeric') {
            $vals = array(
                'headers' => $GLOBALS['conf']['spam']['header'],
                'comparison' => 'ge',
                'value' => $spam->getSpamLevel(),
            );
            $test = new Sieve_Test_Relational($vals);
        } elseif ($GLOBALS['conf']['spam']['compare'] == 'string') {
            $vals = array(
                'headers' => $GLOBALS['conf']['spam']['header'],
                'match-type' => ':contains',
                'strings' => str_repeat($GLOBALS['conf']['spam']['char'],
                                        $spam->getSpamLevel()),
                'comparator' => 'i;ascii-casemap',
            );
            $test = new Sieve_Test_Header($vals);
        }

        $actions[] = new Sieve_Action_Stop();

        $if = new Sieve_If($test);
        $if->setActions($actions);
        $this->_blocks[] = $if;
    }

    /**
     * Generates the Sieve script to do the filtering specified in
     * the rules.
     *
     * @return string  The Sieve script.
     */
    function generate()
    {
        global $ingo_storage;

        $filters = &$ingo_storage->retrieve(INGO_STORAGE_ACTION_FILTERS);
        foreach ($filters->getFilterlist() as $filter) {
            /* Check to make sure this is a valid rule and that the rule
               is not disabled. */
            if (!$this->_validRule($filter['action']) ||
                !empty($filter['disable'])) {
                continue;
            }

            $action = array();
            switch ($filter['action']) {
            case INGO_STORAGE_ACTION_KEEP:
                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags']));
                }

                $action[] = new Sieve_Action_Keep();

                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags']));
                }
                break;

            case INGO_STORAGE_ACTION_DISCARD:
                $action[] = new Sieve_Action_Discard();
                break;

            case INGO_STORAGE_ACTION_MOVE:
                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags']));
                }

                $action[] = new Sieve_Action_Fileinto(array('folder' => $filter['action-value']));

                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags']));
                    }
                break;

            case INGO_STORAGE_ACTION_REJECT:
                $action[] = new Sieve_Action_Reject(array('reason' => $filter['action-value']));
                break;

            case INGO_STORAGE_ACTION_REDIRECT:
                $action[] = new Sieve_Action_Redirect(array('address' => $filter['action-value']));
                break;

            case INGO_STORAGE_ACTION_REDIRECTKEEP:
                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags']));
                }

                $action[] = new Sieve_Action_Redirect(array('address' => $filter['action-value']));
                $action[] = new Sieve_Action_Keep();

                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags']));
                }
                break;

            case INGO_STORAGE_ACTION_MOVEKEEP:
                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags']));
                }

                $action[] = new Sieve_Action_Keep();
                $action[] = new Sieve_Action_Fileinto(array('folder' => $filter['action-value']));

                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_RemoveFlag(array('flags' => $filter['flags']));
                }
                break;

            case INGO_STORAGE_ACTION_FLAGONLY:
                if (!empty($filter['flags'])) {
                    $action[] = new Sieve_Action_Addflag(array('flags' => $filter['flags']));
                }
                break;

            case INGO_STORAGE_ACTION_NOTIFY:
                $action[] = new Sieve_Action_Notify(array('address' => $filter['action-value'], 'name' => $filter['name']));
                break;

            case INGO_STORAGE_ACTION_WHITELIST:
                $this->_addWhitelistBlocks();
                continue 2;

            case INGO_STORAGE_ACTION_BLACKLIST:
                $this->_addBlacklistBlocks();
                continue 2;

            case INGO_STORAGE_ACTION_VACATION:
                $this->_addVacationBlocks();
                continue 2;

            case INGO_STORAGE_ACTION_FORWARD:
                $this->_addForwardBlocks();
                 continue 2;

            case INGO_STORAGE_ACTION_SPAM:
                $this->_addSpamBlocks();
                continue 2;
            }

            $this->_blocks[] = new Sieve_Comment($filter['name']);

            if ($filter['stop']) {
                $action[] = new Sieve_Action_Stop();
            }

            $test = new Sieve_Test();
            if ($filter['combine'] == INGO_STORAGE_COMBINE_ANY) {
                $test = new Sieve_Test_Anyof();
            } else {
                $test = new Sieve_Test_Allof();
            }

            foreach ($filter['conditions'] as $condition) {
                $tmp = '';
                switch ($condition['match']) {
                case 'equal':
                    $tmp = new Sieve_Test_Relational(array('comparison' => 'eq', 'headers' => $condition['field'], 'value' => $condition['value']));
                    $test->addTest($tmp);
                    break;

                case 'not equal':
                    $tmp = new Sieve_Test_Relational(array('comparison' => 'ne', 'headers' => $condition['field'], 'value' => $condition['value']));
                    $test->addTest($tmp);
                    break;

                case 'less than':
                    if ($condition['field'] == 'Size') {
                        /* Message Size Test. */
                        $tmp = new Sieve_Test_Size(array('comparison' => ':under', 'size' => $condition['value']));
                    } else {
                        /* Relational Test. */
                        $tmp = new Sieve_Test_Relational(array('comparison' => 'lt', 'headers' => $condition['field'], 'value' => $condition['value']));
                    }
                    $test->addTest($tmp);
                    break;

                case 'less than or equal to':
                    $tmp = new Sieve_Test_Relational(array('comparison' => 'le', 'headers' => $condition['field'], 'value' => $condition['value']));
                    $test->addTest($tmp);
                    break;

                case 'greater than':
                    if ($condition['field'] == 'Size') {
                        /* Message Size Test. */
                        $tmp = new Sieve_Test_Size(array('comparison' => ':over', 'size' => $condition['value']));
                    } else {
                        /* Relational Test. */
                        $tmp = new Sieve_Test_Relational(array('comparison' => 'gt', 'headers' => $condition['field'], 'value' => $condition['value']));
                    }
                    $test->addTest($tmp);
                    break;

                case 'greater than or equal to':
                    $tmp = new Sieve_Test_Relational(array('comparison' => 'ge', 'headers' => $condition['field'], 'value' => $condition['value']));
                    $test->addTest($tmp);
                    break;

                case 'exists':
                    $tmp = new Sieve_Test_Exists(array('headers' => $condition['field']));
                    $test->addTest($tmp);
                    break;

                case 'not exist':
                    $tmp = new Sieve_Test_Exists(array('headers' => $condition['field']));
                    $test->addTest(new Sieve_Test_Not($tmp));
                    break;

                case 'contains':
                case 'not contain':
                case 'is':
                case 'not is':
                case 'begins with':
                case 'not begins with':
                case 'ends with':
                case 'not ends with':
                case 'regex':
                case 'matches':
                case 'not matches':
                    $comparator = (isset($condition['case']) &&
                                   $condition['case'])
                        ? 'i;octet'
                        : 'i;ascii-casemap';
                    $vals = array('headers' => preg_replace('/(.)(?<!\\\)\,(.)/',
                                                            "$1\n$2",
                                                            $condition['field']),
                                  'comparator' => $comparator);
                    $use_address_test = false;

                    if ($condition['match'] != 'regex') {
                        $condition['value'] = preg_replace('/(.)(?<!\\\)\,(.)/',
                                                           "$1\n$2",
                                                           $condition['value']);
                    }

                    /* Do 'smarter' searching for fields where we know we have
                     * e-mail addresses. */
                    if (preg_match('/^(From|To|Cc|Bcc)/', $condition['field'])) {
                        $vals['addresses'] = $condition['value'];
                        $use_address_test = true;
                    } else {
                        $vals['strings'] = $condition['value'];
                    }

                    switch ($condition['match']) {
                    case 'contains':
                        $vals['match-type'] = ':contains';
                        if ($use_address_test) {
                            $tmp = new Sieve_Test_Address($vals);
                        } elseif ($condition['field'] == 'Body') {
                            $tmp = new Sieve_Test_Body($vals);
                        } else {
                            $tmp = new Sieve_Test_Header($vals);
                        }
                        $test->addTest($tmp);
                        break;

                    case 'not contain':
                        $vals['match-type'] = ':contains';
                        if ($use_address_test) {
                            $tmp = new Sieve_Test_Address($vals);
                        } elseif ($condition['field'] == 'Body') {
                            $tmp = new Sieve_Test_Body($vals);
                        } else {
                            $tmp = new Sieve_Test_Header($vals);
                        }
                        $test->addTest(new Sieve_Test_Not($tmp));
                        break;

                    case 'is':
                        $vals['match-type'] = ':is';
                        if ($use_address_test) {
                            $tmp = new Sieve_Test_Address($vals);
                        } elseif ($condition['field'] == 'Body') {
                            $tmp = new Sieve_Test_Body($vals);
                        } else {
                            $tmp = new Sieve_Test_Header($vals);
                        }
                        $test->addTest($tmp);
                        break;

                    case 'not is':
                        $vals['match-type'] = ':is';
                        if ($use_address_test) {
                            $tmp = new Sieve_Test_Address($vals);
                        } elseif ($condition['field'] == 'Body') {
                            $tmp = new Sieve_Test_Body($vals);
                        } else {
                            $tmp = new Sieve_Test_Header($vals);
                        }
                        $test->addTest(new Sieve_Test_Not($tmp));
                        break;

                    case 'begins with':
                        $vals['match-type'] = ':matches';
                        if ($use_address_test) {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = $v . '*';
                                }
                                $vals['addresses'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['addresses'] .= '*';
                            }
                            $tmp = new Sieve_Test_Address($vals);
                        } else {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = $v . '*';
                                }
                                $vals['strings'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['strings'] .= '*';
                            }
                            if ($condition['field'] == 'Body') {
                                $tmp = new Sieve_Test_Body($vals);
                            } else {
                                $tmp = new Sieve_Test_Header($vals);
                            }
                        }
                        $test->addTest($tmp);
                        break;

                    case 'not begins with':
                        $vals['match-type'] = ':matches';
                        if ($use_address_test) {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = $v . '*';
                                }
                                $vals['addresses'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['addresses'] .= '*';
                            }
                            $tmp = new Sieve_Test_Address($vals);
                        } else {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = $v . '*';
                                }
                                $vals['strings'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['strings'] .= '*';
                            }
                            if ($condition['field'] == 'Body') {
                                $tmp = new Sieve_Test_Body($vals);
                            } else {
                                $tmp = new Sieve_Test_Header($vals);
                            }
                        }
                        $test->addTest(new Sieve_Test_Not($tmp));
                        break;

                    case 'ends with':
                        $vals['match-type'] = ':matches';
                        if ($use_address_test) {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = '*' . $v;
                                }
                                $vals['addresses'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['addresses'] = '*' .  $vals['addresses'];
                            }
                            $tmp = new Sieve_Test_Address($vals);
                        } else {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = '*' . $v;
                                }
                                $vals['strings'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['strings'] = '*' .  $vals['strings'];
                            }
                            if ($condition['field'] == 'Body') {
                                $tmp = new Sieve_Test_Body($vals);
                            } else {
                                $tmp = new Sieve_Test_Header($vals);
                            }
                        }
                        $test->addTest($tmp);
                        break;

                    case 'not ends with':
                        $vals['match-type'] = ':matches';
                        if ($use_address_test) {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['addresses']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = '*' . $v;
                                }
                                $vals['addresses'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['addresses'] = '*' .  $vals['addresses'];
                            }
                            $tmp = new Sieve_Test_Address($vals);
                        } else {
                            $add_arr = preg_split('(\r\n|\n|\r)', $vals['strings']);
                            if (count($add_arr) > 1) {
                                foreach ($add_arr as $k => $v) {
                                    $add_arr[$k] = '*' . $v;
                                }
                                $vals['strings'] = implode("\r\n", $add_arr);
                            } else {
                                $vals['strings'] = '*' .  $vals['strings'];
                            }
                            if ($condition['field'] == 'Body') {
                                $tmp = new Sieve_Test_Body($vals);
                            } else {
                                $tmp = new Sieve_Test_Header($vals);
                            }
                        }
                        $test->addTest(new Sieve_Test_Not($tmp));
                        break;

                    case 'regex':
                        $vals['match-type'] = ':regex';
                        if ($use_address_test) {
                            $tmp = new Sieve_Test_Address($vals);
                        } elseif ($condition['field'] == 'Body') {
                            $tmp = new Sieve_Test_Body($vals);
                        } else {
                            $tmp = new Sieve_Test_Header($vals);
                        }
                        $test->addTest($tmp);
                        break;

                    case 'matches':
                        $vals['match-type'] = ':matches';
                        if ($use_address_test) {
                            $tmp = new Sieve_Test_Address($vals);
                        } elseif ($condition['field'] == 'Body') {
                            $tmp = new Sieve_Test_Body($vals);
                        } else {
                            $tmp = new Sieve_Test_Header($vals);
                        }
                        $test->addTest($tmp);
                        break;

                    case 'not matches':
                        $vals['match-type'] = ':matches';
                        if ($use_address_test) {
                            $tmp = new Sieve_Test_Address($vals);
                        } elseif ($condition['field'] == 'Body') {
                            $tmp = new Sieve_Test_Body($vals);
                        } else {
                            $tmp = new Sieve_Test_Header($vals);
                        }
                        $test->addTest(new Sieve_Test_Not($tmp));
                        break;
                    }
                }
            }

            $if = new Sieve_If($test);
            $if->setActions($action);
            $this->_blocks[] = $if;
        }

        /* Add blocks that have to go to the end. */
        foreach ($this->_endBlocks as $block) {
            $this->_blocks[] = $block;
        }

        return $this->toCode();
    }

}

/**
 * The Sieve_If class represents a Sieve If Statement
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_If {

    /**
     * The Sieve_Test object for the if test.
     *
     * @var Sieve_Test
     */
    var $_test;

    /**
     * A list of Sieve_Action objects that go into the if clause.
     *
     * @var array
     */
    var $_actions = array();

    /**
     * A list of Sieve_Elseif objects that create optional elsif clauses.
     *
     * @var array
     */
    var $_elsifs = array();

    /**
     * A Sieve_Else object that creates an optional else clause.
     *
     * @var Sieve_Else
     */
    var $_else;

    /**
     * Constructor.
     *
     * @params Sieve_Test $test  A Sieve_Test object.
     */
    function Sieve_If($test = null)
    {
        if (is_null($test)) {
            $this->_test = new Sieve_Test_False();
        } else {
            $this->_test = $test;
        }

        $this->_actions[] = new Sieve_Action_Keep();
        $this->_else = new Sieve_Else();
    }

    function getTest()
    {
        return $this->_test;
    }

    function setTest($test)
    {
        $this->_test = $test;
    }

    function getActions()
    {
        return $this->_actions;
    }

    function setActions($actions)
    {
        $this->_actions = $actions;
    }

    function getElsifs()
    {
        return $this->_elsifs;
    }

    function setElsifs($elsifs)
    {
        $this->_elsifs = $elsifs;
    }

    function addElsif($elsif)
    {
        $this->_elsifs[] = $elsif;
    }

    function getElse()
    {
        return $this->_else;
    }

    function setElse($else)
    {
        $this->_else = $else;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = 'if ' . $this->_test->toCode() . " { \n";
        foreach ($this->_actions as $action) {
            $code .= '    ' . $action->toCode() . "\n";
        }
        $code .= "} ";

        foreach ($this->_elsifs as $elsif) {
            $code .= $elsif->toCode();
        }

        $code .= $this->_else->toCode();

        return $code . "\n";
    }

    /**
     * Checks if all sub-rules are valid.
     *
     * @return boolean|string  True if all rules are valid, an error message
     *                         otherwise.
     */
    function check()
    {
        $res = $this->_test->check();
        if ($res !== true) {
            return $res;
        }

        foreach ($this->_elsifs as $elsif) {
            $res = $elsif->check();
            if ($res !== true) {
                return $res;
            }
        }

        $res = $this->_else->check();
        if ($res !== true) {
            return $res;
        }

        foreach ($this->_actions as $action) {
            $res = $action->check();
            if ($res !== true) {
                return $res;
            }
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        $requires = array();

        foreach ($this->_actions as $action) {
            $requires = array_merge($requires, $action->requires());
        }

        foreach ($this->_elsifs as $elsif) {
            $requires = array_merge($requires, $elsif->requires());
        }

        $requires = array_merge($requires, $this->_test->requires(), $this->_else->requires());

        return $requires;
    }

}

/**
 * The Sieve_Else class represents a Sieve Else Statement
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Else {

    /**
     * A list of Sieve_Action objects that go into the else clause.
     *
     * @var array
     */
    var $_actions = array();

    /**
     * Constructor.
     *
     * @params Sieve_Action|array $actions  A Sieve_Action object or a list of
     *                                      Sieve_Action objects.
     */
    function Sieve_Else($actions = null)
    {
        if (is_array($actions)) {
            $this->_actions = $actions;
        } elseif (!is_null($actions)) {
            $this->_actions[] = $actions;
        }
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
     function toCode()
     {
        if (count($this->_actions) == 0) {
            return '';
        }

        $code = 'else' . " { \n";
        foreach ($this->_actions as $action) {
            $code .= '    ' . $action->toCode() . "\n";
        }
        $code .= "} ";

        return $code;
    }

    function setActions($actions)
    {
        $this->_actions = $actions;
    }

    function getActions()
    {
        return $this->_actions;
    }

    /**
     * Checks if all sub-rules are valid.
     *
     * @return boolean|string  True if all rules are valid, an error message
     *                         otherwise.
     */
    function check()
    {
        foreach ($this->_actions as $action) {
            $res = $action->check();
            if ($res !== true) {
                return $res;
            }
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        $requires = array();

        foreach ($this->_actions as $action) {
            $requires = array_merge($requires, $action->requires());
        }

        return $requires;
    }

}

/**
 * The Sieve_Elsif class represents a Sieve Elsif Statement
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Elsif {

    /**
     * The Sieve_Test object for the if test.
     *
     * @var Sieve_Test
     */
    var $_test;

    /**
     * A list of Sieve_Action objects that go into the if clause.
     *
     * @var array
     */
    var $_actions = array();

    /**
     * Constructor.
     *
     * @params Sieve_Test $test  A Sieve_Test object.
     */
    function Sieve_Elsif($test = null)
    {
        if (is_null($test)) {
            $this->_test = new Sieve_Test_False();
        } else {
            $this->_test = $test;
        }
        $this->_actions[] = new Sieve_Action_Keep();
    }

    function getTest()
    {
        return $this->_test;
    }

    function setTest($test)
    {
        $this->_test = $test;
    }

    function getActions()
    {
        return $this->_actions;
    }

    function setActions($actions)
    {
        $this->_actions = $actions;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = 'elsif ' . $this->_test->toCode() . " { \n";
        foreach ($this->_actions as $action) {
            $code .= '    ' . $action->toCode() . "\n";
        }
        $code .= "} ";

        return $code;
    }

    /**
     * Checks if all sub-rules are valid.
     *
     * @return boolean|string  True if all rules are valid, an error message
     *                         otherwise.
     */
    function check()
    {
        $res = $this->_test->check();
        if ($res !== true) {
            return $res;
        }

        foreach ($this->_actions as $action) {
            $res = $action->check();
            if ($res !== true) {
                return $res;
            }
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        $requires = array();

        foreach ($this->_actions as $action) {
            $requires = array_merge($requires, $action->requires());
        }

        $requires = array_merge($requires, $this->_test->requires());

        return $requires;
    }

}

/**
 * The Sieve_Test class represents a Sieve Test.
 *
 * A test is a piece of code that evaluates to true or false.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test {

    /**
     * Any necessary test parameters.
     *
     * @var array
     */
    var $_vars = array();

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'toCode() Function Not Implemented in class ' . get_class($this);
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return 'check() Function Not Implemented in class ' . get_class($this);
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array();
    }

}

/**
 * The Sieve_Test_True class represents a test that always evaluates to true.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_True extends Sieve_Test {

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'true';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return true;
    }

}

/**
 * The Sieve_Test_False class represents a test that always evaluates to
 * false.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_False extends Sieve_Test {

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'false';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return true;
    }

}

/**
 * The Sieve_Test_Allof class represents a Allof test structure.
 *
 * Equivalent to a logical AND of all the tests it contains.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_Allof extends Sieve_Test {

    var $_tests = array();

    /**
     * Constructor.
     *
     * @params Sieve_Test|array $test  A Sieve_Test object or a list of
     *                                 Sieve_Test objects.
     */
    function Sieve_Test_Allof($test = null)
    {
        if (is_array($test)) {
            $this->_tests = $test;
        } elseif (!is_null($test)) {
            $this->_tests[] = $test;
        }
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = '';
        if (count($this->_tests) > 1) {
            $testlist = '';
            foreach ($this->_tests as $test) {
                $testlist .= (empty($testlist)) ? '' : ', ';
                $testlist .= trim($test->toCode());
            }

            $code = "allof ( $testlist )";
        } elseif (count($this->_tests) == 1) {
            $code = $this->_tests[0]->toCode();
        } else {
            return 'true';
        }
        return $code;
    }

    /**
     * Checks if all sub-rules are valid.
     *
     * @return boolean|string  True if all rules are valid, an error message
     *                         otherwise.
     */
    function check()
    {
        foreach ($this->_tests as $test) {
            $res = $test->check();
            if ($res !== true) {
                return $res;
            }
        }

        return true;
    }

    function addTest($test)
    {
        $this->_tests[] = $test;
    }

    function getTests()
    {
        return $this->_tests;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        $requires = array();

        foreach ($this->_tests as $test) {
            $requires = array_merge($requires, $test->requires());
        }

        return $requires;
    }

}

/**
 * The Sieve_Test_Anyof class represents a Anyof test structure.
 *
 * Equivalent to a logical OR of all the tests it contains.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_Anyof extends Sieve_Test {

    var $_tests = array();

    /**
     * Constructor.
     *
     * @params Sieve_Test|array $test  A Sieve_Test object or a list of
     *                                 Sieve_Test objects.
     */
    function Sieve_Test_Anyof($test = null)
    {
        if (is_array($test)) {
            $this->_tests = $test;
        } elseif (!is_null($test)) {
            $this->_tests[] = $test;
        }
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $testlist = '';
        if (count($this->_tests) > 1) {
            $testlist = '';
            foreach ($this->_tests as $test) {
                $testlist .= (empty($testlist)) ? '' : ', ';
                $testlist .= trim($test->toCode());
            }

            $code = "anyof ( $testlist )";
        } elseif (count($this->_tests) == 1) {
            $code = $this->_tests[0]->toCode();
        } else {
            return 'true';
        }
        return $code;
    }

    function addTest($test)
    {
        $this->_tests[] = $test;
    }

    function getTests()
    {
        return $this->_tests;
    }

    /**
     * Checks if all sub-rules are valid.
     *
     * @return boolean|string  True if all rules are valid, an error message
     *                         otherwise.
     */
    function check()
    {
        foreach ($this->_tests as $test) {
            $res = $test->check();
            if ($res !== true) {
                return $res;
            }
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        $requires = array();

        foreach ($this->_tests as $test) {
            $requires = array_merge($requires, $test->requires());
        }

        return $requires;
    }

}

/**
 * The Sieve_Test_Relational class represents a relational test.
 *
 * @author  Todd Merritt <tmerritt@email.arizona.edu>
 * @since   Ingo 1.0
 * @package Ingo
 */
class Sieve_Test_Relational extends Sieve_Test {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Test_Relational($vars = array())
    {
        $this->_vars['comparison'] = (isset($vars['comparison'])) ? $vars['comparison'] : '';
        $this->_vars['headers'] = (isset($vars['headers'])) ? $vars['headers'] : '';
        $this->_vars['value'] = (isset($vars['value'])) ? $vars['value'] : 0;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = 'header :value "' .
            $this->_vars['comparison'] . '" ' .
            ':comparator "i;ascii-numeric" ';

        $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']);
        $header_count = count($headers);

        if ($header_count > 1) {
            $code .= "[";
            $headerstr = '';

            foreach ($headers as $val) {
                $headerstr .= (empty($headerstr) ? '"' : ', "') .
                    Ingo_Script_sieve::escapeString($val) . '"';
            }

            $code .= $headerstr . '] ';
            $headerstr = '[' . $headerstr . ']';
        } elseif ($header_count == 1) {
            $code .= '"' . Ingo_Script_sieve::escapeString($headers[0]) . '" ';
            $headerstr = Ingo_Script_sieve::escapeString($headers[0]);
        }

        $code .= '["' . $this->_vars['value'] . '"]';

        // Add workarounds for negative numbers - works only if the comparison
        // value is positive. Sieve doesn't support comparisons of negative
        // numbers at all so this is the best we can do.
        switch ($this->_vars['comparison']) {
        case 'gt':
        case 'ge':
        case 'eq':
            // Greater than, greater or equal, equal: number must be
            // non-negative.
            return 'allof ( not header :comparator "i;ascii-casemap" :contains "'
                . $headerstr . '" "-", ' . $code . ' )';
            break;
        case 'lt':
        case 'le':
        case 'ne':
            // Less than, less or equal, nonequal: also match negative numbers
            return 'anyof ( header :comparator "i;ascii-casemap" :contains "'
                . $headerstr . '" "-", ' . $code . ' )';
            break;
        }
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
     function check()
     {
         $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']);
         return $headers ? true : _("No headers specified");
     }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
     {
         return array('relational', 'comparator-i;ascii-numeric');
     }

}

/**
 * The Sieve_Test_Size class represents a message size test.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_Size extends Sieve_Test {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Test_Size($vars = array())
    {
        $this->_vars['comparison'] = (isset($vars['comparison'])) ? $vars['comparison'] : '';
        $this->_vars['size'] = (isset($vars['size'])) ? $vars['size'] : '';
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'size ' . $this->_vars['comparison'] . ' ' . $this->_vars['size'];
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        if (!(isset($this->_vars['comparison']) &&
              isset($this->_vars['size']))) {
            return false;
        }

        return true;
    }

}

/**
 * The Sieve_Test_Not class represents the inverse of a given test.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_Not extends Sieve_Test {

    var $_test = array();

    /**
     * Constructor.
     *
     * @params Sieve_Test $test  A Sieve_Test object.
     */
    function Sieve_Test_Not($test)
    {
        $this->_test = $test;
    }

    /**
     * Checks if the sub-rule is valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return $this->_test->check();
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'not ' . $this->_test->toCode();
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return $this->_test->requires();
    }

}

/**
 * The Sieve_Test_Exists class represents a test for the existsance of one or
 * more headers in a message.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_Exists extends Sieve_Test {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Test_Exists($vars = array())
    {
        $this->_vars['headers'] = (isset($vars['headers'])) ? $vars['headers'] : '';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']);
        if (!$headers) {
            return _("No headers specified");
        }

        return true;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = 'exists ';
        $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']);
        if (count($headers) > 1) {
            $code .= "[";
            $headerstr = '';
            foreach ($headers as $header) {
                $headerstr .= (empty($headerstr) ? '"' : ', "') .
                    Ingo_Script_sieve::escapeString($header) . '"';
            }
            $code .= $headerstr . "] ";
        } elseif (count($headers) == 1) {
            $code .= '"' . Ingo_Script_sieve::escapeString($headers[0]) . '" ';
        } else {
            return "**error** No Headers Specified";
        }

        return $code;
    }

}

/**
 * The Sieve_Test_Address class represents a test on parts or all of the
 * addresses in the given fields.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_Address extends Sieve_Test {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Test_Address($vars)
    {
        $this->_vars['headers'] = (isset($vars['headers'])) ? $vars['headers'] : '';
        $this->_vars['comparator'] = (isset($vars['comparator'])) ? $vars['comparator'] : 'i;ascii-casemap';
        $this->_vars['match-type'] = (isset($vars['match-type'])) ? $vars['match-type'] : ':is';
        $this->_vars['address-part'] = (isset($vars['address-part'])) ? $vars['address-part'] : ':all';
        $this->_vars['addresses'] = (isset($vars['addresses'])) ? $vars['addresses'] : '';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']);
        if (!$headers) {
            return false;
        }

        $addresses = preg_split('(\r\n|\n|\r)', $this->_vars['addresses']);
        if (!$addresses) {
            return false;
        }

        return true;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = 'address ' .
            $this->_vars['address-part'] . ' ' .
            ':comparator "' . $this->_vars['comparator'] . '" ' .
            $this->_vars['match-type'] . ' ';

        $headers = preg_split('(\r\n|\n|\r|,)', $this->_vars['headers']);
        $headers = array_filter($headers);
        if (count($headers) > 1) {
            $code .= "[";
            $headerstr = '';
            foreach ($headers as $header) {
                $header = trim($header);
                if (!empty($header)) {
                    $headerstr .= empty($headerstr) ? '"' : ', "';
                    $headerstr .= Ingo_Script_sieve::escapeString($header, $this->_vars['match-type'] == ':regex') . '"';
                }
            }
            $code .= $headerstr . "] ";
        } elseif (count($headers) == 1) {
            $code .= '"' . Ingo_Script_sieve::escapeString($headers[0], $this->_vars['match-type'] == ':regex') . '" ';
        } else {
            return "No Headers Specified";
        }

        $addresses = preg_split('(\r\n|\n|\r)', $this->_vars['addresses']);
        $addresses = array_filter($addresses);
        if (count($addresses) > 1) {
            $code .= "[";
            $addressstr = '';
            foreach ($addresses as $addr) {
                $addr = trim($addr);
                if (!empty($addr)) {
                    $addressstr .= empty($addressstr) ? '"' : ', "';
                    $addressstr .= Ingo_Script_sieve::escapeString($addr, $this->_vars['match-type'] == ':regex') . '"';
                }
            }
            $code .= $addressstr . "] ";
        } elseif (count($addresses) == 1) {
            $code .= '"' . Ingo_Script_sieve::escapeString($addresses[0], $this->_vars['match-type'] == ':regex') . '" ';
        } else {
            return "No Addresses Specified";
        }

        return $code;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        if ($this->_vars['match-type'] == ':regex') {
            return array('regex');
        }
        return array();
    }

}

/**
 * The Sieve_Test_Header class represents a test on the contents of one or
 * more headers in a message.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Test_Header extends Sieve_Test {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Test_Header($vars = array())
    {
        $this->_vars['headers'] = isset($vars['headers'])
            ? $vars['headers']
            : 'Subject';
        $this->_vars['comparator'] = isset($vars['comparator'])
            ? $vars['comparator']
            : 'i;ascii-casemap';
        $this->_vars['match-type'] = isset($vars['match-type'])
            ? $vars['match-type']
            : ':is';
        $this->_vars['strings'] = isset($vars['strings'])
            ? $vars['strings']
            : '';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        $headers = preg_split('((?<!\\\)\,|\r\n|\n|\r)', $this->_vars['headers']);
        if (!$headers) {
            return false;
        }

        $strings = preg_split('((?<!\\\)\,|\r\n|\n|\r)', $this->_vars['strings']);
        if (!$strings) {
            return false;
        }

        return true;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = 'header ' .
            ':comparator "' . $this->_vars['comparator'] . '" ' .
            $this->_vars['match-type'] . ' ';

        $headers = preg_split('(\r\n|\n|\r)', $this->_vars['headers']);
        $headers = array_filter($headers);
        if (count($headers) > 1) {
            $code .= "[";
            $headerstr = '';
            foreach ($headers as $header) {
                $headerstr .= empty($headerstr) ? '"' : ', "';
                $headerstr .= Ingo_Script_sieve::escapeString($header, $this->_vars['match-type'] == ':regex') . '"';
            }
            $code .= $headerstr . "] ";
        } elseif (count($headers) == 1) {
            $code .= '"' . $headers[0] . '" ';
        } else {
            return _("No headers specified");
        }

        $strings = preg_split('(\r\n|\n|\r)', $this->_vars['strings']);
        $strings = array_filter($strings);
        if (count($strings) > 1) {
            $code .= "[";
            $stringlist = '';
            foreach ($strings as $str) {
                $stringlist .= empty($stringlist) ? '"' : ', "';
                $stringlist .= Ingo_Script_sieve::escapeString($str, $this->_vars['match-type'] == ':regex') . '"';
            }
            $code .= $stringlist . "] ";
        } elseif (count($strings) == 1) {
            $code .= '"' . Ingo_Script_sieve::escapeString(reset($strings), $this->_vars['match-type'] == ':regex') . '" ';
        } else {
            return _("No strings specified");
        }

        return $code;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        if ($this->_vars['match-type'] == ':regex') {
            return array('regex');
        }
        return array();
    }

}

/**
 * The Sieve_Test_Body class represents a test on the contents of the body in
 * a message.
 *
 * @author  Michael Menge <michael.menge@zdv.uni-tuebingen.de>
 * @since   Ingo 1.2
 * @package Ingo
 */
class Sieve_Test_Body extends Sieve_Test {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Test_Body($vars = array())
    {
        $this->_vars['comparator'] = (isset($vars['comparator'])) ? $vars['comparator'] : 'i;ascii-casemap';
        $this->_vars['match-type'] = (isset($vars['match-type'])) ? $vars['match-type'] : ':is';
        $this->_vars['strings'] = (isset($vars['strings'])) ? $vars['strings'] : '';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        $strings = preg_split('((?<!\\\)\,|\r\n|\n|\r)', $this->_vars['strings']);
        if (!$strings) {
            return false;
        }

        return true;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = 'body ' .
            ':comparator "' . $this->_vars['comparator'] . '" ' .
            $this->_vars['match-type'] . ' ';

        $strings = preg_split('(\r\n|\n|\r)', $this->_vars['strings']);
        $strings = array_filter($strings);
        if (count($strings) > 1) {
            $code .= "[";
            $stringlist = '';
            foreach ($strings as $str) {
                $stringlist .= empty($stringlist) ? '"' : ', "';
                $stringlist .= Ingo_Script_sieve::escapeString($str, $this->_vars['match-type'] == ':regex') . '"';
            }
            $code .= $stringlist . "] ";
        } elseif (count($strings) == 1) {
            $code .= '"' . Ingo_Script_sieve::escapeString($strings[0], $this->_vars['match-type'] == ':regex') . '" ';
        } else {
            return _("No strings specified");
        }

        return $code;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        if ($this->_vars['match-type'] == ':regex') {
            return array('regex', 'body');
        }

        return array('body');
    }

}

/**
 * A Comment.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 * @todo    This and Sieve_If should really extends a Sieve_Block eventually.
 */
class Sieve_Comment {

    var $_comment;

    /**
     * Constructor.
     *
     * @params string $comment  The comment text.
     */
    function Sieve_Comment($comment)
    {
        $this->_comment = $comment;
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $code = '';
        $lines = preg_split('(\r\n|\n|\r)', $this->_comment);
        foreach ($lines as $line) {
            $line = trim($line);
            if (strlen($line)) {
                $code .= (empty($code) ? '' : "\n") . '# ' . $line;
            }
        }
        return String::convertCharset($code, NLS::getCharset(), 'UTF-8');
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array();
    }

}

/**
 * The Sieve_Action class represents an action in a Sieve script.
 *
 * An action is anything that has a side effect eg: discard, redirect.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action {

    /**
     * Any necessary action parameters.
     *
     * @var array
     */
    var $_vars = array();

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'toCode() Function Not Implemented in class ' . get_class($this) ;
    }

    function toString()
    {
        return $this->toCode();
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return 'check() Function Not Implemented in class ' . get_class($this) ;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array();
    }

}

/**
 * The Sieve_Action_Redirect class represents a redirect action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Redirect extends Sieve_Action {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Action_Redirect($vars = array())
    {
        $this->_vars['address'] = (isset($vars['address'])) ? $vars['address'] : '';
    }

    function toCode($depth = 0)
    {
        return str_repeat(' ', $depth * 4) . 'redirect ' .
            '"' . Ingo_Script_sieve::escapeString($this->_vars['address']) . '";';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        if (empty($this->_vars['address'])) {
            return _("Missing address to redirect message to");
        }

        return true;
    }

}

/**
 * The Sieve_Action_Reject class represents a reject action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Reject extends Sieve_Action {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Action_Reject($vars = array())
    {
        $this->_vars['reason'] = (isset($vars['reason'])) ? $vars['reason'] : '';
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'reject "' . Ingo_Script_sieve::escapeString($this->_vars['reason']) . '";';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        if (empty($this->_vars['reason'])) {
            return _("Missing reason for reject");
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array('reject');
    }

}

/**
 * The Sieve_Action_Keep class represents a keep action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Keep extends Sieve_Action {

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'keep;';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return true;
    }

}

/**
 * The Sieve_Action_Discard class represents a discard action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Discard extends Sieve_Action {

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'discard;';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return true;
    }

}

/**
 * The Sieve_Action_Stop class represents a stop action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Stop extends Sieve_Action {

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'stop;';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return true;
    }

}

/**
 * The Sieve_Action_Fileinto class represents a fileinto action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Fileinto extends Sieve_Action {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Action_Fileinto($vars = array())
    {
        $this->_vars['folder'] = (isset($vars['folder'])) ? $vars['folder'] : '';
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'fileinto "' . Ingo_Script_sieve::escapeString($this->_vars['folder']) . '";';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        if (empty($this->_vars['folder'])) {
            return _("Inexistant mailbox specified for message delivery.");
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array('fileinto');
    }

}

/**
 * The Sieve_Action_Vacation class represents a vacation action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Vacation extends Sieve_Action {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Action_Vacation($vars = array())
    {
        $this->_vars['days'] = isset($vars['days']) ? intval($vars['days']) : '';
        $this->_vars['addresses'] = isset($vars['addresses']) ? $vars['addresses'] : '';
        $this->_vars['subject'] = isset($vars['subject']) ? $vars['subject'] : '';
        $this->_vars['reason'] = isset($vars['reason']) ? $vars['reason'] : '';
        $this->_vars['start'] = isset($vars['start']) ? $vars['start'] : '';
        $this->_vars['start_year'] = isset($vars['start_year']) ? $vars['start_year'] : '';
        $this->_vars['start_month'] = isset($vars['start_month']) ? $vars['start_month'] : '';
        $this->_vars['start_day'] = isset($vars['start_day']) ? $vars['start_day'] : '';
        $this->_vars['end'] = isset($vars['end']) ? $vars['end'] : '';
        $this->_vars['end_year'] = isset($vars['end_year']) ? $vars['end_year'] : '';
        $this->_vars['end_month'] = isset($vars['end_month']) ? $vars['end_month'] : '';
        $this->_vars['end_day'] = isset($vars['end_day']) ? $vars['end_day'] : '';
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        $start_year = $this->_vars['start_year'];
        $start_month = $this->_vars['start_month'];
        $start_day = $this->_vars['start_day'];

        $end_year = $this->_vars['end_year'];
        $end_month = $this->_vars['end_month'];
        $end_day = $this->_vars['end_day'];

        $code = '';

        if (empty($this->_vars['start']) || empty($this->_vars['end'])) {
            return $this->_vacationCode();
        } elseif ($end_year > $start_year + 1) {
            $code .= $this->_yearCheck($start_year + 1, $end_year - 1)
                . $this->_vacationCode()
                . "\n}\n"
                . $this->_yearCheck($start_year, $start_year);
            if ($start_month < 12) {
                $code .= $this->_monthCheck($start_month + 1, 12)
                    . $this->_vacationCode()
                    . "\n}\n";
            }
            $code .= $this->_monthCheck($start_month, $start_month)
                . $this->_dayCheck($start_day, 31)
                . $this->_vacationCode()
                . "\n}\n}\n}\n"
                . $this->_yearCheck($end_year, $end_year);
            if ($end_month > 1) {
                $code .= $this->_monthCheck(1, $end_month - 1)
                    . $this->_vacationCode()
                    . "\n}\n";
            }
            $code .= $this->_monthCheck($end_month, $end_month)
                . $this->_dayCheck(1, $end_day)
                . $this->_vacationCode()
                . "\n}\n}\n}\n";
        } elseif ($end_year == $start_year + 1) {
            $code .= $this->_yearCheck($start_year, $start_year);
            if ($start_month < 12) {
                $code .= $this->_monthCheck($start_month + 1, 12)
                    . $this->_vacationCode()
                    . "\n}\n";
            }
            $code .= $this->_monthCheck($start_month, $start_month)
                . $this->_dayCheck($start_day, 31)
                . $this->_vacationCode()
                . "\n}\n}\n}\n"
                . $this->_yearCheck($end_year, $end_year);
            if ($end_month > 1) {
                $code .= $this->_monthCheck(1, $end_month - 1)
                    . $this->_vacationCode()
                    . "\n}\n";
            }
            $code .= $this->_monthCheck($end_month, $end_month)
                . $this->_dayCheck(1, $end_day)
                . $this->_vacationCode()
                . "\n}\n}\n}\n";
        } elseif ($end_year == $start_year) {
            $code .= $this->_yearCheck($start_year, $start_year);
            if ($end_month > $start_month) {
                if ($end_month > $start_month + 1) {
                    $code .= $this->_monthCheck($start_month + 1, $end_month - 1)
                        . $this->_vacationCode()
                        . "\n}\n";
                }
                $code .= $this->_monthCheck($start_month, $start_month)
                    . $this->_dayCheck($start_day, 31)
                    . $this->_vacationCode()
                    . "\n}\n}\n"
                    . $this->_monthCheck($end_month, $end_month)
                    . $this->_dayCheck(1, $end_day)
                    . $this->_vacationCode()
                    . "\n}\n}\n";
            } elseif ($end_month == $start_month) {
                $code .= $this->_monthCheck($start_month, $start_month)
                    . $this->_dayCheck($start_day, $end_day)
                    . $this->_vacationCode()
                    . "\n}\n}\n";
            }
            $code .= "}\n";
        }

        return $code;
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        if (empty($this->_vars['reason'])) {
            return _("Missing reason in vacation.");
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array('vacation', 'regex');
    }

    /**
     */
    function _vacationCode()
    {
        $code = 'vacation :days ' . $this->_vars['days'] . ' ';
        $addresses = $this->_vars['addresses'];
        $stringlist = '';
        if (count($addresses) > 1) {
            foreach ($addresses as $address) {
                $address = trim($address);
                if (!empty($address)) {
                    $stringlist .= empty($stringlist) ? '"' : ', "';
                    $stringlist .= Ingo_Script_sieve::escapeString($address) . '"';
                }
            }
            $stringlist = "[" . $stringlist . "] ";
        } elseif (count($addresses) == 1) {
            $stringlist = '"' . Ingo_Script_sieve::escapeString($addresses[0]) . '" ';
        }

        if (!empty($stringlist)) {
            $code .= ':addresses ' . $stringlist;
        }

        if (!empty($this->_vars['subject'])) {
            include_once 'Horde/MIME.php';
            $code .= ':subject "' . MIME::encode(Ingo_Script_sieve::escapeString($this->_vars['subject']), 'UTF-8') . '" ';
        }
        return $code
            . '"' . Ingo_Script_sieve::escapeString($this->_vars['reason'])
            . '";';
    }

    /**
     */
    function _yearCheck($begin, $end)
    {
        $code = 'if header :regex "Received" "^.*(' . $begin;
        for ($i = $begin + 1; $i <= $end; $i++) {
            $code .= '|' . $i;
        }
        return $code
            . ') (\\\\(.*\\\\) )?..:..:.. (\\\\(.*\\\\) )?(\\\\+|\\\\-)....( \\\\(.*\\\\))?$" {'
            . "\n    ";
    }

    /**
     */
    function _monthCheck($begin, $end)
    {
        $months = array('Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
                        'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec');
        $code = 'if header :regex "Received" "^.*(' . $months[$begin - 1];
        for ($i = $begin + 1; $i <= $end; $i++) {
            $code .= '|' . $months[$i - 1];
        }
        return $code
            . ') (\\\\(.*\\\\) )?.... (\\\\(.*\\\\) )?..:..:.. (\\\\(.*\\\\) )?(\\\\+|\\\\-)....( \\\\(.*\\\\))?$" {'
            . "\n    ";
    }

    /**
     */
    function _dayCheck($begin, $end)
    {
        $code = 'if header :regex "Received" "^.*(' . str_repeat('[0 ]', 2 - strlen($begin)) . $begin;
        for ($i = $begin + 1; $i <= $end; $i++) {
            $code .= '|' . str_repeat('[0 ]', 2 - strlen($i)) . $i;
        }
        return $code
            . ') (\\\\(.*\\\\) )?... (\\\\(.*\\\\) )?.... (\\\\(.*\\\\) )?..:..:.. (\\\\(.*\\\\) )?(\\\\+|\\\\-)....( \\\\(.*\\\\))?$" {'
            . "\n    ";
    }

}

/**
 * The Sieve_Action_Flag class is the base class for flag actions.
 *
 * @author  Michael Slusarz <slusarz@horde.org>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Flag extends Sieve_Action {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Action_Flag($vars = array())
    {
        if (isset($vars['flags'])) {
            if ($vars['flags'] & INGO_STORAGE_FLAG_ANSWERED) {
                $this->_vars['flags'][] = '\Answered';
            }
            if ($vars['flags'] & INGO_STORAGE_FLAG_DELETED) {
                $this->_vars['flags'][] = '\Deleted';
            }
            if ($vars['flags'] & INGO_STORAGE_FLAG_FLAGGED) {
                $this->_vars['flags'][] = '\Flagged';
            }
            if ($vars['flags'] & INGO_STORAGE_FLAG_SEEN) {
                $this->_vars['flags'][] = '\Seen';
            }
        } else {
            $this->_vars['flags'] = '';
        }
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @param string $mode  The sieve flag command to use. Either 'removeflag'
     *                      or 'addflag'.
     *
     * @return string  A Sieve script snippet.
     */
    function _toCode($mode)
    {
        $code  = '';

        if (is_array($this->_vars['flags']) && !empty($this->_vars['flags'])) {
            $code .= $mode . ' ';
            if (count($this->_vars['flags']) > 1) {
                $stringlist = '';
                foreach ($this->_vars['flags'] as $flag) {
                    $flag = trim($flag);
                    if (!empty($flag)) {
                        $stringlist .= empty($stringlist) ? '"' : ', "';
                        $stringlist .= Ingo_Script_sieve::escapeString($flag) . '"';
                    }
                }
                $stringlist = '[' . $stringlist . ']';
                $code .= $stringlist . ';';
            } else {
                $code .= '"' . Ingo_Script_sieve::escapeString($this->_vars['flags'][0]) . '";';
            }
        }
        return $code;
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array('imapflags');
    }

}

/**
 * The Sieve_Action_Addflag class represents an add flag action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Addflag extends Sieve_Action_Flag {

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return $this->_toCode('addflag');
    }

}

/**
 * The Sieve_Action_Removeflag class represents a remove flag action.
 *
 * @author  Mike Cochrane <mike@graftonhall.co.nz>
 * @since   Ingo 0.1
 * @package Ingo
 */
class Sieve_Action_Removeflag extends Sieve_Action_Flag {

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return $this->_toCode('removeflag');
    }

}

/**
 * The Sieve_Action_Notify class represents a notify action.
 *
 * @author  Paul Wolstenholme <wolstena@sfu.ca>
 * @since   Ingo 1.1
 * @package Ingo
 */
class Sieve_Action_Notify extends Sieve_Action {

    /**
     * Constructor.
     *
     * @params array $vars  Any required parameters.
     */
    function Sieve_Action_Notify($vars = array())
    {
        $this->_vars['address'] = isset($vars['address']) ? $vars['address'] : '';
        $this->_vars['name'] = isset($vars['name']) ? $vars['name'] : '';
    }

    /**
     * Returns a script snippet representing this rule and any sub-rules.
     *
     * @return string  A Sieve script snippet.
     */
    function toCode()
    {
        return 'notify :method "mailto" :options "' .
            Ingo_Script_sieve::escapeString($this->_vars['address']) .
            '" :message "' .
            _("You have received a new message") . "\n" .
            _("From:") . " \$from\$ \n" .
            _("Subject:") . " \$subject\$ \n" .
            _("Rule:") . ' ' . $this->_vars['name'] . '";';
    }

    /**
     * Checks if the rule parameters are valid.
     *
     * @return boolean|string  True if this rule is valid, an error message
     *                         otherwise.
     */
    function check()
    {
        if (empty($this->_vars['address'])) {
            return _("Missing address to notify");
        }

        return true;
    }

    /**
     * Returns a list of sieve extensions required for this rule and any
     * sub-rules.
     *
     * @return array  A Sieve extension list.
     */
    function requires()
    {
        return array('notify');
    }

}
