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

479 lines
18 KiB
PHP

<?php
/**
* Copyright 2003-2017 Horde LLC (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (ASL). If you
* did not receive this file, see http://www.horde.org/licenses/apache.
*
* @author Ben Chavet <ben@horde.org>
* @author Jan Schneider <jan@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Ingo
*/
/**
* The Ingo_Script_Procmail_Recipe class represents a Procmail recipe.
*
* @author Ben Chavet <ben@horde.org>
* @author Jan Schneider <jan@horde.org>
* @category Horde
* @license http://www.horde.org/licenses/apache ASL
* @package Ingo
*/
class Ingo_Script_Procmail_Recipe implements Ingo_Script_Item
{
/**
*/
protected $_action = array();
/**
*/
protected $_conditions = array();
/**
*/
protected $_disable = '';
/**
*/
protected $_flags = '';
/**
*/
protected $_params = array(
'date' => 'date',
'echo' => 'echo',
'ls' => 'ls'
);
/**
*/
protected $_valid = true;
/**
* Constructs a new procmail 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_Procmail.
*/
public function __construct($params = array(), $scriptparams = array())
{
$this->_disable = !empty($params['disable']);
$this->_params = array_merge($this->_params, $scriptparams);
$delivery = '';
if (isset($this->_params['transport'][Ingo::RULE_ALL]['params']['delivery_agent'])) {
$delivery = '| ' . $this->_params['transport'][Ingo::RULE_ALL]['params']['delivery_agent'] . ' ';
}
if (isset($this->_params['transport'][Ingo::RULE_ALL]['params']['delivery_mailbox_prefix'])) {
$delivery .= ' ' . $this->_params['transport'][Ingo::RULE_ALL]['params']['delivery_mailbox_prefix'];
}
if (!empty($this->_params['transport'][Ingo::RULE_ALL]['params']['date'])) {
$this->_params['date'] = $this->_params['transport'][Ingo::RULE_ALL]['params']['date'];
}
if (!empty($this->_params['transport'][Ingo::RULE_ALL]['params']['echo'])) {
$this->_params['echo'] = $this->_params['transport'][Ingo::RULE_ALL]['params']['echo'];
}
if (!empty($this->_params['transport'][Ingo::RULE_ALL]['params']['ls'])) {
$this->_params['ls'] = $this->_params['transport'][Ingo::RULE_ALL]['params']['ls'];
}
switch ($params['action']) {
case Ingo_Storage::ACTION_KEEP:
// Note: you may have to set the DEFAULT variable in your
// backend configuration.
$this->_action[] = $delivery .= '$DEFAULT';
break;
case Ingo_Storage::ACTION_MOVE:
$this->_action[] = $delivery
.= $this->procmailPath($params['action-value']);
break;
case Ingo_Storage::ACTION_DISCARD:
$this->_action[] = '/dev/null';
break;
case Ingo_Storage::ACTION_REDIRECT:
$this->_action[] = '! ' . $params['action-value'];
break;
case Ingo_Storage::ACTION_REDIRECTKEEP:
$this->_action[] = '{';
$this->_action[] = ' :0 c';
$this->_action[] = ' ! ' . $params['action-value'];
if (strpos($this->_flags, 'c') === false) {
$this->_action[] = '';
$this->_action[] = ' :0'
. (isset($this->_params['delivery_agent']) ? ' w' : '');
$this->_action[] = ' ' . $delivery . '$DEFAULT';
}
$this->_action[] = '}';
break;
case Ingo_Storage::ACTION_REJECT:
$this->_action[] = '{';
$this->_action[] = ' :0 h';
$this->_action[] = ' SUBJECT=| formail -xSubject:';
$this->_action[] = '';
$this->_action[] = ' :0 h';
$this->_action[] = ' SENDER=| formail -zxFrom:';
$this->_action[] = '';
$this->_action[] = ' :0 Wh';
$this->_action[] = ' * !^FROM_DAEMON';
$this->_action[] = ' * !^X-Loop: $SENDER';
$this->_action[] = ' | (formail -rA"X-Loop: $SENDER" \\';
$reason = $params['action-value'];
if (Horde_Mime::is8bit($reason)) {
$this->_action[] = ' -i"Subject: Re: $SUBJECT" \\';
$this->_action[] = ' -i"Content-Transfer-Encoding: quoted-printable" \\';
$this->_action[] = ' -i"Content-Type: text/plain; charset=UTF-8" ; \\';
$reason = Horde_Mime::quotedPrintableEncode($reason, "\n");
} else {
$this->_action[] = ' -i"Subject: Re: $SUBJECT" ; \\';
}
$reason = addcslashes($reason, "\\\n\r\t\"`");
$this->_action[] = ' ' . $this->_params['echo']
. ' -e "' . $reason . '" \\';
$this->_action[] = ' ) | $SENDMAIL -oi -t';
$this->_action[] = '}';
break;
case Ingo_Storage::ACTION_VACATION:
$days = $params['action-value']['days'];
$timed = !empty($params['action-value']['start']) &&
!empty($params['action-value']['end']);
$this->_action[] = '{';
foreach ($params['action-value']['addresses'] as $address) {
if (empty($address)) {
continue;
}
$this->_action[] = ' :0';
$this->_action[] = ' * ^TO_' . $address;
$this->_action[] = ' {';
$this->_action[] = ' DATE=`' . $this->_params['date']
. ' +%s`';
if ($days) {
$this->_action[] =
' FILEDATE=`test -f ${VACATION_DIR:-.}/\'.vacation.'
. $address . '\' && '
. $this->_params['ls']
. ' -lcn --time-style=+%s ${VACATION_DIR:-.}/\'.vacation.'
. $address . '\' | '
. 'awk \'{ print $6 + (' . $days * 86400 . ') }\'`';
$this->_action[] =
' DUMMY=`test -f ${VACATION_DIR:-.}/\'.vacation.'
. $address . '\' && '
. 'test $FILEDATE -le $DATE && '
. 'rm ${VACATION_DIR:-.}/\'.vacation.' . $address . '\'`';
}
if ($timed) {
$this->_action[] = ' START='
. $params['action-value']['start'];
$this->_action[] = ' END='
. $params['action-value']['end'];
}
$this->_action[] = '';
$this->_action[] = ' :0 h';
$this->_action[] = ' SUBJECT=| formail -xSubject:';
$this->_action[] = '';
$this->_action[] =
' :0 Wc: ${VACATION_DIR:-.}/vacation.lock';
if ($timed) {
$this->_action[] =
' * ? test $DATE -gt $START && test $END -gt $DATE';
}
$this->_action[] = ' {';
$this->_action[] = ' :0 Wh';
$this->_action[] = ' * ^TO_' . $address;
$this->_action[] = ' * !^X-Loop: ' . $address;
$this->_action[] = ' * !^X-Spam-Flag: YES';
if (count($params['action-value']['excludes']) > 0) {
foreach ($params['action-value']['excludes'] as $exclude) {
if (!empty($exclude)) {
$this->_action[] = ' * !^From.*' . $exclude;
}
}
}
if ($params['action-value']['ignorelist']) {
$this->_action[] = ' * !^FROM_DAEMON';
}
$this->_action[] =
' | formail -rD 8192 ${VACATION_DIR:-.}/.vacation.'
. $address;
$this->_action[] = ' :0 eh';
$this->_action[] = ' | (formail -rI"Precedence: junk" \\';
$this->_action[] = ' -a"From: <' . $address . '>" \\';
$this->_action[] = ' -A"X-Loop: ' . $address . '" \\';
$reason = Ingo_Script_Util::vacationReason(
$params['action-value']['reason'],
$params['action-value']['start'],
$params['action-value']['end']
);
if (Horde_Mime::is8bit($reason)) {
$this->_action[] = ' -i"Subject: '
. Horde_Mime::encode(
$params['action-value']['subject'] . ' (Re: $SUBJECT)'
)
. '" \\';
$this->_action[] =
' -i"Content-Transfer-Encoding: quoted-printable" \\';
$this->_action[] =
' -i"Content-Type: text/plain; charset=UTF-8" ; \\';
$reason = Horde_Mime::quotedPrintableEncode($reason, "\n");
} else {
$this->_action[] = ' -i"Subject: '
. Horde_Mime::encode(
$params['action-value']['subject'] . ' (Re: $SUBJECT)'
)
. '" ; \\';
}
$reason = addcslashes($reason, "\\\n\r\t\"`");
$this->_action[] = ' ' . $this->_params['echo']
. ' -e "' . $reason . '" \\';
$this->_action[] = ' ) | $SENDMAIL -f' . $address
. ' -oi -t';
$this->_action[] = ' :0';
$this->_action[] = ' /dev/null';
$this->_action[] = ' }';
$this->_action[] = ' }';
}
$this->_action[] = '}';
break;
case Ingo_Storage::ACTION_FORWARD:
/* Make sure that we prevent mail loops using 3 methods.
*
* First, we call sendmail -f to set the envelope sender to be the
* same as the original sender, so bounces will go to the original
* sender rather than to us. This unfortunately triggers lots of
* Authentication-Warning: messages in sendmail's logs.
*
* Second, add an X-Loop header, to handle the case where the
* address we forward to forwards back to us.
*
* Third, don't forward mailer daemon messages (i.e., bounces).
* Method 1 above should make this redundant, unless we're sending
* mail from this account and have a bad forward-to account.
*
* Get the from address, saving a call to formail if possible.
* The procmail code for doing this is borrowed from the
* Procmail Library Project, http://pm-lib.sourceforge.net/.
* The Ingo project has the permission to use Procmail Library code
* under Apache licence v 1.x or any later version.
* Permission obtained 2006-04-04 from Author Jari Aalto. */
$this->_action[] = '{';
$this->_action[] = ' :0 ';
$this->_action[] = ' *$ ! ^From *\/[^ ]+';
$this->_action[] = ' *$ ! ^Sender: *\/[^ ]+';
$this->_action[] = ' *$ ! ^From: *\/[^ ]+';
$this->_action[] = ' *$ ! ^Reply-to: *\/[^ ]+';
$this->_action[] = ' {';
$this->_action[] = ' OUTPUT = `formail -zxFrom:`';
$this->_action[] = ' }';
$this->_action[] = ' :0 E';
$this->_action[] = ' {';
$this->_action[] = ' OUTPUT = $MATCH';
$this->_action[] = ' }';
$this->_action[] = '';
/* Forward to each address on our list. */
foreach ($params['action-value'] as $address) {
if (!empty($address)) {
$this->_action[] = ' :0 c';
$this->_action[] = ' * !^FROM_MAILER';
$this->_action[] = ' * !^X-Loop: to-' . $address;
$this->_action[] = ' | formail -A"X-Loop: to-' . $address
. '" | $SENDMAIL -oi -f $OUTPUT ' . $address;
}
}
/* In case of mail loop or bounce, store a copy locally. Note
* that if we forward to more than one address, only a mail loop
* on the last address will cause a local copy to be saved. TODO:
* The next two lines are redundant (and create an extra copy of
* the message) if "Keep a copy of messages in this account" is
* checked. */
$this->_action[] = ' :0 E'
. (isset($this->_params['delivery_agent']) ? 'w' : '');
$this->_action[] = ' ' . $delivery . '$DEFAULT';
$this->_action[] = ' :0 ';
$this->_action[] = ' /dev/null';
$this->_action[] = '}';
break;
default:
$this->_valid = false;
break;
}
}
/**
* 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 array $condition Array of parameters. Required keys are 'field'
* and 'value'. 'case' is an optional key.
*/
public function addCondition($condition = array())
{
$flag = !empty($condition['case']) ? 'D' : '';
$match = isset($condition['match']) ? $condition['match'] : null;
$string = '';
$prefix = '';
switch ($condition['field']) {
case 'Destination':
$string = '^TO_';
break;
case 'Body':
$flag .= 'B';
break;
default:
// convert 'field' to PCRE pattern matching
if (!strpos($condition['field'], ',')) {
$string = '^' . $condition['field'] . ':';
} else {
$string .= '^(' . str_replace(',', '|', $condition['field']) . '):';
}
$prefix = ' ';
}
$reverseCondition = false;
switch ($match) {
case 'regex':
$string .= $prefix . $condition['value'];
break;
case 'address':
$string .= '(.*\<)?' . preg_quote($condition['value']);
break;
case 'not begins with':
$reverseCondition = true;
// fall through
case 'begins with':
$string .= $prefix . preg_quote($condition['value']);
break;
case 'not ends with':
$reverseCondition = true;
// fall through
case 'ends with':
$string .= '.*' . preg_quote($condition['value']) . '$';
break;
case 'not contain':
$reverseCondition = true;
// fall through
case 'contains':
default:
$string .= '.*' . preg_quote($condition['value']);
break;
}
$this->_conditions[] = array('condition' => ($reverseCondition ? '* !' : '* ') . $string,
'flags' => $flag);
}
/**
* Generates procmail code to represent the recipe.
*
* @return string Procmail code to represent the recipe.
*/
public function generate()
{
$nest = 0;
$prefix = '';
$text = array();
if (!$this->_valid) {
return '';
}
// Set the global flags for the whole rule, each condition
// will add its own (such as Body or Case Sensitive)
$global = $this->_flags;
if (isset($this->_conditions[0])) {
$global .= $this->_conditions[0]['flags'];
}
$text[] = ':0 ' . $global . (isset($this->_params['delivery_agent']) ? 'w' : '');
foreach ($this->_conditions as $condition) {
if ($nest > 0) {
$text[] = str_repeat(' ', $nest - 1) . '{';
$text[] = str_repeat(' ', $nest) . ':0 ' . $condition['flags'];
$text[] = str_repeat(' ', $nest) . $condition['condition'];
} else {
$text[] = $condition['condition'];
}
$nest++;
}
if (--$nest > 0) {
$prefix = str_repeat(' ', $nest);
}
foreach ($this->_action as $val) {
$text[] = $prefix . $val;
}
for ($i = $nest; $i > 0; $i--) {
$text[] = str_repeat(' ', $i - 1) . '}';
}
if ($this->_disable) {
$code = '';
foreach ($text as $val) {
$comment = new Ingo_Script_Procmail_Comment($val);
$code .= $comment->generate() . "\n";
}
return $code;
}
return implode("\n", $text);
}
/**
* Returns a procmail-ready mailbox path, converting IMAP folder
* pathname conventions as necessary.
*
* @param string $folder The IMAP folder name.
*
* @return string The procmail mailbox path.
*/
public function procmailPath($folder)
{
/* NOTE: '$DEFAULT' here is a literal, not a PHP variable. */
if (empty($folder) || ($folder == 'INBOX')) {
return '$DEFAULT';
}
if (isset($this->_params) &&
($this->_params['path_style'] == 'maildir')) {
if (substr($folder, 0, 6) == 'INBOX.') {
$folder = substr($folder, 6);
}
$mbox = new Horde_Imap_Client_Mailbox($folder);
return '".' . $mbox->utf7imap . '/"';
}
return str_replace(' ', '\ ', escapeshellcmd($folder));
}
}