<?php
/**
 * @category    Atmail
 * @package     Models
 * @subpackage  Sieve Filter
 * @copyright   Copyright (c) 2009-2011 ATMAIL. All rights reserved
 * @license     See http://atmail.com/license.php for license agreement
 * @author      Allan Wrethman allan@staff.atmail.com
 * @author      Atmail (http://atmail.com)
 */
class sieveFile
{
    public static function sanitizeFilters( $filters )
    {
        $reservedCharacters = array( "\n", '\n', "\r", '\r', "\t", '\t', '\\', "'", '/', '\'', '"', ',' , ';', '<', '>' );
        foreach($filters as &$filter)
        {
            foreach($filter as $i => $v)
            {
                if(is_object($v) || is_array($v))
                {
                    $filter[$i] = self::sanitizeFilters($v);
                }
                else
                {
                    $filter[$i] = str_replace($reservedCharacters, ' ', $v);
                }
            }
        }
        return $filters;
    }

    /**
     * Finds the Sieve file for user, parses/tokenizes it into an array and returns it
     */
    public static function getFilters( $Account )
    {
        if( $Account == null )
        {

            throw new Atmail_Mail_Exception(__METHOD__ . ' must be called with valid Account');

        }

        $userData = Zend_Auth::getInstance()->getStorage()->read();
        $filterFolder = users::getMaildir( $Account, $userData['usersFolderBaseName'] );

        if( file_exists($filterFolder . 'sievefilter') )
        {

            $fileContents = file_get_contents($filterFolder . 'sievefilter');
            $filters = self::processFiltersIntoArray($fileContents);
            return self::sanitizeFilters($filters);

        }

    }

