Files
server/usr/share/psa-horde/ingo/lib/Script/Maildrop/Recipe.php
2026-01-07 20:52:11 +01:00

381 lines
13 KiB
PHP

<?php
/**
* Copyright 2005-2007 Matt Weyland <mathias@weyland.ch>
*
* See the enclosed file LICENSE for license information (ASL). If you
* did not receive this file, see http://www.horde.org/licenses/apache.
*
* @author Matt Weyland <mathias@weyland.ch>
* @author Jan Schneider <jan@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Ingo
*/
/**
* The Ingo_Script_Maildrop_Recipe class represents a Maildrop recipe.
*
* @author Matt Weyland <mathias@weyland.ch>
* @author Jan Schneider <jan@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Ingo
*/
class Ingo_Script_Maildrop_Recipe implements Ingo_Script_Item
{
/**
*/
protected $_action = array();
/**
*/
protected $_conditions = array();
/**
*/
protected $_disable = '';
/**
*/
protected $_flags = '';
/**
*/
protected $_params = array();
/**
*/
protected $_combine = '';
/**
*/
protected $_valid = true;
/**
*/
protected $_operators = array(
'less than' => '<',
'less than or equal to' => '<=',
'equal' => '==',
'not equal' => '!=',
'greater than' => '>',
'greater than or equal to' => '>=',
);
/**
* Constructs a new maildrop recipe.
*
* @param array $params Array of parameters.
* REQUIRED FIELDS:
* 'action'
* OPTIONAL FIELDS:
* 'action-value' (only used if the
* 'action' requires it)
* @param array $scriptparams Array of parameters passed to
* Ingo_Script_Maildrop::.
*/
public function __construct($params = array(), $scriptparams = array())
{
$this->_disable = !empty($params['disable']);
$this->_params = $scriptparams;
$this->_action[] = 'exception {';
switch ($params['action']) {
case Ingo_Storage::ACTION_KEEP:
$this->_action[] = ' to "${DEFAULT}"';
break;
case Ingo_Storage::ACTION_MOVE:
$this->_action[] = ' to ' . $this->maildropPath($params['action-value']);
break;
case Ingo_Storage::ACTION_DISCARD:
$this->_action[] = ' exit';
break;
case Ingo_Storage::ACTION_REDIRECT:
$this->_action[] = ' to "! ' . $params['action-value'] . '"';
break;
case Ingo_Storage::ACTION_REDIRECTKEEP:
$this->_action[] = ' cc "! ' . $params['action-value'] . '"';
$this->_action[] = ' to "${DEFAULT}"';
break;
case Ingo_Storage::ACTION_REJECT:
// EX_NOPERM (permanent failure)
$this->_action[] = ' EXITCODE=77';
$this->_action[] = ' echo "5.7.1 ' . $params['action-value'] . '"';
$this->_action[] = ' exit';
break;
case Ingo_Storage::ACTION_VACATION:
$from = reset($params['action-value']['addresses']);
/* Exclusion of addresses from vacation */
if ($params['action-value']['excludes']) {
$exclude = implode('|', $params['action-value']['excludes']);
// Disable wildcard until officially supported.
// $exclude = str_replace('*', '(.*)', $exclude);
$this->addCondition(array('match' => 'filter',
'field' => '',
'value' => '! /^From:.*(' . $exclude . ')/'));
}
$start = strftime($params['action-value']['start']);
if ($start === false) {
$start = 0;
}
$end = strftime($params['action-value']['end']);
if ($end === false) {
$end = 0;
}
// Rule : Do not send responses to bulk or list messages
if ($params['action-value']['ignorelist'] == 1) {
$params['combine'] = Ingo_Storage::COMBINE_ALL;
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Precedence: (bulk|list|junk)/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Return-Path:.*<#@\[\]>/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Return-Path:.*<>/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^From:.*MAILER-DAEMON/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^X-ClamAV-Notice-Flag: *YES/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Content-Type:.*message\/delivery-status/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Delivery Status Notification/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Undelivered Mail Returned to Sender/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Delivery failure/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Message delay/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Mail Delivery Subsystem/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^Subject:.*Mail System Error.*Returned Mail/'));
$this->addCondition(array('match' => 'filter', 'field' => '', 'value' => '! /^X-Spam-Flag: YES/ '));
} else {
$this->addCondition(array('field' => 'From', 'value' => ''));
}
// Rule : Start/End of vacation
if (($start != 0) && ($end !== 0)) {
$this->_action[] = ' flock "$HOME/vacationprocess.lock" {';
$this->_action[] = ' current_time=time';
$this->_action[] = ' if ( \ ';
$this->_action[] = ' ($current_time >= ' . $start . ') && \ ';
$this->_action[] = ' ($current_time <= ' . $end . ')) ';
$this->_action[] = ' {';
}
$this->_action[] = ' cc "' . str_replace('"', '\\"', sprintf(
'| mailbot %s -D %d -c \'UTF-8\' -t $HOME/vacation.msg -d $HOME/vacation -A %s -s %s /usr/sbin/sendmail -t -f %s',
$this->_params['mailbotargs'],
$params['action-value']['days'] ?: 9999,
escapeshellarg('From: ' . $from),
escapeshellarg(Horde_Mime::encode($params['action-value']['subject'])),
escapeshellarg($from)))
. '"';
if (($start != 0) && ($end !== 0)) {
$this->_action[] = ' }';
$this->_action[] = ' }';
}
break;
case Ingo_Storage::ACTION_FORWARD:
case Ingo_Script_Maildrop::MAILDROP_STORAGE_ACTION_STOREANDFORWARD:
foreach ($params['action-value'] as $address) {
if (!empty($address)) {
$this->_action[] = ' cc "! ' . $address . '"';
}
}
/* The 'to' must be the last action, because maildrop
* stops processing after it. */
if ($params['action'] == Ingo_Script_Maildrop::MAILDROP_STORAGE_ACTION_STOREANDFORWARD) {
$this->_action[] = ' to "${DEFAULT}"';
} else {
$this->_action[] = ' exit';
}
break;
default:
$this->_valid = false;
break;
}
$this->_action[] = '}';
if (isset($params['combine']) &&
($params['combine'] == Ingo_Storage::COMBINE_ALL)) {
$this->_combine = '&& ';
} else {
$this->_combine = '|| ';
}
}
/**
* Adds a flag to the recipe.
*
* @param string $flag String of flags to append to the current flags.
*/
public function addFlag($flag)
{
$this->_flags .= $flag;
}
/**
* Adds a condition to the recipe.
*
* @param optonal array $condition Array of parameters. Required keys
* are 'field' and 'value'. 'case' is
* an optional keys.
*/
public function addCondition($condition = array())
{
$flag = (!empty($condition['case'])) ? 'D' : '';
if (empty($this->_conditions)) {
$this->addFlag($flag);
}
$string = '';
$extra = '';
$match = (isset($condition['match'])) ? $condition['match'] : null;
// negate tests starting with 'not ', except 'not equals', which simply uses the != operator
if ($match != 'not equal' && substr($match, 0, 4) == 'not ') {
$string .= '! ';
}
// convert 'field' to PCRE pattern matching
if (strpos($condition['field'], ',') == false) {
$string .= '/^' . $condition['field'] . ':\\s*';
} else {
$string .= '/^(' . str_replace(',', '|', $condition['field']) . '):\\s*';
}
switch ($match) {
case 'not regex':
case 'regex':
$string .= $condition['value'] . '/:h';
break;
case 'filter':
$string = $condition['value'];
break;
case 'exists':
case 'not exist':
// Just run a match for the header name
$string .= '/:h';
break;
case 'less than or equal to':
case 'less than':
case 'equal':
case 'not equal':
case 'greater than or equal to':
case 'greater than':
$string .= '(\d+(\.\d+)?)/:h';
$extra = ' && $MATCH1 ' . $this->_operators[$match] . ' ' . (int)$condition['value'];
break;
case 'begins with':
case 'not begins with':
$string .= preg_quote($condition['value'], '/') . '/:h';
break;
case 'ends with':
case 'not ends with':
$string .= '.*' . preg_quote($condition['value'], '/') . '$/:h';
break;
case 'is':
case 'not is':
$string .= preg_quote($condition['value'], '/') . '$/:h';
break;
case 'matches':
case 'not matches':
$string .= str_replace(array('\\*', '\\?'), array('.*', '.'), preg_quote($condition['value'], '/') . '$') . '/:h';
break;
case 'contains':
case 'not contain':
default:
$string .= '.*' . preg_quote($condition['value'], '/') . '/:h';
break;
}
$this->_conditions[] = array('condition' => $string, 'flags' => $flag, 'extra' => $extra);
}
/**
* Generates maildrop code to represent the recipe.
*
* @return string maildrop code to represent the recipe.
*/
public function generate()
{
$text = array();
if (!$this->_valid) {
return '';
}
if (count($this->_conditions) > 0) {
$text[] = "if( \\";
$nest = false;
foreach ($this->_conditions as $condition) {
$cond = $nest ? $this->_combine : ' ';
$text[] = $cond . $condition['condition'] . $condition['flags'] . $condition['extra'] . " \\";
$nest = true;
}
$text[] = ')';
}
foreach ($this->_action as $val) {
$text[] = $val;
}
if ($this->_disable) {
$code = '';
foreach ($text as $val) {
$comment = new Ingo_Script_Maildrop_Comment($val);
$code .= $comment->generate() . "\n";
}
return $code;
}
return implode("\n", $text);
}
/**
* Returns a maildrop-ready mailbox path, converting IMAP folder pathname
* conventions as necessary.
*
* @param string $folder The IMAP folder name.
*
* @return string The maildrop mailbox path.
*/
public function maildropPath($folder)
{
/* NOTE: '$DEFAULT' here is a literal, not a PHP variable. */
if (isset($this->_params) &&
($this->_params['path_style'] == 'maildir')) {
if (empty($folder) || ($folder == 'INBOX')) {
return '"${DEFAULT}"';
}
if ($this->_params['strip_inbox'] &&
substr($folder, 0, 6) == 'INBOX.') {
$folder = substr($folder, 6);
}
$mbox = new Horde_Imap_Client_Mailbox($folder);
return '"${DEFAULT}/.' . $mbox->utf7imap . '/"';
} else {
if (empty($folder) || ($folder == 'INBOX')) {
return '${DEFAULT}';
}
return str_replace(' ', '\ ', $folder);
}
}
}