    public static function processFiltersIntoArray($filtersString)
    {
        $filters = array();

        $filterNodes = explode('if allof (', $filtersString, 512);

        $quoteEscapeChar = strpos($filtersString, 'require [\'') === false ? '"' : "'";

        foreach( $filterNodes as $k => $v )
        {
            $filterNodes[$k] = explode("\n", $v);

            if( $k == 0 )
            {
                //check that match 0 contains the "# Sieve Filter", "require" and "if allof ("
                if(
                    trim($filterNodes[$k][0]) != '# Sieve Filter' ||
                    strpos($filterNodes[$k][1], 'require') === false
                )
                {

                    return array();

                }
                else
                {

                    continue;

                }

            }

            foreach( $filterNodes[$k] as $k2 => $v2 )
            {

                $filterNodes[$k][$k2] = trim($filterNodes[$k][$k2]);
                if( $filterNodes[$k][$k2] == '' || $filterNodes[$k][$k2] == null )
                {

                    unset($filterNodes[$k][$k2]);

                }

            }
            //reset array key index
            $filterNodes[$k] = array_merge($filterNodes[$k]);


            /* Filter nodes now expected to be
            (
                [0] => header :contains ["subject"] "spam",
                [2] => header :contains ["subject"] "spam",
                [...n-6] => header :contains ["subject"] "spam"
                [n-5] => )
                [n-4] => {
                [n-3] => fileinto ".Spam";
                [n-2] => stop;
                [n-1] => }
            )
            */

            $filterLinesC = count( $filterNodes[$k] );
            //ignore non-expected filter blocks (for now now, will ignore (and on save delete delete) 3rd party defined blocks unless thay are formatted exactly as we expecting)
            if(
                $filterNodes[$k][$filterLinesC-5] != ')' ||
                $filterNodes[$k][$filterLinesC-4] != '{' ||
                strpos($filterNodes[$k][$filterLinesC-3], 'fileinto') === false ||
                $filterNodes[$k][$filterLinesC-2] != 'stop;' ||
                $filterNodes[$k][$filterLinesC-1] != '}'
            )
            {

                Zend_Registry::get('log')->debug( "\n" . print_r($filterNodes[$k], true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": Ignoring bad filter block\$filterNodes[$k] \n");
                continue;

            }
            //process match element(s) from $k=1 - n-6
            $elements = array();
            for( $a = 0 ; $a < $filterLinesC-5 ; $a++ )
            {

                if(

                    strpos($filterNodes[$k][$a], 'header :contains') === false &&
                    strpos($filterNodes[$k][$a], 'header :is') === false

                )
                {

                    Zend_Registry::get('log')->debug( "\n" . print_r($filterNodes[$k], true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": Ignoring bad filter element block\$filterNodes[$k] \n");
                    continue;

                }

                //extract relevant pieces and place into uniform array format
                $filterString = $filterNodes[$k][$a];
                //get header field
                $headerPosStart = strpos($filterString, '[' . $quoteEscapeChar);
                $headerPosEnd = strpos($filterString, $quoteEscapeChar . ']');

                $firstPartSearchString = substr($filterString, 0, $headerPosStart);

                //search for "not"
                $not = false;
                if( strpos($firstPartSearchString, 'not') !== false)
                {

                    $not = true;

                }
                if( strpos($firstPartSearchString, 'contains') !== false)
                {

                    $matchType = 'contains';

                }
                else
                {

                    $matchType = 'is';

                }

                if( $headerPosStart === false || $headerPosEnd === false || $headerPosStart == $headerPosEnd)
                    continue;
                $header = substr($filterString, $headerPosStart+2, $headerPosEnd-$headerPosStart-2);

                //match from end backwards
                $matchStringPosEnd = strrpos($filterNodes[$k][$a], $quoteEscapeChar);
                $matchStringPosStart = strpos($filterNodes[$k][$a], $quoteEscapeChar, $headerPosEnd+1);
                $matchString = substr($filterNodes[$k][$a], $matchStringPosStart+1, $matchStringPosEnd-$matchStringPosStart-1);
                $elements[] = array('field' => $header, 'matchType' => $matchType, 'matchString' => $matchString, 'not' => $not);

            }

            //get fileinto
            $folderPosStart = strpos($filterNodes[$k][$filterLinesC-3], $quoteEscapeChar);
            $folderPosEnd =   strrpos($filterNodes[$k][$filterLinesC-3], $quoteEscapeChar);
            //abort current block processing and continue to next block if folder string not found
            if( $folderPosStart === false || $folderPosEnd === false || $folderPosStart == $folderPosEnd)
            {

                continue;

            }
            $folder = substr($filterNodes[$k][$filterLinesC-3], $folderPosStart+1, $folderPosEnd-$folderPosStart-1);
            $filters[] = array('matchElements' => $elements, 'fileInto' => $folder );

        }

        return $filters;
    }

    /**
     * recreates Sieve filter file and saves it to disk where EXIM can match it to a user for new mail received
     */
    public static function setFilters( $Account, $filters )
    {

        $filters = self::sanitizeFilters($filters);
        Zend_Registry::get('log')->debug( "\n" . print_r($filters, true) . "\n" . __METHOD__ . ' #' . __LINE__ . ": \$filters \n");
        if( count($filters) == 0 )
        {

            self::disableFilters( $Account );

        }
        else
        {

            self::enableFilters( $Account );

        }

        if( $Account == null )
        {

            throw new Atmail_Mail_Exception(__METHOD__ . ' must be called with valid Account');

        }

        $fileContents = self::createSieveFile($filters, "# Sieve Filter\nrequire [\"fileinto\"];\n");

        $userData = Zend_Auth::getInstance()->getStorage()->read();
        $filterFolder = users::getMaildir( $Account, $userData['usersFolderBaseName'] );

        $result = file_put_contents($filterFolder . 'sievefilter', $fileContents );

        return $result;

    }


    public static function createSieveFile($filters, $prepend="")
    {
        $fileContents = $prepend;

        foreach ($filters as $filter)
        {
           $fileContents .= self::createSieveScript($filter);
        }

        return $fileContents;
    }

    public static function createSieveScript($filter)
    {
        $fileContents = "if allof (\n";
        $elementsC = count( $filter['matchElements'] );

        foreach( $filter['matchElements'] as $elementsK => $element )
        {
            $fileContents .= ($element['not']?'not ':'') . "header :" . $element['matchType'] . " [\"" . $element['field'] . "\"] \"" . $element['matchString'] . "\"";
            if( $elementsK < $elementsC-1 )
            {
                $fileContents .= ",\n";
            }
        }

        $fileContents .= "\n)\n{\nfileinto \"" . $filter['fileInto'] . "\";\nstop;\n}\n\n";
        return $fileContents;
    }

    public static function enableFilters( $Account )
    {

        $dbTables = new dbTables();
        $dbAdapter = Zend_Registry::get('dbAdapter');
        $result = $dbAdapter->update( $dbTables->UserSettings, array('SieveSupport' => '1'), $dbAdapter->quoteInto('Account = ?',$Account) );

    }

    public static function disableFilters( $Account )
    {

        $dbTables = new dbTables();
        $dbAdapter = Zend_Registry::get('dbAdapter');
        $result = $dbAdapter->update( $dbTables->UserSettings, array('SieveSupport' => '0'), $dbAdapter->quoteInto('Account = ?',$Account) );

    }

}
